import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:numbers_to_letters/numbers_to_letters.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:path_provider/path_provider.dart'; import 'package:open_file/open_file.dart'; import 'package:youmazgestion/Components/app_bar.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Components/commandManagementComponents/CommandeActions.dart'; import 'package:youmazgestion/Components/commandManagementComponents/PaswordRequired.dart'; import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart'; import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethodDialog.dart'; import 'package:youmazgestion/Components/paymentType.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/Models/produit.dart'; import '../Components/commandManagementComponents/CommandDetails.dart'; class GestionCommandesPage extends StatefulWidget { const GestionCommandesPage({super.key}); @override _GestionCommandesPageState createState() => _GestionCommandesPageState(); } class _GestionCommandesPageState extends State { final AppDatabase _database = AppDatabase.instance; List _commandes = []; List _filteredCommandes = []; StatutCommande? _selectedStatut; DateTime? _selectedDate; final TextEditingController _searchController = TextEditingController(); bool _showCancelledOrders = false; final userController = Get.find(); @override void initState() { super.initState(); _loadCommandes(); _searchController.addListener(_filterCommandes); } Future _loadCommandes() async { final commandes = await _database.getCommandes(); setState(() { _commandes = commandes; _filterCommandes(); }); } Future loadImage() async { final data = await rootBundle.load('assets/youmaz2.png'); return data.buffer.asUint8List(); } void _filterCommandes() { final query = _searchController.text.toLowerCase(); setState(() { _filteredCommandes = _commandes.where((commande) { final matchesSearch = commande.clientNomComplet.toLowerCase().contains(query) || commande.id.toString().contains(query); final matchesStatut = _selectedStatut == null || commande.statut == _selectedStatut; final matchesDate = _selectedDate == null || DateFormat('yyyy-MM-dd').format(commande.dateCommande) == DateFormat('yyyy-MM-dd').format(_selectedDate!); final shouldShowCancelled = _showCancelledOrders || commande.statut != StatutCommande.annulee; return matchesSearch && matchesStatut && matchesDate && shouldShowCancelled; }).toList(); }); } Future _updateStatut(int commandeId, StatutCommande newStatut, {int? validateurId}) async { final commandeExistante = await _database.getCommandeById(commandeId); if (commandeExistante == null) { Get.snackbar( 'Erreur', 'Commande introuvable', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); return; } if (validateurId != null) { await _database.updateCommande(Commande( id: commandeId, clientId: commandeExistante.clientId, dateCommande: commandeExistante.dateCommande, statut: newStatut, montantTotal: commandeExistante.montantTotal, notes: commandeExistante.notes, dateLivraison: commandeExistante.dateLivraison, commandeurId: commandeExistante.commandeurId, validateurId: validateurId, clientNom: commandeExistante.clientNom, clientPrenom: commandeExistante.clientPrenom, clientEmail: commandeExistante.clientEmail, )); } else { await _database.updateStatutCommande(commandeId, newStatut); } await _loadCommandes(); String message = 'Statut de la commande mis à jour'; Color backgroundColor = Colors.green; switch (newStatut) { case StatutCommande.annulee: message = 'Commande annulée avec succès'; backgroundColor = Colors.orange; break; case StatutCommande.confirmee: message = 'Commande confirmée'; backgroundColor = Colors.blue; break; default: break; } Get.snackbar( 'Succès', message, snackPosition: SnackPosition.BOTTOM, backgroundColor: backgroundColor, colorText: Colors.white, duration: const Duration(seconds: 2), ); } Future _showPaymentOptions(Commande commande) async { final selectedPayment = await showDialog( context: context, builder: (context) => PaymentMethodDialog(commande: commande), ); if (selectedPayment != null) { if (selectedPayment.type == PaymentType.cash) { await _showCashPaymentDialog(commande, selectedPayment.amountGiven); } await _updateStatut( commande.id!, StatutCommande.confirmee, validateurId: userController.userId, ); await _generateReceipt(commande, selectedPayment); } } Future _showCashPaymentDialog(Commande commande, double amountGiven) async { final amountController = TextEditingController( text: amountGiven.toStringAsFixed(2), ); await showDialog( context: context, builder: (context) { final montantFinal = commande.montantTotal; final change = amountGiven - montantFinal; return AlertDialog( title: const Text('Paiement en liquide'), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Montant total: ${montantFinal.toStringAsFixed(2)} MGA'), const SizedBox(height: 10), TextField( controller: amountController, decoration: const InputDecoration( labelText: 'Montant donné', prefixText: 'MGA ', ), keyboardType: TextInputType.number, onChanged: (value) { final newAmount = double.tryParse(value) ?? 0; if (newAmount >= montantFinal) { setState(() {}); } }, ), const SizedBox(height: 20), if (amountGiven >= montantFinal) Text( 'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green, ), ), if (amountGiven < montantFinal) Text( 'Montant insuffisant', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.red.shade700, ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler'), ), ElevatedButton( onPressed: () { Navigator.pop(context); }, child: const Text('Valider'), ), ], ); }, ); } Future buildIconPhoneText() async { final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); return pw.Text(String.fromCharCode(0xf095), style: pw.TextStyle(font: font)); } Future buildIconGift() async { final font = pw.Font.ttf(await rootBundle.load('assets/NotoEmoji-Regular.ttf')); return pw.Text('🎁', style: pw.TextStyle(font: font, fontSize: 16)); } Future buildIconCheckedText() async { final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); return pw.Text(String.fromCharCode(0xf14a), style: pw.TextStyle(font: font)); } Future buildIconGlobeText() async { final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf')); return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font)); } // Bon de livraison============================================== bool verifAdmin() { return userController.role == 'Super Admin'; } 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 List> detailsAvecProduits = []; for (final detail in details) { final produit = await _database.getProductById(detail.produitId); detailsAvecProduits.add({ 'detail': detail, 'produit': produit, }); } 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, ), ), ), ), pw.Expanded( child: pw.Padding( padding: const pw.EdgeInsets.all(8), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // En-tête principal pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ // Logo et infos entreprise 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)), ], ), ], ), // 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), ), 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), // 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), }, children: [ pw.TableRow( decoration: const pw.BoxDecoration(color: PdfColors.grey200), children: [ pw.Padding(padding: const pw.EdgeInsets.all(3), 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)), 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)), pw.Padding(padding: const pw.EdgeInsets.all(3), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)), ], ), ...detailsAvecProduits.map((item) { final detail = item['detail'] as DetailCommande; final produit = item['produit']; return pw.TableRow( decoration: detail.estCadeau ? const pw.BoxDecoration(color: PdfColors.green50) : detail.aRemise ? const pw.BoxDecoration(color: PdfColors.orange50) : null, children: [ pw.Padding( padding: const pw.EdgeInsets.all(3), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Row( children: [ pw.Expanded( child: pw.Text(detail.produitNom ?? 'Produit inconnu', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)), ), if (detail.estCadeau) pw.Container( padding: const pw.EdgeInsets.symmetric(horizontal: 2, vertical: 1), decoration: pw.BoxDecoration( color: PdfColors.green, borderRadius: pw.BorderRadius.circular(2), ), 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.Padding( padding: const pw.EdgeInsets.all(3), child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center), ), pw.Padding( padding: const pw.EdgeInsets.all(3), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, 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)), ] else if (detail.aRemise) ...[ pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 8, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), pw.Text('${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 9, color: PdfColors.orange)), ] else pw.Text('${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, // ), // ), pw.Padding( padding: const pw.EdgeInsets.all(3), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, 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)), ] else if (detail.aRemise) ...[ pw.Text('${detail.sousTotal.toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 8, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600)), pw.Text('${detail.prixFinal.toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), ] else pw.Text('${detail.prixFinal.toStringAsFixed(0)}', style: smallTextStyle), ], ), ), ], ); }).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), ], 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.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(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, 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('Validateur:', style: tinyTextStyle), pw.Text( validateur != null ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() : 'N/A', style: pw.TextStyle(fontSize: 9), ), ], ), ), ], ), ], ), ), 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)), 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.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(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); } //============================================================== // Modifiez la méthode _generateInvoice dans GestionCommandesPage Future _generateInvoice(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 List> detailsAvecProduits = []; for (final detail in details) { final produit = await _database.getProductById(detail.produitId); detailsAvecProduits.add({ 'detail': detail, 'produit': produit, }); } 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 adaptées pour le mode portrait final smallTextStyle = pw.TextStyle(fontSize: 8); final normalTextStyle = pw.TextStyle(fontSize: 9); final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold); final boldClientTextStyle = pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold); final frameTextStyle = pw.TextStyle(fontSize: 9); final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont); final italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont); final emojiSuportFont = pw.Font.ttf(await rootBundle.load('assets/NotoEmoji-Regular.ttf')); final emojifont = pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, // Mode portrait margin: const pw.EdgeInsets.all(20), // Marges normales build: (pw.Context context) { return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // En-tête avec logo et informations - optimisé pour portrait pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ // Section logo et adresses pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Container( width: 200, height: 120, child: pw.Image(image), ), pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo), pw.SizedBox(height: 10), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]), pw.SizedBox(height: 2), pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]), pw.SizedBox(height: 2), pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]), pw.SizedBox(height: 2), pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]), pw.SizedBox(height: 2), pw.Row(children: [iconChecked, pw.SizedBox(width: 4), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]), ], ), pw.SizedBox(height: 8), pw.Row(children: [iconPhone, pw.SizedBox(width: 4), pw.Text('033 37 808 18', style: smallTextStyle)]), pw.Row(children: [iconGlobe, pw.SizedBox(width: 4), pw.Text('www.guycom.mg', style: smallTextStyle)]), pw.Row(children: [iconGlobe, pw.SizedBox(width: 4), pw.Text('NIF: 1026/GC78-20-02-22', style: smallTextStyle)]), pw.Text('Facebook: GuyCom', style: smallTextStyle), ], ), // Section droite - informations commande et client pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldClientTextStyle), pw.SizedBox(height: 8), pw.Container(width: 200, height: 1, color: PdfColors.black), pw.SizedBox(height: 10), // Informations boutique et facture pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Container( width: 100, height: 45, padding: const pw.EdgeInsets.all(6), decoration: pw.BoxDecoration( border: pw.Border.all(color: PdfColors.black, width: 1), ), child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, children: [ pw.Text('Boutique:', style: frameTextStyle), pw.SizedBox(height: 2), pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldClientTextStyle), ] ) ), pw.SizedBox(width: 10), pw.Container( width: 100, height: 45, padding: const pw.EdgeInsets.all(6), decoration: pw.BoxDecoration( border: pw.Border.all(color: PdfColors.black, width: 1), ), child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, children: [ pw.Text('Facture N°:', style: frameTextStyle), pw.SizedBox(height: 2), pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldClientTextStyle), ] ) ), ], ), pw.SizedBox(height: 15), // Section client pw.Container( width: 220, height: 100, decoration: pw.BoxDecoration( border: pw.Border.all(color: PdfColors.black, width: 1), ), padding: const pw.EdgeInsets.all(10), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Text('ID Client: ', style: frameTextStyle), pw.SizedBox(height: 4), pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldClientTextStyle), pw.SizedBox(height: 6), pw.Container(width: 180, height: 1, color: PdfColors.black), pw.SizedBox(height: 4), pw.Text('${client?.nom} \n ${client?.prenom}', style: boldTextStyle), pw.SizedBox(height: 4), pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle), ], ), ), ], ), ], ), pw.SizedBox(height: 15), // Tableau des produits avec cadeaux - optimisé pour portrait pw.Table( border: pw.TableBorder.all(width: 0.5), columnWidths: { 0: const pw.FlexColumnWidth(3), // Désignations 1: const pw.FlexColumnWidth(0.8), // Quantité 2: const pw.FlexColumnWidth(1.2), // Prix unitaire 3: const pw.FlexColumnWidth(1.5), // Remise/cadeau 4: const pw.FlexColumnWidth(1.2), // Montant }, children: [ pw.TableRow( decoration: const pw.BoxDecoration(color: PdfColors.grey200), children: [ pw.Padding( 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) ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right) ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text('Remise/Cadeau', style: boldTextStyle, textAlign: pw.TextAlign.center) ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right) ), ], ), ...detailsAvecProduits.map((item) { final detail = item['detail'] as DetailCommande; final produit = item['produit']; return pw.TableRow( decoration: detail.estCadeau ? const pw.BoxDecoration( color: PdfColors.green50, border: pw.Border( left: pw.BorderSide( color: PdfColors.green300, width: 3, ), ), ) : detail.aRemise ? const pw.BoxDecoration( color: PdfColors.orange50, border: pw.Border( left: pw.BorderSide( color: PdfColors.orange300, width: 3, ), ), ) : null, children: [ pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Row( children: [ pw.Expanded( child: pw.Text(detail.produitNom ?? 'Produit inconnu', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), ), if (detail.estCadeau) pw.Container( padding: const pw.EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: pw.BoxDecoration( color: PdfColors.green100, borderRadius: pw.BorderRadius.circular(4), ), child: pw.Text( 'CADEAU', style: pw.TextStyle( fontSize: 7, fontWeight: pw.FontWeight.bold, color: PdfColors.green700, ), ), ), ], ), pw.SizedBox(height: 2), if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty) pw.Text('${produit.category} - ${produit.marque}', style: smallTextStyle), if (produit?.imei != null && produit!.imei!.isNotEmpty) pw.Text('IMEI: ${produit.imei}', style: smallTextStyle), if (produit?.reference != null && produit!.reference!.isNotEmpty) 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.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center), ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ if (detail.estCadeau) ...[ pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: pw.TextStyle( fontSize: 7, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600, )), pw.Text('GRATUIT', style: pw.TextStyle( fontSize: 8, color: PdfColors.green700, fontWeight: pw.FontWeight.bold, )), ] else if (detail.aRemise && detail.prixUnitaire != detail.sousTotal / detail.quantite) ...[ pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: pw.TextStyle( fontSize: 7, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600, )), pw.Text('${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 9, color: PdfColors.orange)), ] else pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: normalTextStyle), ], ), ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text( detail.estCadeau ? 'CADEAU\nOFFERT' : detail.aRemise ? detail.remiseDescription : '-', style: pw.TextStyle( fontSize: 7, color: detail.estCadeau ? PdfColors.green700 : detail.aRemise ? PdfColors.orange : PdfColors.grey600, fontWeight: detail.estCadeau ? pw.FontWeight.bold : pw.FontWeight.normal, ), textAlign: pw.TextAlign.center, ), ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ if (detail.estCadeau) ...[ pw.Text('${detail.sousTotal.toStringAsFixed(0)}', style: pw.TextStyle( fontSize: 7, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600, )), pw.Text('GRATUIT', style: pw.TextStyle( fontSize: 8, fontWeight: pw.FontWeight.bold, color: PdfColors.green700, )), ] else if (detail.aRemise && detail.sousTotal != detail.prixFinal) ...[ pw.Text('${detail.sousTotal.toStringAsFixed(0)}', style: pw.TextStyle( fontSize: 7, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600, )), pw.Text('${detail.prixFinal.toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), ] else pw.Text('${detail.prixFinal.toStringAsFixed(0)}', style: normalTextStyle), ], ), ), ], ); }).toList(), ], ), pw.SizedBox(height: 12), // Section totaux - alignée à droite pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ if (totalRemises > 0 || totalCadeaux > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('SOUS-TOTAL', style: normalTextStyle), pw.SizedBox(width: 20), pw.Container( width: 80, child: pw.Text('${sousTotal.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right), ), ], ), pw.SizedBox(height: 4), ], if (totalRemises > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('REMISES TOTALES', style: pw.TextStyle(color: PdfColors.orange, fontSize: 9)), pw.SizedBox(width: 20), pw.Container( width: 80, child: pw.Text('-${totalRemises.toStringAsFixed(0)}', style: pw.TextStyle(color: PdfColors.orange, fontWeight: pw.FontWeight.bold, fontSize: 9), textAlign: pw.TextAlign.right), ), ], ), pw.SizedBox(height: 4), ], if (totalCadeaux > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('CADEAUX OFFERTS ($nombreCadeaux)', style: pw.TextStyle(color: PdfColors.green700, fontSize: 9)), pw.SizedBox(width: 20), pw.Container( width: 80, child: pw.Text('-${totalCadeaux.toStringAsFixed(0)}', style: pw.TextStyle(color: PdfColors.green700, fontWeight: pw.FontWeight.bold, fontSize: 9), textAlign: pw.TextAlign.right), ), ], ), pw.SizedBox(height: 4), ], if (totalRemises > 0 || totalCadeaux > 0) ...[ pw.Container( width: 200, height: 1, color: PdfColors.black, margin: const pw.EdgeInsets.symmetric(vertical: 4), ), ], pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('TOTAL', style: boldTextStyle), pw.SizedBox(width: 20), pw.Container( width: 80, child: pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle, textAlign: pw.TextAlign.right), ), ], ), if (totalRemises > 0 || totalCadeaux > 0) ...[ pw.SizedBox(height: 4), pw.Text( 'Économies réalisées: ${(totalRemises + totalCadeaux).toStringAsFixed(0)} MGA', style: pw.TextStyle( fontSize: 8, color: PdfColors.green, fontStyle: pw.FontStyle.italic, ), ), ], ], ), ], ), pw.SizedBox(height: 15), // Montant en lettres pw.Text('Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', style: italicTextStyle), pw.SizedBox(height: 15), // Informations vendeurs - Section dédiée pw.Container( width: double.infinity, padding: const pw.EdgeInsets.all(12), decoration: pw.BoxDecoration( color: PdfColors.grey100, borderRadius: pw.BorderRadius.circular(8), border: pw.Border.all(color: PdfColors.grey300), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'INFORMATIONS VENDEURS', style: pw.TextStyle( fontSize: 11, fontWeight: pw.FontWeight.bold, color: PdfColors.blue700, ), ), pw.SizedBox(height: 8), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'Vendeur initiateur:', style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700, ), ), pw.SizedBox(height: 3), pw.Text( commandeur != null ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() : 'Non spécifié', style: pw.TextStyle( fontSize: 10, color: PdfColors.black, ), ), pw.SizedBox(height: 3), pw.Text( 'Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', style: pw.TextStyle( fontSize: 8, color: PdfColors.grey600, ), ), ], ), ), pw.Container( width: 1, height: 40, color: PdfColors.grey400, ), pw.SizedBox(width: 20), pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'Vendeur validateur:', style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700, ), ), pw.SizedBox(height: 3), pw.Text( validateur != null ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() : 'Non spécifié', style: pw.TextStyle( fontSize: 10, color: PdfColors.black, ), ), pw.SizedBox(height: 3), pw.Text( 'Date: ${DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now())}', style: pw.TextStyle( fontSize: 8, color: PdfColors.grey600, ), ), ], ), ), ], ), ], ), ), pw.SizedBox(height: 12), // Note de remerciement pour les cadeaux if (totalCadeaux > 0) ...[ pw.Container( width: double.infinity, padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( color: PdfColors.blue50, borderRadius: pw.BorderRadius.circular(6), border: pw.Border.all(color: PdfColors.blue200), ), child: pw.Row( children: [ pw.Text('🎁 ', style: emojifont), pw.Expanded( child: pw.Text( 'Merci de votre confiance ! Nous espérons que nos cadeaux vous feront plaisir. ($nombreCadeaux article(s) offert(s) - Valeur: ${totalCadeaux.toStringAsFixed(0)} MGA)', style: pw.TextStyle( fontSize: 9, fontStyle: pw.FontStyle.italic, color: PdfColors.blue700, ), ), ), ], ), ), pw.SizedBox(height: 12), ], // Signatures - espacées sur toute la largeur pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Text( 'Signature vendeur initiateur', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold), ), pw.SizedBox(height: 2), pw.Text( commandeur != null ? '${commandeur.name} ${commandeur.lastName ?? ''}'.trim() : 'Non spécifié', style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600), ), pw.SizedBox(height: 20), pw.Container(width: 120, height: 1, color: PdfColors.black), ], ), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Text( 'Signature vendeur validateur', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold), ), pw.SizedBox(height: 2), pw.Text( validateur != null ? '${validateur.name} ${validateur.lastName ?? ''}'.trim() : 'Non spécifié', style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600), ), pw.SizedBox(height: 20), pw.Container(width: 120, height: 1, color: PdfColors.black), ], ), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Text( 'Signature du client', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold), ), pw.SizedBox(height: 2), pw.Text( client?.nomComplet ?? 'Non spécifié', style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600), ), pw.SizedBox(height: 20), pw.Container(width: 120, height: 1, color: PdfColors.black), ], ), ], ), ], ); }, ), ); final output = await getTemporaryDirectory(); final file = File('${output.path}/facture_${commande.id}.pdf'); await file.writeAsBytes(await pdf.save()); await OpenFile.open(file.path); } String _numberToWords(int number) { NumbersToLetters.toLetters('fr', number); return NumbersToLetters.toLetters('fr', number); } Future _generateInvoiceWithPasswordVerification(Commande commande) async { await showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return PasswordVerificationDialog( title: 'Génération de facture', message: 'Pour générer la facture de la commande #${commande.id}, veuillez confirmer votre identité en saisissant votre mot de passe.', onPasswordVerified: (String password) async { // Afficher un indicateur de chargement Get.dialog( const Center( child: CircularProgressIndicator(), ), barrierDismissible: false, ); try { await _generateInvoice(commande); Get.back(); // Fermer l'indicateur de chargement Get.snackbar( 'Succès', 'Facture générée avec succès', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), ); } catch (e) { Get.back(); // Fermer l'indicateur de chargement Get.snackbar( 'Erreur', 'Erreur lors de la génération de la facture: $e', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 3), ); } }, ); }, ); } Future _generateBon_lifraisonWithPasswordVerification(Commande commande) async { await showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return PasswordVerificationDialog( title: 'Génération de Bon de livraison', message: 'Pour générer de Bon de livraison de la commande #${commande.id}, veuillez confirmer votre identité en saisissant votre mot de passe.', onPasswordVerified: (String password) async { // Afficher un indicateur de chargement Get.dialog( const Center( child: CircularProgressIndicator(), ), barrierDismissible: false, ); try { await _generateBonLivraison(commande); Get.back(); // Fermer l'indicateur de chargement Get.snackbar( 'Succès', 'Facture générée avec succès', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), ); } catch (e) { Get.back(); // Fermer l'indicateur de chargement Get.snackbar( 'Erreur', 'Erreur lors de la génération de la facture: $e', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 3), ); } }, ); }, ); } String _getPaymentMethodLabel(PaymentMethod payment) { switch (payment.type) { case PaymentType.cash: return 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)'; case PaymentType.card: return 'CARTE BANCAIRE'; case PaymentType.mvola: return 'MVOLA'; case PaymentType.orange: return 'ORANGE MONEY'; case PaymentType.airtel: return 'AIRTEL MONEY'; default: return 'MÉTHODE INCONNUE (${payment.type.toString()})'; // Debug info } } 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 ? await _database.getUserById(commande.commandeurId!) : null; final validateur = commande.validateurId != null ? await _database.getUserById(commande.validateurId!) : null; final pointDeVente = commandeur?.pointDeVenteId != null ? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) : null; final emojiSuportFont = pw.Font.ttf( await rootBundle.load('assets/NotoEmoji-Regular.ttf')); final emojifont = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: emojiSuportFont); final List> detailsAvecProduits = []; for (final detail in details) { final produit = await _database.getProductById(detail.produitId); detailsAvecProduits.add({ 'detail': detail, 'produit': produit, }); } 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 pdf = pw.Document(); final imageBytes = await loadImage(); final image = pw.MemoryImage(imageBytes); // DEBUG: Affichage des informations de paiement print('=== DEBUG PAYMENT METHOD ==='); print('Payment type: ${payment.type}'); print('Payment type toString: ${payment.type.toString()}'); print('Payment type runtimeType: ${payment.type.runtimeType}'); print('Payment type index: ${payment.type.index}'); print('Amount given: ${payment.amountGiven}'); print('PaymentType.airtel: ${PaymentType.airtel}'); print('payment.type == PaymentType.airtel: ${payment.type == PaymentType.airtel}'); print('=== END DEBUG ==='); pdf.addPage( pw.Page( pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity), margin: const pw.EdgeInsets.all(4), build: (pw.Context context) { return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Center( child: pw.Container( width: 40, height: 40, child: pw.Image(image), ), ), pw.SizedBox(height: 4), pw.Text('GUYCOM MADAGASCAR', style: pw.TextStyle( fontSize: 10, fontWeight: pw.FontWeight.bold, )), pw.Text('Tél: 033 37 808 18', style: const pw.TextStyle(fontSize: 7)), pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)), pw.SizedBox(height: 6), pw.Text('TICKET DE CAISSE', style: pw.TextStyle( fontSize: 10, fontWeight: pw.FontWeight.bold, decoration: pw.TextDecoration.underline, )), pw.Text('N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}', style: const pw.TextStyle(fontSize: 8)), pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', style: const pw.TextStyle(fontSize: 8)), if (pointDeVente != null) pw.Text('Point de vente: ${pointDeVente['designation']}', style: const pw.TextStyle(fontSize: 8)), pw.Divider(thickness: 0.5), pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}', style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), if (client?.telephone != null) pw.Text('Tél: ${client!.telephone}', style: const pw.TextStyle(fontSize: 7)), if (commandeur != null || validateur != null) pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Divider(thickness: 0.5), if (commandeur != null) pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)), if (validateur != null) pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)), ], ), pw.Divider(thickness: 0.5), // Tableau des produits avec cadeaux pw.Table( columnWidths: { 0: const pw.FlexColumnWidth(3.5), 1: const pw.FlexColumnWidth(1), 2: const pw.FlexColumnWidth(1.5), }, children: [ pw.TableRow( children: [ pw.Text('Désignation', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), pw.Text('P.U', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), ], decoration: const pw.BoxDecoration( border: pw.Border(bottom: pw.BorderSide(width: 0.5)), ), ), ...detailsAvecProduits.map((item) { final detail = item['detail'] as DetailCommande; final produit = item['produit']; return pw.TableRow( decoration: const pw.BoxDecoration( border: pw.Border(bottom: pw.BorderSide(width: 0.2))), children: [ pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Row( children: [ pw.Expanded( child: pw.Text(detail.produitNom ?? 'Produit', style: const pw.TextStyle(fontSize: 7)), ), if (detail.estCadeau) pw.Text('🎁', style: emojifont), ], ), if (produit?.reference != null) pw.Text('Ref: ${produit!.reference}', style: const pw.TextStyle(fontSize: 6)), if (produit?.imei != null) pw.Text('IMEI: ${produit!.imei}', style: const pw.TextStyle(fontSize: 6)), if (detail.estCadeau) pw.Text('CADEAU OFFERT', style: pw.TextStyle( fontSize: 6, color: PdfColors.green700, fontWeight: pw.FontWeight.bold, )), if (detail.aRemise && !detail.estCadeau) pw.Text('Remise: ${detail.remiseDescription}', style: pw.TextStyle(fontSize: 6, color: PdfColors.orange)), ], ), pw.Text(detail.quantite.toString(), style: const pw.TextStyle(fontSize: 7)), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ if (detail.estCadeau) ...[ pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: pw.TextStyle( fontSize: 6, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600, )), pw.Text('GRATUIT', style: pw.TextStyle( fontSize: 7, color: PdfColors.green700, fontWeight: pw.FontWeight.bold, )), ] else if (detail.aRemise && detail.prixUnitaire != detail.prixFinal / detail.quantite) ...[ pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: pw.TextStyle( fontSize: 6, decoration: pw.TextDecoration.lineThrough, color: PdfColors.grey600, )), pw.Text('${(detail.prixFinal / detail.quantite).toStringAsFixed(0)}', style: const pw.TextStyle(fontSize: 7)), ] else pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: const pw.TextStyle(fontSize: 7)), ], ), ], ); }), ], ), pw.Divider(thickness: 0.5), // Totaux avec remises et cadeaux pour le ticket if (totalRemises > 0 || totalCadeaux > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('SOUS-TOTAL:', style: const pw.TextStyle(fontSize: 8)), pw.Text('${sousTotal.toStringAsFixed(0)} MGA', style: const pw.TextStyle(fontSize: 8)), ], ), if (totalRemises > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('REMISES:', style: pw.TextStyle(fontSize: 8, color: PdfColors.orange)), pw.Text('-${totalRemises.toStringAsFixed(0)} MGA', style: pw.TextStyle(fontSize: 8, color: PdfColors.orange)), ], ), ], if (totalCadeaux > 0) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('CADEAUX ($nombreCadeaux):', style: pw.TextStyle(fontSize: 8, color: PdfColors.green700)), pw.Text('-${totalCadeaux.toStringAsFixed(0)} MGA', style: pw.TextStyle(fontSize: 8, color: PdfColors.green700)), ], ), ], pw.Divider(thickness: 0.3), ], // Total final pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('TOTAL:', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), ], ), if (totalRemises > 0 || totalCadeaux > 0) ...[ pw.SizedBox(height: 4), pw.Text('Économies: ${(totalRemises + totalCadeaux).toStringAsFixed(0)} MGA !', style: pw.TextStyle( fontSize: 7, color: PdfColors.green, fontStyle: pw.FontStyle.italic, ), textAlign: pw.TextAlign.center, ), ], pw.Divider(thickness: 0.5), // Détails du paiement pw.Text('MODE DE PAIEMENT:', style: const pw.TextStyle(fontSize: 8)), pw.Text( _getPaymentMethodLabel(payment), style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), ), if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal) pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(0)} MGA', style: const pw.TextStyle(fontSize: 8)), pw.SizedBox(height: 8), // Messages de fin avec cadeaux if (totalCadeaux > 0) ...[ pw.Container( padding: const pw.EdgeInsets.all(4), decoration: pw.BoxDecoration( color: PdfColors.green50, borderRadius: pw.BorderRadius.circular(4), ), child: pw.Column( children: [ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.center, children: [ pw.Text('🎁', style: emojifont, textAlign: pw.TextAlign.center, ), pw.Text('Profitez de vos cadeaux !', style: pw.TextStyle( fontSize: 7, fontWeight: pw.FontWeight.bold, color: PdfColors.green700, ), textAlign: pw.TextAlign.center, ), pw.Text('🎁', style: emojifont, textAlign: pw.TextAlign.center, ), ] ), pw.Text('$nombreCadeaux article(s) offert(s)', style: pw.TextStyle( fontSize: 6, color: PdfColors.green600, ), textAlign: pw.TextAlign.center, ), ], ), ), pw.SizedBox(height: 6), ], pw.Text('Article non échangeable - Garantie selon conditions', style: const pw.TextStyle(fontSize: 6)), pw.Text('Ticket à conserver comme justificatif', style: const pw.TextStyle(fontSize: 6)), pw.SizedBox(height: 8), pw.Text('Merci pour votre confiance !', style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)), ], ); }, ), ); final output = await getTemporaryDirectory(); final file = File('${output.path}/ticket_${commande.id}.pdf'); await file.writeAsBytes(await pdf.save()); await OpenFile.open(file.path); } Color _getStatutColor(StatutCommande statut) { switch (statut) { case StatutCommande.enAttente: return Colors.orange.shade100; case StatutCommande.confirmee: return Colors.blue.shade100; case StatutCommande.annulee: return Colors.red.shade100; } } IconData _getStatutIcon(StatutCommande statut) { switch (statut) { case StatutCommande.enAttente: return Icons.schedule; case StatutCommande.confirmee: return Icons.check_circle_outline; case StatutCommande.annulee: return Icons.cancel; } } String statutLibelle(StatutCommande statut) { switch (statut) { case StatutCommande.enAttente: return 'En attente'; case StatutCommande.confirmee: return 'Confirmée'; case StatutCommande.annulee: return 'Annulée'; } } @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar(title: 'Gestion des Commandes'), drawer: CustomDrawer(), body: Column( children: [ // Header avec logo et statistiques Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue.shade50, Colors.white], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), child: Column( children: [ // Logo et titre Row( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ) ], ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.asset( 'assets/logo.png', fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( decoration: BoxDecoration( color: Colors.blue.shade900, borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.business, color: Colors.white, size: 30, ), ); }, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Gestion des Commandes', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black87, ), ), Text( '${_filteredCommandes.length} commande(s) affichée(s)', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), ], ), ), ], ), const SizedBox(height: 16), // Barre de recherche améliorée Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ) ], ), child: TextField( controller: _searchController, decoration: InputDecoration( labelText: 'Rechercher par client ou numéro de commande', prefixIcon: Icon(Icons.search, color: Colors.blue.shade800), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), ), ), const SizedBox(height: 16), // Filtres améliorés Row( children: [ Expanded( child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: DropdownButtonFormField( value: _selectedStatut, decoration: InputDecoration( labelText: 'Filtrer par statut', prefixIcon: Icon(Icons.filter_list, color: Colors.blue.shade600), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), items: [ const DropdownMenuItem( value: null, child: Text('Tous les statuts'), ), ...StatutCommande.values.map((statut) { return DropdownMenuItem( value: statut, child: Row( children: [ Icon(_getStatutIcon(statut), size: 16), const SizedBox(width: 8), Text(statutLibelle(statut)), ], ), ); }), ], onChanged: (value) { setState(() { _selectedStatut = value; _filterCommandes(); }); }, ), ), ), const SizedBox(width: 12), Expanded( child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( vertical: 16, horizontal: 12, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), onPressed: () async { final date = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime.now(), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.light( primary: Colors.blue.shade900, ), ), child: child!, ); }, ); if (date != null) { setState(() { _selectedDate = date; _filterCommandes(); }); } }, icon: Icon(Icons.calendar_today, color: Colors.blue.shade600), label: Text( _selectedDate == null ? 'Date' : DateFormat('dd/MM/yyyy') .format(_selectedDate!), style: const TextStyle(color: Colors.black87), ), ), ), ), const SizedBox(width: 12), // Bouton reset Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: IconButton( icon: Icon(Icons.refresh, color: Colors.blue.shade600), onPressed: () { setState(() { _selectedStatut = null; _selectedDate = null; _searchController.clear(); _filterCommandes(); }); }, tooltip: 'Réinitialiser les filtres', ), ), ], ), const SizedBox(height: 12), // Toggle pour afficher/masquer les commandes annulées Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ) ], ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon( Icons.visibility, size: 20, color: Colors.grey.shade600, ), const SizedBox(width: 8), Text( 'Afficher commandes annulées', style: TextStyle( fontSize: 14, color: Colors.grey.shade700, ), ), const Spacer(), Switch( value: _showCancelledOrders, onChanged: (value) { setState(() { _showCancelledOrders = value; _filterCommandes(); }); }, activeColor: Colors.blue.shade600, ), ], ), ), ], ), ), // Liste des commandes Expanded( child: _filteredCommandes.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.inbox, size: 64, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( 'Aucune commande trouvée', style: TextStyle( fontSize: 18, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Text( 'Essayez de modifier vos filtres', style: TextStyle( fontSize: 14, color: Colors.grey.shade500, ), ), ], ), ) : ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _filteredCommandes.length, itemBuilder: (context, index) { final commande = _filteredCommandes[index]; return FutureBuilder>( future: _database.getDetailsCommande(commande.id!), builder: (context, snapshot) { double totalRemises = 0; bool aDesRemises = false; if (snapshot.hasData) { for (final detail in snapshot.data!) { totalRemises += detail.montantRemise; if (detail.aRemise) aDesRemises = true; } } return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: _getStatutColor(commande.statut), borderRadius: BorderRadius.circular(12), border: aDesRemises ? Border.all(color: Colors.orange.shade300, width: 2) : null, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: ExpansionTile( tilePadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), leading: Container( width: 50, height: 50, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(25), border: aDesRemises ? Border.all(color: Colors.orange.shade300, width: 2) : null, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( aDesRemises ? Icons.discount : _getStatutIcon(commande.statut), size: 20, color: aDesRemises ? Colors.teal.shade700 : commande.statut == StatutCommande.annulee ? Colors.red : Colors.blue.shade600, ), Text( '#${commande.id}', style: const TextStyle( fontSize: 10, fontWeight: FontWeight.bold, ), ), ], ), ), title: Text( commande.clientNomComplet, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Row( children: [ Icon( Icons.calendar_today, size: 14, color: Colors.grey.shade600, ), const SizedBox(width: 4), Text( DateFormat('dd/MM/yyyy').format(commande.dateCommande), style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), const SizedBox(width: 16), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Text( commande.statutLibelle, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: commande.statut == StatutCommande.annulee ? Colors.red : Colors.blue.shade700, ), ), ), ], ), const SizedBox(height: 4), Row( children: [ Icon( Icons.attach_money, size: 14, color: Colors.green.shade600, ), const SizedBox(width: 4), Text( '${commande.montantTotal.toStringAsFixed(2)} MGA', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.green.shade700, ), ), // Affichage des remises si elles existent if (totalRemises > 0) ...[ const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.discount, size: 12, color: Colors.teal.shade700, ), const SizedBox(width: 2), Text( '-${totalRemises.toStringAsFixed(0)}', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.teal.shade700, ), ), ], ), ), ], ], ), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: IconButton( icon: Icon( Icons.receipt_outlined, color: Colors.blue.shade600, ), onPressed: () => _generateBon_lifraisonWithPasswordVerification(commande), tooltip: 'Générer le Bon de livraison', ), ), const SizedBox(width: 10,), Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: IconButton( icon: Icon( Icons.receipt_long, color: Colors.blue.shade600, ), onPressed: () => _generateInvoiceWithPasswordVerification(commande), tooltip: 'Générer la facture', ), ), ], ), children: [ Container( padding: const EdgeInsets.all(16.0), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12), ), ), child: Column( children: [ CommandeDetails(commande: commande), const SizedBox(height: 16), if (commande.statut != StatutCommande.annulee) CommandeActions( commande: commande, onStatutChanged: _updateStatut, onPaymentSelected: _showPaymentOptions, ), ], ), ), ], ), ); }, ); }, ) ), ], ), ); } }