Browse Source

last update update

20062025_01
b.razafimandimbihery 6 months ago
parent
commit
2af3b01d92
  1. 199
      lib/Components/appDrawer.dart
  2. 13
      lib/Models/users.dart
  3. 55
      lib/Services/Script.sql
  4. 1057
      lib/Services/stock_managementDatabase.dart
  5. 847
      lib/Views/DemandeTransfert.dart
  6. 6
      lib/Views/HandleProduct.dart
  7. 451
      lib/Views/approbation_sorties_page.dart
  8. 582
      lib/Views/commandManagement.dart
  9. 724
      lib/Views/demande_sortie_personnelle_page.dart
  10. 399
      lib/Views/editUser.dart
  11. 439
      lib/Views/gestion_point_de_vente.dart
  12. 354
      lib/Views/historique_sorties_personnelles_page.dart
  13. 661
      lib/Views/listUser.dart
  14. 3190
      lib/Views/newCommand.dart

199
lib/Components/appDrawer.dart

@ -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,7 +477,9 @@ class CustomDrawer extends StatelessWidget {
Widget _buildCategoryHeader(String categoryName) {
return Padding(
padding: const EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
child: Row(
children: [
Text(
categoryName,
style: const TextStyle(
color: Colors.grey,
@ -343,6 +487,25 @@ class CustomDrawer extends StatelessWidget {
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,6 +541,8 @@ class CustomDrawer extends StatelessWidget {
fontWeight: FontWeight.bold,
),
),
Row(
children: [
Text(
controller.role.isNotEmpty ? controller.role : 'Aucun rôle',
style: const TextStyle(
@ -385,6 +550,27 @@ class CustomDrawer extends StatelessWidget {
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),
Text(
@ -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"),

13
lib/Models/users.dart

@ -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
lib/Services/Script.sql

@ -302,3 +302,58 @@ INNER JOIN roles r ON rmp.role_id = r.id
WHERE r.designation = 'Super Admin';
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;

1057
lib/Services/stock_managementDatabase.dart

File diff suppressed because it is too large

847
lib/Views/DemandeTransfert.dart

@ -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,
),
),
),
],
),
],
],
),
),
);
}
}

6
lib/Views/HandleProduct.dart

@ -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

@ -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),
),
),
),
),
],
),
],
),
),
);
}
}

582
lib/Views/commandManagement.dart

File diff suppressed because it is too large

724
lib/Views/demande_sortie_personnelle_page.dart

@ -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();
}
}

399
lib/Views/editUser.dart

@ -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);
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) {
@ -131,15 +232,29 @@ 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,
_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'),
),
),
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 CircularProgressIndicator(color: Colors.white)
: const Text('Mettre à jour', style: TextStyle(color: Colors.white, fontSize: 16)),
? 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(),
],
),
),
);
}
}

439
lib/Views/gestion_point_de_vente.dart

@ -38,6 +38,14 @@ class _AjoutPointDeVentePageState extends State<AjoutPointDeVentePage> {
try {
final points = await _appDatabase.getPointsDeVente();
// 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;
@ -112,8 +120,309 @@ 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;
});
try {
// Afficher un dialog de confirmation final
final confirmed = await Get.dialog<bool>(
AlertDialog(
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'),
),
ElevatedButton(
onPressed: () => Get.back(result: true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Confirmer'),
),
],
),
);
if (confirmed == 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,
);
}
}
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'),
@ -357,45 +666,151 @@ class _AjoutPointDeVentePageState extends State<AjoutPointDeVentePage> {
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: [
Expanded(
flex: 2,
// 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(
point['nom'] ?? 'N/A',
'$constraintCount',
style: const TextStyle(
fontWeight: FontWeight.w500),
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['code'] ?? 'N/A',
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),
color: Colors.grey.shade600,
fontSize: 12,
),
),
SizedBox(
width: 40,
child: IconButton(
icon: const Icon(Icons.delete,
size: 20, color: Colors.red),
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

@ -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),
),
],
),
),
],
],
),
),
);
}
}

661
lib/Views/listUser.dart

@ -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);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> getUsersFromDatabase() async {
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('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(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,
body: isLoading
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Chargement des utilisateurs...'),
],
),
)
: 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,
),
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 = userList[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: 3,
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
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,
),
shadowColor: Colors.deepOrange,
borderOnForeground: true,
child: ListTile(
title: Text(
),
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,
),
),
subtitle: Column(
),
],
),
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),
Text("Username: ${user.username}"),
_buildInfoChip(Icons.badge, user.roleName ?? 'N/A'),
const SizedBox(height: 4),
Text("Privilège: ${user.role}"),
_buildInfoChip(Icons.store, pointDeVenteName),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
),
// Boutons d'actions
Column(
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(
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: () {
Navigator.of(context).pop();
Get.to(() => EditUserPage(user: user))?.then((_) => _loadData());
},
child: const Text("Annuler"),
tooltip: 'Modifier',
),
TextButton(
onPressed: () async {
await AppDatabase.instance
.deleteUser(user.id!);
Navigator.of(context).pop();
setState(() {
userList.removeAt(index);
});
},
child: const Text("Supprimer"),
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',
),
],
);
},
);
},
),
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));
},
],
),
],
),
),
),
);
},
}
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,
),
),
],
);
}
}

3190
lib/Views/newCommand.dart

File diff suppressed because it is too large
Loading…
Cancel
Save