From 58154af6804f99e417117651f65bddc16446ca41 Mon Sep 17 00:00:00 2001 From: Stephane Date: Fri, 16 Jan 2026 12:17:06 +0300 Subject: [PATCH] modification 2026 --- .../CommandDetails.dart | 144 +- lib/Models/Client.dart | 56 +- lib/Services/stock_managementDatabase.dart | 1607 +++++++++-------- lib/Views/commandManagement.dart | 1482 ++++++++------- lib/Views/historique.dart | 30 +- 5 files changed, 1752 insertions(+), 1567 deletions(-) diff --git a/lib/Components/commandManagementComponents/CommandDetails.dart b/lib/Components/commandManagementComponents/CommandDetails.dart index 55afedf..57a519a 100644 --- a/lib/Components/commandManagementComponents/CommandDetails.dart +++ b/lib/Components/commandManagementComponents/CommandDetails.dart @@ -1,5 +1,3 @@ -// Remplacez complètement votre fichier CommandeDetails par celui-ci : - import 'package:flutter/material.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; @@ -24,7 +22,8 @@ class CommandeDetails extends StatelessWidget { ); } - Widget _buildTableCell(String text, {bool isAmount = false, Color? textColor}) { + Widget _buildTableCell(String text, + {bool isAmount = false, Color? textColor}) { return Padding( padding: const EdgeInsets.all(8.0), child: Text( @@ -66,7 +65,9 @@ class CommandeDetails extends StatelessWidget { ), ); } else { - return _buildTableCell('${NumberFormat('#,##0', 'fr_FR').format(detail.prixUnitaire)} MGA', isAmount: true); + return _buildTableCell( + '${NumberFormat('#,##0', 'fr_FR').format(detail.prixUnitaire)} MGA', + isAmount: true); } } @@ -130,7 +131,9 @@ class CommandeDetails extends StatelessWidget { ), ); } else { - return _buildTableCell('${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)} MGA', isAmount: true); + return _buildTableCell( + '${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)} MGA', + isAmount: true); } } @@ -148,13 +151,13 @@ class CommandeDetails extends StatelessWidget { } final details = snapshot.data!; - + // Calculer les totaux double sousTotal = 0; double totalRemises = 0; double totalFinal = 0; bool hasRemises = false; - + for (final detail in details) { sousTotal += detail.sousTotal; totalRemises += detail.montantRemise; @@ -170,7 +173,7 @@ class CommandeDetails extends StatelessWidget { decoration: BoxDecoration( color: hasRemises ? Colors.orange.shade50 : Colors.blue.shade50, borderRadius: BorderRadius.circular(8), - border: hasRemises + border: hasRemises ? Border.all(color: Colors.orange.shade200) : null, ), @@ -178,21 +181,27 @@ class CommandeDetails extends StatelessWidget { children: [ Icon( hasRemises ? Icons.discount : Icons.receipt_long, - color: hasRemises ? Colors.orange.shade700 : Colors.blue.shade700, + color: hasRemises + ? Colors.orange.shade700 + : Colors.blue.shade700, ), const SizedBox(width: 8), Text( - hasRemises ? 'Détails de la commande (avec remises)' : 'Détails de la commande', + hasRemises + ? 'Détails de la commande (avec remises)' + : 'Détails de la commande', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, - color: hasRemises ? Colors.orange.shade800 : Colors.black87, + color: + hasRemises ? Colors.orange.shade800 : Colors.black87, ), ), if (hasRemises) ...[ const Spacer(), Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(12), @@ -231,65 +240,76 @@ class CommandeDetails extends StatelessWidget { ], ), ...details.map((detail) => TableRow( - decoration: detail.aRemise - ? BoxDecoration( - color: const Color.fromARGB(255, 243, 191, 114), - border: Border( - left: BorderSide( - color: Colors.orange.shade300, - width: 3, - ), - ), - ) - : null, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - detail.produitNom ?? 'Produit inconnu', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - ), - ), - if (detail.aRemise) ...[ - const SizedBox(height: 2), - Row( - children: [ - Icon( - Icons.local_offer, - size: 12, - color: Colors.teal.shade700, + decoration: detail.aRemise + ? BoxDecoration( + color: const Color.fromARGB(255, 243, 191, 114), + border: Border( + left: BorderSide( + color: Colors.orange.shade300, + width: 3, ), - const SizedBox(width: 4), + ), + ) + : null, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + detail.produitNom ?? 'Produit inconnu', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + if (detail.produitImei != null) ...[ + const SizedBox(height: 2), Text( - 'Avec remise', - style: TextStyle( + 'IMEI: ${detail.produitImei}', + style: const TextStyle( fontSize: 10, - color: Colors.teal.shade700, + color: Colors.grey, fontStyle: FontStyle.italic, ), + ) + ], + if (detail.aRemise) ...[ + const SizedBox(height: 2), + Row( + children: [ + Icon( + Icons.local_offer, + size: 12, + color: Colors.teal.shade700, + ), + const SizedBox(width: 4), + Text( + 'Avec remise', + style: TextStyle( + fontSize: 10, + color: Colors.teal.shade700, + fontStyle: FontStyle.italic, + ), + ), + ], ), ], - ), - ], - ], - ), - ), - _buildTableCell('${detail.quantite}'), - _buildPriceColumn(detail), - if (hasRemises) _buildRemiseColumn(detail), - _buildTotalColumn(detail), - ], - )), + ], + ), + ), + _buildTableCell('${detail.quantite}'), + _buildPriceColumn(detail), + if (hasRemises) _buildRemiseColumn(detail), + _buildTotalColumn(detail), + ], + )), ], ), ), const SizedBox(height: 12), - + // Section des totaux Container( padding: const EdgeInsets.all(12), @@ -355,7 +375,7 @@ class CommandeDetails extends StatelessWidget { ), const Divider(height: 16), ], - + // Total final Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -385,4 +405,4 @@ class CommandeDetails extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/lib/Models/Client.dart b/lib/Models/Client.dart index 7e20e37..bd709f7 100644 --- a/lib/Models/Client.dart +++ b/lib/Models/Client.dart @@ -36,9 +36,9 @@ class Client { // Fonction helper améliorée pour parser les dates static DateTime _parseDateTime(dynamic dateValue) { if (dateValue == null) return DateTime.now(); - + if (dateValue is DateTime) return dateValue; - + if (dateValue is String) { try { return DateTime.parse(dateValue); @@ -47,13 +47,14 @@ class Client { return DateTime.now(); } } - + // Pour MySQL qui peut retourner un Timestamp if (dateValue is int) { return DateTime.fromMillisecondsSinceEpoch(dateValue); } - - print("Type de date non reconnu: ${dateValue.runtimeType}, valeur: $dateValue"); + + print( + "Type de date non reconnu: ${dateValue.runtimeType}, valeur: $dateValue"); return DateTime.now(); } @@ -73,11 +74,7 @@ class Client { String get nomComplet => '$prenom $nom'; } -enum StatutCommande { - enAttente, - confirmee, - annulee -} +enum StatutCommande { enAttente, confirmee, annulee } class Commande { final int? id; @@ -156,7 +153,7 @@ class Commande { statut: StatutCommande.values[(map['statut'] as int)], montantTotal: (map['montantTotal'] as num).toDouble(), notes: map['notes'] as String?, - dateLivraison: map['dateLivraison'] != null + dateLivraison: map['dateLivraison'] != null ? Client._parseDateTime(map['dateLivraison']) : null, commandeurId: map['commandeurId'] as int?, @@ -173,10 +170,7 @@ class Commande { } // REMPLACEZ COMPLÈTEMENT votre classe DetailCommande dans Models/client.dart par celle-ci : -enum RemiseType { - pourcentage, - montant -} +enum RemiseType { pourcentage, montant } class DetailCommande { final int? id; @@ -193,6 +187,7 @@ class DetailCommande { final String? produitNom; final String? produitImage; final String? produitReference; + final String? produitImei; // NOUVEAU : IMEI du produit, si applicable DetailCommande({ this.id, @@ -209,6 +204,7 @@ class DetailCommande { this.produitNom, this.produitImage, this.produitReference, + this.produitImei, }); // Constructeur pour créer un détail sans remise @@ -222,10 +218,11 @@ class DetailCommande { String? produitNom, String? produitImage, String? produitReference, + String? produitImei, }) { final sousTotal = quantite * prixUnitaire; final prixFinal = estCadeau ? 0.0 : sousTotal; - + return DetailCommande( id: id, commandeId: commandeId, @@ -238,6 +235,7 @@ class DetailCommande { produitNom: produitNom, produitImage: produitImage, produitReference: produitReference, + produitImei: produitImei, ); } @@ -251,6 +249,7 @@ class DetailCommande { String? produitNom, String? produitImage, String? produitReference, + String? produitImei, }) { return DetailCommande( id: id, @@ -264,6 +263,7 @@ class DetailCommande { produitNom: produitNom, produitImage: produitImage, produitReference: produitReference, + produitImei: produitImei, ); } @@ -274,18 +274,18 @@ class DetailCommande { }) { // Les remises ne s'appliquent pas aux cadeaux if (estCadeau) return this; - + double montantRemiseCalcule = 0.0; - + if (type == RemiseType.pourcentage) { final pourcentage = valeur.clamp(0.0, 100.0); montantRemiseCalcule = sousTotal * (pourcentage / 100); } else { montantRemiseCalcule = valeur.clamp(0.0, sousTotal); } - + final prixFinalCalcule = sousTotal - montantRemiseCalcule; - + return DetailCommande( id: id, commandeId: commandeId, @@ -301,6 +301,7 @@ class DetailCommande { produitNom: produitNom, produitImage: produitImage, produitReference: produitReference, + produitImei: produitImei, ); } @@ -321,6 +322,7 @@ class DetailCommande { produitNom: produitNom, produitImage: produitImage, produitReference: produitReference, + produitImei: produitImei, ); } @@ -341,6 +343,7 @@ class DetailCommande { produitNom: produitNom, produitImage: produitImage, produitReference: produitReference, + produitImei: produitImei, ); } @@ -361,12 +364,14 @@ class DetailCommande { produitNom: produitNom, produitImage: produitImage, produitReference: produitReference, + produitImei: produitImei, ); } // Getters utiles bool get aRemise => remiseType != null && montantRemise > 0 && !estCadeau; - + bool get aimei => produitImei != null; + double get pourcentageRemise { if (!aRemise) return 0.0; return (montantRemise / sousTotal) * 100; @@ -375,7 +380,7 @@ class DetailCommande { String get remiseDescription { if (estCadeau) return 'CADEAU'; if (!aRemise) return ''; - + if (remiseType == RemiseType.pourcentage) { return '-${remiseValeur.toStringAsFixed(0)}%'; } else { @@ -426,12 +431,13 @@ class DetailCommande { remiseType: type, remiseValeur: (map['remise_valeur'] as num?)?.toDouble() ?? 0.0, montantRemise: (map['montant_remise'] as num?)?.toDouble() ?? 0.0, - prixFinal: (map['prix_final'] as num?)?.toDouble() ?? - (map['sousTotal'] as num).toDouble(), + prixFinal: (map['prix_final'] as num?)?.toDouble() ?? + (map['sousTotal'] as num).toDouble(), estCadeau: (map['est_cadeau'] as int?) == 1, produitNom: map['produitNom'] as String?, produitImage: map['produitImage'] as String?, produitReference: map['produitReference'] as String?, + produitImei: map['produitImei'] as String?, ); } -} \ No newline at end of file +} diff --git a/lib/Services/stock_managementDatabase.dart b/lib/Services/stock_managementDatabase.dart index f806e39..14b081b 100644 --- a/lib/Services/stock_managementDatabase.dart +++ b/lib/Services/stock_managementDatabase.dart @@ -47,10 +47,9 @@ class AppDatabase { await insertDefaultPointsDeVente(); } - -String _formatDate(DateTime date) { - return DateFormat('yyyy-MM-dd HH:mm:ss').format(date); -} + String _formatDate(DateTime date) { + return DateFormat('yyyy-MM-dd HH:mm:ss').format(date); + } Future _initDB() async { try { @@ -598,7 +597,7 @@ String _formatDate(DateTime date) { await insertDefaultPointsDeVente(); final newResult = await db.query('SELECT * FROM points_de_vente ORDER BY nom ASC'); - print(newResult); + print(newResult); return newResult.map((row) => row.fields).toList(); } @@ -608,22 +607,22 @@ String _formatDate(DateTime date) { return []; } } -Future getValeurTotaleStock() async { - final db = await database; - try { - final result = await db.query( - 'SELECT SUM(stock * price) AS total_stock_value FROM products', - ); + Future getValeurTotaleStock() async { + final db = await database; - final value = result.first['total_stock_value']; - return (value != null) ? (value as num).toDouble() : 0.0; - } catch (e) { - print('Erreur lors du calcul de la valeur totale du stock : $e'); - return 0.0; - } -} + try { + final result = await db.query( + 'SELECT SUM(stock * price) AS total_stock_value FROM products', + ); + final value = result.first['total_stock_value']; + return (value != null) ? (value as num).toDouble() : 0.0; + } catch (e) { + print('Erreur lors du calcul de la valeur totale du stock : $e'); + return 0.0; + } + } // --- STATISTIQUES --- @@ -914,7 +913,8 @@ Future getValeurTotaleStock() async { dc.*, p.name as produitNom, p.image as produitImage, - p.reference as produitReference + p.reference as produitReference, + p.imei as produitImei FROM details_commandes dc LEFT JOIN products p ON dc.produitId = p.id WHERE dc.commandeId = ? @@ -923,49 +923,53 @@ Future getValeurTotaleStock() async { return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); } -Future>> getCommandesParPointDeVente( - int pointVenteId, { - DateTime? dateDebut, - DateTime? dateFin, - bool aujourdHuiSeulement = false, - int limit = 50, -}) async { - final db = await database; - - try { - String whereClause = 'WHERE u.point_de_vente_id = ? AND c.statut != 5'; - List whereArgs = [pointVenteId]; - - if (aujourdHuiSeulement) { - 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), - ]); - } else if (dateDebut != null) { - whereClause += ' AND c.dateCommande >= ?'; - whereArgs.add(_formatDate(dateDebut)); - } else if (dateFin != null) { - final adjustedEndDate = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59); - whereClause += ' AND c.dateCommande <= ?'; - whereArgs.add(_formatDate(adjustedEndDate)); - } - whereClause += ' ORDER BY c.dateCommande DESC LIMIT ?'; - whereArgs.add(limit); + Future>> getCommandesParPointDeVente( + int pointVenteId, { + DateTime? dateDebut, + DateTime? dateFin, + bool aujourdHuiSeulement = false, + int limit = 50, + }) async { + final db = await database; + + try { + String whereClause = 'WHERE u.point_de_vente_id = ? AND c.statut != 5'; + List whereArgs = [pointVenteId]; + + if (aujourdHuiSeulement) { + 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), + ]); + } else if (dateDebut != null) { + whereClause += ' AND c.dateCommande >= ?'; + whereArgs.add(_formatDate(dateDebut)); + } else if (dateFin != null) { + final adjustedEndDate = + DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59); + whereClause += ' AND c.dateCommande <= ?'; + whereArgs.add(_formatDate(adjustedEndDate)); + } - final result = await db.query(''' + whereClause += ' ORDER BY c.dateCommande DESC LIMIT ?'; + whereArgs.add(limit); + + final result = await db.query(''' SELECT c.*, cl.nom AS clientNom, cl.prenom AS clientPrenom, @@ -986,12 +990,12 @@ Future>> getCommandesParPointDeVente( $whereClause ''', whereArgs); - return result.map((row) => row.fields).toList(); - } catch (e) { - print('Erreur récupération commandes: $e'); - return []; + return result.map((row) => row.fields).toList(); + } catch (e) { + print('Erreur récupération commandes: $e'); + return []; + } } -} // --- RECHERCHE PRODUITS --- @@ -1025,10 +1029,11 @@ Future>> getCommandesParPointDeVente( } Future>> getPointsDeVentes() async { - final db = await database; - final result = await db.query('SELECT DISTINCT * FROM pointsdevente ORDER BY nom ASC'); - return result.map((row) => row.fields).toList(); -} + final db = await database; + final result = + await db.query('SELECT DISTINCT * FROM pointsdevente ORDER BY nom ASC'); + return result.map((row) => row.fields).toList(); + } Future> getProductsByCategory(String category) async { final db = await database; @@ -1240,7 +1245,6 @@ Future>> getCommandesParPointDeVente( [newDesignation, id]); return result.affectedRows!; } - // Future deletePointDeVente(int id) async { // final db = await database; @@ -1429,53 +1433,51 @@ Future>> getCommandesParPointDeVente( } } - 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 : null; -} -List parseHeaderInfo(dynamic blobData) { - if (blobData == null) 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 : null; + } - try { - String content = ''; + List parseHeaderInfo(dynamic blobData) { + if (blobData == null) return []; - print("=== TYPE DE DONNÉES BLOB === ${blobData.runtimeType}"); + try { + String content = ''; - if (blobData is String) { - content = blobData; - } else if (blobData is Uint8List || blobData is List) { - try { - content = utf8.decode(blobData); - } catch (eUtf8) { - print('❌ utf8.decode failed: $eUtf8'); + print("=== TYPE DE DONNÉES BLOB === ${blobData.runtimeType}"); + + if (blobData is String) { + content = blobData; + } else if (blobData is Uint8List || blobData is List) { try { - content = latin1.decode(blobData); - } catch (eLatin1) { - print('❌ latin1.decode failed: $eLatin1'); - content = String.fromCharCodes(blobData); + content = utf8.decode(blobData); + } catch (eUtf8) { + print('❌ utf8.decode failed: $eUtf8'); + try { + content = latin1.decode(blobData); + } catch (eLatin1) { + print('❌ latin1.decode failed: $eLatin1'); + content = String.fromCharCodes(blobData); + } } + } else { + content = blobData.toString(); } - } else { - content = blobData.toString(); - } - print('=== LIVRAISON BRUTE ===\n$content\n=== FIN ==='); + print('=== LIVRAISON BRUTE ===\n$content\n=== FIN ==='); - return content - .split('\n') - .map((line) => line.trim()) - .where((line) => line.isNotEmpty) - .toList(); - } catch (e) { - print('❌ Erreur lors du parsing des données d\'en-tête: $e'); - return []; + return content + .split('\n') + .map((line) => line.trim()) + .where((line) => line.isNotEmpty) + .toList(); + } catch (e) { + print('❌ Erreur lors du parsing des données d\'en-tête: $e'); + return []; + } } -} - - - Future getOrCreatePointDeVenteByNom(String nom) async { final db = await database; @@ -1976,48 +1978,47 @@ List parseHeaderInfo(dynamic blobData) { } // 3. Méthodes pour les commandes -Future updateStatutCommande( - int commandeId, StatutCommande statut) async { - final db = await database; - - try { - await db.query('START TRANSACTION'); - - // 🔹 Si le statut devient "annulée" - if (statut == StatutCommande.annulee) { - // 1. Récupérer les détails de la commande - final details = await db.query( - 'SELECT produitId, quantite FROM details_commandes WHERE commandeId = ?', - [commandeId], - ); + Future updateStatutCommande( + int commandeId, StatutCommande statut) async { + final db = await database; - // 2. Remettre le stock pour chaque produit - for (final row in details) { - final produitId = row['produitId']; - final quantite = row['quantite']; + try { + await db.query('START TRANSACTION'); - await db.query( - 'UPDATE products SET stock = stock + ? WHERE id = ?', - [quantite, produitId], + // 🔹 Si le statut devient "annulée" + if (statut == StatutCommande.annulee) { + // 1. Récupérer les détails de la commande + final details = await db.query( + 'SELECT produitId, quantite FROM details_commandes WHERE commandeId = ?', + [commandeId], ); + + // 2. Remettre le stock pour chaque produit + for (final row in details) { + final produitId = row['produitId']; + final quantite = row['quantite']; + + await db.query( + 'UPDATE products SET stock = stock + ? WHERE id = ?', + [quantite, produitId], + ); + } } - } - // 3. Mettre à jour le statut de la commande - final result = await db.query( - 'UPDATE commandes SET statut = ? WHERE id = ?', - [statut.index, commandeId], - ); + // 3. Mettre à jour le statut de la commande + final result = await db.query( + 'UPDATE commandes SET statut = ? WHERE id = ?', + [statut.index, commandeId], + ); - await db.query('COMMIT'); - return result.affectedRows!; - } catch (e) { - await db.query('ROLLBACK'); - print("Erreur lors de la mise à jour du statut de la commande: $e"); - rethrow; + await db.query('COMMIT'); + return result.affectedRows!; + } catch (e) { + await db.query('ROLLBACK'); + print("Erreur lors de la mise à jour du statut de la commande: $e"); + rethrow; + } } -} - Future> getCommandesByClient(int clientId) async { final db = await database; @@ -2557,40 +2558,42 @@ Future updateStatutCommande( 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; - - try { - // 🔹 On ne garde que les commandes confirmées (statut = 1) - String whereClause = "WHERE c.statut = 1"; - List whereArgs = []; - 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); +// --- MÉTHODES POUR LES VENTES PAR POINT DE VENTE --- + Future>> getVentesParPointDeVente({ + DateTime? dateDebut, + DateTime? dateFin, + bool? aujourdHuiSeulement = false, + }) async { + final db = await database; - 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 { + // 🔹 On ne garde que les commandes confirmées (statut = 1) + String whereClause = "WHERE c.statut = 1"; + List whereArgs = []; + + 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(''' + final result = await db.query(''' SELECT pv.id AS point_vente_id, pv.nom AS point_vente_nom, @@ -2610,46 +2613,48 @@ Future>> getVentesParPointDeVente({ 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; - - 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), - ]); - } + Future>> getTopProduitsParPointDeVente( + int pointDeVenteId, { + int limit = 5, + DateTime? dateDebut, + DateTime? dateFin, + bool? aujourdHuiSeulement = false, + }) async { + final db = await database; - final result = await db.query(''' + 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), + ]); + } + + final result = await db.query(''' SELECT p.id, p.name as produit_nom, @@ -2667,19 +2672,19 @@ Future>> getTopProduitsParPointDeVente( LIMIT ? ''', [...whereArgs, limit]); - return result.map((row) => row.fields).toList(); - } catch (e) { - print("Erreur getTopProduitsParPointDeVente: $e"); - return []; + 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(''' + 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, @@ -2695,46 +2700,46 @@ Future>> getTopProduitsParPointDeVente( 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(''' + + 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(''' + + 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, @@ -2746,27 +2751,27 @@ Future>> getTopProduitsParPointDeVente( 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(''' + 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, @@ -2782,113 +2787,113 @@ Future>> getTopProduitsParPointDeVente( 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 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'); } - - 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 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(''' + + 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(''' + 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', @@ -2896,22 +2901,22 @@ Future>> getTopProduitsParPointDeVente( 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(''' + + 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, @@ -2930,19 +2935,19 @@ Future>> getTopProduitsParPointDeVente( 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(''' + + 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, @@ -2966,33 +2971,33 @@ Future>> getTopProduitsParPointDeVente( END, dt.date_demande DESC '''); - - return result.map((row) => row.fields).toList(); - } catch (e) { - print('Erreur récupération toutes demandes transfert: $e'); - return []; - } + + 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'); } - - // 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(''' + + // Mettre à jour le statut de la demande + final result = await db.query(''' UPDATE demandes_transfert SET statut = 'refusee', @@ -3003,33 +3008,33 @@ Future>> getTopProduitsParPointDeVente( '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(''' + 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, @@ -3048,20 +3053,20 @@ Future>> getTopProduitsParPointDeVente( 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(''' + + 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, @@ -3084,21 +3089,21 @@ Future>> getTopProduitsParPointDeVente( 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(''' + + 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, @@ -3107,9 +3112,9 @@ Future>> getTopProduitsParPointDeVente( 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(''' + + // Top des produits les plus transférés + final topProduits = await db.query(''' SELECT p.name as produit_nom, p.category as categorie, @@ -3122,9 +3127,9 @@ Future>> getTopProduitsParPointDeVente( ORDER BY quantite_totale DESC LIMIT 10 '''); - - // Points de vente les plus actifs - final topPointsVente = await db.query(''' + + // Points de vente les plus actifs + final topPointsVente = await db.query(''' SELECT pv.nom as point_vente, COUNT(dt_source.id) as demandes_sortantes, @@ -3138,34 +3143,34 @@ Future>> getTopProduitsParPointDeVente( 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(''' + + 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, @@ -3179,69 +3184,69 @@ Future>> getTopProduitsParPointDeVente( 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 []; - } + + 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'); } - - // 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; - } + + // 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'); } - - // --- 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(''' + + 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, @@ -3253,61 +3258,61 @@ Future>> getTopProduitsParPointDeVente( 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; - } + 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'); } - - 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(''' + + 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', @@ -3315,22 +3320,22 @@ Future>> getTopProduitsParPointDeVente( 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(''' + + 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', @@ -3339,19 +3344,19 @@ Future>> getTopProduitsParPointDeVente( 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(''' + + 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, @@ -3366,76 +3371,84 @@ Future>> getTopProduitsParPointDeVente( 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 []; - } - } - - // 1. Mise à jour de la méthode dans stock_managementDatabase.dart -Future>> getHistoriqueSortiesPersonnelles({ - int? adminId, - String? statut, - int? pointDeVenteId, - DateTime? dateDebut, - DateTime? dateFin, - bool aujourdHuiSeulement = false, - 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 - 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); - } - - // Nouveau filtre par date - if (aujourdHuiSeulement) { - 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 += (whereClause.isEmpty ? 'WHERE' : ' AND') + - ' sp.date_sortie >= ? AND sp.date_sortie <= ?'; - params.add(startOfDay.toIso8601String()); - params.add(endOfDay.toIso8601String()); - } else if (dateDebut != null && dateFin != null) { - final startOfDay = DateTime(dateDebut.year, dateDebut.month, dateDebut.day); - final endOfDay = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59); - - whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + - ' sp.date_sortie >= ? AND sp.date_sortie <= ?'; - params.add(startOfDay.toIso8601String()); - params.add(endOfDay.toIso8601String()); - } else if (dateDebut != null) { - final startOfDay = DateTime(dateDebut.year, dateDebut.month, dateDebut.day); - whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.date_sortie >= ?'; - params.add(startOfDay.toIso8601String()); - } else if (dateFin != null) { - final endOfDay = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59); - whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.date_sortie <= ?'; - params.add(endOfDay.toIso8601String()); + return result.map((row) => row.fields).toList(); + } catch (e) { + print('Erreur récupération sorties en attente: $e'); + return []; } + } - final result = await db.query(''' + // 1. Mise à jour de la méthode dans stock_managementDatabase.dart + Future>> getHistoriqueSortiesPersonnelles({ + int? adminId, + String? statut, + int? pointDeVenteId, + DateTime? dateDebut, + DateTime? dateFin, + bool aujourdHuiSeulement = false, + 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 + 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); + } + + // Nouveau filtre par date + if (aujourdHuiSeulement) { + 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 += (whereClause.isEmpty ? 'WHERE' : ' AND') + + ' sp.date_sortie >= ? AND sp.date_sortie <= ?'; + params.add(startOfDay.toIso8601String()); + params.add(endOfDay.toIso8601String()); + } else if (dateDebut != null && dateFin != null) { + final startOfDay = + DateTime(dateDebut.year, dateDebut.month, dateDebut.day); + final endOfDay = + DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59); + + whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + + ' sp.date_sortie >= ? AND sp.date_sortie <= ?'; + params.add(startOfDay.toIso8601String()); + params.add(endOfDay.toIso8601String()); + } else if (dateDebut != null) { + final startOfDay = + DateTime(dateDebut.year, dateDebut.month, dateDebut.day); + whereClause += + (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.date_sortie >= ?'; + params.add(startOfDay.toIso8601String()); + } else if (dateFin != null) { + final endOfDay = + DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59); + whereClause += + (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.date_sortie <= ?'; + params.add(endOfDay.toIso8601String()); + } + + final result = await db.query(''' SELECT sp.*, p.name as produit_nom, p.reference as produit_reference, @@ -3454,26 +3467,27 @@ Future>> getHistoriqueSortiesPersonnelles({ LIMIT ? ''', [...params, limit]); - return result.map((row) => row.fields).toList(); - } catch (e) { - print('Erreur récupération historique sorties: $e'); - return []; + return result.map((row) => row.fields).toList(); + } catch (e) { + print('Erreur récupération historique sorties: $e'); + return []; + } } -} -Future updatePointDeVentes( - int id, - String nom, - String code, { - String? content, - String? livraison, - String? facture, - Uint8List? imagePath, -}) async { - final db = await database; - - try { - await db.query( - ''' + + Future updatePointDeVentes( + int id, + String nom, + String code, { + String? content, + String? livraison, + String? facture, + Uint8List? imagePath, + }) async { + final db = await database; + + try { + await db.query( + ''' UPDATE points_de_vente SET nom = ?, content = ?, @@ -3482,32 +3496,28 @@ Future updatePointDeVentes( logo = ? WHERE id = ? ''', - [ - nom, - (content?.isEmpty ?? true) ? null : content, - (livraison?.isEmpty ?? true) ? null : livraison, - (facture?.isEmpty ?? true) ? null : facture, - imagePath, - id, - ], - ); - } catch (e, stacktrace) { - print('Erreur lors de la mise à jour du point de vente : $e'); - print('Stacktrace : $stacktrace'); - rethrow; // si tu veux faire remonter l’erreur plus haut + [ + nom, + (content?.isEmpty ?? true) ? null : content, + (livraison?.isEmpty ?? true) ? null : livraison, + (facture?.isEmpty ?? true) ? null : facture, + imagePath, + id, + ], + ); + } catch (e, stacktrace) { + print('Erreur lors de la mise à jour du point de vente : $e'); + print('Stacktrace : $stacktrace'); + rethrow; // si tu veux faire remonter l’erreur plus haut + } } -} - - + Future> getStatistiquesSortiesPersonnelles() async { + final db = await database; - - Future> getStatistiquesSortiesPersonnelles() async { - final db = await database; - - try { - // Total des sorties par statut - final statsStatut = await db.query(''' + try { + // Total des sorties par statut + final statsStatut = await db.query(''' SELECT statut, COUNT(*) as nombre, @@ -3515,9 +3525,9 @@ Future updatePointDeVentes( FROM sorties_stock_personnelles GROUP BY statut '''); - - // Sorties par admin - final statsAdmin = await db.query(''' + + // Sorties par admin + final statsAdmin = await db.query(''' SELECT u.name as admin_nom, u.lastname as admin_nom_famille, @@ -3530,9 +3540,9 @@ Future updatePointDeVentes( ORDER BY quantite_totale DESC LIMIT 10 '''); - - // Produits les plus sortis - final statsProduits = await db.query(''' + + // Produits les plus sortis + final statsProduits = await db.query(''' SELECT p.name as produit_nom, p.reference as produit_reference, @@ -3544,22 +3554,21 @@ Future updatePointDeVentes( 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': [], - }; - } - } + + 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 { + } } + +class _formatDate {} diff --git a/lib/Views/commandManagement.dart b/lib/Views/commandManagement.dart index 4822812..2edcbfc 100644 --- a/lib/Views/commandManagement.dart +++ b/lib/Views/commandManagement.dart @@ -189,7 +189,8 @@ class _GestionCommandesPageState extends State { content: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Montant total: ${NumberFormat('#,##0', 'fr_FR').format(montantFinal)} MGA'), + Text( + 'Montant total: ${NumberFormat('#,##0', 'fr_FR').format(montantFinal)} MGA'), const SizedBox(height: 10), TextField( controller: amountController, @@ -279,349 +280,391 @@ class _GestionCommandesPageState extends State { /// // Dans GestionCommandesPage - Remplacez la méthode _generateBonLivraison complète -Future _generateBonLivraison(Commande commande) async { - final details = await _database.getDetailsCommande(commande.id!); - final client = await _database.getClientById(commande.clientId); - final pointDeVenteId = commande.pointDeVenteId; - ResultRow? pointDeVenteComplet; - - // ✅ MODIFICATION: Récupération complète des données du point de vente - if (pointDeVenteId != null) { - pointDeVenteComplet = await _database.getPointDeVenteById(pointDeVenteId); -} else { - print("ce point de vente n'existe pas"); -} - final pointDeVente = pointDeVenteComplet; - - - // Récupérer les informations des vendeurs - final commandeur = commande.commandeurId != null - ? await _database.getUserById(commande.commandeurId!) - : null; - final validateur = commande.validateurId != null - ? await _database.getUserById(commande.validateurId!) - : null; - - // ✅ NOUVELLE FONCTIONNALITÉ: Parser les informations d'en-tête pour livraison - List infosLivraison = []; - final livraisonBrute = pointDeVenteComplet?['livraison']; - print('=== LIVRAISON BRUTE ==='); - print(livraisonBrute); - print('=== FIN ==='); - - if (livraisonBrute != null) { - infosLivraison = _database.parseHeaderInfo(livraisonBrute); - print('=== INFOS LIVRAISON PARSÉES ==='); - for (int i = 0; i < infosLivraison.length; i++) { - print('Ligne $i: ${infosLivraison[i]}'); - } - print('==============================='); - } + Future _generateBonLivraison(Commande commande) async { + final details = await _database.getDetailsCommande(commande.id!); + final client = await _database.getClientById(commande.clientId); + final pointDeVenteId = commande.pointDeVenteId; + ResultRow? pointDeVenteComplet; - - // Infos par défaut si aucune info personnalisée - final infosLivraisonDefaut = [ - 'REMAX Andravoangy', - 'SUPREME CENTER Behoririka \n BOX 405 | 416 | 119', - 'Tripolisa analankely BOX 7', - '033 37 808 18', - 'www.guycom.mg', - ]; - - // ✅ DEBUG: Vérifiez combien de détails vous avez - print('=== DEBUG BON DE LIVRAISON ==='); - print('Nombre de détails récupérés: ${details.length}'); - - for (int i = 0; i < details.length; i++) { - print('Détail $i: ${details[i].produitNom} x${details[i].quantite}'); - } - - double sousTotal = 0; - double totalRemises = 0; - double totalCadeaux = 0; - int nombreCadeaux = 0; - - for (final detail in details) { - sousTotal += detail.sousTotal; - if (detail.estCadeau) { - totalCadeaux += detail.sousTotal; - nombreCadeaux += detail.quantite; + // ✅ MODIFICATION: Récupération complète des données du point de vente + if (pointDeVenteId != null) { + pointDeVenteComplet = await _database.getPointDeVenteById(pointDeVenteId); } else { - totalRemises += detail.montantRemise; + print("ce point de vente n'existe pas"); } - } + final pointDeVente = pointDeVenteComplet; - // ✅ CORRECTION PRINCIPALE: Améliorer la récupération des produits - final List> detailsAvecProduits = []; - - for (int i = 0; i < details.length; i++) { - final detail = details[i]; - print('Traitement détail $i: ${detail.produitNom}'); - - try { - final produit = await _database.getProductById(detail.produitId); - - if (produit != null) { - detailsAvecProduits.add({ - 'detail': detail, - 'produit': produit, - }); - print(' ✅ Produit trouvé: ${produit.name}'); + // Récupérer les informations des vendeurs + final commandeur = commande.commandeurId != null + ? await _database.getUserById(commande.commandeurId!) + : null; + final validateur = commande.validateurId != null + ? await _database.getUserById(commande.validateurId!) + : null; + + // ✅ NOUVELLE FONCTIONNALITÉ: Parser les informations d'en-tête pour livraison + List infosLivraison = []; + final livraisonBrute = pointDeVenteComplet?['livraison']; + print('=== LIVRAISON BRUTE ==='); + print(livraisonBrute); + print('=== FIN ==='); + + if (livraisonBrute != null) { + infosLivraison = _database.parseHeaderInfo(livraisonBrute); + print('=== INFOS LIVRAISON PARSÉES ==='); + for (int i = 0; i < infosLivraison.length; i++) { + print('Ligne $i: ${infosLivraison[i]}'); + } + print('==============================='); + } + + // Infos par défaut si aucune info personnalisée + final infosLivraisonDefaut = [ + 'REMAX Andravoangy', + 'SUPREME CENTER Behoririka \n BOX 405 | 416 | 119', + 'Tripolisa analankely BOX 7', + '033 37 808 18', + 'www.guycom.mg', + ]; + + // ✅ DEBUG: Vérifiez combien de détails vous avez + print('=== DEBUG BON DE LIVRAISON ==='); + print('Nombre de détails récupérés: ${details.length}'); + + for (int i = 0; i < details.length; i++) { + print('Détail $i: ${details[i].produitNom} x${details[i].quantite}'); + } + + double sousTotal = 0; + double totalRemises = 0; + double totalCadeaux = 0; + int nombreCadeaux = 0; + + for (final detail in details) { + sousTotal += detail.sousTotal; + if (detail.estCadeau) { + totalCadeaux += detail.sousTotal; + nombreCadeaux += detail.quantite; } else { + totalRemises += detail.montantRemise; + } + } + + // ✅ CORRECTION PRINCIPALE: Améliorer la récupération des produits + final List> detailsAvecProduits = []; + + for (int i = 0; i < details.length; i++) { + final detail = details[i]; + print('Traitement détail $i: ${detail.produitNom}'); + + try { + final produit = await _database.getProductById(detail.produitId); + + if (produit != null) { + detailsAvecProduits.add({ + 'detail': detail, + 'produit': produit, + }); + print(' ✅ Produit trouvé: ${produit.name}'); + } else { + detailsAvecProduits.add({ + 'detail': detail, + 'produit': null, + }); + print(' ⚠️ Produit non trouvé, utilisation des données du détail'); + } + } catch (e) { + print(' ❌ Erreur lors de la récupération du produit: $e'); detailsAvecProduits.add({ 'detail': detail, 'produit': null, }); - print(' ⚠️ Produit non trouvé, utilisation des données du détail'); } - } catch (e) { - print(' ❌ Erreur lors de la récupération du produit: $e'); - detailsAvecProduits.add({ - 'detail': detail, - 'produit': null, - }); } - } - - print('Total detailsAvecProduits: ${detailsAvecProduits.length}'); - - final pdf = pw.Document(); - final imageBytes = await loadImage(); - final image = pw.MemoryImage(imageBytes); - - // ✅ AMÉLIORATION: Gestion des polices avec fallback - pw.Font? italicFont; - pw.Font? regularFont; - - try { - italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); - regularFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Regular.ttf')); - } catch (e) { - print('⚠️ Impossible de charger les polices personnalisées: $e'); - } - - // ✅ DÉFINITION DES STYLES DE TEXTE - final tinyTextStyle = pw.TextStyle(fontSize: 9, font: regularFont); - final smallTextStyle = pw.TextStyle(fontSize: 10, font: regularFont); - final normalTextStyle = pw.TextStyle(fontSize: 11, font: regularFont); - final boldTextStyle = pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold, font: regularFont); - final boldClientStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold, font: regularFont); - final frameTextStyle = pw.TextStyle(fontSize: 10, font: regularFont); - final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont ?? regularFont); - final italicLogoStyle = pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold, font: italicFont ?? regularFont); - Future buildLogoWidget() async { - final logoRaw = pointDeVenteComplet?['logo']; - - if (logoRaw != null) { - try { - Uint8List bytes; - if (logoRaw is Uint8List) { - bytes = logoRaw; - } else if (logoRaw is List) { - bytes = Uint8List.fromList(logoRaw); - } else if (logoRaw.runtimeType.toString() == 'Blob') { - // Cast dynamique pour appeler toBytes() - dynamic blobDynamic = logoRaw; - bytes = blobDynamic.toBytes(); - } else { - throw Exception("Format de logo non supporté: ${logoRaw.runtimeType}"); - } - final imageLogo = pw.MemoryImage(bytes); - return pw.Image(imageLogo, width: 100, height: 100); + print('Total detailsAvecProduits: ${detailsAvecProduits.length}'); + + final pdf = pw.Document(); + final imageBytes = await loadImage(); + final image = pw.MemoryImage(imageBytes); + + // ✅ AMÉLIORATION: Gestion des polices avec fallback + pw.Font? italicFont; + pw.Font? regularFont; + + try { + italicFont = + pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); + regularFont = + pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Regular.ttf')); } catch (e) { - print('Erreur chargement logo BDD: $e'); + print('⚠️ Impossible de charger les polices personnalisées: $e'); + } + + // ✅ DÉFINITION DES STYLES DE TEXTE + final tinyTextStyle = pw.TextStyle(fontSize: 9, font: regularFont); + final smallTextStyle = pw.TextStyle(fontSize: 10, font: regularFont); + final normalTextStyle = pw.TextStyle(fontSize: 11, font: regularFont); + final boldTextStyle = pw.TextStyle( + fontSize: 11, fontWeight: pw.FontWeight.bold, font: regularFont); + final boldClientStyle = pw.TextStyle( + fontSize: 12, fontWeight: pw.FontWeight.bold, font: regularFont); + final frameTextStyle = pw.TextStyle(fontSize: 10, font: regularFont); + final italicTextStyle = pw.TextStyle( + fontSize: 9, + fontWeight: pw.FontWeight.bold, + font: italicFont ?? regularFont); + final italicLogoStyle = pw.TextStyle( + fontSize: 8, + fontWeight: pw.FontWeight.bold, + font: italicFont ?? regularFont); + Future buildLogoWidget() async { + final logoRaw = pointDeVenteComplet?['logo']; + + if (logoRaw != null) { + try { + Uint8List bytes; + if (logoRaw is Uint8List) { + bytes = logoRaw; + } else if (logoRaw is List) { + bytes = Uint8List.fromList(logoRaw); + } else if (logoRaw.runtimeType.toString() == 'Blob') { + // Cast dynamique pour appeler toBytes() + dynamic blobDynamic = logoRaw; + bytes = blobDynamic.toBytes(); + } else { + throw Exception( + "Format de logo non supporté: ${logoRaw.runtimeType}"); + } + + final imageLogo = pw.MemoryImage(bytes); + return pw.Image(imageLogo, width: 100, height: 100); + } catch (e) { + print('Erreur chargement logo BDD: $e'); + } + } + return pw.Image(image, width: 100, height: 100); } - } - return pw.Image(image, width: 100, height: 100); -} + final logoWidget = await buildLogoWidget(); -final logoWidget = await buildLogoWidget(); + // ✅ FONCTION POUR CONSTRUIRE L'EN-TÊTE DYNAMIQUE + pw.Widget buildEnteteInfos() { + final infosAUtiliser = + infosLivraison.isNotEmpty ? infosLivraison : infosLivraisonDefaut; - // ✅ FONCTION POUR CONSTRUIRE L'EN-TÊTE DYNAMIQUE - pw.Widget buildEnteteInfos() { - final infosAUtiliser = infosLivraison.isNotEmpty ? infosLivraison : infosLivraisonDefaut; - - return pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: infosAUtiliser.map((info) { - return pw.Padding( - padding: const pw.EdgeInsets.only(bottom: 1), - child: pw.Text(info, style: tinyTextStyle), - ); - }).toList(), - ); - } - - // ✅ Fonction pour créer un exemplaire - AVEC EN-TÊTE DYNAMIQUE - pw.Widget buildExemplaire(String typeExemplaire) { - return pw.Container( - width: double.infinity, - decoration: pw.BoxDecoration( - border: pw.Border.all(color: PdfColors.black, width: 1.5), - ), - child: pw.Column( + return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // En-tête avec indication de l'exemplaire - pw.Container( - width: double.infinity, - padding: const pw.EdgeInsets.all(5), - decoration: pw.BoxDecoration( - color: typeExemplaire == "CLIENT" ? PdfColors.blue100 : PdfColors.green100, - ), - child: pw.Center( - child: pw.Text( - 'BON DE LIVRAISON - EXEMPLAIRE $typeExemplaire', - style: pw.TextStyle( - fontSize: 14, - fontWeight: pw.FontWeight.bold, - color: typeExemplaire == "CLIENT" ? PdfColors.blue800 : PdfColors.green800, - font: regularFont, + children: infosAUtiliser.map((info) { + return pw.Padding( + padding: const pw.EdgeInsets.only(bottom: 1), + child: pw.Text(info, style: tinyTextStyle), + ); + }).toList(), + ); + } + + // ✅ Fonction pour créer un exemplaire - AVEC EN-TÊTE DYNAMIQUE + pw.Widget buildExemplaire(String typeExemplaire) { + return pw.Container( + width: double.infinity, + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 1.5), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // En-tête avec indication de l'exemplaire + pw.Container( + width: double.infinity, + padding: const pw.EdgeInsets.all(5), + decoration: pw.BoxDecoration( + color: typeExemplaire == "CLIENT" + ? PdfColors.blue100 + : PdfColors.green100, + ), + child: pw.Center( + child: pw.Text( + 'BON DE LIVRAISON - EXEMPLAIRE $typeExemplaire', + style: pw.TextStyle( + fontSize: 14, + fontWeight: pw.FontWeight.bold, + color: typeExemplaire == "CLIENT" + ? PdfColors.blue800 + : PdfColors.green800, + font: regularFont, + ), ), ), ), - ), - - pw.Padding( - padding: const pw.EdgeInsets.all(8), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // En-tête principal (logo, infos entreprise, client) - pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - // Logo et infos entreprise - ✅ AVEC INFOS DYNAMIQUES - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - logoWidget, - pw.SizedBox(height: 3), - pw.Text('NOTRE COMPETENCE, A VOTRE SERVICE', style: italicLogoStyle), - pw.SizedBox(height: 4), - buildEnteteInfos(), // ✅ EN-TÊTE DYNAMIQUE ICI - ], - ), - - // Informations centrales - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.center, - children: [ - pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldClientStyle), - pw.SizedBox(height: 4), - pw.Container(width: 100, height: 2, color: PdfColors.black), - pw.SizedBox(height: 4), - pw.Container( - padding: const pw.EdgeInsets.all(6), - decoration: pw.BoxDecoration( - border: pw.Border.all(color: PdfColors.black), - ), - child: pw.Column( - children: [ - pw.Text('Boutique:', style: frameTextStyle), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTextStyle), - pw.SizedBox(height: 2), - pw.Text('Bon N°:', style: frameTextStyle), - pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTextStyle), - ], - ), - ), - ], - ), - - // Informations client - pw.Container( - width: 120, - decoration: pw.BoxDecoration( - border: pw.Border.all(color: PdfColors.black, width: 1), + + pw.Padding( + padding: const pw.EdgeInsets.all(8), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // En-tête principal (logo, infos entreprise, client) + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + // Logo et infos entreprise - ✅ AVEC INFOS DYNAMIQUES + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + logoWidget, + pw.SizedBox(height: 3), + pw.Text('NOTRE COMPETENCE, A VOTRE SERVICE', + style: italicLogoStyle), + pw.SizedBox(height: 4), + buildEnteteInfos(), // ✅ EN-TÊTE DYNAMIQUE ICI + ], ), - padding: const pw.EdgeInsets.all(6), - child: pw.Column( + + // Informations centrales + pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ - pw.Text('CLIENT', style: frameTextStyle), - pw.SizedBox(height: 2), - pw.Text('ID: ${pointDeVente?['nom'] ?? 'S405A'}-${client?.id ?? 'Non spécifié'}', style: smallTextStyle), - pw.Container(width: 100, height: 1, color: PdfColors.black, margin: const pw.EdgeInsets.symmetric(vertical: 2)), - pw.Text('${client?.nom} ${client?.prenom}', style: boldTextStyle), - pw.SizedBox(height: 2), - pw.Text(client?.telephone ?? 'Non spécifié', style: tinyTextStyle), + pw.Text( + 'Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', + style: boldClientStyle), + pw.SizedBox(height: 4), + pw.Container( + width: 100, height: 2, color: PdfColors.black), + pw.SizedBox(height: 4), + pw.Container( + padding: const pw.EdgeInsets.all(6), + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black), + ), + child: pw.Column( + children: [ + pw.Text('Boutique:', style: frameTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', + style: boldTextStyle), + pw.SizedBox(height: 2), + pw.Text('Bon N°:', style: frameTextStyle), + pw.Text( + '${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', + style: boldTextStyle), + ], + ), + ), ], ), - ), - ], - ), - - pw.SizedBox(height: 8), - - // ✅ SOLUTION PRINCIPALE: Tableau avec hauteur dynamique - pw.Column( - children: [ - // Debug: Afficher le nombre d'articles - pw.Text('Articles trouvés: ${detailsAvecProduits.length}', - style: pw.TextStyle(fontSize: 8, color: PdfColors.grey, font: regularFont)), - pw.SizedBox(height: 5), - - // ✅ TABLE SANS CONTRAINTE DE HAUTEUR - Elle s'adapte au contenu - pw.Table( - border: pw.TableBorder.all(width: 1), - columnWidths: { - 0: const pw.FlexColumnWidth(5), // Désignations - 1: const pw.FlexColumnWidth(1.2), // Quantité - 2: const pw.FlexColumnWidth(1.5), // Prix unitaire - 3: const pw.FlexColumnWidth(1.5), // Montant - }, - children: [ + + // Informations client + pw.Container( + width: 120, + decoration: pw.BoxDecoration( + border: + pw.Border.all(color: PdfColors.black, width: 1), + ), + padding: const pw.EdgeInsets.all(6), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Text('CLIENT', style: frameTextStyle), + pw.SizedBox(height: 2), + pw.Text( + 'ID: ${pointDeVente?['nom'] ?? 'S405A'}-${client?.id ?? 'Non spécifié'}', + style: smallTextStyle), + pw.Container( + width: 100, + height: 1, + color: PdfColors.black, + margin: + const pw.EdgeInsets.symmetric(vertical: 2)), + pw.Text('${client?.nom} ${client?.prenom}', + style: boldTextStyle), + pw.SizedBox(height: 2), + pw.Text(client?.telephone ?? 'Non spécifié', + style: tinyTextStyle), + ], + ), + ), + ], + ), + + pw.SizedBox(height: 8), + + // ✅ SOLUTION PRINCIPALE: Tableau avec hauteur dynamique + pw.Column( + children: [ + // Debug: Afficher le nombre d'articles + pw.Text('Articles trouvés: ${detailsAvecProduits.length}', + style: pw.TextStyle( + fontSize: 8, + color: PdfColors.grey, + font: regularFont)), + pw.SizedBox(height: 5), + + // ✅ TABLE SANS CONTRAINTE DE HAUTEUR - Elle s'adapte au contenu + pw.Table( + border: pw.TableBorder.all(width: 1), + columnWidths: { + 0: const pw.FlexColumnWidth(5), // Désignations + 1: const pw.FlexColumnWidth(1.2), // Quantité + 2: const pw.FlexColumnWidth(1.5), // Prix unitaire + 3: const pw.FlexColumnWidth(1.5), // Montant + }, + children: [ // En-tête du tableau pw.TableRow( - decoration: const pw.BoxDecoration(color: PdfColors.grey200), + decoration: const pw.BoxDecoration( + color: PdfColors.grey200), children: [ pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('Désignations', style: boldTextStyle) - ), + padding: const pw.EdgeInsets.all(4), + child: pw.Text('Désignations', + style: boldTextStyle)), pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center) - ), + padding: const pw.EdgeInsets.all(4), + child: pw.Text('Qté', + style: boldTextStyle, + textAlign: pw.TextAlign.center)), pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('P.U.', style: boldTextStyle, textAlign: pw.TextAlign.right) - ), + padding: const pw.EdgeInsets.all(4), + child: pw.Text('P.U.', + style: boldTextStyle, + textAlign: pw.TextAlign.right)), pw.Padding( - padding: const pw.EdgeInsets.all(4), - child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right) - ), + padding: const pw.EdgeInsets.all(4), + child: pw.Text('Montant', + style: boldTextStyle, + textAlign: pw.TextAlign.right)), ], ), - + // ✅ TOUTES LES LIGNES DE PRODUITS - SANS LIMITATION ...detailsAvecProduits.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; final detail = item['detail'] as DetailCommande; final produit = item['produit']; - + // Debug pour chaque ligne - print('📋 Ligne PDF $index: ${detail.produitNom} (Quantité: ${detail.quantite})'); - + print( + '📋 Ligne PDF $index: ${detail.produitNom} (Quantité: ${detail.quantite})'); + return pw.TableRow( decoration: detail.estCadeau - ? const pw.BoxDecoration(color: PdfColors.green50) + ? const pw.BoxDecoration( + color: PdfColors.green50) : detail.aRemise - ? const pw.BoxDecoration(color: PdfColors.orange50) - : index % 2 == 0 - ? const pw.BoxDecoration(color: PdfColors.grey50) + ? const pw.BoxDecoration( + color: PdfColors.orange50) + : index % 2 == 0 + ? const pw.BoxDecoration( + color: PdfColors.grey50) : null, children: [ // ✅ Colonne Désignations - Plus compacte pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, + crossAxisAlignment: + pw.CrossAxisAlignment.start, mainAxisSize: pw.MainAxisSize.min, children: [ // Nom du produit avec badge @@ -629,173 +672,185 @@ final logoWidget = await buildLogoWidget(); children: [ pw.Expanded( child: pw.Text( - '${detail.produitNom ?? 'Produit inconnu'}', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - font: regularFont - ) - ), + '${detail.produitNom ?? 'Produit inconnu'}', + style: pw.TextStyle( + fontSize: 10, + fontWeight: + pw.FontWeight.bold, + font: regularFont)), ), if (detail.estCadeau) pw.Container( - padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 1), + padding: + const pw.EdgeInsets.symmetric( + horizontal: 3, + vertical: 1), decoration: pw.BoxDecoration( color: PdfColors.green600, - borderRadius: pw.BorderRadius.circular(3), - ), - child: pw.Text( - 'CADEAU', - style: pw.TextStyle( - fontSize: 6, - color: PdfColors.white, - font: regularFont, - fontWeight: pw.FontWeight.bold - ) + borderRadius: + pw.BorderRadius.circular(3), ), + child: pw.Text('CADEAU', + style: pw.TextStyle( + fontSize: 6, + color: PdfColors.white, + font: regularFont, + fontWeight: + pw.FontWeight.bold)), ), ], ), - + pw.SizedBox(height: 2), - + // Informations complémentaires sur une seule ligne pw.Text( [ - if (produit?.category?.isNotEmpty == true) produit!.category, - if (produit?.marque?.isNotEmpty == true) produit!.marque, - if (produit?.imei?.isNotEmpty == true) 'IMEI: ${produit!.imei}', - ].where((info) => info != null).join(' , '), - style: pw.TextStyle(fontSize: 8, color: PdfColors.grey700, font: regularFont), + if (produit?.category?.isNotEmpty == + true) + produit!.category, + if (produit?.marque?.isNotEmpty == + true) + produit!.marque, + if (produit?.imei?.isNotEmpty == true) + 'IMEI: ${produit!.imei}', + ] + .where((info) => info != null) + .join(' , '), + style: pw.TextStyle( + fontSize: 8, + color: PdfColors.grey700, + font: regularFont), ), - + // Spécifications techniques - if (produit?.ram?.isNotEmpty == true || produit?.memoireInterne?.isNotEmpty == true || produit?.reference?.isNotEmpty == true) + if (produit?.ram?.isNotEmpty == true || + produit?.memoireInterne?.isNotEmpty == + true || + produit?.reference?.isNotEmpty == + true) pw.Text( [ - if (produit?.ram?.isNotEmpty == true) 'RAM: ${produit!.ram}', - if (produit?.memoireInterne?.isNotEmpty == true) 'Stockage: ${produit!.memoireInterne}', - if (produit?.reference?.isNotEmpty == true) 'Ref: ${produit!.reference}', + if (produit?.ram?.isNotEmpty == + true) + 'RAM: ${produit!.ram}', + if (produit?.memoireInterne + ?.isNotEmpty == + true) + 'Stockage: ${produit!.memoireInterne}', + if (produit + ?.reference?.isNotEmpty == + true) + 'Ref: ${produit!.reference}', ].join(' , '), - style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600, font: regularFont), + style: pw.TextStyle( + fontSize: 8, + color: PdfColors.grey600, + font: regularFont), ), ], ), ), - + // Colonne Quantité pw.Padding( padding: const pw.EdgeInsets.all(4), - child: pw.Text( - '${detail.quantite}', - style: normalTextStyle, - textAlign: pw.TextAlign.center - ), + child: pw.Text('${detail.quantite}', + style: normalTextStyle, + textAlign: pw.TextAlign.center), ), - + // Colonne Prix Unitaire pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, + crossAxisAlignment: + pw.CrossAxisAlignment.end, mainAxisSize: pw.MainAxisSize.min, children: [ if (detail.estCadeau) ...[ pw.Text( - '${NumberFormat('#,##0', 'fr_FR').format(detail.prixUnitaire)}', - style: pw.TextStyle( - fontSize: 8, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - font: regularFont - ) - ), - pw.Text( - 'GRATUIT', - style: pw.TextStyle( - fontSize: 9, - color: PdfColors.green700, - fontWeight: pw.FontWeight.bold, - font: regularFont - ) - ), + '${NumberFormat('#,##0', 'fr_FR').format(detail.prixUnitaire)}', + style: pw.TextStyle( + fontSize: 8, + decoration: pw + .TextDecoration.lineThrough, + color: PdfColors.grey600, + font: regularFont)), + pw.Text('GRATUIT', + style: pw.TextStyle( + fontSize: 9, + color: PdfColors.green700, + fontWeight: pw.FontWeight.bold, + font: regularFont)), ] else if (detail.aRemise) ...[ pw.Text( - '${NumberFormat('#,##0', 'fr_FR').format(detail.prixUnitaire)}', - style: pw.TextStyle( - fontSize: 8, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - font: regularFont - ) - ), + '${NumberFormat('#,##0', 'fr_FR').format(detail.prixUnitaire)}', + style: pw.TextStyle( + fontSize: 8, + decoration: pw + .TextDecoration.lineThrough, + color: PdfColors.grey600, + font: regularFont)), pw.Text( - '${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal / detail.quantite)}', - style: pw.TextStyle( - fontSize: 10, - color: PdfColors.orange700, - fontWeight: pw.FontWeight.bold, - font: regularFont - ) - ), + '${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal / detail.quantite)}', + style: pw.TextStyle( + fontSize: 10, + color: PdfColors.orange700, + fontWeight: pw.FontWeight.bold, + font: regularFont)), ] else pw.Text( - NumberFormat('#,##0', 'fr_FR').format(detail.prixUnitaire), - style: smallTextStyle - ), + NumberFormat('#,##0', 'fr_FR') + .format(detail.prixUnitaire), + style: smallTextStyle), ], ), ), - + // Colonne Montant pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, + crossAxisAlignment: + pw.CrossAxisAlignment.end, mainAxisSize: pw.MainAxisSize.min, children: [ if (detail.estCadeau) ...[ pw.Text( - NumberFormat('#,##0', 'fr_FR').format(detail.sousTotal), - style: pw.TextStyle( - fontSize: 8, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - font: regularFont - ) - ), - pw.Text( - 'GRATUIT', - style: pw.TextStyle( - fontSize: 9, - fontWeight: pw.FontWeight.bold, - color: PdfColors.green700, - font: regularFont - ) - ), + NumberFormat('#,##0', 'fr_FR') + .format(detail.sousTotal), + style: pw.TextStyle( + fontSize: 8, + decoration: pw + .TextDecoration.lineThrough, + color: PdfColors.grey600, + font: regularFont)), + pw.Text('GRATUIT', + style: pw.TextStyle( + fontSize: 9, + fontWeight: pw.FontWeight.bold, + color: PdfColors.green700, + font: regularFont)), ] else if (detail.aRemise) ...[ pw.Text( - '${NumberFormat('#,##0', 'fr_FR').format(detail.sousTotal)}', - style: pw.TextStyle( - fontSize: 8, - decoration: pw.TextDecoration.lineThrough, - color: PdfColors.grey600, - font: regularFont - ) - ), + '${NumberFormat('#,##0', 'fr_FR').format(detail.sousTotal)}', + style: pw.TextStyle( + fontSize: 8, + decoration: pw + .TextDecoration.lineThrough, + color: PdfColors.grey600, + font: regularFont)), pw.Text( - '${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)}', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - font: regularFont - ) - ), + '${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)}', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + font: regularFont)), ] else pw.Text( - '${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)}', - style: smallTextStyle - ), + '${NumberFormat('#,##0', 'fr_FR').format(detail.prixFinal)}', + style: smallTextStyle), ], ), ), @@ -803,234 +858,302 @@ final logoWidget = await buildLogoWidget(); ); }).toList(), ], - ), - ], - ), + ), + ], + ), - pw.SizedBox(height: 12), - - // Section finale - Totaux et signatures - pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // Totaux - pw.Expanded( - flex: 2, - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.end, - children: [ - if (totalRemises > 0 || totalCadeaux > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, - children: [ - pw.Text('SOUS-TOTAL:', style: smallTextStyle), - pw.SizedBox(width: 10), - pw.Text('${NumberFormat('#,##0', 'fr_FR').format(sousTotal)}', style: smallTextStyle), - ], - ), - pw.SizedBox(height: 2), - ], - - if (totalRemises > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, - children: [ - pw.Text('REMISES:', style: pw.TextStyle(color: PdfColors.orange, fontSize: 10, font: regularFont)), - pw.SizedBox(width: 10), - pw.Text('-${NumberFormat('#,##0', 'fr_FR').format(totalRemises)}', style: pw.TextStyle(color: PdfColors.orange, fontSize: 10, font: regularFont)), - ], - ), - pw.SizedBox(height: 2), - ], - - if (totalCadeaux > 0) ...[ + pw.SizedBox(height: 12), + + // Section finale - Totaux et signatures + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // Totaux + pw.Expanded( + flex: 2, + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + if (totalRemises > 0 || totalCadeaux > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('SOUS-TOTAL:', style: smallTextStyle), + pw.SizedBox(width: 10), + pw.Text( + '${NumberFormat('#,##0', 'fr_FR').format(sousTotal)}', + style: smallTextStyle), + ], + ), + pw.SizedBox(height: 2), + ], + if (totalRemises > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('REMISES:', + style: pw.TextStyle( + color: PdfColors.orange, + fontSize: 10, + font: regularFont)), + pw.SizedBox(width: 10), + pw.Text( + '-${NumberFormat('#,##0', 'fr_FR').format(totalRemises)}', + style: pw.TextStyle( + color: PdfColors.orange, + fontSize: 10, + font: regularFont)), + ], + ), + pw.SizedBox(height: 2), + ], + if (totalCadeaux > 0) ...[ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Text('CADEAUX ($nombreCadeaux):', + style: pw.TextStyle( + color: PdfColors.green700, + fontSize: 10, + font: regularFont)), + pw.SizedBox(width: 10), + pw.Text( + '-${NumberFormat('#,##0', 'fr_FR').format(totalCadeaux)}', + style: pw.TextStyle( + color: PdfColors.green700, + fontSize: 10, + font: regularFont)), + ], + ), + pw.SizedBox(height: 2), + ], + pw.Container( + width: 120, + height: 1.5, + color: PdfColors.black, + margin: + const pw.EdgeInsets.symmetric(vertical: 2)), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ - pw.Text('CADEAUX ($nombreCadeaux):', style: pw.TextStyle(color: PdfColors.green700, fontSize: 10, font: regularFont)), + pw.Text('TOTAL:', style: boldTextStyle), pw.SizedBox(width: 10), - pw.Text('-${NumberFormat('#,##0', 'fr_FR').format(totalCadeaux)}', style: pw.TextStyle(color: PdfColors.green700, fontSize: 10, font: regularFont)), + pw.Text( + '${NumberFormat('#,##0', 'fr_FR').format(commande.montantTotal)} MGA', + style: boldTextStyle), ], ), - pw.SizedBox(height: 2), ], - - pw.Container(width: 120, height: 1.5, color: PdfColors.black, margin: const pw.EdgeInsets.symmetric(vertical: 2)), - - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, - children: [ - pw.Text('TOTAL:', style: boldTextStyle), - pw.SizedBox(width: 10), - pw.Text('${NumberFormat('#,##0', 'fr_FR').format(commande.montantTotal)} MGA', style: boldTextStyle), - ], - ), - ], + ), ), - ), - - pw.SizedBox(width: 15), - - // Section vendeurs et signatures - pw.Expanded( - flex: 3, - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // Vendeurs - pw.Container( - padding: const pw.EdgeInsets.all(4), - decoration: pw.BoxDecoration( - color: PdfColors.grey100, - borderRadius: pw.BorderRadius.circular(3), + + pw.SizedBox(width: 15), + + // Section vendeurs et signatures + pw.Expanded( + flex: 3, + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // Vendeurs + pw.Container( + padding: const pw.EdgeInsets.all(4), + decoration: pw.BoxDecoration( + color: PdfColors.grey100, + borderRadius: pw.BorderRadius.circular(3), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('VENDEURS', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + font: regularFont)), + pw.SizedBox(height: 3), + pw.Row( + children: [ + pw.Expanded( + child: pw.Column( + crossAxisAlignment: + pw.CrossAxisAlignment.start, + children: [ + pw.Text('Initiateur:', + style: tinyTextStyle), + pw.Text( + commandeur != null + ? '${commandeur.name} ${commandeur.lastName ?? ''}' + .trim() + : 'N/A', + style: pw.TextStyle( + fontSize: 9, + font: regularFont), + ), + ], + ), + ), + pw.Expanded( + child: pw.Column( + crossAxisAlignment: + pw.CrossAxisAlignment.start, + children: [ + pw.Text('Validateur:', + style: tinyTextStyle), + pw.Text( + validateur != null + ? '${validateur.name} ${validateur.lastName ?? ''}' + .trim() + : 'N/A', + style: pw.TextStyle( + fontSize: 9, + font: regularFont), + ), + ], + ), + ), + ], + ), + ], + ), ), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, + + pw.SizedBox(height: 8), + + // Signatures + pw.Row( + mainAxisAlignment: + pw.MainAxisAlignment.spaceBetween, children: [ - pw.Text('VENDEURS', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, font: regularFont)), - pw.SizedBox(height: 3), - pw.Row( + pw.Column( children: [ - pw.Expanded( - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text('Initiateur:', style: tinyTextStyle), - pw.Text( - commandeur != null ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() : 'N/A', - style: pw.TextStyle(fontSize: 9, font: regularFont), - ), - ], - ), - ), - pw.Expanded( - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text('Validateur:', style: tinyTextStyle), - pw.Text( - validateur != null ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() : 'N/A', - style: pw.TextStyle(fontSize: 9, font: regularFont), - ), - ], - ), - ), + pw.Text('Vendeur', + style: pw.TextStyle( + fontSize: 9, + fontWeight: pw.FontWeight.bold, + font: regularFont)), + pw.SizedBox(height: 15), + pw.Container( + width: 70, + height: 1, + color: PdfColors.black), + ], + ), + pw.Column( + children: [ + pw.Text('Client', + style: pw.TextStyle( + fontSize: 9, + fontWeight: pw.FontWeight.bold, + font: regularFont)), + pw.SizedBox(height: 15), + pw.Container( + width: 70, + height: 1, + color: PdfColors.black), ], ), ], ), - ), - - pw.SizedBox(height: 8), - - // Signatures - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Column( - children: [ - pw.Text('Vendeur', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: regularFont)), - pw.SizedBox(height: 15), - pw.Container(width: 70, height: 1, color: PdfColors.black), - ], - ), - pw.Column( - children: [ - pw.Text('Client', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: regularFont)), - pw.SizedBox(height: 15), - pw.Container(width: 70, height: 1, color: PdfColors.black), - ], - ), - ], - ), - ], + ], + ), ), - ), - ], - ), - - pw.SizedBox(height: 6), - - // Note finale - pw.Text( - 'Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', - style: italicTextStyle, - ), - ], - ), - ), - ], - ), - ); - } - - // PAGE EN MODE PAYSAGE - pdf.addPage( - pw.Page( - pageFormat: PdfPageFormat.a4.landscape, - margin: const pw.EdgeInsets.all(12), - build: (pw.Context context) { - return pw.Row( - children: [ - pw.Expanded(child: buildExemplaire("CLIENT")), - pw.SizedBox(width: 15), - // ✅ AMÉLIORATION: Remplacer les caractères Unicode par du texte simple - pw.Container( - width: 2, - height: double.infinity, - child: pw.Column( - mainAxisAlignment: pw.MainAxisAlignment.center, - children: [ - pw.Container( - width: 20, - height: 20, - decoration: pw.BoxDecoration( - shape: pw.BoxShape.circle, - border: pw.Border.all(color: PdfColors.black, width: 2), - ), - child: pw.Center( - child: pw.Text('X', style: pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold, font: regularFont)), - ), - ), - pw.SizedBox(height: 10), - pw.Transform.rotate( - angle: 1.5708, - child: pw.Text('DÉCOUPER ICI', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, font: regularFont)), + ], ), - pw.SizedBox(height: 10), - pw.Container( - width: 20, - height: 20, - decoration: pw.BoxDecoration( - shape: pw.BoxShape.circle, - border: pw.Border.all(color: PdfColors.black, width: 2), - ), - child: pw.Center( - child: pw.Text('X', style: pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold, font: regularFont)), - ), + + pw.SizedBox(height: 6), + + // Note finale + pw.Text( + 'Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', + style: italicTextStyle, ), ], ), ), - pw.SizedBox(width: 15), - pw.Expanded(child: buildExemplaire("MAGASIN")), ], - ); - }, - ), - ); - - print('=== RÉSULTAT FINAL ==='); - print('PDF généré avec ${detailsAvecProduits.length} produits'); - - // Sauvegarder le PDF - final output = await getTemporaryDirectory(); - final file = File("${output.path}/bon_livraison_${commande.id}.pdf"); - await file.writeAsBytes(await pdf.save()); - - // Partager ou ouvrir le fichier - await OpenFile.open(file.path); -} + ), + ); + } + + // PAGE EN MODE PAYSAGE + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat.a4.landscape, + margin: const pw.EdgeInsets.all(12), + build: (pw.Context context) { + return pw.Row( + children: [ + pw.Expanded(child: buildExemplaire("CLIENT")), + pw.SizedBox(width: 15), + // ✅ AMÉLIORATION: Remplacer les caractères Unicode par du texte simple + pw.Container( + width: 2, + height: double.infinity, + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, + children: [ + pw.Container( + width: 20, + height: 20, + decoration: pw.BoxDecoration( + shape: pw.BoxShape.circle, + border: pw.Border.all(color: PdfColors.black, width: 2), + ), + child: pw.Center( + child: pw.Text('X', + style: pw.TextStyle( + fontSize: 12, + fontWeight: pw.FontWeight.bold, + font: regularFont)), + ), + ), + pw.SizedBox(height: 10), + pw.Transform.rotate( + angle: 1.5708, + child: pw.Text('DÉCOUPER ICI', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + font: regularFont)), + ), + pw.SizedBox(height: 10), + pw.Container( + width: 20, + height: 20, + decoration: pw.BoxDecoration( + shape: pw.BoxShape.circle, + border: pw.Border.all(color: PdfColors.black, width: 2), + ), + child: pw.Center( + child: pw.Text('X', + style: pw.TextStyle( + fontSize: 12, + fontWeight: pw.FontWeight.bold, + font: regularFont)), + ), + ), + ], + ), + ), + pw.SizedBox(width: 15), + pw.Expanded(child: buildExemplaire("MAGASIN")), + ], + ); + }, + ), + ); + + print('=== RÉSULTAT FINAL ==='); + print('PDF généré avec ${detailsAvecProduits.length} produits'); + + // Sauvegarder le PDF + final output = await getTemporaryDirectory(); + final file = File("${output.path}/bon_livraison_${commande.id}.pdf"); + await file.writeAsBytes(await pdf.save()); + + // Partager ou ouvrir le fichier + await OpenFile.open(file.path); + } //============================================================== // Modifiez la méthode _generateInvoice dans GestionCommandesPage @@ -1039,8 +1162,8 @@ final logoWidget = await buildLogoWidget(); final client = await _database.getClientById(commande.clientId); final pointDeVenteId = commande.pointDeVenteId; ResultRow? pointDeVenteComplet; - - if (pointDeVenteId != null) { + + if (pointDeVenteId != null) { pointDeVenteComplet = await _database.getPointDeVenteById(pointDeVenteId); } else { print("ce point de vente n'existe pas"); @@ -1055,7 +1178,7 @@ final logoWidget = await buildLogoWidget(); ? await _database.getUserById(commande.validateurId!) : null; - List infosFacture = []; + List infosFacture = []; final factureBrute = pointDeVenteComplet?['facture']; print('=== FACTURE BRUTE ==='); print(factureBrute); @@ -1074,10 +1197,10 @@ final logoWidget = await buildLogoWidget(); final infosFactureDefaut = [ 'REMAX by GUYCOM Andravoangy', 'SUPREME CENTER Behoririka box 405', - 'SUPREME CENTER Behoririka box 416', + 'SUPREME CENTER Behoririka box 416', 'SUPREME CENTER Behoririka box 119', 'TRIPOLITSA Analakely BOX 7', - '033 37 808 18', + '033 37 808 18', 'www.guycom.mg', 'NIF: 4000106673 - STAT 95210 11 2017 1 003651', 'Facebook: GuyCom', @@ -1135,53 +1258,55 @@ final logoWidget = await buildLogoWidget(); fontSize: 8, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); Future buildLogoWidget() async { - final logoRaw = pointDeVenteComplet?['logo']; - - if (logoRaw != null) { - try { - Uint8List bytes; - if (logoRaw is Uint8List) { - bytes = logoRaw; - } else if (logoRaw is List) { - bytes = Uint8List.fromList(logoRaw); - } else if (logoRaw.runtimeType.toString() == 'Blob') { - // Cast dynamique pour appeler toBytes() - dynamic blobDynamic = logoRaw; - bytes = blobDynamic.toBytes(); - } else { - throw Exception("Format de logo non supporté: ${logoRaw.runtimeType}"); + final logoRaw = pointDeVenteComplet?['logo']; + + if (logoRaw != null) { + try { + Uint8List bytes; + if (logoRaw is Uint8List) { + bytes = logoRaw; + } else if (logoRaw is List) { + bytes = Uint8List.fromList(logoRaw); + } else if (logoRaw.runtimeType.toString() == 'Blob') { + // Cast dynamique pour appeler toBytes() + dynamic blobDynamic = logoRaw; + bytes = blobDynamic.toBytes(); + } else { + throw Exception( + "Format de logo non supporté: ${logoRaw.runtimeType}"); + } + + final imageLogo = pw.MemoryImage(bytes); + return pw.Container( + width: 200, height: 120, child: pw.Image(imageLogo)); + } catch (e) { + print('Erreur chargement logo BDD: $e'); } - - final imageLogo = pw.MemoryImage(bytes); - return pw.Container(width: 200, height: 120, child: pw.Image(imageLogo)); - } catch (e) { - print('Erreur chargement logo BDD: $e'); } + return pw.Container(width: 200, height: 100, child: pw.Image(image)); } - return pw.Container(width: 200, height: 100, child: pw.Image(image)); - } - - final logoWidget = await buildLogoWidget(); -pw.Widget buildEnteteFactureInfos() { - final infosAUtiliser = infosFacture.isNotEmpty ? infosFacture : infosFactureDefaut; - return pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - ...infosAUtiliser.map((info) { - return pw.Row( - children: [ - iconChecked, - pw.SizedBox(width: 4), - pw.Text(info, style: smallTextStyle), - ], - ); - }), - pw.SizedBox(height: 2), // ajouté en fin de liste - ], - ); -} + final logoWidget = await buildLogoWidget(); + pw.Widget buildEnteteFactureInfos() { + final infosAUtiliser = + infosFacture.isNotEmpty ? infosFacture : infosFactureDefaut; + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + ...infosAUtiliser.map((info) { + return pw.Row( + children: [ + iconChecked, + pw.SizedBox(width: 4), + pw.Text(info, style: smallTextStyle), + ], + ); + }), + pw.SizedBox(height: 2), // ajouté en fin de liste + ], + ); + } pdf.addPage( pw.Page( @@ -1566,7 +1691,8 @@ pw.Widget buildEnteteFactureInfos() { pw.SizedBox(width: 20), pw.Container( width: 80, - child: pw.Text('${NumberFormat('#,##0', 'fr_FR').format(sousTotal)}', + child: pw.Text( + '${NumberFormat('#,##0', 'fr_FR').format(sousTotal)}', style: normalTextStyle, textAlign: pw.TextAlign.right), ), @@ -2001,7 +2127,6 @@ pw.Widget buildEnteteFactureInfos() { Future _generateReceipt( Commande commande, PaymentMethod payment) async { - final details = await _database.getDetailsCommande(commande.id!); final client = await _database.getClientById(commande.clientId); final commandeur = commande.commandeurId != null @@ -2242,7 +2367,8 @@ pw.Widget buildEnteteFactureInfos() { children: [ pw.Text('SOUS-TOTAL:', style: const pw.TextStyle(fontSize: 8)), - pw.Text('${NumberFormat('#,##0', 'fr_FR').format(sousTotal)} MGA', + pw.Text( + '${NumberFormat('#,##0', 'fr_FR').format(sousTotal)} MGA', style: const pw.TextStyle(fontSize: 8)), ], ), @@ -2253,7 +2379,8 @@ pw.Widget buildEnteteFactureInfos() { pw.Text('REMISES:', style: pw.TextStyle( fontSize: 8, color: PdfColors.orange)), - pw.Text('-${NumberFormat('#,##0', 'fr_FR').format(totalRemises)} MGA', + pw.Text( + '-${NumberFormat('#,##0', 'fr_FR').format(totalRemises)} MGA', style: pw.TextStyle( fontSize: 8, color: PdfColors.orange)), ], @@ -2266,7 +2393,8 @@ pw.Widget buildEnteteFactureInfos() { pw.Text('CADEAUX ($nombreCadeaux):', style: pw.TextStyle( fontSize: 8, color: PdfColors.green700)), - pw.Text('-${NumberFormat('#,##0', 'fr_FR').format(totalCadeaux)} MGA', + pw.Text( + '-${NumberFormat('#,##0', 'fr_FR').format(totalCadeaux)} MGA', style: pw.TextStyle( fontSize: 8, color: PdfColors.green700)), ], @@ -2282,7 +2410,8 @@ pw.Widget buildEnteteFactureInfos() { pw.Text('TOTAL:', style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold)), - pw.Text('${NumberFormat('#,##0', 'fr_FR').format(commande.montantTotal)} MGA', + pw.Text( + '${NumberFormat('#,##0', 'fr_FR').format(commande.montantTotal)} MGA', style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold)), ], @@ -2974,7 +3103,8 @@ pw.Widget buildEnteteFactureInfos() { borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: + Colors.black.withOpacity(0.1), blurRadius: 2, offset: const Offset(0, 1), ), @@ -2982,16 +3112,18 @@ pw.Widget buildEnteteFactureInfos() { ), child: IconButton( icon: Icon( - Icons.payment, - color: Colors.green.shade600, + Icons.payment, + color: Colors.green.shade600, ), - onPressed: () => _showPaymentOptions(commande), - tooltip: 'Générer le ticket de la commande', + onPressed: () => + _showPaymentOptions(commande), + tooltip: + 'Générer le ticket de la commande', ), ), - const SizedBox( - width: 10, - ), + const SizedBox( + width: 10, + ), Container( decoration: BoxDecoration( color: Colors.white, @@ -3065,10 +3197,10 @@ pw.Widget buildEnteteFactureInfos() { if (commande.statut != StatutCommande.annulee) CommandeActions( - commande: commande, - onStatutChanged: _updateStatut, - onGenerateBonLivraison:_generateBon_lifraisonWithPasswordVerification - ), + commande: commande, + onStatutChanged: _updateStatut, + onGenerateBonLivraison: + _generateBon_lifraisonWithPasswordVerification), ], ), ), diff --git a/lib/Views/historique.dart b/lib/Views/historique.dart index e06f52f..a314951 100644 --- a/lib/Views/historique.dart +++ b/lib/Views/historique.dart @@ -92,7 +92,9 @@ class _HistoriquePageState extends State { final pointDeVenteId = _userController.pointDeVenteId; final commandes = pointDeVenteId == 0 ? allCommandes - : allCommandes.where((cmd) => cmd.pointDeVenteId == pointDeVenteId).toList(); + : allCommandes + .where((cmd) => cmd.pointDeVenteId == pointDeVenteId) + .toList(); setState(() { _commandes.clear(); @@ -146,7 +148,8 @@ class _HistoriquePageState extends State { _filteredCommandes.clear(); for (var commande in _commandes) { - if (pointDeVenteId != 0 && commande.pointDeVenteId != pointDeVenteId) continue; + if (pointDeVenteId != 0 && commande.pointDeVenteId != pointDeVenteId) + continue; bool matchesSearch = searchText.isEmpty || commande.clientNom!.toLowerCase().contains(searchText) || commande.clientPrenom!.toLowerCase().contains(searchText) || @@ -641,10 +644,25 @@ class _HistoriquePageState extends State { ), child: const Icon(Icons.shopping_bag, size: 20), ), - title: Text( - detail.produitNom ?? 'Produit inconnu', - style: - const TextStyle(fontWeight: FontWeight.w500), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + detail.produitNom ?? 'Produit inconnu', + style: const TextStyle( + fontWeight: FontWeight.w500), + ), + if (detail.produitImei != null) ...[ + const SizedBox(height: 4), + Text( + 'IMEI: ${detail.produitImei}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ], ), subtitle: Text( '${detail.quantite} x ${NumberFormat('#,##0.00', 'fr_FR').format(detail.prixUnitaire)} MGA',