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.
 
 
 
 
 
 

764 lines
24 KiB

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:itrimobe/services/restaurant_api_service.dart';
import 'package:itrimobe/models/printerModel.dart';
import 'dart:io';
import 'dart:convert';
import '../pages/commandes_screen.dart';
// Énumération pour les différents états de connexion
enum PrinterConnectionStatus {
notConfigured,
connecting,
connected,
disconnected,
error
}
// Classe pour les résultats de test de connexion
class ConnectionTestResult {
final bool isSuccessful;
final String message;
final PrinterConnectionStatus status;
final Duration responseTime;
const ConnectionTestResult({
required this.isSuccessful,
required this.message,
required this.status,
required this.responseTime,
});
@override
String toString() => 'ConnectionTestResult(success: $isSuccessful, message: "$message", time: ${responseTime.inMilliseconds}ms)';
}
class OrderPrinter {
// Singleton pattern pour garantir une seule instance
static OrderPrinter? _instance;
static OrderPrinter get instance => _instance ??= OrderPrinter._internal();
OrderPrinter._internal();
// Configuration de l'imprimante
PrinterSettings? _settings;
PrinterConnectionStatus _connectionStatus = PrinterConnectionStatus.notConfigured;
// Getters publics
PrinterSettings? get settings => _settings;
PrinterConnectionStatus get connectionStatus => _connectionStatus;
bool get isConfigured => _settings != null &&
_settings!.ipAddress != null &&
_settings!.ipAddress!.isNotEmpty &&
_settings!.port != null;
// Constantes ESC/POS
static const List<int> ESC_CONDENSED_ON = [0x1B, 0x0F];
static const List<int> ESC_CONDENSED_OFF = [0x1B, 0x12];
static const List<int> ESC_INIT = [0x1B, 0x40];
static const List<int> ESC_ALIGN_CENTER = [0x1B, 0x61, 0x01];
static const List<int> ESC_ALIGN_LEFT = [0x1B, 0x61, 0x00];
static const List<int> ESC_ALIGN_RIGHT = [0x1B, 0x61, 0x02];
static const List<int> ESC_DOUBLE_SIZE = [0x1D, 0x21, 0x11];
static const List<int> ESC_NORMAL_SIZE = [0x1D, 0x23, 0x00];
static const List<int> ESC_BOLD_ON = [0x1B, 0x45, 0x01];
static const List<int> ESC_BOLD_OFF = [0x1B, 0x45, 0x00];
static const List<int> ESC_CUT = [0x1D, 0x56, 0x00];
static const List<int> ESC_PAGE_WIDTH_80MM = [0x1D, 0x57, 0x00, 0x02];
static const List<int> ESC_LEFT_MARGIN_0 = [0x1D, 0x4C, 0x015, 0x00];
// Configuration d'impression
static const int maxCharsPerLine = 42;
static const String checkbox = '[ ]';
static const String rightWord = 'cocher';
static const int padding = 1;
/// Initialise la configuration de l'imprimante avec gestion d'erreurs robuste
Future<bool> initialize() async {
try {
_connectionStatus = PrinterConnectionStatus.connecting;
if (kDebugMode) {
print("🔧 Initialisation de la configuration imprimante...");
}
// Récupération des paramètres avec timeout via votre API
var printerSettings = await RestaurantApiService.getPrinterSettings()
.timeout(
const Duration(seconds: 10),
onTimeout: () => throw TimeoutException('Timeout lors de la récupération des paramètres'),
);
// Validation des paramètres
if (printerSettings.ipAddress == null || printerSettings.ipAddress!.isEmpty) {
throw ArgumentError('Adresse IP de l\'imprimante non définie');
}
if (printerSettings.port == null || printerSettings.port! <= 0 || printerSettings.port! > 65535) {
throw ArgumentError('Port de l\'imprimante invalide: ${printerSettings.port}');
}
// Validation du format IP
if (!_isValidIpAddress(printerSettings.ipAddress!)) {
throw ArgumentError('Format d\'adresse IP invalide: ${printerSettings.ipAddress}');
}
_settings=null;
// Configuration réussie
_settings = printerSettings;
_connectionStatus = PrinterConnectionStatus.disconnected;
_settings = PrinterSettings();
_settings = printerSettings;
if (kDebugMode) {
print("✅ Imprimante configurée : ${_settings!.ipAddress}:${_settings!.port}");
if (_settings!.name != null) {
print("📋 Nom: ${_settings!.name}");
}
if (_settings!.type != null) {
print("🏷️ Type: ${_settings!.type}");
}
}
return true;
} on TimeoutException catch (e) {
_connectionStatus = PrinterConnectionStatus.error;
if (kDebugMode) {
print("⏰ Timeout lors de l'initialisation: $e");
}
return false;
} on ArgumentError catch (e) {
_connectionStatus = PrinterConnectionStatus.error;
if (kDebugMode) {
print("❌ Paramètres invalides: $e");
}
return false;
} catch (e) {
_connectionStatus = PrinterConnectionStatus.error;
if (kDebugMode) {
print("💥 Erreur lors de la récupération des paramètres d'imprimante : $e");
}
return false;
}
}
/// Valide le format d'une adresse IP
bool _isValidIpAddress(String ip) {
final ipRegex = RegExp(r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$');
return ipRegex.hasMatch(ip);
}
/// Test de connexion avancé avec métriques
Future<ConnectionTestResult> testConnection({
Duration timeout = const Duration(seconds: 5),
int maxRetries = 3,
}) async {
if (!isConfigured) {
return const ConnectionTestResult(
isSuccessful: false,
message: 'Imprimante non configurée',
status: PrinterConnectionStatus.notConfigured,
responseTime: Duration.zero,
);
}
final stopwatch = Stopwatch()..start();
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
_connectionStatus = PrinterConnectionStatus.connecting;
if (kDebugMode) {
print("🔍 Test de connexion (tentative $attempt/$maxRetries) vers ${_settings!.ipAddress}:${_settings!.port}");
}
final socket = await Socket.connect(
_settings!.ipAddress!,
_settings!.port!,
).timeout(timeout);
// Test d'écriture simple pour vérifier que l'imprimante répond
socket.add(ESC_INIT);
await socket.flush();
await socket.close();
stopwatch.stop();
_connectionStatus = PrinterConnectionStatus.connected;
final result = ConnectionTestResult(
isSuccessful: true,
message: 'Connexion réussie en ${stopwatch.elapsedMilliseconds}ms',
status: PrinterConnectionStatus.connected,
responseTime: stopwatch.elapsed,
);
if (kDebugMode) {
print("${result.message}");
}
return result;
} on SocketException catch (e) {
if (kDebugMode) {
print("🔌 Erreur de socket (tentative $attempt): $e");
}
if (attempt == maxRetries) {
stopwatch.stop();
_connectionStatus = PrinterConnectionStatus.disconnected;
return ConnectionTestResult(
isSuccessful: false,
message: 'Imprimante inaccessible: ${e.message}',
status: PrinterConnectionStatus.disconnected,
responseTime: stopwatch.elapsed,
);
}
// Attendre avant la prochaine tentative
await Future.delayed(Duration(milliseconds: 500 * attempt));
} on TimeoutException {
if (kDebugMode) {
print("⏰ Timeout de connexion (tentative $attempt)");
}
if (attempt == maxRetries) {
stopwatch.stop();
_connectionStatus = PrinterConnectionStatus.disconnected;
return ConnectionTestResult(
isSuccessful: false,
message: 'Timeout de connexion après ${timeout.inSeconds}s',
status: PrinterConnectionStatus.disconnected,
responseTime: stopwatch.elapsed,
);
}
await Future.delayed(Duration(milliseconds: 500 * attempt));
} catch (e) {
if (kDebugMode) {
print("💥 Erreur inattendue (tentative $attempt): $e");
}
if (attempt == maxRetries) {
stopwatch.stop();
_connectionStatus = PrinterConnectionStatus.error;
return ConnectionTestResult(
isSuccessful: false,
message: 'Erreur de connexion: $e',
status: PrinterConnectionStatus.error,
responseTime: stopwatch.elapsed,
);
}
await Future.delayed(Duration(milliseconds: 500 * attempt));
}
}
// Ce code ne devrait jamais être atteint
stopwatch.stop();
_connectionStatus = PrinterConnectionStatus.error;
return ConnectionTestResult(
isSuccessful: false,
message: 'Échec après $maxRetries tentatives',
status: PrinterConnectionStatus.error,
responseTime: stopwatch.elapsed,
);
}
/// Test de connexion simple (rétrocompatibilité)
Future<bool> testConnectionSimple() async {
final result = await testConnection();
return result.isSuccessful;
}
/// Diagnostic complet de l'imprimante
Future<Map<String, dynamic>> runDiagnostics() async {
final diagnostics = <String, dynamic>{
'timestamp': DateTime.now().toIso8601String(),
'configured': isConfigured,
'connectionStatus': _connectionStatus.toString(),
};
if (_settings != null) {
diagnostics['settings'] = {
'ipAddress': _settings!.ipAddress,
'port': _settings!.port,
'name': _settings!.name,
'type': _settings!.type,
'id': _settings!.id,
'createdAt': _settings!.createdAt?.toIso8601String(),
'updatedAt': _settings!.updatedAt?.toIso8601String(),
};
} else {
diagnostics['settings'] = 'Non configuré';
}
if (isConfigured) {
// Test de résolution DNS (si applicable)
try {
final addresses = await InternetAddress.lookup(_settings!.ipAddress!);
diagnostics['dnsResolution'] = {
'success': true,
'addresses': addresses.map((addr) => addr.address).toList(),
};
} catch (e) {
diagnostics['dnsResolution'] = {
'success': false,
'error': e.toString(),
};
}
// Test de connectivité
final connectionResult = await testConnection(maxRetries: 1);
diagnostics['connectionTest'] = {
'success': connectionResult.isSuccessful,
'message': connectionResult.message,
'responseTime': '${connectionResult.responseTime.inMilliseconds}ms',
'status': connectionResult.status.toString(),
};
// Test de ping réseau (optionnel)
diagnostics['networkReachability'] = await _testNetworkReachability();
}
return diagnostics;
}
/// Test de accessibilité réseau
Future<Map<String, dynamic>> _testNetworkReachability() async {
try {
final result = await Process.run('ping', [
'-c', '1', // Une seule tentative
'-W', '3000', // Timeout 3 secondes
_settings!.ipAddress!,
]);
return {
'success': result.exitCode == 0,
'output': result.stdout,
'error': result.stderr,
};
} catch (e) {
return {
'success': false,
'error': 'Ping non disponible: $e',
};
}
}
/// Met à jour la configuration manuellement
bool updateConfiguration(String ipAddress, int port, {String? name, String? type}) {
if (!_isValidIpAddress(ipAddress)) {
if (kDebugMode) {
print("❌ Adresse IP invalide: $ipAddress");
}
return false;
}
if (port <= 0 || port > 65535) {
if (kDebugMode) {
print("❌ Port invalide: $port");
}
return false;
}
_settings = PrinterSettings(
ipAddress: ipAddress,
port: port,
name: name,
type: type,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
_connectionStatus = PrinterConnectionStatus.disconnected;
if (kDebugMode) {
print("🔧 Configuration mise à jour: $ipAddress:$port");
if (name != null) print("📋 Nom: $name");
if (type != null) print("🏷️ Type: $type");
}
return true;
}
/// Recharge la configuration depuis l'API
Future<bool> reloadConfiguration() async {
if (kDebugMode) {
print("🔄 Rechargement de la configuration depuis l'API...");
}
return await initialize();
}
/// Réinitialise la configuration
void resetConfiguration() {
_settings = null;
_connectionStatus = PrinterConnectionStatus.notConfigured;
if (kDebugMode) {
print("🔄 Configuration de l'imprimante réinitialisée");
}
}
/// Méthode pour normaliser les caractères accentués
String safePadRight(String text, int width) {
// Remplace temporairement les accents par des lettres simples
final normalized = text
.replaceAll(RegExp(r'[éèêë]'), 'e')
.replaceAll(RegExp(r'[àâä]'), 'a')
.replaceAll(RegExp(r'[ôö]'), 'o')
.replaceAll(RegExp(r'[ûü]'), 'u')
.replaceAll(RegExp(r'[ç]'), 'c')
.replaceAll(RegExp(r'[ÉÈÊË]'), 'E')
.replaceAll(RegExp(r'[ÀÂÄ]'), 'A')
.replaceAll(RegExp(r'[ÔÖ]'), 'O')
.replaceAll(RegExp(r'[ÛÜ]'), 'U')
.replaceAll(RegExp(r'[Ç]'), 'C');
return normalized.padRight(width);
}
// Méthodes d'impression optimisées
Future<bool> printOrderESCPOS(Order order) async {
final connectionTest = await testConnection();
if (!connectionTest.isSuccessful) {
if (kDebugMode) {
print("❌ Impossible d'imprimer: ${connectionTest.message}");
}
return false;
}
try {
final socket = await Socket.connect(_settings!.ipAddress!, _settings!.port!);
final List<int> bytes = [];
// Initialisation et configuration pour 80mm
bytes.addAll(ESC_INIT);
bytes.addAll(ESC_PAGE_WIDTH_80MM);
bytes.addAll(ESC_LEFT_MARGIN_0);
bytes.addAll(latin1.encode('${'-' * maxCharsPerLine}\n'));
bytes.addAll(ESC_LEFT_MARGIN_0);
// En-tête centré
bytes.addAll(ESC_ALIGN_CENTER);
bytes.addAll(ESC_BOLD_ON);
bytes.addAll(ESC_LEFT_MARGIN_0);
bytes.addAll(latin1.encode('Commande n° ${order.numeroCommande}\n'));
// Informations commande
bytes.addAll(ESC_LEFT_MARGIN_0);
bytes.addAll(ESC_ALIGN_CENTER);
bytes.addAll(ESC_NORMAL_SIZE);
bytes.addAll(latin1.encode('Table: ${order.tablename}\n'));
bytes.addAll(ESC_BOLD_OFF);
bytes.addAll(latin1.encode('Date: ${_formatTime(order.dateCommande)}\n'));
bytes.addAll(latin1.encode('${'-' * maxCharsPerLine}\n'));
// En-tête du tableau
bytes.addAll(ESC_ALIGN_CENTER);
bytes.addAll(ESC_BOLD_ON);
bytes.addAll(ESC_LEFT_MARGIN_0);
String header = 'Designation'.padRight(maxCharsPerLine - rightWord.length - padding) +
' ' * padding + rightWord + '\n';
bytes.addAll(latin1.encode(header));
bytes.addAll(ESC_BOLD_OFF);
bytes.addAll(latin1.encode('${'-' * maxCharsPerLine}\n'));
// Articles
bytes.addAll(ESC_ALIGN_CENTER);
bytes.addAll(ESC_LEFT_MARGIN_0);
for (var item in order.items) {
String designation = '${item.quantite}x ${item.nom ?? 'Item'}';
int maxDesignationLength = maxCharsPerLine - checkbox.length - padding;
if (designation.length > maxDesignationLength) {
designation = designation.substring(0, maxDesignationLength - 3) + '...';
}
String line = designation.padRight(maxDesignationLength) +
' ' * padding + checkbox + '\n';
bytes.addAll(utf8.encode(line));
}
// Pied de page
bytes.addAll(utf8.encode('${'-' * maxCharsPerLine}\n'));
// Espacement avant coupe
bytes.addAll([0x0A, 0x0A, 0x0A]);
bytes.addAll(ESC_CUT);
socket.add(bytes);
await socket.flush();
await socket.close();
if (kDebugMode) {
print("✅ Impression réussie!");
}
return true;
} catch (e) {
if (kDebugMode) {
print('💥 Erreur impression: $e');
}
return false;
}
}
Future<bool> printOrderESC80MM(Order order) async {
final connectionTest = await testConnection();
if (!connectionTest.isSuccessful) {
if (kDebugMode) {
print("❌ Impossible d'imprimer: ${connectionTest.message}");
}
return false;
}
try {
final socket = await Socket.connect(_settings!.ipAddress!, _settings!.port!);
final List<int> bytes = [];
// Configuration spécifique 80mm
bytes.addAll(ESC_INIT);
bytes.addAll(ESC_PAGE_WIDTH_80MM);
bytes.addAll(ESC_LEFT_MARGIN_0);
// En-tête centré et stylisé
bytes.addAll(ESC_ALIGN_CENTER);
bytes.addAll(ESC_DOUBLE_SIZE);
bytes.addAll(ESC_BOLD_ON);
bytes.addAll(ESC_PAGE_WIDTH_80MM);
bytes.addAll(utf8.encode('COMMANDE\n'));
bytes.addAll(ESC_BOLD_OFF);
bytes.addAll(ESC_NORMAL_SIZE);
bytes.addAll(utf8.encode('${'-' * maxCharsPerLine}\n'));
// Informations commande avec formatage amélioré
bytes.addAll(utf8.encode(_createInfoLine('N° Commande', order.numeroCommande.toString())));
bytes.addAll(utf8.encode(_createInfoLine('Date/Heure', _formatTime(order.dateCommande))));
bytes.addAll(utf8.encode('${'=' * maxCharsPerLine}\n'));
// En-tête du tableau avec colonnes bien définies
bytes.addAll(ESC_BOLD_ON);
bytes.addAll(ESC_PAGE_WIDTH_80MM);
bytes.addAll(utf8.encode(_createTableHeader()));
bytes.addAll(ESC_BOLD_OFF);
bytes.addAll(utf8.encode('${'-' * maxCharsPerLine}\n'));
// Articles avec formatage en colonnes
for (var item in order.items) {
bytes.addAll(utf8.encode(_createItemLine(item.quantite, item.nom ?? 'Item')));
}
// Pied de page
bytes.addAll(utf8.encode('${'=' * maxCharsPerLine}\n'));
bytes.addAll(ESC_ALIGN_CENTER);
bytes.addAll(ESC_PAGE_WIDTH_80MM);
// Coupe avec espacement
bytes.addAll([0x0A, 0x0A]);
bytes.addAll(ESC_CUT);
socket.add(bytes);
await socket.flush();
await socket.close();
return true;
} catch (e) {
if (kDebugMode) {
print('Erreur impression: $e');
}
return false;
}
}
Future<bool> printOrderESCPOSOptimized(Order order) async {
final connectionTest = await testConnection();
if (!connectionTest.isSuccessful) {
if (kDebugMode) {
print("❌ Impossible d'imprimer: ${connectionTest.message}");
}
return false;
}
try {
final socket = await Socket.connect(_settings!.ipAddress!, _settings!.port!);
final List<int> bytes = [];
// Configuration initiale optimisée pour 72mm
bytes.addAll(ESC_INIT);
bytes.addAll(ESC_LEFT_MARGIN_0);
// Définir largeur de page explicite
bytes.addAll([0x1B, 0x51, 180]);
// En-tête
bytes.addAll(ESC_ALIGN_CENTER);
bytes.addAll(ESC_DOUBLE_SIZE);
bytes.addAll(utf8.encode('COMMANDE\n\n'));
// Corps avec police condensée
bytes.addAll(ESC_NORMAL_SIZE);
bytes.addAll(ESC_CONDENSED_ON);
const int compactMaxChars = 32;
bytes.addAll(utf8.encode('Cmd: ${order.numeroCommande}\n'));
bytes.addAll(utf8.encode('Table: ${order.tablename}\n'));
bytes.addAll(utf8.encode('${_formatTime(order.dateCommande)}\n'));
bytes.addAll(utf8.encode('${'=' * compactMaxChars}\n'));
// Articles avec formatage optimisé
for (var item in order.items) {
String qty = '${item.quantite}x';
String name = item.nom ?? 'Item';
int availableSpace = compactMaxChars - qty.length - checkbox.length - 2;
if (name.length > availableSpace) {
name = name.substring(0, availableSpace - 3) + '...';
}
String line = '$qty $name'.padRight(compactMaxChars - checkbox.length) + checkbox + '\n';
bytes.addAll(utf8.encode(line));
}
// Finalisation
bytes.addAll(utf8.encode('${'=' * compactMaxChars}\n'));
bytes.addAll(ESC_CONDENSED_OFF);
bytes.addAll(ESC_ALIGN_CENTER);
bytes.addAll(utf8.encode('\nBon service !\n\n\n'));
bytes.addAll(ESC_CUT);
socket.add(bytes);
await socket.flush();
await socket.close();
return true;
} catch (e) {
if (kDebugMode) {
print('Erreur impression: $e');
}
return false;
}
}
// Fonctions utilitaires pour formatage
String _createInfoLine(String label, String value) {
int labelWidth = 12;
int valueWidth = maxCharsPerLine - labelWidth - 2;
String truncatedLabel = label.length > labelWidth ?
label.substring(0, labelWidth) : label.padRight(labelWidth);
String truncatedValue = value.length > valueWidth ?
value.substring(0, valueWidth - 3) + '...' : value;
return '$truncatedLabel: $truncatedValue\n';
}
String _createTableHeader() {
int qtyWidth = 4;
int checkboxWidth = checkbox.length;
int nameWidth = maxCharsPerLine - qtyWidth - checkboxWidth - 2;
String header = 'Qty'.padRight(qtyWidth) +
'Designation'.padRight(nameWidth) +
checkbox + '\n';
return header;
}
String _createItemLine(int quantity, String itemName) {
int qtyWidth = 4;
int checkboxWidth = checkbox.length;
int nameWidth = maxCharsPerLine - qtyWidth - checkboxWidth - 2;
String qtyStr = quantity.toString().padRight(qtyWidth);
String nameStr = itemName.length > nameWidth ?
itemName.substring(0, nameWidth - 3) + '...' : itemName.padRight(nameWidth);
return qtyStr + nameStr + checkbox + '\n';
}
/// Formatage du temps
String _formatTime(DateTime dateTime) {
return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')} ${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}';
}
}
// Fonctions utilitaires pour rétrocompatibilité
Future<bool> printOrderPDF(Order order) async {
final printer = OrderPrinter.instance;
// Initialiser si nécessaire
if (!printer.isConfigured) {
final initialized = await printer.initialize();
if (!initialized) {
if (kDebugMode) {
print('❌ Impossible d\'initialiser l\'imprimante');
}
return false;
}
}
return await printer.printOrderESC80MM(order);
}
Future<void> printOrderWithFeedback(Order order, Function(String, bool) onResult) async {
final printer = OrderPrinter.instance;
try {
// Initialisation si nécessaire
if (!printer.isConfigured) {
onResult('Initialisation de l\'imprimante...', true);
final initialized = await printer.initialize();
if (!initialized) {
onResult('Échec de l\'initialisation', false);
return;
}
}
// Test de connexion
onResult('Test de connexion...', true);
final connectionResult = await printer.testConnection();
if (!connectionResult.isSuccessful) {
onResult(connectionResult.message, false);
return;
}
// Impression
onResult('Impression en cours...', true);
final success = await printer.printOrderESCPOS(order);
if (success) {
onResult('Impression réussie!', true);
} else {
onResult('Échec de l\'impression', false);
}
} catch (e) {
onResult('Erreur: $e', false);
}
}
// Fonction utilitaire pour obtenir des informations sur l'imprimante
Future<Map<String, dynamic>> getPrinterInfo() async {
final printer = OrderPrinter.instance;
if (!printer.isConfigured) {
await printer.initialize();
}
return await printer.runDiagnostics();
}
// Fonction pour tester la connexion avec retour détaillé
Future<ConnectionTestResult> testPrinterConnection() async {
final printer = OrderPrinter.instance;
if (!printer.isConfigured) {
await printer.initialize();
}
return await printer.testConnection();
}