13 changed files with 960 additions and 29 deletions
@ -0,0 +1,305 @@ |
|||||
|
// pages/facture_screen.dart |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter/services.dart'; |
||||
|
import '../models/command_detail.dart'; |
||||
|
import '../services/pdf_service.dart'; |
||||
|
|
||||
|
class FactureScreen extends StatefulWidget { |
||||
|
final CommandeDetail commande; |
||||
|
final String paymentMethod; |
||||
|
|
||||
|
const FactureScreen({ |
||||
|
Key? key, |
||||
|
required this.commande, |
||||
|
required this.paymentMethod, |
||||
|
}) : super(key: key); |
||||
|
|
||||
|
@override |
||||
|
_FactureScreenState createState() => _FactureScreenState(); |
||||
|
} |
||||
|
|
||||
|
class _FactureScreenState extends State<FactureScreen> { |
||||
|
String get paymentMethodText { |
||||
|
switch (widget.paymentMethod) { |
||||
|
case 'mvola': |
||||
|
return 'MVola'; |
||||
|
case 'carte': |
||||
|
return 'CB'; |
||||
|
case 'especes': |
||||
|
return 'Espèces'; |
||||
|
default: |
||||
|
return 'CB'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
String get factureNumber { |
||||
|
return 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Scaffold( |
||||
|
backgroundColor: Colors.grey[100], |
||||
|
appBar: AppBar( |
||||
|
backgroundColor: Colors.white, |
||||
|
elevation: 0, |
||||
|
leading: IconButton( |
||||
|
icon: const Icon(Icons.arrow_back, color: Colors.black), |
||||
|
onPressed: () => Navigator.of(context).pop(), |
||||
|
), |
||||
|
title: const Text( |
||||
|
'Retour', |
||||
|
style: TextStyle( |
||||
|
color: Colors.black, |
||||
|
fontSize: 16, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
), |
||||
|
), |
||||
|
actions: [ |
||||
|
Container( |
||||
|
margin: const EdgeInsets.only(right: 16, top: 8, bottom: 8), |
||||
|
child: ElevatedButton.icon( |
||||
|
onPressed: _printReceipt, |
||||
|
icon: const Icon(Icons.print, size: 18), |
||||
|
label: const Text('Imprimer'), |
||||
|
style: ElevatedButton.styleFrom( |
||||
|
backgroundColor: const Color(0xFF28A745), |
||||
|
foregroundColor: Colors.white, |
||||
|
elevation: 0, |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(6), |
||||
|
), |
||||
|
padding: const EdgeInsets.symmetric( |
||||
|
horizontal: 12, |
||||
|
vertical: 8, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
body: Center( |
||||
|
child: Container( |
||||
|
width: 400, |
||||
|
margin: const EdgeInsets.all(20), |
||||
|
child: Card( |
||||
|
elevation: 2, |
||||
|
color: Colors.white, |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(8), |
||||
|
), |
||||
|
child: Padding( |
||||
|
padding: const EdgeInsets.all(40), |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
_buildHeader(), |
||||
|
const SizedBox(height: 30), |
||||
|
_buildFactureInfo(), |
||||
|
const SizedBox(height: 30), |
||||
|
_buildItemsList(), |
||||
|
const SizedBox(height: 20), |
||||
|
_buildTotal(), |
||||
|
const SizedBox(height: 30), |
||||
|
_buildFooter(), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildHeader() { |
||||
|
return Column( |
||||
|
children: [ |
||||
|
const Text( |
||||
|
'RESTAURANT', |
||||
|
style: TextStyle( |
||||
|
fontSize: 20, |
||||
|
fontWeight: FontWeight.bold, |
||||
|
letterSpacing: 1.2, |
||||
|
), |
||||
|
), |
||||
|
const SizedBox(height: 12), |
||||
|
const Text( |
||||
|
'Adresse: 123 Rue de la Paix', |
||||
|
style: TextStyle(fontSize: 12, color: Colors.black87), |
||||
|
), |
||||
|
const Text( |
||||
|
'Contact: +33 1 23 45 67 89', |
||||
|
style: TextStyle(fontSize: 12, color: Colors.black87), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildFactureInfo() { |
||||
|
final now = DateTime.now(); |
||||
|
final dateStr = |
||||
|
'${now.day.toString().padLeft(2, '0')}/${now.month.toString().padLeft(2, '0')}/${now.year}'; |
||||
|
final timeStr = |
||||
|
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; |
||||
|
|
||||
|
return Column( |
||||
|
children: [ |
||||
|
Text( |
||||
|
'Facture n° $factureNumber', |
||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), |
||||
|
), |
||||
|
const SizedBox(height: 4), |
||||
|
Text( |
||||
|
'Date: $dateStr $timeStr', |
||||
|
style: const TextStyle(fontSize: 12, color: Colors.black87), |
||||
|
), |
||||
|
const SizedBox(height: 4), |
||||
|
Text( |
||||
|
'Table: ${widget.commande.tableId}', |
||||
|
style: const TextStyle(fontSize: 12, color: Colors.black87), |
||||
|
), |
||||
|
const SizedBox(height: 4), |
||||
|
Text( |
||||
|
'Paiement: $paymentMethodText', |
||||
|
style: const TextStyle(fontSize: 12, color: Colors.black87), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildItemsList() { |
||||
|
return Column( |
||||
|
children: [ |
||||
|
const Padding( |
||||
|
padding: EdgeInsets.only(bottom: 10), |
||||
|
child: Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
Text( |
||||
|
'Qté Désignation', |
||||
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), |
||||
|
), |
||||
|
Text( |
||||
|
'Prix', |
||||
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
const Divider(height: 1, color: Colors.black26), |
||||
|
const SizedBox(height: 10), |
||||
|
...widget.commande.items |
||||
|
.map( |
||||
|
(item) => Padding( |
||||
|
padding: const EdgeInsets.only(bottom: 6), |
||||
|
child: Row( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
'${item.quantite} ${item.menuNom}', |
||||
|
style: const TextStyle( |
||||
|
fontSize: 12, |
||||
|
color: Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Text( |
||||
|
'${(item.prixUnitaire * item.quantite).toStringAsFixed(2)} MGA', |
||||
|
style: const TextStyle( |
||||
|
fontSize: 12, |
||||
|
color: Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
) |
||||
|
.toList(), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildTotal() { |
||||
|
return Column( |
||||
|
children: [ |
||||
|
const Divider(height: 1, color: Colors.black26), |
||||
|
const SizedBox(height: 12), |
||||
|
Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
const Text( |
||||
|
'Total:', |
||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), |
||||
|
), |
||||
|
Text( |
||||
|
'${widget.commande.totalTtc.toStringAsFixed(2)} €', |
||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildFooter() { |
||||
|
return const Text( |
||||
|
'Merci et à bientôt !', |
||||
|
style: TextStyle( |
||||
|
fontSize: 12, |
||||
|
fontStyle: FontStyle.italic, |
||||
|
color: Colors.black54, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _printReceipt() async { |
||||
|
try { |
||||
|
HapticFeedback.lightImpact(); |
||||
|
|
||||
|
final success = await PdfService.printFacture( |
||||
|
commande: widget.commande, |
||||
|
paymentMethod: widget.paymentMethod, |
||||
|
); |
||||
|
|
||||
|
if (success) { |
||||
|
ScaffoldMessenger.of(context).showSnackBar( |
||||
|
SnackBar( |
||||
|
content: const Row( |
||||
|
children: [ |
||||
|
Icon(Icons.check_circle, color: Colors.white), |
||||
|
SizedBox(width: 8), |
||||
|
Text('Facture envoyée à l\'impression'), |
||||
|
], |
||||
|
), |
||||
|
backgroundColor: const Color(0xFF28A745), |
||||
|
duration: const Duration(seconds: 2), |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(6), |
||||
|
), |
||||
|
margin: const EdgeInsets.all(16), |
||||
|
behavior: SnackBarBehavior.floating, |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
Future.delayed(const Duration(seconds: 2), () { |
||||
|
if (mounted) { |
||||
|
Navigator.of(context).popUntil((route) => route.isFirst); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
ScaffoldMessenger.of(context).showSnackBar( |
||||
|
SnackBar( |
||||
|
content: Text('Erreur impression: $e'), |
||||
|
backgroundColor: Colors.red, |
||||
|
), |
||||
|
); |
||||
|
} finally { |
||||
|
if (mounted) { |
||||
|
Navigator.of(context).pop(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,266 @@ |
|||||
|
// services/pdf_service.dart |
||||
|
import 'dart:io'; |
||||
|
import 'dart:typed_data'; |
||||
|
import 'package:pdf/pdf.dart'; |
||||
|
import 'package:pdf/widgets.dart' as pw; |
||||
|
import 'package:printing/printing.dart'; |
||||
|
import 'package:path_provider/path_provider.dart'; |
||||
|
import 'package:share_plus/share_plus.dart'; |
||||
|
import '../models/command_detail.dart'; |
||||
|
|
||||
|
class PdfService { |
||||
|
static Future<Uint8List> generateFacturePdf({ |
||||
|
required CommandeDetail commande, |
||||
|
required String paymentMethod, |
||||
|
}) async { |
||||
|
final pdf = pw.Document(); |
||||
|
|
||||
|
// Informations du restaurant |
||||
|
final restaurantInfo = { |
||||
|
'nom': 'RESTAURANT', |
||||
|
'adresse': 'Moramanga, Antananarivo', |
||||
|
'contact': '+261 34 12 34 56', |
||||
|
}; |
||||
|
|
||||
|
// Générer numéro de facture |
||||
|
final factureNumber = |
||||
|
'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; |
||||
|
final dateTime = DateTime.now(); |
||||
|
|
||||
|
pdf.addPage( |
||||
|
pw.Page( |
||||
|
pageFormat: PdfPageFormat.a4, |
||||
|
margin: const pw.EdgeInsets.all(32), |
||||
|
build: (pw.Context context) { |
||||
|
return pw.Column( |
||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
// En-tête Restaurant |
||||
|
pw.Center( |
||||
|
child: pw.Column( |
||||
|
children: [ |
||||
|
pw.Text( |
||||
|
restaurantInfo['nom']!, |
||||
|
style: pw.TextStyle( |
||||
|
fontSize: 24, |
||||
|
fontWeight: pw.FontWeight.bold, |
||||
|
), |
||||
|
), |
||||
|
pw.SizedBox(height: 8), |
||||
|
pw.Text( |
||||
|
'Adresse: ${restaurantInfo['adresse']}', |
||||
|
style: const pw.TextStyle(fontSize: 12), |
||||
|
), |
||||
|
pw.Text( |
||||
|
'Contact: ${restaurantInfo['contact']}', |
||||
|
style: const pw.TextStyle(fontSize: 12), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
|
||||
|
pw.SizedBox(height: 30), |
||||
|
|
||||
|
// Informations facture |
||||
|
pw.Center( |
||||
|
child: pw.Column( |
||||
|
children: [ |
||||
|
pw.Text( |
||||
|
'Facture n° $factureNumber', |
||||
|
style: pw.TextStyle( |
||||
|
fontSize: 14, |
||||
|
fontWeight: pw.FontWeight.bold, |
||||
|
), |
||||
|
), |
||||
|
pw.SizedBox(height: 4), |
||||
|
pw.Text( |
||||
|
'Date: ${_formatDateTime(dateTime)}', |
||||
|
style: const pw.TextStyle(fontSize: 12), |
||||
|
), |
||||
|
pw.SizedBox(height: 4), |
||||
|
pw.Text( |
||||
|
'Table: ${commande.numeroCommande}', |
||||
|
style: const pw.TextStyle(fontSize: 12), |
||||
|
), |
||||
|
pw.SizedBox(height: 4), |
||||
|
pw.Text( |
||||
|
'Paiement: ${_getPaymentMethodText(paymentMethod)}', |
||||
|
style: const pw.TextStyle(fontSize: 12), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
|
||||
|
pw.SizedBox(height: 30), |
||||
|
|
||||
|
// Tableau des articles |
||||
|
pw.Table( |
||||
|
border: pw.TableBorder.all(color: PdfColors.grey300), |
||||
|
columnWidths: { |
||||
|
0: const pw.FlexColumnWidth(3), |
||||
|
1: const pw.FlexColumnWidth(1), |
||||
|
2: const pw.FlexColumnWidth(1), |
||||
|
}, |
||||
|
children: [ |
||||
|
// En-tête du tableau |
||||
|
pw.TableRow( |
||||
|
decoration: const pw.BoxDecoration( |
||||
|
color: PdfColors.grey100, |
||||
|
), |
||||
|
children: [ |
||||
|
pw.Padding( |
||||
|
padding: const pw.EdgeInsets.all(8), |
||||
|
child: pw.Text( |
||||
|
'Qté Désignation', |
||||
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold), |
||||
|
), |
||||
|
), |
||||
|
pw.Padding( |
||||
|
padding: const pw.EdgeInsets.all(8), |
||||
|
child: pw.Text( |
||||
|
'Prix', |
||||
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold), |
||||
|
textAlign: pw.TextAlign.right, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
// Lignes des articles |
||||
|
...commande.items |
||||
|
.map( |
||||
|
(item) => pw.TableRow( |
||||
|
children: [ |
||||
|
pw.Padding( |
||||
|
padding: const pw.EdgeInsets.all(8), |
||||
|
child: pw.Text( |
||||
|
'${item.quantite} TESTNOMCOMMANDE', |
||||
|
), |
||||
|
), |
||||
|
pw.Padding( |
||||
|
padding: const pw.EdgeInsets.all(8), |
||||
|
child: pw.Text( |
||||
|
'${item.prixUnitaire.toStringAsFixed(2)} €', |
||||
|
textAlign: pw.TextAlign.right, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
) |
||||
|
.toList(), |
||||
|
], |
||||
|
), |
||||
|
|
||||
|
pw.SizedBox(height: 20), |
||||
|
|
||||
|
// Total |
||||
|
pw.Container( |
||||
|
alignment: pw.Alignment.centerRight, |
||||
|
child: pw.Container( |
||||
|
padding: const pw.EdgeInsets.all(12), |
||||
|
decoration: pw.BoxDecoration( |
||||
|
border: pw.Border.all(color: PdfColors.grey400), |
||||
|
color: PdfColors.grey50, |
||||
|
), |
||||
|
child: pw.Text( |
||||
|
'Total: ${commande.totalTtc.toStringAsFixed(2)} €', |
||||
|
style: pw.TextStyle( |
||||
|
fontSize: 16, |
||||
|
fontWeight: pw.FontWeight.bold, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
|
||||
|
pw.Spacer(), |
||||
|
|
||||
|
// Message de remerciement |
||||
|
pw.Center( |
||||
|
child: pw.Text( |
||||
|
'Merci et à bientôt !', |
||||
|
style: pw.TextStyle( |
||||
|
fontSize: 12, |
||||
|
fontStyle: pw.FontStyle.italic, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
}, |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
return pdf.save(); |
||||
|
} |
||||
|
|
||||
|
static String _formatDateTime(DateTime dateTime) { |
||||
|
return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; |
||||
|
} |
||||
|
|
||||
|
static String _getPaymentMethodText(String method) { |
||||
|
switch (method) { |
||||
|
case 'mvola': |
||||
|
return 'MVola'; |
||||
|
case 'carte': |
||||
|
return 'CB'; |
||||
|
case 'especes': |
||||
|
return 'Espèces'; |
||||
|
default: |
||||
|
return 'CB'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Imprimer directement |
||||
|
static Future<bool> printFacture({ |
||||
|
required CommandeDetail commande, |
||||
|
required String paymentMethod, |
||||
|
}) async { |
||||
|
try { |
||||
|
final pdfData = await generateFacturePdf( |
||||
|
commande: commande, |
||||
|
paymentMethod: paymentMethod, |
||||
|
); |
||||
|
|
||||
|
await Printing.layoutPdf( |
||||
|
onLayout: (PdfPageFormat format) async => pdfData, |
||||
|
name: |
||||
|
'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}', |
||||
|
); |
||||
|
|
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
print('Erreur impression: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Sauvegarder et partager le PDF |
||||
|
static Future<bool> saveAndShareFacture({ |
||||
|
required CommandeDetail commande, |
||||
|
required String paymentMethod, |
||||
|
}) async { |
||||
|
try { |
||||
|
final pdfData = await generateFacturePdf( |
||||
|
commande: commande, |
||||
|
paymentMethod: paymentMethod, |
||||
|
); |
||||
|
|
||||
|
final directory = await getApplicationDocumentsDirectory(); |
||||
|
final fileName = |
||||
|
'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; |
||||
|
final file = File('${directory.path}/$fileName'); |
||||
|
|
||||
|
await file.writeAsBytes(pdfData); |
||||
|
|
||||
|
await Share.shareXFiles( |
||||
|
[XFile(file.path)], |
||||
|
subject: 'Facture ${commande.numeroCommande}', |
||||
|
text: 'Facture de votre commande au restaurant', |
||||
|
); |
||||
|
|
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
print('Erreur sauvegarde/partage: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue