Browse Source

scan code bar

13062025
b.razafimandimbihery 6 months ago
parent
commit
525b09c81f
  1. 617
      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. 399
      lib/Services/stock_managementDatabase.dart
  13. 4625
      lib/Views/HandleProduct.dart
  14. 1048
      lib/Views/commandManagement.dart
  15. 286
      lib/Views/loginPage.dart
  16. 496
      lib/Views/mobilepage.dart
  17. 858
      lib/Views/newCommand.dart
  18. 12
      lib/config/DatabaseConfig.dart
  19. 152
      lib/controller/userController.dart
  20. 2
      lib/main.dart
  21. 50
      lib/my_app.dart
  22. 8
      pubspec.lock
  23. 1
      pubspec.yaml
  24. 3
      test/widget_test.dart

617
lib/Components/appDrawer.dart

@ -14,7 +14,7 @@ import 'package:youmazgestion/Views/newCommand.dart';
import 'package:youmazgestion/Views/registrationPage.dart';
import 'package:youmazgestion/accueil.dart';
import 'package:youmazgestion/controller/userController.dart';
import 'package:youmazgestion/Views/gestion_point_de_vente.dart'; // Nouvel import
import 'package:youmazgestion/Views/gestion_point_de_vente.dart';
class CustomDrawer extends StatelessWidget {
final UserController userController = Get.find<UserController>();
@ -25,6 +25,7 @@ class CustomDrawer extends StatelessWidget {
await prefs.remove('role');
await prefs.remove('user_id');
// IMPORTANT: Vider le cache de session
userController.clearUserData();
}
@ -34,28 +35,320 @@ class CustomDrawer extends StatelessWidget {
Widget build(BuildContext context) {
return Drawer(
backgroundColor: Colors.white,
child: FutureBuilder(
future: _buildDrawerItems(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
child: GetBuilder<UserController>(
builder: (controller) {
return ListView(
padding: EdgeInsets.zero,
children: snapshot.data as List<Widget>,
children: [
// Header utilisateur
_buildUserHeader(controller),
// CORRIGÉ: Construction avec gestion des valeurs null
..._buildDrawerItemsFromSessionCache(),
// Déconnexion
const Divider(),
_buildLogoutItem(),
],
);
},
),
);
}
/// CORRIGÉ: Construction avec validation robuste des données
List<Widget> _buildDrawerItemsFromSessionCache() {
List<Widget> drawerItems = [];
// Vérifier si le cache est prêt
if (!userController.isCacheReady) {
return [
const Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(height: 8),
Text(
"Chargement du menu...",
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
),
];
}
// Récupérer les menus depuis le cache de session
final rawUserMenus = userController.getUserMenus();
// 🛡 VALIDATION: Filtrer les menus valides
final validMenus = <Map<String, dynamic>>[];
final invalidMenus = <Map<String, dynamic>>[];
for (var menu in rawUserMenus) {
// Vérifier que les champs essentiels ne sont pas null
final name = menu['name'];
final route = menu['route'];
final id = menu['id'];
if (name != null && route != null && route.toString().isNotEmpty) {
validMenus.add({
'id': id,
'name': name.toString(),
'route': route.toString(),
});
} else {
return const Center(child: CircularProgressIndicator());
invalidMenus.add(menu);
print("⚠️ Menu invalide ignoré dans CustomDrawer: id=$id, name='$name', route='$route'");
}
}
// Afficher les statistiques de validation
if (invalidMenus.isNotEmpty) {
print("📊 CustomDrawer: ${validMenus.length} menus valides, ${invalidMenus.length} invalides");
}
if (validMenus.isEmpty) {
return [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
"Aucun menu accessible",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
),
];
}
// 🔧 DÉDUPLICATION: Éliminer les doublons par route
final Map<String, Map<String, dynamic>> uniqueMenus = {};
for (var menu in validMenus) {
final route = menu['route'] as String;
uniqueMenus[route] = menu;
}
final deduplicatedMenus = uniqueMenus.values.toList();
if (deduplicatedMenus.length != validMenus.length) {
print("🔧 CustomDrawer: ${validMenus.length - deduplicatedMenus.length} doublons supprimés");
}
// Organiser les menus par catégories
final Map<String, List<Map<String, dynamic>>> categorizedMenus = {
'GESTION UTILISATEURS': [],
'GESTION PRODUITS': [],
'GESTION COMMANDES': [],
'RAPPORTS': [],
'ADMINISTRATION': [],
};
// Accueil toujours en premier
final accueilMenu = deduplicatedMenus.where((menu) => menu['route'] == '/accueil').firstOrNull;
if (accueilMenu != null) {
drawerItems.add(_buildDrawerItemFromMenu(accueilMenu));
}
// Catégoriser les autres menus avec validation supplémentaire
for (var menu in deduplicatedMenus) {
final route = menu['route'] as String;
// Validation supplémentaire avant categorisation
if (route.isEmpty) {
print("⚠️ Route vide ignorée: ${menu['name']}");
continue;
}
switch (route) {
case '/accueil':
// Déjà traité
break;
case '/ajouter-utilisateur':
case '/modifier-utilisateur':
case '/pointage':
categorizedMenus['GESTION UTILISATEURS']!.add(menu);
break;
case '/ajouter-produit':
case '/gestion-stock':
categorizedMenus['GESTION PRODUITS']!.add(menu);
break;
case '/nouvelle-commande':
case '/gerer-commandes':
categorizedMenus['GESTION COMMANDES']!.add(menu);
break;
case '/bilan':
case '/historique':
categorizedMenus['RAPPORTS']!.add(menu);
break;
case '/gerer-roles':
case '/points-de-vente':
categorizedMenus['ADMINISTRATION']!.add(menu);
break;
default:
// Menu non catégorisé
print("⚠️ Menu non catégorisé: $route");
break;
}
}
// 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,
);
},
);
}
return ListTile(
leading: Icon(
routeData['icon'] as IconData,
color: routeData['color'] as Color,
),
title: Text(name),
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
onTap: () {
final widget = routeData['widget'];
if (widget != null) {
Get.to(widget);
} else {
Get.snackbar(
"Non implémenté",
"Cette fonctionnalité sera bientôt disponible",
snackPosition: SnackPosition.BOTTOM,
);
}
},
);
}
Future<List<Widget>> _buildDrawerItems() async {
List<Widget> drawerItems = [];
/// 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,
),
),
);
}
drawerItems.add(
GetBuilder<UserController>(
builder: (controller) => Container(
/// Header utilisateur amélioré
Widget _buildUserHeader(UserController controller) {
return Container(
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
decoration: const BoxDecoration(
gradient: LinearGradient(
@ -71,12 +364,13 @@ class CustomDrawer extends StatelessWidget {
backgroundImage: AssetImage("assets/youmaz2.png"),
),
const SizedBox(width: 15),
Column(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.name.isNotEmpty
? controller.name
? controller.fullName
: 'Utilisateur',
style: const TextStyle(
color: Colors.white,
@ -91,217 +385,101 @@ class CustomDrawer extends StatelessWidget {
fontSize: 12,
),
),
],
if (controller.pointDeVenteDesignation.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
controller.pointDeVenteDesignation,
style: const TextStyle(
color: Colors.white60,
fontSize: 10,
),
),
],
// Indicateur de statut du cache
const SizedBox(height: 4),
Row(
children: [
Icon(
controller.isCacheReady ? Icons.check_circle : Icons.hourglass_empty,
color: controller.isCacheReady ? Colors.green : Colors.orange,
size: 12,
),
const SizedBox(width: 4),
Text(
controller.isCacheReady ? 'Menu prêt' : 'Chargement...',
style: const TextStyle(
color: Colors.white60,
fontSize: 10,
),
),
);
drawerItems.add(
await _buildDrawerItem(
icon: Icons.home,
title: "Accueil",
color: Colors.blue,
permissionAction: 'view',
permissionRoute: '/accueil',
onTap: () => Get.to(DashboardPage()),
),
);
List<Widget> gestionUtilisateursItems = [
await _buildDrawerItem(
icon: Icons.person_add,
title: "Ajouter un utilisateur",
color: Colors.green,
permissionAction: 'create',
permissionRoute: '/ajouter-utilisateur',
onTap: () => Get.to(const RegistrationPage()),
),
await _buildDrawerItem(
icon: Icons.supervised_user_circle,
title: "Gérer les utilisateurs",
color: const Color.fromARGB(255, 4, 54, 95),
permissionAction: 'update',
permissionRoute: '/modifier-utilisateur',
onTap: () => Get.to(const ListUserPage()),
),
await _buildDrawerItem(
icon: Icons.timer,
title: "Gestion des pointages",
color: const Color.fromARGB(255, 4, 54, 95),
permissionAction: 'update',
permissionRoute: '/pointage',
onTap: () => {},
)
];
if (gestionUtilisateursItems.any((item) => item is ListTile)) {
drawerItems.add(
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,
],
),
],
),
),
// Bouton de rafraîchissement pour les admins
if (controller.role == 'Super Admin' || controller.role == 'Admin') ...[
IconButton(
icon: const Icon(Icons.refresh, color: Colors.white70, size: 20),
onPressed: () async {
Get.snackbar(
"Cache",
"Rechargement des permissions...",
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 1),
);
drawerItems.addAll(gestionUtilisateursItems);
}
List<Widget> gestionProduitsItems = [
await _buildDrawerItem(
icon: Icons.inventory,
title: "Gestion des produits",
color: Colors.indigoAccent,
permissionAction: 'create',
permissionRoute: '/ajouter-produit',
onTap: () => Get.to(const ProductManagementPage()),
),
await _buildDrawerItem(
icon: Icons.storage,
title: "Gestion de stock",
color: Colors.blueAccent,
permissionAction: 'update',
permissionRoute: '/gestion-stock',
onTap: () => Get.to(const GestionStockPage()),
),
];
if (gestionProduitsItems.any((item) => item is ListTile)) {
drawerItems.add(
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
"GESTION PRODUITS",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
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,
);
drawerItems.addAll(gestionProduitsItems);
}
List<Widget> gestionCommandesItems = [
await _buildDrawerItem(
icon: Icons.add_shopping_cart,
title: "Nouvelle commande",
color: Colors.orange,
permissionAction: 'create',
permissionRoute: '/nouvelle-commande',
onTap: () => Get.to(const NouvelleCommandePage()),
),
await _buildDrawerItem(
icon: Icons.list_alt,
title: "Gérer les commandes",
color: Colors.deepPurple,
permissionAction: 'manage',
permissionRoute: '/gerer-commandes',
onTap: () => Get.to(const GestionCommandesPage()),
),
];
if (gestionCommandesItems.any((item) => item is ListTile)) {
drawerItems.add(
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
"GESTION COMMANDES",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
},
tooltip: "Recharger les permissions",
),
);
drawerItems.addAll(gestionCommandesItems);
],
// 🔧 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";
}
List<Widget> rapportsItems = [
await _buildDrawerItem(
icon: Icons.bar_chart,
title: "Bilan ",
color: Colors.teal,
permissionAction: 'read',
permissionRoute: '/bilan',
onTap: () => Get.to(DashboardPage()),
),
await _buildDrawerItem(
icon: Icons.history,
title: "Historique",
color: Colors.blue,
permissionAction: 'read',
permissionRoute: '/historique',
onTap: () => Get.to(const HistoriquePage()),
),
];
if (rapportsItems.any((item) => item is ListTile)) {
drawerItems.add(
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
"RAPPORTS",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
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"),
),
],
),
);
drawerItems.addAll(rapportsItems);
}
List<Widget> administrationItems = [
await _buildDrawerItem(
icon: Icons.admin_panel_settings,
title: "Gérer les rôles",
color: Colors.redAccent,
permissionAction: 'admin',
permissionRoute: '/gerer-roles',
onTap: () => Get.to(const RoleListPage()),
),
await _buildDrawerItem(
icon: Icons.store,
title: "Points de vente",
color: Colors.blueGrey,
permissionAction: 'admin',
permissionRoute: '/points-de-vente',
onTap: () => Get.to(const AjoutPointDeVentePage()),
),
];
if (administrationItems.any((item) => item is ListTile)) {
drawerItems.add(
const Padding(
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
child: Text(
"ADMINISTRATION",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.bold,
),
},
tooltip: "Debug menus",
),
],
],
),
);
drawerItems.addAll(administrationItems);
}
drawerItems.add(const Divider());
drawerItems.add(
ListTile(
/// Item de déconnexion
Widget _buildLogoutItem() {
return ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
title: const Text("Déconnexion"),
onTap: () {
@ -348,7 +526,7 @@ class CustomDrawer extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
"Vous devrez vous reconnecter pour accéder à votre compte.",
"Vos permissions seront rechargées à la prochaine connexion.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
@ -392,6 +570,7 @@ class CustomDrawer extends StatelessWidget {
Expanded(
child: ElevatedButton(
onPressed: () async {
// IMPORTANT: Vider le cache de session lors de la déconnexion
await clearUserData();
Get.offAll(const LoginPage());
},
@ -423,36 +602,6 @@ class CustomDrawer extends StatelessWidget {
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 {
final int? id;
final int commandeId;
@ -226,6 +228,11 @@ class DetailCommande {
final String? produitReference;
final bool? estCadeau;
// NOUVEAUX CHAMPS POUR LA REMISE PAR PRODUIT
final double? remisePourcentage;
final double? remiseMontant;
final double? prixApresRemise;
DetailCommande({
this.id,
required this.commandeId,
@ -237,6 +244,9 @@ class DetailCommande {
this.produitImage,
this.produitReference,
this.estCadeau,
this.remisePourcentage,
this.remiseMontant,
this.prixApresRemise,
});
Map<String, dynamic> toMap() {
@ -248,6 +258,9 @@ class DetailCommande {
'prixUnitaire': prixUnitaire,
'sousTotal': sousTotal,
'estCadeau': estCadeau == true ? 1 : 0,
'remisePourcentage': remisePourcentage,
'remiseMontant': remiseMontant,
'prixApresRemise': prixApresRemise,
};
}
@ -263,6 +276,15 @@ class DetailCommande {
produitImage: map['produitImage'] as String?,
produitReference: map['produitReference'] as String?,
estCadeau: map['estCadeau'] == 1,
remisePourcentage: map['remisePourcentage'] != null
? (map['remisePourcentage'] as num).toDouble()
: null,
remiseMontant: map['remiseMontant'] != null
? (map['remiseMontant'] as num).toDouble()
: null,
prixApresRemise: map['prixApresRemise'] != null
? (map['prixApresRemise'] as num).toDouble()
: null,
);
}
@ -277,6 +299,9 @@ class DetailCommande {
String? produitImage,
String? produitReference,
bool? estCadeau,
double? remisePourcentage,
double? remiseMontant,
double? prixApresRemise,
}) {
return DetailCommande(
id: id ?? this.id,
@ -289,6 +314,29 @@ class DetailCommande {
produitImage: produitImage ?? this.produitImage,
produitReference: produitReference ?? this.produitReference,
estCadeau: estCadeau ?? this.estCadeau,
remisePourcentage: remisePourcentage ?? this.remisePourcentage,
remiseMontant: remiseMontant ?? this.remiseMontant,
prixApresRemise: prixApresRemise ?? this.prixApresRemise,
);
}
// GETTERS QUI RÉSOLVENT LE PROBLÈME "aUneRemise" INTROUVABLE
double get prixFinalUnitaire {
return prixApresRemise ?? prixUnitaire;
}
double get sousTotalAvecRemise {
return quantite * prixFinalUnitaire;
}
bool get aUneRemise {
return remisePourcentage != null || remiseMontant != null || prixApresRemise != null;
}
double get montantRemise {
if (prixApresRemise != null) {
return (prixUnitaire - prixApresRemise!) * quantite;
}
return 0.0;
}
}

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

399
lib/Services/stock_managementDatabase.dart

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

4625
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

286
lib/Views/loginPage.dart

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

496
lib/Views/mobilepage.dart

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'package:youmazgestion/Components/QrScan.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
@ -508,33 +509,471 @@ 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
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold(
floatingActionButton: _buildFloatingCartButton(),
drawer: isMobile ? CustomDrawer() : null,
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Bouton de scan
FloatingActionButton(
heroTag: "scan",
onPressed: _isScanning ? null : _startBarcodeScanning,
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
foregroundColor: Colors.white,
child: _isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.qr_code_scanner),
),
const SizedBox(height: 10),
// Bouton panier existant
_buildFloatingCartButton(),
],
),
appBar: CustomAppBar(title: 'Nouvelle commande'),
drawer: CustomDrawer(),
body: GestureDetector(
onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs
onTap: _hideAllSuggestions,
child: Column(
children: [
// Section des filtres - adaptée comme dans HistoriquePage
// Section d'information sur le scan (desktop)
if (!isMobile)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _buildScanInfoCard(),
),
// Section des filtres
if (!isMobile)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _buildFilterSection(),
),
// Sur mobile, bouton pour afficher les filtres dans un modal
// Boutons pour mobile
if (isMobile) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: SizedBox(
width: double.infinity,
child: Row(
children: [
Expanded(
flex: 2,
child: ElevatedButton.icon(
icon: const Icon(Icons.filter_alt),
label: const Text('Filtres produits'),
label: const Text('Filtres'),
onPressed: () {
showModalBottomSheet(
context: context,
@ -557,8 +996,36 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
),
),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: ElevatedButton.icon(
icon: _isScanning
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.qr_code_scanner),
label: Text(_isScanning ? 'Scan...' : 'Scan'),
onPressed: _isScanning ? null : _startBarcodeScanning,
style: ElevatedButton.styleFrom(
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
),
// Compteur de résultats visible en haut sur mobile
// Compteur de résultats
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Container(
@ -588,6 +1055,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
);
}
Widget _buildSuggestionsList({required bool isNom}) {
if (_clientSuggestions.isEmpty) return const SizedBox();
@ -1727,21 +2195,19 @@ void _fillFormWithClient(Client client) {
@override
void dispose() {
// Nettoyer les suggestions
_hideAllSuggestions();
_qrController?.dispose();
// Disposer les contrôleurs
// Vos disposals existants...
_hideAllSuggestions();
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
// Disposal des contrôleurs de filtre
_searchNameController.dispose();
_searchImeiController.dispose();
_searchReferenceController.dispose();
super.dispose();
}
}
}

858
lib/Views/newCommand.dart

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Models/client.dart';
@ -47,8 +49,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
List<Client> _clientSuggestions = [];
bool _showNomSuggestions = false;
bool _showTelephoneSuggestions = false;
GlobalKey _nomFieldKey = GlobalKey();
GlobalKey _telephoneFieldKey = GlobalKey();
// Variables pour le scanner (identiques à ProductManagementPage)
QRViewController? _qrController;
bool _isScanning = false;
final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR');
@override
void initState() {
@ -79,6 +85,518 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
});
}
// === NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE (identiques à ProductManagementPage) ===
void _startAutomaticScanning() {
if (_isScanning) return;
setState(() {
_isScanning = true;
});
Get.to(() => _buildAutomaticScannerPage())?.then((_) {
setState(() {
_isScanning = false;
});
});
}
Widget _buildAutomaticScannerPage() {
return Scaffold(
appBar: AppBar(
title: const Text('Scanner Produit'),
backgroundColor: Colors.green.shade700,
foregroundColor: Colors.white,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
_qrController?.dispose();
Get.back();
},
),
actions: [
IconButton(
icon: const Icon(Icons.flash_on),
onPressed: () async {
await _qrController?.toggleFlash();
},
),
IconButton(
icon: const Icon(Icons.flip_camera_ios),
onPressed: () async {
await _qrController?.flipCamera();
},
),
],
),
body: Stack(
children: [
// Scanner view
QRView(
key: _qrKey,
onQRViewCreated: _onAutomaticQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.green,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: 250,
),
),
// Instructions overlay
Positioned(
bottom: 100,
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(12),
),
child: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.qr_code_scanner, color: Colors.white, size: 40),
SizedBox(height: 8),
Text(
'Scanner automatiquement un produit',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
SizedBox(height: 4),
Text(
'Pointez vers QR Code, IMEI ou code-barres',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
),
),
),
],
),
);
}
void _onAutomaticQRViewCreated(QRViewController controller) {
_qrController = controller;
controller.scannedDataStream.listen((scanData) {
if (scanData.code != null && scanData.code!.isNotEmpty) {
// Pauser le scanner pour éviter les scans multiples
controller.pauseCamera();
// Fermer la page du scanner
Get.back();
// Traiter le résultat avec identification automatique
_processScannedData(scanData.code!);
}
});
}
Future<void> _processScannedData(String scannedData) async {
try {
// Montrer un indicateur de chargement
Get.dialog(
AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.green.shade700),
const SizedBox(height: 16),
const Text('Identification du produit...'),
const SizedBox(height: 8),
Text(
'Code: $scannedData',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontFamily: 'monospace',
),
),
],
),
),
barrierDismissible: false,
);
// Attendre un court instant pour l'effet visuel
await Future.delayed(const Duration(milliseconds: 300));
// Recherche automatique du produit par différents critères
Product? foundProduct = await _findProductAutomatically(scannedData);
// Fermer l'indicateur de chargement
Get.back();
if (foundProduct == null) {
_showProductNotFoundDialog(scannedData);
return;
}
// Vérifier le stock
if (foundProduct.stock != null && foundProduct.stock! <= 0) {
Get.snackbar(
'Stock insuffisant',
'Le produit "${foundProduct.name}" n\'est plus en stock',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange.shade600,
colorText: Colors.white,
duration: const Duration(seconds: 3),
icon: const Icon(Icons.warning_amber, color: Colors.white),
);
return;
}
// Vérifier si le produit peut être ajouté (stock disponible)
final currentQuantity = _quantites[foundProduct.id] ?? 0;
if (foundProduct.stock != null && currentQuantity >= foundProduct.stock!) {
Get.snackbar(
'Stock limite atteint',
'Quantité maximum atteinte pour "${foundProduct.name}"',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange.shade600,
colorText: Colors.white,
duration: const Duration(seconds: 3),
icon: const Icon(Icons.warning_amber, color: Colors.white),
);
return;
}
// Ajouter le produit au panier
setState(() {
_quantites[foundProduct!.id!] = currentQuantity + 1;
});
// Afficher le dialogue de succès
_showProductFoundAndAddedDialog(foundProduct, currentQuantity + 1);
} catch (e) {
// Fermer l'indicateur de chargement si il est encore ouvert
if (Get.isDialogOpen!) Get.back();
Get.snackbar(
'Erreur',
'Une erreur est survenue: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade600,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
}
Future<Product?> _findProductAutomatically(String scannedData) async {
// Nettoyer les données scannées
final cleanedData = scannedData.trim();
// 1. Essayer de trouver par IMEI exact
for (var product in _products) {
if (product.imei?.toLowerCase().trim() == cleanedData.toLowerCase()) {
return product;
}
}
// 2. Essayer de trouver par référence exacte
for (var product in _products) {
if (product.reference?.toLowerCase().trim() == cleanedData.toLowerCase()) {
return product;
}
}
// 3. Si c'est une URL QR code, extraire la référence
if (cleanedData.contains('stock.guycom.mg/')) {
final reference = cleanedData.split('/').last;
for (var product in _products) {
if (product.reference?.toLowerCase().trim() == reference.toLowerCase()) {
return product;
}
}
}
// 4. Recherche par correspondance partielle dans le nom
for (var product in _products) {
if (product.name.toLowerCase().contains(cleanedData.toLowerCase()) &&
cleanedData.length >= 3) {
return product;
}
}
// 5. Utiliser la base de données pour une recherche plus approfondie
try {
// Recherche par IMEI dans la base
final productByImei = await _appDatabase.getProductByIMEI(cleanedData);
if (productByImei != null) {
return productByImei;
}
// Recherche par référence dans la base
final productByRef = await _appDatabase.getProductByReference(cleanedData);
if (productByRef != null) {
return productByRef;
}
} catch (e) {
print('Erreur recherche base de données: $e');
}
return null;
}
void _showProductFoundAndAddedDialog(Product product, int newQuantity) {
Get.dialog(
AlertDialog(
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.check_circle, color: Colors.green.shade700),
),
const SizedBox(width: 12),
const Expanded(child: Text('Produit identifié et ajouté !')),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
if (product.imei != null && product.imei!.isNotEmpty)
Text('IMEI: ${product.imei}'),
if (product.reference != null && product.reference!.isNotEmpty)
Text('Référence: ${product.reference}'),
Text('Prix: ${product.price.toStringAsFixed(2)} MGA'),
Text('Quantité dans le panier: $newQuantity'),
if (product.stock != null)
Text('Stock restant: ${product.stock! - newQuantity}'),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.auto_awesome,
color: Colors.green.shade700, size: 16),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Produit identifié automatiquement',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Continuer'),
),
ElevatedButton(
onPressed: () {
Get.back();
_showCartBottomSheet();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade700,
foregroundColor: Colors.white,
),
child: const Text('Voir le panier'),
),
ElevatedButton(
onPressed: () {
Get.back();
_startAutomaticScanning(); // Scanner un autre produit
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
),
child: const Text('Scanner encore'),
),
],
),
);
}
void _showProductNotFoundDialog(String scannedData) {
Get.dialog(
AlertDialog(
title: Row(
children: [
Icon(Icons.search_off, color: Colors.red.shade600),
const SizedBox(width: 8),
const Text('Produit non trouvé'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Aucun produit trouvé avec ce code:'),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
scannedData,
style: const TextStyle(
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 12),
Text(
'Vérifiez que le code est correct ou que le produit existe dans la base de données.',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Types de codes supportés:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 4),
Text(
'• QR Code produit\n• IMEI (téléphones)\n• Référence produit\n• Code-barres',
style: TextStyle(
fontSize: 11,
color: Colors.blue.shade600,
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
Get.back();
_startAutomaticScanning();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade700,
foregroundColor: Colors.white,
),
child: const Text('Scanner à nouveau'),
),
],
),
);
}
Widget _buildAutoScanInfoCard() {
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.auto_awesome,
color: Colors.green.shade700,
size: 20,
),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Scanner automatiquement: QR Code, IMEI, Référence ou code-barres',
style: TextStyle(
fontSize: 14,
color: Color.fromARGB(255, 9, 56, 95),
),
),
),
ElevatedButton.icon(
onPressed: _isScanning ? null : _startAutomaticScanning,
icon: _isScanning
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.qr_code_scanner, size: 18),
label: Text(_isScanning ? 'Scan...' : 'Scanner'),
style: ElevatedButton.styleFrom(
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
],
),
),
);
}
// === FIN DES NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE ===
// Méthode pour vider complètement le formulaire et le panier
void _clearFormAndCart() {
setState(() {
@ -107,28 +625,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
Future<void> _showClientSuggestions(String query, {required bool isNom}) async {
if (query.length < 3) {
_hideAllSuggestions();
return;
}
final suggestions = await _appDatabase.suggestClients(query);
setState(() {
_clientSuggestions = suggestions;
if (isNom) {
_showNomSuggestions = true;
_showTelephoneSuggestions = false;
} else {
_showTelephoneSuggestions = true;
_showNomSuggestions = false;
_hideAllSuggestions();
return;
}
});
}
void _showOverlay({required bool isNom}) {
// Utiliser une approche plus simple avec setState
final suggestions = await _appDatabase.suggestClients(query);
setState(() {
_clientSuggestions = _clientSuggestions;
_clientSuggestions = suggestions;
if (isNom) {
_showNomSuggestions = true;
_showTelephoneSuggestions = false;
@ -139,6 +643,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
});
}
void _fillClientForm(Client client) {
setState(() {
_nomController.text = client.nom;
@ -419,145 +924,6 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
);
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold(
floatingActionButton: _buildFloatingCartButton(),
drawer: isMobile ? CustomDrawer() : null,
body: GestureDetector(
onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs
child: Column(
children: [
// Section des filtres - adaptée comme dans HistoriquePage
if (!isMobile)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _buildFilterSection(),
),
// Sur mobile, bouton pour afficher les filtres dans un modal
if (isMobile) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.filter_alt),
label: const Text('Filtres produits'),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: _buildFilterSection(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
),
// Compteur de résultats visible en haut sur mobile
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${_filteredProducts.length} produit(s)',
style: TextStyle(
color: Colors.blue.shade700,
fontWeight: FontWeight.w600,
),
),
),
),
],
// Liste des produits
Expanded(
child: _buildProductList(),
),
],
),
),
);
}
Widget _buildSuggestionsList({required bool isNom}) {
if (_clientSuggestions.isEmpty) return const SizedBox();
return Container(
margin: const EdgeInsets.only(top: 4),
constraints: const BoxConstraints(maxHeight: 150),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: _clientSuggestions.length,
itemBuilder: (context, index) {
final client = _clientSuggestions[index];
return ListTile(
dense: true,
leading: CircleAvatar(
radius: 16,
backgroundColor: Colors.blue.shade100,
child: Icon(
Icons.person,
size: 16,
color: Colors.blue.shade700,
),
),
title: Text(
'${client.nom} ${client.prenom}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
'${client.telephone}${client.email}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
onTap: () {
_fillClientForm(client);
_hideAllSuggestions();
},
);
},
),
);
}
Widget _buildFloatingCartButton() {
final isMobile = MediaQuery.of(context).size.width < 600;
final cartItemCount = _quantites.values.where((q) => q > 0).length;
@ -576,6 +942,8 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
);
}
void _showClientFormDialog() {
final isMobile = MediaQuery.of(context).size.width < 600;
@ -890,10 +1258,10 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
},
),
);
}
}
// Widget pour créer un TextFormField avec une clé
Widget _buildTextFormFieldWithKey({
Widget _buildTextFormFieldWithKey({
required GlobalKey key,
required TextEditingController controller,
required String label,
@ -901,7 +1269,7 @@ Widget _buildTextFormFieldWithKey({
int maxLines = 1,
String? Function(String?)? validator,
void Function(String)? onChanged,
}) {
}) {
return Container(
key: key,
child: _buildTextFormField(
@ -913,9 +1281,10 @@ Widget _buildTextFormFieldWithKey({
onChanged: onChanged,
),
);
}
}
// Widget pour l'overlay des suggestions
// Widget pour l'overlay des suggestions
Widget _buildSuggestionOverlay({
required GlobalKey fieldKey,
required List<Client> suggestions,
@ -1020,10 +1389,10 @@ Widget _buildSuggestionOverlay({
),
),
);
}
}
// Méthode pour remplir le formulaire avec les données du client
void _fillFormWithClient(Client client) {
void _fillFormWithClient(Client client) {
_nomController.text = client.nom;
_prenomController.text = client.prenom;
_emailController.text = client.email;
@ -1038,7 +1407,7 @@ void _fillFormWithClient(Client client) {
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
}
}
Widget _buildTextFormField({
required TextEditingController controller,
@ -1638,20 +2007,163 @@ void _fillFormWithClient(Client client) {
@override
void dispose() {
// Nettoyer les suggestions
_hideAllSuggestions();
_qrController?.dispose();
// Disposer les contrôleurs
// Vos disposals existants...
_hideAllSuggestions();
_nomController.dispose();
_prenomController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
// Disposal des contrôleurs de filtre
_searchNameController.dispose();
_searchImeiController.dispose();
_searchReferenceController.dispose();
super.dispose();
}}
}
// 10. Modifier le Widget build pour utiliser le nouveau scan automatique
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold(
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Bouton de scan automatique (remplace l'ancien scan IMEI)
FloatingActionButton(
heroTag: "auto_scan",
onPressed: _isScanning ? null : _startAutomaticScanning,
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
foregroundColor: Colors.white,
child: _isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.auto_awesome),
),
const SizedBox(height: 10),
// Bouton panier existant
_buildFloatingCartButton(),
],
),
appBar: CustomAppBar(title: 'Nouvelle commande'),
drawer: CustomDrawer(),
body: GestureDetector(
onTap: _hideAllSuggestions,
child: Column(
children: [
// Section d'information sur le scan automatique (desktop)
if (!isMobile)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _buildAutoScanInfoCard(),
),
// Section des filtres
if (!isMobile)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _buildFilterSection(),
),
// Boutons pour mobile
if (isMobile) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: [
Expanded(
flex: 2,
child: ElevatedButton.icon(
icon: const Icon(Icons.filter_alt),
label: const Text('Filtres'),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: _buildFilterSection(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: ElevatedButton.icon(
icon: _isScanning
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.auto_awesome),
label: Text(_isScanning ? 'Scan...' : 'Auto-scan'),
onPressed: _isScanning ? null : _startAutomaticScanning,
style: ElevatedButton.styleFrom(
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
),
// Compteur de résultats
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${_filteredProducts.length} produit(s)',
style: TextStyle(
color: Colors.blue.shade700,
fontWeight: FontWeight.w600,
),
),
),
),
],
// Liste des produits
Expanded(
child: _buildProductList(),
),
],
),
),
);
}
}

12
lib/config/DatabaseConfig.dart

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

152
lib/controller/userController.dart

@ -2,7 +2,7 @@ import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
//import 'package:youmazgestion/Services/app_database.dart';
import 'package:youmazgestion/Services/PermissionCacheService.dart';
class UserController extends GetxController {
final _username = ''.obs;
@ -11,10 +11,14 @@ class UserController extends GetxController {
final _name = ''.obs;
final _lastname = ''.obs;
final _password = ''.obs;
final _userId = 0.obs; // Ajout de l'ID utilisateur
final _userId = 0.obs;
final _pointDeVenteId = 0.obs;
final _pointDeVenteDesignation = ''.obs;
// Cache service
final PermissionCacheService _cacheService = PermissionCacheService.instance;
// Getters
String get username => _username.value;
String get email => _email.value;
String get role => _role.value;
@ -28,10 +32,10 @@ class UserController extends GetxController {
@override
void onInit() {
super.onInit();
loadUserData(); // Charger les données au démarrage
loadUserData();
}
// CORRECTION : Charger les données complètes depuis SharedPreferences ET la base de données
/// SIMPLIFIÉ: Charge les données utilisateur sans cache persistant
Future<void> loadUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
@ -56,13 +60,15 @@ class UserController extends GetxController {
_pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
// Si la désignation n'est pas sauvegardée, on peut la récupérer
if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) {
await loadPointDeVenteDesignation();
}
// Précharger les permissions en arrière-plan (non bloquant)
_preloadPermissionsInBackground();
} catch (dbError) {
// Fallback
print("❌ Erreur BDD, utilisation du fallback: $dbError");
_username.value = storedUsername;
_email.value = prefs.getString('email') ?? '';
_role.value = storedRole;
@ -71,49 +77,66 @@ class UserController extends GetxController {
_userId.value = storedUserId;
_pointDeVenteId.value = storedPointDeVenteId;
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
// Précharger quand même
_preloadPermissionsInBackground();
}
}
} 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)
void _preloadPermissionsInBackground() {
if (_username.value.isNotEmpty) {
// Lancer en arrière-plan sans attendre
Future.microtask(() async {
try {
await _cacheService.preloadUserData(_username.value);
} catch (e) {
print("⚠️ Erreur préchargement permissions (non critique): $e");
}
});
}
}
Future<void> loadPointDeVenteDesignation() async {
if (_pointDeVenteId.value <= 0) return;
try {
final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value);
if (pointDeVente != null) {
_pointDeVenteDesignation.value = pointDeVente['designation'] as String;
await saveUserData(); // Sauvegarder la désignation
_pointDeVenteDesignation.value = pointDeVente['nom'] as String;
await saveUserData();
}
} 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
/// Mise à jour avec préchargement des permissions
void setUserWithCredentials(Users user, String role, int userId) {
_username.value = user.username;
_email.value = user.email;
_role.value = role; // Rôle depuis les credentials
_role.value = role;
_name.value = user.name;
_lastname.value = user.lastName;
_password.value = user.password;
_userId.value = userId; // ID depuis les credentials
_userId.value = userId;
_pointDeVenteId.value = user.pointDeVenteId ?? 0;
print("✅ Utilisateur mis à jour avec credentials:");
print(" Username: ${_username.value}");
print(" Name: ${_name.value}");
print(" Email: ${_email.value}");
print(" Role: ${_role.value}");
print(" UserID: ${_userId.value}");
// Sauvegarder dans SharedPreferences
saveUserData();
// Précharger immédiatement les permissions après connexion
_preloadPermissionsInBackground();
}
// MÉTHODE EXISTANTE AMÉLIORÉE
void setUser(Users user) {
_username.value = user.username;
_email.value = user.email;
@ -121,17 +144,11 @@ Future<void> loadPointDeVenteDesignation() async {
_name.value = user.name;
_lastname.value = user.lastName;
_password.value = user.password;
// Note: _userId reste inchangé si pas fourni
print("✅ Utilisateur mis à jour (méthode legacy):");
print(" Username: ${_username.value}");
print(" Role: ${_role.value}");
// Sauvegarder dans SharedPreferences
saveUserData();
_preloadPermissionsInBackground();
}
// CORRECTION : Sauvegarder TOUTES les données importantes
Future<void> saveUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
@ -145,17 +162,21 @@ Future<void> loadPointDeVenteDesignation() async {
await prefs.setInt('point_de_vente_id', _pointDeVenteId.value);
await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value);
print("✅ Données sauvegardées avec succès dans SharedPreferences");
print("✅ Données sauvegardées avec succès");
} 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)
Future<void> clearUserData() async {
/// MODIFIÉ: Vider les données ET le cache de session
Future<void> clearUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
// IMPORTANT: Vider le cache de session
_cacheService.clearAllCache();
// Effacer SharedPreferences
await prefs.remove('username');
await prefs.remove('email');
await prefs.remove('role');
@ -165,6 +186,7 @@ Future<void> clearUserData() async {
await prefs.remove('point_de_vente_id');
await prefs.remove('point_de_vente_designation');
// Effacer les observables
_username.value = '';
_email.value = '';
_role.value = '';
@ -175,36 +197,54 @@ Future<void> clearUserData() async {
_pointDeVenteId.value = 0;
_pointDeVenteDesignation.value = '';
print("✅ Données utilisateur et cache de session vidés");
} catch (e) {
print('❌ Erreur lors de l\'effacement des données utilisateur: $e');
print('❌ Erreur lors de l\'effacement: $e');
}
}
}
// MÉTHODE UTILITAIRE : Vérifier si un utilisateur est connecté
// Getters utilitaires
bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0;
// MÉTHODE UTILITAIRE : Obtenir le nom complet
String get fullName => '${_name.value} ${_lastname.value}'.trim();
/// OPTIMISÉ: Vérification des permissions depuis le cache de session
Future<bool> hasPermission(String permission, String route) async {
try {
if (_username.value.isEmpty) {
print('⚠️ Username vide, rechargement des données...');
print('⚠️ Username vide, rechargement...');
await loadUserData();
}
if (_username.value.isEmpty) {
print('Impossible de vérifier les permissions : utilisateur non connecté');
print('Utilisateur non connecté');
return false;
}
return await AppDatabase.instance.hasPermission(username, permission, route);
// Essayer d'abord le cache
if (_cacheService.isLoaded) {
return _cacheService.hasPermission(_username.value, permission, route);
}
// Si pas encore chargé, charger et essayer de nouveau
print("🔄 Cache non chargé, chargement des permissions...");
await _cacheService.loadUserPermissions(_username.value);
return _cacheService.hasPermission(_username.value, permission, route);
} catch (e) {
print('❌ Erreur vérification permission: $e');
return false; // Sécurité : refuser l'accès en cas d'erreur
// Fallback vers la méthode originale en cas d'erreur
try {
return await AppDatabase.instance.hasPermission(_username.value, permission, route);
} catch (fallbackError) {
print('❌ Erreur fallback permission: $fallbackError');
return false;
}
}
}
/// Vérification de permissions multiples
Future<bool> hasAnyPermission(List<String> permissionNames, String menuRoute) async {
for (String permissionName in permissionNames) {
if (await hasPermission(permissionName, menuRoute)) {
@ -214,18 +254,40 @@ Future<void> clearUserData() async {
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() {
print("=== ÉTAT UTILISATEUR ===");
print("Username: ${_username.value}");
print("Name: ${_name.value}");
print("Lastname: ${_lastname.value}");
print("Email: ${_email.value}");
print("Role: ${_role.value}");
print("UserID: ${_userId.value}");
print("PointDeVenteID: ${_pointDeVenteId.value}");
print("PointDeVente: ${_pointDeVenteDesignation.value}");
print("IsLoggedIn: $isLoggedIn");
print("Cache Ready: $isCacheReady");
print("========================");
}
// Debug du cache
_cacheService.debugPrintCache();
}
}

2
lib/main.dart

@ -17,7 +17,7 @@ void main() async {
// Initialiser la base de données MySQL
print("Connexion à la base de données MySQL...");
await AppDatabase.instance.initDatabase();
// await AppDatabase.instance.initDatabase();
print("Base de données initialisée avec succès !");
// Afficher les informations de la base (pour debug)

50
lib/my_app.dart

@ -1,8 +1,4 @@
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'Views/ErreurPage.dart';
import 'Views/loginPage.dart';
class MyApp extends StatelessWidget {
@ -10,12 +6,11 @@ class MyApp extends StatelessWidget {
static bool isRegisterOpen = false;
static DateTime? startDate;
static late String path;
static const Gradient primaryGradient = LinearGradient(
colors: [
Colors.white,
const Color.fromARGB(255, 4, 54, 95),
Color.fromARGB(255, 4, 54, 95),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
@ -24,56 +19,17 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: 'GUYCOM',
debugShowCheckedModeBanner: false,
theme: ThemeData(
canvasColor: Colors.transparent,
),
home: Builder(
builder: (context) {
return FutureBuilder<bool>(
future:
checkLocalDatabasesExist(), // Appel à la fonction de vérification
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Affichez un indicateur de chargement si nécessaire
return const CircularProgressIndicator();
} else if (snapshot.hasError || !(snapshot.data ?? false)) {
// S'il y a une erreur ou si les bases de données n'existent pas
return ErreurPage(
dbPath:
path); // Redirigez vers la page d'erreur en affichant le chemin de la base de données
} else {
// Si les bases de données existent, affichez la page d'accueil normalement
return Container(
home: 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"
source: hosted
version: "3.0.2"
qr_code_scanner_plus:
dependency: "direct main"
description:
name: qr_code_scanner_plus
sha256: "39696b50d277097ee4d90d4292de36f38c66213a4f5216a06b2bdd2b63117859"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
qr_flutter:
dependency: "direct main"
description:

1
pubspec.yaml

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

3
test/widget_test.dart

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

Loading…
Cancel
Save