You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
438 lines
13 KiB
438 lines
13 KiB
// 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 '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<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 - VERSION IDENTIQUE À L'ÉCRAN
|
|
static Future<Uint8List> _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,
|
|
margin: const pw.EdgeInsets.only(right: 20),
|
|
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: 20),
|
|
child: pw.Text(
|
|
formatTemplateContent(restaurantContent),
|
|
style: pw.TextStyle(fontSize: smallSize),
|
|
textAlign: pw.TextAlign.center,
|
|
),
|
|
),
|
|
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.totalHt)}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<bool> 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<bool> 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<bool> saveFacturePdf({
|
|
required CommandeDetail commande,
|
|
required PrintTemplate template,
|
|
required String paymentMethod,
|
|
}) async {
|
|
return await saveTicketPdf(
|
|
commande: commande,
|
|
template: template,
|
|
paymentMethod: paymentMethod,
|
|
);
|
|
}
|
|
|
|
static Future<bool> 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é';
|
|
}
|
|
}
|
|
}
|