You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2437 lines
92 KiB

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';
import 'package:youmazgestion/Models/produit.dart';
class GestionCommandesPage extends StatefulWidget {
const GestionCommandesPage({super.key});
@override
_GestionCommandesPageState createState() => _GestionCommandesPageState();
}
class _GestionCommandesPageState extends State<GestionCommandesPage> {
final AppDatabase _database = AppDatabase.instance;
List<Commande> _commandes = [];
List<Commande> _filteredCommandes = [];
StatutCommande? _selectedStatut;
DateTime? _selectedDate;
final TextEditingController _searchController = TextEditingController();
bool _showCancelledOrders = false;
final userController = Get.find<UserController>();
@override
void initState() {
super.initState();
_loadCommandes();
_searchController.addListener(_filterCommandes);
}
Future<void> _loadCommandes() async {
final commandes = await _database.getCommandes();
setState(() {
_commandes = commandes;
_filterCommandes();
});
}
Future<Uint8List> 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<void> _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<void> _showPaymentOptions(Commande commande) async {
final selectedPayment = await showDialog<PaymentMethod>(
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<void> _showDiscountDialog(Commande commande) async {
final discountData = await showDialog<Map<String, dynamic>>(
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<void> _showGiftDialog(Commande commande) async {
final selectedProduct = await showDialog<Product>(
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<void> _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<pw.Widget> 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<pw.Widget> 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<pw.Widget> 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<void> _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<Map<String, dynamic>> 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<void> _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<Map<String, dynamic>> 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;
}
}
@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<StatutCommande>(
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<StatutCommande>(
value: null,
child: Text('Tous les statuts'),
),
...StatutCommande.values.map((statut) {
return DropdownMenuItem<StatutCommande>(
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,
),
],
),
),
],
),
);
},
),
),
],
),
);
}
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();
}
}
class _CommandeDetails extends StatelessWidget {
final Commande commande;
const _CommandeDetails({required this.commande});
@override
Widget build(BuildContext context) {
return FutureBuilder<List<DetailCommande>>(
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.estCadeau == true
? '${detail.produitNom ?? 'Produit inconnu'} (CADEAU)'
: detail.produitNom ?? 'Produit inconnu'
),
_buildTableCell('${detail.quantite}'),
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
_buildTableCell(detail.estCadeau == true ? 'OFFERT' : '${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: Column(
children: [
if (commande.montantApresRemise != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Sous-total:',
style: TextStyle(fontSize: 14),
),
Text(
'${commande.montantTotal.toStringAsFixed(2)} MGA',
style: const TextStyle(fontSize: 14),
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Remise:',
style: TextStyle(fontSize: 14),
),
Text(
'-${(commande.montantTotal - commande.montantApresRemise!).toStringAsFixed(2)} MGA',
style: const TextStyle(
fontSize: 14,
color: Colors.red,
),
),
],
),
const Divider(),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total de la commande:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'${(commande.montantApresRemise ?? 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;
final Function(Commande) onDiscountSelected;
final Function(Commande) onGiftSelected;
const _CommandeActions({
required this.commande,
required this.onStatutChanged,
required this.onPaymentSelected,
required this.onDiscountSelected,
required this.onGiftSelected,
});
@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<Widget> _buildActionButtons(BuildContext context) {
List<Widget> buttons = [];
switch (commande.statut) {
case StatutCommande.enAttente:
buttons.addAll([
_buildActionButton(
label: 'Remise',
icon: Icons.percent,
color: Colors.orange,
onPressed: () => onDiscountSelected(commande),
),
_buildActionButton(
label: 'Cadeau',
icon: Icons.card_giftcard,
color: Colors.purple,
onPressed: () => onGiftSelected(commande),
),
_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:
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'),
),
],
);
},
);
}
}
// Dialog pour la remise
class DiscountDialog extends StatefulWidget {
final Commande commande;
const DiscountDialog({super.key, required this.commande});
@override
_DiscountDialogState createState() => _DiscountDialogState();
}
class _DiscountDialogState extends State<DiscountDialog> {
final _pourcentageController = TextEditingController();
final _montantController = TextEditingController();
bool _isPercentage = true;
double _montantFinal = 0;
@override
void initState() {
super.initState();
_montantFinal = widget.commande.montantTotal;
}
void _calculateDiscount() {
double discount = 0;
if (_isPercentage) {
final percentage = double.tryParse(_pourcentageController.text) ?? 0;
discount = (widget.commande.montantTotal * percentage) / 100;
} else {
discount = double.tryParse(_montantController.text) ?? 0;
}
setState(() {
_montantFinal = widget.commande.montantTotal - discount;
if (_montantFinal < 0) _montantFinal = 0;
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Appliquer une remise'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Montant original: ${widget.commande.montantTotal.toStringAsFixed(2)} MGA'),
const SizedBox(height: 16),
// Choix du type de remise
Row(
children: [
Expanded(
child: RadioListTile<bool>(
title: const Text('Pourcentage'),
value: true,
groupValue: _isPercentage,
onChanged: (value) {
setState(() {
_isPercentage = value!;
_calculateDiscount();
});
},
),
),
Expanded(
child: RadioListTile<bool>(
title: const Text('Montant fixe'),
value: false,
groupValue: _isPercentage,
onChanged: (value) {
setState(() {
_isPercentage = value!;
_calculateDiscount();
});
},
),
),
],
),
const SizedBox(height: 16),
if (_isPercentage)
TextField(
controller: _pourcentageController,
decoration: const InputDecoration(
labelText: 'Pourcentage de remise',
suffixText: '%',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
onChanged: (value) => _calculateDiscount(),
)
else
TextField(
controller: _montantController,
decoration: const InputDecoration(
labelText: 'Montant de remise',
suffixText: 'MGA',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
onChanged: (value) => _calculateDiscount(),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant final:'),
Text(
'${_montantFinal.toStringAsFixed(2)} MGA',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
if (_montantFinal < widget.commande.montantTotal)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Économie:'),
Text(
'${(widget.commande.montantTotal - _montantFinal).toStringAsFixed(2)} MGA',
style: const TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _montantFinal < widget.commande.montantTotal
? () {
final pourcentage = _isPercentage
? double.tryParse(_pourcentageController.text)
: null;
final montant = !_isPercentage
? double.tryParse(_montantController.text)
: null;
Navigator.pop(context, {
'pourcentage': pourcentage,
'montant': montant,
'montantFinal': _montantFinal,
});
}
: null,
child: const Text('Appliquer'),
),
],
);
}
@override
void dispose() {
_pourcentageController.dispose();
_montantController.dispose();
super.dispose();
}
}
// Dialog pour sélectionner un cadeau
class GiftSelectionDialog extends StatefulWidget {
final Commande commande;
const GiftSelectionDialog({super.key, required this.commande});
@override
_GiftSelectionDialogState createState() => _GiftSelectionDialogState();
}
class _GiftSelectionDialogState extends State<GiftSelectionDialog> {
List<Product> _products = [];
List<Product> _filteredProducts = [];
final _searchController = TextEditingController();
Product? _selectedProduct;
@override
void initState() {
super.initState();
_loadProducts();
_searchController.addListener(_filterProducts);
}
Future<void> _loadProducts() async {
final products = await AppDatabase.instance.getProducts();
setState(() {
_products = products.where((p) => p.stock > 0).toList();
_filteredProducts = _products;
});
}
void _filterProducts() {
final query = _searchController.text.toLowerCase();
setState(() {
_filteredProducts = _products.where((product) {
return product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false) ||
(product.category.toLowerCase().contains(query));
}).toList();
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Sélectionner un cadeau'),
content: SizedBox(
width: double.maxFinite,
height: 400,
child: Column(
children: [
TextField(
controller: _searchController,
decoration: const InputDecoration(
labelText: 'Rechercher un produit',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _filteredProducts[index];
return Card(
child: ListTile(
leading: product.image != null
? Image.network(
product.image!,
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.image_not_supported),
)
: const Icon(Icons.phone_android),
title: Text(product.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Catégorie: ${product.category}'),
Text('Stock: ${product.stock}'),
if (product.reference != null)
Text('Réf: ${product.reference}'),
],
),
trailing: Radio<Product>(
value: product,
groupValue: _selectedProduct,
onChanged: (value) {
setState(() {
_selectedProduct = value;
});
},
),
onTap: () {
setState(() {
_selectedProduct = product;
});
},
),
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _selectedProduct != null
? () => Navigator.pop(context, _selectedProduct)
: null,
child: const Text('Ajouter le cadeau'),
),
],
);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
}
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<PaymentMethodDialog> {
PaymentType _selectedPayment = PaymentType.cash;
final _amountController = TextEditingController();
void _validatePayment() {
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal;
if (_selectedPayment == PaymentType.cash) {
final amountGiven = double.tryParse(_amountController.text) ?? 0;
if (amountGiven < montantFinal) {
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)
: montantFinal,
));
}
@override
void initState() {
super.initState();
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal;
_amountController.text = montantFinal.toStringAsFixed(2);
}
@override
void dispose() {
_amountController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final amount = double.tryParse(_amountController.text) ?? 0;
final montantFinal = widget.commande.montantApresRemise ?? widget.commande.montantTotal;
final change = amount - montantFinal;
return AlertDialog(
title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Affichage du montant à payer
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
children: [
if (widget.commande.montantApresRemise != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant original:'),
Text('${widget.commande.montantTotal.toStringAsFixed(2)} MGA'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Remise:'),
Text('-${(widget.commande.montantTotal - widget.commande.montantApresRemise!).toStringAsFixed(2)} MGA',
style: const TextStyle(color: Colors.red)),
],
),
const Divider(),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Montant à payer:', style: TextStyle(fontWeight: FontWeight.bold)),
Text('${montantFinal.toStringAsFixed(2)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
],
),
],
),
),
const SizedBox(height: 16),
// 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),
],
),
),
),
);
}
}