diff --git a/lib/pages/printer_page.dart b/lib/pages/printer_page.dart new file mode 100644 index 0000000..e5924c6 --- /dev/null +++ b/lib/pages/printer_page.dart @@ -0,0 +1,305 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'dart:io'; +import 'dart:convert'; + +class PrinterPage extends StatefulWidget { + const PrinterPage({super.key}); + + @override + _PrinterPageState createState() => _PrinterPageState(); +} + +class _PrinterPageState extends State { + static const String printerIP = '192.168.123.251'; + static const int printerPort = 9100; + + bool _isPrinting = false; + bool _isConnected = false; + bool _isTestingConnection = false; + String _statusMessage = 'Non testé'; + + @override + void initState() { + super.initState(); + _testConnection(); + } + + Future _testConnection() async { + setState(() { + _isTestingConnection = true; + _statusMessage = 'Test en cours...'; + }); + + try { + final socket = await Socket.connect( + printerIP, + printerPort, + ).timeout(const Duration(seconds: 5)); + await socket.close(); + + setState(() { + _isConnected = true; + _statusMessage = 'Connectée'; + _isTestingConnection = false; + }); + } catch (e) { + setState(() { + _isConnected = false; + _statusMessage = 'Erreur: $e'; + _isTestingConnection = false; + }); + } + } + + Future _printReceipt() async { + try { + final socket = await Socket.connect(printerIP, printerPort); + final List bytes = []; + + // Initialisation ESC/POS + bytes.addAll([0x1B, 0x40]); // ESC @ + + // Titre centré et agrandi + bytes.addAll([0x1B, 0x61, 0x01]); // Centre + bytes.addAll([0x1D, 0x21, 0x11]); // Taille double + bytes.addAll(utf8.encode('REÇU DE VENTE')); + bytes.addAll([0x0A, 0x0A]); + + // Retour taille normale + bytes.addAll([0x1D, 0x21, 0x00]); + bytes.addAll([0x1B, 0x61, 0x00]); // Alignement gauche + + // Informations transaction + final now = DateTime.now(); + final dateStr = + '${now.day.toString().padLeft(2, '0')}/${now.month.toString().padLeft(2, '0')}/${now.year} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; + + bytes.addAll(utf8.encode('Date: $dateStr\n')); + bytes.addAll(utf8.encode('Caissier: Admin\n')); + bytes.addAll(utf8.encode('--------------------------------\n')); + + // En-têtes colonnes + bytes.addAll(utf8.encode('Article Qte Prix\n')); + bytes.addAll(utf8.encode('--------------------------------\n')); + + // Articles + bytes.addAll(utf8.encode('Produit 1 2 25.00€\n')); + bytes.addAll(utf8.encode('Produit 2 1 15.50€\n')); + bytes.addAll(utf8.encode('--------------------------------\n')); + + // Total + bytes.addAll([0x1B, 0x45, 0x01]); // Gras ON + bytes.addAll(utf8.encode('TOTAL 40.50€\n')); + bytes.addAll([0x1B, 0x45, 0x00]); // Gras OFF + + bytes.addAll([0x0A, 0x0A]); + + // Message de remerciement centré + bytes.addAll([0x1B, 0x61, 0x01]); // Centre + bytes.addAll(utf8.encode('Merci de votre visite !\n')); + bytes.addAll([0x0A, 0x0A]); + + // Coupe papier + bytes.addAll([0x1D, 0x56, 0x00]); // GS V 0 + + // Envoi des données + socket.add(bytes); + await socket.flush(); + await socket.close(); + + return true; + } catch (e) { + if (kDebugMode) { + print('Erreur impression: $e'); + } + return false; + } + } + + Future _handlePrint() async { + setState(() => _isPrinting = true); + + final success = await _printReceipt(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + success ? 'Impression réussie!' : 'Échec de l\'impression', + ), + backgroundColor: success ? Colors.green : Colors.red, + duration: const Duration(seconds: 3), + ), + ); + + setState(() => _isPrinting = false); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Imprimante POS'), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Status de connexion + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Status de l\'imprimante', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 10), + Row( + children: [ + if (_isTestingConnection) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + Icon( + _isConnected ? Icons.wifi : Icons.wifi_off, + color: _isConnected ? Colors.green : Colors.red, + size: 20, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + 'IP: $printerIP:$printerPort\nStatus: $_statusMessage', + style: const TextStyle(fontSize: 14), + ), + ), + ], + ), + const SizedBox(height: 10), + ElevatedButton.icon( + onPressed: _isTestingConnection ? null : _testConnection, + icon: const Icon(Icons.refresh), + label: const Text('Tester la connexion'), + ), + ], + ), + ), + ), + + const SizedBox(height: 20), + + // Aperçu du reçu + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Aperçu du reçu', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(4), + color: Colors.grey[50], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'REÇU DE VENTE', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Date: ${DateTime.now().toString().substring(0, 16)}', + ), + const Text('Caissier: Admin'), + const Text('--------------------------------'), + const Text('Article Qte Prix'), + const Text('--------------------------------'), + const Text('Produit 1 2 25.00€'), + const Text('Produit 2 1 15.50€'), + const Text('--------------------------------'), + const Text( + 'TOTAL 40.50€', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + const SizedBox(height: 10), + const Text('Merci de votre visite !'), + ], + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 20), + + // Bouton d'impression + ElevatedButton.icon( + onPressed: + (_isConnected && !_isPrinting && !_isTestingConnection) + ? _handlePrint + : null, + icon: + _isPrinting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : const Icon(Icons.print), + label: Text(_isPrinting ? 'Impression...' : 'Imprimer le reçu'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 15), + textStyle: const TextStyle(fontSize: 16), + ), + ), + + if (!_isConnected && !_isTestingConnection) + const Padding( + padding: EdgeInsets.only(top: 10), + child: Text( + 'Vérifiez que l\'imprimante est allumée et connectée au réseau', + style: TextStyle(color: Colors.orange, fontSize: 12), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 8e6c710..12b2a94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,8 @@ dependencies: share_plus: ^7.2.1 permission_handler: ^11.1.0 intl: ^0.18.1 + esc_pos_printer: ^4.1.0 + esc_pos_utils: ^1.1.0 # Dépendances de développement/test dev_dependencies: @@ -34,6 +36,9 @@ dev_dependencies: sdk: flutter flutter_lints: ^3.0.1 +dependency_overrides: + image: ^4.1.0 + # Paramètres spécifiques à Flutter flutter: uses-material-design: true