Browse Source

scan code bar

13062025
b.razafimandimbihery 6 months ago
parent
commit
525b09c81f
  1. 883
      lib/Components/appDrawer.dart
  2. 175
      lib/Components/commandManagementComponents/CommandDetails.dart
  3. 226
      lib/Components/commandManagementComponents/CommandeActions.dart
  4. 189
      lib/Components/commandManagementComponents/DiscountDialog.dart
  5. 136
      lib/Components/commandManagementComponents/GiftSelectionDialog.dart
  6. 8
      lib/Components/commandManagementComponents/PaymentMethod.dart
  7. 288
      lib/Components/commandManagementComponents/PaymentMethodDialog.dart
  8. 7
      lib/Components/commandManagementComponents/PaymentType.dart
  9. 2125
      lib/Components/teat.dart
  10. 48
      lib/Models/Client.dart
  11. 258
      lib/Services/PermissionCacheService.dart
  12. 471
      lib/Services/stock_managementDatabase.dart
  13. 4643
      lib/Views/HandleProduct.dart
  14. 1048
      lib/Views/commandManagement.dart
  15. 400
      lib/Views/loginPage.dart
  16. 536
      lib/Views/mobilepage.dart
  17. 1496
      lib/Views/newCommand.dart
  18. 12
      lib/config/DatabaseConfig.dart
  19. 324
      lib/controller/userController.dart
  20. 2
      lib/main.dart
  21. 58
      lib/my_app.dart
  22. 8
      pubspec.lock
  23. 1
      pubspec.yaml
  24. 3
      test/widget_test.dart

883
lib/Components/appDrawer.dart

@ -14,7 +14,7 @@ import 'package:youmazgestion/Views/newCommand.dart';
import 'package:youmazgestion/Views/registrationPage.dart'; import 'package:youmazgestion/Views/registrationPage.dart';
import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/controller/userController.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 { class CustomDrawer extends StatelessWidget {
final UserController userController = Get.find<UserController>(); final UserController userController = Get.find<UserController>();
@ -25,6 +25,7 @@ class CustomDrawer extends StatelessWidget {
await prefs.remove('role'); await prefs.remove('role');
await prefs.remove('user_id'); await prefs.remove('user_id');
// IMPORTANT: Vider le cache de session
userController.clearUserData(); userController.clearUserData();
} }
@ -34,425 +35,573 @@ class CustomDrawer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Drawer( return Drawer(
backgroundColor: Colors.white, backgroundColor: Colors.white,
child: FutureBuilder( child: GetBuilder<UserController>(
future: _buildDrawerItems(), builder: (controller) {
builder: (context, snapshot) { return ListView(
if (snapshot.connectionState == ConnectionState.done) { padding: EdgeInsets.zero,
return ListView( children: [
padding: EdgeInsets.zero, // Header utilisateur
children: snapshot.data as List<Widget>, _buildUserHeader(controller),
);
} else { // CORRIGÉ: Construction avec gestion des valeurs null
return const Center(child: CircularProgressIndicator()); ..._buildDrawerItemsFromSessionCache(),
}
// Déconnexion
const Divider(),
_buildLogoutItem(),
],
);
}, },
), ),
); );
} }
Future<List<Widget>> _buildDrawerItems() async { /// CORRIGÉ: Construction avec validation robuste des données
List<Widget> _buildDrawerItemsFromSessionCache() {
List<Widget> drawerItems = []; List<Widget> drawerItems = [];
drawerItems.add( // Vérifier si le cache est prêt
GetBuilder<UserController>( if (!userController.isCacheReady) {
builder: (controller) => Container( return [
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20), const Padding(
decoration: const BoxDecoration( padding: EdgeInsets.all(16.0),
gradient: LinearGradient( child: Column(
colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [ children: [
const CircleAvatar( SizedBox(
radius: 30, height: 20,
backgroundImage: AssetImage("assets/youmaz2.png"), width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
), ),
const SizedBox(width: 15), SizedBox(height: 8),
Column( Text(
crossAxisAlignment: CrossAxisAlignment.start, "Chargement du menu...",
children: [ style: TextStyle(color: Colors.grey, fontSize: 12),
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,
),
),
],
), ),
], ],
), ),
), ),
), ];
); }
drawerItems.add( // Récupérer les menus depuis le cache de session
await _buildDrawerItem( final rawUserMenus = userController.getUserMenus();
icon: Icons.home,
title: "Accueil",
color: Colors.blue,
permissionAction: 'view',
permissionRoute: '/accueil',
onTap: () => Get.to(DashboardPage()),
),
);
List<Widget> gestionUtilisateursItems = [ // 🛡 VALIDATION: Filtrer les menus valides
await _buildDrawerItem( final validMenus = <Map<String, dynamic>>[];
icon: Icons.person_add, final invalidMenus = <Map<String, dynamic>>[];
title: "Ajouter un utilisateur",
color: Colors.green, for (var menu in rawUserMenus) {
permissionAction: 'create', // Vérifier que les champs essentiels ne sont pas null
permissionRoute: '/ajouter-utilisateur', final name = menu['name'];
onTap: () => Get.to(const RegistrationPage()), final route = menu['route'];
), final id = menu['id'];
await _buildDrawerItem(
icon: Icons.supervised_user_circle, if (name != null && route != null && route.toString().isNotEmpty) {
title: "Gérer les utilisateurs", validMenus.add({
color: const Color.fromARGB(255, 4, 54, 95), 'id': id,
permissionAction: 'update', 'name': name.toString(),
permissionRoute: '/modifier-utilisateur', 'route': route.toString(),
onTap: () => Get.to(const ListUserPage()), });
), } else {
await _buildDrawerItem( invalidMenus.add(menu);
icon: Icons.timer, print("⚠️ Menu invalide ignoré dans CustomDrawer: id=$id, name='$name', route='$route'");
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(
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
"GESTION UTILISATEURS",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
);
drawerItems.addAll(gestionUtilisateursItems);
} }
List<Widget> gestionProduitsItems = [ // Afficher les statistiques de validation
await _buildDrawerItem( if (invalidMenus.isNotEmpty) {
icon: Icons.inventory, print("📊 CustomDrawer: ${validMenus.length} menus valides, ${invalidMenus.length} invalides");
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)) { if (validMenus.isEmpty) {
drawerItems.add( return [
const Padding( const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), padding: EdgeInsets.all(16.0),
child: Text( child: Text(
"GESTION PRODUITS", "Aucun menu accessible",
style: TextStyle( textAlign: TextAlign.center,
color: Colors.grey, style: TextStyle(color: Colors.grey),
fontSize: 12,
fontWeight: FontWeight.bold,
),
), ),
), ),
); ];
drawerItems.addAll(gestionProduitsItems);
} }
List<Widget> gestionCommandesItems = [ // 🔧 DÉDUPLICATION: Éliminer les doublons par route
await _buildDrawerItem( final Map<String, Map<String, dynamic>> uniqueMenus = {};
icon: Icons.add_shopping_cart, for (var menu in validMenus) {
title: "Nouvelle commande", final route = menu['route'] as String;
color: Colors.orange, uniqueMenus[route] = menu;
permissionAction: 'create', }
permissionRoute: '/nouvelle-commande', final deduplicatedMenus = uniqueMenus.values.toList();
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()),
),
];
if (gestionCommandesItems.any((item) => item is ListTile)) { if (deduplicatedMenus.length != validMenus.length) {
drawerItems.add( print("🔧 CustomDrawer: ${validMenus.length - deduplicatedMenus.length} doublons supprimés");
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);
} }
List<Widget> rapportsItems = [ // Organiser les menus par catégories
await _buildDrawerItem( final Map<String, List<Map<String, dynamic>>> categorizedMenus = {
icon: Icons.bar_chart, 'GESTION UTILISATEURS': [],
title: "Bilan ", 'GESTION PRODUITS': [],
color: Colors.teal, 'GESTION COMMANDES': [],
permissionAction: 'read', 'RAPPORTS': [],
permissionRoute: '/bilan', 'ADMINISTRATION': [],
onTap: () => Get.to(DashboardPage()), };
),
await _buildDrawerItem(
icon: Icons.history,
title: "Historique",
color: Colors.blue,
permissionAction: 'read',
permissionRoute: '/historique',
onTap: () => Get.to(const HistoriquePage()),
),
];
if (rapportsItems.any((item) => item is ListTile)) { // Accueil toujours en premier
drawerItems.add( final accueilMenu = deduplicatedMenus.where((menu) => menu['route'] == '/accueil').firstOrNull;
const Padding( if (accueilMenu != null) {
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), drawerItems.add(_buildDrawerItemFromMenu(accueilMenu));
child: Text( }
"RAPPORTS",
style: TextStyle( // Catégoriser les autres menus avec validation supplémentaire
color: Colors.grey, for (var menu in deduplicatedMenus) {
fontSize: 12, final route = menu['route'] as String;
fontWeight: FontWeight.bold,
), // 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;
}
}
// 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<String, dynamic> 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<String, Map<String, dynamic>> 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<Widget> administrationItems = [ return ListTile(
await _buildDrawerItem( leading: Icon(
icon: Icons.admin_panel_settings, routeData['icon'] as IconData,
title: "Gérer les rôles", color: routeData['color'] as Color,
color: Colors.redAccent,
permissionAction: 'admin',
permissionRoute: '/gerer-roles',
onTap: () => Get.to(const RoleListPage()),
), ),
await _buildDrawerItem( title: Text(name),
icon: Icons.store, trailing: const Icon(Icons.chevron_right, color: Colors.grey),
title: "Points de vente", onTap: () {
color: Colors.blueGrey, final widget = routeData['widget'];
permissionAction: 'admin', if (widget != null) {
permissionRoute: '/points-de-vente', Get.to(widget);
onTap: () => Get.to(const AjoutPointDeVentePage()), } 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)) { /// Header utilisateur amélioré
drawerItems.add( Widget _buildUserHeader(UserController controller) {
const Padding( return Container(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5), padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
child: Text( decoration: const BoxDecoration(
"ADMINISTRATION", gradient: LinearGradient(
style: TextStyle( colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
color: Colors.grey, begin: Alignment.topLeft,
fontSize: 12, end: Alignment.bottomRight,
fontWeight: FontWeight.bold, ),
),
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,
),
),
],
),
],
), ),
), ),
), // Bouton de rafraîchissement pour les admins
); if (controller.role == 'Super Admin' || controller.role == 'Admin') ...[
drawerItems.addAll(administrationItems); 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";
}
drawerItems.add(const Divider()); 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( /// Item de déconnexion
ListTile( Widget _buildLogoutItem() {
leading: const Icon(Icons.logout, color: Colors.red), return ListTile(
title: const Text("Déconnexion"), leading: const Icon(Icons.logout, color: Colors.red),
onTap: () { title: const Text("Déconnexion"),
Get.dialog( onTap: () {
AlertDialog( Get.dialog(
shape: RoundedRectangleBorder( AlertDialog(
borderRadius: BorderRadius.circular(16), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(16),
contentPadding: EdgeInsets.zero, ),
content: Container( contentPadding: EdgeInsets.zero,
constraints: const BoxConstraints(maxWidth: 400), content: Container(
child: Column( constraints: const BoxConstraints(maxWidth: 400),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
// Header children: [
Container( // Header
width: double.infinity, Container(
padding: const EdgeInsets.all(24), width: double.infinity,
child: Column( padding: const EdgeInsets.all(24),
children: [ child: Column(
Icon( children: [
Icons.logout_rounded, Icon(
size: 48, Icons.logout_rounded,
color: Colors.orange.shade600, size: 48,
), color: Colors.orange.shade600,
const SizedBox(height: 16), ),
const Text( const SizedBox(height: 16),
"Déconnexion", const Text(
style: TextStyle( "Déconnexion",
fontSize: 20, style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 20,
color: Colors.black87, fontWeight: FontWeight.w600,
), color: Colors.black87,
), ),
const SizedBox(height: 12), ),
const Text( const SizedBox(height: 12),
"Êtes-vous sûr de vouloir vous déconnecter ?", const Text(
textAlign: TextAlign.center, "Êtes-vous sûr de vouloir vous déconnecter ?",
style: TextStyle( textAlign: TextAlign.center,
fontSize: 16, style: TextStyle(
color: Colors.black87, fontSize: 16,
height: 1.4, color: Colors.black87,
), height: 1.4,
), ),
const SizedBox(height: 8), ),
Text( const SizedBox(height: 8),
"Vous devrez vous reconnecter pour accéder à votre compte.", Text(
textAlign: TextAlign.center, "Vos permissions seront rechargées à la prochaine connexion.",
style: TextStyle( textAlign: TextAlign.center,
fontSize: 14, style: TextStyle(
color: Colors.grey.shade600, fontSize: 14,
height: 1.3, color: Colors.grey.shade600,
), height: 1.3,
), ),
], ),
), ],
), ),
// Actions ),
Container( // Actions
width: double.infinity, Container(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), width: double.infinity,
child: Row( padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
children: [ child: Row(
Expanded( children: [
child: OutlinedButton( Expanded(
onPressed: () => Get.back(), child: OutlinedButton(
style: OutlinedButton.styleFrom( onPressed: () => Get.back(),
padding: const EdgeInsets.symmetric(vertical: 12), style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder( padding: const EdgeInsets.symmetric(vertical: 12),
borderRadius: BorderRadius.circular(8), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: Colors.grey.shade300,
width: 1.5,
),
), ),
child: const Text( side: BorderSide(
"Annuler", color: Colors.grey.shade300,
style: TextStyle( width: 1.5,
fontSize: 16, ),
fontWeight: FontWeight.w500, ),
color: Colors.black87, child: const Text(
), "Annuler",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
), ),
), ),
), ),
const SizedBox(width: 12), ),
Expanded( const SizedBox(width: 12),
child: ElevatedButton( Expanded(
onPressed: () async { child: ElevatedButton(
await clearUserData(); onPressed: () async {
Get.offAll(const LoginPage()); // IMPORTANT: Vider le cache de session lors de la déconnexion
}, await clearUserData();
style: ElevatedButton.styleFrom( Get.offAll(const LoginPage());
backgroundColor: Colors.red.shade600, },
foregroundColor: Colors.white, style: ElevatedButton.styleFrom(
elevation: 2, backgroundColor: Colors.red.shade600,
padding: const EdgeInsets.symmetric(vertical: 12), foregroundColor: Colors.white,
shape: RoundedRectangleBorder( elevation: 2,
borderRadius: BorderRadius.circular(8), padding: const EdgeInsets.symmetric(vertical: 12),
), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
), ),
child: const Text( ),
"Se déconnecter", child: const Text(
style: TextStyle( "Se déconnecter",
fontSize: 16, style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 16,
), fontWeight: FontWeight.w600,
), ),
), ),
), ),
], ),
), ],
), ),
], ),
), ],
), ),
), ),
barrierDismissible: true, ),
); barrierDismissible: true,
}, );
), },
);
return drawerItems;
}
Future<Widget> _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,
); );
} }
} }

175
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<List<DetailCommande>>(
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,
),
),
],
),
],
),
),
],
);
},
);
}
}

226
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<Widget> _buildActionButtons(BuildContext context) {
List<Widget> 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),
),
],
),
);
}
}

189
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<DiscountDialog> {
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<bool>(
title: const Text('Pourcentage'),
value: true,
groupValue: _isPercentage,
onChanged: (value) {
setState(() {
_isPercentage = value!;
_calculateDiscount();
});
},
),
),
Expanded(
child: RadioListTile<bool>(
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();
}
}

136
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<GiftSelectionDialog> {
List<Product> _products = [];
List<Product> _filteredProducts = [];
final _searchController = TextEditingController();
Product? _selectedProduct;
@override
void initState() {
super.initState();
_loadProducts();
_searchController.addListener(_filterProducts);
}
Future<void> _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<Product>(
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();
}
}

8
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});
}

288
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<PaymentMethodDialog> {
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),
],
),
),
),
);
}
}

7
lib/Components/commandManagementComponents/PaymentType.dart

@ -0,0 +1,7 @@
enum PaymentType {
cash,
card,
mvola,
orange,
airtel
}

2125
lib/Components/teat.dart

File diff suppressed because it is too large

48
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 { class DetailCommande {
final int? id; final int? id;
final int commandeId; final int commandeId;
@ -226,6 +228,11 @@ class DetailCommande {
final String? produitReference; final String? produitReference;
final bool? estCadeau; final bool? estCadeau;
// NOUVEAUX CHAMPS POUR LA REMISE PAR PRODUIT
final double? remisePourcentage;
final double? remiseMontant;
final double? prixApresRemise;
DetailCommande({ DetailCommande({
this.id, this.id,
required this.commandeId, required this.commandeId,
@ -237,6 +244,9 @@ class DetailCommande {
this.produitImage, this.produitImage,
this.produitReference, this.produitReference,
this.estCadeau, this.estCadeau,
this.remisePourcentage,
this.remiseMontant,
this.prixApresRemise,
}); });
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
@ -248,6 +258,9 @@ class DetailCommande {
'prixUnitaire': prixUnitaire, 'prixUnitaire': prixUnitaire,
'sousTotal': sousTotal, 'sousTotal': sousTotal,
'estCadeau': estCadeau == true ? 1 : 0, 'estCadeau': estCadeau == true ? 1 : 0,
'remisePourcentage': remisePourcentage,
'remiseMontant': remiseMontant,
'prixApresRemise': prixApresRemise,
}; };
} }
@ -263,6 +276,15 @@ class DetailCommande {
produitImage: map['produitImage'] as String?, produitImage: map['produitImage'] as String?,
produitReference: map['produitReference'] as String?, produitReference: map['produitReference'] as String?,
estCadeau: map['estCadeau'] == 1, 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? produitImage,
String? produitReference, String? produitReference,
bool? estCadeau, bool? estCadeau,
double? remisePourcentage,
double? remiseMontant,
double? prixApresRemise,
}) { }) {
return DetailCommande( return DetailCommande(
id: id ?? this.id, id: id ?? this.id,
@ -289,6 +314,29 @@ class DetailCommande {
produitImage: produitImage ?? this.produitImage, produitImage: produitImage ?? this.produitImage,
produitReference: produitReference ?? this.produitReference, produitReference: produitReference ?? this.produitReference,
estCadeau: estCadeau ?? this.estCadeau, 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;
}
} }

258
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<String, Map<String, bool>> _permissionCache = {};
final Map<String, List<Map<String, dynamic>>> _menuCache = {};
bool _isLoaded = false;
String _currentUsername = '';
/// OPTIMISÉ: Une seule requête complexe pour charger tout
Future<void> 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<String, bool> permissions = {};
Set<Map<String, dynamic>> 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<List<Map<String, dynamic>>> _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<Map<String, dynamic>> 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<void> preloadUserDataAsync(String username) async {
// Lancer en arrière-plan sans bloquer l'UI
unawaited(_preloadInBackground(username));
}
Future<void> _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<void> 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<void> 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<String, dynamic> 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<bool> 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<void> 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");
});
}

471
lib/Services/stock_managementDatabase.dart

@ -35,7 +35,7 @@ class AppDatabase {
Future<void> initDatabase() async { Future<void> initDatabase() async {
_connection = await _initDB(); _connection = await _initDB();
await _createDB(); // await _createDB();
// Effectuer la migration pour les bases existantes // Effectuer la migration pour les bases existantes
await migrateDatabaseForDiscountAndGift(); await migrateDatabaseForDiscountAndGift();
@ -70,242 +70,203 @@ class AppDatabase {
// Méthode mise à jour pour créer les tables avec les nouvelles colonnes // Méthode mise à jour pour créer les tables avec les nouvelles colonnes
Future<void> _createDB() async { Future<void> _createDB() async {
final db = await database; // final db = await database;
try { // try {
// Table roles // // Table roles
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS roles ( // CREATE TABLE IF NOT EXISTS roles (
id INT AUTO_INCREMENT PRIMARY KEY, // id INT AUTO_INCREMENT PRIMARY KEY,
designation VARCHAR(255) NOT NULL UNIQUE // designation VARCHAR(255) NOT NULL UNIQUE
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table permissions // // Table permissions
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS permissions ( // CREATE TABLE IF NOT EXISTS permissions (
id INT AUTO_INCREMENT PRIMARY KEY, // id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE // name VARCHAR(255) NOT NULL UNIQUE
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table menu // // Table menu
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS menu ( // CREATE TABLE IF NOT EXISTS menu (
id INT AUTO_INCREMENT PRIMARY KEY, // id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, // name VARCHAR(255) NOT NULL,
route VARCHAR(255) NOT NULL // route VARCHAR(255) NOT NULL
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table role_permissions // // Table role_permissions
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS role_permissions ( // CREATE TABLE IF NOT EXISTS role_permissions (
role_id INT, // role_id INT,
permission_id INT, // permission_id INT,
PRIMARY KEY (role_id, permission_id), // PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, // FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE // FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table role_menu_permissions // // Table role_menu_permissions
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS role_menu_permissions ( // CREATE TABLE IF NOT EXISTS role_menu_permissions (
role_id INT, // role_id INT,
menu_id INT, // menu_id INT,
permission_id INT, // permission_id INT,
PRIMARY KEY (role_id, menu_id, permission_id), // PRIMARY KEY (role_id, menu_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, // FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE, // FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE // FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table points_de_vente // // Table points_de_vente
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS points_de_vente ( // CREATE TABLE IF NOT EXISTS points_de_vente (
id INT AUTO_INCREMENT PRIMARY KEY, // id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(255) NOT NULL UNIQUE // nom VARCHAR(255) NOT NULL UNIQUE
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table users // // Table users
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS users ( // CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY, // id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, // name VARCHAR(255) NOT NULL,
lastname VARCHAR(255) NOT NULL, // lastname VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE, // email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, // password VARCHAR(255) NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE, // username VARCHAR(255) NOT NULL UNIQUE,
role_id INT NOT NULL, // role_id INT NOT NULL,
point_de_vente_id INT, // point_de_vente_id INT,
FOREIGN KEY (role_id) REFERENCES roles(id), // FOREIGN KEY (role_id) REFERENCES roles(id),
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id) // FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table products // // Table products
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS products ( // CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY, // id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, // name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL, // price DECIMAL(10,2) NOT NULL,
image VARCHAR(2000), // image VARCHAR(2000),
category VARCHAR(255) NOT NULL, // category VARCHAR(255) NOT NULL,
stock INT NOT NULL DEFAULT 0, // stock INT NOT NULL DEFAULT 0,
description VARCHAR(1000), // description VARCHAR(1000),
qrCode VARCHAR(500), // qrCode VARCHAR(500),
reference VARCHAR(255), // reference VARCHAR(255),
point_de_vente_id INT, // point_de_vente_id INT,
marque VARCHAR(255), // marque VARCHAR(255),
ram VARCHAR(100), // ram VARCHAR(100),
memoire_interne VARCHAR(100), // memoire_interne VARCHAR(100),
imei VARCHAR(255) UNIQUE, // imei VARCHAR(255) UNIQUE,
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id), // FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id),
INDEX idx_products_category (category), // INDEX idx_products_category (category),
INDEX idx_products_reference (reference), // INDEX idx_products_reference (reference),
INDEX idx_products_imei (imei) // INDEX idx_products_imei (imei)
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table clients // // Table clients
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS clients ( // CREATE TABLE IF NOT EXISTS clients (
id INT AUTO_INCREMENT PRIMARY KEY, // id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(255) NOT NULL, // nom VARCHAR(255) NOT NULL,
prenom VARCHAR(255) NOT NULL, // prenom VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE, // email VARCHAR(255) NOT NULL UNIQUE,
telephone VARCHAR(255) NOT NULL, // telephone VARCHAR(255) NOT NULL,
adresse VARCHAR(500), // adresse VARCHAR(500),
dateCreation DATETIME NOT NULL, // dateCreation DATETIME NOT NULL,
actif TINYINT(1) NOT NULL DEFAULT 1, // actif TINYINT(1) NOT NULL DEFAULT 1,
INDEX idx_clients_email (email), // INDEX idx_clients_email (email),
INDEX idx_clients_telephone (telephone) // INDEX idx_clients_telephone (telephone)
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table commandes MISE À JOUR avec les champs de remise // // Table commandes MISE À JOUR avec les champs de remise
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS commandes ( // CREATE TABLE IF NOT EXISTS commandes (
id INT AUTO_INCREMENT PRIMARY KEY, // id INT AUTO_INCREMENT PRIMARY KEY,
clientId INT NOT NULL, // clientId INT NOT NULL,
dateCommande DATETIME NOT NULL, // dateCommande DATETIME NOT NULL,
statut INT NOT NULL DEFAULT 0, // statut INT NOT NULL DEFAULT 0,
montantTotal DECIMAL(10,2) NOT NULL, // montantTotal DECIMAL(10,2) NOT NULL,
notes VARCHAR(1000), // notes VARCHAR(1000),
dateLivraison DATETIME, // dateLivraison DATETIME,
commandeurId INT, // commandeurId INT,
validateurId INT, // validateurId INT,
remisePourcentage DECIMAL(5,2) NULL, // remisePourcentage DECIMAL(5,2) NULL,
remiseMontant DECIMAL(10,2) NULL, // remiseMontant DECIMAL(10,2) NULL,
montantApresRemise DECIMAL(10,2) NULL, // montantApresRemise DECIMAL(10,2) NULL,
FOREIGN KEY (commandeurId) REFERENCES users(id), // FOREIGN KEY (commandeurId) REFERENCES users(id),
FOREIGN KEY (validateurId) REFERENCES users(id), // FOREIGN KEY (validateurId) REFERENCES users(id),
FOREIGN KEY (clientId) REFERENCES clients(id), // FOREIGN KEY (clientId) REFERENCES clients(id),
INDEX idx_commandes_client (clientId), // INDEX idx_commandes_client (clientId),
INDEX idx_commandes_date (dateCommande) // INDEX idx_commandes_date (dateCommande)
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
// Table details_commandes MISE À JOUR avec le champ cadeau // // Table details_commandes MISE À JOUR avec le champ cadeau
await db.query(''' // await db.query('''
CREATE TABLE IF NOT EXISTS details_commandes ( // CREATE TABLE IF NOT EXISTS details_commandes (
id INT AUTO_INCREMENT PRIMARY KEY, // id INT AUTO_INCREMENT PRIMARY KEY,
commandeId INT NOT NULL, // commandeId INT NOT NULL,
produitId INT NOT NULL, // produitId INT NOT NULL,
quantite INT NOT NULL, // quantite INT NOT NULL,
prixUnitaire DECIMAL(10,2) NOT NULL, // prixUnitaire DECIMAL(10,2) NOT NULL,
sousTotal DECIMAL(10,2) NOT NULL, // sousTotal DECIMAL(10,2) NOT NULL,
estCadeau TINYINT(1) DEFAULT 0, // estCadeau TINYINT(1) DEFAULT 0,
FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE, // FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE,
FOREIGN KEY (produitId) REFERENCES products(id), // FOREIGN KEY (produitId) REFERENCES products(id),
INDEX idx_details_commande (commandeId) // INDEX idx_details_commande (commandeId)
) ENGINE=InnoDB // ) ENGINE=InnoDB
'''); // ''');
print("Tables créées avec succès avec les nouveaux champs !"); // print("Tables créées avec succès avec les nouveaux champs !");
} catch (e) { // } catch (e) {
print("Erreur lors de la création des tables: $e"); // print("Erreur lors de la création des tables: $e");
rethrow; // rethrow;
} // }
} }
// --- MÉTHODES D'INSERTION PAR DÉFAUT --- // --- MÉTHODES D'INSERTION PAR DÉFAUT ---
//
Future<void> insertDefaultPermissions() async { Future<void> insertDefaultPermissions() async {
final db = await database; 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) { try {
await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]); // Vérifier et ajouter uniquement les nouvelles permissions si elles n'existent pas
} final newPermissions = ['manage', 'read'];
print("Permissions par défaut insérées"); for (var permission in newPermissions) {
} else { final existingPermission = await db.query(
// Vérifier et ajouter les nouvelles permissions si elles n'existent pas 'SELECT COUNT(*) as count FROM permissions WHERE name = ?',
final newPermissions = ['manage', 'read']; [permission]
for (var permission in newPermissions) { );
final existingPermission = await db.query( final permCount = existingPermission.first['count'] as int;
'SELECT COUNT(*) as count FROM permissions WHERE name = ?', if (permCount == 0) {
[permission] await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]);
); print("Permission ajoutée: $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<void> insertDefaultMenus() async { Future<void> insertDefaultMenus() async {
final db = await database; 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) { try {
await db.query( await _addMissingMenus(db); // Seulement ajouter les menus manquants
'INSERT INTO menu (name, route) VALUES (?, ?)', } catch (e) {
[menu['name'], menu['route']] print("Erreur insertDefaultMenus: $e");
);
}
print("Menus par défaut insérés");
} else {
await _addMissingMenus(db);
}
} catch (e) {
print("Erreur insertDefaultMenus: $e");
}
} }
}
Future<void> insertDefaultRoles() async { Future<void> insertDefaultRoles() async {
final db = await database; final db = await database;
@ -741,29 +702,53 @@ Future<void> _createDB() async {
// --- MÉTHODES UTILITAIRES --- // --- MÉTHODES UTILITAIRES ---
// Future<void> _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<void> _addMissingMenus(MySqlConnection db) async { Future<void> _addMissingMenus(MySqlConnection db) async {
final menusToAdd = [ final menusToAdd = [
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
{'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, {'name': 'Gérer les commandes', 'route': '/gerer-commandes'},
{'name': 'Points de vente', 'route': '/points-de-vente'}, {'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) { for (var menu in menusToAdd) {
await db.query( final existing = await db.query(
'INSERT INTO menu (name, route) VALUES (?, ?)', 'SELECT COUNT(*) as count FROM menu WHERE route = ?',
[menu['name'], menu['route']] [menu['route']]
); );
print("Menu ajouté: ${menu['name']}"); 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<void> _updateExistingRolePermissions(MySqlConnection db) async { Future<void> _updateExistingRolePermissions(MySqlConnection db) async {
final superAdminRole = await db.query('SELECT id FROM roles WHERE designation = ?', ['Super Admin']); final superAdminRole = await db.query('SELECT id FROM roles WHERE designation = ?', ['Super Admin']);

4643
lib/Views/HandleProduct.dart

File diff suppressed because it is too large

1048
lib/Views/commandManagement.dart

File diff suppressed because it is too large

400
lib/Views/loginPage.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.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/Dashboard.dart';
import 'package:youmazgestion/Views/mobilepage.dart'; import 'package:youmazgestion/Views/mobilepage.dart';
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground; import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
@ -19,9 +20,12 @@ class _LoginPageState extends State<LoginPage> {
late TextEditingController _usernameController; late TextEditingController _usernameController;
late TextEditingController _passwordController; late TextEditingController _passwordController;
final UserController userController = Get.put(UserController()); final UserController userController = Get.put(UserController());
final PermissionCacheService _cacheService = PermissionCacheService.instance; // Nouveau
bool _isErrorVisible = false; bool _isErrorVisible = false;
bool _isLoading = false; bool _isLoading = false;
String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide'; String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
String _loadingMessage = 'Connexion en cours...'; // Nouveau
@override @override
void initState() { void initState() {
@ -51,105 +55,185 @@ class _LoginPageState extends State<LoginPage> {
super.dispose(); super.dispose();
} }
Future<void> saveUserData(Users user, String role, int userId) async { // /// OPTIMISÉ: Sauvegarde avec préchargement des permissions
try { // Future<void> saveUserData(Users user, String role, int userId) async {
userController.setUserWithCredentials(user, role, userId); // try {
// userController.setUserWithCredentials(user, role, userId);
if (user.pointDeVenteId != null) { // if (user.pointDeVenteId != null) {
await userController.loadPointDeVenteDesignation(); // await userController.loadPointDeVenteDesignation();
} // }
print('Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}'); // print('✅ Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}');
} catch (error) { // } catch (error) {
print('Erreur lors de la sauvegarde: $error'); // print('❌ Erreur lors de la sauvegarde: $error');
throw Exception('Erreur lors de la sauvegarde des données utilisateur'); // throw Exception('Erreur lors de la sauvegarde des données utilisateur');
} // }
// }
/// NOUVEAU: Préchargement des permissions en arrière-plan
Future<void> _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 { /// OPTIMISÉ: Connexion avec préchargement parallèle
if (_isLoading) return; void _login() async {
if (_isLoading) return;
final String username = _usernameController.text.trim(); final String username = _usernameController.text.trim();
final String password = _passwordController.text.trim(); final String password = _passwordController.text.trim();
if (username.isEmpty || password.isEmpty) { if (username.isEmpty || password.isEmpty) {
setState(() { setState(() {
_errorMessage = _errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
'Veuillez saisir le nom d\'utilisateur et le mot de passe'; _isErrorVisible = true;
_isErrorVisible = true; });
}); return;
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(() { setState(() {
_isLoading = true; _loadingMessage = 'Vérification...';
_isErrorVisible = false;
}); });
try { try {
print('Tentative de connexion pour: $username'); final userCount = await dbInstance.getUserCount();
final dbInstance = AppDatabase.instance; print('✅ Base accessible, $userCount utilisateurs');
} catch (dbError) {
try { throw Exception('Base de données inaccessible: $dbError');
final userCount = await dbInstance.getUserCount(); }
print('Base de données accessible, $userCount utilisateurs trouvés');
} catch (dbError) { // 2. Vérification des identifiants
throw Exception('Impossible d\'accéder à la base de données: $dbError'); setState(() {
} _loadingMessage = 'Authentification...';
});
bool isValidUser = await dbInstance.verifyUser(username, password);
if (isValidUser) {
setState(() {
_loadingMessage = 'Chargement du profil...';
});
// 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<String, dynamic>?;
if (userCredentials != null) {
print('✅ Connexion réussie pour: ${user.username}');
print(' Rôle: ${userCredentials['role']}');
bool isValidUser = await dbInstance.verifyUser(username, password); setState(() {
_loadingMessage = 'Préparation...';
if (isValidUser) { });
Users user = await dbInstance.getUser(username);
print('Utilisateur récupéré: ${user.username}'); // 4. Sauvegarde des données utilisateur
await saveUserData(
Map<String, dynamic>? userCredentials = user,
await dbInstance.getUserCredentials(username, password); userCredentials['role'] as String,
userCredentials['id'] as int,
if (userCredentials != null) { );
print('Connexion réussie pour: ${user.username}');
print('Rôle: ${userCredentials['role']}'); // 5. Préchargement des permissions EN PARALLÈLE avec la navigation
print('ID: ${userCredentials['id']}'); setState(() {
_loadingMessage = 'Finalisation...';
await saveUserData( });
user,
userCredentials['role'] as String, // Lancer le préchargement en arrière-plan SANS attendre
userCredentials['id'] as int, _cacheService.preloadUserDataAsync(username);
);
// 6. Navigation immédiate
// MODIFICATION PRINCIPALE ICI if (mounted) {
if (mounted) { if (userCredentials['role'] == 'commercial') {
if (userCredentials['role'] == 'commercial') { Navigator.pushReplacement(
// Redirection vers MainLayout pour les commerciaux context,
Navigator.pushReplacement( MaterialPageRoute(builder: (context) => const MainLayout()),
context, );
MaterialPageRoute(builder: (context) => const MainLayout()), } else {
); Navigator.pushReplacement(
} else { context,
Navigator.pushReplacement( MaterialPageRoute(builder: (context) => DashboardPage()),
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 { } else {
setState(() { throw Exception('Erreur lors de la récupération des credentials');
_errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
_isErrorVisible = true;
});
} }
} catch (error) { } else {
setState(() { setState(() {
_errorMessage = 'Erreur de connexion: ${error.toString()}'; _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
_isErrorVisible = true; _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<void> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -169,8 +253,7 @@ class _LoginPageState extends State<LoginPage> {
width: MediaQuery.of(context).size.width < 500 width: MediaQuery.of(context).size.width < 500
? double.infinity ? double.infinity
: 400, : 400,
padding: padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardColor.withOpacity(0.98), color: cardColor.withOpacity(0.98),
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
@ -186,6 +269,7 @@ class _LoginPageState extends State<LoginPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Header
Center( Center(
child: Column( child: Column(
children: [ children: [
@ -219,6 +303,8 @@ class _LoginPageState extends State<LoginPage> {
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Username Field
TextField( TextField(
controller: _usernameController, controller: _usernameController,
enabled: !_isLoading, enabled: !_isLoading,
@ -241,6 +327,8 @@ class _LoginPageState extends State<LoginPage> {
), ),
), ),
const SizedBox(height: 18.0), const SizedBox(height: 18.0),
// Password Field
TextField( TextField(
controller: _passwordController, controller: _passwordController,
enabled: !_isLoading, enabled: !_isLoading,
@ -263,19 +351,104 @@ class _LoginPageState extends State<LoginPage> {
), ),
onSubmitted: (_) => _login(), 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<Color>(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) ...[ if (_isErrorVisible) ...[
const SizedBox(height: 12.0), const SizedBox(height: 12.0),
Text( Container(
_errorMessage, padding: const EdgeInsets.all(12),
style: const TextStyle( decoration: BoxDecoration(
color: Colors.redAccent, color: Colors.red.withOpacity(0.1),
fontSize: 15, borderRadius: BorderRadius.circular(8),
fontWeight: FontWeight.w600, 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), const SizedBox(height: 26.0),
// Login Button
ElevatedButton( ElevatedButton(
onPressed: _isLoading ? null : _login, onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -289,13 +462,27 @@ class _LoginPageState extends State<LoginPage> {
minimumSize: const Size(double.infinity, 52), minimumSize: const Size(double.infinity, 52),
), ),
child: _isLoading child: _isLoading
? const SizedBox( ? Row(
height: 24, mainAxisAlignment: MainAxisAlignment.center,
width: 24, children: [
child: CircularProgressIndicator( const SizedBox(
color: Colors.white, height: 20,
strokeWidth: 2.5, 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( : const Text(
'Se connecter', 'Se connecter',
@ -307,16 +494,23 @@ class _LoginPageState extends State<LoginPage> {
), ),
), ),
), ),
// Option debug, à enlever en prod
if (_isErrorVisible) ...[ // Debug Button (à enlever en production)
if (_isErrorVisible && !_isLoading) ...[
const SizedBox(height: 8),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
try { try {
final count = final count = await AppDatabase.instance.getUserCount();
await AppDatabase.instance.getUserCount(); final stats = _cacheService.getCacheStats();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( 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) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -324,7 +518,13 @@ class _LoginPageState extends State<LoginPage> {
); );
} }
}, },
child: const Text('Debug: Vérifier BDD'), child: Text(
'Debug: Vérifier BDD & Cache',
style: TextStyle(
color: primaryColor.withOpacity(0.6),
fontSize: 12,
),
),
), ),
], ],
], ],

536
lib/Views/mobilepage.dart

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/QrScan.dart';
import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Components/appDrawer.dart';
@ -508,57 +509,523 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
); );
} }
// 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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600; final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold( return Scaffold(
floatingActionButton: _buildFloatingCartButton(), floatingActionButton: Column(
drawer: isMobile ? CustomDrawer() : null, 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( body: GestureDetector(
onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs onTap: _hideAllSuggestions,
child: Column( child: Column(
children: [ 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) if (!isMobile)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _buildFilterSection(), child: _buildFilterSection(),
), ),
// Sur mobile, bouton pour afficher les filtres dans un modal // Boutons pour mobile
if (isMobile) ...[ if (isMobile) ...[
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: SizedBox( child: Row(
width: double.infinity, children: [
child: ElevatedButton.icon( Expanded(
icon: const Icon(Icons.filter_alt), flex: 2,
label: const Text('Filtres produits'), child: ElevatedButton.icon(
onPressed: () { icon: const Icon(Icons.filter_alt),
showModalBottomSheet( label: const Text('Filtres'),
context: context, onPressed: () {
isScrollControlled: true, showModalBottomSheet(
builder: (context) => SingleChildScrollView( context: context,
padding: EdgeInsets.only( isScrollControlled: true,
bottom: MediaQuery.of(context).viewInsets.bottom, 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(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Container( child: Container(
@ -588,6 +1055,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
); );
} }
Widget _buildSuggestionsList({required bool isNom}) { Widget _buildSuggestionsList({required bool isNom}) {
if (_clientSuggestions.isEmpty) return const SizedBox(); if (_clientSuggestions.isEmpty) return const SizedBox();
@ -1725,23 +2193,21 @@ void _fillFormWithClient(Client client) {
} }
} }
@override @override
void dispose() { void dispose() {
// Nettoyer les suggestions _qrController?.dispose();
_hideAllSuggestions();
// Disposer les contrôleurs // Vos disposals existants...
_hideAllSuggestions();
_nomController.dispose(); _nomController.dispose();
_prenomController.dispose(); _prenomController.dispose();
_emailController.dispose(); _emailController.dispose();
_telephoneController.dispose(); _telephoneController.dispose();
_adresseController.dispose(); _adresseController.dispose();
// Disposal des contrôleurs de filtre
_searchNameController.dispose(); _searchNameController.dispose();
_searchImeiController.dispose(); _searchImeiController.dispose();
_searchReferenceController.dispose(); _searchReferenceController.dispose();
super.dispose(); super.dispose();
} }
} }

1496
lib/Views/newCommand.dart

File diff suppressed because it is too large

12
lib/config/DatabaseConfig.dart

@ -1,12 +1,12 @@
// Config/database_config.dart - Version améliorée // Config/database_config.dart - Version améliorée
class DatabaseConfig { class DatabaseConfig {
static const String host = 'database.c4m.mg'; static const String host = '172.20.10.5';
static const int port = 3306; static const int port = 3306;
static const String username = 'guycom'; static const String username = 'root';
static const String password = '3iV59wjRdbuXAPR'; static const String? password = null;
static const String database = 'guycom'; 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 prodUsername = 'guycom';
static const String prodPassword = '3iV59wjRdbuXAPR'; static const String prodPassword = '3iV59wjRdbuXAPR';
static const String prodDatabase = 'guycom'; static const String prodDatabase = 'guycom';
@ -17,7 +17,7 @@ class DatabaseConfig {
static const int maxConnections = 10; static const int maxConnections = 10;
static const int minConnections = 2; static const int minConnections = 2;
static bool get isDevelopment => false; static bool get isDevelopment => true;
static Map<String, dynamic> getConfig() { static Map<String, dynamic> getConfig() {
if (isDevelopment) { if (isDevelopment) {

324
lib/controller/userController.dart

@ -2,7 +2,7 @@ import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.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 { class UserController extends GetxController {
final _username = ''.obs; final _username = ''.obs;
@ -11,10 +11,14 @@ class UserController extends GetxController {
final _name = ''.obs; final _name = ''.obs;
final _lastname = ''.obs; final _lastname = ''.obs;
final _password = ''.obs; final _password = ''.obs;
final _userId = 0.obs; // Ajout de l'ID utilisateur final _userId = 0.obs;
final _pointDeVenteId = 0.obs; final _pointDeVenteId = 0.obs;
final _pointDeVenteDesignation = ''.obs; final _pointDeVenteDesignation = ''.obs;
// Cache service
final PermissionCacheService _cacheService = PermissionCacheService.instance;
// Getters
String get username => _username.value; String get username => _username.value;
String get email => _email.value; String get email => _email.value;
String get role => _role.value; String get role => _role.value;
@ -28,92 +32,111 @@ class UserController extends GetxController {
@override @override
void onInit() { void onInit() {
super.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 /// SIMPLIFIÉ: Charge les données utilisateur sans cache persistant
Future<void> loadUserData() async { Future<void> loadUserData() async {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final storedUsername = prefs.getString('username') ?? ''; final storedUsername = prefs.getString('username') ?? '';
final storedRole = prefs.getString('role') ?? ''; final storedRole = prefs.getString('role') ?? '';
final storedUserId = prefs.getInt('user_id') ?? 0; final storedUserId = prefs.getInt('user_id') ?? 0;
final storedPointDeVenteId = prefs.getInt('point_de_vente_id') ?? 0; final storedPointDeVenteId = prefs.getInt('point_de_vente_id') ?? 0;
final storedPointDeVenteDesignation = prefs.getString('point_de_vente_designation') ?? ''; final storedPointDeVenteDesignation = prefs.getString('point_de_vente_designation') ?? '';
if (storedUsername.isNotEmpty) { if (storedUsername.isNotEmpty) {
try { try {
Users user = await AppDatabase.instance.getUser(storedUsername); Users user = await AppDatabase.instance.getUser(storedUsername);
_username.value = user.username; _username.value = user.username;
_email.value = user.email; _email.value = user.email;
_name.value = user.name; _name.value = user.name;
_lastname.value = user.lastName; _lastname.value = user.lastName;
_password.value = user.password; _password.value = user.password;
_role.value = storedRole; _role.value = storedRole;
_userId.value = storedUserId; _userId.value = storedUserId;
_pointDeVenteId.value = storedPointDeVenteId; _pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation; _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) {
if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) { await loadPointDeVenteDesignation();
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;
} catch (dbError) { // Précharger quand même
// Fallback _preloadPermissionsInBackground();
_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<void> loadPointDeVenteDesignation() async { /// Précharge les permissions en arrière-plan (non bloquant)
if (_pointDeVenteId.value <= 0) return; void _preloadPermissionsInBackground() {
if (_username.value.isNotEmpty) {
try { // Lancer en arrière-plan sans attendre
final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value); Future.microtask(() async {
if (pointDeVente != null) { try {
_pointDeVenteDesignation.value = pointDeVente['designation'] as String; await _cacheService.preloadUserData(_username.value);
await saveUserData(); // Sauvegarder la désignation } 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<void> 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) { void setUserWithCredentials(Users user, String role, int userId) {
_username.value = user.username; _username.value = user.username;
_email.value = user.email; _email.value = user.email;
_role.value = role; // Rôle depuis les credentials _role.value = role;
_name.value = user.name; _name.value = user.name;
_lastname.value = user.lastName; _lastname.value = user.lastName;
_password.value = user.password; _password.value = user.password;
_userId.value = userId; // ID depuis les credentials _userId.value = userId;
_pointDeVenteId.value = user.pointDeVenteId ?? 0; _pointDeVenteId.value = user.pointDeVenteId ?? 0;
print("✅ Utilisateur mis à jour avec credentials:"); print("✅ Utilisateur mis à jour avec credentials:");
print(" Username: ${_username.value}"); print(" Username: ${_username.value}");
print(" Name: ${_name.value}");
print(" Email: ${_email.value}");
print(" Role: ${_role.value}"); print(" Role: ${_role.value}");
print(" UserID: ${_userId.value}"); print(" UserID: ${_userId.value}");
// Sauvegarder dans SharedPreferences
saveUserData(); saveUserData();
// Précharger immédiatement les permissions après connexion
_preloadPermissionsInBackground();
} }
// MÉTHODE EXISTANTE AMÉLIORÉE
void setUser(Users user) { void setUser(Users user) {
_username.value = user.username; _username.value = user.username;
_email.value = user.email; _email.value = user.email;
@ -121,90 +144,107 @@ Future<void> loadPointDeVenteDesignation() async {
_name.value = user.name; _name.value = user.name;
_lastname.value = user.lastName; _lastname.value = user.lastName;
_password.value = user.password; _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(); saveUserData();
_preloadPermissionsInBackground();
} }
// CORRECTION : Sauvegarder TOUTES les données importantes
Future<void> saveUserData() async { Future<void> saveUserData() async {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', _username.value); await prefs.setString('username', _username.value);
await prefs.setString('email', _email.value); await prefs.setString('email', _email.value);
await prefs.setString('role', _role.value); await prefs.setString('role', _role.value);
await prefs.setString('name', _name.value); await prefs.setString('name', _name.value);
await prefs.setString('lastname', _lastname.value); await prefs.setString('lastname', _lastname.value);
await prefs.setInt('user_id', _userId.value); await prefs.setInt('user_id', _userId.value);
await prefs.setInt('point_de_vente_id', _pointDeVenteId.value); await prefs.setInt('point_de_vente_id', _pointDeVenteId.value);
await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value); await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value);
print("✅ Données sauvegardées avec succès dans SharedPreferences"); print("✅ Données sauvegardées avec succès");
} catch (e) { } catch (e) {
print('❌ Erreur lors de la sauvegarde des données utilisateur: $e'); print('❌ Erreur lors de la sauvegarde: $e');
}
} }
}
// CORRECTION : Vider TOUTES les données (SharedPreferences + Observables) /// MODIFIÉ: Vider les données ET le cache de session
Future<void> clearUserData() async { Future<void> clearUserData() async {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove('username'); // IMPORTANT: Vider le cache de session
await prefs.remove('email'); _cacheService.clearAllCache();
await prefs.remove('role');
await prefs.remove('name'); // Effacer SharedPreferences
await prefs.remove('lastname'); await prefs.remove('username');
await prefs.remove('user_id'); await prefs.remove('email');
await prefs.remove('point_de_vente_id'); await prefs.remove('role');
await prefs.remove('point_de_vente_designation'); await prefs.remove('name');
await prefs.remove('lastname');
_username.value = ''; await prefs.remove('user_id');
_email.value = ''; await prefs.remove('point_de_vente_id');
_role.value = ''; await prefs.remove('point_de_vente_designation');
_name.value = '';
_lastname.value = ''; // Effacer les observables
_password.value = ''; _username.value = '';
_userId.value = 0; _email.value = '';
_pointDeVenteId.value = 0; _role.value = '';
_pointDeVenteDesignation.value = ''; _name.value = '';
_lastname.value = '';
} catch (e) { _password.value = '';
print('❌ Erreur lors de l\'effacement des données utilisateur: $e'); _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; bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0;
// MÉTHODE UTILITAIRE : Obtenir le nom complet
String get fullName => '${_name.value} ${_lastname.value}'.trim(); String get fullName => '${_name.value} ${_lastname.value}'.trim();
/// OPTIMISÉ: Vérification des permissions depuis le cache de session
Future<bool> hasPermission(String permission, String route) async { Future<bool> hasPermission(String permission, String route) async {
try { try {
if (_username.value.isEmpty) { if (_username.value.isEmpty) {
print('⚠️ Username vide, rechargement des données...'); print('⚠️ Username vide, rechargement...');
await loadUserData(); await loadUserData();
} }
if (_username.value.isEmpty) { if (_username.value.isEmpty) {
print('Impossible de vérifier les permissions : utilisateur non connecté'); print('Utilisateur non connecté');
return false; 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) { } catch (e) {
print('❌ Erreur vérification permission: $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<bool> hasAnyPermission(List<String> permissionNames, String menuRoute) async { Future<bool> hasAnyPermission(List<String> permissionNames, String menuRoute) async {
for (String permissionName in permissionNames) { for (String permissionName in permissionNames) {
if (await hasPermission(permissionName, menuRoute)) { if (await hasPermission(permissionName, menuRoute)) {
@ -214,18 +254,40 @@ Future<void> clearUserData() async {
return false; return false;
} }
// MÉTHODE DEBUG : Afficher l'état actuel /// Obtenir les menus accessibles depuis le cache
List<Map<String, dynamic>> 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<void> 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() { void debugPrintUserState() {
print("=== ÉTAT UTILISATEUR ==="); print("=== ÉTAT UTILISATEUR ===");
print("Username: ${_username.value}"); print("Username: ${_username.value}");
print("Name: ${_name.value}"); print("Name: ${_name.value}");
print("Lastname: ${_lastname.value}"); print("Role: ${_role.value}");
print("Email: ${_email.value}"); print("UserID: ${_userId.value}");
print("Role: ${_role.value}"); print("IsLoggedIn: $isLoggedIn");
print("UserID: ${_userId.value}"); print("Cache Ready: $isCacheReady");
print("PointDeVenteID: ${_pointDeVenteId.value}"); print("========================");
print("PointDeVente: ${_pointDeVenteDesignation.value}");
print("IsLoggedIn: $isLoggedIn"); // Debug du cache
print("========================"); _cacheService.debugPrintCache();
} }
} }

2
lib/main.dart

@ -17,7 +17,7 @@ void main() async {
// Initialiser la base de données MySQL // Initialiser la base de données MySQL
print("Connexion à 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 !"); print("Base de données initialisée avec succès !");
// Afficher les informations de la base (pour debug) // Afficher les informations de la base (pour debug)

58
lib/my_app.dart

@ -1,8 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'Views/ErreurPage.dart';
import 'Views/loginPage.dart'; import 'Views/loginPage.dart';
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@ -10,12 +6,11 @@ class MyApp extends StatelessWidget {
static bool isRegisterOpen = false; static bool isRegisterOpen = false;
static DateTime? startDate; static DateTime? startDate;
static late String path;
static const Gradient primaryGradient = LinearGradient( static const Gradient primaryGradient = LinearGradient(
colors: [ colors: [
Colors.white, Colors.white,
const Color.fromARGB(255, 4, 54, 95), Color.fromARGB(255, 4, 54, 95),
], ],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@ -24,56 +19,17 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'GUYCOM',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
canvasColor: Colors.transparent, canvasColor: Colors.transparent,
), ),
home: Builder( home: Container(
builder: (context) { decoration: const BoxDecoration(
return FutureBuilder<bool>( gradient: MyApp.primaryGradient,
future: ),
checkLocalDatabasesExist(), // Appel à la fonction de vérification child: const LoginPage(),
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(),
);
}
},
);
},
), ),
); );
} }
Future<bool> 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;
}
} }

8
pubspec.lock

@ -872,6 +872,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" 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: qr_flutter:
dependency: "direct main" dependency: "direct main"
description: description:

1
pubspec.yaml

@ -66,6 +66,7 @@ dependencies:
mobile_scanner: ^5.0.0 # ou la version la plus récente 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 fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
numbers_to_letters: ^1.0.0 numbers_to_letters: ^1.0.0
qr_code_scanner_plus: ^2.0.10+1

3
test/widget_test.dart

@ -7,8 +7,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:youmazgestion/my_app.dart';
import 'package:guycom/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {

Loading…
Cancel
Save