import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cashumit/models/receipt_item.dart'; import 'package:cashumit/screens/add_item_screen.dart'; import 'package:cashumit/screens/settings_screen.dart'; import 'package:cashumit/screens/printer_setup_screen.dart'; import 'package:cashumit/services/pdf_export_service.dart'; import 'package:cashumit/services/firefly_api_service.dart'; import 'package:bluetooth_print/bluetooth_print.dart'; import 'package:bluetooth_print/bluetooth_print_model.dart'; class ReceiptScreen extends StatefulWidget { const ReceiptScreen({super.key}); @override State createState() => _ReceiptScreenState(); } class _ReceiptScreenState extends State { List items = [ ReceiptItem(description: 'Apel', quantity: 3, price: 5000), ReceiptItem(description: 'Roti', quantity: 2, price: 12000), ReceiptItem(description: 'Susu', quantity: 1, price: 15000), ]; final String storeName = 'TOKO SEMBAKO MURAH'; final String storeAddress = 'Jl. Merdeka No. 123'; final String storePhone = 'Telp: 021-12345678'; 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 BluetoothPrint bluetoothPrint = BluetoothPrint.instance; bool _bluetoothConnected = false; BluetoothDevice? _bluetoothDevice; bool _bluetoothScanning = false; @override void initState() { super.initState(); _transactionDate = DateTime.now(); _loadCredentialsAndAccounts(); _initBluetooth(); _loadSavedBluetoothDevice(); } @override void dispose() { _sourceAccountController.dispose(); _destinationAccountController.dispose(); super.dispose(); } String? _fireflyUrl; String? _accessToken; /// Memuat kredensial dari shared preferences dan kemudian memuat akun. 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.')), ); } return; } setState(() { _fireflyUrl = url; _accessToken = token; }); // Jika kredensial ada, lanjutkan untuk memuat akun _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; }); } } /// Menyimpan device bluetooth yang terhubung Future _saveBluetoothDevice(BluetoothDevice device) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('bluetooth_device_address', device.address ?? ''); await prefs.setString('bluetooth_device_name', device.name ?? ''); } /// Memuat daftar akun sumber (revenue) dan tujuan (asset) dari API. Future _loadAccounts() async { if (_fireflyUrl == null || _accessToken == null) { return; } try { // 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; }); } 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; } } }); } /// Memeriksa ulang status koneksi bluetooth Future _checkBluetoothStatus() async { final isConnected = await bluetoothPrint.isConnected ?? false; if (mounted) { setState(() { _bluetoothConnected = isConnected; }); if (isConnected) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Printer terhubung')), ); } else { // Jika ada device yang sudah dipilih sebelumnya, coba sambungkan kembali if (_bluetoothDevice != null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Mencoba menyambungkan kembali ke printer...')), ); _reconnectToBluetoothDevice(); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Printer tidak terhubung. Silakan sambungkan melalui menu Setup Printer atau ikon Bluetooth di atas.')), ); } } } } /// Menyambungkan kembali ke printer bluetooth yang sudah dipilih Future _reconnectToBluetoothDevice() async { if (_bluetoothDevice != null) { try { await bluetoothPrint.connect(_bluetoothDevice!); if (mounted) { setState(() { _bluetoothConnected = true; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Berhasil terhubung kembali ke printer')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal menyambungkan kembali ke printer: $e')), ); } } } } /// Scan perangkat Bluetooth Future _scanBluetoothDevices() async { setState(() { _bluetoothScanning = true; }); try { // Mulai scan perangkat Bluetooth await bluetoothPrint.startScan(timeout: const Duration(seconds: 4)); if (mounted) { final selectedDevice = await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Pilih Perangkat Printer'), content: StreamBuilder>( stream: bluetoothPrint.scanResults, initialData: const [], builder: (c, snapshot) { if (snapshot.data!.isEmpty) { return const Text('Tidak ada perangkat ditemukan'); } return SizedBox( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: snapshot.data!.length, itemBuilder: (context, index) { final device = snapshot.data![index]; return ListTile( title: Text(device.name ?? 'Unknown Device'), subtitle: Text(device.address ?? ''), onTap: () { Navigator.of(context).pop(device); }, ); }, ), ); }, ), ); }, ); if (selectedDevice != null) { setState(() { _bluetoothDevice = selectedDevice; }); // Simpan device yang dipilih await _saveBluetoothDevice(selectedDevice); // Hubungkan ke perangkat yang dipilih await bluetoothPrint.connect(selectedDevice); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal memindai perangkat: $e')), ); } } finally { setState(() { _bluetoothScanning = false; }); } } /// Cetak struk ke printer thermal Future _printToThermalPrinter() async { print('=== MULAI MENCETAK KE PRINTER THERMAL ==='); // Periksa koneksi printer final isConnected = await bluetoothPrint.isConnected ?? false; if (!isConnected) { print('ERROR: Printer tidak terhubung'); if (mounted) { // Coba sambungkan kembali jika ada device yang tersimpan if (_bluetoothDevice != null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Mencoba menyambungkan kembali ke printer...')), ); try { await bluetoothPrint.connect(_bluetoothDevice!); // Perbarui status koneksi setState(() { _bluetoothConnected = true; }); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal menyambungkan kembali ke printer: $e')), ); return; } } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Harap hubungkan printer terlebih dahulu')), ); return; } } else { return; } } print('Printer terhubung, mencoba mencetak...'); print('Device address: ${_bluetoothDevice?.address}'); print('Device name: ${_bluetoothDevice?.name}'); try { // Konfigurasi untuk mencetak struk Map config = {}; // Data yang akan dicetak List list = []; print('Membuat data struk untuk dicetak...'); // Header struk list.add(LineText(type: LineText.TYPE_TEXT, content: storeName, weight: 1, align: LineText.ALIGN_CENTER, fontZoom: 2, linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: storeAddress, align: LineText.ALIGN_CENTER, linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: storePhone, align: LineText.ALIGN_CENTER, linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1)); // Info transaksi list.add(LineText(type: LineText.TYPE_TEXT, content: 'TANGGAL: ${_transactionDate.day.toString().padLeft(2, '0')}/${_transactionDate.month.toString().padLeft(2, '0')}/${_transactionDate.year}', linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: 'KASIR: $cashierId', linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: 'NOTA: $transactionId', linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1)); // Header item list.add(LineText(type: LineText.TYPE_TEXT, content: 'ITEM Q HARGA TOTAL', weight: 1, linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1)); // Item list for (var item in items) { // Nama item (potong jika terlalu panjang) String itemName = item.description.length > 12 ? item.description.substring(0, 12) : item.description.padRight(12); // Quantity (3 karakter) String qty = item.quantity.toString().padLeft(3); // Harga (8 karakter) String price = item.price.toStringAsFixed(0).padLeft(8); // Total (8 karakter) String total = item.total.toStringAsFixed(0).padLeft(8); list.add(LineText(type: LineText.TYPE_TEXT, content: '$itemName $qty $price $total', linefeed: 1)); } list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1)); // Total list.add(LineText(type: LineText.TYPE_TEXT, content: 'TOTAL: ${_calculateTotal().toStringAsFixed(0).padLeft(8)}', weight: 1, linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1)); // Footer list.add(LineText(type: LineText.TYPE_TEXT, content: '*** TERIMA KASIH ***', align: LineText.ALIGN_CENTER, fontZoom: 1, linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: 'Barang yang sudah dibeli', align: LineText.ALIGN_CENTER, linefeed: 1)); list.add(LineText(type: LineText.TYPE_TEXT, content: 'tidak dapat dikembalikan/ditukar', align: LineText.ALIGN_CENTER, linefeed: 1)); // Garis kosong di akhir list.add(LineText(linefeed: 3)); print('Data struk berhasil dibuat, mengirim ke printer...'); print('Jumlah baris: ${list.length}'); // Kirim perintah cetak ke printer await bluetoothPrint.printReceipt(config, list); print('Perintah cetak dikirim ke printer'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Struk berhasil dicetak')), ); } } catch (e, stackTrace) { print('ERROR saat mencetak struk: $e'); print('Stack trace: $stackTrace'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal mencetak struk: $e')), ); } } print('=== SELESAI MENCETAK KE PRINTER THERMAL ==='); } void _addItem() async { final newItem = await Navigator.push( context, MaterialPageRoute(builder: (context) => const AddItemScreen()), ); if (newItem != null) { setState(() { items.add(newItem); }); } } void _editItem(int index) async { final editedItem = await Navigator.push( context, MaterialPageRoute( builder: (context) => AddItemScreen.fromItem(items[index]), ), ); if (editedItem != null) { setState(() { items[index] = editedItem; }); } } void _removeItem(int index) { setState(() { items.removeAt(index); }); } Future _printReceipt() async { // Generate PDF final pdfPath = await PdfExportService.generateReceiptPdf( items, storeName, storeAddress, storePhone, ); if (pdfPath != null) { // Menampilkan pesan sukses if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('PDF berhasil dibuat')), ); } // Membuka file PDF await PdfExportService.openPdf(pdfPath); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Gagal membuat file PDF')), ); } } } Future _selectDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, initialDate: _transactionDate, firstDate: DateTime(2020), lastDate: DateTime(2030), ); if (picked != null && picked != _transactionDate) { setState(() { _transactionDate = picked; }); } } Future _selectSourceAccount() async { if (_accounts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Daftar akun belum dimuat')), ); 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: _accounts.length, itemBuilder: (context, index) { final account = _accounts[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']; }); } } Future _selectDestinationAccount() async { if (_accounts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Daftar akun belum dimuat')), ); 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: _accounts.length, itemBuilder: (context, index) { final account = _accounts[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']; }); } } // Method to handle manual input of source account void _onSourceAccountChanged(String value) { // Clear selected account if user is typing if (_sourceAccountName != value) { setState(() { _sourceAccountId = null; _sourceAccountName = value; }); } } // Method to handle manual input of destination account void _onDestinationAccountChanged(String value) { // Clear selected account if user is typing if (_destinationAccountName != value) { setState(() { _destinationAccountId = null; _destinationAccountName = value; }); } } // Method to find account ID by name when user inputs manually String? _findAccountIdByName(String name) { if (name.isEmpty) return null; try { final account = _accounts.firstWhere( (account) => account['name'].toString().toLowerCase() == name.toLowerCase(), ); return account['id'] as String?; } catch (e) { // Jika tidak ditemukan, kembalikan null return null; } } Future _sendToFirefly() async { if (items.isEmpty) { if (mounted) { 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!); // Jika tidak ditemukan, coba cari dengan pendekatan yang lebih fleksibel if (_sourceAccountId == null) { for (var account in _accounts) { if (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!); // Jika tidak ditemukan, coba cari dengan pendekatan yang lebih fleksibel if (_destinationAccountId == null) { for (var account in _accounts) { if (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) { 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) { 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; for (var account in _accounts) { if (account['id'].toString() == _sourceAccountId) { sourceAccountExists = true; } if (account['id'].toString() == _destinationAccountId) { destinationAccountExists = true; } } if (!sourceAccountExists) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Akun sumber tidak ditemukan di Firefly III. Silakan pilih akun yang valid.')), ); } return; } if (!destinationAccountExists) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Akun tujuan tidak ditemukan di Firefly III. Silakan pilih akun yang valid.')), ); } return; } final total = _calculateTotal(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Mengirim transaksi ke Firefly III...')), ); } print('=== MULAI MENGIRIM TRANSAKSI ==='); print('Jumlah item: ${items.length}'); print('Total: $total'); print('Source Account ID: $_sourceAccountId'); print('Destination Account ID: $_destinationAccountId'); print('Transaction Date: $_transactionDate'); for (var i = 0; i < items.length; i++) { var item = items[i]; print(' Item ${i+1}: ${item.description} - ${item.quantity} x ${item.price} = ${item.total}'); } bool success = false; if (_fireflyUrl != null && _accessToken != null) { success = await FireflyApiService.submitDummyTransaction( baseUrl: _fireflyUrl!, accessToken: _accessToken!, sourceId: _sourceAccountId!, destinationId: _destinationAccountId!, type: 'deposit', description: 'Transaksi Struk Belanja', date: '${_transactionDate.year}-${_transactionDate.month.toString().padLeft(2, '0')}-${_transactionDate.day.toString().padLeft(2, '0')}', amount: total.toStringAsFixed(2), ); } if (mounted) { if (success) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Transaksi berhasil dikirim ke Firefly III')), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Gagal mengirim transaksi ke Firefly III. Periksa log untuk detail kesalahan.')), ); } } print('=== SELESAI MENGIRIM TRANSAKSI ==='); } void _openSettings() { Navigator.push( context, MaterialPageRoute(builder: (context) => const SettingsScreen()), ); } void _openPrinterSetup() { Navigator.push( context, MaterialPageRoute(builder: (context) => const PrinterSetupScreen()), ); } @override Widget build(BuildContext context) { // Gunakan font Courier Prime untuk menyerupai font struk fisik final courierPrime = GoogleFonts.courierPrime( textStyle: const TextStyle( fontSize: 14, height: 1.2, ), ); return Scaffold( appBar: AppBar( title: const Text('Struk Belanja'), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.settings), onPressed: _openSettings, ), IconButton( icon: const Icon(Icons.print), onPressed: _printReceipt, ), IconButton( icon: Icon(_bluetoothConnected ? Icons.bluetooth_connected : Icons.bluetooth), onPressed: _scanBluetoothDevices, ), IconButton( icon: const Icon(Icons.refresh), onPressed: _checkBluetoothStatus, ), IconButton( icon: const Icon(Icons.cloud_upload), onPressed: _sendToFirefly, ), ], ), body: Padding( padding: const EdgeInsets.all(16.0), child: SingleChildScrollView( child: Column( children: [ // Receipt paper style container - desain ulang untuk menyerupai struk fisik Container( width: 320, // Lebar tetap untuk menyerupai struk fisik decoration: BoxDecoration( border: Border.all(color: Colors.black), color: Colors.white, ), child: Padding( padding: const EdgeInsets.all(8.0), // Kurangi padding child: Column( children: [ // Header struk Text( storeName, style: courierPrime.copyWith( fontSize: 16, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 2), Text(storeAddress, style: courierPrime, textAlign: TextAlign.center), Text(storePhone, style: courierPrime, textAlign: TextAlign.center), const SizedBox(height: 4), const Divider(thickness: 1, height: 1, color: Colors.black), const SizedBox(height: 4), // Info transaksi tambahan Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('TANGGAL:', style: courierPrime), Text( '${_transactionDate.day.toString().padLeft(2, '0')}/${_transactionDate.month.toString().padLeft(2, '0')}/${_transactionDate.year}', style: courierPrime, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('KASIR:', style: courierPrime), Text(cashierId, style: courierPrime), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('NOTA:', style: courierPrime), Text(transactionId, style: courierPrime), ], ), const SizedBox(height: 4), const Divider(thickness: 1, height: 1, color: Colors.black), const SizedBox(height: 4), // Item list header const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( flex: 4, child: Text( 'ITEM', style: TextStyle(fontWeight: FontWeight.bold), ), ), Expanded( flex: 1, child: Text( 'Q', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), Expanded( flex: 2, child: Text( '@HARGA', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.right, ), ), Expanded( flex: 2, child: Text( 'TOTAL', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.right, ), ), ], ), const SizedBox(height: 2), const Divider(thickness: 1, height: 1, color: Colors.black), const SizedBox(height: 4), // Item list ...items.map((item) { return Padding( padding: const EdgeInsets.only(bottom: 2.0), // Kurangi jarak antar item child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( flex: 4, child: Text( item.description, style: courierPrime, ), ), Expanded( flex: 1, child: Text( item.quantity.toString(), textAlign: TextAlign.center, style: courierPrime, ), ), Expanded( flex: 2, child: Text( item.price.toStringAsFixed(0), textAlign: TextAlign.right, style: courierPrime, ), ), Expanded( flex: 2, child: Text( item.total.toStringAsFixed(0), textAlign: TextAlign.right, style: courierPrime, ), ), ], ), ); }).toList(), const SizedBox(height: 4), const Divider(thickness: 1, height: 1, color: Colors.black), const SizedBox(height: 4), // Total Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'TOTAL:', style: courierPrime.copyWith( fontWeight: FontWeight.bold, ), ), Text( _calculateTotal().toStringAsFixed(0), style: courierPrime.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 8), Text( '*** TERIMA KASIH ***', style: courierPrime.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), Text( 'Barang yang sudah dibeli tidak dapat', style: courierPrime, textAlign: TextAlign.center, ), Text( 'dikembalikan/ditukar', style: courierPrime, textAlign: TextAlign.center, ), ], ), ), ), const SizedBox(height: 16), // Account selection section - tetap di luar area struk Container( width: 320, // Sesuaikan lebar dengan area struk decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), color: Colors.grey.shade50, ), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Pengaturan Transaksi', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), // Source account const Text( 'Akun Sumber:', style: TextStyle(fontWeight: FontWeight.bold), ), _sourceAccountId != null ? ListTile( title: Text(_sourceAccountName ?? ''), contentPadding: EdgeInsets.zero, dense: true, trailing: const Icon(Icons.edit, size: 18), onTap: _selectSourceAccount, ) : TextField( controller: _sourceAccountController, decoration: const InputDecoration( hintText: 'Ketik nama akun atau pilih dari daftar', contentPadding: EdgeInsets.symmetric(horizontal: 8), isDense: true, border: OutlineInputBorder(), ), onChanged: _onSourceAccountChanged, ), if (_sourceAccountId == null) ElevatedButton( onPressed: _selectSourceAccount, child: const Text('Pilih Akun Sumber'), ), const SizedBox(height: 8), // Destination account const Text( 'Akun Tujuan:', style: TextStyle(fontWeight: FontWeight.bold), ), _destinationAccountId != null ? ListTile( title: Text(_destinationAccountName ?? ''), contentPadding: EdgeInsets.zero, dense: true, trailing: const Icon(Icons.edit, size: 18), onTap: _selectDestinationAccount, ) : TextField( controller: _destinationAccountController, decoration: const InputDecoration( hintText: 'Ketik nama akun atau pilih dari daftar', contentPadding: EdgeInsets.symmetric(horizontal: 8), isDense: true, border: OutlineInputBorder(), ), onChanged: _onDestinationAccountChanged, ), if (_destinationAccountId == null) ElevatedButton( onPressed: _selectDestinationAccount, child: const Text('Pilih Akun Tujuan'), ), ], ), ), ), const SizedBox(height: 16), // Action buttons SizedBox( width: 320, // Sesuaikan lebar dengan area struk child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: _addItem, child: const Text('Tambah Item'), ), ElevatedButton( onPressed: items.isNotEmpty ? _sendToFirefly : null, child: const Text('Kirim ke Firefly'), ), ], ), ), const SizedBox(height: 16), // Additional action buttons SizedBox( width: 320, // Sesuaikan lebar dengan area struk child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: ElevatedButton( onPressed: _openPrinterSetup, child: const Text('Setup Printer', textAlign: TextAlign.center), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: ElevatedButton( onPressed: _printReceipt, child: const Text('Cetak PDF', textAlign: TextAlign.center), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: ElevatedButton( onPressed: _bluetoothConnected ? _printToThermalPrinter : null, child: const Text('Cetak Struk', textAlign: TextAlign.center), ), ), ), ], ), ), if (!_bluetoothConnected) const Padding( padding: EdgeInsets.only(top: 8.0), child: Text( '⚠️ Printer tidak terhubung. Hubungkan printer melalui menu Setup Printer atau ikon Bluetooth di atas.', style: TextStyle( color: Colors.orange, fontSize: 12, ), textAlign: TextAlign.center, ), ), if (_sourceAccountId == null || _destinationAccountId == null) const Padding( padding: EdgeInsets.only(top: 8.0), child: Text( '⚠️ Pilih akun sumber dan tujuan untuk mengaktifkan tombol kirim', style: TextStyle( color: Colors.orange, fontSize: 12, ), textAlign: TextAlign.center, ), ), ], ), ), ), ); } double _calculateTotal() { return items.fold(0.0, (sum, item) => sum + item.total); } }