diff --git a/lib/Components/appDrawer.dart b/lib/Components/appDrawer.dart index cdea699..fcded5e 100644 --- a/lib/Components/appDrawer.dart +++ b/lib/Components/appDrawer.dart @@ -3,15 +3,14 @@ import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:youmazgestion/Views/HandleProduct.dart'; import 'package:youmazgestion/Views/RoleListPage.dart'; +import 'package:youmazgestion/Views/commandManagement.dart'; import 'package:youmazgestion/Views/historique.dart'; -import 'package:youmazgestion/Views/addProduct.dart'; import 'package:youmazgestion/Views/bilanMois.dart'; -import 'package:youmazgestion/Views/gestionProduct.dart'; import 'package:youmazgestion/Views/gestionStock.dart'; import 'package:youmazgestion/Views/listUser.dart'; import 'package:youmazgestion/Views/loginPage.dart'; +import 'package:youmazgestion/Views/newCommand.dart'; import 'package:youmazgestion/Views/registrationPage.dart'; -import 'package:youmazgestion/Views/gestionRole.dart'; import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/controller/userController.dart'; @@ -31,201 +30,230 @@ class CustomDrawer extends StatelessWidget { return Drawer( backgroundColor: Colors.white, child: ListView( + padding: EdgeInsets.zero, children: [ + // Header avec informations utilisateur GetBuilder( - builder: (controller) => UserAccountsDrawerHeader( - accountEmail: Text(controller.email), - accountName: Text(controller.name), - currentAccountPicture: const CircleAvatar( - backgroundImage: AssetImage("assets/youmaz2.png"), - ), + builder: (controller) => Container( + padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20), decoration: const BoxDecoration( gradient: LinearGradient( - colors: [Colors.white, Color.fromARGB(255, 4, 54, 95)], + colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), + child: Row( + children: [ + const CircleAvatar( + radius: 30, + backgroundImage: AssetImage("assets/youmaz2.png"), + ), + const SizedBox(width: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.name, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + controller.email, + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + Text( + controller.role, + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ], + ), ), ), - ListTile( - leading: const Icon(Icons.home), - iconColor: Colors.lightBlueAccent, - title: const Text("Accueil"), - onTap: () async { - bool hasPermission = await userController.hasPermission('view', '/accueil'); - if (hasPermission) { - Get.to(const AccueilPage()); - } else { - Get.snackbar( - "Accès refusé", - "Vous n'avez pas les droits pour accéder à cette page", - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.error), - duration: const Duration(seconds: 3), - snackPosition: SnackPosition.TOP, - ); - } - }, + + // Section Accueil + _buildDrawerItem( + icon: Icons.home, + title: "Accueil", + color: Colors.blue, + permissionAction: 'view', + permissionRoute: '/accueil', + onTap: () => Get.to(const AccueilPage()), ), - ListTile( - leading: const Icon(Icons.person_add), - iconColor: Colors.green, - title: const Text("Ajouter un utilisateur"), - onTap: () async { - bool hasPermission = await userController.hasPermission('create', '/ajouter-utilisateur'); - if (hasPermission) { - Get.to(const RegistrationPage()); - } else { - Get.snackbar( - "Accès refusé", - "Vous n'avez pas les droits pour ajouter un utilisateur", - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.error), - duration: const Duration(seconds: 3), - snackPosition: SnackPosition.TOP, - ); - } - }, + + // Section Utilisateurs + const Padding( + padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), + child: Text( + "GESTION UTILISATEURS", + style: TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), ), - ListTile( - leading: const Icon(Icons.supervised_user_circle), - iconColor: const Color.fromARGB(255, 4, 54, 95), - title: const Text("Modifier/Supprimer un utilisateur"), - onTap: () async { - bool hasPermission = await userController.hasPermission('update', '/modifier-utilisateur'); - if (hasPermission) { - Get.to(const ListUserPage()); - } else { - Get.snackbar( - "Accès refusé", - "Vous n'avez pas les droits pour modifier/supprimer un utilisateur", - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.error), - duration: const Duration(seconds: 3), - snackPosition: SnackPosition.TOP, - ); - } - }, + _buildDrawerItem( + icon: Icons.person_add, + title: "Ajouter un utilisateur", + color: Colors.green, + permissionAction: 'create', + permissionRoute: '/ajouter-utilisateur', + onTap: () => Get.to(const RegistrationPage()), ), - ListTile( - leading: const Icon(Icons.add), - iconColor: Colors.indigoAccent, - title: const Text("Gestion des produit"), - onTap: () async { - bool hasPermission = await userController.hasPermission('create', '/ajouter-produit'); - if (hasPermission) { - Get.to(() => const ProductManagementPage()); - } else { - Get.snackbar( - "Accès refusé", - "Vous n'avez pas les droits pour ajouter un produit", - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.error), - duration: const Duration(seconds: 3), - snackPosition: SnackPosition.TOP, - ); - } - }, + _buildDrawerItem( + icon: Icons.supervised_user_circle, + title: "Gérer les utilisateurs", + color: Color.fromARGB(255, 4, 54, 95), + permissionAction: 'update', + permissionRoute: '/modifier-utilisateur', + onTap: () => Get.to(const ListUserPage()), ), - ListTile( - leading: const Icon(Icons.bar_chart), - title: const Text("Bilan"), - onTap: () async { - bool hasPermission = await userController.hasPermission('read', '/bilan'); - if (hasPermission) { - Get.to(const BilanMois()); - } else { - Get.snackbar( - "Accès refusé", - "Vous n'avez pas les droits pour accéder au bilan", - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.error_outline_outlined), - duration: const Duration(seconds: 3), - snackPosition: SnackPosition.TOP, - ); - } - }, + // Section Produits + const Padding( + padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), + child: Text( + "GESTION PRODUITS", + style: TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), ), - ListTile( - leading: const Icon(Icons.warning_amber), - title: const Text("Gérer les rôles"), - onTap: () async { - bool hasPermission = await userController.hasPermission('admin', '/gerer-roles'); - if (hasPermission) { - Get.to(const RoleListPage()); - print("permission accepted"); - } else { - print("permission not accepted for" +userController.username); - Get.snackbar( - "Accès refusé ", - "Vous n'avez pas les droits pour gérer les rôles", - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.error_outline_outlined), - duration: const Duration(seconds: 3), - snackPosition: SnackPosition.TOP, - ); - } - }, + _buildDrawerItem( + icon: Icons.inventory, + title: "Gestion des produits", + color: Colors.indigoAccent, + permissionAction: 'create', + permissionRoute: '/ajouter-produit', + onTap: () => Get.to(const ProductManagementPage()), ), - ListTile( - leading: const Icon(Icons.inventory), - iconColor: Colors.blueAccent, - title: const Text("Gestion de stock"), - onTap: () async { - bool hasPermission = await userController.hasPermission('update', '/gestion-stock'); - if (hasPermission) { - Get.to(const GestionStockPage()); - } else { - Get.snackbar( - "Accès refusé", - "Vous n'avez pas les droits pour accéder à la gestion de stock", - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.error), - duration: const Duration(seconds: 3), - snackPosition: SnackPosition.TOP, - ); - } - }, + _buildDrawerItem( + icon: Icons.storage, + title: "Gestion de stock", + color: Colors.blueAccent, + permissionAction: 'update', + permissionRoute: '/gestion-stock', + onTap: () => Get.to(const GestionStockPage()), ), - ListTile( - leading: const Icon(Icons.history), - iconColor: Colors.blue, - title: const Text("Historique"), - onTap: () { - Get.to(HistoryPage()); - }, + + // Section Commandes + const Padding( + padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), + child: Text( + "GESTION COMMANDES", + style: TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + _buildDrawerItem( + icon: Icons.add_shopping_cart, + title: "Nouvelle commande", + color: Colors.orange, + permissionAction: 'create', + permissionRoute: '/nouvelle-commande', + onTap: () => Get.to(const NouvelleCommandePage()), ), - ListTile( - leading: const Icon(Icons.logout), - iconColor: Colors.red, - title: const Text("Déconnexion"), + _buildDrawerItem( + icon: Icons.list_alt, + title: "Gérer les commandes", + color: Colors.deepPurple, + permissionAction: 'manage', + permissionRoute: '/gerer-commandes', + onTap: () => Get.to(const GestionCommandesPage()), + ), + + // Section Rapports + const Padding( + padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), + child: Text( + "RAPPORTS", + style: TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + _buildDrawerItem( + icon: Icons.bar_chart, + title: "Bilan mensuel", + color: Colors.teal, + permissionAction: 'read', + permissionRoute: '/bilan', + onTap: () => Get.to(const BilanMois()), + ), + _buildDrawerItem( + icon: Icons.history, + title: "Historique", + color: Colors.blue, + permissionAction: 'read', + permissionRoute: '/historique', + onTap: () => Get.to(HistoryPage()), + ), + + // Section Administration + const Padding( + padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), + child: Text( + "ADMINISTRATION", + style: TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + _buildDrawerItem( + icon: Icons.admin_panel_settings, + title: "Gérer les rôles", + color: Colors.redAccent, + permissionAction: 'admin', + permissionRoute: '/gerer-roles', + onTap: () => Get.to(const RoleListPage()), + ), + + // Déconnexion + const Divider(), + _buildDrawerItem( + icon: Icons.logout, + title: "Déconnexion", + color: Colors.red, onTap: () { Get.defaultDialog( title: "Déconnexion", content: const Text("Voulez-vous vraiment vous déconnecter ?"), actions: [ + TextButton( + child: const Text("Non"), + onPressed: () => Get.back(), + ), ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), child: const Text("Oui"), onPressed: () { clearUserData(); Get.offAll(const LoginPage()); }, ), - ElevatedButton( - child: const Text("Non"), - onPressed: () { - Get.back(); - }, - ), ], ); }, @@ -234,4 +262,41 @@ class CustomDrawer extends StatelessWidget { ), ); } -} + + Widget _buildDrawerItem({ + required IconData icon, + required String title, + required Color color, + String? permissionAction, + String? permissionRoute, + required VoidCallback onTap, + }) { + return ListTile( + leading: Icon(icon, color: color), + title: Text(title), + trailing: permissionAction != null + ? const Icon(Icons.chevron_right, color: Colors.grey) + : null, + onTap: () async { + if (permissionAction != null && permissionRoute != null) { + bool hasPermission = await userController.hasPermission(permissionAction, permissionRoute); + if (hasPermission) { + onTap(); + } else { + Get.snackbar( + "Accès refusé", + "Vous n'avez pas les droits pour accéder à cette page", + backgroundColor: Colors.red, + colorText: Colors.white, + icon: const Icon(Icons.error), + duration: const Duration(seconds: 3), + snackPosition: SnackPosition.TOP, + ); + } + } else { + onTap(); + } + }, + ); + } +} \ No newline at end of file diff --git a/lib/Models/Client.dart b/lib/Models/Client.dart new file mode 100644 index 0000000..e450d23 --- /dev/null +++ b/lib/Models/Client.dart @@ -0,0 +1,191 @@ +// Models/client.dart +class Client { + final int? id; + final String nom; + final String prenom; + final String email; + final String telephone; + final String? adresse; + final DateTime dateCreation; + final bool actif; + + Client({ + this.id, + required this.nom, + required this.prenom, + required this.email, + required this.telephone, + this.adresse, + required this.dateCreation, + this.actif = true, + }); + + Map toMap() { + return { + 'id': id, + 'nom': nom, + 'prenom': prenom, + 'email': email, + 'telephone': telephone, + 'adresse': adresse, + 'dateCreation': dateCreation.toIso8601String(), + 'actif': actif ? 1 : 0, + }; + } + + factory Client.fromMap(Map map) { + return Client( + id: map['id'], + nom: map['nom'], + prenom: map['prenom'], + email: map['email'], + telephone: map['telephone'], + adresse: map['adresse'], + dateCreation: DateTime.parse(map['dateCreation']), + actif: map['actif'] == 1, + ); + } + + String get nomComplet => '$prenom $nom'; +} + +// Models/commande.dart +enum StatutCommande { + enAttente, + confirmee, + enPreparation, + expediee, + livree, + annulee +} + +class Commande { + final int? id; + final int clientId; + final DateTime dateCommande; + final StatutCommande statut; + final double montantTotal; + final String? notes; + final DateTime? dateLivraison; + + // Données du client (pour les jointures) + final String? clientNom; + final String? clientPrenom; + final String? clientEmail; + + Commande({ + this.id, + required this.clientId, + required this.dateCommande, + this.statut = StatutCommande.enAttente, + required this.montantTotal, + this.notes, + this.dateLivraison, + this.clientNom, + this.clientPrenom, + this.clientEmail, + }); + + Map toMap() { + return { + 'id': id, + 'clientId': clientId, + 'dateCommande': dateCommande.toIso8601String(), + 'statut': statut.index, + 'montantTotal': montantTotal, + 'notes': notes, + 'dateLivraison': dateLivraison?.toIso8601String(), + }; + } + + factory Commande.fromMap(Map map) { + return Commande( + id: map['id'], + clientId: map['clientId'], + dateCommande: DateTime.parse(map['dateCommande']), + statut: StatutCommande.values[map['statut']], + montantTotal: map['montantTotal'].toDouble(), + notes: map['notes'], + dateLivraison: map['dateLivraison'] != null + ? DateTime.parse(map['dateLivraison']) + : null, + clientNom: map['clientNom'], + clientPrenom: map['clientPrenom'], + clientEmail: map['clientEmail'], + ); + } + + String get statutLibelle { + switch (statut) { + case StatutCommande.enAttente: + return 'En attente'; + case StatutCommande.confirmee: + return 'Confirmée'; + case StatutCommande.enPreparation: + return 'En préparation'; + case StatutCommande.expediee: + return 'Expédiée'; + case StatutCommande.livree: + return 'Livrée'; + case StatutCommande.annulee: + return 'Annulée'; + } + } + + String get clientNomComplet => + clientPrenom != null && clientNom != null + ? '$clientPrenom $clientNom' + : 'Client inconnu'; +} + +// Models/detail_commande.dart +class DetailCommande { + final int? id; + final int commandeId; + final int produitId; + final int quantite; + final double prixUnitaire; + final double sousTotal; + + // Données du produit (pour les jointures) + final String? produitNom; + final String? produitImage; + final String? produitReference; + + DetailCommande({ + this.id, + required this.commandeId, + required this.produitId, + required this.quantite, + required this.prixUnitaire, + required this.sousTotal, + this.produitNom, + this.produitImage, + this.produitReference, + }); + + Map toMap() { + return { + 'id': id, + 'commandeId': commandeId, + 'produitId': produitId, + 'quantite': quantite, + 'prixUnitaire': prixUnitaire, + 'sousTotal': sousTotal, + }; + } + + factory DetailCommande.fromMap(Map map) { + return DetailCommande( + id: map['id'], + commandeId: map['commandeId'], + produitId: map['produitId'], + quantite: map['quantite'], + prixUnitaire: map['prixUnitaire'].toDouble(), + sousTotal: map['sousTotal'].toDouble(), + produitNom: map['produitNom'], + produitImage: map['produitImage'], + produitReference: map['produitReference'], + ); + } +} \ No newline at end of file diff --git a/lib/Services/app_database.dart b/lib/Services/app_database.dart index a2669dc..ac40cb1 100644 --- a/lib/Services/app_database.dart +++ b/lib/Services/app_database.dart @@ -142,37 +142,78 @@ class AppDatabase { } - Future insertDefaultPermissions() async { - final db = await database; - final existing = await db.query('permissions'); - if (existing.isEmpty) { - await db.insert('permissions', {'name': 'view'}); - await db.insert('permissions', {'name': 'create'}); - await db.insert('permissions', {'name': 'update'}); - await db.insert('permissions', {'name': 'delete'}); - await db.insert('permissions', {'name': 'admin'}); - print("Permissions par défaut insérées"); + Future insertDefaultPermissions() async { + final db = await database; + final existing = await db.query('permissions'); + if (existing.isEmpty) { + await db.insert('permissions', {'name': 'view'}); + await db.insert('permissions', {'name': 'create'}); + await db.insert('permissions', {'name': 'update'}); + await db.insert('permissions', {'name': 'delete'}); + await db.insert('permissions', {'name': 'admin'}); + await db.insert('permissions', {'name': 'manage'}); // Nouvelle permission + await db.insert('permissions', {'name': 'read'}); // Nouvelle permission + print("Permissions par défaut insérées"); + } else { + // Vérifier et ajouter les nouvelles permissions si elles n'existent pas + final newPermissions = ['manage', 'read']; + for (var permission in newPermissions) { + final existingPermission = await db.query('permissions', where: 'name = ?', whereArgs: [permission]); + if (existingPermission.isEmpty) { + await db.insert('permissions', {'name': permission}); + print("Permission ajoutée: $permission"); + } } } +} Future insertDefaultMenus() async { - final db = await database; - final existingMenus = await db.query('menu'); - - if (existingMenus.isEmpty) { - await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'}); - await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'}); - await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'}); - await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'}); - await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'}); - await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'}); - await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'}); - await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'}); - await db.insert('menu', {'name': 'Historique', 'route': '/historique'}); - await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'}); - print("Menus par défaut insérés"); + final db = await database; + final existingMenus = await db.query('menu'); + + if (existingMenus.isEmpty) { + // Menus existants + await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'}); + await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'}); + await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'}); + await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'}); + await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'}); + await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'}); + await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'}); + await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'}); + await db.insert('menu', {'name': 'Historique', 'route': '/historique'}); + await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'}); + + // Nouveaux menus ajoutés + await db.insert('menu', {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}); + await db.insert('menu', {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}); + + print("Menus par défaut insérés"); + } else { + // Si des menus existent déjà, vérifier et ajouter les nouveaux menus manquants + await _addMissingMenus(db); + } +} + +Future _addMissingMenus(Database db) async { + final menusToAdd = [ + {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, + {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, + ]; + + for (var menu in menusToAdd) { + final existing = await db.query( + 'menu', + where: 'route = ?', + whereArgs: [menu['route']], + ); + + if (existing.isEmpty) { + await db.insert('menu', menu); + print("Menu ajouté: ${menu['name']}"); } } +} Future insertDefaultRoles() async { final db = await database; @@ -197,69 +238,94 @@ class AppDatabase { } } - // Assigner quelques permissions à l'Admin et à l'User - final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']); - final createPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['create']); - final updatePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['update']); - - if (viewPermission.isNotEmpty) { - await db.insert('role_menu_permissions', { - 'role_id': adminRoleId, - 'menu_id': 1, // Assurez-vous que l'ID du menu est correct - 'permission_id': viewPermission.first['id'], - }); - await db.insert('role_menu_permissions', { - 'role_id': userRoleId, - 'menu_id': 1, // Assurez-vous que l'ID du menu est correct - 'permission_id': viewPermission.first['id'], - }); - } - - if (createPermission.isNotEmpty) { - await db.insert('role_menu_permissions', { - 'role_id': adminRoleId, - 'menu_id': 1, // Assurez-vous que l'ID du menu est correct - 'permission_id': createPermission.first['id'], - }); - } - - if (updatePermission.isNotEmpty) { - await db.insert('role_menu_permissions', { - 'role_id': adminRoleId, - 'menu_id': 1, // Assurez-vous que l'ID du menu est correct - 'permission_id': updatePermission.first['id'], - }); - } + // Assigner quelques permissions à l'Admin et à l'User pour les nouveaux menus + await _assignBasicPermissionsToRoles(db, adminRoleId, userRoleId); print("Rôles par défaut créés et permissions assignées"); } else { - // Si les rôles existent déjà, vérifier et ajouter les permissions manquantes pour le Super Admin - final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']); - if (superAdminRole.isNotEmpty) { - final superAdminRoleId = superAdminRole.first['id'] as int; - final permissions = await db.query('permissions'); - final menus = await db.query('menu'); - - // Vérifier et ajouter les permissions manquantes pour le Super Admin - for (var menu in menus) { - for (var permission in permissions) { - final existingPermission = await db.query( - 'role_menu_permissions', - where: 'role_id = ? AND menu_id = ? AND permission_id = ?', - whereArgs: [superAdminRoleId, menu['id'], permission['id']], - ); - if (existingPermission.isEmpty) { - await db.insert('role_menu_permissions', { - 'role_id': superAdminRoleId, - 'menu_id': menu['id'], - 'permission_id': permission['id'], - }); - } + // Si les rôles existent déjà, vérifier et ajouter les permissions manquantes + await _updateExistingRolePermissions(db); + } +} +// Nouvelle méthode pour assigner les permissions de base aux nouveaux menus +Future _assignBasicPermissionsToRoles(Database db, int adminRoleId, int userRoleId) async { + final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']); + final createPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['create']); + final updatePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['update']); + final managePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['manage']); + + // Récupérer les IDs des nouveaux menus + final nouvelleCommandeMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/nouvelle-commande']); + final gererCommandesMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/gerer-commandes']); + + if (nouvelleCommandeMenu.isNotEmpty && createPermission.isNotEmpty) { + // Admin peut créer de nouvelles commandes + await db.insert('role_menu_permissions', { + 'role_id': adminRoleId, + 'menu_id': nouvelleCommandeMenu.first['id'], + 'permission_id': createPermission.first['id'], + }); + + // User peut aussi créer de nouvelles commandes + await db.insert('role_menu_permissions', { + 'role_id': userRoleId, + 'menu_id': nouvelleCommandeMenu.first['id'], + 'permission_id': createPermission.first['id'], + }); + } + + if (gererCommandesMenu.isNotEmpty && managePermission.isNotEmpty) { + // Admin peut gérer les commandes + await db.insert('role_menu_permissions', { + 'role_id': adminRoleId, + 'menu_id': gererCommandesMenu.first['id'], + 'permission_id': managePermission.first['id'], + }); + } + + if (gererCommandesMenu.isNotEmpty && viewPermission.isNotEmpty) { + // User peut voir les commandes + await db.insert('role_menu_permissions', { + 'role_id': userRoleId, + 'menu_id': gererCommandesMenu.first['id'], + 'permission_id': viewPermission.first['id'], + }); + } +} +Future _updateExistingRolePermissions(Database db) async { + final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']); + if (superAdminRole.isNotEmpty) { + final superAdminRoleId = superAdminRole.first['id'] as int; + final permissions = await db.query('permissions'); + final menus = await db.query('menu'); + + // Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus + for (var menu in menus) { + for (var permission in permissions) { + final existingPermission = await db.query( + 'role_menu_permissions', + where: 'role_id = ? AND menu_id = ? AND permission_id = ?', + whereArgs: [superAdminRoleId, menu['id'], permission['id']], + ); + if (existingPermission.isEmpty) { + await db.insert('role_menu_permissions', { + 'role_id': superAdminRoleId, + 'menu_id': menu['id'], + 'permission_id': permission['id'], + }); } } + } - print("Permissions manquantes ajoutées pour le Super Admin"); + // Assigner les permissions de base aux autres rôles pour les nouveaux menus + final adminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Admin']); + final userRole = await db.query('roles', where: 'designation = ?', whereArgs: ['User']); + + if (adminRole.isNotEmpty && userRole.isNotEmpty) { + await _assignBasicPermissionsToRoles(db, adminRole.first['id'] as int, userRole.first['id'] as int); } + + print("Permissions mises à jour pour tous les rôles"); } } @@ -576,7 +642,7 @@ Future deleteDatabaseFile() async { final file = File(path); if (await file.exists()) { await file.delete(); - print("Base de données supprimée"); + print("Base de données utilisateur supprimée"); } } Future assignRoleMenuPermission(int roleId, int menuId, int permissionId) async { diff --git a/lib/Services/productDatabase.dart b/lib/Services/productDatabase.dart index ce66a7e..7e93e34 100644 --- a/lib/Services/productDatabase.dart +++ b/lib/Services/productDatabase.dart @@ -5,6 +5,8 @@ import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import '../Models/produit.dart'; +import '../Models/client.dart'; + class ProductDatabase { static final ProductDatabase instance = ProductDatabase._init(); @@ -25,6 +27,8 @@ class ProductDatabase { Future initDatabase() async { _database = await _initDB('products2.db'); await _createDB(_database, 1); + await _insertDefaultClients(); + await _insertDefaultCommandes(); } Future _initDB(String filePath) async { @@ -33,74 +37,129 @@ class ProductDatabase { bool dbExists = await File(path).exists(); if (!dbExists) { - ByteData data = await rootBundle.load('assets/database/$filePath'); - List bytes = - data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - await File(path).writeAsBytes(bytes); + try { + ByteData data = await rootBundle.load('assets/database/$filePath'); + List bytes = + data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); + await File(path).writeAsBytes(bytes); + } catch (e) { + print('Pas de fichier DB dans assets, création nouvelle DB'); + } } return await databaseFactoryFfi.openDatabase(path); } - Future _createDB(Database db, int version) async { - // Récupère la liste des colonnes de la table "products" - final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'"); - final tableNames = tables.map((row) => row['name'] as String).toList(); - - // Si la table "products" n'existe pas encore, on la crée entièrement - if (!tableNames.contains('products')) { - await db.execute(''' - CREATE TABLE products( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT, - price REAL, - image TEXT, - category TEXT, - stock INTEGER, - description TEXT, - qrCode TEXT, - reference TEXT - ) - '''); - print("Table 'products' créée avec toutes les colonnes."); - } else { - // Vérifie si les colonnes "description", "qrCode" et "reference" existent déjà + Future _createDB(Database db, int version) async { + final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'"); + final tableNames = tables.map((row) => row['name'] as String).toList(); + + // Table products (existante avec améliorations) + if (!tableNames.contains('products')) { + await db.execute(''' + CREATE TABLE products( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + price REAL NOT NULL, + image TEXT, + category TEXT NOT NULL, + stock INTEGER NOT NULL DEFAULT 0, + description TEXT, + qrCode TEXT, + reference TEXT UNIQUE + ) + '''); + print("Table 'products' créée."); + } else { + // Vérifier et ajouter les colonnes manquantes + await _updateProductsTable(db); + } + + // Table clients + if (!tableNames.contains('clients')) { + await db.execute(''' + CREATE TABLE clients( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nom TEXT NOT NULL, + prenom TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + telephone TEXT NOT NULL, + adresse TEXT, + dateCreation TEXT NOT NULL, + actif INTEGER NOT NULL DEFAULT 1 + ) + '''); + print("Table 'clients' créée."); + } + + // Table commandes + if (!tableNames.contains('commandes')) { + await db.execute(''' + CREATE TABLE commandes( + id INTEGER PRIMARY KEY AUTOINCREMENT, + clientId INTEGER NOT NULL, + dateCommande TEXT NOT NULL, + statut INTEGER NOT NULL DEFAULT 0, + montantTotal REAL NOT NULL, + notes TEXT, + dateLivraison TEXT, + FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE + ) + '''); + print("Table 'commandes' créée."); + } + + // Table détails commandes + if (!tableNames.contains('details_commandes')) { + await db.execute(''' + CREATE TABLE details_commandes( + id INTEGER PRIMARY KEY AUTOINCREMENT, + commandeId INTEGER NOT NULL, + produitId INTEGER NOT NULL, + quantite INTEGER NOT NULL, + prixUnitaire REAL NOT NULL, + sousTotal REAL NOT NULL, + FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE, + FOREIGN KEY (produitId) REFERENCES products(id) ON DELETE CASCADE + ) + '''); + print("Table 'details_commandes' créée."); + } + + // Créer les index pour optimiser les performances + await _createIndexes(db); + } + + Future _updateProductsTable(Database db) async { final columns = await db.rawQuery('PRAGMA table_info(products)'); final columnNames = columns.map((e) => e['name'] as String).toList(); - // Ajoute la colonne "description" si elle n'existe pas if (!columnNames.contains('description')) { - try { - await db.execute("ALTER TABLE products ADD COLUMN description TEXT"); - print("Colonne 'description' ajoutée."); - } catch (e) { - print("Erreur ajout colonne description : $e"); - } + await db.execute("ALTER TABLE products ADD COLUMN description TEXT"); + print("Colonne 'description' ajoutée."); } - - // Ajoute la colonne "qrCode" si elle n'existe pas if (!columnNames.contains('qrCode')) { - try { - await db.execute("ALTER TABLE products ADD COLUMN qrCode TEXT"); - print("Colonne 'qrCode' ajoutée."); - } catch (e) { - print("Erreur ajout colonne qrCode : $e"); - } + await db.execute("ALTER TABLE products ADD COLUMN qrCode TEXT"); + print("Colonne 'qrCode' ajoutée."); } - - // Ajoute la colonne "reference" si elle n'existe pas if (!columnNames.contains('reference')) { - try { - await db.execute("ALTER TABLE products ADD COLUMN reference TEXT"); - print("Colonne 'reference' ajoutée."); - } catch (e) { - print("Erreur ajout colonne reference : $e"); - } + await db.execute("ALTER TABLE products ADD COLUMN reference TEXT"); + print("Colonne 'reference' ajoutée."); } } -} + Future _createIndexes(Database db) async { + await db.execute('CREATE INDEX IF NOT EXISTS idx_products_category ON products(category)'); + await db.execute('CREATE INDEX IF NOT EXISTS idx_products_reference ON products(reference)'); + await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_client ON commandes(clientId)'); + await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_date ON commandes(dateCommande)'); + await db.execute('CREATE INDEX IF NOT EXISTS idx_details_commande ON details_commandes(commandeId)'); + print("Index créés pour optimiser les performances."); + } + // ========================= + // MÉTHODES PRODUCTS (existantes) + // ========================= Future createProduct(Product product) async { final db = await database; return await db.insert('products', product.toMap()); @@ -108,7 +167,7 @@ class ProductDatabase { Future> getProducts() async { final db = await database; - final maps = await db.query('products'); + final maps = await db.query('products', orderBy: 'name ASC'); return List.generate(maps.length, (i) { return Product.fromMap(maps[i]); }); @@ -135,7 +194,7 @@ class ProductDatabase { Future> getCategories() async { final db = await database; - final result = await db.rawQuery('SELECT DISTINCT category FROM products'); + final result = await db.rawQuery('SELECT DISTINCT category FROM products ORDER BY category'); return List.generate( result.length, (index) => result[index]['category'] as String); } @@ -143,7 +202,7 @@ class ProductDatabase { Future> getProductsByCategory(String category) async { final db = await database; final maps = await db - .query('products', where: 'category = ?', whereArgs: [category]); + .query('products', where: 'category = ?', whereArgs: [category], orderBy: 'name ASC'); return List.generate(maps.length, (i) { return Product.fromMap(maps[i]); }); @@ -154,19 +213,347 @@ class ProductDatabase { return await db .rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]); } - // Ajouter cette méthode dans la classe ProductDatabase -Future getProductByReference(String reference) async { + Future getProductByReference(String reference) async { + final db = await database; + final maps = await db.query( + 'products', + where: 'reference = ?', + whereArgs: [reference], + ); + + if (maps.isNotEmpty) { + return Product.fromMap(maps.first); + } + return null; + } + + // ========================= + // MÉTHODES CLIENTS + // ========================= + Future createClient(Client client) async { + final db = await database; + return await db.insert('clients', client.toMap()); + } + + Future> getClients() async { + final db = await database; + final maps = await db.query('clients', where: 'actif = 1', orderBy: 'nom ASC, prenom ASC'); + return List.generate(maps.length, (i) { + return Client.fromMap(maps[i]); + }); + } + + Future getClientById(int id) async { + final db = await database; + final maps = await db.query('clients', where: 'id = ?', whereArgs: [id]); + if (maps.isNotEmpty) { + return Client.fromMap(maps.first); + } + return null; + } + + Future updateClient(Client client) async { + final db = await database; + return await db.update( + 'clients', + client.toMap(), + where: 'id = ?', + whereArgs: [client.id], + ); + } + + Future deleteClient(int id) async { + final db = await database; + // Soft delete + return await db.update( + 'clients', + {'actif': 0}, + where: 'id = ?', + whereArgs: [id], + ); + } + + Future> searchClients(String query) async { + final db = await database; + final maps = await db.query( + 'clients', + where: 'actif = 1 AND (nom LIKE ? OR prenom LIKE ? OR email LIKE ?)', + whereArgs: ['%$query%', '%$query%', '%$query%'], + orderBy: 'nom ASC, prenom ASC', + ); + return List.generate(maps.length, (i) { + return Client.fromMap(maps[i]); + }); + } + + // ========================= + // MÉTHODES COMMANDES + // ========================= + Future createCommande(Commande commande) async { + final db = await database; + return await db.insert('commandes', commande.toMap()); + } + + Future> getCommandes() async { + final db = await database; + final maps = await db.rawQuery(''' + SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail + FROM commandes c + LEFT JOIN clients cl ON c.clientId = cl.id + ORDER BY c.dateCommande DESC + '''); + return List.generate(maps.length, (i) { + return Commande.fromMap(maps[i]); + }); + } + + Future> getCommandesByClient(int clientId) async { + final db = await database; + final maps = await db.rawQuery(''' + SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail + FROM commandes c + LEFT JOIN clients cl ON c.clientId = cl.id + WHERE c.clientId = ? + ORDER BY c.dateCommande DESC + ''', [clientId]); + return List.generate(maps.length, (i) { + return Commande.fromMap(maps[i]); + }); + } + + Future getCommandeById(int id) async { + final db = await database; + final maps = await db.rawQuery(''' + SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail + FROM commandes c + LEFT JOIN clients cl ON c.clientId = cl.id + WHERE c.id = ? + ''', [id]); + if (maps.isNotEmpty) { + return Commande.fromMap(maps.first); + } + return null; + } + + Future updateCommande(Commande commande) async { + final db = await database; + return await db.update( + 'commandes', + commande.toMap(), + where: 'id = ?', + whereArgs: [commande.id], + ); + } + + Future updateStatutCommande(int commandeId, StatutCommande statut) async { + final db = await database; + return await db.update( + 'commandes', + {'statut': statut.index}, + where: 'id = ?', + whereArgs: [commandeId], + ); + } + + // ========================= + // MÉTHODES DÉTAILS COMMANDES + // ========================= + Future createDetailCommande(DetailCommande detail) async { + final db = await database; + return await db.insert('details_commandes', detail.toMap()); + } + + Future> getDetailsCommande(int commandeId) async { + final db = await database; + final maps = await db.rawQuery(''' + SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference + FROM details_commandes dc + LEFT JOIN products p ON dc.produitId = p.id + WHERE dc.commandeId = ? + ORDER BY dc.id + ''', [commandeId]); + return List.generate(maps.length, (i) { + return DetailCommande.fromMap(maps[i]); + }); + } + + // ========================= + // MÉTHODES TRANSACTION COMPLÈTE + // ========================= + Future createCommandeComplete(Client client, Commande commande, List details) async { final db = await database; - final maps = await db.query( - 'products', - where: 'reference = ?', - whereArgs: [reference], - ); - if (maps.isNotEmpty) { - return Product.fromMap(maps.first); - } - return null; + return await db.transaction((txn) async { + // Créer le client + final clientId = await txn.insert('clients', client.toMap()); + + // Créer la commande + final commandeMap = commande.toMap(); + commandeMap['clientId'] = clientId; + final commandeId = await txn.insert('commandes', commandeMap); + + // Créer les détails et mettre à jour le stock + for (var detail in details) { + final detailMap = detail.toMap(); + detailMap['commandeId'] = commandeId; // Ajoute l'ID de la commande + await txn.insert('details_commandes', detailMap); + + // Mettre à jour le stock du produit + await txn.rawUpdate( + 'UPDATE products SET stock = stock - ? WHERE id = ?', + [detail.quantite, detail.produitId], + ); + } + + return commandeId; + }); } + + // ========================= + // STATISTIQUES + // ========================= + Future> getStatistiques() async { + final db = await database; + + final totalClients = await db.rawQuery('SELECT COUNT(*) as count FROM clients WHERE actif = 1'); + final totalCommandes = await db.rawQuery('SELECT COUNT(*) as count FROM commandes'); + final totalProduits = await db.rawQuery('SELECT COUNT(*) as count FROM products'); + final chiffreAffaires = await db.rawQuery('SELECT SUM(montantTotal) as total FROM commandes WHERE statut != 5'); // 5 = annulée + + return { + 'totalClients': totalClients.first['count'], + 'totalCommandes': totalCommandes.first['count'], + 'totalProduits': totalProduits.first['count'], + 'chiffreAffaires': chiffreAffaires.first['total'] ?? 0.0, + }; + } + + // ========================= + // DONNÉES PAR DÉFAUT + // ========================= + Future _insertDefaultClients() async { + final db = await database; + final existingClients = await db.query('clients'); + + if (existingClients.isEmpty) { + final defaultClients = [ + Client( + nom: 'Dupont', + prenom: 'Jean', + email: 'jean.dupont@email.com', + telephone: '0123456789', + adresse: '123 Rue de la Paix, Paris', + dateCreation: DateTime.now(), + ), + Client( + nom: 'Martin', + prenom: 'Marie', + email: 'marie.martin@email.com', + telephone: '0987654321', + adresse: '456 Avenue des Champs, Lyon', + dateCreation: DateTime.now(), + ), + Client( + nom: 'Bernard', + prenom: 'Pierre', + email: 'pierre.bernard@email.com', + telephone: '0456789123', + adresse: '789 Boulevard Saint-Michel, Marseille', + dateCreation: DateTime.now(), + ), + ]; + + for (var client in defaultClients) { + await db.insert('clients', client.toMap()); + } + print("Clients par défaut insérés"); + } + } + + Future _insertDefaultCommandes() async { + final db = await database; + final existingCommandes = await db.query('commandes'); + + if (existingCommandes.isEmpty) { + // Récupérer quelques produits pour créer des commandes + final produits = await db.query('products', limit: 3); + final clients = await db.query('clients', limit: 3); + + if (produits.isNotEmpty && clients.isNotEmpty) { + // Commande 1 + final commande1Id = await db.insert('commandes', { + 'clientId': clients[0]['id'], + 'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(), + 'statut': StatutCommande.livree.index, + 'montantTotal': 150.0, + 'notes': 'Commande urgente', + }); + + await db.insert('details_commandes', { + 'commandeId': commande1Id, + 'produitId': produits[0]['id'], + 'quantite': 2, + 'prixUnitaire': 75.0, + 'sousTotal': 150.0, + }); + + // Commande 2 + final commande2Id = await db.insert('commandes', { + 'clientId': clients[1]['id'], + 'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(), + 'statut': StatutCommande.enPreparation.index, + 'montantTotal': 225.0, + 'notes': 'Livraison prévue demain', + }); + + if (produits.length > 1) { + await db.insert('details_commandes', { + 'commandeId': commande2Id, + 'produitId': produits[1]['id'], + 'quantite': 3, + 'prixUnitaire': 75.0, + 'sousTotal': 225.0, + }); + } + + // Commande 3 + final commande3Id = await db.insert('commandes', { + 'clientId': clients[2]['id'], + 'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(), + 'statut': StatutCommande.confirmee.index, + 'montantTotal': 300.0, + 'notes': 'Commande standard', + }); + + if (produits.length > 2) { + await db.insert('details_commandes', { + 'commandeId': commande3Id, + 'produitId': produits[2]['id'], + 'quantite': 4, + 'prixUnitaire': 75.0, + 'sousTotal': 300.0, + }); + } + + print("Commandes par défaut insérées"); + } + } + } + + Future close() async { + if (_database.isOpen) { + await _database.close(); + } + } + // Ajoutez cette méthode temporaire pour supprimer la DB corrompue +Future deleteDatabaseFile() async { + final documentsDirectory = await getApplicationDocumentsDirectory(); + final path = join(documentsDirectory.path, 'products2.db'); + final file = File(path); + if (await file.exists()) { + await file.delete(); + print("Base de données product supprimée"); + } } +} \ No newline at end of file diff --git a/lib/Views/addProduct.dart b/lib/Views/addProduct.dart deleted file mode 100644 index dcdaf3a..0000000 --- a/lib/Views/addProduct.dart +++ /dev/null @@ -1,812 +0,0 @@ -import 'dart:io'; -import 'dart:typed_data'; -import 'dart:ui'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:excel/excel.dart' hide Border; -import 'package:flutter/services.dart'; - -import '../Components/appDrawer.dart'; -import '../Components/app_bar.dart'; -import '../Models/produit.dart'; -import '../Services/productDatabase.dart'; - -class AddProductPage extends StatefulWidget { - const AddProductPage({super.key}); - - @override - _AddProductPageState createState() => _AddProductPageState(); -} - -class _AddProductPageState extends State { - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _priceController = TextEditingController(); - final TextEditingController _imageController = TextEditingController(); - final TextEditingController _descriptionController = TextEditingController(); - final TextEditingController _stockController = TextEditingController(); - - final List _categories = ['Sucré', 'Salé', 'Jus', 'Gateaux', 'Non catégorisé']; - String? _selectedCategory; - File? _pickedImage; - String? _qrData; - String? _currentReference; // Ajout pour stocker la référence actuelle - late ProductDatabase _productDatabase; - - // Variables pour la barre de progression - bool _isImporting = false; - double _importProgress = 0.0; - String _importStatusText = ''; - - @override - void initState() { - super.initState(); - _productDatabase = ProductDatabase.instance; - _productDatabase.initDatabase(); - _nameController.addListener(_updateQrData); - } - - @override - void dispose() { - _nameController.removeListener(_updateQrData); - _nameController.dispose(); - _priceController.dispose(); - _imageController.dispose(); - _descriptionController.dispose(); - _stockController.dispose(); - super.dispose(); - } - - // Méthode pour générer une référence unique - String _generateUniqueReference() { - final timestamp = DateTime.now().millisecondsSinceEpoch; - final randomSuffix = DateTime.now().microsecond.toString().padLeft(6, '0'); - return 'PROD_${timestamp}${randomSuffix}'; - } - - void _updateQrData() { - if (_nameController.text.isNotEmpty) { - // Générer une nouvelle référence si elle n'existe pas encore - if (_currentReference == null) { - _currentReference = _generateUniqueReference(); - } - - setState(() { - // Utiliser la référence courante dans l'URL du QR code - _qrData = 'https://stock.guycom.mg/$_currentReference'; - }); - } else { - setState(() { - _currentReference = null; - _qrData = null; - }); - } -} - Future _selectImage() async { - final result = await FilePicker.platform.pickFiles(type: FileType.image); - if (result != null && result.files.single.path != null) { - setState(() { - _pickedImage = File(result.files.single.path!); - _imageController.text = _pickedImage!.path; - }); - } - } - - // Assurez-vous aussi que _generateAndSaveQRCode utilise bien la référence passée : -Future _generateAndSaveQRCode(String reference) async { - final qrUrl = 'https://stock.guycom.mg/$reference'; // Utilise le paramètre reference - - final validation = QrValidator.validate( - data: qrUrl, - version: QrVersions.auto, - errorCorrectionLevel: QrErrorCorrectLevel.L, - ); - - if (validation.status != QrValidationStatus.valid) { - throw Exception('Données QR invalides: ${validation.error}'); - } - - final qrCode = validation.qrCode!; - final painter = QrPainter.withQr( - qr: qrCode, - color: Colors.black, - emptyColor: Colors.white, - gapless: true, - ); - - final directory = await getApplicationDocumentsDirectory(); - final path = '${directory.path}/$reference.png'; // Utilise le paramètre reference - - try { - final picData = await painter.toImageData(2048, format: ImageByteFormat.png); - if (picData != null) { - await File(path).writeAsBytes(picData.buffer.asUint8List()); - } else { - throw Exception('Impossible de générer l\'image QR'); - } - } catch (e) { - throw Exception('Erreur lors de la génération du QR code: $e'); - } - - return path; -} - - void _addProduct() async { - final name = _nameController.text.trim(); - final price = double.tryParse(_priceController.text.trim()) ?? 0.0; - final image = _imageController.text.trim(); - final category = _selectedCategory ?? 'Non catégorisé'; - final description = _descriptionController.text.trim(); - final stock = int.tryParse(_stockController.text.trim()) ?? 0; - - if (name.isEmpty || price <= 0) { - Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); - return; - } - - // Utiliser la référence générée ou en créer une nouvelle - String finalReference = _currentReference ?? _generateUniqueReference(); - - // Vérifier l'unicité de la référence en base - var existingProduct = await _productDatabase.getProductByReference(finalReference); - - // Si la référence existe déjà, en générer une nouvelle - while (existingProduct != null) { - finalReference = _generateUniqueReference(); - existingProduct = await _productDatabase.getProductByReference(finalReference); - } - - // Mettre à jour la référence courante avec la référence finale - _currentReference = finalReference; - - // Générer le QR code avec la référence finale - final qrPath = await _generateAndSaveQRCode(finalReference); - - final product = Product( - name: name, - price: price, - image: image, - category: category, - description: description, - qrCode: qrPath, - reference: finalReference, // Utiliser la référence finale - stock: stock, - ); - - try { - await _productDatabase.createProduct(product); - Get.snackbar('Succès', 'Produit ajouté avec succès\nRéférence: $finalReference'); - - setState(() { - _nameController.clear(); - _priceController.clear(); - _imageController.clear(); - _descriptionController.clear(); - _stockController.clear(); - _selectedCategory = null; - _pickedImage = null; - _qrData = null; - _currentReference = null; // Reset de la référence - }); - } catch (e) { - Get.snackbar('Erreur', 'Ajout du produit échoué : $e'); - print(e); - } -} - // Méthode pour réinitialiser l'état d'importation - void _resetImportState() { - setState(() { - _isImporting = false; - _importProgress = 0.0; - _importStatusText = ''; - }); - } - - Future _importFromExcel() async { - - try { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['xlsx', 'xls','csv'], - allowMultiple: false, - ); - - if (result == null || result.files.isEmpty) { - Get.snackbar('Annulé', 'Aucun fichier sélectionné'); - return; - } - - // Démarrer la progression - setState(() { - _isImporting = true; - _importProgress = 0.0; - _importStatusText = 'Lecture du fichier...'; - }); - - final file = File(result.files.single.path!); - - // Vérifier que le fichier existe - if (!await file.exists()) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); - return; - } - - setState(() { - _importProgress = 0.1; - _importStatusText = 'Vérification du fichier...'; - }); - - final bytes = await file.readAsBytes(); - - // Vérifier que le fichier n'est pas vide - if (bytes.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier Excel est vide'); - return; - } - - setState(() { - _importProgress = 0.2; - _importStatusText = 'Décodage du fichier Excel...'; - }); - - Excel excel; - try { - // Initialisation - setState(() { - _isImporting = true; - _importProgress = 0.0; - _importStatusText = 'Initialisation...'; - }); - - // Petit délai pour permettre au build de s'exécuter - await Future.delayed(Duration(milliseconds: 50)); - excel = Excel.decodeBytes(bytes); - } catch (e) { - _resetImportState(); - debugPrint('Erreur décodage Excel: $e'); - - if (e.toString().contains('styles') || e.toString().contains('Damaged')) { - _showExcelCompatibilityError(); - return; - } else { - Get.snackbar('Erreur', 'Impossible de lire le fichier Excel. Format non supporté.'); - return; - } - } - - if (excel.tables.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille'); - return; - } - - setState(() { - _importProgress = 0.3; - _importStatusText = 'Analyse des données...'; - }); - - int successCount = 0; - int errorCount = 0; - List errorMessages = []; - - // Prendre la première feuille disponible - final sheetName = excel.tables.keys.first; - final sheet = excel.tables[sheetName]!; - - if (sheet.rows.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'La feuille Excel est vide'); - return; - } - - final totalRows = sheet.rows.length - 1; // -1 pour exclure l'en-tête - - setState(() { - _importStatusText = 'Importation en cours... (0/$totalRows)'; - }); - - // Ignorer la première ligne (en-têtes) et traiter les données - for (var i = 1; i < sheet.rows.length; i++) { - try { - // Mettre à jour la progression - final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows); - setState(() { - _importProgress = currentProgress; - _importStatusText = 'Importation en cours... (${i - 1}/$totalRows)'; - }); - - // Petite pause pour permettre à l'UI de se mettre à jour - await Future.delayed(const Duration(milliseconds: 10)); - - final row = sheet.rows[i]; - - // Vérifier que la ligne a au moins les colonnes obligatoires (nom et prix) - if (row.isEmpty || row.length < 2) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Données insuffisantes'); - continue; - } - - // Extraire les valeurs avec vérifications sécurisées - final nameCell = row[0]; - final priceCell = row[1]; - - // Extraction sécurisée des valeurs - String? nameValue; - String? priceValue; - - if (nameCell?.value != null) { - nameValue = nameCell!.value.toString().trim(); - } - - if (priceCell?.value != null) { - priceValue = priceCell!.value.toString().trim(); - } - - if (nameValue == null || nameValue.isEmpty) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Nom du produit manquant'); - continue; - } - - if (priceValue == null || priceValue.isEmpty) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Prix manquant'); - continue; - } - - final name = nameValue; - // Remplacer les virgules par des points pour les décimaux - final price = double.tryParse(priceValue.replaceAll(',', '.')); - - if (price == null || price <= 0) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Prix invalide ($priceValue)'); - continue; - } - - // Extraire les autres colonnes optionnelles de manière sécurisée - String category = 'Non catégorisé'; - if (row.length > 2 && row[2]?.value != null) { - final categoryValue = row[2]!.value.toString().trim(); - if (categoryValue.isNotEmpty) { - category = categoryValue; - } - } - - String description = ''; - if (row.length > 3 && row[3]?.value != null) { - description = row[3]!.value.toString().trim(); - } - - int stock = 0; - if (row.length > 4 && row[4]?.value != null) { - final stockStr = row[4]!.value.toString().trim(); - stock = int.tryParse(stockStr) ?? 0; - } - - // Générer une référence unique et vérifier son unicité - String reference = _generateUniqueReference(); - - // Vérifier l'unicité en base de données - var existingProduct = await _productDatabase.getProductByReference(reference); - while (existingProduct != null) { - reference = _generateUniqueReference(); - existingProduct = await _productDatabase.getProductByReference(reference); - } - - // Créer le produit - final product = Product( - name: name, - price: price, - image: '', // Pas d'image lors de l'import - category: category, - description: description, - stock: stock, - qrCode: '', // Sera généré après - reference: reference, - ); - - // Générer et sauvegarder le QR code avec la nouvelle URL - setState(() { - _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; - }); - - final qrPath = await _generateAndSaveQRCode(reference); - product.qrCode = qrPath; - - // Sauvegarder en base de données - await _productDatabase.createProduct(product); - successCount++; - - } catch (e) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Erreur de traitement - $e'); - debugPrint('Erreur ligne ${i + 1}: $e'); - } - } - - // Finalisation - setState(() { - _importProgress = 1.0; - _importStatusText = 'Finalisation...'; - }); - - await Future.delayed(const Duration(milliseconds: 500)); - - // Réinitialiser l'état d'importation - _resetImportState(); - - // Afficher le résultat - String message = '$successCount produits importés avec succès'; - if (errorCount > 0) { - message += ', $errorCount erreurs'; - - // Afficher les détails des erreurs si pas trop nombreuses - if (errorMessages.length <= 5) { - message += ':\n${errorMessages.join('\n')}'; - } - } - - Get.snackbar( - 'Importation terminée', - message, - duration: const Duration(seconds: 6), - colorText: Colors.white, - backgroundColor: successCount > 0 ? Colors.green : Colors.orange, - ); - - } catch (e) { - _resetImportState(); - Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); - debugPrint('Erreur générale import Excel: $e'); - } - } - - void _showExcelCompatibilityError() { - Get.dialog( - AlertDialog( - title: const Text('Fichier Excel incompatible'), - content: const Text( - 'Ce fichier Excel contient des éléments qui ne sont pas compatibles avec notre système d\'importation.\n\n' - 'Solutions recommandées :\n' - '• Téléchargez notre modèle Excel et copiez-y vos données\n' - '• Ou exportez votre fichier en format simple: Classeur Excel .xlsx depuis Excel\n' - '• Ou créez un nouveau fichier Excel simple sans formatage complexe' - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - TextButton( - onPressed: () { - Get.back(); - _downloadExcelTemplate(); - }, - child: const Text('Télécharger modèle'), - style: TextButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - ), - ], - ), - ); - } - - Future _downloadExcelTemplate() async { - try { - // Créer un fichier Excel temporaire comme modèle - final excel = Excel.createExcel(); - - // Supprimer la feuille par défaut et créer une nouvelle - excel.delete('Sheet1'); - excel.copy('Sheet1', 'Produits'); - excel.delete('Sheet1'); - - final sheet = excel['Produits']; - - // Ajouter les en-têtes avec du style - final headers = ['Nom', 'Prix', 'Catégorie', 'Description', 'Stock']; - for (int i = 0; i < headers.length; i++) { - final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0)); - cell.value = headers[i]; - cell.cellStyle = CellStyle( - bold: true, - backgroundColorHex: '#E8F4FD', - ); - } - - // Ajouter des exemples - final examples = [ - ['Croissant', '1.50', 'Sucré', 'Délicieux croissant beurré', '20'], - ['Sandwich jambon', '4.00', 'Salé', 'Sandwich fait maison', '15'], - ['Jus d\'orange', '2.50', 'Jus', 'Jus d\'orange frais', '30'], - ['Gâteau chocolat', '18.00', 'Gateaux', 'Gâteau au chocolat portion 8 personnes', '5'], - ]; - - for (int row = 0; row < examples.length; row++) { - for (int col = 0; col < examples[row].length; col++) { - final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row + 1)); - cell.value = examples[row][col]; - } - } - - // Ajuster la largeur des colonnes - sheet.setColWidth(0, 20); // Nom - sheet.setColWidth(1, 10); // Prix - sheet.setColWidth(2, 15); // Catégorie - sheet.setColWidth(3, 30); // Description - sheet.setColWidth(4, 10); // Stock - - // Sauvegarder en mémoire - final bytes = excel.save(); - - if (bytes == null) { - Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); - return; - } - - // Demander où sauvegarder - final String? outputFile = await FilePicker.platform.saveFile( - fileName: 'modele_import_produits.xlsx', - allowedExtensions: ['xlsx'], - type: FileType.custom, - ); - - if (outputFile != null) { - try { - await File(outputFile).writeAsBytes(bytes); - Get.snackbar( - 'Succès', - 'Modèle téléchargé avec succès\n$outputFile', - duration: const Duration(seconds: 4), - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } catch (e) { - Get.snackbar('Erreur', 'Impossible d\'écrire le fichier: $e'); - } - } - } catch (e) { - Get.snackbar('Erreur', 'Erreur lors de la création du modèle: $e'); - debugPrint('Erreur création modèle Excel: $e'); - } - } - - Widget _displayImage() { - if (_pickedImage != null) { - return ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: Image.file( - _pickedImage!, - width: 100, - height: 100, - fit: BoxFit.cover, - ), - ); - } else { - return Container( - width: 100, - height: 100, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon(Icons.image, size: 32, color: Colors.grey), - Text('Aucune image', style: TextStyle(color: Colors.grey)), - ], - ), - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(8.0), - - )); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: const CustomAppBar(title: 'Ajouter un produit'), - drawer: CustomDrawer(), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Ajouter un produit', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), - const SizedBox(height: 16), - - // Boutons d'importation - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: _isImporting ? null : _importFromExcel, - icon: const Icon(Icons.upload), - label: const Text('Importer depuis Excel'), - ), - ), - const SizedBox(width: 10), - TextButton( - onPressed: _isImporting ? null : _downloadExcelTemplate, - child: const Text('Modèle'), - ), - ], - ), - const SizedBox(height: 16), - - // Barre de progression - if (_isImporting) ...[ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Importation en cours...', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), - ), - const SizedBox(height: 8), - LinearProgressIndicator( - value: _importProgress, - backgroundColor: Colors.blue.shade100, - valueColor: AlwaysStoppedAnimation(Colors.blue.shade600), - ), - const SizedBox(height: 8), - Text( - _importStatusText, - style: TextStyle( - fontSize: 14, - color: Colors.blue.shade700, - ), - ), - const SizedBox(height: 8), - Text( - '${(_importProgress * 100).round()}%', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.blue.shade600, - ), - ), - ], - ), - ), - const SizedBox(height: 16), - ], - - const Divider(), - const SizedBox(height: 16), - - // Formulaire d'ajout manuel - TextField( - controller: _nameController, - enabled: !_isImporting, - decoration: const InputDecoration( - labelText: 'Nom du produit*', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - - TextField( - controller: _priceController, - enabled: !_isImporting, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: const InputDecoration( - labelText: 'Prix*', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - - TextField( - controller: _stockController, - enabled: !_isImporting, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'Stock', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - - // Section image (optionnelle) - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: TextField( - controller: _imageController, - enabled: !_isImporting, - decoration: const InputDecoration( - labelText: 'Chemin de l\'image (optionnel)', - border: OutlineInputBorder(), - ), - readOnly: true, - ), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _isImporting ? null : _selectImage, - child: const Text('Sélectionner'), - ), - ], - ), - const SizedBox(height: 16), - _displayImage(), - const SizedBox(height: 16), - - DropdownButtonFormField( - value: _selectedCategory, - items: _categories - .map((c) => DropdownMenuItem(value: c, child: Text(c))) - .toList(), - onChanged: _isImporting ? null : (value) => setState(() => _selectedCategory = value), - decoration: const InputDecoration( - labelText: 'Catégorie', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - - TextField( - controller: _descriptionController, - enabled: !_isImporting, - maxLines: 3, - decoration: const InputDecoration( - labelText: 'Description (optionnel)', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - - if (_qrData != null) ...[ - const Text('Aperçu du QR Code :'), - const SizedBox(height: 8), - Center( - child: QrImageView( - data: _qrData!, - version: QrVersions.auto, - size: 120, - ), - ), - const SizedBox(height: 8), - Center( - child: Text( - _qrData!, - style: const TextStyle(fontSize: 12, color: Colors.grey), - textAlign: TextAlign.center, - ), - ), - ], - const SizedBox(height: 24), - - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _isImporting ? null : _addProduct, - child: const Text('Ajouter le produit'), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/Views/commandManagement.dart b/lib/Views/commandManagement.dart new file mode 100644 index 0000000..bb7934e --- /dev/null +++ b/lib/Views/commandManagement.dart @@ -0,0 +1,1329 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:path_provider/path_provider.dart'; +import 'package:open_file/open_file.dart'; +import 'package:youmazgestion/Components/app_bar.dart'; +import 'package:youmazgestion/Components/appDrawer.dart'; +import 'package:youmazgestion/Models/client.dart'; +import 'package:youmazgestion/Services/productDatabase.dart'; + +class GestionCommandesPage extends StatefulWidget { + const GestionCommandesPage({super.key}); + + @override + _GestionCommandesPageState createState() => _GestionCommandesPageState(); +} + +class _GestionCommandesPageState extends State { + final ProductDatabase _database = ProductDatabase.instance; + List _commandes = []; + List _filteredCommandes = []; + StatutCommande? _selectedStatut; + DateTime? _selectedDate; + final TextEditingController _searchController = TextEditingController(); + bool _showCancelledOrders = false; // Nouveau: contrôle l'affichage des commandes annulées + + @override + void initState() { + super.initState(); + _loadCommandes(); + _searchController.addListener(_filterCommandes); + } + + Future _loadCommandes() async { + final commandes = await _database.getCommandes(); + setState(() { + _commandes = commandes; + _filterCommandes(); + }); + } + Future loadImage() async { + final data = await rootBundle.load('assets/youmaz2.png'); + return data.buffer.asUint8List(); +} + + void _filterCommandes() { + final query = _searchController.text.toLowerCase(); + setState(() { + _filteredCommandes = _commandes.where((commande) { + final matchesSearch = commande.clientNomComplet.toLowerCase().contains(query) || + commande.id.toString().contains(query); + final matchesStatut = _selectedStatut == null || commande.statut == _selectedStatut; + final matchesDate = _selectedDate == null || + DateFormat('yyyy-MM-dd').format(commande.dateCommande) == + DateFormat('yyyy-MM-dd').format(_selectedDate!); + + // Nouveau: filtrer les commandes annulées selon le toggle + final shouldShowCancelled = _showCancelledOrders || commande.statut != StatutCommande.annulee; + + return matchesSearch && matchesStatut && matchesDate && shouldShowCancelled; + }).toList(); + }); + } + + Future _updateStatut(int commandeId, StatutCommande newStatut) async { + await _database.updateStatutCommande(commandeId, newStatut); + await _loadCommandes(); + + // Amélioration: message plus spécifique selon le statut + String message = 'Statut de la commande mis à jour'; + Color backgroundColor = Colors.green; + + switch (newStatut) { + case StatutCommande.annulee: + message = 'Commande annulée avec succès'; + backgroundColor = Colors.orange; + break; + case StatutCommande.livree: + message = 'Commande marquée comme livrée'; + backgroundColor = Colors.green; + break; + case StatutCommande.confirmee: + message = 'Commande confirmée'; + backgroundColor = Colors.blue; + break; + default: + break; + } + + Get.snackbar( + 'Succès', + message, + snackPosition: SnackPosition.BOTTOM, + backgroundColor: backgroundColor, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } + + Future _generateInvoice(Commande commande) async { + final details = await _database.getDetailsCommande(commande.id!); + final client = await _database.getClientById(commande.clientId); + + final pdf = pw.Document(); + final imageBytes = await loadImage(); // Charge les données de l'image + + final image = pw.MemoryImage(imageBytes); + // Amélioration: styles plus professionnels + final headerStyle = pw.TextStyle( + fontSize: 18, + fontWeight: pw.FontWeight.bold, + color: PdfColors.blue900, + ); + + final titleStyle = pw.TextStyle( + fontSize: 14, + fontWeight: pw.FontWeight.bold, + ); + + final subtitleStyle = pw.TextStyle( + fontSize: 12, + color: PdfColors.grey600, + ); + + // Contenu du PDF amélioré + pdf.addPage( + pw.Page( + margin: const pw.EdgeInsets.all(20), + build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // En-tête avec logo (si disponible) + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // Placeholder pour le logo - à remplacer par votre logo + pw.Container( + width: 100, + height: 80, + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.blue900, width: 2), + borderRadius: pw.BorderRadius.circular(8), + ), + child: pw.Center( + child: pw.Image(image) + ), + ), + pw.SizedBox(height: 10), + pw.Text('guycom', style: headerStyle), + pw.Text('123 Rue des Entreprises', style: subtitleStyle), + pw.Text('Antananarivo, Madagascar', style: subtitleStyle), + pw.Text('Tél: +213 123 456 789', style: subtitleStyle), + pw.Text('Site: guycom.mg', style: subtitleStyle), + ], + ), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + pw.Container( + padding: const pw.EdgeInsets.all(12), + decoration: pw.BoxDecoration( + color: PdfColors.blue50, + borderRadius: pw.BorderRadius.circular(8), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('FACTURE', + style: pw.TextStyle( + fontSize: 20, + fontWeight: pw.FontWeight.bold, + color: PdfColors.blue900, + ), + ), + pw.SizedBox(height: 8), + pw.Text('N°: ${commande.id}', style: titleStyle), + pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(commande.dateCommande)}'), + ], + ), + ), + ], + ), + ], + ), + + pw.SizedBox(height: 30), + + // Informations client + pw.Container( + width: double.infinity, + padding: const pw.EdgeInsets.all(12), + decoration: pw.BoxDecoration( + color: PdfColors.grey100, + borderRadius: pw.BorderRadius.circular(8), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('FACTURÉ À:', style: titleStyle), + pw.SizedBox(height: 5), + pw.Text(client?.nomComplet ?? 'Client inconnu', + style: pw.TextStyle(fontSize: 12)), + if (client?.telephone != null) + pw.Text('Tél: ${client!.telephone}', + style: pw.TextStyle(fontSize: 10, color: PdfColors.grey600)), + ], + ), + ), + + pw.SizedBox(height: 30), + + // Tableau des produits + pw.Text('DÉTAILS DE LA COMMANDE', style: titleStyle), + pw.SizedBox(height: 10), + + pw.Table( + border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5), + children: [ + pw.TableRow( + decoration: const pw.BoxDecoration(color: PdfColors.blue900), + children: [ + _buildTableCell('Produit', titleStyle.copyWith(color: PdfColors.white)), + _buildTableCell('Qté', titleStyle.copyWith(color: PdfColors.white)), + _buildTableCell('Prix unit.', titleStyle.copyWith(color: PdfColors.white)), + _buildTableCell('Total', titleStyle.copyWith(color: PdfColors.white)), + ], + ), + ...details.asMap().entries.map((entry) { + final index = entry.key; + final detail = entry.value; + final isEven = index % 2 == 0; + + return pw.TableRow( + decoration: pw.BoxDecoration( + color: isEven ? PdfColors.white : PdfColors.grey50, + ), + children: [ + _buildTableCell(detail.produitNom ?? 'Produit inconnu'), + _buildTableCell(detail.quantite.toString()), + _buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} DA'), + _buildTableCell('${detail.sousTotal.toStringAsFixed(2)} DA'), + ], + ); + }), + ], + ), + + pw.SizedBox(height: 20), + + // Total + pw.Container( + alignment: pw.Alignment.centerRight, + child: pw.Container( + padding: const pw.EdgeInsets.all(12), + decoration: pw.BoxDecoration( + color: PdfColors.blue900, + borderRadius: pw.BorderRadius.circular(8), + ), + child: pw.Text( + 'TOTAL: ${commande.montantTotal.toStringAsFixed(2)} DA', + style: pw.TextStyle( + fontSize: 16, + fontWeight: pw.FontWeight.bold, + color: PdfColors.white, + ), + ), + ), + ), + + pw.Spacer(), + + // Pied de page + pw.Container( + width: double.infinity, + padding: const pw.EdgeInsets.all(12), + decoration: pw.BoxDecoration( + border: pw.Border( + top: pw.BorderSide(color: PdfColors.grey400, width: 1), + ), + ), + child: pw.Column( + children: [ + pw.Text( + 'Merci pour votre confiance!', + style: pw.TextStyle( + fontSize: 14, + fontStyle: pw.FontStyle.italic, + color: PdfColors.blue900, + ), + ), + pw.SizedBox(height: 5), + pw.Text( + 'Cette facture est générée automatiquement par le système Youmaz Gestion', + style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600), + ), + ], + ), + ), + ], + ); + }, + ), + ); + + // Sauvegarder le PDF + final output = await getTemporaryDirectory(); + final file = File('${output.path}/facture_${commande.id}.pdf'); + await file.writeAsBytes(await pdf.save()); + + // Ouvrir le PDF + await OpenFile.open(file.path); + } + + pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) { + return pw.Padding( + padding: const pw.EdgeInsets.all(8.0), + child: pw.Text(text, style: style ?? pw.TextStyle(fontSize: 10)), + ); + } + + Color _getStatutColor(StatutCommande statut) { + switch (statut) { + case StatutCommande.enAttente: + return Colors.orange.shade100; + case StatutCommande.confirmee: + return Colors.blue.shade100; + case StatutCommande.enPreparation: + return Colors.amber.shade100; + case StatutCommande.expediee: + return Colors.purple.shade100; + case StatutCommande.livree: + return Colors.green.shade100; + case StatutCommande.annulee: + return Colors.red.shade100; + } + } + + IconData _getStatutIcon(StatutCommande statut) { + switch (statut) { + case StatutCommande.enAttente: + return Icons.schedule; + case StatutCommande.confirmee: + return Icons.check_circle_outline; + case StatutCommande.enPreparation: + return Icons.settings; + case StatutCommande.expediee: + return Icons.local_shipping; + case StatutCommande.livree: + return Icons.check_circle; + case StatutCommande.annulee: + return Icons.cancel; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const CustomAppBar(title: 'Gestion des Commandes'), + drawer: CustomDrawer(), + body: Column( + children: [ + // Header avec logo et statistiques + Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade50, Colors.white], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Column( + children: [ + // Logo et titre + Row( + children: [ + // Logo de l'entreprise + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + 'assets/logo.png', // Remplacez par le chemin de votre logo + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + decoration: BoxDecoration( + color: Colors.blue.shade900, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.business, + color: Colors.white, + size: 30, + ), + ); + }, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Gestion des Commandes', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + '${_filteredCommandes.length} commande(s) affichée(s)', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Barre de recherche améliorée + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + labelText: 'Rechercher par client ou numéro de commande', + prefixIcon: Icon(Icons.search, color: Colors.blue.shade800), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + ), + + const SizedBox(height: 16), + + // Filtres améliorés + Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: DropdownButtonFormField( + value: _selectedStatut, + decoration: InputDecoration( + labelText: 'Filtrer par statut', + prefixIcon: Icon(Icons.filter_list, color: Colors.blue.shade600), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('Tous les statuts'), + ), + ...StatutCommande.values.map((statut) { + return DropdownMenuItem( + value: statut, + child: Row( + children: [ + Icon(_getStatutIcon(statut), size: 16), + const SizedBox(width: 8), + Text(statutLibelle(statut)), + ], + ), + ); + }), + ], + onChanged: (value) { + setState(() { + _selectedStatut = value; + _filterCommandes(); + }); + }, + ), + ), + ), + + const SizedBox(width: 12), + + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () async { + final date = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Colors.blue.shade900, + ), + ), + child: child!, + ); + }, + ); + if (date != null) { + setState(() { + _selectedDate = date; + _filterCommandes(); + }); + } + }, + icon: Icon(Icons.calendar_today, color: Colors.blue.shade600), + label: Text( + _selectedDate == null + ? 'Date' + : DateFormat('dd/MM/yyyy').format(_selectedDate!), + style: const TextStyle(color: Colors.black87), + ), + ), + ), + ), + + const SizedBox(width: 12), + + // Bouton reset + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: IconButton( + icon: Icon(Icons.refresh, color: Colors.blue.shade600), + onPressed: () { + setState(() { + _selectedStatut = null; + _selectedDate = null; + _searchController.clear(); + _filterCommandes(); + }); + }, + tooltip: 'Réinitialiser les filtres', + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Toggle pour afficher/masquer les commandes annulées + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Icon( + Icons.visibility, + size: 20, + color: Colors.grey.shade600, + ), + const SizedBox(width: 8), + Text( + 'Afficher commandes annulées', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + const Spacer(), + Switch( + value: _showCancelledOrders, + onChanged: (value) { + setState(() { + _showCancelledOrders = value; + _filterCommandes(); + }); + }, + activeColor: Colors.blue.shade600, + ), + ], + ), + ), + ], + ), + ), + + // Liste des commandes + Expanded( + child: _filteredCommandes.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox, + size: 64, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Aucune commande trouvée', + style: TextStyle( + fontSize: 18, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + 'Essayez de modifier vos filtres', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _filteredCommandes.length, + itemBuilder: (context, index) { + final commande = _filteredCommandes[index]; + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: _getStatutColor(commande.statut), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + leading: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _getStatutIcon(commande.statut), + size: 20, + color: commande.statut == StatutCommande.annulee + ? Colors.red + : Colors.blue.shade600, + ), + Text( + '#${commande.id}', + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + title: Text( + commande.clientNomComplet, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + DateFormat('dd/MM/yyyy').format(commande.dateCommande), + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(width: 16), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + commande.statutLibelle, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: commande.statut == StatutCommande.annulee + ? Colors.red + : Colors.blue.shade700, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.attach_money, + size: 14, + color: Colors.green.shade600, + ), + const SizedBox(width: 4), + Text( + '${commande.montantTotal.toStringAsFixed(2)} DA', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + ], + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: IconButton( + icon: Icon( + Icons.receipt_long, + color: Colors.blue.shade600, + ), + onPressed: () => _generateInvoice(commande), + tooltip: 'Générer la facture', + ), + ), + ], + ), + children: [ + Container( + padding: const EdgeInsets.all(16.0), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Column( + children: [ + _CommandeDetails(commande: commande), + const SizedBox(height: 16), + if (commande.statut != StatutCommande.annulee) + _CommandeActions( + commande: commande, + onStatutChanged: _updateStatut, + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } + + String statutLibelle(StatutCommande statut) { + switch (statut) { + case StatutCommande.enAttente: + return 'En attente'; + case StatutCommande.confirmee: + return 'Confirmée'; + case StatutCommande.enPreparation: + return 'En préparation'; + case StatutCommande.expediee: + return 'Expédiée'; + case StatutCommande.livree: + return 'Livrée'; + case StatutCommande.annulee: + return 'Annulée'; + } + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } +} + +class _CommandeDetails extends StatelessWidget { + final Commande commande; + + const _CommandeDetails({required this.commande}); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: ProductDatabase.instance.getDetailsCommande(commande.id!), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Text('Aucun détail disponible'); + } + + final details = snapshot.data!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + 'Détails de la commande', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.black87, + ), + ), + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: Table( + children: [ + TableRow( + decoration: BoxDecoration( + color: Colors.grey.shade100, + ), + children: [ + _buildTableHeader('Produit'), + _buildTableHeader('Qté'), + _buildTableHeader('Prix unit.'), + _buildTableHeader('Total'), + ], + ), + ...details.map((detail) => TableRow( + children: [ + _buildTableCell(detail.produitNom ?? 'Produit inconnu'), + _buildTableCell('${detail.quantite}'), + _buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} DA'), + _buildTableCell('${detail.sousTotal.toStringAsFixed(2)} DA'), + ], + )), + ], + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total de la commande:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + '${commande.montantTotal.toStringAsFixed(2)} DA', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.green.shade700, + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + + Widget _buildTableHeader(String text) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + text, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ); + } + + Widget _buildTableCell(String text) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + text, + style: const TextStyle(fontSize: 13), + textAlign: TextAlign.center, + ), + ); + } +} + +class _CommandeActions extends StatelessWidget { + final Commande commande; + final Function(int, StatutCommande) onStatutChanged; + + const _CommandeActions({ + required this.commande, + required this.onStatutChanged, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Actions sur la commande', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: _buildActionButtons(context), + ), + ], + ), + ); + } + + List _buildActionButtons(BuildContext context) { + List buttons = []; + + switch (commande.statut) { + case StatutCommande.enAttente: + buttons.addAll([ + _buildActionButton( + label: 'Confirmer', + icon: Icons.check_circle, + color: Colors.blue, + onPressed: () => _showConfirmDialog( + context, + 'Confirmer la commande', + 'Êtes-vous sûr de vouloir confirmer cette commande?', + () => onStatutChanged(commande.id!, StatutCommande.confirmee), + ), + ), + _buildActionButton( + label: 'Annuler', + icon: Icons.cancel, + color: Colors.red, + onPressed: () => _showConfirmDialog( + context, + 'Annuler la commande', + 'Êtes-vous sûr de vouloir annuler cette commande?', + () => onStatutChanged(commande.id!, StatutCommande.annulee), + ), + ), + ]); + break; + + case StatutCommande.confirmee: + buttons.addAll([ + _buildActionButton( + label: 'Préparer', + icon: Icons.settings, + color: Colors.amber, + onPressed: () => _showConfirmDialog( + context, + 'Marquer en préparation', + 'La commande va être marquée comme étant en cours de préparation.', + () => onStatutChanged(commande.id!, StatutCommande.enPreparation), + ), + ), + _buildActionButton( + label: 'Annuler', + icon: Icons.cancel, + color: Colors.red, + onPressed: () => _showConfirmDialog( + context, + 'Annuler la commande', + 'Êtes-vous sûr de vouloir annuler cette commande?', + () => onStatutChanged(commande.id!, StatutCommande.annulee), + ), + ), + ]); + break; + + case StatutCommande.enPreparation: + buttons.addAll([ + _buildActionButton( + label: 'Expédier', + icon: Icons.local_shipping, + color: Colors.purple, + onPressed: () => _showConfirmDialog( + context, + 'Expédier la commande', + 'La commande va être marquée comme expédiée.', + () => onStatutChanged(commande.id!, StatutCommande.expediee), + ), + ), + _buildActionButton( + label: 'Annuler', + icon: Icons.cancel, + color: Colors.red, + onPressed: () => _showConfirmDialog( + context, + 'Annuler la commande', + 'Êtes-vous sûr de vouloir annuler cette commande?', + () => onStatutChanged(commande.id!, StatutCommande.annulee), + ), + ), + ]); + break; + + case StatutCommande.expediee: + buttons.add( + _buildActionButton( + label: 'Marquer livrée', + icon: Icons.check_circle, + color: Colors.green, + onPressed: () => _showConfirmDialog( + context, + 'Marquer comme livrée', + 'La commande va être marquée comme livrée.', + () => onStatutChanged(commande.id!, StatutCommande.livree), + ), + ), + ); + break; + + case StatutCommande.livree: + buttons.add( + Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade300), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.check_circle, color: Colors.green.shade600, size: 16), + const SizedBox(width: 8), + Text( + 'Commande livrée', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + break; + + case StatutCommande.annulee: + buttons.add( + Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + decoration: BoxDecoration( + color: Colors.red.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.shade300), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.cancel, color: Colors.red.shade600, size: 16), + const SizedBox(width: 8), + Text( + 'Commande annulée', + style: TextStyle( + color: Colors.red.shade700, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + break; + } + + return buttons; + } + + Widget _buildActionButton({ + required String label, + required IconData icon, + required Color color, + required VoidCallback onPressed, + }) { + return ElevatedButton.icon( + onPressed: onPressed, + icon: Icon(icon, size: 16), + label: Text(label), + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 2, + ), + ); + } + + void _showConfirmDialog( + BuildContext context, + String title, + String content, + VoidCallback onConfirm, + ) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + title: Row( + children: [ + Icon( + Icons.help_outline, + color: Colors.blue.shade600, + ), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle(fontSize: 18), + ), + ], + ), + content: Text(content), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'Annuler', + style: TextStyle(color: Colors.grey.shade600), + ), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + onConfirm(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('Confirmer'), + ), + ], + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/Views/newCommand.dart b/lib/Views/newCommand.dart new file mode 100644 index 0000000..725a4fe --- /dev/null +++ b/lib/Views/newCommand.dart @@ -0,0 +1,623 @@ +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/Models/client.dart'; +import 'package:youmazgestion/Models/produit.dart'; +import 'package:youmazgestion/Services/productDatabase.dart'; + +class NouvelleCommandePage extends StatefulWidget { + const NouvelleCommandePage({super.key}); + + @override + _NouvelleCommandePageState createState() => _NouvelleCommandePageState(); +} + +class _NouvelleCommandePageState extends State { + final ProductDatabase _database = ProductDatabase.instance; + final _formKey = GlobalKey(); + + // Informations client + final TextEditingController _nomController = TextEditingController(); + final TextEditingController _prenomController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _telephoneController = TextEditingController(); + final TextEditingController _adresseController = TextEditingController(); + + // Panier + final List _products = []; + final Map _quantites = {}; // productId -> quantity + + @override + void initState() { + super.initState(); + _loadProducts(); + } + + Future _loadProducts() async { + final products = await _database.getProducts(); + setState(() { + _products.addAll(products); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const CustomAppBar(title: 'Nouvelle Commande'), + drawer: CustomDrawer(), + body: Column( + children: [ + // Header avec logo et titre + Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade50, Colors.white], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Column( + children: [ + // Logo et titre + Row( + children: [ + // Logo de l'entreprise + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + 'assets/logo.png', + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + decoration: BoxDecoration( + color: Colors.blue.shade800, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.shopping_cart, + color: Colors.white, + size: 30, + ), + ); + }, + ), + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Nouvelle Commande', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + 'Créez une nouvelle commande pour un client', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + + // Contenu principal + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildClientForm(), + const SizedBox(height: 20), + _buildProductList(), + const SizedBox(height: 20), + _buildCartSection(), + const SizedBox(height: 20), + _buildTotalSection(), + const SizedBox(height: 20), + _buildSubmitButton(), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildClientForm() { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Informations Client', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + TextFormField( + controller: _nomController, + decoration: InputDecoration( + labelText: 'Nom', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer un nom'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _prenomController, + decoration: InputDecoration( + labelText: 'Prénom', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer un prénom'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _emailController, + decoration: InputDecoration( + labelText: 'Email', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white, + ), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer un email'; + } + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + return 'Veuillez entrer un email valide'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _telephoneController, + decoration: InputDecoration( + labelText: 'Téléphone', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white, + ), + keyboardType: TextInputType.phone, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer un numéro de téléphone'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _adresseController, + decoration: InputDecoration( + labelText: 'Adresse de livraison', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white, + ), + maxLines: 2, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer une adresse'; + } + return null; + }, + ), + ], + ), + ), + ), + ); + } + + Widget _buildProductList() { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Produits Disponibles', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + _products.isEmpty + ? const Center(child: CircularProgressIndicator()) + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _products.length, + itemBuilder: (context, index) { + final product = _products[index]; + final quantity = _quantites[product.id] ?? 0; + + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + leading: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.shopping_bag, + color: Colors.blue), + ), + title: Text( + product.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + '${product.price.toStringAsFixed(2)} DA', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.w600, + ), + ), + if (product.stock != null) + Text( + 'Stock: ${product.stock}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + trailing: Container( + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove, size: 18), + onPressed: () { + if (quantity > 0) { + setState(() { + _quantites[product.id!] = quantity - 1; + }); + } + }, + ), + Text( + quantity.toString(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + icon: const Icon(Icons.add, size: 18), + onPressed: () { + if (product.stock == null || quantity < product.stock!) { + setState(() { + _quantites[product.id!] = quantity + 1; + }); + } else { + Get.snackbar( + 'Stock insuffisant', + 'Quantité demandée non disponible', + snackPosition: SnackPosition.BOTTOM, + ); + } + }, + ), + ], + ), + ), + ), + ); + }, + ), + ], + ), + ), + ); + } + + Widget _buildCartSection() { + final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); + + if (itemsInCart.isEmpty) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: Text( + 'Votre panier est vide', + style: TextStyle(color: Colors.grey), + ), + ), + ), + ); + } + + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Votre Panier', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + ...itemsInCart.map((entry) { + final product = _products.firstWhere((p) => p.id == entry.key); + return Card( + margin: const EdgeInsets.only(bottom: 8), + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.shopping_bag, size: 20), + ), + title: Text(product.name), + subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} DA'), + trailing: Text( + '${(entry.value * product.price).toStringAsFixed(2)} DA', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + ), + ); + }), + ], + ), + ), + ); + } + + Widget _buildTotalSection() { + double total = 0; + _quantites.forEach((productId, quantity) { + final product = _products.firstWhere((p) => p.id == productId); + total += quantity * product.price; + }); + + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Text( + '${total.toStringAsFixed(2)} DA', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildSubmitButton() { + return ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.blue.shade600, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + ), + onPressed: _submitOrder, + child: const Text( + 'Valider la Commande', + style: TextStyle(fontSize: 16, color: Colors.white), + ), + ); + } + + Future _submitOrder() async { + if (!_formKey.currentState!.validate()) { + return; + } + + final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); + if (itemsInCart.isEmpty) { + Get.snackbar( + 'Panier vide', + 'Veuillez ajouter des produits à votre commande', + snackPosition: SnackPosition.BOTTOM, + ); + return; + } + + // Créer le client + final client = Client( + nom: _nomController.text, + prenom: _prenomController.text, + email: _emailController.text, + telephone: _telephoneController.text, + adresse: _adresseController.text, + dateCreation: DateTime.now(), + ); + + // Calculer le total et préparer les détails + double total = 0; + final details = []; + + for (final entry in itemsInCart) { + final product = _products.firstWhere((p) => p.id == entry.key); + total += entry.value * product.price; + + details.add(DetailCommande( + commandeId: 0, // Valeur temporaire, sera remplacée dans la transaction + produitId: product.id!, + quantite: entry.value, + prixUnitaire: product.price, + sousTotal: entry.value * product.price, + )); + } + + // Créer la commande + final commande = Commande( + clientId: 0, // sera mis à jour après création du client + dateCommande: DateTime.now(), + statut: StatutCommande.enAttente, + montantTotal: total, + notes: 'Commande passée via l\'application', + ); + + try { + // Enregistrer la commande dans la base de données + await _database.createCommandeComplete(client, commande, details); + + Get.snackbar( + 'Succès', + 'Votre commande a été enregistrée', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + + // Réinitialiser le formulaire + _formKey.currentState!.reset(); + setState(() { + _quantites.clear(); + }); + + } catch (e) { + Get.snackbar( + 'Erreur', + 'Une erreur est survenue lors de l\'enregistrement de la commande: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + @override + void dispose() { + _nomController.dispose(); + _prenomController.dispose(); + _emailController.dispose(); + _telephoneController.dispose(); + _adresseController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/Views/produitsCard.dart b/lib/Views/produitsCard.dart index efc9ca1..255ebfd 100644 --- a/lib/Views/produitsCard.dart +++ b/lib/Views/produitsCard.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'dart:io'; -import 'package:quantity_input/quantity_input.dart'; import 'package:youmazgestion/Models/produit.dart'; class ProductCard extends StatefulWidget { @@ -163,7 +162,7 @@ class _ProductCardState extends State with TickerProviderStateMixin ), ], ), - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( @@ -300,81 +299,84 @@ class _ProductCardState extends State with TickerProviderStateMixin const SizedBox(width: 8), Expanded( - child: GestureDetector( - onTapDown: _onTapDown, - onTapUp: _onTapUp, - onTapCancel: _onTapCancel, - onTap: () { - widget.onAddToCart(widget.product, selectedQuantity); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon( - Icons.shopping_cart, - color: Colors.white, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - '${widget.product.name} (x$selectedQuantity) ajouté au panier', - overflow: TextOverflow.ellipsis, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + onTap: () { + widget.onAddToCart(widget.product, selectedQuantity); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon( + Icons.shopping_cart, + color: Colors.white, ), - ), - ], - ), - backgroundColor: Colors.green, - duration: const Duration(seconds: 1), - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + const SizedBox(width: 8), + Expanded( + child: Text( + '${widget.product.name} (x$selectedQuantity) ajouté au panier', + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + backgroundColor: Colors.green, + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 12, ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 12, - ), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - Color.fromARGB(255, 4, 54, 95), - Color.fromARGB(255, 6, 80, 140), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color.fromARGB(255, 4, 54, 95), + Color.fromARGB(255, 6, 80, 140), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color.fromARGB(255, 4, 54, 95).withOpacity(0.3), + blurRadius: 6, + offset: const Offset(0, 3), + ), ], ), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: const Color.fromARGB(255, 4, 54, 95).withOpacity(0.3), - blurRadius: 6, - offset: const Offset(0, 3), - ), - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.add_shopping_cart, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 6), - const Flexible( - child: Text( - 'Ajouter', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.add_shopping_cart, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 6), + const Flexible( + child: Text( + 'Ajouter', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, ), - ), - ], + ], + ), ), ), ), diff --git a/lib/accueil.dart b/lib/accueil.dart index e0dfcd3..8112462 100644 --- a/lib/accueil.dart +++ b/lib/accueil.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:intl/intl.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:quantity_input/quantity_input.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:youmazgestion/Views/particles.dart' show ParticleBackground; import 'package:youmazgestion/Views/produitsCard.dart'; @@ -39,27 +38,21 @@ class _AccueilPageState extends State { int selectedQuantity = 1; double totalCartPrice = 0; double amountPaid = 0; + final TextEditingController _amountController = TextEditingController(); @override void initState() { super.initState(); - initorder(); - initwork(); + _initializeDatabases(); loadUserData(); productsFuture = _initDatabaseAndFetchProducts(); - _initializeRegister(); - super.initState(); - _initializeDatabases(); - loadUserData(); - productsFuture = _initDatabaseAndFetchProducts(); } - -Future _initializeDatabases() async { - await orderDatabase.initDatabase(); - await workDatabase.initDatabase(); // Attendre l'initialisation complète - await _initializeRegister(); -} + Future _initializeDatabases() async { + await orderDatabase.initDatabase(); + await workDatabase.initDatabase(); + await _initializeRegister(); + } Future _initializeRegister() async { if (!MyApp.isRegisterOpen) { @@ -86,6 +79,30 @@ Future _initializeDatabases() async { final dateTime = DateTime.now().toString(); String user = userController.username; + if (selectedProducts.isEmpty) { + Get.snackbar( + 'Panier vide', + 'Ajoutez des produits avant de passer commande.', + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 3), + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + + if (amountPaid < totalPrice) { + Get.snackbar( + 'Paiement incomplet', + 'Le montant payé est insuffisant.', + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 3), + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + orderId = await orderDatabase.insertOrder( totalPrice, dateTime, MyApp.startDate!, user); @@ -100,7 +117,14 @@ Future _initializeDatabases() async { final updatedStock = product.stock! - quantity; await productDatabase.updateStock(product.id!, updatedStock); } + + // Afficher le ticket et réinitialiser le panier showTicketPage(); + setState(() { + selectedProducts.clear(); + _amountController.clear(); + amountPaid = 0; + }); } Future>> _initDatabaseAndFetchProducts() async { @@ -119,14 +143,6 @@ Future _initializeDatabases() async { return productsByCategory; } - void initorder() async { - await orderDatabase.initDatabase(); - } - - void initwork() async { - await workDatabase.initDatabase(); - } - double calculateTotalPrice() { double totalPrice = 0; for (final cartItem in selectedProducts) { @@ -159,49 +175,24 @@ Future _initializeDatabases() async { } void showTicketPage() { - final double totalCartPrice = calculateTotalPrice(); - - if (selectedProducts.isNotEmpty) { - if (amountPaid >= totalCartPrice) { - Get.offAll(TicketPage( - businessName: 'Youmaz', - businessAddress: - 'quartier escale, Diourbel, Sénégal, en face de Sonatel', - businessPhoneNumber: '77 446 92 68', - cartItems: selectedProducts, - totalCartPrice: totalCartPrice, - amountPaid: amountPaid, - )); - } else { - Get.snackbar( - 'Paiement incomplet', - 'Le montant payé est insuffisant.', - snackPosition: SnackPosition.BOTTOM, - duration: const Duration(seconds: 3), - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - } else { - Get.snackbar( - 'Panier vide', - 'Ajoutez des produits avant de passer commande.', - snackPosition: SnackPosition.BOTTOM, - duration: const Duration(seconds: 3), - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } + Get.offAll(TicketPage( + businessName: 'Youmaz', + businessAddress: + 'quartier escale, Diourbel, Sénégal, en face de Sonatel', + businessPhoneNumber: '77 446 92 68', + cartItems: selectedProducts, + totalCartPrice: calculateTotalPrice(), + amountPaid: amountPaid, + )); } - @override - @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar( title: "Accueil", - subtitle: Text('Bienvenue $username ! (Rôle: $role)'), + subtitle: Text('Bienvenue $username ! (Rôle: $role)', + style: const TextStyle(color: Colors.white70, fontSize: 14)), ), drawer: CustomDrawer(), body: ParticleBackground( @@ -216,98 +207,159 @@ Future _initializeDatabases() async { future: productsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color.fromARGB(255, 4, 54, 95),), + )); } else if (snapshot.hasError) { - return const Center(child: Text("Erreur de chargement")); + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error, color: Colors.red, size: 48), + SizedBox(height: 16), + Text("Erreur de chargement des produits", + style: TextStyle(fontSize: 16, color: Colors.white)), + ], + )); } else if (snapshot.hasData) { final productsByCategory = snapshot.data!; final categories = productsByCategory.keys.toList(); return Row( children: [ + // Section produits Expanded( flex: 3, - child: ListView.builder( - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - final products = productsByCategory[category]!; + child: Container( + padding:const EdgeInsets.all(8), + decoration: BoxDecoration( + + color: Colors.white.withOpacity(0.9), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(20), + ), + + ), + child: ListView.builder( + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + final products = productsByCategory[category]!; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Text( - category, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 8), + padding:const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Color.fromARGB(255, 4, 54, 95), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: Offset(0, 2),) + ], + ), + child: Center( + child: Text( + category, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), ), ), - ), - GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 1, + GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 0.9, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + return ProductCard( + product: product, + onAddToCart: (product, quantity) { + addToCartWithDetails(product, quantity); + }, + ); + }, ), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: products.length, - itemBuilder: (context, index) { - final product = products[index]; - return ProductCard( - product: product, - onAddToCart: (product, quantity) { - addToCartWithDetails(product, quantity); - }, - ); - }, - ), - ], - ); - }, - ), + ], + ); + }, + ), ), - Expanded( - flex: 1, + + // Section panier + + + ), + Expanded(flex: 1, child: Container( decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: const BorderRadius.only( + borderRadius:const BorderRadius.only( topLeft: Radius.circular(20), - bottomLeft: Radius.circular(20), ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withOpacity(0.2), blurRadius: 10, spreadRadius: 2, ), ], ), - padding: const EdgeInsets.all(16), + padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text( - 'Panier', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Color.fromARGB(255, 4, 54, 95), + borderRadius: BorderRadius.circular(12), + ), + child:const Text( + 'Panier', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, ), - const SizedBox(height: 16), + SizedBox(height: 16), + + // Liste des produits dans le panier Expanded( child: selectedProducts.isEmpty ? const Center( - child: Text( - "Votre panier est vide", - style: TextStyle(fontSize: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.shopping_cart, + size: 48, color: Colors.grey), + SizedBox(height: 16), + Text( + "Votre panier est vide", + style: TextStyle( + fontSize: 16, + color: Colors.grey), + ), + ], ), ) : ListView.builder( @@ -315,86 +367,135 @@ Future _initializeDatabases() async { itemBuilder: (context, index) { final cartItem = selectedProducts[index]; return Card( - margin: const EdgeInsets.symmetric( - vertical: 4), + margin: EdgeInsets.symmetric(vertical: 4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 2, child: ListTile( + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: 4), + leading: Icon(Icons.shopping_basket, + color: Color.fromARGB(255, 4, 54, 95),), title: Text( cartItem.product.name, style: const TextStyle( - fontWeight: FontWeight.bold), + fontWeight: FontWeight.bold), ), subtitle: Text( '${NumberFormat('#,##0').format(cartItem.product.price)} FCFA x ${cartItem.quantity}', - style: const TextStyle( - fontSize: 14), + style:const TextStyle(fontSize: 14), ), - trailing: IconButton( - icon: const Icon(Icons.delete, - color: Colors.red), - onPressed: () { - setState(() { - selectedProducts - .removeAt(index); - }); - }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${NumberFormat('#,##0').format(cartItem.product.price * cartItem.quantity)}', + style:const TextStyle( + fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.delete, + color: Colors.red), + onPressed: () { + setState(() { + selectedProducts + .removeAt(index); + }); + }, + ), + ], ), ), ); }, ), ), - const Divider(thickness: 1), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - 'Total: ${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ), - TextField( - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Montant payé', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - ), - filled: true, - fillColor: Colors.white, - ), - onChanged: (value) { - amountPaid = double.tryParse(value) ?? 0; - }, - ), - const SizedBox(height: 16), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), + + // Total et paiement + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: Offset(0, 2), + ) + ], ), - onPressed: saveOrderToDatabase, - child: const Text( - 'Valider la commande', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Total:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold)), + Text( + '${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 4, 54, 95),), + ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: _amountController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Montant payé', + prefixIcon: Icon(Icons.attach_money), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + filled: true, + fillColor: Colors.grey[100], + ), + onChanged: (value) { + setState(() { + amountPaid = double.tryParse(value) ?? 0; + }); + }, + ), + SizedBox(height: 16), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: saveOrderToDatabase, + icon:const Icon(Icons.check_circle), + label:const Text( + 'Valider la commande', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white), + ), + ), + ], ), ), ], ), - ), - ), + ),) ], ); } else { - return const Center(child: Text("Aucun produit disponible")); + return const Center( + child: Text("Aucun produit disponible", + style: TextStyle(color: Colors.white)), + ); } }, ), @@ -402,4 +503,10 @@ Future _initializeDatabases() async { ), ); } + + @override + void dispose() { + _amountController.dispose(); + super.dispose(); + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 4760cbb..4df3e92 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,8 @@ void main() async { try { // Initialiser les bases de données une seule fois // await AppDatabase.instance.deleteDatabaseFile(); - + //await ProductDatabase.instance.deleteDatabaseFile(); + await ProductDatabase.instance.initDatabase(); await AppDatabase.instance.initDatabase();