last update update
This commit is contained in:
parent
be8c169ad1
commit
2af3b01d92
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:youmazgestion/Views/Dashboard.dart';
|
||||
import 'package:youmazgestion/Views/DemandeTransfert.dart';
|
||||
import 'package:youmazgestion/Views/HandleProduct.dart';
|
||||
import 'package:youmazgestion/Views/RoleListPage.dart';
|
||||
import 'package:youmazgestion/Views/commandManagement.dart';
|
||||
@ -15,6 +16,10 @@ import 'package:youmazgestion/Views/registrationPage.dart';
|
||||
import 'package:youmazgestion/accueil.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
import 'package:youmazgestion/Views/gestion_point_de_vente.dart';
|
||||
// ✅ NOUVEAU: Imports pour les sorties personnelles
|
||||
import 'package:youmazgestion/Views/demande_sortie_personnelle_page.dart';
|
||||
import 'package:youmazgestion/Views/approbation_sorties_page.dart';
|
||||
import 'package:youmazgestion/Views/historique_sorties_personnelles_page.dart';
|
||||
|
||||
class CustomDrawer extends StatelessWidget {
|
||||
final UserController userController = Get.find<UserController>();
|
||||
@ -108,6 +113,24 @@ class CustomDrawer extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ NOUVEAU: Ajouter le menu Demande Assignation pour Super Admin uniquement
|
||||
if (userController.role == 'Super Admin') {
|
||||
// Vérifier si le menu n'existe pas déjà
|
||||
final bool demandeAssignationExists = validMenus.any((menu) => menu['route'] == '/demande-assignation');
|
||||
|
||||
if (!demandeAssignationExists) {
|
||||
validMenus.add({
|
||||
'id': 'super_admin_demande_assignation',
|
||||
'name': 'Demande Assignation',
|
||||
'route': '/demande-assignation',
|
||||
});
|
||||
print("✅ Menu 'Demande Assignation' ajouté pour Super Admin");
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ NOUVEAU: Ajouter les menus de sorties personnelles
|
||||
_addSortiesPersonnellesMenus(validMenus);
|
||||
|
||||
// Afficher les statistiques de validation
|
||||
if (invalidMenus.isNotEmpty) {
|
||||
print("📊 CustomDrawer: ${validMenus.length} menus valides, ${invalidMenus.length} invalides");
|
||||
@ -143,8 +166,10 @@ class CustomDrawer extends StatelessWidget {
|
||||
'GESTION UTILISATEURS': [],
|
||||
'GESTION PRODUITS': [],
|
||||
'GESTION COMMANDES': [],
|
||||
'GESTION STOCK': [], // ✅ NOUVEAU: Catégorie pour les sorties personnelles
|
||||
'RAPPORTS': [],
|
||||
'ADMINISTRATION': [],
|
||||
'SUPER ADMIN': [], // ✅ NOUVEAU: Catégorie spéciale pour Super Admin
|
||||
};
|
||||
|
||||
// Accueil toujours en premier
|
||||
@ -173,13 +198,17 @@ class CustomDrawer extends StatelessWidget {
|
||||
categorizedMenus['GESTION UTILISATEURS']!.add(menu);
|
||||
break;
|
||||
case '/ajouter-produit':
|
||||
case '/gestion-stock':
|
||||
categorizedMenus['GESTION PRODUITS']!.add(menu);
|
||||
break;
|
||||
case '/nouvelle-commande':
|
||||
case '/gerer-commandes':
|
||||
categorizedMenus['GESTION COMMANDES']!.add(menu);
|
||||
break;
|
||||
case '/gestion-stock':
|
||||
case '/demande-sortie-personnelle':
|
||||
case '/historique-sorties-personnelles':
|
||||
categorizedMenus['GESTION STOCK']!.add(menu);
|
||||
break;
|
||||
case '/bilan':
|
||||
case '/historique':
|
||||
categorizedMenus['RAPPORTS']!.add(menu);
|
||||
@ -188,6 +217,11 @@ class CustomDrawer extends StatelessWidget {
|
||||
case '/points-de-vente':
|
||||
categorizedMenus['ADMINISTRATION']!.add(menu);
|
||||
break;
|
||||
case '/demande-assignation':
|
||||
case '/approbation-sorties':
|
||||
// ✅ NOUVEAU: Placer dans la catégorie SUPER ADMIN
|
||||
categorizedMenus['SUPER ADMIN']!.add(menu);
|
||||
break;
|
||||
default:
|
||||
// Menu non catégorisé
|
||||
print("⚠️ Menu non catégorisé: $route");
|
||||
@ -198,6 +232,11 @@ class CustomDrawer extends StatelessWidget {
|
||||
// Ajouter les catégories avec leurs menus
|
||||
categorizedMenus.forEach((categoryName, menus) {
|
||||
if (menus.isNotEmpty) {
|
||||
// ✅ Afficher la catégorie SUPER ADMIN seulement pour les Super Admin
|
||||
if (categoryName == 'SUPER ADMIN' && userController.role != 'Super Admin') {
|
||||
return; // Skip cette catégorie
|
||||
}
|
||||
|
||||
drawerItems.add(_buildCategoryHeader(categoryName));
|
||||
for (var menu in menus) {
|
||||
drawerItems.add(_buildDrawerItemFromMenu(menu));
|
||||
@ -208,6 +247,44 @@ class CustomDrawer extends StatelessWidget {
|
||||
return drawerItems;
|
||||
}
|
||||
|
||||
/// ✅ NOUVEAU: Méthode pour ajouter les menus de sorties personnelles
|
||||
void _addSortiesPersonnellesMenus(List<Map<String, dynamic>> validMenus) {
|
||||
// Menu Demande Sortie Personnelle - Accessible à tous les utilisateurs connectés
|
||||
final bool demandeSortieExists = validMenus.any((menu) => menu['route'] == '/demande-sortie-personnelle');
|
||||
if (!demandeSortieExists) {
|
||||
validMenus.add({
|
||||
'id': 'demande_sortie_personnelle',
|
||||
'name': 'Demande sortie personnelle',
|
||||
'route': '/demande-sortie-personnelle',
|
||||
});
|
||||
print("✅ Menu 'Demande sortie personnelle' ajouté pour tous les utilisateurs");
|
||||
}
|
||||
|
||||
// Menu Historique Sorties Personnelles - Accessible à tous les utilisateurs connectés
|
||||
final bool historiqueSortiesExists = validMenus.any((menu) => menu['route'] == '/historique-sorties-personnelles');
|
||||
if (!historiqueSortiesExists) {
|
||||
validMenus.add({
|
||||
'id': 'historique_sorties_personnelles',
|
||||
'name': 'Historique sorties personnelles',
|
||||
'route': '/historique-sorties-personnelles',
|
||||
});
|
||||
print("✅ Menu 'Historique sorties personnelles' ajouté pour tous les utilisateurs");
|
||||
}
|
||||
|
||||
// Menu Approbation Sorties - Accessible SEULEMENT aux Super Admin
|
||||
if (userController.role == 'Super Admin') {
|
||||
final bool approbationSortiesExists = validMenus.any((menu) => menu['route'] == '/approbation-sorties');
|
||||
if (!approbationSortiesExists) {
|
||||
validMenus.add({
|
||||
'id': 'approbation_sorties',
|
||||
'name': 'Approuver sorties',
|
||||
'route': '/approbation-sorties',
|
||||
});
|
||||
print("✅ Menu 'Approuver sorties' ajouté pour Super Admin");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ CORRIGÉ: Construction d'un item de menu avec validation
|
||||
Widget _buildDrawerItemFromMenu(Map<String, dynamic> menu) {
|
||||
// 🛡️ VALIDATION: Vérification des types avec gestion des null
|
||||
@ -289,6 +366,27 @@ class CustomDrawer extends StatelessWidget {
|
||||
'color': Colors.blueGrey,
|
||||
'widget': const AjoutPointDeVentePage(),
|
||||
},
|
||||
'/demande-assignation': {
|
||||
'icon': Icons.assignment_turned_in,
|
||||
'color': Colors.deepOrange,
|
||||
'widget': const GestionTransfertsPage(),
|
||||
},
|
||||
// ✅ NOUVEAU: Routes pour les sorties personnelles
|
||||
'/demande-sortie-personnelle': {
|
||||
'icon': Icons.person_remove,
|
||||
'color': Colors.teal,
|
||||
'widget': const DemandeSortiePersonnellePage(),
|
||||
},
|
||||
'/approbation-sorties': {
|
||||
'icon': Icons.approval,
|
||||
'color': Colors.red,
|
||||
'widget': const ApprobationSortiesPage(),
|
||||
},
|
||||
'/historique-sorties-personnelles': {
|
||||
'icon': Icons.history_edu,
|
||||
'color': Colors.indigo,
|
||||
'widget': const HistoriqueSortiesPersonnellesPage(),
|
||||
},
|
||||
};
|
||||
|
||||
final routeData = routeMapping[route];
|
||||
@ -315,7 +413,51 @@ class CustomDrawer extends StatelessWidget {
|
||||
color: routeData['color'] as Color,
|
||||
),
|
||||
title: Text(name),
|
||||
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ✅ NOUVEAU: Badge Super Admin pour les menus réservés
|
||||
if ((route == '/demande-assignation' || route == '/approbation-sorties') &&
|
||||
userController.role == 'Super Admin') ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade600,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'ADMIN',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
// ✅ NOUVEAU: Badge "Tous" pour les menus accessibles à tous
|
||||
if (route == '/demande-sortie-personnelle' || route == '/historique-sorties-personnelles') ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade600,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'TOUS',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
const Icon(Icons.chevron_right, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final widget = routeData['widget'];
|
||||
if (widget != null) {
|
||||
@ -335,13 +477,34 @@ class CustomDrawer extends StatelessWidget {
|
||||
Widget _buildCategoryHeader(String categoryName) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
categoryName,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
categoryName,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
// ✅ NOUVEAU: Icône spéciale pour les catégories importantes
|
||||
if (categoryName == 'SUPER ADMIN') ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.security,
|
||||
size: 14,
|
||||
color: Colors.red.shade600,
|
||||
),
|
||||
],
|
||||
if (categoryName == 'GESTION STOCK') ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.inventory_2,
|
||||
size: 14,
|
||||
color: Colors.teal.shade600,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -378,12 +541,35 @@ class CustomDrawer extends StatelessWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.role.isNotEmpty ? controller.role : 'Aucun rôle',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
controller.role.isNotEmpty ? controller.role : 'Aucun rôle',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
// ✅ NOUVEAU: Badge Super Admin dans le header
|
||||
if (controller.role == 'Super Admin') ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade600,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'SA',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (controller.pointDeVenteDesignation.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
@ -454,6 +640,13 @@ class CustomDrawer extends StatelessWidget {
|
||||
debugInfo += "[$i] ID:${menu['id']}, Name:'${menu['name']}', Route:'${menu['route']}'\n";
|
||||
}
|
||||
|
||||
// ✅ NOUVEAU: Ajouter info sur les menus sorties personnelles
|
||||
debugInfo += "\n--- MENUS SORTIES PERSONNELLES ---\n";
|
||||
debugInfo += "Rôle actuel: ${controller.role}\n";
|
||||
debugInfo += "Menu Demande Sortie: Accessible à tous\n";
|
||||
debugInfo += "Menu Historique: Accessible à tous\n";
|
||||
debugInfo += "Menu Approbation: ${controller.role == 'Super Admin' ? 'Accessible (Super Admin)' : 'Non accessible'}\n";
|
||||
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text("Debug Menus"),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Models/users.dart - Version corrigée
|
||||
// 1. Models/users.dart - Version corrigée
|
||||
class Users {
|
||||
int? id;
|
||||
String name;
|
||||
@ -22,10 +22,12 @@ class Users {
|
||||
this.pointDeVenteId,
|
||||
});
|
||||
|
||||
// ✅ CORRIGÉ: Méthode toMap() qui correspond exactement aux colonnes de la DB
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id, // ✅ Inclure l'ID pour les updates
|
||||
'name': name,
|
||||
'lastname': lastName, // Correspond à la colonne DB
|
||||
'lastname': lastName, // ✅ Correspond à la colonne DB
|
||||
'email': email,
|
||||
'password': password,
|
||||
'username': username,
|
||||
@ -34,9 +36,10 @@ class Users {
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMapWithId() {
|
||||
// ✅ Méthode pour créer un map sans l'ID (pour les insertions)
|
||||
Map<String, dynamic> toMapForInsert() {
|
||||
final map = toMap();
|
||||
if (id != null) map['id'] = id;
|
||||
map.remove('id');
|
||||
return map;
|
||||
}
|
||||
|
||||
@ -44,7 +47,7 @@ class Users {
|
||||
return Users(
|
||||
id: map['id'] as int?,
|
||||
name: map['name'] as String,
|
||||
lastName: map['lastname'] as String, // Correspond à la colonne DB
|
||||
lastName: map['lastname'] as String, // ✅ Correspond à la colonne DB
|
||||
email: map['email'] as String,
|
||||
password: map['password'] as String,
|
||||
username: map['username'] as String,
|
||||
@ -55,4 +58,4 @@ class Users {
|
||||
}
|
||||
|
||||
String get role => roleName ?? '';
|
||||
}
|
||||
}
|
||||
@ -301,4 +301,59 @@ FROM role_menu_permissions rmp
|
||||
INNER JOIN roles r ON rmp.role_id = r.id
|
||||
WHERE r.designation = 'Super Admin';
|
||||
|
||||
SELECT 'Script terminé avec succès!' as resultat;
|
||||
SELECT 'Script terminé avec succès!' as resultat;
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE `demandes_transfert` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`produit_id` int(11) NOT NULL,
|
||||
`point_de_vente_source_id` int(11) NOT NULL,
|
||||
`point_de_vente_destination_id` int(11) NOT NULL,
|
||||
`demandeur_id` int(11) NOT NULL,
|
||||
`validateur_id` int(11) DEFAULT NULL,
|
||||
`quantite` int(11) NOT NULL DEFAULT 1,
|
||||
`statut` enum('en_attente','validee','refusee') NOT NULL DEFAULT 'en_attente',
|
||||
`date_demande` datetime NOT NULL,
|
||||
`date_validation` datetime DEFAULT NULL,
|
||||
`notes` text DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `produit_id` (`produit_id`),
|
||||
KEY `point_de_vente_source_id` (`point_de_vente_source_id`),
|
||||
KEY `point_de_vente_destination_id` (`point_de_vente_destination_id`),
|
||||
KEY `demandeur_id` (`demandeur_id`),
|
||||
KEY `validateur_id` (`validateur_id`),
|
||||
CONSTRAINT `demandes_transfert_ibfk_1` FOREIGN KEY (`produit_id`) REFERENCES `products` (`id`),
|
||||
CONSTRAINT `demandes_transfert_ibfk_2` FOREIGN KEY (`point_de_vente_source_id`) REFERENCES `points_de_vente` (`id`),
|
||||
CONSTRAINT `demandes_transfert_ibfk_3` FOREIGN KEY (`point_de_vente_destination_id`) REFERENCES `points_de_vente` (`id`),
|
||||
CONSTRAINT `demandes_transfert_ibfk_4` FOREIGN KEY (`demandeur_id`) REFERENCES `users` (`id`),
|
||||
CONSTRAINT `demandes_transfert_ibfk_5` FOREIGN KEY (`validateur_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
|
||||
|
||||
|
||||
-- Table pour tracer les sorties de stock personnelles
|
||||
CREATE TABLE `sorties_stock_personnelles` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`produit_id` int(11) NOT NULL,
|
||||
`admin_id` int(11) NOT NULL,
|
||||
`quantite` int(11) NOT NULL DEFAULT 1,
|
||||
`motif` varchar(500) NOT NULL,
|
||||
`date_sortie` datetime NOT NULL,
|
||||
`point_de_vente_id` int(11) DEFAULT NULL,
|
||||
`notes` text DEFAULT NULL,
|
||||
`statut` enum('en_attente','approuvee','refusee') NOT NULL DEFAULT 'en_attente',
|
||||
`approbateur_id` int(11) DEFAULT NULL,
|
||||
`date_approbation` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `produit_id` (`produit_id`),
|
||||
KEY `admin_id` (`admin_id`),
|
||||
KEY `point_de_vente_id` (`point_de_vente_id`),
|
||||
KEY `approbateur_id` (`approbateur_id`),
|
||||
CONSTRAINT `sorties_personnelles_ibfk_1` FOREIGN KEY (`produit_id`) REFERENCES `products` (`id`),
|
||||
CONSTRAINT `sorties_personnelles_ibfk_2` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`),
|
||||
CONSTRAINT `sorties_personnelles_ibfk_3` FOREIGN KEY (`point_de_vente_id`) REFERENCES `points_de_vente` (`id`),
|
||||
CONSTRAINT `sorties_personnelles_ibfk_4` FOREIGN KEY (`approbateur_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
File diff suppressed because it is too large
Load Diff
847
lib/Views/DemandeTransfert.dart
Normal file
847
lib/Views/DemandeTransfert.dart
Normal file
@ -0,0 +1,847 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
|
||||
class GestionTransfertsPage extends StatefulWidget {
|
||||
const GestionTransfertsPage({super.key});
|
||||
|
||||
@override
|
||||
_GestionTransfertsPageState createState() => _GestionTransfertsPageState();
|
||||
}
|
||||
|
||||
class _GestionTransfertsPageState extends State<GestionTransfertsPage> with TickerProviderStateMixin {
|
||||
final AppDatabase _appDatabase = AppDatabase.instance;
|
||||
final UserController _userController = Get.find<UserController>();
|
||||
|
||||
List<Map<String, dynamic>> _demandes = [];
|
||||
List<Map<String, dynamic>> _filteredDemandes = [];
|
||||
bool _isLoading = false;
|
||||
String _selectedStatut = 'en_attente';
|
||||
String _searchQuery = '';
|
||||
|
||||
late TabController _tabController;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_loadDemandes();
|
||||
_searchController.addListener(_filterDemandes);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadDemandes() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
List<Map<String, dynamic>> demandes;
|
||||
|
||||
switch (_selectedStatut) {
|
||||
case 'en_attente':
|
||||
demandes = await _appDatabase.getDemandesTransfertEnAttente();
|
||||
break;
|
||||
case 'validees':
|
||||
demandes = await _appDatabase.getDemandesTransfertValidees();
|
||||
break;
|
||||
case 'toutes':
|
||||
demandes = await _appDatabase.getToutesDemandesTransfert();
|
||||
break;
|
||||
default:
|
||||
demandes = await _appDatabase.getDemandesTransfertEnAttente();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_demandes = demandes;
|
||||
_filteredDemandes = demandes;
|
||||
});
|
||||
_filterDemandes();
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible de charger les demandes: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _filterDemandes() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
_filteredDemandes = _demandes.where((demande) {
|
||||
final produitNom = (demande['produit_nom'] ?? '').toString().toLowerCase();
|
||||
final produitRef = (demande['produit_reference'] ?? '').toString().toLowerCase();
|
||||
final demandeurNom = (demande['demandeur_nom'] ?? '').toString().toLowerCase();
|
||||
final pointVenteSource = (demande['point_vente_source'] ?? '').toString().toLowerCase();
|
||||
final pointVenteDestination = (demande['point_vente_destination'] ?? '').toString().toLowerCase();
|
||||
|
||||
return produitNom.contains(query) ||
|
||||
produitRef.contains(query) ||
|
||||
demandeurNom.contains(query) ||
|
||||
pointVenteSource.contains(query) ||
|
||||
pointVenteDestination.contains(query);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _validerDemande(int demandeId, Map<String, dynamic> demande) async {
|
||||
// Vérifier seulement si le produit est en rupture de stock (stock = 0)
|
||||
final stockDisponible = demande['stock_source'] as int? ?? 0;
|
||||
|
||||
if (stockDisponible == 0) {
|
||||
await _showRuptureStockDialog(demande);
|
||||
return;
|
||||
}
|
||||
|
||||
final confirmation = await _showConfirmationDialog(demande);
|
||||
if (!confirmation) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await _appDatabase.validerTransfert(demandeId, _userController.userId);
|
||||
|
||||
Get.snackbar(
|
||||
'Succès',
|
||||
'Transfert validé avec succès',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
icon: const Icon(Icons.check_circle, color: Colors.white),
|
||||
);
|
||||
|
||||
await _loadDemandes();
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible de valider le transfert: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 4),
|
||||
icon: const Icon(Icons.error, color: Colors.white),
|
||||
);
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _rejeterDemande(int demandeId, Map<String, dynamic> demande) async {
|
||||
final motif = await _showRejectionDialog();
|
||||
if (motif == null) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await _appDatabase.rejeterTransfert(demandeId, _userController.userId, motif);
|
||||
|
||||
Get.snackbar(
|
||||
'Demande rejetée',
|
||||
'La demande de transfert a été rejetée',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
|
||||
await _loadDemandes();
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible de rejeter la demande: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _showConfirmationDialog(Map<String, dynamic> demande) async {
|
||||
final stockDisponible = demande['stock_source'] as int? ?? 0;
|
||||
final quantiteDemandee = demande['quantite'] as int;
|
||||
final stockInsuffisant = stockDisponible < quantiteDemandee && stockDisponible > 0;
|
||||
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.swap_horiz, color: Colors.blue.shade700),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Confirmer le transfert'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Êtes-vous sûr de vouloir valider ce transfert ?'),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Produit: ${demande['produit_nom']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text('Référence: ${demande['produit_reference']}'),
|
||||
Text('Quantité: ${demande['quantite']}'),
|
||||
Text('De: ${demande['point_vente_source']}'),
|
||||
Text('Vers: ${demande['point_vente_destination']}'),
|
||||
Text(
|
||||
'Stock disponible: $stockDisponible',
|
||||
style: TextStyle(
|
||||
color: stockInsuffisant ? Colors.orange.shade700 : Colors.green.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (stockInsuffisant) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info, size: 16, color: Colors.orange.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Le stock sera insuffisant après ce transfert',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.orange.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Valider'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
Future<void> _showRuptureStockDialog(Map<String, dynamic> demande) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.red.shade700),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Rupture de stock'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Impossible d\'effectuer ce transfert car le produit est en rupture de stock.',
|
||||
style: TextStyle(color: Colors.red.shade700),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Produit: ${demande['produit_nom']}'),
|
||||
Text('Quantité demandée: ${demande['quantite']}'),
|
||||
Text(
|
||||
'Stock disponible: 0',
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Compris'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _showRejectionDialog() async {
|
||||
final TextEditingController motifController = TextEditingController();
|
||||
|
||||
return await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Rejeter la demande'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Veuillez indiquer le motif du rejet :'),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: motifController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Motif du rejet',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (motifController.text.trim().isNotEmpty) {
|
||||
Navigator.pop(context, motifController.text.trim());
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Rejeter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Gestion des transferts'),
|
||||
backgroundColor: Colors.blue.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadDemandes,
|
||||
tooltip: 'Actualiser',
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
switch (index) {
|
||||
case 0:
|
||||
_selectedStatut = 'en_attente';
|
||||
break;
|
||||
case 1:
|
||||
_selectedStatut = 'validees';
|
||||
break;
|
||||
case 2:
|
||||
_selectedStatut = 'toutes';
|
||||
break;
|
||||
}
|
||||
});
|
||||
_loadDemandes();
|
||||
},
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.pending_actions),
|
||||
text: 'En attente',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.check_circle),
|
||||
text: 'Validées',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.list),
|
||||
text: 'Toutes',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
drawer: CustomDrawer(),
|
||||
body: Column(
|
||||
children: [
|
||||
// Barre de recherche
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher par produit, référence, demandeur...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_filterDemandes();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Compteur de résultats
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${_filteredDemandes.length} demande(s)',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_selectedStatut == 'en_attente' && _filteredDemandes.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'Action requise',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.orange.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Liste des demandes
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _filteredDemandes.isEmpty
|
||||
? _buildEmptyState()
|
||||
: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildDemandesEnAttente(),
|
||||
_buildDemandesValidees(),
|
||||
_buildToutesLesDemandes(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
String message;
|
||||
IconData icon;
|
||||
|
||||
switch (_selectedStatut) {
|
||||
case 'en_attente':
|
||||
message = 'Aucune demande en attente';
|
||||
icon = Icons.inbox;
|
||||
break;
|
||||
case 'validees':
|
||||
message = 'Aucune demande validée';
|
||||
icon = Icons.check_circle_outline;
|
||||
break;
|
||||
default:
|
||||
message = 'Aucune demande trouvée';
|
||||
icon = Icons.search_off;
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 64,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (_searchController.text.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucun résultat pour "${_searchController.text}"',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDemandesEnAttente() {
|
||||
return _buildDemandesList(showActions: true);
|
||||
}
|
||||
|
||||
Widget _buildDemandesValidees() {
|
||||
return _buildDemandesList(showActions: false);
|
||||
}
|
||||
|
||||
Widget _buildToutesLesDemandes() {
|
||||
return _buildDemandesList(showActions: _selectedStatut == 'en_attente');
|
||||
}
|
||||
|
||||
Widget _buildDemandesList({required bool showActions}) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _loadDemandes,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _filteredDemandes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final demande = _filteredDemandes[index];
|
||||
return _buildDemandeCard(demande, showActions);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDemandeCard(Map<String, dynamic> demande, bool showActions) {
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
final statut = demande['statut'] as String? ?? 'en_attente';
|
||||
final stockDisponible = demande['stock_source'] as int? ?? 0;
|
||||
final quantiteDemandee = demande['quantite'] as int;
|
||||
final enRuptureStock = stockDisponible == 0;
|
||||
|
||||
Color statutColor;
|
||||
IconData statutIcon;
|
||||
String statutText;
|
||||
|
||||
switch (statut) {
|
||||
case 'validee':
|
||||
statutColor = Colors.green;
|
||||
statutIcon = Icons.check_circle;
|
||||
statutText = 'Validée';
|
||||
break;
|
||||
case 'rejetee':
|
||||
statutColor = Colors.red;
|
||||
statutIcon = Icons.cancel;
|
||||
statutText = 'Rejetée';
|
||||
break;
|
||||
default:
|
||||
statutColor = Colors.orange;
|
||||
statutIcon = Icons.pending;
|
||||
statutText = 'En attente';
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: enRuptureStock && statut == 'en_attente'
|
||||
? BorderSide(color: Colors.red.shade300, width: 1.5)
|
||||
: BorderSide.none,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec produit et statut
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
demande['produit_nom'] ?? 'Produit inconnu',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Réf: ${demande['produit_reference'] ?? 'N/A'}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statutColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: statutColor.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(statutIcon, size: 16, color: statutColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
statutText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statutColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Informations de transfert
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.store, size: 16, color: Colors.blue.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${demande['point_vente_source'] ?? 'N/A'}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward, size: 16, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${demande['point_vente_destination'] ?? 'N/A'}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.inventory_2, size: 16, color: Colors.green.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Text('Quantité: $quantiteDemandee'),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'Stock source: $stockDisponible',
|
||||
style: TextStyle(
|
||||
color: enRuptureStock
|
||||
? Colors.red.shade600
|
||||
: stockDisponible < quantiteDemandee
|
||||
? Colors.orange.shade600
|
||||
: Colors.green.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Informations de la demande
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person, size: 16, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Demandé par: ${demande['demandeur_nom'] ?? 'N/A'}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.access_time, size: 16, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
DateFormat('dd/MM/yyyy à HH:mm').format(
|
||||
(demande['date_demande'] as DateTime).toLocal()
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Alerte rupture de stock
|
||||
if (enRuptureStock && statut == 'en_attente') ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning, size: 16, color: Colors.red.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Produit en rupture de stock',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Actions (seulement pour les demandes en attente)
|
||||
if (showActions && statut == 'en_attente') ...[
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _rejeterDemande(
|
||||
demande['id'] as int,
|
||||
demande,
|
||||
),
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
label: Text(isMobile ? 'Rejeter' : 'Rejeter'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red.shade600,
|
||||
side: BorderSide(color: Colors.red.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: !enRuptureStock
|
||||
? () => _validerDemande(
|
||||
demande['id'] as int,
|
||||
demande,
|
||||
)
|
||||
: null,
|
||||
icon: const Icon(Icons.check, size: 18),
|
||||
label: Text(isMobile ? 'Valider' : 'Valider'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: !enRuptureStock
|
||||
? Colors.green.shade600
|
||||
: Colors.grey.shade400,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -53,7 +53,9 @@ class _ProductManagementPageState extends State<ProductManagementPage> {
|
||||
'Laptop',
|
||||
'Non catégorisé'
|
||||
];
|
||||
|
||||
bool _isUserSuperAdmin() {
|
||||
return _userController.role == 'Super Admin';
|
||||
}
|
||||
// Variables pour l'import Excel (conservées du code original)
|
||||
bool _isImporting = false;
|
||||
double _importProgress = 0.0;
|
||||
@ -2569,11 +2571,13 @@ class _ProductManagementPageState extends State<ProductManagementPage> {
|
||||
icon: const Icon(Icons.qr_code_2, color: Colors.blue),
|
||||
tooltip: 'Voir QR Code',
|
||||
),
|
||||
if(_isUserSuperAdmin())
|
||||
IconButton(
|
||||
onPressed: () => _editProduct(product),
|
||||
icon: const Icon(Icons.edit, color: Colors.orange),
|
||||
tooltip: 'Modifier',
|
||||
),
|
||||
if(_isUserSuperAdmin())
|
||||
IconButton(
|
||||
onPressed: () => _deleteProduct(product),
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
|
||||
451
lib/Views/approbation_sorties_page.dart
Normal file
451
lib/Views/approbation_sorties_page.dart
Normal file
@ -0,0 +1,451 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:youmazgestion/Components/app_bar.dart';
|
||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
|
||||
class ApprobationSortiesPage extends StatefulWidget {
|
||||
const ApprobationSortiesPage({super.key});
|
||||
|
||||
@override
|
||||
_ApprobationSortiesPageState createState() => _ApprobationSortiesPageState();
|
||||
}
|
||||
|
||||
class _ApprobationSortiesPageState extends State<ApprobationSortiesPage> {
|
||||
final AppDatabase _database = AppDatabase.instance;
|
||||
final UserController _userController = Get.find<UserController>();
|
||||
|
||||
List<Map<String, dynamic>> _sortiesEnAttente = [];
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSortiesEnAttente();
|
||||
}
|
||||
|
||||
Future<void> _loadSortiesEnAttente() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final sorties = await _database.getSortiesPersonnellesEnAttente();
|
||||
setState(() {
|
||||
_sortiesEnAttente = sorties;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
Get.snackbar('Erreur', 'Impossible de charger les demandes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _approuverSortie(Map<String, dynamic> sortie) async {
|
||||
final confirm = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('Approuver la demande'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Produit: ${sortie['produit_nom']}'),
|
||||
Text('Quantité: ${sortie['quantite']}'),
|
||||
Text('Demandeur: ${sortie['admin_nom']}'),
|
||||
Text('Motif: ${sortie['motif']}'),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Confirmer l\'approbation de cette demande de sortie personnelle ?',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
|
||||
child: const Text('Approuver', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
try {
|
||||
await _database.approuverSortiePersonnelle(
|
||||
sortie['id'] as int,
|
||||
_userController.userId,
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
'Demande approuvée',
|
||||
'La sortie personnelle a été approuvée et le stock mis à jour',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
_loadSortiesEnAttente();
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible d\'approuver la demande: $e',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refuserSortie(Map<String, dynamic> sortie) async {
|
||||
final motifController = TextEditingController();
|
||||
|
||||
final confirm = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('Refuser la demande'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Demande de: ${sortie['admin_nom']}'),
|
||||
Text('Produit: ${sortie['produit_nom']}'),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: motifController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Motif du refus *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (motifController.text.trim().isNotEmpty) {
|
||||
Get.back(result: true);
|
||||
} else {
|
||||
Get.snackbar('Erreur', 'Veuillez indiquer un motif de refus');
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: const Text('Refuser', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true && motifController.text.trim().isNotEmpty) {
|
||||
try {
|
||||
await _database.refuserSortiePersonnelle(
|
||||
sortie['id'] as int,
|
||||
_userController.userId,
|
||||
motifController.text.trim(),
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
'Demande refusée',
|
||||
'La sortie personnelle a été refusée',
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
_loadSortiesEnAttente();
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible de refuser la demande: $e',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(title: 'Approbation sorties personnelles'),
|
||||
drawer: CustomDrawer(),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadSortiesEnAttente,
|
||||
child: _sortiesEnAttente.isEmpty
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.inbox, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucune demande en attente',
|
||||
style: TextStyle(fontSize: 18, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _sortiesEnAttente.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sortie = _sortiesEnAttente[index];
|
||||
return _buildSortieCard(sortie);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSortieCard(Map<String, dynamic> sortie) {
|
||||
final dateSortie = DateTime.parse(sortie['date_sortie'].toString());
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec statut
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'EN ATTENTE',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
DateFormat('dd/MM/yyyy HH:mm').format(dateSortie),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Informations du produit
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.inventory, color: Colors.blue.shade700, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Produit demandé',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
sortie['produit_nom'].toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text('Référence: ${sortie['produit_reference'] ?? 'N/A'}'),
|
||||
const SizedBox(width: 16),
|
||||
Text('Stock actuel: ${sortie['stock_actuel']}'),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
'Quantité demandée: ${sortie['quantite']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Informations du demandeur
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person, color: Colors.green.shade700, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Demandeur',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${sortie['admin_nom']} ${sortie['admin_nom_famille'] ?? ''}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (sortie['point_vente_nom'] != null)
|
||||
Text('Point de vente: ${sortie['point_vente_nom']}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Motif
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.note, color: Colors.purple.shade700, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Motif',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.purple.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
sortie['motif'].toString(),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
if (sortie['notes'] != null && sortie['notes'].toString().isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Notes: ${sortie['notes']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Vérification de stock
|
||||
if ((sortie['stock_actuel'] as int) < (sortie['quantite'] as int))
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.red.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'ATTENTION: Stock insuffisant pour cette demande',
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Boutons d'action
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _refuserSortie(sortie),
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
label: const Text('Refuser'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: ((sortie['stock_actuel'] as int) >= (sortie['quantite'] as int))
|
||||
? () => _approuverSortie(sortie)
|
||||
: null,
|
||||
icon: const Icon(Icons.check, size: 18),
|
||||
label: const Text('Approuver'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
724
lib/Views/demande_sortie_personnelle_page.dart
Normal file
724
lib/Views/demande_sortie_personnelle_page.dart
Normal file
@ -0,0 +1,724 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:youmazgestion/Components/app_bar.dart';
|
||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
import '../Models/produit.dart';
|
||||
|
||||
class DemandeSortiePersonnellePage extends StatefulWidget {
|
||||
const DemandeSortiePersonnellePage({super.key});
|
||||
|
||||
@override
|
||||
_DemandeSortiePersonnellePageState createState() => _DemandeSortiePersonnellePageState();
|
||||
}
|
||||
|
||||
class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnellePage>
|
||||
with TickerProviderStateMixin {
|
||||
final AppDatabase _database = AppDatabase.instance;
|
||||
final UserController _userController = Get.find<UserController>();
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _quantiteController = TextEditingController(text: '1');
|
||||
final _motifController = TextEditingController();
|
||||
final _notesController = TextEditingController();
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
Product? _selectedProduct;
|
||||
List<Product> _products = [];
|
||||
List<Product> _filteredProducts = [];
|
||||
bool _isLoading = false;
|
||||
bool _isSearching = false;
|
||||
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
_slideAnimation = Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic),
|
||||
);
|
||||
|
||||
_loadProducts();
|
||||
_searchController.addListener(_filterProducts);
|
||||
}
|
||||
|
||||
void _filterProducts() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
if (query.isEmpty) {
|
||||
_filteredProducts = _products;
|
||||
_isSearching = false;
|
||||
} else {
|
||||
_isSearching = true;
|
||||
_filteredProducts = _products.where((product) {
|
||||
return product.name.toLowerCase().contains(query) ||
|
||||
(product.reference?.toLowerCase().contains(query) ?? false);
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadProducts() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final products = await _database.getProducts();
|
||||
setState(() {
|
||||
_products = products.where((p) => (p.stock ?? 0) > 0).toList();
|
||||
_filteredProducts = _products;
|
||||
_isLoading = false;
|
||||
});
|
||||
_animationController.forward();
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
_showErrorSnackbar('Impossible de charger les produits: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _soumettreDemandePersonnelle() async {
|
||||
if (!_formKey.currentState!.validate() || _selectedProduct == null) {
|
||||
_showErrorSnackbar('Veuillez remplir tous les champs obligatoires');
|
||||
return;
|
||||
}
|
||||
|
||||
final quantite = int.tryParse(_quantiteController.text) ?? 0;
|
||||
|
||||
if (quantite <= 0) {
|
||||
_showErrorSnackbar('La quantité doit être supérieure à 0');
|
||||
return;
|
||||
}
|
||||
|
||||
if ((_selectedProduct!.stock ?? 0) < quantite) {
|
||||
_showErrorSnackbar('Stock insuffisant (disponible: ${_selectedProduct!.stock})');
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirmation dialog
|
||||
final confirmed = await _showConfirmationDialog();
|
||||
if (!confirmed) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
await _database.createSortieStockPersonnelle(
|
||||
produitId: _selectedProduct!.id!,
|
||||
adminId: _userController.userId,
|
||||
quantite: quantite,
|
||||
motif: _motifController.text.trim(),
|
||||
pointDeVenteId: _userController.pointDeVenteId > 0 ? _userController.pointDeVenteId : null,
|
||||
notes: _notesController.text.trim().isNotEmpty ? _notesController.text.trim() : null,
|
||||
);
|
||||
|
||||
_showSuccessSnackbar('Votre demande de sortie personnelle a été soumise pour approbation');
|
||||
|
||||
// Réinitialiser le formulaire avec animation
|
||||
_resetForm();
|
||||
_loadProducts();
|
||||
} catch (e) {
|
||||
_showErrorSnackbar('Impossible de soumettre la demande: $e');
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _resetForm() {
|
||||
_formKey.currentState!.reset();
|
||||
_quantiteController.text = '1';
|
||||
_motifController.clear();
|
||||
_notesController.clear();
|
||||
_searchController.clear();
|
||||
setState(() {
|
||||
_selectedProduct = null;
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> _showConfirmationDialog() async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.help_outline, color: Colors.orange.shade700),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Confirmer la demande'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Êtes-vous sûr de vouloir soumettre cette demande ?'),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Produit: ${_selectedProduct?.name}'),
|
||||
Text('Quantité: ${_quantiteController.text}'),
|
||||
Text('Motif: ${_motifController.text}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Confirmer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
void _showSuccessSnackbar(String message) {
|
||||
Get.snackbar(
|
||||
'',
|
||||
'',
|
||||
titleText: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Succès', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
messageText: Text(message, style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor: Colors.green.shade600,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 4),
|
||||
margin: const EdgeInsets.all(16),
|
||||
borderRadius: 12,
|
||||
icon: Icon(Icons.check_circle_outline, color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorSnackbar(String message) {
|
||||
Get.snackbar(
|
||||
'',
|
||||
'',
|
||||
titleText: Row(
|
||||
children: [
|
||||
Icon(Icons.error, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Erreur', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
messageText: Text(message, style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor: Colors.red.shade600,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 4),
|
||||
margin: const EdgeInsets.all(16),
|
||||
borderRadius: 12,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.blue.shade600, Colors.blue.shade400],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.shade200,
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(Icons.inventory_2, color: Colors.white, size: 28),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Sortie personnelle de stock',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Demande d\'approbation requise',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'Cette fonctionnalité permet aux administrateurs de demander '
|
||||
'la sortie d\'un produit du stock pour usage personnel. '
|
||||
'Toute demande nécessite une approbation avant traitement.',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductSelector() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Sélection du produit *',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Barre de recherche
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher un produit...',
|
||||
prefixIcon: Icon(Icons.search, color: Colors.grey.shade600),
|
||||
suffixIcon: _isSearching
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, color: Colors.grey.shade600),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Liste des produits
|
||||
Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: _filteredProducts.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.search_off, size: 48, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_isSearching ? 'Aucun produit trouvé' : 'Aucun produit disponible',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: _filteredProducts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = _filteredProducts[index];
|
||||
final isSelected = _selectedProduct?.id == product.id;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.orange.shade50 : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.orange.shade300 : Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.orange.shade100 : Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory,
|
||||
color: isSelected ? Colors.orange.shade700 : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
product.name,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
|
||||
color: isSelected ? Colors.orange.shade800 : Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Stock: ${product.stock} • Réf: ${product.reference ?? 'N/A'}',
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.orange.shade600 : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(Icons.check_circle, color: Colors.orange.shade700)
|
||||
: Icon(Icons.radio_button_unchecked, color: Colors.grey.shade400),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedProduct = product;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormSection() {
|
||||
return Column(
|
||||
children: [
|
||||
// Quantité
|
||||
_buildInputField(
|
||||
label: 'Quantité *',
|
||||
controller: _quantiteController,
|
||||
keyboardType: TextInputType.number,
|
||||
icon: Icons.format_list_numbered,
|
||||
suffix: _selectedProduct != null
|
||||
? Text('max: ${_selectedProduct!.stock}', style: TextStyle(color: Colors.grey.shade600))
|
||||
: null,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer une quantité';
|
||||
}
|
||||
final quantite = int.tryParse(value);
|
||||
if (quantite == null || quantite <= 0) {
|
||||
return 'Quantité invalide';
|
||||
}
|
||||
if (_selectedProduct != null && quantite > (_selectedProduct!.stock ?? 0)) {
|
||||
return 'Quantité supérieure au stock disponible';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Motif
|
||||
_buildInputField(
|
||||
label: 'Motif *',
|
||||
controller: _motifController,
|
||||
icon: Icons.description,
|
||||
hintText: 'Raison de cette sortie personnelle',
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Veuillez indiquer le motif';
|
||||
}
|
||||
if (value.trim().length < 5) {
|
||||
return 'Le motif doit contenir au moins 5 caractères';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Notes
|
||||
_buildInputField(
|
||||
label: 'Notes complémentaires',
|
||||
controller: _notesController,
|
||||
icon: Icons.note_add,
|
||||
hintText: 'Informations complémentaires (optionnel)',
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
required IconData icon,
|
||||
String? hintText,
|
||||
TextInputType? keyboardType,
|
||||
int maxLines = 1,
|
||||
Widget? suffix,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
prefixIcon: Icon(icon, color: Colors.grey.shade600),
|
||||
suffix: suffix,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.orange.shade400, width: 2),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserInfoCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person, color: Colors.grey.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Informations de la demande',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow(Icons.account_circle, 'Demandeur', _userController.name),
|
||||
if (_userController.pointDeVenteId > 0)
|
||||
_buildInfoRow(Icons.store, 'Point de vente', _userController.pointDeVenteDesignation),
|
||||
_buildInfoRow(Icons.calendar_today, 'Date', DateTime.now().toLocal().toString().split(' ')[0]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(IconData icon, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$label: ',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(color: Colors.grey.shade800),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubmitButton() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.orange.shade700, Colors.orange.shade500],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.orange.shade300,
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _soumettreDemandePersonnelle,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
'Traitement...',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.send, color: Colors.white),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Soumettre la demande',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(title: 'Demande sortie personnelle'),
|
||||
drawer: CustomDrawer(),
|
||||
body: _isLoading && _products.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderCard(),
|
||||
const SizedBox(height: 24),
|
||||
_buildProductSelector(),
|
||||
const SizedBox(height: 24),
|
||||
_buildFormSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildUserInfoCard(),
|
||||
const SizedBox(height: 32),
|
||||
_buildSubmitButton(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
_quantiteController.dispose();
|
||||
_motifController.dispose();
|
||||
_notesController.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:youmazgestion/Models/users.dart';
|
||||
import 'package:youmazgestion/Models/role.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
//import '../Services/app_database.dart';
|
||||
|
||||
class EditUserPage extends StatefulWidget {
|
||||
final Users user;
|
||||
@ -21,9 +20,11 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
late TextEditingController _passwordController;
|
||||
|
||||
List<Role> _roles = [];
|
||||
List<Map<String, dynamic>> _pointsDeVente = [];
|
||||
Role? _selectedRole;
|
||||
Map<String, dynamic>? _selectedPointDeVente;
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingRoles = true;
|
||||
bool _isLoadingData = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -34,28 +35,47 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
_usernameController = TextEditingController(text: widget.user.username);
|
||||
_passwordController = TextEditingController();
|
||||
|
||||
_loadRoles();
|
||||
_loadInitialData();
|
||||
}
|
||||
|
||||
Future<void> _loadRoles() async {
|
||||
Future<void> _loadInitialData() async {
|
||||
try {
|
||||
// Charger les rôles
|
||||
final roles = await AppDatabase.instance.getRoles();
|
||||
final currentRole = roles.firstWhere(
|
||||
(r) => r.id == widget.user.roleId,
|
||||
orElse: () => Role(id: widget.user.roleId, designation: widget.user.roleName ?? 'Inconnu'),
|
||||
);
|
||||
|
||||
// Charger les points de vente
|
||||
final pointsDeVente = await AppDatabase.instance.getPointsDeVente();
|
||||
|
||||
// Trouver le point de vente actuel de l'utilisateur
|
||||
Map<String, dynamic>? currentPointDeVente;
|
||||
if (widget.user.pointDeVenteId != null) {
|
||||
try {
|
||||
currentPointDeVente = pointsDeVente.firstWhere(
|
||||
(pv) => pv['id'] == widget.user.pointDeVenteId,
|
||||
);
|
||||
} catch (e) {
|
||||
// Point de vente non trouvé, on garde null
|
||||
print('Point de vente ${widget.user.pointDeVenteId} non trouvé');
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_roles = roles;
|
||||
_selectedRole = currentRole;
|
||||
_isLoadingRoles = false;
|
||||
_pointsDeVente = pointsDeVente;
|
||||
_selectedPointDeVente = currentPointDeVente;
|
||||
_isLoadingData = false;
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur lors du chargement des rôles: $e');
|
||||
print('Erreur lors du chargement des données: $e');
|
||||
setState(() {
|
||||
_isLoadingRoles = false;
|
||||
_isLoadingData = false;
|
||||
});
|
||||
_showErrorDialog('Erreur', 'Impossible de charger les rôles.');
|
||||
_showErrorDialog('Erreur', 'Impossible de charger les données nécessaires.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,13 +89,30 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ✅ AMÉLIORÉ: Validation des champs avec messages plus précis
|
||||
bool _validateFields() {
|
||||
if (_nameController.text.trim().isEmpty ||
|
||||
_lastNameController.text.trim().isEmpty ||
|
||||
_emailController.text.trim().isEmpty ||
|
||||
_usernameController.text.trim().isEmpty ||
|
||||
_selectedRole == null) {
|
||||
_showErrorDialog('Champs manquants', 'Veuillez remplir tous les champs requis.');
|
||||
if (_nameController.text.trim().isEmpty) {
|
||||
_showErrorDialog('Champ manquant', 'Le prénom est requis.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_lastNameController.text.trim().isEmpty) {
|
||||
_showErrorDialog('Champ manquant', 'Le nom de famille est requis.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_emailController.text.trim().isEmpty) {
|
||||
_showErrorDialog('Champ manquant', 'L\'email est requis.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_usernameController.text.trim().isEmpty) {
|
||||
_showErrorDialog('Champ manquant', 'Le nom d\'utilisateur est requis.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_selectedRole == null) {
|
||||
_showErrorDialog('Champ manquant', 'Veuillez sélectionner un rôle.');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -87,13 +124,19 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
|
||||
if (_passwordController.text.isNotEmpty &&
|
||||
_passwordController.text.length < 6) {
|
||||
_showErrorDialog('Mot de passe trop court', 'Minimum 6 caractères.');
|
||||
_showErrorDialog('Mot de passe trop court', 'Le mot de passe doit contenir au minimum 6 caractères.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_usernameController.text.trim().length < 3) {
|
||||
_showErrorDialog('Nom d\'utilisateur trop court', 'Le nom d\'utilisateur doit contenir au minimum 3 caractères.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ✅ CORRIGÉ: Méthode _updateUser avec validation et gestion d'erreurs améliorée
|
||||
Future<void> _updateUser() async {
|
||||
if (!_validateFields() || _isLoading) return;
|
||||
|
||||
@ -102,6 +145,9 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
});
|
||||
|
||||
try {
|
||||
print("🔄 Début de la mise à jour utilisateur...");
|
||||
|
||||
// ✅ Créer l'objet utilisateur avec toutes les données
|
||||
final updatedUser = Users(
|
||||
id: widget.user.id,
|
||||
name: _nameController.text.trim(),
|
||||
@ -113,14 +159,69 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
: widget.user.password,
|
||||
roleId: _selectedRole!.id!,
|
||||
roleName: _selectedRole!.designation,
|
||||
pointDeVenteId: _selectedPointDeVente?['id'],
|
||||
);
|
||||
|
||||
await AppDatabase.instance.updateUser(updatedUser);
|
||||
if (mounted) _showSuccessDialog();
|
||||
print("📝 Données utilisateur à mettre à jour:");
|
||||
print(" ID: ${updatedUser.id}");
|
||||
print(" Nom: ${updatedUser.name} ${updatedUser.lastName}");
|
||||
print(" Email: ${updatedUser.email}");
|
||||
print(" Username: ${updatedUser.username}");
|
||||
print(" Role ID: ${updatedUser.roleId}");
|
||||
print(" Point de vente ID: ${updatedUser.pointDeVenteId}");
|
||||
|
||||
// ✅ Validation avant mise à jour
|
||||
final validationError = await AppDatabase.instance.validateUserUpdate(updatedUser);
|
||||
if (validationError != null) {
|
||||
if (mounted) {
|
||||
_showErrorDialog('Erreur de validation', validationError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Vérifier que l'utilisateur existe
|
||||
final userExists = await AppDatabase.instance.userExists(widget.user.id!);
|
||||
if (!userExists) {
|
||||
if (mounted) {
|
||||
_showErrorDialog('Erreur', 'L\'utilisateur à modifier n\'existe plus dans la base de données.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Effectuer la mise à jour
|
||||
final affectedRows = await AppDatabase.instance.updateUser(updatedUser);
|
||||
|
||||
if (affectedRows > 0) {
|
||||
print("✅ Utilisateur mis à jour avec succès!");
|
||||
if (mounted) _showSuccessDialog();
|
||||
} else {
|
||||
print("⚠️ Aucune ligne affectée lors de la mise à jour");
|
||||
if (mounted) {
|
||||
_showErrorDialog('Information', 'Aucune modification n\'a été effectuée.');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
print('Erreur de mise à jour: $e');
|
||||
print('❌ Erreur de mise à jour: $e');
|
||||
if (mounted) {
|
||||
_showErrorDialog('Échec', 'Une erreur est survenue lors de la mise à jour.');
|
||||
String errorMessage = 'Une erreur est survenue lors de la mise à jour.';
|
||||
|
||||
// Messages d'erreur plus spécifiques
|
||||
if (e.toString().contains('Duplicate entry')) {
|
||||
if (e.toString().contains('email')) {
|
||||
errorMessage = 'Cet email est déjà utilisé par un autre utilisateur.';
|
||||
} else if (e.toString().contains('username')) {
|
||||
errorMessage = 'Ce nom d\'utilisateur est déjà utilisé.';
|
||||
} else {
|
||||
errorMessage = 'Ces informations sont déjà utilisées par un autre utilisateur.';
|
||||
}
|
||||
} else if (e.toString().contains('Cannot add or update a child row')) {
|
||||
errorMessage = 'Le rôle ou le point de vente sélectionné n\'existe pas.';
|
||||
} else if (e.toString().contains('Connection')) {
|
||||
errorMessage = 'Erreur de connexion à la base de données. Veuillez réessayer.';
|
||||
}
|
||||
|
||||
_showErrorDialog('Échec', errorMessage);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@ -130,16 +231,30 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _showSuccessDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Mise à jour réussie'),
|
||||
content: const Text('Les informations de l\'utilisateur ont été mises à jour.'),
|
||||
title: Row(
|
||||
children: const [
|
||||
Icon(Icons.check_circle, color: Colors.green),
|
||||
SizedBox(width: 8),
|
||||
Text('Mise à jour réussie'),
|
||||
],
|
||||
),
|
||||
content: const Text('Les informations de l\'utilisateur ont été mises à jour avec succès.'),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // Fermer le dialog
|
||||
Navigator.of(context).pop(); // Retourner à la page précédente
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('OK'),
|
||||
)
|
||||
],
|
||||
@ -151,11 +266,21 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.error, color: Colors.red),
|
||||
const SizedBox(width: 8),
|
||||
Text(title),
|
||||
],
|
||||
),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('OK'),
|
||||
)
|
||||
],
|
||||
@ -172,50 +297,144 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: _isLoadingRoles
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
body: _isLoadingData
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Chargement des données...'),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.edit, size: 64, color: Colors.blue),
|
||||
const SizedBox(height: 16),
|
||||
// En-tête
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit, size: 32, color: Colors.blue),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Modification d\'utilisateur',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'ID: ${widget.user.id}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Informations personnelles
|
||||
_buildSectionTitle('Informations personnelles'),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(_nameController, 'Prénom', Icons.person),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(_lastNameController, 'Nom', Icons.person_outline),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(_emailController, 'Email', Icons.email, keyboardType: TextInputType.emailAddress),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Informations de connexion
|
||||
_buildSectionTitle('Informations de connexion'),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(_usernameController, 'Nom d\'utilisateur', Icons.account_circle),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
_passwordController,
|
||||
'Mot de passe (laisser vide si inchangé)',
|
||||
'Nouveau mot de passe (optionnel)',
|
||||
Icons.lock,
|
||||
obscureText: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Permissions et affectation
|
||||
_buildSectionTitle('Permissions et affectation'),
|
||||
const SizedBox(height: 12),
|
||||
_buildDropdown(),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _updateUser,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0015B7),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
_buildRoleDropdown(),
|
||||
const SizedBox(height: 12),
|
||||
_buildPointDeVenteDropdown(),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Boutons d'action
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
side: const BorderSide(color: Colors.grey),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text('Mettre à jour', style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _updateUser,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0015B7),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Mettre à jour',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -225,6 +444,26 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.grey, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color.fromARGB(255, 4, 54, 95),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(
|
||||
TextEditingController controller,
|
||||
String label,
|
||||
@ -236,21 +475,31 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
prefixIcon: Icon(icon),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
prefixIcon: Icon(icon, color: Colors.grey.shade600),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF0015B7), width: 2),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
),
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDropdown() {
|
||||
Widget _buildRoleDropdown() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.grey.shade50,
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<Role>(
|
||||
@ -269,7 +518,7 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
value: role,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.badge, size: 20),
|
||||
Icon(Icons.badge, size: 20, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Text(role.designation),
|
||||
],
|
||||
@ -280,4 +529,72 @@ class _EditUserPageState extends State<EditUserPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPointDeVenteDropdown() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.grey.shade50,
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<Map<String, dynamic>?>(
|
||||
value: _selectedPointDeVente,
|
||||
isExpanded: true,
|
||||
hint: const Text('Sélectionner un point de vente (optionnel)'),
|
||||
onChanged: _isLoading
|
||||
? null
|
||||
: (Map<String, dynamic>? newValue) {
|
||||
setState(() {
|
||||
_selectedPointDeVente = newValue;
|
||||
});
|
||||
},
|
||||
items: [
|
||||
// Option "Aucun point de vente"
|
||||
const DropdownMenuItem<Map<String, dynamic>?>(
|
||||
value: null,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.not_interested, size: 20, color: Colors.grey),
|
||||
SizedBox(width: 8),
|
||||
Text('Aucun point de vente'),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Points de vente disponibles
|
||||
..._pointsDeVente.map((pointDeVente) {
|
||||
return DropdownMenuItem<Map<String, dynamic>>(
|
||||
value: pointDeVente,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.store, size: 20, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(pointDeVente['nom'] ?? 'N/A'),
|
||||
if (pointDeVente['code'] != null)
|
||||
Text(
|
||||
'Code: ${pointDeVente['code']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -31,30 +31,38 @@ class _AjoutPointDeVentePageState extends State<AjoutPointDeVentePage> {
|
||||
_searchController.addListener(_filterPointsDeVente);
|
||||
}
|
||||
|
||||
Future<void> _loadPointsDeVente() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
Future<void> _loadPointsDeVente() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final points = await _appDatabase.getPointsDeVente();
|
||||
|
||||
try {
|
||||
final points = await _appDatabase.getPointsDeVente();
|
||||
setState(() {
|
||||
_pointsDeVente = points;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible de charger les points de vente: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
// Enrichir chaque point de vente avec les informations de contraintes
|
||||
for (var point in points) {
|
||||
final verification = await _appDatabase.checkCanDeletePointDeVente(point['id']);
|
||||
point['canDelete'] = verification['canDelete'];
|
||||
point['constraintCount'] = (verification['reasons'] as List).length;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_pointsDeVente = points;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible de charger les points de vente: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _filterPointsDeVente() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
@ -112,56 +120,357 @@ class _AjoutPointDeVentePageState extends State<AjoutPointDeVentePage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Future<void> _showConstraintDialog(int id, Map<String, dynamic> verificationResult) async {
|
||||
final reasons = verificationResult['reasons'] as List<String>;
|
||||
final suggestions = verificationResult['suggestions'] as List<String>;
|
||||
|
||||
await Get.dialog(
|
||||
AlertDialog(
|
||||
title: Row(
|
||||
children: const [
|
||||
Icon(Icons.warning, color: Colors.orange),
|
||||
SizedBox(width: 8),
|
||||
Text('Suppression impossible'),
|
||||
],
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Ce point de vente ne peut pas être supprimé pour les raisons suivantes :',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...reasons.map((reason) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ', style: TextStyle(color: Colors.red)),
|
||||
Expanded(child: Text(reason)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
if (suggestions.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Solutions possibles :',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...suggestions.map((suggestion) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('💡 ', style: TextStyle(fontSize: 12)),
|
||||
Expanded(child: Text(suggestion, style: const TextStyle(fontSize: 13))),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
if (reasons.any((r) => r.contains('produit')))
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_showTransferDialog(id);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Transférer les produits'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Future<void> _showTransferDialog(int sourcePointDeVenteId) async {
|
||||
final pointsDeVente = await _appDatabase.getPointsDeVenteForTransfer(sourcePointDeVenteId);
|
||||
|
||||
if (pointsDeVente.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Aucun autre point de vente disponible pour le transfert',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int? selectedPointDeVenteId;
|
||||
|
||||
await Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text('Transférer les produits'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Sélectionnez le point de vente de destination pour les produits :'),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: DropdownButtonFormField<int>(
|
||||
value: selectedPointDeVenteId,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Point de vente de destination',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: pointsDeVente.map((pv) => DropdownMenuItem<int>(
|
||||
value: pv['id'] as int,
|
||||
child: Text(pv['nom'] as String),
|
||||
)).toList(),
|
||||
onChanged: (value) {
|
||||
selectedPointDeVenteId = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (selectedPointDeVenteId != null) {
|
||||
Get.back();
|
||||
await _performTransferAndDelete(sourcePointDeVenteId, selectedPointDeVenteId!);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Veuillez sélectionner un point de vente de destination',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Transférer et supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Nouvelle méthode pour effectuer le transfert et la suppression
|
||||
Future<void> _performTransferAndDelete(int sourceId, int targetId) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
Future<void> _deletePointDeVente(int id) async {
|
||||
try {
|
||||
// Afficher un dialog de confirmation final
|
||||
final confirmed = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('Confirmer la suppression'),
|
||||
content: const Text('Voulez-vous vraiment supprimer ce point de vente ?'),
|
||||
title: const Text('Confirmation finale'),
|
||||
content: const Text(
|
||||
'Cette action va transférer tous les produits vers le point de vente sélectionné '
|
||||
'puis supprimer définitivement le point de vente original. '
|
||||
'Cette action est irréversible. Continuer ?'
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Confirmer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
await _appDatabase.deletePointDeVenteWithTransfer(sourceId, targetId);
|
||||
await _loadPointsDeVente();
|
||||
|
||||
Get.snackbar(
|
||||
'Succès',
|
||||
'Produits transférés et point de vente supprimé avec succès',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Erreur lors du transfert: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
// Vous pouvez aussi ajouter une méthode pour voir les détails d'un point de vente
|
||||
Future<void> _showPointDeVenteDetails(Map<String, dynamic> pointDeVente) async {
|
||||
final id = pointDeVente['id'] as int;
|
||||
|
||||
try {
|
||||
// Récupérer les statistiques
|
||||
final stats = await _getPointDeVenteStats(id);
|
||||
|
||||
await Get.dialog(
|
||||
AlertDialog(
|
||||
title: Text('Détails: ${pointDeVente['nom']}'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildStatRow('Produits associés', '${stats['produits']}'),
|
||||
_buildStatRow('Utilisateurs associés', '${stats['utilisateurs']}'),
|
||||
_buildStatRow('Demandes de transfert', '${stats['transferts']}'),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Code: ${pointDeVente['code'] ?? 'N/A'}',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible de récupérer les détails: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await _appDatabase.deletePointDeVente(id);
|
||||
await _loadPointsDeVente();
|
||||
|
||||
Get.snackbar(
|
||||
'Succès',
|
||||
'Point de vente supprimé avec succès',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible de supprimer le point de vente: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
Widget _buildStatRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label),
|
||||
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Méthode helper pour récupérer les stats
|
||||
Future<Map<String, int>> _getPointDeVenteStats(int id) async {
|
||||
final verification = await _appDatabase.checkCanDeletePointDeVente(id);
|
||||
|
||||
// Parser les raisons pour extraire les nombres
|
||||
int produits = 0, utilisateurs = 0, transferts = 0;
|
||||
|
||||
for (String reason in verification['reasons']) {
|
||||
if (reason.contains('produit')) {
|
||||
produits = int.tryParse(reason.split(' ')[0]) ?? 0;
|
||||
} else if (reason.contains('utilisateur')) {
|
||||
utilisateurs = int.tryParse(reason.split(' ')[0]) ?? 0;
|
||||
} else if (reason.contains('transfert')) {
|
||||
transferts = int.tryParse(reason.split(' ')[0]) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'produits': produits,
|
||||
'utilisateurs': utilisateurs,
|
||||
'transferts': transferts,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _deletePointDeVente(int id) async {
|
||||
// 1. D'abord vérifier si la suppression est possible
|
||||
final verificationResult = await _appDatabase.checkCanDeletePointDeVente(id);
|
||||
|
||||
if (!verificationResult['canDelete']) {
|
||||
// Afficher un dialog avec les détails des contraintes
|
||||
await _showConstraintDialog(id, verificationResult);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Si pas de contraintes, procéder normalement
|
||||
final confirmed = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('Confirmer la suppression'),
|
||||
content: const Text('Voulez-vous vraiment supprimer ce point de vente ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await _appDatabase.deletePointDeVente(id);
|
||||
await _loadPointsDeVente();
|
||||
|
||||
Get.snackbar(
|
||||
'Succès',
|
||||
'Point de vente supprimé avec succès',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Impossible de supprimer le point de vente: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -354,48 +663,154 @@ class _AjoutPointDeVentePageState extends State<AjoutPointDeVentePage> {
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: _pointsDeVente.length,
|
||||
itemBuilder: (context, index) {
|
||||
final point = _pointsDeVente[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
point['nom'] ?? 'N/A',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
point['code'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 40,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.delete,
|
||||
size: 20, color: Colors.red),
|
||||
onPressed: () => _deletePointDeVente(point['id']),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
itemCount: _pointsDeVente.length,
|
||||
itemBuilder: (context, index) {
|
||||
final point = _pointsDeVente[index];
|
||||
final canDelete = point['canDelete'] ?? true;
|
||||
final constraintCount = point['constraintCount'] ?? 0;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: !canDelete ? Border.all(
|
||||
color: Colors.orange.shade300,
|
||||
width: 1,
|
||||
) : null,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icône avec indicateur de statut
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: canDelete ? Colors.blue.shade50 : Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.store,
|
||||
color: canDelete ? Colors.blue.shade700 : Colors.orange.shade700,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
if (!canDelete)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text(
|
||||
'$constraintCount',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Informations du point de vente
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
point['nom'] ?? 'N/A',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!canDelete)
|
||||
Icon(
|
||||
Icons.link,
|
||||
size: 16,
|
||||
color: Colors.orange.shade600,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (point['code'] != null && point['code'].toString().isNotEmpty)
|
||||
Text(
|
||||
'Code: ${point['code']}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (!canDelete) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$constraintCount contrainte(s)',
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade600,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Boutons d'actions
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Bouton détails
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
color: Colors.blue.shade600,
|
||||
),
|
||||
onPressed: () => _showPointDeVenteDetails(point),
|
||||
tooltip: 'Voir les détails',
|
||||
),
|
||||
|
||||
// Bouton suppression avec indication visuelle
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
canDelete ? Icons.delete_outline : Icons.delete_forever_outlined,
|
||||
size: 20,
|
||||
color: canDelete ? Colors.red : Colors.orange.shade700,
|
||||
),
|
||||
onPressed: () => _deletePointDeVente(point['id']),
|
||||
tooltip: canDelete ? 'Supprimer' : 'Supprimer (avec contraintes)',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
354
lib/Views/historique_sorties_personnelles_page.dart
Normal file
354
lib/Views/historique_sorties_personnelles_page.dart
Normal file
@ -0,0 +1,354 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:youmazgestion/Components/app_bar.dart';
|
||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
|
||||
class HistoriqueSortiesPersonnellesPage extends StatefulWidget {
|
||||
const HistoriqueSortiesPersonnellesPage({super.key});
|
||||
|
||||
@override
|
||||
_HistoriqueSortiesPersonnellesPageState createState() => _HistoriqueSortiesPersonnellesPageState();
|
||||
}
|
||||
|
||||
class _HistoriqueSortiesPersonnellesPageState extends State<HistoriqueSortiesPersonnellesPage> {
|
||||
final AppDatabase _database = AppDatabase.instance;
|
||||
final UserController _userController = Get.find<UserController>();
|
||||
|
||||
List<Map<String, dynamic>> _historique = [];
|
||||
String? _filtreStatut;
|
||||
bool _isLoading = false;
|
||||
bool _afficherSeulementMesDemandes = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadHistorique();
|
||||
}
|
||||
|
||||
Future<void> _loadHistorique() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final historique = await _database.getHistoriqueSortiesPersonnelles(
|
||||
adminId: _afficherSeulementMesDemandes ? _userController.userId : null,
|
||||
statut: _filtreStatut,
|
||||
limit: 100,
|
||||
);
|
||||
setState(() {
|
||||
_historique = historique;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
Get.snackbar('Erreur', 'Impossible de charger l\'historique: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatutColor(String statut) {
|
||||
switch (statut) {
|
||||
case 'en_attente':
|
||||
return Colors.orange;
|
||||
case 'approuvee':
|
||||
return Colors.green;
|
||||
case 'refusee':
|
||||
return Colors.red;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatutText(String statut) {
|
||||
switch (statut) {
|
||||
case 'en_attente':
|
||||
return 'En attente';
|
||||
case 'approuvee':
|
||||
return 'Approuvée';
|
||||
case 'refusee':
|
||||
return 'Refusée';
|
||||
default:
|
||||
return statut;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(title: 'Historique sorties personnelles'),
|
||||
drawer: CustomDrawer(),
|
||||
body: Column(
|
||||
children: [
|
||||
// Filtres
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.grey.shade50,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _filtreStatut,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Filtrer par statut',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: null, child: Text('Tous les statuts')),
|
||||
DropdownMenuItem(value: 'en_attente', child: Text('En attente')),
|
||||
DropdownMenuItem(value: 'approuvee', child: Text('Approuvées')),
|
||||
DropdownMenuItem(value: 'refusee', child: Text('Refusées')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_filtreStatut = value;
|
||||
});
|
||||
_loadHistorique();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _loadHistorique,
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: const Text('Actualiser'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _afficherSeulementMesDemandes,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_afficherSeulementMesDemandes = value ?? false;
|
||||
});
|
||||
_loadHistorique();
|
||||
},
|
||||
),
|
||||
const Text('Afficher seulement mes demandes'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Liste
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadHistorique,
|
||||
child: _historique.isEmpty
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.history, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucun historique trouvé',
|
||||
style: TextStyle(fontSize: 18, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _historique.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sortie = _historique[index];
|
||||
return _buildHistoriqueCard(sortie);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistoriqueCard(Map<String, dynamic> sortie) {
|
||||
final dateSortie = DateTime.parse(sortie['date_sortie'].toString());
|
||||
final statut = sortie['statut'].toString();
|
||||
final statutColor = _getStatutColor(statut);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: statutColor.withOpacity(0.3), width: 1),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec statut et date
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statutColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: statutColor.withOpacity(0.5)),
|
||||
),
|
||||
child: Text(
|
||||
_getStatutText(statut),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statutColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
DateFormat('dd/MM/yyyy HH:mm').format(dateSortie),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Informations principales
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sortie['produit_nom'].toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text('Réf: ${sortie['produit_reference'] ?? 'N/A'}'),
|
||||
Text('Quantité: ${sortie['quantite']}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Demandeur:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${sortie['admin_nom']} ${sortie['admin_nom_famille'] ?? ''}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Motif
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Motif:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
Text(sortie['motif'].toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Informations d'approbation/refus
|
||||
if (statut != 'en_attente' && sortie['approbateur_nom'] != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: statutColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
statut == 'approuvee' ? 'Approuvé par:' : 'Refusé par:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
Text('${sortie['approbateur_nom']} ${sortie['approbateur_nom_famille'] ?? ''}'),
|
||||
if (sortie['date_approbation'] != null)
|
||||
Text(
|
||||
'Le ${DateFormat('dd/MM/yyyy HH:mm').format(DateTime.parse(sortie['date_approbation'].toString()))}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Notes supplémentaires
|
||||
if (sortie['notes'] != null && sortie['notes'].toString().isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Notes:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
sortie['notes'].toString(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ import 'package:get/get.dart';
|
||||
import 'package:youmazgestion/Models/users.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
import '../Components/app_bar.dart';
|
||||
//import '../Services/app_database.dart';
|
||||
import 'editUser.dart';
|
||||
|
||||
class ListUserPage extends StatefulWidget {
|
||||
@ -15,112 +14,654 @@ class ListUserPage extends StatefulWidget {
|
||||
|
||||
class _ListUserPageState extends State<ListUserPage> {
|
||||
List<Users> userList = [];
|
||||
List<Users> filteredUserList = [];
|
||||
List<Map<String, dynamic>> pointsDeVente = [];
|
||||
bool isLoading = true;
|
||||
String searchQuery = '';
|
||||
int? selectedPointDeVenteFilter;
|
||||
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getUsersFromDatabase();
|
||||
_loadData();
|
||||
_searchController.addListener(_filterUsers);
|
||||
}
|
||||
|
||||
Future<void> getUsersFromDatabase() async {
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
List<Users> users = await AppDatabase.instance.getAllUsers();
|
||||
// Charger les utilisateurs et points de vente en parallèle
|
||||
final futures = await Future.wait([
|
||||
AppDatabase.instance.getAllUsers(),
|
||||
AppDatabase.instance.getPointsDeVente(),
|
||||
]);
|
||||
|
||||
final users = futures[0] as List<Users>;
|
||||
final points = futures[1] as List<Map<String, dynamic>>;
|
||||
|
||||
setState(() {
|
||||
userList = users;
|
||||
filteredUserList = users;
|
||||
pointsDeVente = points;
|
||||
isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
print('Erreur lors du chargement: $e');
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
_showErrorSnackbar('Erreur lors du chargement des données');
|
||||
}
|
||||
}
|
||||
|
||||
void _filterUsers() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
searchQuery = query;
|
||||
filteredUserList = userList.where((user) {
|
||||
final matchesSearch = query.isEmpty ||
|
||||
user.name.toLowerCase().contains(query) ||
|
||||
user.lastName.toLowerCase().contains(query) ||
|
||||
user.username.toLowerCase().contains(query) ||
|
||||
user.email.toLowerCase().contains(query) ||
|
||||
(user.roleName?.toLowerCase().contains(query) ?? false);
|
||||
|
||||
final matchesPointDeVente = selectedPointDeVenteFilter == null ||
|
||||
user.pointDeVenteId == selectedPointDeVenteFilter;
|
||||
|
||||
return matchesSearch && matchesPointDeVente;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
void _onPointDeVenteFilterChanged(int? pointDeVenteId) {
|
||||
setState(() {
|
||||
selectedPointDeVenteFilter = pointDeVenteId;
|
||||
});
|
||||
_filterUsers();
|
||||
}
|
||||
|
||||
String _getPointDeVenteName(int? pointDeVenteId) {
|
||||
if (pointDeVenteId == null) return 'Aucun';
|
||||
|
||||
try {
|
||||
final point = pointsDeVente.firstWhere((p) => p['id'] == pointDeVenteId);
|
||||
return point['nom'] ?? 'Inconnu';
|
||||
} catch (e) {
|
||||
return 'Inconnu';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteUser(Users user, int index) async {
|
||||
// Vérifier si l'utilisateur peut être supprimé
|
||||
final canDelete = await _checkCanDeleteUser(user);
|
||||
|
||||
if (!canDelete['canDelete']) {
|
||||
_showCannotDeleteDialog(user, canDelete['reason']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Afficher la confirmation de suppression
|
||||
final confirmed = await _showDeleteConfirmation(user);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await AppDatabase.instance.deleteUser(user.id!);
|
||||
|
||||
setState(() {
|
||||
userList.removeWhere((u) => u.id == user.id);
|
||||
filteredUserList.removeWhere((u) => u.id == user.id);
|
||||
});
|
||||
|
||||
_showSuccessSnackbar('Utilisateur supprimé avec succès');
|
||||
} catch (e) {
|
||||
print('Erreur lors de la suppression: $e');
|
||||
_showErrorSnackbar('Erreur lors de la suppression de l\'utilisateur');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _checkCanDeleteUser(Users user) async {
|
||||
// Ici vous pouvez ajouter des vérifications métier
|
||||
// Par exemple, vérifier si l'utilisateur a des commandes en cours, etc.
|
||||
|
||||
// Pour l'instant, on autorise la suppression sauf pour le Super Admin
|
||||
if (user.roleName?.toLowerCase() == 'super admin') {
|
||||
return {
|
||||
'canDelete': false,
|
||||
'reason': 'Le Super Admin ne peut pas être supprimé pour des raisons de sécurité.'
|
||||
};
|
||||
}
|
||||
|
||||
// Vérifier s'il y a des commandes associées à cet utilisateur
|
||||
try {
|
||||
final db = AppDatabase.instance;
|
||||
final commandes = await db.database.then((connection) =>
|
||||
connection.query('SELECT COUNT(*) as count FROM commandes WHERE commandeurId = ? OR validateurId = ?',
|
||||
[user.id, user.id])
|
||||
);
|
||||
|
||||
final commandeCount = commandes.first['count'] as int;
|
||||
if (commandeCount > 0) {
|
||||
return {
|
||||
'canDelete': false,
|
||||
'reason': 'Cet utilisateur a $commandeCount commande(s) associée(s). Impossible de le supprimer.'
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
print('Erreur lors de la vérification des contraintes: $e');
|
||||
}
|
||||
|
||||
return {'canDelete': true, 'reason': ''};
|
||||
}
|
||||
|
||||
Future<bool> _showDeleteConfirmation(Users user) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: const [
|
||||
Icon(Icons.warning, color: Colors.orange),
|
||||
SizedBox(width: 8),
|
||||
Text("Confirmer la suppression"),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Êtes-vous sûr de vouloir supprimer cet utilisateur ?"),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Nom: ${user.name} ${user.lastName}",
|
||||
style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
Text("Username: ${user.username}"),
|
||||
Text("Email: ${user.email}"),
|
||||
Text("Rôle: ${user.roleName ?? 'N/A'}"),
|
||||
Text("Point de vente: ${_getPointDeVenteName(user.pointDeVenteId)}"),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
"Cette action est irréversible.",
|
||||
style: TextStyle(color: Colors.red, fontStyle: FontStyle.italic),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text("Annuler"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text("Supprimer"),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
void _showCannotDeleteDialog(Users user, String reason) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: const [
|
||||
Icon(Icons.block, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text("Suppression impossible"),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"L'utilisateur ${user.name} ${user.lastName} ne peut pas être supprimé.",
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info, color: Colors.red, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(reason)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text("Compris"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showUserDetails(Users user) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text("Détails de ${user.name} ${user.lastName}"),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildDetailRow("ID", "${user.id}"),
|
||||
_buildDetailRow("Prénom", user.name),
|
||||
_buildDetailRow("Nom", user.lastName),
|
||||
_buildDetailRow("Username", user.username),
|
||||
_buildDetailRow("Email", user.email),
|
||||
_buildDetailRow("Rôle", user.roleName ?? 'N/A'),
|
||||
_buildDetailRow("Point de vente", _getPointDeVenteName(user.pointDeVenteId)),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("Fermer"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Get.to(() => EditUserPage(user: user))?.then((_) => _loadData());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text("Modifier"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
"$label:",
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Expanded(child: Text(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSuccessSnackbar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.check_circle, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
Text(message),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorSnackbar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.error, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
Text(message),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(title: 'Liste des utilisateurs'),
|
||||
body: ListView.builder(
|
||||
itemCount: userList.length,
|
||||
itemBuilder: (context, index) {
|
||||
Users user = userList[index];
|
||||
return Card(
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shadowColor: Colors.deepOrange,
|
||||
borderOnForeground: true,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
"${user.name} ${user.lastName}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
appBar: CustomAppBar(title: 'Liste des utilisateurs'),
|
||||
body: isLoading
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text("Username: ${user.username}"),
|
||||
const SizedBox(height: 4),
|
||||
Text("Privilège: ${user.role}"),
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Chargement des utilisateurs...'),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
color: Colors.red,
|
||||
onPressed: () {
|
||||
// Action de suppression
|
||||
// Vous pouvez appeler une méthode de suppression appropriée ici
|
||||
// confirmation de suppression
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text("Supprimer"),
|
||||
content: const Text(
|
||||
"Êtes-vous sûr de vouloir supprimer cet utilisateur?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text("Annuler"),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
// Barre de recherche et filtres
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.grey.shade50,
|
||||
child: Column(
|
||||
children: [
|
||||
// Barre de recherche
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher par nom, username, email...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Filtre par point de vente
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.filter_list, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Point de vente:'),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<int?>(
|
||||
value: selectedPointDeVenteFilter,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await AppDatabase.instance
|
||||
.deleteUser(user.id!);
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
userList.removeAt(index);
|
||||
});
|
||||
},
|
||||
child: const Text("Supprimer"),
|
||||
items: [
|
||||
const DropdownMenuItem<int?>(
|
||||
value: null,
|
||||
child: Text('Tous'),
|
||||
),
|
||||
...pointsDeVente.map((point) => DropdownMenuItem<int?>(
|
||||
value: point['id'] as int,
|
||||
child: Text(point['nom'] ?? 'N/A'),
|
||||
)),
|
||||
],
|
||||
onChanged: _onPointDeVenteFilterChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Statistiques
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
color: Colors.blue.shade50,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Total: ${userList.length} utilisateur(s)',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (filteredUserList.length != userList.length)
|
||||
Text(
|
||||
'Affichés: ${filteredUserList.length}',
|
||||
style: TextStyle(
|
||||
color: Colors.blue.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Liste des utilisateurs
|
||||
Expanded(
|
||||
child: filteredUserList.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
searchQuery.isNotEmpty ? Icons.search_off : Icons.people_outline,
|
||||
size: 64,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
searchQuery.isNotEmpty
|
||||
? 'Aucun utilisateur trouvé'
|
||||
: 'Aucun utilisateur dans la base de données',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadData,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: filteredUserList.length,
|
||||
itemBuilder: (context, index) {
|
||||
Users user = filteredUserList[index];
|
||||
return _buildUserCard(user, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserCard(Users user, int index) {
|
||||
final pointDeVenteName = _getPointDeVenteName(user.pointDeVenteId);
|
||||
final isSuperAdmin = user.roleName?.toLowerCase() == 'super admin';
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: isSuperAdmin
|
||||
? BorderSide(color: Colors.orange.shade300, width: 1)
|
||||
: BorderSide.none,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _showUserDetails(user),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec nom et badge
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: isSuperAdmin ? Colors.orange.shade100 : Colors.blue.shade100,
|
||||
child: Icon(
|
||||
isSuperAdmin ? Icons.admin_panel_settings : Icons.person,
|
||||
color: isSuperAdmin ? Colors.orange.shade700 : Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
color: Colors.blue,
|
||||
onPressed: () {
|
||||
// Action de modification
|
||||
// Vous pouvez naviguer vers la page de modification avec les détails de l'utilisateur
|
||||
// en utilisant Navigator.push ou showDialog, selon votre besoin
|
||||
Get.to(() => EditUserPage(user: user));
|
||||
},
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${user.name} ${user.lastName}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"@${user.username}",
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSuperAdmin)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange.shade300),
|
||||
),
|
||||
child: Text(
|
||||
'ADMIN',
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade800,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Informations détaillées
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoChip(Icons.email, user.email),
|
||||
const SizedBox(height: 4),
|
||||
_buildInfoChip(Icons.badge, user.roleName ?? 'N/A'),
|
||||
const SizedBox(height: 4),
|
||||
_buildInfoChip(Icons.store, pointDeVenteName),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Boutons d'actions
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.visibility, size: 20),
|
||||
color: Colors.blue.shade600,
|
||||
onPressed: () => _showUserDetails(user),
|
||||
tooltip: 'Voir les détails',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
color: Colors.green.shade600,
|
||||
onPressed: () {
|
||||
Get.to(() => EditUserPage(user: user))?.then((_) => _loadData());
|
||||
},
|
||||
tooltip: 'Modifier',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isSuperAdmin ? Icons.lock : Icons.delete,
|
||||
size: 20,
|
||||
),
|
||||
color: isSuperAdmin ? Colors.grey : Colors.red.shade600,
|
||||
onPressed: isSuperAdmin
|
||||
? null
|
||||
: () => _deleteUser(user, index),
|
||||
tooltip: isSuperAdmin ? 'Protection Super Admin' : 'Supprimer',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInfoChip(IconData icon, String text) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 14, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user