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

3086 lines
135 KiB

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package: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\nOFFERT'
: 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
),
],
),
),
],
),
);
},
);
},
)),
],
),
);
}
}