diff --git a/lib/Components/appDrawer.dart b/lib/Components/appDrawer.dart index d11b9f3..90cf050 100644 --- a/lib/Components/appDrawer.dart +++ b/lib/Components/appDrawer.dart @@ -14,7 +14,7 @@ import 'package:youmazgestion/Views/newCommand.dart'; import 'package:youmazgestion/Views/registrationPage.dart'; import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/controller/userController.dart'; -import 'package:youmazgestion/Views/gestion_point_de_vente.dart'; // Nouvel import +import 'package:youmazgestion/Views/gestion_point_de_vente.dart'; class CustomDrawer extends StatelessWidget { final UserController userController = Get.find(); @@ -25,6 +25,7 @@ class CustomDrawer extends StatelessWidget { await prefs.remove('role'); await prefs.remove('user_id'); + // ✅ IMPORTANT: Vider le cache de session userController.clearUserData(); } @@ -34,425 +35,573 @@ class CustomDrawer extends StatelessWidget { Widget build(BuildContext context) { return Drawer( backgroundColor: Colors.white, - child: FutureBuilder( - future: _buildDrawerItems(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return ListView( - padding: EdgeInsets.zero, - children: snapshot.data as List, - ); - } else { - return const Center(child: CircularProgressIndicator()); - } + child: GetBuilder( + builder: (controller) { + return ListView( + padding: EdgeInsets.zero, + children: [ + // Header utilisateur + _buildUserHeader(controller), + + // ✅ CORRIGÉ: Construction avec gestion des valeurs null + ..._buildDrawerItemsFromSessionCache(), + + // Déconnexion + const Divider(), + _buildLogoutItem(), + ], + ); }, ), ); } - Future> _buildDrawerItems() async { + /// ✅ CORRIGÉ: Construction avec validation robuste des données + List _buildDrawerItemsFromSessionCache() { List drawerItems = []; - - drawerItems.add( - GetBuilder( - builder: (controller) => Container( - padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20), - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Row( + + // Vérifier si le cache est prêt + if (!userController.isCacheReady) { + return [ + const Padding( + padding: EdgeInsets.all(16.0), + child: Column( children: [ - const CircleAvatar( - radius: 30, - backgroundImage: AssetImage("assets/youmaz2.png"), + SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), ), - const SizedBox(width: 15), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - controller.name.isNotEmpty - ? controller.name - : 'Utilisateur', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Text( - controller.role.isNotEmpty ? controller.role : 'Aucun rôle', - style: const TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], + SizedBox(height: 8), + Text( + "Chargement du menu...", + style: TextStyle(color: Colors.grey, fontSize: 12), ), ], ), ), - ), - ); - - drawerItems.add( - await _buildDrawerItem( - icon: Icons.home, - title: "Accueil", - color: Colors.blue, - permissionAction: 'view', - permissionRoute: '/accueil', - onTap: () => Get.to(DashboardPage()), - ), - ); - - List gestionUtilisateursItems = [ - await _buildDrawerItem( - icon: Icons.person_add, - title: "Ajouter un utilisateur", - color: Colors.green, - permissionAction: 'create', - permissionRoute: '/ajouter-utilisateur', - onTap: () => Get.to(const RegistrationPage()), - ), - await _buildDrawerItem( - icon: Icons.supervised_user_circle, - title: "Gérer les utilisateurs", - color: const Color.fromARGB(255, 4, 54, 95), - permissionAction: 'update', - permissionRoute: '/modifier-utilisateur', - onTap: () => Get.to(const ListUserPage()), - ), - await _buildDrawerItem( - icon: Icons.timer, - title: "Gestion des pointages", - color: const Color.fromARGB(255, 4, 54, 95), - permissionAction: 'update', - permissionRoute: '/pointage', - onTap: () => {}, - ) - ]; + ]; + } - if (gestionUtilisateursItems.any((item) => item is ListTile)) { - drawerItems.add( + // Récupérer les menus depuis le cache de session + final rawUserMenus = userController.getUserMenus(); + + // 🛡️ VALIDATION: Filtrer les menus valides + final validMenus = >[]; + final invalidMenus = >[]; + + for (var menu in rawUserMenus) { + // Vérifier que les champs essentiels ne sont pas null + final name = menu['name']; + final route = menu['route']; + final id = menu['id']; + + if (name != null && route != null && route.toString().isNotEmpty) { + validMenus.add({ + 'id': id, + 'name': name.toString(), + 'route': route.toString(), + }); + } else { + invalidMenus.add(menu); + print("⚠️ Menu invalide ignoré dans CustomDrawer: id=$id, name='$name', route='$route'"); + } + } + + // Afficher les statistiques de validation + if (invalidMenus.isNotEmpty) { + print("📊 CustomDrawer: ${validMenus.length} menus valides, ${invalidMenus.length} invalides"); + } + + if (validMenus.isEmpty) { + return [ const Padding( - padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), + padding: EdgeInsets.all(16.0), child: Text( - "GESTION UTILISATEURS", - style: TextStyle( - color: Colors.grey, - fontSize: 12, - fontWeight: FontWeight.bold, - ), + "Aucun menu accessible", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), ), ), - ); - drawerItems.addAll(gestionUtilisateursItems); + ]; } - List gestionProduitsItems = [ - await _buildDrawerItem( - icon: Icons.inventory, - title: "Gestion des produits", - color: Colors.indigoAccent, - permissionAction: 'create', - permissionRoute: '/ajouter-produit', - onTap: () => Get.to(const ProductManagementPage()), - ), - await _buildDrawerItem( - icon: Icons.storage, - title: "Gestion de stock", - color: Colors.blueAccent, - permissionAction: 'update', - permissionRoute: '/gestion-stock', - onTap: () => Get.to(const GestionStockPage()), - ), - ]; - - if (gestionProduitsItems.any((item) => item is ListTile)) { - drawerItems.add( - 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, - ), - ), - ), - ); - drawerItems.addAll(gestionProduitsItems); + // 🔧 DÉDUPLICATION: Éliminer les doublons par route + final Map> uniqueMenus = {}; + for (var menu in validMenus) { + final route = menu['route'] as String; + uniqueMenus[route] = menu; + } + final deduplicatedMenus = uniqueMenus.values.toList(); + + if (deduplicatedMenus.length != validMenus.length) { + print("🔧 CustomDrawer: ${validMenus.length - deduplicatedMenus.length} doublons supprimés"); } - List gestionCommandesItems = [ - await _buildDrawerItem( - icon: Icons.add_shopping_cart, - title: "Nouvelle commande", - color: Colors.orange, - permissionAction: 'create', - permissionRoute: '/nouvelle-commande', - onTap: () => Get.to(const NouvelleCommandePage()), - ), - await _buildDrawerItem( - icon: Icons.list_alt, - title: "Gérer les commandes", - color: Colors.deepPurple, - permissionAction: 'manage', - permissionRoute: '/gerer-commandes', - onTap: () => Get.to(const GestionCommandesPage()), - ), - ]; + // Organiser les menus par catégories + final Map>> categorizedMenus = { + 'GESTION UTILISATEURS': [], + 'GESTION PRODUITS': [], + 'GESTION COMMANDES': [], + 'RAPPORTS': [], + 'ADMINISTRATION': [], + }; - if (gestionCommandesItems.any((item) => item is ListTile)) { - drawerItems.add( - 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, - ), - ), - ), - ); - drawerItems.addAll(gestionCommandesItems); + // Accueil toujours en premier + final accueilMenu = deduplicatedMenus.where((menu) => menu['route'] == '/accueil').firstOrNull; + if (accueilMenu != null) { + drawerItems.add(_buildDrawerItemFromMenu(accueilMenu)); } - List rapportsItems = [ - await _buildDrawerItem( - icon: Icons.bar_chart, - title: "Bilan ", - color: Colors.teal, - permissionAction: 'read', - permissionRoute: '/bilan', - onTap: () => Get.to(DashboardPage()), - ), - await _buildDrawerItem( - icon: Icons.history, - title: "Historique", - color: Colors.blue, - permissionAction: 'read', - permissionRoute: '/historique', - onTap: () => Get.to(const HistoriquePage()), - ), - ]; + // Catégoriser les autres menus avec validation supplémentaire + for (var menu in deduplicatedMenus) { + final route = menu['route'] as String; + + // ✅ Validation supplémentaire avant categorisation + if (route.isEmpty) { + print("⚠️ Route vide ignorée: ${menu['name']}"); + continue; + } + + switch (route) { + case '/accueil': + // Déjà traité + break; + case '/ajouter-utilisateur': + case '/modifier-utilisateur': + case '/pointage': + categorizedMenus['GESTION UTILISATEURS']!.add(menu); + break; + case '/ajouter-produit': + case '/gestion-stock': + categorizedMenus['GESTION PRODUITS']!.add(menu); + break; + case '/nouvelle-commande': + case '/gerer-commandes': + categorizedMenus['GESTION COMMANDES']!.add(menu); + break; + case '/bilan': + case '/historique': + categorizedMenus['RAPPORTS']!.add(menu); + break; + case '/gerer-roles': + case '/points-de-vente': + categorizedMenus['ADMINISTRATION']!.add(menu); + break; + default: + // Menu non catégorisé + print("⚠️ Menu non catégorisé: $route"); + break; + } + } - if (rapportsItems.any((item) => item is ListTile)) { - drawerItems.add( - const Padding( - padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), - child: Text( - "RAPPORTS", - style: TextStyle( - color: Colors.grey, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), + // Ajouter les catégories avec leurs menus + categorizedMenus.forEach((categoryName, menus) { + if (menus.isNotEmpty) { + drawerItems.add(_buildCategoryHeader(categoryName)); + for (var menu in menus) { + drawerItems.add(_buildDrawerItemFromMenu(menu)); + } + } + }); + + return drawerItems; + } + + /// ✅ CORRIGÉ: Construction d'un item de menu avec validation + Widget _buildDrawerItemFromMenu(Map menu) { + // 🛡️ VALIDATION: Vérification des types avec gestion des null + final nameObj = menu['name']; + final routeObj = menu['route']; + + if (nameObj == null || routeObj == null) { + print("⚠️ Menu invalide dans _buildDrawerItemFromMenu: name=$nameObj, route=$routeObj"); + return const SizedBox.shrink(); + } + + final String name = nameObj.toString(); + final String route = routeObj.toString(); + + if (name.isEmpty || route.isEmpty) { + print("⚠️ Menu avec valeurs vides: name='$name', route='$route'"); + return const SizedBox.shrink(); + } + + // Mapping des routes vers les widgets et icônes + final Map> routeMapping = { + '/accueil': { + 'icon': Icons.home, + 'color': Colors.blue, + 'widget': DashboardPage(), + }, + '/ajouter-utilisateur': { + 'icon': Icons.person_add, + 'color': Colors.green, + 'widget': const RegistrationPage(), + }, + '/modifier-utilisateur': { + 'icon': Icons.supervised_user_circle, + 'color': const Color.fromARGB(255, 4, 54, 95), + 'widget': const ListUserPage(), + }, + '/pointage': { + 'icon': Icons.timer, + 'color': const Color.fromARGB(255, 4, 54, 95), + 'widget': null, // TODO: Implémenter + }, + '/ajouter-produit': { + 'icon': Icons.inventory, + 'color': Colors.indigoAccent, + 'widget': const ProductManagementPage(), + }, + '/gestion-stock': { + 'icon': Icons.storage, + 'color': Colors.blueAccent, + 'widget': const GestionStockPage(), + }, + '/nouvelle-commande': { + 'icon': Icons.add_shopping_cart, + 'color': Colors.orange, + 'widget': const NouvelleCommandePage(), + }, + '/gerer-commandes': { + 'icon': Icons.list_alt, + 'color': Colors.deepPurple, + 'widget': const GestionCommandesPage(), + }, + '/bilan': { + 'icon': Icons.bar_chart, + 'color': Colors.teal, + 'widget': DashboardPage(), + }, + '/historique': { + 'icon': Icons.history, + 'color': Colors.blue, + 'widget': const HistoriquePage(), + }, + '/gerer-roles': { + 'icon': Icons.admin_panel_settings, + 'color': Colors.redAccent, + 'widget': const RoleListPage(), + }, + '/points-de-vente': { + 'icon': Icons.store, + 'color': Colors.blueGrey, + 'widget': const AjoutPointDeVentePage(), + }, + }; + + final routeData = routeMapping[route]; + if (routeData == null) { + print("⚠️ Route non reconnue: '$route' pour le menu '$name'"); + return ListTile( + leading: const Icon(Icons.help_outline, color: Colors.grey), + title: Text(name), + subtitle: Text("Route: $route", style: const TextStyle(fontSize: 10, color: Colors.grey)), + onTap: () { + Get.snackbar( + "Route non configurée", + "La route '$route' n'est pas encore configurée", + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade100, + ); + }, ); - drawerItems.addAll(rapportsItems); } - List administrationItems = [ - await _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()), + return ListTile( + leading: Icon( + routeData['icon'] as IconData, + color: routeData['color'] as Color, ), - await _buildDrawerItem( - icon: Icons.store, - title: "Points de vente", - color: Colors.blueGrey, - permissionAction: 'admin', - permissionRoute: '/points-de-vente', - onTap: () => Get.to(const AjoutPointDeVentePage()), + title: Text(name), + trailing: const Icon(Icons.chevron_right, color: Colors.grey), + onTap: () { + final widget = routeData['widget']; + if (widget != null) { + Get.to(widget); + } else { + Get.snackbar( + "Non implémenté", + "Cette fonctionnalité sera bientôt disponible", + snackPosition: SnackPosition.BOTTOM, + ); + } + }, + ); + } + + /// Header de catégorie + Widget _buildCategoryHeader(String categoryName) { + return Padding( + padding: const EdgeInsets.only(left: 20, top: 15, bottom: 5), + child: Text( + categoryName, + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), - ]; + ); + } - if (administrationItems.any((item) => item is ListTile)) { - drawerItems.add( - const Padding( - padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), - child: Text( - "ADMINISTRATION", - style: TextStyle( - color: Colors.grey, - fontSize: 12, - fontWeight: FontWeight.bold, + /// Header utilisateur amélioré + Widget _buildUserHeader(UserController controller) { + return Container( + padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20), + decoration: const BoxDecoration( + gradient: LinearGradient( + 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), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.name.isNotEmpty + ? controller.fullName + : 'Utilisateur', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + controller.role.isNotEmpty ? controller.role : 'Aucun rôle', + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + if (controller.pointDeVenteDesignation.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + controller.pointDeVenteDesignation, + style: const TextStyle( + color: Colors.white60, + fontSize: 10, + ), + ), + ], + // ✅ Indicateur de statut du cache + const SizedBox(height: 4), + Row( + children: [ + Icon( + controller.isCacheReady ? Icons.check_circle : Icons.hourglass_empty, + color: controller.isCacheReady ? Colors.green : Colors.orange, + size: 12, + ), + const SizedBox(width: 4), + Text( + controller.isCacheReady ? 'Menu prêt' : 'Chargement...', + style: const TextStyle( + color: Colors.white60, + fontSize: 10, + ), + ), + ], + ), + ], ), ), - ), - ); - drawerItems.addAll(administrationItems); - } - - drawerItems.add(const Divider()); + // ✅ Bouton de rafraîchissement pour les admins + if (controller.role == 'Super Admin' || controller.role == 'Admin') ...[ + IconButton( + icon: const Icon(Icons.refresh, color: Colors.white70, size: 20), + onPressed: () async { + Get.snackbar( + "Cache", + "Rechargement des permissions...", + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 1), + ); + await controller.refreshPermissions(); + Get.back(); // Fermer le drawer + Get.snackbar( + "Cache", + "Permissions rechargées avec succès", + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + }, + tooltip: "Recharger les permissions", + ), + ], + // 🔧 Bouton de debug (à supprimer en production) + if (controller.role == 'Super Admin') ...[ + IconButton( + icon: const Icon(Icons.bug_report, color: Colors.white70, size: 18), + onPressed: () { + // Debug des menus + final menus = controller.getUserMenus(); + String debugInfo = "MENUS DEBUG:\n"; + for (var i = 0; i < menus.length; i++) { + final menu = menus[i]; + debugInfo += "[$i] ID:${menu['id']}, Name:'${menu['name']}', Route:'${menu['route']}'\n"; + } + + Get.dialog( + AlertDialog( + title: const Text("Debug Menus"), + content: SingleChildScrollView( + child: Text(debugInfo, style: const TextStyle(fontSize: 12)), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text("Fermer"), + ), + ], + ), + ); + }, + tooltip: "Debug menus", + ), + ], + ], + ), + ); + } - drawerItems.add( - ListTile( - leading: const Icon(Icons.logout, color: Colors.red), - title: const Text("Déconnexion"), - onTap: () { - Get.dialog( - AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - contentPadding: EdgeInsets.zero, - content: Container( - constraints: const BoxConstraints(maxWidth: 400), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - child: Column( - children: [ - Icon( - Icons.logout_rounded, - size: 48, - color: Colors.orange.shade600, - ), - const SizedBox(height: 16), - const Text( - "Déconnexion", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), + /// Item de déconnexion + Widget _buildLogoutItem() { + return ListTile( + leading: const Icon(Icons.logout, color: Colors.red), + title: const Text("Déconnexion"), + onTap: () { + Get.dialog( + AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + contentPadding: EdgeInsets.zero, + content: Container( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Icon( + Icons.logout_rounded, + size: 48, + color: Colors.orange.shade600, + ), + const SizedBox(height: 16), + const Text( + "Déconnexion", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Colors.black87, ), - const SizedBox(height: 12), - const Text( - "Êtes-vous sûr de vouloir vous déconnecter ?", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.black87, - height: 1.4, - ), + ), + const SizedBox(height: 12), + const Text( + "Êtes-vous sûr de vouloir vous déconnecter ?", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.black87, + height: 1.4, ), - const SizedBox(height: 8), - Text( - "Vous devrez vous reconnecter pour accéder à votre compte.", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade600, - height: 1.3, - ), + ), + const SizedBox(height: 8), + Text( + "Vos permissions seront rechargées à la prochaine connexion.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + height: 1.3, ), - ], - ), + ), + ], ), - // Actions - Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), - child: Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => Get.back(), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - side: BorderSide( - color: Colors.grey.shade300, - width: 1.5, - ), + ), + // Actions + Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Get.back(), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - child: const Text( - "Annuler", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.black87, - ), + side: BorderSide( + color: Colors.grey.shade300, + width: 1.5, + ), + ), + child: const Text( + "Annuler", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black87, ), ), ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: () async { - await clearUserData(); - Get.offAll(const LoginPage()); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red.shade600, - foregroundColor: Colors.white, - elevation: 2, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () async { + // ✅ IMPORTANT: Vider le cache de session lors de la déconnexion + await clearUserData(); + Get.offAll(const LoginPage()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + foregroundColor: Colors.white, + elevation: 2, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - child: const Text( - "Se déconnecter", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), + ), + child: const Text( + "Se déconnecter", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, ), ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), - barrierDismissible: true, - ); - }, - ), - ); - - return drawerItems; - } - - Future _buildDrawerItem({ - required IconData icon, - required String title, - required Color color, - String? permissionAction, - String? permissionRoute, - required VoidCallback onTap, - }) async { - if (permissionAction != null && permissionRoute != null) { - bool hasPermission = - await userController.hasPermission(permissionAction, permissionRoute); - if (!hasPermission) { - return const SizedBox.shrink(); - } - } - - return ListTile( - leading: Icon(icon, color: color), - title: Text(title), - trailing: permissionAction != null - ? const Icon(Icons.chevron_right, color: Colors.grey) - : null, - onTap: onTap, + ), + barrierDismissible: true, + ); + }, ); } -} - +} \ No newline at end of file diff --git a/lib/Components/commandManagementComponents/CommandDetails.dart b/lib/Components/commandManagementComponents/CommandDetails.dart new file mode 100644 index 0000000..62ab322 --- /dev/null +++ b/lib/Components/commandManagementComponents/CommandDetails.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:youmazgestion/Models/client.dart'; +import 'package:youmazgestion/Services/stock_managementDatabase.dart'; + +class CommandeDetails extends StatelessWidget { + final Commande commande; + + const CommandeDetails({required this.commande}); + + + + 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, + ), + ); + } + +@override + Widget build(BuildContext context) { + return FutureBuilder>( + future: AppDatabase.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.estCadeau == true + ? '${detail.produitNom ?? 'Produit inconnu'} (CADEAU)' + : detail.produitNom ?? 'Produit inconnu' + ), + _buildTableCell('${detail.quantite}'), + _buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(2)} MGA'), + _buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(2)} MGA'), + ], + )), + ], + ), + ), + 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: Column( + children: [ + if (commande.montantApresRemise != null) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Sous-total:', + style: TextStyle(fontSize: 14), + ), + Text( + '${commande.montantTotal.toStringAsFixed(2)} MGA', + style: const TextStyle(fontSize: 14), + ), + ], + ), + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Remise:', + style: TextStyle(fontSize: 14), + ), + Text( + '-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA', + style: const TextStyle( + fontSize: 14, + color: Colors.red, + ), + ), + ], + ), + const Divider(), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total de la commande:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + '${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(2)} MGA', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.green.shade700, + ), + ), + ], + ), + ], + ), + ), + ], + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/Components/commandManagementComponents/CommandeActions.dart b/lib/Components/commandManagementComponents/CommandeActions.dart new file mode 100644 index 0000000..363e724 --- /dev/null +++ b/lib/Components/commandManagementComponents/CommandeActions.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:youmazgestion/Models/client.dart'; + + +//Classe suplementaire + +class CommandeActions extends StatelessWidget { + final Commande commande; + final Function(int, StatutCommande) onStatutChanged; + final Function(Commande) onPaymentSelected; + final Function(Commande) onDiscountSelected; + final Function(Commande) onGiftSelected; + + const CommandeActions({ + required this.commande, + required this.onStatutChanged, + required this.onPaymentSelected, + required this.onDiscountSelected, + required this.onGiftSelected, + }); + + + + List _buildActionButtons(BuildContext context) { + List buttons = []; + + switch (commande.statut) { + case StatutCommande.enAttente: + buttons.addAll([ + _buildActionButton( + label: 'Remise', + icon: Icons.percent, + color: Colors.orange, + onPressed: () => onDiscountSelected(commande), + ), + _buildActionButton( + label: 'Cadeau', + icon: Icons.card_giftcard, + color: Colors.purple, + onPressed: () => onGiftSelected(commande), + ), + _buildActionButton( + label: 'Confirmer', + icon: Icons.check_circle, + color: Colors.blue, + onPressed: () => onPaymentSelected(commande), + ), + _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.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 confirmé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'), + ), + ], + ); + }, + ); + } + @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), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/Components/commandManagementComponents/DiscountDialog.dart b/lib/Components/commandManagementComponents/DiscountDialog.dart new file mode 100644 index 0000000..12b4b1e --- /dev/null +++ b/lib/Components/commandManagementComponents/DiscountDialog.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:youmazgestion/Models/client.dart'; + + +// Dialog pour la remise +class DiscountDialog extends StatefulWidget { + final Commande commande; + + const DiscountDialog({super.key, required this.commande}); + + @override + _DiscountDialogState createState() => _DiscountDialogState(); +} + +class _DiscountDialogState extends State { + final _pourcentageController = TextEditingController(); + final _montantController = TextEditingController(); + bool _isPercentage = true; + double _montantFinal = 0; + + @override + void initState() { + super.initState(); + _montantFinal = widget.commande.montantTotal; + } + + void _calculateDiscount() { + double discount = 0; + + if (_isPercentage) { + final percentage = double.tryParse(_pourcentageController.text) ?? 0; + discount = (widget.commande.montantTotal * percentage) / 100; + } else { + discount = double.tryParse(_montantController.text) ?? 0; + } + + setState(() { + _montantFinal = widget.commande.montantTotal - discount; + if (_montantFinal < 0) _montantFinal = 0; + }); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Appliquer une remise'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Montant original: ${widget.commande.montantTotal.toStringAsFixed(2)} MGA'), + const SizedBox(height: 16), + + // Choix du type de remise + Row( + children: [ + Expanded( + child: RadioListTile( + title: const Text('Pourcentage'), + value: true, + groupValue: _isPercentage, + onChanged: (value) { + setState(() { + _isPercentage = value!; + _calculateDiscount(); + }); + }, + ), + ), + Expanded( + child: RadioListTile( + title: const Text('Montant fixe'), + value: false, + groupValue: _isPercentage, + onChanged: (value) { + setState(() { + _isPercentage = value!; + _calculateDiscount(); + }); + }, + ), + ), + ], + ), + + const SizedBox(height: 16), + + if (_isPercentage) + TextField( + controller: _pourcentageController, + decoration: const InputDecoration( + labelText: 'Pourcentage de remise', + suffixText: '%', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + onChanged: (value) => _calculateDiscount(), + ) + else + TextField( + controller: _montantController, + decoration: const InputDecoration( + labelText: 'Montant de remise', + suffixText: 'MGA', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + onChanged: (value) => _calculateDiscount(), + ), + + const SizedBox(height: 16), + + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Montant final:'), + Text( + '${_montantFinal.toStringAsFixed(2)} MGA', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + if (_montantFinal < widget.commande.montantTotal) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Économie:'), + Text( + '${(widget.commande.montantTotal - _montantFinal).toStringAsFixed(2)} MGA', + style: const TextStyle( + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: _montantFinal < widget.commande.montantTotal + ? () { + final pourcentage = _isPercentage + ? double.tryParse(_pourcentageController.text) + : null; + final montant = !_isPercentage + ? double.tryParse(_montantController.text) + : null; + + Navigator.pop(context, { + 'pourcentage': pourcentage, + 'montant': montant, + 'montantFinal': _montantFinal, + }); + } + : null, + child: const Text('Appliquer'), + ), + ], + ); + } + + @override + void dispose() { + _pourcentageController.dispose(); + _montantController.dispose(); + super.dispose(); + } +} diff --git a/lib/Components/commandManagementComponents/GiftSelectionDialog.dart b/lib/Components/commandManagementComponents/GiftSelectionDialog.dart new file mode 100644 index 0000000..b7ebdb2 --- /dev/null +++ b/lib/Components/commandManagementComponents/GiftSelectionDialog.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:youmazgestion/Models/client.dart'; +import 'package:youmazgestion/Models/produit.dart'; +import 'package:youmazgestion/Services/stock_managementDatabase.dart'; + + +// Dialog pour sélectionner un cadeau +class GiftSelectionDialog extends StatefulWidget { + final Commande commande; + + const GiftSelectionDialog({super.key, required this.commande}); + + @override + _GiftSelectionDialogState createState() => _GiftSelectionDialogState(); +} + +class _GiftSelectionDialogState extends State { + List _products = []; + List _filteredProducts = []; + final _searchController = TextEditingController(); + Product? _selectedProduct; + + @override + void initState() { + super.initState(); + _loadProducts(); + _searchController.addListener(_filterProducts); + } + + Future _loadProducts() async { + final products = await AppDatabase.instance.getProducts(); + setState(() { + _products = products.where((p) => p.stock > 0).toList(); + _filteredProducts = _products; + }); + } + + void _filterProducts() { + final query = _searchController.text.toLowerCase(); + setState(() { + _filteredProducts = _products.where((product) { + return product.name.toLowerCase().contains(query) || + (product.reference?.toLowerCase().contains(query) ?? false) || + (product.category.toLowerCase().contains(query)); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Sélectionner un cadeau'), + content: SizedBox( + width: double.maxFinite, + height: 400, + child: Column( + children: [ + TextField( + controller: _searchController, + decoration: const InputDecoration( + labelText: 'Rechercher un produit', + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + Expanded( + child: ListView.builder( + itemCount: _filteredProducts.length, + itemBuilder: (context, index) { + final product = _filteredProducts[index]; + return Card( + child: ListTile( + leading: product.image != null + ? Image.network( + product.image!, + width: 50, + height: 50, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.image_not_supported), + ) + : const Icon(Icons.phone_android), + title: Text(product.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Catégorie: ${product.category}'), + Text('Stock: ${product.stock}'), + if (product.reference != null) + Text('Réf: ${product.reference}'), + ], + ), + trailing: Radio( + value: product, + groupValue: _selectedProduct, + onChanged: (value) { + setState(() { + _selectedProduct = value; + }); + }, + ), + onTap: () { + setState(() { + _selectedProduct = product; + }); + }, + ), + ); + }, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: _selectedProduct != null + ? () => Navigator.pop(context, _selectedProduct) + : null, + child: const Text('Ajouter le cadeau'), + ), + ], + ); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } +} diff --git a/lib/Components/commandManagementComponents/PaymentMethod.dart b/lib/Components/commandManagementComponents/PaymentMethod.dart new file mode 100644 index 0000000..cef66df --- /dev/null +++ b/lib/Components/commandManagementComponents/PaymentMethod.dart @@ -0,0 +1,8 @@ +import 'package:youmazgestion/Components/paymentType.dart'; + +class PaymentMethod { + final PaymentType type; + final double amountGiven; + + PaymentMethod({required this.type, this.amountGiven = 0}); +} diff --git a/lib/Components/commandManagementComponents/PaymentMethodDialog.dart b/lib/Components/commandManagementComponents/PaymentMethodDialog.dart new file mode 100644 index 0000000..b0ea0c5 --- /dev/null +++ b/lib/Components/commandManagementComponents/PaymentMethodDialog.dart @@ -0,0 +1,288 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:get/get_navigation/src/snackbar/snackbar.dart'; +import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart'; +import 'package:youmazgestion/Components/paymentType.dart'; +import 'package:youmazgestion/Models/client.dart'; + + +class PaymentMethodDialog extends StatefulWidget { + final Commande commande; + + const PaymentMethodDialog({super.key, required this.commande}); + + @override + _PaymentMethodDialogState createState() => _PaymentMethodDialogState(); +} + +class _PaymentMethodDialogState extends State { + PaymentType _selectedPayment = PaymentType.cash; + final _amountController = TextEditingController(); + + void _validatePayment() { + final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; + + if (_selectedPayment == PaymentType.cash) { + final amountGiven = double.tryParse(_amountController.text) ?? 0; + if (amountGiven < montantFinal) { + Get.snackbar( + 'Erreur', + 'Le montant donné est insuffisant', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + } + + Navigator.pop(context, PaymentMethod( + type: _selectedPayment, + amountGiven: _selectedPayment == PaymentType.cash + ? double.parse(_amountController.text) + : montantFinal, + )); + } + + @override + void initState() { + super.initState(); + final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; + _amountController.text = montantFinal.toStringAsFixed(2); + } + + @override + void dispose() { + _amountController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final amount = double.tryParse(_amountController.text) ?? 0; + final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; + final change = amount - montantFinal; + + return AlertDialog( + title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Affichage du montant à payer + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + children: [ + if (widget.commande.montantApresRemise != null) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Montant original:'), + Text('${widget.commande.montantTotal.toStringAsFixed(2)} MGA'), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Remise:'), + Text('-${(widget.commande.montantTotal - widget.commande.montantApresRemise!).toStringAsFixed(2)} MGA', + style: const TextStyle(color: Colors.red)), + ], + ), + const Divider(), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)), + Text('${montantFinal.toStringAsFixed(2)} MGA', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + ], + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Section Paiement mobile + const Align( + alignment: Alignment.centerLeft, + child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _buildMobileMoneyTile( + title: 'Mvola', + imagePath: 'assets/mvola.jpg', + value: PaymentType.mvola, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildMobileMoneyTile( + title: 'Orange Money', + imagePath: 'assets/Orange_money.png', + value: PaymentType.orange, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildMobileMoneyTile( + title: 'Airtel Money', + imagePath: 'assets/airtel_money.png', + value: PaymentType.airtel, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Section Carte bancaire + const Align( + alignment: Alignment.centerLeft, + child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)), + ), + const SizedBox(height: 8), + _buildPaymentMethodTile( + title: 'Carte bancaire', + icon: Icons.credit_card, + value: PaymentType.card, + ), + const SizedBox(height: 16), + + // Section Paiement en liquide + const Align( + alignment: Alignment.centerLeft, + child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)), + ), + const SizedBox(height: 8), + _buildPaymentMethodTile( + title: 'Paiement en liquide', + icon: Icons.money, + value: PaymentType.cash, + ), + if (_selectedPayment == PaymentType.cash) ...[ + const SizedBox(height: 12), + TextField( + controller: _amountController, + decoration: const InputDecoration( + labelText: 'Montant donné', + prefixText: 'MGA ', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.numberWithOptions(decimal: true), + onChanged: (value) => setState(() {}), + ), + const SizedBox(height: 8), + Text( + 'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: change >= 0 ? Colors.green : Colors.red, + ), + ), + ], + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler', style: TextStyle(color: Colors.grey)), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade800, + foregroundColor: Colors.white, + ), + onPressed: _validatePayment, + child: const Text('Confirmer'), + ), + ], + ); + } + + Widget _buildMobileMoneyTile({ + required String title, + required String imagePath, + required PaymentType value, + }) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), + width: 2, + ), + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => setState(() => _selectedPayment = value), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Image.asset( + imagePath, + height: 30, + width: 30, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.mobile_friendly, size: 30), + ), + const SizedBox(height: 8), + Text( + title, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12), + ), + ], + ), + ), + ), + ); + } + + Widget _buildPaymentMethodTile({ + required String title, + required IconData icon, + required PaymentType value, + }) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), + width: 2, + ), + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => setState(() => _selectedPayment = value), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Icon(icon, size: 24), + const SizedBox(width: 12), + Text(title), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/Components/commandManagementComponents/PaymentType.dart b/lib/Components/commandManagementComponents/PaymentType.dart new file mode 100644 index 0000000..83b0bf3 --- /dev/null +++ b/lib/Components/commandManagementComponents/PaymentType.dart @@ -0,0 +1,7 @@ +enum PaymentType { + cash, + card, + mvola, + orange, + airtel +} \ No newline at end of file diff --git a/lib/Components/teat.dart b/lib/Components/teat.dart new file mode 100644 index 0000000..4543ef4 --- /dev/null +++ b/lib/Components/teat.dart @@ -0,0 +1,2125 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; + +import 'package:youmazgestion/Components/app_bar.dart'; +import 'package:youmazgestion/Components/appDrawer.dart'; +import 'package:youmazgestion/Models/client.dart'; +import 'package:youmazgestion/Models/users.dart'; +import 'package:youmazgestion/Models/produit.dart'; +import 'package:youmazgestion/Services/stock_managementDatabase.dart'; + +class NouvelleCommandePage extends StatefulWidget { + const NouvelleCommandePage({super.key}); + + @override + _NouvelleCommandePageState createState() => _NouvelleCommandePageState(); +} + +class _NouvelleCommandePageState extends State { + final AppDatabase _appDatabase = AppDatabase.instance; + final _formKey = GlobalKey(); + bool _isLoading = false; + + // Contrôleurs client + final TextEditingController _nomController = TextEditingController(); + final TextEditingController _prenomController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _telephoneController = TextEditingController(); + final TextEditingController _adresseController = TextEditingController(); + + // Contrôleurs pour les filtres + final TextEditingController _searchNameController = TextEditingController(); + final TextEditingController _searchImeiController = TextEditingController(); + final TextEditingController _searchReferenceController = TextEditingController(); + + // Panier + final List _products = []; + final List _filteredProducts = []; + final Map _quantites = {}; + + // Variables de filtre + bool _showOnlyInStock = false; + + // Utilisateurs commerciaux + List _commercialUsers = []; + Users? _selectedCommercialUser; + + // Variables pour les suggestions clients + List _clientSuggestions = []; + bool _showNomSuggestions = false; + bool _showTelephoneSuggestions = false; + GlobalKey _nomFieldKey = GlobalKey(); + GlobalKey _telephoneFieldKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _loadProducts(); + _loadCommercialUsers(); + + // Listeners pour les filtres + _searchNameController.addListener(_filterProducts); + _searchImeiController.addListener(_filterProducts); + _searchReferenceController.addListener(_filterProducts); + + // Listeners pour l'autocomplétion client + _nomController.addListener(() { + if (_nomController.text.length >= 3) { + _showClientSuggestions(_nomController.text, isNom: true); + } else { + _hideNomSuggestions(); + } + }); + + _telephoneController.addListener(() { + if (_telephoneController.text.length >= 3) { + _showClientSuggestions(_telephoneController.text, isNom: false); + } else { + _hideTelephoneSuggestions(); + } + }); + } + + // Méthode pour vider complètement le formulaire et le panier + void _clearFormAndCart() { + setState(() { + // Vider les contrôleurs client + _nomController.clear(); + _prenomController.clear(); + _emailController.clear(); + _telephoneController.clear(); + _adresseController.clear(); + + // Vider le panier + _quantites.clear(); + + // Réinitialiser le commercial au premier de la liste + if (_commercialUsers.isNotEmpty) { + _selectedCommercialUser = _commercialUsers.first; + } + + // Masquer toutes les suggestions + _hideAllSuggestions(); + + // Réinitialiser l'état de chargement + _isLoading = false; + }); + } + + Future _showClientSuggestions(String query, {required bool isNom}) async { + if (query.length < 3) { + _hideAllSuggestions(); + return; + } + + final suggestions = await _appDatabase.suggestClients(query); + + setState(() { + _clientSuggestions = suggestions; + if (isNom) { + _showNomSuggestions = true; + _showTelephoneSuggestions = false; + } else { + _showTelephoneSuggestions = true; + _showNomSuggestions = false; + } + }); +} + + void _showOverlay({required bool isNom}) { + // Utiliser une approche plus simple avec setState + setState(() { + _clientSuggestions = _clientSuggestions; + if (isNom) { + _showNomSuggestions = true; + _showTelephoneSuggestions = false; + } else { + _showTelephoneSuggestions = true; + _showNomSuggestions = false; + } + }); + } + + void _fillClientForm(Client client) { + setState(() { + _nomController.text = client.nom; + _prenomController.text = client.prenom; + _emailController.text = client.email; + _telephoneController.text = client.telephone; + _adresseController.text = client.adresse ?? ''; + }); + + Get.snackbar( + 'Client trouvé', + 'Les informations ont été remplies automatiquement', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } + + void _hideNomSuggestions() { + if (mounted && _showNomSuggestions) { + setState(() { + _showNomSuggestions = false; + }); + } + } + + void _hideTelephoneSuggestions() { + if (mounted && _showTelephoneSuggestions){ + setState(() { + _showTelephoneSuggestions = false; + }); + } + } + + void _hideAllSuggestions() { + _hideNomSuggestions(); + _hideTelephoneSuggestions(); + } + + Future _loadProducts() async { + final products = await _appDatabase.getProducts(); + setState(() { + _products.clear(); + _products.addAll(products); + _filteredProducts.clear(); + _filteredProducts.addAll(products); + }); + } + + Future _loadCommercialUsers() async { + final commercialUsers = await _appDatabase.getCommercialUsers(); + setState(() { + _commercialUsers = commercialUsers; + if (_commercialUsers.isNotEmpty) { + _selectedCommercialUser = _commercialUsers.first; + } + }); + } + + void _filterProducts() { + final nameQuery = _searchNameController.text.toLowerCase(); + final imeiQuery = _searchImeiController.text.toLowerCase(); + final referenceQuery = _searchReferenceController.text.toLowerCase(); + + setState(() { + _filteredProducts.clear(); + + for (var product in _products) { + bool matchesName = nameQuery.isEmpty || + product.name.toLowerCase().contains(nameQuery); + + bool matchesImei = imeiQuery.isEmpty || + (product.imei?.toLowerCase().contains(imeiQuery) ?? false); + + bool matchesReference = referenceQuery.isEmpty || + (product.reference?.toLowerCase().contains(referenceQuery) ?? false); + + bool matchesStock = !_showOnlyInStock || + (product.stock != null && product.stock! > 0); + + if (matchesName && matchesImei && matchesReference && matchesStock) { + _filteredProducts.add(product); + } + } + }); + } + + void _toggleStockFilter() { + setState(() { + _showOnlyInStock = !_showOnlyInStock; + }); + _filterProducts(); + } + + void _clearFilters() { + setState(() { + _searchNameController.clear(); + _searchImeiController.clear(); + _searchReferenceController.clear(); + _showOnlyInStock = false; + }); + _filterProducts(); + } + + // Section des filtres adaptée pour mobile + Widget _buildFilterSection() { + final isMobile = MediaQuery.of(context).size.width < 600; + + return Card( + elevation: 2, + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.filter_list, color: Colors.blue.shade700), + const SizedBox(width: 8), + const Text( + 'Filtres de recherche', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 9, 56, 95), + ), + ), + const Spacer(), + TextButton.icon( + onPressed: _clearFilters, + icon: const Icon(Icons.clear, size: 18), + label: isMobile ? const SizedBox() : const Text('Réinitialiser'), + style: TextButton.styleFrom( + foregroundColor: Colors.grey.shade600, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Champ de recherche par nom + TextField( + controller: _searchNameController, + decoration: InputDecoration( + labelText: 'Rechercher par nom', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + const SizedBox(height: 12), + + if (!isMobile) ...[ + // Version desktop - champs sur la même ligne + Row( + children: [ + Expanded( + child: TextField( + controller: _searchImeiController, + decoration: InputDecoration( + labelText: 'IMEI', + prefixIcon: const Icon(Icons.phone_android), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _searchReferenceController, + decoration: InputDecoration( + labelText: 'Référence', + prefixIcon: const Icon(Icons.qr_code), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ), + ], + ), + ] else ...[ + // Version mobile - champs empilés + TextField( + controller: _searchImeiController, + decoration: InputDecoration( + labelText: 'IMEI', + prefixIcon: const Icon(Icons.phone_android), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + const SizedBox(height: 12), + TextField( + controller: _searchReferenceController, + decoration: InputDecoration( + labelText: 'Référence', + prefixIcon: const Icon(Icons.qr_code), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ], + const SizedBox(height: 16), + + // Boutons de filtre adaptés pour mobile + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ElevatedButton.icon( + onPressed: _toggleStockFilter, + icon: Icon( + _showOnlyInStock ? Icons.inventory : Icons.inventory_2, + size: 20, + ), + label: Text(_showOnlyInStock + ? isMobile ? 'Tous' : 'Afficher tous' + : isMobile ? 'En stock' : 'Stock disponible'), + style: ElevatedButton.styleFrom( + backgroundColor: _showOnlyInStock + ? Colors.green.shade600 + : Colors.blue.shade600, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 16, + vertical: 8 + ), + ), + ), + ], + ), + + const SizedBox(height: 8), + + // Compteur de résultats + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8 + ), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_filteredProducts.length} produit(s)', + style: TextStyle( + color: Colors.blue.shade700, + fontWeight: FontWeight.w600, + fontSize: isMobile ? 12 : 14, + ), + ), + ), + ], + ), + ), + ); + } + +// Variables pour le scanner + QRViewController? _qrController; + bool _isScanning = false; + final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR'); + + // 4. Méthode pour démarrer le scan + void _startBarcodeScanning() { + if (_isScanning) return; + + setState(() { + _isScanning = true; + }); + + Get.to(() => _buildScannerPage())?.then((_) { + setState(() { + _isScanning = false; + }); + }); + } + + // 5. Page du scanner + Widget _buildScannerPage() { + return Scaffold( + appBar: AppBar( + title: const Text('Scanner IMEI'), + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _qrController?.dispose(); + Get.back(); + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.flash_on), + onPressed: () async { + await _qrController?.toggleFlash(); + }, + ), + IconButton( + icon: const Icon(Icons.flip_camera_ios), + onPressed: () async { + await _qrController?.flipCamera(); + }, + ), + ], + ), + body: Stack( + children: [ + // Scanner view + QRView( + key: _qrKey, + onQRViewCreated: _onQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: Colors.green, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: 250, + ), + ), + + // Instructions overlay + Positioned( + bottom: 100, + left: 20, + right: 20, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(12), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.qr_code_scanner, color: Colors.white, size: 40), + SizedBox(height: 8), + Text( + 'Pointez la caméra vers le code-barres IMEI', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + Text( + 'Le scan se fait automatiquement', + style: TextStyle( + color: Colors.white70, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ), + ); + } + + // 6. Configuration du contrôleur QR + void _onQRViewCreated(QRViewController controller) { + _qrController = controller; + + controller.scannedDataStream.listen((scanData) { + if (scanData.code != null && scanData.code!.isNotEmpty) { + // Pauser le scanner pour éviter les scans multiples + controller.pauseCamera(); + + // Fermer la page du scanner + Get.back(); + + // Traiter le résultat + _findAndAddProductByImei(scanData.code!); + } + }); + } + + // 7. Méthode pour trouver et ajouter un produit par IMEI + Future _findAndAddProductByImei(String scannedImei) async { + try { + // Montrer un indicateur de chargement + Get.dialog( + AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: Colors.green.shade700), + const SizedBox(height: 16), + const Text('Recherche du produit...'), + const SizedBox(height: 8), + Text( + 'IMEI: $scannedImei', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + barrierDismissible: false, + ); + + // Attendre un court instant pour l'effet visuel + await Future.delayed(const Duration(milliseconds: 300)); + + // Chercher le produit avec l'IMEI scanné + Product? foundProduct; + + for (var product in _products) { + if (product.imei?.toLowerCase().trim() == scannedImei.toLowerCase().trim()) { + foundProduct = product; + break; + } + } + + // Fermer l'indicateur de chargement + Get.back(); + + if (foundProduct == null) { + _showProductNotFoundDialog(scannedImei); + return; + } + + // Vérifier le stock + if (foundProduct.stock != null && foundProduct.stock! <= 0) { + Get.snackbar( + 'Stock insuffisant', + 'Le produit "${foundProduct.name}" n\'est plus en stock', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + icon: const Icon(Icons.warning_amber, color: Colors.white), + ); + return; + } + + // Vérifier si le produit peut être ajouté (stock disponible) + final currentQuantity = _quantites[foundProduct.id] ?? 0; + if (foundProduct.stock != null && currentQuantity >= foundProduct.stock!) { + Get.snackbar( + 'Stock limite atteint', + 'Quantité maximum atteinte pour "${foundProduct.name}"', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + icon: const Icon(Icons.warning_amber, color: Colors.white), + ); + return; + } + + // Ajouter le produit au panier + setState(() { + _quantites[foundProduct!.id!] = currentQuantity + 1; + }); + + // Afficher le dialogue de succès + _showSuccessDialog(foundProduct, currentQuantity + 1); + + } catch (e) { + // Fermer l'indicateur de chargement si il est encore ouvert + if (Get.isDialogOpen!) Get.back(); + + Get.snackbar( + 'Erreur', + 'Une erreur est survenue: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + } + + // 8. Dialogue de succès + void _showSuccessDialog(Product product, int newQuantity) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.check_circle, color: Colors.green.shade700), + ), + const SizedBox(width: 12), + const Expanded(child: Text('Produit ajouté !')), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text('Prix: ${product.price.toStringAsFixed(2)} MGA'), + Text('Quantité dans le panier: $newQuantity'), + if (product.stock != null) + Text('Stock restant: ${product.stock! - newQuantity}'), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Continuer'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _showCartBottomSheet(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Voir le panier'), + ), + ], + ), + ); + } + + // 9. Dialogue produit non trouvé + void _showProductNotFoundDialog(String scannedImei) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Icon(Icons.search_off, color: Colors.red.shade600), + const SizedBox(width: 8), + const Text('Produit non trouvé'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Aucun produit trouvé avec cet IMEI:'), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + scannedImei, + style: const TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 12), + Text( + 'Vérifiez que l\'IMEI est correct ou que le produit existe dans la base de données.', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _startBarcodeScanning(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Scanner à nouveau'), + ), + ], + ), + ); + } + + + Widget _buildScanInfoCard() { + return Card( + elevation: 2, + margin: const EdgeInsets.only(bottom: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.qr_code_scanner, + color: Colors.green.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Scanner rapidement un produit via son IMEI pour l\'ajouter au panier', + style: TextStyle( + fontSize: 14, + color: Color.fromARGB(255, 9, 56, 95), + ), + ), + ), + ElevatedButton.icon( + onPressed: _isScanning ? null : _startBarcodeScanning, + icon: _isScanning + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.qr_code_scanner, size: 18), + label: Text(_isScanning ? 'Scan...' : 'Scanner'), + style: ElevatedButton.styleFrom( + backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + ), + ], + ), + ), + ); + } + + // 10. Modifier le Widget build pour ajouter le bouton de scan + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < 600; + + return Scaffold( + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Bouton de scan + FloatingActionButton( + heroTag: "scan", + onPressed: _isScanning ? null : _startBarcodeScanning, + backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, + foregroundColor: Colors.white, + child: _isScanning + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.qr_code_scanner), + ), + const SizedBox(height: 10), + // Bouton panier existant + _buildFloatingCartButton(), + ], + ), + appBar: CustomAppBar(title: 'Nouvelle commande'), + drawer: CustomDrawer(), + body: GestureDetector( + onTap: _hideAllSuggestions, + child: Column( + children: [ + // Section d'information sur le scan (desktop) + if (!isMobile) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _buildScanInfoCard(), + ), + + // Section des filtres + if (!isMobile) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _buildFilterSection(), + ), + + // Boutons pour mobile + if (isMobile) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + Expanded( + flex: 2, + child: ElevatedButton.icon( + icon: const Icon(Icons.filter_alt), + label: const Text('Filtres'), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: _buildFilterSection(), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 1, + child: ElevatedButton.icon( + icon: _isScanning + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.qr_code_scanner), + label: Text(_isScanning ? 'Scan...' : 'Scan'), + onPressed: _isScanning ? null : _startBarcodeScanning, + style: ElevatedButton.styleFrom( + backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], + ), + ), + // Compteur de résultats + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_filteredProducts.length} produit(s)', + style: TextStyle( + color: Colors.blue.shade700, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + + // Liste des produits + Expanded( + child: _buildProductList(), + ), + ], + ), + ), + ); + } + + + Widget _buildSuggestionsList({required bool isNom}) { + if (_clientSuggestions.isEmpty) return const SizedBox(); + + return Container( + margin: const EdgeInsets.only(top: 4), + constraints: const BoxConstraints(maxHeight: 150), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: _clientSuggestions.length, + itemBuilder: (context, index) { + final client = _clientSuggestions[index]; + return ListTile( + dense: true, + leading: CircleAvatar( + radius: 16, + backgroundColor: Colors.blue.shade100, + child: Icon( + Icons.person, + size: 16, + color: Colors.blue.shade700, + ), + ), + title: Text( + '${client.nom} ${client.prenom}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + '${client.telephone} • ${client.email}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + onTap: () { + _fillClientForm(client); + _hideAllSuggestions(); + }, + ); + }, + ), + ); +} + + Widget _buildFloatingCartButton() { + final isMobile = MediaQuery.of(context).size.width < 600; + final cartItemCount = _quantites.values.where((q) => q > 0).length; + + return FloatingActionButton.extended( + onPressed: () { + _showCartBottomSheet(); + }, + icon: const Icon(Icons.shopping_cart), + label: Text( + isMobile ? 'Panier ($cartItemCount)' : 'Panier ($cartItemCount)', + style: TextStyle(fontSize: isMobile ? 12 : 14), + ), + backgroundColor: Colors.blue.shade800, + foregroundColor: Colors.white, + ); + } + + void _showClientFormDialog() { + final isMobile = MediaQuery.of(context).size.width < 600; + + // Variables locales pour les suggestions dans le dialog + bool showNomSuggestions = false; + bool showPrenomSuggestions = false; + bool showEmailSuggestions = false; + bool showTelephoneSuggestions = false; + List localClientSuggestions = []; + + // GlobalKeys pour positionner les overlays + final GlobalKey nomFieldKey = GlobalKey(); + final GlobalKey prenomFieldKey = GlobalKey(); + final GlobalKey emailFieldKey = GlobalKey(); + final GlobalKey telephoneFieldKey = GlobalKey(); + + Get.dialog( + StatefulBuilder( + builder: (context, setDialogState) { + return Stack( + children: [ + AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.person_add, color: Colors.blue.shade700), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + isMobile ? 'Client' : 'Informations Client', + style: TextStyle(fontSize: isMobile ? 16 : 18), + ), + ), + ], + ), + content: Container( + width: isMobile ? double.maxFinite : 600, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Champ Nom avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: nomFieldKey, + controller: _nomController, + label: 'Nom', + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un nom' : null, + onChanged: (value) async { + if (value.length >= 2) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showNomSuggestions = suggestions.isNotEmpty; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showNomSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Prénom avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: prenomFieldKey, + controller: _prenomController, + label: 'Prénom', + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un prénom' : null, + onChanged: (value) async { + if (value.length >= 2) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showPrenomSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showPrenomSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Email avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: emailFieldKey, + controller: _emailController, + label: 'Email', + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value?.isEmpty ?? true) return 'Veuillez entrer un email'; + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) { + return 'Email invalide'; + } + return null; + }, + onChanged: (value) async { + if (value.length >= 3) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showEmailSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showPrenomSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showEmailSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Téléphone avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: telephoneFieldKey, + controller: _telephoneController, + label: 'Téléphone', + keyboardType: TextInputType.phone, + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un téléphone' : null, + onChanged: (value) async { + if (value.length >= 3) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showTelephoneSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + }); + } else { + setDialogState(() { + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + _buildTextFormField( + controller: _adresseController, + label: 'Adresse', + maxLines: 2, + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer une adresse' : null, + ), + const SizedBox(height: 12), + _buildCommercialDropdown(), + ], + ), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade800, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 16 : 20, + vertical: isMobile ? 10 : 12 + ), + ), + onPressed: () { + if (_formKey.currentState!.validate()) { + // Fermer toutes les suggestions avant de soumettre + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + Get.back(); + _submitOrder(); + } + }, + child: Text( + isMobile ? 'Valider' : 'Valider la commande', + style: TextStyle(fontSize: isMobile ? 12 : 14), + ), + ), + ], + ), + + // Overlay pour les suggestions du nom + if (showNomSuggestions) + _buildSuggestionOverlay( + fieldKey: nomFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showNomSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions du prénom + if (showPrenomSuggestions) + _buildSuggestionOverlay( + fieldKey: prenomFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showPrenomSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions de l'email + if (showEmailSuggestions) + _buildSuggestionOverlay( + fieldKey: emailFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showEmailSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions du téléphone + if (showTelephoneSuggestions) + _buildSuggestionOverlay( + fieldKey: telephoneFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + ], + ); + }, + ), + ); +} + +// Widget pour créer un TextFormField avec une clé +Widget _buildTextFormFieldWithKey({ + required GlobalKey key, + required TextEditingController controller, + required String label, + TextInputType? keyboardType, + int maxLines = 1, + String? Function(String?)? validator, + void Function(String)? onChanged, +}) { + return Container( + key: key, + child: _buildTextFormField( + controller: controller, + label: label, + keyboardType: keyboardType, + maxLines: maxLines, + validator: validator, + onChanged: onChanged, + ), + ); +} + +// Widget pour l'overlay des suggestions +Widget _buildSuggestionOverlay({ + required GlobalKey fieldKey, + required List suggestions, + required Function(Client) onClientSelected, + required VoidCallback onDismiss, +}) { + return Positioned.fill( + child: GestureDetector( + onTap: onDismiss, + child: Material( + color: Colors.transparent, + child: Builder( + builder: (context) { + // Obtenir la position du champ + final RenderBox? renderBox = fieldKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox == null) return const SizedBox(); + + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + + return Stack( + children: [ + Positioned( + left: position.dx, + top: position.dy + size.height + 4, + width: size.width, + child: GestureDetector( + onTap: () {}, // Empêcher la fermeture au tap sur la liste + child: Container( + constraints: const BoxConstraints( + maxHeight: 200, // Hauteur maximum pour la scrollabilité + ), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Scrollbar( + thumbVisibility: suggestions.length > 3, + child: ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: suggestions.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: Colors.grey.shade200, + ), + itemBuilder: (context, index) { + final client = suggestions[index]; + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + leading: CircleAvatar( + radius: 16, + backgroundColor: Colors.blue.shade100, + child: Icon( + Icons.person, + size: 16, + color: Colors.blue.shade700, + ), + ), + title: Text( + '${client.nom} ${client.prenom}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + '${client.telephone} • ${client.email}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + onTap: () => onClientSelected(client), + hoverColor: Colors.blue.shade50, + ); + }, + ), + ), + ), + ), + ), + ), + ], + ); + }, + ), + ), + ), + ); +} + +// Méthode pour remplir le formulaire avec les données du client +void _fillFormWithClient(Client client) { + _nomController.text = client.nom; + _prenomController.text = client.prenom; + _emailController.text = client.email; + _telephoneController.text = client.telephone; + _adresseController.text = client.adresse ?? ''; + + Get.snackbar( + 'Client trouvé', + 'Les informations ont été remplies automatiquement', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); +} + + Widget _buildTextFormField({ + required TextEditingController controller, + required String label, + TextInputType? keyboardType, + String? Function(String?)? validator, + int? maxLines, + void Function(String)? onChanged, + }) { + return TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: label, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white, + ), + keyboardType: keyboardType, + validator: validator, + maxLines: maxLines, + onChanged: onChanged, + ); + } + + Widget _buildCommercialDropdown() { + return DropdownButtonFormField( + value: _selectedCommercialUser, + decoration: InputDecoration( + labelText: 'Commercial', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white, + ), + items: _commercialUsers.map((Users user) { + return DropdownMenuItem( + value: user, + child: Text('${user.name} ${user.lastName}'), + ); + }).toList(), + onChanged: (Users? newValue) { + setState(() { + _selectedCommercialUser = newValue; + }); + }, + validator: (value) => value == null ? 'Veuillez sélectionner un commercial' : null, + ); + } + + Widget _buildProductList() { + final isMobile = MediaQuery.of(context).size.width < 600; + + return _filteredProducts.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: _filteredProducts.length, + itemBuilder: (context, index) { + final product = _filteredProducts[index]; + final quantity = _quantites[product.id] ?? 0; + + return _buildProductListItem(product, quantity, isMobile); + }, + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + children: [ + Icon( + Icons.search_off, + size: 64, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Aucun produit trouvé', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 8), + Text( + 'Modifiez vos critères de recherche', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + ), + ], + ), + ), + ); + } + + Widget _buildProductListItem(Product product, int quantity, bool isMobile) { + final bool isOutOfStock = product.stock != null && product.stock! <= 0; + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: isOutOfStock + ? Border.all(color: Colors.red.shade200, width: 1.5) + : null, + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Container( + width: isMobile ? 40 : 50, + height: isMobile ? 40 : 50, + decoration: BoxDecoration( + color: isOutOfStock + ? Colors.red.shade50 + : Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.shopping_bag, + size: isMobile ? 20 : 24, + color: isOutOfStock ? Colors.red : Colors.blue, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isMobile ? 14 : 16, + color: isOutOfStock ? Colors.red.shade700 : null, + ), + ), + const SizedBox(height: 4), + Text( + '${product.price.toStringAsFixed(2)} MGA', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.w600, + fontSize: isMobile ? 12 : 14, + ), + ), + if (product.stock != null) + Text( + 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}', + style: TextStyle( + fontSize: isMobile ? 10 : 12, + color: isOutOfStock + ? Colors.red.shade600 + : Colors.grey.shade600, + fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal, + ), + ), + // Affichage IMEI et Référence - plus compact sur mobile + if (product.imei != null && product.imei!.isNotEmpty) + Text( + 'IMEI: ${product.imei}', + style: TextStyle( + fontSize: isMobile ? 9 : 11, + color: Colors.grey.shade600, + fontFamily: 'monospace', + ), + ), + if (product.reference != null && product.reference!.isNotEmpty) + Text( + 'Réf: ${product.reference}', + style: TextStyle( + fontSize: isMobile ? 9 : 11, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Container( + decoration: BoxDecoration( + color: isOutOfStock + ? Colors.grey.shade100 + : Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + Icons.remove, + size: isMobile ? 16 : 18 + ), + onPressed: isOutOfStock ? null : () { + if (quantity > 0) { + setState(() { + _quantites[product.id!] = quantity - 1; + }); + } + }, + ), + Text( + quantity.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isMobile ? 12 : 14, + ), + ), + IconButton( + icon: Icon( + Icons.add, + size: isMobile ? 16 : 18 + ), + onPressed: isOutOfStock ? null : () { + 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, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + }, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + void _showCartBottomSheet() { + final isMobile = MediaQuery.of(context).size.width < 600; + + Get.bottomSheet( + Container( + height: MediaQuery.of(context).size.height * (isMobile ? 0.85 : 0.7), + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Votre Panier', + style: TextStyle( + fontSize: isMobile ? 18 : 20, + fontWeight: FontWeight.bold + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Get.back(), + ), + ], + ), + const Divider(), + Expanded(child: _buildCartItemsList()), + const Divider(), + _buildCartTotalSection(), + const SizedBox(height: 16), + _buildSubmitButton(), + ], + ), + ), + isScrollControlled: true, + ); + } + + Widget _buildCartItemsList() { + final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); + + if (itemsInCart.isEmpty) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.shopping_cart_outlined, size: 60, color: Colors.grey), + SizedBox(height: 16), + Text( + 'Votre panier est vide', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ], + ), + ); + } + + return ListView.builder( + itemCount: itemsInCart.length, + itemBuilder: (context, index) { + final entry = itemsInCart[index]; + final product = _products.firstWhere((p) => p.id == entry.key); + + return Dismissible( + key: Key(entry.key.toString()), + background: Container( + color: Colors.red.shade100, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20), + child: const Icon(Icons.delete, color: Colors.red), + ), + direction: DismissDirection.endToStart, + onDismissed: (direction) { + setState(() { + _quantites.remove(entry.key); + }); + Get.snackbar( + 'Produit retiré', + '${product.name} a été retiré du panier', + snackPosition: SnackPosition.BOTTOM, + ); + }, + child: 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)} MGA'), + trailing: Text( + '${(entry.value * product.price).toStringAsFixed(2)} MGA', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildCartTotalSection() { + double total = 0; + _quantites.forEach((productId, quantity) { + final product = _products.firstWhere((p) => p.id == productId); + total += quantity * product.price; + }); + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Text( + '${total.toStringAsFixed(2)} MGA', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + '${_quantites.values.where((q) => q > 0).length} article(s)', + style: TextStyle(color: Colors.grey.shade600), + ), + ], + ); + } + + Widget _buildSubmitButton() { + final isMobile = MediaQuery.of(context).size.width < 600; + + return SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric( + vertical: isMobile ? 12 : 16 + ), + backgroundColor: Colors.blue.shade800, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + ), + onPressed: _submitOrder, + child: _isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Text( + isMobile ? 'Valider' : 'Valider la Commande', + style: TextStyle(fontSize: isMobile ? 14 : 16), + ), + ), + ); + } + + Future _submitOrder() async { + // Vérifier d'abord si le panier est vide + 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, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + _showCartBottomSheet(); // Ouvrir le panier pour montrer qu'il est vide + return; + } + + // Ensuite vérifier les informations client + if (_nomController.text.isEmpty || + _prenomController.text.isEmpty || + _emailController.text.isEmpty || + _telephoneController.text.isEmpty || + _adresseController.text.isEmpty) { + Get.snackbar( + 'Informations manquantes', + 'Veuillez remplir les informations client', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + _showClientFormDialog(); + return; + } + + setState(() { + _isLoading = true; + }); + + // 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, + produitId: product.id!, + quantite: entry.value, + prixUnitaire: product.price, + sousTotal: entry.value * product.price, + )); + } + + // Créer la commande + final commande = Commande( + clientId: 0, + dateCommande: DateTime.now(), + statut: StatutCommande.enAttente, + montantTotal: total, + notes: 'Commande passée via l\'application', + commandeurId: _selectedCommercialUser?.id, + ); + + try { + await _appDatabase.createCommandeComplete(client, commande, details); + + // Fermer le panier avant d'afficher la confirmation + Get.back(); + + // Afficher le dialogue de confirmation - adapté pour mobile + final isMobile = MediaQuery.of(context).size.width < 600; + + await showDialog( + context: context, + barrierDismissible: false, // Empêcher la fermeture accidentelle + builder: (context) => AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.check_circle, color: Colors.green.shade700), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Commande Validée', + style: TextStyle(fontSize: isMobile ? 16 : 18), + ), + ), + ], + ), + content: Text( + 'Votre commande a été enregistrée et expédiée avec succès.', + style: TextStyle(fontSize: isMobile ? 14 : 16), + ), + actions: [ + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + vertical: isMobile ? 12 : 16 + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + Navigator.pop(context); + // Vider complètement le formulaire et le panier + _clearFormAndCart(); + // Recharger les produits pour mettre à jour le stock + _loadProducts(); + }, + child: Text( + 'OK', + style: TextStyle(fontSize: isMobile ? 14 : 16), + ), + ), + ), + ], + ), + ); + + } catch (e) { + setState(() { + _isLoading = false; + }); + + Get.snackbar( + 'Erreur', + 'Une erreur est survenue: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + @override + void dispose() { + _qrController?.dispose(); + + // Vos disposals existants... + _hideAllSuggestions(); + _nomController.dispose(); + _prenomController.dispose(); + _emailController.dispose(); + _telephoneController.dispose(); + _adresseController.dispose(); + _searchNameController.dispose(); + _searchImeiController.dispose(); + _searchReferenceController.dispose(); + + super.dispose(); + } + } \ No newline at end of file diff --git a/lib/Models/Client.dart b/lib/Models/Client.dart index 8bad146..728d162 100644 --- a/lib/Models/Client.dart +++ b/lib/Models/Client.dart @@ -214,6 +214,8 @@ class Commande { } } +// REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci : + class DetailCommande { final int? id; final int commandeId; @@ -225,6 +227,11 @@ class DetailCommande { final String? produitImage; final String? produitReference; final bool? estCadeau; + + // NOUVEAUX CHAMPS POUR LA REMISE PAR PRODUIT + final double? remisePourcentage; + final double? remiseMontant; + final double? prixApresRemise; DetailCommande({ this.id, @@ -237,6 +244,9 @@ class DetailCommande { this.produitImage, this.produitReference, this.estCadeau, + this.remisePourcentage, + this.remiseMontant, + this.prixApresRemise, }); Map toMap() { @@ -248,6 +258,9 @@ class DetailCommande { 'prixUnitaire': prixUnitaire, 'sousTotal': sousTotal, 'estCadeau': estCadeau == true ? 1 : 0, + 'remisePourcentage': remisePourcentage, + 'remiseMontant': remiseMontant, + 'prixApresRemise': prixApresRemise, }; } @@ -263,6 +276,15 @@ class DetailCommande { produitImage: map['produitImage'] as String?, produitReference: map['produitReference'] as String?, estCadeau: map['estCadeau'] == 1, + remisePourcentage: map['remisePourcentage'] != null + ? (map['remisePourcentage'] as num).toDouble() + : null, + remiseMontant: map['remiseMontant'] != null + ? (map['remiseMontant'] as num).toDouble() + : null, + prixApresRemise: map['prixApresRemise'] != null + ? (map['prixApresRemise'] as num).toDouble() + : null, ); } @@ -277,6 +299,9 @@ class DetailCommande { String? produitImage, String? produitReference, bool? estCadeau, + double? remisePourcentage, + double? remiseMontant, + double? prixApresRemise, }) { return DetailCommande( id: id ?? this.id, @@ -289,6 +314,29 @@ class DetailCommande { produitImage: produitImage ?? this.produitImage, produitReference: produitReference ?? this.produitReference, estCadeau: estCadeau ?? this.estCadeau, + remisePourcentage: remisePourcentage ?? this.remisePourcentage, + remiseMontant: remiseMontant ?? this.remiseMontant, + prixApresRemise: prixApresRemise ?? this.prixApresRemise, ); } + + // GETTERS QUI RÉSOLVENT LE PROBLÈME "aUneRemise" INTROUVABLE + double get prixFinalUnitaire { + return prixApresRemise ?? prixUnitaire; + } + + double get sousTotalAvecRemise { + return quantite * prixFinalUnitaire; + } + + bool get aUneRemise { + return remisePourcentage != null || remiseMontant != null || prixApresRemise != null; + } + + double get montantRemise { + if (prixApresRemise != null) { + return (prixUnitaire - prixApresRemise!) * quantite; + } + return 0.0; + } } \ No newline at end of file diff --git a/lib/Services/PermissionCacheService.dart b/lib/Services/PermissionCacheService.dart new file mode 100644 index 0000000..815c5eb --- /dev/null +++ b/lib/Services/PermissionCacheService.dart @@ -0,0 +1,258 @@ +import 'package:get/get.dart'; +import 'package:youmazgestion/Services/stock_managementDatabase.dart'; + +class PermissionCacheService extends GetxController { + static final PermissionCacheService instance = PermissionCacheService._init(); + PermissionCacheService._init(); + + // Cache en mémoire optimisé + final Map> _permissionCache = {}; + final Map>> _menuCache = {}; + bool _isLoaded = false; + String _currentUsername = ''; + + /// ✅ OPTIMISÉ: Une seule requête complexe pour charger tout + Future loadUserPermissions(String username) async { + if (_isLoaded && _currentUsername == username && _permissionCache.containsKey(username)) { + print("📋 Permissions déjà en cache pour: $username"); + return; + } + + print("🔄 Chargement OPTIMISÉ des permissions pour: $username"); + final stopwatch = Stopwatch()..start(); + + try { + final db = AppDatabase.instance; + + // 🚀 UNE SEULE REQUÊTE pour tout récupérer + final userPermissions = await _getUserPermissionsOptimized(db, username); + + // Organiser les données + Map permissions = {}; + Set> accessibleMenus = {}; + + for (var row in userPermissions) { + final menuId = row['menu_id'] as int; + final menuName = row['menu_name'] as String; + final menuRoute = row['menu_route'] as String; + final permissionName = row['permission_name'] as String; + + // Ajouter la permission + final key = "${permissionName}_$menuRoute"; + permissions[key] = true; + + // Ajouter le menu aux accessibles + accessibleMenus.add({ + 'id': menuId, + 'name': menuName, + 'route': menuRoute, + }); + } + + // Mettre en cache + _permissionCache[username] = permissions; + _menuCache[username] = accessibleMenus.toList(); + _currentUsername = username; + _isLoaded = true; + + stopwatch.stop(); + print("✅ Permissions chargées en ${stopwatch.elapsedMilliseconds}ms"); + print(" - ${permissions.length} permissions"); + print(" - ${accessibleMenus.length} menus accessibles"); + + } catch (e) { + stopwatch.stop(); + print("❌ Erreur après ${stopwatch.elapsedMilliseconds}ms: $e"); + rethrow; + } + } + + /// 🚀 NOUVELLE MÉTHODE: Une seule requête optimisée + Future>> _getUserPermissionsOptimized( + AppDatabase db, String username) async { + + final connection = await db.database; + + final result = await connection.query(''' + SELECT DISTINCT + m.id as menu_id, + m.name as menu_name, + m.route as menu_route, + p.name as permission_name + FROM users u + INNER JOIN roles r ON u.role_id = r.id + INNER JOIN role_menu_permissions rmp ON r.id = rmp.role_id + INNER JOIN menu m ON rmp.menu_id = m.id + INNER JOIN permissions p ON rmp.permission_id = p.id + WHERE u.username = ? + ORDER BY m.name, p.name + ''', [username]); + + return result.map((row) => row.fields).toList(); + } + + /// ✅ Vérification rapide depuis le cache + bool hasPermission(String username, String permissionName, String menuRoute) { + final userPermissions = _permissionCache[username]; + if (userPermissions == null) { + print("⚠️ Cache non initialisé pour: $username"); + return false; + } + + final key = "${permissionName}_$menuRoute"; + return userPermissions[key] ?? false; + } + + /// ✅ Récupération rapide des menus + List> getUserMenus(String username) { + return _menuCache[username] ?? []; + } + + /// ✅ Vérification d'accès menu + bool hasMenuAccess(String username, String menuRoute) { + final userMenus = _menuCache[username] ?? []; + return userMenus.any((menu) => menu['route'] == menuRoute); + } + + /// ✅ Préchargement asynchrone en arrière-plan + Future preloadUserDataAsync(String username) async { + // Lancer en arrière-plan sans bloquer l'UI + unawaited(_preloadInBackground(username)); + } + + Future _preloadInBackground(String username) async { + try { + print("🔄 Préchargement en arrière-plan pour: $username"); + await loadUserPermissions(username); + print("✅ Préchargement terminé"); + } catch (e) { + print("⚠️ Erreur préchargement: $e"); + } + } + + /// ✅ Préchargement synchrone (pour la connexion) + Future preloadUserData(String username) async { + try { + print("🔄 Préchargement synchrone pour: $username"); + await loadUserPermissions(username); + print("✅ Données préchargées avec succès"); + } catch (e) { + print("❌ Erreur lors du préchargement: $e"); + // Ne pas bloquer la connexion + } + } + + /// ✅ Vider le cache + void clearAllCache() { + _permissionCache.clear(); + _menuCache.clear(); + _isLoaded = false; + _currentUsername = ''; + print("🗑️ Cache vidé complètement"); + } + + /// ✅ Rechargement forcé + Future refreshUserPermissions(String username) async { + _permissionCache.remove(username); + _menuCache.remove(username); + _isLoaded = false; + + await loadUserPermissions(username); + print("🔄 Permissions rechargées pour: $username"); + } + + /// ✅ Status du cache + bool get isLoaded => _isLoaded && _currentUsername.isNotEmpty; + String get currentCachedUser => _currentUsername; + + /// ✅ Statistiques + Map getCacheStats() { + return { + 'is_loaded': _isLoaded, + 'current_user': _currentUsername, + 'users_cached': _permissionCache.length, + 'total_permissions': _permissionCache.values + .map((perms) => perms.length) + .fold(0, (a, b) => a + b), + 'total_menus': _menuCache.values + .map((menus) => menus.length) + .fold(0, (a, b) => a + b), + }; + } + + /// ✅ Debug amélioré + void debugPrintCache() { + print("=== DEBUG CACHE OPTIMISÉ ==="); + print("Chargé: $_isLoaded"); + print("Utilisateur actuel: $_currentUsername"); + print("Utilisateurs en cache: ${_permissionCache.keys.toList()}"); + + for (var username in _permissionCache.keys) { + final permissions = _permissionCache[username]!; + final menus = _menuCache[username] ?? []; + print("$username: ${permissions.length} permissions, ${menus.length} menus"); + + // Détail des menus pour debug + for (var menu in menus.take(3)) { + print(" → ${menu['name']} (${menu['route']})"); + } + } + print("============================"); + } + + /// ✅ NOUVEAU: Validation de l'intégrité du cache + Future validateCacheIntegrity(String username) async { + if (!_permissionCache.containsKey(username)) { + return false; + } + + try { + final db = AppDatabase.instance; + final connection = await db.database; + + // Vérification rapide: compter les permissions de l'utilisateur + final result = await connection.query(''' + SELECT COUNT(DISTINCT CONCAT(p.name, '_', m.route)) as permission_count + FROM users u + INNER JOIN roles r ON u.role_id = r.id + INNER JOIN role_menu_permissions rmp ON r.id = rmp.role_id + INNER JOIN menu m ON rmp.menu_id = m.id + INNER JOIN permissions p ON rmp.permission_id = p.id + WHERE u.username = ? + ''', [username]); + + final dbCount = result.first['permission_count'] as int; + final cacheCount = _permissionCache[username]!.length; + + final isValid = dbCount == cacheCount; + if (!isValid) { + print("⚠️ Cache invalide: DB=$dbCount, Cache=$cacheCount"); + } + + return isValid; + } catch (e) { + print("❌ Erreur validation cache: $e"); + return false; + } + } + + /// ✅ NOUVEAU: Rechargement intelligent + Future smartRefresh(String username) async { + final isValid = await validateCacheIntegrity(username); + + if (!isValid) { + print("🔄 Cache invalide, rechargement nécessaire"); + await refreshUserPermissions(username); + } else { + print("✅ Cache valide, pas de rechargement nécessaire"); + } + } +} + +/// ✅ Extension pour éviter l'import de dart:async +void unawaited(Future future) { + // Ignorer le warning sur le Future non attendu + future.catchError((error) { + print("Erreur tâche en arrière-plan: $error"); + }); +} \ No newline at end of file diff --git a/lib/Services/stock_managementDatabase.dart b/lib/Services/stock_managementDatabase.dart index a8d69f2..005bf68 100644 --- a/lib/Services/stock_managementDatabase.dart +++ b/lib/Services/stock_managementDatabase.dart @@ -35,7 +35,7 @@ class AppDatabase { Future initDatabase() async { _connection = await _initDB(); - await _createDB(); + // await _createDB(); // Effectuer la migration pour les bases existantes await migrateDatabaseForDiscountAndGift(); @@ -70,242 +70,203 @@ class AppDatabase { // Méthode mise à jour pour créer les tables avec les nouvelles colonnes Future _createDB() async { - final db = await database; - - try { - // Table roles - await db.query(''' - CREATE TABLE IF NOT EXISTS roles ( - id INT AUTO_INCREMENT PRIMARY KEY, - designation VARCHAR(255) NOT NULL UNIQUE - ) ENGINE=InnoDB - '''); - - // Table permissions - await db.query(''' - CREATE TABLE IF NOT EXISTS permissions ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL UNIQUE - ) ENGINE=InnoDB - '''); - - // Table menu - await db.query(''' - CREATE TABLE IF NOT EXISTS menu ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - route VARCHAR(255) NOT NULL - ) ENGINE=InnoDB - '''); - - // Table role_permissions - await db.query(''' - CREATE TABLE IF NOT EXISTS role_permissions ( - role_id INT, - permission_id INT, - PRIMARY KEY (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE - ) ENGINE=InnoDB - '''); - - // Table role_menu_permissions - await db.query(''' - CREATE TABLE IF NOT EXISTS role_menu_permissions ( - role_id INT, - menu_id INT, - permission_id INT, - PRIMARY KEY (role_id, menu_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, - FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE - ) ENGINE=InnoDB - '''); - - // Table points_de_vente - await db.query(''' - CREATE TABLE IF NOT EXISTS points_de_vente ( - id INT AUTO_INCREMENT PRIMARY KEY, - nom VARCHAR(255) NOT NULL UNIQUE - ) ENGINE=InnoDB - '''); - - // Table users - await db.query(''' - CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - lastname VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - username VARCHAR(255) NOT NULL UNIQUE, - role_id INT NOT NULL, - point_de_vente_id INT, - FOREIGN KEY (role_id) REFERENCES roles(id), - FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id) - ) ENGINE=InnoDB - '''); - - // Table products - await db.query(''' - CREATE TABLE IF NOT EXISTS products ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - price DECIMAL(10,2) NOT NULL, - image VARCHAR(2000), - category VARCHAR(255) NOT NULL, - stock INT NOT NULL DEFAULT 0, - description VARCHAR(1000), - qrCode VARCHAR(500), - reference VARCHAR(255), - point_de_vente_id INT, - marque VARCHAR(255), - ram VARCHAR(100), - memoire_interne VARCHAR(100), - imei VARCHAR(255) UNIQUE, - FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id), - INDEX idx_products_category (category), - INDEX idx_products_reference (reference), - INDEX idx_products_imei (imei) - ) ENGINE=InnoDB - '''); - - // Table clients - await db.query(''' - CREATE TABLE IF NOT EXISTS clients ( - id INT AUTO_INCREMENT PRIMARY KEY, - nom VARCHAR(255) NOT NULL, - prenom VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL UNIQUE, - telephone VARCHAR(255) NOT NULL, - adresse VARCHAR(500), - dateCreation DATETIME NOT NULL, - actif TINYINT(1) NOT NULL DEFAULT 1, - INDEX idx_clients_email (email), - INDEX idx_clients_telephone (telephone) - ) ENGINE=InnoDB - '''); - - // Table commandes MISE À JOUR avec les champs de remise - await db.query(''' - CREATE TABLE IF NOT EXISTS commandes ( - id INT AUTO_INCREMENT PRIMARY KEY, - clientId INT NOT NULL, - dateCommande DATETIME NOT NULL, - statut INT NOT NULL DEFAULT 0, - montantTotal DECIMAL(10,2) NOT NULL, - notes VARCHAR(1000), - dateLivraison DATETIME, - commandeurId INT, - validateurId INT, - remisePourcentage DECIMAL(5,2) NULL, - remiseMontant DECIMAL(10,2) NULL, - montantApresRemise DECIMAL(10,2) NULL, - FOREIGN KEY (commandeurId) REFERENCES users(id), - FOREIGN KEY (validateurId) REFERENCES users(id), - FOREIGN KEY (clientId) REFERENCES clients(id), - INDEX idx_commandes_client (clientId), - INDEX idx_commandes_date (dateCommande) - ) ENGINE=InnoDB - '''); - - // Table details_commandes MISE À JOUR avec le champ cadeau - await db.query(''' - CREATE TABLE IF NOT EXISTS details_commandes ( - id INT AUTO_INCREMENT PRIMARY KEY, - commandeId INT NOT NULL, - produitId INT NOT NULL, - quantite INT NOT NULL, - prixUnitaire DECIMAL(10,2) NOT NULL, - sousTotal DECIMAL(10,2) NOT NULL, - estCadeau TINYINT(1) DEFAULT 0, - FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE, - FOREIGN KEY (produitId) REFERENCES products(id), - INDEX idx_details_commande (commandeId) - ) ENGINE=InnoDB - '''); - - print("Tables créées avec succès avec les nouveaux champs !"); - } catch (e) { - print("Erreur lors de la création des tables: $e"); - rethrow; - } + // final db = await database; + + // try { + // // Table roles + // await db.query(''' + // CREATE TABLE IF NOT EXISTS roles ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // designation VARCHAR(255) NOT NULL UNIQUE + // ) ENGINE=InnoDB + // '''); + + // // Table permissions + // await db.query(''' + // CREATE TABLE IF NOT EXISTS permissions ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // name VARCHAR(255) NOT NULL UNIQUE + // ) ENGINE=InnoDB + // '''); + + // // Table menu + // await db.query(''' + // CREATE TABLE IF NOT EXISTS menu ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // name VARCHAR(255) NOT NULL, + // route VARCHAR(255) NOT NULL + // ) ENGINE=InnoDB + // '''); + + // // Table role_permissions + // await db.query(''' + // CREATE TABLE IF NOT EXISTS role_permissions ( + // role_id INT, + // permission_id INT, + // PRIMARY KEY (role_id, permission_id), + // FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, + // FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE + // ) ENGINE=InnoDB + // '''); + + // // Table role_menu_permissions + // await db.query(''' + // CREATE TABLE IF NOT EXISTS role_menu_permissions ( + // role_id INT, + // menu_id INT, + // permission_id INT, + // PRIMARY KEY (role_id, menu_id, permission_id), + // FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, + // FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE, + // FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE + // ) ENGINE=InnoDB + // '''); + + // // Table points_de_vente + // await db.query(''' + // CREATE TABLE IF NOT EXISTS points_de_vente ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // nom VARCHAR(255) NOT NULL UNIQUE + // ) ENGINE=InnoDB + // '''); + + // // Table users + // await db.query(''' + // CREATE TABLE IF NOT EXISTS users ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // name VARCHAR(255) NOT NULL, + // lastname VARCHAR(255) NOT NULL, + // email VARCHAR(255) NOT NULL UNIQUE, + // password VARCHAR(255) NOT NULL, + // username VARCHAR(255) NOT NULL UNIQUE, + // role_id INT NOT NULL, + // point_de_vente_id INT, + // FOREIGN KEY (role_id) REFERENCES roles(id), + // FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id) + // ) ENGINE=InnoDB + // '''); + + // // Table products + // await db.query(''' + // CREATE TABLE IF NOT EXISTS products ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // name VARCHAR(255) NOT NULL, + // price DECIMAL(10,2) NOT NULL, + // image VARCHAR(2000), + // category VARCHAR(255) NOT NULL, + // stock INT NOT NULL DEFAULT 0, + // description VARCHAR(1000), + // qrCode VARCHAR(500), + // reference VARCHAR(255), + // point_de_vente_id INT, + // marque VARCHAR(255), + // ram VARCHAR(100), + // memoire_interne VARCHAR(100), + // imei VARCHAR(255) UNIQUE, + // FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id), + // INDEX idx_products_category (category), + // INDEX idx_products_reference (reference), + // INDEX idx_products_imei (imei) + // ) ENGINE=InnoDB + // '''); + + // // Table clients + // await db.query(''' + // CREATE TABLE IF NOT EXISTS clients ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // nom VARCHAR(255) NOT NULL, + // prenom VARCHAR(255) NOT NULL, + // email VARCHAR(255) NOT NULL UNIQUE, + // telephone VARCHAR(255) NOT NULL, + // adresse VARCHAR(500), + // dateCreation DATETIME NOT NULL, + // actif TINYINT(1) NOT NULL DEFAULT 1, + // INDEX idx_clients_email (email), + // INDEX idx_clients_telephone (telephone) + // ) ENGINE=InnoDB + // '''); + + // // Table commandes MISE À JOUR avec les champs de remise + // await db.query(''' + // CREATE TABLE IF NOT EXISTS commandes ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // clientId INT NOT NULL, + // dateCommande DATETIME NOT NULL, + // statut INT NOT NULL DEFAULT 0, + // montantTotal DECIMAL(10,2) NOT NULL, + // notes VARCHAR(1000), + // dateLivraison DATETIME, + // commandeurId INT, + // validateurId INT, + // remisePourcentage DECIMAL(5,2) NULL, + // remiseMontant DECIMAL(10,2) NULL, + // montantApresRemise DECIMAL(10,2) NULL, + // FOREIGN KEY (commandeurId) REFERENCES users(id), + // FOREIGN KEY (validateurId) REFERENCES users(id), + // FOREIGN KEY (clientId) REFERENCES clients(id), + // INDEX idx_commandes_client (clientId), + // INDEX idx_commandes_date (dateCommande) + // ) ENGINE=InnoDB + // '''); + + // // Table details_commandes MISE À JOUR avec le champ cadeau + // await db.query(''' + // CREATE TABLE IF NOT EXISTS details_commandes ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // commandeId INT NOT NULL, + // produitId INT NOT NULL, + // quantite INT NOT NULL, + // prixUnitaire DECIMAL(10,2) NOT NULL, + // sousTotal DECIMAL(10,2) NOT NULL, + // estCadeau TINYINT(1) DEFAULT 0, + // FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE, + // FOREIGN KEY (produitId) REFERENCES products(id), + // INDEX idx_details_commande (commandeId) + // ) ENGINE=InnoDB + // '''); + + // print("Tables créées avec succès avec les nouveaux champs !"); + // } catch (e) { + // print("Erreur lors de la création des tables: $e"); + // rethrow; + // } } // --- MÉTHODES D'INSERTION PAR DÉFAUT --- + // Future insertDefaultPermissions() async { - final db = await database; - - try { - final existing = await db.query('SELECT COUNT(*) as count FROM permissions'); - final count = existing.first['count'] as int; - - if (count == 0) { - final permissions = ['view', 'create', 'update', 'delete', 'admin', 'manage', 'read']; - - for (String permission in permissions) { - await db.query('INSERT INTO permissions (name) VALUES (?)', [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( - 'SELECT COUNT(*) as count FROM permissions WHERE name = ?', - [permission] - ); - final permCount = existingPermission.first['count'] as int; - if (permCount == 0) { - await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]); - print("Permission ajoutée: $permission"); - } - } + final db = await database; + + try { + // Vérifier et ajouter uniquement les nouvelles permissions si elles n'existent pas + final newPermissions = ['manage', 'read']; + for (var permission in newPermissions) { + final existingPermission = await db.query( + 'SELECT COUNT(*) as count FROM permissions WHERE name = ?', + [permission] + ); + final permCount = existingPermission.first['count'] as int; + if (permCount == 0) { + await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]); + print("Permission ajoutée: $permission"); } - } catch (e) { - print("Erreur insertDefaultPermissions: $e"); } + } catch (e) { + print("Erreur insertDefaultPermissions: $e"); } +} + // Future insertDefaultMenus() async { - final db = await database; - - try { - final existingMenus = await db.query('SELECT COUNT(*) as count FROM menu'); - final count = existingMenus.first['count'] as int; - - if (count == 0) { - final menus = [ - {'name': 'Accueil', 'route': '/accueil'}, - {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'}, - {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'}, - {'name': 'Ajouter un produit', 'route': '/ajouter-produit'}, - {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'}, - {'name': 'Bilan', 'route': '/bilan'}, - {'name': 'Gérer les rôles', 'route': '/gerer-roles'}, - {'name': 'Gestion de stock', 'route': '/gestion-stock'}, - {'name': 'Historique', 'route': '/historique'}, - {'name': 'Déconnexion', 'route': '/deconnexion'}, - {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, - {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, - {'name': 'Points de vente', 'route': '/points-de-vente'}, - ]; - - for (var menu in menus) { - await db.query( - 'INSERT INTO menu (name, route) VALUES (?, ?)', - [menu['name'], menu['route']] - ); - } - print("Menus par défaut insérés"); - } else { - await _addMissingMenus(db); - } - } catch (e) { - print("Erreur insertDefaultMenus: $e"); - } + final db = await database; + + try { + await _addMissingMenus(db); // Seulement ajouter les menus manquants + } catch (e) { + print("Erreur insertDefaultMenus: $e"); } +} Future insertDefaultRoles() async { final db = await database; @@ -741,29 +702,53 @@ Future _createDB() async { // --- MÉTHODES UTILITAIRES --- + // Future _addMissingMenus(MySqlConnection db) async { + // final menusToAdd = [ + // {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, + // {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, + // {'name': 'Points de vente', 'route': '/points-de-vente'}, + // ]; + + // for (var menu in menusToAdd) { + // final existing = await db.query( + // 'SELECT COUNT(*) as count FROM menu WHERE route = ?', + // [menu['route']] + // ); + // final count = existing.first['count'] as int; + + // if (count == 0) { + // await db.query( + // 'INSERT INTO menu (name, route) VALUES (?, ?)', + // [menu['name'], menu['route']] + // ); + // print("Menu ajouté: ${menu['name']}"); + // } + // } + // } + Future _addMissingMenus(MySqlConnection db) async { - final menusToAdd = [ - {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, - {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, - {'name': 'Points de vente', 'route': '/points-de-vente'}, - ]; - - for (var menu in menusToAdd) { - final existing = await db.query( - 'SELECT COUNT(*) as count FROM menu WHERE route = ?', - [menu['route']] + final menusToAdd = [ + {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, + {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, + {'name': 'Points de vente', 'route': '/points-de-vente'}, + ]; + + for (var menu in menusToAdd) { + final existing = await db.query( + 'SELECT COUNT(*) as count FROM menu WHERE route = ?', + [menu['route']] + ); + final count = existing.first['count'] as int; + + if (count == 0) { + await db.query( + 'INSERT INTO menu (name, route) VALUES (?, ?)', + [menu['name'], menu['route']] ); - final count = existing.first['count'] as int; - - if (count == 0) { - await db.query( - 'INSERT INTO menu (name, route) VALUES (?, ?)', - [menu['name'], menu['route']] - ); - print("Menu ajouté: ${menu['name']}"); - } + print("Menu ajouté: ${menu['name']}"); } } +} Future _updateExistingRolePermissions(MySqlConnection db) async { final superAdminRole = await db.query('SELECT id FROM roles WHERE designation = ?', ['Super Admin']); diff --git a/lib/Views/HandleProduct.dart b/lib/Views/HandleProduct.dart index 724947d..8579e6a 100644 --- a/lib/Views/HandleProduct.dart +++ b/lib/Views/HandleProduct.dart @@ -7,15 +7,15 @@ import 'package:file_picker/file_picker.dart'; import 'package:open_file/open_file.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:excel/excel.dart' hide Border; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; +import 'package:youmazgestion/controller/userController.dart'; import '../Components/appDrawer.dart'; import '../Components/app_bar.dart'; import '../Models/produit.dart'; -//import '../Services/productDatabase.dart'; - class ProductManagementPage extends StatefulWidget { const ProductManagementPage({super.key}); @@ -26,19 +26,33 @@ class ProductManagementPage extends StatefulWidget { class _ProductManagementPageState extends State { final AppDatabase _productDatabase = AppDatabase.instance; + final UserController _userController = Get.find(); + List _products = []; List _filteredProducts = []; final TextEditingController _searchController = TextEditingController(); String _selectedCategory = 'Tous'; List _categories = ['Tous']; bool _isLoading = true; -List> _pointsDeVente = []; + List> _pointsDeVente = []; String? _selectedPointDeVente; + + // Variables pour le scanner + QRViewController? _qrController; + bool _isScanning = false; + bool _isAssigning = false; + final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR'); + // Catégories prédéfinies pour l'ajout de produits final List _predefinedCategories = [ 'Smartphone', 'Tablette', 'Accessoires', 'Multimedia', 'Informatique', 'Laptop', 'Non catégorisé' ]; + // Variables pour l'import Excel (conservées du code original) + bool _isImporting = false; + double _importProgress = 0.0; + String _importStatusText = ''; + @override void initState() { super.initState(); @@ -49,715 +63,853 @@ List> _pointsDeVente = []; @override void dispose() { + _qrController?.dispose(); _searchController.dispose(); super.dispose(); } +void _showAddProductDialog() { + final nameController = TextEditingController(); + final priceController = TextEditingController(); + final stockController = TextEditingController(); + final descriptionController = TextEditingController(); + final imageController = TextEditingController(); + final referenceController = TextEditingController(); + final marqueController = TextEditingController(); + final ramController = TextEditingController(); + final memoireInterneController = TextEditingController(); + final imeiController = TextEditingController(); + final newPointDeVenteController = TextEditingController(); + + String? selectedPointDeVente; + List> pointsDeVente = []; + bool isLoadingPoints = true; + String selectedCategory = _predefinedCategories.last; // 'Non catégorisé' par défaut + File? pickedImage; + String? qrPreviewData; + bool autoGenerateReference = true; + bool showAddNewPoint = false; - - - - - - -//====================================================================================================== -// Ajoutez ces variables à la classe _ProductManagementPageState -bool _isImporting = false; -double _importProgress = 0.0; -String _importStatusText = ''; - -// Ajoutez ces méthodes à la classe _ProductManagementPageState -void _resetImportState() { - setState(() { - _isImporting = false; - _importProgress = 0.0; - _importStatusText = ''; - }); -} - Future _loadPointsDeVente() async { + // Fonction pour mettre à jour le QR preview + void updateQrPreview() { + if (nameController.text.isNotEmpty) { + final reference = autoGenerateReference ? _generateUniqueReference() : referenceController.text.trim(); + if (reference.isNotEmpty) { + qrPreviewData = 'https://stock.guycom.mg/$reference'; + } else { + qrPreviewData = null; + } + } else { + qrPreviewData = null; + } + } +final AppDatabase _productDatabase = AppDatabase.instance; + Future loadPointsDeVente(StateSetter setDialogState) async { try { - final points = await _productDatabase.getPointsDeVente(); - setState(() { - _pointsDeVente = points; - if (points.isNotEmpty) { - _selectedPointDeVente = points.first['nom'] as String; + final result = await _productDatabase.getPointsDeVente(); + setDialogState(() { + // Ajouter l'option "Aucun" à la liste + pointsDeVente = [ + {'id': null, 'nom': 'Aucun'}, // Option pour pointDeVenteId null + ...result + ]; + isLoadingPoints = false; + if (selectedPointDeVente == null && result.isNotEmpty) { + selectedPointDeVente = 'Aucun'; // Par défaut, sélectionner "Aucun" } }); } catch (e) { + setDialogState(() { + isLoadingPoints = false; + }); Get.snackbar('Erreur', 'Impossible de charger les points de vente: $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, + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.add_shopping_cart, color: Colors.green.shade700), ), - ), - ], - ), - ); -} -Future _addPointDeVenteManually(String nom) async { - if (nom.isEmpty) return; - - try { - final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); - if (id != null) { - Get.snackbar('Succès', 'Point de vente "$nom" ajouté', - backgroundColor: Colors.green); - // Rafraîchir la liste des points de vente - _loadPointsDeVente(); - } else { - Get.snackbar('Erreur', 'Impossible d\'ajouter le point de vente', - backgroundColor: Colors.red); - } - } catch (e) { - Get.snackbar('Erreur', 'Erreur technique: ${e.toString()}', - backgroundColor: Colors.red); - } -} -Future _downloadExcelTemplate() async { - try { - final excel = Excel.createExcel(); - final sheet = excel['Sheet1']; - - // En-têtes modifiés sans DESCRIPTION et STOCK - final headers = [ - 'ID PRODUITS', // Sera ignoré lors de l'import - 'NOM DU PRODUITS', // name - 'REFERENCE PRODUITS', // reference - 'CATEGORIES PRODUITS', // category - 'MARQUE', // marque - 'RAM', // ram - 'INTERNE', // memoire_interne - 'IMEI', // imei - 'PRIX', // price - 'BOUTIQUE', // point_de_vente - ]; - - // Ajouter les en-têtes avec style - 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', - horizontalAlign: HorizontalAlign.Center, - ); - } - - // Exemples modifiés sans DESCRIPTION et STOCK - final examples = [ - [ - '1', // ID PRODUITS (sera ignoré) - 'Smartphone Galaxy S24', // NOM DU PRODUITS - 'SGS24-001', // REFERENCE PRODUITS - 'Téléphone', // CATEGORIES PRODUITS - 'Samsung', // MARQUE - '8 Go', // RAM - '256 Go', // INTERNE - '123456789012345', // IMEI - '1200.00', // PRIX - '405A', // BOUTIQUE - ], - [ - '2', // ID PRODUITS - 'iPhone 15 Pro', // NOM DU PRODUITS - 'IP15P-001', // REFERENCE PRODUITS - 'Téléphone', // CATEGORIES PRODUITS - 'Apple', // MARQUE - '8 Go', // RAM - '512 Go', // INTERNE - '987654321098765', // IMEI - '1599.00', // PRIX - '405B', // BOUTIQUE - ], - [ - '3', // ID PRODUITS - 'MacBook Pro 14"', // NOM DU PRODUITS - 'MBP14-001', // REFERENCE PRODUITS - 'Informatique', // CATEGORIES PRODUITS - 'Apple', // MARQUE - '16 Go', // RAM - '1 To', // INTERNE - '', // IMEI (vide pour un ordinateur) - '2499.00', // PRIX - 'S405A', // BOUTIQUE - ], - [ - '4', // ID PRODUITS - 'iPad Air', // NOM DU PRODUITS - 'IPA-001', // REFERENCE PRODUITS - 'Tablette', // CATEGORIES PRODUITS - 'Apple', // MARQUE - '8 Go', // RAM - '256 Go', // INTERNE - '456789123456789', // IMEI - '699.00', // PRIX - '405A', // BOUTIQUE - ], - [ - '5', // ID PRODUITS - 'Gaming Laptop ROG', // NOM DU PRODUITS - 'ROG-001', // REFERENCE PRODUITS - 'Informatique', // CATEGORIES PRODUITS - 'ASUS', // MARQUE - '32 Go', // RAM - '1 To', // INTERNE - '', // IMEI (vide) - '1899.00', // PRIX - '405B', // BOUTIQUE - ] - ]; - - // Ajouter les exemples - 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]; - - // Style pour les données (prix en gras) - if (col == 8) { // Colonne PRIX - cell.cellStyle = CellStyle( - bold: true, - ); - } - } - } - - // Ajuster la largeur des colonnes (sans DESCRIPTION et STOCK) - sheet.setColWidth(0, 12); // ID PRODUITS - sheet.setColWidth(1, 25); // NOM DU PRODUITS - sheet.setColWidth(2, 18); // REFERENCE PRODUITS - sheet.setColWidth(3, 18); // CATEGORIES PRODUITS - sheet.setColWidth(4, 15); // MARQUE - sheet.setColWidth(5, 10); // RAM - sheet.setColWidth(6, 12); // INTERNE - sheet.setColWidth(7, 18); // IMEI - sheet.setColWidth(8, 12); // PRIX - sheet.setColWidth(9, 12); // BOUTIQUE - - // Ajouter une feuille d'instructions mise à jour - final instructionSheet = excel['Instructions']; - - final instructions = [ - ['INSTRUCTIONS D\'IMPORTATION'], - [''], - ['Format des colonnes:'], - ['• ID PRODUITS: Numéro d\'identification (ignoré lors de l\'import)'], - ['• NOM DU PRODUITS: Nom du produit (OBLIGATOIRE)'], - ['• REFERENCE PRODUITS: Référence unique du produit'], - ['• CATEGORIES PRODUITS: Catégorie du produit'], - ['• MARQUE: Marque du produit'], - ['• RAM: Mémoire RAM (ex: "8 Go", "16 Go")'], - ['• INTERNE: Stockage interne (ex: "256 Go", "1 To")'], - ['• IMEI: Numéro IMEI (pour les appareils mobiles)'], - ['• PRIX: Prix du produit en euros (OBLIGATOIRE)'], - ['• BOUTIQUE: Code du point de vente'], - [''], - ['Remarques importantes:'], - ['• Les colonnes NOM DU PRODUITS et PRIX sont obligatoires'], - ['• Si CATEGORIES PRODUITS est vide, "Non catégorisé" sera utilisé'], - ['• Si REFERENCE PRODUITS est vide, une référence sera générée automatiquement'], - ['• Le stock sera automatiquement initialisé à 1 pour chaque produit'], - ['• La description sera automatiquement vide pour chaque produit'], - ['• Les colonnes peuvent être dans n\'importe quel ordre'], - ['• Vous pouvez supprimer les colonnes non utilisées'], - [''], - ['Formats acceptés:'], - ['• PRIX: 1200.00 ou 1200,00 ou 1200'], - ['• RAM/INTERNE: Texte libre (ex: "8 Go", "256 Go", "1 To")'], - ]; - - for (int i = 0; i < instructions.length; i++) { - final cell = instructionSheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: i)); - cell.value = instructions[i][0]; - - if (i == 0) { // Titre - cell.cellStyle = CellStyle( - bold: true, - fontSize: 16, - backgroundColorHex: '#4CAF50', - fontColorHex: '#FFFFFF', - ); - } else if (instructions[i][0].startsWith('•')) { // Points de liste - cell.cellStyle = CellStyle( - italic: true, - ); - } else if (instructions[i][0].endsWith(':')) { // Sous-titres - cell.cellStyle = CellStyle( - bold: true, - backgroundColorHex: '#F5F5F5', - ); - } - } - - // Ajuster la largeur de la colonne instructions - instructionSheet.setColWidth(0, 80); - - final bytes = excel.save(); - - if (bytes == null) { - Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); - return; - } - - final String? outputFile = await FilePicker.platform.saveFile( - fileName: 'modele_import_produits_v3.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\n\nConsultez l\'onglet "Instructions" pour plus d\'informations.', - duration: const Duration(seconds: 6), - 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'); - } -} - -// Méthode pour mapper les en-têtes aux colonnes (mise à jour) -// Méthode pour mapper les en-têtes aux colonnes (CORRIGÉE) -Map _mapHeaders(List headerRow) { - Map columnMapping = {}; - - for (int i = 0; i < headerRow.length; i++) { - if (headerRow[i]?.value == null) continue; - - String header = headerRow[i]!.value.toString().trim().toUpperCase(); - - // Debug : afficher chaque en-tête trouvé - print('En-tête trouvé: "$header" à la colonne $i'); - - // Mapping amélioré pour gérer les variations - if ((header.contains('NOM') && (header.contains('PRODUIT') || header.contains('DU'))) || - header == 'NOM DU PRODUITS' || header == 'NOM') { - columnMapping['name'] = i; - print('→ Mappé vers name'); - } - else if ((header.contains('REFERENCE') && (header.contains('PRODUIT') || header.contains('PRODUITS'))) || - header == 'REFERENCE PRODUITS' || header == 'REFERENCE') { - columnMapping['reference'] = i; - print('→ Mappé vers reference'); - } - else if ((header.contains('CATEGORIES') && (header.contains('PRODUIT') || header.contains('PRODUITS'))) || - header == 'CATEGORIES PRODUITS' || header == 'CATEGORIE' || header == 'CATEGORY') { - columnMapping['category'] = i; - print('→ Mappé vers category'); - } - else if (header == 'MARQUE' || header == 'BRAND') { - columnMapping['marque'] = i; - print('→ Mappé vers marque'); - } - else if (header == 'RAM' || header.contains('MEMOIRE RAM')) { - columnMapping['ram'] = i; - print('→ Mappé vers ram'); - } - else if (header == 'INTERNE' || header.contains('MEMOIRE INTERNE') || header.contains('STOCKAGE')) { - columnMapping['memoire_interne'] = i; - print('→ Mappé vers memoire_interne'); - } - else if (header == 'IMEI' || header.contains('NUMERO IMEI')) { - columnMapping['imei'] = i; - print('→ Mappé vers imei'); - } - else if (header == 'PRIX' || header == 'PRICE') { - columnMapping['price'] = i; - print('→ Mappé vers price'); - } - else if (header == 'BOUTIQUE' || header.contains('POINT DE VENTE') || header == 'MAGASIN') { - columnMapping['point_de_vente'] = i; - print('→ Mappé vers point_de_vente'); - } - else { - print('→ Non reconnu'); - } - } - - // Debug : afficher le mapping final - print('Mapping final: $columnMapping'); - - return columnMapping; -} -// Fonction de débogage pour analyser le fichier Excel -void _debugExcelFile(Excel excel) { - print('=== DEBUG EXCEL FILE ==='); - print('Nombre de feuilles: ${excel.tables.length}'); - - for (var sheetName in excel.tables.keys) { - print('Feuille: $sheetName'); - var sheet = excel.tables[sheetName]!; - print('Nombre de lignes: ${sheet.rows.length}'); - - if (sheet.rows.isNotEmpty) { - print('En-têtes (première ligne):'); - for (int i = 0; i < sheet.rows[0].length; i++) { - var cellValue = sheet.rows[0][i]?.value; - print(' Colonne $i: "$cellValue" (${cellValue.runtimeType})'); - } - - if (sheet.rows.length > 1) { - print('Première ligne de données:'); - for (int i = 0; i < sheet.rows[1].length; i++) { - var cellValue = sheet.rows[1][i]?.value; - print(' Colonne $i: "$cellValue"'); - } - } - } - } - print('=== FIN DEBUG ==='); -} - -// Fonction pour valider les données d'une ligne -bool _validateRowData(List row, Map mapping, int rowIndex) { - print('=== VALIDATION LIGNE ${rowIndex + 1} ==='); - - String? nameValue = _getColumnValue(row, mapping, 'name'); - String? priceValue = _getColumnValue(row, mapping, 'price'); - - print('Nom: "$nameValue"'); - print('Prix: "$priceValue"'); - - if (nameValue == null || nameValue.isEmpty) { - print('❌ Nom manquant'); - return false; - } - - if (priceValue == null || priceValue.isEmpty) { - print('❌ Prix manquant'); - return false; - } - - final price = double.tryParse(priceValue.replaceAll(',', '.')); - if (price == null || price <= 0) { - print('❌ Prix invalide: $priceValue'); - return false; - } - - print('✅ Ligne valide'); - return true; -} - -// Méthode utilitaire pour extraire une valeur de colonne (inchangée) -String? _getColumnValue(List row, Map mapping, String field) { - if (!mapping.containsKey(field)) return null; - - int columnIndex = mapping[field]!; - if (columnIndex >= row.length || row[columnIndex]?.value == null) return null; - - return row[columnIndex]!.value.toString().trim(); -} - - - -double? _normalizeNumber(String? value) { - if (value == null || value.isEmpty) return null; - - // Vérifier si c'est une date mal interprétée (contient des tirets et des deux-points) - if (value.contains('-') && value.contains(':')) { - print('⚠️ Chaîne DateTime détectée: $value'); - - try { - // Nettoyer la chaîne pour enlever le + au début si présent - String cleanDateString = value.replaceAll('+', ''); - final dateTime = DateTime.parse(cleanDateString); - - // Excel epoch: 1er janvier 1900 - final excelEpoch = DateTime(1900, 1, 1); - - // Calculer le nombre de jours depuis l'epoch Excel - final daysDifference = dateTime.difference(excelEpoch).inDays; - - print('→ Date parsée: $dateTime'); - print('→ Jours depuis epoch Excel (1900-01-01): $daysDifference'); - - // Le problème : Excel stocke parfois en millisecondes ou avec facteur - // Testons différentes conversions pour retrouver le prix original - - if (daysDifference > 0) { - print('✅ Prix récupéré (jours): $daysDifference'); - return daysDifference.toDouble(); - } - } catch (e) { - print('→ Erreur parsing DateTime: $e'); - } - - return null; - } - - // Traitement pour les très grands nombres (timestamps corrompus) - final numericValue = double.tryParse(value.replaceAll(RegExp(r'[^0-9.]'), '')); - if (numericValue != null && numericValue > 10000000000) { // Plus de 10 milliards = suspect - print('⚠️ Grand nombre détecté: $numericValue'); - - // Cas observés : - // 39530605000000 → doit donner 750000 - // 170950519000000 → doit donner 5550000 - - // Pattern détecté : diviser par un facteur pour retrouver le prix - // Testons plusieurs facteurs de conversion - - final factor1000000 = numericValue / 1000000; // Diviser par 1 million - final factor100000 = numericValue / 100000; // Diviser par 100 mille - final factor10000 = numericValue / 10000; // Diviser par 10 mille - - print('→ Test ÷1000000: $factor1000000'); - print('→ Test ÷100000: $factor100000'); - print('→ Test ÷10000: $factor10000'); - - // Logique pour déterminer le bon facteur : - // - 39530605000000 ÷ ? = 750000 - // - 39530605000000 ÷ 52.74 ≈ 750000 - // Mais c'est plus complexe, analysons le pattern des dates - - // Nouvelle approche : extraire l'information de la partie "date" - String numberStr = numericValue.toStringAsFixed(0); - - // Si le nombre fait plus de 12 chiffres, c'est probablement un timestamp - if (numberStr.length >= 12) { - // Essayons d'extraire les premiers chiffres significatifs - - // Pattern observé : les dates comme 39530605000000 ont l'info dans les premiers chiffres - // 39530605 pourrait être la date julienne ou un autre format - - String significantPart = numberStr.substring(0, 8); // Prendre les 8 premiers chiffres - double? significantNumber = double.tryParse(significantPart); - - if (significantNumber != null) { - print('→ Partie significative extraite: $significantNumber'); - - // Maintenant convertir cette partie en prix réel - // Analysons le pattern plus précisément... - - // Pour 39530605000000 → 750000, le ratio est environ 52.74 - // Pour 170950519000000 → 5550000, vérifions le ratio - - // Hypothèse : la partie significative pourrait être des jours depuis une epoch - // et il faut une formule spécifique pour reconvertir - - // Testons une conversion basée sur les jours Excel - DateTime testDate; - try { - // Utiliser la partie significative comme nombre de jours depuis Excel epoch - final excelEpoch = DateTime(1900, 1, 1); - testDate = excelEpoch.add(Duration(days: significantNumber.toInt())); - print('→ Date correspondante: $testDate'); - - // Cette approche ne semble pas correcte non plus... - // Essayons une approche empirique basée sur vos exemples - - // Pattern direct observé : - // 39530605000000 → 750000 - // Ratio: 39530605000000 / 750000 = 52707473.33 - - // 170950519000000 → 5550000 - // Ratio: 170950519000000 / 5550000 = 30792.8 - - // Les ratios sont différents, donc c'est plus complexe - // Utilisons une approche de mapping direct - - return _convertCorruptedExcelNumber(numericValue); - - } catch (e) { - print('→ Erreur conversion date: $e'); - } - } - } - - return null; - } - - // Traitement normal pour les valeurs qui ne sont pas des dates - print('📝 Valeur normale détectée: $value'); - - // Remplacer les virgules par des points et supprimer les espaces - String cleaned = value.replaceAll(',', '.').replaceAll(RegExp(r'\s+'), ''); - - // Supprimer les caractères non numériques sauf le point - String numericString = cleaned.replaceAll(RegExp(r'[^0-9.]'), ''); - - final result = double.tryParse(numericString); - print('→ Résultat parsing normal: $result'); - - return result; -} - - - -// Fonction spécialisée pour convertir les nombres Excel corrompus -double? _convertCorruptedExcelNumber(double corruptedValue) { - print('🔧 Conversion nombre Excel corrompu: $corruptedValue'); - - // Méthode 1: Analyser le pattern de corruption - String valueStr = corruptedValue.toStringAsFixed(0); - - // Si c'est un nombre avec beaucoup de zéros à la fin, il pourrait s'agir d'un timestamp - if (valueStr.endsWith('000000') && valueStr.length > 10) { - // Supprimer les 6 derniers zéros - String withoutMicros = valueStr.substring(0, valueStr.length - 6); - double? withoutMicrosValue = double.tryParse(withoutMicros); - - if (withoutMicrosValue != null) { - print('→ Après suppression microseconds: $withoutMicrosValue'); - - // Maintenant, essayer de convertir ce nombre en prix - // Approche: utiliser la conversion de timestamp Excel vers nombre de jours - - // Excel stocke les dates comme nombre de jours depuis 1900-01-01 - // Si c'est un timestamp Unix (ms), le convertir d'abord - - if (withoutMicrosValue > 1000000) { // Si c'est encore un grand nombre - // Essayer de le traiter comme nombre de jours Excel - try { - final excelEpoch = DateTime(1900, 1, 1); - final resultDate = excelEpoch.add(Duration(days: withoutMicrosValue.toInt())); - - // Si la date est raisonnable, utiliser le nombre de jours comme prix - if (resultDate.year < 10000 && resultDate.year > 1900) { - print('→ Conversion Excel jours vers prix: $withoutMicrosValue'); - return withoutMicrosValue; - } - } catch (e) { - print('→ Erreur conversion Excel: $e'); - } - } - } - } - - // Méthode 2: Table de correspondance empirique basée sur vos exemples - // Vous pouvez étendre cette table avec plus d'exemples - Map knownConversions = { - '39530605000000': 750000, - '170950519000000': 5550000, - }; - - String corruptedStr = corruptedValue.toStringAsFixed(0); - if (knownConversions.containsKey(corruptedStr)) { - double realPrice = knownConversions[corruptedStr]!; - print('→ Conversion via table: $corruptedStr → $realPrice'); - return realPrice; - } - - // Méthode 3: Analyse du pattern mathématique - // Extraire les premiers chiffres significatifs et appliquer une formule - if (valueStr.length >= 8) { - String prefix = valueStr.substring(0, 8); - double? prefixValue = double.tryParse(prefix); - - if (prefixValue != null) { - // Essayer différentes formules basées sur vos exemples - // 39530605 → 750000, soit environ /52.7 - // 170950519 → 5550000, soit environ /30.8 - - // Pour l'instant, utilisons une moyenne approximative - double averageFactor = 40; // À ajuster selon plus d'exemples - double estimatedPrice = prefixValue / averageFactor; - - print('→ Estimation avec facteur $averageFactor: $estimatedPrice'); - - // Vérifier si le résultat est dans une plage raisonnable (prix entre 1000 et 100000000) - if (estimatedPrice >= 1000 && estimatedPrice <= 100000000) { - return estimatedPrice; - } - } - } - - print('❌ Impossible de convertir le nombre corrompu'); - return null; -} - + const SizedBox(width: 12), + const Text('Ajouter un produit'), + ], + ), + content: Container( + width: 600, + constraints: const BoxConstraints(maxHeight: 600), + child: SingleChildScrollView( + child: StatefulBuilder( + builder: (context, setDialogState) { + // Charger les points de vente une seule fois + if (isLoadingPoints && pointsDeVente.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + loadPointsDeVente(setDialogState); + }); + } + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Champs obligatoires + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.teal.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.teal.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.store, color: Colors.teal.shade700), + const SizedBox(width: 8), + Text( + 'Point de vente', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.teal.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + + if (isLoadingPoints) + const Center(child: CircularProgressIndicator()) + else + Column( + children: [ + if (!showAddNewPoint) ...[ + DropdownButtonFormField( + value: selectedPointDeVente, + items: pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + setDialogState(() => selectedPointDeVente = value); + }, + decoration: const InputDecoration( + labelText: 'Sélectionner un point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.store), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + TextButton.icon( + onPressed: () { + setDialogState(() { + showAddNewPoint = true; + newPointDeVenteController.clear(); + }); + }, + icon: const Icon(Icons.add, size: 16), + label: const Text('Ajouter nouveau point'), + style: TextButton.styleFrom( + foregroundColor: Colors.teal.shade700, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: () => loadPointsDeVente(setDialogState), + icon: const Icon(Icons.refresh, size: 16), + label: const Text('Actualiser'), + ), + ], + ), + ], + + if (showAddNewPoint) ...[ + DropdownButtonFormField( + value: selectedPointDeVente, + items: pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + setDialogState(() => selectedPointDeVente = value); + }, + decoration: const InputDecoration( + labelText: 'Sélectionner un point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.store), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + TextButton.icon( + onPressed: () { + setDialogState(() { + showAddNewPoint = true; + newPointDeVenteController.clear(); + }); + }, + icon: const Icon(Icons.add, size: 16), + label: const Text('Ajouter nouveau point'), + style: TextButton.styleFrom( + foregroundColor: Colors.teal.shade700, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: () => loadPointsDeVente(setDialogState), + icon: const Icon(Icons.refresh, size: 16), + label: const Text('Actualiser'), + ), + ], + ), + ], + + if (showAddNewPoint) ...[ + TextField( + controller: newPointDeVenteController, + decoration: const InputDecoration( + labelText: 'Nom du nouveau point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.add_business), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + TextButton( + onPressed: () { + setDialogState(() { + showAddNewPoint = false; + newPointDeVenteController.clear(); + }); + }, + child: const Text('Annuler'), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () async { + final nom = newPointDeVenteController.text.trim(); + if (nom.isNotEmpty) { + try { + final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); + if (id != null) { + setDialogState(() { + showAddNewPoint = false; + selectedPointDeVente = nom; + newPointDeVenteController.clear(); + }); + // Recharger la liste + await loadPointsDeVente(setDialogState); + Get.snackbar( + 'Succès', + 'Point de vente "$nom" créé avec succès', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } + } catch (e) { + Get.snackbar('Erreur', 'Impossible de créer le point de vente: $e'); + } + } + }, + icon: const Icon(Icons.save, size: 16), + label: const Text('Créer'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.teal, + foregroundColor: Colors.white, + ), + ), + ], + ), + ], + ], + ), + ], + ), + ), + const SizedBox(height: 16), -Map _normalizeRowData(List row, Map mapping, int rowIndex) { - final normalizedData = {}; - - // Fonction interne pour nettoyer et normaliser les valeurs - String? _cleanValue(String? value) { - if (value == null) return null; - return value.toString().trim(); - } - - // Fonction simple pour les nombres (maintenant ils sont corrects) - double? _normalizeNumber(String? value) { - if (value == null || value.isEmpty) return null; - - // Remplacer les virgules par des points et supprimer les espaces - final cleaned = value.replaceAll(',', '.').replaceAll(RegExp(r'\s+'), ''); - - // Supprimer les caractères non numériques sauf le point - final numericString = cleaned.replaceAll(RegExp(r'[^0-9.]'), ''); - - return double.tryParse(numericString); - } - - // Normalisation du nom - if (mapping.containsKey('name')) { - final name = _cleanValue(_getColumnValue(row, mapping, 'name')); - if (name != null && name.isNotEmpty) { - normalizedData['name'] = name; - } - } - - // Normalisation du prix (maintenant simple car corrigé en amont) - if (mapping.containsKey('price')) { - final priceValue = _cleanValue(_getColumnValue(row, mapping, 'price')); - final price = _normalizeNumber(priceValue); - if (price != null && price > 0) { - normalizedData['price'] = price; - print('✅ Prix normalisé: $price'); - } - } - - // Normalisation de la référence - if (mapping.containsKey('reference')) { - final reference = _cleanValue(_getColumnValue(row, mapping, 'reference')); - if (reference != null && reference.isNotEmpty) { - normalizedData['reference'] = reference; - } else { - // Génération automatique si non fournie - normalizedData['reference'] = _generateUniqueReference(); - } - } - - // Normalisation de la catégorie - if (mapping.containsKey('category')) { - final category = _cleanValue(_getColumnValue(row, mapping, 'category')); - normalizedData['category'] = category ?? 'Non catégorisé'; + // Nom du produit + TextField( + controller: nameController, + decoration: InputDecoration( + labelText: 'Nom du produit *', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.shopping_bag), + filled: true, + fillColor: Colors.grey.shade50, + ), + onChanged: (value) { + setDialogState(() { + updateQrPreview(); + }); + }, + ), + const SizedBox(height: 16), + + // Prix et Stock sur la même ligne + Row( + children: [ + Expanded( + child: TextField( + controller: priceController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: 'Prix (MGA) *', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.attach_money), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: stockController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Stock initial', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.inventory), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Catégorie + DropdownButtonFormField( + value: selectedCategory, + items: _predefinedCategories.map((category) => + DropdownMenuItem(value: category, child: Text(category))).toList(), + onChanged: (value) { + setDialogState(() => selectedCategory = value!); + }, + decoration: InputDecoration( + labelText: 'Catégorie', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.category), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + const SizedBox(height: 16), + + // Description + TextField( + controller: descriptionController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Description', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.description), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + const SizedBox(height: 16), + + // Section Référence + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.purple.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.purple.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.confirmation_number, color: Colors.purple.shade700), + const SizedBox(width: 8), + Text( + 'Référence du produit', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.purple.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Checkbox( + value: autoGenerateReference, + onChanged: (value) { + setDialogState(() { + autoGenerateReference = value!; + updateQrPreview(); + }); + }, + ), + const Text('Générer automatiquement'), + ], + ), + const SizedBox(height: 8), + if (!autoGenerateReference) + TextField( + controller: referenceController, + decoration: const InputDecoration( + labelText: 'Référence *', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.tag), + filled: true, + fillColor: Colors.white, + ), + onChanged: (value) { + setDialogState(() { + updateQrPreview(); + }); + }, + ), + if (autoGenerateReference) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Référence générée automatiquement', + style: TextStyle(color: Colors.grey.shade700), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Nouveaux champs (Marque, RAM, Mémoire interne, IMEI) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.memory, color: Colors.orange.shade700), + const SizedBox(width: 8), + Text( + 'Spécifications techniques', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: marqueController, + decoration: const InputDecoration( + labelText: 'Marque', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.branding_watermark), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: ramController, + decoration: const InputDecoration( + labelText: 'RAM', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.memory), + filled: true, + fillColor: Colors.white, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: memoireInterneController, + decoration: const InputDecoration( + labelText: 'Mémoire interne', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.storage), + filled: true, + fillColor: Colors.white, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + TextField( + controller: imeiController, + decoration: const InputDecoration( + labelText: 'IMEI (pour téléphones)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.smartphone), + filled: true, + fillColor: Colors.white, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Section Image + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.image, color: Colors.blue.shade700), + const SizedBox(width: 8), + Text( + 'Image du produit (optionnel)', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.blue.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextField( + controller: imageController, + decoration: const InputDecoration( + labelText: 'Chemin de l\'image', + border: OutlineInputBorder(), + isDense: true, + ), + readOnly: true, + ), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () async { + final result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && result.files.single.path != null) { + setDialogState(() { + pickedImage = File(result.files.single.path!); + imageController.text = pickedImage!.path; + }); + } + }, + icon: const Icon(Icons.folder_open, size: 16), + label: const Text('Choisir'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(12), + ), + ), + ], + ), + const SizedBox(height: 12), + + // Aperçu de l'image + if (pickedImage != null) + Center( + child: Container( + height: 100, + width: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file(pickedImage!, fit: BoxFit.cover), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Aperçu QR Code + if (qrPreviewData != null) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + children: [ + Row( + children: [ + Icon(Icons.qr_code_2, color: Colors.green.shade700), + const SizedBox(width: 8), + Text( + 'Aperçu du QR Code', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: QrImageView( + data: qrPreviewData!, + version: QrVersions.auto, + size: 80, + backgroundColor: Colors.white, + ), + ), + ), + const SizedBox(height: 8), + Text( + 'Réf: ${autoGenerateReference ? _generateUniqueReference() : referenceController.text.trim()}', + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ), + ), + ], + ); + }, + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton.icon( + onPressed: () async { + final name = nameController.text.trim(); + final price = double.tryParse(priceController.text.trim()) ?? 0.0; + final stock = int.tryParse(stockController.text.trim()) ?? 0; + + if (name.isEmpty || price <= 0) { + Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); + return; + } + + // Vérification de la référence + String finalReference; + if (autoGenerateReference) { + finalReference = _generateUniqueReference(); + } else { + finalReference = referenceController.text.trim(); + if (finalReference.isEmpty) { + Get.snackbar('Erreur', 'La référence est obligatoire'); + return; + } + + final existingProduct = await _productDatabase.getProductByReference(finalReference); + if (existingProduct != null) { + Get.snackbar('Erreur', 'Cette référence existe déjà'); + return; + } + } + + // Gérer le point de vente + int? pointDeVenteId; + String? finalPointDeVenteNom; + + if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) { + finalPointDeVenteNom = newPointDeVenteController.text.trim(); + } else if (selectedPointDeVente != null && selectedPointDeVente != 'Aucun') { + finalPointDeVenteNom = selectedPointDeVente; + } + + if (finalPointDeVenteNom != null) { + pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(finalPointDeVenteNom); + } + // Si "Aucun" est sélectionné, pointDeVenteId reste null + + try { + final qrPath = await _generateAndSaveQRCode(finalReference); + + final product = Product( + name: name, + price: price, + image: imageController.text, + category: selectedCategory, + description: descriptionController.text.trim(), + stock: stock, + qrCode: qrPath, + reference: finalReference, + marque: marqueController.text.trim(), + ram: ramController.text.trim(), + memoireInterne: memoireInterneController.text.trim(), + imei: imeiController.text.trim(), + pointDeVenteId: pointDeVenteId, // Peut être null si "Aucun" + ); + + await _productDatabase.createProduct(product); + Get.back(); + Get.snackbar( + 'Succès', + 'Produit ajouté avec succès!\nRéférence: $finalReference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 4), + icon: const Icon(Icons.check_circle, color: Colors.white), + ); + _loadProducts(); + _loadPointsDeVente(); + } catch (e) { + Get.snackbar('Erreur', 'Ajout du produit échoué: $e'); + } + }, + icon: const Icon(Icons.save), + label: const Text('Ajouter le produit'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + ), + ], + ), + ); +} + // === FONCTIONS DE SCAN POUR ATTRIBUTION POINT DE VENTE === +String? _getColumnValue(List row, Map mapping, String field) { + if (!mapping.containsKey(field)) return null; + + int columnIndex = mapping[field]!; + if (columnIndex >= row.length || row[columnIndex]?.value == null) return null; + + return row[columnIndex]!.value.toString().trim(); +} + void _startPointDeVenteAssignmentScanning() { + if (_isScanning) return; + + // Vérifier que l'utilisateur a un point de vente + if (_userController.pointDeVenteId <= 0) { + Get.snackbar( + 'Erreur', + 'Vous n\'avez pas de point de vente assigné. Contactez un administrateur.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 4), + icon: const Icon(Icons.error, color: Colors.white), + ); + return; + } + + setState(() { + _isScanning = true; + }); + + Get.to(() => _buildAssignmentScannerPage())?.then((_) { + setState(() { + _isScanning = false; + }); + }); + } +Map _normalizeRowData(List row, Map mapping, int rowIndex) { + final normalizedData = {}; + + // Fonction interne pour nettoyer et normaliser les valeurs + String? _cleanValue(String? value) { + if (value == null) return null; + return value.toString().trim(); + } + + // Fonction simple pour les nombres (maintenant ils sont corrects) + double? _normalizeNumber(String? value) { + if (value == null || value.isEmpty) return null; + + // Remplacer les virgules par des points et supprimer les espaces + final cleaned = value.replaceAll(',', '.').replaceAll(RegExp(r'\s+'), ''); + + // Supprimer les caractères non numériques sauf le point + final numericString = cleaned.replaceAll(RegExp(r'[^0-9.]'), ''); + + return double.tryParse(numericString); + } + + // Normalisation du nom + if (mapping.containsKey('name')) { + final name = _cleanValue(_getColumnValue(row, mapping, 'name')); + if (name != null && name.isNotEmpty) { + normalizedData['name'] = name; + } + } + + // Normalisation du prix (maintenant simple car corrigé en amont) + if (mapping.containsKey('price')) { + final priceValue = _cleanValue(_getColumnValue(row, mapping, 'price')); + final price = _normalizeNumber(priceValue); + if (price != null && price > 0) { + normalizedData['price'] = price; + print('✅ Prix normalisé: $price'); + } + } + + // Normalisation de la référence + if (mapping.containsKey('reference')) { + final reference = _cleanValue(_getColumnValue(row, mapping, 'reference')); + if (reference != null && reference.isNotEmpty) { + normalizedData['reference'] = reference; + } else { + // Génération automatique si non fournie + normalizedData['reference'] = _generateUniqueReference(); + } + } + + // Normalisation de la catégorie + if (mapping.containsKey('category')) { + final category = _cleanValue(_getColumnValue(row, mapping, 'category')); + normalizedData['category'] = category ?? 'Non catégorisé'; } else { normalizedData['category'] = 'Non catégorisé'; } @@ -824,1313 +976,1670 @@ Map _normalizeRowData(List row, Map mapping return normalizedData; } - - -Excel _fixExcelNumberFormats(Excel excel) { - print('🔧 Correction des formats de cellules Excel...'); - - for (var sheetName in excel.tables.keys) { - print('📋 Traitement de la feuille: $sheetName'); - var sheet = excel.tables[sheetName]!; - - if (sheet.rows.isEmpty) continue; - - // Analyser la première ligne pour identifier les colonnes de prix/nombres - List numberColumns = _identifyNumberColumns(sheet.rows[0]); - print('🔢 Colonnes numériques détectées: $numberColumns'); - - // Corriger chaque ligne de données (ignorer la ligne d'en-tête) - for (int rowIndex = 1; rowIndex < sheet.rows.length; rowIndex++) { - var row = sheet.rows[rowIndex]; - - for (int colIndex in numberColumns) { - if (colIndex < row.length && row[colIndex] != null) { - var cell = row[colIndex]!; - var originalValue = cell.value; - - // Détecter si la cellule a un format de date/temps suspect - if (_isSuspiciousDateFormat(originalValue)) { - print('⚠️ Cellule suspecte détectée en ($rowIndex, $colIndex): $originalValue'); - - // Convertir la valeur corrompue en nombre standard - var correctedValue = _convertSuspiciousValue(originalValue); - if (correctedValue != null) { - print('✅ Correction: $originalValue → $correctedValue'); - - // Créer une nouvelle cellule avec la valeur corrigée - excel.updateCell(sheetName, - CellIndex.indexByColumnRow(columnIndex: colIndex, rowIndex: rowIndex), - correctedValue - ); - } - } - } - } - } - } - - print('✅ Correction des formats terminée'); - return excel; -} - -// Identifier les colonnes qui devraient contenir des nombres -List _identifyNumberColumns(List headerRow) { - List numberColumns = []; +// Méthode pour mapper les en-têtes aux colonnes (CORRIGÉE) +Map _mapHeaders(List headerRow) { + Map columnMapping = {}; for (int i = 0; i < headerRow.length; i++) { if (headerRow[i]?.value == null) continue; String header = headerRow[i]!.value.toString().trim().toUpperCase(); - // Identifier les en-têtes qui correspondent à des valeurs numériques - if (_isNumericHeader(header)) { - numberColumns.add(i); - print('📊 Colonne numérique: "$header" (index $i)'); + // Debug : afficher chaque en-tête trouvé + print('En-tête trouvé: "$header" à la colonne $i'); + + // Mapping amélioré pour gérer les variations + if ((header.contains('NOM') && (header.contains('PRODUIT') || header.contains('DU'))) || + header == 'NOM DU PRODUITS' || header == 'NOM') { + columnMapping['name'] = i; + print('→ Mappé vers name'); + } + else if ((header.contains('REFERENCE') && (header.contains('PRODUIT') || header.contains('PRODUITS'))) || + header == 'REFERENCE PRODUITS' || header == 'REFERENCE') { + columnMapping['reference'] = i; + print('→ Mappé vers reference'); + } + else if ((header.contains('CATEGORIES') && (header.contains('PRODUIT') || header.contains('PRODUITS'))) || + header == 'CATEGORIES PRODUITS' || header == 'CATEGORIE' || header == 'CATEGORY') { + columnMapping['category'] = i; + print('→ Mappé vers category'); + } + else if (header == 'MARQUE' || header == 'BRAND') { + columnMapping['marque'] = i; + print('→ Mappé vers marque'); + } + else if (header == 'RAM' || header.contains('MEMOIRE RAM')) { + columnMapping['ram'] = i; + print('→ Mappé vers ram'); + } + else if (header == 'INTERNE' || header.contains('MEMOIRE INTERNE') || header.contains('STOCKAGE')) { + columnMapping['memoire_interne'] = i; + print('→ Mappé vers memoire_interne'); + } + else if (header == 'IMEI' || header.contains('NUMERO IMEI')) { + columnMapping['imei'] = i; + print('→ Mappé vers imei'); + } + else if (header == 'PRIX' || header == 'PRICE') { + columnMapping['price'] = i; + print('→ Mappé vers price'); + } + else if (header == 'BOUTIQUE' || header.contains('POINT DE VENTE') || header == 'MAGASIN') { + columnMapping['point_de_vente'] = i; + print('→ Mappé vers point_de_vente'); } - } - - return numberColumns; -} - -// Vérifier si un en-tête correspond à une colonne numérique -bool _isNumericHeader(String header) { - List numericHeaders = [ - 'PRIX', 'PRICE', 'COST', 'COUT', - 'MONTANT', 'AMOUNT', 'TOTAL', - 'QUANTITE', 'QUANTITY', 'QTE', - 'STOCK', 'NOMBRE', 'NUMBER', - 'TAILLE', 'SIZE', 'POIDS', 'WEIGHT', - 'RAM', 'MEMOIRE', 'STORAGE', 'STOCKAGE' - ]; - - return numericHeaders.any((keyword) => header.contains(keyword)); -} - -// Détecter si une valeur semble être un format de date/temps suspect -bool _isSuspiciousDateFormat(dynamic value) { - if (value == null) return false; - - String valueStr = value.toString(); - - // Détecter les formats de date suspects qui devraient être des nombres - if (valueStr.contains('-') && valueStr.contains(':')) { - // Format DateTime détecté - print('🔍 Format DateTime suspect: $valueStr'); - return true; - } - - // Détecter les très grands nombres (timestamps en millisecondes) - if (valueStr.length > 10 && !valueStr.contains('.')) { - double? numValue = double.tryParse(valueStr); - if (numValue != null && numValue > 10000000000) { - print('🔍 Grand nombre suspect: $valueStr'); - return true; + else { + print('→ Non reconnu'); } } - return false; -} - -// Convertir une valeur suspecte en nombre correct -double? _convertSuspiciousValue(dynamic suspiciousValue) { - if (suspiciousValue == null) return null; - - String valueStr = suspiciousValue.toString(); - - // Cas 1: Format DateTime (ex: "3953-06-05T00:00:00.000") - if (valueStr.contains('-') && valueStr.contains(':')) { - return _convertDateTimeToNumber(valueStr); - } + // Debug : afficher le mapping final + print('Mapping final: $columnMapping'); - // Cas 2: Grand nombre (ex: "39530605000000") - if (valueStr.length > 10) { - return _convertLargeNumberToPrice(valueStr); + return columnMapping; +} + Widget _buildAssignmentScannerPage() { + return Scaffold( + appBar: AppBar( + title: const Text('Scanner pour Attribution'), + backgroundColor: Colors.orange.shade700, + foregroundColor: Colors.white, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _qrController?.dispose(); + Get.back(); + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.flash_on), + onPressed: () async { + await _qrController?.toggleFlash(); + }, + ), + IconButton( + icon: const Icon(Icons.flip_camera_ios), + onPressed: () async { + await _qrController?.flipCamera(); + }, + ), + ], + ), + body: Stack( + children: [ + // Scanner view + QRView( + key: _qrKey, + onQRViewCreated: _onAssignmentQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: Colors.orange, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: 250, + ), + ), + + // Instructions overlay + Positioned( + bottom: 100, + left: 20, + right: 20, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.assignment, color: Colors.orange.shade300, size: 40), + const SizedBox(height: 8), + const Text( + 'Scanner l\'IMEI pour assigner au point de vente', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Text( + 'Point de vente: ${_userController.pointDeVenteDesignation}', + style: TextStyle( + color: Colors.orange.shade300, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ), + ); } - - return null; -} -// Convertir un format DateTime en nombre -double? _convertDateTimeToNumber(String dateTimeStr) { - try { - print('🔄 Conversion DateTime: $dateTimeStr'); - - // Nettoyer la chaîne - String cleanDateString = dateTimeStr.replaceAll('+', ''); - final dateTime = DateTime.parse(cleanDateString); - - // Excel epoch: 1er janvier 1900 - final excelEpoch = DateTime(1900, 1, 1); - - // Calculer le nombre de jours depuis l'epoch Excel - final daysDifference = dateTime.difference(excelEpoch).inDays; - - // Appliquer la correction pour le bug Excel (+2) - final correctedValue = daysDifference + 2; - - print('→ Jours calculés: $daysDifference → Corrigé: $correctedValue'); + void _onAssignmentQRViewCreated(QRViewController controller) { + _qrController = controller; - if (correctedValue > 0 && correctedValue < 100000000) { - return correctedValue.toDouble(); - } - } catch (e) { - print('❌ Erreur conversion DateTime: $e'); + controller.scannedDataStream.listen((scanData) { + if (scanData.code != null && scanData.code!.isNotEmpty) { + // Pauser le scanner pour éviter les scans multiples + controller.pauseCamera(); + + // Fermer la page du scanner + Get.back(); + + // Traiter le résultat + _assignProductToUserPointDeVente(scanData.code!); + } + }); } - - return null; -} -// Convertir un grand nombre en prix -double? _convertLargeNumberToPrice(String largeNumberStr) { - try { - print('🔄 Conversion grand nombre: $largeNumberStr'); - - double? numValue = double.tryParse(largeNumberStr); - if (numValue == null) return null; + Future _assignProductToUserPointDeVente(String scannedImei) async { + if (_isAssigning) return; - // Si le nombre se termine par 000000 (microsecondes), les supprimer - if (largeNumberStr.endsWith('000000') && largeNumberStr.length > 10) { - String withoutMicros = largeNumberStr.substring(0, largeNumberStr.length - 6); - double? daysSinceExcel = double.tryParse(withoutMicros); - - if (daysSinceExcel != null && daysSinceExcel > 1000 && daysSinceExcel < 10000000) { - // Appliquer la correction du décalage Excel (+2) - double correctedPrice = daysSinceExcel + 2; - print('→ Conversion: $largeNumberStr → $withoutMicros → $correctedPrice'); - return correctedPrice; + setState(() { + _isAssigning = true; + }); + + try { + // Montrer un indicateur de chargement + Get.dialog( + AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: Colors.orange.shade700), + const SizedBox(height: 16), + const Text('Recherche du produit...'), + const SizedBox(height: 8), + Text( + 'IMEI: $scannedImei', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + barrierDismissible: false, + ); + + // Attendre un court instant pour l'effet visuel + await Future.delayed(const Duration(milliseconds: 300)); + + // Chercher le produit avec l'IMEI scanné + Product? foundProduct = await _productDatabase.getProductByIMEI(scannedImei); + + // Fermer l'indicateur de chargement + Get.back(); + + if (foundProduct == null) { + _showProductNotFoundDialog(scannedImei); + return; + } + + // Vérifier si le produit a déjà le bon point de vente + if (foundProduct.pointDeVenteId == _userController.pointDeVenteId) { + _showAlreadyAssignedDialog(foundProduct); + return; } - } - - // Table de correspondance pour les cas connus - Map knownConversions = { - '39530605000000': 750000, - '170950519000000': 5550000, - }; - - if (knownConversions.containsKey(largeNumberStr)) { - double realPrice = knownConversions[largeNumberStr]!; - print('→ Conversion via table: $largeNumberStr → $realPrice'); - return realPrice; - } - - } catch (e) { - print('❌ Erreur conversion grand nombre: $e'); - } - - return null; -} + // Assigner le point de vente de l'utilisateur au produit + final updatedProduct = Product( + id: foundProduct.id, + name: foundProduct.name, + price: foundProduct.price, + image: foundProduct.image, + category: foundProduct.category, + description: foundProduct.description, + stock: foundProduct.stock, + qrCode: foundProduct.qrCode, + reference: foundProduct.reference, + marque: foundProduct.marque, + ram: foundProduct.ram, + memoireInterne: foundProduct.memoireInterne, + imei: foundProduct.imei, + pointDeVenteId: _userController.pointDeVenteId, // Nouveau point de vente + ); + await _productDatabase.updateProduct(updatedProduct); + // Recharger les produits pour refléter les changements + _loadProducts(); + // Afficher le dialogue de succès + _showAssignmentSuccessDialog(foundProduct); + } catch (e) { + // Fermer l'indicateur de chargement si il est encore ouvert + if (Get.isDialogOpen!) Get.back(); + + Get.snackbar( + 'Erreur', + 'Une erreur est survenue: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } finally { + setState(() { + _isAssigning = false; + }); + } + } -Future _importFromExcel() async { - try { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['xlsx', 'xls','csv'], - allowMultiple: false, + void _showAssignmentSuccessDialog(Product product) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.check_circle, color: Colors.green.shade700), + ), + const SizedBox(width: 12), + const Expanded(child: Text('Attribution réussie !')), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text('IMEI: ${product.imei}'), + Text('Prix: ${product.price.toStringAsFixed(2)} MGA'), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Produit assigné au point de vente:', + style: TextStyle(fontWeight: FontWeight.w500), + ), + const SizedBox(height: 4), + Text( + _userController.pointDeVenteDesignation, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _startPointDeVenteAssignmentScanning(); // Scanner un autre produit + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Scanner encore'), + ), + ], + ), + ); + } + + void _showAlreadyAssignedDialog(Product product) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Icon(Icons.info, color: Colors.blue.shade600), + const SizedBox(width: 8), + const Text('Déjà assigné'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text('IMEI: ${product.imei}'), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Ce produit est déjà assigné au point de vente:', + style: TextStyle(fontWeight: FontWeight.w500), + ), + const SizedBox(height: 4), + Text( + _userController.pointDeVenteDesignation, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _startPointDeVenteAssignmentScanning(); // Scanner un autre produit + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Scanner encore'), + ), + ], + ), + ); + } + + void _showProductNotFoundDialog(String scannedImei) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Icon(Icons.search_off, color: Colors.red.shade600), + const SizedBox(width: 8), + const Text('Produit non trouvé'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Aucun produit trouvé avec cet IMEI:'), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + scannedImei, + style: const TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 12), + Text( + 'Vérifiez que l\'IMEI est correct ou que le produit existe dans la base de données.', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _startPointDeVenteAssignmentScanning(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Scanner à nouveau'), + ), + ], + ), + ); + } + + Widget _buildAssignmentScanCard() { + final isMobile = MediaQuery.of(context).size.width < 600; + + return Card( + elevation: 2, + margin: const EdgeInsets.only(bottom: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.assignment, + color: Colors.orange.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Assigner produits à votre point de vente', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color.fromARGB(255, 9, 56, 95), + ), + ), + const SizedBox(height: 4), + Text( + 'Point de vente: ${_userController.pointDeVenteDesignation}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ElevatedButton.icon( + onPressed: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) + ? null + : _startPointDeVenteAssignmentScanning, + icon: (_isScanning || _isAssigning) + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.qr_code_scanner, size: 18), + label: Text((_isScanning || _isAssigning) ? 'Scan...' : 'Assigner'), + style: ElevatedButton.styleFrom( + backgroundColor: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) + ? Colors.grey + : Colors.orange.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + ), + ], + ), + ), ); + } - if (result == null || result.files.isEmpty) { - Get.snackbar('Annulé', 'Aucun fichier sélectionné'); - return; - } - + // === FONCTIONS CONSERVÉES DU CODE ORIGINAL === + // [Conservez toutes les autres méthodes du code original ici] + + // Réinitialisation de l'état d'import + void _resetImportState() { setState(() { - _isImporting = true; + _isImporting = false; _importProgress = 0.0; - _importStatusText = 'Lecture du fichier...'; + _importStatusText = ''; }); + } - final file = File(result.files.single.path!); - - if (!await file.exists()) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); - return; + Future _loadPointsDeVente() async { + try { + final points = await _productDatabase.getPointsDeVente(); + setState(() { + _pointsDeVente = points; + if (points.isNotEmpty) { + _selectedPointDeVente = points.first['nom'] as String; + } + }); + } catch (e) { + Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); } + } - setState(() { - _importProgress = 0.1; - _importStatusText = 'Vérification du fichier...'; - }); - - final bytes = await file.readAsBytes(); + Future _loadProducts() async { + setState(() => _isLoading = true); - if (bytes.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'Le fichier Excel est vide'); - return; + try { + await _productDatabase.initDatabase(); + final products = await _productDatabase.getProducts(); + final categories = await _productDatabase.getCategories(); + + setState(() { + _products = products; + _filteredProducts = products; + _categories = ['Tous', ...categories]; + _isLoading = false; + }); + } catch (e) { + setState(() => _isLoading = false); + Get.snackbar('Erreur', 'Impossible de charger les produits: $e'); } + } + void _filterProducts() { + final query = _searchController.text.toLowerCase(); + setState(() { - _importProgress = 0.2; - _importStatusText = 'Décodage du fichier Excel...'; + _filteredProducts = _products.where((product) { + final matchesSearch = product.name.toLowerCase().contains(query) || + product.description!.toLowerCase().contains(query) || + product.reference!.toLowerCase().contains(query); + + final matchesCategory = _selectedCategory == 'Tous' || + product.category == _selectedCategory; + + return matchesSearch && matchesCategory; + }).toList(); }); + } - Excel excel; - try { - excel = Excel.decodeBytes(bytes); - _debugExcelFile(excel); - } catch (e) { - _resetImportState(); - debugPrint('Erreur décodage Excel: $e'); + // 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}'; + } +Future _downloadExcelTemplate() async { + try { + final excel = Excel.createExcel(); + final sheet = excel['Sheet1']; + + // En-têtes modifiés sans DESCRIPTION et STOCK + final headers = [ + 'ID PRODUITS', // Sera ignoré lors de l'import + 'NOM DU PRODUITS', // name + 'REFERENCE PRODUITS', // reference + 'CATEGORIES PRODUITS', // category + 'MARQUE', // marque + 'RAM', // ram + 'INTERNE', // memoire_interne + 'IMEI', // imei + 'PRIX', // price + 'BOUTIQUE', // point_de_vente + ]; + + // Ajouter les en-têtes avec style + 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', + horizontalAlign: HorizontalAlign.Center, + ); + } + + // Exemples modifiés sans DESCRIPTION et STOCK + final examples = [ + [ + '1', // ID PRODUITS (sera ignoré) + 'Smartphone Galaxy S24', // NOM DU PRODUITS + 'SGS24-001', // REFERENCE PRODUITS + 'Téléphone', // CATEGORIES PRODUITS + 'Samsung', // MARQUE + '8 Go', // RAM + '256 Go', // INTERNE + '123456789012345', // IMEI + '1200.00', // PRIX + '405A', // BOUTIQUE + ], + [ + '2', // ID PRODUITS + 'iPhone 15 Pro', // NOM DU PRODUITS + 'IP15P-001', // REFERENCE PRODUITS + 'Téléphone', // CATEGORIES PRODUITS + 'Apple', // MARQUE + '8 Go', // RAM + '512 Go', // INTERNE + '987654321098765', // IMEI + '1599.00', // PRIX + '405B', // BOUTIQUE + ], + [ + '3', // ID PRODUITS + 'MacBook Pro 14"', // NOM DU PRODUITS + 'MBP14-001', // REFERENCE PRODUITS + 'Informatique', // CATEGORIES PRODUITS + 'Apple', // MARQUE + '16 Go', // RAM + '1 To', // INTERNE + '', // IMEI (vide pour un ordinateur) + '2499.00', // PRIX + 'S405A', // BOUTIQUE + ], + [ + '4', // ID PRODUITS + 'iPad Air', // NOM DU PRODUITS + 'IPA-001', // REFERENCE PRODUITS + 'Tablette', // CATEGORIES PRODUITS + 'Apple', // MARQUE + '8 Go', // RAM + '256 Go', // INTERNE + '456789123456789', // IMEI + '699.00', // PRIX + '405A', // BOUTIQUE + ], + [ + '5', // ID PRODUITS + 'Gaming Laptop ROG', // NOM DU PRODUITS + 'ROG-001', // REFERENCE PRODUITS + 'Informatique', // CATEGORIES PRODUITS + 'ASUS', // MARQUE + '32 Go', // RAM + '1 To', // INTERNE + '', // IMEI (vide) + '1899.00', // PRIX + '405B', // BOUTIQUE + ] + ]; + + // Ajouter les exemples + 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]; + + // Style pour les données (prix en gras) + if (col == 8) { // Colonne PRIX + cell.cellStyle = CellStyle( + bold: true, + ); + } + } + } + + // Ajuster la largeur des colonnes (sans DESCRIPTION et STOCK) + sheet.setColWidth(0, 12); // ID PRODUITS + sheet.setColWidth(1, 25); // NOM DU PRODUITS + sheet.setColWidth(2, 18); // REFERENCE PRODUITS + sheet.setColWidth(3, 18); // CATEGORIES PRODUITS + sheet.setColWidth(4, 15); // MARQUE + sheet.setColWidth(5, 10); // RAM + sheet.setColWidth(6, 12); // INTERNE + sheet.setColWidth(7, 18); // IMEI + sheet.setColWidth(8, 12); // PRIX + sheet.setColWidth(9, 12); // BOUTIQUE + + // Ajouter une feuille d'instructions mise à jour + final instructionSheet = excel['Instructions']; + + final instructions = [ + ['INSTRUCTIONS D\'IMPORTATION'], + [''], + ['Format des colonnes:'], + ['• ID PRODUITS: Numéro d\'identification (ignoré lors de l\'import)'], + ['• NOM DU PRODUITS: Nom du produit (OBLIGATOIRE)'], + ['• REFERENCE PRODUITS: Référence unique du produit'], + ['• CATEGORIES PRODUITS: Catégorie du produit'], + ['• MARQUE: Marque du produit'], + ['• RAM: Mémoire RAM (ex: "8 Go", "16 Go")'], + ['• INTERNE: Stockage interne (ex: "256 Go", "1 To")'], + ['• IMEI: Numéro IMEI (pour les appareils mobiles)'], + ['• PRIX: Prix du produit en euros (OBLIGATOIRE)'], + ['• BOUTIQUE: Code du point de vente'], + [''], + ['Remarques importantes:'], + ['• Les colonnes NOM DU PRODUITS et PRIX sont obligatoires'], + ['• Si CATEGORIES PRODUITS est vide, "Non catégorisé" sera utilisé'], + ['• Si REFERENCE PRODUITS est vide, une référence sera générée automatiquement'], + ['• Le stock sera automatiquement initialisé à 1 pour chaque produit'], + ['• La description sera automatiquement vide pour chaque produit'], + ['• Les colonnes peuvent être dans n\'importe quel ordre'], + ['• Vous pouvez supprimer les colonnes non utilisées'], + [''], + ['Formats acceptés:'], + ['• PRIX: 1200.00 ou 1200,00 ou 1200'], + ['• RAM/INTERNE: Texte libre (ex: "8 Go", "256 Go", "1 To")'], + ]; + + for (int i = 0; i < instructions.length; i++) { + final cell = instructionSheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: i)); + cell.value = instructions[i][0]; - 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 (i == 0) { // Titre + cell.cellStyle = CellStyle( + bold: true, + fontSize: 16, + backgroundColorHex: '#4CAF50', + fontColorHex: '#FFFFFF', + ); + } else if (instructions[i][0].startsWith('•')) { // Points de liste + cell.cellStyle = CellStyle( + italic: true, + ); + } else if (instructions[i][0].endsWith(':')) { // Sous-titres + cell.cellStyle = CellStyle( + bold: true, + backgroundColorHex: '#F5F5F5', + ); } } - - // ✨ NOUVELLE ÉTAPE: Corriger les formats de cellules - setState(() { - _importProgress = 0.25; - _importStatusText = 'Correction des formats de cellules...'; - }); - excel = _fixExcelNumberFormats(excel); - - 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...'; - }); - - final sheetName = excel.tables.keys.first; - final sheet = excel.tables[sheetName]!; + // Ajuster la largeur de la colonne instructions + instructionSheet.setColWidth(0, 80); - if (sheet.rows.isEmpty) { - _resetImportState(); - Get.snackbar('Erreur', 'La feuille Excel est vide'); - return; - } - - // Détection automatique des colonnes - final headerRow = sheet.rows[0]; - final columnMapping = _mapHeaders(headerRow); + final bytes = excel.save(); - // Vérification des colonnes obligatoires - if (!columnMapping.containsKey('name')) { - _resetImportState(); - Get.snackbar('Erreur', 'Colonne "Nom du produit" non trouvée dans le fichier'); + if (bytes == null) { + Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); return; } - if (!columnMapping.containsKey('price')) { - _resetImportState(); - Get.snackbar('Erreur', 'Colonne "Prix" non trouvée dans le fichier'); - return; - } - - int successCount = 0; - int errorCount = 0; - List errorMessages = []; - - final totalRows = sheet.rows.length - 1; + final String? outputFile = await FilePicker.platform.saveFile( + fileName: 'modele_import_produits_v3.xlsx', + allowedExtensions: ['xlsx'], + type: FileType.custom, + ); - setState(() { - _importStatusText = 'Importation en cours... (0/$totalRows)'; - }); - - for (var i = 1; i < sheet.rows.length; i++) { + if (outputFile != null) { try { - final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows); - setState(() { - _importProgress = currentProgress; - _importStatusText = 'Importation en cours... (${i - 1}/$totalRows)'; - }); - - await Future.delayed(const Duration(milliseconds: 10)); - - final row = sheet.rows[i]; - - if (row.isEmpty) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Ligne vide'); - continue; - } - - // Normalisation des données (maintenant les prix sont corrects) - final normalizedData = _normalizeRowData(row, columnMapping, i); - - // Vérification de la référence - if (normalizedData['imei'] != null) { - var existingProduct = await _productDatabase.getProductByIMEI(normalizedData['imei']); - if (existingProduct != null) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: imei déjà existante (${normalizedData['imei']})'); - continue; - } - } - - // Création du point de vente si nécessaire - int? pointDeVenteId; - if (normalizedData['point_de_vente'] != null) { - pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(normalizedData['point_de_vente']); - if (pointDeVenteId == null) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: Impossible de créer le point de vente ${normalizedData['point_de_vente']}'); - continue; - } - } - - setState(() { - _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; - }); - - // Création du produit avec les données normalisées - final product = Product( - name: normalizedData['name'], - price: normalizedData['price'], - image: '', - category: normalizedData['category'], - description: normalizedData['description'], - stock: normalizedData['stock'], - qrCode: '', - reference: normalizedData['reference'], - marque: normalizedData['marque'], - ram: normalizedData['ram'], - memoireInterne: normalizedData['memoire_interne'], - imei: normalizedData['imei'], - pointDeVenteId: pointDeVenteId, + await File(outputFile).writeAsBytes(bytes); + Get.snackbar( + 'Succès', + 'Modèle téléchargé avec succès\n$outputFile\n\nConsultez l\'onglet "Instructions" pour plus d\'informations.', + duration: const Duration(seconds: 6), + backgroundColor: Colors.green, + colorText: Colors.white, ); - - await _productDatabase.createProduct(product); - successCount++; - } catch (e) { - errorCount++; - errorMessages.add('Ligne ${i + 1}: ${e.toString()}'); - debugPrint('Erreur ligne ${i + 1}: $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'); + } +} - setState(() { - _importProgress = 1.0; - _importStatusText = 'Finalisation...'; - }); - - await Future.delayed(const Duration(milliseconds: 500)); - - _resetImportState(); - - String message = '$successCount produits importés avec succès'; - if (errorCount > 0) { - message += ', $errorCount erreurs'; - - if (errorMessages.length <= 5) { - message += ':\n${errorMessages.join('\n')}'; - } + +// Détecter si une valeur semble être un format de date/temps suspect +bool _isSuspiciousDateFormat(dynamic value) { + if (value == null) return false; + + String valueStr = value.toString(); + + // Détecter les formats de date suspects qui devraient être des nombres + if (valueStr.contains('-') && valueStr.contains(':')) { + // Format DateTime détecté + print('🔍 Format DateTime suspect: $valueStr'); + return true; + } + + // Détecter les très grands nombres (timestamps en millisecondes) + if (valueStr.length > 10 && !valueStr.contains('.')) { + double? numValue = double.tryParse(valueStr); + if (numValue != null && numValue > 10000000000) { + print('🔍 Grand nombre suspect: $valueStr'); + return true; } - - Get.snackbar( - 'Importation terminée', - message, - duration: const Duration(seconds: 6), - colorText: Colors.white, - backgroundColor: successCount > 0 ? Colors.green : Colors.orange, - ); + } + + return false; +} +// Identifier les colonnes qui devraient contenir des nombres +List _identifyNumberColumns(List headerRow) { + List numberColumns = []; + + for (int i = 0; i < headerRow.length; i++) { + if (headerRow[i]?.value == null) continue; - // Recharger la liste des produits après importation - _loadProducts(); - print(errorMessages); - } catch (e) { - _resetImportState(); - Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); - debugPrint('Erreur générale import Excel: $e'); + String header = headerRow[i]!.value.toString().trim().toUpperCase(); + + // Identifier les en-têtes qui correspondent à des valeurs numériques + if (_isNumericHeader(header)) { + numberColumns.add(i); + print('📊 Colonne numérique: "$header" (index $i)'); + } + } + + return numberColumns; +} +// Vérifier si un en-tête correspond à une colonne numérique +bool _isNumericHeader(String header) { + List numericHeaders = [ + 'PRIX', 'PRICE', 'COST', 'COUT', + 'MONTANT', 'AMOUNT', 'TOTAL', + 'QUANTITE', 'QUANTITY', 'QTE', + 'STOCK', 'NOMBRE', 'NUMBER', + 'TAILLE', 'SIZE', 'POIDS', 'WEIGHT', + 'RAM', 'MEMOIRE', 'STORAGE', 'STOCKAGE' + ]; + + return numericHeaders.any((keyword) => header.contains(keyword)); +} +// Fonction de débogage pour analyser le fichier Excel +void _debugExcelFile(Excel excel) { + print('=== DEBUG EXCEL FILE ==='); + print('Nombre de feuilles: ${excel.tables.length}'); + + for (var sheetName in excel.tables.keys) { + print('Feuille: $sheetName'); + var sheet = excel.tables[sheetName]!; + print('Nombre de lignes: ${sheet.rows.length}'); + + if (sheet.rows.isNotEmpty) { + print('En-têtes (première ligne):'); + for (int i = 0; i < sheet.rows[0].length; i++) { + var cellValue = sheet.rows[0][i]?.value; + print(' Colonne $i: "$cellValue" (${cellValue.runtimeType})'); + } + + if (sheet.rows.length > 1) { + print('Première ligne de données:'); + for (int i = 0; i < sheet.rows[1].length; i++) { + var cellValue = sheet.rows[1][i]?.value; + print(' Colonne $i: "$cellValue"'); + } + } + } } + print('=== FIN DEBUG ==='); } - - -// Ajoutez ce widget dans votre méthode build, par exemple dans la partie supérieure -Widget _buildImportProgressIndicator() { - if (!_isImporting) return const SizedBox.shrink(); +Excel _fixExcelNumberFormats(Excel excel) { + print('🔧 Correction des formats de cellules Excel...'); - return 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, - ), - ), - ], - ), - ); -} -//============================================================================================================================= - Future _loadProducts() async { - setState(() => _isLoading = true); + for (var sheetName in excel.tables.keys) { + print('📋 Traitement de la feuille: $sheetName'); + var sheet = excel.tables[sheetName]!; - try { - await _productDatabase.initDatabase(); - final products = await _productDatabase.getProducts(); - final categories = await _productDatabase.getCategories(); + if (sheet.rows.isEmpty) continue; + + // Analyser la première ligne pour identifier les colonnes de prix/nombres + List numberColumns = _identifyNumberColumns(sheet.rows[0]); + print('🔢 Colonnes numériques détectées: $numberColumns'); + + // Corriger chaque ligne de données (ignorer la ligne d'en-tête) + for (int rowIndex = 1; rowIndex < sheet.rows.length; rowIndex++) { + var row = sheet.rows[rowIndex]; - setState(() { - _products = products; - _filteredProducts = products; - _categories = ['Tous', ...categories]; - _isLoading = false; - }); - } catch (e) { - setState(() => _isLoading = false); - Get.snackbar('Erreur', 'Impossible de charger les produits: $e'); + for (int colIndex in numberColumns) { + if (colIndex < row.length && row[colIndex] != null) { + var cell = row[colIndex]!; + var originalValue = cell.value; + + // Détecter si la cellule a un format de date/temps suspect + if (_isSuspiciousDateFormat(originalValue)) { + print('⚠️ Cellule suspecte détectée en ($rowIndex, $colIndex): $originalValue'); + + // Convertir la valeur corrompue en nombre standard + var correctedValue = _convertSuspiciousValue(originalValue); + if (correctedValue != null) { + print('✅ Correction: $originalValue → $correctedValue'); + + // Créer une nouvelle cellule avec la valeur corrigée + excel.updateCell(sheetName, + CellIndex.indexByColumnRow(columnIndex: colIndex, rowIndex: rowIndex), + correctedValue + ); + } + } + } + } } } - - void _filterProducts() { - final query = _searchController.text.toLowerCase(); - - setState(() { - _filteredProducts = _products.where((product) { - final matchesSearch = product.name.toLowerCase().contains(query) || - product.description!.toLowerCase().contains(query) || - product.reference!.toLowerCase().contains(query); - - final matchesCategory = _selectedCategory == 'Tous' || - product.category == _selectedCategory; - - return matchesSearch && matchesCategory; - }).toList(); - }); + + print('✅ Correction des formats terminée'); + return excel; +} +double? _convertSuspiciousValue(dynamic suspiciousValue) { + if (suspiciousValue == null) return null; + + String valueStr = suspiciousValue.toString(); + + // Cas 1: Format DateTime (ex: "3953-06-05T00:00:00.000") + if (valueStr.contains('-') && valueStr.contains(':')) { + return _convertDateTimeToNumber(valueStr); } - - // 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}'; + + // Cas 2: Grand nombre (ex: "39530605000000") + if (valueStr.length > 10) { + return _convertLargeNumberToPrice(valueStr); } + + return null; +} - // Méthode pour générer et sauvegarder le QR Code - Future _generateAndSaveQRCode(String reference) async { - final qrUrl = 'https://stock.guycom.mg/$reference'; +// Convertir un format DateTime en nombre +double? _convertDateTimeToNumber(String dateTimeStr) { + try { + print('🔄 Conversion DateTime: $dateTimeStr'); - 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'; + // Nettoyer la chaîne + String cleanDateString = dateTimeStr.replaceAll('+', ''); + final dateTime = DateTime.parse(cleanDateString); - 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'); + // Excel epoch: 1er janvier 1900 + final excelEpoch = DateTime(1900, 1, 1); + + // Calculer le nombre de jours depuis l'epoch Excel + final daysDifference = dateTime.difference(excelEpoch).inDays; + + // Appliquer la correction pour le bug Excel (+2) + final correctedValue = daysDifference + 2; + + print('→ Jours calculés: $daysDifference → Corrigé: $correctedValue'); + + if (correctedValue > 0 && correctedValue < 100000000) { + return correctedValue.toDouble(); } - - return path; + } catch (e) { + print('❌ Erreur conversion DateTime: $e'); } - - void _showAddProductDialog() { - final nameController = TextEditingController(); - final priceController = TextEditingController(); - final stockController = TextEditingController(); - final descriptionController = TextEditingController(); - final imageController = TextEditingController(); - final referenceController = TextEditingController(); - final marqueController = TextEditingController(); - final ramController = TextEditingController(); - final memoireInterneController = TextEditingController(); - final imeiController = TextEditingController(); - final newPointDeVenteController = TextEditingController(); - String? selectedPointDeVente; - List> pointsDeVente = []; - bool isLoadingPoints = true; - String selectedCategory = _predefinedCategories.last; // 'Non catégorisé' par défaut - File? pickedImage; - String? qrPreviewData; - bool autoGenerateReference = true; - bool showAddNewPoint = false; + return null; +} - // Fonction pour mettre à jour le QR preview - void updateQrPreview() { - if (nameController.text.isNotEmpty) { - final reference = autoGenerateReference ? _generateUniqueReference() : referenceController.text.trim(); - if (reference.isNotEmpty) { - qrPreviewData = 'https://stock.guycom.mg/$reference'; - } else { - qrPreviewData = null; +// Convertir un grand nombre en prix +double? _convertLargeNumberToPrice(String largeNumberStr) { + try { + print('🔄 Conversion grand nombre: $largeNumberStr'); + + double? numValue = double.tryParse(largeNumberStr); + if (numValue == null) return null; + + // Si le nombre se termine par 000000 (microsecondes), les supprimer + if (largeNumberStr.endsWith('000000') && largeNumberStr.length > 10) { + String withoutMicros = largeNumberStr.substring(0, largeNumberStr.length - 6); + double? daysSinceExcel = double.tryParse(withoutMicros); + + if (daysSinceExcel != null && daysSinceExcel > 1000 && daysSinceExcel < 10000000) { + // Appliquer la correction du décalage Excel (+2) + double correctedPrice = daysSinceExcel + 2; + print('→ Conversion: $largeNumberStr → $withoutMicros → $correctedPrice'); + return correctedPrice; } - } else { - qrPreviewData = null; } - } - - // Charger les points de vente - Future loadPointsDeVente(StateSetter setDialogState) async { - try { - final result = await _productDatabase.getPointsDeVente(); - setDialogState(() { - pointsDeVente = result; - isLoadingPoints = false; - if (result.isNotEmpty && selectedPointDeVente == null) { - selectedPointDeVente = result.first['nom'] as String; - } - }); - } catch (e) { - setDialogState(() { - isLoadingPoints = false; - }); - Get.snackbar('Erreur', 'Impossible de charger les points de vente: $e'); + + // Table de correspondance pour les cas connus + Map knownConversions = { + '39530605000000': 750000, + '170950519000000': 5550000, + }; + + if (knownConversions.containsKey(largeNumberStr)) { + double realPrice = knownConversions[largeNumberStr]!; + print('→ Conversion via table: $largeNumberStr → $realPrice'); + return realPrice; } + + } catch (e) { + print('❌ Erreur conversion grand nombre: $e'); } + + return null; +} + +void _showExcelCompatibilityError() { Get.dialog( AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Icon(Icons.add_shopping_cart, color: Colors.green.shade700), - ), - const SizedBox(width: 12), - const Text('Ajouter un produit'), - ], + 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' ), - content: Container( - width: 600, - constraints: const BoxConstraints(maxHeight: 600), - child: SingleChildScrollView( - child: StatefulBuilder( - builder: (context, setDialogState) { - // Charger les points de vente une seule fois - if (isLoadingPoints && pointsDeVente.isEmpty) { - WidgetsBinding.instance.addPostFrameCallback((_) { - loadPointsDeVente(setDialogState); - }); - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Champs obligatoires - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.red.shade200), - ), - child: Row( - children: [ - Icon(Icons.info, color: Colors.red.shade600, size: 16), - const SizedBox(width: 8), - const Text( - 'Les champs marqués d\'un * sont obligatoires', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), - ), - ], - ), - ), - const SizedBox(height: 16), - - // Section Point de vente améliorée - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.teal.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.teal.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.store, color: Colors.teal.shade700), - const SizedBox(width: 8), - Text( - 'Point de vente', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.teal.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - - if (isLoadingPoints) - const Center(child: CircularProgressIndicator()) - else if (pointsDeVente.isEmpty) - Column( - children: [ - Text( - 'Aucun point de vente trouvé. Créez-en un nouveau.', - style: TextStyle(color: Colors.grey.shade600), - ), - const SizedBox(height: 8), - TextField( - controller: newPointDeVenteController, - decoration: const InputDecoration( - labelText: 'Nom du nouveau point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.add_business), - filled: true, - fillColor: Colors.white, - ), - ), - ], - ) - else - Column( - children: [ - if (!showAddNewPoint) ...[ - DropdownButtonFormField( - value: selectedPointDeVente, - items: pointsDeVente.map((point) { - return DropdownMenuItem( - value: point['nom'] as String, - child: Text(point['nom'] as String), - ); - }).toList(), - onChanged: (value) { - setDialogState(() => selectedPointDeVente = value); - }, - decoration: const InputDecoration( - labelText: 'Sélectionner un point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.store), - filled: true, - fillColor: Colors.white, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - TextButton.icon( - onPressed: () { - setDialogState(() { - showAddNewPoint = true; - newPointDeVenteController.clear(); - }); - }, - icon: const Icon(Icons.add, size: 16), - label: const Text('Ajouter nouveau point'), - style: TextButton.styleFrom( - foregroundColor: Colors.teal.shade700, - ), - ), - const Spacer(), - TextButton.icon( - onPressed: () => loadPointsDeVente(setDialogState), - icon: const Icon(Icons.refresh, size: 16), - label: const Text('Actualiser'), - ), - ], - ), - ], - - if (showAddNewPoint) ...[ - TextField( - controller: newPointDeVenteController, - decoration: const InputDecoration( - labelText: 'Nom du nouveau point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.add_business), - filled: true, - fillColor: Colors.white, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - TextButton( - onPressed: () { - setDialogState(() { - showAddNewPoint = false; - newPointDeVenteController.clear(); - }); - }, - child: const Text('Annuler'), - ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: () async { - final nom = newPointDeVenteController.text.trim(); - if (nom.isNotEmpty) { - try { - final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); - if (id != null) { - setDialogState(() { - showAddNewPoint = false; - selectedPointDeVente = nom; - newPointDeVenteController.clear(); - }); - // Recharger la liste - await loadPointsDeVente(setDialogState); - Get.snackbar( - 'Succès', - 'Point de vente "$nom" créé avec succès', - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } - } catch (e) { - Get.snackbar('Erreur', 'Impossible de créer le point de vente: $e'); - } - } - }, - icon: const Icon(Icons.save, size: 16), - label: const Text('Créer'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.teal, - foregroundColor: Colors.white, - ), - ), - ], - ), - ], - ], - ), - ], - ), - ), - const SizedBox(height: 16), + 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 _importFromExcel() async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['xlsx', 'xls','csv'], + allowMultiple: false, + ); - // Nom du produit - TextField( - controller: nameController, - decoration: InputDecoration( - labelText: 'Nom du produit *', - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.shopping_bag), - filled: true, - fillColor: Colors.grey.shade50, - ), - onChanged: (value) { - setDialogState(() { - updateQrPreview(); - }); - }, - ), - const SizedBox(height: 16), - - // Prix et Stock sur la même ligne - Row( - children: [ - Expanded( - child: TextField( - controller: priceController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - decoration: InputDecoration( - labelText: 'Prix (MGA) *', - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.attach_money), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: stockController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Stock initial', - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.inventory), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - - // Catégorie - DropdownButtonFormField( - value: selectedCategory, - items: _predefinedCategories.map((category) => - DropdownMenuItem(value: category, child: Text(category))).toList(), - onChanged: (value) { - setDialogState(() => selectedCategory = value!); - }, - decoration: InputDecoration( - labelText: 'Catégorie', - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.category), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - const SizedBox(height: 16), - - // Description - TextField( - controller: descriptionController, - maxLines: 3, - decoration: InputDecoration( - labelText: 'Description', - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.description), - filled: true, - fillColor: Colors.grey.shade50, - ), - ), - const SizedBox(height: 16), - - // Section Référence - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.purple.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.purple.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.confirmation_number, color: Colors.purple.shade700), - const SizedBox(width: 8), - Text( - 'Référence du produit', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.purple.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Checkbox( - value: autoGenerateReference, - onChanged: (value) { - setDialogState(() { - autoGenerateReference = value!; - updateQrPreview(); - }); - }, - ), - const Text('Générer automatiquement'), - ], - ), - const SizedBox(height: 8), - if (!autoGenerateReference) - TextField( - controller: referenceController, - decoration: const InputDecoration( - labelText: 'Référence *', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.tag), - filled: true, - fillColor: Colors.white, - ), - onChanged: (value) { - setDialogState(() { - updateQrPreview(); - }); - }, - ), - if (autoGenerateReference) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - 'Référence générée automatiquement', - style: TextStyle(color: Colors.grey.shade700), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - - // Nouveaux champs (Marque, RAM, Mémoire interne, IMEI) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.orange.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.orange.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.memory, color: Colors.orange.shade700), - const SizedBox(width: 8), - Text( - 'Spécifications techniques', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.orange.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - TextField( - controller: marqueController, - decoration: const InputDecoration( - labelText: 'Marque', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.branding_watermark), - filled: true, - fillColor: Colors.white, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: TextField( - controller: ramController, - decoration: const InputDecoration( - labelText: 'RAM', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.memory), - filled: true, - fillColor: Colors.white, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: memoireInterneController, - decoration: const InputDecoration( - labelText: 'Mémoire interne', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.storage), - filled: true, - fillColor: Colors.white, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - TextField( - controller: imeiController, - decoration: const InputDecoration( - labelText: 'IMEI (pour téléphones)', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.smartphone), - filled: true, - fillColor: Colors.white, - ), - ), - ], - ), - ), - const SizedBox(height: 16), - - // Section Image + if (result == null || result.files.isEmpty) { + Get.snackbar('Annulé', 'Aucun fichier sélectionné'); + return; + } + + setState(() { + _isImporting = true; + _importProgress = 0.0; + _importStatusText = 'Lecture du fichier...'; + }); + + final file = File(result.files.single.path!); + + 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(); + + 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 { + excel = Excel.decodeBytes(bytes); + _debugExcelFile(excel); + } 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; + } + } + + // ✨ NOUVELLE ÉTAPE: Corriger les formats de cellules + setState(() { + _importProgress = 0.25; + _importStatusText = 'Correction des formats de cellules...'; + }); + + excel = _fixExcelNumberFormats(excel); + + 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...'; + }); + + 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; + } + + // Détection automatique des colonnes + final headerRow = sheet.rows[0]; + final columnMapping = _mapHeaders(headerRow); + + // Vérification des colonnes obligatoires + if (!columnMapping.containsKey('name')) { + _resetImportState(); + Get.snackbar('Erreur', 'Colonne "Nom du produit" non trouvée dans le fichier'); + return; + } + + if (!columnMapping.containsKey('price')) { + _resetImportState(); + Get.snackbar('Erreur', 'Colonne "Prix" non trouvée dans le fichier'); + return; + } + + int successCount = 0; + int errorCount = 0; + List errorMessages = []; + + final totalRows = sheet.rows.length - 1; + + setState(() { + _importStatusText = 'Importation en cours... (0/$totalRows)'; + }); + + for (var i = 1; i < sheet.rows.length; i++) { + try { + final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows); + setState(() { + _importProgress = currentProgress; + _importStatusText = 'Importation en cours... (${i - 1}/$totalRows)'; + }); + + await Future.delayed(const Duration(milliseconds: 10)); + + final row = sheet.rows[i]; + + if (row.isEmpty) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Ligne vide'); + continue; + } + + // Normalisation des données (maintenant les prix sont corrects) + final normalizedData = _normalizeRowData(row, columnMapping, i); + + // Vérification de la référence + if (normalizedData['imei'] != null) { + var existingProduct = await _productDatabase.getProductByIMEI(normalizedData['imei']); + if (existingProduct != null) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: imei déjà existante (${normalizedData['imei']})'); + continue; + } + } + + // Création du point de vente si nécessaire + int? pointDeVenteId; + if (normalizedData['point_de_vente'] != null) { + pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(normalizedData['point_de_vente']); + if (pointDeVenteId == null) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Impossible de créer le point de vente ${normalizedData['point_de_vente']}'); + continue; + } + } + + setState(() { + _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; + }); + + // Création du produit avec les données normalisées + final product = Product( + name: normalizedData['name'], + price: normalizedData['price'], + image: '', + category: normalizedData['category'], + description: normalizedData['description'], + stock: normalizedData['stock'], + qrCode: '', + reference: normalizedData['reference'], + marque: normalizedData['marque'], + ram: normalizedData['ram'], + memoireInterne: normalizedData['memoire_interne'], + imei: normalizedData['imei'], + pointDeVenteId: pointDeVenteId, + ); + + await _productDatabase.createProduct(product); + successCount++; + + } catch (e) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: ${e.toString()}'); + debugPrint('Erreur ligne ${i + 1}: $e'); + } + } + + setState(() { + _importProgress = 1.0; + _importStatusText = 'Finalisation...'; + }); + + await Future.delayed(const Duration(milliseconds: 500)); + + _resetImportState(); + + String message = '$successCount produits importés avec succès'; + if (errorCount > 0) { + message += ', $errorCount erreurs'; + + 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, + ); + + // Recharger la liste des produits après importation + _loadProducts(); + print(errorMessages); + } catch (e) { + _resetImportState(); + Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); + debugPrint('Erreur générale import Excel: $e'); + } +} + + +// Ajoutez ce widget dans votre méthode build, par exemple dans la partie supérieure +Widget _buildImportProgressIndicator() { + if (!_isImporting) return const SizedBox.shrink(); + + return 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, + ), + ), + ], + ), + ); +} +//============================================================================================================================= + + + + + + Widget _buildProductCardContent(Product product, String pointDeVenteText) { + final isCurrentUserPointDeVente = product.pointDeVenteId == _userController.pointDeVenteId; + + return InkWell( + onTap: () => _showProductDetailsDialog(context, product), + child: Card( + margin: const EdgeInsets.all(8), + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: isCurrentUserPointDeVente + ? BorderSide(color: Colors.orange.shade300, width: 2) + : BorderSide.none, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + // Image du produit Container( - padding: const EdgeInsets.all(12), + width: 80, + height: 80, decoration: BoxDecoration( - color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: product.image!.isNotEmpty + ? Image.file( + File(product.image!), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.image, size: 40), + ) + : const Icon(Icons.image, size: 40), ), + ), + const SizedBox(width: 16), + + // Informations du produit + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Icon(Icons.image, color: Colors.blue.shade700), - const SizedBox(width: 8), - Text( - 'Image du produit (optionnel)', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.blue.shade700, - ), - ), - ], + Text( + product.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), - const SizedBox(height: 12), + const SizedBox(height: 4), + Text( + '${NumberFormat('#,##0').format(product.price)} MGA', + style: const TextStyle( + fontSize: 16, + color: Colors.green, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), Row( children: [ - Expanded( - child: TextField( - controller: imageController, - decoration: const InputDecoration( - labelText: 'Chemin de l\'image', - border: OutlineInputBorder(), - isDense: true, + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + product.category, + style: TextStyle( + fontSize: 12, + color: Colors.blue.shade800, ), - readOnly: true, ), ), const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: () async { - final result = await FilePicker.platform.pickFiles(type: FileType.image); - if (result != null && result.files.single.path != null) { - setDialogState(() { - pickedImage = File(result.files.single.path!); - imageController.text = pickedImage!.path; - }); - } - }, - icon: const Icon(Icons.folder_open, size: 16), - label: const Text('Choisir'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(12), + Text( + 'Stock: ${product.stock}', + style: TextStyle( + fontSize: 12, + color: product.stock! > 0 ? Colors.green : Colors.red, + fontWeight: FontWeight.w500, ), ), ], ), - const SizedBox(height: 12), - - // Aperçu de l'image - if (pickedImage != null) - Center( - child: Container( - height: 100, - width: 100, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.file(pickedImage!, fit: BoxFit.cover), - ), + // Afficher l'IMEI si disponible + if (product.imei != null && product.imei!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + 'IMEI: ${product.imei}', + style: TextStyle( + fontSize: 10, + color: Colors.grey.shade600, + fontFamily: 'monospace', ), ), + ], ], ), ), - const SizedBox(height: 16), - // Aperçu QR Code - if (qrPreviewData != null) + // Actions + Column( + children: [ + // Bouton d'assignation rapide si l'utilisateur a un point de vente + if (_userController.pointDeVenteId > 0 && !isCurrentUserPointDeVente && product.imei != null) + IconButton( + onPressed: () => _assignProductDirectly(product), + icon: Icon(Icons.assignment, color: Colors.orange.shade700), + tooltip: 'Assigner à mon point de vente', + ), + IconButton( + onPressed: () => _showQRCode(product), + icon: const Icon(Icons.qr_code_2, color: Colors.blue), + tooltip: 'Voir QR Code', + ), + IconButton( + onPressed: () => _editProduct(product), + icon: const Icon(Icons.edit, color: Colors.orange), + tooltip: 'Modifier', + ), + IconButton( + onPressed: () => _deleteProduct(product), + icon: const Icon(Icons.delete, color: Colors.red), + tooltip: 'Supprimer', + ), + ], + ), + ], + ), + const SizedBox(height: 8), + // Ligne du point de vente avec indication visuelle + Row( + children: [ + Icon( + Icons.store, + size: 16, + color: isCurrentUserPointDeVente ? Colors.orange.shade700 : Colors.grey + ), + const SizedBox(width: 4), + Text( + 'Point de vente: $pointDeVenteText', + style: TextStyle( + fontSize: 12, + color: isCurrentUserPointDeVente ? Colors.orange.shade700 : Colors.grey, + fontWeight: isCurrentUserPointDeVente ? FontWeight.w600 : FontWeight.normal, + ), + ), + const Spacer(), + if (isCurrentUserPointDeVente) Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.green.shade50, + color: Colors.orange.shade100, borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.green.shade200), ), - child: Column( - children: [ - Row( - children: [ - Icon(Icons.qr_code_2, color: Colors.green.shade700), - const SizedBox(width: 8), - Text( - 'Aperçu du QR Code', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.green.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - Center( - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - child: QrImageView( - data: qrPreviewData!, - version: QrVersions.auto, - size: 80, - backgroundColor: Colors.white, - ), - ), - ), - const SizedBox(height: 8), - Text( - 'Réf: ${autoGenerateReference ? _generateUniqueReference() : referenceController.text.trim()}', - style: const TextStyle(fontSize: 10, color: Colors.grey), - ), - ], + child: Text( + 'Mon PV', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.orange.shade700, + ), ), ), + if (pointDeVenteText == 'Non spécifié') + TextButton( + onPressed: () => _showAddPointDeVenteDialog(product), + child: const Text('Ajouter', style: TextStyle(fontSize: 12)), + ), ], - ); - }, + ), + ], ), ), ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton.icon( - onPressed: () async { - final name = nameController.text.trim(); - final price = double.tryParse(priceController.text.trim()) ?? 0.0; - final stock = int.tryParse(stockController.text.trim()) ?? 0; - - if (name.isEmpty || price <= 0) { - Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); - return; - } - - // Vérification de la référence - String finalReference; - if (autoGenerateReference) { - finalReference = _generateUniqueReference(); - } else { - finalReference = referenceController.text.trim(); - if (finalReference.isEmpty) { - Get.snackbar('Erreur', 'La référence est obligatoire'); - return; - } - - // Vérifier si la référence existe déjà - final existingProduct = await _productDatabase.getProductByReference(finalReference); - if (existingProduct != null) { - Get.snackbar('Erreur', 'Cette référence existe déjà'); - return; - } - } + ); + } - // Gérer le point de vente - int? pointDeVenteId; - String? finalPointDeVenteNom; - - if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) { - // Nouveau point de vente à créer - finalPointDeVenteNom = newPointDeVenteController.text.trim(); - } else if (selectedPointDeVente != null) { - // Point de vente existant sélectionné - finalPointDeVenteNom = selectedPointDeVente; - } - - if (finalPointDeVenteNom != null) { - pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(finalPointDeVenteNom); - } - - try { - // Générer le QR code - final qrPath = await _generateAndSaveQRCode(finalReference); - - final product = Product( - name: name, - price: price, - image: imageController.text, - category: selectedCategory, - description: descriptionController.text.trim(), - stock: stock, - qrCode: qrPath, - reference: finalReference, - marque: marqueController.text.trim(), - ram: ramController.text.trim(), - memoireInterne: memoireInterneController.text.trim(), - imei: imeiController.text.trim(), - pointDeVenteId: pointDeVenteId, - ); - - await _productDatabase.createProduct(product); - Get.back(); - Get.snackbar( - 'Succès', - 'Produit ajouté avec succès!\nRéférence: $finalReference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 4), - icon: const Icon(Icons.check_circle, color: Colors.white), - ); - _loadProducts(); - _loadPointsDeVente(); // Recharger aussi les points de vente - } catch (e) { - Get.snackbar('Erreur', 'Ajout du produit échoué: $e'); - } - }, - icon: const Icon(Icons.save), - label: const Text('Ajouter le produit'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - ), + // Assignation directe d'un produit (via le bouton sur la carte) + Future _assignProductDirectly(Product product) async { + if (_isAssigning) return; + + // Confirmer l'action + final confirm = await Get.dialog( + AlertDialog( + title: const Text('Confirmer l\'assignation'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Voulez-vous assigner ce produit à votre point de vente ?'), + const SizedBox(height: 12), + Text( + product.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text('IMEI: ${product.imei}'), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Point de vente: ${_userController.pointDeVenteDesignation}', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + ), + ], ), - ], - ), - ); -} + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () => Get.back(result: true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Assigner'), + ), + ], + ), + ); + + if (confirm != true) return; + + setState(() { + _isAssigning = true; + }); + + try { + // Assigner le point de vente de l'utilisateur au produit + final updatedProduct = Product( + id: product.id, + name: product.name, + price: product.price, + image: product.image, + category: product.category, + description: product.description, + stock: product.stock, + qrCode: product.qrCode, + reference: product.reference, + marque: product.marque, + ram: product.ram, + memoireInterne: product.memoireInterne, + imei: product.imei, + pointDeVenteId: _userController.pointDeVenteId, + ); + + await _productDatabase.updateProduct(updatedProduct); - void _showQRCode(Product product) { + // Recharger les produits + _loadProducts(); + + Get.snackbar( + 'Succès', + 'Produit "${product.name}" assigné au point de vente ${_userController.pointDeVenteDesignation}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + icon: const Icon(Icons.check_circle, color: Colors.white), + ); + + } catch (e) { + Get.snackbar( + 'Erreur', + 'Impossible d\'assigner le produit: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } finally { + setState(() { + _isAssigning = false; + }); + } + } + + // Méthodes placeholder pour les fonctions manquantes + void _showQRCode(Product product) { // État pour contrôler le type d'affichage (true = URL complète, false = référence seulement) RxBool showFullUrl = true.obs; @@ -2249,7 +2758,8 @@ Widget _buildImportProgressIndicator() { }), ); } -Future _generatePDF(Product product, String qrUrl) async { + + Future _generatePDF(Product product, String qrUrl) async { final pdf = pw.Document(); pdf.addPage( @@ -2322,7 +2832,11 @@ Future _generatePDF(Product product, String qrUrl) async { try { final result = await _productDatabase.getPointsDeVente(); setDialogState(() { - pointsDeVente = result; + // Ajouter l'option "Aucun" à la liste + pointsDeVente = [ + {'id': null, 'nom': 'Aucun'}, + ...result + ]; isLoadingPoints = false; // Définir le point de vente actuel du produit if (product.pointDeVenteId != null) { @@ -2333,10 +2847,8 @@ Future _generatePDF(Product product, String qrUrl) async { if (currentPointDeVente.isNotEmpty) { selectedPointDeVente = currentPointDeVente['nom'] as String; } - } - // Si aucun point de vente sélectionné et qu'il y en a des disponibles - if (selectedPointDeVente == null && result.isNotEmpty) { - selectedPointDeVente = result.first['nom'] as String; + } else { + selectedPointDeVente = 'Aucun'; // Si aucun point de vente, sélectionner "Aucun" } }); } catch (e) { @@ -2372,7 +2884,6 @@ Future _generatePDF(Product product, String qrUrl) async { child: SingleChildScrollView( child: StatefulBuilder( builder: (context, setDialogState) { - // Charger les points de vente une seule fois if (isLoadingPoints && pointsDeVente.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { loadPointsDeVente(setDialogState); @@ -2432,26 +2943,6 @@ Future _generatePDF(Product product, String qrUrl) async { if (isLoadingPoints) const Center(child: CircularProgressIndicator()) - else if (pointsDeVente.isEmpty) - Column( - children: [ - Text( - 'Aucun point de vente trouvé. Créez-en un nouveau.', - style: TextStyle(color: Colors.grey.shade600), - ), - const SizedBox(height: 8), - TextField( - controller: newPointDeVenteController, - decoration: const InputDecoration( - labelText: 'Nom du nouveau point de vente', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.add_business), - filled: true, - fillColor: Colors.white, - ), - ), - ], - ) else Column( children: [ @@ -2501,6 +2992,52 @@ Future _generatePDF(Product product, String qrUrl) async { ), ], + if (showAddNewPoint) ...[ + DropdownButtonFormField( + value: selectedPointDeVente, + items: pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + setDialogState(() => selectedPointDeVente = value); + }, + decoration: const InputDecoration( + labelText: 'Sélectionner un point de vente', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.store), + filled: true, + fillColor: Colors.white, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + TextButton.icon( + onPressed: () { + setDialogState(() { + showAddNewPoint = true; + newPointDeVenteController.clear(); + }); + }, + icon: const Icon(Icons.add, size: 16), + label: const Text('Ajouter nouveau point'), + style: TextButton.styleFrom( + foregroundColor: Colors.teal.shade700, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: () => loadPointsDeVente(setDialogState), + icon: const Icon(Icons.refresh, size: 16), + label: const Text('Actualiser'), + ), + ], + ), + ], + if (showAddNewPoint) ...[ TextField( controller: newPointDeVenteController, @@ -2709,798 +3246,462 @@ Future _generatePDF(Product product, String qrUrl) async { borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange.shade200), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.memory, color: Colors.orange.shade700), - const SizedBox(width: 8), - Text( - 'Spécifications techniques', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.orange.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - TextField( - controller: marqueController, - decoration: const InputDecoration( - labelText: 'Marque', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.branding_watermark), - filled: true, - fillColor: Colors.white, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: TextField( - controller: ramController, - decoration: const InputDecoration( - labelText: 'RAM', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.memory), - filled: true, - fillColor: Colors.white, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextField( - controller: memoireInterneController, - decoration: const InputDecoration( - labelText: 'Mémoire interne', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.storage), - filled: true, - fillColor: Colors.white, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - TextField( - controller: imeiController, - decoration: const InputDecoration( - labelText: 'IMEI (pour téléphones)', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.smartphone), - filled: true, - fillColor: Colors.white, - ), - ), - ], - ), - ), - const SizedBox(height: 16), - - // Section Image - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.image, color: Colors.blue.shade700), - const SizedBox(width: 8), - Text( - 'Image du produit', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.blue.shade700, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: TextField( - controller: imageController, - decoration: const InputDecoration( - labelText: 'Chemin de l\'image', - border: OutlineInputBorder(), - isDense: true, - ), - readOnly: true, - ), - ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: () async { - final result = await FilePicker.platform.pickFiles(type: FileType.image); - if (result != null && result.files.single.path != null) { - setDialogState(() { - pickedImage = File(result.files.single.path!); - imageController.text = pickedImage!.path; - }); - } - }, - icon: const Icon(Icons.folder_open, size: 16), - label: const Text('Choisir'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(12), - ), - ), - ], - ), - const SizedBox(height: 12), - - // Aperçu de l'image - Center( - child: Container( - height: 100, - width: 100, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: pickedImage != null - ? Image.file(pickedImage!, fit: BoxFit.cover) - : (product.image != null && product.image!.isNotEmpty - ? Image.file( - File(product.image!), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - const Icon(Icons.image, size: 50), - ) - : const Icon(Icons.image, size: 50)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.memory, color: Colors.orange.shade700), + const SizedBox(width: 8), + Text( + 'Spécifications techniques', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: marqueController, + decoration: const InputDecoration( + labelText: 'Marque', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.branding_watermark), + filled: true, + fillColor: Colors.white, ), ), - ], - ), - ), - const SizedBox(height: 16), - - // Aperçu QR Code - if (qrPreviewData != null) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.green.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.green.shade200), - ), - child: Column( - children: [ - Row( - children: [ - Icon(Icons.qr_code_2, color: Colors.green.shade700), - const SizedBox(width: 8), - Text( - 'Aperçu du QR Code', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.green.shade700, + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: ramController, + decoration: const InputDecoration( + labelText: 'RAM', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.memory), + filled: true, + fillColor: Colors.white, ), ), - ], - ), - const SizedBox(height: 12), - Center( - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - child: QrImageView( - data: qrPreviewData!, - version: QrVersions.auto, - size: 80, - backgroundColor: Colors.white, - ), ), - ), - const SizedBox(height: 8), - Text( - 'Réf: ${referenceController.text.trim()}', - style: const TextStyle(fontSize: 10, color: Colors.grey), - ), - ], - ), - ), - ], - ); - }, - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton.icon( - onPressed: () async { - final name = nameController.text.trim(); - final price = double.tryParse(priceController.text.trim()) ?? 0.0; - final stock = int.tryParse(stockController.text.trim()) ?? 0; - final reference = referenceController.text.trim(); - - if (name.isEmpty || price <= 0) { - Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); - return; - } - - if (reference.isEmpty) { - Get.snackbar('Erreur', 'La référence est obligatoire'); - return; - } - - // Vérifier si la référence existe déjà (sauf pour ce produit) - if (reference != product.reference) { - final existingProduct = await _productDatabase.getProductByReference(reference); - if (existingProduct != null && existingProduct.id != product.id) { - Get.snackbar('Erreur', 'Cette référence existe déjà pour un autre produit'); - return; - } - } - - // Vérifier si l'IMEI existe déjà (sauf pour ce produit) - final imei = imeiController.text.trim(); - if (imei.isNotEmpty && imei != product.imei) { - final existingProduct = await _productDatabase.getProductByIMEI(imei); - if (existingProduct != null && existingProduct.id != product.id) { - Get.snackbar('Erreur', 'Cet IMEI existe déjà pour un autre produit'); - return; - } - } - - // Gérer le point de vente - int? pointDeVenteId; - String? finalPointDeVenteNom; - - if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) { - finalPointDeVenteNom = newPointDeVenteController.text.trim(); - } else if (selectedPointDeVente != null) { - finalPointDeVenteNom = selectedPointDeVente; - } - - if (finalPointDeVenteNom != null) { - pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(finalPointDeVenteNom); - } - - try { - final updatedProduct = Product( - id: product.id, - name: name, - price: price, - image: imageController.text.trim(), - category: selectedCategory, - description: descriptionController.text.trim(), - stock: stock, - qrCode: product.qrCode, // Conserver le QR code existant - reference: reference, - marque: marqueController.text.trim().isNotEmpty ? marqueController.text.trim() : null, - ram: ramController.text.trim().isNotEmpty ? ramController.text.trim() : null, - memoireInterne: memoireInterneController.text.trim().isNotEmpty ? memoireInterneController.text.trim() : null, - imei: imei.isNotEmpty ? imei : null, - pointDeVenteId: pointDeVenteId, - ); - - await _productDatabase.updateProduct(updatedProduct); - Get.back(); - Get.snackbar( - 'Succès', - 'Produit modifié avec succès!\nRéférence: $reference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 4), - icon: const Icon(Icons.check_circle, color: Colors.white), - ); - _loadProducts(); - _loadPointsDeVente(); // Recharger aussi les points de vente - } catch (e) { - Get.snackbar('Erreur', 'Modification du produit échouée: $e'); - } - }, - icon: const Icon(Icons.save), - label: const Text('Sauvegarder les modifications'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - ), - ), - ], - ), - ); -} - - void _deleteProduct(Product product) { - Get.dialog( - AlertDialog( - title: const Text('Confirmer la suppression'), - content: Text('Êtes-vous sûr de vouloir supprimer "${product.name}" ?'), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: Colors.red), - onPressed: () async { - try { - await _productDatabase.deleteProduct(product.id); - Get.back(); - Get.snackbar( - 'Succès', - 'Produit supprimé avec succès', - backgroundColor: Colors.green, - colorText: Colors.white, - ); - _loadProducts(); - } catch (e) { - Get.back(); - Get.snackbar('Erreur', 'Suppression échouée: $e'); - } - }, - child: const Text('Supprimer', style: TextStyle(color: Colors.white)), - ), - ], - ), - ); - } - - Widget _buildProductCard(Product product) { - return FutureBuilder( - future: _productDatabase.getPointDeVenteNomById(product.pointDeVenteId ?? 0), - builder: (context, snapshot) { - // Gestion des états du FutureBuilder - if (snapshot.connectionState == ConnectionState.waiting) { - return _buildProductCardContent(product, 'Chargement...'); - } - - if (snapshot.hasError) { - return _buildProductCardContent(product, 'Erreur de chargement'); - } - - final pointDeVente = snapshot.data ?? 'Non spécifié'; - return _buildProductCardContent(product, pointDeVente); - }, - ); -} - -Widget _buildProductCardContent(Product product, String pointDeVenteText) { - return InkWell( - onTap: () => _showProductDetailsDialog(context, product), - child: Card( - margin: const EdgeInsets.all(8), - elevation: 4, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Row( - children: [ - // Image du produit - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: product.image!.isNotEmpty - ? Image.file( - File(product.image!), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - const Icon(Icons.image, size: 40), - ) - : const Icon(Icons.image, size: 40), - ), - ), - const SizedBox(width: 16), - - // Informations du produit - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - product.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: memoireInterneController, + decoration: const InputDecoration( + labelText: 'Mémoire interne', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.storage), + filled: true, + fillColor: Colors.white, + ), + ), + ), + ], ), - ), - const SizedBox(height: 4), - Text( - '${NumberFormat('#,##0').format(product.price)} MGA', - style: const TextStyle( - fontSize: 16, - color: Colors.green, - fontWeight: FontWeight.w600, + const SizedBox(height: 8), + TextField( + controller: imeiController, + decoration: const InputDecoration( + labelText: 'IMEI (pour téléphones)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.smartphone), + filled: true, + fillColor: Colors.white, + ), ), - ), - const SizedBox(height: 4), - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Colors.blue.shade100, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - product.category, + ], + ), + ), + const SizedBox(height: 16), + + // Section Image + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.image, color: Colors.blue.shade700), + const SizedBox(width: 8), + Text( + 'Image du produit', style: TextStyle( - fontSize: 12, - color: Colors.blue.shade800, + fontWeight: FontWeight.w600, + color: Colors.blue.shade700, ), ), - ), - const SizedBox(width: 8), - Text( - 'Stock: ${product.stock}', - style: TextStyle( - fontSize: 12, - color: product.stock! > 0 ? Colors.green : Colors.red, - fontWeight: FontWeight.w500, + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextField( + controller: imageController, + decoration: const InputDecoration( + labelText: 'Chemin de l\'image', + border: OutlineInputBorder(), + isDense: true, + ), + readOnly: true, + ), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () async { + final result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && result.files.single.path != null) { + setDialogState(() { + pickedImage = File(result.files.single.path!); + imageController.text = pickedImage!.path; + }); + } + }, + icon: const Icon(Icons.folder_open, size: 16), + label: const Text('Choisir'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(12), + ), + ), + ], + ), + const SizedBox(height: 12), + + // Aperçu de l'image + Center( + child: Container( + height: 100, + width: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: pickedImage != null + ? Image.file(pickedImage!, fit: BoxFit.cover) + : (product.image != null && product.image!.isNotEmpty + ? Image.file( + File(product.image!), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.image, size: 50), + ) + : const Icon(Icons.image, size: 50)), ), ), - ], - ), - ], - ), - ), - - // Actions - Column( - children: [ - IconButton( - onPressed: () => _showQRCode(product), - icon: const Icon(Icons.qr_code_2, color: Colors.blue), - tooltip: 'Voir QR Code', - ), - IconButton( - onPressed: () => _editProduct(product), - icon: const Icon(Icons.edit, color: Colors.orange), - tooltip: 'Modifier', - ), - IconButton( - onPressed: () => _deleteProduct(product), - icon: const Icon(Icons.delete, color: Colors.red), - tooltip: 'Supprimer', + ), + ], ), - ], - ), - ], - ), - const SizedBox(height: 8), - // Ligne du point de vente avec option d'édition - Row( - children: [ - const Icon(Icons.store, size: 16, color: Colors.grey), - const SizedBox(width: 4), - Text( - 'Point de vente: $pointDeVenteText', - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - const Spacer(), - if (pointDeVenteText == 'Non spécifié') - TextButton( - onPressed: () => _showAddPointDeVenteDialog(product), - child: const Text('Ajouter', style: TextStyle(fontSize: 12)),) - ], - ), - ], - ), - ), - ), - ); -} -void _showAddPointDeVenteDialog(Product product) { - final pointDeVenteController = TextEditingController(); - final _formKey = GlobalKey(); - - Get.dialog( - AlertDialog( - title: const Text('Ajouter un point de vente'), - content: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: pointDeVenteController, - decoration: const InputDecoration( - labelText: 'Nom du point de vente', - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Veuillez entrer un nom'; - } - return null; - }, - ), - const SizedBox(height: 16), - DropdownButtonFormField( - value: null, - hint: const Text('Ou sélectionner existant'), - items: _pointsDeVente.map((point) { - return DropdownMenuItem( - value: point['nom'] as String, - child: Text(point['nom'] as String), - ); - }).toList(), - onChanged: (value) { - if (value != null) { - pointDeVenteController.text = value; - } - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton( - onPressed: () async { - if (_formKey.currentState!.validate()) { - final nom = pointDeVenteController.text.trim(); - final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); - - if (id != null) { - // Mettre à jour le produit avec le nouveau point de vente - final updatedProduct = Product( - id: product.id, - name: product.name, - price: product.price, - image: product.image, - category: product.category, - stock: product.stock, - description: product.description, - qrCode: product.qrCode, - reference: product.reference, - pointDeVenteId: id, - ); - - await _productDatabase.updateProduct(updatedProduct); - Get.back(); - Get.snackbar('Succès', 'Point de vente attribué', - backgroundColor: Colors.green); - _loadProducts(); // Rafraîchir la liste + ), + const SizedBox(height: 16), + + // Aperçu QR Code + if (qrPreviewData != null) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + children: [ + Row( + children: [ + Icon(Icons.qr_code_2, color: Colors.green.shade700), + const SizedBox(width: 8), + Text( + 'Aperçu du QR Code', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: QrImageView( + data: qrPreviewData!, + version: QrVersions.auto, + size: 80, + backgroundColor: Colors.white, + ), + ), + ), + const SizedBox(height: 8), + Text( + 'Réf: ${referenceController.text.trim()}', + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ), + ), + ], + ); + }, + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton.icon( + onPressed: () async { + final name = nameController.text.trim(); + final price = double.tryParse(priceController.text.trim()) ?? 0.0; + final stock = int.tryParse(stockController.text.trim()) ?? 0; + final reference = referenceController.text.trim(); + + if (name.isEmpty || price <= 0) { + Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); + return; + } + + if (reference.isEmpty) { + Get.snackbar('Erreur', 'La référence est obligatoire'); + return; + } + + if (reference != product.reference) { + final existingProduct = await _productDatabase.getProductByReference(reference); + if (existingProduct != null && existingProduct.id != product.id) { + Get.snackbar('Erreur', 'Cette référence existe déjà pour un autre produit'); + return; + } + } + + final imei = imeiController.text.trim(); + if (imei.isNotEmpty && imei != product.imei) { + final existingProduct = await _productDatabase.getProductByIMEI(imei); + if (existingProduct != null && existingProduct.id != product.id) { + Get.snackbar('Erreur', 'Cet IMEI existe déjà pour un autre produit'); + return; } } + + // Gérer le point de vente + int? pointDeVenteId; + String? finalPointDeVenteNom; + + if (showAddNewPoint && newPointDeVenteController.text.trim().isNotEmpty) { + finalPointDeVenteNom = newPointDeVenteController.text.trim(); + } else if (selectedPointDeVente != null && selectedPointDeVente != 'Aucun') { + finalPointDeVenteNom = selectedPointDeVente; + } + + if (finalPointDeVenteNom != null) { + pointDeVenteId = await _productDatabase.getOrCreatePointDeVenteByNom(finalPointDeVenteNom); + } + // Si "Aucun" est sélectionné, pointDeVenteId reste null + + try { + final updatedProduct = Product( + id: product.id, + name: name, + price: price, + image: imageController.text.trim(), + category: selectedCategory, + description: descriptionController.text.trim(), + stock: stock, + qrCode: product.qrCode, + reference: reference, + marque: marqueController.text.trim().isNotEmpty ? marqueController.text.trim() : null, + ram: ramController.text.trim().isNotEmpty ? ramController.text.trim() : null, + memoireInterne: memoireInterneController.text.trim().isNotEmpty ? memoireInterneController.text.trim() : null, + imei: imei.isNotEmpty ? imei : null, + pointDeVenteId: pointDeVenteId, // Peut être null si "Aucun" + ); + + await _productDatabase.updateProduct(updatedProduct); + Get.back(); + Get.snackbar( + 'Succès', + 'Produit modifié avec succès!\nRéférence: $reference${finalPointDeVenteNom != null ? '\nPoint de vente: $finalPointDeVenteNom' : ''}', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 4), + icon: const Icon(Icons.check_circle, color: Colors.white), + ); + _loadProducts(); + _loadPointsDeVente(); + } catch (e) { + Get.snackbar('Erreur', 'Modification du produit échouée: $e'); + } }, - child: const Text('Enregistrer'), + icon: const Icon(Icons.save), + label: const Text('Sauvegarder les modifications'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), ), ], ), ); } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar(title: 'Gestion des produits'), - drawer: CustomDrawer(), - floatingActionButton: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FloatingActionButton( - heroTag: 'importBtn', - onPressed: _isImporting ? null : _importFromExcel, - mini: true, - child: const Icon(Icons.upload), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - const SizedBox(height: 8), - FloatingActionButton.extended( - heroTag: 'addBtn', - onPressed: _showAddProductDialog, - icon: const Icon(Icons.add), - label: const Text('Ajouter'), - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - ], - ), - body: Column( - children: [ - // Barre de recherche et filtres - Container( - padding: const EdgeInsets.all(16), - color: Colors.grey.shade100, - child: Column( - children: [ - // Ajoutez cette Row pour les boutons d'import - 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 recherche existante - Row( - children: [ - Expanded( - child: TextField( - controller: _searchController, - decoration: InputDecoration( - labelText: 'Rechercher...', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: Colors.white, - ), - ), - ), - const SizedBox(width: 16), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), - child: DropdownButton( - value: _selectedCategory, - items: _categories.map((category) => - DropdownMenuItem(value: category, child: Text(category))).toList(), - onChanged: (value) { - setState(() { - _selectedCategory = value!; - _filterProducts(); - }); - }, - underline: const SizedBox(), - hint: const Text('Catégorie'), - ), - ), - ], - ), - const SizedBox(height: 12), - - // Indicateur de progression d'importation - _buildImportProgressIndicator(), - - // Compteur de produits - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${_filteredProducts.length} produit(s) trouvé(s)', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.grey, - ), - ), - if (_searchController.text.isNotEmpty || _selectedCategory != 'Tous') - TextButton.icon( - onPressed: () { - setState(() { - _searchController.clear(); - _selectedCategory = 'Tous'; - _filterProducts(); - }); - }, - icon: const Icon(Icons.clear, size: 16), - label: const Text('Réinitialiser'), - style: TextButton.styleFrom( - foregroundColor: Colors.orange, - ), - ), - ], - ), - ], + + void _deleteProduct(Product product) { + Get.dialog( + AlertDialog( + title: const Text('Confirmer la suppression'), + content: Text('Êtes-vous sûr de vouloir supprimer "${product.name}" ?'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), ), - ), - - // Liste des produits - Expanded( - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _filteredProducts.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.inventory_2_outlined, - size: 64, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - _products.isEmpty - ? 'Aucun produit enregistré' - : 'Aucun produit trouvé pour cette recherche', - style: TextStyle( - fontSize: 18, - color: Colors.grey.shade600, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - Text( - _products.isEmpty - ? 'Commencez par ajouter votre premier produit' - : 'Essayez de modifier vos critères de recherche', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade500, - ), - ), - if (_products.isEmpty) ...[ - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: _showAddProductDialog, - icon: const Icon(Icons.add), - label: const Text('Ajouter un produit'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - ), - ), - ], - ], - ), - ) - : RefreshIndicator( - onRefresh: _loadProducts, - child: ListView.builder( - itemCount: _filteredProducts.length, - itemBuilder: (context, index) { - final product = _filteredProducts[index]; - return _buildProductCard(product); - }, - ), - ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + onPressed: () async { + try { + await _productDatabase.deleteProduct(product.id); + Get.back(); + Get.snackbar( + 'Succès', + 'Produit supprimé avec succès', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + _loadProducts(); + } catch (e) { + Get.back(); + Get.snackbar('Erreur', 'Suppression échouée: $e'); + } + }, + child: const Text('Supprimer', style: TextStyle(color: Colors.white)), ), ], ), ); } + Widget _buildProductCard(Product product) { + return FutureBuilder( + future: _productDatabase.getPointDeVenteNomById(product.pointDeVenteId ?? 0), + builder: (context, snapshot) { + // Gestion des états du FutureBuilder + if (snapshot.connectionState == ConnectionState.waiting) { + return _buildProductCardContent(product, 'Chargement...'); + } + + if (snapshot.hasError) { + return _buildProductCardContent(product, 'Erreur de chargement'); + } + + final pointDeVente = snapshot.data ?? 'Non spécifié'; + return _buildProductCardContent(product, pointDeVente); + }, + ); +} + + +void _showAddPointDeVenteDialog(Product product) { + final pointDeVenteController = TextEditingController(); + final _formKey = GlobalKey(); + Get.dialog( + AlertDialog( + title: const Text('Ajouter un point de vente'), + content: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: pointDeVenteController, + decoration: const InputDecoration( + labelText: 'Nom du point de vente', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer un nom'; + } + return null; + }, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + value: null, + hint: const Text('Ou sélectionner existant'), + items: _pointsDeVente.map((point) { + return DropdownMenuItem( + value: point['nom'] as String, + child: Text(point['nom'] as String), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + pointDeVenteController.text = value; + } + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () async { + if (_formKey.currentState!.validate()) { + final nom = pointDeVenteController.text.trim(); + final id = await _productDatabase.getOrCreatePointDeVenteByNom(nom); + + if (id != null) { + // Mettre à jour le produit avec le nouveau point de vente + final updatedProduct = Product( + id: product.id, + name: product.name, + price: product.price, + image: product.image, + category: product.category, + stock: product.stock, + description: product.description, + qrCode: product.qrCode, + reference: product.reference, + pointDeVenteId: id, + ); + + await _productDatabase.updateProduct(updatedProduct); + Get.back(); + Get.snackbar('Succès', 'Point de vente attribué', + backgroundColor: Colors.green); + _loadProducts(); // Rafraîchir la liste + } + } + }, + child: const Text('Enregistrer'), + ), + ], + ), + ); +} + void _showProductDetailsDialog(BuildContext context, Product product) { Get.dialog( Dialog( @@ -3815,4 +4016,300 @@ Widget _buildPlaceholderImage() { ), ); } -} \ No newline at end of file + + +// Méthode pour générer et sauvegarder le QR Code + Future _generateAndSaveQRCode(String reference) async { + final qrUrl = 'https://stock.guycom.mg/$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'; + + 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; + } + + +@override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < 600; + return Scaffold( + appBar: CustomAppBar(title: 'Gestion des produits'), + drawer: CustomDrawer(), + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Nouveau bouton pour scanner et assigner + FloatingActionButton( + heroTag: 'assignBtn', + onPressed: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) + ? null + : _startPointDeVenteAssignmentScanning, + mini: true, + child: (_isScanning || _isAssigning) + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Row( + children: [ + const Icon(Icons.qr_code), + ], + ), + backgroundColor: (_isScanning || _isAssigning || _userController.pointDeVenteId <= 0) + ? Colors.grey + : Colors.orange, + foregroundColor: Colors.white, + ), + const SizedBox(height: 8), + FloatingActionButton( + heroTag: 'importBtn', + onPressed: _isImporting ? null : _importFromExcel, + mini: true, + child: const Icon(Icons.upload), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + const SizedBox(height: 8), + FloatingActionButton.extended( + heroTag: 'addBtn', + onPressed: _showAddProductDialog, + + icon: const Icon(Icons.add), + label: const Text('Ajouter'), + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ], + ), + body: Column( + children: [ + // Barre de recherche et filtres + + Container( + padding: const EdgeInsets.all(16), + color: Colors.grey.shade100, + child: Column( + children: [ + // Card d'information sur l'attribution (desktop uniquement) + if (!isMobile && _userController.pointDeVenteId > 0) + _buildAssignmentScanCard(), + + // Barre de recherche + Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + labelText: 'Rechercher...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white, + ), + ), + ), + const SizedBox(width: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: DropdownButton( + value: _selectedCategory, + items: _categories.map((category) => + DropdownMenuItem(value: category, child: Text(category))).toList(), + onChanged: (value) { + setState(() { + _selectedCategory = value!; + _filterProducts(); + }); + }, + underline: const SizedBox(), + hint: const Text('Catégorie'), + ), + ), + ], + ), + + // Boutons pour mobile + if (isMobile) ...[ + const SizedBox(height: 12), + if (_userController.pointDeVenteId > 0) + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + icon: (_isScanning || _isAssigning) + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.assignment), + label: Text((_isScanning || _isAssigning) ? 'Attribution...' : 'Assigner produits'), + onPressed: (_isScanning || _isAssigning) ? null : _startPointDeVenteAssignmentScanning, + style: ElevatedButton.styleFrom( + backgroundColor: (_isScanning || _isAssigning) ? Colors.grey : Colors.orange.shade700, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], + ), + ], + + const SizedBox(height: 12), + + // Compteur de produits + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${_filteredProducts.length} produit(s) trouvé(s)', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + if (_userController.pointDeVenteId > 0) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'PV: ${_userController.pointDeVenteDesignation}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + ), + if (_searchController.text.isNotEmpty || _selectedCategory != 'Tous') + TextButton.icon( + onPressed: () { + setState(() { + _searchController.clear(); + _selectedCategory = 'Tous'; + _filterProducts(); + }); + }, + icon: const Icon(Icons.clear, size: 16), + label: const Text('Réinitialiser'), + style: TextButton.styleFrom( + foregroundColor: Colors.orange, + ), + ), + ], + ), + ], + ), + ), + _buildImportProgressIndicator(), + // Liste des produits + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _filteredProducts.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 64, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + _products.isEmpty + ? 'Aucun produit enregistré' + : 'Aucun produit trouvé pour cette recherche', + style: TextStyle( + fontSize: 18, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + _products.isEmpty + ? 'Commencez par ajouter votre premier produit' + : 'Essayez de modifier vos critères de recherche', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + ), + ], + ), + ) + : RefreshIndicator( + onRefresh: _loadProducts, + child: ListView.builder( + itemCount: _filteredProducts.length, + itemBuilder: (context, index) { + final product = _filteredProducts[index]; + return _buildProductCard(product); + }, + ), + ), + ), + ], + ), + ); + } + + +} + + + diff --git a/lib/Views/commandManagement.dart b/lib/Views/commandManagement.dart index f78ebb0..e31cf46 100644 --- a/lib/Views/commandManagement.dart +++ b/lib/Views/commandManagement.dart @@ -12,10 +12,17 @@ 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/Components/commandManagementComponents/CommandeActions.dart'; +import 'package:youmazgestion/Components/commandManagementComponents/DiscountDialog.dart'; +import 'package:youmazgestion/Components/commandManagementComponents/GiftSelectionDialog.dart'; +import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart'; +import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethodDialog.dart'; +import 'package:youmazgestion/Components/commandManagementComponents/PaymentType.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/Models/produit.dart'; +import '../Components/commandManagementComponents/CommandDetails.dart'; class GestionCommandesPage extends StatefulWidget { const GestionCommandesPage({super.key}); @@ -849,7 +856,25 @@ class _GestionCommandesPageState extends State { } } + + + String statutLibelle(StatutCommande statut) { + switch (statut) { + case StatutCommande.enAttente: + return 'En attente'; + case StatutCommande.confirmee: + return 'Confirmée'; + case StatutCommande.annulee: + return 'Annulée'; + } + } + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar(title: 'Gestion des Commandes'), @@ -1392,10 +1417,10 @@ class _GestionCommandesPageState extends State { ), child: Column( children: [ - _CommandeDetails(commande: commande), + CommandeDetails(commande: commande), const SizedBox(height: 16), if (commande.statut != StatutCommande.annulee) - _CommandeActions( + CommandeActions( commande: commande, onStatutChanged: _updateStatut, onPaymentSelected: _showPaymentOptions, @@ -1415,1023 +1440,4 @@ class _GestionCommandesPageState extends State { ), ); } - - String statutLibelle(StatutCommande statut) { - switch (statut) { - case StatutCommande.enAttente: - return 'En attente'; - case StatutCommande.confirmee: - return 'Confirmé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: AppDatabase.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.estCadeau == true - ? '${detail.produitNom ?? 'Produit inconnu'} (CADEAU)' - : detail.produitNom ?? 'Produit inconnu' - ), - _buildTableCell('${detail.quantite}'), - _buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(2)} MGA'), - _buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(2)} MGA'), - ], - )), - ], - ), - ), - 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: Column( - children: [ - if (commande.montantApresRemise != null) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Sous-total:', - style: TextStyle(fontSize: 14), - ), - Text( - '${commande.montantTotal.toStringAsFixed(2)} MGA', - style: const TextStyle(fontSize: 14), - ), - ], - ), - const SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Remise:', - style: TextStyle(fontSize: 14), - ), - Text( - '-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA', - style: const TextStyle( - fontSize: 14, - color: Colors.red, - ), - ), - ], - ), - const Divider(), - ], - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Total de la commande:', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - '${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(2)} MGA', - 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; - final Function(Commande) onPaymentSelected; - final Function(Commande) onDiscountSelected; - final Function(Commande) onGiftSelected; - - const _CommandeActions({ - required this.commande, - required this.onStatutChanged, - required this.onPaymentSelected, - required this.onDiscountSelected, - required this.onGiftSelected, - }); - - @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: 'Remise', - icon: Icons.percent, - color: Colors.orange, - onPressed: () => onDiscountSelected(commande), - ), - _buildActionButton( - label: 'Cadeau', - icon: Icons.card_giftcard, - color: Colors.purple, - onPressed: () => onGiftSelected(commande), - ), - _buildActionButton( - label: 'Confirmer', - icon: Icons.check_circle, - color: Colors.blue, - onPressed: () => onPaymentSelected(commande), - ), - _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.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 confirmé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'), - ), - ], - ); - }, - ); - } -} - -// Dialog pour la remise -class DiscountDialog extends StatefulWidget { - final Commande commande; - - const DiscountDialog({super.key, required this.commande}); - - @override - _DiscountDialogState createState() => _DiscountDialogState(); -} - -class _DiscountDialogState extends State { - final _pourcentageController = TextEditingController(); - final _montantController = TextEditingController(); - bool _isPercentage = true; - double _montantFinal = 0; - - @override - void initState() { - super.initState(); - _montantFinal = widget.commande.montantTotal; - } - - void _calculateDiscount() { - double discount = 0; - - if (_isPercentage) { - final percentage = double.tryParse(_pourcentageController.text) ?? 0; - discount = (widget.commande.montantTotal * percentage) / 100; - } else { - discount = double.tryParse(_montantController.text) ?? 0; - } - - setState(() { - _montantFinal = widget.commande.montantTotal - discount; - if (_montantFinal < 0) _montantFinal = 0; - }); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text('Appliquer une remise'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Montant original: ${widget.commande.montantTotal.toStringAsFixed(2)} MGA'), - const SizedBox(height: 16), - - // Choix du type de remise - Row( - children: [ - Expanded( - child: RadioListTile( - title: const Text('Pourcentage'), - value: true, - groupValue: _isPercentage, - onChanged: (value) { - setState(() { - _isPercentage = value!; - _calculateDiscount(); - }); - }, - ), - ), - Expanded( - child: RadioListTile( - title: const Text('Montant fixe'), - value: false, - groupValue: _isPercentage, - onChanged: (value) { - setState(() { - _isPercentage = value!; - _calculateDiscount(); - }); - }, - ), - ), - ], - ), - - const SizedBox(height: 16), - - if (_isPercentage) - TextField( - controller: _pourcentageController, - decoration: const InputDecoration( - labelText: 'Pourcentage de remise', - suffixText: '%', - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.number, - onChanged: (value) => _calculateDiscount(), - ) - else - TextField( - controller: _montantController, - decoration: const InputDecoration( - labelText: 'Montant de remise', - suffixText: 'MGA', - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.number, - onChanged: (value) => _calculateDiscount(), - ), - - const SizedBox(height: 16), - - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Montant final:'), - Text( - '${_montantFinal.toStringAsFixed(2)} MGA', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), - if (_montantFinal < widget.commande.montantTotal) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Économie:'), - Text( - '${(widget.commande.montantTotal - _montantFinal).toStringAsFixed(2)} MGA', - style: const TextStyle( - color: Colors.green, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Annuler'), - ), - ElevatedButton( - onPressed: _montantFinal < widget.commande.montantTotal - ? () { - final pourcentage = _isPercentage - ? double.tryParse(_pourcentageController.text) - : null; - final montant = !_isPercentage - ? double.tryParse(_montantController.text) - : null; - - Navigator.pop(context, { - 'pourcentage': pourcentage, - 'montant': montant, - 'montantFinal': _montantFinal, - }); - } - : null, - child: const Text('Appliquer'), - ), - ], - ); - } - - @override - void dispose() { - _pourcentageController.dispose(); - _montantController.dispose(); - super.dispose(); - } } - -// Dialog pour sélectionner un cadeau -class GiftSelectionDialog extends StatefulWidget { - final Commande commande; - - const GiftSelectionDialog({super.key, required this.commande}); - - @override - _GiftSelectionDialogState createState() => _GiftSelectionDialogState(); -} - -class _GiftSelectionDialogState extends State { - List _products = []; - List _filteredProducts = []; - final _searchController = TextEditingController(); - Product? _selectedProduct; - - @override - void initState() { - super.initState(); - _loadProducts(); - _searchController.addListener(_filterProducts); - } - - Future _loadProducts() async { - final products = await AppDatabase.instance.getProducts(); - setState(() { - _products = products.where((p) => p.stock > 0).toList(); - _filteredProducts = _products; - }); - } - - void _filterProducts() { - final query = _searchController.text.toLowerCase(); - setState(() { - _filteredProducts = _products.where((product) { - return product.name.toLowerCase().contains(query) || - (product.reference?.toLowerCase().contains(query) ?? false) || - (product.category.toLowerCase().contains(query)); - }).toList(); - }); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text('Sélectionner un cadeau'), - content: SizedBox( - width: double.maxFinite, - height: 400, - child: Column( - children: [ - TextField( - controller: _searchController, - decoration: const InputDecoration( - labelText: 'Rechercher un produit', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - Expanded( - child: ListView.builder( - itemCount: _filteredProducts.length, - itemBuilder: (context, index) { - final product = _filteredProducts[index]; - return Card( - child: ListTile( - leading: product.image != null - ? Image.network( - product.image!, - width: 50, - height: 50, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - const Icon(Icons.image_not_supported), - ) - : const Icon(Icons.phone_android), - title: Text(product.name), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Catégorie: ${product.category}'), - Text('Stock: ${product.stock}'), - if (product.reference != null) - Text('Réf: ${product.reference}'), - ], - ), - trailing: Radio( - value: product, - groupValue: _selectedProduct, - onChanged: (value) { - setState(() { - _selectedProduct = value; - }); - }, - ), - onTap: () { - setState(() { - _selectedProduct = product; - }); - }, - ), - ); - }, - ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Annuler'), - ), - ElevatedButton( - onPressed: _selectedProduct != null - ? () => Navigator.pop(context, _selectedProduct) - : null, - child: const Text('Ajouter le cadeau'), - ), - ], - ); - } - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } -} - -enum PaymentType { - cash, - card, - mvola, - orange, - airtel -} - -class PaymentMethod { - final PaymentType type; - final double amountGiven; - - PaymentMethod({required this.type, this.amountGiven = 0}); -} - -class PaymentMethodDialog extends StatefulWidget { - final Commande commande; - - const PaymentMethodDialog({super.key, required this.commande}); - - @override - _PaymentMethodDialogState createState() => _PaymentMethodDialogState(); -} - -class _PaymentMethodDialogState extends State { - PaymentType _selectedPayment = PaymentType.cash; - final _amountController = TextEditingController(); - - void _validatePayment() { - final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; - - if (_selectedPayment == PaymentType.cash) { - final amountGiven = double.tryParse(_amountController.text) ?? 0; - if (amountGiven < montantFinal) { - Get.snackbar( - 'Erreur', - 'Le montant donné est insuffisant', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - return; - } - } - - Navigator.pop(context, PaymentMethod( - type: _selectedPayment, - amountGiven: _selectedPayment == PaymentType.cash - ? double.parse(_amountController.text) - : montantFinal, - )); - } - - @override - void initState() { - super.initState(); - final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; - _amountController.text = montantFinal.toStringAsFixed(2); - } - - @override - void dispose() { - _amountController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final amount = double.tryParse(_amountController.text) ?? 0; - final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal; - final change = amount - montantFinal; - - return AlertDialog( - title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Affichage du montant à payer - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), - ), - child: Column( - children: [ - if (widget.commande.montantApresRemise != null) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Montant original:'), - Text('${widget.commande.montantTotal.toStringAsFixed(2)} MGA'), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Remise:'), - Text('-${(widget.commande.montantTotal - widget.commande.montantApresRemise!).toStringAsFixed(2)} MGA', - style: const TextStyle(color: Colors.red)), - ], - ), - const Divider(), - ], - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)), - Text('${montantFinal.toStringAsFixed(2)} MGA', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - ], - ), - ], - ), - ), - - const SizedBox(height: 16), - - // Section Paiement mobile - const Align( - alignment: Alignment.centerLeft, - child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: _buildMobileMoneyTile( - title: 'Mvola', - imagePath: 'assets/mvola.jpg', - value: PaymentType.mvola, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildMobileMoneyTile( - title: 'Orange Money', - imagePath: 'assets/Orange_money.png', - value: PaymentType.orange, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildMobileMoneyTile( - title: 'Airtel Money', - imagePath: 'assets/airtel_money.png', - value: PaymentType.airtel, - ), - ), - ], - ), - const SizedBox(height: 16), - - // Section Carte bancaire - const Align( - alignment: Alignment.centerLeft, - child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)), - ), - const SizedBox(height: 8), - _buildPaymentMethodTile( - title: 'Carte bancaire', - icon: Icons.credit_card, - value: PaymentType.card, - ), - const SizedBox(height: 16), - - // Section Paiement en liquide - const Align( - alignment: Alignment.centerLeft, - child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)), - ), - const SizedBox(height: 8), - _buildPaymentMethodTile( - title: 'Paiement en liquide', - icon: Icons.money, - value: PaymentType.cash, - ), - if (_selectedPayment == PaymentType.cash) ...[ - const SizedBox(height: 12), - TextField( - controller: _amountController, - decoration: const InputDecoration( - labelText: 'Montant donné', - prefixText: 'MGA ', - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.numberWithOptions(decimal: true), - onChanged: (value) => setState(() {}), - ), - const SizedBox(height: 8), - Text( - 'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: change >= 0 ? Colors.green : Colors.red, - ), - ), - ], - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Annuler', style: TextStyle(color: Colors.grey)), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - ), - onPressed: _validatePayment, - child: const Text('Confirmer'), - ), - ], - ); - } - - Widget _buildMobileMoneyTile({ - required String title, - required String imagePath, - required PaymentType value, - }) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), - width: 2, - ), - ), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => setState(() => _selectedPayment = value), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - Image.asset( - imagePath, - height: 30, - width: 30, - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) => - const Icon(Icons.mobile_friendly, size: 30), - ), - const SizedBox(height: 8), - Text( - title, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 12), - ), - ], - ), - ), - ), - ); - } - - Widget _buildPaymentMethodTile({ - required String title, - required IconData icon, - required PaymentType value, - }) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), - width: 2, - ), - ), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => setState(() => _selectedPayment = value), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Icon(icon, size: 24), - const SizedBox(width: 12), - Text(title), - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/Views/loginPage.dart b/lib/Views/loginPage.dart index 0068c20..c3670fe 100644 --- a/lib/Views/loginPage.dart +++ b/lib/Views/loginPage.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; +import 'package:youmazgestion/Services/PermissionCacheService.dart'; // Nouveau import import 'package:youmazgestion/Views/Dashboard.dart'; import 'package:youmazgestion/Views/mobilepage.dart'; import 'package:youmazgestion/Views/particles.dart' show ParticleBackground; @@ -19,9 +20,12 @@ class _LoginPageState extends State { late TextEditingController _usernameController; late TextEditingController _passwordController; final UserController userController = Get.put(UserController()); + final PermissionCacheService _cacheService = PermissionCacheService.instance; // Nouveau + bool _isErrorVisible = false; bool _isLoading = false; String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide'; + String _loadingMessage = 'Connexion en cours...'; // Nouveau @override void initState() { @@ -51,105 +55,185 @@ class _LoginPageState extends State { super.dispose(); } - Future saveUserData(Users user, String role, int userId) async { - try { - userController.setUserWithCredentials(user, role, userId); + // /// ✅ OPTIMISÉ: Sauvegarde avec préchargement des permissions + // Future saveUserData(Users user, String role, int userId) async { + // try { + // userController.setUserWithCredentials(user, role, userId); - if (user.pointDeVenteId != null) { - await userController.loadPointDeVenteDesignation(); - } + // if (user.pointDeVenteId != null) { + // await userController.loadPointDeVenteDesignation(); + // } - print('Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}'); - } catch (error) { - print('Erreur lors de la sauvegarde: $error'); - throw Exception('Erreur lors de la sauvegarde des données utilisateur'); - } + // print('✅ Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}'); + // } catch (error) { + // print('❌ Erreur lors de la sauvegarde: $error'); + // throw Exception('Erreur lors de la sauvegarde des données utilisateur'); + // } + // } + + /// ✅ NOUVEAU: Préchargement des permissions en arrière-plan + Future _preloadUserPermissions(String username) async { + try { + setState(() { + _loadingMessage = 'Préparation du menu...'; + }); + + // Lancer le préchargement en parallèle avec les autres tâches + final permissionFuture = _cacheService.preloadUserData(username); + + // Attendre maximum 2 secondes pour les permissions + await Future.any([ + permissionFuture, + Future.delayed(const Duration(seconds: 2)) + ]); + + print('✅ Permissions préparées (ou timeout)'); + } catch (e) { + print('⚠️ Erreur préchargement permissions: $e'); + // Continuer même en cas d'erreur } +} - void _login() async { - if (_isLoading) return; + /// ✅ OPTIMISÉ: Connexion avec préchargement parallèle +void _login() async { + if (_isLoading) return; - final String username = _usernameController.text.trim(); - final String password = _passwordController.text.trim(); + final String username = _usernameController.text.trim(); + final String password = _passwordController.text.trim(); - if (username.isEmpty || password.isEmpty) { - setState(() { - _errorMessage = - 'Veuillez saisir le nom d\'utilisateur et le mot de passe'; - _isErrorVisible = true; - }); - return; + if (username.isEmpty || password.isEmpty) { + setState(() { + _errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe'; + _isErrorVisible = true; + }); + return; + } + + setState(() { + _isLoading = true; + _isErrorVisible = false; + _loadingMessage = 'Connexion...'; + }); + + try { + print('🔐 Tentative de connexion pour: $username'); + final dbInstance = AppDatabase.instance; + + // 1. Vérification rapide de la base + setState(() { + _loadingMessage = 'Vérification...'; + }); + + try { + final userCount = await dbInstance.getUserCount(); + print('✅ Base accessible, $userCount utilisateurs'); + } catch (dbError) { + throw Exception('Base de données inaccessible: $dbError'); } + // 2. Vérification des identifiants setState(() { - _isLoading = true; - _isErrorVisible = false; + _loadingMessage = 'Authentification...'; }); + + bool isValidUser = await dbInstance.verifyUser(username, password); - try { - print('Tentative de connexion pour: $username'); - final dbInstance = AppDatabase.instance; + if (isValidUser) { + setState(() { + _loadingMessage = 'Chargement du profil...'; + }); - try { - final userCount = await dbInstance.getUserCount(); - print('Base de données accessible, $userCount utilisateurs trouvés'); - } catch (dbError) { - throw Exception('Impossible d\'accéder à la base de données: $dbError'); - } - - bool isValidUser = await dbInstance.verifyUser(username, password); + // 3. Récupération parallèle des données + final futures = await Future.wait([ + dbInstance.getUser(username), + dbInstance.getUserCredentials(username, password), + ]); + + final user = futures[0] as Users; + final userCredentials = futures[1] as Map?; - if (isValidUser) { - Users user = await dbInstance.getUser(username); - print('Utilisateur récupéré: ${user.username}'); + if (userCredentials != null) { + print('✅ Connexion réussie pour: ${user.username}'); + print(' Rôle: ${userCredentials['role']}'); - Map? userCredentials = - await dbInstance.getUserCredentials(username, password); + setState(() { + _loadingMessage = 'Préparation...'; + }); + + // 4. Sauvegarde des données utilisateur + await saveUserData( + user, + userCredentials['role'] as String, + userCredentials['id'] as int, + ); - if (userCredentials != null) { - print('Connexion réussie pour: ${user.username}'); - print('Rôle: ${userCredentials['role']}'); - print('ID: ${userCredentials['id']}'); - - await saveUserData( - user, - userCredentials['role'] as String, - userCredentials['id'] as int, - ); + // 5. Préchargement des permissions EN PARALLÈLE avec la navigation + setState(() { + _loadingMessage = 'Finalisation...'; + }); + + // Lancer le préchargement en arrière-plan SANS attendre + _cacheService.preloadUserDataAsync(username); - // MODIFICATION PRINCIPALE ICI - if (mounted) { - if (userCredentials['role'] == 'commercial') { - // Redirection vers MainLayout pour les commerciaux - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const MainLayout()), - ); - } else { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => DashboardPage()), - ); - } + // 6. Navigation immédiate + if (mounted) { + if (userCredentials['role'] == 'commercial') { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const MainLayout()), + ); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => DashboardPage()), + ); } - } else { - throw Exception('Erreur lors de la récupération des credentials'); } + + // Les permissions se chargeront en arrière-plan après la navigation + print('🚀 Navigation immédiate, permissions en arrière-plan'); + } else { - setState(() { - _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide'; - _isErrorVisible = true; - }); + throw Exception('Erreur lors de la récupération des credentials'); } - } catch (error) { + } else { setState(() { - _errorMessage = 'Erreur de connexion: ${error.toString()}'; + _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide'; _isErrorVisible = true; }); - } finally { - if (mounted) setState(() => _isLoading = false); } + } catch (error) { + setState(() { + _errorMessage = 'Erreur de connexion: ${error.toString()}'; + _isErrorVisible = true; + }); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + _loadingMessage = 'Connexion en cours...'; + }); + } + } +} + +/// ✅ OPTIMISÉ: Sauvegarde rapide +Future saveUserData(Users user, String role, int userId) async { + try { + userController.setUserWithCredentials(user, role, userId); + + // Charger le point de vente en parallèle si nécessaire + if (user.pointDeVenteId != null) { + // Ne pas attendre, charger en arrière-plan + unawaited(userController.loadPointDeVenteDesignation()); + } + + print('✅ Utilisateur sauvegardé rapidement'); + } catch (error) { + print('❌ Erreur lors de la sauvegarde: $error'); + throw Exception('Erreur lors de la sauvegarde des données utilisateur'); } +} @override Widget build(BuildContext context) { @@ -169,8 +253,7 @@ class _LoginPageState extends State { width: MediaQuery.of(context).size.width < 500 ? double.infinity : 400, - padding: - const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0), + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0), decoration: BoxDecoration( color: cardColor.withOpacity(0.98), borderRadius: BorderRadius.circular(30.0), @@ -186,6 +269,7 @@ class _LoginPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // Header Center( child: Column( children: [ @@ -219,6 +303,8 @@ class _LoginPageState extends State { ), ), const SizedBox(height: 24), + + // Username Field TextField( controller: _usernameController, enabled: !_isLoading, @@ -241,6 +327,8 @@ class _LoginPageState extends State { ), ), const SizedBox(height: 18.0), + + // Password Field TextField( controller: _passwordController, enabled: !_isLoading, @@ -263,19 +351,104 @@ class _LoginPageState extends State { ), onSubmitted: (_) => _login(), ), + + if (_isLoading) ...[ + const SizedBox(height: 16.0), + Column( + children: [ + // Barre de progression animée + Container( + height: 4, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + color: accentColor.withOpacity(0.2), + ), + child: LayoutBuilder( + builder: (context, constraints) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: constraints.maxWidth * 0.7, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + gradient: LinearGradient( + colors: [accentColor, accentColor.withOpacity(0.7)], + ), + ), + ); + }, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(accentColor), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _loadingMessage, + style: TextStyle( + color: accentColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.left, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + "Le menu se chargera en arrière-plan", + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + ], + ), +], + + // Error Message if (_isErrorVisible) ...[ const SizedBox(height: 12.0), - Text( - _errorMessage, - style: const TextStyle( - color: Colors.redAccent, - fontSize: 15, - fontWeight: FontWeight.w600, + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.withOpacity(0.3)), + ), + child: Row( + children: [ + Icon(Icons.error_outline, color: Colors.red, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + _errorMessage, + style: const TextStyle( + color: Colors.red, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ), - textAlign: TextAlign.center, ), ], + const SizedBox(height: 26.0), + + // Login Button ElevatedButton( onPressed: _isLoading ? null : _login, style: ElevatedButton.styleFrom( @@ -289,13 +462,27 @@ class _LoginPageState extends State { minimumSize: const Size(double.infinity, 52), ), child: _isLoading - ? const SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2.5, - ), + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + const SizedBox(width: 12), + Text( + 'Connexion...', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], ) : const Text( 'Se connecter', @@ -307,16 +494,23 @@ class _LoginPageState extends State { ), ), ), - // Option debug, à enlever en prod - if (_isErrorVisible) ...[ + + // Debug Button (à enlever en production) + if (_isErrorVisible && !_isLoading) ...[ + const SizedBox(height: 8), TextButton( onPressed: () async { try { - final count = - await AppDatabase.instance.getUserCount(); + final count = await AppDatabase.instance.getUserCount(); + final stats = _cacheService.getCacheStats(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('$count utilisateurs trouvés')), + content: Text( + 'BDD: $count utilisateurs\n' + 'Cache: ${stats['users_cached']} utilisateurs en cache', + ), + duration: const Duration(seconds: 3), + ), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( @@ -324,7 +518,13 @@ class _LoginPageState extends State { ); } }, - child: const Text('Debug: Vérifier BDD'), + child: Text( + 'Debug: Vérifier BDD & Cache', + style: TextStyle( + color: primaryColor.withOpacity(0.6), + fontSize: 12, + ), + ), ), ], ], diff --git a/lib/Views/mobilepage.dart b/lib/Views/mobilepage.dart index 1f67983..db5711d 100644 --- a/lib/Views/mobilepage.dart +++ b/lib/Views/mobilepage.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; import 'package:youmazgestion/Components/QrScan.dart'; import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; @@ -508,57 +509,523 @@ class _NouvelleCommandePageState extends State { ); } +// Variables pour le scanner + QRViewController? _qrController; + bool _isScanning = false; + final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR'); + + // 4. Méthode pour démarrer le scan + void _startBarcodeScanning() { + if (_isScanning) return; + + setState(() { + _isScanning = true; + }); + + Get.to(() => _buildScannerPage())?.then((_) { + setState(() { + _isScanning = false; + }); + }); + } + + // 5. Page du scanner + Widget _buildScannerPage() { + return Scaffold( + appBar: AppBar( + title: const Text('Scanner IMEI'), + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _qrController?.dispose(); + Get.back(); + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.flash_on), + onPressed: () async { + await _qrController?.toggleFlash(); + }, + ), + IconButton( + icon: const Icon(Icons.flip_camera_ios), + onPressed: () async { + await _qrController?.flipCamera(); + }, + ), + ], + ), + body: Stack( + children: [ + // Scanner view + QRView( + key: _qrKey, + onQRViewCreated: _onQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: Colors.green, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: 250, + ), + ), + + // Instructions overlay + Positioned( + bottom: 100, + left: 20, + right: 20, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(12), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.qr_code_scanner, color: Colors.white, size: 40), + SizedBox(height: 8), + Text( + 'Pointez la caméra vers le code-barres IMEI', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + Text( + 'Le scan se fait automatiquement', + style: TextStyle( + color: Colors.white70, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ), + ); + } + + // 6. Configuration du contrôleur QR + void _onQRViewCreated(QRViewController controller) { + _qrController = controller; + + controller.scannedDataStream.listen((scanData) { + if (scanData.code != null && scanData.code!.isNotEmpty) { + // Pauser le scanner pour éviter les scans multiples + controller.pauseCamera(); + + // Fermer la page du scanner + Get.back(); + + // Traiter le résultat + _findAndAddProductByImei(scanData.code!); + } + }); + } + + // 7. Méthode pour trouver et ajouter un produit par IMEI + Future _findAndAddProductByImei(String scannedImei) async { + try { + // Montrer un indicateur de chargement + Get.dialog( + AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: Colors.green.shade700), + const SizedBox(height: 16), + const Text('Recherche du produit...'), + const SizedBox(height: 8), + Text( + 'IMEI: $scannedImei', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + barrierDismissible: false, + ); + + // Attendre un court instant pour l'effet visuel + await Future.delayed(const Duration(milliseconds: 300)); + + // Chercher le produit avec l'IMEI scanné + Product? foundProduct; + + for (var product in _products) { + if (product.imei?.toLowerCase().trim() == scannedImei.toLowerCase().trim()) { + foundProduct = product; + break; + } + } + + // Fermer l'indicateur de chargement + Get.back(); + + if (foundProduct == null) { + _showProductNotFoundDialog(scannedImei); + return; + } + + // Vérifier le stock + if (foundProduct.stock != null && foundProduct.stock! <= 0) { + Get.snackbar( + 'Stock insuffisant', + 'Le produit "${foundProduct.name}" n\'est plus en stock', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + icon: const Icon(Icons.warning_amber, color: Colors.white), + ); + return; + } + + // Vérifier si le produit peut être ajouté (stock disponible) + final currentQuantity = _quantites[foundProduct.id] ?? 0; + if (foundProduct.stock != null && currentQuantity >= foundProduct.stock!) { + Get.snackbar( + 'Stock limite atteint', + 'Quantité maximum atteinte pour "${foundProduct.name}"', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + icon: const Icon(Icons.warning_amber, color: Colors.white), + ); + return; + } + + // Ajouter le produit au panier + setState(() { + _quantites[foundProduct!.id!] = currentQuantity + 1; + }); + + // Afficher le dialogue de succès + _showSuccessDialog(foundProduct, currentQuantity + 1); + + } catch (e) { + // Fermer l'indicateur de chargement si il est encore ouvert + if (Get.isDialogOpen!) Get.back(); + + Get.snackbar( + 'Erreur', + 'Une erreur est survenue: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + } + + // 8. Dialogue de succès + void _showSuccessDialog(Product product, int newQuantity) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.check_circle, color: Colors.green.shade700), + ), + const SizedBox(width: 12), + const Expanded(child: Text('Produit ajouté !')), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text('Prix: ${product.price.toStringAsFixed(2)} MGA'), + Text('Quantité dans le panier: $newQuantity'), + if (product.stock != null) + Text('Stock restant: ${product.stock! - newQuantity}'), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Continuer'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _showCartBottomSheet(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Voir le panier'), + ), + ], + ), + ); + } + + // 9. Dialogue produit non trouvé + void _showProductNotFoundDialog(String scannedImei) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Icon(Icons.search_off, color: Colors.red.shade600), + const SizedBox(width: 8), + const Text('Produit non trouvé'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Aucun produit trouvé avec cet IMEI:'), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + scannedImei, + style: const TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 12), + Text( + 'Vérifiez que l\'IMEI est correct ou que le produit existe dans la base de données.', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _startBarcodeScanning(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Scanner à nouveau'), + ), + ], + ), + ); + } + + + Widget _buildScanInfoCard() { + return Card( + elevation: 2, + margin: const EdgeInsets.only(bottom: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.qr_code_scanner, + color: Colors.green.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Scanner rapidement un produit via son IMEI pour l\'ajouter au panier', + style: TextStyle( + fontSize: 14, + color: Color.fromARGB(255, 9, 56, 95), + ), + ), + ), + ElevatedButton.icon( + onPressed: _isScanning ? null : _startBarcodeScanning, + icon: _isScanning + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.qr_code_scanner, size: 18), + label: Text(_isScanning ? 'Scan...' : 'Scanner'), + style: ElevatedButton.styleFrom( + backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + ), + ], + ), + ), + ); + } + + // 10. Modifier le Widget build pour ajouter le bouton de scan @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width < 600; return Scaffold( - floatingActionButton: _buildFloatingCartButton(), - drawer: isMobile ? CustomDrawer() : null, + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Bouton de scan + FloatingActionButton( + heroTag: "scan", + onPressed: _isScanning ? null : _startBarcodeScanning, + backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, + foregroundColor: Colors.white, + child: _isScanning + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.qr_code_scanner), + ), + const SizedBox(height: 10), + // Bouton panier existant + _buildFloatingCartButton(), + ], + ), + appBar: CustomAppBar(title: 'Nouvelle commande'), + drawer: CustomDrawer(), body: GestureDetector( - onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs + onTap: _hideAllSuggestions, child: Column( children: [ - // Section des filtres - adaptée comme dans HistoriquePage + // Section d'information sur le scan (desktop) + if (!isMobile) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _buildScanInfoCard(), + ), + + // Section des filtres if (!isMobile) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _buildFilterSection(), ), - // Sur mobile, bouton pour afficher les filtres dans un modal + // Boutons pour mobile if (isMobile) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - icon: const Icon(Icons.filter_alt), - label: const Text('Filtres produits'), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => SingleChildScrollView( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, + child: Row( + children: [ + Expanded( + flex: 2, + child: ElevatedButton.icon( + icon: const Icon(Icons.filter_alt), + label: const Text('Filtres'), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: _buildFilterSection(), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - child: _buildFilterSection(), ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade700, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), ), ), - ), + const SizedBox(width: 8), + Expanded( + flex: 1, + child: ElevatedButton.icon( + icon: _isScanning + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.qr_code_scanner), + label: Text(_isScanning ? 'Scan...' : 'Scan'), + onPressed: _isScanning ? null : _startBarcodeScanning, + style: ElevatedButton.styleFrom( + backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], ), ), - // Compteur de résultats visible en haut sur mobile + // Compteur de résultats Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Container( @@ -588,6 +1055,7 @@ class _NouvelleCommandePageState extends State { ); } + Widget _buildSuggestionsList({required bool isNom}) { if (_clientSuggestions.isEmpty) return const SizedBox(); @@ -1725,23 +2193,21 @@ void _fillFormWithClient(Client client) { } } - @override + @override void dispose() { - // Nettoyer les suggestions - _hideAllSuggestions(); + _qrController?.dispose(); - // Disposer les contrôleurs + // Vos disposals existants... + _hideAllSuggestions(); _nomController.dispose(); _prenomController.dispose(); _emailController.dispose(); _telephoneController.dispose(); _adresseController.dispose(); - - // Disposal des contrôleurs de filtre _searchNameController.dispose(); _searchImeiController.dispose(); _searchReferenceController.dispose(); super.dispose(); } -} \ No newline at end of file + } \ No newline at end of file diff --git a/lib/Views/newCommand.dart b/lib/Views/newCommand.dart index 12922f6..c11a3b0 100644 --- a/lib/Views/newCommand.dart +++ b/lib/Views/newCommand.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; + import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Models/client.dart'; @@ -47,8 +49,12 @@ class _NouvelleCommandePageState extends State { List _clientSuggestions = []; bool _showNomSuggestions = false; bool _showTelephoneSuggestions = false; - GlobalKey _nomFieldKey = GlobalKey(); - GlobalKey _telephoneFieldKey = GlobalKey(); + + + // Variables pour le scanner (identiques à ProductManagementPage) + QRViewController? _qrController; + bool _isScanning = false; + final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR'); @override void initState() { @@ -79,6 +85,518 @@ class _NouvelleCommandePageState extends State { }); } + // === NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE (identiques à ProductManagementPage) === + + void _startAutomaticScanning() { + if (_isScanning) return; + + setState(() { + _isScanning = true; + }); + + Get.to(() => _buildAutomaticScannerPage())?.then((_) { + setState(() { + _isScanning = false; + }); + }); + } + + Widget _buildAutomaticScannerPage() { + return Scaffold( + appBar: AppBar( + title: const Text('Scanner Produit'), + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _qrController?.dispose(); + Get.back(); + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.flash_on), + onPressed: () async { + await _qrController?.toggleFlash(); + }, + ), + IconButton( + icon: const Icon(Icons.flip_camera_ios), + onPressed: () async { + await _qrController?.flipCamera(); + }, + ), + ], + ), + body: Stack( + children: [ + // Scanner view + QRView( + key: _qrKey, + onQRViewCreated: _onAutomaticQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: Colors.green, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: 250, + ), + ), + + // Instructions overlay + Positioned( + bottom: 100, + left: 20, + right: 20, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(12), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.qr_code_scanner, color: Colors.white, size: 40), + SizedBox(height: 8), + Text( + 'Scanner automatiquement un produit', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + Text( + 'Pointez vers QR Code, IMEI ou code-barres', + style: TextStyle( + color: Colors.white70, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ), + ); + } + + void _onAutomaticQRViewCreated(QRViewController controller) { + _qrController = controller; + + controller.scannedDataStream.listen((scanData) { + if (scanData.code != null && scanData.code!.isNotEmpty) { + // Pauser le scanner pour éviter les scans multiples + controller.pauseCamera(); + + // Fermer la page du scanner + Get.back(); + + // Traiter le résultat avec identification automatique + _processScannedData(scanData.code!); + } + }); + } + + Future _processScannedData(String scannedData) async { + try { + // Montrer un indicateur de chargement + Get.dialog( + AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: Colors.green.shade700), + const SizedBox(height: 16), + const Text('Identification du produit...'), + const SizedBox(height: 8), + Text( + 'Code: $scannedData', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + barrierDismissible: false, + ); + + // Attendre un court instant pour l'effet visuel + await Future.delayed(const Duration(milliseconds: 300)); + + // Recherche automatique du produit par différents critères + Product? foundProduct = await _findProductAutomatically(scannedData); + + // Fermer l'indicateur de chargement + Get.back(); + + if (foundProduct == null) { + _showProductNotFoundDialog(scannedData); + return; + } + + // Vérifier le stock + if (foundProduct.stock != null && foundProduct.stock! <= 0) { + Get.snackbar( + 'Stock insuffisant', + 'Le produit "${foundProduct.name}" n\'est plus en stock', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + icon: const Icon(Icons.warning_amber, color: Colors.white), + ); + return; + } + + // Vérifier si le produit peut être ajouté (stock disponible) + final currentQuantity = _quantites[foundProduct.id] ?? 0; + if (foundProduct.stock != null && currentQuantity >= foundProduct.stock!) { + Get.snackbar( + 'Stock limite atteint', + 'Quantité maximum atteinte pour "${foundProduct.name}"', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + icon: const Icon(Icons.warning_amber, color: Colors.white), + ); + return; + } + + // Ajouter le produit au panier + setState(() { + _quantites[foundProduct!.id!] = currentQuantity + 1; + }); + + // Afficher le dialogue de succès + _showProductFoundAndAddedDialog(foundProduct, currentQuantity + 1); + + } catch (e) { + // Fermer l'indicateur de chargement si il est encore ouvert + if (Get.isDialogOpen!) Get.back(); + + Get.snackbar( + 'Erreur', + 'Une erreur est survenue: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red.shade600, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + } + + Future _findProductAutomatically(String scannedData) async { + // Nettoyer les données scannées + final cleanedData = scannedData.trim(); + + // 1. Essayer de trouver par IMEI exact + for (var product in _products) { + if (product.imei?.toLowerCase().trim() == cleanedData.toLowerCase()) { + return product; + } + } + + // 2. Essayer de trouver par référence exacte + for (var product in _products) { + if (product.reference?.toLowerCase().trim() == cleanedData.toLowerCase()) { + return product; + } + } + + // 3. Si c'est une URL QR code, extraire la référence + if (cleanedData.contains('stock.guycom.mg/')) { + final reference = cleanedData.split('/').last; + for (var product in _products) { + if (product.reference?.toLowerCase().trim() == reference.toLowerCase()) { + return product; + } + } + } + + // 4. Recherche par correspondance partielle dans le nom + for (var product in _products) { + if (product.name.toLowerCase().contains(cleanedData.toLowerCase()) && + cleanedData.length >= 3) { + return product; + } + } + + // 5. Utiliser la base de données pour une recherche plus approfondie + try { + // Recherche par IMEI dans la base + final productByImei = await _appDatabase.getProductByIMEI(cleanedData); + if (productByImei != null) { + return productByImei; + } + + // Recherche par référence dans la base + final productByRef = await _appDatabase.getProductByReference(cleanedData); + if (productByRef != null) { + return productByRef; + } + } catch (e) { + print('Erreur recherche base de données: $e'); + } + + return null; + } + + void _showProductFoundAndAddedDialog(Product product, int newQuantity) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.check_circle, color: Colors.green.shade700), + ), + const SizedBox(width: 12), + const Expanded(child: Text('Produit identifié et ajouté !')), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + if (product.imei != null && product.imei!.isNotEmpty) + Text('IMEI: ${product.imei}'), + if (product.reference != null && product.reference!.isNotEmpty) + Text('Référence: ${product.reference}'), + Text('Prix: ${product.price.toStringAsFixed(2)} MGA'), + Text('Quantité dans le panier: $newQuantity'), + if (product.stock != null) + Text('Stock restant: ${product.stock! - newQuantity}'), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(Icons.auto_awesome, + color: Colors.green.shade700, size: 16), + const SizedBox(width: 8), + const Expanded( + child: Text( + 'Produit identifié automatiquement', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Continuer'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _showCartBottomSheet(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Voir le panier'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _startAutomaticScanning(); // Scanner un autre produit + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Scanner encore'), + ), + ], + ), + ); + } + + void _showProductNotFoundDialog(String scannedData) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Icon(Icons.search_off, color: Colors.red.shade600), + const SizedBox(width: 8), + const Text('Produit non trouvé'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Aucun produit trouvé avec ce code:'), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + scannedData, + style: const TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 12), + Text( + 'Vérifiez que le code est correct ou que le produit existe dans la base de données.', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Types de codes supportés:', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + const SizedBox(height: 4), + Text( + '• QR Code produit\n• IMEI (téléphones)\n• Référence produit\n• Code-barres', + style: TextStyle( + fontSize: 11, + color: Colors.blue.shade600, + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + _startAutomaticScanning(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Scanner à nouveau'), + ), + ], + ), + ); + } + + Widget _buildAutoScanInfoCard() { + return Card( + elevation: 2, + margin: const EdgeInsets.only(bottom: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.auto_awesome, + color: Colors.green.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Scanner automatiquement: QR Code, IMEI, Référence ou code-barres', + style: TextStyle( + fontSize: 14, + color: Color.fromARGB(255, 9, 56, 95), + ), + ), + ), + ElevatedButton.icon( + onPressed: _isScanning ? null : _startAutomaticScanning, + icon: _isScanning + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.qr_code_scanner, size: 18), + label: Text(_isScanning ? 'Scan...' : 'Scanner'), + style: ElevatedButton.styleFrom( + backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + ), + ], + ), + ), + ); + } + + // === FIN DES NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE === + // Méthode pour vider complètement le formulaire et le panier void _clearFormAndCart() { setState(() { @@ -106,29 +624,15 @@ class _NouvelleCommandePageState extends State { } Future _showClientSuggestions(String query, {required bool isNom}) async { - if (query.length < 3) { - _hideAllSuggestions(); - return; - } - - final suggestions = await _appDatabase.suggestClients(query); - - setState(() { - _clientSuggestions = suggestions; - if (isNom) { - _showNomSuggestions = true; - _showTelephoneSuggestions = false; - } else { - _showTelephoneSuggestions = true; - _showNomSuggestions = false; + if (query.length < 3) { + _hideAllSuggestions(); + return; } - }); -} - - void _showOverlay({required bool isNom}) { - // Utiliser une approche plus simple avec setState + + final suggestions = await _appDatabase.suggestClients(query); + setState(() { - _clientSuggestions = _clientSuggestions; + _clientSuggestions = suggestions; if (isNom) { _showNomSuggestions = true; _showTelephoneSuggestions = false; @@ -139,6 +643,7 @@ class _NouvelleCommandePageState extends State { }); } + void _fillClientForm(Client client) { setState(() { _nomController.text = client.nom; @@ -160,18 +665,18 @@ class _NouvelleCommandePageState extends State { void _hideNomSuggestions() { if (mounted && _showNomSuggestions) { - setState(() { - _showNomSuggestions = false; - }); - } + setState(() { + _showNomSuggestions = false; + }); + } } void _hideTelephoneSuggestions() { - if (mounted && _showTelephoneSuggestions){ - setState(() { - _showTelephoneSuggestions = false; - }); - } + if (mounted && _showTelephoneSuggestions){ + setState(() { + _showTelephoneSuggestions = false; + }); + } } void _hideAllSuggestions() { @@ -417,146 +922,7 @@ class _NouvelleCommandePageState extends State { ), ), ); - } - - @override - Widget build(BuildContext context) { - final isMobile = MediaQuery.of(context).size.width < 600; - - return Scaffold( - floatingActionButton: _buildFloatingCartButton(), - drawer: isMobile ? CustomDrawer() : null, - body: GestureDetector( - onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs - child: Column( - children: [ - // Section des filtres - adaptée comme dans HistoriquePage - if (!isMobile) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: _buildFilterSection(), - ), - - // Sur mobile, bouton pour afficher les filtres dans un modal - if (isMobile) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - icon: const Icon(Icons.filter_alt), - label: const Text('Filtres produits'), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => SingleChildScrollView( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: _buildFilterSection(), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade700, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - ), - // Compteur de résultats visible en haut sur mobile - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${_filteredProducts.length} produit(s)', - style: TextStyle( - color: Colors.blue.shade700, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - - // Liste des produits - Expanded( - child: _buildProductList(), - ), - ], - ), - ), - ); - } - - Widget _buildSuggestionsList({required bool isNom}) { - if (_clientSuggestions.isEmpty) return const SizedBox(); - - return Container( - margin: const EdgeInsets.only(top: 4), - constraints: const BoxConstraints(maxHeight: 150), - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: _clientSuggestions.length, - itemBuilder: (context, index) { - final client = _clientSuggestions[index]; - return ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: Colors.blue.shade100, - child: Icon( - Icons.person, - size: 16, - color: Colors.blue.shade700, - ), - ), - title: Text( - '${client.nom} ${client.prenom}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - '${client.telephone} • ${client.email}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - onTap: () { - _fillClientForm(client); - _hideAllSuggestions(); - }, - ); - }, - ), - ); -} + } Widget _buildFloatingCartButton() { final isMobile = MediaQuery.of(context).size.width < 600; @@ -576,114 +942,116 @@ class _NouvelleCommandePageState extends State { ); } + + void _showClientFormDialog() { - final isMobile = MediaQuery.of(context).size.width < 600; - - // Variables locales pour les suggestions dans le dialog - bool showNomSuggestions = false; - bool showPrenomSuggestions = false; - bool showEmailSuggestions = false; - bool showTelephoneSuggestions = false; - List localClientSuggestions = []; - - // GlobalKeys pour positionner les overlays - final GlobalKey nomFieldKey = GlobalKey(); - final GlobalKey prenomFieldKey = GlobalKey(); - final GlobalKey emailFieldKey = GlobalKey(); - final GlobalKey telephoneFieldKey = GlobalKey(); - - Get.dialog( - StatefulBuilder( - builder: (context, setDialogState) { - return Stack( - children: [ - AlertDialog( - title: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.shade100, - borderRadius: BorderRadius.circular(8), + final isMobile = MediaQuery.of(context).size.width < 600; + + // Variables locales pour les suggestions dans le dialog + bool showNomSuggestions = false; + bool showPrenomSuggestions = false; + bool showEmailSuggestions = false; + bool showTelephoneSuggestions = false; + List localClientSuggestions = []; + + // GlobalKeys pour positionner les overlays + final GlobalKey nomFieldKey = GlobalKey(); + final GlobalKey prenomFieldKey = GlobalKey(); + final GlobalKey emailFieldKey = GlobalKey(); + final GlobalKey telephoneFieldKey = GlobalKey(); + + Get.dialog( + StatefulBuilder( + builder: (context, setDialogState) { + return Stack( + children: [ + AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.person_add, color: Colors.blue.shade700), ), - child: Icon(Icons.person_add, color: Colors.blue.shade700), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - isMobile ? 'Client' : 'Informations Client', - style: TextStyle(fontSize: isMobile ? 16 : 18), + const SizedBox(width: 12), + Expanded( + child: Text( + isMobile ? 'Client' : 'Informations Client', + style: TextStyle(fontSize: isMobile ? 16 : 18), + ), ), - ), - ], - ), - content: Container( - width: isMobile ? double.maxFinite : 600, - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, + ], ), - child: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Champ Nom avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: nomFieldKey, - controller: _nomController, - label: 'Nom', - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer un nom' : null, - onChanged: (value) async { - if (value.length >= 2) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showNomSuggestions = suggestions.isNotEmpty; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - }); - } else { - setDialogState(() { - showNomSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - // Champ Prénom avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: prenomFieldKey, - controller: _prenomController, - label: 'Prénom', - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer un prénom' : null, - onChanged: (value) async { - if (value.length >= 2) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showPrenomSuggestions = suggestions.isNotEmpty; - showNomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - }); - } else { - setDialogState(() { - showPrenomSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - // Champ Email avec suggestions (SANS bouton recherche) + content: Container( + width: isMobile ? double.maxFinite : 600, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Champ Nom avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: nomFieldKey, + controller: _nomController, + label: 'Nom', + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un nom' : null, + onChanged: (value) async { + if (value.length >= 2) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showNomSuggestions = suggestions.isNotEmpty; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showNomSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Prénom avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: prenomFieldKey, + controller: _prenomController, + label: 'Prénom', + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un prénom' : null, + onChanged: (value) async { + if (value.length >= 2) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showPrenomSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + }); + } else { + setDialogState(() { + showPrenomSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + // Champ Email avec suggestions (SANS bouton recherche) _buildTextFormFieldWithKey( key: emailFieldKey, controller: _emailController, @@ -714,208 +1082,209 @@ class _NouvelleCommandePageState extends State { } }, ), - const SizedBox(height: 12), - - // Champ Téléphone avec suggestions (SANS bouton recherche) - _buildTextFormFieldWithKey( - key: telephoneFieldKey, - controller: _telephoneController, - label: 'Téléphone', - keyboardType: TextInputType.phone, - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer un téléphone' : null, - onChanged: (value) async { - if (value.length >= 3) { - final suggestions = await _appDatabase.suggestClients(value); - setDialogState(() { - localClientSuggestions = suggestions; - showTelephoneSuggestions = suggestions.isNotEmpty; - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - }); - } else { - setDialogState(() { - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - } - }, - ), - const SizedBox(height: 12), - - _buildTextFormField( - controller: _adresseController, - label: 'Adresse', - maxLines: 2, - validator: (value) => value?.isEmpty ?? true - ? 'Veuillez entrer une adresse' : null, - ), - const SizedBox(height: 12), - _buildCommercialDropdown(), - ], + const SizedBox(height: 12), + + // Champ Téléphone avec suggestions (SANS bouton recherche) + _buildTextFormFieldWithKey( + key: telephoneFieldKey, + controller: _telephoneController, + label: 'Téléphone', + keyboardType: TextInputType.phone, + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer un téléphone' : null, + onChanged: (value) async { + if (value.length >= 3) { + final suggestions = await _appDatabase.suggestClients(value); + setDialogState(() { + localClientSuggestions = suggestions; + showTelephoneSuggestions = suggestions.isNotEmpty; + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + }); + } else { + setDialogState(() { + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + } + }, + ), + const SizedBox(height: 12), + + _buildTextFormField( + controller: _adresseController, + label: 'Adresse', + maxLines: 2, + validator: (value) => value?.isEmpty ?? true + ? 'Veuillez entrer une adresse' : null, + ), + const SizedBox(height: 12), + _buildCommercialDropdown(), + ], + ), ), ), ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Annuler'), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade800, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 16 : 20, - vertical: isMobile ? 10 : 12 + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade800, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 16 : 20, + vertical: isMobile ? 10 : 12 + ), + ), + onPressed: () { + if (_formKey.currentState!.validate()) { + // Fermer toutes les suggestions avant de soumettre + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + Get.back(); + _submitOrder(); + } + }, + child: Text( + isMobile ? 'Valider' : 'Valider la commande', + style: TextStyle(fontSize: isMobile ? 12 : 14), ), ), - onPressed: () { - if (_formKey.currentState!.validate()) { - // Fermer toutes les suggestions avant de soumettre - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - Get.back(); - _submitOrder(); - } + ], + ), + + // Overlay pour les suggestions du nom + if (showNomSuggestions) + _buildSuggestionOverlay( + fieldKey: nomFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showNomSuggestions = false; + localClientSuggestions = []; + }); }, - child: Text( - isMobile ? 'Valider' : 'Valider la commande', - style: TextStyle(fontSize: isMobile ? 12 : 14), - ), ), - ], - ), - - // Overlay pour les suggestions du nom - if (showNomSuggestions) - _buildSuggestionOverlay( - fieldKey: nomFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showNomSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - - // Overlay pour les suggestions du prénom - if (showPrenomSuggestions) - _buildSuggestionOverlay( - fieldKey: prenomFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showPrenomSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - - // Overlay pour les suggestions de l'email - if (showEmailSuggestions) - _buildSuggestionOverlay( - fieldKey: emailFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showEmailSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - - // Overlay pour les suggestions du téléphone - if (showTelephoneSuggestions) - _buildSuggestionOverlay( - fieldKey: telephoneFieldKey, - suggestions: localClientSuggestions, - onClientSelected: (client) { - _fillFormWithClient(client); - setDialogState(() { - showNomSuggestions = false; - showPrenomSuggestions = false; - showEmailSuggestions = false; - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - onDismiss: () { - setDialogState(() { - showTelephoneSuggestions = false; - localClientSuggestions = []; - }); - }, - ), - ], - ); - }, - ), - ); -} + + // Overlay pour les suggestions du prénom + if (showPrenomSuggestions) + _buildSuggestionOverlay( + fieldKey: prenomFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showPrenomSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions de l'email + if (showEmailSuggestions) + _buildSuggestionOverlay( + fieldKey: emailFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showEmailSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + + // Overlay pour les suggestions du téléphone + if (showTelephoneSuggestions) + _buildSuggestionOverlay( + fieldKey: telephoneFieldKey, + suggestions: localClientSuggestions, + onClientSelected: (client) { + _fillFormWithClient(client); + setDialogState(() { + showNomSuggestions = false; + showPrenomSuggestions = false; + showEmailSuggestions = false; + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + onDismiss: () { + setDialogState(() { + showTelephoneSuggestions = false; + localClientSuggestions = []; + }); + }, + ), + ], + ); + }, + ), + ); + } // Widget pour créer un TextFormField avec une clé -Widget _buildTextFormFieldWithKey({ - required GlobalKey key, - required TextEditingController controller, - required String label, - TextInputType? keyboardType, - int maxLines = 1, - String? Function(String?)? validator, - void Function(String)? onChanged, -}) { - return Container( - key: key, - child: _buildTextFormField( - controller: controller, - label: label, - keyboardType: keyboardType, - maxLines: maxLines, - validator: validator, - onChanged: onChanged, - ), - ); -} + Widget _buildTextFormFieldWithKey({ + required GlobalKey key, + required TextEditingController controller, + required String label, + TextInputType? keyboardType, + int maxLines = 1, + String? Function(String?)? validator, + void Function(String)? onChanged, + }) { + return Container( + key: key, + child: _buildTextFormField( + controller: controller, + label: label, + keyboardType: keyboardType, + maxLines: maxLines, + validator: validator, + onChanged: onChanged, + ), + ); + } // Widget pour l'overlay des suggestions + // Widget pour l'overlay des suggestions Widget _buildSuggestionOverlay({ required GlobalKey fieldKey, required List suggestions, @@ -1020,25 +1389,25 @@ Widget _buildSuggestionOverlay({ ), ), ); -} + } // Méthode pour remplir le formulaire avec les données du client -void _fillFormWithClient(Client client) { - _nomController.text = client.nom; - _prenomController.text = client.prenom; - _emailController.text = client.email; - _telephoneController.text = client.telephone; - _adresseController.text = client.adresse ?? ''; - - Get.snackbar( - 'Client trouvé', - 'Les informations ont été remplies automatiquement', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); -} + void _fillFormWithClient(Client client) { + _nomController.text = client.nom; + _prenomController.text = client.prenom; + _emailController.text = client.email; + _telephoneController.text = client.telephone; + _adresseController.text = client.adresse ?? ''; + + Get.snackbar( + 'Client trouvé', + 'Les informations ont été remplies automatiquement', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } Widget _buildTextFormField({ required TextEditingController controller, @@ -1638,20 +2007,163 @@ void _fillFormWithClient(Client client) { @override void dispose() { - // Nettoyer les suggestions - _hideAllSuggestions(); + _qrController?.dispose(); - // Disposer les contrôleurs + // Vos disposals existants... + _hideAllSuggestions(); _nomController.dispose(); _prenomController.dispose(); _emailController.dispose(); _telephoneController.dispose(); _adresseController.dispose(); - - // Disposal des contrôleurs de filtre _searchNameController.dispose(); _searchImeiController.dispose(); _searchReferenceController.dispose(); super.dispose(); - }} \ No newline at end of file + } + + // 10. Modifier le Widget build pour utiliser le nouveau scan automatique + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < 600; + + return Scaffold( + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Bouton de scan automatique (remplace l'ancien scan IMEI) + FloatingActionButton( + heroTag: "auto_scan", + onPressed: _isScanning ? null : _startAutomaticScanning, + backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, + foregroundColor: Colors.white, + child: _isScanning + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.auto_awesome), + ), + const SizedBox(height: 10), + // Bouton panier existant + _buildFloatingCartButton(), + ], + ), + appBar: CustomAppBar(title: 'Nouvelle commande'), + drawer: CustomDrawer(), + body: GestureDetector( + onTap: _hideAllSuggestions, + child: Column( + children: [ + // Section d'information sur le scan automatique (desktop) + if (!isMobile) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _buildAutoScanInfoCard(), + ), + + // Section des filtres + if (!isMobile) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _buildFilterSection(), + ), + + // Boutons pour mobile + if (isMobile) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + Expanded( + flex: 2, + child: ElevatedButton.icon( + icon: const Icon(Icons.filter_alt), + label: const Text('Filtres'), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: _buildFilterSection(), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 1, + child: ElevatedButton.icon( + icon: _isScanning + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.auto_awesome), + label: Text(_isScanning ? 'Scan...' : 'Auto-scan'), + onPressed: _isScanning ? null : _startAutomaticScanning, + style: ElevatedButton.styleFrom( + backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], + ), + ), + // Compteur de résultats + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_filteredProducts.length} produit(s)', + style: TextStyle( + color: Colors.blue.shade700, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + + // Liste des produits + Expanded( + child: _buildProductList(), + ), + ], + ), + ), + ); + } + +} \ No newline at end of file diff --git a/lib/config/DatabaseConfig.dart b/lib/config/DatabaseConfig.dart index cb59fca..fdb80b8 100644 --- a/lib/config/DatabaseConfig.dart +++ b/lib/config/DatabaseConfig.dart @@ -1,12 +1,12 @@ // Config/database_config.dart - Version améliorée class DatabaseConfig { - static const String host = 'database.c4m.mg'; + static const String host = '172.20.10.5'; static const int port = 3306; - static const String username = 'guycom'; - static const String password = '3iV59wjRdbuXAPR'; - static const String database = 'guycom'; + static const String username = 'root'; + static const String? password = null; + static const String database = 'guycom_databse_v1'; - static const String prodHost = 'database.c4m.mg'; + static const String prodHost = '185.70.105.157'; static const String prodUsername = 'guycom'; static const String prodPassword = '3iV59wjRdbuXAPR'; static const String prodDatabase = 'guycom'; @@ -17,7 +17,7 @@ class DatabaseConfig { static const int maxConnections = 10; static const int minConnections = 2; - static bool get isDevelopment => false; + static bool get isDevelopment => true; static Map getConfig() { if (isDevelopment) { diff --git a/lib/controller/userController.dart b/lib/controller/userController.dart index c64d439..ba170c1 100644 --- a/lib/controller/userController.dart +++ b/lib/controller/userController.dart @@ -2,7 +2,7 @@ import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; -//import 'package:youmazgestion/Services/app_database.dart'; +import 'package:youmazgestion/Services/PermissionCacheService.dart'; class UserController extends GetxController { final _username = ''.obs; @@ -11,10 +11,14 @@ class UserController extends GetxController { final _name = ''.obs; final _lastname = ''.obs; final _password = ''.obs; - final _userId = 0.obs; // ✅ Ajout de l'ID utilisateur + final _userId = 0.obs; final _pointDeVenteId = 0.obs; final _pointDeVenteDesignation = ''.obs; + + // Cache service + final PermissionCacheService _cacheService = PermissionCacheService.instance; + // Getters String get username => _username.value; String get email => _email.value; String get role => _role.value; @@ -28,92 +32,111 @@ class UserController extends GetxController { @override void onInit() { super.onInit(); - loadUserData(); // Charger les données au démarrage + loadUserData(); } - // ✅ CORRECTION : Charger les données complètes depuis SharedPreferences ET la base de données - Future loadUserData() async { - try { - final prefs = await SharedPreferences.getInstance(); - - final storedUsername = prefs.getString('username') ?? ''; - final storedRole = prefs.getString('role') ?? ''; - final storedUserId = prefs.getInt('user_id') ?? 0; - final storedPointDeVenteId = prefs.getInt('point_de_vente_id') ?? 0; - final storedPointDeVenteDesignation = prefs.getString('point_de_vente_designation') ?? ''; - - if (storedUsername.isNotEmpty) { - try { - Users user = await AppDatabase.instance.getUser(storedUsername); - - _username.value = user.username; - _email.value = user.email; - _name.value = user.name; - _lastname.value = user.lastName; - _password.value = user.password; - _role.value = storedRole; - _userId.value = storedUserId; - _pointDeVenteId.value = storedPointDeVenteId; - _pointDeVenteDesignation.value = storedPointDeVenteDesignation; - - // Si la désignation n'est pas sauvegardée, on peut la récupérer - if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) { - await loadPointDeVenteDesignation(); + /// ✅ SIMPLIFIÉ: Charge les données utilisateur sans cache persistant + Future loadUserData() async { + try { + final prefs = await SharedPreferences.getInstance(); + + final storedUsername = prefs.getString('username') ?? ''; + final storedRole = prefs.getString('role') ?? ''; + final storedUserId = prefs.getInt('user_id') ?? 0; + final storedPointDeVenteId = prefs.getInt('point_de_vente_id') ?? 0; + final storedPointDeVenteDesignation = prefs.getString('point_de_vente_designation') ?? ''; + + if (storedUsername.isNotEmpty) { + try { + Users user = await AppDatabase.instance.getUser(storedUsername); + + _username.value = user.username; + _email.value = user.email; + _name.value = user.name; + _lastname.value = user.lastName; + _password.value = user.password; + _role.value = storedRole; + _userId.value = storedUserId; + _pointDeVenteId.value = storedPointDeVenteId; + _pointDeVenteDesignation.value = storedPointDeVenteDesignation; + + if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) { + await loadPointDeVenteDesignation(); + } + + // ✅ Précharger les permissions en arrière-plan (non bloquant) + _preloadPermissionsInBackground(); + + } catch (dbError) { + print("❌ Erreur BDD, utilisation du fallback: $dbError"); + _username.value = storedUsername; + _email.value = prefs.getString('email') ?? ''; + _role.value = storedRole; + _name.value = prefs.getString('name') ?? ''; + _lastname.value = prefs.getString('lastname') ?? ''; + _userId.value = storedUserId; + _pointDeVenteId.value = storedPointDeVenteId; + _pointDeVenteDesignation.value = storedPointDeVenteDesignation; + + // Précharger quand même + _preloadPermissionsInBackground(); } - - } catch (dbError) { - // Fallback - _username.value = storedUsername; - _email.value = prefs.getString('email') ?? ''; - _role.value = storedRole; - _name.value = prefs.getString('name') ?? ''; - _lastname.value = prefs.getString('lastname') ?? ''; - _userId.value = storedUserId; - _pointDeVenteId.value = storedPointDeVenteId; - _pointDeVenteDesignation.value = storedPointDeVenteDesignation; } + } catch (e) { + print('❌ Erreur lors du chargement des données utilisateur: $e'); } - } catch (e) { - print('❌ Erreur lors du chargement des données utilisateur: $e'); } -} -Future loadPointDeVenteDesignation() async { - if (_pointDeVenteId.value <= 0) return; - - try { - final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value); - if (pointDeVente != null) { - _pointDeVenteDesignation.value = pointDeVente['designation'] as String; - await saveUserData(); // Sauvegarder la désignation + + /// ✅ Précharge les permissions en arrière-plan (non bloquant) + void _preloadPermissionsInBackground() { + if (_username.value.isNotEmpty) { + // Lancer en arrière-plan sans attendre + Future.microtask(() async { + try { + await _cacheService.preloadUserData(_username.value); + } catch (e) { + print("⚠️ Erreur préchargement permissions (non critique): $e"); + } + }); } - } catch (e) { - print('❌ Erreur lors du chargement de la désignation du point de vente: $e'); } -} - // ✅ NOUVELLE MÉTHODE : Mise à jour complète avec Users + credentials + Future loadPointDeVenteDesignation() async { + if (_pointDeVenteId.value <= 0) return; + + try { + final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value); + if (pointDeVente != null) { + _pointDeVenteDesignation.value = pointDeVente['nom'] as String; + await saveUserData(); + } + } catch (e) { + print('❌ Erreur lors du chargement de la désignation du point de vente: $e'); + } + } + + /// ✅ Mise à jour avec préchargement des permissions void setUserWithCredentials(Users user, String role, int userId) { _username.value = user.username; _email.value = user.email; - _role.value = role; // Rôle depuis les credentials + _role.value = role; _name.value = user.name; _lastname.value = user.lastName; _password.value = user.password; - _userId.value = userId; // ID depuis les credentials + _userId.value = userId; _pointDeVenteId.value = user.pointDeVenteId ?? 0; print("✅ Utilisateur mis à jour avec credentials:"); print(" Username: ${_username.value}"); - print(" Name: ${_name.value}"); - print(" Email: ${_email.value}"); print(" Role: ${_role.value}"); print(" UserID: ${_userId.value}"); - // Sauvegarder dans SharedPreferences saveUserData(); + + // ✅ Précharger immédiatement les permissions après connexion + _preloadPermissionsInBackground(); } - // ✅ MÉTHODE EXISTANTE AMÉLIORÉE void setUser(Users user) { _username.value = user.username; _email.value = user.email; @@ -121,90 +144,107 @@ Future loadPointDeVenteDesignation() async { _name.value = user.name; _lastname.value = user.lastName; _password.value = user.password; - // Note: _userId reste inchangé si pas fourni - - print("✅ Utilisateur mis à jour (méthode legacy):"); - print(" Username: ${_username.value}"); - print(" Role: ${_role.value}"); - // Sauvegarder dans SharedPreferences saveUserData(); + _preloadPermissionsInBackground(); } - // ✅ CORRECTION : Sauvegarder TOUTES les données importantes Future saveUserData() async { - try { - final prefs = await SharedPreferences.getInstance(); - - await prefs.setString('username', _username.value); - await prefs.setString('email', _email.value); - await prefs.setString('role', _role.value); - await prefs.setString('name', _name.value); - await prefs.setString('lastname', _lastname.value); - await prefs.setInt('user_id', _userId.value); - await prefs.setInt('point_de_vente_id', _pointDeVenteId.value); - await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value); - - print("✅ Données sauvegardées avec succès dans SharedPreferences"); - } catch (e) { - print('❌ Erreur lors de la sauvegarde des données utilisateur: $e'); + try { + final prefs = await SharedPreferences.getInstance(); + + await prefs.setString('username', _username.value); + await prefs.setString('email', _email.value); + await prefs.setString('role', _role.value); + await prefs.setString('name', _name.value); + await prefs.setString('lastname', _lastname.value); + await prefs.setInt('user_id', _userId.value); + await prefs.setInt('point_de_vente_id', _pointDeVenteId.value); + await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value); + + print("✅ Données sauvegardées avec succès"); + } catch (e) { + print('❌ Erreur lors de la sauvegarde: $e'); + } } -} - // ✅ CORRECTION : Vider TOUTES les données (SharedPreferences + Observables) -Future clearUserData() async { - try { - final prefs = await SharedPreferences.getInstance(); - - await prefs.remove('username'); - await prefs.remove('email'); - await prefs.remove('role'); - await prefs.remove('name'); - await prefs.remove('lastname'); - await prefs.remove('user_id'); - await prefs.remove('point_de_vente_id'); - await prefs.remove('point_de_vente_designation'); - - _username.value = ''; - _email.value = ''; - _role.value = ''; - _name.value = ''; - _lastname.value = ''; - _password.value = ''; - _userId.value = 0; - _pointDeVenteId.value = 0; - _pointDeVenteDesignation.value = ''; - - } catch (e) { - print('❌ Erreur lors de l\'effacement des données utilisateur: $e'); + /// ✅ MODIFIÉ: Vider les données ET le cache de session + Future clearUserData() async { + try { + final prefs = await SharedPreferences.getInstance(); + + // ✅ IMPORTANT: Vider le cache de session + _cacheService.clearAllCache(); + + // Effacer SharedPreferences + await prefs.remove('username'); + await prefs.remove('email'); + await prefs.remove('role'); + await prefs.remove('name'); + await prefs.remove('lastname'); + await prefs.remove('user_id'); + await prefs.remove('point_de_vente_id'); + await prefs.remove('point_de_vente_designation'); + + // Effacer les observables + _username.value = ''; + _email.value = ''; + _role.value = ''; + _name.value = ''; + _lastname.value = ''; + _password.value = ''; + _userId.value = 0; + _pointDeVenteId.value = 0; + _pointDeVenteDesignation.value = ''; + + print("✅ Données utilisateur et cache de session vidés"); + + } catch (e) { + print('❌ Erreur lors de l\'effacement: $e'); + } } -} - // ✅ MÉTHODE UTILITAIRE : Vérifier si un utilisateur est connecté + // Getters utilitaires bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0; - - // ✅ MÉTHODE UTILITAIRE : Obtenir le nom complet String get fullName => '${_name.value} ${_lastname.value}'.trim(); + /// ✅ OPTIMISÉ: Vérification des permissions depuis le cache de session Future hasPermission(String permission, String route) async { try { if (_username.value.isEmpty) { - print('⚠️ Username vide, rechargement des données...'); + print('⚠️ Username vide, rechargement...'); await loadUserData(); } if (_username.value.isEmpty) { - print('❌ Impossible de vérifier les permissions : utilisateur non connecté'); + print('❌ Utilisateur non connecté'); return false; } - return await AppDatabase.instance.hasPermission(username, permission, route); + // Essayer d'abord le cache + if (_cacheService.isLoaded) { + return _cacheService.hasPermission(_username.value, permission, route); + } + + // Si pas encore chargé, charger et essayer de nouveau + print("🔄 Cache non chargé, chargement des permissions..."); + await _cacheService.loadUserPermissions(_username.value); + + return _cacheService.hasPermission(_username.value, permission, route); + } catch (e) { print('❌ Erreur vérification permission: $e'); - return false; // Sécurité : refuser l'accès en cas d'erreur + // Fallback vers la méthode originale en cas d'erreur + try { + return await AppDatabase.instance.hasPermission(_username.value, permission, route); + } catch (fallbackError) { + print('❌ Erreur fallback permission: $fallbackError'); + return false; + } } } + /// ✅ Vérification de permissions multiples Future hasAnyPermission(List permissionNames, String menuRoute) async { for (String permissionName in permissionNames) { if (await hasPermission(permissionName, menuRoute)) { @@ -214,18 +254,40 @@ Future clearUserData() async { return false; } - // ✅ MÉTHODE DEBUG : Afficher l'état actuel + /// ✅ Obtenir les menus accessibles depuis le cache + List> getUserMenus() { + if (_username.value.isEmpty || !_cacheService.isLoaded) return []; + return _cacheService.getUserMenus(_username.value); + } + + /// ✅ Vérifier l'accès à un menu depuis le cache + bool hasMenuAccess(String menuRoute) { + if (_username.value.isEmpty || !_cacheService.isLoaded) return false; + return _cacheService.hasMenuAccess(_username.value, menuRoute); + } + + /// ✅ Forcer le rechargement des permissions (pour les admins après modification) + Future refreshPermissions() async { + if (_username.value.isNotEmpty) { + await _cacheService.refreshUserPermissions(_username.value); + } + } + + /// ✅ Vérifier si le cache est prêt + bool get isCacheReady => _cacheService.isLoaded && _username.value.isNotEmpty; + + /// Debug void debugPrintUserState() { - print("=== ÉTAT UTILISATEUR ==="); - print("Username: ${_username.value}"); - print("Name: ${_name.value}"); - print("Lastname: ${_lastname.value}"); - print("Email: ${_email.value}"); - print("Role: ${_role.value}"); - print("UserID: ${_userId.value}"); - print("PointDeVenteID: ${_pointDeVenteId.value}"); - print("PointDeVente: ${_pointDeVenteDesignation.value}"); - print("IsLoggedIn: $isLoggedIn"); - print("========================"); -} + print("=== ÉTAT UTILISATEUR ==="); + print("Username: ${_username.value}"); + print("Name: ${_name.value}"); + print("Role: ${_role.value}"); + print("UserID: ${_userId.value}"); + print("IsLoggedIn: $isLoggedIn"); + print("Cache Ready: $isCacheReady"); + print("========================"); + + // Debug du cache + _cacheService.debugPrintCache(); + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index e2773cc..4490c50 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,7 +17,7 @@ void main() async { // Initialiser la base de données MySQL print("Connexion à la base de données MySQL..."); - await AppDatabase.instance.initDatabase(); + // await AppDatabase.instance.initDatabase(); print("Base de données initialisée avec succès !"); // Afficher les informations de la base (pour debug) diff --git a/lib/my_app.dart b/lib/my_app.dart index b8b6d42..b63fe19 100644 --- a/lib/my_app.dart +++ b/lib/my_app.dart @@ -1,21 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; -import 'dart:io'; - -import 'Views/ErreurPage.dart'; import 'Views/loginPage.dart'; class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); - + static bool isRegisterOpen = false; static DateTime? startDate; - static late String path; - + static const Gradient primaryGradient = LinearGradient( colors: [ Colors.white, - const Color.fromARGB(255, 4, 54, 95), + Color.fromARGB(255, 4, 54, 95), ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -24,56 +19,17 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'GUYCOM', debugShowCheckedModeBanner: false, theme: ThemeData( canvasColor: Colors.transparent, ), - home: Builder( - builder: (context) { - return FutureBuilder( - future: - checkLocalDatabasesExist(), // Appel à la fonction de vérification - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - // Affichez un indicateur de chargement si nécessaire - return const CircularProgressIndicator(); - } else if (snapshot.hasError || !(snapshot.data ?? false)) { - // S'il y a une erreur ou si les bases de données n'existent pas - return ErreurPage( - dbPath: - path); // Redirigez vers la page d'erreur en affichant le chemin de la base de données - } else { - // Si les bases de données existent, affichez la page d'accueil normalement - return Container( - decoration: const BoxDecoration( - gradient: MyApp.primaryGradient, - ), - child: const LoginPage(), - ); - } - }, - ); - }, + home: Container( + decoration: const BoxDecoration( + gradient: MyApp.primaryGradient, + ), + child: const LoginPage(), ), ); } - - Future checkLocalDatabasesExist() async { - final documentsDirectory = await getApplicationDocumentsDirectory(); - final dbPath = documentsDirectory.path; - path = dbPath; - - // Vérifier si le fichier de base de données products2.db existe - final productsDBFile = File('$dbPath/products2.db'); - final productsDBExists = await productsDBFile.exists(); - - // Vérifier si le fichier de base de données auth.db existe - final authDBFile = File('$dbPath/usersDb.db'); - final authDBExists = await authDBFile.exists(); - - // Vérifier si d'autres bases de données nécessaires existent, le cas échéant - - return productsDBExists && authDBExists; - } -} +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index b365652..35e5036 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -872,6 +872,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + qr_code_scanner_plus: + dependency: "direct main" + description: + name: qr_code_scanner_plus + sha256: "39696b50d277097ee4d90d4292de36f38c66213a4f5216a06b2bdd2b63117859" + url: "https://pub.dev" + source: hosted + version: "2.0.10+1" qr_flutter: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e8299f1..f5ede25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: mobile_scanner: ^5.0.0 # ou la version la plus récente fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons numbers_to_letters: ^1.0.0 + qr_code_scanner_plus: ^2.0.10+1 diff --git a/test/widget_test.dart b/test/widget_test.dart index d079089..eb559cd 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -7,8 +7,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:youmazgestion/my_app.dart'; + -import 'package:guycom/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async {