import 'package:flutter/material.dart'; import 'package:cashumit/models/item.dart'; import 'package:cashumit/models/transaction.dart'; import 'package:cashumit/services/print_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'; import 'package:cashumit/utils/currency_format.dart'; import 'package:cashumit/widgets/printing_status_card.dart'; import 'package:shared_preferences/shared_preferences.dart'; class TransactionScreen extends StatefulWidget { final List items; const TransactionScreen({ super.key, required this.items, }); @override State createState() => _TransactionScreenState(); } class _TransactionScreenState extends State { final BluetoothPrint bluetoothPrint = BluetoothPrint.instance; List _devices = []; BluetoothDevice? _selectedDevice; bool _isScanning = false; final List> _cart = []; int _total = 0; String _paymentMethod = 'Tunai'; final TextEditingController _searchController = TextEditingController(); List _filteredItems = []; // Transaction settings directly in this screen late DateTime _transactionDate; List> _accounts = []; String? _sourceAccountId; String? _sourceAccountName; String? _destinationAccountId; String? _destinationAccountName; bool _isLoadingAccounts = false; // Printing status bool _isPrinting = false; // Controllers for manual account input final TextEditingController _sourceAccountController = TextEditingController(); final TextEditingController _destinationAccountController = TextEditingController(); String? _fireflyUrl; String? _accessToken; @override void initState() { super.initState(); _filteredItems = widget.items; _transactionDate = DateTime.now(); _startScan(); _loadCredentialsAndAccounts(); } @override void dispose() { _sourceAccountController.dispose(); _destinationAccountController.dispose(); super.dispose(); } /// 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 daftar akun sumber (revenue) dan tujuan (asset) dari API. Future _loadAccounts() async { if (_fireflyUrl == null || _accessToken == null) { return; } setState(() { _isLoadingAccounts = true; }); 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; _isLoadingAccounts = false; }); } catch (error) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal memuat akun: $error')), ); } setState(() { _isLoadingAccounts = false; }); } } void _startScan() { setState(() { _isScanning = true; }); bluetoothPrint.startScan(timeout: const Duration(seconds: 4)); bluetoothPrint.scanResults.listen((devices) { if (mounted) { setState(() { _devices = devices; _isScanning = false; }); } }); } void _addToCart(Item item) { setState(() { // Cek apakah item sudah ada di cart final existingItemIndex = _cart.indexWhere((cartItem) => cartItem['item'].id == item.id); if (existingItemIndex != -1) { // Jika sudah ada, tambahkan quantity _cart[existingItemIndex]['quantity']++; } else { // Jika belum ada, tambahkan sebagai item baru _cart.add({ 'item': item, 'quantity': 1, }); } _calculateTotal(); }); } void _removeFromCart(int index) { setState(() { _cart.removeAt(index); _calculateTotal(); }); } void _updateQuantity(int index, int quantity) { if (quantity <= 0) { _removeFromCart(index); return; } setState(() { _cart[index]['quantity'] = quantity; _calculateTotal(); }); } void _calculateTotal() { _total = _cart.fold( 0, (sum, item) => sum + (item['item'].price * item['quantity']) as int); } void _searchItems(String query) { if (query.isEmpty) { setState(() { _filteredItems = widget.items; }); return; } final filtered = widget.items .where((item) => item.name.toLowerCase().contains(query.toLowerCase()) || item.id.toLowerCase().contains(query.toLowerCase())) .toList(); setState(() { _filteredItems = filtered; }); } 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) { if (mounted) { 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) { if (mounted) { 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; final account = _accounts.firstWhere( (account) => account['name'].toLowerCase() == name.toLowerCase(), orElse: () => {}, ); return account.isEmpty ? null : account['id']; } Future _completeTransaction() async { if (_cart.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Keranjang masih kosong!')), ); } return; } if (_selectedDevice == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Pilih printer terlebih dahulu!')), ); } return; } // Cek apakah user memasukkan akun secara manual if (_sourceAccountId == null && _sourceAccountName != null && _sourceAccountName!.isNotEmpty) { _sourceAccountId = _findAccountIdByName(_sourceAccountName!); } if (_destinationAccountId == null && _destinationAccountName != null && _destinationAccountName!.isNotEmpty) { _destinationAccountId = _findAccountIdByName(_destinationAccountName!); } // Validasi pengaturan transaksi if (_sourceAccountId == null || _destinationAccountId == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Harap pilih atau masukkan akun sumber dan tujuan yang valid!')), ); } return; } if (_sourceAccountId == _destinationAccountId) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Akun sumber dan tujuan tidak boleh sama!')), ); } return; } // Buat objek transaksi final transaction = Transaction( id: DateTime.now().millisecondsSinceEpoch.toString(), items: _cart .map((cartItem) => TransactionItem( itemId: cartItem['item'].id, name: cartItem['item'].name, price: cartItem['item'].price, quantity: cartItem['quantity'], )) .toList(), total: _total, timestamp: DateTime.now(), paymentMethod: _paymentMethod, ); // Tampilkan dialog konfirmasi final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Konfirmasi Transaksi'), content: Text( 'Total: Rp ${CurrencyFormat.formatRupiahWithoutSymbol(_total)}\n' 'Metode Bayar: $_paymentMethod\n' 'Tanggal: ${_transactionDate.day}/${_transactionDate.month}/${_transactionDate.year}\n' 'Sumber: $_sourceAccountName\n' 'Tujuan: $_destinationAccountName' ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Batal'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), child: const Text('Bayar'), ), ], ), ); if (confirmed != true) return; // Tampilkan status printing setState(() { _isPrinting = true; }); // Cetak struk final printService = PrintService(); final printed = await printService.printTransaction( transaction, 'TOKO SEMBAKO MURAH', 'Jl. Merdeka No. 123, Jakarta', ); // Sembunyikan status printing setState(() { _isPrinting = false; }); if (!printed) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Gagal mencetak struk!')), ); } return; } // Kirim ke FireFly III dengan pengaturan transaksi String? transactionId; if (_fireflyUrl != null && _accessToken != null) { transactionId = await FireflyApiService.submitDummyTransaction( baseUrl: _fireflyUrl!, accessToken: _accessToken!, sourceId: _sourceAccountId!, destinationId: _destinationAccountId!, type: 'deposit', description: 'Transaksi dari Aplikasi Cashumit', date: '${_transactionDate.year}-${_transactionDate.month.toString().padLeft(2, '0')}-${_transactionDate.day.toString().padLeft(2, '0')}', amount: (_total / 1000).toStringAsFixed(2), // Mengonversi ke satuan mata uang ); } final bool synced = transactionId != null; // Tampilkan hasil if (mounted) { if (synced) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Transaksi berhasil!')), ); // Reset keranjang setState(() { _cart.clear(); _total = 0; }); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Gagal menyinkronkan dengan FireFly III!')), ); } } } @override Widget build(BuildContext context) { return Stack( children: [ Scaffold( appBar: AppBar( title: const Text('Aplikasi Kasir'), actions: [ IconButton( onPressed: _loadAccounts, icon: _isLoadingAccounts ? const CircularProgressIndicator() : const Icon(Icons.refresh), ), IconButton( onPressed: _startScan, icon: _isScanning ? const CircularProgressIndicator() : const Icon(Icons.bluetooth), ), ], ), body: Column( children: [ // Pencarian item Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: _searchController, decoration: const InputDecoration( labelText: 'Cari barang...', prefixIcon: Icon(Icons.search), border: OutlineInputBorder(), ), onChanged: _searchItems, ), ), // Dropdown printer if (_devices.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: DropdownButton( hint: const Text('Pilih Printer'), value: _selectedDevice, items: _devices .map((device) => DropdownMenuItem( value: device, child: Text(device.name ?? device.address ?? '-'), )) .toList(), onChanged: (device) { setState(() { _selectedDevice = device; }); }, isExpanded: true, ), ), // Dropdown metode pembayaran Padding( padding: const EdgeInsets.all(8.0), child: DropdownButton( value: _paymentMethod, items: ['Tunai', 'Debit', 'Kredit', 'QRIS'] .map((method) => DropdownMenuItem( value: method, child: Text(method), )) .toList(), onChanged: (method) { setState(() { _paymentMethod = method!; }); }, isExpanded: true, ), ), // Transaction settings directly in this screen Card( margin: const EdgeInsets.all(8.0), child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pengaturan Transaksi:', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), // Date picker const Text('Tanggal Transaksi:'), ListTile( title: Text( '${_transactionDate.day}/${_transactionDate.month}/${_transactionDate.year}', ), trailing: const Icon(Icons.calendar_today), onTap: () => _selectDate(context), ), const SizedBox(height: 8), // Source account const Text('Akun Sumber:'), _sourceAccountId != null ? Card( child: ListTile( title: Text(_sourceAccountName ?? ''), trailing: const Icon(Icons.edit), onTap: _selectSourceAccount, ), ) : Column( children: [ TextField( controller: _sourceAccountController, decoration: const InputDecoration( labelText: 'Nama Akun Sumber', hintText: 'Ketik nama akun atau pilih dari daftar', ), onChanged: _onSourceAccountChanged, ), ElevatedButton( onPressed: _selectSourceAccount, child: const Text('Pilih Akun Sumber dari Daftar'), ), ], ), const SizedBox(height: 8), // Destination account const Text('Akun Tujuan:'), _destinationAccountId != null ? Card( child: ListTile( title: Text(_destinationAccountName ?? ''), trailing: const Icon(Icons.edit), onTap: _selectDestinationAccount, ), ) : Column( children: [ TextField( controller: _destinationAccountController, decoration: const InputDecoration( labelText: 'Nama Akun Tujuan', hintText: 'Ketik nama akun atau pilih dari daftar', ), onChanged: _onDestinationAccountChanged, ), ElevatedButton( onPressed: _selectDestinationAccount, child: const Text('Pilih Akun Tujuan dari Daftar'), ), ], ), if (_isLoadingAccounts) const Padding( padding: EdgeInsets.all(8.0), child: Center(child: CircularProgressIndicator()), ), ], ), ), ), // Total Container( padding: const EdgeInsets.all(16.0), color: Colors.grey[200], child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Total:', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), Text( 'Rp ${CurrencyFormat.formatRupiahWithoutSymbol(_total)}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), ], ), ), // TabBar untuk navigasi Expanded( child: DefaultTabController( length: 2, child: Column( children: [ const TabBar( tabs: [ Tab(text: 'Barang'), Tab(text: 'Keranjang'), ], ), Expanded( child: TabBarView( children: [ // Tab Barang _buildItemsTab(), // Tab Keranjang _buildCartTab(), ], ), ), ], ), ), ), ], ), bottomNavigationBar: Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( onPressed: _completeTransaction, style: ElevatedButton.styleFrom( padding: const EdgeInsets.all(16.0), backgroundColor: Colors.green, ), child: const Text( 'BAYAR', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), ), ), ), PrintingStatusCard( isVisible: _isPrinting, onDismiss: () { setState(() { _isPrinting = false; }); }, ), ], ); } Widget _buildItemsTab() { return GridView.builder( padding: const EdgeInsets.all(8.0), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 8.0, mainAxisSpacing: 8.0, childAspectRatio: 1.2, ), itemCount: _filteredItems.length, itemBuilder: (context, index) { final item = _filteredItems[index]; return Card( child: InkWell( onTap: () => _addToCart(item), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( item.name, style: const TextStyle(fontWeight: FontWeight.bold), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( 'Rp ${CurrencyFormat.formatRupiahWithoutSymbol(item.price)}', style: const TextStyle( color: Colors.green, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( 'ID: ${item.id}', style: const TextStyle( fontSize: 12, color: Colors.grey, ), ), ], ), ), ), ); }, ); } Widget _buildCartTab() { if (_cart.isEmpty) { return const Center( child: Text('Keranjang masih kosong'), ); } return ListView.builder( padding: const EdgeInsets.all(8.0), itemCount: _cart.length, itemBuilder: (context, index) { final cartItem = _cart[index]; final item = cartItem['item'] as Item; final quantity = cartItem['quantity'] as int; final subtotal = item.price * quantity; return Card( child: ListTile( title: Text(item.name), subtitle: Text('Rp ${CurrencyFormat.formatRupiahWithoutSymbol(item.price)}'), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: () => _updateQuantity(index, quantity - 1), icon: const Icon(Icons.remove), ), Text('$quantity'), IconButton( onPressed: () => _updateQuantity(index, quantity + 1), icon: const Icon(Icons.add), ), const SizedBox(width: 8), Text('Rp ${CurrencyFormat.formatRupiahWithoutSymbol(subtotal)}'), IconButton( onPressed: () => _removeFromCart(index), icon: const Icon(Icons.delete, color: Colors.red), ), ], ), ), ); }, ); } }