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/Models/client.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/controller/userController.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 { // D'abord récupérer la commande existante pour avoir toutes ses valeurs 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) { // Mise à jour avec validateur 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, // On met à jour le validateur clientNom: commandeExistante.clientNom, clientPrenom: commandeExistante.clientPrenom, clientEmail: commandeExistante.clientEmail, )); } else { // Mise à jour simple du statut 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); } // Confirmer la commande avec le validateur actuel await _updateStatut( commande.id!, StatutCommande.confirmee, validateurId: userController.userId, ); // Générer le ticket de caisse 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 change = amountGiven - commande.montantTotal; return AlertDialog( title: const Text('Paiement en liquide'), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Montant total: ${commande.montantTotal.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 >= commande.montantTotal) { setState(() {}); } }, ), const SizedBox(height: 20), if (amountGiven >= commande.montantTotal) Text( 'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green, ), ), if (amountGiven < commande.montantTotal) 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(); // IMPORTANT: Récupérer tous les détails des produits AVANT de créer le PDF 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')); // Styles de texte 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: [ // Première ligne: Logo à gauche, informations à droite pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ // Colonne de gauche avec logo et points de vente pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // Logo pw.Container( width: 150, height: 150, child: pw.Image(image), ), pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo), pw.SizedBox(height: 12), // Liste des points de vente avec checkbox 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)]), ], ), // Informations de contact 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), ], ), // Colonne de droite avec cadres de texte 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), // Deux petits cadres côte à côte 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), ] ) ), ], ), // Grand cadre en dessous 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 avec plus de colonnes pw.Table( border: pw.TableBorder.all(width: 0.5), columnWidths: { 0: const pw.FlexColumnWidth(3), // Désignation 1: const pw.FlexColumnWidth(1), // Qté 2: const pw.FlexColumnWidth(2), // Prix unitaire 3: const pw.FlexColumnWidth(2), // Montant }, children: [ // En-tête du tableau 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)), ], ), // Lignes des produits avec détails complets ...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: [ // Nom du produit pw.Text(detail.produitNom ?? 'Produit inconnu', style: pw.TextStyle(fontSize: 10, 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), // IMEI if (produit?.imei != null && produit!.imei!.isNotEmpty) pw.Text('${produit.imei}', style: smallTextStyle), // Référence 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), // // IMEI // if (produit?.imei != null && produit!.imei!.isNotEmpty) // pw.Text('IMEI: ${produit.imei}', style: smallTextStyle), // // RAM // if (produit?.ram != null && produit!.ram!.isNotEmpty) // pw.Text('RAM: ${produit.ram}', style: smallTextStyle), // // Stockage // if (produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty) // pw.Text('Stockage: ${produit.memoireInterne}', style: smallTextStyle), // // Catégorie ], ), ), 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.prixUnitaire.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right), ), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text('${detail.sousTotal.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right), ), ], ); }).toList(), ], ), pw.SizedBox(height: 10), // Total pw.Row( mainAxisAlignment: pw.MainAxisAlignment.end, children: [ pw.Text('TOTAL', style: boldTextStyle), pw.SizedBox(width: 20), pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle), ], ), pw.SizedBox(height: 10), // Montant en lettres pw.Text('Arrêté à la somme de: ${_numberToWords(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); } pw.Widget _buildCheckboxPointDeVente(String text, bool checked) { return pw.Row( children: [ pw.Container( width: 10, height: 10, decoration: pw.BoxDecoration( border: pw.Border.all(width: 1), color: checked ? PdfColors.black : PdfColors.white, ), ), pw.SizedBox(width: 5), pw.Text(text, style: pw.TextStyle(fontSize: 9)), ], ); } String _numberToWords(int number) { // Implémentez la conversion du nombre en lettres ici // Exemple simplifié: 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; // Récupérer les détails complets des produits 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: [ // En-tête avec logo pw.Center( child: pw.Container( width: 40, height: 40, child: pw.Image(image), ), ), pw.SizedBox(height: 4), // Informations de l'entreprise 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), // Titre et numéro de ticket 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), // Informations client 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)), // Personnel impliqué 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: [ // En-tête du tableau 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)), ),), // Lignes des produits ...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.Text(detail.produitNom ?? 'Produit', style: const pw.TextStyle(fontSize: 7)), 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.prixUnitaire.toStringAsFixed(0)}', style: const pw.TextStyle(fontSize: 7)), ], ); }), ], ), pw.Divider(thickness: 0.5), // Total et paiement 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)), ], ), 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)' : 'CARTE BANCAIRE', 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: 12), // Mentions légales et remerciements 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); } pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) { return pw.Padding( padding: const pw.EdgeInsets.all(4.0), child: pw.Text(text, style: style ?? pw.TextStyle(fontSize: 8)), ); } Color _getStatutColor(StatutCommande statut) { switch (statut) { case StatutCommande.enAttente: return Colors.orange.shade100; case StatutCommande.confirmee: return Colors.blue.shade100; // case StatutCommande.enPreparation: // return Colors.amber.shade100; // case StatutCommande.expediee: // return Colors.purple.shade100; // case StatutCommande.livree: // return Colors.green.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.enPreparation: // return Icons.settings; // case StatutCommande.expediee: // return Icons.local_shipping; // case StatutCommande.livree: // return Icons.check_circle; case StatutCommande.annulee: return Icons.cancel; } } @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: [ // Logo de l'entreprise 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), 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, ), ], ), ), ], ), ); }, ), ), ], ), ); } String statutLibelle(StatutCommande statut) { switch (statut) { case StatutCommande.enAttente: return 'En attente'; case StatutCommande.confirmee: return 'Confirmée'; // case StatutCommande.enPreparation: // return 'En préparation'; // case StatutCommande.expediee: // return 'Expédiée'; // case StatutCommande.livree: // return 'Livrée'; case StatutCommande.annulee: return 'Annulée'; } } @override void dispose() { _searchController.dispose(); super.dispose(); } } class _CommandeDetails extends StatelessWidget { final Commande commande; const _CommandeDetails({required this.commande}); @override Widget build(BuildContext context) { return FutureBuilder>( future: AppDatabase.instance.getDetailsCommande(commande.id!), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Text('Aucun détail disponible'); } final details = snapshot.data!; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: const Text( 'Détails de la commande', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: Colors.black87, ), ), ), const SizedBox(height: 12), Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), ), child: Table( children: [ TableRow( decoration: BoxDecoration( color: Colors.grey.shade100, ), children: [ _buildTableHeader('Produit'), _buildTableHeader('Qté'), _buildTableHeader('Prix unit.'), _buildTableHeader('Total'), ], ), ...details.map((detail) => TableRow( children: [ _buildTableCell(detail.produitNom ?? 'Produit inconnu'), _buildTableCell('${detail.quantite}'), _buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'), _buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'), ], )), ], ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green.shade200), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Total de la commande:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), Text( '${commande.montantTotal.toStringAsFixed(2)} MGA', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18, color: Colors.green.shade700, ), ), ], ), ), ], ); }, ); } Widget _buildTableHeader(String text) { return Padding( padding: const EdgeInsets.all(8.0), child: Text( text, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), textAlign: TextAlign.center, ), ); } Widget _buildTableCell(String text) { return Padding( padding: const EdgeInsets.all(8.0), child: Text( text, style: const TextStyle(fontSize: 13), textAlign: TextAlign.center, ), ); } } class _CommandeActions extends StatelessWidget { final Commande commande; final Function(int, StatutCommande) onStatutChanged; final Function(Commande) onPaymentSelected; const _CommandeActions({ required this.commande, required this.onStatutChanged, required this.onPaymentSelected, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Actions sur la commande', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: _buildActionButtons(context), ), ], ), ); } List _buildActionButtons(BuildContext context) { List buttons = []; switch (commande.statut) { case StatutCommande.enAttente: buttons.addAll([ _buildActionButton( label: 'Confirmer', icon: Icons.check_circle, color: Colors.blue, onPressed: () => onPaymentSelected(commande), ), _buildActionButton( label: 'Annuler', icon: Icons.cancel, color: Colors.red, onPressed: () => _showConfirmDialog( context, 'Annuler la commande', 'Êtes-vous sûr de vouloir annuler cette commande?', () => onStatutChanged(commande.id!, StatutCommande.annulee), ), ), ]); break; case StatutCommande.confirmee: // case StatutCommande.enPreparation: // case StatutCommande.expediee: // case StatutCommande.livree: buttons.add( Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( color: Colors.green.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green.shade300), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.check_circle, color: Colors.green.shade600, size: 16), const SizedBox(width: 8), Text( 'Commande confirmée', style: TextStyle( color: Colors.green.shade700, fontWeight: FontWeight.w600, ), ), ], ), ), ); break; case StatutCommande.annulee: buttons.add( Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( color: Colors.red.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade300), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.cancel, color: Colors.red.shade600, size: 16), const SizedBox(width: 8), Text( 'Commande annulée', style: TextStyle( color: Colors.red.shade700, fontWeight: FontWeight.w600, ), ), ], ), ), ); break; } return buttons; } Widget _buildActionButton({ required String label, required IconData icon, required Color color, required VoidCallback onPressed, }) { return ElevatedButton.icon( onPressed: onPressed, icon: Icon(icon, size: 16), label: Text(label), style: ElevatedButton.styleFrom( backgroundColor: color, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 2, ), ); } void _showConfirmDialog( BuildContext context, String title, String content, VoidCallback onConfirm, ) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), title: Row( children: [ Icon( Icons.help_outline, color: Colors.blue.shade600, ), const SizedBox(width: 8), Text( title, style: const TextStyle(fontSize: 18), ), ], ), content: Text(content), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( 'Annuler', style: TextStyle(color: Colors.grey.shade600), ), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); onConfirm(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade600, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text('Confirmer'), ), ], ); }, ); } } enum PaymentType { cash, card , mvola, orange, airtel } class PaymentMethod { final PaymentType type; final double amountGiven; PaymentMethod({required this.type, this.amountGiven = 0}); } class PaymentMethodDialog extends StatefulWidget { final Commande commande; const PaymentMethodDialog({super.key, required this.commande}); @override _PaymentMethodDialogState createState() => _PaymentMethodDialogState(); } class _PaymentMethodDialogState extends State { PaymentType _selectedPayment = PaymentType.cash; final _amountController = TextEditingController(); void _validatePayment() { if (_selectedPayment == PaymentType.cash) { final amountGiven = double.tryParse(_amountController.text) ?? 0; if (amountGiven < widget.commande.montantTotal) { Get.snackbar( 'Erreur', 'Le montant donné est insuffisant', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); return; } } Navigator.pop(context, PaymentMethod( type: _selectedPayment, amountGiven: _selectedPayment == PaymentType.cash ? double.parse(_amountController.text) : widget.commande.montantTotal, )); } @override void initState() { super.initState(); // Initialiser avec le montant total de la commande _amountController.text = widget.commande.montantTotal.toStringAsFixed(2); } @override void dispose() { _amountController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final amount = double.tryParse(_amountController.text) ?? 0; final change = amount - widget.commande.montantTotal; return AlertDialog( title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ // Section Paiement mobile const Align( alignment: Alignment.centerLeft, child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)), ), const SizedBox(height: 8), Row( children: [ Expanded( child: _buildMobileMoneyTile( title: 'Mvola', imagePath: 'assets/mvola.jpg', value: PaymentType.mvola, ), ), const SizedBox(width: 8), Expanded( child: _buildMobileMoneyTile( title: 'Orange Money', imagePath: 'assets/Orange_money.png', value: PaymentType.orange, ), ), const SizedBox(width: 8), Expanded( child: _buildMobileMoneyTile( title: 'Airtel Money', imagePath: 'assets/airtel_money.png', value: PaymentType.airtel, ), ), ], ), const SizedBox(height: 16), // Section Carte bancaire const Align( alignment: Alignment.centerLeft, child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)), ), const SizedBox(height: 8), _buildPaymentMethodTile( title: 'Carte bancaire', icon: Icons.credit_card, value: PaymentType.card, ), const SizedBox(height: 16), // Section Paiement en liquide const Align( alignment: Alignment.centerLeft, child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)), ), const SizedBox(height: 8), _buildPaymentMethodTile( title: 'Paiement en liquide', icon: Icons.money, value: PaymentType.cash, ), if (_selectedPayment == PaymentType.cash) ...[ const SizedBox(height: 12), TextField( controller: _amountController, decoration: const InputDecoration( labelText: 'Montant donné', prefixText: 'MGA ', border: OutlineInputBorder(), ), keyboardType: TextInputType.numberWithOptions(decimal: true), onChanged: (value) => setState(() {}), ), const SizedBox(height: 8), Text( 'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: change >= 0 ? Colors.green : Colors.red, ), ), ], ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler', style: TextStyle(color: Colors.grey)), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, ), onPressed: _validatePayment, child: const Text('Confirmer'), ), ], ); } Widget _buildMobileMoneyTile({ required String title, required String imagePath, required PaymentType value, }) { return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide( color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), width: 2, ), ), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () => setState(() => _selectedPayment = value), child: Padding( padding: const EdgeInsets.all(12), child: Column( children: [ Image.asset( imagePath, height: 30, width: 30, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) => const Icon(Icons.mobile_friendly, size: 30), ), const SizedBox(height: 8), Text( title, textAlign: TextAlign.center, style: const TextStyle(fontSize: 12), ), ], ), ), ), ); } Widget _buildPaymentMethodTile({ required String title, required IconData icon, required PaymentType value, }) { return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide( color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2), width: 2, ), ), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () => setState(() => _selectedPayment = value), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Icon(icon, size: 24), const SizedBox(width: 12), Text(title), ], ), ), ), ); } }