import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; // import 'dart:io'; // Add this import for file operations // PDF Removed import 'dart:typed_data'; // Untuk Uint8List import 'package:intl/intl.dart'; // Untuk format angka import 'package:cashumit/models/receipt_item.dart'; import 'package:cashumit/screens/add_item_screen.dart'; // import 'package:cashumit/services/pdf_export_service.dart'; // PDF Removed import 'package:cashumit/services/firefly_api_service.dart'; import 'package:cashumit/services/struk_text_generator.dart'; // Tambahkan import ini import 'package:cashumit/services/esc_pos_print_service.dart'; // Tambahkan import ini import 'package:bluetooth_print/bluetooth_print.dart'; import 'package:bluetooth_print/bluetooth_print_model.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; // Tambahkan import ini // Import widget komponen struk baru import 'package:cashumit/widgets/receipt_tear_effect.dart'; import 'package:cashumit/widgets/store_info_widget.dart'; import 'package:cashumit/widgets/dotted_line.dart'; import 'package:cashumit/widgets/receipt_item_widget.dart'; import 'package:cashumit/widgets/add_item_button.dart'; import 'package:cashumit/widgets/store_disclaimer.dart'; import 'package:cashumit/widgets/thank_you_pantun.dart'; import 'package:cashumit/widgets/store_info_config_dialog.dart'; import 'package:cashumit/widgets/custom_text_config_dialog.dart'; import 'package:cashumit/widgets/account_settings_widget.dart'; import 'package:cashumit/widgets/horizontal_divider.dart'; // Tambahkan import ini // import 'package:cashumit/widgets/struk_printer.dart'; // DIHAPUS import 'package:cashumit/screens/webview_screen.dart'; // import 'package:google_fonts/google_fonts.dart'; // DIHAPUS karena tidak digunakan di file ini class ReceiptScreen extends StatefulWidget { const ReceiptScreen({super.key}); @override State createState() => _ReceiptScreenState(); } class _ReceiptScreenState extends State { List items = []; final String cashierId = 'KSR001'; final String transactionId = 'TXN202508200001'; // Fields for transaction settings directly in main UI late DateTime _transactionDate; List> _accounts = []; String? _sourceAccountId; String? _sourceAccountName; String? _destinationAccountId; String? _destinationAccountName; // Controllers for manual account input final TextEditingController _sourceAccountController = TextEditingController(); final TextEditingController _destinationAccountController = TextEditingController(); // Bluetooth printer variables late BluetoothPrint bluetoothPrint; bool _bluetoothConnected = false; BluetoothDevice? _bluetoothDevice; bool _isPrinting = false; // State variable to track printing status @override void initState() { super.initState(); print('Inisialisasi ReceiptScreen...'); bluetoothPrint = BluetoothPrint.instance; _transactionDate = DateTime.now(); _loadCredentialsAndAccounts(); _initBluetooth(); _loadSavedBluetoothDevice(); print('Selesai inisialisasi ReceiptScreen'); } @override void didChangeDependencies() { super.didChangeDependencies(); // Memuat ulang kredensial dan akun setiap kali ada perubahan dependencies // Ini memastikan akun dimuat ulang ketika pengguna kembali dari ConfigScreen _loadCredentialsAndAccounts(); } @override void dispose() { _sourceAccountController.dispose(); _destinationAccountController.dispose(); super.dispose(); } String? _fireflyUrl; String? _accessToken; /// Memuat kredensial dari shared preferences dan kemudian memuat akun. /// /// Metode ini dipanggil saat: /// 1. Widget pertama kali dibuat (initState) /// 2. Pengguna kembali dari ConfigScreen (melalui _openSettings) /// 3. Pengguna mengetuk tombol "Muat Ulang Akun" /// /// Jika kredensial berubah atau daftar akun kosong, metode ini akan memanggil /// _loadAccounts untuk memperbarui daftar akun dari API Firefly III. Future _loadCredentialsAndAccounts() async { final prefs = await SharedPreferences.getInstance(); final url = prefs.getString('firefly_url'); final token = prefs.getString('firefly_token'); if (url == null || token == null || url.isEmpty || token.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Kredensial Firefly III belum dikonfigurasi. Silakan atur di menu Pengaturan.')), ); } return; } // Periksa apakah kredensial berubah final credentialsChanged = _fireflyUrl != url || _accessToken != token; setState(() { _fireflyUrl = url; _accessToken = token; }); // Jika kredensial ada dan berubah, lanjutkan untuk memuat akun if (credentialsChanged) { _loadAccounts(); } else if (_accounts.isEmpty) { // Jika akun belum pernah dimuat, muat sekarang _loadAccounts(); } } /// Memuat device bluetooth yang tersimpan Future _loadSavedBluetoothDevice() async { final prefs = await SharedPreferences.getInstance(); final deviceAddress = prefs.getString('bluetooth_device_address'); final deviceName = prefs.getString('bluetooth_device_name'); if (deviceAddress != null && deviceName != null) { final device = BluetoothDevice(); device.name = deviceName; device.address = deviceAddress; setState(() { _bluetoothDevice = device; }); } } /// Memeriksa status koneksi Bluetooth printer secara real-time Future _checkBluetoothConnection() async { try { final isConnected = await bluetoothPrint.isConnected ?? false; print('Status koneksi Bluetooth printer (real-time): $isConnected'); // Update state jika perlu if (isConnected != _bluetoothConnected) { setState(() { _bluetoothConnected = isConnected; }); print('Status koneksi Bluetooth diperbarui di state: $isConnected'); } return isConnected; } catch (e) { print('Error saat memeriksa koneksi Bluetooth: $e'); return false; } } /// Memuat daftar akun sumber (revenue) dan tujuan (asset) dari API. /// /// Metode ini mengambil akun revenue dan asset secara terpisah dari API Firefly III, /// kemudian menggabungkannya ke dalam satu daftar `_accounts` yang digunakan /// untuk mengisi dropdown akun sumber dan tujuan. /// /// Jika berhasil memuat akun, metode ini akan menampilkan pesan jumlah akun yang dimuat. /// Jika tidak ada akun yang dimuat, metode ini akan menampilkan pesan peringatan. /// Jika terjadi kesalahan saat memuat akun, metode ini akan menampilkan pesan kesalahan. Future _loadAccounts() async { if (_fireflyUrl == null || _accessToken == null) { return; } try { // Tampilkan pesan loading if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Memuat daftar akun dari Firefly III...')), ); } // Mengambil akun revenue final revenueAccounts = await FireflyApiService.fetchAccounts( baseUrl: _fireflyUrl!, accessToken: _accessToken!, type: 'revenue', ); // Mengambil akun asset final assetAccounts = await FireflyApiService.fetchAccounts( baseUrl: _fireflyUrl!, accessToken: _accessToken!, type: 'asset', ); // Menggabungkan akun revenue dan asset untuk dropdown final allAccounts = >[]; for (var account in revenueAccounts) { allAccounts.add({ 'id': account.id, 'name': account.name, 'type': account.type, }); } for (var account in assetAccounts) { allAccounts.add({ 'id': account.id, 'name': account.name, 'type': account.type, }); } setState(() { _accounts = allAccounts; }); if (mounted) { if (allAccounts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Tidak ada akun yang ditemukan. Pastikan kredensial dan koneksi internet Anda benar.')), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Berhasil memuat ${allAccounts.length} akun dari Firefly III')), ); } } } catch (error) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal memuat akun: $error')), ); } } } /// Inisialisasi Bluetooth printer Future _initBluetooth() async { // Memeriksa status koneksi Bluetooth final isConnected = await bluetoothPrint.isConnected ?? false; if (isConnected) { setState(() { _bluetoothConnected = true; }); } // Listen to bluetooth state changes bluetoothPrint.state.listen((state) { if (mounted) { switch (state) { case BluetoothPrint.connected: setState(() { _bluetoothConnected = true; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Printer terhubung')), ); } break; case BluetoothPrint.disconnected: setState(() { _bluetoothConnected = false; // Jangan set _bluetoothDevice ke null, biarkan device yang sudah dipilih }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Printer terputus')), ); } break; default: break; } } }); } /// Cetak struk ke printer thermal Future _printToThermalPrinter() async { print('=== FUNGSI _printToThermalPrinter DIPANGGIL ==='); print('Memulai proses pencetakan struk...'); print('Jumlah item: ${items.length}'); print('Tanggal transaksi: $_transactionDate'); print('ID kasir: $cashierId'); print('ID transaksi: $transactionId'); // Periksa koneksi printer secara real-time bool isConnected = await _checkBluetoothConnection(); print('Status koneksi printer awal (real-time): $isConnected'); if (!isConnected) { print('Printer tidak terhubung'); if (!mounted) return; // Coba sambungkan kembali jika ada device yang tersimpan if (_bluetoothDevice != null) { print('Mencoba menyambungkan kembali ke printer: ${_bluetoothDevice!.name}'); print('Alamat printer: ${_bluetoothDevice!.address}'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Mencoba menyambungkan kembali ke printer...')), ); try { // Putuskan koneksi yang mungkin tersisa try { await bluetoothPrint.disconnect(); print('Berhasil memutuskan koneksi yang tersisa'); } catch (disconnectError) { print('Tidak ada koneksi yang perlu diputuskan: $disconnectError'); } // Tunggu sebentar sebelum menyambungkan kembali await Future.delayed(const Duration(milliseconds: 500)); // Sambungkan kembali await bluetoothPrint.connect(_bluetoothDevice!); print('Berhasil menyambungkan kembali ke printer'); // Tunggu sebentar untuk memastikan koneksi stabil await Future.delayed(const Duration(milliseconds: 500)); // Periksa koneksi lagi isConnected = await _checkBluetoothConnection(); print('Status koneksi printer setelah reconnect (real-time): $isConnected'); if (isConnected) { // Perbarui status koneksi setState(() { _bluetoothConnected = true; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Printer berhasil terhubung kembali')), ); } else { throw Exception('Koneksi tidak stabil setelah reconnect'); } } catch (e) { print('Gagal menyambungkan kembali ke printer: $e'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal menyambungkan kembali ke printer: $e')), ); return; } } else { print('Tidak ada device printer yang tersimpan'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Harap hubungkan printer terlebih dahulu')), ); return; } } else { print('Printer sudah terhubung'); } try { print('Menghasilkan byte array ESC/POS menggunakan flutter_esc_pos_utils...'); print('Jumlah item: ${items.length}'); print('Tanggal transaksi: $_transactionDate'); print('ID kasir: $cashierId'); print('ID transaksi: $transactionId'); // Generate struk dalam format byte array menggunakan EscPosPrintService final bytes = await EscPosPrintService.generateEscPosBytes( items: items, transactionDate: _transactionDate, cashierId: cashierId, transactionId: transactionId, ); print('Byte array ESC/POS berhasil dihasilkan'); print('Jumlah byte: ${bytes.length}'); // Tampilkan byte array untuk debugging (dalam format hex) print('Isi byte array (hex):'); if (bytes.length <= 1000) { // Batasi tampilan untuk mencegah output terlalu panjang print(bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')); } else { print('Terlalu banyak byte untuk ditampilkan (${bytes.length} bytes)'); } // Verifikasi koneksi sebelum mencetak final isConnectedBeforePrint = await bluetoothPrint.isConnected ?? false; if (!isConnectedBeforePrint) { print('Printer tidak terhubung saat akan mencetak'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Printer tidak terhubung saat akan mencetak')), ); return; } print('Mengirim byte array ke printer...'); bool printSuccess = false; String printError = ''; try { // Coba kirim byte array beberapa kali jika gagal for (int attempt = 1; attempt <= 3; attempt++) { print('Percobaan cetak ke-$attempt'); try { // Konversi List ke Uint8List final Uint8List data = Uint8List.fromList(bytes); await bluetoothPrint.printRawData(data); print('Perintah cetak berhasil dikirim pada percobaan ke-$attempt'); printSuccess = true; break; } catch (e) { print('Error saat mengirim perintah cetak pada percobaan ke-$attempt: $e'); printError = e.toString(); if (attempt < 3) { // Tunggu sebentar sebelum mencoba lagi await Future.delayed(const Duration(milliseconds: 500)); } } } if (printSuccess) { print('Perintah cetak berhasil dikirim'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Perintah cetak dikirim ke printer')), ); } else { print('Gagal mengirim perintah cetak setelah 3 percobaan: $printError'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal mengirim perintah cetak: $printError')), ); return; } } catch (printError) { print('Error saat mengirim perintah cetak ke printer: $printError'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal mengirim perintah cetak: $printError')), ); return; } } catch (e, stackTrace) { print('Error saat mencetak struk: $e'); print('Stack trace: $stackTrace'); // Cetak detail error tambahan print('Tipe error: ${e.runtimeType}'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal mencetak struk: $e')), ); return; } } void _addItem() async { final newItem = await showDialog( context: context, builder: (context) => const AddItemScreen(), // Tampilkan sebagai dialog ); if (newItem != null) { setState(() { items.add(newItem); }); } } void _editItem(int index) async { // Pastikan index valid sebelum mengakses item if (index < 0 || index >= items.length) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Gagal mengedit item: Index tidak valid.")), ); } return; } final originalItem = items[index]; // Simpan item asli untuk fallback jika diperlukan final editedItem = await showDialog( context: context, builder: (context) => AddItemScreen.fromItem(originalItem), // Tampilkan sebagai dialog ); // Hanya update jika item tidak null (bukan hasil dari 'Batal') if (editedItem != null) { setState(() { items[index] = editedItem; }); } // Jika editedItem null (dibatalkan), tidak ada perubahan pada items[index] } /// Menampilkan dialog konfirmasi sebelum menghapus item /// /// Mengembalikan Future yang menunjukkan apakah pengguna mengkonfirmasi penghapusan. /// - true: Pengguna mengkonfirmasi. /// - false: Pengguna membatalkan. /// - null: Dialog ditutup tanpa memilih opsi. Future _confirmRemoveItem(int index) async { // Validasi index untuk mencegah error if (index < 0 || index >= items.length) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Gagal menghapus item: Index tidak valid.")), ); } return false; // Atau throw exception tergantung kebijakan error handling } // Simpan referensi item sebelum dialog muncul final itemToRemove = items[index]; final bool? shouldRemove = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Konfirmasi Hapus'), content: Text( 'Apakah Anda yakin ingin menghapus item ${itemToRemove.description}'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), // Batal child: const Text('Batal'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), // Hapus child: const Text('Hapus'), ), ], ), ); // Tampilkan snackbar informasi berdasarkan hasil konfirmasi if (shouldRemove == true) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( "Item '${itemToRemove.description}' akan dihapus setelah gestur selesai.")), ); } } else if (shouldRemove == false) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( "Penghapusan item '${itemToRemove.description}' dibatalkan.")), ); } } // Jika shouldRemove == null, pengguna menutup dialog, tidak perlu snackbar tambahan return shouldRemove; } /// Menampilkan dialog untuk memilih akun sumber (revenue). /// /// Metode ini memfilter daftar akun untuk hanya menampilkan akun dengan tipe 'revenue', /// kemudian menampilkan dialog dengan daftar akun tersebut. Ketika pengguna memilih /// akun, metode ini akan memperbarui state `_sourceAccountId` dan `_sourceAccountName`. Future _selectSourceAccount() async { if (_accounts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Daftar akun kosong. Pastikan kredensial sudah diatur dan akun telah dimuat. Klik "Muat Ulang Akun" untuk mencoba lagi.')), ); return; } // Filter akun sumber (revenue) final revenueAccounts = _accounts.where((account) => account['type'] == 'revenue').toList(); if (revenueAccounts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Tidak ada akun sumber (revenue) yang ditemukan. Klik "Muat Ulang Akun" untuk mencoba lagi atau periksa akun Anda di Firefly III.')), ); return; } final selectedAccount = await showDialog?>( context: context, builder: (context) { return AlertDialog( title: const Text('Pilih Akun Sumber'), content: SizedBox( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: revenueAccounts.length, itemBuilder: (context, index) { final account = revenueAccounts[index]; return ListTile( title: Text(account['name']), subtitle: Text(account['type']), onTap: () => Navigator.of(context).pop(account), ); }, ), ), ); }, ); if (selectedAccount != null) { setState(() { _sourceAccountId = selectedAccount['id']; _sourceAccountName = selectedAccount['name']; // Update controller with selected account name _sourceAccountController.text = selectedAccount['name']; }); } } /// Menampilkan dialog untuk memilih akun tujuan (asset). /// /// Metode ini memfilter daftar akun untuk hanya menampilkan akun dengan tipe 'asset', /// kemudian menampilkan dialog dengan daftar akun tersebut. Ketika pengguna memilih /// akun, metode ini akan memperbarui state `_destinationAccountId` dan `_destinationAccountName`. Future _selectDestinationAccount() async { if (_accounts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Daftar akun kosong. Pastikan kredensial sudah diatur dan akun telah dimuat. Klik "Muat Ulang Akun" untuk mencoba lagi.')), ); return; } // Filter akun tujuan (asset) final assetAccounts = _accounts.where((account) => account['type'] == 'asset').toList(); if (assetAccounts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Tidak ada akun tujuan (asset) yang ditemukan. Klik "Muat Ulang Akun" untuk mencoba lagi atau periksa akun Anda di Firefly III.')), ); return; } final selectedAccount = await showDialog?>( context: context, builder: (context) { return AlertDialog( title: const Text('Pilih Akun Tujuan'), content: SizedBox( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: assetAccounts.length, itemBuilder: (context, index) { final account = assetAccounts[index]; return ListTile( title: Text(account['name']), subtitle: Text(account['type']), onTap: () => Navigator.of(context).pop(account), ); }, ), ), ); }, ); if (selectedAccount != null) { setState(() { _destinationAccountId = selectedAccount['id']; _destinationAccountName = selectedAccount['name']; // Update controller with selected account name _destinationAccountController.text = selectedAccount['name']; }); } } /// Mencari ID akun berdasarkan nama dan tipe akun. /// /// Metode ini digunakan ketika pengguna memasukkan nama akun secara manual /// (bukan memilih dari dropdown). Metode ini akan mencari akun dengan nama /// yang cocok dan tipe yang diharapkan (revenue untuk sumber, asset untuk tujuan). /// /// Jika tidak ditemukan akun yang cocok, metode ini akan mencoba pencarian /// yang lebih fleksibel dengan mencari apakah nama akun mengandung teks yang dimasukkan. /// /// Parameter: /// - [name]: Nama akun yang dicari /// - [expectedType]: Tipe akun yang diharapkan (revenue atau asset) /// /// Mengembalikan ID akun jika ditemukan, atau null jika tidak ditemukan. String? _findAccountIdByName(String name, String expectedType) { if (name.isEmpty) return null; try { // Cari akun dengan nama yang cocok dan tipe yang diharapkan final account = _accounts.firstWhere( (account) => account['name'].toString().toLowerCase() == name.toLowerCase() && account['type'] == expectedType, ); return account['id'] as String?; } catch (e) { // Jika tidak ditemukan, coba pencarian yang lebih fleksibel for (var account in _accounts) { if (account['type'] == expectedType && account['name'] .toString() .toLowerCase() .contains(name.toLowerCase())) { return account['id'] as String?; } } // Jika masih tidak ditemukan, kembalikan null return null; } } /// Generates a transaction description based on item names String _generateTransactionDescription() { if (items.isEmpty) { return 'Transaksi Struk Belanja'; } // Take the first 5 item descriptions final itemNames = items.take(5).map((item) => item.description).toList(); // If there are more than 5 items, append ', dll' to the last item if (items.length > 5) { itemNames[4] += ', dll'; } // Join the item names with ', ' return itemNames.join(', '); } /// Mengirim transaksi ke Firefly III. /// /// Sebelum mengirim transaksi, metode ini melakukan beberapa validasi: /// 1. Memastikan ada item yang akan dikirim /// 2. Memastikan akun sumber dan tujuan telah dipilih atau dimasukkan /// 3. Memastikan akun sumber dan tujuan berbeda /// 4. Memastikan akun sumber dan tujuan ada di daftar akun yang dimuat /// 5. Memastikan tipe akun sesuai (sumber: revenue, tujuan: asset) /// /// Jika semua validasi lolos, metode ini akan mengirim transaksi ke API Firefly III /// dan menampilkan pesan hasilnya. Future _sendToFirefly() async { if (items.isEmpty) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Tidak ada item untuk dikirim')), ); return; } // Cek apakah user memasukkan akun secara manual if (_sourceAccountId == null && _sourceAccountName != null && _sourceAccountName!.isNotEmpty) { _sourceAccountId = _findAccountIdByName(_sourceAccountName!, 'revenue'); // Jika tidak ditemukan, coba cari dengan pendekatan yang lebih fleksibel if (_sourceAccountId == null) { for (var account in _accounts) { if (account['type'] == 'revenue' && account['name'] .toString() .toLowerCase() .contains(_sourceAccountName!.toLowerCase())) { _sourceAccountId = account['id'] as String?; _sourceAccountName = account['name'] as String?; _sourceAccountController.text = _sourceAccountName!; break; } } } } if (_destinationAccountId == null && _destinationAccountName != null && _destinationAccountName!.isNotEmpty) { _destinationAccountId = _findAccountIdByName(_destinationAccountName!, 'asset'); // Jika tidak ditemukan, coba cari dengan pendekatan yang lebih fleksibel if (_destinationAccountId == null) { for (var account in _accounts) { if (account['type'] == 'asset' && account['name'] .toString() .toLowerCase() .contains(_destinationAccountName!.toLowerCase())) { _destinationAccountId = account['id'] as String?; _destinationAccountName = account['name'] as String?; _destinationAccountController.text = _destinationAccountName!; break; } } } } // Validasi input if (_sourceAccountId == null || _destinationAccountId == null) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Silakan pilih atau masukkan akun sumber dan tujuan yang valid. ' 'Anda bisa memilih dari daftar atau mengetik nama akun yang sesuai.'), duration: const Duration(seconds: 5), ), ); return; } if (_sourceAccountId == _destinationAccountId) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Akun sumber dan tujuan tidak boleh sama')), ); return; } // Validasi apakah akun benar-benar ada di Firefly III bool sourceAccountExists = false; bool destinationAccountExists = false; // Cari detail akun untuk validasi tipe akun Map? sourceAccountDetails; Map? destinationAccountDetails; for (var account in _accounts) { if (account['id'].toString() == _sourceAccountId) { sourceAccountExists = true; sourceAccountDetails = account; } if (account['id'].toString() == _destinationAccountId) { destinationAccountExists = true; destinationAccountDetails = account; } } if (!sourceAccountExists) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Akun sumber tidak ditemukan di daftar akun yang dimuat. Klik "Muat Ulang Akun" dan coba lagi.')), ); return; } if (!destinationAccountExists) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Akun tujuan tidak ditemukan di daftar akun yang dimuat. Klik "Muat Ulang Akun" dan coba lagi.')), ); return; } // Validasi tipe akun (sumber harus revenue, tujuan harus asset) if (sourceAccountDetails != null && sourceAccountDetails['type'] != 'revenue') { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Peringatan: Akun sumber sebaiknya bertipe revenue, tetapi akun ini bertipe ${sourceAccountDetails['type']}. Transaksi mungkin tidak akan berhasil.')), ); } if (destinationAccountDetails != null && destinationAccountDetails['type'] != 'asset') { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Peringatan: Akun tujuan sebaiknya bertipe asset, tetapi akun ini bertipe ${destinationAccountDetails['type']}. Transaksi mungkin tidak akan berhasil.')), ); } final total = _calculateTotal(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Mengirim transaksi ke Firefly III...')), ); // Generate transaction description final transactionDescription = _generateTransactionDescription(); String? transactionId; // To store the transaction ID for attachment if (_fireflyUrl != null && _accessToken != null) { transactionId = await FireflyApiService.submitDummyTransaction( baseUrl: _fireflyUrl!, accessToken: _accessToken!, sourceId: _sourceAccountId!, destinationId: _destinationAccountId!, type: 'deposit', description: transactionDescription, // Use the generated description date: '${_transactionDate.year}-${_transactionDate.month.toString().padLeft(2, '0')}-${_transactionDate.day.toString().padLeft(2, '0')}', amount: total.toStringAsFixed(2), ); } if (!mounted) return; if (transactionId != null && transactionId != "success") { // PDF generation and upload removed as per request // Navigasi ke WebViewScreen untuk menampilkan transaksi if (_fireflyUrl != null) { final transactionUrl = '$_fireflyUrl/transactions/show/$transactionId'; if (mounted) { Navigator.push( context, MaterialPageRoute( builder: (context) => WebViewScreen( url: transactionUrl, title: 'Transaksi Firefly III', ), ), ); } } } else if (transactionId == "success") { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Transaksi berhasil dikirim ke Firefly III (tanpa ID)')), ); } else { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Gagal mengirim transaksi ke Firefly III. Periksa log untuk detail kesalahan.')), ); } } /// Membuka dialog konfigurasi informasi toko Future _openStoreInfoConfig() async { final result = await showDialog( context: context, builder: (context) => const StoreInfoConfigDialog(), ); // Jika informasi toko berhasil disimpan, refresh widget if (result == true) { setState(() { // setState akan memicu rebuild, sehingga StoreInfoWidget akan memuat ulang data }); } } /// Membuka dialog konfigurasi teks kustom (disclaimer, thank you, pantun) Future _openCustomTextConfig() async { final result = await showDialog( context: context, builder: (context) => const CustomTextConfigDialog(), ); // Jika teks berhasil disimpan, refresh widget if (result == true) { setState(() { // setState akan memicu rebuild, sehingga StoreDisclaimer dan ThankYouPantun akan memuat ulang data }); } } /// Membuka layar konfigurasi. /// /// Jika pengguna menyimpan konfigurasi di ConfigScreen, metode ini akan /// menerima hasil `true` dan memanggil `_loadCredentialsAndAccounts` untuk /// memuat ulang kredensial dan akun dengan pengaturan baru. void _openSettings() async { final result = await Navigator.pushNamed(context, '/config'); // Jika pengguna kembali dari ConfigScreen dengan hasil true, muat ulang kredensial dan akun if (result == true) { _loadCredentialsAndAccounts(); } } /// Memformat angka menjadi format mata uang Rupiah /// Memformat angka menjadi format mata uang Rupiah String _formatRupiah(double amount) { // Menggunakan NumberFormat untuk memformat angka final formatter = NumberFormat("#,##0", "id_ID"); return "Rp ${formatter.format(amount)}"; } final TextStyle baseTextStyle = const TextStyle( fontFamily: 'Courier', // Gunakan font courier jika tersedia fontSize: 14, height: 1.2, ); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[300], // Latar belakang abu-abu untuk efek struk floatingActionButton: SpeedDial( icon: Icons.menu, activeIcon: Icons.close, spacing: 3, spaceBetweenChildren: 4, children: [ SpeedDialChild( child: const Icon(Icons.send), label: 'Kirim ke Firefly', onTap: items.isNotEmpty && _sourceAccountId != null && _destinationAccountId != null ? _sendToFirefly : () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Pilih akun sumber dan tujuan terlebih dahulu'), duration: Duration(seconds: 2), ), ); }, backgroundColor: items.isNotEmpty && _sourceAccountId != null && _destinationAccountId != null ? Colors.blue : Colors.grey, ), SpeedDialChild( child: const Icon(Icons.refresh), label: 'Muat Ulang Akun', onTap: _loadAccounts, backgroundColor: Colors.orange, ), SpeedDialChild( child: const Icon(Icons.settings), label: 'Pengaturan', onTap: _openSettings, backgroundColor: Colors.green, ), // SpeedDialChild for PDF printing removed as per request SpeedDialChild( child: _isPrinting ? const CircularProgressIndicator(color: Colors.white, strokeWidth: 2) : const Icon(Icons.receipt), label: 'Cetak Struk', onTap: _isPrinting ? null : () async { // Periksa koneksi secara real-time final isConnected = await _checkBluetoothConnection(); if (isConnected) { _printToThermalPrinter(); } else { // Coba sambungkan kembali jika ada device yang tersimpan if (_bluetoothDevice != null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Mencoba menyambungkan ke printer...')), ); try { await bluetoothPrint.connect(_bluetoothDevice!); // Tunggu sebentar untuk memastikan koneksi stabil await Future.delayed(const Duration(milliseconds: 500)); // Periksa koneksi lagi final isConnectedAfterConnect = await _checkBluetoothConnection(); if (isConnectedAfterConnect) { _printToThermalPrinter(); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Gagal menyambungkan ke printer')), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal menyambungkan ke printer: $e')), ); } } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Hubungkan printer terlebih dahulu')), ); } } }, backgroundColor: Colors.purple, ), ], ), 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: [ const SizedBox(height: 15), // Jarak atas yang lebih besar const ReceiptTearTop(), // Efek kertas struk tersobek di bagian atas ], ), ), // Konten struk Container( width: 360, // Lebar tetap untuk menyerupai struk fisik decoration: const BoxDecoration( color: Colors.white, ), child: Column( children: [ // Informasi toko dengan widget yang dapat diedit StoreInfoWidget( onTap: _openStoreInfoConfig, // Buka dialog konfigurasi saat di-tap ), // Garis pembatas const DottedLine(), const SizedBox(height: 8), // Pengaturan akun sumber dan destinasi AccountSettingsWidget( sourceAccount: _sourceAccountName ?? 'Pilih Sumber', destinationAccount: _destinationAccountName ?? 'Pilih Tujuan', onSelectSource: _selectSourceAccount, onSelectDestination: _selectDestinationAccount, ), const SizedBox(height: 8), // Garis pemisah const HorizontalDivider(), // Baris tabel keterangan Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( flex: 4, child: Text( 'ITEM', style: baseTextStyle.copyWith( fontWeight: FontWeight.bold), textAlign: TextAlign.left, ), ), Expanded( flex: 1, child: Text( 'QTY', style: baseTextStyle.copyWith( fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), Expanded( flex: 2, child: Text( 'HARGA', style: baseTextStyle.copyWith( fontWeight: FontWeight.bold), textAlign: TextAlign.right, ), ), Expanded( flex: 2, child: Text( 'TOTAL', style: baseTextStyle.copyWith( fontWeight: FontWeight.bold), textAlign: TextAlign.right, ), ), ], ), // Garis pembatas const HorizontalDivider(), // Daftar item dengan Dismissible untuk swipe-to-delete ...items.asMap().entries.map((entry) { int index = entry.key; ReceiptItem item = entry.value; return Dismissible( key: Key( '${item.hashCode}_${index}'), // Unique key combining item hash and index for stability direction: DismissDirection .horizontal, // Geser ke kiri atau kanan background: Container( color: Colors .red, // Latar belakang merah saat digeser ke kanan alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 20.0), child: const Icon(Icons.delete, color: Colors.white), ), secondaryBackground: Container( color: Colors .red, // Latar belakang merah saat digeser ke kiri alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20.0), child: const Icon(Icons.delete, color: Colors.white), ), confirmDismiss: (direction) async { // Tampilkan dialog konfirmasi sebelum menghapus dan kembalikan hasilnya final bool? shouldRemove = await _confirmRemoveItem(index); // Jika shouldRemove adalah null (dialog ditutup), perlakukan sebagai pembatalan (false) return shouldRemove ?? false; }, onDismissed: (direction) { // Validasi index untuk mencegah error if (index >= 0 && index < items.length) { // Simpan deskripsi item untuk pesan snackbar final itemDescription = items[index].description; // Hapus item dari daftar items.removeAt(index); // Update UI if (mounted) { setState(() { // items sudah diupdate oleh removeAt di atas }); // Tampilkan snackbar bahwa item telah dihapus ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( "Item '$itemDescription' telah dihapus.")), ); } } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( "Gagal menghapus item: Index tidak valid setelah dismiss.")), ); } } }, child: GestureDetector( onTap: () => _editItem(index), // Tap untuk edit // onLongPress dihapus karena fungsi utamanya sudah digantikan oleh Dismissible child: ReceiptItemWidget( item: item, ), ), ); }), // .toList() dihapus karena spread operator tidak memerlukannya // Tombol tambah item AddItemButton(onTap: _addItem), // Garis pembatas const HorizontalDivider(), // Total harga keseluruhan Container( width: double.infinity, padding: const EdgeInsets.all(8.0), color: Colors.white, child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Expanded( flex: 4, child: Text( 'TOTAL:', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), Expanded( flex: 4, child: Text( _formatRupiah(_calculateTotal()), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), textAlign: TextAlign.right, ), ), ], ), ], ), ), const SizedBox(height: 8), // Garis pembatas const DottedLine(), // Disclaimer toko StoreDisclaimer(onTap: _openCustomTextConfig), // Ucapan terima kasih dan pantun ThankYouPantun(onTap: _openCustomTextConfig), const SizedBox( height: 16), // Add some space at the very bottom ], ), ), // 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: [ const ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah const SizedBox( height: 15), // Jarak bawah yang lebih besar ], ), ), ], ), ), ), ), ); } /// Menghitung total harga semua item double _calculateTotal() { return items.fold(0.0, (sum, item) => sum + item.total); } }