scan code bar
This commit is contained in:
parent
b5a11aa3c9
commit
525b09c81f
@ -14,7 +14,7 @@ import 'package:youmazgestion/Views/newCommand.dart';
|
|||||||
import 'package:youmazgestion/Views/registrationPage.dart';
|
import 'package:youmazgestion/Views/registrationPage.dart';
|
||||||
import 'package:youmazgestion/accueil.dart';
|
import 'package:youmazgestion/accueil.dart';
|
||||||
import 'package:youmazgestion/controller/userController.dart';
|
import 'package:youmazgestion/controller/userController.dart';
|
||||||
import 'package:youmazgestion/Views/gestion_point_de_vente.dart'; // Nouvel import
|
import 'package:youmazgestion/Views/gestion_point_de_vente.dart';
|
||||||
|
|
||||||
class CustomDrawer extends StatelessWidget {
|
class CustomDrawer extends StatelessWidget {
|
||||||
final UserController userController = Get.find<UserController>();
|
final UserController userController = Get.find<UserController>();
|
||||||
@ -25,6 +25,7 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
await prefs.remove('role');
|
await prefs.remove('role');
|
||||||
await prefs.remove('user_id');
|
await prefs.remove('user_id');
|
||||||
|
|
||||||
|
// ✅ IMPORTANT: Vider le cache de session
|
||||||
userController.clearUserData();
|
userController.clearUserData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,28 +35,320 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Drawer(
|
return Drawer(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
child: FutureBuilder(
|
child: GetBuilder<UserController>(
|
||||||
future: _buildDrawerItems(),
|
builder: (controller) {
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: EdgeInsets.zero,
|
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(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Widget>> _buildDrawerItems() async {
|
/// ✅ CORRIGÉ: Construction avec validation robuste des données
|
||||||
|
List<Widget> _buildDrawerItemsFromSessionCache() {
|
||||||
List<Widget> drawerItems = [];
|
List<Widget> drawerItems = [];
|
||||||
|
|
||||||
drawerItems.add(
|
// Vérifier si le cache est prêt
|
||||||
GetBuilder<UserController>(
|
if (!userController.isCacheReady) {
|
||||||
builder: (controller) => Container(
|
return [
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Header utilisateur amélioré
|
||||||
|
Widget _buildUserHeader(UserController controller) {
|
||||||
|
return Container(
|
||||||
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
|
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
@ -71,12 +364,13 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
backgroundImage: AssetImage("assets/youmaz2.png"),
|
backgroundImage: AssetImage("assets/youmaz2.png"),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 15),
|
const SizedBox(width: 15),
|
||||||
Column(
|
Expanded(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
controller.name.isNotEmpty
|
controller.name.isNotEmpty
|
||||||
? controller.name
|
? controller.fullName
|
||||||
: 'Utilisateur',
|
: 'Utilisateur',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@ -91,217 +385,101 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (controller.pointDeVenteDesignation.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
controller.pointDeVenteDesignation,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white60,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// ✅ Indicateur de statut du cache
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
controller.isCacheReady ? Icons.check_circle : Icons.hourglass_empty,
|
||||||
|
color: controller.isCacheReady ? Colors.green : Colors.orange,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
controller.isCacheReady ? 'Menu prêt' : 'Chargement...',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white60,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
// ✅ Bouton de rafraîchissement pour les admins
|
||||||
|
if (controller.role == 'Super Admin' || controller.role == 'Admin') ...[
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh, color: Colors.white70, size: 20),
|
||||||
|
onPressed: () async {
|
||||||
|
Get.snackbar(
|
||||||
|
"Cache",
|
||||||
|
"Rechargement des permissions...",
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
);
|
);
|
||||||
|
await controller.refreshPermissions();
|
||||||
drawerItems.add(
|
Get.back(); // Fermer le drawer
|
||||||
await _buildDrawerItem(
|
Get.snackbar(
|
||||||
icon: Icons.home,
|
"Cache",
|
||||||
title: "Accueil",
|
"Permissions rechargées avec succès",
|
||||||
color: Colors.blue,
|
snackPosition: SnackPosition.TOP,
|
||||||
permissionAction: 'view',
|
backgroundColor: Colors.green,
|
||||||
permissionRoute: '/accueil',
|
colorText: Colors.white,
|
||||||
onTap: () => Get.to(DashboardPage()),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
},
|
||||||
List<Widget> gestionUtilisateursItems = [
|
tooltip: "Recharger les permissions",
|
||||||
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,
|
// 🔧 Bouton de debug (à supprimer en production)
|
||||||
title: "Gérer les utilisateurs",
|
if (controller.role == 'Super Admin') ...[
|
||||||
color: const Color.fromARGB(255, 4, 54, 95),
|
IconButton(
|
||||||
permissionAction: 'update',
|
icon: const Icon(Icons.bug_report, color: Colors.white70, size: 18),
|
||||||
permissionRoute: '/modifier-utilisateur',
|
onPressed: () {
|
||||||
onTap: () => Get.to(const ListUserPage()),
|
// Debug des menus
|
||||||
),
|
final menus = controller.getUserMenus();
|
||||||
await _buildDrawerItem(
|
String debugInfo = "MENUS DEBUG:\n";
|
||||||
icon: Icons.timer,
|
for (var i = 0; i < menus.length; i++) {
|
||||||
title: "Gestion des pointages",
|
final menu = menus[i];
|
||||||
color: const Color.fromARGB(255, 4, 54, 95),
|
debugInfo += "[$i] ID:${menu['id']}, Name:'${menu['name']}', Route:'${menu['route']}'\n";
|
||||||
permissionAction: 'update',
|
|
||||||
permissionRoute: '/pointage',
|
|
||||||
onTap: () => {},
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
if (gestionUtilisateursItems.any((item) => item is ListTile)) {
|
|
||||||
drawerItems.add(
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
|
||||||
child: Text(
|
|
||||||
"GESTION UTILISATEURS",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
drawerItems.addAll(gestionUtilisateursItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> gestionProduitsItems = [
|
Get.dialog(
|
||||||
await _buildDrawerItem(
|
AlertDialog(
|
||||||
icon: Icons.inventory,
|
title: const Text("Debug Menus"),
|
||||||
title: "Gestion des produits",
|
content: SingleChildScrollView(
|
||||||
color: Colors.indigoAccent,
|
child: Text(debugInfo, style: const TextStyle(fontSize: 12)),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: const Text("Fermer"),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
drawerItems.addAll(gestionProduitsItems);
|
},
|
||||||
}
|
tooltip: "Debug menus",
|
||||||
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
drawerItems.addAll(gestionCommandesItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> rapportsItems = [
|
/// Item de déconnexion
|
||||||
await _buildDrawerItem(
|
Widget _buildLogoutItem() {
|
||||||
icon: Icons.bar_chart,
|
return ListTile(
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
drawerItems.addAll(administrationItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawerItems.add(const Divider());
|
|
||||||
|
|
||||||
drawerItems.add(
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.logout, color: Colors.red),
|
leading: const Icon(Icons.logout, color: Colors.red),
|
||||||
title: const Text("Déconnexion"),
|
title: const Text("Déconnexion"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -348,7 +526,7 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
"Vous devrez vous reconnecter pour accéder à votre compte.",
|
"Vos permissions seront rechargées à la prochaine connexion.",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@ -392,6 +570,7 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
// ✅ IMPORTANT: Vider le cache de session lors de la déconnexion
|
||||||
await clearUserData();
|
await clearUserData();
|
||||||
Get.offAll(const LoginPage());
|
Get.offAll(const LoginPage());
|
||||||
},
|
},
|
||||||
@ -423,36 +602,6 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return drawerItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Widget> _buildDrawerItem({
|
|
||||||
required IconData icon,
|
|
||||||
required String title,
|
|
||||||
required Color color,
|
|
||||||
String? permissionAction,
|
|
||||||
String? permissionRoute,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
}) async {
|
|
||||||
if (permissionAction != null && permissionRoute != null) {
|
|
||||||
bool hasPermission =
|
|
||||||
await userController.hasPermission(permissionAction, permissionRoute);
|
|
||||||
if (!hasPermission) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: Icon(icon, color: color),
|
|
||||||
title: Text(title),
|
|
||||||
trailing: permissionAction != null
|
|
||||||
? const Icon(Icons.chevron_right, color: Colors.grey)
|
|
||||||
: null,
|
|
||||||
onTap: onTap,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
175
lib/Components/commandManagementComponents/CommandDetails.dart
Normal file
175
lib/Components/commandManagementComponents/CommandDetails.dart
Normal file
@ -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
Normal file
226
lib/Components/commandManagementComponents/CommandeActions.dart
Normal file
@ -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
Normal file
189
lib/Components/commandManagementComponents/DiscountDialog.dart
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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});
|
||||||
|
}
|
||||||
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
enum PaymentType {
|
||||||
|
cash,
|
||||||
|
card,
|
||||||
|
mvola,
|
||||||
|
orange,
|
||||||
|
airtel
|
||||||
|
}
|
||||||
2125
lib/Components/teat.dart
Normal file
2125
lib/Components/teat.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -214,6 +214,8 @@ class Commande {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci :
|
||||||
|
|
||||||
class DetailCommande {
|
class DetailCommande {
|
||||||
final int? id;
|
final int? id;
|
||||||
final int commandeId;
|
final int commandeId;
|
||||||
@ -226,6 +228,11 @@ class DetailCommande {
|
|||||||
final String? produitReference;
|
final String? produitReference;
|
||||||
final bool? estCadeau;
|
final bool? estCadeau;
|
||||||
|
|
||||||
|
// NOUVEAUX CHAMPS POUR LA REMISE PAR PRODUIT
|
||||||
|
final double? remisePourcentage;
|
||||||
|
final double? remiseMontant;
|
||||||
|
final double? prixApresRemise;
|
||||||
|
|
||||||
DetailCommande({
|
DetailCommande({
|
||||||
this.id,
|
this.id,
|
||||||
required this.commandeId,
|
required this.commandeId,
|
||||||
@ -237,6 +244,9 @@ class DetailCommande {
|
|||||||
this.produitImage,
|
this.produitImage,
|
||||||
this.produitReference,
|
this.produitReference,
|
||||||
this.estCadeau,
|
this.estCadeau,
|
||||||
|
this.remisePourcentage,
|
||||||
|
this.remiseMontant,
|
||||||
|
this.prixApresRemise,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
@ -248,6 +258,9 @@ class DetailCommande {
|
|||||||
'prixUnitaire': prixUnitaire,
|
'prixUnitaire': prixUnitaire,
|
||||||
'sousTotal': sousTotal,
|
'sousTotal': sousTotal,
|
||||||
'estCadeau': estCadeau == true ? 1 : 0,
|
'estCadeau': estCadeau == true ? 1 : 0,
|
||||||
|
'remisePourcentage': remisePourcentage,
|
||||||
|
'remiseMontant': remiseMontant,
|
||||||
|
'prixApresRemise': prixApresRemise,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,6 +276,15 @@ class DetailCommande {
|
|||||||
produitImage: map['produitImage'] as String?,
|
produitImage: map['produitImage'] as String?,
|
||||||
produitReference: map['produitReference'] as String?,
|
produitReference: map['produitReference'] as String?,
|
||||||
estCadeau: map['estCadeau'] == 1,
|
estCadeau: map['estCadeau'] == 1,
|
||||||
|
remisePourcentage: map['remisePourcentage'] != null
|
||||||
|
? (map['remisePourcentage'] as num).toDouble()
|
||||||
|
: null,
|
||||||
|
remiseMontant: map['remiseMontant'] != null
|
||||||
|
? (map['remiseMontant'] as num).toDouble()
|
||||||
|
: null,
|
||||||
|
prixApresRemise: map['prixApresRemise'] != null
|
||||||
|
? (map['prixApresRemise'] as num).toDouble()
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,6 +299,9 @@ class DetailCommande {
|
|||||||
String? produitImage,
|
String? produitImage,
|
||||||
String? produitReference,
|
String? produitReference,
|
||||||
bool? estCadeau,
|
bool? estCadeau,
|
||||||
|
double? remisePourcentage,
|
||||||
|
double? remiseMontant,
|
||||||
|
double? prixApresRemise,
|
||||||
}) {
|
}) {
|
||||||
return DetailCommande(
|
return DetailCommande(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@ -289,6 +314,29 @@ class DetailCommande {
|
|||||||
produitImage: produitImage ?? this.produitImage,
|
produitImage: produitImage ?? this.produitImage,
|
||||||
produitReference: produitReference ?? this.produitReference,
|
produitReference: produitReference ?? this.produitReference,
|
||||||
estCadeau: estCadeau ?? this.estCadeau,
|
estCadeau: estCadeau ?? this.estCadeau,
|
||||||
|
remisePourcentage: remisePourcentage ?? this.remisePourcentage,
|
||||||
|
remiseMontant: remiseMontant ?? this.remiseMontant,
|
||||||
|
prixApresRemise: prixApresRemise ?? this.prixApresRemise,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GETTERS QUI RÉSOLVENT LE PROBLÈME "aUneRemise" INTROUVABLE
|
||||||
|
double get prixFinalUnitaire {
|
||||||
|
return prixApresRemise ?? prixUnitaire;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get sousTotalAvecRemise {
|
||||||
|
return quantite * prixFinalUnitaire;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get aUneRemise {
|
||||||
|
return remisePourcentage != null || remiseMontant != null || prixApresRemise != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get montantRemise {
|
||||||
|
if (prixApresRemise != null) {
|
||||||
|
return (prixUnitaire - prixApresRemise!) * quantite;
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
258
lib/Services/PermissionCacheService.dart
Normal file
258
lib/Services/PermissionCacheService.dart
Normal file
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -35,7 +35,7 @@ class AppDatabase {
|
|||||||
|
|
||||||
Future<void> initDatabase() async {
|
Future<void> initDatabase() async {
|
||||||
_connection = await _initDB();
|
_connection = await _initDB();
|
||||||
await _createDB();
|
// await _createDB();
|
||||||
|
|
||||||
// Effectuer la migration pour les bases existantes
|
// Effectuer la migration pour les bases existantes
|
||||||
await migrateDatabaseForDiscountAndGift();
|
await migrateDatabaseForDiscountAndGift();
|
||||||
@ -70,186 +70,176 @@ class AppDatabase {
|
|||||||
|
|
||||||
// Méthode mise à jour pour créer les tables avec les nouvelles colonnes
|
// Méthode mise à jour pour créer les tables avec les nouvelles colonnes
|
||||||
Future<void> _createDB() async {
|
Future<void> _createDB() async {
|
||||||
final db = await database;
|
// final db = await database;
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
// Table roles
|
// // Table roles
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS roles (
|
// CREATE TABLE IF NOT EXISTS roles (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
designation VARCHAR(255) NOT NULL UNIQUE
|
// designation VARCHAR(255) NOT NULL UNIQUE
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table permissions
|
// // Table permissions
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS permissions (
|
// CREATE TABLE IF NOT EXISTS permissions (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL UNIQUE
|
// name VARCHAR(255) NOT NULL UNIQUE
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table menu
|
// // Table menu
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS menu (
|
// CREATE TABLE IF NOT EXISTS menu (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
// name VARCHAR(255) NOT NULL,
|
||||||
route VARCHAR(255) NOT NULL
|
// route VARCHAR(255) NOT NULL
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table role_permissions
|
// // Table role_permissions
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
// CREATE TABLE IF NOT EXISTS role_permissions (
|
||||||
role_id INT,
|
// role_id INT,
|
||||||
permission_id INT,
|
// permission_id INT,
|
||||||
PRIMARY KEY (role_id, permission_id),
|
// PRIMARY KEY (role_id, permission_id),
|
||||||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
// FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
// FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table role_menu_permissions
|
// // Table role_menu_permissions
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS role_menu_permissions (
|
// CREATE TABLE IF NOT EXISTS role_menu_permissions (
|
||||||
role_id INT,
|
// role_id INT,
|
||||||
menu_id INT,
|
// menu_id INT,
|
||||||
permission_id INT,
|
// permission_id INT,
|
||||||
PRIMARY KEY (role_id, menu_id, permission_id),
|
// PRIMARY KEY (role_id, menu_id, permission_id),
|
||||||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
// FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE,
|
// FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
// FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table points_de_vente
|
// // Table points_de_vente
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS points_de_vente (
|
// CREATE TABLE IF NOT EXISTS points_de_vente (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
nom VARCHAR(255) NOT NULL UNIQUE
|
// nom VARCHAR(255) NOT NULL UNIQUE
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table users
|
// // Table users
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
// CREATE TABLE IF NOT EXISTS users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
// name VARCHAR(255) NOT NULL,
|
||||||
lastname VARCHAR(255) NOT NULL,
|
// lastname VARCHAR(255) NOT NULL,
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
// email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
password VARCHAR(255) NOT NULL,
|
// password VARCHAR(255) NOT NULL,
|
||||||
username VARCHAR(255) NOT NULL UNIQUE,
|
// username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
role_id INT NOT NULL,
|
// role_id INT NOT NULL,
|
||||||
point_de_vente_id INT,
|
// point_de_vente_id INT,
|
||||||
FOREIGN KEY (role_id) REFERENCES roles(id),
|
// FOREIGN KEY (role_id) REFERENCES roles(id),
|
||||||
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
|
// FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table products
|
// // Table products
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS products (
|
// CREATE TABLE IF NOT EXISTS products (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
// name VARCHAR(255) NOT NULL,
|
||||||
price DECIMAL(10,2) NOT NULL,
|
// price DECIMAL(10,2) NOT NULL,
|
||||||
image VARCHAR(2000),
|
// image VARCHAR(2000),
|
||||||
category VARCHAR(255) NOT NULL,
|
// category VARCHAR(255) NOT NULL,
|
||||||
stock INT NOT NULL DEFAULT 0,
|
// stock INT NOT NULL DEFAULT 0,
|
||||||
description VARCHAR(1000),
|
// description VARCHAR(1000),
|
||||||
qrCode VARCHAR(500),
|
// qrCode VARCHAR(500),
|
||||||
reference VARCHAR(255),
|
// reference VARCHAR(255),
|
||||||
point_de_vente_id INT,
|
// point_de_vente_id INT,
|
||||||
marque VARCHAR(255),
|
// marque VARCHAR(255),
|
||||||
ram VARCHAR(100),
|
// ram VARCHAR(100),
|
||||||
memoire_interne VARCHAR(100),
|
// memoire_interne VARCHAR(100),
|
||||||
imei VARCHAR(255) UNIQUE,
|
// imei VARCHAR(255) UNIQUE,
|
||||||
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id),
|
// FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id),
|
||||||
INDEX idx_products_category (category),
|
// INDEX idx_products_category (category),
|
||||||
INDEX idx_products_reference (reference),
|
// INDEX idx_products_reference (reference),
|
||||||
INDEX idx_products_imei (imei)
|
// INDEX idx_products_imei (imei)
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table clients
|
// // Table clients
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS clients (
|
// CREATE TABLE IF NOT EXISTS clients (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
nom VARCHAR(255) NOT NULL,
|
// nom VARCHAR(255) NOT NULL,
|
||||||
prenom VARCHAR(255) NOT NULL,
|
// prenom VARCHAR(255) NOT NULL,
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
// email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
telephone VARCHAR(255) NOT NULL,
|
// telephone VARCHAR(255) NOT NULL,
|
||||||
adresse VARCHAR(500),
|
// adresse VARCHAR(500),
|
||||||
dateCreation DATETIME NOT NULL,
|
// dateCreation DATETIME NOT NULL,
|
||||||
actif TINYINT(1) NOT NULL DEFAULT 1,
|
// actif TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
INDEX idx_clients_email (email),
|
// INDEX idx_clients_email (email),
|
||||||
INDEX idx_clients_telephone (telephone)
|
// INDEX idx_clients_telephone (telephone)
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table commandes MISE À JOUR avec les champs de remise
|
// // Table commandes MISE À JOUR avec les champs de remise
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS commandes (
|
// CREATE TABLE IF NOT EXISTS commandes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
clientId INT NOT NULL,
|
// clientId INT NOT NULL,
|
||||||
dateCommande DATETIME NOT NULL,
|
// dateCommande DATETIME NOT NULL,
|
||||||
statut INT NOT NULL DEFAULT 0,
|
// statut INT NOT NULL DEFAULT 0,
|
||||||
montantTotal DECIMAL(10,2) NOT NULL,
|
// montantTotal DECIMAL(10,2) NOT NULL,
|
||||||
notes VARCHAR(1000),
|
// notes VARCHAR(1000),
|
||||||
dateLivraison DATETIME,
|
// dateLivraison DATETIME,
|
||||||
commandeurId INT,
|
// commandeurId INT,
|
||||||
validateurId INT,
|
// validateurId INT,
|
||||||
remisePourcentage DECIMAL(5,2) NULL,
|
// remisePourcentage DECIMAL(5,2) NULL,
|
||||||
remiseMontant DECIMAL(10,2) NULL,
|
// remiseMontant DECIMAL(10,2) NULL,
|
||||||
montantApresRemise DECIMAL(10,2) NULL,
|
// montantApresRemise DECIMAL(10,2) NULL,
|
||||||
FOREIGN KEY (commandeurId) REFERENCES users(id),
|
// FOREIGN KEY (commandeurId) REFERENCES users(id),
|
||||||
FOREIGN KEY (validateurId) REFERENCES users(id),
|
// FOREIGN KEY (validateurId) REFERENCES users(id),
|
||||||
FOREIGN KEY (clientId) REFERENCES clients(id),
|
// FOREIGN KEY (clientId) REFERENCES clients(id),
|
||||||
INDEX idx_commandes_client (clientId),
|
// INDEX idx_commandes_client (clientId),
|
||||||
INDEX idx_commandes_date (dateCommande)
|
// INDEX idx_commandes_date (dateCommande)
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
// Table details_commandes MISE À JOUR avec le champ cadeau
|
// // Table details_commandes MISE À JOUR avec le champ cadeau
|
||||||
await db.query('''
|
// await db.query('''
|
||||||
CREATE TABLE IF NOT EXISTS details_commandes (
|
// CREATE TABLE IF NOT EXISTS details_commandes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
commandeId INT NOT NULL,
|
// commandeId INT NOT NULL,
|
||||||
produitId INT NOT NULL,
|
// produitId INT NOT NULL,
|
||||||
quantite INT NOT NULL,
|
// quantite INT NOT NULL,
|
||||||
prixUnitaire DECIMAL(10,2) NOT NULL,
|
// prixUnitaire DECIMAL(10,2) NOT NULL,
|
||||||
sousTotal DECIMAL(10,2) NOT NULL,
|
// sousTotal DECIMAL(10,2) NOT NULL,
|
||||||
estCadeau TINYINT(1) DEFAULT 0,
|
// estCadeau TINYINT(1) DEFAULT 0,
|
||||||
FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE,
|
// FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (produitId) REFERENCES products(id),
|
// FOREIGN KEY (produitId) REFERENCES products(id),
|
||||||
INDEX idx_details_commande (commandeId)
|
// INDEX idx_details_commande (commandeId)
|
||||||
) ENGINE=InnoDB
|
// ) ENGINE=InnoDB
|
||||||
''');
|
// ''');
|
||||||
|
|
||||||
print("Tables créées avec succès avec les nouveaux champs !");
|
// print("Tables créées avec succès avec les nouveaux champs !");
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
print("Erreur lors de la création des tables: $e");
|
// print("Erreur lors de la création des tables: $e");
|
||||||
rethrow;
|
// rethrow;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MÉTHODES D'INSERTION PAR DÉFAUT ---
|
// --- MÉTHODES D'INSERTION PAR DÉFAUT ---
|
||||||
|
|
||||||
|
//
|
||||||
Future<void> insertDefaultPermissions() async {
|
Future<void> insertDefaultPermissions() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final existing = await db.query('SELECT COUNT(*) as count FROM permissions');
|
// Vérifier et ajouter uniquement les nouvelles permissions si elles n'existent pas
|
||||||
final count = existing.first['count'] as int;
|
|
||||||
|
|
||||||
if (count == 0) {
|
|
||||||
final permissions = ['view', 'create', 'update', 'delete', 'admin', 'manage', 'read'];
|
|
||||||
|
|
||||||
for (String permission in permissions) {
|
|
||||||
await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]);
|
|
||||||
}
|
|
||||||
print("Permissions par défaut insérées");
|
|
||||||
} else {
|
|
||||||
// Vérifier et ajouter les nouvelles permissions si elles n'existent pas
|
|
||||||
final newPermissions = ['manage', 'read'];
|
final newPermissions = ['manage', 'read'];
|
||||||
for (var permission in newPermissions) {
|
for (var permission in newPermissions) {
|
||||||
final existingPermission = await db.query(
|
final existingPermission = await db.query(
|
||||||
@ -262,50 +252,21 @@ Future<void> _createDB() async {
|
|||||||
print("Permission ajoutée: $permission");
|
print("Permission ajoutée: $permission");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Erreur insertDefaultPermissions: $e");
|
print("Erreur insertDefaultPermissions: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
Future<void> insertDefaultMenus() async {
|
Future<void> insertDefaultMenus() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final existingMenus = await db.query('SELECT COUNT(*) as count FROM menu');
|
await _addMissingMenus(db); // Seulement ajouter les menus manquants
|
||||||
final count = existingMenus.first['count'] as int;
|
|
||||||
|
|
||||||
if (count == 0) {
|
|
||||||
final menus = [
|
|
||||||
{'name': 'Accueil', 'route': '/accueil'},
|
|
||||||
{'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'},
|
|
||||||
{'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'},
|
|
||||||
{'name': 'Ajouter un produit', 'route': '/ajouter-produit'},
|
|
||||||
{'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'},
|
|
||||||
{'name': 'Bilan', 'route': '/bilan'},
|
|
||||||
{'name': 'Gérer les rôles', 'route': '/gerer-roles'},
|
|
||||||
{'name': 'Gestion de stock', 'route': '/gestion-stock'},
|
|
||||||
{'name': 'Historique', 'route': '/historique'},
|
|
||||||
{'name': 'Déconnexion', 'route': '/deconnexion'},
|
|
||||||
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
|
|
||||||
{'name': 'Gérer les commandes', 'route': '/gerer-commandes'},
|
|
||||||
{'name': 'Points de vente', 'route': '/points-de-vente'},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (var menu in menus) {
|
|
||||||
await db.query(
|
|
||||||
'INSERT INTO menu (name, route) VALUES (?, ?)',
|
|
||||||
[menu['name'], menu['route']]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
print("Menus par défaut insérés");
|
|
||||||
} else {
|
|
||||||
await _addMissingMenus(db);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Erreur insertDefaultMenus: $e");
|
print("Erreur insertDefaultMenus: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> insertDefaultRoles() async {
|
Future<void> insertDefaultRoles() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
@ -741,6 +702,30 @@ Future<void> _createDB() async {
|
|||||||
|
|
||||||
// --- MÉTHODES UTILITAIRES ---
|
// --- MÉTHODES UTILITAIRES ---
|
||||||
|
|
||||||
|
// Future<void> _addMissingMenus(MySqlConnection db) async {
|
||||||
|
// final menusToAdd = [
|
||||||
|
// {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
|
||||||
|
// {'name': 'Gérer les commandes', 'route': '/gerer-commandes'},
|
||||||
|
// {'name': 'Points de vente', 'route': '/points-de-vente'},
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// for (var menu in menusToAdd) {
|
||||||
|
// final existing = await db.query(
|
||||||
|
// 'SELECT COUNT(*) as count FROM menu WHERE route = ?',
|
||||||
|
// [menu['route']]
|
||||||
|
// );
|
||||||
|
// final count = existing.first['count'] as int;
|
||||||
|
|
||||||
|
// if (count == 0) {
|
||||||
|
// await db.query(
|
||||||
|
// 'INSERT INTO menu (name, route) VALUES (?, ?)',
|
||||||
|
// [menu['name'], menu['route']]
|
||||||
|
// );
|
||||||
|
// print("Menu ajouté: ${menu['name']}");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
Future<void> _addMissingMenus(MySqlConnection db) async {
|
Future<void> _addMissingMenus(MySqlConnection db) async {
|
||||||
final menusToAdd = [
|
final menusToAdd = [
|
||||||
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
|
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
|
||||||
@ -763,7 +748,7 @@ Future<void> _createDB() async {
|
|||||||
print("Menu ajouté: ${menu['name']}");
|
print("Menu ajouté: ${menu['name']}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateExistingRolePermissions(MySqlConnection db) async {
|
Future<void> _updateExistingRolePermissions(MySqlConnection db) async {
|
||||||
final superAdminRole = await db.query('SELECT id FROM roles WHERE designation = ?', ['Super Admin']);
|
final superAdminRole = await db.query('SELECT id FROM roles WHERE designation = ?', ['Super Admin']);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||||
|
import 'package:youmazgestion/Services/PermissionCacheService.dart'; // Nouveau import
|
||||||
import 'package:youmazgestion/Views/Dashboard.dart';
|
import 'package:youmazgestion/Views/Dashboard.dart';
|
||||||
import 'package:youmazgestion/Views/mobilepage.dart';
|
import 'package:youmazgestion/Views/mobilepage.dart';
|
||||||
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
||||||
@ -19,9 +20,12 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
late TextEditingController _usernameController;
|
late TextEditingController _usernameController;
|
||||||
late TextEditingController _passwordController;
|
late TextEditingController _passwordController;
|
||||||
final UserController userController = Get.put(UserController());
|
final UserController userController = Get.put(UserController());
|
||||||
|
final PermissionCacheService _cacheService = PermissionCacheService.instance; // Nouveau
|
||||||
|
|
||||||
bool _isErrorVisible = false;
|
bool _isErrorVisible = false;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
|
String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
|
||||||
|
String _loadingMessage = 'Connexion en cours...'; // Nouveau
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -51,22 +55,47 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveUserData(Users user, String role, int userId) async {
|
// /// ✅ OPTIMISÉ: Sauvegarde avec préchargement des permissions
|
||||||
|
// 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 {
|
try {
|
||||||
userController.setUserWithCredentials(user, role, userId);
|
setState(() {
|
||||||
|
_loadingMessage = 'Préparation du menu...';
|
||||||
|
});
|
||||||
|
|
||||||
if (user.pointDeVenteId != null) {
|
// Lancer le préchargement en parallèle avec les autres tâches
|
||||||
await userController.loadPointDeVenteDesignation();
|
final permissionFuture = _cacheService.preloadUserData(username);
|
||||||
}
|
|
||||||
|
|
||||||
print('Utilisateur sauvegardé avec point de vente: ${userController.pointDeVenteDesignation}');
|
// Attendre maximum 2 secondes pour les permissions
|
||||||
} catch (error) {
|
await Future.any([
|
||||||
print('Erreur lors de la sauvegarde: $error');
|
permissionFuture,
|
||||||
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
|
Future.delayed(const Duration(seconds: 2))
|
||||||
}
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
void _login() async {
|
print('✅ Permissions préparées (ou timeout)');
|
||||||
|
} catch (e) {
|
||||||
|
print('⚠️ Erreur préchargement permissions: $e');
|
||||||
|
// Continuer même en cas d'erreur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ OPTIMISÉ: Connexion avec préchargement parallèle
|
||||||
|
void _login() async {
|
||||||
if (_isLoading) return;
|
if (_isLoading) return;
|
||||||
|
|
||||||
final String username = _usernameController.text.trim();
|
final String username = _usernameController.text.trim();
|
||||||
@ -74,8 +103,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
if (username.isEmpty || password.isEmpty) {
|
if (username.isEmpty || password.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage =
|
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
|
||||||
'Veuillez saisir le nom d\'utilisateur et le mot de passe';
|
|
||||||
_isErrorVisible = true;
|
_isErrorVisible = true;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -84,43 +112,72 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_isErrorVisible = false;
|
_isErrorVisible = false;
|
||||||
|
_loadingMessage = 'Connexion...';
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('Tentative de connexion pour: $username');
|
print('🔐 Tentative de connexion pour: $username');
|
||||||
final dbInstance = AppDatabase.instance;
|
final dbInstance = AppDatabase.instance;
|
||||||
|
|
||||||
|
// 1. Vérification rapide de la base
|
||||||
|
setState(() {
|
||||||
|
_loadingMessage = 'Vérification...';
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final userCount = await dbInstance.getUserCount();
|
final userCount = await dbInstance.getUserCount();
|
||||||
print('Base de données accessible, $userCount utilisateurs trouvés');
|
print('✅ Base accessible, $userCount utilisateurs');
|
||||||
} catch (dbError) {
|
} 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);
|
bool isValidUser = await dbInstance.verifyUser(username, password);
|
||||||
|
|
||||||
if (isValidUser) {
|
if (isValidUser) {
|
||||||
Users user = await dbInstance.getUser(username);
|
setState(() {
|
||||||
print('Utilisateur récupéré: ${user.username}');
|
_loadingMessage = 'Chargement du profil...';
|
||||||
|
});
|
||||||
|
|
||||||
Map<String, dynamic>? userCredentials =
|
// 3. Récupération parallèle des données
|
||||||
await dbInstance.getUserCredentials(username, password);
|
final futures = await Future.wait([
|
||||||
|
dbInstance.getUser(username),
|
||||||
|
dbInstance.getUserCredentials(username, password),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final user = futures[0] as Users;
|
||||||
|
final userCredentials = futures[1] as Map<String, dynamic>?;
|
||||||
|
|
||||||
if (userCredentials != null) {
|
if (userCredentials != null) {
|
||||||
print('Connexion réussie pour: ${user.username}');
|
print('✅ Connexion réussie pour: ${user.username}');
|
||||||
print('Rôle: ${userCredentials['role']}');
|
print(' Rôle: ${userCredentials['role']}');
|
||||||
print('ID: ${userCredentials['id']}');
|
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_loadingMessage = 'Préparation...';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Sauvegarde des données utilisateur
|
||||||
await saveUserData(
|
await saveUserData(
|
||||||
user,
|
user,
|
||||||
userCredentials['role'] as String,
|
userCredentials['role'] as String,
|
||||||
userCredentials['id'] as int,
|
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 (mounted) {
|
||||||
if (userCredentials['role'] == 'commercial') {
|
if (userCredentials['role'] == 'commercial') {
|
||||||
// Redirection vers MainLayout pour les commerciaux
|
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const MainLayout()),
|
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 {
|
} else {
|
||||||
throw Exception('Erreur lors de la récupération des credentials');
|
throw Exception('Erreur lors de la récupération des credentials');
|
||||||
}
|
}
|
||||||
@ -147,9 +208,32 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
_isErrorVisible = true;
|
_isErrorVisible = true;
|
||||||
});
|
});
|
||||||
} finally {
|
} 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -169,8 +253,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
width: MediaQuery.of(context).size.width < 500
|
width: MediaQuery.of(context).size.width < 500
|
||||||
? double.infinity
|
? double.infinity
|
||||||
: 400,
|
: 400,
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
||||||
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: cardColor.withOpacity(0.98),
|
color: cardColor.withOpacity(0.98),
|
||||||
borderRadius: BorderRadius.circular(30.0),
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
@ -186,6 +269,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
// Header
|
||||||
Center(
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -219,6 +303,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Username Field
|
||||||
TextField(
|
TextField(
|
||||||
controller: _usernameController,
|
controller: _usernameController,
|
||||||
enabled: !_isLoading,
|
enabled: !_isLoading,
|
||||||
@ -241,6 +327,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18.0),
|
const SizedBox(height: 18.0),
|
||||||
|
|
||||||
|
// Password Field
|
||||||
TextField(
|
TextField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
enabled: !_isLoading,
|
enabled: !_isLoading,
|
||||||
@ -263,19 +351,104 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
onSubmitted: (_) => _login(),
|
onSubmitted: (_) => _login(),
|
||||||
),
|
),
|
||||||
if (_isErrorVisible) ...[
|
|
||||||
const SizedBox(height: 12.0),
|
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(
|
Text(
|
||||||
_errorMessage,
|
"Le menu se chargera en arrière-plan",
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.redAccent,
|
color: Colors.grey.shade600,
|
||||||
fontSize: 15,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Error Message
|
||||||
|
if (_isErrorVisible) ...[
|
||||||
|
const SizedBox(height: 12.0),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.red.withOpacity(0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, color: Colors.red, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_errorMessage,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
const SizedBox(height: 26.0),
|
const SizedBox(height: 26.0),
|
||||||
|
|
||||||
|
// Login Button
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _isLoading ? null : _login,
|
onPressed: _isLoading ? null : _login,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@ -289,13 +462,27 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
minimumSize: const Size(double.infinity, 52),
|
minimumSize: const Size(double.infinity, 52),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const SizedBox(
|
? Row(
|
||||||
height: 24,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
width: 24,
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
color: Colors.white,
|
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(
|
: const Text(
|
||||||
'Se connecter',
|
'Se connecter',
|
||||||
@ -307,16 +494,23 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Option debug, à enlever en prod
|
|
||||||
if (_isErrorVisible) ...[
|
// Debug Button (à enlever en production)
|
||||||
|
if (_isErrorVisible && !_isLoading) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
final count =
|
final count = await AppDatabase.instance.getUserCount();
|
||||||
await AppDatabase.instance.getUserCount();
|
final stats = _cacheService.getCacheStats();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('$count utilisateurs trouvés')),
|
content: Text(
|
||||||
|
'BDD: $count utilisateurs\n'
|
||||||
|
'Cache: ${stats['users_cached']} utilisateurs en cache',
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -324,7 +518,13 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Debug: Vérifier BDD'),
|
child: Text(
|
||||||
|
'Debug: Vérifier BDD & Cache',
|
||||||
|
style: TextStyle(
|
||||||
|
color: primaryColor.withOpacity(0.6),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||||
import 'package:youmazgestion/Components/QrScan.dart';
|
import 'package:youmazgestion/Components/QrScan.dart';
|
||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||||
@ -508,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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
floatingActionButton: _buildFloatingCartButton(),
|
floatingActionButton: Column(
|
||||||
drawer: isMobile ? CustomDrawer() : null,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
// Bouton de scan
|
||||||
|
FloatingActionButton(
|
||||||
|
heroTag: "scan",
|
||||||
|
onPressed: _isScanning ? null : _startBarcodeScanning,
|
||||||
|
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
child: _isScanning
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.qr_code_scanner),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Bouton panier existant
|
||||||
|
_buildFloatingCartButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
appBar: CustomAppBar(title: 'Nouvelle commande'),
|
||||||
|
drawer: CustomDrawer(),
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs
|
onTap: _hideAllSuggestions,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Section des filtres - adaptée comme dans HistoriquePage
|
// Section d'information sur le scan (desktop)
|
||||||
|
if (!isMobile)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: _buildScanInfoCard(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Section des filtres
|
||||||
if (!isMobile)
|
if (!isMobile)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: _buildFilterSection(),
|
child: _buildFilterSection(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Sur mobile, bouton pour afficher les filtres dans un modal
|
// Boutons pour mobile
|
||||||
if (isMobile) ...[
|
if (isMobile) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
child: SizedBox(
|
child: Row(
|
||||||
width: double.infinity,
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.filter_alt),
|
icon: const Icon(Icons.filter_alt),
|
||||||
label: const Text('Filtres produits'),
|
label: const Text('Filtres'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
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,
|
||||||
),
|
),
|
||||||
// Compteur de résultats visible en haut sur mobile
|
)
|
||||||
|
: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: Text(_isScanning ? 'Scan...' : 'Scan'),
|
||||||
|
onPressed: _isScanning ? null : _startBarcodeScanning,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(double.infinity, 48),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Compteur de résultats
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -588,6 +1055,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _buildSuggestionsList({required bool isNom}) {
|
Widget _buildSuggestionsList({required bool isNom}) {
|
||||||
if (_clientSuggestions.isEmpty) return const SizedBox();
|
if (_clientSuggestions.isEmpty) return const SizedBox();
|
||||||
|
|
||||||
@ -1727,21 +2195,19 @@ void _fillFormWithClient(Client client) {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Nettoyer les suggestions
|
_qrController?.dispose();
|
||||||
_hideAllSuggestions();
|
|
||||||
|
|
||||||
// Disposer les contrôleurs
|
// Vos disposals existants...
|
||||||
|
_hideAllSuggestions();
|
||||||
_nomController.dispose();
|
_nomController.dispose();
|
||||||
_prenomController.dispose();
|
_prenomController.dispose();
|
||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_telephoneController.dispose();
|
_telephoneController.dispose();
|
||||||
_adresseController.dispose();
|
_adresseController.dispose();
|
||||||
|
|
||||||
// Disposal des contrôleurs de filtre
|
|
||||||
_searchNameController.dispose();
|
_searchNameController.dispose();
|
||||||
_searchImeiController.dispose();
|
_searchImeiController.dispose();
|
||||||
_searchReferenceController.dispose();
|
_searchReferenceController.dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||||
|
|
||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||||
import 'package:youmazgestion/Models/client.dart';
|
import 'package:youmazgestion/Models/client.dart';
|
||||||
@ -47,8 +49,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
List<Client> _clientSuggestions = [];
|
List<Client> _clientSuggestions = [];
|
||||||
bool _showNomSuggestions = false;
|
bool _showNomSuggestions = false;
|
||||||
bool _showTelephoneSuggestions = 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
|
@override
|
||||||
void initState() {
|
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
|
// Méthode pour vider complètement le formulaire et le panier
|
||||||
void _clearFormAndCart() {
|
void _clearFormAndCart() {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -123,21 +641,8 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
_showNomSuggestions = false;
|
_showNomSuggestions = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showOverlay({required bool isNom}) {
|
|
||||||
// Utiliser une approche plus simple avec setState
|
|
||||||
setState(() {
|
|
||||||
_clientSuggestions = _clientSuggestions;
|
|
||||||
if (isNom) {
|
|
||||||
_showNomSuggestions = true;
|
|
||||||
_showTelephoneSuggestions = false;
|
|
||||||
} else {
|
|
||||||
_showTelephoneSuggestions = true;
|
|
||||||
_showNomSuggestions = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _fillClientForm(Client client) {
|
void _fillClientForm(Client client) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -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() {
|
Widget _buildFloatingCartButton() {
|
||||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||||
final cartItemCount = _quantites.values.where((q) => q > 0).length;
|
final cartItemCount = _quantites.values.where((q) => q > 0).length;
|
||||||
@ -576,6 +942,8 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void _showClientFormDialog() {
|
void _showClientFormDialog() {
|
||||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
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 pour créer un TextFormField avec une clé
|
||||||
Widget _buildTextFormFieldWithKey({
|
Widget _buildTextFormFieldWithKey({
|
||||||
required GlobalKey key,
|
required GlobalKey key,
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
required String label,
|
required String label,
|
||||||
@ -901,7 +1269,7 @@ Widget _buildTextFormFieldWithKey({
|
|||||||
int maxLines = 1,
|
int maxLines = 1,
|
||||||
String? Function(String?)? validator,
|
String? Function(String?)? validator,
|
||||||
void Function(String)? onChanged,
|
void Function(String)? onChanged,
|
||||||
}) {
|
}) {
|
||||||
return Container(
|
return Container(
|
||||||
key: key,
|
key: key,
|
||||||
child: _buildTextFormField(
|
child: _buildTextFormField(
|
||||||
@ -913,9 +1281,10 @@ Widget _buildTextFormFieldWithKey({
|
|||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget pour l'overlay des suggestions
|
// Widget pour l'overlay des suggestions
|
||||||
|
// Widget pour l'overlay des suggestions
|
||||||
Widget _buildSuggestionOverlay({
|
Widget _buildSuggestionOverlay({
|
||||||
required GlobalKey fieldKey,
|
required GlobalKey fieldKey,
|
||||||
required List<Client> suggestions,
|
required List<Client> suggestions,
|
||||||
@ -1020,10 +1389,10 @@ Widget _buildSuggestionOverlay({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour remplir le formulaire avec les données du client
|
// Méthode pour remplir le formulaire avec les données du client
|
||||||
void _fillFormWithClient(Client client) {
|
void _fillFormWithClient(Client client) {
|
||||||
_nomController.text = client.nom;
|
_nomController.text = client.nom;
|
||||||
_prenomController.text = client.prenom;
|
_prenomController.text = client.prenom;
|
||||||
_emailController.text = client.email;
|
_emailController.text = client.email;
|
||||||
@ -1038,7 +1407,7 @@ void _fillFormWithClient(Client client) {
|
|||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextFormField({
|
Widget _buildTextFormField({
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
@ -1638,20 +2007,163 @@ void _fillFormWithClient(Client client) {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Nettoyer les suggestions
|
_qrController?.dispose();
|
||||||
_hideAllSuggestions();
|
|
||||||
|
|
||||||
// Disposer les contrôleurs
|
// Vos disposals existants...
|
||||||
|
_hideAllSuggestions();
|
||||||
_nomController.dispose();
|
_nomController.dispose();
|
||||||
_prenomController.dispose();
|
_prenomController.dispose();
|
||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_telephoneController.dispose();
|
_telephoneController.dispose();
|
||||||
_adresseController.dispose();
|
_adresseController.dispose();
|
||||||
|
|
||||||
// Disposal des contrôleurs de filtre
|
|
||||||
_searchNameController.dispose();
|
_searchNameController.dispose();
|
||||||
_searchImeiController.dispose();
|
_searchImeiController.dispose();
|
||||||
_searchReferenceController.dispose();
|
_searchReferenceController.dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}}
|
}
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,12 +1,12 @@
|
|||||||
// Config/database_config.dart - Version améliorée
|
// Config/database_config.dart - Version améliorée
|
||||||
class DatabaseConfig {
|
class DatabaseConfig {
|
||||||
static const String host = 'database.c4m.mg';
|
static const String host = '172.20.10.5';
|
||||||
static const int port = 3306;
|
static const int port = 3306;
|
||||||
static const String username = 'guycom';
|
static const String username = 'root';
|
||||||
static const String password = '3iV59wjRdbuXAPR';
|
static const String? password = null;
|
||||||
static const String database = 'guycom';
|
static const String database = 'guycom_databse_v1';
|
||||||
|
|
||||||
static const String prodHost = 'database.c4m.mg';
|
static const String prodHost = '185.70.105.157';
|
||||||
static const String prodUsername = 'guycom';
|
static const String prodUsername = 'guycom';
|
||||||
static const String prodPassword = '3iV59wjRdbuXAPR';
|
static const String prodPassword = '3iV59wjRdbuXAPR';
|
||||||
static const String prodDatabase = 'guycom';
|
static const String prodDatabase = 'guycom';
|
||||||
@ -17,7 +17,7 @@ class DatabaseConfig {
|
|||||||
static const int maxConnections = 10;
|
static const int maxConnections = 10;
|
||||||
static const int minConnections = 2;
|
static const int minConnections = 2;
|
||||||
|
|
||||||
static bool get isDevelopment => false;
|
static bool get isDevelopment => true;
|
||||||
|
|
||||||
static Map<String, dynamic> getConfig() {
|
static Map<String, dynamic> getConfig() {
|
||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:youmazgestion/Models/users.dart';
|
import 'package:youmazgestion/Models/users.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||||
//import 'package:youmazgestion/Services/app_database.dart';
|
import 'package:youmazgestion/Services/PermissionCacheService.dart';
|
||||||
|
|
||||||
class UserController extends GetxController {
|
class UserController extends GetxController {
|
||||||
final _username = ''.obs;
|
final _username = ''.obs;
|
||||||
@ -11,10 +11,14 @@ class UserController extends GetxController {
|
|||||||
final _name = ''.obs;
|
final _name = ''.obs;
|
||||||
final _lastname = ''.obs;
|
final _lastname = ''.obs;
|
||||||
final _password = ''.obs;
|
final _password = ''.obs;
|
||||||
final _userId = 0.obs; // ✅ Ajout de l'ID utilisateur
|
final _userId = 0.obs;
|
||||||
final _pointDeVenteId = 0.obs;
|
final _pointDeVenteId = 0.obs;
|
||||||
final _pointDeVenteDesignation = ''.obs;
|
final _pointDeVenteDesignation = ''.obs;
|
||||||
|
|
||||||
|
// Cache service
|
||||||
|
final PermissionCacheService _cacheService = PermissionCacheService.instance;
|
||||||
|
|
||||||
|
// Getters
|
||||||
String get username => _username.value;
|
String get username => _username.value;
|
||||||
String get email => _email.value;
|
String get email => _email.value;
|
||||||
String get role => _role.value;
|
String get role => _role.value;
|
||||||
@ -28,10 +32,10 @@ class UserController extends GetxController {
|
|||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
loadUserData(); // Charger les données au démarrage
|
loadUserData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ CORRECTION : Charger les données complètes depuis SharedPreferences ET la base de données
|
/// ✅ SIMPLIFIÉ: Charge les données utilisateur sans cache persistant
|
||||||
Future<void> loadUserData() async {
|
Future<void> loadUserData() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@ -56,13 +60,15 @@ class UserController extends GetxController {
|
|||||||
_pointDeVenteId.value = storedPointDeVenteId;
|
_pointDeVenteId.value = storedPointDeVenteId;
|
||||||
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
||||||
|
|
||||||
// Si la désignation n'est pas sauvegardée, on peut la récupérer
|
|
||||||
if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) {
|
if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) {
|
||||||
await loadPointDeVenteDesignation();
|
await loadPointDeVenteDesignation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Précharger les permissions en arrière-plan (non bloquant)
|
||||||
|
_preloadPermissionsInBackground();
|
||||||
|
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
// Fallback
|
print("❌ Erreur BDD, utilisation du fallback: $dbError");
|
||||||
_username.value = storedUsername;
|
_username.value = storedUsername;
|
||||||
_email.value = prefs.getString('email') ?? '';
|
_email.value = prefs.getString('email') ?? '';
|
||||||
_role.value = storedRole;
|
_role.value = storedRole;
|
||||||
@ -71,49 +77,66 @@ class UserController extends GetxController {
|
|||||||
_userId.value = storedUserId;
|
_userId.value = storedUserId;
|
||||||
_pointDeVenteId.value = storedPointDeVenteId;
|
_pointDeVenteId.value = storedPointDeVenteId;
|
||||||
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
||||||
|
|
||||||
|
// Précharger quand même
|
||||||
|
_preloadPermissionsInBackground();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Erreur lors du chargement des données utilisateur: $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;
|
if (_pointDeVenteId.value <= 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value);
|
final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value);
|
||||||
if (pointDeVente != null) {
|
if (pointDeVente != null) {
|
||||||
_pointDeVenteDesignation.value = pointDeVente['designation'] as String;
|
_pointDeVenteDesignation.value = pointDeVente['nom'] as String;
|
||||||
await saveUserData(); // Sauvegarder la désignation
|
await saveUserData();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Erreur lors du chargement de la désignation du point de vente: $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) {
|
void setUserWithCredentials(Users user, String role, int userId) {
|
||||||
_username.value = user.username;
|
_username.value = user.username;
|
||||||
_email.value = user.email;
|
_email.value = user.email;
|
||||||
_role.value = role; // Rôle depuis les credentials
|
_role.value = role;
|
||||||
_name.value = user.name;
|
_name.value = user.name;
|
||||||
_lastname.value = user.lastName;
|
_lastname.value = user.lastName;
|
||||||
_password.value = user.password;
|
_password.value = user.password;
|
||||||
_userId.value = userId; // ID depuis les credentials
|
_userId.value = userId;
|
||||||
_pointDeVenteId.value = user.pointDeVenteId ?? 0;
|
_pointDeVenteId.value = user.pointDeVenteId ?? 0;
|
||||||
|
|
||||||
print("✅ Utilisateur mis à jour avec credentials:");
|
print("✅ Utilisateur mis à jour avec credentials:");
|
||||||
print(" Username: ${_username.value}");
|
print(" Username: ${_username.value}");
|
||||||
print(" Name: ${_name.value}");
|
|
||||||
print(" Email: ${_email.value}");
|
|
||||||
print(" Role: ${_role.value}");
|
print(" Role: ${_role.value}");
|
||||||
print(" UserID: ${_userId.value}");
|
print(" UserID: ${_userId.value}");
|
||||||
|
|
||||||
// Sauvegarder dans SharedPreferences
|
|
||||||
saveUserData();
|
saveUserData();
|
||||||
|
|
||||||
|
// ✅ Précharger immédiatement les permissions après connexion
|
||||||
|
_preloadPermissionsInBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ MÉTHODE EXISTANTE AMÉLIORÉE
|
|
||||||
void setUser(Users user) {
|
void setUser(Users user) {
|
||||||
_username.value = user.username;
|
_username.value = user.username;
|
||||||
_email.value = user.email;
|
_email.value = user.email;
|
||||||
@ -121,17 +144,11 @@ Future<void> loadPointDeVenteDesignation() async {
|
|||||||
_name.value = user.name;
|
_name.value = user.name;
|
||||||
_lastname.value = user.lastName;
|
_lastname.value = user.lastName;
|
||||||
_password.value = user.password;
|
_password.value = user.password;
|
||||||
// Note: _userId reste inchangé si pas fourni
|
|
||||||
|
|
||||||
print("✅ Utilisateur mis à jour (méthode legacy):");
|
|
||||||
print(" Username: ${_username.value}");
|
|
||||||
print(" Role: ${_role.value}");
|
|
||||||
|
|
||||||
// Sauvegarder dans SharedPreferences
|
|
||||||
saveUserData();
|
saveUserData();
|
||||||
|
_preloadPermissionsInBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ CORRECTION : Sauvegarder TOUTES les données importantes
|
|
||||||
Future<void> saveUserData() async {
|
Future<void> saveUserData() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@ -145,17 +162,21 @@ Future<void> loadPointDeVenteDesignation() async {
|
|||||||
await prefs.setInt('point_de_vente_id', _pointDeVenteId.value);
|
await prefs.setInt('point_de_vente_id', _pointDeVenteId.value);
|
||||||
await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value);
|
await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value);
|
||||||
|
|
||||||
print("✅ Données sauvegardées avec succès dans SharedPreferences");
|
print("✅ Données sauvegardées avec succès");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Erreur lors de la sauvegarde des données utilisateur: $e');
|
print('❌ Erreur lors de la sauvegarde: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ CORRECTION : Vider TOUTES les données (SharedPreferences + Observables)
|
/// ✅ MODIFIÉ: Vider les données ET le cache de session
|
||||||
Future<void> clearUserData() async {
|
Future<void> clearUserData() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
// ✅ IMPORTANT: Vider le cache de session
|
||||||
|
_cacheService.clearAllCache();
|
||||||
|
|
||||||
|
// Effacer SharedPreferences
|
||||||
await prefs.remove('username');
|
await prefs.remove('username');
|
||||||
await prefs.remove('email');
|
await prefs.remove('email');
|
||||||
await prefs.remove('role');
|
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_id');
|
||||||
await prefs.remove('point_de_vente_designation');
|
await prefs.remove('point_de_vente_designation');
|
||||||
|
|
||||||
|
// Effacer les observables
|
||||||
_username.value = '';
|
_username.value = '';
|
||||||
_email.value = '';
|
_email.value = '';
|
||||||
_role.value = '';
|
_role.value = '';
|
||||||
@ -175,36 +197,54 @@ Future<void> clearUserData() async {
|
|||||||
_pointDeVenteId.value = 0;
|
_pointDeVenteId.value = 0;
|
||||||
_pointDeVenteDesignation.value = '';
|
_pointDeVenteDesignation.value = '';
|
||||||
|
|
||||||
|
print("✅ Données utilisateur et cache de session vidés");
|
||||||
|
|
||||||
} catch (e) {
|
} 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;
|
bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0;
|
||||||
|
|
||||||
// ✅ MÉTHODE UTILITAIRE : Obtenir le nom complet
|
|
||||||
String get fullName => '${_name.value} ${_lastname.value}'.trim();
|
String get fullName => '${_name.value} ${_lastname.value}'.trim();
|
||||||
|
|
||||||
|
/// ✅ OPTIMISÉ: Vérification des permissions depuis le cache de session
|
||||||
Future<bool> hasPermission(String permission, String route) async {
|
Future<bool> hasPermission(String permission, String route) async {
|
||||||
try {
|
try {
|
||||||
if (_username.value.isEmpty) {
|
if (_username.value.isEmpty) {
|
||||||
print('⚠️ Username vide, rechargement des données...');
|
print('⚠️ Username vide, rechargement...');
|
||||||
await loadUserData();
|
await loadUserData();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_username.value.isEmpty) {
|
if (_username.value.isEmpty) {
|
||||||
print('❌ Impossible de vérifier les permissions : utilisateur non connecté');
|
print('❌ Utilisateur non connecté');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await AppDatabase.instance.hasPermission(username, permission, route);
|
// Essayer d'abord le cache
|
||||||
|
if (_cacheService.isLoaded) {
|
||||||
|
return _cacheService.hasPermission(_username.value, permission, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si pas encore chargé, charger et essayer de nouveau
|
||||||
|
print("🔄 Cache non chargé, chargement des permissions...");
|
||||||
|
await _cacheService.loadUserPermissions(_username.value);
|
||||||
|
|
||||||
|
return _cacheService.hasPermission(_username.value, permission, route);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Erreur vérification permission: $e');
|
print('❌ Erreur vérification permission: $e');
|
||||||
return false; // Sécurité : refuser l'accès en cas d'erreur
|
// Fallback vers la méthode originale en cas d'erreur
|
||||||
|
try {
|
||||||
|
return await AppDatabase.instance.hasPermission(_username.value, permission, route);
|
||||||
|
} catch (fallbackError) {
|
||||||
|
print('❌ Erreur fallback permission: $fallbackError');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ✅ Vérification de permissions multiples
|
||||||
Future<bool> hasAnyPermission(List<String> permissionNames, String menuRoute) async {
|
Future<bool> hasAnyPermission(List<String> permissionNames, String menuRoute) async {
|
||||||
for (String permissionName in permissionNames) {
|
for (String permissionName in permissionNames) {
|
||||||
if (await hasPermission(permissionName, menuRoute)) {
|
if (await hasPermission(permissionName, menuRoute)) {
|
||||||
@ -214,18 +254,40 @@ Future<void> clearUserData() async {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ MÉTHODE DEBUG : Afficher l'état actuel
|
/// ✅ Obtenir les menus accessibles depuis le cache
|
||||||
|
List<Map<String, dynamic>> getUserMenus() {
|
||||||
|
if (_username.value.isEmpty || !_cacheService.isLoaded) return [];
|
||||||
|
return _cacheService.getUserMenus(_username.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ Vérifier l'accès à un menu depuis le cache
|
||||||
|
bool hasMenuAccess(String menuRoute) {
|
||||||
|
if (_username.value.isEmpty || !_cacheService.isLoaded) return false;
|
||||||
|
return _cacheService.hasMenuAccess(_username.value, menuRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ Forcer le rechargement des permissions (pour les admins après modification)
|
||||||
|
Future<void> refreshPermissions() async {
|
||||||
|
if (_username.value.isNotEmpty) {
|
||||||
|
await _cacheService.refreshUserPermissions(_username.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ Vérifier si le cache est prêt
|
||||||
|
bool get isCacheReady => _cacheService.isLoaded && _username.value.isNotEmpty;
|
||||||
|
|
||||||
|
/// Debug
|
||||||
void debugPrintUserState() {
|
void debugPrintUserState() {
|
||||||
print("=== ÉTAT UTILISATEUR ===");
|
print("=== ÉTAT UTILISATEUR ===");
|
||||||
print("Username: ${_username.value}");
|
print("Username: ${_username.value}");
|
||||||
print("Name: ${_name.value}");
|
print("Name: ${_name.value}");
|
||||||
print("Lastname: ${_lastname.value}");
|
|
||||||
print("Email: ${_email.value}");
|
|
||||||
print("Role: ${_role.value}");
|
print("Role: ${_role.value}");
|
||||||
print("UserID: ${_userId.value}");
|
print("UserID: ${_userId.value}");
|
||||||
print("PointDeVenteID: ${_pointDeVenteId.value}");
|
|
||||||
print("PointDeVente: ${_pointDeVenteDesignation.value}");
|
|
||||||
print("IsLoggedIn: $isLoggedIn");
|
print("IsLoggedIn: $isLoggedIn");
|
||||||
|
print("Cache Ready: $isCacheReady");
|
||||||
print("========================");
|
print("========================");
|
||||||
}
|
|
||||||
|
// Debug du cache
|
||||||
|
_cacheService.debugPrintCache();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -17,7 +17,7 @@ void main() async {
|
|||||||
|
|
||||||
// Initialiser la base de données MySQL
|
// Initialiser la base de données MySQL
|
||||||
print("Connexion à la base de données MySQL...");
|
print("Connexion à la base de données MySQL...");
|
||||||
await AppDatabase.instance.initDatabase();
|
// await AppDatabase.instance.initDatabase();
|
||||||
print("Base de données initialisée avec succès !");
|
print("Base de données initialisée avec succès !");
|
||||||
|
|
||||||
// Afficher les informations de la base (pour debug)
|
// Afficher les informations de la base (pour debug)
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'Views/ErreurPage.dart';
|
|
||||||
import 'Views/loginPage.dart';
|
import 'Views/loginPage.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
@ -10,12 +6,11 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
static bool isRegisterOpen = false;
|
static bool isRegisterOpen = false;
|
||||||
static DateTime? startDate;
|
static DateTime? startDate;
|
||||||
static late String path;
|
|
||||||
|
|
||||||
static const Gradient primaryGradient = LinearGradient(
|
static const Gradient primaryGradient = LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
Colors.white,
|
Colors.white,
|
||||||
const Color.fromARGB(255, 4, 54, 95),
|
Color.fromARGB(255, 4, 54, 95),
|
||||||
],
|
],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
@ -24,56 +19,17 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Flutter Demo',
|
title: 'GUYCOM',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
canvasColor: Colors.transparent,
|
canvasColor: Colors.transparent,
|
||||||
),
|
),
|
||||||
home: Builder(
|
home: Container(
|
||||||
builder: (context) {
|
|
||||||
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(
|
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: MyApp.primaryGradient,
|
gradient: MyApp.primaryGradient,
|
||||||
),
|
),
|
||||||
child: const LoginPage(),
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -872,6 +872,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
qr_code_scanner_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: qr_code_scanner_plus
|
||||||
|
sha256: "39696b50d277097ee4d90d4292de36f38c66213a4f5216a06b2bdd2b63117859"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.10+1"
|
||||||
qr_flutter:
|
qr_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -66,6 +66,7 @@ dependencies:
|
|||||||
mobile_scanner: ^5.0.0 # ou la version la plus récente
|
mobile_scanner: ^5.0.0 # ou la version la plus récente
|
||||||
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
|
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
|
||||||
numbers_to_letters: ^1.0.0
|
numbers_to_letters: ^1.0.0
|
||||||
|
qr_code_scanner_plus: ^2.0.10+1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:youmazgestion/my_app.dart';
|
||||||
|
|
||||||
|
|
||||||
import 'package:guycom/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user