From b88c301d7d553ee22203e851ac3e735af130fd48 Mon Sep 17 00:00:00 2001 From: a2nr Date: Fri, 5 Sep 2025 21:44:38 +0700 Subject: [PATCH] Implement PrintingStatusCard in ReceiptSpeedDial and improve error handling for Bluetooth printer connection --- lib/screens/receipt_screen.dart | 251 ++++++++++++++---------- lib/services/bluetooth_service.dart | 40 +++- lib/services/esc_pos_print_service.dart | 14 +- lib/widgets/printing_status_card.dart | 6 +- lib/widgets/receipt_speed_dial.dart | 88 +++++---- 5 files changed, 254 insertions(+), 145 deletions(-) diff --git a/lib/screens/receipt_screen.dart b/lib/screens/receipt_screen.dart index 714716c..2eeb742 100644 --- a/lib/screens/receipt_screen.dart +++ b/lib/screens/receipt_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'dart:typed_data'; import 'package:cashumit/models/receipt_item.dart'; import 'package:cashumit/screens/add_item_screen.dart'; import 'package:bluetooth_print/bluetooth_print.dart'; @@ -12,6 +11,7 @@ import 'package:cashumit/widgets/store_info_config_dialog.dart'; import 'package:cashumit/widgets/custom_text_config_dialog.dart'; import 'package:cashumit/screens/webview_screen.dart'; import 'package:cashumit/widgets/receipt_speed_dial.dart'; +import 'package:cashumit/widgets/printing_status_card.dart'; // Import service baru import 'package:cashumit/services/account_dialog_service.dart'; @@ -21,6 +21,10 @@ import 'package:cashumit/services/esc_pos_print_service.dart'; import 'package:provider/provider.dart'; import 'package:cashumit/providers/receipt_provider.dart'; +// Import untuk penanganan error +import 'dart:io'; +import 'package:flutter/services.dart'; + class ReceiptScreen extends StatefulWidget { const ReceiptScreen({super.key}); @@ -31,6 +35,9 @@ class ReceiptScreen extends StatefulWidget { class _ReceiptScreenState extends State { // Bluetooth service final BluetoothService _bluetoothService = BluetoothService(); + + // Printing status + bool _isPrinting = false; @override void initState() { @@ -108,25 +115,25 @@ class _ReceiptScreenState extends State { final receiptProvider = context.read(); final state = receiptProvider.state; - // Cek dan reconnect jika perlu - final isConnected = await _bluetoothService.reconnectIfNeeded(); - if (!isConnected) { - if (!mounted) return; - if (_bluetoothService.connectedDevice != null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Gagal menyambungkan ke printer')), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Harap hubungkan printer terlebih dahulu')), - ); - } - return; - } - try { + // Cek dan reconnect jika perlu + final isConnected = await _bluetoothService.reconnectIfNeeded(); + if (!isConnected) { + if (!mounted) return; + if (_bluetoothService.connectedDevice != null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Gagal menyambungkan ke printer')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Harap hubungkan printer terlebih dahulu')), + ); + } + return; + } + await EscPosPrintService.printToThermalPrinter( items: state.items, transactionDate: state.transactionDate, @@ -138,13 +145,39 @@ class _ReceiptScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Perintah cetak dikirim ke printer')), ); + } on SocketException catch (e) { + // Tangani error koneksi secara spesifik + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Koneksi ke printer terputus: ${e.message}')), + ); + } on PlatformException catch (e) { + // Tangani error platform secara spesifik + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error printer: ${e.message}')), + ); } catch (e) { + // Tangani error umum if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal mencetak struk: $e')), ); } } + + /// Methods to control printing status + void _startPrinting() { + setState(() { + _isPrinting = true; + }); + } + + void _endPrinting() { + setState(() { + _isPrinting = false; + }); + } void _addItem() async { final newItem = await showDialog( @@ -330,95 +363,109 @@ class _ReceiptScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: - Colors.grey[300], // Latar belakang abu-abu untuk efek struk - floatingActionButton: Consumer( - builder: (context, receiptProvider, child) { - final state = receiptProvider.state; - return ReceiptSpeedDial( - bluetoothService: _bluetoothService, - onCheckConnection: _checkBluetoothConnection, - onPrint: _printToThermalPrinter, - onSettings: _openSettings, - onReloadAccounts: receiptProvider.loadAccounts, - hasItems: state.items.isNotEmpty, - hasSourceAccount: state.sourceAccountId != null, - hasDestinationAccount: state.destinationAccountId != null, - onSendToFirefly: _sendToFirefly, - ); - } - ), - body: SafeArea( - child: Center( - // Membungkus dengan widget Center untuk memastikan struk berada di tengah - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment - .center, // Memusatkan konten secara horizontal - children: [ - // Background untuk efek kertas struk tersobek di bagian atas - Container( - width: 360, - color: const Color(0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar - child: const Column( - children: [ - SizedBox(height: 15), // Jarak atas yang lebih besar - ReceiptTearTop(), // Efek kertas struk tersobek di bagian atas - ], - ), - ), + return Stack( + children: [ + Scaffold( + backgroundColor: + Colors.grey[300], // Latar belakang abu-abu untuk efek struk + floatingActionButton: Consumer( + builder: (context, receiptProvider, child) { + final state = receiptProvider.state; + return ReceiptSpeedDial( + bluetoothService: _bluetoothService, + onCheckConnection: _checkBluetoothConnection, + onPrint: _printToThermalPrinter, + onSettings: _openSettings, + onReloadAccounts: receiptProvider.loadAccounts, + hasItems: state.items.isNotEmpty, + hasSourceAccount: state.sourceAccountId != null, + hasDestinationAccount: state.destinationAccountId != null, + onSendToFirefly: _sendToFirefly, + onPrintingStart: _startPrinting, + onPrintingEnd: _endPrinting, + ); + } + ), + body: SafeArea( + child: Center( + // Membungkus dengan widget Center untuk memastikan struk berada di tengah + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment + .center, // Memusatkan konten secara horizontal + children: [ + // Background untuk efek kertas struk tersobek di bagian atas + Container( + width: 360, + color: const Color(0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar + child: const Column( + children: [ + SizedBox(height: 15), // Jarak atas yang lebih besar + ReceiptTearTop(), // Efek kertas struk tersobek di bagian atas + ], + ), + ), - // Konten struk - Consumer( - builder: (context, receiptProvider, child) { - final state = receiptProvider.state; - return ReceiptBody( - items: state.items, - sourceAccountName: state.sourceAccountName, - destinationAccountName: state.destinationAccountName, - onOpenStoreInfoConfig: _openStoreInfoConfig, - onSelectSourceAccount: () => _selectSourceAccount(), // Memanggil fungsi langsung - onSelectDestinationAccount: () => _selectDestinationAccount(), // Memanggil fungsi langsung - onOpenCustomTextConfig: _openCustomTextConfig, - total: state.total, - onEditItem: (index) => _editItem(index), // Memanggil fungsi langsung - onRemoveItem: (index) { - // Validasi index untuk mencegah error - if (index >= 0 && index < state.items.length) { - // Hapus item dari daftar melalui provider - receiptProvider.removeItem(index); - } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - "Gagal menghapus item: Index tidak valid.")), - ); - } - } - }, - onAddItem: () => _addItem(), // Memanggil fungsi langsung - ); - } - ), + // Konten struk + Consumer( + builder: (context, receiptProvider, child) { + final state = receiptProvider.state; + return ReceiptBody( + items: state.items, + sourceAccountName: state.sourceAccountName, + destinationAccountName: state.destinationAccountName, + onOpenStoreInfoConfig: _openStoreInfoConfig, + onSelectSourceAccount: () => _selectSourceAccount(), // Memanggil fungsi langsung + onSelectDestinationAccount: () => _selectDestinationAccount(), // Memanggil fungsi langsung + onOpenCustomTextConfig: _openCustomTextConfig, + total: state.total, + onEditItem: (index) => _editItem(index), // Memanggil fungsi langsung + onRemoveItem: (index) { + // Validasi index untuk mencegah error + if (index >= 0 && index < state.items.length) { + // Hapus item dari daftar melalui provider + receiptProvider.removeItem(index); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + "Gagal menghapus item: Index tidak valid.")), + ); + } + } + }, + onAddItem: () => _addItem(), // Memanggil fungsi langsung + ); + } + ), - // Background untuk efek kertas struk tersobek di bagian bawah - Container( - width: 360, - color: const Color(0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar - child: const Column( - children: [ - ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah - SizedBox(height: 15), // Jarak bawah yang lebih besar - ], - ), + // Background untuk efek kertas struk tersobek di bagian bawah + Container( + width: 360, + color: const Color(0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar + child: const Column( + children: [ + ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah + SizedBox(height: 15), // Jarak bawah yang lebih besar + ], + ), + ), + ], ), - ], + ), ), ), ), - ), + PrintingStatusCard( + isVisible: _isPrinting, + onDismiss: () { + setState(() { + _isPrinting = false; + }); + }, + ), + ], ); } } \ No newline at end of file diff --git a/lib/services/bluetooth_service.dart b/lib/services/bluetooth_service.dart index d8b5bd6..011ae87 100644 --- a/lib/services/bluetooth_service.dart +++ b/lib/services/bluetooth_service.dart @@ -1,7 +1,8 @@ import 'package:bluetooth_print/bluetooth_print.dart'; import 'package:bluetooth_print/bluetooth_print_model.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:typed_data'; +import 'dart:io'; +import 'package:flutter/services.dart'; class BluetoothService { late BluetoothPrint _bluetoothPrint; @@ -44,7 +45,13 @@ class BluetoothService { final isConnected = await _bluetoothPrint.isConnected ?? false; _isConnected = isConnected; return isConnected; + } on SocketException { + // Tangani error koneksi secara spesifik + _isConnected = false; + return false; } catch (e) { + // Tangani error umum + _isConnected = false; return false; } } @@ -62,7 +69,17 @@ class BluetoothService { await prefs.setString('bluetooth_device_name', device.name ?? ''); return true; + } on SocketException { + // Tangani error koneksi secara spesifik + _isConnected = false; + return false; + } on PlatformException { + // Tangani error platform secara spesifik + _isConnected = false; + return false; } catch (e) { + // Tangani error umum + _isConnected = false; return false; } } @@ -80,12 +97,24 @@ class BluetoothService { /// Mencetak struk ke printer thermal Future printReceipt(Uint8List data) async { if (!_isConnected) { - throw Exception('Printer tidak terhubung'); + throw SocketException('Printer tidak terhubung'); } _isPrinting = true; try { await _bluetoothPrint.printRawData(data); + } on SocketException { + // Tangani error koneksi saat mencetak + _isPrinting = false; + rethrow; + } on PlatformException { + // Tangani error platform saat mencetak + _isPrinting = false; + rethrow; + } catch (e) { + // Tangani error umum saat mencetak + _isPrinting = false; + throw Exception('Gagal mencetak: $e'); } finally { _isPrinting = false; } @@ -124,7 +153,14 @@ class BluetoothService { isConnected = await checkConnection(); return isConnected; + } on SocketException { + // Tangani error koneksi secara spesifik + return false; + } on PlatformException { + // Tangani error platform secara spesifik + return false; } catch (e) { + // Tangani error umum return false; } } else { diff --git a/lib/services/esc_pos_print_service.dart b/lib/services/esc_pos_print_service.dart index a802f34..b2e7f2f 100644 --- a/lib/services/esc_pos_print_service.dart +++ b/lib/services/esc_pos_print_service.dart @@ -5,8 +5,6 @@ import 'package:cashumit/models/receipt_item.dart'; import 'package:flutter_esc_pos_utils/flutter_esc_pos_utils.dart'; import 'package:image/image.dart' as img; import 'dart:io'; -import 'dart:ui' as ui; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Service untuk menghasilkan perintah ESC/POS menggunakan flutter_esc_pos_utils @@ -342,7 +340,7 @@ class EscPosPrintService { final isConnectedBeforePrint = await bluetoothService.checkConnection(); if (!isConnectedBeforePrint) { print('Printer tidak terhubung saat akan mencetak'); - throw Exception('Printer tidak terhubung saat akan mencetak'); + throw SocketException('Printer tidak terhubung saat akan mencetak'); } print('Mengirim byte array ke printer...'); @@ -352,10 +350,20 @@ class EscPosPrintService { final Uint8List data = Uint8List.fromList(bytes); await bluetoothService.printReceipt(data); print('Perintah cetak berhasil dikirim'); + } on SocketException catch (e) { + print('Socket error saat mengirim perintah cetak ke printer: $e'); + throw SocketException('Koneksi ke printer terputus: ${e.message}'); + } on PlatformException catch (e) { + print('Platform error saat mengirim perintah cetak ke printer: $e'); + throw PlatformException(code: e.code, message: 'Error printer: ${e.message}'); } catch (printError) { print('Error saat mengirim perintah cetak ke printer: $printError'); throw Exception('Gagal mengirim perintah cetak: $printError'); } + } on SocketException { + rethrow; // Lempar ulang error koneksi + } on PlatformException { + rethrow; // Lempar ulang error platform } catch (e, stackTrace) { print('Error saat mencetak struk: $e'); print('Stack trace: $stackTrace'); diff --git a/lib/widgets/printing_status_card.dart b/lib/widgets/printing_status_card.dart index 2d90865..87b7d19 100644 --- a/lib/widgets/printing_status_card.dart +++ b/lib/widgets/printing_status_card.dart @@ -8,7 +8,7 @@ class PrintingStatusCard extends StatefulWidget { super.key, required this.isVisible, this.onDismiss, - }) : super(); + }); @override State createState() => _PrintingStatusCardState(); @@ -92,10 +92,10 @@ class _PrintingStatusCardState extends State padding: const EdgeInsets.all(20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - gradient: const LinearGradient( + gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ + colors: const [ Color(0xFF6A11CB), Color(0xFF2575FC), ], diff --git a/lib/widgets/receipt_speed_dial.dart b/lib/widgets/receipt_speed_dial.dart index e235022..fb2ee8b 100644 --- a/lib/widgets/receipt_speed_dial.dart +++ b/lib/widgets/receipt_speed_dial.dart @@ -12,9 +12,11 @@ class ReceiptSpeedDial extends StatelessWidget { final bool hasSourceAccount; final bool hasDestinationAccount; final Future Function() onSendToFirefly; + final VoidCallback onPrintingStart; + final VoidCallback onPrintingEnd; const ReceiptSpeedDial({ - Key? key, + super.key, required this.bluetoothService, required this.onCheckConnection, required this.onPrint, @@ -24,7 +26,9 @@ class ReceiptSpeedDial extends StatelessWidget { required this.hasSourceAccount, required this.hasDestinationAccount, required this.onSendToFirefly, - }) : super(key: key); + required this.onPrintingStart, + required this.onPrintingEnd, + }); @override Widget build(BuildContext context) { @@ -74,46 +78,60 @@ class ReceiptSpeedDial extends StatelessWidget { onTap: bluetoothService.isPrinting ? null : () async { - // Periksa koneksi secara real-time - final isConnected = await onCheckConnection(); - if (isConnected) { - onPrint(); - } else { - // Coba sambungkan kembali jika ada device yang tersimpan - if (bluetoothService.connectedDevice != null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Mencoba menyambungkan ke printer...')), - ); - try { - bool connectResult = await bluetoothService.connectToDevice(bluetoothService.connectedDevice!); - if (!connectResult) { - throw Exception('Gagal menyambungkan ke printer'); - } - // Tunggu sebentar untuk memastikan koneksi stabil - await Future.delayed(const Duration(milliseconds: 500)); - // Periksa koneksi lagi - final isConnectedAfterConnect = await onCheckConnection(); - if (isConnectedAfterConnect) { - onPrint(); - } else { + // Panggil callback untuk memulai printing status + onPrintingStart(); + + try { + // Periksa koneksi secara real-time + final isConnected = await onCheckConnection(); + if (isConnected) { + await onPrint(); + } else { + // Coba sambungkan kembali jika ada device yang tersimpan + if (bluetoothService.connectedDevice != null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Mencoba menyambungkan ke printer...')), + ); + try { + bool connectResult = await bluetoothService.connectToDevice(bluetoothService.connectedDevice!); + if (!connectResult) { + throw Exception('Gagal menyambungkan ke printer'); + } + // Tunggu sebentar untuk memastikan koneksi stabil + await Future.delayed(const Duration(milliseconds: 500)); + // Periksa koneksi lagi + final isConnectedAfterConnect = await onCheckConnection(); + if (isConnectedAfterConnect) { + await onPrint(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Gagal menyambungkan ke printer')), + ); + } + } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Gagal menyambungkan ke printer')), + SnackBar( + content: Text('Gagal menyambungkan ke printer: $e')), ); } - } catch (e) { + } else { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Gagal menyambungkan ke printer: $e')), + const SnackBar( + content: Text('Hubungkan printer terlebih dahulu')), ); } - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Hubungkan printer terlebih dahulu')), - ); } + } catch (e) { + // Tangani error secara umum + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Terjadi kesalahan: $e')), + ); + } finally { + // Pastikan printing status selalu diakhiri + onPrintingEnd(); } }, backgroundColor: Colors.purple,