import 'dart:async'; import 'dart:math' as console; import 'package:get/get.dart'; import 'package:mysql1/mysql1.dart'; import 'package:youmazgestion/controller/userController.dart'; // Models import '../Models/users.dart'; import '../Models/role.dart'; import '../Models/Permission.dart'; import '../Models/client.dart'; import '../Models/produit.dart'; import '../config/DatabaseConfig.dart'; import 'package:intl/intl.dart'; class AppDatabase { static final AppDatabase instance = AppDatabase._init(); MySqlConnection? _connection; AppDatabase._init(); Future get database async { if (_connection != null) { try { // Test si la connexion est toujours active en exécutant une requête simple await _connection!.query('SELECT 1'); return _connection!; } catch (e) { // Si la requête échoue, la connexion est fermée, on la recrée print("Connexion MySQL fermée, reconnexion..."); _connection = null; } } _connection = await _initDB(); return _connection!; } Future initDatabase() async { _connection = await _initDB(); // await _createDB(); await insertDefaultPermissions(); await insertDefaultMenus(); await insertDefaultRoles(); await insertDefaultSuperAdmin(); await insertDefaultPointsDeVente(); } Future _initDB() async { try { final config = await DatabaseConfig.getSmartConfig(); // console.log(config as num); final settings = ConnectionSettings( host: config['host'], port: config['port'], user: config['user'], password: config['password'], db: config['database'], timeout: Duration(seconds: config['timeout']), ); final connection = await MySqlConnection.connect(settings); print("Connexion MySQL établie avec succès !"); return connection; } catch (e) { print("Erreur de connexion MySQL: $e"); rethrow; } throw Exception("La connexion MySQL n'a pas pu être établie."); } // --- MÉTHODES D'INSERTION PAR DÉFAUT --- // Future insertDefaultPermissions() async { 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"); } } // Future insertDefaultMenus() async { final db = await database; try { await _addMissingMenus(db); // Seulement ajouter les menus manquants } catch (e) { print("Erreur insertDefaultMenus: $e"); } } Future insertDefaultRoles() async { final db = await database; try { final existingRoles = await db.query('SELECT COUNT(*) as count FROM roles'); final count = existingRoles.first['count'] as int; if (count == 0) { // Créer les rôles final roles = ['Super Admin', 'Admin', 'User', 'commercial', 'caisse']; Map roleIds = {}; for (String role in roles) { final result = await db .query('INSERT INTO roles (designation) VALUES (?)', [role]); roleIds[role] = result.insertId!; } // Récupérer les permissions et menus final permissions = await db.query('SELECT * FROM permissions'); final menus = await db.query('SELECT * FROM menu'); // Assigner toutes les permissions à tous les menus pour le Super Admin final superAdminRoleId = roleIds['Super Admin']!; for (var menu in menus) { for (var permission in permissions) { await db.query(''' INSERT IGNORE INTO role_menu_permissions (role_id, menu_id, permission_id) VALUES (?, ?, ?) ''', [superAdminRoleId, menu['id'], permission['id']]); } } // Assigner quelques permissions à l'Admin et à l'User await _assignBasicPermissionsToRoles( db, roleIds['Admin']!, roleIds['User']!); print("Rôles par défaut créés et permissions assignées"); } else { await _updateExistingRolePermissions(db); } } catch (e) { print("Erreur insertDefaultRoles: $e"); } } Future insertDefaultPointsDeVente() async { final db = await database; try { final existing = await db.query('SELECT COUNT(*) as count FROM points_de_vente'); final count = existing.first['count'] as int; if (count == 0) { final defaultPoints = ['405A', '405B', '416', 'S405A', '417']; for (var point in defaultPoints) { try { await db.query( 'INSERT IGNORE INTO points_de_vente (nom) VALUES (?)', [point]); } catch (e) { print("Erreur insertion point de vente $point: $e"); } } print("Points de vente par défaut insérés"); } } catch (e) { print("Erreur insertDefaultPointsDeVente: $e"); } } Future insertDefaultSuperAdmin() async { final db = await database; try { final existingSuperAdmin = await db.query(''' SELECT u.* FROM users u INNER JOIN roles r ON u.role_id = r.id WHERE r.designation = 'Super Admin' '''); if (existingSuperAdmin.isEmpty) { final superAdminRole = await db.query( 'SELECT id FROM roles WHERE designation = ?', ['Super Admin']); if (superAdminRole.isNotEmpty) { final superAdminRoleId = superAdminRole.first['id']; await db.query(''' INSERT INTO users (name, lastname, email, password, username, role_id) VALUES (?, ?, ?, ?, ?, ?) ''', [ 'Super', 'Admin', 'superadmin@youmazgestion.com', 'admin123', 'superadmin', superAdminRoleId ]); print("Super Admin créé avec succès !"); print("Username: superadmin"); print("Password: admin123"); print( "ATTENTION: Changez ce mot de passe après la première connexion !"); } } else { print("Super Admin existe déjà"); } } catch (e) { print("Erreur insertDefaultSuperAdmin: $e"); } } // --- CRUD USERS --- Future createUser(Users user) async { final db = await database; final userMap = user.toMap(); userMap.remove('id'); // Remove ID for auto-increment final fields = userMap.keys.join(', '); final placeholders = List.filled(userMap.length, '?').join(', '); final result = await db.query( 'INSERT INTO users ($fields) VALUES ($placeholders)', userMap.values.toList()); return result.insertId!; } // ✅ CORRIGÉ: Méthode updateUser simplifiée et corrigée Future updateUser(Users user) async { final db = await database; try { print("🔄 Mise à jour utilisateur ID: ${user.id}"); print("Données: ${user.toMap()}"); final result = await db.query(''' UPDATE users SET name = ?, lastname = ?, email = ?, username = ?, password = ?, role_id = ?, point_de_vente_id = ? WHERE id = ? ''', [ user.name, user.lastName, user.email, user.username, user.password, user.roleId, user.pointDeVenteId, user.id ]); print( "✅ Utilisateur mis à jour. Lignes affectées: ${result.affectedRows}"); return result.affectedRows!; } catch (e) { print("❌ Erreur lors de la mise à jour de l'utilisateur: $e"); rethrow; } } // ✅ AJOUTÉ: Méthode pour vérifier si l'utilisateur existe Future userExists(int userId) async { final db = await database; try { final result = await db .query('SELECT COUNT(*) as count FROM users WHERE id = ?', [userId]); return (result.first['count'] as int) > 0; } catch (e) { print("❌ Erreur vérification existence utilisateur: $e"); return false; } } // ✅ AJOUTÉ: Méthode pour vérifier les contraintes avant mise à jour Future validateUserUpdate(Users user) async { final db = await database; try { // Vérifier si l'email existe déjà pour un autre utilisateur final emailCheck = await db.query( 'SELECT COUNT(*) as count FROM users WHERE email = ? AND id != ?', [user.email, user.id]); if ((emailCheck.first['count'] as int) > 0) { return 'Cet email est déjà utilisé par un autre utilisateur'; } // Vérifier si le username existe déjà pour un autre utilisateur final usernameCheck = await db.query( 'SELECT COUNT(*) as count FROM users WHERE username = ? AND id != ?', [user.username, user.id]); if ((usernameCheck.first['count'] as int) > 0) { return 'Ce nom d\'utilisateur est déjà utilisé'; } // Vérifier si le rôle existe final roleCheck = await db.query( 'SELECT COUNT(*) as count FROM roles WHERE id = ?', [user.roleId]); if ((roleCheck.first['count'] as int) == 0) { return 'Le rôle sélectionné n\'existe pas'; } // Vérifier si le point de vente existe (si spécifié) if (user.pointDeVenteId != null && user.pointDeVenteId! > 0) { final pointDeVenteCheck = await db.query( 'SELECT COUNT(*) as count FROM points_de_vente WHERE id = ?', [user.pointDeVenteId]); if ((pointDeVenteCheck.first['count'] as int) == 0) { return 'Le point de vente sélectionné n\'existe pas'; } } return null; // Aucune erreur } catch (e) { return 'Erreur lors de la validation: $e'; } } Future deleteUser(int id) async { final db = await database; final result = await db.query('DELETE FROM users WHERE id = ?', [id]); return result.affectedRows!; } Future> getAllUsers() async { final db = await database; final result = await db.query(''' SELECT users.*, roles.designation as role_name FROM users INNER JOIN roles ON users.role_id = roles.id ORDER BY users.id ASC '''); return result.map((row) => Users.fromMap(row.fields)).toList(); } // --- CRUD ROLES --- Future createRole(Role role) async { final db = await database; final result = await db.query( 'INSERT INTO roles (designation) VALUES (?)', [role.designation]); return result.insertId!; } Future> getRoles() async { final db = await database; final result = await db.query('SELECT * FROM roles ORDER BY designation ASC'); return result.map((row) => Role.fromMap(row.fields)).toList(); } Future updateRole(Role role) async { final db = await database; final result = await db.query( 'UPDATE roles SET designation = ? WHERE id = ?', [role.designation, role.id]); return result.affectedRows!; } Future deleteRole(int? id) async { final db = await database; final result = await db.query('DELETE FROM roles WHERE id = ?', [id]); return result.affectedRows!; } // --- PERMISSIONS --- Future> getAllPermissions() async { final db = await database; final result = await db.query('SELECT * FROM permissions ORDER BY name ASC'); return result.map((row) => Permission.fromMap(row.fields)).toList(); } Future> getPermissionsForRole(int roleId) async { final db = await database; final result = await db.query(''' SELECT p.id, p.name FROM permissions p JOIN role_permissions rp ON p.id = rp.permission_id WHERE rp.role_id = ? ORDER BY p.name ASC ''', [roleId]); return result.map((row) => Permission.fromMap(row.fields)).toList(); } Future> getPermissionsForRoleAndMenu( int roleId, int menuId) async { final db = await database; final result = await db.query(''' SELECT p.id, p.name FROM permissions p JOIN role_menu_permissions rmp ON p.id = rmp.permission_id WHERE rmp.role_id = ? AND rmp.menu_id = ? ORDER BY p.name ASC ''', [roleId, menuId]); return result.map((row) => Permission.fromMap(row.fields)).toList(); } // --- AUTHENTIFICATION --- Future verifyUser(String username, String password) async { final db = await database; final result = await db.query(''' SELECT COUNT(*) as count FROM users WHERE username = ? AND password = ? ''', [username, password]); return (result.first['count'] as int) > 0; } Future getUser(String username) async { final db = await database; final result = await db.query(''' SELECT users.*, roles.designation as role_name FROM users INNER JOIN roles ON users.role_id = roles.id WHERE users.username = ? ''', [username]); if (result.isNotEmpty) { return Users.fromMap(result.first.fields); } else { throw Exception('User not found'); } } Future?> getUserCredentials( String username, String password) async { final db = await database; final result = await db.query(''' SELECT users.username, users.id, roles.designation as role_name, roles.id as role_id FROM users INNER JOIN roles ON users.role_id = roles.id WHERE username = ? AND password = ? ''', [username, password]); if (result.isNotEmpty) { final row = result.first; return { 'id': row['id'], 'username': row['username'] as String, 'role': row['role_name'] as String, 'role_id': row['role_id'], }; } else { return null; } } // --- CRUD PRODUCTS --- Future createProduct(Product product) async { final db = await database; // Si le produit a un point_de_vente_id, on l'utilise directement if (product.pointDeVenteId != null && product.pointDeVenteId! > 0) { final productMap = product.toMap(); productMap.remove('id'); final fields = productMap.keys.join(', '); final placeholders = List.filled(productMap.length, '?').join(', '); final result = await db.query( 'INSERT INTO products ($fields) VALUES ($placeholders)', productMap.values.toList()); return result.insertId!; } // Sinon, on utilise le point de vente de l'utilisateur connecté final userCtrl = Get.find(); final currentPointDeVenteId = userCtrl.pointDeVenteId; final Map productData = product.toMap(); productData.remove('id'); if (currentPointDeVenteId > 0) { productData['point_de_vente_id'] = currentPointDeVenteId; } final fields = productData.keys.join(', '); final placeholders = List.filled(productData.length, '?').join(', '); final result = await db.query( 'INSERT INTO products ($fields) VALUES ($placeholders)', productData.values.toList()); return result.insertId!; } Future> getProducts() async { final db = await database; final result = await db.query('SELECT * FROM products ORDER BY name ASC'); return result.map((row) => Product.fromMap(row.fields)).toList(); } Future updateProduct(Product product) async { final db = await database; final productMap = product.toMap(); final id = productMap.remove('id'); final setClause = productMap.keys.map((key) => '$key = ?').join(', '); final values = [...productMap.values, id]; final result = await db.query('UPDATE products SET $setClause WHERE id = ?', values); return result.affectedRows!; } Future getProductById(int id) async { final db = await database; final result = await db.query('SELECT * FROM products WHERE id = ?', [id]); if (result.isNotEmpty) { return Product.fromMap(result.first.fields); } return null; } Future deleteProduct(int? id) async { final db = await database; final result = await db.query('DELETE FROM products WHERE id = ?', [id]); return result.affectedRows!; } // --- CRUD CLIENTS --- Future createClient(Client client) async { final db = await database; final clientMap = client.toMap(); clientMap.remove('id'); final fields = clientMap.keys.join(', '); final placeholders = List.filled(clientMap.length, '?').join(', '); final result = await db.query( 'INSERT INTO clients ($fields) VALUES ($placeholders)', clientMap.values.toList()); return result.insertId!; } Future> getClients() async { final db = await database; final result = await db.query( 'SELECT * FROM clients WHERE actif = 1 ORDER BY nom ASC, prenom ASC'); return result.map((row) => Client.fromMap(row.fields)).toList(); } Future getClientById(int id) async { final db = await database; final result = await db.query('SELECT * FROM clients WHERE id = ?', [id]); if (result.isNotEmpty) { return Client.fromMap(result.first.fields); } return null; } // --- POINTS DE VENTE --- Future>> getPointsDeVente() async { final db = await database; try { final result = await db.query( 'SELECT * FROM points_de_vente WHERE nom IS NOT NULL AND nom != "" ORDER BY nom ASC'); if (result.isEmpty) { print("Aucun point de vente trouvé dans la base de données"); await insertDefaultPointsDeVente(); final newResult = await db.query('SELECT * FROM points_de_vente ORDER BY nom ASC'); return newResult.map((row) => row.fields).toList(); } return result.map((row) => row.fields).toList(); } catch (e) { print("Erreur lors de la récupération des points de vente: $e"); return []; } } // --- STATISTIQUES --- Future> getStatistiques() async { final db = await database; final totalClients = await db.query('SELECT COUNT(*) as count FROM clients WHERE actif = 1'); final totalCommandes = await db.query('SELECT COUNT(*) as count FROM commandes'); final totalProduits = await db.query('SELECT COUNT(*) as count FROM products'); final chiffreAffaires = await db.query( 'SELECT SUM(montantTotal) as total FROM commandes WHERE statut != 5'); return { 'totalClients': totalClients.first['count'], 'totalCommandes': totalCommandes.first['count'], 'totalProduits': totalProduits.first['count'], 'chiffreAffaires': chiffreAffaires.first['total'] ?? 0.0, }; } // --- MÉTHODES UTILITAIRES --- // Future _addMissingMenus(MySqlConnection db) async { // final menusToAdd = [ // {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, // {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, // {'name': 'Points de vente', 'route': '/points-de-vente'}, // ]; // for (var menu in menusToAdd) { // final existing = await db.query( // 'SELECT COUNT(*) as count FROM menu WHERE route = ?', // [menu['route']] // ); // final count = existing.first['count'] as int; // if (count == 0) { // await db.query( // 'INSERT INTO menu (name, route) VALUES (?, ?)', // [menu['name'], menu['route']] // ); // print("Menu ajouté: ${menu['name']}"); // } // } // } Future _addMissingMenus(MySqlConnection db) async { final menusToAdd = [ {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'}, {'name': 'Gérer les commandes', 'route': '/gerer-commandes'}, {'name': 'Points de vente', 'route': '/points-de-vente'}, ]; for (var menu in menusToAdd) { final existing = await db.query( 'SELECT COUNT(*) as count FROM menu WHERE route = ?', [menu['route']]); final 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 _updateExistingRolePermissions(MySqlConnection db) async { final superAdminRole = await db .query('SELECT id FROM roles WHERE designation = ?', ['Super Admin']); if (superAdminRole.isNotEmpty) { final superAdminRoleId = superAdminRole.first['id']; final permissions = await db.query('SELECT * FROM permissions'); final menus = await db.query('SELECT * FROM menu'); // Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus for (var menu in menus) { for (var permission in permissions) { final existingPermission = await db.query(''' SELECT COUNT(*) as count FROM role_menu_permissions WHERE role_id = ? AND menu_id = ? AND permission_id = ? ''', [superAdminRoleId, menu['id'], permission['id']]); final count = existingPermission.first['count'] as int; if (count == 0) { await db.query(''' INSERT IGNORE INTO role_menu_permissions (role_id, menu_id, permission_id) VALUES (?, ?, ?) ''', [superAdminRoleId, menu['id'], permission['id']]); } } } // Assigner les permissions de base aux autres rôles pour les nouveaux menus final adminRole = await db .query('SELECT id FROM roles WHERE designation = ?', ['Admin']); final userRole = await db .query('SELECT id FROM roles WHERE designation = ?', ['User']); if (adminRole.isNotEmpty && userRole.isNotEmpty) { await _assignBasicPermissionsToRoles( db, adminRole.first['id'], userRole.first['id']); } print("Permissions mises à jour pour tous les rôles"); } } Future _assignBasicPermissionsToRoles( MySqlConnection db, int adminRoleId, int userRoleId) async { // Implémentation similaire mais adaptée pour MySQL print("Permissions de base assignées aux rôles Admin et User"); } // --- FERMETURE --- Future close() async { if (_connection != null) { try { await _connection!.close(); _connection = null; print("Connexion MySQL fermée"); } catch (e) { print("Erreur lors de la fermeture de la connexion: $e"); _connection = null; } } } // Pour le débogage - supprimer toutes les tables (équivalent à supprimer la DB) Future deleteDatabaseFile() async { final db = await database; try { // Désactiver les contraintes de clés étrangères temporairement await db.query('SET FOREIGN_KEY_CHECKS = 0'); // Lister toutes les tables final tables = await db.query('SHOW TABLES'); // Supprimer toutes les tables for (var table in tables) { final tableName = table.values?.first; await db.query('DROP TABLE IF EXISTS `$tableName`'); } // Réactiver les contraintes de clés étrangères await db.query('SET FOREIGN_KEY_CHECKS = 1'); print("Toutes les tables ont été supprimées"); } catch (e) { print("Erreur lors de la suppression des tables: $e"); } } Future printDatabaseInfo() async { final db = await database; print("=== INFORMATIONS DE LA BASE DE DONNÉES MYSQL ==="); try { final userCountResult = await db.query('SELECT COUNT(*) as count FROM users'); final userCount = userCountResult.first['count'] as int; print("Nombre d'utilisateurs: $userCount"); final users = await getAllUsers(); print("Utilisateurs:"); for (var user in users) { print(" - ${user.username} (${user.name}) - Email: ${user.email}"); } final roles = await getRoles(); print("Rôles:"); for (var role in roles) { print(" - ${role.designation} (ID: ${role.id})"); } final permissions = await getAllPermissions(); print("Permissions:"); for (var permission in permissions) { print(" - ${permission.name} (ID: ${permission.id})"); } print("========================================="); } catch (e) { print("Erreur lors de l'affichage des informations: $e"); } } // --- MÉTHODES SUPPLÉMENTAIRES POUR COMMANDES --- Future createCommande(Commande commande) async { final db = await database; final commandeMap = commande.toMap(); commandeMap.remove('id'); final fields = commandeMap.keys.join(', '); final placeholders = List.filled(commandeMap.length, '?').join(', '); final result = await db.query( 'INSERT INTO commandes ($fields) VALUES ($placeholders)', commandeMap.values.toList()); return result.insertId!; } Future> getCommandes() async { final db = await database; final result = await db.query(''' SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail FROM commandes c LEFT JOIN clients cl ON c.clientId = cl.id ORDER BY c.dateCommande DESC '''); return result.map((row) => Commande.fromMap(row.fields)).toList(); } Future getCommandeById(int id) async { final db = await database; final result = await db.query(''' SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail FROM commandes c LEFT JOIN clients cl ON c.clientId = cl.id WHERE c.id = ? ''', [id]); if (result.isNotEmpty) { return Commande.fromMap(result.first.fields); } return null; } Future updateCommande(Commande commande) async { final db = await database; final commandeMap = commande.toMap(); final id = commandeMap.remove('id'); final date = DateTime.parse(commandeMap['dateCommande']); commandeMap['dateCommande'] = DateFormat('yyyy-MM-dd HH:mm:ss').format(date); final setClause = commandeMap.keys.map((key) => '$key = ?').join(', '); final values = [...commandeMap.values, id]; final result = await db.query('UPDATE commandes SET $setClause WHERE id = ?', values); return result.affectedRows!; } Future deleteCommande(int id) async { final db = await database; final result = await db.query('DELETE FROM commandes WHERE id = ?', [id]); return result.affectedRows!; } // --- DÉTAILS COMMANDES --- Future createDetailCommande(DetailCommande detail) async { final db = await database; final detailMap = detail.toMap(); detailMap.remove('id'); final fields = detailMap.keys.join(', '); final placeholders = List.filled(detailMap.length, '?').join(', '); final result = await db.query( 'INSERT INTO details_commandes ($fields) VALUES ($placeholders)', detailMap.values.toList()); return result.insertId!; } // Méthode mise à jour pour récupérer les détails avec les remises Future> getDetailsCommande(int commandeId) async { final db = await database; final result = await db.query(''' SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference FROM details_commandes dc LEFT JOIN products p ON dc.produitId = p.id WHERE dc.commandeId = ? ORDER BY dc.est_cadeau ASC, dc.id ''', [commandeId]); return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); } // --- RECHERCHE PRODUITS --- Future getProductByReference(String reference) async { final db = await database; final result = await db .query('SELECT * FROM products WHERE reference = ?', [reference]); if (result.isNotEmpty) { return Product.fromMap(result.first.fields); } return null; } Future getProductByIMEI(String imei) async { final db = await database; final result = await db.query('SELECT * FROM products WHERE imei = ?', [imei]); if (result.isNotEmpty) { return Product.fromMap(result.first.fields); } return null; } Future> getCategories() async { final db = await database; final result = await db .query('SELECT DISTINCT category FROM products ORDER BY category'); return result.map((row) => row['category'] as String).toList(); } Future> getProductsByCategory(String category) async { final db = await database; final result = await db.query( 'SELECT * FROM products WHERE category = ? ORDER BY name ASC', [category]); return result.map((row) => Product.fromMap(row.fields)).toList(); } // --- RECHERCHE CLIENTS --- Future updateClient(Client client) async { final db = await database; final clientMap = client.toMap(); final id = clientMap.remove('id'); final setClause = clientMap.keys.map((key) => '$key = ?').join(', '); final values = [...clientMap.values, id]; final result = await db.query('UPDATE clients SET $setClause WHERE id = ?', values); return result.affectedRows!; } Future deleteClient(int id) async { final db = await database; // Soft delete final result = await db.query('UPDATE clients SET actif = 0 WHERE id = ?', [id]); return result.affectedRows!; } Future> searchClients(String query) async { final db = await database; final result = await db.query(''' SELECT * FROM clients WHERE actif = 1 AND (nom LIKE ? OR prenom LIKE ? OR email LIKE ?) ORDER BY nom ASC, prenom ASC ''', ['%$query%', '%$query%', '%$query%']); return result.map((row) => Client.fromMap(row.fields)).toList(); } Future getClientByEmail(String email) async { final db = await database; final result = await db.query( 'SELECT * FROM clients WHERE email = ? AND actif = 1 LIMIT 1', [email.trim().toLowerCase()]); if (result.isNotEmpty) { return Client.fromMap(result.first.fields); } return null; } Future findExistingClient({ String? email, String? telephone, String? nom, String? prenom, }) async { // Priorité 1: Recherche par email if (email != null && email.isNotEmpty) { final clientByEmail = await getClientByEmail(email); if (clientByEmail != null) { return clientByEmail; } } // Priorité 2: Recherche par téléphone if (telephone != null && telephone.isNotEmpty) { final db = await database; final result = await db.query( 'SELECT * FROM clients WHERE telephone = ? AND actif = 1 LIMIT 1', [telephone.trim()]); if (result.isNotEmpty) { return Client.fromMap(result.first.fields); } } // Priorité 3: Recherche par nom et prénom if (nom != null && nom.isNotEmpty && prenom != null && prenom.isNotEmpty) { final db = await database; final result = await db.query( 'SELECT * FROM clients WHERE LOWER(nom) = ? AND LOWER(prenom) = ? AND actif = 1 LIMIT 1', [nom.trim().toLowerCase(), prenom.trim().toLowerCase()]); if (result.isNotEmpty) { return Client.fromMap(result.first.fields); } } return null; } // --- UTILISATEURS SPÉCIALISÉS --- Future> getCommercialUsers() async { final db = await database; final result = await db.query(''' SELECT users.*, roles.designation as role_name FROM users INNER JOIN roles ON users.role_id = roles.id WHERE roles.designation = 'commercial' ORDER BY users.id ASC '''); return result.map((row) => Users.fromMap(row.fields)).toList(); } Future getUserById(int id) async { final db = await database; final result = await db.query(''' SELECT users.*, roles.designation as role_name FROM users INNER JOIN roles ON users.role_id = roles.id WHERE users.id = ? ''', [id]); if (result.isNotEmpty) { return Users.fromMap(result.first.fields); } return null; } Future getUserCount() async { final db = await database; final result = await db.query('SELECT COUNT(*) as count FROM users'); return result.first['count'] as int; } // --- PERMISSIONS AVANCÉES --- Future assignRoleMenuPermission( int roleId, int menuId, int permissionId) async { final db = await database; await db.query(''' INSERT IGNORE INTO role_menu_permissions (role_id, menu_id, permission_id) VALUES (?, ?, ?) ''', [roleId, menuId, permissionId]); } Future removeRoleMenuPermission( int roleId, int menuId, int permissionId) async { final db = await database; await db.query(''' DELETE FROM role_menu_permissions WHERE role_id = ? AND menu_id = ? AND permission_id = ? ''', [roleId, menuId, permissionId]); } Future isSuperAdmin(String username) async { final db = await database; final result = await db.query(''' SELECT COUNT(*) as count FROM users u INNER JOIN roles r ON u.role_id = r.id WHERE u.username = ? AND r.designation = 'Super Admin' ''', [username]); return (result.first['count'] as int) > 0; } Future hasPermission( String username, String permissionName, String menuRoute) async { final db = await database; final result = await db.query(''' SELECT COUNT(*) as count FROM permissions p JOIN role_menu_permissions rmp ON p.id = rmp.permission_id JOIN roles r ON rmp.role_id = r.id JOIN users u ON u.role_id = r.id JOIN menu m ON m.route = ? WHERE u.username = ? AND p.name = ? AND rmp.menu_id = m.id ''', [menuRoute, username, permissionName]); return (result.first['count'] as int) > 0; } // --- GESTION STOCK --- Future updateStock(int productId, int newStock) async { final db = await database; final result = await db.query( 'UPDATE products SET stock = ? WHERE id = ?', [newStock, productId]); return result.affectedRows!; } Future> getLowStockProducts({int threshold = 5}) async { final db = await database; final result = await db.query( 'SELECT * FROM products WHERE stock <= ? AND stock > 0 ORDER BY stock ASC', [threshold]); return result.map((row) => Product.fromMap(row.fields)).toList(); } // --- POINTS DE VENTE AVANCÉS --- Future createPointDeVente(String designation, String code) async { final db = await database; final result = await db.query( 'INSERT IGNORE INTO points_de_vente (nom) VALUES (?)', [designation]); return result.insertId ?? 0; } Future updatePointDeVente( int id, String newDesignation, String newCode) async { final db = await database; final result = await db.query( 'UPDATE points_de_vente SET nom = ? WHERE id = ?', [newDesignation, id]); return result.affectedRows!; } // Future deletePointDeVente(int id) async { // final db = await database; // final result = await db.query('DELETE FROM points_de_vente WHERE id = ?', [id]); // return result.affectedRows!; // } // Dans votre classe AppDatabase, remplacez la méthode deletePointDeVente par ceci : // SOLUTION 1: Vérification avant suppression avec gestion des produits Future deletePointDeVente(int id) async { final db = await database; try { await db.query('START TRANSACTION'); // 1. Vérifier s'il y a des produits liés à ce point de vente final produitsLies = await db.query( 'SELECT COUNT(*) as count FROM products WHERE point_de_vente_id = ?', [id]); final nombreProduits = produitsLies.first['count'] as int; if (nombreProduits > 0) { // Option A: Transférer les produits vers un point de vente par défaut // Récupérer le premier point de vente disponible (ou créer un "point de vente général") final pointsDeVente = await db.query( 'SELECT id FROM points_de_vente WHERE id != ? ORDER BY id ASC LIMIT 1', [id]); if (pointsDeVente.isNotEmpty) { final pointDeVenteParDefaut = pointsDeVente.first['id'] as int; // Transférer tous les produits vers le point de vente par défaut await db.query( 'UPDATE products SET point_de_vente_id = ? WHERE point_de_vente_id = ?', [pointDeVenteParDefaut, id]); print( "$nombreProduits produits transférés vers le point de vente ID $pointDeVenteParDefaut"); } else { // Si aucun autre point de vente, créer un point de vente "Général" final result = await db.query( 'INSERT INTO points_de_vente (nom) VALUES (?)', ['Général']); final nouveauPointId = result.insertId!; await db.query( 'UPDATE products SET point_de_vente_id = ? WHERE point_de_vente_id = ?', [nouveauPointId, id]); print( "Point de vente 'Général' créé et $nombreProduits produits transférés"); } } // 2. Maintenant supprimer le point de vente final deleteResult = await db.query('DELETE FROM points_de_vente WHERE id = ?', [id]); await db.query('COMMIT'); return deleteResult.affectedRows!; } catch (e) { await db.query('ROLLBACK'); print('Erreur lors de la suppression du point de vente: $e'); rethrow; } } // SOLUTION 2: Méthode alternative avec suppression douce (soft delete) Future deletePointDeVenteSoft(int id) async { final db = await database; try { // Ajouter une colonne 'actif' si elle n'existe pas déjà // Cette solution nécessite d'ajouter une colonne, mais c'est moins invasif // Pour l'instant, on peut utiliser une astuce en renommant le point de vente final timestamp = DateTime.now().millisecondsSinceEpoch; final result = await db.query( 'UPDATE points_de_vente SET nom = CONCAT(nom, " (Supprimé ", ?, ")") WHERE id = ?', [timestamp, id]); return result.affectedRows!; } catch (e) { print('Erreur lors de la suppression douce: $e'); rethrow; } } // SOLUTION 3: Vérification avec message d'erreur personnalisé Future> checkCanDeletePointDeVente(int id) async { final db = await database; try { // Vérifier les produits final produitsResult = await db.query( 'SELECT COUNT(*) as count FROM products WHERE point_de_vente_id = ?', [id]); final nombreProduits = produitsResult.first['count'] as int; // Vérifier les utilisateurs final usersResult = await db.query( 'SELECT COUNT(*) as count FROM users WHERE point_de_vente_id = ?', [id]); final nombreUsers = usersResult.first['count'] as int; // Vérifier les demandes de transfert final transfertsResult = await db.query(''' SELECT COUNT(*) as count FROM demandes_transfert WHERE point_de_vente_source_id = ? OR point_de_vente_destination_id = ? ''', [id, id]); final nombreTransferts = transfertsResult.first['count'] as int; if (nombreProduits > 0 || nombreUsers > 0 || nombreTransferts > 0) { return { 'canDelete': false, 'reasons': [ if (nombreProduits > 0) '$nombreProduits produit(s) associé(s)', if (nombreUsers > 0) '$nombreUsers utilisateur(s) associé(s)', if (nombreTransferts > 0) '$nombreTransferts demande(s) de transfert associée(s)', ], 'suggestions': [ if (nombreProduits > 0) 'Transférez d\'abord les produits vers un autre point de vente', if (nombreUsers > 0) 'Réassignez d\'abord les utilisateurs à un autre point de vente', if (nombreTransferts > 0) 'Les demandes de transfert resteront pour l\'historique', ] }; } return {'canDelete': true, 'reasons': [], 'suggestions': []}; } catch (e) { return { 'canDelete': false, 'reasons': ['Erreur lors de la vérification: $e'], 'suggestions': [] }; } } // SOLUTION 4: Suppression avec transfert automatique vers un point de vente spécifique Future deletePointDeVenteWithTransfer( int idToDelete, int targetPointDeVenteId) async { final db = await database; try { await db.query('START TRANSACTION'); // 1. Transférer tous les produits await db.query( 'UPDATE products SET point_de_vente_id = ? WHERE point_de_vente_id = ?', [targetPointDeVenteId, idToDelete]); // 2. Transférer tous les utilisateurs (si applicable) await db.query( 'UPDATE users SET point_de_vente_id = ? WHERE point_de_vente_id = ?', [targetPointDeVenteId, idToDelete]); // 3. Supprimer le point de vente final result = await db .query('DELETE FROM points_de_vente WHERE id = ?', [idToDelete]); await db.query('COMMIT'); return result.affectedRows!; } catch (e) { await db.query('ROLLBACK'); rethrow; } } // SOLUTION 5: Méthode pour obtenir les points de vente de destination possibles Future>> getPointsDeVenteForTransfer( int excludeId) async { final db = await database; try { final result = await db.query( 'SELECT * FROM points_de_vente WHERE id != ? ORDER BY nom ASC', [excludeId]); return result.map((row) => row.fields).toList(); } catch (e) { print('Erreur récupération points de vente pour transfert: $e'); return []; } } Future?> getPointDeVenteById(int id) async { final db = await database; final result = await db.query('SELECT * FROM points_de_vente WHERE id = ?', [id]); return result.isNotEmpty ? result.first.fields : null; } Future getOrCreatePointDeVenteByNom(String nom) async { final db = await database; // Vérifier si le point de vente existe déjà final existing = await db .query('SELECT id FROM points_de_vente WHERE nom = ?', [nom.trim()]); if (existing.isNotEmpty) { return existing.first['id'] as int; } // Créer le point de vente s'il n'existe pas try { final result = await db .query('INSERT INTO points_de_vente (nom) VALUES (?)', [nom.trim()]); print("Point de vente créé: $nom (ID: ${result.insertId})"); return result.insertId; } catch (e) { print("Erreur lors de la création du point de vente $nom: $e"); return null; } } Future getPointDeVenteNomById(int id) async { if (id == 0) return null; final db = await database; try { final result = await db .query('SELECT nom FROM points_de_vente WHERE id = ? LIMIT 1', [id]); return result.isNotEmpty ? result.first['nom'] as String : null; } catch (e) { print("Erreur getPointDeVenteNomById: $e"); return null; } } // --- RECHERCHE AVANCÉE --- Future> searchProducts({ String? name, String? imei, String? reference, bool onlyInStock = false, String? category, int? pointDeVenteId, }) async { final db = await database; List whereConditions = []; List whereArgs = []; if (name != null && name.isNotEmpty) { whereConditions.add('name LIKE ?'); whereArgs.add('%$name%'); } if (imei != null && imei.isNotEmpty) { whereConditions.add('imei LIKE ?'); whereArgs.add('%$imei%'); } if (reference != null && reference.isNotEmpty) { whereConditions.add('reference LIKE ?'); whereArgs.add('%$reference%'); } if (onlyInStock) { whereConditions.add('stock > 0'); } if (category != null && category.isNotEmpty) { whereConditions.add('category = ?'); whereArgs.add(category); } if (pointDeVenteId != null && pointDeVenteId > 0) { whereConditions.add('point_de_vente_id = ?'); whereArgs.add(pointDeVenteId); } String whereClause = whereConditions.isNotEmpty ? 'WHERE ${whereConditions.join(' AND ')}' : ''; final result = await db.query( 'SELECT * FROM products $whereClause ORDER BY name ASC', whereArgs); return result.map((row) => Product.fromMap(row.fields)).toList(); } Future findProductByCode(String code) async { final db = await database; // Essayer de trouver par référence d'abord var result = await db .query('SELECT * FROM products WHERE reference = ? LIMIT 1', [code]); if (result.isNotEmpty) { return Product.fromMap(result.first.fields); } // Ensuite par IMEI result = await db.query('SELECT * FROM products WHERE imei = ? LIMIT 1', [code]); if (result.isNotEmpty) { return Product.fromMap(result.first.fields); } // Enfin par QR code si disponible result = await db .query('SELECT * FROM products WHERE qrCode = ? LIMIT 1', [code]); if (result.isNotEmpty) { return Product.fromMap(result.first.fields); } return null; } // --- TRANSACTIONS COMPLEXES --- // Méthode pour créer une commande complète avec remises Future createCommandeComplete( Client client, Commande commande, List details) async { final db = await database; try { await db.query('START TRANSACTION'); // 1. Créer ou récupérer le client final existingOrNewClient = await createOrGetClient(client); final clientId = existingOrNewClient.id!; // 2. Créer la commande final commandeMap = commande.toMap(); commandeMap.remove('id'); commandeMap['clientId'] = clientId; final commandeFields = commandeMap.keys.join(', '); final commandePlaceholders = List.filled(commandeMap.length, '?').join(', '); final commandeResult = await db.query( 'INSERT INTO commandes ($commandeFields) VALUES ($commandePlaceholders)', commandeMap.values.toList()); final commandeId = commandeResult.insertId!; // 3. Créer les détails de commande avec remises for (final detail in details) { final detailMap = detail.toMap(); detailMap.remove('id'); detailMap['commandeId'] = commandeId; final detailFields = detailMap.keys.join(', '); final detailPlaceholders = List.filled(detailMap.length, '?').join(', '); await db.query( 'INSERT INTO details_commandes ($detailFields) VALUES ($detailPlaceholders)', detailMap.values.toList()); // 4. Mettre à jour le stock await db.query('UPDATE products SET stock = stock - ? WHERE id = ?', [detail.quantite, detail.produitId]); } await db.query('COMMIT'); return commandeId; } catch (e) { await db.query('ROLLBACK'); print("Erreur lors de la création de la commande complète: $e"); rethrow; } } // Méthode pour mettre à jour un détail de commande (utile pour modifier les remises) Future updateDetailCommande(DetailCommande detail) async { final db = await database; final detailMap = detail.toMap(); final id = detailMap.remove('id'); final setClause = detailMap.keys.map((key) => '$key = ?').join(', '); final values = [...detailMap.values, id]; final result = await db.query( 'UPDATE details_commandes SET $setClause WHERE id = ?', values); return result.affectedRows!; } // Méthode pour obtenir les statistiques des remises Future> getRemiseStatistics() async { final db = await database; try { // Total des remises accordées final totalRemisesResult = await db.query(''' SELECT COUNT(*) as nombre_remises, SUM(montant_remise) as total_remises, AVG(montant_remise) as moyenne_remise FROM details_commandes WHERE remise_type IS NOT NULL AND montant_remise > 0 '''); // Remises par type final remisesParTypeResult = await db.query(''' SELECT remise_type, COUNT(*) as nombre, SUM(montant_remise) as total, AVG(remise_valeur) as moyenne_valeur FROM details_commandes WHERE remise_type IS NOT NULL AND montant_remise > 0 GROUP BY remise_type '''); // Produits avec le plus de remises final produitsRemisesResult = await db.query(''' SELECT p.name as produit_nom, COUNT(*) as nombre_remises, SUM(dc.montant_remise) as total_remises FROM details_commandes dc INNER JOIN products p ON dc.produitId = p.id WHERE dc.remise_type IS NOT NULL AND dc.montant_remise > 0 GROUP BY dc.produitId, p.name ORDER BY total_remises DESC LIMIT 10 '''); return { 'total_remises': totalRemisesResult.first.fields, 'remises_par_type': remisesParTypeResult.map((row) => row.fields).toList(), 'produits_remises': produitsRemisesResult.map((row) => row.fields).toList(), }; } catch (e) { print("Erreur lors du calcul des statistiques de remises: $e"); return { 'total_remises': { 'nombre_remises': 0, 'total_remises': 0.0, 'moyenne_remise': 0.0 }, 'remises_par_type': [], 'produits_remises': [], }; } } // Méthode pour obtenir les commandes avec le plus de remises Future>> getCommandesAvecRemises( {int limit = 20}) async { final db = await database; try { final result = await db.query(''' SELECT c.id as commande_id, c.dateCommande, c.montantTotal, cl.nom as client_nom, cl.prenom as client_prenom, SUM(dc.montant_remise) as total_remises, COUNT(CASE WHEN dc.remise_type IS NOT NULL THEN 1 END) as nombre_articles_remise, COUNT(dc.id) as total_articles FROM commandes c INNER JOIN clients cl ON c.clientId = cl.id INNER JOIN details_commandes dc ON c.id = dc.commandeId GROUP BY c.id, c.dateCommande, c.montantTotal, cl.nom, cl.prenom HAVING total_remises > 0 ORDER BY total_remises DESC LIMIT ? ''', [limit]); return result.map((row) => row.fields).toList(); } catch (e) { print("Erreur lors de la récupération des commandes avec remises: $e"); return []; } } // --- STATISTIQUES AVANCÉES --- Future> getProductCountByCategory() async { final db = await database; final result = await db.query(''' SELECT category, COUNT(*) as count FROM products GROUP BY category ORDER BY count DESC '''); return Map.fromEntries(result.map( (row) => MapEntry(row['category'] as String, row['count'] as int))); } Future>> getStockStatsByCategory() async { final db = await database; final result = await db.query(''' SELECT category, COUNT(*) as total_products, SUM(CASE WHEN stock > 0 THEN 1 ELSE 0 END) as in_stock, SUM(CASE WHEN stock = 0 OR stock IS NULL THEN 1 ELSE 0 END) as out_of_stock, SUM(stock) as total_stock FROM products GROUP BY category ORDER BY category '''); Map> stats = {}; for (var row in result) { stats[row['category'] as String] = { 'total': row['total_products'] as int, 'in_stock': row['in_stock'] as int, 'out_of_stock': row['out_of_stock'] as int, 'total_stock': (row['total_stock'] as int?) ?? 0, }; } return stats; } Future>> getMostSoldProducts( {int limit = 10}) async { final db = await database; final result = await db.query(''' SELECT p.id, p.name, p.price, p.stock, p.category, SUM(dc.quantite) as total_sold, COUNT(DISTINCT dc.commandeId) as order_count FROM products p INNER JOIN details_commandes dc ON p.id = dc.produitId INNER JOIN commandes c ON dc.commandeId = c.id WHERE c.statut != 5 -- Exclure les commandes annulées GROUP BY p.id, p.name, p.price, p.stock, p.category ORDER BY total_sold DESC LIMIT ? ''', [limit]); return result.map((row) => row.fields).toList(); } // --- DÉBOGAGE --- Future debugPointsDeVenteTable() async { final db = await database; try { // Compte le nombre d'entrées final count = await db.query("SELECT COUNT(*) as count FROM points_de_vente"); print("Nombre de points de vente: ${count.first['count']}"); // Affiche le contenu final content = await db.query('SELECT * FROM points_de_vente'); print("Contenu de la table points_de_vente:"); for (var row in content) { print("ID: ${row['id']}, Nom: ${row['nom']}"); } } catch (e) { print("Erreur debug table points_de_vente: $e"); } } // 1. Méthodes pour les clients Future getClientByTelephone(String telephone) async { final db = await database; final result = await db.query( 'SELECT * FROM clients WHERE telephone = ? AND actif = 1 LIMIT 1', [telephone.trim()]); if (result.isNotEmpty) { return Client.fromMap(result.first.fields); } return null; } Future getClientByNomPrenom(String nom, String prenom) async { final db = await database; final result = await db.query( 'SELECT * FROM clients WHERE LOWER(nom) = ? AND LOWER(prenom) = ? AND actif = 1 LIMIT 1', [nom.trim().toLowerCase(), prenom.trim().toLowerCase()]); if (result.isNotEmpty) { return Client.fromMap(result.first.fields); } return null; } Future> suggestClients(String query) async { if (query.trim().isEmpty) return []; final db = await database; final searchQuery = '%${query.trim().toLowerCase()}%'; final result = await db.query(''' SELECT * FROM clients WHERE actif = 1 AND ( LOWER(nom) LIKE ? OR LOWER(prenom) LIKE ? OR LOWER(email) LIKE ? OR telephone LIKE ? ) ORDER BY nom ASC, prenom ASC LIMIT 10 ''', [searchQuery, searchQuery, searchQuery, searchQuery]); return result.map((row) => Client.fromMap(row.fields)).toList(); } Future> checkPotentialDuplicates({ required String nom, required String prenom, required String email, required String telephone, }) async { final db = await database; final result = await db.query(''' SELECT * FROM clients WHERE actif = 1 AND ( (LOWER(nom) = ? AND LOWER(prenom) = ?) OR email = ? OR telephone = ? ) ORDER BY nom ASC, prenom ASC ''', [ nom.trim().toLowerCase(), prenom.trim().toLowerCase(), email.trim().toLowerCase(), telephone.trim() ]); return result.map((row) => Client.fromMap(row.fields)).toList(); } Future createOrGetClient(Client newClient) async { final existingClient = await findExistingClient( email: newClient.email, telephone: newClient.telephone, nom: newClient.nom, prenom: newClient.prenom, ); if (existingClient != null) { return existingClient; } final clientId = await createClient(newClient); final createdClient = await getClientById(clientId); if (createdClient != null) { return createdClient; } else { throw Exception("Erreur lors de la création du client"); } } // 2. Méthodes pour les produits Future> getSimilarProducts(Product product, {int limit = 5}) async { final db = await database; final result = await db.query(''' SELECT * FROM products WHERE id != ? AND ( category = ? OR name LIKE ? ) ORDER BY CASE WHEN category = ? THEN 1 ELSE 2 END, name ASC LIMIT ? ''', [ product.id, product.category, '%${product.name.split(' ').first}%', product.category, limit ]); return result.map((row) => Product.fromMap(row.fields)).toList(); } // 3. Méthodes pour les commandes Future updateStatutCommande( int commandeId, StatutCommande statut) async { final db = await database; final result = await db.query( 'UPDATE commandes SET statut = ? WHERE id = ?', [statut.index, commandeId]); return result.affectedRows!; } Future> getCommandesByClient(int clientId) async { final db = await database; final result = await db.query(''' SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail FROM commandes c LEFT JOIN clients cl ON c.clientId = cl.id WHERE c.clientId = ? ORDER BY c.dateCommande DESC ''', [clientId]); return result.map((row) => Commande.fromMap(row.fields)).toList(); } Future> getCommandesByStatut(StatutCommande statut) async { final db = await database; final result = await db.query(''' SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail FROM commandes c LEFT JOIN clients cl ON c.clientId = cl.id WHERE c.statut = ? ORDER BY c.dateCommande DESC ''', [statut.index]); return result.map((row) => Commande.fromMap(row.fields)).toList(); } Future updateValidateurCommande(int commandeId, int validateurId) async { final db = await database; final result = await db.query(''' UPDATE commandes SET validateurId = ?, statut = ? WHERE id = ? ''', [validateurId, StatutCommande.confirmee.index, commandeId]); return result.affectedRows!; } // --- CRUD MENUS --- // Ajoutez ces méthodes dans votre classe AppDatabase Future>> getAllMenus() async { final db = await database; try { final result = await db.query('SELECT * FROM menu ORDER BY name ASC'); return result.map((row) => row.fields).toList(); } catch (e) { print("Erreur lors de la récupération des menus: $e"); return []; } } Future?> getMenuById(int id) async { final db = await database; try { final result = await db.query('SELECT * FROM menu WHERE id = ? LIMIT 1', [id]); return result.isNotEmpty ? result.first.fields : null; } catch (e) { print("Erreur getMenuById: $e"); return null; } } Future?> getMenuByRoute(String route) async { final db = await database; try { final result = await db.query('SELECT * FROM menu WHERE route = ? LIMIT 1', [route]); return result.isNotEmpty ? result.first.fields : null; } catch (e) { print("Erreur getMenuByRoute: $e"); return null; } } Future createMenu(String name, String route) async { final db = await database; try { // Vérifier si le menu existe déjà final existing = await db .query('SELECT COUNT(*) as count FROM menu WHERE route = ?', [route]); final count = existing.first['count'] as int; if (count > 0) { throw Exception('Un menu avec cette route existe déjà'); } final result = await db .query('INSERT INTO menu (name, route) VALUES (?, ?)', [name, route]); return result.insertId!; } catch (e) { print("Erreur createMenu: $e"); rethrow; } } Future updateMenu(int id, String name, String route) async { final db = await database; try { final result = await db.query( 'UPDATE menu SET name = ?, route = ? WHERE id = ?', [name, route, id]); return result.affectedRows!; } catch (e) { print("Erreur updateMenu: $e"); rethrow; } } Future deleteMenu(int id) async { final db = await database; try { // D'abord supprimer les permissions associées await db .query('DELETE FROM role_menu_permissions WHERE menu_id = ?', [id]); // Ensuite supprimer le menu final result = await db.query('DELETE FROM menu WHERE id = ?', [id]); return result.affectedRows!; } catch (e) { print("Erreur deleteMenu: $e"); rethrow; } } Future>> getMenusForRole(int roleId) async { final db = await database; try { final result = await db.query(''' SELECT DISTINCT m.* FROM menu m INNER JOIN role_menu_permissions rmp ON m.id = rmp.menu_id WHERE rmp.role_id = ? ORDER BY m.name ASC ''', [roleId]); return result.map((row) => row.fields).toList(); } catch (e) { print("Erreur getMenusForRole: $e"); return []; } } Future hasMenuAccess(int roleId, String menuRoute) async { final db = await database; try { final result = await db.query(''' SELECT COUNT(*) as count FROM role_menu_permissions rmp INNER JOIN menu m ON rmp.menu_id = m.id WHERE rmp.role_id = ? AND m.route = ? ''', [roleId, menuRoute]); return (result.first['count'] as int) > 0; } catch (e) { print("Erreur hasMenuAccess: $e"); return false; } } Future findClientByAnyIdentifier({ String? email, String? telephone, String? nom, String? prenom, }) async { // Recherche par email si fourni if (email != null && email.isNotEmpty) { final client = await getClientByEmail(email); if (client != null) return client; } // Recherche par téléphone si fourni if (telephone != null && telephone.isNotEmpty) { final client = await getClientByTelephone(telephone); if (client != null) return client; } // Recherche par nom et prénom si fournis if (nom != null && nom.isNotEmpty && prenom != null && prenom.isNotEmpty) { final client = await getClientByNomPrenom(nom, prenom); if (client != null) return client; } return null; } // // Méthode pour obtenir les statistiques des cadeaux Future> getCadeauStatistics() async { final db = await database; try { // Total des cadeaux offerts final totalCadeauxResult = await db.query(''' SELECT COUNT(*) as nombre_cadeaux, SUM(sousTotal) as valeur_totale_cadeaux, AVG(sousTotal) as valeur_moyenne_cadeau, SUM(quantite) as quantite_totale_cadeaux FROM details_commandes WHERE est_cadeau = 1 '''); // Cadeaux par produit final cadeauxParProduitResult = await db.query(''' SELECT p.name as produit_nom, p.category as produit_categorie, COUNT(*) as nombre_fois_offert, SUM(dc.quantite) as quantite_totale_offerte, SUM(dc.sousTotal) as valeur_totale_offerte FROM details_commandes dc INNER JOIN products p ON dc.produitId = p.id WHERE dc.est_cadeau = 1 GROUP BY dc.produitId, p.name, p.category ORDER BY quantite_totale_offerte DESC LIMIT 10 '''); // Commandes avec cadeaux final commandesAvecCadeauxResult = await db.query(''' SELECT COUNT(DISTINCT c.id) as nombre_commandes_avec_cadeaux, AVG(cadeau_stats.nombre_cadeaux_par_commande) as moyenne_cadeaux_par_commande, AVG(cadeau_stats.valeur_cadeaux_par_commande) as valeur_moyenne_cadeaux_par_commande FROM commandes c INNER JOIN ( SELECT commandeId, COUNT(*) as nombre_cadeaux_par_commande, SUM(sousTotal) as valeur_cadeaux_par_commande FROM details_commandes WHERE est_cadeau = 1 GROUP BY commandeId ) cadeau_stats ON c.id = cadeau_stats.commandeId '''); // Évolution des cadeaux par mois final evolutionMensuelleResult = await db.query(''' SELECT DATE_FORMAT(c.dateCommande, '%Y-%m') as mois, COUNT(dc.id) as nombre_cadeaux, SUM(dc.sousTotal) as valeur_cadeaux FROM details_commandes dc INNER JOIN commandes c ON dc.commandeId = c.id WHERE dc.est_cadeau = 1 AND c.dateCommande >= DATE_SUB(NOW(), INTERVAL 12 MONTH) GROUP BY DATE_FORMAT(c.dateCommande, '%Y-%m') ORDER BY mois DESC LIMIT 12 '''); return { 'total_cadeaux': totalCadeauxResult.first.fields, 'cadeaux_par_produit': cadeauxParProduitResult.map((row) => row.fields).toList(), 'commandes_avec_cadeaux': commandesAvecCadeauxResult.first.fields, 'evolution_mensuelle': evolutionMensuelleResult.map((row) => row.fields).toList(), }; } catch (e) { print("Erreur lors du calcul des statistiques de cadeaux: $e"); return { 'total_cadeaux': { 'nombre_cadeaux': 0, 'valeur_totale_cadeaux': 0.0, 'valeur_moyenne_cadeau': 0.0, 'quantite_totale_cadeaux': 0 }, 'cadeaux_par_produit': [], 'commandes_avec_cadeaux': { 'nombre_commandes_avec_cadeaux': 0, 'moyenne_cadeaux_par_commande': 0.0, 'valeur_moyenne_cadeaux_par_commande': 0.0 }, 'evolution_mensuelle': [], }; } } // Méthode pour obtenir les commandes avec des cadeaux Future>> getCommandesAvecCadeaux( {int limit = 20}) async { final db = await database; try { final result = await db.query(''' SELECT c.id as commande_id, c.dateCommande, c.montantTotal, cl.nom as client_nom, cl.prenom as client_prenom, cadeau_stats.nombre_cadeaux, cadeau_stats.valeur_cadeaux, cadeau_stats.quantite_cadeaux, (SELECT COUNT(*) FROM details_commandes WHERE commandeId = c.id) as total_articles FROM commandes c INNER JOIN clients cl ON c.clientId = cl.id INNER JOIN ( SELECT commandeId, COUNT(*) as nombre_cadeaux, SUM(sousTotal) as valeur_cadeaux, SUM(quantite) as quantite_cadeaux FROM details_commandes WHERE est_cadeau = 1 GROUP BY commandeId ) cadeau_stats ON c.id = cadeau_stats.commandeId ORDER BY cadeau_stats.valeur_cadeaux DESC LIMIT ? ''', [limit]); return result.map((row) => row.fields).toList(); } catch (e) { print("Erreur lors de la récupération des commandes avec cadeaux: $e"); return []; } } // Méthode pour obtenir les produits les plus offerts en cadeau Future>> getProduitsLesPlusOffertsEnCadeau( {int limit = 10}) async { final db = await database; try { final result = await db.query(''' SELECT p.id, p.name as produit_nom, p.price as prix_unitaire, p.category as categorie, p.stock, COUNT(dc.id) as nombre_fois_offert, SUM(dc.quantite) as quantite_totale_offerte, SUM(dc.sousTotal) as valeur_totale_offerte, COUNT(DISTINCT dc.commandeId) as nombre_commandes_distinctes FROM products p INNER JOIN details_commandes dc ON p.id = dc.produitId WHERE dc.est_cadeau = 1 GROUP BY p.id, p.name, p.price, p.category, p.stock ORDER BY quantite_totale_offerte DESC LIMIT ? ''', [limit]); return result.map((row) => row.fields).toList(); } catch (e) { print("Erreur lors de la récupération des produits les plus offerts: $e"); return []; } } // Méthode pour obtenir les clients qui ont reçu le plus de cadeaux Future>> getClientsAvecLePlusDeCadeaux( {int limit = 10}) async { final db = await database; try { final result = await db.query(''' SELECT cl.id as client_id, cl.nom, cl.prenom, cl.email, cl.telephone, COUNT(dc.id) as nombre_cadeaux_recus, SUM(dc.quantite) as quantite_cadeaux_recus, SUM(dc.sousTotal) as valeur_cadeaux_recus, COUNT(DISTINCT c.id) as nombre_commandes_avec_cadeaux FROM clients cl INNER JOIN commandes c ON cl.id = c.clientId INNER JOIN details_commandes dc ON c.id = dc.commandeId WHERE dc.est_cadeau = 1 GROUP BY cl.id, cl.nom, cl.prenom, cl.email, cl.telephone ORDER BY valeur_cadeaux_recus DESC LIMIT ? ''', [limit]); return result.map((row) => row.fields).toList(); } catch (e) { print( "Erreur lors de la récupération des clients avec le plus de cadeaux: $e"); return []; } } // Méthode pour calculer l'impact des cadeaux sur les ventes Future> getImpactCadeauxSurVentes() async { final db = await database; try { // Comparaison des commandes avec et sans cadeaux final comparisonResult = await db.query(''' SELECT 'avec_cadeaux' as type_commande, COUNT(DISTINCT c.id) as nombre_commandes, AVG(c.montantTotal) as panier_moyen, SUM(c.montantTotal) as chiffre_affaires_total FROM commandes c WHERE EXISTS ( SELECT 1 FROM details_commandes dc WHERE dc.commandeId = c.id AND dc.est_cadeau = 1 ) UNION ALL SELECT 'sans_cadeaux' as type_commande, COUNT(DISTINCT c.id) as nombre_commandes, AVG(c.montantTotal) as panier_moyen, SUM(c.montantTotal) as chiffre_affaires_total FROM commandes c WHERE NOT EXISTS ( SELECT 1 FROM details_commandes dc WHERE dc.commandeId = c.id AND dc.est_cadeau = 1 ) '''); // Ratio de conversion (commandes avec cadeaux / total commandes) final ratioResult = await db.query(''' SELECT (SELECT COUNT(DISTINCT c.id) FROM commandes c WHERE EXISTS ( SELECT 1 FROM details_commandes dc WHERE dc.commandeId = c.id AND dc.est_cadeau = 1 ) ) * 100.0 / COUNT(*) as pourcentage_commandes_avec_cadeaux FROM commandes '''); return { 'comparaison': comparisonResult.map((row) => row.fields).toList(), 'pourcentage_commandes_avec_cadeaux': ratioResult.first['pourcentage_commandes_avec_cadeaux'] ?? 0.0, }; } catch (e) { print("Erreur lors du calcul de l'impact des cadeaux: $e"); return { 'comparaison': [], 'pourcentage_commandes_avec_cadeaux': 0.0, }; } } // Méthode pour créer une commande complète avec cadeaux (mise à jour) Future createCommandeCompleteAvecCadeaux( Client client, Commande commande, List details) async { final db = await database; try { await db.query('START TRANSACTION'); // 1. Créer ou récupérer le client final existingOrNewClient = await createOrGetClient(client); final clientId = existingOrNewClient.id!; // 2. Créer la commande final commandeMap = commande.toMap(); commandeMap.remove('id'); commandeMap['clientId'] = clientId; final commandeFields = commandeMap.keys.join(', '); final commandePlaceholders = List.filled(commandeMap.length, '?').join(', '); final commandeResult = await db.query( 'INSERT INTO commandes ($commandeFields) VALUES ($commandePlaceholders)', commandeMap.values.toList()); final commandeId = commandeResult.insertId!; // 3. Créer les détails de commande avec remises et cadeaux for (final detail in details) { final detailMap = detail.toMap(); detailMap.remove('id'); detailMap['commandeId'] = commandeId; final detailFields = detailMap.keys.join(', '); final detailPlaceholders = List.filled(detailMap.length, '?').join(', '); await db.query( 'INSERT INTO details_commandes ($detailFields) VALUES ($detailPlaceholders)', detailMap.values.toList()); // 4. Mettre à jour le stock (même pour les cadeaux) await db.query('UPDATE products SET stock = stock - ? WHERE id = ?', [detail.quantite, detail.produitId]); } await db.query('COMMIT'); // Log des cadeaux offerts (optionnel) final cadeaux = details.where((d) => d.estCadeau).toList(); if (cadeaux.isNotEmpty) { print("Cadeaux offerts dans la commande $commandeId:"); for (final cadeau in cadeaux) { print( " - ${cadeau.produitNom} x${cadeau.quantite} (valeur: ${cadeau.sousTotal.toStringAsFixed(2)} MGA)"); } } return commandeId; } catch (e) { await db.query('ROLLBACK'); print( "Erreur lors de la création de la commande complète avec cadeaux: $e"); rethrow; } } // Méthode pour valider la disponibilité des cadeaux avant la commande Future> verifierDisponibiliteCadeaux( List details) async { final db = await database; List erreurs = []; try { for (final detail in details.where((d) => d.estCadeau)) { final produit = await getProductById(detail.produitId); if (produit == null) { erreurs.add("Produit cadeau introuvable (ID: ${detail.produitId})"); continue; } if (produit.stock != null && produit.stock! < detail.quantite) { erreurs.add( "Stock insuffisant pour le cadeau: ${produit.name} (demandé: ${detail.quantite}, disponible: ${produit.stock})"); } } } catch (e) { erreurs.add("Erreur lors de la vérification des cadeaux: $e"); } return erreurs; } // --- MÉTHODES POUR LES VENTES PAR POINT DE VENTE --- Future>> getVentesParPointDeVente() async { final db = await database; try { final result = await db.query(''' SELECT pv.id as point_vente_id, pv.nom as point_vente_nom, COUNT(DISTINCT c.id) as nombre_commandes, COUNT(dc.id) as nombre_articles_vendus, SUM(dc.quantite) as quantite_totale_vendue, SUM(c.montantTotal) as chiffre_affaires, AVG(c.montantTotal) as panier_moyen, MIN(c.dateCommande) as premiere_vente, MAX(c.dateCommande) as derniere_vente FROM points_de_vente pv LEFT JOIN products p ON pv.id = p.point_de_vente_id LEFT JOIN details_commandes dc ON p.id = dc.produitId LEFT JOIN commandes c ON dc.commandeId = c.id WHERE c.statut != 5 -- Exclure les commandes annulées GROUP BY pv.id, pv.nom ORDER BY chiffre_affaires DESC '''); return result.map((row) => row.fields).toList(); } catch (e) { print("Erreur getVentesParPointDeVente: $e"); return []; } } Future>> getTopProduitsParPointDeVente( int pointDeVenteId, {int limit = 5}) async { final db = await database; try { final result = await db.query(''' SELECT p.id, p.name as produit_nom, p.price as prix_unitaire, p.category as categorie, SUM(dc.quantite) as quantite_vendue, SUM(dc.sousTotal) as chiffre_affaires_produit, COUNT(DISTINCT dc.commandeId) as nombre_commandes FROM products p INNER JOIN details_commandes dc ON p.id = dc.produitId INNER JOIN commandes c ON dc.commandeId = c.id WHERE p.point_de_vente_id = ? AND c.statut != 5 GROUP BY p.id, p.name, p.price, p.category ORDER BY quantite_vendue DESC LIMIT ? ''', [pointDeVenteId, limit]); return result.map((row) => row.fields).toList(); } catch (e) { print("Erreur getTopProduitsParPointDeVente: $e"); return []; } } Future>> getVentesParPointDeVenteParMois( int pointDeVenteId) async { final db = await database; try { final result = await db.query(''' SELECT DATE_FORMAT(c.dateCommande, '%Y-%m') as mois, COUNT(DISTINCT c.id) as nombre_commandes, SUM(c.montantTotal) as chiffre_affaires, SUM(dc.quantite) as quantite_vendue FROM commandes c INNER JOIN details_commandes dc ON c.id = dc.commandeId INNER JOIN products p ON dc.produitId = p.id WHERE p.point_de_vente_id = ? AND c.statut != 5 AND c.dateCommande >= DATE_SUB(NOW(), INTERVAL 12 MONTH) GROUP BY DATE_FORMAT(c.dateCommande, '%Y-%m') ORDER BY mois DESC LIMIT 12 ''', [pointDeVenteId]); return result.map((row) => row.fields).toList(); } catch (e) { print("Erreur getVentesParPointDeVenteParMois: $e"); return []; } } // Dans la classe AppDatabase, ajoutez cette méthode : Future verifyCurrentUserPassword(String password) async { final db = await database; final userController = Get.find(); try { final result = await db.query(''' SELECT COUNT(*) as count FROM users WHERE id = ? AND password = ? ''', [userController.userId, password]); return (result.first['count'] as int) > 0; } catch (e) { print("Erreur lors de la vérification du mot de passe: $e"); return false; } } // Dans AppDatabase Future createDemandeTransfert({ required int produitId, required int pointDeVenteSourceId, required int pointDeVenteDestinationId, required int demandeurId, int quantite = 1, String? notes, }) async { final db = await database; try { final result = await db.query(''' INSERT INTO demandes_transfert ( produit_id, point_de_vente_source_id, point_de_vente_destination_id, demandeur_id, quantite, statut, date_demande, notes ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', [ produitId, pointDeVenteSourceId, pointDeVenteDestinationId, demandeurId, quantite, 'en_attente', // Statut initial DateTime.now().toUtc(), notes, ]); return result.insertId!; } catch (e) { print('Erreur création demande transfert: $e'); rethrow; } } Future>> getDemandesTransfertEnAttente() async { final db = await database; try { final result = await db.query(''' SELECT dt.*, p.name as produit_nom, p.reference as produit_reference, p.stock as stock_source, -- AJOUT : récupérer le stock du produit pv_source.nom as point_vente_source, pv_dest.nom as point_vente_destination, u.name as demandeur_nom FROM demandes_transfert dt JOIN products p ON dt.produit_id = p.id JOIN points_de_vente pv_source ON dt.point_de_vente_source_id = pv_source.id JOIN points_de_vente pv_dest ON dt.point_de_vente_destination_id = pv_dest.id JOIN users u ON dt.demandeur_id = u.id WHERE dt.statut = 'en_attente' ORDER BY dt.date_demande DESC '''); return result.map((row) => row.fields).toList(); } catch (e) { print('Erreur récupération demandes transfert: $e'); return []; } } Future validerTransfert(int demandeId, int validateurId) async { final db = await database; try { await db.query('START TRANSACTION'); // 1. Récupérer les infos de la demande final demande = await db.query( 'SELECT * FROM demandes_transfert WHERE id = ? FOR UPDATE', [demandeId]); if (demande.isEmpty) { throw Exception('Demande de transfert introuvable'); } final fields = demande.first.fields; final produitId = fields['produit_id'] as int; final quantite = fields['quantite'] as int; final sourceId = fields['point_de_vente_source_id'] as int; final destinationId = fields['point_de_vente_destination_id'] as int; final getpointDeventeSource = await db.query( 'Select point_de_vente_source_id FROM demandes_transfert WHERE id=?', [demandeId]); final getpointDeventeDest = await db.query( 'Select point_de_vente_destination_id FROM demandes_transfert WHERE id=?', [demandeId]); final getpointDeventeSourceValue = getpointDeventeSource.first.fields['point_de_vente_source_id']; final getpointDeventedestValue = getpointDeventeDest.first.fields['point_de_vente_destination_id']; if (getpointDeventeSourceValue == getpointDeventedestValue) { await db.query('update products set point_de_vente_id=? where id = ?', [getpointDeventedestValue, produitId]); } else { // 2. Vérifier le stock source final stockSource = await db.query( 'SELECT stock FROM products WHERE id = ? AND point_de_vente_id = ? FOR UPDATE', [produitId, sourceId]); if (stockSource.isEmpty) { throw Exception('Produit introuvable dans le point de vente source'); } final stockDisponible = stockSource.first['stock'] as int; if (stockDisponible < quantite) { throw Exception('Stock insuffisant dans le point de vente source'); } // 3. Mettre à jour le stock source await db.query( 'UPDATE products SET stock = stock - ? WHERE id = ? AND point_de_vente_id = ?', [quantite, produitId, sourceId]); // 4. Vérifier si le produit existe déjà dans le point de vente destination final produitDestination = await db.query( 'SELECT id, stock FROM products WHERE id = ? AND point_de_vente_id = ?', [produitId, destinationId]); if (produitDestination.isNotEmpty) { // Mettre à jour le stock existant await db.query( 'UPDATE products SET stock = stock + ? WHERE id = ? AND point_de_vente_id = ?', [quantite, produitId, destinationId]); } else { // Créer une copie du produit dans le nouveau point de vente final produit = await db .query('SELECT * FROM products WHERE id = ?', [produitId]); if (produit.isEmpty) { throw Exception('Produit introuvable'); } final produitFields = produit.first.fields; await db.query(''' INSERT INTO products ( name, price, image, category, stock, description, qrCode, reference, point_de_vente_id, marque, ram, memoire_interne, imei ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', [ produitFields['name'], produitFields['price'], produitFields['image'], produitFields['category'], quantite, // Nouveau stock produitFields['description'], produitFields['qrCode'], produitFields['reference'], destinationId, produitFields['marque'], produitFields['ram'], produitFields['memoire_interne'], null, // IMEI doit être unique donc on ne le copie pas ]); } } // 5. Mettre à jour le statut de la demande await db.query(''' UPDATE demandes_transfert SET statut = 'validee', validateur_id = ?, date_validation = ? WHERE id = ? ''', [validateurId, DateTime.now().toUtc(), demandeId]); await db.query('COMMIT'); return 1; } catch (e) { await db.query('ROLLBACK'); print('Erreur validation transfert: $e'); rethrow; } } // Ajoutez ces méthodes dans votre classe AppDatabase // 1. Méthode pour récupérer les demandes de transfert validées Future>> getDemandesTransfertValidees() async { final db = await database; try { final result = await db.query(''' SELECT dt.*, p.name as produit_nom, p.reference as produit_reference, p.stock as stock_source, pv_source.nom as point_vente_source, pv_dest.nom as point_vente_destination, u_demandeur.name as demandeur_nom, u_validateur.name as validateur_nom, u_validateur.lastname as validateur_lastname FROM demandes_transfert dt JOIN products p ON dt.produit_id = p.id JOIN points_de_vente pv_source ON dt.point_de_vente_source_id = pv_source.id JOIN points_de_vente pv_dest ON dt.point_de_vente_destination_id = pv_dest.id JOIN users u_demandeur ON dt.demandeur_id = u_demandeur.id LEFT JOIN users u_validateur ON dt.validateur_id = u_validateur.id WHERE dt.statut = 'validee' ORDER BY dt.date_validation DESC '''); return result.map((row) => row.fields).toList(); } catch (e) { print('Erreur récupération demandes transfert validées: $e'); return []; } } // 2. Méthode pour récupérer toutes les demandes de transfert Future>> getToutesDemandesTransfert() async { final db = await database; try { final result = await db.query(''' SELECT dt.*, p.name as produit_nom, p.reference as produit_reference, p.stock as stock_source, pv_source.nom as point_vente_source, pv_dest.nom as point_vente_destination, u_demandeur.name as demandeur_nom, u_validateur.name as validateur_nom, u_validateur.lastname as validateur_lastname FROM demandes_transfert dt JOIN products p ON dt.produit_id = p.id JOIN points_de_vente pv_source ON dt.point_de_vente_source_id = pv_source.id JOIN points_de_vente pv_dest ON dt.point_de_vente_destination_id = pv_dest.id JOIN users u_demandeur ON dt.demandeur_id = u_demandeur.id LEFT JOIN users u_validateur ON dt.validateur_id = u_validateur.id ORDER BY CASE dt.statut WHEN 'en_attente' THEN 1 WHEN 'validee' THEN 2 WHEN 'refusee' THEN 3 END, dt.date_demande DESC '''); return result.map((row) => row.fields).toList(); } catch (e) { print('Erreur récupération toutes demandes transfert: $e'); return []; } } // 3. Méthode pour rejeter une demande de transfert Future rejeterTransfert( int demandeId, int validateurId, String motif) async { final db = await database; try { await db.query('START TRANSACTION'); // Vérifier que la demande existe et est en attente final demande = await db.query( 'SELECT * FROM demandes_transfert WHERE id = ? AND statut = ? FOR UPDATE', [demandeId, 'en_attente']); if (demande.isEmpty) { throw Exception('Demande de transfert introuvable ou déjà traitée'); } // Mettre à jour le statut de la demande final result = await db.query(''' UPDATE demandes_transfert SET statut = 'refusee', validateur_id = ?, date_validation = ?, notes = CONCAT(COALESCE(notes, ''), CASE WHEN notes IS NULL OR notes = '' THEN '' ELSE '\n--- REJET ---\n' END, 'Rejetée le ', ?, ' par validateur ID ', ?, ': ', ?) WHERE id = ? ''', [ validateurId, DateTime.now().toUtc(), DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now()), validateurId, motif, demandeId ]); await db.query('COMMIT'); print( 'Demande de transfert $demandeId rejetée par l\'utilisateur $validateurId'); print('Motif: $motif'); return result.affectedRows!; } catch (e) { await db.query('ROLLBACK'); print('Erreur rejet transfert: $e'); rethrow; } } // 4. Méthode supplémentaire : récupérer les demandes de transfert refusées Future>> getDemandesTransfertRefusees() async { final db = await database; try { final result = await db.query(''' SELECT dt.*, p.name as produit_nom, p.reference as produit_reference, p.stock as stock_source, pv_source.nom as point_vente_source, pv_dest.nom as point_vente_destination, u_demandeur.name as demandeur_nom, u_validateur.name as validateur_nom, u_validateur.lastname as validateur_lastname FROM demandes_transfert dt JOIN products p ON dt.produit_id = p.id JOIN points_de_vente pv_source ON dt.point_de_vente_source_id = pv_source.id JOIN points_de_vente pv_dest ON dt.point_de_vente_destination_id = pv_dest.id JOIN users u_demandeur ON dt.demandeur_id = u_demandeur.id LEFT JOIN users u_validateur ON dt.validateur_id = u_validateur.id WHERE dt.statut = 'refusee' ORDER BY dt.date_validation DESC '''); return result.map((row) => row.fields).toList(); } catch (e) { print('Erreur récupération demandes transfert refusées: $e'); return []; } } // 5. Méthode pour récupérer les demandes par statut spécifique Future>> getDemandesTransfertParStatut( String statut) async { final db = await database; try { final result = await db.query(''' SELECT dt.*, p.name as produit_nom, p.reference as produit_reference, p.stock as stock_source, pv_source.nom as point_vente_source, pv_dest.nom as point_vente_destination, u_demandeur.name as demandeur_nom, u_validateur.name as validateur_nom, u_validateur.lastname as validateur_lastname FROM demandes_transfert dt JOIN products p ON dt.produit_id = p.id JOIN points_de_vente pv_source ON dt.point_de_vente_source_id = pv_source.id JOIN points_de_vente pv_dest ON dt.point_de_vente_destination_id = pv_dest.id JOIN users u_demandeur ON dt.demandeur_id = u_demandeur.id LEFT JOIN users u_validateur ON dt.validateur_id = u_validateur.id WHERE dt.statut = ? ORDER BY CASE WHEN dt.statut = 'en_attente' THEN dt.date_demande ELSE dt.date_validation END DESC ''', [statut]); return result.map((row) => row.fields).toList(); } catch (e) { print('Erreur récupération demandes transfert par statut: $e'); return []; } } // 6. Méthode pour récupérer les statistiques des transferts Future> getStatistiquesTransferts() async { final db = await database; try { // Statistiques générales final statsGenerales = await db.query(''' SELECT COUNT(*) as total_demandes, SUM(CASE WHEN statut = 'en_attente' THEN 1 ELSE 0 END) as en_attente, SUM(CASE WHEN statut = 'validee' THEN 1 ELSE 0 END) as validees, SUM(CASE WHEN statut = 'refusee' THEN 1 ELSE 0 END) as refusees, SUM(CASE WHEN statut = 'validee' THEN quantite ELSE 0 END) as quantite_totale_transferee FROM demandes_transfert '''); // Top des produits les plus transférés final topProduits = await db.query(''' SELECT p.name as produit_nom, p.category as categorie, COUNT(*) as nombre_demandes, SUM(dt.quantite) as quantite_totale, SUM(CASE WHEN dt.statut = 'validee' THEN dt.quantite ELSE 0 END) as quantite_validee FROM demandes_transfert dt JOIN products p ON dt.produit_id = p.id GROUP BY dt.produit_id, p.name, p.category ORDER BY quantite_totale DESC LIMIT 10 '''); // Points de vente les plus actifs final topPointsVente = await db.query(''' SELECT pv.nom as point_vente, COUNT(dt_source.id) as demandes_sortantes, COUNT(dt_dest.id) as demandes_entrantes, (COUNT(dt_source.id) + COUNT(dt_dest.id)) as total_activite FROM points_de_vente pv LEFT JOIN demandes_transfert dt_source ON pv.id = dt_source.point_de_vente_source_id LEFT JOIN demandes_transfert dt_dest ON pv.id = dt_dest.point_de_vente_destination_id WHERE (dt_source.id IS NOT NULL OR dt_dest.id IS NOT NULL) GROUP BY pv.id, pv.nom ORDER BY total_activite DESC LIMIT 10 '''); return { 'stats_generales': statsGenerales.first.fields, 'top_produits': topProduits.map((row) => row.fields).toList(), 'top_points_vente': topPointsVente.map((row) => row.fields).toList(), }; } catch (e) { print('Erreur récupération statistiques transferts: $e'); return { 'stats_generales': { 'total_demandes': 0, 'en_attente': 0, 'validees': 0, 'refusees': 0, 'quantite_totale_transferee': 0 }, 'top_produits': [], 'top_points_vente': [], }; } } // 7. Méthode pour récupérer l'historique des transferts d'un produit Future>> getHistoriqueTransfertsProduit( int produitId) async { final db = await database; try { final result = await db.query(''' SELECT dt.*, pv_source.nom as point_vente_source, pv_dest.nom as point_vente_destination, u_demandeur.name as demandeur_nom, u_validateur.name as validateur_nom FROM demandes_transfert dt JOIN points_de_vente pv_source ON dt.point_de_vente_source_id = pv_source.id JOIN points_de_vente pv_dest ON dt.point_de_vente_destination_id = pv_dest.id JOIN users u_demandeur ON dt.demandeur_id = u_demandeur.id LEFT JOIN users u_validateur ON dt.validateur_id = u_validateur.id WHERE dt.produit_id = ? ORDER BY dt.date_demande DESC ''', [produitId]); return result.map((row) => row.fields).toList(); } catch (e) { print('Erreur récupération historique transferts produit: $e'); return []; } } // 8. Méthode pour annuler une demande de transfert (si en attente) Future annulerDemandeTransfert(int demandeId, int utilisateurId) async { final db = await database; try { // Vérifier que la demande existe et est en attente final demande = await db.query( 'SELECT * FROM demandes_transfert WHERE id = ? AND statut = ? AND demandeur_id = ?', [demandeId, 'en_attente', utilisateurId]); if (demande.isEmpty) { throw Exception( 'Demande introuvable, déjà traitée, ou vous n\'êtes pas autorisé à l\'annuler'); } // Supprimer la demande (ou la marquer comme annulée si vous préférez garder l'historique) final result = await db.query( 'DELETE FROM demandes_transfert WHERE id = ? AND statut = ? AND demandeur_id = ?', [demandeId, 'en_attente', utilisateurId]); return result.affectedRows!; } catch (e) { print('Erreur annulation demande transfert: $e'); rethrow; } } // --- MÉTHODES POUR SORTIES STOCK PERSONNELLES --- Future createSortieStockPersonnelle({ required int produitId, required int adminId, required int quantite, required String motif, int? pointDeVenteId, String? notes, }) async { final db = await database; try { await db.query('START TRANSACTION'); // 1. Vérifier que le produit existe et a assez de stock final produit = await getProductById(produitId); if (produit == null) { throw Exception('Produit introuvable'); } if (produit.stock != null && produit.stock! < quantite) { throw Exception( 'Stock insuffisant (disponible: ${produit.stock}, demandé: $quantite)'); } // 2. Créer la demande de sortie final result = await db.query(''' INSERT INTO sorties_stock_personnelles ( produit_id, admin_id, quantite, motif, date_sortie, point_de_vente_id, notes, statut ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', [ produitId, adminId, quantite, motif, DateTime.now().toUtc(), pointDeVenteId, notes, 'en_attente', // Par défaut en attente d'approbation ]); await db.query('COMMIT'); return result.insertId!; } catch (e) { await db.query('ROLLBACK'); print('Erreur création sortie personnelle: $e'); rethrow; } } Future approuverSortiePersonnelle( int sortieId, int approbateurId) async { final db = await database; try { await db.query('START TRANSACTION'); // 1. Récupérer les détails de la sortie final sortie = await db.query( 'SELECT * FROM sorties_stock_personnelles WHERE id = ? AND statut = ?', [sortieId, 'en_attente']); if (sortie.isEmpty) { throw Exception('Sortie introuvable ou déjà traitée'); } final fields = sortie.first.fields; final produitId = fields['produit_id'] as int; final quantite = fields['quantite'] as int; // 2. Vérifier le stock actuel final produit = await getProductById(produitId); if (produit == null) { throw Exception('Produit introuvable'); } if (produit.stock != null && produit.stock! < quantite) { throw Exception('Stock insuffisant pour approuver cette sortie'); } // 3. Décrémenter le stock await db.query('UPDATE products SET stock = stock - ? WHERE id = ?', [quantite, produitId]); // 4. Marquer la sortie comme approuvée await db.query(''' UPDATE sorties_stock_personnelles SET statut = 'approuvee', approbateur_id = ?, date_approbation = ? WHERE id = ? ''', [approbateurId, DateTime.now().toUtc(), sortieId]); await db.query('COMMIT'); return 1; } catch (e) { await db.query('ROLLBACK'); print('Erreur approbation sortie: $e'); rethrow; } } Future refuserSortiePersonnelle( int sortieId, int approbateurId, String motifRefus) async { final db = await database; try { final result = await db.query(''' UPDATE sorties_stock_personnelles SET statut = 'refusee', approbateur_id = ?, date_approbation = ?, notes = CONCAT(COALESCE(notes, ''), '\n--- REFUS ---\n', ?) WHERE id = ? AND statut = 'en_attente' ''', [approbateurId, DateTime.now().toUtc(), motifRefus, sortieId]); return result.affectedRows!; } catch (e) { print('Erreur refus sortie: $e'); rethrow; } } Future>> getSortiesPersonnellesEnAttente() async { final db = await database; try { final result = await db.query(''' SELECT sp.*, p.name as produit_nom, p.reference as produit_reference, p.stock as stock_actuel, u_admin.name as admin_nom, u_admin.lastname as admin_nom_famille, pv.nom as point_vente_nom FROM sorties_stock_personnelles sp JOIN products p ON sp.produit_id = p.id JOIN users u_admin ON sp.admin_id = u_admin.id LEFT JOIN points_de_vente pv ON sp.point_de_vente_id = pv.id WHERE sp.statut = 'en_attente' ORDER BY sp.date_sortie DESC '''); return result.map((row) => row.fields).toList(); } catch (e) { print('Erreur récupération sorties en attente: $e'); return []; } } Future>> getHistoriqueSortiesPersonnelles({ int? adminId, String? statut, int limit = 50, }) async { final db = await database; try { String whereClause = ''; List params = []; if (adminId != null) { whereClause = 'WHERE sp.admin_id = ?'; params.add(adminId); } if (statut != null) { whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.statut = ?'; params.add(statut); } final result = await db.query(''' SELECT sp.*, p.name as produit_nom, p.reference as produit_reference, u_admin.name as admin_nom, u_admin.lastname as admin_nom_famille, u_approb.name as approbateur_nom, u_approb.lastname as approbateur_nom_famille, pv.nom as point_vente_nom FROM sorties_stock_personnelles sp JOIN products p ON sp.produit_id = p.id JOIN users u_admin ON sp.admin_id = u_admin.id LEFT JOIN users u_approb ON sp.approbateur_id = u_approb.id LEFT JOIN points_de_vente pv ON sp.point_de_vente_id = pv.id $whereClause ORDER BY sp.date_sortie DESC LIMIT ? ''', [...params, limit]); return result.map((row) => row.fields).toList(); } catch (e) { print('Erreur récupération historique sorties: $e'); return []; } } Future> getStatistiquesSortiesPersonnelles() async { final db = await database; try { // Total des sorties par statut final statsStatut = await db.query(''' SELECT statut, COUNT(*) as nombre, SUM(quantite) as quantite_totale FROM sorties_stock_personnelles GROUP BY statut '''); // Sorties par admin final statsAdmin = await db.query(''' SELECT u.name as admin_nom, u.lastname as admin_nom_famille, COUNT(*) as nombre_sorties, SUM(sp.quantite) as quantite_totale FROM sorties_stock_personnelles sp JOIN users u ON sp.admin_id = u.id WHERE sp.statut = 'approuvee' GROUP BY u.id, u.name, u.lastname ORDER BY quantite_totale DESC LIMIT 10 '''); // Produits les plus sortis final statsProduits = await db.query(''' SELECT p.name as produit_nom, p.reference as produit_reference, SUM(sp.quantite) as quantite_sortie FROM sorties_stock_personnelles sp JOIN products p ON sp.produit_id = p.id WHERE sp.statut = 'approuvee' GROUP BY p.id, p.name, p.reference ORDER BY quantite_sortie DESC LIMIT 10 '''); return { 'stats_statut': statsStatut.map((row) => row.fields).toList(), 'stats_admin': statsAdmin.map((row) => row.fields).toList(), 'stats_produits': statsProduits.map((row) => row.fields).toList(), }; } catch (e) { print('Erreur statistiques sorties: $e'); return { 'stats_statut': [], 'stats_admin': [], 'stats_produits': [], }; } } }