diff --git a/lib/Views/commandManagement.dart b/lib/Views/commandManagement.dart index 0a7e8bc..8ff5438 100644 --- a/lib/Views/commandManagement.dart +++ b/lib/Views/commandManagement.dart @@ -277,430 +277,459 @@ class _GestionCommandesPageState extends State { /// via le mécanisme de partage de fichiers du système. /// Future _generateBonLivraison(Commande commande) async { - final details = await _database.getDetailsCommande(commande.id!); - final client = await _database.getClientById(commande.clientId); - final pointDeVente = await _database.getPointDeVenteById(1); - - // 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; - - final iconPhone = await buildIconPhoneText(); - final iconChecked = await buildIconCheckedText(); - final iconGlobe = await buildIconGlobeText(); - - 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; - } + final details = await _database.getDetailsCommande(commande.id!); + final client = await _database.getClientById(commande.clientId); + final pointDeVente = await _database.getPointDeVenteById(1); + + // 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; + + // ✅ 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; } + } - final List> detailsAvecProduits = []; - for (final detail in details) { + // ✅ 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 { + // ✅ Même si le produit est null, on l'ajoute quand même avec les infos du détail + detailsAvecProduits.add({ + 'detail': detail, + 'produit': null, // On garde null mais on utilisera les infos du détail + }); + print(' ⚠️ Produit non trouvé, utilisation des données du détail'); + } + } catch (e) { + print(' ❌ Erreur lors de la récupération du produit: $e'); + // En cas d'erreur, on ajoute quand même le détail detailsAvecProduits.add({ 'detail': detail, - 'produit': produit, + 'produit': null, }); } - - final pdf = pw.Document(); - final imageBytes = await loadImage(); - final image = pw.MemoryImage(imageBytes); - final italicFont = - pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf')); - - // Tailles de texte agrandies pour une meilleure lisibilité - final tinyTextStyle = pw.TextStyle(fontSize: 9); - final smallTextStyle = pw.TextStyle(fontSize: 10); - final normalTextStyle = pw.TextStyle(fontSize: 11); - final boldTextStyle = - pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold); - final boldClientStyle = - pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold); - final frameTextStyle = pw.TextStyle(fontSize: 10); - final italicTextStyle = pw.TextStyle( - fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont); - final italicLogoStyle = pw.TextStyle( - fontSize: 8, fontWeight: pw.FontWeight.bold, font: italicFont); - final titleStyle = - pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold); - final headerStyle = - pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold); - - // Fonction pour créer un exemplaire en mode paysage - pw.Widget buildExemplaire(String typeExemplaire) { - return pw.Container( - height: 380, // Hauteur ajustée pour le mode paysage - 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, - ), + } + + 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'); + // Utiliser les polices par défaut + } + + // ✅ DÉFINITION DES STYLES DE TEXTE - Variables globales dans la fonction + 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); + + // ✅ Fonction pour créer un exemplaire - CORRIGÉE + pw.Widget buildExemplaire(String typeExemplaire) { + return pw.Container( + // ✅ PAS DE HAUTEUR FIXE - Elle s'adapte au contenu + 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.Expanded( - child: pw.Padding( - padding: const pw.EdgeInsets.all(8), - child: pw.Column( + ), + + 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: [ - // En-tête principal - pw.Row( + // Logo et infos entreprise + pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - // Logo et infos entreprise + pw.Container( + width: 100, + height: 100, + child: pw.Image(image), + ), + pw.SizedBox(height: 3), + pw.Text('NOTRE COMPETENCE, A VOTRE SERVICE', style: italicLogoStyle), + pw.SizedBox(height: 4), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Container( - width: 100, - height: 100, - child: pw.Image(image), - ), - pw.SizedBox(height: 3), - pw.Text('NOTRE COMPETENCE, A VOTRE SERVICE', - style: italicLogoStyle), - pw.SizedBox(height: 4), - pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - pw.Text('📍 REMAX Andravoangy', - style: tinyTextStyle), - pw.Text( - '📍 SUPREME CENTER Behoririka \n BOX 405 | 416 | 119', - style: tinyTextStyle), - pw.Text('📍 Tripolisa analankely BOX 7', - style: tinyTextStyle), - pw.Text('📞 033 37 808 18', - style: tinyTextStyle), - pw.Text('🌐 www.guycom.mg', - style: tinyTextStyle), - pw.SizedBox(height: 2), - // pw.Text('NIF: 4000106673 - STAT 95210 11 2017 1 003651', - // style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), - ], - ), + pw.Text('REMAX Andravoangy', style: tinyTextStyle), + pw.Text('SUPREME CENTER Behoririka \n BOX 405 | 416 | 119', style: tinyTextStyle), + pw.Text('Tripolisa analankely BOX 7', style: tinyTextStyle), + pw.Text('033 37 808 18', style: tinyTextStyle), + pw.Text('www.guycom.mg', style: tinyTextStyle), ], ), - - // 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 + ], + ), + + // 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( - width: 120, + padding: const pw.EdgeInsets.all(6), decoration: pw.BoxDecoration( - border: - pw.Border.all(color: PdfColors.black, width: 1), + border: pw.Border.all(color: PdfColors.black), ), - 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} \n ${client?.prenom}', - style: boldTextStyle), + pw.Text('Boutique:', style: frameTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTextStyle), pw.SizedBox(height: 2), - pw.Text(client?.telephone ?? 'Non spécifié', - style: tinyTextStyle), + pw.Text('Bon N°:', style: frameTextStyle), + pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTextStyle), ], ), ), ], ), - - pw.SizedBox(height: 8), - - // Tableau des produits (ajusté pour le mode paysage) - pw.Expanded( - child: pw.Table( - border: pw.TableBorder.all(width: 1), - columnWidths: { - 0: const pw.FlexColumnWidth(5), - 1: const pw.FlexColumnWidth(1.2), - 2: const pw.FlexColumnWidth(1.5), - 3: const pw.FlexColumnWidth(1.5), - 4: const pw.FlexColumnWidth(1.5), - }, + + // 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} \n ${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(3), - 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(3), - 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(3), - child: pw.Text('P.U.', - style: boldTextStyle, - textAlign: pw.TextAlign.right)), - // pw.Padding(padding: const pw.EdgeInsets.all(3), - // child: pw.Text('Remise/Cadeau', style: boldTextStyle, textAlign: pw.TextAlign.center)), + padding: const pw.EdgeInsets.all(4), + child: pw.Text('P.U.', style: boldTextStyle, textAlign: pw.TextAlign.right) + ), pw.Padding( - padding: const pw.EdgeInsets.all(3), - 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) + ), ], ), - ...detailsAvecProduits.map((item) { + + // ✅ 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})'); + 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) - : null, + ? 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(3), + 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 pw.Row( children: [ pw.Expanded( child: pw.Text( - detail.produitNom ?? - 'Produit inconnu', - style: pw.TextStyle( - fontSize: 10, - fontWeight: - pw.FontWeight.bold)), + '${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: 2, - vertical: 1), + padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 1), decoration: pw.BoxDecoration( - color: PdfColors.green, - borderRadius: - pw.BorderRadius.circular(2), + 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 + ) ), - child: pw.Text('🎁', - style: pw.TextStyle( - fontSize: 5, - color: PdfColors.white)), ), ], ), - if (produit?.category != null && - produit!.category.isNotEmpty) - pw.Text( - '${produit.category}${produit?.marque != null && produit!.marque.isNotEmpty ? ' - ${produit.marque}' : ''}', - style: tinyTextStyle), - if (produit?.imei != null && - produit!.imei!.isNotEmpty) - pw.Text('IMEI: ${produit.imei}', - style: tinyTextStyle), - pw.Row( - children: [ - if (produit?.ram != null && - produit!.ram!.isNotEmpty) - pw.Text('${produit.ram}', - style: smallTextStyle), - if (produit?.memoireInterne != null && - produit! - .memoireInterne!.isNotEmpty) - pw.Text( - ' | ${produit.memoireInterne}', - style: smallTextStyle), - pw.Text(' | ${produit.reference}', - style: smallTextStyle), - ], + + 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), ), + + // Spécifications techniques + 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}', + ].join(' , '), + style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600, font: regularFont), + ), ], ), ), + + // Colonne Quantité pw.Padding( - padding: const pw.EdgeInsets.all(3), - child: pw.Text('${detail.quantite}', - style: normalTextStyle, - textAlign: pw.TextAlign.center), + padding: const pw.EdgeInsets.all(4), + child: pw.Text( + '${detail.quantite}', + style: normalTextStyle, + textAlign: pw.TextAlign.center + ), ), + + // Colonne Prix Unitaire pw.Padding( - padding: const pw.EdgeInsets.all(3), + 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( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 8, - decoration: pw - .TextDecoration.lineThrough, - color: PdfColors.grey600)), - pw.Text('GRATUIT', - style: pw.TextStyle( - fontSize: 9, - color: PdfColors.green700, - fontWeight: - pw.FontWeight.bold)), + '${detail.prixUnitaire.toStringAsFixed(0)}', + 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( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 8, - decoration: pw - .TextDecoration.lineThrough, - color: PdfColors.grey600)), + '${detail.prixUnitaire.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 8, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + font: regularFont + ) + ), pw.Text( - '${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 9, - color: PdfColors.orange)), + '${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 10, + color: PdfColors.orange700, + fontWeight: pw.FontWeight.bold, + font: regularFont + ) + ), ] else pw.Text( - '${detail.prixUnitaire.toStringAsFixed(0)}', - style: smallTextStyle), + '${detail.prixUnitaire.toStringAsFixed(0)}', + style: smallTextStyle + ), ], ), ), - // pw.Padding( - // padding: const pw.EdgeInsets.all(3), - // child: pw.Text( - // detail.estCadeau - // ? 'CADEAU' - // : detail.aRemise - // ? 'REMISE' - // : '-', - // style: pw.TextStyle( - // fontSize: 9, - // color: detail.estCadeau ? PdfColors.green700 : detail.aRemise ? PdfColors.orange : PdfColors.grey600, - // fontWeight: detail.estCadeau ? pw.FontWeight.bold : pw.FontWeight.normal, - // ), - // textAlign: pw.TextAlign.center, - // ), - // ), + + // Colonne Montant pw.Padding( - padding: const pw.EdgeInsets.all(3), + 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( - '${detail.sousTotal.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 8, - decoration: pw - .TextDecoration.lineThrough, - color: PdfColors.grey600)), - pw.Text('GRATUIT', - style: pw.TextStyle( - fontSize: 9, - fontWeight: pw.FontWeight.bold, - color: PdfColors.green700)), + '${detail.sousTotal.toStringAsFixed(0)}', + 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( - '${detail.sousTotal.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 8, - decoration: pw - .TextDecoration.lineThrough, - color: PdfColors.grey600)), + '${detail.sousTotal.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 8, + decoration: pw.TextDecoration.lineThrough, + color: PdfColors.grey600, + font: regularFont + ) + ), pw.Text( - '${detail.prixFinal.toStringAsFixed(0)}', - style: pw.TextStyle( - fontSize: 9, - fontWeight: - pw.FontWeight.bold)), + '${detail.prixFinal.toStringAsFixed(0)}', + style: pw.TextStyle( + fontSize: 10, + fontWeight: pw.FontWeight.bold, + font: regularFont + ) + ), ] else pw.Text( - '${detail.prixFinal.toStringAsFixed(0)}', - style: smallTextStyle), + '${detail.prixFinal.toStringAsFixed(0)}', + style: smallTextStyle + ), ], ), ), @@ -708,286 +737,233 @@ class _GestionCommandesPageState extends State { ); }).toList(), ], - ), ), + ], + ), - pw.SizedBox(height: 8), - - // Section finale (ajustée pour le mode paysage) - 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('${sousTotal.toStringAsFixed(0)}', - 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)), - pw.SizedBox(width: 10), - pw.Text( - '-${totalRemises.toStringAsFixed(0)}', - style: pw.TextStyle( - color: PdfColors.orange, - fontSize: 10)), - ], - ), - pw.SizedBox(height: 2), + 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('${sousTotal.toStringAsFixed(0)}', style: smallTextStyle), ], - if (totalCadeaux > 0) ...[ - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.end, - children: [ - pw.Text('CADEAUX ($nombreCadeaux):', - style: pw.TextStyle( - color: PdfColors.green700, - fontSize: 10)), - pw.SizedBox(width: 10), - pw.Text( - '-${totalCadeaux.toStringAsFixed(0)}', - style: pw.TextStyle( - color: PdfColors.green700, - fontSize: 10)), - ], - ), - pw.SizedBox(height: 2), + ), + 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('-${totalRemises.toStringAsFixed(0)}', style: pw.TextStyle(color: PdfColors.orange, fontSize: 10, font: regularFont)), ], - 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( - '${commande.montantTotal.toStringAsFixed(0)} MGA', - style: boldTextStyle), - ], - ), - if (totalCadeaux > 0) ...[ - pw.SizedBox(height: 3), - pw.Container( - padding: const pw.EdgeInsets.all(3), - decoration: pw.BoxDecoration( - color: PdfColors.green50, - borderRadius: pw.BorderRadius.circular(3), - ), - child: pw.Text( - '🎁 $nombreCadeaux cadeau(s) offert(s) (${totalCadeaux.toStringAsFixed(0)} MGA)', - style: pw.TextStyle( - fontSize: 9, color: PdfColors.green700), - ), - ), + ), + 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('-${totalCadeaux.toStringAsFixed(0)}', 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('TOTAL:', style: boldTextStyle), + pw.SizedBox(width: 10), + pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', style: boldTextStyle), ], ), - ), - - pw.SizedBox(width: 15), - - // Informations 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, + ], + ), + ), + + 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.Text('VENDEURS', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold)), - 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), - ), - ], + 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), - ), - ], + ], + ), + ), + 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.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.SizedBox(height: 8), - - // Signatures - pw.Row( - mainAxisAlignment: - pw.MainAxisAlignment.spaceBetween, + pw.Column( children: [ - pw.Column( - children: [ - pw.Text('Vendeur', - style: pw.TextStyle( - fontSize: 9, - fontWeight: pw.FontWeight.bold)), - 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)), - pw.SizedBox(height: 15), - pw.Container( - width: 70, - height: 1, - color: PdfColors.black), - ], - ), + 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: 4), - - // Note finale - pw.Text( - 'Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', - style: italicTextStyle, + ], + ), ), ], ), - ), - ), - ], - ), - ); - } - - // PAGE EN MODE PAYSAGE : Les deux exemplaires sur une seule page - pdf.addPage( - pw.Page( - pageFormat: PdfPageFormat.a4.landscape, // Mode paysage - margin: const pw.EdgeInsets.all(12), - build: (pw.Context context) { - return pw.Row( - // Utilisation de Row au lieu de Column pour placer côte à côte - children: [ - // Premier exemplaire (CLIENT) - pw.Expanded( - child: buildExemplaire("CLIENT"), - ), - - pw.SizedBox(width: 15), - - // Trait de séparation vertical - pw.Container( - width: 2, - height: double.infinity, - child: pw.Column( - mainAxisAlignment: pw.MainAxisAlignment.center, - children: [ - pw.Text('✂️', style: pw.TextStyle(fontSize: 14)), - pw.SizedBox(height: 10), - pw.Transform.rotate( - angle: 1.5708, // 90 degrés en radians (π/2) - child: pw.Text('DÉCOUPER ICI', - style: pw.TextStyle( - fontSize: 10, fontWeight: pw.FontWeight.bold)), - ), - pw.SizedBox(height: 10), - pw.Text('✂️', style: pw.TextStyle(fontSize: 14)), - ], + + pw.SizedBox(height: 6), + + // Note finale + pw.Text( + 'Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', + style: italicTextStyle, ), - ), - - pw.SizedBox(width: 15), - - // Deuxième exemplaire (MAGASIN) - pw.Expanded( - child: buildExemplaire("MAGASIN"), - ), - ], - ); - }, + ], + ), + ), + ], ), ); - - // 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); } //============================================================== @@ -2284,14 +2260,6 @@ class _GestionCommandesPageState extends State { textAlign: pw.TextAlign.center, ), ]), - pw.Text( - '$nombreCadeaux article(s) offert(s)', - style: pw.TextStyle( - fontSize: 6, - color: PdfColors.green600, - ), - textAlign: pw.TextAlign.center, - ), ], ), ), diff --git a/lib/Views/historique.dart b/lib/Views/historique.dart index a5a8efd..e48d92d 100644 --- a/lib/Views/historique.dart +++ b/lib/Views/historique.dart @@ -776,6 +776,13 @@ class _HistoriquePageState extends State { fontSize: 14, ), ), + Text( + '${commande.clientNom} ${commande.clientPrenom}', + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), Text( DateFormat('dd/MM/yyyy').format(commande.dateCommande), style: TextStyle( diff --git a/lib/Views/loginPage.dart b/lib/Views/loginPage.dart index c3670fe..0c5244e 100644 --- a/lib/Views/loginPage.dart +++ b/lib/Views/loginPage.dart @@ -177,7 +177,7 @@ void _login() async { // 6. Navigation immédiate if (mounted) { - if (userCredentials['role'] == 'commercial') { + if (userCredentials['role'] == 'commercial' || userCredentials['role'] == 'caisse') { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const MainLayout()), diff --git a/lib/Views/newCommand.dart b/lib/Views/newCommand.dart index 83e1e34..b9ef0d8 100644 --- a/lib/Views/newCommand.dart +++ b/lib/Views/newCommand.dart @@ -1613,11 +1613,12 @@ void _showMobileFilters(BuildContext context) { label: 'Email', keyboardType: TextInputType.emailAddress, validator: (value) { - if (value?.isEmpty ?? true) return 'Veuillez entrer un email'; - if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) { - return 'Email invalide'; + if (value != null && value.isNotEmpty) { + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + return 'Email invalide'; + } } - return null; + return null; // valide même si vide }, onChanged: (value) async { if (value.length >= 3) { @@ -3940,7 +3941,6 @@ Future _submitOrder() async { // Vérification informations client if (_nomController.text.isEmpty || _prenomController.text.isEmpty || - _emailController.text.isEmpty || _telephoneController.text.isEmpty || _adresseController.text.isEmpty) { Get.snackbar( diff --git a/pubspec.lock b/pubspec.lock index 3fac1ac..a24cbe2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" barcode: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -516,10 +516,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -1193,10 +1193,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: