Browse Source

impression

master
Stephane 4 months ago
parent
commit
f65ab1e397
  1. 198
      lib/pages/facture_screen.dart
  2. 475
      lib/services/pdf_service.dart
  3. 1
      pubspec.yaml

198
lib/pages/facture_screen.dart

@ -1,4 +1,6 @@
// pages/facture_screen.dart // pages/facture_screen.dart
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../models/command_detail.dart'; import '../models/command_detail.dart';
@ -255,51 +257,183 @@ class _FactureScreenState extends State<FactureScreen> {
} }
void _printReceipt() async { void _printReceipt() async {
bool isPrinting;
setState(() => isPrinting = true);
try { try {
HapticFeedback.lightImpact(); // Vérifier si l'impression est disponible
final canPrint = await PlatformPrintService.canPrint();
final success = await PdfService.printFacture( if (!canPrint) {
commande: widget.commande, // Si pas d'imprimante, proposer seulement la sauvegarde
paymentMethod: widget.paymentMethod, _showSaveOnlyDialog();
); return;
}
if (success) { // Afficher les options d'impression
ScaffoldMessenger.of(context).showSnackBar( final action = await showDialog<String>(
SnackBar( context: context,
content: const Row( builder: (BuildContext context) {
return AlertDialog(
title: Row(
children: [ children: [
Icon(Icons.check_circle, color: Colors.white), Icon(Icons.print, color: Theme.of(context).primaryColor),
SizedBox(width: 8), const SizedBox(width: 8),
Text('Facture envoyée à l\'impression'), const Text('Options d\'impression'),
], ],
), ),
backgroundColor: const Color(0xFF28A745), content: Column(
duration: const Duration(seconds: 2), mainAxisSize: MainAxisSize.min,
shape: RoundedRectangleBorder( crossAxisAlignment: CrossAxisAlignment.start,
borderRadius: BorderRadius.circular(6), 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), actions: [
behavior: SnackBarBehavior.floating, 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 (success) {
if (mounted) { _showSuccessMessage(
Navigator.of(context).popUntil((route) => route.isFirst); 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) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( _showErrorMessage('Erreur: $e');
SnackBar(
content: Text('Erreur impression: $e'),
backgroundColor: Colors.red,
),
);
} finally { } finally {
if (mounted) { setState(() => isPrinting = false);
Navigator.of(context).pop(); }
}
void _showSaveOnlyDialog() async {
final shouldSave = await showDialog<bool>(
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';
}
} }

475
lib/services/pdf_service.dart

@ -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': 'contact@restaurant.fr',
}; };
// 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: [ children: [
// En-tête Restaurant // En-tête Restaurant (centré et compact)
pw.Center( pw.Text(
child: pw.Column( restaurantInfo['nom']!,
children: [ style: pw.TextStyle(
pw.Text( fontSize: titleSize,
restaurantInfo['nom']!, fontWeight: pw.FontWeight.bold,
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),
),
],
), ),
textAlign: pw.TextAlign.center,
), ),
pw.SizedBox(height: 30), pw.SizedBox(height: 1),
// Informations facture pw.Text(
pw.Center( restaurantInfo['adresse']!,
child: pw.Column( style: pw.TextStyle(fontSize: smallSize),
children: [ textAlign: pw.TextAlign.center,
pw.Text( ),
'Facture n° $factureNumber',
style: pw.TextStyle( pw.Text(
fontSize: 14, restaurantInfo['ville']!,
fontWeight: pw.FontWeight.bold, style: pw.TextStyle(fontSize: smallSize),
), textAlign: pw.TextAlign.center,
), ),
pw.SizedBox(height: 4),
pw.Text( pw.Text(
'Date: ${_formatDateTime(dateTime)}', 'Tel: ${restaurantInfo['contact']!}',
style: const pw.TextStyle(fontSize: 12), style: pw.TextStyle(fontSize: smallSize),
), textAlign: pw.TextAlign.center,
pw.SizedBox(height: 4), ),
pw.Text(
'Table: ${commande.numeroCommande}', pw.SizedBox(height: 3),
style: const pw.TextStyle(fontSize: 12),
), // Ligne de séparation
pw.SizedBox(height: 4), pw.Container(
pw.Text( width: double.infinity,
'Paiement: ${_getPaymentMethodText(paymentMethod)}', height: 0.5,
style: const pw.TextStyle(fontSize: 12), color: PdfColors.black,
),
],
),
), ),
pw.SizedBox(height: 30), pw.SizedBox(height: 2),
// Tableau des articles // Informations ticket
pw.Table( pw.Row(
border: pw.TableBorder.all(color: PdfColors.grey300), mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
columnWidths: {
0: const pw.FlexColumnWidth(3),
1: const pw.FlexColumnWidth(1),
2: const pw.FlexColumnWidth(1),
},
children: [ children: [
// En-tête du tableau pw.Text(
pw.TableRow( 'Ticket: $factureNumber',
decoration: const pw.BoxDecoration( style: pw.TextStyle(
color: PdfColors.grey100, 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 // pw.Text(
...commande.items // 'Table: ${commande.tableName}',
.map( // style: pw.TextStyle(fontSize: bodySize),
(item) => pw.TableRow( // ),
children: [ ],
pw.Padding( ),
padding: const pw.EdgeInsets.all(8),
child: pw.Text( pw.SizedBox(height: 1),
'${item.quantite} TESTNOMCOMMANDE',
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.Text(
pw.Padding( '${(item.prixUnitaire * item.quantite).toStringAsFixed(2)}',
padding: const pw.EdgeInsets.all(8), style: pw.TextStyle(
child: pw.Text( fontSize: bodySize,
'${item.prixUnitaire.toStringAsFixed(2)}', fontWeight: pw.FontWeight.bold,
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( pw.Text(
'Total: ${commande.totalTtc.toStringAsFixed(2)}', '${commande.totalTtc.toStringAsFixed(2)}',
style: pw.TextStyle( style: pw.TextStyle(
fontSize: 16, fontSize: titleSize,
fontWeight: pw.FontWeight.bold, 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 // 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: bodySize,
fontSize: 12, 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é';
}
}
} }

1
pubspec.yaml

@ -25,6 +25,7 @@ dependencies:
printing: ^5.11.1 printing: ^5.11.1
path_provider: ^2.1.1 path_provider: ^2.1.1
share_plus: ^7.2.1 share_plus: ^7.2.1
permission_handler: ^11.1.0
# Dépendances de développement/test # Dépendances de développement/test
dev_dependencies: dev_dependencies:

Loading…
Cancel
Save