Implement PrintingStatusCard in ReceiptSpeedDial and improve error handling for Bluetooth printer connection

master
a2nr 2025-09-05 21:44:38 +07:00
parent a2eedc8efc
commit b88c301d7d
5 changed files with 254 additions and 145 deletions

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:typed_data';
import 'package:cashumit/models/receipt_item.dart'; import 'package:cashumit/models/receipt_item.dart';
import 'package:cashumit/screens/add_item_screen.dart'; import 'package:cashumit/screens/add_item_screen.dart';
import 'package:bluetooth_print/bluetooth_print.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/widgets/custom_text_config_dialog.dart';
import 'package:cashumit/screens/webview_screen.dart'; import 'package:cashumit/screens/webview_screen.dart';
import 'package:cashumit/widgets/receipt_speed_dial.dart'; import 'package:cashumit/widgets/receipt_speed_dial.dart';
import 'package:cashumit/widgets/printing_status_card.dart';
// Import service baru // Import service baru
import 'package:cashumit/services/account_dialog_service.dart'; 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:provider/provider.dart';
import 'package:cashumit/providers/receipt_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 { class ReceiptScreen extends StatefulWidget {
const ReceiptScreen({super.key}); const ReceiptScreen({super.key});
@ -31,6 +35,9 @@ class ReceiptScreen extends StatefulWidget {
class _ReceiptScreenState extends State<ReceiptScreen> { class _ReceiptScreenState extends State<ReceiptScreen> {
// Bluetooth service // Bluetooth service
final BluetoothService _bluetoothService = BluetoothService(); final BluetoothService _bluetoothService = BluetoothService();
// Printing status
bool _isPrinting = false;
@override @override
void initState() { void initState() {
@ -108,25 +115,25 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
final receiptProvider = context.read<ReceiptProvider>(); final receiptProvider = context.read<ReceiptProvider>();
final state = receiptProvider.state; 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 { 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( await EscPosPrintService.printToThermalPrinter(
items: state.items, items: state.items,
transactionDate: state.transactionDate, transactionDate: state.transactionDate,
@ -138,13 +145,39 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Perintah cetak dikirim ke printer')), 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) { } catch (e) {
// Tangani error umum
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal mencetak struk: $e')), 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 { void _addItem() async {
final newItem = await showDialog<ReceiptItem>( final newItem = await showDialog<ReceiptItem>(
@ -330,95 +363,109 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Stack(
backgroundColor: children: [
Colors.grey[300], // Latar belakang abu-abu untuk efek struk Scaffold(
floatingActionButton: Consumer<ReceiptProvider>( backgroundColor:
builder: (context, receiptProvider, child) { Colors.grey[300], // Latar belakang abu-abu untuk efek struk
final state = receiptProvider.state; floatingActionButton: Consumer<ReceiptProvider>(
return ReceiptSpeedDial( builder: (context, receiptProvider, child) {
bluetoothService: _bluetoothService, final state = receiptProvider.state;
onCheckConnection: _checkBluetoothConnection, return ReceiptSpeedDial(
onPrint: _printToThermalPrinter, bluetoothService: _bluetoothService,
onSettings: _openSettings, onCheckConnection: _checkBluetoothConnection,
onReloadAccounts: receiptProvider.loadAccounts, onPrint: _printToThermalPrinter,
hasItems: state.items.isNotEmpty, onSettings: _openSettings,
hasSourceAccount: state.sourceAccountId != null, onReloadAccounts: receiptProvider.loadAccounts,
hasDestinationAccount: state.destinationAccountId != null, hasItems: state.items.isNotEmpty,
onSendToFirefly: _sendToFirefly, hasSourceAccount: state.sourceAccountId != null,
); hasDestinationAccount: state.destinationAccountId != null,
} onSendToFirefly: _sendToFirefly,
), onPrintingStart: _startPrinting,
body: SafeArea( onPrintingEnd: _endPrinting,
child: Center( );
// Membungkus dengan widget Center untuk memastikan struk berada di tengah }
child: SingleChildScrollView( ),
child: Column( body: SafeArea(
crossAxisAlignment: CrossAxisAlignment child: Center(
.center, // Memusatkan konten secara horizontal // Membungkus dengan widget Center untuk memastikan struk berada di tengah
children: [ child: SingleChildScrollView(
// Background untuk efek kertas struk tersobek di bagian atas child: Column(
Container( crossAxisAlignment: CrossAxisAlignment
width: 360, .center, // Memusatkan konten secara horizontal
color: const Color(0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar children: [
child: const Column( // Background untuk efek kertas struk tersobek di bagian atas
children: [ Container(
SizedBox(height: 15), // Jarak atas yang lebih besar width: 360,
ReceiptTearTop(), // Efek kertas struk tersobek di bagian atas 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 // Konten struk
Consumer<ReceiptProvider>( Consumer<ReceiptProvider>(
builder: (context, receiptProvider, child) { builder: (context, receiptProvider, child) {
final state = receiptProvider.state; final state = receiptProvider.state;
return ReceiptBody( return ReceiptBody(
items: state.items, items: state.items,
sourceAccountName: state.sourceAccountName, sourceAccountName: state.sourceAccountName,
destinationAccountName: state.destinationAccountName, destinationAccountName: state.destinationAccountName,
onOpenStoreInfoConfig: _openStoreInfoConfig, onOpenStoreInfoConfig: _openStoreInfoConfig,
onSelectSourceAccount: () => _selectSourceAccount(), // Memanggil fungsi langsung onSelectSourceAccount: () => _selectSourceAccount(), // Memanggil fungsi langsung
onSelectDestinationAccount: () => _selectDestinationAccount(), // Memanggil fungsi langsung onSelectDestinationAccount: () => _selectDestinationAccount(), // Memanggil fungsi langsung
onOpenCustomTextConfig: _openCustomTextConfig, onOpenCustomTextConfig: _openCustomTextConfig,
total: state.total, total: state.total,
onEditItem: (index) => _editItem(index), // Memanggil fungsi langsung onEditItem: (index) => _editItem(index), // Memanggil fungsi langsung
onRemoveItem: (index) { onRemoveItem: (index) {
// Validasi index untuk mencegah error // Validasi index untuk mencegah error
if (index >= 0 && index < state.items.length) { if (index >= 0 && index < state.items.length) {
// Hapus item dari daftar melalui provider // Hapus item dari daftar melalui provider
receiptProvider.removeItem(index); receiptProvider.removeItem(index);
} else { } else {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text(
"Gagal menghapus item: Index tidak valid.")), "Gagal menghapus item: Index tidak valid.")),
); );
} }
} }
}, },
onAddItem: () => _addItem(), // Memanggil fungsi langsung onAddItem: () => _addItem(), // Memanggil fungsi langsung
); );
} }
), ),
// Background untuk efek kertas struk tersobek di bagian bawah // Background untuk efek kertas struk tersobek di bagian bawah
Container( Container(
width: 360, width: 360,
color: const Color(0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar color: const Color(0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar
child: const Column( child: const Column(
children: [ children: [
ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah
SizedBox(height: 15), // Jarak bawah yang lebih besar SizedBox(height: 15), // Jarak bawah yang lebih besar
], ],
), ),
),
],
), ),
], ),
), ),
), ),
), ),
), PrintingStatusCard(
isVisible: _isPrinting,
onDismiss: () {
setState(() {
_isPrinting = false;
});
},
),
],
); );
} }
} }

View File

@ -1,7 +1,8 @@
import 'package:bluetooth_print/bluetooth_print.dart'; import 'package:bluetooth_print/bluetooth_print.dart';
import 'package:bluetooth_print/bluetooth_print_model.dart'; import 'package:bluetooth_print/bluetooth_print_model.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'dart:typed_data'; import 'dart:io';
import 'package:flutter/services.dart';
class BluetoothService { class BluetoothService {
late BluetoothPrint _bluetoothPrint; late BluetoothPrint _bluetoothPrint;
@ -44,7 +45,13 @@ class BluetoothService {
final isConnected = await _bluetoothPrint.isConnected ?? false; final isConnected = await _bluetoothPrint.isConnected ?? false;
_isConnected = isConnected; _isConnected = isConnected;
return isConnected; return isConnected;
} on SocketException {
// Tangani error koneksi secara spesifik
_isConnected = false;
return false;
} catch (e) { } catch (e) {
// Tangani error umum
_isConnected = false;
return false; return false;
} }
} }
@ -62,7 +69,17 @@ class BluetoothService {
await prefs.setString('bluetooth_device_name', device.name ?? ''); await prefs.setString('bluetooth_device_name', device.name ?? '');
return true; 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) { } catch (e) {
// Tangani error umum
_isConnected = false;
return false; return false;
} }
} }
@ -80,12 +97,24 @@ class BluetoothService {
/// Mencetak struk ke printer thermal /// Mencetak struk ke printer thermal
Future<void> printReceipt(Uint8List data) async { Future<void> printReceipt(Uint8List data) async {
if (!_isConnected) { if (!_isConnected) {
throw Exception('Printer tidak terhubung'); throw SocketException('Printer tidak terhubung');
} }
_isPrinting = true; _isPrinting = true;
try { try {
await _bluetoothPrint.printRawData(data); 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 { } finally {
_isPrinting = false; _isPrinting = false;
} }
@ -124,7 +153,14 @@ class BluetoothService {
isConnected = await checkConnection(); isConnected = await checkConnection();
return isConnected; return isConnected;
} on SocketException {
// Tangani error koneksi secara spesifik
return false;
} on PlatformException {
// Tangani error platform secara spesifik
return false;
} catch (e) { } catch (e) {
// Tangani error umum
return false; return false;
} }
} else { } else {

View File

@ -5,8 +5,6 @@ import 'package:cashumit/models/receipt_item.dart';
import 'package:flutter_esc_pos_utils/flutter_esc_pos_utils.dart'; import 'package:flutter_esc_pos_utils/flutter_esc_pos_utils.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'dart:io'; import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Service untuk menghasilkan perintah ESC/POS menggunakan flutter_esc_pos_utils /// Service untuk menghasilkan perintah ESC/POS menggunakan flutter_esc_pos_utils
@ -342,7 +340,7 @@ class EscPosPrintService {
final isConnectedBeforePrint = await bluetoothService.checkConnection(); final isConnectedBeforePrint = await bluetoothService.checkConnection();
if (!isConnectedBeforePrint) { if (!isConnectedBeforePrint) {
print('Printer tidak terhubung saat akan mencetak'); 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...'); print('Mengirim byte array ke printer...');
@ -352,10 +350,20 @@ class EscPosPrintService {
final Uint8List data = Uint8List.fromList(bytes); final Uint8List data = Uint8List.fromList(bytes);
await bluetoothService.printReceipt(data); await bluetoothService.printReceipt(data);
print('Perintah cetak berhasil dikirim'); 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) { } catch (printError) {
print('Error saat mengirim perintah cetak ke printer: $printError'); print('Error saat mengirim perintah cetak ke printer: $printError');
throw Exception('Gagal mengirim perintah cetak: $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) { } catch (e, stackTrace) {
print('Error saat mencetak struk: $e'); print('Error saat mencetak struk: $e');
print('Stack trace: $stackTrace'); print('Stack trace: $stackTrace');

View File

@ -8,7 +8,7 @@ class PrintingStatusCard extends StatefulWidget {
super.key, super.key,
required this.isVisible, required this.isVisible,
this.onDismiss, this.onDismiss,
}) : super(); });
@override @override
State<PrintingStatusCard> createState() => _PrintingStatusCardState(); State<PrintingStatusCard> createState() => _PrintingStatusCardState();
@ -92,10 +92,10 @@ class _PrintingStatusCardState extends State<PrintingStatusCard>
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
gradient: const LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: const [
Color(0xFF6A11CB), Color(0xFF6A11CB),
Color(0xFF2575FC), Color(0xFF2575FC),
], ],

View File

@ -12,9 +12,11 @@ class ReceiptSpeedDial extends StatelessWidget {
final bool hasSourceAccount; final bool hasSourceAccount;
final bool hasDestinationAccount; final bool hasDestinationAccount;
final Future<void> Function() onSendToFirefly; final Future<void> Function() onSendToFirefly;
final VoidCallback onPrintingStart;
final VoidCallback onPrintingEnd;
const ReceiptSpeedDial({ const ReceiptSpeedDial({
Key? key, super.key,
required this.bluetoothService, required this.bluetoothService,
required this.onCheckConnection, required this.onCheckConnection,
required this.onPrint, required this.onPrint,
@ -24,7 +26,9 @@ class ReceiptSpeedDial extends StatelessWidget {
required this.hasSourceAccount, required this.hasSourceAccount,
required this.hasDestinationAccount, required this.hasDestinationAccount,
required this.onSendToFirefly, required this.onSendToFirefly,
}) : super(key: key); required this.onPrintingStart,
required this.onPrintingEnd,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -74,46 +78,60 @@ class ReceiptSpeedDial extends StatelessWidget {
onTap: bluetoothService.isPrinting onTap: bluetoothService.isPrinting
? null ? null
: () async { : () async {
// Periksa koneksi secara real-time // Panggil callback untuk memulai printing status
final isConnected = await onCheckConnection(); onPrintingStart();
if (isConnected) {
onPrint(); try {
} else { // Periksa koneksi secara real-time
// Coba sambungkan kembali jika ada device yang tersimpan final isConnected = await onCheckConnection();
if (bluetoothService.connectedDevice != null) { if (isConnected) {
ScaffoldMessenger.of(context).showSnackBar( await onPrint();
const SnackBar( } else {
content: Text('Mencoba menyambungkan ke printer...')), // Coba sambungkan kembali jika ada device yang tersimpan
); if (bluetoothService.connectedDevice != null) {
try { ScaffoldMessenger.of(context).showSnackBar(
bool connectResult = await bluetoothService.connectToDevice(bluetoothService.connectedDevice!); const SnackBar(
if (!connectResult) { content: Text('Mencoba menyambungkan ke printer...')),
throw Exception('Gagal menyambungkan ke printer'); );
} try {
// Tunggu sebentar untuk memastikan koneksi stabil bool connectResult = await bluetoothService.connectToDevice(bluetoothService.connectedDevice!);
await Future.delayed(const Duration(milliseconds: 500)); if (!connectResult) {
// Periksa koneksi lagi throw Exception('Gagal menyambungkan ke printer');
final isConnectedAfterConnect = await onCheckConnection(); }
if (isConnectedAfterConnect) { // Tunggu sebentar untuk memastikan koneksi stabil
onPrint(); await Future.delayed(const Duration(milliseconds: 500));
} else { // 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( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Gagal menyambungkan ke printer')), content: Text('Gagal menyambungkan ke printer: $e')),
); );
} }
} catch (e) { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( const SnackBar(
content: Text('Gagal menyambungkan ke printer: $e')), 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, backgroundColor: Colors.purple,