|
|
|
@ -1,188 +1,290 @@ |
|
|
|
// services/pdf_service.dart |
|
|
|
// services/platform_print_service.dart |
|
|
|
import 'dart:io'; |
|
|
|
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/widgets.dart' as pw; |
|
|
|
import 'package:printing/printing.dart'; |
|
|
|
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'; |
|
|
|
|
|
|
|
class PdfService { |
|
|
|
static Future<Uint8List> generateFacturePdf({ |
|
|
|
class PlatformPrintService { |
|
|
|
// 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 String paymentMethod, |
|
|
|
}) async { |
|
|
|
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 = { |
|
|
|
'nom': 'RESTAURANT', |
|
|
|
'adresse': 'Moramanga, Antananarivo', |
|
|
|
'contact': '+261 34 12 34 56', |
|
|
|
'adresse': '123 Rue de la Paix', |
|
|
|
'ville': '75000 PARIS', |
|
|
|
'contact': '01.23.45.67.89', |
|
|
|
'email': 'contact@restaurant.fr', |
|
|
|
}; |
|
|
|
|
|
|
|
// Générer numéro de facture |
|
|
|
final factureNumber = |
|
|
|
'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; |
|
|
|
'T${DateTime.now().millisecondsSinceEpoch.toString().substring(8)}'; |
|
|
|
final dateTime = DateTime.now(); |
|
|
|
|
|
|
|
pdf.addPage( |
|
|
|
pw.Page( |
|
|
|
pageFormat: PdfPageFormat.a4, |
|
|
|
margin: const pw.EdgeInsets.all(32), |
|
|
|
pageFormat: ticket58mmFormat, |
|
|
|
build: (pw.Context context) { |
|
|
|
return pw.Column( |
|
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
// En-tête Restaurant |
|
|
|
pw.Center( |
|
|
|
child: pw.Column( |
|
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center, |
|
|
|
children: [ |
|
|
|
// En-tête Restaurant (centré et compact) |
|
|
|
pw.Text( |
|
|
|
restaurantInfo['nom']!, |
|
|
|
style: pw.TextStyle( |
|
|
|
fontSize: 24, |
|
|
|
fontSize: titleSize, |
|
|
|
fontWeight: pw.FontWeight.bold, |
|
|
|
), |
|
|
|
textAlign: pw.TextAlign.center, |
|
|
|
), |
|
|
|
pw.SizedBox(height: 8), |
|
|
|
|
|
|
|
pw.SizedBox(height: 1), |
|
|
|
|
|
|
|
pw.Text( |
|
|
|
'Adresse: ${restaurantInfo['adresse']}', |
|
|
|
style: const pw.TextStyle(fontSize: 12), |
|
|
|
restaurantInfo['adresse']!, |
|
|
|
style: pw.TextStyle(fontSize: smallSize), |
|
|
|
textAlign: pw.TextAlign.center, |
|
|
|
), |
|
|
|
|
|
|
|
pw.Text( |
|
|
|
'Contact: ${restaurantInfo['contact']}', |
|
|
|
style: const pw.TextStyle(fontSize: 12), |
|
|
|
restaurantInfo['ville']!, |
|
|
|
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 |
|
|
|
pw.Center( |
|
|
|
child: pw.Column( |
|
|
|
// Informations ticket |
|
|
|
pw.Row( |
|
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
|
|
|
children: [ |
|
|
|
pw.Text( |
|
|
|
'Facture n° $factureNumber', |
|
|
|
'Ticket: $factureNumber', |
|
|
|
style: pw.TextStyle( |
|
|
|
fontSize: 14, |
|
|
|
fontSize: bodySize, |
|
|
|
fontWeight: pw.FontWeight.bold, |
|
|
|
), |
|
|
|
), |
|
|
|
pw.SizedBox(height: 4), |
|
|
|
pw.Text( |
|
|
|
'Date: ${_formatDateTime(dateTime)}', |
|
|
|
style: const pw.TextStyle(fontSize: 12), |
|
|
|
// pw.Text( |
|
|
|
// 'Table: ${commande.tableName}', |
|
|
|
// style: pw.TextStyle(fontSize: bodySize), |
|
|
|
// ), |
|
|
|
], |
|
|
|
), |
|
|
|
pw.SizedBox(height: 4), |
|
|
|
|
|
|
|
pw.SizedBox(height: 1), |
|
|
|
|
|
|
|
pw.Row( |
|
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
|
|
|
children: [ |
|
|
|
pw.Text( |
|
|
|
'Table: ${commande.numeroCommande}', |
|
|
|
style: const pw.TextStyle(fontSize: 12), |
|
|
|
_formatDate(dateTime), |
|
|
|
style: pw.TextStyle(fontSize: smallSize), |
|
|
|
), |
|
|
|
pw.SizedBox(height: 4), |
|
|
|
pw.Text( |
|
|
|
'Paiement: ${_getPaymentMethodText(paymentMethod)}', |
|
|
|
style: const pw.TextStyle(fontSize: 12), |
|
|
|
_formatTime(dateTime), |
|
|
|
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 |
|
|
|
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), |
|
|
|
}, |
|
|
|
// Articles (format très compact) |
|
|
|
...commande.items |
|
|
|
.map( |
|
|
|
(item) => pw.Container( |
|
|
|
margin: const pw.EdgeInsets.only(bottom: 1), |
|
|
|
child: pw.Column( |
|
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
// En-tête du tableau |
|
|
|
pw.TableRow( |
|
|
|
decoration: const pw.BoxDecoration( |
|
|
|
color: PdfColors.grey100, |
|
|
|
// Nom du plat |
|
|
|
pw.Text( |
|
|
|
"NOMPLAT", |
|
|
|
style: pw.TextStyle(fontSize: bodySize), |
|
|
|
maxLines: 2, |
|
|
|
), |
|
|
|
|
|
|
|
// Quantité, prix unitaire et total sur une ligne |
|
|
|
pw.Row( |
|
|
|
mainAxisAlignment: |
|
|
|
pw.MainAxisAlignment.spaceBetween, |
|
|
|
children: [ |
|
|
|
pw.Padding( |
|
|
|
padding: const pw.EdgeInsets.all(8), |
|
|
|
child: pw.Text( |
|
|
|
'Qté Désignation', |
|
|
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold), |
|
|
|
), |
|
|
|
pw.Text( |
|
|
|
'${item.quantite}x ${item.prixUnitaire.toStringAsFixed(2)}€', |
|
|
|
style: pw.TextStyle(fontSize: smallSize), |
|
|
|
), |
|
|
|
pw.Padding( |
|
|
|
padding: const pw.EdgeInsets.all(8), |
|
|
|
child: pw.Text( |
|
|
|
'Prix', |
|
|
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold), |
|
|
|
textAlign: pw.TextAlign.right, |
|
|
|
pw.Text( |
|
|
|
'${(item.prixUnitaire * item.quantite).toStringAsFixed(2)}€', |
|
|
|
style: pw.TextStyle( |
|
|
|
fontSize: bodySize, |
|
|
|
fontWeight: pw.FontWeight.bold, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
// 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: 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 |
|
|
|
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)} €', |
|
|
|
pw.Row( |
|
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, |
|
|
|
children: [ |
|
|
|
pw.Text( |
|
|
|
'TOTAL', |
|
|
|
style: pw.TextStyle( |
|
|
|
fontSize: titleSize, |
|
|
|
fontWeight: pw.FontWeight.bold, |
|
|
|
), |
|
|
|
), |
|
|
|
pw.Text( |
|
|
|
'${commande.totalTtc.toStringAsFixed(2)}€', |
|
|
|
style: pw.TextStyle( |
|
|
|
fontSize: 16, |
|
|
|
fontSize: titleSize, |
|
|
|
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 |
|
|
|
pw.Center( |
|
|
|
child: pw.Text( |
|
|
|
'Merci et à bientôt !', |
|
|
|
pw.Text( |
|
|
|
'Merci de votre visite !', |
|
|
|
style: pw.TextStyle( |
|
|
|
fontSize: 12, |
|
|
|
fontSize: bodySize, |
|
|
|
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(); |
|
|
|
} |
|
|
|
|
|
|
|
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({ |
|
|
|
// Imprimer ticket 58mm |
|
|
|
static Future<bool> printTicket({ |
|
|
|
required CommandeDetail commande, |
|
|
|
required String paymentMethod, |
|
|
|
}) async { |
|
|
|
try { |
|
|
|
final pdfData = await generateFacturePdf( |
|
|
|
final hasPermission = await _checkPermissions(); |
|
|
|
if (!hasPermission) { |
|
|
|
throw Exception('Permissions requises pour l\'impression'); |
|
|
|
} |
|
|
|
|
|
|
|
final pdfData = await _generate58mmTicketPdf( |
|
|
|
commande: commande, |
|
|
|
paymentMethod: paymentMethod, |
|
|
|
); |
|
|
|
|
|
|
|
final fileName = |
|
|
|
'Ticket_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}'; |
|
|
|
|
|
|
|
await Printing.layoutPdf( |
|
|
|
onLayout: (PdfPageFormat format) async => pdfData, |
|
|
|
name: |
|
|
|
'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}', |
|
|
|
name: fileName, |
|
|
|
format: ticket58mmFormat, |
|
|
|
); |
|
|
|
|
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
print('Erreur impression: $e'); |
|
|
|
print('Erreur impression 58mm: $e'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Sauvegarder et partager le PDF |
|
|
|
static Future<bool> saveAndShareFacture({ |
|
|
|
// Sauvegarder ticket 58mm |
|
|
|
static Future<bool> saveTicketPdf({ |
|
|
|
required CommandeDetail commande, |
|
|
|
required String paymentMethod, |
|
|
|
}) async { |
|
|
|
try { |
|
|
|
final pdfData = await generateFacturePdf( |
|
|
|
final hasPermission = await _checkPermissions(); |
|
|
|
if (!hasPermission) return false; |
|
|
|
|
|
|
|
final pdfData = await _generate58mmTicketPdf( |
|
|
|
commande: commande, |
|
|
|
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 = |
|
|
|
'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; |
|
|
|
'Ticket_58mm_${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', |
|
|
|
subject: 'Ticket ${commande.numeroCommande}', |
|
|
|
text: 'Ticket de caisse 58mm', |
|
|
|
); |
|
|
|
|
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
print('Erreur sauvegarde/partage: $e'); |
|
|
|
print('Erreur sauvegarde 58mm: $e'); |
|
|
|
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é'; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|