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:share_plus/share_plus.dart'; |
||||
import 'package:permission_handler/permission_handler.dart'; |
import 'package:permission_handler/permission_handler.dart'; |
||||
import '../models/command_detail.dart'; |
import '../models/command_detail.dart'; |
||||
|
import 'package:intl/intl.dart'; |
||||
|
|
||||
class PlatformPrintService { |
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( |
static const PdfPageFormat ticket58mmFormat = PdfPageFormat( |
||||
58 * PdfPageFormat.mm, // Largeur exacte 58mm |
48 * PdfPageFormat.mm, // Largeur exacte 58mm |
||||
double.infinity, // Hauteur automatique |
double.infinity, // Hauteur automatique |
||||
marginLeft: 1 * PdfPageFormat.mm, |
marginLeft: 4 * PdfPageFormat.mm, // ✅ Marges équilibrées pour centrer |
||||
marginRight: 1 * PdfPageFormat.mm, |
marginRight: 4 * PdfPageFormat.mm, // ✅ Marges équilibrées pour centrer |
||||
marginTop: 2 * PdfPageFormat.mm, |
marginTop: 2 * PdfPageFormat.mm, |
||||
marginBottom: 2 * PdfPageFormat.mm, |
marginBottom: 2 * PdfPageFormat.mm, |
||||
); |
); |
||||
@ -40,15 +41,14 @@ class PlatformPrintService { |
|||||
} |
} |
||||
} |
} |
||||
|
|
||||
// Générer PDF optimisé pour 58mm |
// Générer PDF optimisé pour 58mm - VERSION IDENTIQUE À L'ÉCRAN |
||||
static Future<Uint8List> _generate58mmTicketPdf({ |
static Future<Uint8List> _generate58mmTicketPdf({ |
||||
required CommandeDetail commande, |
required CommandeDetail commande, |
||||
required String paymentMethod, |
required String paymentMethod, |
||||
}) async { |
}) async { |
||||
final pdf = pw.Document(); |
final pdf = pw.Document(); |
||||
|
|
||||
// Configuration pour 58mm (très petit) |
const double titleSize = 8; |
||||
const double titleSize = 9; |
|
||||
const double headerSize = 8; |
const double headerSize = 8; |
||||
const double bodySize = 7; |
const double bodySize = 7; |
||||
const double smallSize = 6; |
const double smallSize = 6; |
||||
@ -56,30 +56,42 @@ class PlatformPrintService { |
|||||
|
|
||||
final restaurantInfo = { |
final restaurantInfo = { |
||||
'nom': 'RESTAURANT ITRIMOBE', |
'nom': 'RESTAURANT ITRIMOBE', |
||||
'adresse': 'Moramanga, Antananarivo', |
'adresse': 'Moramanga, Madagascar', |
||||
'ville': 'Madagascar', |
'contact': '+261 34 12 34 56', |
||||
'contact': '261348415301', |
|
||||
'email': '[email protected]', |
|
||||
'nif': '4002141594', |
'nif': '4002141594', |
||||
'stat': '10715 33 2025 0 00414', |
'stat': '10715 33 2025 0 00414', |
||||
}; |
}; |
||||
|
|
||||
final factureNumber = |
final factureNumber = 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; |
||||
'T${DateTime.now().millisecondsSinceEpoch.toString().substring(8)}'; |
|
||||
final dateTime = DateTime.now(); |
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( |
pdf.addPage( |
||||
pw.Page( |
pw.Page( |
||||
pageFormat: ticket58mmFormat, |
pageFormat: ticket58mmFormat, |
||||
margin: const pw.EdgeInsets.all(2), // 🔧 Marges minimales |
margin: const pw.EdgeInsets.all(2), |
||||
|
|
||||
build: (pw.Context context) { |
build: (pw.Context context) { |
||||
return pw.Container( |
return pw.Container( |
||||
width: double.infinity, // 🔧 Forcer la largeur complète |
width: double.infinity, |
||||
child: pw.Column( |
child: pw.Column( |
||||
crossAxisAlignment: |
crossAxisAlignment: pw.CrossAxisAlignment.start, |
||||
pw.CrossAxisAlignment.start, // 🔧 Alignement à gauche |
|
||||
children: [ |
children: [ |
||||
// En-tête Restaurant (centré et compact) |
// TITRE CENTRÉ |
||||
pw.Container( |
pw.Container( |
||||
width: double.infinity, |
width: double.infinity, |
||||
child: pw.Text( |
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( |
pw.Container( |
||||
width: double.infinity, |
width: double.infinity, |
||||
|
margin: const pw.EdgeInsets.only(right: 6), |
||||
child: pw.Text( |
child: pw.Text( |
||||
restaurantInfo['adresse']!, |
'Adresse: ${restaurantInfo['adresse']!}', |
||||
style: pw.TextStyle(fontSize: smallSize), |
style: pw.TextStyle(fontSize: smallSize), |
||||
textAlign: pw.TextAlign.center, |
textAlign: pw.TextAlign.left, |
||||
), |
), |
||||
), |
), |
||||
|
|
||||
|
// CONTACT GAUCHE DÉCALÉE |
||||
pw.Container( |
pw.Container( |
||||
width: double.infinity, |
width: double.infinity, |
||||
|
margin: const pw.EdgeInsets.only(right: 8), |
||||
child: pw.Text( |
child: pw.Text( |
||||
restaurantInfo['ville']!, |
'Contact: ${restaurantInfo['contact']!}', |
||||
style: pw.TextStyle(fontSize: smallSize), |
style: pw.TextStyle(fontSize: smallSize), |
||||
textAlign: pw.TextAlign.center, |
textAlign: pw.TextAlign.left, |
||||
), |
), |
||||
), |
), |
||||
|
|
||||
|
// NIF GAUCHE DÉCALÉE |
||||
pw.Container( |
pw.Container( |
||||
width: double.infinity, |
width: double.infinity, |
||||
|
margin: const pw.EdgeInsets.only(right: 8), |
||||
child: pw.Text( |
child: pw.Text( |
||||
'Tel: ${restaurantInfo['contact']!}', |
'NIF: ${restaurantInfo['nif']!}', |
||||
style: pw.TextStyle(fontSize: smallSize), |
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), |
pw.SizedBox(height: 2), |
||||
|
|
||||
// Informations ticket |
// FACTURE CENTRÉE |
||||
pw.Container( |
pw.Container( |
||||
width: double.infinity, |
width: double.infinity, |
||||
child: pw.Row( |
child: pw.Text( |
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
'Facture n° $factureNumber', |
||||
children: [ |
|
||||
pw.Text( |
|
||||
'Ticket: $factureNumber', |
|
||||
style: pw.TextStyle( |
style: pw.TextStyle( |
||||
fontSize: bodySize, |
fontSize: bodySize, |
||||
fontWeight: pw.FontWeight.bold, |
fontWeight: pw.FontWeight.bold, |
||||
), |
), |
||||
), |
textAlign: pw.TextAlign.center, |
||||
], |
|
||||
), |
), |
||||
), |
), |
||||
|
|
||||
pw.SizedBox(height: 1), |
pw.SizedBox(height: 1), |
||||
|
|
||||
|
// DATE CENTRÉE |
||||
pw.Container( |
pw.Container( |
||||
width: double.infinity, |
width: double.infinity, |
||||
child: pw.Row( |
child: pw.Text( |
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
'Date: ${_formatDate(dateTime)} ${_formatTime(dateTime)}', |
||||
children: [ |
|
||||
pw.Text( |
|
||||
_formatDate(dateTime), |
|
||||
style: pw.TextStyle(fontSize: smallSize), |
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), |
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 |
// Ligne de séparation |
||||
pw.Container( |
pw.Container( |
||||
@ -179,38 +217,21 @@ class PlatformPrintService { |
|||||
|
|
||||
pw.SizedBox(height: 2), |
pw.SizedBox(height: 2), |
||||
|
|
||||
// Articles (format très compact) |
// EN-TÊTE DES ARTICLES |
||||
...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 |
|
||||
pw.Container( |
pw.Container( |
||||
width: double.infinity, |
width: double.infinity, |
||||
child: pw.Row( |
child: pw.Row( |
||||
mainAxisAlignment: |
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
||||
pw.MainAxisAlignment.spaceBetween, |
|
||||
children: [ |
children: [ |
||||
pw.Text( |
pw.Text( |
||||
'${item.quantite}x ${item.prixUnitaire.toStringAsFixed(2)}MGA', |
'Qte Designation', |
||||
style: pw.TextStyle(fontSize: smallSize), |
style: pw.TextStyle( |
||||
|
fontSize: bodySize, |
||||
|
fontWeight: pw.FontWeight.bold, |
||||
|
), |
||||
), |
), |
||||
pw.Text( |
pw.Text( |
||||
'${(item.prixUnitaire * item.quantite).toStringAsFixed(2)}MGA', |
'Prix', |
||||
style: pw.TextStyle( |
style: pw.TextStyle( |
||||
fontSize: bodySize, |
fontSize: bodySize, |
||||
fontWeight: pw.FontWeight.bold, |
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), |
pw.SizedBox(height: 2), |
||||
|
|
||||
// Total |
// TOTAL |
||||
pw.Container( |
pw.Container( |
||||
width: double.infinity, |
width: double.infinity, |
||||
child: pw.Row( |
child: pw.Row( |
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
||||
children: [ |
children: [ |
||||
pw.Text( |
pw.Text( |
||||
'TOTAL', |
'Total:', |
||||
style: pw.TextStyle( |
style: pw.TextStyle( |
||||
fontSize: titleSize, |
fontSize: titleSize, |
||||
fontWeight: pw.FontWeight.bold, |
fontWeight: pw.FontWeight.bold, |
||||
), |
), |
||||
), |
), |
||||
pw.Text( |
pw.Text( |
||||
'${commande.totalTtc.toStringAsFixed(2)}MGA', |
'${NumberFormat("#,##0.00", "fr_FR").format(commande.totalTtc)}AR', |
||||
style: pw.TextStyle( |
style: pw.TextStyle( |
||||
fontSize: titleSize, |
fontSize: titleSize, |
||||
fontWeight: pw.FontWeight.bold, |
fontWeight: pw.FontWeight.bold, |
||||
@ -260,34 +311,13 @@ class PlatformPrintService { |
|||||
), |
), |
||||
), |
), |
||||
|
|
||||
pw.SizedBox(height: 3), |
pw.SizedBox(height: 4), |
||||
|
|
||||
// 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), |
|
||||
|
|
||||
// Message de remerciement |
// MESSAGE FINAL CENTRÉ |
||||
pw.Container( |
pw.Container( |
||||
width: double.infinity, |
width: double.infinity, |
||||
child: pw.Text( |
child: pw.Text( |
||||
'Merci de votre visite !', |
'Merci et a bientot !', |
||||
style: pw.TextStyle( |
style: pw.TextStyle( |
||||
fontSize: bodySize, |
fontSize: bodySize, |
||||
fontStyle: pw.FontStyle.italic, |
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), |
pw.SizedBox(height: 4), |
||||
|
|
||||
// Ligne de découpe |
// Ligne de découpe |
||||
@ -338,7 +347,8 @@ class PlatformPrintService { |
|||||
); |
); |
||||
|
|
||||
return pdf.save(); |
return pdf.save(); |
||||
} |
} |
||||
|
|
||||
|
|
||||
// Imprimer ticket 58mm |
// Imprimer ticket 58mm |
||||
static Future<bool> printTicket({ |
static Future<bool> printTicket({ |
||||
@ -399,20 +409,24 @@ class PlatformPrintService { |
|||||
} |
} |
||||
|
|
||||
final fileName = |
final fileName = |
||||
'Ticket_58mm_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; |
'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; |
||||
final file = File('${directory.path}/$fileName'); |
final file = File('${directory.path}/$fileName'); |
||||
|
|
||||
await file.writeAsBytes(pdfData); |
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( |
await Share.shareXFiles( |
||||
[XFile(file.path)], |
[XFile(file.path)], |
||||
subject: 'Ticket ${commande.numeroCommande}', |
subject: 'Facture ${commande.numeroCommande}', |
||||
text: 'Ticket de caisse 58mm', |
text: 'Facture de restaurant', |
||||
); |
); |
||||
|
} |
||||
|
|
||||
return true; |
return true; |
||||
} catch (e) { |
} catch (e) { |
||||
print('Erreur sauvegarde 58mm: $e'); |
print('Erreur sauvegarde: $e'); |
||||
return false; |
return false; |
||||
} |
} |
||||
} |
} |
||||
@ -435,7 +449,7 @@ class PlatformPrintService { |
|||||
return await printTicket(commande: commande, paymentMethod: paymentMethod); |
return await printTicket(commande: commande, paymentMethod: paymentMethod); |
||||
} |
} |
||||
|
|
||||
// Utilitaires de formatageπ |
// Utilitaires de formatage |
||||
static String _formatDate(DateTime dateTime) { |
static String _formatDate(DateTime dateTime) { |
||||
return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}'; |
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