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.
1942 lines
72 KiB
1942 lines
72 KiB
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:numbers_to_letters/numbers_to_letters.dart';
|
|
import 'package:pdf/pdf.dart';
|
|
import 'package:pdf/widgets.dart' as pw;
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:open_file/open_file.dart';
|
|
import 'package:youmazgestion/Components/app_bar.dart';
|
|
import 'package:youmazgestion/Components/appDrawer.dart';
|
|
import 'package:youmazgestion/Models/client.dart';
|
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
import 'package:youmazgestion/controller/userController.dart';
|
|
|
|
class GestionCommandesPage extends StatefulWidget {
|
|
const GestionCommandesPage({super.key});
|
|
|
|
@override
|
|
_GestionCommandesPageState createState() => _GestionCommandesPageState();
|
|
}
|
|
|
|
class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|
final AppDatabase _database = AppDatabase.instance;
|
|
List<Commande> _commandes = [];
|
|
List<Commande> _filteredCommandes = [];
|
|
StatutCommande? _selectedStatut;
|
|
DateTime? _selectedDate;
|
|
final TextEditingController _searchController = TextEditingController();
|
|
bool _showCancelledOrders = false;
|
|
final userController = Get.find<UserController>();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadCommandes();
|
|
_searchController.addListener(_filterCommandes);
|
|
}
|
|
|
|
Future<void> _loadCommandes() async {
|
|
final commandes = await _database.getCommandes();
|
|
setState(() {
|
|
_commandes = commandes;
|
|
_filterCommandes();
|
|
});
|
|
}
|
|
|
|
Future<Uint8List> loadImage() async {
|
|
final data = await rootBundle.load('assets/youmaz2.png');
|
|
return data.buffer.asUint8List();
|
|
}
|
|
|
|
void _filterCommandes() {
|
|
final query = _searchController.text.toLowerCase();
|
|
setState(() {
|
|
_filteredCommandes = _commandes.where((commande) {
|
|
final matchesSearch =
|
|
commande.clientNomComplet.toLowerCase().contains(query) ||
|
|
commande.id.toString().contains(query);
|
|
final matchesStatut =
|
|
_selectedStatut == null || commande.statut == _selectedStatut;
|
|
final matchesDate = _selectedDate == null ||
|
|
DateFormat('yyyy-MM-dd').format(commande.dateCommande) ==
|
|
DateFormat('yyyy-MM-dd').format(_selectedDate!);
|
|
|
|
final shouldShowCancelled = _showCancelledOrders || commande.statut != StatutCommande.annulee;
|
|
|
|
return matchesSearch && matchesStatut && matchesDate && shouldShowCancelled;
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Future<void> _updateStatut(int commandeId, StatutCommande newStatut, {int? validateurId}) async {
|
|
// D'abord récupérer la commande existante pour avoir toutes ses valeurs
|
|
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) {
|
|
// Mise à jour avec validateur
|
|
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, // On met à jour le validateur
|
|
clientNom: commandeExistante.clientNom,
|
|
clientPrenom: commandeExistante.clientPrenom,
|
|
clientEmail: commandeExistante.clientEmail,
|
|
));
|
|
} else {
|
|
// Mise à jour simple du statut
|
|
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);
|
|
}
|
|
|
|
// Confirmer la commande avec le validateur actuel
|
|
await _updateStatut(
|
|
commande.id!,
|
|
StatutCommande.confirmee,
|
|
validateurId: userController.userId,
|
|
);
|
|
|
|
// Générer le ticket de caisse
|
|
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 change = amountGiven - commande.montantTotal;
|
|
return AlertDialog(
|
|
title: const Text('Paiement en liquide'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text('Montant total: ${commande.montantTotal.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 >= commande.montantTotal) {
|
|
setState(() {});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
if (amountGiven >= commande.montantTotal)
|
|
Text(
|
|
'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA',
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green,
|
|
),
|
|
),
|
|
if (amountGiven < commande.montantTotal)
|
|
Text(
|
|
'Montant insuffisant',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.red.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
child: const Text('Valider'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
Future<pw.Widget> buildIconPhoneText() async {
|
|
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
|
|
return pw.Text(String.fromCharCode(0xf095), style: pw.TextStyle(font: font));
|
|
}
|
|
Future<pw.Widget> buildIconCheckedText() async {
|
|
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
|
|
return pw.Text(String.fromCharCode(0xf14a), style: pw.TextStyle(font: font));
|
|
}
|
|
|
|
Future<pw.Widget> buildIconGlobeText() async {
|
|
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
|
|
return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font));
|
|
}
|
|
|
|
|
|
|
|
Future<void> _generateInvoice(Commande commande) async {
|
|
final details = await _database.getDetailsCommande(commande.id!);
|
|
final client = await _database.getClientById(commande.clientId);
|
|
final pointDeVente = await _database.getPointDeVenteById(1);
|
|
final iconPhone = await buildIconPhoneText();
|
|
final iconChecked = await buildIconCheckedText();
|
|
final iconGlobe = await buildIconGlobeText();
|
|
|
|
// IMPORTANT: Récupérer tous les détails des produits AVANT de créer le PDF
|
|
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'));
|
|
|
|
// Styles de texte
|
|
final smallTextStyle = pw.TextStyle(fontSize: 9);
|
|
final smallBoldTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold);
|
|
final normalTextStyle = pw.TextStyle(fontSize: 10);
|
|
final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold);
|
|
final boldTexClienttStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold);
|
|
final frameTextStyle = pw.TextStyle(fontSize: 10);
|
|
final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont);
|
|
final italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont);
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
margin: const pw.EdgeInsets.all(20),
|
|
build: (pw.Context context) {
|
|
return pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
// Première ligne: Logo à gauche, informations à droite
|
|
pw.Row(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
// Colonne de gauche avec logo et points de vente
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
// Logo
|
|
pw.Container(
|
|
width: 150,
|
|
height: 150,
|
|
child: pw.Image(image),
|
|
),
|
|
pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo),
|
|
pw.SizedBox(height: 12),
|
|
// Liste des points de vente avec checkbox
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]),
|
|
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]),
|
|
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]),
|
|
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]),
|
|
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]),
|
|
],
|
|
),
|
|
|
|
// Informations de contact
|
|
pw.SizedBox(height: 10),
|
|
pw.Row(children: [iconPhone, pw.SizedBox(width: 5), pw.Text('033 37 808 18', style: smallTextStyle)]),
|
|
pw.Row(children: [iconGlobe, pw.SizedBox(width: 5), pw.Text('www.guycom.mg', style: smallTextStyle)]),
|
|
pw.Text('Facebook: GuyCom', style: smallTextStyle),
|
|
],
|
|
),
|
|
|
|
// Colonne de droite avec cadres de texte
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldTexClienttStyle),
|
|
pw.SizedBox(height: 10),
|
|
pw.Container(width: 200, height: 1, color: PdfColors.black),
|
|
|
|
// Deux petits cadres côte à côte
|
|
pw.SizedBox(height: 10),
|
|
pw.Row(
|
|
children: [
|
|
pw.Container(
|
|
width: 100,
|
|
height: 40,
|
|
padding: const pw.EdgeInsets.all(5),
|
|
child: pw.Column(
|
|
children: [
|
|
pw.Text('Boutique:', style: frameTextStyle),
|
|
pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTexClienttStyle),
|
|
]
|
|
)
|
|
),
|
|
pw.SizedBox(width: 10),
|
|
pw.Container(
|
|
width: 100,
|
|
height: 40,
|
|
padding: const pw.EdgeInsets.all(5),
|
|
child: pw.Column(
|
|
children: [
|
|
pw.Text('Bon de livraison N°:', style: frameTextStyle),
|
|
pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTexClienttStyle),
|
|
]
|
|
)
|
|
),
|
|
],
|
|
),
|
|
|
|
// Grand cadre en dessous
|
|
pw.SizedBox(height: 20),
|
|
pw.Container(
|
|
width: 300,
|
|
height: 100,
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border.all(color: PdfColors.black, width: 1),
|
|
),
|
|
padding: const pw.EdgeInsets.all(10),
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
pw.Text('ID Client: ', style: frameTextStyle),
|
|
pw.SizedBox(height: 5),
|
|
pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle),
|
|
pw.SizedBox(height: 5),
|
|
pw.Container(width: 200, height: 1, color: PdfColors.black),
|
|
pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle),
|
|
pw.SizedBox(height: 10),
|
|
pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
pw.SizedBox(height: 20),
|
|
|
|
// Tableau des produits avec plus de colonnes
|
|
pw.Table(
|
|
border: pw.TableBorder.all(width: 0.5),
|
|
columnWidths: {
|
|
0: const pw.FlexColumnWidth(3), // Désignation
|
|
1: const pw.FlexColumnWidth(1), // Qté
|
|
2: const pw.FlexColumnWidth(2), // Prix unitaire
|
|
3: const pw.FlexColumnWidth(2), // 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('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right)),
|
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)),
|
|
],
|
|
),
|
|
|
|
// Lignes des produits avec détails complets
|
|
...detailsAvecProduits.map((item) {
|
|
final detail = item['detail'] as DetailCommande;
|
|
final produit = item['produit'];
|
|
|
|
return pw.TableRow(
|
|
children: [
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(4),
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
// Nom du produit
|
|
pw.Text(detail.produitNom ?? 'Produit inconnu',
|
|
style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)),
|
|
pw.SizedBox(height: 2),
|
|
|
|
|
|
if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty)
|
|
pw.Text('${produit.category} ${produit.marque}', style: smallTextStyle),
|
|
|
|
// IMEI
|
|
if (produit?.imei != null && produit!.imei!.isNotEmpty)
|
|
pw.Text('${produit.imei}', style: smallTextStyle),
|
|
|
|
|
|
// Référence
|
|
if (produit?.reference != null && produit!.reference!.isNotEmpty && produit?.ram != null && produit!.ram!.isNotEmpty && produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty)
|
|
pw.Text('${produit.ram} | ${produit.memoireInterne} | ${produit.reference}', style: smallTextStyle),
|
|
|
|
// // IMEI
|
|
// if (produit?.imei != null && produit!.imei!.isNotEmpty)
|
|
// pw.Text('IMEI: ${produit.imei}', style: smallTextStyle),
|
|
|
|
// // RAM
|
|
// if (produit?.ram != null && produit!.ram!.isNotEmpty)
|
|
// pw.Text('RAM: ${produit.ram}', style: smallTextStyle),
|
|
|
|
// // Stockage
|
|
// if (produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty)
|
|
// pw.Text('Stockage: ${produit.memoireInterne}', style: smallTextStyle),
|
|
|
|
// // Catégorie
|
|
|
|
],
|
|
),
|
|
),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(4),
|
|
child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center),
|
|
),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(4),
|
|
child: pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right),
|
|
),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(4),
|
|
child: pw.Text('${detail.sousTotal.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right),
|
|
),
|
|
],
|
|
);
|
|
}).toList(),
|
|
],
|
|
),
|
|
|
|
pw.SizedBox(height: 10),
|
|
|
|
// Total
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.end,
|
|
children: [
|
|
pw.Text('TOTAL', style: boldTextStyle),
|
|
pw.SizedBox(width: 20),
|
|
pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle),
|
|
],
|
|
),
|
|
|
|
pw.SizedBox(height: 10),
|
|
|
|
// Montant en lettres
|
|
pw.Text('Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', style: italicTextStyle),
|
|
|
|
pw.SizedBox(height: 30),
|
|
|
|
// Signatures
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text('Signature du vendeur', style: smallTextStyle),
|
|
pw.SizedBox(height: 20),
|
|
pw.Container(width: 150, height: 1, color: PdfColors.black),
|
|
],
|
|
),
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text('Signature du client', style: smallTextStyle),
|
|
pw.SizedBox(height: 20),
|
|
pw.Container(width: 150, height: 1, color: PdfColors.black),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
final output = await getTemporaryDirectory();
|
|
final file = File('${output.path}/facture_${commande.id}.pdf');
|
|
await file.writeAsBytes(await pdf.save());
|
|
await OpenFile.open(file.path);
|
|
}
|
|
|
|
|
|
pw.Widget _buildCheckboxPointDeVente(String text, bool checked) {
|
|
return pw.Row(
|
|
children: [
|
|
pw.Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border.all(width: 1),
|
|
color: checked ? PdfColors.black : PdfColors.white,
|
|
),
|
|
),
|
|
pw.SizedBox(width: 5),
|
|
pw.Text(text, style: pw.TextStyle(fontSize: 9)),
|
|
],
|
|
);
|
|
}
|
|
|
|
String _numberToWords(int number) {
|
|
// Implémentez la conversion du nombre en lettres ici
|
|
// Exemple simplifié:
|
|
NumbersToLetters.toLetters('fr', number);
|
|
return NumbersToLetters.toLetters('fr', number);
|
|
}
|
|
Future<void> _generateReceipt(Commande commande, PaymentMethod payment) async {
|
|
final details = await _database.getDetailsCommande(commande.id!);
|
|
final client = await _database.getClientById(commande.clientId);
|
|
final commandeur = commande.commandeurId != null
|
|
? await _database.getUserById(commande.commandeurId!)
|
|
: null;
|
|
final validateur = commande.validateurId != null
|
|
? await _database.getUserById(commande.validateurId!)
|
|
: null;
|
|
final pointDeVente = commandeur?.pointDeVenteId != null
|
|
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
|
|
: null;
|
|
|
|
// Récupérer les détails complets des produits
|
|
final List<Map<String, dynamic>> detailsAvecProduits = [];
|
|
for (final detail in details) {
|
|
final produit = await _database.getProductById(detail.produitId);
|
|
detailsAvecProduits.add({
|
|
'detail': detail,
|
|
'produit': produit,
|
|
});
|
|
}
|
|
|
|
final pdf = pw.Document();
|
|
final imageBytes = await loadImage();
|
|
final image = pw.MemoryImage(imageBytes);
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity),
|
|
margin: const pw.EdgeInsets.all(4),
|
|
build: (pw.Context context) {
|
|
return pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
// En-tête avec logo
|
|
pw.Center(
|
|
child: pw.Container(
|
|
width: 40,
|
|
height: 40,
|
|
child: pw.Image(image),
|
|
),
|
|
),
|
|
pw.SizedBox(height: 4),
|
|
|
|
// Informations de l'entreprise
|
|
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),
|
|
|
|
// Titre et numéro de ticket
|
|
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),
|
|
|
|
// Informations client
|
|
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)),
|
|
|
|
// Personnel impliqué
|
|
if (commandeur != null || validateur != null)
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Divider(thickness: 0.5),
|
|
if (commandeur != null)
|
|
pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)),
|
|
if (validateur != null)
|
|
pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)),
|
|
],
|
|
),
|
|
|
|
pw.Divider(thickness: 0.5),
|
|
|
|
// Détails des produits
|
|
pw.Table(
|
|
columnWidths: {
|
|
0: const pw.FlexColumnWidth(3.5),
|
|
1: const pw.FlexColumnWidth(1),
|
|
2: const pw.FlexColumnWidth(1.5),
|
|
},
|
|
children: [
|
|
// En-tête du tableau
|
|
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)),
|
|
|
|
),),
|
|
|
|
// Lignes des produits
|
|
...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.Text(detail.produitNom ?? 'Produit',
|
|
style: const pw.TextStyle(fontSize: 7)),
|
|
if (produit?.reference != null)
|
|
pw.Text('Ref: ${produit!.reference}',
|
|
style: const pw.TextStyle(fontSize: 6)),
|
|
if (produit?.imei != null)
|
|
pw.Text('IMEI: ${produit!.imei}',
|
|
style: const pw.TextStyle(fontSize: 6)),
|
|
],
|
|
),
|
|
pw.Text(detail.quantite.toString(),
|
|
style: const pw.TextStyle(fontSize: 7)),
|
|
pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}',
|
|
style: const pw.TextStyle(fontSize: 7)),
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
|
|
pw.Divider(thickness: 0.5),
|
|
|
|
// Total et paiement
|
|
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)),
|
|
],
|
|
),
|
|
|
|
pw.SizedBox(height: 6),
|
|
|
|
// Détails du paiement
|
|
pw.Text('MODE DE PAIEMENT:',
|
|
style: const pw.TextStyle(fontSize: 8)),
|
|
pw.Text(
|
|
payment.type == PaymentType.cash
|
|
? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)'
|
|
: 'CARTE BANCAIRE',
|
|
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: 12),
|
|
|
|
// Mentions légales et remerciements
|
|
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);
|
|
}
|
|
|
|
pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) {
|
|
return pw.Padding(
|
|
padding: const pw.EdgeInsets.all(4.0),
|
|
child: pw.Text(text, style: style ?? pw.TextStyle(fontSize: 8)),
|
|
);
|
|
}
|
|
|
|
Color _getStatutColor(StatutCommande statut) {
|
|
switch (statut) {
|
|
case StatutCommande.enAttente:
|
|
return Colors.orange.shade100;
|
|
case StatutCommande.confirmee:
|
|
return Colors.blue.shade100;
|
|
// case StatutCommande.enPreparation:
|
|
// return Colors.amber.shade100;
|
|
// case StatutCommande.expediee:
|
|
// return Colors.purple.shade100;
|
|
// case StatutCommande.livree:
|
|
// return Colors.green.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.enPreparation:
|
|
// return Icons.settings;
|
|
// case StatutCommande.expediee:
|
|
// return Icons.local_shipping;
|
|
// case StatutCommande.livree:
|
|
// return Icons.check_circle;
|
|
case StatutCommande.annulee:
|
|
return Icons.cancel;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: CustomAppBar(title: 'Gestion des Commandes'),
|
|
drawer: CustomDrawer(),
|
|
body: Column(
|
|
children: [
|
|
// Header avec logo et statistiques
|
|
Container(
|
|
padding: const EdgeInsets.all(16.0),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [Colors.blue.shade50, Colors.white],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Logo et titre
|
|
Row(
|
|
children: [
|
|
// Logo de l'entreprise
|
|
Container(
|
|
width: 50,
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),)
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Image.asset(
|
|
'assets/logo.png',
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade900,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(
|
|
Icons.business,
|
|
color: Colors.white,
|
|
size: 30,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Gestion des Commandes',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
Text(
|
|
'${_filteredCommandes.length} commande(s) affichée(s)',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Barre de recherche améliorée
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),)
|
|
],
|
|
),
|
|
child: TextField(
|
|
controller: _searchController,
|
|
decoration: InputDecoration(
|
|
labelText: 'Rechercher par client ou numéro de commande',
|
|
prefixIcon:
|
|
Icon(Icons.search, color: Colors.blue.shade800),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Filtres améliorés
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: DropdownButtonFormField<StatutCommande>(
|
|
value: _selectedStatut,
|
|
decoration: InputDecoration(
|
|
labelText: 'Filtrer par statut',
|
|
prefixIcon: Icon(Icons.filter_list,
|
|
color: Colors.blue.shade600),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
items: [
|
|
const DropdownMenuItem<StatutCommande>(
|
|
value: null,
|
|
child: Text('Tous les statuts'),
|
|
),
|
|
...StatutCommande.values.map((statut) {
|
|
return DropdownMenuItem<StatutCommande>(
|
|
value: statut,
|
|
child: Row(
|
|
children: [
|
|
Icon(_getStatutIcon(statut), size: 16),
|
|
const SizedBox(width: 8),
|
|
Text(statutLibelle(statut)),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_selectedStatut = value;
|
|
_filterCommandes();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: TextButton.icon(
|
|
style: TextButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 16,
|
|
horizontal: 12,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
onPressed: () async {
|
|
final date = await showDatePicker(
|
|
context: context,
|
|
initialDate: DateTime.now(),
|
|
firstDate: DateTime(2020),
|
|
lastDate: DateTime.now(),
|
|
builder: (context, child) {
|
|
return Theme(
|
|
data: Theme.of(context).copyWith(
|
|
colorScheme: ColorScheme.light(
|
|
primary: Colors.blue.shade900,
|
|
),
|
|
),
|
|
child: child!,
|
|
);
|
|
},
|
|
);
|
|
if (date != null) {
|
|
setState(() {
|
|
_selectedDate = date;
|
|
_filterCommandes();
|
|
});
|
|
}
|
|
},
|
|
icon: Icon(Icons.calendar_today,
|
|
color: Colors.blue.shade600),
|
|
label: Text(
|
|
_selectedDate == null
|
|
? 'Date'
|
|
: DateFormat('dd/MM/yyyy')
|
|
.format(_selectedDate!),
|
|
style: const TextStyle(color: Colors.black87),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
// Bouton reset
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: IconButton(
|
|
icon: Icon(Icons.refresh, color: Colors.blue.shade600),
|
|
onPressed: () {
|
|
setState(() {
|
|
_selectedStatut = null;
|
|
_selectedDate = null;
|
|
_searchController.clear();
|
|
_filterCommandes();
|
|
});
|
|
},
|
|
tooltip: 'Réinitialiser les filtres',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Toggle pour afficher/masquer les commandes annulées
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),)
|
|
],
|
|
),
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.visibility,
|
|
size: 20,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Afficher commandes annulées',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Switch(
|
|
value: _showCancelledOrders,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_showCancelledOrders = value;
|
|
_filterCommandes();
|
|
});
|
|
},
|
|
activeColor: Colors.blue.shade600,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Liste des commandes
|
|
Expanded(
|
|
child: _filteredCommandes.isEmpty
|
|
? Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.inbox,
|
|
size: 64,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Aucune commande trouvée',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.grey.shade600,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Essayez de modifier vos filtres',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
itemCount: _filteredCommandes.length,
|
|
itemBuilder: (context, index) {
|
|
final commande = _filteredCommandes[index];
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
decoration: BoxDecoration(
|
|
color: _getStatutColor(commande.statut),
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: ExpansionTile(
|
|
tilePadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
leading: Container(
|
|
width: 50,
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(25),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 2,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
_getStatutIcon(commande.statut),
|
|
size: 20,
|
|
color:
|
|
commande.statut == StatutCommande.annulee
|
|
? Colors.red
|
|
: Colors.blue.shade600,
|
|
),
|
|
Text(
|
|
'#${commande.id}',
|
|
style: const TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
title: Text(
|
|
commande.clientNomComplet,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.calendar_today,
|
|
size: 14,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
DateFormat('dd/MM/yyyy')
|
|
.format(commande.dateCommande),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
commande.statutLibelle,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: commande.statut ==
|
|
StatutCommande.annulee
|
|
? Colors.red
|
|
: Colors.blue.shade700,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.attach_money,
|
|
size: 14,
|
|
color: Colors.green.shade600,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'${commande.montantTotal.toStringAsFixed(2)} MGA',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(8),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 2,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: IconButton(
|
|
icon: Icon(
|
|
Icons.receipt_long,
|
|
color: Colors.blue.shade600,
|
|
),
|
|
onPressed: () => _generateInvoice(commande),
|
|
tooltip: 'Générer la facture',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(16.0),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(12),
|
|
bottomRight: Radius.circular(12),
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
_CommandeDetails(commande: commande),
|
|
const SizedBox(height: 16),
|
|
if (commande.statut != StatutCommande.annulee)
|
|
_CommandeActions(
|
|
commande: commande,
|
|
onStatutChanged: _updateStatut,
|
|
onPaymentSelected: _showPaymentOptions,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String statutLibelle(StatutCommande statut) {
|
|
switch (statut) {
|
|
case StatutCommande.enAttente:
|
|
return 'En attente';
|
|
case StatutCommande.confirmee:
|
|
return 'Confirmée';
|
|
// case StatutCommande.enPreparation:
|
|
// return 'En préparation';
|
|
// case StatutCommande.expediee:
|
|
// return 'Expédiée';
|
|
// case StatutCommande.livree:
|
|
// return 'Livrée';
|
|
case StatutCommande.annulee:
|
|
return 'Annulée';
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
class _CommandeDetails extends StatelessWidget {
|
|
final Commande commande;
|
|
|
|
const _CommandeDetails({required this.commande});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FutureBuilder<List<DetailCommande>>(
|
|
future: AppDatabase.instance.getDetailsCommande(commande.id!),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
|
return const Text('Aucun détail disponible');
|
|
}
|
|
|
|
final details = snapshot.data!;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Text(
|
|
'Détails de la commande',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Table(
|
|
children: [
|
|
TableRow(
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
),
|
|
children: [
|
|
_buildTableHeader('Produit'),
|
|
_buildTableHeader('Qté'),
|
|
_buildTableHeader('Prix unit.'),
|
|
_buildTableHeader('Total'),
|
|
],
|
|
),
|
|
...details.map((detail) => TableRow(
|
|
children: [
|
|
_buildTableCell(detail.produitNom ?? 'Produit inconnu'),
|
|
_buildTableCell('${detail.quantite}'),
|
|
_buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
|
|
_buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'),
|
|
],
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.green.shade200),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Total de la commande:',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
Text(
|
|
'${commande.montantTotal.toStringAsFixed(2)} MGA',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18,
|
|
color: Colors.green.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildTableHeader(String text) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
text,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTableCell(String text) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
text,
|
|
style: const TextStyle(fontSize: 13),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CommandeActions extends StatelessWidget {
|
|
final Commande commande;
|
|
final Function(int, StatutCommande) onStatutChanged;
|
|
final Function(Commande) onPaymentSelected;
|
|
|
|
const _CommandeActions({
|
|
required this.commande,
|
|
required this.onStatutChanged,
|
|
required this.onPaymentSelected,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey.shade200),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const Text(
|
|
'Actions sur la commande',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: _buildActionButtons(context),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildActionButtons(BuildContext context) {
|
|
List<Widget> buttons = [];
|
|
|
|
switch (commande.statut) {
|
|
case StatutCommande.enAttente:
|
|
buttons.addAll([
|
|
_buildActionButton(
|
|
label: 'Confirmer',
|
|
icon: Icons.check_circle,
|
|
color: Colors.blue,
|
|
onPressed: () => onPaymentSelected(commande),
|
|
),
|
|
_buildActionButton(
|
|
label: 'Annuler',
|
|
icon: Icons.cancel,
|
|
color: Colors.red,
|
|
onPressed: () => _showConfirmDialog(
|
|
context,
|
|
'Annuler la commande',
|
|
'Êtes-vous sûr de vouloir annuler cette commande?',
|
|
() => onStatutChanged(commande.id!, StatutCommande.annulee),
|
|
),
|
|
),
|
|
]);
|
|
break;
|
|
|
|
case StatutCommande.confirmee:
|
|
// case StatutCommande.enPreparation:
|
|
// case StatutCommande.expediee:
|
|
// case StatutCommande.livree:
|
|
buttons.add(
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.green.shade300),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.check_circle,
|
|
color: Colors.green.shade600, size: 16),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Commande confirmée',
|
|
style: TextStyle(
|
|
color: Colors.green.shade700,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
break;
|
|
|
|
case StatutCommande.annulee:
|
|
buttons.add(
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.red.shade300),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.cancel, color: Colors.red.shade600, size: 16),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Commande annulée',
|
|
style: TextStyle(
|
|
color: Colors.red.shade700,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
|
|
return buttons;
|
|
}
|
|
|
|
Widget _buildActionButton({
|
|
required String label,
|
|
required IconData icon,
|
|
required Color color,
|
|
required VoidCallback onPressed,
|
|
}) {
|
|
return ElevatedButton.icon(
|
|
onPressed: onPressed,
|
|
icon: Icon(icon, size: 16),
|
|
label: Text(label),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: color,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
elevation: 2,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showConfirmDialog(
|
|
BuildContext context,
|
|
String title,
|
|
String content,
|
|
VoidCallback onConfirm,
|
|
) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
title: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.help_outline,
|
|
color: Colors.blue.shade600,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(fontSize: 18),
|
|
),
|
|
],
|
|
),
|
|
content: Text(content),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: Text(
|
|
'Annuler',
|
|
style: TextStyle(color: Colors.grey.shade600),
|
|
),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
onConfirm();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue.shade600,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: const Text('Confirmer'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
enum PaymentType { cash, card ,
|
|
|
|
mvola,
|
|
orange,
|
|
airtel
|
|
}
|
|
|
|
class PaymentMethod {
|
|
final PaymentType type;
|
|
final double amountGiven;
|
|
|
|
PaymentMethod({required this.type, this.amountGiven = 0});
|
|
}
|
|
|
|
class PaymentMethodDialog extends StatefulWidget {
|
|
final Commande commande;
|
|
|
|
const PaymentMethodDialog({super.key, required this.commande});
|
|
|
|
@override
|
|
_PaymentMethodDialogState createState() => _PaymentMethodDialogState();
|
|
}
|
|
|
|
class _PaymentMethodDialogState extends State<PaymentMethodDialog> {
|
|
PaymentType _selectedPayment = PaymentType.cash;
|
|
final _amountController = TextEditingController();
|
|
void _validatePayment() {
|
|
if (_selectedPayment == PaymentType.cash) {
|
|
final amountGiven = double.tryParse(_amountController.text) ?? 0;
|
|
if (amountGiven < widget.commande.montantTotal) {
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Le montant donné est insuffisant',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
Navigator.pop(context, PaymentMethod(
|
|
type: _selectedPayment,
|
|
amountGiven: _selectedPayment == PaymentType.cash
|
|
? double.parse(_amountController.text)
|
|
: widget.commande.montantTotal,
|
|
));
|
|
}
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Initialiser avec le montant total de la commande
|
|
_amountController.text = widget.commande.montantTotal.toStringAsFixed(2);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_amountController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final amount = double.tryParse(_amountController.text) ?? 0;
|
|
final change = amount - widget.commande.montantTotal;
|
|
|
|
return AlertDialog(
|
|
title: const Text('Méthode de paiement', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Section Paiement mobile
|
|
const Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text('Mobile Money', style: TextStyle(fontWeight: FontWeight.w500)),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildMobileMoneyTile(
|
|
title: 'Mvola',
|
|
imagePath: 'assets/mvola.jpg',
|
|
value: PaymentType.mvola,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: _buildMobileMoneyTile(
|
|
title: 'Orange Money',
|
|
imagePath: 'assets/Orange_money.png',
|
|
value: PaymentType.orange,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: _buildMobileMoneyTile(
|
|
title: 'Airtel Money',
|
|
imagePath: 'assets/airtel_money.png',
|
|
value: PaymentType.airtel,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Section Carte bancaire
|
|
const Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text('Carte Bancaire', style: TextStyle(fontWeight: FontWeight.w500)),
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildPaymentMethodTile(
|
|
title: 'Carte bancaire',
|
|
icon: Icons.credit_card,
|
|
value: PaymentType.card,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Section Paiement en liquide
|
|
const Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text('Espèces', style: TextStyle(fontWeight: FontWeight.w500)),
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildPaymentMethodTile(
|
|
title: 'Paiement en liquide',
|
|
icon: Icons.money,
|
|
value: PaymentType.cash,
|
|
),
|
|
if (_selectedPayment == PaymentType.cash) ...[
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _amountController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Montant donné',
|
|
prefixText: 'MGA ',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
onChanged: (value) => setState(() {}),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Monnaie à rendre: ${change.toStringAsFixed(2)} MGA',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: change >= 0 ? Colors.green : Colors.red,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Annuler', style: TextStyle(color: Colors.grey)),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue.shade800,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
onPressed: _validatePayment,
|
|
child: const Text('Confirmer'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMobileMoneyTile({
|
|
required String title,
|
|
required String imagePath,
|
|
required PaymentType value,
|
|
}) {
|
|
return Card(
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
side: BorderSide(
|
|
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2),
|
|
width: 2,
|
|
),
|
|
),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(8),
|
|
onTap: () => setState(() => _selectedPayment = value),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
children: [
|
|
Image.asset(
|
|
imagePath,
|
|
height: 30,
|
|
width: 30,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
const Icon(Icons.mobile_friendly, size: 30),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
title,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPaymentMethodTile({
|
|
required String title,
|
|
required IconData icon,
|
|
required PaymentType value,
|
|
}) {
|
|
return Card(
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
side: BorderSide(
|
|
color: _selectedPayment == value ? Colors.blue : Colors.grey.withOpacity(0.2),
|
|
width: 2,
|
|
),
|
|
),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(8),
|
|
onTap: () => setState(() => _selectedPayment = value),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 24),
|
|
const SizedBox(width: 12),
|
|
Text(title),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|