From f65ab1e397d4abcada48b675f0621093a91a2a6c Mon Sep 17 00:00:00 2001 From: Stephane Date: Sun, 3 Aug 2025 16:09:14 +0300 Subject: [PATCH] impression --- lib/pages/facture_screen.dart | 198 +++++++++++--- lib/services/pdf_service.dart | 475 ++++++++++++++++++++++------------ pubspec.yaml | 1 + 3 files changed, 478 insertions(+), 196 deletions(-) diff --git a/lib/pages/facture_screen.dart b/lib/pages/facture_screen.dart index 29a34bd..a647305 100644 --- a/lib/pages/facture_screen.dart +++ b/lib/pages/facture_screen.dart @@ -1,4 +1,6 @@ // pages/facture_screen.dart +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/command_detail.dart'; @@ -255,51 +257,183 @@ class _FactureScreenState extends State { } void _printReceipt() async { + bool isPrinting; + setState(() => isPrinting = true); + try { - HapticFeedback.lightImpact(); + // Vérifier si l'impression est disponible + final canPrint = await PlatformPrintService.canPrint(); - final success = await PdfService.printFacture( - commande: widget.commande, - paymentMethod: widget.paymentMethod, - ); + if (!canPrint) { + // Si pas d'imprimante, proposer seulement la sauvegarde + _showSaveOnlyDialog(); + return; + } - if (success) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Row( + // Afficher les options d'impression + final action = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Row( children: [ - Icon(Icons.check_circle, color: Colors.white), - SizedBox(width: 8), - Text('Facture envoyée à l\'impression'), + Icon(Icons.print, color: Theme.of(context).primaryColor), + const SizedBox(width: 8), + const Text('Options d\'impression'), ], ), - backgroundColor: const Color(0xFF28A745), - duration: const Duration(seconds: 2), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Plateforme: ${_getPlatformName()}', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + const SizedBox(height: 16), + const Text('Que souhaitez-vous faire ?'), + ], ), - margin: const EdgeInsets.all(16), - behavior: SnackBarBehavior.floating, - ), + actions: [ + TextButton.icon( + onPressed: () => Navigator.of(context).pop('print'), + icon: const Icon(Icons.print), + label: const Text('Imprimer'), + ), + TextButton.icon( + onPressed: () => Navigator.of(context).pop('save'), + icon: const Icon(Icons.save), + label: const Text('Sauvegarder PDF'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop('cancel'), + child: const Text('Annuler'), + ), + ], + ); + }, + ); + + if (action == null || action == 'cancel') return; + + HapticFeedback.lightImpact(); + bool success = false; + + if (action == 'print') { + success = await PlatformPrintService.printFacture( + commande: widget.commande, + paymentMethod: widget.paymentMethod, ); + } else if (action == 'save') { + success = await PlatformPrintService.saveFacturePdf( + commande: widget.commande, + paymentMethod: widget.paymentMethod, + ); + } - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - Navigator.of(context).popUntil((route) => route.isFirst); - } - }); + if (success) { + _showSuccessMessage( + action == 'print' + ? 'Facture envoyée à l\'imprimante ${_getPlatformName()}' + : 'PDF sauvegardé et partagé', + ); + } else { + _showErrorMessage( + 'Erreur lors de ${action == 'print' ? 'l\'impression' : 'la sauvegarde'}', + ); } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Erreur impression: $e'), - backgroundColor: Colors.red, - ), - ); + _showErrorMessage('Erreur: $e'); } finally { - if (mounted) { - Navigator.of(context).pop(); + setState(() => isPrinting = false); + } + } + + void _showSaveOnlyDialog() async { + final shouldSave = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Aucune imprimante'), + content: const Text( + 'Aucune imprimante détectée. Voulez-vous sauvegarder le PDF ?', + ), + actions: [ + TextButton.icon( + onPressed: () => Navigator.of(context).pop(true), + icon: const Icon(Icons.save), + label: const Text('Sauvegarder'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Annuler'), + ), + ], + ); + }, + ); + + if (shouldSave == true) { + final success = await PlatformPrintService.saveFacturePdf( + commande: widget.commande, + paymentMethod: widget.paymentMethod, + ); + + if (success) { + _showSuccessMessage('PDF sauvegardé avec succès'); + } else { + _showErrorMessage('Erreur lors de la sauvegarde'); } } } + + void _showSuccessMessage(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.white), + const SizedBox(width: 8), + Expanded(child: Text(message)), + ], + ), + backgroundColor: const Color(0xFF28A745), + duration: const Duration(seconds: 3), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + margin: const EdgeInsets.all(16), + behavior: SnackBarBehavior.floating, + ), + ); + + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + Navigator.of(context).popUntil((route) => route.isFirst); + } + }); + } + + void _showErrorMessage(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.error, color: Colors.white), + const SizedBox(width: 8), + Expanded(child: Text(message)), + ], + ), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + margin: const EdgeInsets.all(16), + behavior: SnackBarBehavior.floating, + ), + ); + } + + String _getPlatformName() { + if (Platform.isAndroid) return 'Android'; + if (Platform.isMacOS) return 'macOS'; + if (Platform.isWindows) return 'Windows'; + return 'cette plateforme'; + } } diff --git a/lib/services/pdf_service.dart b/lib/services/pdf_service.dart index 139a9c9..06a60df 100644 --- a/lib/services/pdf_service.dart +++ b/lib/services/pdf_service.dart @@ -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 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 _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 canPrint() async { + try { + return await Printing.info().then((info) => info.canPrint); + } catch (e) { + return false; + } + } + + // Générer PDF optimisé pour 58mm + static Future _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, + crossAxisAlignment: pw.CrossAxisAlignment.center, 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), - ), - ], + // En-tête Restaurant (centré et compact) + pw.Text( + restaurantInfo['nom']!, + style: pw.TextStyle( + fontSize: titleSize, + fontWeight: pw.FontWeight.bold, ), + textAlign: pw.TextAlign.center, ), - 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: 1), + + pw.Text( + restaurantInfo['adresse']!, + style: pw.TextStyle(fontSize: smallSize), + textAlign: pw.TextAlign.center, + ), + + pw.Text( + 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), - // 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), - }, + // Informations ticket + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ - // En-tête du tableau - pw.TableRow( - decoration: const pw.BoxDecoration( - color: PdfColors.grey100, + pw.Text( + 'Ticket: $factureNumber', + style: pw.TextStyle( + fontSize: bodySize, + fontWeight: pw.FontWeight.bold, ), - 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.Text( + // 'Table: ${commande.tableName}', + // style: pw.TextStyle(fontSize: bodySize), + // ), + ], + ), + + pw.SizedBox(height: 1), + + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text( + _formatDate(dateTime), + style: pw.TextStyle(fontSize: smallSize), + ), + pw.Text( + _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: 2), + + // 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: [ + // 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.Text( + '${item.quantite}x ${item.prixUnitaire.toStringAsFixed(2)}€', + style: pw.TextStyle(fontSize: smallSize), ), - ), - pw.Padding( - padding: const pw.EdgeInsets.all(8), - child: pw.Text( - '${item.prixUnitaire.toStringAsFixed(2)} €', - textAlign: pw.TextAlign.right, + pw.Text( + '${(item.prixUnitaire * item.quantite).toStringAsFixed(2)}€', + style: pw.TextStyle( + fontSize: bodySize, + fontWeight: pw.FontWeight.bold, + ), ), - ), - ], - ), - ) - .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 - 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, + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text( + 'TOTAL', + style: pw.TextStyle( + fontSize: titleSize, + fontWeight: pw.FontWeight.bold, + ), ), - child: pw.Text( - 'Total: ${commande.totalTtc.toStringAsFixed(2)} €', + pw.Text( + '${commande.totalTtc.toStringAsFixed(2)}€', style: pw.TextStyle( - fontSize: 16, + fontSize: titleSize, fontWeight: pw.FontWeight.bold, ), ), - ), + ], ), - pw.Spacer(), + pw.SizedBox(height: 3), + + // Mode de paiement + pw.Text( + 'Paiement: ${_getPaymentMethodText(paymentMethod)}', + 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 - pw.Center( - child: pw.Text( - 'Merci et à bientôt !', - style: pw.TextStyle( - fontSize: 12, - fontStyle: pw.FontStyle.italic, - ), + pw.Text( + 'Merci de votre visite !', + style: pw.TextStyle( + 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 printFacture({ + // Imprimer ticket 58mm + static Future 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 saveAndShareFacture({ + // Sauvegarder ticket 58mm + static Future 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 saveFacturePdf({ + required CommandeDetail commande, + required String paymentMethod, + }) async { + return await saveTicketPdf( + commande: commande, + paymentMethod: paymentMethod, + ); + } + + static Future 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é'; + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index f1f324a..0124d0c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: printing: ^5.11.1 path_provider: ^2.1.1 share_plus: ^7.2.1 + permission_handler: ^11.1.0 # Dépendances de développement/test dev_dependencies: