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: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.livree: message = 'Commande marquée comme livrée'; backgroundColor = Colors.green; 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 _generateInvoice(Commande commande) 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 pdf = pw.Document(); final imageBytes = await loadImage(); final image = pw.MemoryImage(imageBytes); final headerStyle = pw.TextStyle( fontSize: 18, fontWeight: pw.FontWeight.bold, color: PdfColors.blue900, ); final titleStyle = pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ); final subtitleStyle = pw.TextStyle( fontSize: 12, color: PdfColors.grey600, ); pdf.addPage( pw.Page( margin: const pw.EdgeInsets.all(20), build: (pw.Context context) { return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Container( width: 100, height: 80, decoration: pw.BoxDecoration( border: pw.Border.all(color: PdfColors.blue900, width: 2), borderRadius: pw.BorderRadius.circular(8), ), child: pw.Center( child: pw.Image(image) ), ), pw.SizedBox(height: 10), pw.Text('guycom', style: headerStyle), if (pointDeVente != null) pw.Text('Point de vente: ${pointDeVente['designation']}', style: subtitleStyle), pw.Text('Tél: +213 123 456 789', style: subtitleStyle), ], ), pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ pw.Container( padding: const pw.EdgeInsets.all(12), decoration: pw.BoxDecoration( color: PdfColors.blue50, borderRadius: pw.BorderRadius.circular(8), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('FACTURE', style: pw.TextStyle( fontSize: 20, fontWeight: pw.FontWeight.bold, color: PdfColors.blue900, ), ), pw.SizedBox(height: 8), pw.Text('N°: ${commande.id}', style: titleStyle), pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(commande.dateCommande)}'), ], ), ), ], ), ], ), pw.SizedBox(height: 30), // Informations client pw.Container( width: double.infinity, padding: const pw.EdgeInsets.all(12), decoration: pw.BoxDecoration( color: PdfColors.grey100, borderRadius: pw.BorderRadius.circular(8), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('FACTURÉ À:', style: titleStyle), pw.SizedBox(height: 5), pw.Text(client?.nomComplet ?? 'Client inconnu', style: pw.TextStyle(fontSize: 12)), if (client?.telephone != null) pw.Text('Tél: ${client!.telephone}', style: pw.TextStyle(fontSize: 10, color: PdfColors.grey600)), ], ), ), pw.SizedBox(height: 20), // Informations personnel if (commandeur != null || validateur != null) pw.Container( width: double.infinity, padding: const pw.EdgeInsets.all(12), decoration: pw.BoxDecoration( color: PdfColors.grey100, borderRadius: pw.BorderRadius.circular(8), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('PERSONNEL:', style: titleStyle), pw.SizedBox(height: 5), if (commandeur != null) pw.Text('Commandeur: ${commandeur.name} ', style: pw.TextStyle(fontSize: 12)), if (validateur != null) pw.Text('Validateur: ${validateur.name}', style: pw.TextStyle(fontSize: 12)), ], ), ), pw.SizedBox(height: 20), // Tableau des produits pw.Text('DÉTAILS DE LA COMMANDE', style: titleStyle), pw.SizedBox(height: 10), pw.Table( border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5), children: [ pw.TableRow( decoration: const pw.BoxDecoration(color: PdfColors.blue900), children: [ _buildTableCell('Produit', titleStyle.copyWith(color: PdfColors.white)), _buildTableCell('Qté', titleStyle.copyWith(color: PdfColors.white)), _buildTableCell('Prix unit.', titleStyle.copyWith(color: PdfColors.white)), _buildTableCell('Total', titleStyle.copyWith(color: PdfColors.white)), ], ), ...details.asMap().entries.map((entry) { final index = entry.key; final detail = entry.value; final isEven = index % 2 == 0; return pw.TableRow( decoration: pw.BoxDecoration( color: isEven ? PdfColors.white : PdfColors.grey50, ), children: [ _buildTableCell(detail.produitNom ?? 'Produit inconnu'), _buildTableCell(detail.quantite.toString()), _buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'), _buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'), ], ); }), ], ), pw.SizedBox(height: 20), // Total pw.Container( alignment: pw.Alignment.centerRight, child: pw.Container( padding: const pw.EdgeInsets.all(12), decoration: pw.BoxDecoration( color: PdfColors.blue900, borderRadius: pw.BorderRadius.circular(8), ), child: pw.Text( 'TOTAL: ${commande.montantTotal.toStringAsFixed(2)} MGA', style: pw.TextStyle( fontSize: 16, fontWeight: pw.FontWeight.bold, color: PdfColors.white, ), ), ), ), pw.Spacer(), // Pied de page pw.Container( width: double.infinity, padding: const pw.EdgeInsets.all(12), decoration: pw.BoxDecoration( border: pw.Border( top: pw.BorderSide(color: PdfColors.grey400, width: 1), ), ), child: pw.Column( children: [ pw.Text( 'Merci pour votre confiance!', style: pw.TextStyle( fontSize: 14, fontStyle: pw.FontStyle.italic, color: PdfColors.blue900, ), ), pw.SizedBox(height: 5), pw.Text( 'Cette facture est générée automatiquement par le système Youmaz Gestion', style: pw.TextStyle(fontSize: 8, color: PdfColors.grey600), ), ], ), ), ], ); }, ), ); final output = await getTemporaryDirectory(); final file = File('${output.path}/facture_${commande.id}.pdf'); await file.writeAsBytes(await pdf.save()); await OpenFile.open(file.path); } 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 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 pw.Center( child: pw.Container( width: 50, height: 50, child: pw.Image(image), ), ), pw.SizedBox(height: 4), pw.Text('TICKET DE CAISSE', style: pw.TextStyle( fontSize: 10, fontWeight: pw.FontWeight.bold, ), ), pw.Text('N°: ${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), // Client pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}', style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), // Personnel if (commandeur != null) pw.Text('Commandeur: ${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 pw.Table( columnWidths: { 0: const pw.FlexColumnWidth(3), 1: const pw.FlexColumnWidth(1), 2: const pw.FlexColumnWidth(2), }, children: [ pw.TableRow( children: [ pw.Text('Produit', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), pw.Text('Total', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), ], ), ...details.map((detail) => pw.TableRow( children: [ pw.Text(detail.produitNom ?? 'Produit', style: const pw.TextStyle(fontSize: 7)), pw.Text(detail.quantite.toString(), style: const pw.TextStyle(fontSize: 7)), pw.Text('${detail.sousTotal.toStringAsFixed(2)} MGA', style: const pw.TextStyle(fontSize: 7)), ], )), ], ), pw.Divider(thickness: 0.5), // Total pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('TOTAL:', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), pw.Text('${commande.montantTotal.toStringAsFixed(2)} MGA', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), ], ), // Paiement pw.SizedBox(height: 8), pw.Text('MODE DE PAIEMENT:', style: const pw.TextStyle(fontSize: 8)), pw.Text( payment.type == PaymentType.cash ? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(2)} 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(2)} MGA', style: const pw.TextStyle(fontSize: 8)), pw.SizedBox(height: 12), pw.Text('Merci pour votre achat !', style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)), pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)), ], ); }, ), ); 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 } 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(); @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'), content: Column( mainAxisSize: MainAxisSize.min, children: [ RadioListTile( title: const Text('Paiement en liquide'), value: PaymentType.cash, groupValue: _selectedPayment, onChanged: (value) { setState(() { _selectedPayment = value!; }); }, ), if (_selectedPayment == PaymentType.cash) TextField( controller: _amountController, decoration: const InputDecoration( labelText: 'Montant donné', prefixText: 'MGA ', ), keyboardType: TextInputType.number, onChanged: (value) { setState(() {}); // Rafraîchir l'UI pour calculer la monnaie }, ), RadioListTile( title: const Text('Carte bancaire'), value: PaymentType.card, groupValue: _selectedPayment, onChanged: (value) { setState(() { _selectedPayment = value!; }); }, ), if (_selectedPayment == PaymentType.cash) Padding( padding: const EdgeInsets.only(top: 8.0), child: 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'), ), ElevatedButton( onPressed: () { final amount = _selectedPayment == PaymentType.cash ? double.tryParse(_amountController.text) ?? 0 : widget.commande.montantTotal; if (_selectedPayment == PaymentType.cash && amount < widget.commande.montantTotal) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Le montant donné est insuffisant'), backgroundColor: Colors.red, ), ); return; } Navigator.pop( context, PaymentMethod( type: _selectedPayment, amountGiven: amount, ), ); }, child: const Text('Confirmer'), ), ], ); } }