2 changed files with 310 additions and 0 deletions
@ -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<PrinterPage> { |
||||
|
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<void> _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<bool> _printReceipt() async { |
||||
|
try { |
||||
|
final socket = await Socket.connect(printerIP, printerPort); |
||||
|
final List<int> 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<void> _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<Color>( |
||||
|
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, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue