3 changed files with 478 additions and 196 deletions
@ -1,188 +1,290 @@ |
|||||
// services/pdf_service.dart |
// services/platform_print_service.dart |
||||
import 'dart:io'; |
import 'dart:io'; |
||||
import 'dart:typed_data'; |
import 'dart:typed_data'; |
||||
|
import 'package:flutter/foundation.dart'; |
||||
|
import 'package:flutter/services.dart'; |
||||
|
import 'package:itrimobe/models/command_detail.dart'; |
||||
import 'package:pdf/pdf.dart'; |
import 'package:pdf/pdf.dart'; |
||||
import 'package:pdf/widgets.dart' as pw; |
import 'package:pdf/widgets.dart' as pw; |
||||
import 'package:printing/printing.dart'; |
import 'package:printing/printing.dart'; |
||||
import 'package:path_provider/path_provider.dart'; |
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 '../models/command_detail.dart'; |
import '../models/command_detail.dart'; |
||||
|
|
||||
class PdfService { |
class PlatformPrintService { |
||||
static Future<Uint8List> generateFacturePdf({ |
// Format spécifique 58mm pour petites imprimantes |
||||
|
static const PdfPageFormat ticket58mmFormat = PdfPageFormat( |
||||
|
58 * PdfPageFormat.mm, // Largeur exacte 58mm |
||||
|
double.infinity, // Hauteur automatique |
||||
|
marginLeft: 1 * PdfPageFormat.mm, |
||||
|
marginRight: 1 * PdfPageFormat.mm, |
||||
|
marginTop: 2 * PdfPageFormat.mm, |
||||
|
marginBottom: 2 * PdfPageFormat.mm, |
||||
|
); |
||||
|
|
||||
|
// Vérifier les permissions |
||||
|
static Future<bool> _checkPermissions() async { |
||||
|
if (!Platform.isAndroid) return true; |
||||
|
|
||||
|
final storagePermission = await Permission.storage.request(); |
||||
|
return storagePermission == PermissionStatus.granted; |
||||
|
} |
||||
|
|
||||
|
// Vérifier si l'impression est possible |
||||
|
static Future<bool> canPrint() async { |
||||
|
try { |
||||
|
return await Printing.info().then((info) => info.canPrint); |
||||
|
} catch (e) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Générer PDF optimisé pour 58mm |
||||
|
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(); |
||||
|
|
||||
// Informations du restaurant |
// Configuration pour 58mm (très petit) |
||||
|
const double titleSize = 9; |
||||
|
const double headerSize = 8; |
||||
|
const double bodySize = 7; |
||||
|
const double smallSize = 6; |
||||
|
const double lineHeight = 1.2; |
||||
|
|
||||
final restaurantInfo = { |
final restaurantInfo = { |
||||
'nom': 'RESTAURANT', |
'nom': 'RESTAURANT', |
||||
'adresse': 'Moramanga, Antananarivo', |
'adresse': '123 Rue de la Paix', |
||||
'contact': '+261 34 12 34 56', |
'ville': '75000 PARIS', |
||||
|
'contact': '01.23.45.67.89', |
||||
|
'email': '[email protected]', |
||||
}; |
}; |
||||
|
|
||||
// Générer numéro de facture |
|
||||
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(); |
||||
|
|
||||
pdf.addPage( |
pdf.addPage( |
||||
pw.Page( |
pw.Page( |
||||
pageFormat: PdfPageFormat.a4, |
pageFormat: ticket58mmFormat, |
||||
margin: const pw.EdgeInsets.all(32), |
|
||||
build: (pw.Context context) { |
build: (pw.Context context) { |
||||
return pw.Column( |
return pw.Column( |
||||
crossAxisAlignment: pw.CrossAxisAlignment.start, |
crossAxisAlignment: pw.CrossAxisAlignment.center, |
||||
children: [ |
|
||||
// En-tête Restaurant |
|
||||
pw.Center( |
|
||||
child: pw.Column( |
|
||||
children: [ |
children: [ |
||||
|
// En-tête Restaurant (centré et compact) |
||||
pw.Text( |
pw.Text( |
||||
restaurantInfo['nom']!, |
restaurantInfo['nom']!, |
||||
style: pw.TextStyle( |
style: pw.TextStyle( |
||||
fontSize: 24, |
fontSize: titleSize, |
||||
fontWeight: pw.FontWeight.bold, |
fontWeight: pw.FontWeight.bold, |
||||
), |
), |
||||
|
textAlign: pw.TextAlign.center, |
||||
), |
), |
||||
pw.SizedBox(height: 8), |
|
||||
|
pw.SizedBox(height: 1), |
||||
|
|
||||
pw.Text( |
pw.Text( |
||||
'Adresse: ${restaurantInfo['adresse']}', |
restaurantInfo['adresse']!, |
||||
style: const pw.TextStyle(fontSize: 12), |
style: pw.TextStyle(fontSize: smallSize), |
||||
|
textAlign: pw.TextAlign.center, |
||||
), |
), |
||||
|
|
||||
pw.Text( |
pw.Text( |
||||
'Contact: ${restaurantInfo['contact']}', |
restaurantInfo['ville']!, |
||||
style: const pw.TextStyle(fontSize: 12), |
style: pw.TextStyle(fontSize: smallSize), |
||||
|
textAlign: pw.TextAlign.center, |
||||
), |
), |
||||
], |
|
||||
|
pw.Text( |
||||
|
'Tel: ${restaurantInfo['contact']!}', |
||||
|
style: pw.TextStyle(fontSize: smallSize), |
||||
|
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: 30), |
pw.SizedBox(height: 2), |
||||
|
|
||||
// Informations facture |
// Informations ticket |
||||
pw.Center( |
pw.Row( |
||||
child: pw.Column( |
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
||||
children: [ |
children: [ |
||||
pw.Text( |
pw.Text( |
||||
'Facture n° $factureNumber', |
'Ticket: $factureNumber', |
||||
style: pw.TextStyle( |
style: pw.TextStyle( |
||||
fontSize: 14, |
fontSize: bodySize, |
||||
fontWeight: pw.FontWeight.bold, |
fontWeight: pw.FontWeight.bold, |
||||
), |
), |
||||
), |
), |
||||
pw.SizedBox(height: 4), |
// pw.Text( |
||||
pw.Text( |
// 'Table: ${commande.tableName}', |
||||
'Date: ${_formatDateTime(dateTime)}', |
// style: pw.TextStyle(fontSize: bodySize), |
||||
style: const pw.TextStyle(fontSize: 12), |
// ), |
||||
|
], |
||||
), |
), |
||||
pw.SizedBox(height: 4), |
|
||||
|
pw.SizedBox(height: 1), |
||||
|
|
||||
|
pw.Row( |
||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
pw.Text( |
pw.Text( |
||||
'Table: ${commande.numeroCommande}', |
_formatDate(dateTime), |
||||
style: const pw.TextStyle(fontSize: 12), |
style: pw.TextStyle(fontSize: smallSize), |
||||
), |
), |
||||
pw.SizedBox(height: 4), |
|
||||
pw.Text( |
pw.Text( |
||||
'Paiement: ${_getPaymentMethodText(paymentMethod)}', |
_formatTime(dateTime), |
||||
style: const pw.TextStyle(fontSize: 12), |
style: pw.TextStyle(fontSize: smallSize), |
||||
), |
), |
||||
], |
], |
||||
), |
), |
||||
|
|
||||
|
pw.SizedBox(height: 2), |
||||
|
|
||||
|
// Ligne de séparation |
||||
|
pw.Container( |
||||
|
width: double.infinity, |
||||
|
height: 0.5, |
||||
|
color: PdfColors.black, |
||||
), |
), |
||||
|
|
||||
pw.SizedBox(height: 30), |
pw.SizedBox(height: 2), |
||||
|
|
||||
// Tableau des articles |
// Articles (format très compact) |
||||
pw.Table( |
...commande.items |
||||
border: pw.TableBorder.all(color: PdfColors.grey300), |
.map( |
||||
columnWidths: { |
(item) => pw.Container( |
||||
0: const pw.FlexColumnWidth(3), |
margin: const pw.EdgeInsets.only(bottom: 1), |
||||
1: const pw.FlexColumnWidth(1), |
child: pw.Column( |
||||
2: const pw.FlexColumnWidth(1), |
crossAxisAlignment: pw.CrossAxisAlignment.start, |
||||
}, |
|
||||
children: [ |
children: [ |
||||
// En-tête du tableau |
// Nom du plat |
||||
pw.TableRow( |
pw.Text( |
||||
decoration: const pw.BoxDecoration( |
"NOMPLAT", |
||||
color: PdfColors.grey100, |
style: pw.TextStyle(fontSize: bodySize), |
||||
|
maxLines: 2, |
||||
), |
), |
||||
|
|
||||
|
// Quantité, prix unitaire et total sur une ligne |
||||
|
pw.Row( |
||||
|
mainAxisAlignment: |
||||
|
pw.MainAxisAlignment.spaceBetween, |
||||
children: [ |
children: [ |
||||
pw.Padding( |
pw.Text( |
||||
padding: const pw.EdgeInsets.all(8), |
'${item.quantite}x ${item.prixUnitaire.toStringAsFixed(2)}€', |
||||
child: pw.Text( |
style: pw.TextStyle(fontSize: smallSize), |
||||
'Qté Désignation', |
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold), |
|
||||
), |
|
||||
), |
), |
||||
pw.Padding( |
pw.Text( |
||||
padding: const pw.EdgeInsets.all(8), |
'${(item.prixUnitaire * item.quantite).toStringAsFixed(2)}€', |
||||
child: pw.Text( |
style: pw.TextStyle( |
||||
'Prix', |
fontSize: bodySize, |
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold), |
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(), |
.toList(), |
||||
], |
|
||||
|
pw.SizedBox(height: 2), |
||||
|
|
||||
|
// Ligne de séparation |
||||
|
pw.Container( |
||||
|
width: double.infinity, |
||||
|
height: 0.5, |
||||
|
color: PdfColors.black, |
||||
), |
), |
||||
|
|
||||
pw.SizedBox(height: 20), |
pw.SizedBox(height: 2), |
||||
|
|
||||
// Total |
// Total |
||||
pw.Container( |
pw.Row( |
||||
alignment: pw.Alignment.centerRight, |
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
||||
child: pw.Container( |
children: [ |
||||
padding: const pw.EdgeInsets.all(12), |
pw.Text( |
||||
decoration: pw.BoxDecoration( |
'TOTAL', |
||||
border: pw.Border.all(color: PdfColors.grey400), |
style: pw.TextStyle( |
||||
color: PdfColors.grey50, |
fontSize: titleSize, |
||||
), |
fontWeight: pw.FontWeight.bold, |
||||
child: pw.Text( |
), |
||||
'Total: ${commande.totalTtc.toStringAsFixed(2)} €', |
), |
||||
|
pw.Text( |
||||
|
'${commande.totalTtc.toStringAsFixed(2)}€', |
||||
style: pw.TextStyle( |
style: pw.TextStyle( |
||||
fontSize: 16, |
fontSize: titleSize, |
||||
fontWeight: pw.FontWeight.bold, |
fontWeight: pw.FontWeight.bold, |
||||
), |
), |
||||
), |
), |
||||
|
], |
||||
), |
), |
||||
|
|
||||
|
pw.SizedBox(height: 3), |
||||
|
|
||||
|
// Mode de paiement |
||||
|
pw.Text( |
||||
|
'Paiement: ${_getPaymentMethodText(paymentMethod)}', |
||||
|
style: pw.TextStyle(fontSize: bodySize), |
||||
|
textAlign: pw.TextAlign.center, |
||||
), |
), |
||||
|
|
||||
pw.Spacer(), |
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 de remerciement |
||||
pw.Center( |
pw.Text( |
||||
child: pw.Text( |
'Merci de votre visite !', |
||||
'Merci et à bientôt !', |
|
||||
style: pw.TextStyle( |
style: pw.TextStyle( |
||||
fontSize: 12, |
fontSize: bodySize, |
||||
fontStyle: pw.FontStyle.italic, |
fontStyle: pw.FontStyle.italic, |
||||
), |
), |
||||
|
textAlign: pw.TextAlign.center, |
||||
|
), |
||||
|
|
||||
|
pw.Text( |
||||
|
'A bientôt !', |
||||
|
style: pw.TextStyle(fontSize: smallSize), |
||||
|
textAlign: pw.TextAlign.center, |
||||
|
), |
||||
|
|
||||
|
pw.SizedBox(height: 3), |
||||
|
|
||||
|
// Code de suivi (optionnel) |
||||
|
pw.Text( |
||||
|
'Code: ${factureNumber}', |
||||
|
style: pw.TextStyle(fontSize: smallSize), |
||||
|
textAlign: pw.TextAlign.center, |
||||
), |
), |
||||
|
|
||||
|
pw.SizedBox(height: 4), |
||||
|
|
||||
|
// Ligne de découpe |
||||
|
pw.Text( |
||||
|
'- - - - - - - - - - - - - - - -', |
||||
|
style: pw.TextStyle(fontSize: smallSize), |
||||
|
textAlign: pw.TextAlign.center, |
||||
), |
), |
||||
|
|
||||
|
pw.SizedBox(height: 2), |
||||
], |
], |
||||
); |
); |
||||
}, |
}, |
||||
@ -192,75 +294,120 @@ class PdfService { |
|||||
return pdf.save(); |
return pdf.save(); |
||||
} |
} |
||||
|
|
||||
static String _formatDateTime(DateTime dateTime) { |
// Imprimer ticket 58mm |
||||
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 Future<bool> printTicket({ |
||||
} |
|
||||
|
|
||||
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 CommandeDetail commande, |
||||
required String paymentMethod, |
required String paymentMethod, |
||||
}) async { |
}) async { |
||||
try { |
try { |
||||
final pdfData = await generateFacturePdf( |
final hasPermission = await _checkPermissions(); |
||||
|
if (!hasPermission) { |
||||
|
throw Exception('Permissions requises pour l\'impression'); |
||||
|
} |
||||
|
|
||||
|
final pdfData = await _generate58mmTicketPdf( |
||||
commande: commande, |
commande: commande, |
||||
paymentMethod: paymentMethod, |
paymentMethod: paymentMethod, |
||||
); |
); |
||||
|
|
||||
|
final fileName = |
||||
|
'Ticket_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}'; |
||||
|
|
||||
await Printing.layoutPdf( |
await Printing.layoutPdf( |
||||
onLayout: (PdfPageFormat format) async => pdfData, |
onLayout: (PdfPageFormat format) async => pdfData, |
||||
name: |
name: fileName, |
||||
'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}', |
format: ticket58mmFormat, |
||||
); |
); |
||||
|
|
||||
return true; |
return true; |
||||
} catch (e) { |
} catch (e) { |
||||
print('Erreur impression: $e'); |
print('Erreur impression 58mm: $e'); |
||||
return false; |
return false; |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
// Sauvegarder et partager le PDF |
// Sauvegarder ticket 58mm |
||||
static Future<bool> saveAndShareFacture({ |
static Future<bool> saveTicketPdf({ |
||||
required CommandeDetail commande, |
required CommandeDetail commande, |
||||
required String paymentMethod, |
required String paymentMethod, |
||||
}) async { |
}) async { |
||||
try { |
try { |
||||
final pdfData = await generateFacturePdf( |
final hasPermission = await _checkPermissions(); |
||||
|
if (!hasPermission) return false; |
||||
|
|
||||
|
final pdfData = await _generate58mmTicketPdf( |
||||
commande: commande, |
commande: commande, |
||||
paymentMethod: paymentMethod, |
paymentMethod: paymentMethod, |
||||
); |
); |
||||
|
|
||||
final directory = await getApplicationDocumentsDirectory(); |
Directory directory; |
||||
|
if (Platform.isAndroid) { |
||||
|
directory = Directory('/storage/emulated/0/Download'); |
||||
|
if (!directory.existsSync()) { |
||||
|
directory = |
||||
|
await getExternalStorageDirectory() ?? |
||||
|
await getApplicationDocumentsDirectory(); |
||||
|
} |
||||
|
} else { |
||||
|
directory = await getApplicationDocumentsDirectory(); |
||||
|
} |
||||
|
|
||||
final fileName = |
final fileName = |
||||
'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; |
'Ticket_58mm_${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); |
||||
|
|
||||
await Share.shareXFiles( |
await Share.shareXFiles( |
||||
[XFile(file.path)], |
[XFile(file.path)], |
||||
subject: 'Facture ${commande.numeroCommande}', |
subject: 'Ticket ${commande.numeroCommande}', |
||||
text: 'Facture de votre commande au restaurant', |
text: 'Ticket de caisse 58mm', |
||||
); |
); |
||||
|
|
||||
return true; |
return true; |
||||
} catch (e) { |
} catch (e) { |
||||
print('Erreur sauvegarde/partage: $e'); |
print('Erreur sauvegarde 58mm: $e'); |
||||
return false; |
return false; |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
|
// Méthodes pour compatibilité |
||||
|
static Future<bool> saveFacturePdf({ |
||||
|
required CommandeDetail commande, |
||||
|
required String paymentMethod, |
||||
|
}) async { |
||||
|
return await saveTicketPdf( |
||||
|
commande: commande, |
||||
|
paymentMethod: paymentMethod, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
static Future<bool> printFacture({ |
||||
|
required CommandeDetail commande, |
||||
|
required String paymentMethod, |
||||
|
}) async { |
||||
|
return await printTicket(commande: commande, paymentMethod: paymentMethod); |
||||
|
} |
||||
|
|
||||
|
// Utilitaires de formatage |
||||
|
static String _formatDate(DateTime dateTime) { |
||||
|
return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}'; |
||||
|
} |
||||
|
|
||||
|
static String _formatTime(DateTime dateTime) { |
||||
|
return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; |
||||
|
} |
||||
|
|
||||
|
static String _getPaymentMethodText(String method) { |
||||
|
switch (method) { |
||||
|
case 'cash': |
||||
|
return 'Espèces'; |
||||
|
case 'card': |
||||
|
return 'Carte bancaire'; |
||||
|
case 'mobile': |
||||
|
return 'Paiement mobile'; |
||||
|
default: |
||||
|
return 'Non spécifié'; |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue