import ' dart:io ' ;
import ' dart:typed_data ' ;
import ' package:flutter/material.dart ' ;
import ' package:flutter/services.dart ' ;
import ' package:get/get.dart ' ;
import ' package:intl/intl.dart ' ;
import ' package:numbers_to_letters/numbers_to_letters.dart ' ;
import ' package:pdf/pdf.dart ' ;
import ' package:pdf/widgets.dart ' as pw ;
import ' package:path_provider/path_provider.dart ' ;
import ' package:open_file/open_file.dart ' ;
import ' package:youmazgestion/Components/app_bar.dart ' ;
import ' package:youmazgestion/Components/appDrawer.dart ' ;
import ' package:youmazgestion/Components/commandManagementComponents/CommandeActions.dart ' ;
import ' package:youmazgestion/Components/commandManagementComponents/PaswordRequired.dart ' ;
import ' package:youmazgestion/Components/commandManagementComponents/PaymentMethod.dart ' ;
import ' package:youmazgestion/Components/commandManagementComponents/PaymentMethodDialog.dart ' ;
import ' package:youmazgestion/Components/paymentType.dart ' ;
import ' package:youmazgestion/Models/client.dart ' ;
import ' package:youmazgestion/Services/stock_managementDatabase.dart ' ;
import ' package:youmazgestion/controller/userController.dart ' ;
import ' package:youmazgestion/Models/produit.dart ' ;
import ' ../Components/commandManagementComponents/CommandDetails.dart ' ;
class GestionCommandesPage extends StatefulWidget {
const GestionCommandesPage ( { super . key } ) ;
@ override
_GestionCommandesPageState createState ( ) = > _GestionCommandesPageState ( ) ;
}
class _GestionCommandesPageState extends State < 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 ,
) ) ;
} 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 > _showCashPaymentDialog ( Commande commande , double amountGiven ) async {
final amountController = TextEditingController (
text: amountGiven . toStringAsFixed ( 2 ) ,
) ;
await showDialog (
context: context ,
builder: ( context ) {
final montantFinal = commande . montantTotal ;
final change = amountGiven - montantFinal ;
return AlertDialog (
title: const Text ( ' Paiement en liquide ' ) ,
content: Column (
mainAxisSize: MainAxisSize . min ,
children: [
Text ( ' Montant total: ${ montantFinal . toStringAsFixed ( 2 ) } MGA ' ) ,
const SizedBox ( height: 10 ) ,
TextField (
controller: amountController ,
decoration: const InputDecoration (
labelText: ' Montant donné ' ,
prefixText: ' MGA ' ,
) ,
keyboardType: TextInputType . number ,
onChanged: ( value ) {
final newAmount = double . tryParse ( value ) ? ? 0 ;
if ( newAmount > = montantFinal ) {
setState ( ( ) { } ) ;
}
} ,
) ,
const SizedBox ( height: 20 ) ,
if ( amountGiven > = montantFinal )
Text (
' Monnaie à rendre: ${ change . toStringAsFixed ( 2 ) } MGA ' ,
style: const TextStyle (
fontSize: 18 ,
fontWeight: FontWeight . bold ,
color: Colors . green ,
) ,
) ,
if ( amountGiven < montantFinal )
Text (
' Montant insuffisant ' ,
style: TextStyle (
fontSize: 18 ,
fontWeight: FontWeight . bold ,
color: Colors . red . shade700 ,
) ,
) ,
] ,
) ,
actions: [
TextButton (
onPressed: ( ) = > Navigator . pop ( context ) ,
child: const Text ( ' Annuler ' ) ,
) ,
ElevatedButton (
onPressed: ( ) {
Navigator . pop ( context ) ;
} ,
child: const Text ( ' Valider ' ) ,
) ,
] ,
) ;
} ,
) ;
}
Future < 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 > buildIconGift ( ) async {
final font = pw . Font . ttf ( await rootBundle . load ( ' assets/NotoEmoji-Regular.ttf ' ) ) ;
return pw . Text ( ' 🎁 ' , style: pw . TextStyle ( font: font , fontSize: 16 ) ) ;
}
Future < 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 ) ) ;
}
// Bon de livraison==============================================
Future < void > _generateBonLivraison ( Commande commande ) async {
final details = await _database . getDetailsCommande ( commande . id ! ) ;
final client = await _database . getClientById ( commande . clientId ) ;
final pointDeVente = await _database . getPointDeVenteById ( 1 ) ;
// Récupérer les informations des vendeurs
final commandeur = commande . commandeurId ! = null
? await _database . getUserById ( commande . commandeurId ! )
: null ;
final validateur = commande . validateurId ! = null
? await _database . getUserById ( commande . validateurId ! )
: null ;
final iconPhone = await buildIconPhoneText ( ) ;
final iconChecked = await buildIconCheckedText ( ) ;
final iconGlobe = await buildIconGlobeText ( ) ;
double sousTotal = 0 ;
double totalRemises = 0 ;
double totalCadeaux = 0 ;
int nombreCadeaux = 0 ;
for ( final detail in details ) {
sousTotal + = detail . sousTotal ;
if ( detail . estCadeau ) {
totalCadeaux + = detail . sousTotal ;
nombreCadeaux + = detail . quantite ;
} else {
totalRemises + = detail . montantRemise ;
}
}
final List < 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 ' ) ) ;
// Tailles de texte agrandies pour une meilleure lisibilité
final tinyTextStyle = pw . TextStyle ( fontSize: 9 ) ;
final smallTextStyle = pw . TextStyle ( fontSize: 10 ) ;
final normalTextStyle = pw . TextStyle ( fontSize: 11 ) ;
final boldTextStyle = pw . TextStyle ( fontSize: 11 , fontWeight: pw . FontWeight . bold ) ;
final boldClientStyle = pw . TextStyle ( fontSize: 12 , fontWeight: pw . FontWeight . bold ) ;
final frameTextStyle = pw . TextStyle ( fontSize: 10 ) ;
final italicTextStyle = pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold , font: italicFont ) ;
final italicLogoStyle = pw . TextStyle ( fontSize: 8 , fontWeight: pw . FontWeight . bold , font: italicFont ) ;
final titleStyle = pw . TextStyle ( fontSize: 14 , fontWeight: pw . FontWeight . bold ) ;
final headerStyle = pw . TextStyle ( fontSize: 12 , fontWeight: pw . FontWeight . bold ) ;
// Fonction pour créer un exemplaire en mode paysage
pw . Widget buildExemplaire ( String typeExemplaire ) {
return pw . Container (
height: 380 , // Hauteur ajustée pour le mode paysage
width: double . infinity ,
decoration: pw . BoxDecoration (
border: pw . Border . all ( color: PdfColors . black , width: 1.5 ) ,
) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
// En-tête avec indication de l'exemplaire
pw . Container (
width: double . infinity ,
padding: const pw . EdgeInsets . all ( 5 ) ,
decoration: pw . BoxDecoration (
color: typeExemplaire = = " CLIENT " ? PdfColors . blue100 : PdfColors . green100 ,
) ,
child: pw . Center (
child: pw . Text (
' BON DE LIVRAISON - EXEMPLAIRE $ typeExemplaire ' ,
style: pw . TextStyle (
fontSize: 14 ,
fontWeight: pw . FontWeight . bold ,
color: typeExemplaire = = " CLIENT " ? PdfColors . blue800 : PdfColors . green800 ,
) ,
) ,
) ,
) ,
pw . Expanded (
child: pw . Padding (
padding: const pw . EdgeInsets . all ( 8 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
// En-tête principal
pw . Row (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
// Logo et infos entreprise
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Container (
width: 100 ,
height: 100 ,
child: pw . Image ( image ) ,
) ,
pw . SizedBox ( height: 3 ) ,
pw . Text ( ' NOTRE COMPETENCE, A VOTRE SERVICE ' , style: italicLogoStyle ) ,
pw . SizedBox ( height: 4 ) ,
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Text ( ' 📍 REMAX Andravoangy ' , style: tinyTextStyle ) ,
pw . Text ( ' 📍 SUPREME CENTER Behoririka \n BOX 405 | 416 | 119 ' , style: tinyTextStyle ) ,
pw . Text ( ' 📍 Tripolisa analankely BOX 7 ' , style: tinyTextStyle ) ,
pw . Text ( ' 📞 033 37 808 18 ' , style: tinyTextStyle ) ,
pw . Text ( ' 🌐 www.guycom.mg ' , style: tinyTextStyle ) ,
pw . SizedBox ( height: 2 ) ,
pw . Text ( ' NIF: 1026/GC78-20-02-22 ' ,
style: pw . TextStyle ( fontSize: 7 , fontWeight: pw . FontWeight . bold ) ) ,
] ,
) ,
] ,
) ,
// Informations centrales
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . center ,
children: [
pw . Text ( ' Date: ${ DateFormat ( ' dd/MM/yyyy ' ) . format ( DateTime . now ( ) ) } ' , style: boldClientStyle ) ,
pw . SizedBox ( height: 4 ) ,
pw . Container ( width: 100 , height: 2 , color: PdfColors . black ) ,
pw . SizedBox ( height: 4 ) ,
pw . Container (
padding: const pw . EdgeInsets . all ( 6 ) ,
decoration: pw . BoxDecoration (
border: pw . Border . all ( color: PdfColors . black ) ,
) ,
child: pw . Column (
children: [
pw . Text ( ' Boutique: ' , style: frameTextStyle ) ,
pw . Text ( ' ${ pointDeVente ? [ ' nom ' ] ? ? ' S405A ' } ' , style: boldTextStyle ) ,
pw . SizedBox ( height: 2 ) ,
pw . Text ( ' Bon N°: ' , style: frameTextStyle ) ,
pw . Text ( ' ${ pointDeVente ? [ ' nom ' ] ? ? ' S405A ' } -P ${ commande . id } ' , style: boldTextStyle ) ,
] ,
) ,
) ,
] ,
) ,
// Informations client
pw . Container (
width: 120 ,
decoration: pw . BoxDecoration (
border: pw . Border . all ( color: PdfColors . black , width: 1 ) ,
) ,
padding: const pw . EdgeInsets . all ( 6 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . center ,
children: [
pw . Text ( ' CLIENT ' , style: frameTextStyle ) ,
pw . SizedBox ( height: 2 ) ,
pw . Text ( ' ID: ${ pointDeVente ? [ ' nom ' ] ? ? ' S405A ' } - ${ client ? . id ? ? ' Non spécifié ' } ' , style: smallTextStyle ) ,
pw . Container ( width: 100 , height: 1 , color: PdfColors . black , margin: const pw . EdgeInsets . symmetric ( vertical: 2 ) ) ,
pw . Text ( ' ${ client ? . nom } \n ${ client ? . prenom } ' , style: boldTextStyle ) ,
pw . SizedBox ( height: 2 ) ,
pw . Text ( client ? . telephone ? ? ' Non spécifié ' , style: tinyTextStyle ) ,
] ,
) ,
) ,
] ,
) ,
pw . SizedBox ( height: 8 ) ,
// Tableau des produits (ajusté pour le mode paysage)
pw . Expanded (
child: pw . Table (
border: pw . TableBorder . all ( width: 1 ) ,
columnWidths: {
0 : const pw . FlexColumnWidth ( 5 ) ,
1 : const pw . FlexColumnWidth ( 1.2 ) ,
2 : const pw . FlexColumnWidth ( 1.5 ) ,
3 : const pw . FlexColumnWidth ( 1.5 ) ,
4 : const pw . FlexColumnWidth ( 1.5 ) ,
} ,
children: [
pw . TableRow (
decoration: const pw . BoxDecoration ( color: PdfColors . grey200 ) ,
children: [
pw . Padding ( padding: const pw . EdgeInsets . all ( 3 ) ,
child: pw . Text ( ' Désignations ' , style: boldTextStyle ) ) ,
pw . Padding ( padding: const pw . EdgeInsets . all ( 3 ) ,
child: pw . Text ( ' Qté ' , style: boldTextStyle , textAlign: pw . TextAlign . center ) ) ,
pw . Padding ( padding: const pw . EdgeInsets . all ( 3 ) ,
child: pw . Text ( ' P.U. ' , style: boldTextStyle , textAlign: pw . TextAlign . right ) ) ,
// pw.Padding(padding: const pw.EdgeInsets.all(3),
// child: pw.Text('Remise/Cadeau', style: boldTextStyle, textAlign: pw.TextAlign.center)),
pw . Padding ( padding: const pw . EdgeInsets . all ( 3 ) ,
child: pw . Text ( ' Montant ' , style: boldTextStyle , textAlign: pw . TextAlign . right ) ) ,
] ,
) ,
. . . detailsAvecProduits . map ( ( item ) {
final detail = item [ ' detail ' ] as DetailCommande ;
final produit = item [ ' produit ' ] ;
return pw . TableRow (
decoration: detail . estCadeau
? const pw . BoxDecoration ( color: PdfColors . green50 )
: detail . aRemise
? const pw . BoxDecoration ( color: PdfColors . orange50 )
: null ,
children: [
pw . Padding (
padding: const pw . EdgeInsets . all ( 3 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Row (
children: [
pw . Expanded (
child: pw . Text ( detail . produitNom ? ? ' Produit inconnu ' ,
style: pw . TextStyle ( fontSize: 10 , fontWeight: pw . FontWeight . bold ) ) ,
) ,
if ( detail . estCadeau )
pw . Container (
padding: const pw . EdgeInsets . symmetric ( horizontal: 2 , vertical: 1 ) ,
decoration: pw . BoxDecoration (
color: PdfColors . green ,
borderRadius: pw . BorderRadius . circular ( 2 ) ,
) ,
child: pw . Text ( ' 🎁 ' , style: pw . TextStyle ( fontSize: 5 , color: PdfColors . white ) ) ,
) ,
] ,
) ,
if ( produit ? . category ! = null & & produit ! . category . isNotEmpty )
pw . Text ( ' ${ produit . category } ${ produit ? . marque ! = null & & produit ! . marque . isNotEmpty ? ' - $ {produit.marque } ' : ' ' } ' ,
style: tinyTextStyle ) ,
if ( produit ? . imei ! = null & & produit ! . imei ! . isNotEmpty )
pw . Text ( ' IMEI: ${ produit . imei } ' , style: tinyTextStyle ) ,
] ,
) ,
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 3 ) ,
child: pw . Text ( ' ${ detail . quantite } ' , style: normalTextStyle , textAlign: pw . TextAlign . center ) ,
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 3 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
children: [
if ( detail . estCadeau ) . . . [
pw . Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle ( fontSize: 8 , decoration: pw . TextDecoration . lineThrough , color: PdfColors . grey600 ) ) ,
pw . Text ( ' GRATUIT ' , style: pw . TextStyle ( fontSize: 9 , color: PdfColors . green700 , fontWeight: pw . FontWeight . bold ) ) ,
] else if ( detail . aRemise ) . . . [
pw . Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle ( fontSize: 8 , decoration: pw . TextDecoration . lineThrough , color: PdfColors . grey600 ) ) ,
pw . Text ( ' ${ ( detail . prixFinal / detail . quantite ) . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle ( fontSize: 9 , color: PdfColors . orange ) ) ,
] else
pw . Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 0 ) } ' , style: smallTextStyle ) ,
] ,
) ,
) ,
// pw.Padding(
// padding: const pw.EdgeInsets.all(3),
// child: pw.Text(
// detail.estCadeau
// ? 'CADEAU'
// : detail.aRemise
// ? 'REMISE'
// : '-',
// style: pw.TextStyle(
// fontSize: 9,
// color: detail.estCadeau ? PdfColors.green700 : detail.aRemise ? PdfColors.orange : PdfColors.grey600,
// fontWeight: detail.estCadeau ? pw.FontWeight.bold : pw.FontWeight.normal,
// ),
// textAlign: pw.TextAlign.center,
// ),
// ),
pw . Padding (
padding: const pw . EdgeInsets . all ( 3 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
children: [
if ( detail . estCadeau ) . . . [
pw . Text ( ' ${ detail . sousTotal . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle ( fontSize: 8 , decoration: pw . TextDecoration . lineThrough , color: PdfColors . grey600 ) ) ,
pw . Text ( ' GRATUIT ' , style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold , color: PdfColors . green700 ) ) ,
] else if ( detail . aRemise ) . . . [
pw . Text ( ' ${ detail . sousTotal . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle ( fontSize: 8 , decoration: pw . TextDecoration . lineThrough , color: PdfColors . grey600 ) ) ,
pw . Text ( ' ${ detail . prixFinal . toStringAsFixed ( 0 ) } ' , style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ) ,
] else
pw . Text ( ' ${ detail . prixFinal . toStringAsFixed ( 0 ) } ' , style: smallTextStyle ) ,
] ,
) ,
) ,
] ,
) ;
} ) . toList ( ) ,
] ,
) ,
) ,
pw . SizedBox ( height: 8 ) ,
// Section finale (ajustée pour le mode paysage)
pw . Row (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
// Totaux
pw . Expanded (
flex: 2 ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
children: [
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Text ( ' SOUS-TOTAL: ' , style: smallTextStyle ) ,
pw . SizedBox ( width: 10 ) ,
pw . Text ( ' ${ sousTotal . toStringAsFixed ( 0 ) } ' , style: smallTextStyle ) ,
] ,
) ,
pw . SizedBox ( height: 2 ) ,
] ,
if ( totalRemises > 0 ) . . . [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Text ( ' REMISES: ' , style: pw . TextStyle ( color: PdfColors . orange , fontSize: 10 ) ) ,
pw . SizedBox ( width: 10 ) ,
pw . Text ( ' - ${ totalRemises . toStringAsFixed ( 0 ) } ' , style: pw . TextStyle ( color: PdfColors . orange , fontSize: 10 ) ) ,
] ,
) ,
pw . SizedBox ( height: 2 ) ,
] ,
if ( totalCadeaux > 0 ) . . . [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Text ( ' CADEAUX ( $ nombreCadeaux ): ' , style: pw . TextStyle ( color: PdfColors . green700 , fontSize: 10 ) ) ,
pw . SizedBox ( width: 10 ) ,
pw . Text ( ' - ${ totalCadeaux . toStringAsFixed ( 0 ) } ' , style: pw . TextStyle ( color: PdfColors . green700 , fontSize: 10 ) ) ,
] ,
) ,
pw . SizedBox ( height: 2 ) ,
] ,
pw . Container ( width: 120 , height: 1.5 , color: PdfColors . black , margin: const pw . EdgeInsets . symmetric ( vertical: 2 ) ) ,
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Text ( ' TOTAL: ' , style: boldTextStyle ) ,
pw . SizedBox ( width: 10 ) ,
pw . Text ( ' ${ commande . montantTotal . toStringAsFixed ( 0 ) } MGA ' , style: boldTextStyle ) ,
] ,
) ,
if ( totalCadeaux > 0 ) . . . [
pw . SizedBox ( height: 3 ) ,
pw . Container (
padding: const pw . EdgeInsets . all ( 3 ) ,
decoration: pw . BoxDecoration (
color: PdfColors . green50 ,
borderRadius: pw . BorderRadius . circular ( 3 ) ,
) ,
child: pw . Text (
' 🎁 $ nombreCadeaux cadeau(s) offert(s) ( ${ totalCadeaux . toStringAsFixed ( 0 ) } MGA) ' ,
style: pw . TextStyle ( fontSize: 9 , color: PdfColors . green700 ) ,
) ,
) ,
] ,
] ,
) ,
) ,
pw . SizedBox ( width: 15 ) ,
// Informations vendeurs et signatures
pw . Expanded (
flex: 3 ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
// Vendeurs
pw . Container (
padding: const pw . EdgeInsets . all ( 4 ) ,
decoration: pw . BoxDecoration (
color: PdfColors . grey100 ,
borderRadius: pw . BorderRadius . circular ( 3 ) ,
) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Text ( ' VENDEURS ' , style: pw . TextStyle ( fontSize: 10 , fontWeight: pw . FontWeight . bold ) ) ,
pw . SizedBox ( height: 3 ) ,
pw . Row (
children: [
pw . Expanded (
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Text ( ' Initiateur: ' , style: tinyTextStyle ) ,
pw . Text (
commandeur ! = null ? ' ${ commandeur . name } ${ commandeur . lastName ? ? ' ' } ' . trim ( ) : ' N/A ' ,
style: pw . TextStyle ( fontSize: 9 ) ,
) ,
] ,
) ,
) ,
pw . Expanded (
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Text ( ' Validateur: ' , style: tinyTextStyle ) ,
pw . Text (
validateur ! = null ? ' ${ validateur . name } ${ validateur . lastName ? ? ' ' } ' . trim ( ) : ' N/A ' ,
style: pw . TextStyle ( fontSize: 9 ) ,
) ,
] ,
) ,
) ,
] ,
) ,
] ,
) ,
) ,
pw . SizedBox ( height: 8 ) ,
// Signatures
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
pw . Column (
children: [
pw . Text ( ' Vendeur ' , style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ) ,
pw . SizedBox ( height: 15 ) ,
pw . Container ( width: 70 , height: 1 , color: PdfColors . black ) ,
] ,
) ,
pw . Column (
children: [
pw . Text ( ' Client ' , style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ) ,
pw . SizedBox ( height: 15 ) ,
pw . Container ( width: 70 , height: 1 , color: PdfColors . black ) ,
] ,
) ,
] ,
) ,
] ,
) ,
) ,
] ,
) ,
pw . SizedBox ( height: 4 ) ,
// Note finale
pw . Text (
' Arrêté à la somme de: ${ _numberToWords ( commande . montantTotal . toInt ( ) ) } Ariary ' ,
style: italicTextStyle ,
) ,
] ,
) ,
) ,
) ,
] ,
) ,
) ;
}
// PAGE EN MODE PAYSAGE : Les deux exemplaires sur une seule page
pdf . addPage (
pw . Page (
pageFormat: PdfPageFormat . a4 . landscape , // Mode paysage
margin: const pw . EdgeInsets . all ( 12 ) ,
build: ( pw . Context context ) {
return pw . Row ( // Utilisation de Row au lieu de Column pour placer côte à côte
children: [
// Premier exemplaire (CLIENT)
pw . Expanded (
child: buildExemplaire ( " CLIENT " ) ,
) ,
pw . SizedBox ( width: 15 ) ,
// Trait de séparation vertical
pw . Container (
width: 2 ,
height: double . infinity ,
child: pw . Column (
mainAxisAlignment: pw . MainAxisAlignment . center ,
children: [
pw . Text ( ' ✂️ ' , style: pw . TextStyle ( fontSize: 14 ) ) ,
pw . SizedBox ( height: 10 ) ,
pw . Transform . rotate (
angle: 1.5708 , // 90 degrés en radians (π/2)
child: pw . Text ( ' DÉCOUPER ICI ' , style: pw . TextStyle ( fontSize: 10 , fontWeight: pw . FontWeight . bold ) ) ,
) ,
pw . SizedBox ( height: 10 ) ,
pw . Text ( ' ✂️ ' , style: pw . TextStyle ( fontSize: 14 ) ) ,
] ,
) ,
) ,
pw . SizedBox ( width: 15 ) ,
// Deuxième exemplaire (MAGASIN)
pw . Expanded (
child: buildExemplaire ( " MAGASIN " ) ,
) ,
] ,
) ;
} ,
) ,
) ;
// Sauvegarder le PDF
final output = await getTemporaryDirectory ( ) ;
final file = File ( " ${ output . path } /bon_livraison_ ${ commande . id } .pdf " ) ;
await file . writeAsBytes ( await pdf . save ( ) ) ;
// Partager ou ouvrir le fichier
await OpenFile . open ( file . path ) ;
}
//==============================================================
// Modifiez la méthode _generateInvoice dans GestionCommandesPage
Future < 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 ) ;
// Récupérer les informations des vendeurs
final commandeur = commande . commandeurId ! = null
? await _database . getUserById ( commande . commandeurId ! )
: null ;
final validateur = commande . validateurId ! = null
? await _database . getUserById ( commande . validateurId ! )
: null ;
final iconPhone = await buildIconPhoneText ( ) ;
final iconChecked = await buildIconCheckedText ( ) ;
final iconGlobe = await buildIconGlobeText ( ) ;
double sousTotal = 0 ;
double totalRemises = 0 ;
double totalCadeaux = 0 ;
int nombreCadeaux = 0 ;
for ( final detail in details ) {
sousTotal + = detail . sousTotal ;
if ( detail . estCadeau ) {
totalCadeaux + = detail . sousTotal ;
nombreCadeaux + = detail . quantite ;
} else {
totalRemises + = detail . montantRemise ;
}
}
final List < 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 ' ) ) ;
// Tailles de texte adaptées pour le mode portrait
final smallTextStyle = pw . TextStyle ( fontSize: 8 ) ;
final normalTextStyle = pw . TextStyle ( fontSize: 9 ) ;
final boldTextStyle = pw . TextStyle ( fontSize: 10 , fontWeight: pw . FontWeight . bold ) ;
final boldClientTextStyle = pw . TextStyle ( fontSize: 11 , fontWeight: pw . FontWeight . bold ) ;
final frameTextStyle = pw . TextStyle ( fontSize: 9 ) ;
final italicTextStyle = pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold , font: italicFont ) ;
final italicTextStyleLogo = pw . TextStyle ( fontSize: 7 , fontWeight: pw . FontWeight . bold , font: italicFont ) ;
final emojiSuportFont = pw . Font . ttf ( await rootBundle . load ( ' assets/NotoEmoji-Regular.ttf ' ) ) ;
final emojifont = pw . TextStyle ( fontSize: 8 , fontWeight: pw . FontWeight . bold , font: emojiSuportFont ) ;
pdf . addPage (
pw . Page (
pageFormat: PdfPageFormat . a4 , // Mode portrait
margin: const pw . EdgeInsets . all ( 20 ) , // Marges normales
build: ( pw . Context context ) {
return pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
// En-tête avec logo et informations - optimisé pour portrait
pw . Row (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
// Section logo et adresses
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Container (
width: 200 ,
height: 120 ,
child: pw . Image ( image ) ,
) ,
pw . Text ( ' NOTRE COMPETENCE, A VOTRE SERVICE ' , style: italicTextStyleLogo ) ,
pw . SizedBox ( height: 10 ) ,
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Row ( children: [ iconChecked , pw . SizedBox ( width: 4 ) , pw . Text ( ' REMAX by GUYCOM Andravoangy ' , style: smallTextStyle ) ] ) ,
pw . SizedBox ( height: 2 ) ,
pw . Row ( children: [ iconChecked , pw . SizedBox ( width: 4 ) , pw . Text ( ' SUPREME CENTER Behoririka box 405 ' , style: smallTextStyle ) ] ) ,
pw . SizedBox ( height: 2 ) ,
pw . Row ( children: [ iconChecked , pw . SizedBox ( width: 4 ) , pw . Text ( ' SUPREME CENTER Behoririka box 416 ' , style: smallTextStyle ) ] ) ,
pw . SizedBox ( height: 2 ) ,
pw . Row ( children: [ iconChecked , pw . SizedBox ( width: 4 ) , pw . Text ( ' SUPREME CENTER Behoririka box 119 ' , style: smallTextStyle ) ] ) ,
pw . SizedBox ( height: 2 ) ,
pw . Row ( children: [ iconChecked , pw . SizedBox ( width: 4 ) , pw . Text ( ' TRIPOLITSA Analakely BOX 7 ' , style: smallTextStyle ) ] ) ,
] ,
) ,
pw . SizedBox ( height: 8 ) ,
pw . Row ( children: [ iconPhone , pw . SizedBox ( width: 4 ) , pw . Text ( ' 033 37 808 18 ' , style: smallTextStyle ) ] ) ,
pw . Row ( children: [ iconGlobe , pw . SizedBox ( width: 4 ) , pw . Text ( ' www.guycom.mg ' , style: smallTextStyle ) ] ) ,
pw . Row ( children: [ iconGlobe , pw . SizedBox ( width: 4 ) , pw . Text ( ' NIF: 1026/GC78-20-02-22 ' , style: smallTextStyle ) ] ) ,
pw . Text ( ' Facebook: GuyCom ' , style: smallTextStyle ) ,
] ,
) ,
// Section droite - informations commande et client
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
children: [
pw . Text ( ' Date: ${ DateFormat ( ' dd/MM/yyyy ' ) . format ( DateTime . now ( ) ) } ' , style: boldClientTextStyle ) ,
pw . SizedBox ( height: 8 ) ,
pw . Container ( width: 200 , height: 1 , color: PdfColors . black ) ,
pw . SizedBox ( height: 10 ) ,
// Informations boutique et facture
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Container (
width: 100 ,
height: 45 ,
padding: const pw . EdgeInsets . all ( 6 ) ,
decoration: pw . BoxDecoration (
border: pw . Border . all ( color: PdfColors . black , width: 1 ) ,
) ,
child: pw . Column (
mainAxisAlignment: pw . MainAxisAlignment . center ,
children: [
pw . Text ( ' Boutique: ' , style: frameTextStyle ) ,
pw . SizedBox ( height: 2 ) ,
pw . Text ( ' ${ pointDeVente ? [ ' nom ' ] ? ? ' S405A ' } ' , style: boldClientTextStyle ) ,
]
)
) ,
pw . SizedBox ( width: 10 ) ,
pw . Container (
width: 100 ,
height: 45 ,
padding: const pw . EdgeInsets . all ( 6 ) ,
decoration: pw . BoxDecoration (
border: pw . Border . all ( color: PdfColors . black , width: 1 ) ,
) ,
child: pw . Column (
mainAxisAlignment: pw . MainAxisAlignment . center ,
children: [
pw . Text ( ' Facture N°: ' , style: frameTextStyle ) ,
pw . SizedBox ( height: 2 ) ,
pw . Text ( ' ${ pointDeVente ? [ ' nom ' ] ? ? ' S405A ' } -P ${ commande . id } ' , style: boldClientTextStyle ) ,
]
)
) ,
] ,
) ,
pw . SizedBox ( height: 15 ) ,
// Section client
pw . Container (
width: 220 ,
height: 100 ,
decoration: pw . BoxDecoration (
border: pw . Border . all ( color: PdfColors . black , width: 1 ) ,
) ,
padding: const pw . EdgeInsets . all ( 10 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . center ,
children: [
pw . Text ( ' ID Client: ' , style: frameTextStyle ) ,
pw . SizedBox ( height: 4 ) ,
pw . Text ( ' ${ pointDeVente ? [ ' nom ' ] ? ? ' S405A ' } - ${ client ? . id ? ? ' Non spécifié ' } ' , style: boldClientTextStyle ) ,
pw . SizedBox ( height: 6 ) ,
pw . Container ( width: 180 , height: 1 , color: PdfColors . black ) ,
pw . SizedBox ( height: 4 ) ,
pw . Text ( ' ${ client ? . nom } \n ${ client ? . prenom } ' , style: boldTextStyle ) ,
pw . SizedBox ( height: 4 ) ,
pw . Text ( client ? . telephone ? ? ' Non spécifié ' , style: frameTextStyle ) ,
] ,
) ,
) ,
] ,
) ,
] ,
) ,
pw . SizedBox ( height: 15 ) ,
// Tableau des produits avec cadeaux - optimisé pour portrait
pw . Table (
border: pw . TableBorder . all ( width: 0.5 ) ,
columnWidths: {
0 : const pw . FlexColumnWidth ( 3 ) , // Désignations
1 : const pw . FlexColumnWidth ( 0.8 ) , // Quantité
2 : const pw . FlexColumnWidth ( 1.2 ) , // Prix unitaire
3 : const pw . FlexColumnWidth ( 1.5 ) , // Remise/cadeau
4 : const pw . FlexColumnWidth ( 1.2 ) , // Montant
} ,
children: [
pw . TableRow (
decoration: const pw . BoxDecoration ( color: PdfColors . grey200 ) ,
children: [
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' Désignations ' , style: boldTextStyle )
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' Qté ' , style: boldTextStyle , textAlign: pw . TextAlign . center )
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' Prix unitaire ' , style: boldTextStyle , textAlign: pw . TextAlign . right )
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' Remise/Cadeau ' , style: boldTextStyle , textAlign: pw . TextAlign . center )
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' Montant ' , style: boldTextStyle , textAlign: pw . TextAlign . right )
) ,
] ,
) ,
. . . detailsAvecProduits . map ( ( item ) {
final detail = item [ ' detail ' ] as DetailCommande ;
final produit = item [ ' produit ' ] ;
return pw . TableRow (
decoration: detail . estCadeau
? const pw . BoxDecoration (
color: PdfColors . green50 ,
border: pw . Border (
left: pw . BorderSide (
color: PdfColors . green300 ,
width: 3 ,
) ,
) ,
)
: detail . aRemise
? const pw . BoxDecoration (
color: PdfColors . orange50 ,
border: pw . Border (
left: pw . BorderSide (
color: PdfColors . orange300 ,
width: 3 ,
) ,
) ,
)
: null ,
children: [
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Row (
children: [
pw . Expanded (
child: pw . Text ( detail . produitNom ? ? ' Produit inconnu ' ,
style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ) ,
) ,
if ( detail . estCadeau )
pw . Container (
padding: const pw . EdgeInsets . symmetric ( horizontal: 4 , vertical: 2 ) ,
decoration: pw . BoxDecoration (
color: PdfColors . green100 ,
borderRadius: pw . BorderRadius . circular ( 4 ) ,
) ,
child: pw . Text (
' CADEAU ' ,
style: pw . TextStyle (
fontSize: 7 ,
fontWeight: pw . FontWeight . bold ,
color: PdfColors . green700 ,
) ,
) ,
) ,
] ,
) ,
pw . SizedBox ( height: 2 ) ,
if ( produit ? . category ! = null & & produit ! . category . isNotEmpty & & produit ? . marque ! = null & & produit ! . marque . isNotEmpty )
pw . Text ( ' ${ produit . category } - ${ produit . marque } ' , style: smallTextStyle ) ,
if ( produit ? . imei ! = null & & produit ! . imei ! . isNotEmpty )
pw . Text ( ' IMEI: ${ produit . imei } ' , style: smallTextStyle ) ,
if ( produit ? . reference ! = null & & produit ! . reference ! . isNotEmpty )
pw . Row (
children: [
if ( produit ? . ram ! = null & & produit ! . ram ! . isNotEmpty )
pw . Text ( ' ${ produit . ram } ' , style: smallTextStyle ) ,
if ( produit ? . memoireInterne ! = null & & produit ! . memoireInterne ! . isNotEmpty )
pw . Text ( ' | ${ produit . memoireInterne } ' , style: smallTextStyle ) ,
pw . Text ( ' | ${ produit . reference } ' , style: smallTextStyle ) ,
] ,
) ,
] ,
) ,
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' ${ detail . quantite } ' , style: normalTextStyle , textAlign: pw . TextAlign . center ) ,
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
children: [
if ( detail . estCadeau ) . . . [
pw . Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle (
fontSize: 7 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
) ) ,
pw . Text ( ' GRATUIT ' ,
style: pw . TextStyle (
fontSize: 8 ,
color: PdfColors . green700 ,
fontWeight: pw . FontWeight . bold ,
) ) ,
] else if ( detail . aRemise & & detail . prixUnitaire ! = detail . sousTotal / detail . quantite ) . . . [
pw . Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle (
fontSize: 7 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
) ) ,
pw . Text ( ' ${ ( detail . prixFinal / detail . quantite ) . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle ( fontSize: 9 , color: PdfColors . orange ) ) ,
] else
pw . Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 0 ) } ' ,
style: normalTextStyle ) ,
] ,
) ,
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text (
detail . estCadeau
? ' CADEAU \n OFFERT '
: detail . aRemise
? detail . remiseDescription
: ' - ' ,
style: pw . TextStyle (
fontSize: 7 ,
color: detail . estCadeau
? PdfColors . green700
: detail . aRemise
? PdfColors . orange
: PdfColors . grey600 ,
fontWeight: detail . estCadeau ? pw . FontWeight . bold : pw . FontWeight . normal ,
) ,
textAlign: pw . TextAlign . center ,
) ,
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
children: [
if ( detail . estCadeau ) . . . [
pw . Text ( ' ${ detail . sousTotal . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle (
fontSize: 7 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
) ) ,
pw . Text ( ' GRATUIT ' ,
style: pw . TextStyle (
fontSize: 8 ,
fontWeight: pw . FontWeight . bold ,
color: PdfColors . green700 ,
) ) ,
] else if ( detail . aRemise & & detail . sousTotal ! = detail . prixFinal ) . . . [
pw . Text ( ' ${ detail . sousTotal . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle (
fontSize: 7 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
) ) ,
pw . Text ( ' ${ detail . prixFinal . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ) ,
] else
pw . Text ( ' ${ detail . prixFinal . toStringAsFixed ( 0 ) } ' ,
style: normalTextStyle ) ,
] ,
) ,
) ,
] ,
) ;
} ) . toList ( ) ,
] ,
) ,
pw . SizedBox ( height: 12 ) ,
// Section totaux - alignée à droite
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
children: [
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Text ( ' SOUS-TOTAL ' , style: normalTextStyle ) ,
pw . SizedBox ( width: 20 ) ,
pw . Container (
width: 80 ,
child: pw . Text ( ' ${ sousTotal . toStringAsFixed ( 0 ) } ' ,
style: normalTextStyle , textAlign: pw . TextAlign . right ) ,
) ,
] ,
) ,
pw . SizedBox ( height: 4 ) ,
] ,
if ( totalRemises > 0 ) . . . [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Text ( ' REMISES TOTALES ' , style: pw . TextStyle ( color: PdfColors . orange , fontSize: 9 ) ) ,
pw . SizedBox ( width: 20 ) ,
pw . Container (
width: 80 ,
child: pw . Text ( ' - ${ totalRemises . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle ( color: PdfColors . orange , fontWeight: pw . FontWeight . bold , fontSize: 9 ) ,
textAlign: pw . TextAlign . right ) ,
) ,
] ,
) ,
pw . SizedBox ( height: 4 ) ,
] ,
if ( totalCadeaux > 0 ) . . . [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Text ( ' CADEAUX OFFERTS ( $ nombreCadeaux ) ' ,
style: pw . TextStyle ( color: PdfColors . green700 , fontSize: 9 ) ) ,
pw . SizedBox ( width: 20 ) ,
pw . Container (
width: 80 ,
child: pw . Text ( ' - ${ totalCadeaux . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle ( color: PdfColors . green700 , fontWeight: pw . FontWeight . bold , fontSize: 9 ) ,
textAlign: pw . TextAlign . right ) ,
) ,
] ,
) ,
pw . SizedBox ( height: 4 ) ,
] ,
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
pw . Container (
width: 200 ,
height: 1 ,
color: PdfColors . black ,
margin: const pw . EdgeInsets . symmetric ( vertical: 4 ) ,
) ,
] ,
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . end ,
children: [
pw . Text ( ' TOTAL ' , style: boldTextStyle ) ,
pw . SizedBox ( width: 20 ) ,
pw . Container (
width: 80 ,
child: pw . Text ( ' ${ commande . montantTotal . toStringAsFixed ( 0 ) } ' ,
style: boldTextStyle , textAlign: pw . TextAlign . right ) ,
) ,
] ,
) ,
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
pw . SizedBox ( height: 4 ) ,
pw . Text (
' Économies réalisées: ${ ( totalRemises + totalCadeaux ) . toStringAsFixed ( 0 ) } MGA ' ,
style: pw . TextStyle (
fontSize: 8 ,
color: PdfColors . green ,
fontStyle: pw . FontStyle . italic ,
) ,
) ,
] ,
] ,
) ,
] ,
) ,
pw . SizedBox ( height: 15 ) ,
// Montant en lettres
pw . Text ( ' Arrêté à la somme de: ${ _numberToWords ( commande . montantTotal . toInt ( ) ) } Ariary ' ,
style: italicTextStyle ) ,
pw . SizedBox ( height: 15 ) ,
// Informations vendeurs - Section dédiée
pw . Container (
width: double . infinity ,
padding: const pw . EdgeInsets . all ( 12 ) ,
decoration: pw . BoxDecoration (
color: PdfColors . grey100 ,
borderRadius: pw . BorderRadius . circular ( 8 ) ,
border: pw . Border . all ( color: PdfColors . grey300 ) ,
) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Text (
' INFORMATIONS VENDEURS ' ,
style: pw . TextStyle (
fontSize: 11 ,
fontWeight: pw . FontWeight . bold ,
color: PdfColors . blue700 ,
) ,
) ,
pw . SizedBox ( height: 8 ) ,
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
pw . Expanded (
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Text (
' Vendeur initiateur: ' ,
style: pw . TextStyle (
fontSize: 9 ,
fontWeight: pw . FontWeight . bold ,
color: PdfColors . grey700 ,
) ,
) ,
pw . SizedBox ( height: 3 ) ,
pw . Text (
commandeur ! = null
? ' ${ commandeur . name } ${ commandeur . lastName ? ? ' ' } ' . trim ( )
: ' Non spécifié ' ,
style: pw . TextStyle (
fontSize: 10 ,
color: PdfColors . black ,
) ,
) ,
pw . SizedBox ( height: 3 ) ,
pw . Text (
' Date: ${ DateFormat ( ' dd/MM/yyyy HH:mm ' ) . format ( commande . dateCommande ) } ' ,
style: pw . TextStyle (
fontSize: 8 ,
color: PdfColors . grey600 ,
) ,
) ,
] ,
) ,
) ,
pw . Container (
width: 1 ,
height: 40 ,
color: PdfColors . grey400 ,
) ,
pw . SizedBox ( width: 20 ) ,
pw . Expanded (
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Text (
' Vendeur validateur: ' ,
style: pw . TextStyle (
fontSize: 9 ,
fontWeight: pw . FontWeight . bold ,
color: PdfColors . grey700 ,
) ,
) ,
pw . SizedBox ( height: 3 ) ,
pw . Text (
validateur ! = null
? ' ${ validateur . name } ${ validateur . lastName ? ? ' ' } ' . trim ( )
: ' Non spécifié ' ,
style: pw . TextStyle (
fontSize: 10 ,
color: PdfColors . black ,
) ,
) ,
pw . SizedBox ( height: 3 ) ,
pw . Text (
' Date: ${ DateFormat ( ' dd/MM/yyyy HH:mm ' ) . format ( DateTime . now ( ) ) } ' ,
style: pw . TextStyle (
fontSize: 8 ,
color: PdfColors . grey600 ,
) ,
) ,
] ,
) ,
) ,
] ,
) ,
] ,
) ,
) ,
pw . SizedBox ( height: 12 ) ,
// Note de remerciement pour les cadeaux
if ( totalCadeaux > 0 ) . . . [
pw . Container (
width: double . infinity ,
padding: const pw . EdgeInsets . all ( 10 ) ,
decoration: pw . BoxDecoration (
color: PdfColors . blue50 ,
borderRadius: pw . BorderRadius . circular ( 6 ) ,
border: pw . Border . all ( color: PdfColors . blue200 ) ,
) ,
child: pw . Row (
children: [
pw . Text ( ' 🎁 ' , style: emojifont ) ,
pw . Expanded (
child: pw . Text (
' Merci de votre confiance ! Nous espérons que nos cadeaux vous feront plaisir. ( $ nombreCadeaux article(s) offert(s) - Valeur: ${ totalCadeaux . toStringAsFixed ( 0 ) } MGA) ' ,
style: pw . TextStyle (
fontSize: 9 ,
fontStyle: pw . FontStyle . italic ,
color: PdfColors . blue700 ,
) ,
) ,
) ,
] ,
) ,
) ,
pw . SizedBox ( height: 12 ) ,
] ,
// Signatures - espacées sur toute la largeur
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . center ,
children: [
pw . Text (
' Signature vendeur initiateur ' ,
style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ,
) ,
pw . SizedBox ( height: 2 ) ,
pw . Text (
commandeur ! = null
? ' ${ commandeur . name } ${ commandeur . lastName ? ? ' ' } ' . trim ( )
: ' Non spécifié ' ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . grey600 ) ,
) ,
pw . SizedBox ( height: 20 ) ,
pw . Container ( width: 120 , height: 1 , color: PdfColors . black ) ,
] ,
) ,
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . center ,
children: [
pw . Text (
' Signature vendeur validateur ' ,
style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ,
) ,
pw . SizedBox ( height: 2 ) ,
pw . Text (
validateur ! = null
? ' ${ validateur . name } ${ validateur . lastName ? ? ' ' } ' . trim ( )
: ' Non spécifié ' ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . grey600 ) ,
) ,
pw . SizedBox ( height: 20 ) ,
pw . Container ( width: 120 , height: 1 , color: PdfColors . black ) ,
] ,
) ,
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . center ,
children: [
pw . Text (
' Signature du client ' ,
style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ,
) ,
pw . SizedBox ( height: 2 ) ,
pw . Text (
client ? . nomComplet ? ? ' Non spécifié ' ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . grey600 ) ,
) ,
pw . SizedBox ( height: 20 ) ,
pw . Container ( width: 120 , height: 1 , color: PdfColors . black ) ,
] ,
) ,
] ,
) ,
] ,
) ;
} ,
) ,
) ;
final output = await getTemporaryDirectory ( ) ;
final file = File ( ' ${ output . path } /facture_ ${ commande . id } .pdf ' ) ;
await file . writeAsBytes ( await pdf . save ( ) ) ;
await OpenFile . open ( file . path ) ;
}
String _numberToWords ( int number ) {
NumbersToLetters . toLetters ( ' fr ' , number ) ;
return NumbersToLetters . toLetters ( ' fr ' , number ) ;
}
Future < void > _generateInvoiceWithPasswordVerification ( Commande commande ) async {
await showDialog < void > (
context: context ,
barrierDismissible: false ,
builder: ( BuildContext context ) {
return PasswordVerificationDialog (
title: ' Génération de facture ' ,
message: ' Pour générer la facture de la commande # ${ commande . id } , veuillez confirmer votre identité en saisissant votre mot de passe. ' ,
onPasswordVerified: ( String password ) async {
// Afficher un indicateur de chargement
Get . dialog (
const Center (
child: CircularProgressIndicator ( ) ,
) ,
barrierDismissible: false ,
) ;
try {
await _generateInvoice ( commande ) ;
Get . back ( ) ; // Fermer l'indicateur de chargement
Get . snackbar (
' Succès ' ,
' Facture générée avec succès ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . green ,
colorText: Colors . white ,
duration: const Duration ( seconds: 2 ) ,
) ;
} catch ( e ) {
Get . back ( ) ; // Fermer l'indicateur de chargement
Get . snackbar (
' Erreur ' ,
' Erreur lors de la génération de la facture: $ e ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
duration: const Duration ( seconds: 3 ) ,
) ;
}
} ,
) ;
} ,
) ;
}
Future < void > _generateBon_lifraisonWithPasswordVerification ( Commande commande ) async {
await showDialog < void > (
context: context ,
barrierDismissible: false ,
builder: ( BuildContext context ) {
return PasswordVerificationDialog (
title: ' Génération de Bon de livraison ' ,
message: ' Pour générer de Bon de livraison de la commande # ${ commande . id } , veuillez confirmer votre identité en saisissant votre mot de passe. ' ,
onPasswordVerified: ( String password ) async {
// Afficher un indicateur de chargement
Get . dialog (
const Center (
child: CircularProgressIndicator ( ) ,
) ,
barrierDismissible: false ,
) ;
try {
await _generateBonLivraison ( commande ) ;
Get . back ( ) ; // Fermer l'indicateur de chargement
Get . snackbar (
' Succès ' ,
' Facture générée avec succès ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . green ,
colorText: Colors . white ,
duration: const Duration ( seconds: 2 ) ,
) ;
} catch ( e ) {
Get . back ( ) ; // Fermer l'indicateur de chargement
Get . snackbar (
' Erreur ' ,
' Erreur lors de la génération de la facture: $ e ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
duration: const Duration ( seconds: 3 ) ,
) ;
}
} ,
) ;
} ,
) ;
}
String _getPaymentMethodLabel ( PaymentMethod payment ) {
switch ( payment . type ) {
case PaymentType . cash:
return ' LIQUIDE ( ${ payment . amountGiven . toStringAsFixed ( 0 ) } MGA) ' ;
case PaymentType . card:
return ' CARTE BANCAIRE ' ;
case PaymentType . mvola:
return ' MVOLA ' ;
case PaymentType . orange:
return ' ORANGE MONEY ' ;
case PaymentType . airtel:
return ' AIRTEL MONEY ' ;
default :
return ' MÉTHODE INCONNUE ( ${ payment . type . toString ( ) } ) ' ; // Debug info
}
}
Future < 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 emojiSuportFont = pw . Font . ttf ( await rootBundle . load ( ' assets/NotoEmoji-Regular.ttf ' ) ) ;
final emojifont = pw . TextStyle ( fontSize: 7 , fontWeight: pw . FontWeight . bold , font: emojiSuportFont ) ;
final List < Map < String , dynamic > > detailsAvecProduits = [ ] ;
for ( final detail in details ) {
final produit = await _database . getProductById ( detail . produitId ) ;
detailsAvecProduits . add ( {
' detail ' : detail ,
' produit ' : produit ,
} ) ;
}
double sousTotal = 0 ;
double totalRemises = 0 ;
double totalCadeaux = 0 ;
int nombreCadeaux = 0 ;
for ( final detail in details ) {
sousTotal + = detail . sousTotal ;
if ( detail . estCadeau ) {
totalCadeaux + = detail . sousTotal ;
nombreCadeaux + = detail . quantite ;
} else {
totalRemises + = detail . montantRemise ;
}
}
final pdf = pw . Document ( ) ;
final imageBytes = await loadImage ( ) ;
final image = pw . MemoryImage ( imageBytes ) ;
// DEBUG: Affichage des informations de paiement
print ( ' === DEBUG PAYMENT METHOD === ' ) ;
print ( ' Payment type: ${ payment . type } ' ) ;
print ( ' Payment type toString: ${ payment . type . toString ( ) } ' ) ;
print ( ' Payment type runtimeType: ${ payment . type . runtimeType } ' ) ;
print ( ' Payment type index: ${ payment . type . index } ' ) ;
print ( ' Amount given: ${ payment . amountGiven } ' ) ;
print ( ' PaymentType.airtel: ${ PaymentType . airtel } ' ) ;
print ( ' payment.type == PaymentType.airtel: ${ payment . type = = PaymentType . airtel } ' ) ;
print ( ' === END DEBUG === ' ) ;
pdf . addPage (
pw . Page (
pageFormat: PdfPageFormat ( 70 * PdfPageFormat . mm , double . infinity ) ,
margin: const pw . EdgeInsets . all ( 4 ) ,
build: ( pw . Context context ) {
return pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . center ,
children: [
pw . Center (
child: pw . Container (
width: 40 ,
height: 40 ,
child: pw . Image ( image ) ,
) ,
) ,
pw . SizedBox ( height: 4 ) ,
pw . Text ( ' GUYCOM MADAGASCAR ' ,
style: pw . TextStyle (
fontSize: 10 ,
fontWeight: pw . FontWeight . bold ,
) ) ,
pw . Text ( ' Tél: 033 37 808 18 ' , style: const pw . TextStyle ( fontSize: 7 ) ) ,
pw . Text ( ' www.guycom.mg ' , style: const pw . TextStyle ( fontSize: 7 ) ) ,
pw . SizedBox ( height: 6 ) ,
pw . Text ( ' TICKET DE CAISSE ' ,
style: pw . TextStyle (
fontSize: 10 ,
fontWeight: pw . FontWeight . bold ,
decoration: pw . TextDecoration . underline ,
) ) ,
pw . Text ( ' N°: ${ pointDeVente ? [ ' abreviation ' ] ? ? ' PV ' } - ${ commande . id } ' ,
style: const pw . TextStyle ( fontSize: 8 ) ) ,
pw . Text ( ' Date: ${ DateFormat ( ' dd/MM/yyyy HH:mm ' ) . format ( commande . dateCommande ) } ' ,
style: const pw . TextStyle ( fontSize: 8 ) ) ,
if ( pointDeVente ! = null )
pw . Text ( ' Point de vente: ${ pointDeVente [ ' designation ' ] } ' ,
style: const pw . TextStyle ( fontSize: 8 ) ) ,
pw . Divider ( thickness: 0.5 ) ,
pw . Text ( ' CLIENT: ${ client ? . nomComplet ? ? ' Non spécifié ' } ' ,
style: pw . TextStyle ( fontSize: 8 , fontWeight: pw . FontWeight . bold ) ) ,
if ( client ? . telephone ! = null )
pw . Text ( ' Tél: ${ client ! . telephone } ' , style: const pw . TextStyle ( fontSize: 7 ) ) ,
if ( commandeur ! = null | | validateur ! = null )
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Divider ( thickness: 0.5 ) ,
if ( commandeur ! = null )
pw . Text ( ' Vendeur: ${ commandeur . name } ' , style: const pw . TextStyle ( fontSize: 7 ) ) ,
if ( validateur ! = null )
pw . Text ( ' Validateur: ${ validateur . name } ' , style: const pw . TextStyle ( fontSize: 7 ) ) ,
] ,
) ,
pw . Divider ( thickness: 0.5 ) ,
// Tableau des produits avec cadeaux
pw . Table (
columnWidths: {
0 : const pw . FlexColumnWidth ( 3.5 ) ,
1 : const pw . FlexColumnWidth ( 1 ) ,
2 : const pw . FlexColumnWidth ( 1.5 ) ,
} ,
children: [
pw . TableRow (
children: [
pw . Text ( ' Désignation ' , style: pw . TextStyle ( fontSize: 7 , fontWeight: pw . FontWeight . bold ) ) ,
pw . Text ( ' Qté ' , style: pw . TextStyle ( fontSize: 7 , fontWeight: pw . FontWeight . bold ) ) ,
pw . Text ( ' P.U ' , style: pw . TextStyle ( fontSize: 7 , fontWeight: pw . FontWeight . bold ) ) ,
] ,
decoration: const pw . BoxDecoration (
border: pw . Border ( bottom: pw . BorderSide ( width: 0.5 ) ) ,
) ,
) ,
. . . detailsAvecProduits . map ( ( item ) {
final detail = item [ ' detail ' ] as DetailCommande ;
final produit = item [ ' produit ' ] ;
return pw . TableRow (
decoration: const pw . BoxDecoration (
border: pw . Border ( bottom: pw . BorderSide ( width: 0.2 ) ) ) ,
children: [
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
pw . Row (
children: [
pw . Expanded (
child: pw . Text ( detail . produitNom ? ? ' Produit ' ,
style: const pw . TextStyle ( fontSize: 7 ) ) ,
) ,
if ( detail . estCadeau )
pw . Text ( ' 🎁 ' , style: emojifont ) ,
] ,
) ,
if ( produit ? . reference ! = null )
pw . Text ( ' Ref: ${ produit ! . reference } ' ,
style: const pw . TextStyle ( fontSize: 6 ) ) ,
if ( produit ? . imei ! = null )
pw . Text ( ' IMEI: ${ produit ! . imei } ' ,
style: const pw . TextStyle ( fontSize: 6 ) ) ,
if ( detail . estCadeau )
pw . Text ( ' CADEAU OFFERT ' ,
style: pw . TextStyle (
fontSize: 6 ,
color: PdfColors . green700 ,
fontWeight: pw . FontWeight . bold ,
) ) ,
if ( detail . aRemise & & ! detail . estCadeau )
pw . Text ( ' Remise: ${ detail . remiseDescription } ' ,
style: pw . TextStyle ( fontSize: 6 , color: PdfColors . orange ) ) ,
] ,
) ,
pw . Text ( detail . quantite . toString ( ) ,
style: const pw . TextStyle ( fontSize: 7 ) ) ,
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
children: [
if ( detail . estCadeau ) . . . [
pw . Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle (
fontSize: 6 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
) ) ,
pw . Text ( ' GRATUIT ' ,
style: pw . TextStyle (
fontSize: 7 ,
color: PdfColors . green700 ,
fontWeight: pw . FontWeight . bold ,
) ) ,
] else if ( detail . aRemise & & detail . prixUnitaire ! = detail . prixFinal / detail . quantite ) . . . [
pw . Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 0 ) } ' ,
style: pw . TextStyle (
fontSize: 6 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
) ) ,
pw . Text ( ' ${ ( detail . prixFinal / detail . quantite ) . toStringAsFixed ( 0 ) } ' ,
style: const pw . TextStyle ( fontSize: 7 ) ) ,
] else
pw . Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 0 ) } ' ,
style: const pw . TextStyle ( fontSize: 7 ) ) ,
] ,
) ,
] ,
) ;
} ) ,
] ,
) ,
pw . Divider ( thickness: 0.5 ) ,
// Totaux avec remises et cadeaux pour le ticket
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
pw . Text ( ' SOUS-TOTAL: ' ,
style: const pw . TextStyle ( fontSize: 8 ) ) ,
pw . Text ( ' ${ sousTotal . toStringAsFixed ( 0 ) } MGA ' ,
style: const pw . TextStyle ( fontSize: 8 ) ) ,
] ,
) ,
if ( totalRemises > 0 ) . . . [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
pw . Text ( ' REMISES: ' ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . orange ) ) ,
pw . Text ( ' - ${ totalRemises . toStringAsFixed ( 0 ) } MGA ' ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . orange ) ) ,
] ,
) ,
] ,
if ( totalCadeaux > 0 ) . . . [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
pw . Text ( ' CADEAUX ( $ nombreCadeaux ): ' ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . green700 ) ) ,
pw . Text ( ' - ${ totalCadeaux . toStringAsFixed ( 0 ) } MGA ' ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . green700 ) ) ,
] ,
) ,
] ,
pw . Divider ( thickness: 0.3 ) ,
] ,
// Total final
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
pw . Text ( ' TOTAL: ' ,
style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ) ,
pw . Text ( ' ${ commande . montantTotal . toStringAsFixed ( 0 ) } MGA ' ,
style: pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold ) ) ,
] ,
) ,
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
pw . SizedBox ( height: 4 ) ,
pw . Text ( ' Économies: ${ ( totalRemises + totalCadeaux ) . toStringAsFixed ( 0 ) } MGA ! ' ,
style: pw . TextStyle (
fontSize: 7 ,
color: PdfColors . green ,
fontStyle: pw . FontStyle . italic ,
) ,
textAlign: pw . TextAlign . center ,
) ,
] ,
pw . Divider ( thickness: 0.5 ) ,
// Détails du paiement
pw . Text ( ' MODE DE PAIEMENT: ' ,
style: const pw . TextStyle ( fontSize: 8 ) ) ,
pw . Text (
_getPaymentMethodLabel ( payment ) ,
style: pw . TextStyle ( fontSize: 8 , fontWeight: pw . FontWeight . bold ) ,
) ,
if ( payment . type = = PaymentType . cash & & payment . amountGiven > commande . montantTotal )
pw . Text ( ' Monnaie rendue: ${ ( payment . amountGiven - commande . montantTotal ) . toStringAsFixed ( 0 ) } MGA ' ,
style: const pw . TextStyle ( fontSize: 8 ) ) ,
pw . SizedBox ( height: 8 ) ,
// Messages de fin avec cadeaux
if ( totalCadeaux > 0 ) . . . [
pw . Container (
padding: const pw . EdgeInsets . all ( 4 ) ,
decoration: pw . BoxDecoration (
color: PdfColors . green50 ,
borderRadius: pw . BorderRadius . circular ( 4 ) ,
) ,
child: pw . Column (
children: [
pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . center ,
children: [
pw . Text ( ' 🎁 ' ,
style: emojifont ,
textAlign: pw . TextAlign . center ,
) ,
pw . Text ( ' Profitez de vos cadeaux ! ' ,
style: pw . TextStyle (
fontSize: 7 ,
fontWeight: pw . FontWeight . bold ,
color: PdfColors . green700 ,
) ,
textAlign: pw . TextAlign . center ,
) ,
pw . Text ( ' 🎁 ' ,
style: emojifont ,
textAlign: pw . TextAlign . center ,
) ,
]
) ,
pw . Text ( ' $ nombreCadeaux article(s) offert(s) ' ,
style: pw . TextStyle (
fontSize: 6 ,
color: PdfColors . green600 ,
) ,
textAlign: pw . TextAlign . center ,
) ,
] ,
) ,
) ,
pw . SizedBox ( height: 6 ) ,
] ,
pw . Text ( ' Article non échangeable - Garantie selon conditions ' ,
style: const pw . TextStyle ( fontSize: 6 ) ) ,
pw . Text ( ' Ticket à conserver comme justificatif ' ,
style: const pw . TextStyle ( fontSize: 6 ) ) ,
pw . SizedBox ( height: 8 ) ,
pw . Text ( ' Merci pour votre confiance ! ' ,
style: pw . TextStyle ( fontSize: 8 , fontStyle: pw . FontStyle . italic ) ) ,
] ,
) ;
} ,
) ,
) ;
final output = await getTemporaryDirectory ( ) ;
final file = File ( ' ${ output . path } /ticket_ ${ commande . id } .pdf ' ) ;
await file . writeAsBytes ( await pdf . save ( ) ) ;
await OpenFile . open ( file . path ) ;
}
Color _getStatutColor ( StatutCommande statut ) {
switch ( statut ) {
case StatutCommande . enAttente:
return Colors . orange . shade100 ;
case StatutCommande . confirmee:
return Colors . blue . shade100 ;
case StatutCommande . annulee:
return Colors . red . shade100 ;
}
}
IconData _getStatutIcon ( StatutCommande statut ) {
switch ( statut ) {
case StatutCommande . enAttente:
return Icons . schedule ;
case StatutCommande . confirmee:
return Icons . check_circle_outline ;
case StatutCommande . annulee:
return Icons . cancel ;
}
}
String statutLibelle ( StatutCommande statut ) {
switch ( statut ) {
case StatutCommande . enAttente:
return ' En attente ' ;
case StatutCommande . confirmee:
return ' Confirmée ' ;
case StatutCommande . annulee:
return ' Annulée ' ;
}
}
@ override
void dispose ( ) {
_searchController . dispose ( ) ;
super . dispose ( ) ;
}
@ override
Widget build ( BuildContext context ) {
return Scaffold (
appBar: CustomAppBar ( title: ' Gestion des Commandes ' ) ,
drawer: CustomDrawer ( ) ,
body: Column (
children: [
// Header avec logo et statistiques
Container (
padding: const EdgeInsets . all ( 16.0 ) ,
decoration: BoxDecoration (
gradient: LinearGradient (
colors: [ Colors . blue . shade50 , Colors . white ] ,
begin: Alignment . topCenter ,
end: Alignment . bottomCenter ,
) ,
) ,
child: Column (
children: [
// Logo et titre
Row (
children: [
Container (
width: 50 ,
height: 50 ,
decoration: BoxDecoration (
borderRadius: BorderRadius . circular ( 8 ) ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.1 ) ,
blurRadius: 4 ,
offset: const Offset ( 0 , 2 ) ,
)
] ,
) ,
child: ClipRRect (
borderRadius: BorderRadius . circular ( 8 ) ,
child: Image . asset (
' assets/logo.png ' ,
fit: BoxFit . cover ,
errorBuilder: ( context , error , stackTrace ) {
return Container (
decoration: BoxDecoration (
color: Colors . blue . shade900 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: const Icon (
Icons . business ,
color: Colors . white ,
size: 30 ,
) ,
) ;
} ,
) ,
) ,
) ,
const SizedBox ( width: 12 ) ,
Expanded (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
const Text (
' Gestion des Commandes ' ,
style: TextStyle (
fontSize: 20 ,
fontWeight: FontWeight . bold ,
color: Colors . black87 ,
) ,
) ,
Text (
' ${ _filteredCommandes . length } commande(s) affichée(s) ' ,
style: TextStyle (
fontSize: 14 ,
color: Colors . grey . shade600 ,
) ,
) ,
] ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 16 ) ,
// Barre de recherche améliorée
Container (
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 12 ) ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.1 ) ,
blurRadius: 4 ,
offset: const Offset ( 0 , 2 ) ,
)
] ,
) ,
child: TextField (
controller: _searchController ,
decoration: InputDecoration (
labelText: ' Rechercher par client ou numéro de commande ' ,
prefixIcon:
Icon ( Icons . search , color: Colors . blue . shade800 ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
borderSide: BorderSide . none ,
) ,
filled: true ,
fillColor: Colors . white ,
contentPadding: const EdgeInsets . symmetric (
horizontal: 16 ,
vertical: 12 ,
) ,
) ,
) ,
) ,
const SizedBox ( height: 16 ) ,
// Filtres améliorés
Row (
children: [
Expanded (
child: Container (
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 12 ) ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.1 ) ,
blurRadius: 4 ,
offset: const Offset ( 0 , 2 ) ,
) ,
] ,
) ,
child: DropdownButtonFormField < 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 FutureBuilder < List < DetailCommande > > (
future: _database . getDetailsCommande ( commande . id ! ) ,
builder: ( context , snapshot ) {
double totalRemises = 0 ;
bool aDesRemises = false ;
if ( snapshot . hasData ) {
for ( final detail in snapshot . data ! ) {
totalRemises + = detail . montantRemise ;
if ( detail . aRemise ) aDesRemises = true ;
}
}
return Container (
margin: const EdgeInsets . only ( bottom: 12 ) ,
decoration: BoxDecoration (
color: _getStatutColor ( commande . statut ) ,
borderRadius: BorderRadius . circular ( 12 ) ,
border: aDesRemises
? Border . all ( color: Colors . orange . shade300 , width: 2 )
: null ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.1 ) ,
blurRadius: 4 ,
offset: const Offset ( 0 , 2 ) ,
) ,
] ,
) ,
child: ExpansionTile (
tilePadding: const EdgeInsets . symmetric (
horizontal: 16 ,
vertical: 8 ,
) ,
leading: Container (
width: 50 ,
height: 50 ,
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 25 ) ,
border: aDesRemises
? Border . all ( color: Colors . orange . shade300 , width: 2 )
: null ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.1 ) ,
blurRadius: 2 ,
offset: const Offset ( 0 , 1 ) ,
) ,
] ,
) ,
child: Column (
mainAxisAlignment: MainAxisAlignment . center ,
children: [
Icon (
aDesRemises ? Icons . discount : _getStatutIcon ( commande . statut ) ,
size: 20 ,
color: aDesRemises
? Colors . teal . shade700
: commande . statut = = StatutCommande . annulee
? Colors . red
: Colors . blue . shade600 ,
) ,
Text (
' # ${ commande . id } ' ,
style: const TextStyle (
fontSize: 10 ,
fontWeight: FontWeight . bold ,
) ,
) ,
] ,
) ,
) ,
title: Text (
commande . clientNomComplet ,
style: const TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
) ,
) ,
subtitle: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
const SizedBox ( height: 4 ) ,
Row (
children: [
Icon (
Icons . calendar_today ,
size: 14 ,
color: Colors . grey . shade600 ,
) ,
const SizedBox ( width: 4 ) ,
Text (
DateFormat ( ' dd/MM/yyyy ' ) . format ( commande . dateCommande ) ,
style: TextStyle (
fontSize: 12 ,
color: Colors . grey . shade600 ,
) ,
) ,
const SizedBox ( width: 16 ) ,
Container (
padding: const EdgeInsets . symmetric (
horizontal: 8 ,
vertical: 2 ,
) ,
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Text (
commande . statutLibelle ,
style: TextStyle (
fontSize: 11 ,
fontWeight: FontWeight . w600 ,
color: commande . statut = = StatutCommande . annulee
? Colors . red
: Colors . blue . shade700 ,
) ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 4 ) ,
Row (
children: [
Icon (
Icons . attach_money ,
size: 14 ,
color: Colors . green . shade600 ,
) ,
const SizedBox ( width: 4 ) ,
Text (
' ${ commande . montantTotal . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . bold ,
color: Colors . green . shade700 ,
) ,
) ,
// Affichage des remises si elles existent
if ( totalRemises > 0 ) . . . [
const SizedBox ( width: 12 ) ,
Container (
padding: const EdgeInsets . symmetric (
horizontal: 6 ,
vertical: 2 ,
) ,
decoration: BoxDecoration (
color: Colors . orange . shade100 ,
borderRadius: BorderRadius . circular ( 10 ) ,
) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon (
Icons . discount ,
size: 12 ,
color: Colors . teal . shade700 ,
) ,
const SizedBox ( width: 2 ) ,
Text (
' - ${ totalRemises . toStringAsFixed ( 0 ) } ' ,
style: TextStyle (
fontSize: 10 ,
fontWeight: FontWeight . bold ,
color: Colors . teal . shade700 ,
) ,
) ,
] ,
) ,
) ,
] ,
] ,
) ,
] ,
) ,
trailing: Row (
mainAxisSize: MainAxisSize . min ,
children: [
Container (
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 8 ) ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.1 ) ,
blurRadius: 2 ,
offset: const Offset ( 0 , 1 ) ,
) ,
] ,
) ,
child: IconButton (
icon: Icon (
Icons . receipt_outlined ,
color: Colors . blue . shade600 ,
) ,
onPressed: ( ) = > _generateBon_lifraisonWithPasswordVerification ( commande ) ,
tooltip: ' Générer le Bon de livraison ' ,
) ,
) ,
const SizedBox ( width: 10 , ) ,
Container (
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 8 ) ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.1 ) ,
blurRadius: 2 ,
offset: const Offset ( 0 , 1 ) ,
) ,
] ,
) ,
child: IconButton (
icon: Icon (
Icons . receipt_long ,
color: Colors . blue . shade600 ,
) ,
onPressed: ( ) = > _generateInvoiceWithPasswordVerification ( commande ) ,
tooltip: ' Générer la facture ' ,
) ,
) ,
] ,
) ,
children: [
Container (
padding: const EdgeInsets . all ( 16.0 ) ,
decoration: const BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . only (
bottomLeft: Radius . circular ( 12 ) ,
bottomRight: Radius . circular ( 12 ) ,
) ,
) ,
child: Column (
children: [
CommandeDetails ( commande: commande ) ,
const SizedBox ( height: 16 ) ,
if ( commande . statut ! = StatutCommande . annulee )
CommandeActions (
commande: commande ,
onStatutChanged: _updateStatut ,
onPaymentSelected: _showPaymentOptions ,
) ,
] ,
) ,
) ,
] ,
) ,
) ;
} ,
) ;
} ,
)
) ,
] ,
) ,
) ;
}
}