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/DiscountDialog.dart'; import 'package:youmazgestion/Components/commandManagementComponents/GiftSelectionDialog.dart'; import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart'; import 'package:youmazgestion/Components/commandManagementComponents/PaymentMethodDialog.dart'; import 'package:youmazgestion/Components/commandManagementComponents/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, remisePourcentage: commandeExistante.remisePourcentage, remiseMontant: commandeExistante.remiseMontant, montantApresRemise: commandeExistante.montantApresRemise, )); } 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 _showDiscountDialog(Commande commande) async { final discountData = await showDialog>( context: context, builder: (context) => DiscountDialog(commande: commande), ); if (discountData != null) { // Mettre à jour la commande avec la remise final commandeAvecRemise = Commande( id: commande.id, clientId: commande.clientId, dateCommande: commande.dateCommande, statut: commande.statut, montantTotal: commande.montantTotal, notes: commande.notes, dateLivraison: commande.dateLivraison, commandeurId: commande.commandeurId, validateurId: commande.validateurId, clientNom: commande.clientNom, clientPrenom: commande.clientPrenom, clientEmail: commande.clientEmail, remisePourcentage: discountData['pourcentage'], remiseMontant: discountData['montant'], montantApresRemise: discountData['montantFinal'], ); await _database.updateCommande(commandeAvecRemise); await _loadCommandes(); Get.snackbar( 'Succès', 'Remise appliquée avec succès', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, colorText: Colors.white, ); } } Future _showGiftDialog(Commande commande) async { final selectedProduct = await showDialog( context: context, builder: (context) => GiftSelectionDialog(commande: commande), ); if (selectedProduct != null) { // Ajouter le produit cadeau à la commande avec prix = 0 final detailCadeau = DetailCommande( commandeId: commande.id!, produitId: selectedProduct.id!, quantite: 1, prixUnitaire: 0.0, // Prix = 0 pour un cadeau sousTotal: 0.0, produitNom: selectedProduct.name, estCadeau: true, // Nouveau champ pour identifier les cadeaux ); await _database.createDetailCommande(detailCadeau); await _loadCommandes(); Get.snackbar( 'Succès', 'Cadeau ajouté à la commande', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, colorText: Colors.white, ); } } Future _showCashPaymentDialog(Commande commande, double amountGiven) async { final amountController = TextEditingController( text: amountGiven.toStringAsFixed(2), ); await showDialog( context: context, builder: (context) { final montantFinal = commande.montantApresRemise ?? commande.montantTotal; final change = amountGiven - montantFinal; return AlertDialog( title: const Text('Paiement en liquide'), content: Column( mainAxisSize: MainAxisSize.min, children: [ if (commande.montantApresRemise != null) ...[ Text('Montant original: ${commande.montantTotal.toStringAsFixed(2)} MGA'), Text('Remise: ${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA'), const SizedBox(height: 5), Text('Montant à payer: ${montantFinal.toStringAsFixed(2)} MGA', style: const TextStyle(fontWeight: FontWeight.bold)), ] else 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 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)); } 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); final iconPhone = await buildIconPhoneText(); final iconChecked = await buildIconCheckedText(); final iconGlobe = await buildIconGlobeText(); 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')); final smallTextStyle = pw.TextStyle(fontSize: 9); final smallBoldTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold); final normalTextStyle = pw.TextStyle(fontSize: 10); final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold); final boldTexClienttStyle = 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 italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont); pdf.addPage( pw.Page( margin: const pw.EdgeInsets.all(20), build: (pw.Context context) { return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // En-tête avec logo et informations pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Container( width: 150, height: 150, child: pw.Image(image), ), pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo), pw.SizedBox(height: 12), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]), pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]), pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]), pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]), pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]), ], ), pw.SizedBox(height: 10), pw.Row(children: [iconPhone, pw.SizedBox(width: 5), pw.Text('033 37 808 18', style: smallTextStyle)]), pw.Row(children: [iconGlobe, pw.SizedBox(width: 5), pw.Text('www.guycom.mg', style: smallTextStyle)]), pw.Text('Facebook: GuyCom', style: smallTextStyle), ], ), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldTexClienttStyle), pw.SizedBox(height: 10), pw.Container(width: 200, height: 1, color: PdfColors.black), pw.SizedBox(height: 10), pw.Row( children: [ pw.Container( width: 100, height: 40, padding: const pw.EdgeInsets.all(5), child: pw.Column( children: [ pw.Text('Boutique:', style: frameTextStyle), pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTexClienttStyle), ] ) ), pw.SizedBox(width: 10), pw.Container( width: 100, height: 40, padding: const pw.EdgeInsets.all(5), child: pw.Column( children: [ pw.Text('Bon de livraison N°:', style: frameTextStyle), pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTexClienttStyle), ] ) ), ], ), pw.SizedBox(height: 20), pw.Container( width: 300, 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: 5), pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle), pw.SizedBox(height: 5), pw.Container(width: 200, height: 1, color: PdfColors.black), pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle), pw.SizedBox(height: 10), pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle), ], ), ), ], ), ], ), pw.SizedBox(height: 20), // Tableau des produits pw.Table( border: pw.TableBorder.all(width: 0.5), columnWidths: { 0: const pw.FlexColumnWidth(3), 1: const pw.FlexColumnWidth(1), 2: const pw.FlexColumnWidth(2), 3: const pw.FlexColumnWidth(2), }, 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('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)), ], ), ...detailsAvecProduits.map((item) { final detail = item['detail'] as DetailCommande; final produit = item['produit']; return pw.TableRow( children: [ pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Row( children: [ pw.Text(detail.produitNom ?? 'Produit inconnu', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)), if (detail.estCadeau == true) pw.Text(' (CADEAU)', style: pw.TextStyle(fontSize: 8, color: PdfColors.red, fontWeight: pw.FontWeight.bold)), ], ), 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('${produit.imei}', style: smallTextStyle), if (produit?.reference != null && produit!.reference!.isNotEmpty && produit?.ram != null && produit!.ram!.isNotEmpty && produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty) pw.Text('${produit.ram} | ${produit.memoireInterne} | ${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.Text(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right), ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text(detail.estCadeau == true ? 'OFFERT' : '${detail.sousTotal.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right), ), ], ); }).toList(), ], ), pw.SizedBox(height: 10), // Totaux avec remise pw.Column( children: [ if (commande.montantApresRemise != null) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('SOUS-TOTAL', style: normalTextStyle), pw.SizedBox(width: 20), pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: normalTextStyle), ], ), pw.SizedBox(height: 5), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('REMISE', style: normalTextStyle), pw.SizedBox(width: 20), pw.Text('-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 10, color: PdfColors.red)), ], ), pw.SizedBox(height: 5), pw.Container(width: 200, height: 1, color: PdfColors.black), pw.SizedBox(height: 5), ], pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('TOTAL', style: boldTextStyle), pw.SizedBox(width: 20), pw.Text('${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(0)}', style: boldTextStyle), ], ), ], ), pw.SizedBox(height: 10), pw.Text('Arrêté à la somme de: ${_numberToWords((commande.montantApresRemise ?? commande.montantTotal).toInt())} Ariary', style: italicTextStyle), pw.SizedBox(height: 30), // Signatures pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('Signature du vendeur', style: smallTextStyle), pw.SizedBox(height: 20), pw.Container(width: 150, height: 1, color: PdfColors.black), ], ), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('Signature du client', style: smallTextStyle), pw.SizedBox(height: 20), pw.Container(width: 150, 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 _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 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); 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), // Détails des produits 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.Text(detail.produitNom ?? 'Produit', style: const pw.TextStyle(fontSize: 7)), if (detail.estCadeau == true) pw.Text(' (CADEAU)', style: pw.TextStyle(fontSize: 6, color: PdfColors.red)), ], ), 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)), ], ), pw.Text(detail.quantite.toString(), style: const pw.TextStyle(fontSize: 7)), pw.Text(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(0)}', style: const pw.TextStyle(fontSize: 7)), ], ); }), ], ), pw.Divider(thickness: 0.5), // Totaux avec remise if (commande.montantApresRemise != null) ...[ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('SOUS-TOTAL:', style: const pw.TextStyle(fontSize: 8)), pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA', style: const pw.TextStyle(fontSize: 8)), ], ), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('REMISE:', style: const pw.TextStyle(fontSize: 8)), pw.Text('-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(0)} MGA', style: pw.TextStyle(fontSize: 8, color: PdfColors.red)), ], ), pw.SizedBox(height: 3), ], pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('TOTAL:', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), pw.Text('${(commande.montantApresRemise ?? commande.montantTotal).toStringAsFixed(0)} MGA', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), ], ), pw.SizedBox(height: 6), // Détails du paiement pw.Text('MODE DE PAIEMENT:', style: const pw.TextStyle(fontSize: 8)), pw.Text( payment.type == PaymentType.cash ? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)' : payment.type == PaymentType.card ? 'CARTE BANCAIRE' : payment.type == PaymentType.mvola ? 'MVOLA' : payment.type == PaymentType.orange ? 'ORANGE MONEY' : 'AIRTEL MONEY', style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), ), if (payment.type == PaymentType.cash && payment.amountGiven > (commande.montantApresRemise ?? commande.montantTotal)) pw.Text('Monnaie rendue: ${(payment.amountGiven - (commande.montantApresRemise ?? commande.montantTotal)).toStringAsFixed(0)} MGA', style: const pw.TextStyle(fontSize: 8)), pw.SizedBox(height: 12), 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 Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: _getStatutColor(commande.statut), borderRadius: BorderRadius.circular(12), 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), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( _getStatutIcon(commande.statut), size: 20, color: 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), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (commande.montantApresRemise != null) ...[ Text( 'Total: ${commande.montantTotal.toStringAsFixed(2)} MGA', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, decoration: TextDecoration.lineThrough, ), ), Text( 'Final: ${commande.montantApresRemise!.toStringAsFixed(2)} MGA', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.green.shade700, ), ), ] else Text( '${commande.montantTotal.toStringAsFixed(2)} MGA', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.green.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_long, color: Colors.blue.shade600, ), onPressed: () => _generateInvoice(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, onDiscountSelected: _showDiscountDialog, onGiftSelected: _showGiftDialog, ), ], ), ), ], ), ); }, ), ), ], ), ); } }