// 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'; import 'package:intl/intl.dart'; import '../pages/information.dart'; class PlatformPrintService { // Format spécifique 58mm pour petites imprimantes - CENTRÉ POUR L'IMPRESSION static const PdfPageFormat ticket58mmFormat = PdfPageFormat( 48 * PdfPageFormat.mm, // Largeur exacte 58mm double.infinity, // Hauteur automatique 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, ); // 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 - VERSION IDENTIQUE À L'ÉCRAN static Future _generate58mmTicketPdf({ required CommandeDetail commande, required PrintTemplate template, required String paymentMethod, }) async { final pdf = pw.Document(); const double titleSize = 8; const double headerSize = 8; const double bodySize = 7; const double smallSize = 5; const double lineHeight = 1.2; final restauranTitle = template.title ?? 'Nom du restaurant'; final restaurantContent = template.content ?? 'Adresse inconnue'; 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'; } String formatTemplateContent(String content) { return content.replaceAll('\\r\\n', '\n').replaceAll('\\n', '\n'); } pdf.addPage( pw.Page( pageFormat: ticket58mmFormat, margin: const pw.EdgeInsets.all(2), build: (pw.Context context) { return pw.Container( width: double.infinity, child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // TITRE CENTRÉ pw.Container( width: double.infinity, child: pw.Text( restauranTitle, style: pw.TextStyle( fontSize: titleSize, fontWeight: pw.FontWeight.bold, ), textAlign: pw.TextAlign.center, ), ), pw.SizedBox(height: 2), pw.Container( width: double.infinity, margin: const pw.EdgeInsets.only(right: 2), child: pw.Text( formatTemplateContent(restaurantContent), style: pw.TextStyle(fontSize: smallSize), textAlign: pw.TextAlign.left, ), ), pw.SizedBox(height: 2), // FACTURE CENTRÉE pw.Container( width: double.infinity, 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.Text( 'Date: ${_formatDate(dateTime)} ${_formatTime(dateTime)}', style: pw.TextStyle(fontSize: smallSize), textAlign: pw.TextAlign.center, ), ), // 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: 3), // Ligne de séparation pw.Container( width: double.infinity, height: 0.5, color: PdfColors.black, ), pw.SizedBox(height: 2), // EN-TÊTE DES ARTICLES pw.Container( width: double.infinity, child: pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text( 'Qte Designation', style: pw.TextStyle( fontSize: bodySize, fontWeight: pw.FontWeight.bold, ), ), pw.Text( 'Prix', style: pw.TextStyle( fontSize: bodySize, fontWeight: pw.FontWeight.bold, ), ), ], ), ), 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), ), ], ), ), ) .toList(), pw.SizedBox(height: 2), // Ligne de séparation pw.Container( width: double.infinity, height: 0.5, color: PdfColors.black, ), pw.SizedBox(height: 2), // TOTAL pw.Container( width: double.infinity, child: pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text( 'Total:', style: pw.TextStyle( fontSize: titleSize, fontWeight: pw.FontWeight.bold, ), ), pw.Text( '${NumberFormat("#,##0.00", "fr_FR").format(commande.totalTtc)}AR', style: pw.TextStyle( fontSize: titleSize, fontWeight: pw.FontWeight.bold, ), ), ], ), ), pw.SizedBox(height: 4), // MESSAGE FINAL CENTRÉ pw.Container( width: double.infinity, child: pw.Text( 'Merci et a bientot !', style: pw.TextStyle( fontSize: bodySize, fontStyle: pw.FontStyle.italic, ), textAlign: pw.TextAlign.center, ), ), pw.SizedBox(height: 4), // Ligne de découpe pw.Container( width: double.infinity, child: pw.Text( '- - - - - - - - - - - - - - - -', style: pw.TextStyle(fontSize: smallSize), textAlign: pw.TextAlign.center, ), ), pw.SizedBox(height: 2), ], ), ); }, ), ); return pdf.save(); } // Imprimer ticket 58mm static Future printTicket({ required CommandeDetail commande, required PrintTemplate template, required String paymentMethod, }) async { try { final hasPermission = await _checkPermissions(); if (!hasPermission) { throw Exception('Permissions requises pour l\'impression'); } final pdfData = await _generate58mmTicketPdf( commande: commande, template: template, paymentMethod: paymentMethod, ); final fileName = 'Ticket_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}'; await Printing.layoutPdf( onLayout: (PdfPageFormat format) async => pdfData, name: fileName, format: ticket58mmFormat, ); return true; } catch (e) { print('Erreur impression 58mm: $e'); return false; } } // Sauvegarder ticket 58mm static Future saveTicketPdf({ required CommandeDetail commande, required PrintTemplate template, required String paymentMethod, }) async { try { final hasPermission = await _checkPermissions(); if (!hasPermission) return false; final pdfData = await _generate58mmTicketPdf( commande: commande, template: template, paymentMethod: paymentMethod, ); 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'; 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: 'Facture ${commande.numeroCommande}', text: 'Facture de restaurant', ); } return true; } catch (e) { print('Erreur sauvegarde: $e'); return false; } } // Méthodes pour compatibilité static Future saveFacturePdf({ required CommandeDetail commande, required PrintTemplate template, required String paymentMethod, }) async { return await saveTicketPdf( commande: commande, template: template, paymentMethod: paymentMethod, ); } static Future printFacture({ required CommandeDetail commande, required PrintTemplate template, required String paymentMethod, }) async { return await printTicket(commande: commande,template: template, 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é'; } } }