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