diff --git a/lib/Services/stock_managementDatabase.dart b/lib/Services/stock_managementDatabase.dart index b1768c7..b82c3b5 100644 --- a/lib/Services/stock_managementDatabase.dart +++ b/lib/Services/stock_managementDatabase.dart @@ -45,6 +45,11 @@ class AppDatabase { await insertDefaultPointsDeVente(); } + +String _formatDate(DateTime date) { + return DateFormat('yyyy-MM-dd HH:mm:ss').format(date); +} + Future _initDB() async { try { final config = await DatabaseConfig.getSmartConfig(); @@ -2395,45 +2400,95 @@ Future getValeurTotaleStock() async { return erreurs; } // --- MÉTHODES POUR LES VENTES PAR POINT DE VENTE --- +Future>> getVentesParPointDeVente({ + DateTime? dateDebut, + DateTime? dateFin, + bool? aujourdHuiSeulement = false, +}) async { + final db = await database; - Future>> getVentesParPointDeVente() async { - final db = await database; + try { + String whereClause = 'WHERE c.statut != 5'; + List whereArgs = []; - try { - final result = await db.query(''' + if (aujourdHuiSeulement == true) { + final today = DateTime.now(); + final startOfDay = DateTime(today.year, today.month, today.day); + final endOfDay = DateTime(today.year, today.month, today.day, 23, 59, 59); + + whereClause += ' AND c.dateCommande >= ? AND c.dateCommande <= ?'; + whereArgs.addAll([ + _formatDate(startOfDay), + _formatDate(endOfDay), + ]); + } else if (dateDebut != null && dateFin != null) { + final adjustedEndDate = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59); + whereClause += ' AND c.dateCommande >= ? AND c.dateCommande <= ?'; + whereArgs.addAll([ + _formatDate(dateDebut), + _formatDate(adjustedEndDate), + ]); + } + + 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 + 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 + LEFT JOIN users u ON u.point_de_vente_id = pv.id + LEFT JOIN commandes c ON c.commandeurId = u.id + LEFT JOIN details_commandes dc ON dc.commandeId = c.id + $whereClause GROUP BY pv.id, pv.nom - ORDER BY chiffre_affaires DESC - '''); + ORDER BY chiffre_affaires DESC; + ''', whereArgs); - return result.map((row) => row.fields).toList(); - } catch (e) { - print("Erreur getVentesParPointDeVente: $e"); - return []; - } + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur getVentesParPointDeVente: $e"); + return []; } +} +Future>> getTopProduitsParPointDeVente( + int pointDeVenteId, { + int limit = 5, + DateTime? dateDebut, + DateTime? dateFin, + bool? aujourdHuiSeulement = false, +}) async { + final db = await database; - Future>> getTopProduitsParPointDeVente( - int pointDeVenteId, - {int limit = 5}) async { - final db = await database; + try { + String whereClause = 'WHERE p.point_de_vente_id = ? AND c.statut != 5'; + List whereArgs = [pointDeVenteId]; + + if (aujourdHuiSeulement == true) { + final today = DateTime.now(); + final startOfDay = DateTime(today.year, today.month, today.day); + final endOfDay = DateTime(today.year, today.month, today.day, 23, 59, 59); + + whereClause += ' AND c.dateCommande >= ? AND c.dateCommande <= ?'; + whereArgs.addAll([ + _formatDate(startOfDay), + _formatDate(endOfDay), + ]); + } else if (dateDebut != null && dateFin != null) { + final adjustedEndDate = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59); + whereClause += ' AND c.dateCommande >= ? AND c.dateCommande <= ?'; + whereArgs.addAll([ + _formatDate(dateDebut), + _formatDate(adjustedEndDate), + ]); + } - try { - final result = await db.query(''' + final result = await db.query(''' SELECT p.id, p.name as produit_nom, @@ -2445,828 +2500,831 @@ Future getValeurTotaleStock() async { 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 + $whereClause GROUP BY p.id, p.name, p.price, p.category ORDER BY quantite_vendue DESC LIMIT ? - ''', [pointDeVenteId, limit]); + ''', [...whereArgs, 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 []; - } + return result.map((row) => row.fields).toList(); + } catch (e) { + print("Erreur getTopProduitsParPointDeVente: $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'); + 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 []; + } } - - 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'); + + // 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; } - - final stockDisponible = stockSource.first['stock'] as int; - if (stockDisponible < quantite) { - throw Exception('Stock insuffisant dans le point de vente source'); + } + + // 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; } - - // 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'); + } + + 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 produitFields = produit.first.fields; + + 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(''' - 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 + 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; } } - // 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'); + + // 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 []; + } } - - // 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'); + + // 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 []; + } } - - // 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'); + + // 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': [], + }; + } } - - if (produit.stock != null && produit.stock! < quantite) { - throw Exception( - 'Stock insuffisant (disponible: ${produit.stock}, demandé: $quantite)'); + + // 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 []; + } } - - // 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'); + + // 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; + } } - - 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'); + + // --- 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; + } } - - if (produit.stock != null && produit.stock! < quantite) { - throw Exception('Stock insuffisant pour approuver cette sortie'); + + 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? pointDeVenteId, + int limit = 50, + }) async { + final db = await database; + + try { + String whereClause = ''; + List params = []; + + // Filtre par point de vente seulement si pointDeVenteId n'est pas null + // (null signifie que l'utilisateur a pointDeVenteId = 0 et peut tout voir) + if (pointDeVenteId != null) { + whereClause = 'WHERE sp.point_de_vente_id = ?'; + params.add(pointDeVenteId); + } + + if (adminId != null) { + whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' 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 []; } - - // 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? pointDeVenteId, - int limit = 50, -}) async { - final db = await database; - - try { - String whereClause = ''; - List params = []; - - // Filtre par point de vente seulement si pointDeVenteId n'est pas null - // (null signifie que l'utilisateur a pointDeVenteId = 0 et peut tout voir) - if (pointDeVenteId != null) { - whereClause = 'WHERE sp.point_de_vente_id = ?'; - params.add(pointDeVenteId); - } - - if (adminId != null) { - whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' 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': [], - }; + + 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': [], + }; + } + } } - } + + class _formatDate { } diff --git a/lib/Views/Dashboard.dart b/lib/Views/Dashboard.dart index 21e6d1a..5cff380 100644 --- a/lib/Views/Dashboard.dart +++ b/lib/Views/Dashboard.dart @@ -1100,7 +1100,6 @@ Widget _buildMiniStatistics() { //widget vente // 2. Ajoutez cette méthode dans la classe _DashboardPageState - Widget _buildVentesParPointDeVenteCard() { return Card( elevation: 4, @@ -1123,13 +1122,91 @@ Widget _buildVentesParPointDeVenteCard() { fontWeight: FontWeight.bold, ), ), + Spacer(), + // Boutons de filtre dans le header + Row( + children: [ + IconButton( + onPressed: _toggleTodayFilter, + icon: Icon( + _showOnlyToday ? Icons.today : Icons.calendar_today, + color: _showOnlyToday ? Colors.green : Colors.grey, + ), + tooltip: _showOnlyToday ? 'Toutes les dates' : 'Aujourd\'hui seulement', + ), + IconButton( + onPressed: () => _selectDateRange(context), + icon: Icon( + Icons.date_range, + color: _dateRange != null ? Colors.orange : Colors.grey, + ), + tooltip: 'Sélectionner une période', + ), + ], + ), ], ), + // Affichage de la période sélectionnée + if (_showOnlyToday || _dateRange != null) + Padding( + padding: EdgeInsets.only(bottom: 8), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: (_showOnlyToday ? Colors.green : Colors.orange).withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: (_showOnlyToday ? Colors.green : Colors.orange).withOpacity(0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _showOnlyToday ? Icons.today : Icons.date_range, + size: 16, + color: _showOnlyToday ? Colors.green : Colors.orange, + ), + SizedBox(width: 4), + Text( + _showOnlyToday + ? 'Aujourd\'hui' + : _dateRange != null + ? '${DateFormat('dd/MM/yyyy').format(_dateRange!.start)} - ${DateFormat('dd/MM/yyyy').format(_dateRange!.end)}' + : 'Toutes les dates', + style: TextStyle( + fontSize: 12, + color: _showOnlyToday ? Colors.green : Colors.orange, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 4), + InkWell( + onTap: () { + setState(() { + _showOnlyToday = false; + _dateRange = null; + }); + }, + child: Icon( + Icons.close, + size: 16, + color: _showOnlyToday ? Colors.green : Colors.orange, + ), + ), + ], + ), + ), + ), SizedBox(height: 16), Container( height: 400, child: FutureBuilder>>( - future: _database.getVentesParPointDeVente(), + future: _database.getVentesParPointDeVente( + dateDebut: _dateRange?.start, + dateFin: _dateRange?.end, + aujourdHuiSeulement: _showOnlyToday, + ), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); @@ -1142,7 +1219,11 @@ Widget _buildVentesParPointDeVenteCard() { children: [ Icon(Icons.store_mall_directory_outlined, size: 64, color: Colors.grey), SizedBox(height: 16), - Text('Aucune donnée de vente par point de vente', style: TextStyle(color: Colors.grey)), + Text( + 'Aucune donnée de vente${_showOnlyToday ? ' pour aujourd\'hui' : _dateRange != null ? ' pour cette période' : ''}', + style: TextStyle(color: Colors.grey), + textAlign: TextAlign.center, + ), ], ), ); @@ -1435,140 +1516,222 @@ String _formatCurrency(double value) { } } - void _toggleTodayFilter() { - setState(() { - _showOnlyToday = !_showOnlyToday; - }); - } - +void _toggleTodayFilter() { + setState(() { + _showOnlyToday = !_showOnlyToday; + if (_showOnlyToday) { + _dateRange = null; // Reset date range when showing only today + } + }); +} void _showPointVenteDetails(Map pointVenteData) async { final isMobile = MediaQuery.of(context).size.width < 600; final pointVenteId = pointVenteData['point_vente_id'] as int; final pointVenteNom = pointVenteData['point_vente_nom'] as String; - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Détails - $pointVenteNom'), - content: Container( - width: double.maxFinite, - height: 500, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, + + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text('Détails - $pointVenteNom'), + content: Container( + width: double.maxFinite, + height: 500, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ElevatedButton.icon( - onPressed: _toggleTodayFilter, - icon: Icon( - _showOnlyToday ? Icons.today : Icons.calendar_today, - size: 20, - ), - label: Text(_showOnlyToday - ? isMobile - ? 'Toutes dates' - : 'Toutes les dates' - : isMobile - ? 'Aujourd\'hui' - : 'Aujourd\'hui seulement'), - style: ElevatedButton.styleFrom( - backgroundColor: _showOnlyToday - ? Colors.green.shade600 - : Colors.blue.shade600, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 12 : 16, - vertical: 8, + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ElevatedButton.icon( + onPressed: () { + setDialogState(() { + _showOnlyToday = !_showOnlyToday; + if (_showOnlyToday) _dateRange = null; + }); + }, + icon: Icon( + _showOnlyToday ? Icons.today : Icons.calendar_today, + size: 20, + ), + label: Text(_showOnlyToday + ? isMobile + ? 'Toutes dates' + : 'Toutes les dates' + : isMobile + ? 'Aujourd\'hui' + : 'Aujourd\'hui seulement'), + style: ElevatedButton.styleFrom( + backgroundColor: _showOnlyToday + ? Colors.green.shade600 + : Colors.blue.shade600, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 16, + vertical: 8, + ), + ), ), - ), - ), - ElevatedButton.icon( - onPressed: () => _selectDateRange(context), - icon: const Icon(Icons.date_range, size: 20), - label: Text(_dateRange != null - ? isMobile - ? 'Période' - : 'Période sélectionnée' - : isMobile - ? 'Période' - : 'Choisir période'), - style: ElevatedButton.styleFrom( - backgroundColor: _dateRange != null - ? Colors.orange.shade600 - : Colors.grey.shade600, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 12 : 16, - vertical: 8, + ElevatedButton.icon( + onPressed: () async { + final DateTimeRange? picked = await showDateRangePicker( + context: context, + firstDate: DateTime(2020), + lastDate: DateTime.now().add(const Duration(days: 365)), + initialDateRange: _dateRange ?? + DateTimeRange( + start: DateTime.now().subtract(const Duration(days: 30)), + end: DateTime.now(), + ), + ); + + if (picked != null) { + setDialogState(() { + _dateRange = picked; + _showOnlyToday = false; + }); + } + }, + icon: const Icon(Icons.date_range, size: 20), + label: Text(_dateRange != null + ? isMobile + ? 'Période' + : 'Période sélectionnée' + : isMobile + ? 'Période' + : 'Choisir période'), + style: ElevatedButton.styleFrom( + backgroundColor: _dateRange != null + ? Colors.orange.shade600 + : Colors.grey.shade600, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 16, + vertical: 8, + ), + ), ), - ), + ], ), - ], - ), - // Statistiques générales - _buildStatRow('Chiffre d\'affaires:', '${NumberFormat('#,##0.00', 'fr_FR').format((pointVenteData['chiffre_affaires'] as num?)?.toDouble() ?? 0.0)} MGA'), - _buildStatRow('Nombre de commandes:', '${pointVenteData['nombre_commandes'] ?? 0}'), - _buildStatRow('Articles vendus:', '${pointVenteData['nombre_articles_vendus'] ?? 0}'), - _buildStatRow('Quantité totale:', '${pointVenteData['quantite_totale_vendue'] ?? 0}'), - _buildStatRow('Panier moyen:', '${NumberFormat('#,##0.00', 'fr_FR').format((pointVenteData['panier_moyen'] as num?)?.toDouble() ?? 0.0)} MGA'), - SizedBox(height: 16), - Text('Top 5 des produits:', style: TextStyle(fontWeight: FontWeight.bold)), - SizedBox(height: 8), + SizedBox(height: 16), + // Statistiques générales avec FutureBuilder pour refresh automatique + FutureBuilder>( + future: _getDetailedPointVenteStats(pointVenteId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } - SizedBox(height: 16), + if (snapshot.hasError || !snapshot.hasData) { + return Text('Erreur de chargement des statistiques'); + } - // ✅ Top produits - FutureBuilder>>( - future: _database.getTopProduitsParPointDeVente(pointVenteId), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } + final stats = snapshot.data!; + return Column( + children: [ + _buildStatRow('Chiffre d\'affaires:', '${NumberFormat('#,##0.00', 'fr_FR').format((stats['chiffre_affaires'] as num?)?.toDouble() ?? 0.0)} MGA'), + _buildStatRow('Nombre de commandes:', '${stats['nombre_commandes'] ?? 0}'), + _buildStatRow('Articles vendus:', '${stats['nombre_articles_vendus'] ?? 0}'), + _buildStatRow('Quantité totale:', '${stats['quantite_totale_vendue'] ?? 0}'), + _buildStatRow('Panier moyen:', '${NumberFormat('#,##0.00', 'fr_FR').format((stats['panier_moyen'] as num?)?.toDouble() ?? 0.0)} MGA'), + ], + ); + }, + ), - if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { - return Text('Aucun produit vendu', style: TextStyle(color: Colors.grey)); - } + SizedBox(height: 16), + Text('Top 5 des produits:', style: TextStyle(fontWeight: FontWeight.bold)), + SizedBox(height: 8), - final produits = snapshot.data!; - return Column( - children: produits.map((produit) => Padding( - padding: EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - produit['produit_nom'] ?? 'N/A', - style: TextStyle(fontSize: 12), - overflow: TextOverflow.ellipsis, - ), - ), - Text( - '${produit['quantite_vendue'] ?? 0} vendus', - style: TextStyle(fontSize: 12, color: Colors.grey), + // Top produits avec filtre + FutureBuilder>>( + future: _database.getTopProduitsParPointDeVente( + pointVenteId, + dateDebut: _dateRange?.start, + dateFin: _dateRange?.end, + aujourdHuiSeulement: _showOnlyToday, + ), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { + return Text('Aucun produit vendu${_showOnlyToday ? ' aujourd\'hui' : _dateRange != null ? ' pour cette période' : ''}', style: TextStyle(color: Colors.grey)); + } + + final produits = snapshot.data!; + return Column( + children: produits.map((produit) => Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + produit['produit_nom'] ?? 'N/A', + style: TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '${produit['quantite_vendue'] ?? 0} vendus', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], ), - ], - ), - )).toList(), - ); - }, + )).toList(), + ); + }, + ), + ], ), - ], + ), ), + actions: [ + if (_showOnlyToday || _dateRange != null) + TextButton( + onPressed: () { + setDialogState(() { + _showOnlyToday = false; + _dateRange = null; + }); + }, + child: Text('Réinitialiser'), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Fermer'), + ), + ], ), ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Fermer'), - ), - ], - ), -); + ); +} + +Future> _getDetailedPointVenteStats(int pointVenteId) async { + final ventesData = await _database.getVentesParPointDeVente( + dateDebut: _dateRange?.start, + dateFin: _dateRange?.end, + aujourdHuiSeulement: _showOnlyToday, + ); + + final pointVenteStats = ventesData.firstWhere( + (data) => data['point_vente_id'] == pointVenteId, + orElse: () => { + 'chiffre_affaires': 0.0, + 'nombre_commandes': 0, + 'nombre_articles_vendus': 0, + 'quantite_totale_vendue': 0, + 'panier_moyen': 0.0, + }, + ); + + return pointVenteStats; } Widget _buildStatRow(String label, String value) { diff --git a/lib/config/DatabaseConfig.dart b/lib/config/DatabaseConfig.dart index c21e9f7..2651345 100644 --- a/lib/config/DatabaseConfig.dart +++ b/lib/config/DatabaseConfig.dart @@ -11,7 +11,8 @@ class DatabaseConfig { static const String localDatabase = 'guycom'; // Production (public) MySQL settings - static const String prodHost = '185.70.105.157'; + // static const String prodHost = '185.70.105.157'; + static const String prodHost = '102.17.52.31'; static const String prodUsername = 'guycom'; static const String prodPassword = '3iV59wjRdbuXAPR'; static const String prodDatabase = 'guycom';