import 'dart:async'; import 'package:flutter/material.dart'; import 'package:cashumit/models/receipt_item.dart'; import 'package:cashumit/screens/add_item_screen.dart'; import 'package:bluetooth_print/bluetooth_print.dart'; import 'package:cashumit/services/bluetooth_service.dart'; import 'package:cashumit/widgets/receipt_body.dart'; import 'package:cashumit/widgets/receipt_tear_effect.dart'; import 'package:cashumit/widgets/store_info_config_dialog.dart'; import 'package:cashumit/widgets/custom_text_config_dialog.dart'; import 'package:cashumit/widgets/receipt_speed_dial.dart'; import 'package:cashumit/widgets/printing_status_card.dart'; import 'package:cashumit/widgets/transaction_submission_dialog.dart'; import 'package:cashumit/models/group_submission_result.dart'; import 'package:cashumit/services/account_dialog_service.dart'; import 'package:cashumit/services/esc_pos_print_service.dart'; import 'package:cashumit/services/receipt_service.dart'; import 'package:provider/provider.dart'; import 'package:cashumit/providers/receipt_provider.dart'; import 'dart:io'; import 'package:flutter/services.dart'; class ReceiptScreen extends StatefulWidget { const ReceiptScreen({super.key}); @override State createState() => _ReceiptScreenState(); } class _ReceiptScreenState extends State { final BluetoothService _bluetoothService = BluetoothService(); bool _isPrinting = false; // Simpan subscription agar bisa dibatalkan (perbaikan memory leak). StreamSubscription? _bluetoothStateSubscription; @override void initState() { super.initState(); _initBluetooth(); _loadSavedBluetoothDevice(); WidgetsBinding.instance.addPostFrameCallback((_) { final receiptProvider = context.read(); receiptProvider.initialize(); }); } @override void dispose() { _bluetoothStateSubscription?.cancel(); super.dispose(); } Future _loadSavedBluetoothDevice() async { await _bluetoothService.loadSavedDevice(); } Future _checkBluetoothConnection() async { return await _bluetoothService.checkConnection(); } Future _initBluetooth() async { await _bluetoothService.initialize(); // Simpan subscription agar bisa dibatalkan di dispose. _bluetoothStateSubscription = _bluetoothService.state.listen((state) { if (!mounted) return; switch (state) { case BluetoothPrint.connected: ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Printer terhubung')), ); break; case BluetoothPrint.disconnected: ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Printer terputus')), ); break; default: break; } }); } Future _printToThermalPrinter() async { final receiptProvider = context.read(); final state = receiptProvider.state; try { 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( groups: state.groups, transactionDate: state.transactionDate, destinationAccountName: state.destinationAccountName, context: context, bluetoothService: _bluetoothService, paymentInfo: PaymentInfo( paymentAmount: state.paymentAmount, isTip: state.hasTip, ), tipSourceAccountName: state.tipSourceAccountName, ); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Perintah cetak dikirim ke printer')), ); } on SocketException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Koneksi ke printer terputus: ${e.message}')), ); } on PlatformException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error printer: ${e.message}')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal mencetak struk: $e')), ); } } void _startPrinting() { setState(() => _isPrinting = true); } void _endPrinting() { setState(() => _isPrinting = false); } // ============ GRUP & ITEM ============ void _addGroup() { context.read().addGroup(); } void _removeGroup(String groupId) { context.read().removeGroup(groupId); } void _addItem(String groupId) async { final newItem = await showDialog( context: context, builder: (context) => const AddItemScreen(), ); if (newItem != null) { context.read().addItem(groupId, newItem); } } void _editItem(String groupId, int index) async { final receiptProvider = context.read(); final group = receiptProvider.state.groups.firstWhere((g) => g.id == groupId); if (index < 0 || index >= group.items.length) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Gagal mengedit item: Index tidak valid.")), ); } return; } final originalItem = group.items[index]; final editedItem = await showDialog( context: context, builder: (context) => AddItemScreen.fromItem(originalItem), ); if (editedItem != null) { receiptProvider.editItem(groupId, index, editedItem); } } void _removeItem(String groupId, int index) { final receiptProvider = context.read(); final group = receiptProvider.state.groups.firstWhere((g) => g.id == groupId); if (index < 0 || index >= group.items.length) return; receiptProvider.removeItem(groupId, index); } // ============ AKUN ============ Future _selectGroupSource(String groupId) async { final receiptProvider = context.read(); final selectedAccount = await AccountDialogService.showSourceAccountDialog( context, receiptProvider.state.accounts); if (selectedAccount != null) { receiptProvider.selectGroupSource( groupId, selectedAccount['id'].toString(), selectedAccount['name'].toString(), ); } } // ============ TIP ============ Future _setTipSource() async { final receiptProvider = context.read(); final selectedAccount = await AccountDialogService.showSourceAccountDialog( context, receiptProvider.state.accounts); if (selectedAccount != null) { receiptProvider.setTipSourceAccount( selectedAccount['id'].toString(), selectedAccount['name'].toString(), ); } } void _clearTip() { context.read().clearTip(); } // ============ TANGGAL ============ Future _changeDate() async { final receiptProvider = context.read(); final currentDate = receiptProvider.state.transactionDate; final pickedDate = await showDatePicker( context: context, initialDate: currentDate, firstDate: DateTime(2000), lastDate: DateTime(2100), ); if (pickedDate == null) return; if (!mounted) return; final pickedTime = await showTimePicker( context: context, initialTime: TimeOfDay.fromDateTime(currentDate), ); if (pickedTime == null) return; final newDate = DateTime( pickedDate.year, pickedDate.month, pickedDate.day, pickedTime.hour, pickedTime.minute, ); receiptProvider.setTransactionDate(newDate); } // ============ FIREFLY ============ Future _sendToFirefly() async { final receiptProvider = context.read(); final state = receiptProvider.state; // Validasi awal final groupsWithItems = state.groups.where((g) => g.items.isNotEmpty).toList(); if (groupsWithItems.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Tidak ada item untuk dikirim')), ); return; } if (state.destinationAccountId == null || state.fireflyUrl == null || state.accessToken == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Akun tujuan / kredensial belum lengkap')), ); return; } // Hitung tipAmount jika tip aktif final hasTip = state.hasTip; final tipAmount = hasTip ? state.tipAmount : null; // Bangun daftar task dari state struk saat ini final tasks = ReceiptService.buildTasksFromReceipt( groups: groupsWithItems, destinationAccountId: state.destinationAccountId!, destinationAccountName: state.destinationAccountName ?? '', transactionDate: state.transactionDate, tipSourceAccountId: hasTip ? state.tipSourceAccountId : null, tipSourceAccountName: hasTip ? state.tipSourceAccountName : null, tipAmount: tipAmount, ); // Tampilkan popup pengiriman final results = await showDialog>( context: context, barrierDismissible: false, builder: (context) => TransactionSubmissionDialog( tasks: tasks, baseUrl: state.fireflyUrl!, accessToken: state.accessToken!, ), ); if (results == null) return; if (!mounted) return; final successes = results.where((r) => r.isSuccess && r.transactionId != null).toList(); final failures = results.where((r) => r.isFailed).toList(); // Link transaksi yang berhasil dapat di-copy/open langsung dari popup card. // Simpan grup yang gagal sebagai nota lokal + reset state. await receiptProvider.saveFailedGroupsLocally(results); if (!mounted) return; if (failures.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${successes.length} transaksi berhasil dikirim ke Firefly III'), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${successes.length} berhasil, ${failures.length} gagal disimpan sebagai nota lokal'), duration: const Duration(seconds: 4), ), ); } } // ============ CONFIG DIALOGS ============ Future _openStoreInfoConfig() async { await showDialog( context: context, builder: (context) => const StoreInfoConfigDialog(), ); // Refresh store info widget setelah dialog tutup if (mounted) { setState(() {}); } } Future _openCustomTextConfig() async { await showDialog( context: context, builder: (context) => const CustomTextConfigDialog(), ); if (mounted) { setState(() {}); } } void _openSettings() async { final result = await Navigator.pushNamed(context, '/config'); if (result == true) { context.read().loadCredentialsAndAccounts(); } } void _openLocalReceipts() async { await Navigator.pushNamed(context, '/local-receipts'); } @override Widget build(BuildContext context) { final receiptProvider = context.watch(); final state = receiptProvider.state; return Scaffold( backgroundColor: Colors.grey[300], floatingActionButton: ReceiptSpeedDial( bluetoothService: _bluetoothService, onCheckConnection: _checkBluetoothConnection, onPrint: _printToThermalPrinter, onSettings: _openSettings, onReloadAccounts: receiptProvider.loadAccounts, hasItems: state.hasItems, allGroupsHaveSource: state.allGroupsHaveSource, hasDestination: state.destinationAccountId != null, onSendToFirefly: _sendToFirefly, onPrintingStart: _startPrinting, onPrintingEnd: _endPrinting, onOpenLocalReceipts: _openLocalReceipts, ), body: Stack( children: [ Center( child: SingleChildScrollView( child: Container( width: 360, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Tear effect atas Container( color: const Color(0xFFE0E0E0), child: const Column( children: [ SizedBox(height: 15), ReceiptTearTop(), ], ), ), // Receipt body ReceiptBody( groups: state.groups, transactionDate: state.transactionDate, onOpenStoreInfoConfig: _openStoreInfoConfig, onChangeDate: _changeDate, onOpenCustomTextConfig: _openCustomTextConfig, total: state.total, onEditItem: (groupId, index) => _editItem(groupId, index), onRemoveItem: (groupId, index) => _removeItem(groupId, index), onAddItem: (groupId) => _addItem(groupId), onRemoveGroup: (groupId) => _removeGroup(groupId), onSelectGroupSource: (groupId) => _selectGroupSource(groupId), onAddGroup: _addGroup, onSetTip: _setTipSource, onClearTip: _clearTip, ), // Tear effect bawah Container( color: const Color(0xFFE0E0E0), child: const Column( children: [ ReceiptTearBottom(), SizedBox(height: 15), ], ), ), ], ), ), ), ), if (_isPrinting) PrintingStatusCard( isVisible: _isPrinting, onDismiss: () { setState(() => _isPrinting = false); }, ), ], ), ); } }