|
After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 113 KiB |
@ -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; |
|||
} |
|||
} |
|||
} |
|||