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:mysql1/mysql1.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 > ( ) ;
bool verifAdmin ( ) {
return userController . role = = ' Super Admin ' ;
}
@ 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: ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( montantFinal ) } 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: ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( change ) } 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==============================================
/// Génère un PDF Bon de livraison en mode paysage, contenant deux exemplaires.
/// Un exemplaire est destiné au client, l'autre au magasin.
///
/// Les deux exemplaires sont placés côte à côte sur une seule page,
/// avec un trait de séparation vertical en leur centre.
/// Le PDF est sauvegardé dans un fichier temporaire, qui est partagé
/// via le mécanisme de partage de fichiers du système.
///
// Dans GestionCommandesPage - Remplacez la méthode _generateBonLivraison complète
Future < void > _generateBonLivraison ( Commande commande ) async {
final details = await _database . getDetailsCommande ( commande . id ! ) ;
final client = await _database . getClientById ( commande . clientId ) ;
final pointDeVenteId = commande . pointDeVenteId ;
ResultRow ? pointDeVenteComplet ;
// ✅ MODIFICATION: Récupération complète des données du point de vente
if ( pointDeVenteId ! = null ) {
pointDeVenteComplet = await _database . getPointDeVenteById ( pointDeVenteId ) ;
} else {
print ( " ce point de vente n'existe pas " ) ;
}
final pointDeVente = pointDeVenteComplet ;
// 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 ;
// ✅ NOUVELLE FONCTIONNALITÉ: Parser les informations d'en-tête pour livraison
List < String > infosLivraison = [ ] ;
final livraisonBrute = pointDeVenteComplet ? [ ' livraison ' ] ;
print ( ' === LIVRAISON BRUTE === ' ) ;
print ( livraisonBrute ) ;
print ( ' === FIN === ' ) ;
if ( livraisonBrute ! = null ) {
infosLivraison = _database . parseHeaderInfo ( livraisonBrute ) ;
print ( ' === INFOS LIVRAISON PARSÉES === ' ) ;
for ( int i = 0 ; i < infosLivraison . length ; i + + ) {
print ( ' Ligne $ i : ${ infosLivraison [ i ] } ' ) ;
}
print ( ' =============================== ' ) ;
}
// Infos par défaut si aucune info personnalisée
final infosLivraisonDefaut = [
' REMAX Andravoangy ' ,
' SUPREME CENTER Behoririka \n BOX 405 | 416 | 119 ' ,
' Tripolisa analankely BOX 7 ' ,
' 033 37 808 18 ' ,
' www.guycom.mg ' ,
] ;
// ✅ DEBUG: Vérifiez combien de détails vous avez
print ( ' === DEBUG BON DE LIVRAISON === ' ) ;
print ( ' Nombre de détails récupérés: ${ details . length } ' ) ;
for ( int i = 0 ; i < details . length ; i + + ) {
print ( ' Détail $ i : ${ details [ i ] . produitNom } x ${ details [ i ] . quantite } ' ) ;
}
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 ;
}
}
// ✅ CORRECTION PRINCIPALE: Améliorer la récupération des produits
final List < Map < String , dynamic > > detailsAvecProduits = [ ] ;
for ( int i = 0 ; i < details . length ; i + + ) {
final detail = details [ i ] ;
print ( ' Traitement détail $ i : ${ detail . produitNom } ' ) ;
try {
final produit = await _database . getProductById ( detail . produitId ) ;
if ( produit ! = null ) {
detailsAvecProduits . add ( {
' detail ' : detail ,
' produit ' : produit ,
} ) ;
print ( ' ✅ Produit trouvé: ${ produit . name } ' ) ;
} else {
detailsAvecProduits . add ( {
' detail ' : detail ,
' produit ' : null ,
} ) ;
print ( ' ⚠️ Produit non trouvé, utilisation des données du détail ' ) ;
}
} catch ( e ) {
print ( ' ❌ Erreur lors de la récupération du produit: $ e ' ) ;
detailsAvecProduits . add ( {
' detail ' : detail ,
' produit ' : null ,
} ) ;
}
}
print ( ' Total detailsAvecProduits: ${ detailsAvecProduits . length } ' ) ;
final pdf = pw . Document ( ) ;
final imageBytes = await loadImage ( ) ;
final image = pw . MemoryImage ( imageBytes ) ;
// ✅ AMÉLIORATION: Gestion des polices avec fallback
pw . Font ? italicFont ;
pw . Font ? regularFont ;
try {
italicFont = pw . Font . ttf ( await rootBundle . load ( ' assets/fonts/Roboto-Italic.ttf ' ) ) ;
regularFont = pw . Font . ttf ( await rootBundle . load ( ' assets/fonts/Roboto-Regular.ttf ' ) ) ;
} catch ( e ) {
print ( ' ⚠️ Impossible de charger les polices personnalisées: $ e ' ) ;
}
// ✅ DÉFINITION DES STYLES DE TEXTE
final tinyTextStyle = pw . TextStyle ( fontSize: 9 , font: regularFont ) ;
final smallTextStyle = pw . TextStyle ( fontSize: 10 , font: regularFont ) ;
final normalTextStyle = pw . TextStyle ( fontSize: 11 , font: regularFont ) ;
final boldTextStyle = pw . TextStyle ( fontSize: 11 , fontWeight: pw . FontWeight . bold , font: regularFont ) ;
final boldClientStyle = pw . TextStyle ( fontSize: 12 , fontWeight: pw . FontWeight . bold , font: regularFont ) ;
final frameTextStyle = pw . TextStyle ( fontSize: 10 , font: regularFont ) ;
final italicTextStyle = pw . TextStyle ( fontSize: 9 , fontWeight: pw . FontWeight . bold , font: italicFont ? ? regularFont ) ;
final italicLogoStyle = pw . TextStyle ( fontSize: 8 , fontWeight: pw . FontWeight . bold , font: italicFont ? ? regularFont ) ;
Future < pw . Widget > buildLogoWidget ( ) async {
final logoRaw = pointDeVenteComplet ? [ ' logo ' ] ;
if ( logoRaw ! = null ) {
try {
Uint8List bytes ;
if ( logoRaw is Uint8List ) {
bytes = logoRaw ;
} else if ( logoRaw is List < int > ) {
bytes = Uint8List . fromList ( logoRaw ) ;
} else if ( logoRaw . runtimeType . toString ( ) = = ' Blob ' ) {
// Cast dynamique pour appeler toBytes()
dynamic blobDynamic = logoRaw ;
bytes = blobDynamic . toBytes ( ) ;
} else {
throw Exception ( " Format de logo non supporté: ${ logoRaw . runtimeType } " ) ;
}
final imageLogo = pw . MemoryImage ( bytes ) ;
return pw . Image ( imageLogo , width: 100 , height: 100 ) ;
} catch ( e ) {
print ( ' Erreur chargement logo BDD: $ e ' ) ;
}
}
return pw . Image ( image , width: 100 , height: 100 ) ;
}
final logoWidget = await buildLogoWidget ( ) ;
// ✅ FONCTION POUR CONSTRUIRE L'EN-TÊTE DYNAMIQUE
pw . Widget buildEnteteInfos ( ) {
final infosAUtiliser = infosLivraison . isNotEmpty ? infosLivraison : infosLivraisonDefaut ;
return pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: infosAUtiliser . map ( ( info ) {
return pw . Padding (
padding: const pw . EdgeInsets . only ( bottom: 1 ) ,
child: pw . Text ( info , style: tinyTextStyle ) ,
) ;
} ) . toList ( ) ,
) ;
}
// ✅ Fonction pour créer un exemplaire - AVEC EN-TÊTE DYNAMIQUE
pw . Widget buildExemplaire ( String typeExemplaire ) {
return pw . Container (
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 ,
font: regularFont ,
) ,
) ,
) ,
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 8 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
// En-tête principal (logo, infos entreprise, client)
pw . Row (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
// Logo et infos entreprise - ✅ AVEC INFOS DYNAMIQUES
pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
logoWidget ,
pw . SizedBox ( height: 3 ) ,
pw . Text ( ' NOTRE COMPETENCE, A VOTRE SERVICE ' , style: italicLogoStyle ) ,
pw . SizedBox ( height: 4 ) ,
buildEnteteInfos ( ) , // ✅ EN-TÊTE DYNAMIQUE ICI
] ,
) ,
// 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 } ${ client ? . prenom } ' , style: boldTextStyle ) ,
pw . SizedBox ( height: 2 ) ,
pw . Text ( client ? . telephone ? ? ' Non spécifié ' , style: tinyTextStyle ) ,
] ,
) ,
) ,
] ,
) ,
pw . SizedBox ( height: 8 ) ,
// ✅ SOLUTION PRINCIPALE: Tableau avec hauteur dynamique
pw . Column (
children: [
// Debug: Afficher le nombre d'articles
pw . Text ( ' Articles trouvés: ${ detailsAvecProduits . length } ' ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . grey , font: regularFont ) ) ,
pw . SizedBox ( height: 5 ) ,
// ✅ TABLE SANS CONTRAINTE DE HAUTEUR - Elle s'adapte au contenu
pw . Table (
border: pw . TableBorder . all ( width: 1 ) ,
columnWidths: {
0 : const pw . FlexColumnWidth ( 5 ) , // Désignations
1 : const pw . FlexColumnWidth ( 1.2 ) , // Quantité
2 : const pw . FlexColumnWidth ( 1.5 ) , // Prix unitaire
3 : const pw . FlexColumnWidth ( 1.5 ) , // Montant
} ,
children: [
// En-tête du tableau
pw . TableRow (
decoration: const pw . BoxDecoration ( color: PdfColors . grey200 ) ,
children: [
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' Désignations ' , style: boldTextStyle )
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' Qté ' , style: boldTextStyle , textAlign: pw . TextAlign . center )
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' P.U. ' , style: boldTextStyle , textAlign: pw . TextAlign . right )
) ,
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text ( ' Montant ' , style: boldTextStyle , textAlign: pw . TextAlign . right )
) ,
] ,
) ,
// ✅ TOUTES LES LIGNES DE PRODUITS - SANS LIMITATION
. . . detailsAvecProduits . asMap ( ) . entries . map ( ( entry ) {
final index = entry . key ;
final item = entry . value ;
final detail = item [ ' detail ' ] as DetailCommande ;
final produit = item [ ' produit ' ] ;
// Debug pour chaque ligne
print ( ' 📋 Ligne PDF $ index : ${ detail . produitNom } (Quantité: ${ detail . quantite } ) ' ) ;
return pw . TableRow (
decoration: detail . estCadeau
? const pw . BoxDecoration ( color: PdfColors . green50 )
: detail . aRemise
? const pw . BoxDecoration ( color: PdfColors . orange50 )
: index % 2 = = 0
? const pw . BoxDecoration ( color: PdfColors . grey50 )
: null ,
children: [
// ✅ Colonne Désignations - Plus compacte
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
mainAxisSize: pw . MainAxisSize . min ,
children: [
// Nom du produit avec badge
pw . Row (
children: [
pw . Expanded (
child: pw . Text (
' ${ detail . produitNom ? ? ' Produit inconnu ' } ' ,
style: pw . TextStyle (
fontSize: 10 ,
fontWeight: pw . FontWeight . bold ,
font: regularFont
)
) ,
) ,
if ( detail . estCadeau )
pw . Container (
padding: const pw . EdgeInsets . symmetric ( horizontal: 3 , vertical: 1 ) ,
decoration: pw . BoxDecoration (
color: PdfColors . green600 ,
borderRadius: pw . BorderRadius . circular ( 3 ) ,
) ,
child: pw . Text (
' CADEAU ' ,
style: pw . TextStyle (
fontSize: 6 ,
color: PdfColors . white ,
font: regularFont ,
fontWeight: pw . FontWeight . bold
)
) ,
) ,
] ,
) ,
pw . SizedBox ( height: 2 ) ,
// Informations complémentaires sur une seule ligne
pw . Text (
[
if ( produit ? . category ? . isNotEmpty = = true ) produit ! . category ,
if ( produit ? . marque ? . isNotEmpty = = true ) produit ! . marque ,
if ( produit ? . imei ? . isNotEmpty = = true ) ' IMEI: ${ produit ! . imei } ' ,
] . where ( ( info ) = > info ! = null ) . join ( ' , ' ) ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . grey700 , font: regularFont ) ,
) ,
// Spécifications techniques
if ( produit ? . ram ? . isNotEmpty = = true | | produit ? . memoireInterne ? . isNotEmpty = = true | | produit ? . reference ? . isNotEmpty = = true )
pw . Text (
[
if ( produit ? . ram ? . isNotEmpty = = true ) ' RAM: ${ produit ! . ram } ' ,
if ( produit ? . memoireInterne ? . isNotEmpty = = true ) ' Stockage: ${ produit ! . memoireInterne } ' ,
if ( produit ? . reference ? . isNotEmpty = = true ) ' Ref: ${ produit ! . reference } ' ,
] . join ( ' , ' ) ,
style: pw . TextStyle ( fontSize: 8 , color: PdfColors . grey600 , font: regularFont ) ,
) ,
] ,
) ,
) ,
// Colonne Quantité
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Text (
' ${ detail . quantite } ' ,
style: normalTextStyle ,
textAlign: pw . TextAlign . center
) ,
) ,
// Colonne Prix Unitaire
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
mainAxisSize: pw . MainAxisSize . min ,
children: [
if ( detail . estCadeau ) . . . [
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixUnitaire ) } ' ,
style: pw . TextStyle (
fontSize: 8 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
font: regularFont
)
) ,
pw . Text (
' GRATUIT ' ,
style: pw . TextStyle (
fontSize: 9 ,
color: PdfColors . green700 ,
fontWeight: pw . FontWeight . bold ,
font: regularFont
)
) ,
] else if ( detail . aRemise ) . . . [
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixUnitaire ) } ' ,
style: pw . TextStyle (
fontSize: 8 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
font: regularFont
)
) ,
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixFinal / detail . quantite ) } ' ,
style: pw . TextStyle (
fontSize: 10 ,
color: PdfColors . orange700 ,
fontWeight: pw . FontWeight . bold ,
font: regularFont
)
) ,
] else
pw . Text (
NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixUnitaire ) ,
style: smallTextStyle
) ,
] ,
) ,
) ,
// Colonne Montant
pw . Padding (
padding: const pw . EdgeInsets . all ( 4 ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
mainAxisSize: pw . MainAxisSize . min ,
children: [
if ( detail . estCadeau ) . . . [
pw . Text (
NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . sousTotal ) ,
style: pw . TextStyle (
fontSize: 8 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
font: regularFont
)
) ,
pw . Text (
' GRATUIT ' ,
style: pw . TextStyle (
fontSize: 9 ,
fontWeight: pw . FontWeight . bold ,
color: PdfColors . green700 ,
font: regularFont
)
) ,
] else if ( detail . aRemise ) . . . [
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . sousTotal ) } ' ,
style: pw . TextStyle (
fontSize: 8 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
font: regularFont
)
) ,
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixFinal ) } ' ,
style: pw . TextStyle (
fontSize: 10 ,
fontWeight: pw . FontWeight . bold ,
font: regularFont
)
) ,
] else
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixFinal ) } ' ,
style: smallTextStyle
) ,
] ,
) ,
) ,
] ,
) ;
} ) . toList ( ) ,
] ,
) ,
] ,
) ,
pw . SizedBox ( height: 12 ) ,
// Section finale - Totaux et signatures
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 ( ' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( sousTotal ) } ' , 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 , font: regularFont ) ) ,
pw . SizedBox ( width: 10 ) ,
pw . Text ( ' - ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalRemises ) } ' , style: pw . TextStyle ( color: PdfColors . orange , fontSize: 10 , font: regularFont ) ) ,
] ,
) ,
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 , font: regularFont ) ) ,
pw . SizedBox ( width: 10 ) ,
pw . Text ( ' - ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalCadeaux ) } ' , style: pw . TextStyle ( color: PdfColors . green700 , fontSize: 10 , font: regularFont ) ) ,
] ,
) ,
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 ( ' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( commande . montantTotal ) } MGA ' , style: boldTextStyle ) ,
] ,
) ,
] ,
) ,
) ,
pw . SizedBox ( width: 15 ) ,
// Section 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 , font: regularFont ) ) ,
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 , font: regularFont ) ,
) ,
] ,
) ,
) ,
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 , font: regularFont ) ,
) ,
] ,
) ,
) ,
] ,
) ,
] ,
) ,
) ,
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 , font: regularFont ) ) ,
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 , font: regularFont ) ) ,
pw . SizedBox ( height: 15 ) ,
pw . Container ( width: 70 , height: 1 , color: PdfColors . black ) ,
] ,
) ,
] ,
) ,
] ,
) ,
) ,
] ,
) ,
pw . SizedBox ( height: 6 ) ,
// Note finale
pw . Text (
' Arrêté à la somme de: ${ _numberToWords ( commande . montantTotal . toInt ( ) ) } Ariary ' ,
style: italicTextStyle ,
) ,
] ,
) ,
) ,
] ,
) ,
) ;
}
// PAGE EN MODE PAYSAGE
pdf . addPage (
pw . Page (
pageFormat: PdfPageFormat . a4 . landscape ,
margin: const pw . EdgeInsets . all ( 12 ) ,
build: ( pw . Context context ) {
return pw . Row (
children: [
pw . Expanded ( child: buildExemplaire ( " CLIENT " ) ) ,
pw . SizedBox ( width: 15 ) ,
// ✅ AMÉLIORATION: Remplacer les caractères Unicode par du texte simple
pw . Container (
width: 2 ,
height: double . infinity ,
child: pw . Column (
mainAxisAlignment: pw . MainAxisAlignment . center ,
children: [
pw . Container (
width: 20 ,
height: 20 ,
decoration: pw . BoxDecoration (
shape: pw . BoxShape . circle ,
border: pw . Border . all ( color: PdfColors . black , width: 2 ) ,
) ,
child: pw . Center (
child: pw . Text ( ' X ' , style: pw . TextStyle ( fontSize: 12 , fontWeight: pw . FontWeight . bold , font: regularFont ) ) ,
) ,
) ,
pw . SizedBox ( height: 10 ) ,
pw . Transform . rotate (
angle: 1.5708 ,
child: pw . Text ( ' DÉCOUPER ICI ' , style: pw . TextStyle ( fontSize: 10 , fontWeight: pw . FontWeight . bold , font: regularFont ) ) ,
) ,
pw . SizedBox ( height: 10 ) ,
pw . Container (
width: 20 ,
height: 20 ,
decoration: pw . BoxDecoration (
shape: pw . BoxShape . circle ,
border: pw . Border . all ( color: PdfColors . black , width: 2 ) ,
) ,
child: pw . Center (
child: pw . Text ( ' X ' , style: pw . TextStyle ( fontSize: 12 , fontWeight: pw . FontWeight . bold , font: regularFont ) ) ,
) ,
) ,
] ,
) ,
) ,
pw . SizedBox ( width: 15 ) ,
pw . Expanded ( child: buildExemplaire ( " MAGASIN " ) ) ,
] ,
) ;
} ,
) ,
) ;
print ( ' === RÉSULTAT FINAL === ' ) ;
print ( ' PDF généré avec ${ detailsAvecProduits . length } produits ' ) ;
// 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 pointDeVenteId = commande . pointDeVenteId ;
ResultRow ? pointDeVenteComplet ;
if ( pointDeVenteId ! = null ) {
pointDeVenteComplet = await _database . getPointDeVenteById ( pointDeVenteId ) ;
} else {
print ( " ce point de vente n'existe pas " ) ;
}
final pointDeVente = pointDeVenteComplet ;
// 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 ;
List < String > infosFacture = [ ] ;
final factureBrute = pointDeVenteComplet ? [ ' facture ' ] ;
print ( ' === FACTURE BRUTE === ' ) ;
print ( factureBrute ) ;
print ( ' === FIN === ' ) ;
if ( factureBrute ! = null ) {
infosFacture = _database . parseHeaderInfo ( factureBrute ) ;
print ( ' === INFOS FACTURE PARSÉES === ' ) ;
for ( int i = 0 ; i < infosFacture . length ; i + + ) {
print ( ' Ligne $ i : ${ infosFacture [ i ] } ' ) ;
}
print ( ' =============================== ' ) ;
}
// Infos par défaut si aucune info personnalisée
final infosFactureDefaut = [
' REMAX by GUYCOM Andravoangy ' ,
' SUPREME CENTER Behoririka box 405 ' ,
' SUPREME CENTER Behoririka box 416 ' ,
' SUPREME CENTER Behoririka box 119 ' ,
' TRIPOLITSA Analakely BOX 7 ' ,
' 033 37 808 18 ' ,
' www.guycom.mg ' ,
' NIF: 4000106673 - STAT 95210 11 2017 1 003651 ' ,
' Facebook: GuyCom ' ,
] ;
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 ) ;
Future < pw . Widget > buildLogoWidget ( ) async {
final logoRaw = pointDeVenteComplet ? [ ' logo ' ] ;
if ( logoRaw ! = null ) {
try {
Uint8List bytes ;
if ( logoRaw is Uint8List ) {
bytes = logoRaw ;
} else if ( logoRaw is List < int > ) {
bytes = Uint8List . fromList ( logoRaw ) ;
} else if ( logoRaw . runtimeType . toString ( ) = = ' Blob ' ) {
// Cast dynamique pour appeler toBytes()
dynamic blobDynamic = logoRaw ;
bytes = blobDynamic . toBytes ( ) ;
} else {
throw Exception ( " Format de logo non supporté: ${ logoRaw . runtimeType } " ) ;
}
final imageLogo = pw . MemoryImage ( bytes ) ;
return pw . Container ( width: 200 , height: 120 , child: pw . Image ( imageLogo ) ) ;
} catch ( e ) {
print ( ' Erreur chargement logo BDD: $ e ' ) ;
}
}
return pw . Container ( width: 200 , height: 100 , child: pw . Image ( image ) ) ;
}
final logoWidget = await buildLogoWidget ( ) ;
pw . Widget buildEnteteFactureInfos ( ) {
final infosAUtiliser = infosFacture . isNotEmpty ? infosFacture : infosFactureDefaut ;
return pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . start ,
children: [
. . . infosAUtiliser . map ( ( info ) {
return pw . Row (
children: [
iconChecked ,
pw . SizedBox ( width: 4 ) ,
pw . Text ( info , style: smallTextStyle ) ,
] ,
) ;
} ) ,
pw . SizedBox ( height: 2 ) , // ajouté en fin de liste
] ,
) ;
}
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: [
logoWidget ,
pw . Text ( ' NOTRE COMPETENCE, A VOTRE SERVICE ' ,
style: italicTextStyleLogo ) ,
pw . SizedBox ( height: 10 ) ,
buildEnteteFactureInfos ( ) ,
] ,
) ,
// 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 } ${ 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 (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixUnitaire ) } ' ,
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 (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixUnitaire ) } ' ,
style: pw . TextStyle (
fontSize: 7 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
) ) ,
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixFinal / detail . quantite ) } ' ,
style: pw . TextStyle (
fontSize: 9 , color: PdfColors . orange ) ) ,
] else
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixUnitaire ) } ' ,
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 (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . sousTotal ) } ' ,
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 (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . sousTotal ) } ' ,
style: pw . TextStyle (
fontSize: 7 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
) ) ,
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixFinal ) } ' ,
style: pw . TextStyle (
fontSize: 9 ,
fontWeight: pw . FontWeight . bold ) ) ,
] else
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixFinal ) } ' ,
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 ( ' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( sousTotal ) } ' ,
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 (
' - ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalRemises ) } ' ,
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 (
' - ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalCadeaux ) } ' ,
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 (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( commande . montantTotal ) } ' ,
style: boldTextStyle ,
textAlign: pw . TextAlign . right ) ,
) ,
] ,
) ,
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
pw . SizedBox ( height: 4 ) ,
pw . Text (
' Économies réalisées: ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalRemises + totalCadeaux ) } 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: ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalCadeaux ) } 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 ( ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( payment . amountGiven ) } 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
}
}
// Dans GestionCommandesPage - Remplacez la méthode _generateReceipt complète
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 (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixUnitaire ) } ' ,
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 (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixUnitaire ) } ' ,
style: pw . TextStyle (
fontSize: 6 ,
decoration: pw . TextDecoration . lineThrough ,
color: PdfColors . grey600 ,
) ) ,
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixFinal / detail . quantite ) } ' ,
style: const pw . TextStyle ( fontSize: 7 ) ) ,
] else
pw . Text (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( detail . prixUnitaire ) } ' ,
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 ( ' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( sousTotal ) } 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 ( ' - ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalRemises ) } 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 ( ' - ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalCadeaux ) } 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 ( ' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( commande . montantTotal ) } MGA ' ,
style: pw . TextStyle (
fontSize: 9 , fontWeight: pw . FontWeight . bold ) ) ,
] ,
) ,
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
pw . SizedBox ( height: 4 ) ,
pw . Text (
' Économies: ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalRemises + totalCadeaux ) } 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: ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( payment . amountGiven - commande . montantTotal ) } 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 . 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 (
' ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( commande . montantTotal ) } 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 (
' - ${ NumberFormat ( ' #,##0 ' , ' fr_FR ' ) . format ( totalRemises ) } ' ,
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 . payment ,
color: Colors . green . shade600 ,
) ,
onPressed: ( ) = > _showPaymentOptions ( commande ) ,
tooltip: ' Générer le ticket de la commande ' ,
) ,
) ,
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_outlined ,
color: Colors . blue . shade600 ,
) ,
onPressed: ( ) = >
_generateBon_lifraisonWithPasswordVerification (
commande ) ,
tooltip: ' Générer le Bon de livraison ' ,
) ,
) ,
if ( verifAdmin ( ) ) . . . [
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 ,
onGenerateBonLivraison: _generateBon_lifraisonWithPasswordVerification
) ,
] ,
) ,
) ,
] ,
) ,
) ;
} ,
) ;
} ,
) ) ,
] ,
) ,
) ;
}
}