12 changed files with 1140 additions and 561 deletions
@ -11,14 +11,15 @@ import 'package:path_provider/path_provider.dart'; |
|||
import 'package:share_plus/share_plus.dart'; |
|||
import 'package:permission_handler/permission_handler.dart'; |
|||
import '../models/command_detail.dart'; |
|||
import 'package:intl/intl.dart'; |
|||
|
|||
class PlatformPrintService { |
|||
// Format spécifique 58mm pour petites imprimantes |
|||
// Format spécifique 58mm pour petites imprimantes - CENTRÉ POUR L'IMPRESSION |
|||
static const PdfPageFormat ticket58mmFormat = PdfPageFormat( |
|||
58 * PdfPageFormat.mm, // Largeur exacte 58mm |
|||
48 * PdfPageFormat.mm, // Largeur exacte 58mm |
|||
double.infinity, // Hauteur automatique |
|||
marginLeft: 1 * PdfPageFormat.mm, |
|||
marginRight: 1 * PdfPageFormat.mm, |
|||
marginLeft: 4 * PdfPageFormat.mm, // ✅ Marges équilibrées pour centrer |
|||
marginRight: 4 * PdfPageFormat.mm, // ✅ Marges équilibrées pour centrer |
|||
marginTop: 2 * PdfPageFormat.mm, |
|||
marginBottom: 2 * PdfPageFormat.mm, |
|||
); |
|||
@ -40,15 +41,14 @@ class PlatformPrintService { |
|||
} |
|||
} |
|||
|
|||
// Générer PDF optimisé pour 58mm |
|||
static Future<Uint8List> _generate58mmTicketPdf({ |
|||
// Générer PDF optimisé pour 58mm - VERSION IDENTIQUE À L'ÉCRAN |
|||
static Future<Uint8List> _generate58mmTicketPdf({ |
|||
required CommandeDetail commande, |
|||
required String paymentMethod, |
|||
}) async { |
|||
}) async { |
|||
final pdf = pw.Document(); |
|||
|
|||
// Configuration pour 58mm (très petit) |
|||
const double titleSize = 9; |
|||
const double titleSize = 8; |
|||
const double headerSize = 8; |
|||
const double bodySize = 7; |
|||
const double smallSize = 6; |
|||
@ -56,30 +56,42 @@ class PlatformPrintService { |
|||
|
|||
final restaurantInfo = { |
|||
'nom': 'RESTAURANT ITRIMOBE', |
|||
'adresse': 'Moramanga, Antananarivo', |
|||
'ville': 'Madagascar', |
|||
'contact': '261348415301', |
|||
'email': '[email protected]', |
|||
'adresse': 'Moramanga, Madagascar', |
|||
'contact': '+261 34 12 34 56', |
|||
'nif': '4002141594', |
|||
'stat': '10715 33 2025 0 00414', |
|||
}; |
|||
|
|||
final factureNumber = |
|||
'T${DateTime.now().millisecondsSinceEpoch.toString().substring(8)}'; |
|||
final factureNumber = 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; |
|||
final dateTime = DateTime.now(); |
|||
|
|||
String paymentMethodText; |
|||
switch (paymentMethod) { |
|||
case 'mvola': |
|||
paymentMethodText = 'MVola'; |
|||
break; |
|||
case 'carte': |
|||
paymentMethodText = 'CB'; |
|||
break; |
|||
case 'especes': |
|||
paymentMethodText = 'Espèces'; |
|||
break; |
|||
default: |
|||
paymentMethodText = 'CB'; |
|||
} |
|||
|
|||
pdf.addPage( |
|||
pw.Page( |
|||
pageFormat: ticket58mmFormat, |
|||
margin: const pw.EdgeInsets.all(2), // 🔧 Marges minimales |
|||
margin: const pw.EdgeInsets.all(2), |
|||
|
|||
build: (pw.Context context) { |
|||
return pw.Container( |
|||
width: double.infinity, // 🔧 Forcer la largeur complète |
|||
width: double.infinity, |
|||
child: pw.Column( |
|||
crossAxisAlignment: |
|||
pw.CrossAxisAlignment.start, // 🔧 Alignement à gauche |
|||
crossAxisAlignment: pw.CrossAxisAlignment.start, |
|||
children: [ |
|||
// En-tête Restaurant (centré et compact) |
|||
// TITRE CENTRÉ |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Text( |
|||
@ -92,32 +104,49 @@ class PlatformPrintService { |
|||
), |
|||
), |
|||
|
|||
pw.SizedBox(height: 1), |
|||
pw.SizedBox(height: 2), |
|||
|
|||
// ADRESSE GAUCHE DÉCALÉE VERS LA GAUCHE (marginRight) |
|||
pw.Container( |
|||
width: double.infinity, |
|||
margin: const pw.EdgeInsets.only(right: 6), |
|||
child: pw.Text( |
|||
restaurantInfo['adresse']!, |
|||
'Adresse: ${restaurantInfo['adresse']!}', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
textAlign: pw.TextAlign.center, |
|||
textAlign: pw.TextAlign.left, |
|||
), |
|||
), |
|||
|
|||
// CONTACT GAUCHE DÉCALÉE |
|||
pw.Container( |
|||
width: double.infinity, |
|||
margin: const pw.EdgeInsets.only(right: 8), |
|||
child: pw.Text( |
|||
restaurantInfo['ville']!, |
|||
'Contact: ${restaurantInfo['contact']!}', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
textAlign: pw.TextAlign.center, |
|||
textAlign: pw.TextAlign.left, |
|||
), |
|||
), |
|||
|
|||
// NIF GAUCHE DÉCALÉE |
|||
pw.Container( |
|||
width: double.infinity, |
|||
margin: const pw.EdgeInsets.only(right: 8), |
|||
child: pw.Text( |
|||
'Tel: ${restaurantInfo['contact']!}', |
|||
'NIF: ${restaurantInfo['nif']!}', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
textAlign: pw.TextAlign.center, |
|||
textAlign: pw.TextAlign.left, |
|||
), |
|||
), |
|||
|
|||
// STAT GAUCHE DÉCALÉE |
|||
pw.Container( |
|||
width: double.infinity, |
|||
margin: const pw.EdgeInsets.only(right: 8), |
|||
child: pw.Text( |
|||
'STAT: ${restaurantInfo['stat']!}', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
textAlign: pw.TextAlign.left, |
|||
), |
|||
), |
|||
|
|||
@ -132,43 +161,52 @@ class PlatformPrintService { |
|||
|
|||
pw.SizedBox(height: 2), |
|||
|
|||
// Informations ticket |
|||
// FACTURE CENTRÉE |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Row( |
|||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
pw.Text( |
|||
'Ticket: $factureNumber', |
|||
child: pw.Text( |
|||
'Facture n° $factureNumber', |
|||
style: pw.TextStyle( |
|||
fontSize: bodySize, |
|||
fontWeight: pw.FontWeight.bold, |
|||
), |
|||
), |
|||
], |
|||
textAlign: pw.TextAlign.center, |
|||
), |
|||
), |
|||
|
|||
pw.SizedBox(height: 1), |
|||
|
|||
// DATE CENTRÉE |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Row( |
|||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
pw.Text( |
|||
_formatDate(dateTime), |
|||
child: pw.Text( |
|||
'Date: ${_formatDate(dateTime)} ${_formatTime(dateTime)}', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
textAlign: pw.TextAlign.center, |
|||
), |
|||
pw.Text( |
|||
_formatTime(dateTime), |
|||
), |
|||
|
|||
// TABLE CENTRÉE |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Text( |
|||
'Via: ${commande.tablename ?? "N/A"}', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
textAlign: pw.TextAlign.center, |
|||
), |
|||
], |
|||
), |
|||
|
|||
// PAIEMENT CENTRÉ |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Text( |
|||
'Paiement: $paymentMethodText', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
textAlign: pw.TextAlign.center, |
|||
), |
|||
), |
|||
|
|||
pw.SizedBox(height: 2), |
|||
pw.SizedBox(height: 3), |
|||
|
|||
// Ligne de séparation |
|||
pw.Container( |
|||
@ -179,38 +217,21 @@ class PlatformPrintService { |
|||
|
|||
pw.SizedBox(height: 2), |
|||
|
|||
// Articles (format très compact) |
|||
...commande.items |
|||
.map( |
|||
(item) => pw.Container( |
|||
width: double.infinity, // 🔧 Largeur complète |
|||
margin: const pw.EdgeInsets.only(bottom: 1), |
|||
child: pw.Column( |
|||
crossAxisAlignment: pw.CrossAxisAlignment.start, |
|||
children: [ |
|||
// Nom du plat |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Text( |
|||
'${item.menuNom}', |
|||
style: pw.TextStyle(fontSize: bodySize), |
|||
maxLines: 2, |
|||
), |
|||
), |
|||
|
|||
// Quantité, prix unitaire et total sur une ligne |
|||
// EN-TÊTE DES ARTICLES |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Row( |
|||
mainAxisAlignment: |
|||
pw.MainAxisAlignment.spaceBetween, |
|||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
pw.Text( |
|||
'${item.quantite}x ${item.prixUnitaire.toStringAsFixed(2)}MGA', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
'Qte Designation', |
|||
style: pw.TextStyle( |
|||
fontSize: bodySize, |
|||
fontWeight: pw.FontWeight.bold, |
|||
), |
|||
), |
|||
pw.Text( |
|||
'${(item.prixUnitaire * item.quantite).toStringAsFixed(2)}MGA', |
|||
'Prix', |
|||
style: pw.TextStyle( |
|||
fontSize: bodySize, |
|||
fontWeight: pw.FontWeight.bold, |
|||
@ -219,6 +240,36 @@ class PlatformPrintService { |
|||
], |
|||
), |
|||
), |
|||
|
|||
pw.Container( |
|||
width: double.infinity, |
|||
height: 0.5, |
|||
color: PdfColors.black, |
|||
), |
|||
|
|||
pw.SizedBox(height: 2), |
|||
|
|||
// ARTICLES |
|||
...commande.items |
|||
.map( |
|||
(item) => pw.Container( |
|||
width: double.infinity, |
|||
margin: const pw.EdgeInsets.only(bottom: 1), |
|||
child: pw.Row( |
|||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
|||
crossAxisAlignment: pw.CrossAxisAlignment.start, |
|||
children: [ |
|||
pw.Expanded( |
|||
child: pw.Text( |
|||
'${item.quantite} ${item.menuNom}', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
maxLines: 2, |
|||
), |
|||
), |
|||
pw.Text( |
|||
'${NumberFormat("#,##0.00", "fr_FR").format(item.prixUnitaire * item.quantite)}AR', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
@ -236,21 +287,21 @@ class PlatformPrintService { |
|||
|
|||
pw.SizedBox(height: 2), |
|||
|
|||
// Total |
|||
// TOTAL |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Row( |
|||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
pw.Text( |
|||
'TOTAL', |
|||
'Total:', |
|||
style: pw.TextStyle( |
|||
fontSize: titleSize, |
|||
fontWeight: pw.FontWeight.bold, |
|||
), |
|||
), |
|||
pw.Text( |
|||
'${commande.totalTtc.toStringAsFixed(2)}MGA', |
|||
'${NumberFormat("#,##0.00", "fr_FR").format(commande.totalTtc)}AR', |
|||
style: pw.TextStyle( |
|||
fontSize: titleSize, |
|||
fontWeight: pw.FontWeight.bold, |
|||
@ -260,34 +311,13 @@ class PlatformPrintService { |
|||
), |
|||
), |
|||
|
|||
pw.SizedBox(height: 3), |
|||
|
|||
// Mode de paiement |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Text( |
|||
'Paiement: ${paymentMethod.toLowerCase()}', |
|||
style: pw.TextStyle(fontSize: bodySize), |
|||
textAlign: pw.TextAlign.center, |
|||
), |
|||
), |
|||
|
|||
pw.SizedBox(height: 3), |
|||
|
|||
// Ligne de séparation |
|||
pw.Container( |
|||
width: double.infinity, |
|||
height: 0.5, |
|||
color: PdfColors.black, |
|||
), |
|||
|
|||
pw.SizedBox(height: 2), |
|||
pw.SizedBox(height: 4), |
|||
|
|||
// Message de remerciement |
|||
// MESSAGE FINAL CENTRÉ |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Text( |
|||
'Merci de votre visite !', |
|||
'Merci et a bientot !', |
|||
style: pw.TextStyle( |
|||
fontSize: bodySize, |
|||
fontStyle: pw.FontStyle.italic, |
|||
@ -296,27 +326,6 @@ class PlatformPrintService { |
|||
), |
|||
), |
|||
|
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Text( |
|||
'A bientôt !', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
textAlign: pw.TextAlign.center, |
|||
), |
|||
), |
|||
|
|||
pw.SizedBox(height: 3), |
|||
|
|||
// Code de suivi (optionnel) |
|||
pw.Container( |
|||
width: double.infinity, |
|||
child: pw.Text( |
|||
'Code: ${factureNumber}', |
|||
style: pw.TextStyle(fontSize: smallSize), |
|||
textAlign: pw.TextAlign.center, |
|||
), |
|||
), |
|||
|
|||
pw.SizedBox(height: 4), |
|||
|
|||
// Ligne de découpe |
|||
@ -338,7 +347,8 @@ class PlatformPrintService { |
|||
); |
|||
|
|||
return pdf.save(); |
|||
} |
|||
} |
|||
|
|||
|
|||
// Imprimer ticket 58mm |
|||
static Future<bool> printTicket({ |
|||
@ -399,20 +409,24 @@ class PlatformPrintService { |
|||
} |
|||
|
|||
final fileName = |
|||
'Ticket_58mm_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; |
|||
'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; |
|||
final file = File('${directory.path}/$fileName'); |
|||
|
|||
await file.writeAsBytes(pdfData); |
|||
|
|||
// ✅ VRAIE SAUVEGARDE au lieu de partage automatique |
|||
if (Platform.isAndroid) { |
|||
// Sur Android, on peut proposer les deux options |
|||
await Share.shareXFiles( |
|||
[XFile(file.path)], |
|||
subject: 'Ticket ${commande.numeroCommande}', |
|||
text: 'Ticket de caisse 58mm', |
|||
subject: 'Facture ${commande.numeroCommande}', |
|||
text: 'Facture de restaurant', |
|||
); |
|||
} |
|||
|
|||
return true; |
|||
} catch (e) { |
|||
print('Erreur sauvegarde 58mm: $e'); |
|||
print('Erreur sauvegarde: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
@ -435,7 +449,7 @@ class PlatformPrintService { |
|||
return await printTicket(commande: commande, paymentMethod: paymentMethod); |
|||
} |
|||
|
|||
// Utilitaires de formatageπ |
|||
// Utilitaires de formatage |
|||
static String _formatDate(DateTime dateTime) { |
|||
return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}'; |
|||
} |
|||
|
|||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 4.1 KiB |
Loading…
Reference in new issue