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/accueil.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
import 'package:youmazgestion/Views/gestion_point_de_vente.dart'; // Nouvel import
|
||||
import 'package:youmazgestion/Views/gestion_point_de_vente.dart';
|
||||
|
||||
class CustomDrawer extends StatelessWidget {
|
||||
final UserController userController = Get.find<UserController>();
|
||||
@ -25,6 +25,7 @@ class CustomDrawer extends StatelessWidget {
|
||||
await prefs.remove('role');
|
||||
await prefs.remove('user_id');
|
||||
|
||||
// ✅ IMPORTANT: Vider le cache de session
|
||||
userController.clearUserData();
|
||||
}
|
||||
|
||||
@ -34,425 +35,573 @@ class CustomDrawer extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
backgroundColor: Colors.white,
|
||||
child: FutureBuilder(
|
||||
future: _buildDrawerItems(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: snapshot.data as List<Widget>,
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Widget>> _buildDrawerItems() async {
|
||||
List<Widget> drawerItems = [];
|
||||
|
||||
drawerItems.add(
|
||||
GetBuilder<UserController>(
|
||||
builder: (controller) => Container(
|
||||
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
child: GetBuilder<UserController>(
|
||||
builder: (controller) {
|
||||
return ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: AssetImage("assets/youmaz2.png"),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.name.isNotEmpty
|
||||
? controller.name
|
||||
: 'Utilisateur',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.role.isNotEmpty ? controller.role : 'Aucun rôle',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Header utilisateur
|
||||
_buildUserHeader(controller),
|
||||
|
||||
// ✅ CORRIGÉ: Construction avec gestion des valeurs null
|
||||
..._buildDrawerItemsFromSessionCache(),
|
||||
|
||||
// Déconnexion
|
||||
const Divider(),
|
||||
_buildLogoutItem(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
drawerItems.add(
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.home,
|
||||
title: "Accueil",
|
||||
color: Colors.blue,
|
||||
permissionAction: 'view',
|
||||
permissionRoute: '/accueil',
|
||||
onTap: () => Get.to(DashboardPage()),
|
||||
),
|
||||
);
|
||||
|
||||
List<Widget> gestionUtilisateursItems = [
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.person_add,
|
||||
title: "Ajouter un utilisateur",
|
||||
color: Colors.green,
|
||||
permissionAction: 'create',
|
||||
permissionRoute: '/ajouter-utilisateur',
|
||||
onTap: () => Get.to(const RegistrationPage()),
|
||||
),
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.supervised_user_circle,
|
||||
title: "Gérer les utilisateurs",
|
||||
color: const Color.fromARGB(255, 4, 54, 95),
|
||||
permissionAction: 'update',
|
||||
permissionRoute: '/modifier-utilisateur',
|
||||
onTap: () => Get.to(const ListUserPage()),
|
||||
),
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.timer,
|
||||
title: "Gestion des pointages",
|
||||
color: const Color.fromARGB(255, 4, 54, 95),
|
||||
permissionAction: 'update',
|
||||
permissionRoute: '/pointage',
|
||||
onTap: () => {},
|
||||
)
|
||||
];
|
||||
|
||||
if (gestionUtilisateursItems.any((item) => item is ListTile)) {
|
||||
drawerItems.add(
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
"GESTION UTILISATEURS",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
drawerItems.addAll(gestionUtilisateursItems);
|
||||
}
|
||||
|
||||
List<Widget> gestionProduitsItems = [
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.inventory,
|
||||
title: "Gestion des produits",
|
||||
color: Colors.indigoAccent,
|
||||
permissionAction: 'create',
|
||||
permissionRoute: '/ajouter-produit',
|
||||
onTap: () => Get.to(const ProductManagementPage()),
|
||||
),
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.storage,
|
||||
title: "Gestion de stock",
|
||||
color: Colors.blueAccent,
|
||||
permissionAction: 'update',
|
||||
permissionRoute: '/gestion-stock',
|
||||
onTap: () => Get.to(const GestionStockPage()),
|
||||
),
|
||||
];
|
||||
|
||||
if (gestionProduitsItems.any((item) => item is ListTile)) {
|
||||
drawerItems.add(
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
"GESTION PRODUITS",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
drawerItems.addAll(gestionProduitsItems);
|
||||
}
|
||||
|
||||
List<Widget> gestionCommandesItems = [
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.add_shopping_cart,
|
||||
title: "Nouvelle commande",
|
||||
color: Colors.orange,
|
||||
permissionAction: 'create',
|
||||
permissionRoute: '/nouvelle-commande',
|
||||
onTap: () => Get.to(const NouvelleCommandePage()),
|
||||
),
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.list_alt,
|
||||
title: "Gérer les commandes",
|
||||
color: Colors.deepPurple,
|
||||
permissionAction: 'manage',
|
||||
permissionRoute: '/gerer-commandes',
|
||||
onTap: () => Get.to(const GestionCommandesPage()),
|
||||
),
|
||||
];
|
||||
|
||||
if (gestionCommandesItems.any((item) => item is ListTile)) {
|
||||
drawerItems.add(
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
"GESTION COMMANDES",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
drawerItems.addAll(gestionCommandesItems);
|
||||
}
|
||||
|
||||
List<Widget> rapportsItems = [
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.bar_chart,
|
||||
title: "Bilan ",
|
||||
color: Colors.teal,
|
||||
permissionAction: 'read',
|
||||
permissionRoute: '/bilan',
|
||||
onTap: () => Get.to(DashboardPage()),
|
||||
),
|
||||
await _buildDrawerItem(
|
||||
icon: Icons.history,
|
||||
title: "Historique",
|
||||
color: Colors.blue,
|
||||
permissionAction: 'read',
|
||||
permissionRoute: '/historique',
|
||||
onTap: () => Get.to(const HistoriquePage()),
|
||||
),
|
||||
];
|
||||
|
||||
if (rapportsItems.any((item) => item is ListTile)) {
|
||||
drawerItems.add(
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
"RAPPORTS",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
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),
|
||||
title: const Text("Déconnexion"),
|
||||
onTap: () {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.logout_rounded,
|
||||
size: 48,
|
||||
color: Colors.orange.shade600,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Déconnexion",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
"Êtes-vous sûr de vouloir vous déconnecter ?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Vous devrez vous reconnecter pour accéder à votre compte.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Actions
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Annuler",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await clearUserData();
|
||||
Get.offAll(const LoginPage());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Se déconnecter",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// ✅ CORRIGÉ: Construction avec validation robuste des données
|
||||
List<Widget> _buildDrawerItemsFromSessionCache() {
|
||||
List<Widget> drawerItems = [];
|
||||
|
||||
// Vérifier si le cache est prêt
|
||||
if (!userController.isCacheReady) {
|
||||
return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
"Chargement du menu...",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// Récupérer les menus depuis le cache de session
|
||||
final rawUserMenus = userController.getUserMenus();
|
||||
|
||||
// 🛡️ VALIDATION: Filtrer les menus valides
|
||||
final validMenus = <Map<String, dynamic>>[];
|
||||
final invalidMenus = <Map<String, dynamic>>[];
|
||||
|
||||
for (var menu in rawUserMenus) {
|
||||
// Vérifier que les champs essentiels ne sont pas null
|
||||
final name = menu['name'];
|
||||
final route = menu['route'];
|
||||
final id = menu['id'];
|
||||
|
||||
if (name != null && route != null && route.toString().isNotEmpty) {
|
||||
validMenus.add({
|
||||
'id': id,
|
||||
'name': name.toString(),
|
||||
'route': route.toString(),
|
||||
});
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
/// ✅ 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(icon, color: color),
|
||||
title: Text(title),
|
||||
trailing: permissionAction != null
|
||||
? const Icon(Icons.chevron_right, color: Colors.grey)
|
||||
: null,
|
||||
onTap: onTap,
|
||||
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),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: AssetImage("assets/youmaz2.png"),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.name.isNotEmpty
|
||||
? controller.fullName
|
||||
: 'Utilisateur',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.role.isNotEmpty ? controller.role : 'Aucun rôle',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (controller.pointDeVenteDesignation.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
controller.pointDeVenteDesignation,
|
||||
style: const TextStyle(
|
||||
color: Colors.white60,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
// ✅ Indicateur de statut du cache
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
controller.isCacheReady ? Icons.check_circle : Icons.hourglass_empty,
|
||||
color: controller.isCacheReady ? Colors.green : Colors.orange,
|
||||
size: 12,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.isCacheReady ? 'Menu prêt' : 'Chargement...',
|
||||
style: const TextStyle(
|
||||
color: Colors.white60,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// ✅ Bouton de rafraîchissement pour les admins
|
||||
if (controller.role == 'Super Admin' || controller.role == 'Admin') ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, color: Colors.white70, size: 20),
|
||||
onPressed: () async {
|
||||
Get.snackbar(
|
||||
"Cache",
|
||||
"Rechargement des permissions...",
|
||||
snackPosition: SnackPosition.TOP,
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
await controller.refreshPermissions();
|
||||
Get.back(); // Fermer le drawer
|
||||
Get.snackbar(
|
||||
"Cache",
|
||||
"Permissions rechargées avec succès",
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
},
|
||||
tooltip: "Recharger les permissions",
|
||||
),
|
||||
],
|
||||
// 🔧 Bouton de debug (à supprimer en production)
|
||||
if (controller.role == 'Super Admin') ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report, color: Colors.white70, size: 18),
|
||||
onPressed: () {
|
||||
// Debug des menus
|
||||
final menus = controller.getUserMenus();
|
||||
String debugInfo = "MENUS DEBUG:\n";
|
||||
for (var i = 0; i < menus.length; i++) {
|
||||
final menu = menus[i];
|
||||
debugInfo += "[$i] ID:${menu['id']}, Name:'${menu['name']}', Route:'${menu['route']}'\n";
|
||||
}
|
||||
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text("Debug Menus"),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(debugInfo, style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("Fermer"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: "Debug menus",
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Item de déconnexion
|
||||
Widget _buildLogoutItem() {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.logout, color: Colors.red),
|
||||
title: const Text("Déconnexion"),
|
||||
onTap: () {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.logout_rounded,
|
||||
size: 48,
|
||||
color: Colors.orange.shade600,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Déconnexion",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
"Êtes-vous sûr de vouloir vous déconnecter ?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Vos permissions seront rechargées à la prochaine connexion.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Actions
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Annuler",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
// ✅ IMPORTANT: Vider le cache de session lors de la déconnexion
|
||||
await clearUserData();
|
||||
Get.offAll(const LoginPage());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Se déconnecter",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
final int? id;
|
||||
final int commandeId;
|
||||
@ -225,6 +227,11 @@ class DetailCommande {
|
||||
final String? produitImage;
|
||||
final String? produitReference;
|
||||
final bool? estCadeau;
|
||||
|
||||
// NOUVEAUX CHAMPS POUR LA REMISE PAR PRODUIT
|
||||
final double? remisePourcentage;
|
||||
final double? remiseMontant;
|
||||
final double? prixApresRemise;
|
||||
|
||||
DetailCommande({
|
||||
this.id,
|
||||
@ -237,6 +244,9 @@ class DetailCommande {
|
||||
this.produitImage,
|
||||
this.produitReference,
|
||||
this.estCadeau,
|
||||
this.remisePourcentage,
|
||||
this.remiseMontant,
|
||||
this.prixApresRemise,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
@ -248,6 +258,9 @@ class DetailCommande {
|
||||
'prixUnitaire': prixUnitaire,
|
||||
'sousTotal': sousTotal,
|
||||
'estCadeau': estCadeau == true ? 1 : 0,
|
||||
'remisePourcentage': remisePourcentage,
|
||||
'remiseMontant': remiseMontant,
|
||||
'prixApresRemise': prixApresRemise,
|
||||
};
|
||||
}
|
||||
|
||||
@ -263,6 +276,15 @@ class DetailCommande {
|
||||
produitImage: map['produitImage'] as String?,
|
||||
produitReference: map['produitReference'] as String?,
|
||||
estCadeau: map['estCadeau'] == 1,
|
||||
remisePourcentage: map['remisePourcentage'] != null
|
||||
? (map['remisePourcentage'] as num).toDouble()
|
||||
: null,
|
||||
remiseMontant: map['remiseMontant'] != null
|
||||
? (map['remiseMontant'] as num).toDouble()
|
||||
: null,
|
||||
prixApresRemise: map['prixApresRemise'] != null
|
||||
? (map['prixApresRemise'] as num).toDouble()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@ -277,6 +299,9 @@ class DetailCommande {
|
||||
String? produitImage,
|
||||
String? produitReference,
|
||||
bool? estCadeau,
|
||||
double? remisePourcentage,
|
||||
double? remiseMontant,
|
||||
double? prixApresRemise,
|
||||
}) {
|
||||
return DetailCommande(
|
||||
id: id ?? this.id,
|
||||
@ -289,6 +314,29 @@ class DetailCommande {
|
||||
produitImage: produitImage ?? this.produitImage,
|
||||
produitReference: produitReference ?? this.produitReference,
|
||||
estCadeau: estCadeau ?? this.estCadeau,
|
||||
remisePourcentage: remisePourcentage ?? this.remisePourcentage,
|
||||
remiseMontant: remiseMontant ?? this.remiseMontant,
|
||||
prixApresRemise: prixApresRemise ?? this.prixApresRemise,
|
||||
);
|
||||
}
|
||||
|
||||
// GETTERS QUI RÉSOLVENT LE PROBLÈME "aUneRemise" INTROUVABLE
|
||||
double get prixFinalUnitaire {
|
||||
return prixApresRemise ?? prixUnitaire;
|
||||
}
|
||||
|
||||
double get sousTotalAvecRemise {
|
||||
return quantite * prixFinalUnitaire;
|
||||
}
|
||||
|
||||
bool get aUneRemise {
|
||||
return remisePourcentage != null || remiseMontant != null || prixApresRemise != null;
|
||||
}
|
||||
|
||||
double get montantRemise {
|
||||
if (prixApresRemise != null) {
|
||||
return (prixUnitaire - prixApresRemise!) * quantite;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
258
lib/Services/PermissionCacheService.dart
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 {
|
||||
_connection = await _initDB();
|
||||
await _createDB();
|
||||
// await _createDB();
|
||||
|
||||
// Effectuer la migration pour les bases existantes
|
||||
await migrateDatabaseForDiscountAndGift();
|
||||
@ -70,242 +70,203 @@ class AppDatabase {
|
||||
|
||||
// Méthode mise à jour pour créer les tables avec les nouvelles colonnes
|
||||
Future<void> _createDB() async {
|
||||
final db = await database;
|
||||
// final db = await database;
|
||||
|
||||
try {
|
||||
// Table roles
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
designation VARCHAR(255) NOT NULL UNIQUE
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// try {
|
||||
// // Table roles
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS roles (
|
||||
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
// designation VARCHAR(255) NOT NULL UNIQUE
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table permissions
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL UNIQUE
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table permissions
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS permissions (
|
||||
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
// name VARCHAR(255) NOT NULL UNIQUE
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table menu
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS menu (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
route VARCHAR(255) NOT NULL
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table menu
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS menu (
|
||||
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
// name VARCHAR(255) NOT NULL,
|
||||
// route VARCHAR(255) NOT NULL
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table role_permissions
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
role_id INT,
|
||||
permission_id INT,
|
||||
PRIMARY KEY (role_id, permission_id),
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table role_permissions
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
// role_id INT,
|
||||
// permission_id INT,
|
||||
// PRIMARY KEY (role_id, permission_id),
|
||||
// FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||
// FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table role_menu_permissions
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS role_menu_permissions (
|
||||
role_id INT,
|
||||
menu_id INT,
|
||||
permission_id INT,
|
||||
PRIMARY KEY (role_id, menu_id, permission_id),
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table role_menu_permissions
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS role_menu_permissions (
|
||||
// role_id INT,
|
||||
// menu_id INT,
|
||||
// permission_id INT,
|
||||
// PRIMARY KEY (role_id, menu_id, permission_id),
|
||||
// FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||
// FOREIGN KEY (menu_id) REFERENCES menu(id) ON DELETE CASCADE,
|
||||
// FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table points_de_vente
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS points_de_vente (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nom VARCHAR(255) NOT NULL UNIQUE
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table points_de_vente
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS points_de_vente (
|
||||
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
// nom VARCHAR(255) NOT NULL UNIQUE
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table users
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
lastname VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
role_id INT NOT NULL,
|
||||
point_de_vente_id INT,
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id),
|
||||
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table users
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS users (
|
||||
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
// name VARCHAR(255) NOT NULL,
|
||||
// lastname VARCHAR(255) NOT NULL,
|
||||
// email VARCHAR(255) NOT NULL UNIQUE,
|
||||
// password VARCHAR(255) NOT NULL,
|
||||
// username VARCHAR(255) NOT NULL UNIQUE,
|
||||
// role_id INT NOT NULL,
|
||||
// point_de_vente_id INT,
|
||||
// FOREIGN KEY (role_id) REFERENCES roles(id),
|
||||
// FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table products
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
image VARCHAR(2000),
|
||||
category VARCHAR(255) NOT NULL,
|
||||
stock INT NOT NULL DEFAULT 0,
|
||||
description VARCHAR(1000),
|
||||
qrCode VARCHAR(500),
|
||||
reference VARCHAR(255),
|
||||
point_de_vente_id INT,
|
||||
marque VARCHAR(255),
|
||||
ram VARCHAR(100),
|
||||
memoire_interne VARCHAR(100),
|
||||
imei VARCHAR(255) UNIQUE,
|
||||
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id),
|
||||
INDEX idx_products_category (category),
|
||||
INDEX idx_products_reference (reference),
|
||||
INDEX idx_products_imei (imei)
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table products
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS products (
|
||||
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
// name VARCHAR(255) NOT NULL,
|
||||
// price DECIMAL(10,2) NOT NULL,
|
||||
// image VARCHAR(2000),
|
||||
// category VARCHAR(255) NOT NULL,
|
||||
// stock INT NOT NULL DEFAULT 0,
|
||||
// description VARCHAR(1000),
|
||||
// qrCode VARCHAR(500),
|
||||
// reference VARCHAR(255),
|
||||
// point_de_vente_id INT,
|
||||
// marque VARCHAR(255),
|
||||
// ram VARCHAR(100),
|
||||
// memoire_interne VARCHAR(100),
|
||||
// imei VARCHAR(255) UNIQUE,
|
||||
// FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id),
|
||||
// INDEX idx_products_category (category),
|
||||
// INDEX idx_products_reference (reference),
|
||||
// INDEX idx_products_imei (imei)
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table clients
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nom VARCHAR(255) NOT NULL,
|
||||
prenom VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
telephone VARCHAR(255) NOT NULL,
|
||||
adresse VARCHAR(500),
|
||||
dateCreation DATETIME NOT NULL,
|
||||
actif TINYINT(1) NOT NULL DEFAULT 1,
|
||||
INDEX idx_clients_email (email),
|
||||
INDEX idx_clients_telephone (telephone)
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table clients
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS clients (
|
||||
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
// nom VARCHAR(255) NOT NULL,
|
||||
// prenom VARCHAR(255) NOT NULL,
|
||||
// email VARCHAR(255) NOT NULL UNIQUE,
|
||||
// telephone VARCHAR(255) NOT NULL,
|
||||
// adresse VARCHAR(500),
|
||||
// dateCreation DATETIME NOT NULL,
|
||||
// actif TINYINT(1) NOT NULL DEFAULT 1,
|
||||
// INDEX idx_clients_email (email),
|
||||
// INDEX idx_clients_telephone (telephone)
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table commandes MISE À JOUR avec les champs de remise
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS commandes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
clientId INT NOT NULL,
|
||||
dateCommande DATETIME NOT NULL,
|
||||
statut INT NOT NULL DEFAULT 0,
|
||||
montantTotal DECIMAL(10,2) NOT NULL,
|
||||
notes VARCHAR(1000),
|
||||
dateLivraison DATETIME,
|
||||
commandeurId INT,
|
||||
validateurId INT,
|
||||
remisePourcentage DECIMAL(5,2) NULL,
|
||||
remiseMontant DECIMAL(10,2) NULL,
|
||||
montantApresRemise DECIMAL(10,2) NULL,
|
||||
FOREIGN KEY (commandeurId) REFERENCES users(id),
|
||||
FOREIGN KEY (validateurId) REFERENCES users(id),
|
||||
FOREIGN KEY (clientId) REFERENCES clients(id),
|
||||
INDEX idx_commandes_client (clientId),
|
||||
INDEX idx_commandes_date (dateCommande)
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table commandes MISE À JOUR avec les champs de remise
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS commandes (
|
||||
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
// clientId INT NOT NULL,
|
||||
// dateCommande DATETIME NOT NULL,
|
||||
// statut INT NOT NULL DEFAULT 0,
|
||||
// montantTotal DECIMAL(10,2) NOT NULL,
|
||||
// notes VARCHAR(1000),
|
||||
// dateLivraison DATETIME,
|
||||
// commandeurId INT,
|
||||
// validateurId INT,
|
||||
// remisePourcentage DECIMAL(5,2) NULL,
|
||||
// remiseMontant DECIMAL(10,2) NULL,
|
||||
// montantApresRemise DECIMAL(10,2) NULL,
|
||||
// FOREIGN KEY (commandeurId) REFERENCES users(id),
|
||||
// FOREIGN KEY (validateurId) REFERENCES users(id),
|
||||
// FOREIGN KEY (clientId) REFERENCES clients(id),
|
||||
// INDEX idx_commandes_client (clientId),
|
||||
// INDEX idx_commandes_date (dateCommande)
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
// Table details_commandes MISE À JOUR avec le champ cadeau
|
||||
await db.query('''
|
||||
CREATE TABLE IF NOT EXISTS details_commandes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
commandeId INT NOT NULL,
|
||||
produitId INT NOT NULL,
|
||||
quantite INT NOT NULL,
|
||||
prixUnitaire DECIMAL(10,2) NOT NULL,
|
||||
sousTotal DECIMAL(10,2) NOT NULL,
|
||||
estCadeau TINYINT(1) DEFAULT 0,
|
||||
FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (produitId) REFERENCES products(id),
|
||||
INDEX idx_details_commande (commandeId)
|
||||
) ENGINE=InnoDB
|
||||
''');
|
||||
// // Table details_commandes MISE À JOUR avec le champ cadeau
|
||||
// await db.query('''
|
||||
// CREATE TABLE IF NOT EXISTS details_commandes (
|
||||
// id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
// commandeId INT NOT NULL,
|
||||
// produitId INT NOT NULL,
|
||||
// quantite INT NOT NULL,
|
||||
// prixUnitaire DECIMAL(10,2) NOT NULL,
|
||||
// sousTotal DECIMAL(10,2) NOT NULL,
|
||||
// estCadeau TINYINT(1) DEFAULT 0,
|
||||
// FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE,
|
||||
// FOREIGN KEY (produitId) REFERENCES products(id),
|
||||
// INDEX idx_details_commande (commandeId)
|
||||
// ) ENGINE=InnoDB
|
||||
// ''');
|
||||
|
||||
print("Tables créées avec succès avec les nouveaux champs !");
|
||||
} catch (e) {
|
||||
print("Erreur lors de la création des tables: $e");
|
||||
rethrow;
|
||||
}
|
||||
// print("Tables créées avec succès avec les nouveaux champs !");
|
||||
// } catch (e) {
|
||||
// print("Erreur lors de la création des tables: $e");
|
||||
// rethrow;
|
||||
// }
|
||||
}
|
||||
|
||||
// --- MÉTHODES D'INSERTION PAR DÉFAUT ---
|
||||
|
||||
//
|
||||
Future<void> insertDefaultPermissions() async {
|
||||
final db = await database;
|
||||
|
||||
try {
|
||||
final existing = await db.query('SELECT COUNT(*) as count FROM permissions');
|
||||
final count = existing.first['count'] as int;
|
||||
|
||||
if (count == 0) {
|
||||
final permissions = ['view', 'create', 'update', 'delete', 'admin', 'manage', 'read'];
|
||||
|
||||
for (String permission in permissions) {
|
||||
await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]);
|
||||
}
|
||||
print("Permissions par défaut insérées");
|
||||
} else {
|
||||
// Vérifier et ajouter les nouvelles permissions si elles n'existent pas
|
||||
final newPermissions = ['manage', 'read'];
|
||||
for (var permission in newPermissions) {
|
||||
final existingPermission = await db.query(
|
||||
'SELECT COUNT(*) as count FROM permissions WHERE name = ?',
|
||||
[permission]
|
||||
);
|
||||
final permCount = existingPermission.first['count'] as int;
|
||||
if (permCount == 0) {
|
||||
await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]);
|
||||
print("Permission ajoutée: $permission");
|
||||
}
|
||||
}
|
||||
final db = await database;
|
||||
|
||||
try {
|
||||
// Vérifier et ajouter uniquement les nouvelles permissions si elles n'existent pas
|
||||
final newPermissions = ['manage', 'read'];
|
||||
for (var permission in newPermissions) {
|
||||
final existingPermission = await db.query(
|
||||
'SELECT COUNT(*) as count FROM permissions WHERE name = ?',
|
||||
[permission]
|
||||
);
|
||||
final permCount = existingPermission.first['count'] as int;
|
||||
if (permCount == 0) {
|
||||
await db.query('INSERT INTO permissions (name) VALUES (?)', [permission]);
|
||||
print("Permission ajoutée: $permission");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Erreur insertDefaultPermissions: $e");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Erreur insertDefaultPermissions: $e");
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
Future<void> insertDefaultMenus() async {
|
||||
final db = await database;
|
||||
|
||||
try {
|
||||
final existingMenus = await db.query('SELECT COUNT(*) as count FROM menu');
|
||||
final count = existingMenus.first['count'] as int;
|
||||
|
||||
if (count == 0) {
|
||||
final menus = [
|
||||
{'name': 'Accueil', 'route': '/accueil'},
|
||||
{'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'},
|
||||
{'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'},
|
||||
{'name': 'Ajouter un produit', 'route': '/ajouter-produit'},
|
||||
{'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'},
|
||||
{'name': 'Bilan', 'route': '/bilan'},
|
||||
{'name': 'Gérer les rôles', 'route': '/gerer-roles'},
|
||||
{'name': 'Gestion de stock', 'route': '/gestion-stock'},
|
||||
{'name': 'Historique', 'route': '/historique'},
|
||||
{'name': 'Déconnexion', 'route': '/deconnexion'},
|
||||
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
|
||||
{'name': 'Gérer les commandes', 'route': '/gerer-commandes'},
|
||||
{'name': 'Points de vente', 'route': '/points-de-vente'},
|
||||
];
|
||||
|
||||
for (var menu in menus) {
|
||||
await db.query(
|
||||
'INSERT INTO menu (name, route) VALUES (?, ?)',
|
||||
[menu['name'], menu['route']]
|
||||
);
|
||||
}
|
||||
print("Menus par défaut insérés");
|
||||
} else {
|
||||
await _addMissingMenus(db);
|
||||
}
|
||||
} catch (e) {
|
||||
print("Erreur insertDefaultMenus: $e");
|
||||
}
|
||||
final db = await database;
|
||||
|
||||
try {
|
||||
await _addMissingMenus(db); // Seulement ajouter les menus manquants
|
||||
} catch (e) {
|
||||
print("Erreur insertDefaultMenus: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> insertDefaultRoles() async {
|
||||
final db = await database;
|
||||
@ -741,29 +702,53 @@ Future<void> _createDB() async {
|
||||
|
||||
// --- MÉTHODES UTILITAIRES ---
|
||||
|
||||
Future<void> _addMissingMenus(MySqlConnection db) async {
|
||||
final menusToAdd = [
|
||||
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
|
||||
{'name': 'Gérer les commandes', 'route': '/gerer-commandes'},
|
||||
{'name': 'Points de vente', 'route': '/points-de-vente'},
|
||||
];
|
||||
// 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;
|
||||
// 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']}");
|
||||
}
|
||||
// if (count == 0) {
|
||||
// await db.query(
|
||||
// 'INSERT INTO menu (name, route) VALUES (?, ?)',
|
||||
// [menu['name'], menu['route']]
|
||||
// );
|
||||
// print("Menu ajouté: ${menu['name']}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<void> _addMissingMenus(MySqlConnection db) async {
|
||||
final menusToAdd = [
|
||||
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
|
||||
{'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> _updateExistingRolePermissions(MySqlConnection db) async {
|
||||
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:get/get.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
import 'package:youmazgestion/Services/PermissionCacheService.dart'; // Nouveau import
|
||||
import 'package:youmazgestion/Views/Dashboard.dart';
|
||||
import 'package:youmazgestion/Views/mobilepage.dart';
|
||||
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
||||
@ -19,9 +20,12 @@ class _LoginPageState extends State<LoginPage> {
|
||||
late TextEditingController _usernameController;
|
||||
late TextEditingController _passwordController;
|
||||
final UserController userController = Get.put(UserController());
|
||||
final PermissionCacheService _cacheService = PermissionCacheService.instance; // Nouveau
|
||||
|
||||
bool _isErrorVisible = false;
|
||||
bool _isLoading = false;
|
||||
String _errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
|
||||
String _loadingMessage = 'Connexion en cours...'; // Nouveau
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -51,105 +55,185 @@ class _LoginPageState extends State<LoginPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> saveUserData(Users user, String role, int userId) async {
|
||||
try {
|
||||
userController.setUserWithCredentials(user, role, userId);
|
||||
// /// ✅ 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();
|
||||
}
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
|
||||
void _login() async {
|
||||
if (_isLoading) return;
|
||||
|
||||
final String username = _usernameController.text.trim();
|
||||
final String password = _passwordController.text.trim();
|
||||
|
||||
if (username.isEmpty || password.isEmpty) {
|
||||
setState(() {
|
||||
_errorMessage =
|
||||
'Veuillez saisir le nom d\'utilisateur et le mot de passe';
|
||||
_isErrorVisible = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 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 {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_isErrorVisible = false;
|
||||
_loadingMessage = 'Préparation du menu...';
|
||||
});
|
||||
|
||||
// Lancer le préchargement en parallèle avec les autres tâches
|
||||
final permissionFuture = _cacheService.preloadUserData(username);
|
||||
|
||||
// Attendre maximum 2 secondes pour les permissions
|
||||
await Future.any([
|
||||
permissionFuture,
|
||||
Future.delayed(const Duration(seconds: 2))
|
||||
]);
|
||||
|
||||
print('✅ Permissions préparées (ou timeout)');
|
||||
} catch (e) {
|
||||
print('⚠️ Erreur préchargement permissions: $e');
|
||||
// Continuer même en cas d'erreur
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ OPTIMISÉ: Connexion avec préchargement parallèle
|
||||
void _login() async {
|
||||
if (_isLoading) return;
|
||||
|
||||
final String username = _usernameController.text.trim();
|
||||
final String password = _passwordController.text.trim();
|
||||
|
||||
if (username.isEmpty || password.isEmpty) {
|
||||
setState(() {
|
||||
_errorMessage = 'Veuillez saisir le nom d\'utilisateur et le mot de passe';
|
||||
_isErrorVisible = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_isErrorVisible = false;
|
||||
_loadingMessage = 'Connexion...';
|
||||
});
|
||||
|
||||
try {
|
||||
print('🔐 Tentative de connexion pour: $username');
|
||||
final dbInstance = AppDatabase.instance;
|
||||
|
||||
// 1. Vérification rapide de la base
|
||||
setState(() {
|
||||
_loadingMessage = 'Vérification...';
|
||||
});
|
||||
|
||||
try {
|
||||
print('Tentative de connexion pour: $username');
|
||||
final dbInstance = AppDatabase.instance;
|
||||
|
||||
try {
|
||||
final userCount = await dbInstance.getUserCount();
|
||||
print('Base de données accessible, $userCount utilisateurs trouvés');
|
||||
} catch (dbError) {
|
||||
throw Exception('Impossible d\'accéder à la base de données: $dbError');
|
||||
}
|
||||
final userCount = await dbInstance.getUserCount();
|
||||
print('✅ Base accessible, $userCount utilisateurs');
|
||||
} catch (dbError) {
|
||||
throw Exception('Base de données inaccessible: $dbError');
|
||||
}
|
||||
|
||||
bool isValidUser = await dbInstance.verifyUser(username, password);
|
||||
// 2. Vérification des identifiants
|
||||
setState(() {
|
||||
_loadingMessage = 'Authentification...';
|
||||
});
|
||||
|
||||
bool isValidUser = await dbInstance.verifyUser(username, password);
|
||||
|
||||
if (isValidUser) {
|
||||
Users user = await dbInstance.getUser(username);
|
||||
print('Utilisateur récupéré: ${user.username}');
|
||||
|
||||
Map<String, dynamic>? userCredentials =
|
||||
await dbInstance.getUserCredentials(username, password);
|
||||
|
||||
if (userCredentials != null) {
|
||||
print('Connexion réussie pour: ${user.username}');
|
||||
print('Rôle: ${userCredentials['role']}');
|
||||
print('ID: ${userCredentials['id']}');
|
||||
|
||||
await saveUserData(
|
||||
user,
|
||||
userCredentials['role'] as String,
|
||||
userCredentials['id'] as int,
|
||||
);
|
||||
|
||||
// MODIFICATION PRINCIPALE ICI
|
||||
if (mounted) {
|
||||
if (userCredentials['role'] == 'commercial') {
|
||||
// Redirection vers MainLayout pour les commerciaux
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainLayout()),
|
||||
);
|
||||
} else {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => DashboardPage()),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw Exception('Erreur lors de la récupération des credentials');
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
|
||||
_isErrorVisible = true;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isValidUser) {
|
||||
setState(() {
|
||||
_errorMessage = 'Erreur de connexion: ${error.toString()}';
|
||||
_loadingMessage = 'Chargement du profil...';
|
||||
});
|
||||
|
||||
// 3. Récupération parallèle des données
|
||||
final futures = await Future.wait([
|
||||
dbInstance.getUser(username),
|
||||
dbInstance.getUserCredentials(username, password),
|
||||
]);
|
||||
|
||||
final user = futures[0] as Users;
|
||||
final userCredentials = futures[1] as Map<String, dynamic>?;
|
||||
|
||||
if (userCredentials != null) {
|
||||
print('✅ Connexion réussie pour: ${user.username}');
|
||||
print(' Rôle: ${userCredentials['role']}');
|
||||
|
||||
setState(() {
|
||||
_loadingMessage = 'Préparation...';
|
||||
});
|
||||
|
||||
// 4. Sauvegarde des données utilisateur
|
||||
await saveUserData(
|
||||
user,
|
||||
userCredentials['role'] as String,
|
||||
userCredentials['id'] as int,
|
||||
);
|
||||
|
||||
// 5. Préchargement des permissions EN PARALLÈLE avec la navigation
|
||||
setState(() {
|
||||
_loadingMessage = 'Finalisation...';
|
||||
});
|
||||
|
||||
// Lancer le préchargement en arrière-plan SANS attendre
|
||||
_cacheService.preloadUserDataAsync(username);
|
||||
|
||||
// 6. Navigation immédiate
|
||||
if (mounted) {
|
||||
if (userCredentials['role'] == 'commercial') {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainLayout()),
|
||||
);
|
||||
} else {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => DashboardPage()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Les permissions se chargeront en arrière-plan après la navigation
|
||||
print('🚀 Navigation immédiate, permissions en arrière-plan');
|
||||
|
||||
} else {
|
||||
throw Exception('Erreur lors de la récupération des credentials');
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_errorMessage = 'Nom d\'utilisateur ou mot de passe invalide';
|
||||
_isErrorVisible = true;
|
||||
});
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
} catch (error) {
|
||||
setState(() {
|
||||
_errorMessage = 'Erreur de connexion: ${error.toString()}';
|
||||
_isErrorVisible = true;
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_loadingMessage = 'Connexion en cours...';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ OPTIMISÉ: Sauvegarde rapide
|
||||
Future<void> saveUserData(Users user, String role, int userId) async {
|
||||
try {
|
||||
userController.setUserWithCredentials(user, role, userId);
|
||||
|
||||
// Charger le point de vente en parallèle si nécessaire
|
||||
if (user.pointDeVenteId != null) {
|
||||
// Ne pas attendre, charger en arrière-plan
|
||||
unawaited(userController.loadPointDeVenteDesignation());
|
||||
}
|
||||
|
||||
print('✅ Utilisateur sauvegardé rapidement');
|
||||
} catch (error) {
|
||||
print('❌ Erreur lors de la sauvegarde: $error');
|
||||
throw Exception('Erreur lors de la sauvegarde des données utilisateur');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -169,8 +253,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
width: MediaQuery.of(context).size.width < 500
|
||||
? double.infinity
|
||||
: 400,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor.withOpacity(0.98),
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
@ -186,6 +269,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Header
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
@ -219,6 +303,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Username Field
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
enabled: !_isLoading,
|
||||
@ -241,6 +327,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18.0),
|
||||
|
||||
// Password Field
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
enabled: !_isLoading,
|
||||
@ -263,19 +351,104 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
onSubmitted: (_) => _login(),
|
||||
),
|
||||
|
||||
if (_isLoading) ...[
|
||||
const SizedBox(height: 16.0),
|
||||
Column(
|
||||
children: [
|
||||
// Barre de progression animée
|
||||
Container(
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
color: accentColor.withOpacity(0.2),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: constraints.maxWidth * 0.7,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
gradient: LinearGradient(
|
||||
colors: [accentColor, accentColor.withOpacity(0.7)],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(accentColor),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_loadingMessage,
|
||||
style: TextStyle(
|
||||
color: accentColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Le menu se chargera en arrière-plan",
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 12,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
// Error Message
|
||||
if (_isErrorVisible) ...[
|
||||
const SizedBox(height: 12.0),
|
||||
Text(
|
||||
_errorMessage,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_errorMessage,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 26.0),
|
||||
|
||||
// Login Button
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _login,
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -289,13 +462,27 @@ class _LoginPageState extends State<LoginPage> {
|
||||
minimumSize: const Size(double.infinity, 52),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Connexion...',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const Text(
|
||||
'Se connecter',
|
||||
@ -307,16 +494,23 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Option debug, à enlever en prod
|
||||
if (_isErrorVisible) ...[
|
||||
|
||||
// Debug Button (à enlever en production)
|
||||
if (_isErrorVisible && !_isLoading) ...[
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
final count =
|
||||
await AppDatabase.instance.getUserCount();
|
||||
final count = await AppDatabase.instance.getUserCount();
|
||||
final stats = _cacheService.getCacheStats();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$count utilisateurs trouvés')),
|
||||
content: Text(
|
||||
'BDD: $count utilisateurs\n'
|
||||
'Cache: ${stats['users_cached']} utilisateurs en cache',
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -324,7 +518,13 @@ class _LoginPageState extends State<LoginPage> {
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Debug: Vérifier BDD'),
|
||||
child: Text(
|
||||
'Debug: Vérifier BDD & Cache',
|
||||
style: TextStyle(
|
||||
color: primaryColor.withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||
import 'package:youmazgestion/Components/QrScan.dart';
|
||||
import 'package:youmazgestion/Components/app_bar.dart';
|
||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||
@ -508,57 +509,523 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
);
|
||||
}
|
||||
|
||||
// Variables pour le scanner
|
||||
QRViewController? _qrController;
|
||||
bool _isScanning = false;
|
||||
final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR');
|
||||
|
||||
// 4. Méthode pour démarrer le scan
|
||||
void _startBarcodeScanning() {
|
||||
if (_isScanning) return;
|
||||
|
||||
setState(() {
|
||||
_isScanning = true;
|
||||
});
|
||||
|
||||
Get.to(() => _buildScannerPage())?.then((_) {
|
||||
setState(() {
|
||||
_isScanning = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Page du scanner
|
||||
Widget _buildScannerPage() {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Scanner IMEI'),
|
||||
backgroundColor: Colors.green.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_qrController?.dispose();
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.flash_on),
|
||||
onPressed: () async {
|
||||
await _qrController?.toggleFlash();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.flip_camera_ios),
|
||||
onPressed: () async {
|
||||
await _qrController?.flipCamera();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// Scanner view
|
||||
QRView(
|
||||
key: _qrKey,
|
||||
onQRViewCreated: _onQRViewCreated,
|
||||
overlay: QrScannerOverlayShape(
|
||||
borderColor: Colors.green,
|
||||
borderRadius: 10,
|
||||
borderLength: 30,
|
||||
borderWidth: 10,
|
||||
cutOutSize: 250,
|
||||
),
|
||||
),
|
||||
|
||||
// Instructions overlay
|
||||
Positioned(
|
||||
bottom: 100,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.qr_code_scanner, color: Colors.white, size: 40),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Pointez la caméra vers le code-barres IMEI',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Le scan se fait automatiquement',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Configuration du contrôleur QR
|
||||
void _onQRViewCreated(QRViewController controller) {
|
||||
_qrController = controller;
|
||||
|
||||
controller.scannedDataStream.listen((scanData) {
|
||||
if (scanData.code != null && scanData.code!.isNotEmpty) {
|
||||
// Pauser le scanner pour éviter les scans multiples
|
||||
controller.pauseCamera();
|
||||
|
||||
// Fermer la page du scanner
|
||||
Get.back();
|
||||
|
||||
// Traiter le résultat
|
||||
_findAndAddProductByImei(scanData.code!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 7. Méthode pour trouver et ajouter un produit par IMEI
|
||||
Future<void> _findAndAddProductByImei(String scannedImei) async {
|
||||
try {
|
||||
// Montrer un indicateur de chargement
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(color: Colors.green.shade700),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Recherche du produit...'),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'IMEI: $scannedImei',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
// Attendre un court instant pour l'effet visuel
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
// Chercher le produit avec l'IMEI scanné
|
||||
Product? foundProduct;
|
||||
|
||||
for (var product in _products) {
|
||||
if (product.imei?.toLowerCase().trim() == scannedImei.toLowerCase().trim()) {
|
||||
foundProduct = product;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fermer l'indicateur de chargement
|
||||
Get.back();
|
||||
|
||||
if (foundProduct == null) {
|
||||
_showProductNotFoundDialog(scannedImei);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier le stock
|
||||
if (foundProduct.stock != null && foundProduct.stock! <= 0) {
|
||||
Get.snackbar(
|
||||
'Stock insuffisant',
|
||||
'Le produit "${foundProduct.name}" n\'est plus en stock',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange.shade600,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
icon: const Icon(Icons.warning_amber, color: Colors.white),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si le produit peut être ajouté (stock disponible)
|
||||
final currentQuantity = _quantites[foundProduct.id] ?? 0;
|
||||
if (foundProduct.stock != null && currentQuantity >= foundProduct.stock!) {
|
||||
Get.snackbar(
|
||||
'Stock limite atteint',
|
||||
'Quantité maximum atteinte pour "${foundProduct.name}"',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange.shade600,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
icon: const Icon(Icons.warning_amber, color: Colors.white),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ajouter le produit au panier
|
||||
setState(() {
|
||||
_quantites[foundProduct!.id!] = currentQuantity + 1;
|
||||
});
|
||||
|
||||
// Afficher le dialogue de succès
|
||||
_showSuccessDialog(foundProduct, currentQuantity + 1);
|
||||
|
||||
} catch (e) {
|
||||
// Fermer l'indicateur de chargement si il est encore ouvert
|
||||
if (Get.isDialogOpen!) Get.back();
|
||||
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Une erreur est survenue: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red.shade600,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Dialogue de succès
|
||||
void _showSuccessDialog(Product product, int newQuantity) {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.check_circle, color: Colors.green.shade700),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: Text('Produit ajouté !')),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('Prix: ${product.price.toStringAsFixed(2)} MGA'),
|
||||
Text('Quantité dans le panier: $newQuantity'),
|
||||
if (product.stock != null)
|
||||
Text('Stock restant: ${product.stock! - newQuantity}'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Continuer'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_showCartBottomSheet();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Voir le panier'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 9. Dialogue produit non trouvé
|
||||
void _showProductNotFoundDialog(String scannedImei) {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.search_off, color: Colors.red.shade600),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Produit non trouvé'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Aucun produit trouvé avec cet IMEI:'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
scannedImei,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Vérifiez que l\'IMEI est correct ou que le produit existe dans la base de données.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_startBarcodeScanning();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Scanner à nouveau'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildScanInfoCard() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.qr_code_scanner,
|
||||
color: Colors.green.shade700,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Scanner rapidement un produit via son IMEI pour l\'ajouter au panier',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color.fromARGB(255, 9, 56, 95),
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isScanning ? null : _startBarcodeScanning,
|
||||
icon: _isScanning
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.qr_code_scanner, size: 18),
|
||||
label: Text(_isScanning ? 'Scan...' : 'Scanner'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 10. Modifier le Widget build pour ajouter le bouton de scan
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
|
||||
return Scaffold(
|
||||
floatingActionButton: _buildFloatingCartButton(),
|
||||
drawer: isMobile ? CustomDrawer() : null,
|
||||
floatingActionButton: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// Bouton de scan
|
||||
FloatingActionButton(
|
||||
heroTag: "scan",
|
||||
onPressed: _isScanning ? null : _startBarcodeScanning,
|
||||
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
child: _isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.qr_code_scanner),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Bouton panier existant
|
||||
_buildFloatingCartButton(),
|
||||
],
|
||||
),
|
||||
appBar: CustomAppBar(title: 'Nouvelle commande'),
|
||||
drawer: CustomDrawer(),
|
||||
body: GestureDetector(
|
||||
onTap: _hideAllSuggestions, // Masquer les suggestions quand on tape ailleurs
|
||||
onTap: _hideAllSuggestions,
|
||||
child: Column(
|
||||
children: [
|
||||
// Section des filtres - adaptée comme dans HistoriquePage
|
||||
// Section d'information sur le scan (desktop)
|
||||
if (!isMobile)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: _buildScanInfoCard(),
|
||||
),
|
||||
|
||||
// Section des filtres
|
||||
if (!isMobile)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: _buildFilterSection(),
|
||||
),
|
||||
|
||||
// Sur mobile, bouton pour afficher les filtres dans un modal
|
||||
// Boutons pour mobile
|
||||
if (isMobile) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.filter_alt),
|
||||
label: const Text('Filtres produits'),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.filter_alt),
|
||||
label: const Text('Filtres'),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: _buildFilterSection(),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: _buildFilterSection(),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ElevatedButton.icon(
|
||||
icon: _isScanning
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.qr_code_scanner),
|
||||
label: Text(_isScanning ? 'Scan...' : 'Scan'),
|
||||
onPressed: _isScanning ? null : _startBarcodeScanning,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _isScanning ? Colors.grey : Colors.green.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Compteur de résultats visible en haut sur mobile
|
||||
// Compteur de résultats
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Container(
|
||||
@ -588,6 +1055,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildSuggestionsList({required bool isNom}) {
|
||||
if (_clientSuggestions.isEmpty) return const SizedBox();
|
||||
|
||||
@ -1725,23 +2193,21 @@ void _fillFormWithClient(Client client) {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
void dispose() {
|
||||
// Nettoyer les suggestions
|
||||
_hideAllSuggestions();
|
||||
_qrController?.dispose();
|
||||
|
||||
// Disposer les contrôleurs
|
||||
// Vos disposals existants...
|
||||
_hideAllSuggestions();
|
||||
_nomController.dispose();
|
||||
_prenomController.dispose();
|
||||
_emailController.dispose();
|
||||
_telephoneController.dispose();
|
||||
_adresseController.dispose();
|
||||
|
||||
// Disposal des contrôleurs de filtre
|
||||
_searchNameController.dispose();
|
||||
_searchImeiController.dispose();
|
||||
_searchReferenceController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,12 @@
|
||||
// Config/database_config.dart - Version améliorée
|
||||
class DatabaseConfig {
|
||||
static const String host = 'database.c4m.mg';
|
||||
static const String host = '172.20.10.5';
|
||||
static const int port = 3306;
|
||||
static const String username = 'guycom';
|
||||
static const String password = '3iV59wjRdbuXAPR';
|
||||
static const String database = 'guycom';
|
||||
static const String username = 'root';
|
||||
static const String? password = null;
|
||||
static const String database = 'guycom_databse_v1';
|
||||
|
||||
static const String prodHost = 'database.c4m.mg';
|
||||
static const String prodHost = '185.70.105.157';
|
||||
static const String prodUsername = 'guycom';
|
||||
static const String prodPassword = '3iV59wjRdbuXAPR';
|
||||
static const String prodDatabase = 'guycom';
|
||||
@ -17,7 +17,7 @@ class DatabaseConfig {
|
||||
static const int maxConnections = 10;
|
||||
static const int minConnections = 2;
|
||||
|
||||
static bool get isDevelopment => false;
|
||||
static bool get isDevelopment => true;
|
||||
|
||||
static Map<String, dynamic> getConfig() {
|
||||
if (isDevelopment) {
|
||||
|
||||
@ -2,7 +2,7 @@ import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:youmazgestion/Models/users.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
//import 'package:youmazgestion/Services/app_database.dart';
|
||||
import 'package:youmazgestion/Services/PermissionCacheService.dart';
|
||||
|
||||
class UserController extends GetxController {
|
||||
final _username = ''.obs;
|
||||
@ -11,10 +11,14 @@ class UserController extends GetxController {
|
||||
final _name = ''.obs;
|
||||
final _lastname = ''.obs;
|
||||
final _password = ''.obs;
|
||||
final _userId = 0.obs; // ✅ Ajout de l'ID utilisateur
|
||||
final _userId = 0.obs;
|
||||
final _pointDeVenteId = 0.obs;
|
||||
final _pointDeVenteDesignation = ''.obs;
|
||||
|
||||
// Cache service
|
||||
final PermissionCacheService _cacheService = PermissionCacheService.instance;
|
||||
|
||||
// Getters
|
||||
String get username => _username.value;
|
||||
String get email => _email.value;
|
||||
String get role => _role.value;
|
||||
@ -28,92 +32,111 @@ class UserController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadUserData(); // Charger les données au démarrage
|
||||
loadUserData();
|
||||
}
|
||||
|
||||
// ✅ CORRECTION : Charger les données complètes depuis SharedPreferences ET la base de données
|
||||
Future<void> loadUserData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final storedUsername = prefs.getString('username') ?? '';
|
||||
final storedRole = prefs.getString('role') ?? '';
|
||||
final storedUserId = prefs.getInt('user_id') ?? 0;
|
||||
final storedPointDeVenteId = prefs.getInt('point_de_vente_id') ?? 0;
|
||||
final storedPointDeVenteDesignation = prefs.getString('point_de_vente_designation') ?? '';
|
||||
|
||||
if (storedUsername.isNotEmpty) {
|
||||
try {
|
||||
Users user = await AppDatabase.instance.getUser(storedUsername);
|
||||
|
||||
_username.value = user.username;
|
||||
_email.value = user.email;
|
||||
_name.value = user.name;
|
||||
_lastname.value = user.lastName;
|
||||
_password.value = user.password;
|
||||
_role.value = storedRole;
|
||||
_userId.value = storedUserId;
|
||||
_pointDeVenteId.value = storedPointDeVenteId;
|
||||
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
||||
|
||||
// Si la désignation n'est pas sauvegardée, on peut la récupérer
|
||||
if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) {
|
||||
await loadPointDeVenteDesignation();
|
||||
/// ✅ SIMPLIFIÉ: Charge les données utilisateur sans cache persistant
|
||||
Future<void> loadUserData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final storedUsername = prefs.getString('username') ?? '';
|
||||
final storedRole = prefs.getString('role') ?? '';
|
||||
final storedUserId = prefs.getInt('user_id') ?? 0;
|
||||
final storedPointDeVenteId = prefs.getInt('point_de_vente_id') ?? 0;
|
||||
final storedPointDeVenteDesignation = prefs.getString('point_de_vente_designation') ?? '';
|
||||
|
||||
if (storedUsername.isNotEmpty) {
|
||||
try {
|
||||
Users user = await AppDatabase.instance.getUser(storedUsername);
|
||||
|
||||
_username.value = user.username;
|
||||
_email.value = user.email;
|
||||
_name.value = user.name;
|
||||
_lastname.value = user.lastName;
|
||||
_password.value = user.password;
|
||||
_role.value = storedRole;
|
||||
_userId.value = storedUserId;
|
||||
_pointDeVenteId.value = storedPointDeVenteId;
|
||||
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
||||
|
||||
if (_pointDeVenteDesignation.value.isEmpty && _pointDeVenteId.value > 0) {
|
||||
await loadPointDeVenteDesignation();
|
||||
}
|
||||
|
||||
// ✅ Précharger les permissions en arrière-plan (non bloquant)
|
||||
_preloadPermissionsInBackground();
|
||||
|
||||
} catch (dbError) {
|
||||
print("❌ Erreur BDD, utilisation du fallback: $dbError");
|
||||
_username.value = storedUsername;
|
||||
_email.value = prefs.getString('email') ?? '';
|
||||
_role.value = storedRole;
|
||||
_name.value = prefs.getString('name') ?? '';
|
||||
_lastname.value = prefs.getString('lastname') ?? '';
|
||||
_userId.value = storedUserId;
|
||||
_pointDeVenteId.value = storedPointDeVenteId;
|
||||
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
||||
|
||||
// Précharger quand même
|
||||
_preloadPermissionsInBackground();
|
||||
}
|
||||
|
||||
} catch (dbError) {
|
||||
// Fallback
|
||||
_username.value = storedUsername;
|
||||
_email.value = prefs.getString('email') ?? '';
|
||||
_role.value = storedRole;
|
||||
_name.value = prefs.getString('name') ?? '';
|
||||
_lastname.value = prefs.getString('lastname') ?? '';
|
||||
_userId.value = storedUserId;
|
||||
_pointDeVenteId.value = storedPointDeVenteId;
|
||||
_pointDeVenteDesignation.value = storedPointDeVenteDesignation;
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Erreur lors du chargement des données utilisateur: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Erreur lors du chargement des données utilisateur: $e');
|
||||
}
|
||||
}
|
||||
Future<void> loadPointDeVenteDesignation() async {
|
||||
if (_pointDeVenteId.value <= 0) return;
|
||||
|
||||
try {
|
||||
final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value);
|
||||
if (pointDeVente != null) {
|
||||
_pointDeVenteDesignation.value = pointDeVente['designation'] as String;
|
||||
await saveUserData(); // Sauvegarder la désignation
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Erreur lors du chargement de la désignation du point de vente: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ NOUVELLE MÉTHODE : Mise à jour complète avec Users + credentials
|
||||
/// ✅ Précharge les permissions en arrière-plan (non bloquant)
|
||||
void _preloadPermissionsInBackground() {
|
||||
if (_username.value.isNotEmpty) {
|
||||
// Lancer en arrière-plan sans attendre
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
await _cacheService.preloadUserData(_username.value);
|
||||
} catch (e) {
|
||||
print("⚠️ Erreur préchargement permissions (non critique): $e");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadPointDeVenteDesignation() async {
|
||||
if (_pointDeVenteId.value <= 0) return;
|
||||
|
||||
try {
|
||||
final pointDeVente = await AppDatabase.instance.getPointDeVenteById(_pointDeVenteId.value);
|
||||
if (pointDeVente != null) {
|
||||
_pointDeVenteDesignation.value = pointDeVente['nom'] as String;
|
||||
await saveUserData();
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Erreur lors du chargement de la désignation du point de vente: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ Mise à jour avec préchargement des permissions
|
||||
void setUserWithCredentials(Users user, String role, int userId) {
|
||||
_username.value = user.username;
|
||||
_email.value = user.email;
|
||||
_role.value = role; // Rôle depuis les credentials
|
||||
_role.value = role;
|
||||
_name.value = user.name;
|
||||
_lastname.value = user.lastName;
|
||||
_password.value = user.password;
|
||||
_userId.value = userId; // ID depuis les credentials
|
||||
_userId.value = userId;
|
||||
_pointDeVenteId.value = user.pointDeVenteId ?? 0;
|
||||
|
||||
print("✅ Utilisateur mis à jour avec credentials:");
|
||||
print(" Username: ${_username.value}");
|
||||
print(" Name: ${_name.value}");
|
||||
print(" Email: ${_email.value}");
|
||||
print(" Role: ${_role.value}");
|
||||
print(" UserID: ${_userId.value}");
|
||||
|
||||
// Sauvegarder dans SharedPreferences
|
||||
saveUserData();
|
||||
|
||||
// ✅ Précharger immédiatement les permissions après connexion
|
||||
_preloadPermissionsInBackground();
|
||||
}
|
||||
|
||||
// ✅ MÉTHODE EXISTANTE AMÉLIORÉE
|
||||
void setUser(Users user) {
|
||||
_username.value = user.username;
|
||||
_email.value = user.email;
|
||||
@ -121,90 +144,107 @@ Future<void> loadPointDeVenteDesignation() async {
|
||||
_name.value = user.name;
|
||||
_lastname.value = user.lastName;
|
||||
_password.value = user.password;
|
||||
// Note: _userId reste inchangé si pas fourni
|
||||
|
||||
print("✅ Utilisateur mis à jour (méthode legacy):");
|
||||
print(" Username: ${_username.value}");
|
||||
print(" Role: ${_role.value}");
|
||||
|
||||
// Sauvegarder dans SharedPreferences
|
||||
saveUserData();
|
||||
_preloadPermissionsInBackground();
|
||||
}
|
||||
|
||||
// ✅ CORRECTION : Sauvegarder TOUTES les données importantes
|
||||
Future<void> saveUserData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await prefs.setString('username', _username.value);
|
||||
await prefs.setString('email', _email.value);
|
||||
await prefs.setString('role', _role.value);
|
||||
await prefs.setString('name', _name.value);
|
||||
await prefs.setString('lastname', _lastname.value);
|
||||
await prefs.setInt('user_id', _userId.value);
|
||||
await prefs.setInt('point_de_vente_id', _pointDeVenteId.value);
|
||||
await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value);
|
||||
|
||||
print("✅ Données sauvegardées avec succès dans SharedPreferences");
|
||||
} catch (e) {
|
||||
print('❌ Erreur lors de la sauvegarde des données utilisateur: $e');
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await prefs.setString('username', _username.value);
|
||||
await prefs.setString('email', _email.value);
|
||||
await prefs.setString('role', _role.value);
|
||||
await prefs.setString('name', _name.value);
|
||||
await prefs.setString('lastname', _lastname.value);
|
||||
await prefs.setInt('user_id', _userId.value);
|
||||
await prefs.setInt('point_de_vente_id', _pointDeVenteId.value);
|
||||
await prefs.setString('point_de_vente_designation', _pointDeVenteDesignation.value);
|
||||
|
||||
print("✅ Données sauvegardées avec succès");
|
||||
} catch (e) {
|
||||
print('❌ Erreur lors de la sauvegarde: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECTION : Vider TOUTES les données (SharedPreferences + Observables)
|
||||
Future<void> clearUserData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await prefs.remove('username');
|
||||
await prefs.remove('email');
|
||||
await prefs.remove('role');
|
||||
await prefs.remove('name');
|
||||
await prefs.remove('lastname');
|
||||
await prefs.remove('user_id');
|
||||
await prefs.remove('point_de_vente_id');
|
||||
await prefs.remove('point_de_vente_designation');
|
||||
|
||||
_username.value = '';
|
||||
_email.value = '';
|
||||
_role.value = '';
|
||||
_name.value = '';
|
||||
_lastname.value = '';
|
||||
_password.value = '';
|
||||
_userId.value = 0;
|
||||
_pointDeVenteId.value = 0;
|
||||
_pointDeVenteDesignation.value = '';
|
||||
|
||||
} catch (e) {
|
||||
print('❌ Erreur lors de l\'effacement des données utilisateur: $e');
|
||||
/// ✅ MODIFIÉ: Vider les données ET le cache de session
|
||||
Future<void> clearUserData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// ✅ IMPORTANT: Vider le cache de session
|
||||
_cacheService.clearAllCache();
|
||||
|
||||
// Effacer SharedPreferences
|
||||
await prefs.remove('username');
|
||||
await prefs.remove('email');
|
||||
await prefs.remove('role');
|
||||
await prefs.remove('name');
|
||||
await prefs.remove('lastname');
|
||||
await prefs.remove('user_id');
|
||||
await prefs.remove('point_de_vente_id');
|
||||
await prefs.remove('point_de_vente_designation');
|
||||
|
||||
// Effacer les observables
|
||||
_username.value = '';
|
||||
_email.value = '';
|
||||
_role.value = '';
|
||||
_name.value = '';
|
||||
_lastname.value = '';
|
||||
_password.value = '';
|
||||
_userId.value = 0;
|
||||
_pointDeVenteId.value = 0;
|
||||
_pointDeVenteDesignation.value = '';
|
||||
|
||||
print("✅ Données utilisateur et cache de session vidés");
|
||||
|
||||
} catch (e) {
|
||||
print('❌ Erreur lors de l\'effacement: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ MÉTHODE UTILITAIRE : Vérifier si un utilisateur est connecté
|
||||
// Getters utilitaires
|
||||
bool get isLoggedIn => _username.value.isNotEmpty && _userId.value > 0;
|
||||
|
||||
// ✅ MÉTHODE UTILITAIRE : Obtenir le nom complet
|
||||
String get fullName => '${_name.value} ${_lastname.value}'.trim();
|
||||
|
||||
/// ✅ OPTIMISÉ: Vérification des permissions depuis le cache de session
|
||||
Future<bool> hasPermission(String permission, String route) async {
|
||||
try {
|
||||
if (_username.value.isEmpty) {
|
||||
print('⚠️ Username vide, rechargement des données...');
|
||||
print('⚠️ Username vide, rechargement...');
|
||||
await loadUserData();
|
||||
}
|
||||
|
||||
if (_username.value.isEmpty) {
|
||||
print('❌ Impossible de vérifier les permissions : utilisateur non connecté');
|
||||
print('❌ Utilisateur non connecté');
|
||||
return false;
|
||||
}
|
||||
|
||||
return await AppDatabase.instance.hasPermission(username, permission, route);
|
||||
// Essayer d'abord le cache
|
||||
if (_cacheService.isLoaded) {
|
||||
return _cacheService.hasPermission(_username.value, permission, route);
|
||||
}
|
||||
|
||||
// Si pas encore chargé, charger et essayer de nouveau
|
||||
print("🔄 Cache non chargé, chargement des permissions...");
|
||||
await _cacheService.loadUserPermissions(_username.value);
|
||||
|
||||
return _cacheService.hasPermission(_username.value, permission, route);
|
||||
|
||||
} catch (e) {
|
||||
print('❌ Erreur vérification permission: $e');
|
||||
return false; // Sécurité : refuser l'accès en cas d'erreur
|
||||
// Fallback vers la méthode originale en cas d'erreur
|
||||
try {
|
||||
return await AppDatabase.instance.hasPermission(_username.value, permission, route);
|
||||
} catch (fallbackError) {
|
||||
print('❌ Erreur fallback permission: $fallbackError');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ Vérification de permissions multiples
|
||||
Future<bool> hasAnyPermission(List<String> permissionNames, String menuRoute) async {
|
||||
for (String permissionName in permissionNames) {
|
||||
if (await hasPermission(permissionName, menuRoute)) {
|
||||
@ -214,18 +254,40 @@ Future<void> clearUserData() async {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ MÉTHODE DEBUG : Afficher l'état actuel
|
||||
/// ✅ Obtenir les menus accessibles depuis le cache
|
||||
List<Map<String, dynamic>> getUserMenus() {
|
||||
if (_username.value.isEmpty || !_cacheService.isLoaded) return [];
|
||||
return _cacheService.getUserMenus(_username.value);
|
||||
}
|
||||
|
||||
/// ✅ Vérifier l'accès à un menu depuis le cache
|
||||
bool hasMenuAccess(String menuRoute) {
|
||||
if (_username.value.isEmpty || !_cacheService.isLoaded) return false;
|
||||
return _cacheService.hasMenuAccess(_username.value, menuRoute);
|
||||
}
|
||||
|
||||
/// ✅ Forcer le rechargement des permissions (pour les admins après modification)
|
||||
Future<void> refreshPermissions() async {
|
||||
if (_username.value.isNotEmpty) {
|
||||
await _cacheService.refreshUserPermissions(_username.value);
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ Vérifier si le cache est prêt
|
||||
bool get isCacheReady => _cacheService.isLoaded && _username.value.isNotEmpty;
|
||||
|
||||
/// Debug
|
||||
void debugPrintUserState() {
|
||||
print("=== ÉTAT UTILISATEUR ===");
|
||||
print("Username: ${_username.value}");
|
||||
print("Name: ${_name.value}");
|
||||
print("Lastname: ${_lastname.value}");
|
||||
print("Email: ${_email.value}");
|
||||
print("Role: ${_role.value}");
|
||||
print("UserID: ${_userId.value}");
|
||||
print("PointDeVenteID: ${_pointDeVenteId.value}");
|
||||
print("PointDeVente: ${_pointDeVenteDesignation.value}");
|
||||
print("IsLoggedIn: $isLoggedIn");
|
||||
print("========================");
|
||||
}
|
||||
print("=== ÉTAT UTILISATEUR ===");
|
||||
print("Username: ${_username.value}");
|
||||
print("Name: ${_name.value}");
|
||||
print("Role: ${_role.value}");
|
||||
print("UserID: ${_userId.value}");
|
||||
print("IsLoggedIn: $isLoggedIn");
|
||||
print("Cache Ready: $isCacheReady");
|
||||
print("========================");
|
||||
|
||||
// Debug du cache
|
||||
_cacheService.debugPrintCache();
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,7 @@ void main() async {
|
||||
|
||||
// Initialiser la base de données MySQL
|
||||
print("Connexion à la base de données MySQL...");
|
||||
await AppDatabase.instance.initDatabase();
|
||||
// await AppDatabase.instance.initDatabase();
|
||||
print("Base de données initialisée avec succès !");
|
||||
|
||||
// Afficher les informations de la base (pour debug)
|
||||
|
||||
@ -1,21 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'Views/ErreurPage.dart';
|
||||
import 'Views/loginPage.dart';
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
|
||||
|
||||
static bool isRegisterOpen = false;
|
||||
static DateTime? startDate;
|
||||
static late String path;
|
||||
|
||||
|
||||
static const Gradient primaryGradient = LinearGradient(
|
||||
colors: [
|
||||
Colors.white,
|
||||
const Color.fromARGB(255, 4, 54, 95),
|
||||
Color.fromARGB(255, 4, 54, 95),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
@ -24,56 +19,17 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
title: 'GUYCOM',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
canvasColor: Colors.transparent,
|
||||
),
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
return FutureBuilder<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(
|
||||
gradient: MyApp.primaryGradient,
|
||||
),
|
||||
child: const LoginPage(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
home: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: MyApp.primaryGradient,
|
||||
),
|
||||
child: const LoginPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> checkLocalDatabasesExist() async {
|
||||
final documentsDirectory = await getApplicationDocumentsDirectory();
|
||||
final dbPath = documentsDirectory.path;
|
||||
path = dbPath;
|
||||
|
||||
// Vérifier si le fichier de base de données products2.db existe
|
||||
final productsDBFile = File('$dbPath/products2.db');
|
||||
final productsDBExists = await productsDBFile.exists();
|
||||
|
||||
// Vérifier si le fichier de base de données auth.db existe
|
||||
final authDBFile = File('$dbPath/usersDb.db');
|
||||
final authDBExists = await authDBFile.exists();
|
||||
|
||||
// Vérifier si d'autres bases de données nécessaires existent, le cas échéant
|
||||
|
||||
return productsDBExists && authDBExists;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -872,6 +872,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
qr_code_scanner_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: qr_code_scanner_plus
|
||||
sha256: "39696b50d277097ee4d90d4292de36f38c66213a4f5216a06b2bdd2b63117859"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.10+1"
|
||||
qr_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@ -66,6 +66,7 @@ dependencies:
|
||||
mobile_scanner: ^5.0.0 # ou la version la plus récente
|
||||
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
|
||||
numbers_to_letters: ^1.0.0
|
||||
qr_code_scanner_plus: ^2.0.10+1
|
||||
|
||||
|
||||
|
||||
|
||||
@ -7,8 +7,9 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:youmazgestion/my_app.dart';
|
||||
|
||||
|
||||
import 'package:guycom/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user