diff --git a/lib/extensions/double_extensions.dart b/lib/extensions/double_extensions.dart deleted file mode 100644 index d6f7b37..0000000 --- a/lib/extensions/double_extensions.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:intl/intl.dart'; - -extension DoubleFormatting on double { - /// Memformat angka menjadi format mata uang Rupiah - String toRupiah() { - final formatter = NumberFormat("#,##0", "id_ID"); - return "Rp ${formatter.format(this)}"; - } -} \ No newline at end of file diff --git a/lib/features/cashier_home.dart b/lib/features/cashier_home.dart deleted file mode 100644 index d2e53f6..0000000 --- a/lib/features/cashier_home.dart +++ /dev/null @@ -1,33 +0,0 @@ -// Fitur utama aplikasi kasir -// - Input transaksi -// - Cetak struk ke printer thermal -// - Integrasi FireFly III - -import 'package:flutter/material.dart'; -import 'package:cashumit/models/item.dart'; -import 'package:cashumit/features/transaction_screen.dart'; - -class CashierHomePage extends StatelessWidget { - const CashierHomePage({super.key}); - - @override - Widget build(BuildContext context) { - // Data contoh barang - final items = [ - Item(id: '001', name: 'Beras 1kg', price: 12000, category: 'Sembako'), - Item(id: '002', name: 'Minyak Goreng 1L', price: 15000, category: 'Sembako'), - Item(id: '003', name: 'Gula Pasir 1kg', price: 13000, category: 'Sembako'), - Item(id: '004', name: 'Telur Ayam 1kg', price: 25000, category: 'Sembako'), - Item(id: '005', name: 'Tepung Terigu 1kg', price: 9000, category: 'Sembako'), - Item(id: '006', name: 'Susu Kental Manis', price: 10000, category: 'Minuman'), - Item(id: '007', name: 'Kopi Sachet', price: 1500, category: 'Minuman'), - Item(id: '008', name: 'Teh Celup 25 sachet', price: 8000, category: 'Minuman'), - Item(id: '009', name: 'Sabun Mandi Lux', price: 4000, category: 'Toiletries'), - Item(id: '010', name: 'Pasta Gigi Pepsodent', price: 12000, category: 'Toiletries'), - ]; - - return TransactionScreen( - items: items, - ); - } -} \ No newline at end of file diff --git a/lib/features/transaction_screen.dart b/lib/features/transaction_screen.dart deleted file mode 100644 index 28b0a8e..0000000 --- a/lib/features/transaction_screen.dart +++ /dev/null @@ -1,854 +0,0 @@ -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), - ), - ], - ), - ), - ); - }, - ); - } -} \ No newline at end of file diff --git a/lib/features/transaction_screen.dart.bak b/lib/features/transaction_screen.dart.bak deleted file mode 100644 index 7557076..0000000 --- a/lib/features/transaction_screen.dart.bak +++ /dev/null @@ -1,1064 +0,0 @@ -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; - }); - }, - ), - ], - ); - } - 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), - ), - ], - ), - ), - ); - }, - ); - } -} \ No newline at end of file diff --git a/lib/providers/receipt_provider.dart b/lib/providers/receipt_provider.dart index 67c5a82..6575df5 100644 --- a/lib/providers/receipt_provider.dart +++ b/lib/providers/receipt_provider.dart @@ -4,11 +4,9 @@ import 'package:flutter/material.dart'; import 'package:cashumit/providers/receipt_state.dart'; import 'package:cashumit/models/receipt_item.dart'; import 'package:cashumit/services/receipt_service.dart'; // Pastikan ReceiptService sudah dibuat -import 'package:cashumit/models/firefly_account.dart'; // Untuk tipe akun import 'package:cashumit/models/local_receipt.dart'; import 'package:cashumit/services/local_receipt_service.dart'; import 'package:cashumit/services/account_cache_service.dart'; -import 'package:cashumit/services/account_mirror_service.dart'; import 'dart:math'; class ReceiptProvider with ChangeNotifier { diff --git a/lib/screens/local_receipts_screen.dart b/lib/screens/local_receipts_screen.dart index 57daa53..4f9bf34 100644 --- a/lib/screens/local_receipts_screen.dart +++ b/lib/screens/local_receipts_screen.dart @@ -142,86 +142,6 @@ class _LocalReceiptsScreenState extends State { ), body: Column( children: [ - // Summary card - Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Column( - children: [ - Text( - receipts - .where((r) => !r.isSubmitted) - .length - .toString(), - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.orange, - ), - ), - const Text( - 'Menunggu', - style: TextStyle(color: Colors.grey), - ), - ], - ), - Column( - children: [ - Text( - receipts - .where((r) => r.isSubmitted) - .length - .toString(), - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.green, - ), - ), - const Text( - 'Terkirim', - style: TextStyle(color: Colors.grey), - ), - ], - ), - Column( - children: [ - Text( - receipts.length.toString(), - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.blue, - ), - ), - const Text( - 'Total', - style: TextStyle(color: Colors.grey), - ), - ], - ), - ], - ), - ], - ), - ), // Receipts list Expanded( child: isLoading diff --git a/lib/screens/receipt_screen.dart b/lib/screens/receipt_screen.dart index 63c5974..5ee5f9e 100644 --- a/lib/screens/receipt_screen.dart +++ b/lib/screens/receipt_screen.dart @@ -4,9 +4,6 @@ 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/screens/local_receipts_screen.dart'; - -// Import widget komponen struk baru 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'; @@ -17,7 +14,6 @@ import 'package:cashumit/widgets/printing_status_card.dart'; // Import service baru import 'package:cashumit/services/account_dialog_service.dart'; import 'package:cashumit/services/esc_pos_print_service.dart'; -import 'package:cashumit/services/account_cache_service.dart'; // Import provider import 'package:provider/provider.dart'; @@ -140,6 +136,8 @@ class _ReceiptScreenState extends State { transactionDate: state.transactionDate, context: context, bluetoothService: _bluetoothService, + paymentAmount: state.paymentAmount, + isTip: state.isTip, ); if (!mounted) return; diff --git a/lib/screens/settings_screen.dart.bak b/lib/screens/settings_screen.dart.bak deleted file mode 100644 index 17e2d6e..0000000 --- a/lib/screens/settings_screen.dart.bak +++ /dev/null @@ -1,520 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.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/widgets/custom_text_config_dialog.dart'; // Tambahkan import ini - -class SettingsScreen extends StatefulWidget { - const SettingsScreen({super.key}); - - @override - State createState() => _SettingsScreenState(); -} - -class _SettingsScreenState extends State { - final _formKey = GlobalKey(); - final _urlController = TextEditingController(); - final _tokenController = TextEditingController(); - bool _isTestingConnection = false; - bool _isTestingAuth = false; - - // Bluetooth printer variables - BluetoothPrint bluetoothPrint = BluetoothPrint.instance; - bool _isScanning = false; - List _devices = []; - BluetoothDevice? _selectedDevice; - bool _connected = false; - - @override - void initState() { - super.initState(); - _loadSettings(); - _initBluetooth(); - } - - @override - void dispose() { - _urlController.dispose(); - _tokenController.dispose(); - super.dispose(); - } - - Future _loadSettings() async { - final prefs = await SharedPreferences.getInstance(); - // Tambahkan pengecekan mounted sebelum setState - if (mounted) { - setState(() { - _urlController.text = prefs.getString('firefly_url') ?? ''; - _tokenController.text = prefs.getString('firefly_token') ?? ''; - }); - } - } - - Future _initBluetooth() async { - // Periksa status koneksi - final isConnected = await bluetoothPrint.isConnected ?? false; - if (mounted) { - setState(() { - _connected = isConnected; - }); - } - - // Listen to bluetooth state changes - bluetoothPrint.state.listen((state) { - if (mounted) { - switch (state) { - case BluetoothPrint.CONNECTED: - setState(() { - _connected = true; - }); - break; - case BluetoothPrint.DISCONNECTED: - setState(() { - _connected = false; - _selectedDevice = null; - }); - break; - default: - break; - } - } - }); - - // Load saved device - await _loadSavedBluetoothDevice(); - } - - /// 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; - - if (mounted) { - setState(() { - _selectedDevice = 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 ?? ''); - } - - Future _saveSettings() async { - if (_formKey.currentState!.validate()) { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('firefly_url', _urlController.text.trim()); - await prefs.setString('firefly_token', _tokenController.text.trim()); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Pengaturan berhasil disimpan')), - ); - } - } - } - - Future _testConnection() async { - // Tambahkan pengecekan mounted - if (!mounted) return; - - setState(() { - _isTestingConnection = true; - }); - - // Simpan pengaturan terlebih dahulu - await _saveSettings(); - - // Uji koneksi - final success = await FireflyApiService.testConnection(baseUrl: _urlController.text.trim()); - - // Tambahkan pengecekan mounted sebelum setState - if (mounted) { - setState(() { - _isTestingConnection = false; - }); - - if (success) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Koneksi berhasil!')), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Koneksi gagal. Periksa URL dan pastikan Firefly III berjalan.')), - ); - } - } - } - - Future _testAuthentication() async { - // Tambahkan pengecekan mounted - if (!mounted) return; - - setState(() { - _isTestingAuth = true; - }); - - // Simpan pengaturan terlebih dahulu - await _saveSettings(); - - // Uji autentikasi - final success = await FireflyApiService.testAuthentication(baseUrl: _urlController.text.trim(), accessToken: _tokenController.text.trim()); - - // Tambahkan pengecekan mounted sebelum setState - if (mounted) { - setState(() { - _isTestingAuth = false; - }); - - if (success) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Autentikasi berhasil!')), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Autentikasi gagal. Periksa token yang digunakan.')), - ); - } - } - } - - Future _scanDevices() async { - setState(() { - _isScanning = true; - _devices = []; - }); - - try { - // Mulai scan perangkat Bluetooth - await bluetoothPrint.startScan(timeout: const Duration(seconds: 4)); - - // Listen to scan results - bluetoothPrint.scanResults.listen((devices) { - if (mounted) { - setState(() { - _devices = devices; - }); - } - }); - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Gagal memindai perangkat: $e')), - ); - } - } finally { - if (mounted) { - setState(() { - _isScanning = false; - }); - } - } - } - - Future _connectToDevice(BluetoothDevice device) async { - try { - await bluetoothPrint.connect(device); - setState(() { - _selectedDevice = device; - }); - - // Simpan device yang dipilih - await _saveBluetoothDevice(device); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Berhasil terhubung ke printer')), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Gagal terhubung ke printer: $e')), - ); - } - } - } - - /// Memutuskan koneksi dari printer bluetooth - Future _disconnect() async { - try { - await bluetoothPrint.disconnect(); - setState(() { - _connected = false; - _selectedDevice = null; - }); - - // Hapus device yang tersimpan - final prefs = await SharedPreferences.getInstance(); - await prefs.remove('bluetooth_device_address'); - await prefs.remove('bluetooth_device_name'); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Berhasil memutus koneksi dari printer')), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Gagal memutus koneksi dari printer: $e')), - ); - } - } - } - - /// Membuka dialog konfigurasi teks kustom - Future _openCustomTextConfig() async { - final result = await showDialog( - context: context, - builder: (context) => const CustomTextConfigDialog(), // Tambahkan import di bagian atas - ); - - // Jika teks kustom berhasil disimpan, tampilkan snackbar - if (result == true && mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Teks kustom berhasil disimpan')), - ); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Pengaturan'), - centerTitle: true, - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // === Firefly III Settings === - const Text( - 'Koneksi ke Firefly III', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - const Text( - 'Masukkan URL lengkap ke instance Firefly III Anda (contoh: https://firefly.example.com)', - ), - const SizedBox(height: 8), - TextFormField( - controller: _urlController, - decoration: const InputDecoration( - labelText: 'Firefly III URL', - border: OutlineInputBorder(), - hintText: 'https://firefly.example.com', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Mohon masukkan URL Firefly III'; - } - return null; - }, - ), - const SizedBox(height: 16), - const Text( - 'Masukkan Personal Access Token dari Firefly III', - ), - const SizedBox(height: 8), - TextFormField( - controller: _tokenController, - decoration: const InputDecoration( - labelText: 'Access Token', - border: OutlineInputBorder(), - ), - obscureText: true, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Mohon masukkan access token'; - } - return null; - }, - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: _isTestingConnection ? null : _testConnection, - child: _isTestingConnection - ? const Text('Menguji Koneksi...') - : const Text('Uji Koneksi'), - ), - ), - const SizedBox(width: 16), - Expanded( - child: ElevatedButton( - onPressed: _isTestingAuth ? null : _testAuthentication, - child: _isTestingAuth - ? const Text('Menguji Auth...') - : const Text('Uji Auth'), - ), - ), - ], - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _saveSettings, - child: const Text('Simpan Pengaturan Firefly III'), - ), - ), - - const SizedBox(height: 32), - - // === Printer Settings === - const Text( - 'Pengaturan Printer Bluetooth', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - const Text( - 'Hubungkan printer thermal Anda melalui Bluetooth untuk mencetak struk.', - ), - const SizedBox(height: 16), - - // Status koneksi - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Status Printer:'), - Text( - _connected ? 'Terhubung' : 'Terputus', - style: TextStyle( - color: _connected ? Colors.green : Colors.red, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - - const SizedBox(height: 16), - - // Tombol scan - ElevatedButton.icon( - onPressed: _isScanning ? null : _scanDevices, - icon: _isScanning - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.bluetooth_searching), - label: Text(_isScanning ? 'Memindai...' : 'Cari Printer'), - ), - - const SizedBox(height: 16), - - // Daftar perangkat - SizedBox( - height: 200, // Tentukan tinggi tetap untuk daftar - child: _devices.isEmpty - ? const Center( - child: Text('Tidak ada perangkat ditemukan'), - ) - : ListView.builder( - itemCount: _devices.length, - itemBuilder: (context, index) { - final device = _devices[index]; - final isSelected = _selectedDevice?.address == device.address; - - return Card( - child: ListTile( - title: Text(device.name ?? 'Unknown Device'), - subtitle: Text(device.address ?? ''), - trailing: isSelected - ? const Icon(Icons.check, color: Colors.green) - : null, - onTap: () => _connectToDevice(device), - ), - ); - }, - ), - ), - - // Tombol disconnect - if (_connected) - Padding( - padding: const EdgeInsets.only(top: 16), - child: ElevatedButton.icon( - onPressed: _disconnect, - icon: const Icon(Icons.bluetooth_disabled), - label: const Text('Putus Koneksi'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - ), - ), - ), - - ), - ), - - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - // Fungsi tambahan jika diperlukan - }, - child: const Text('Simpan Pengaturan Printer'), - ), - ), - - const SizedBox(height: 32), - - // === Custom Text Settings === - const Text( - 'Teks Kustom Struk', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - const Text( - 'Sesuaikan teks disclaimer, ucapan terima kasih, dan pantun di struk Anda.', - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _openCustomTextConfig, - child: const Text('Edit Teks Kustom'), - ), - ), - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/transaction_screen.dart b/lib/screens/transaction_screen.dart index 5062d57..d22b519 100644 --- a/lib/screens/transaction_screen.dart +++ b/lib/screens/transaction_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/firefly_account.dart'; import '../services/firefly_api_service.dart'; -import '../services/account_mirror_service.dart'; import '../services/account_cache_service.dart'; class TransactionScreen extends StatefulWidget { diff --git a/lib/services/esc_pos_print_service.dart b/lib/services/esc_pos_print_service.dart index 1f783c2..038ca34 100644 --- a/lib/services/esc_pos_print_service.dart +++ b/lib/services/esc_pos_print_service.dart @@ -13,6 +13,8 @@ class EscPosPrintService { static Future> generateEscPosBytes({ required List items, required DateTime transactionDate, + double paymentAmount = 0.0, + bool isTip = false, }) async { print('Memulai generateEscPosBytes...'); print('Jumlah item: ${items.length}'); @@ -184,6 +186,64 @@ class EscPosPrintService { bytes += generator.text('================================', styles: PosStyles(align: PosAlign.center)); + // Informasi pembayaran dan kembalian + if (paymentAmount > 0) { + final changeAmount = paymentAmount - totalAmount; + bytes += generator.row([ + PosColumn( + text: 'BAYAR', + width: 8, + styles: PosStyles(align: PosAlign.left), + ), + PosColumn( + text: formatRupiah(paymentAmount), + width: 12, + styles: PosStyles(align: PosAlign.right), + ), + ]); + + if (changeAmount >= 0) { + bytes += generator.row([ + PosColumn( + text: 'KEMBALI', + width: 8, + styles: PosStyles(align: PosAlign.left), + ), + PosColumn( + text: formatRupiah(changeAmount), + width: 12, + styles: PosStyles(align: PosAlign.right), + ), + ]); + + if (isTip && changeAmount > 0) { + bytes += generator.text('SEBAGAI TIP: YA', + styles: PosStyles(align: PosAlign.center, bold: true)); + bytes += generator.text('(Uang kembalian sebagai tip)', + styles: PosStyles(align: PosAlign.center)); + } else if (changeAmount > 0) { + bytes += generator.text('SEBAGAI TIP: TIDAK', + styles: PosStyles(align: PosAlign.center)); + } + } else { + bytes += generator.row([ + PosColumn( + text: 'KURANG', + width: 8, + styles: PosStyles(align: PosAlign.left), + ), + PosColumn( + text: formatRupiah(changeAmount.abs()), + width: 12, + styles: PosStyles(align: PosAlign.right), + ), + ]); + } + + bytes += generator.text('--------------------------------', + styles: PosStyles(align: PosAlign.center)); + } + // Memuat teks kustom dari shared preferences String customDisclaimer; try { @@ -289,6 +349,8 @@ class EscPosPrintService { required DateTime transactionDate, required BuildContext context, required dynamic bluetoothService, // Kita akan sesuaikan tipe ini nanti + double paymentAmount = 0.0, + bool isTip = false, }) async { print('=== FUNGSI printToThermalPrinter DIPANGGIL ==='); print('Memulai proses pencetakan struk...'); @@ -305,6 +367,8 @@ class EscPosPrintService { final bytes = await generateEscPosBytes( items: items, transactionDate: transactionDate, + paymentAmount: paymentAmount, + isTip: isTip, ); print('Byte array ESC/POS berhasil dihasilkan'); diff --git a/lib/services/pdf_export_service.dart b/lib/services/pdf_export_service.dart deleted file mode 100644 index f4a03f5..0000000 --- a/lib/services/pdf_export_service.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'dart:io'; -import 'package:path_provider/path_provider.dart'; -import 'package:pdf/widgets.dart' as pw; -import 'package:cashumit/models/receipt_item.dart'; -import 'package:open_file/open_file.dart'; - -class PdfExportService { - static Future generateReceiptPdf( - List items, - String storeName, - String storeAddress, - String storePhone, - ) async { - try { - final pdf = pw.Document(); - - pdf.addPage( - pw.Page( - build: (pw.Context context) => pw.Column( - children: [ - // Header - pw.Text( - storeName, - style: pw.TextStyle( - fontSize: 20, - fontWeight: pw.FontWeight.bold, - ), - textAlign: pw.TextAlign.center, - ), - pw.SizedBox(height: 4), - pw.Text(storeAddress, textAlign: pw.TextAlign.center), - pw.Text(storePhone, textAlign: pw.TextAlign.center), - pw.SizedBox(height: 8), - pw.Divider(), - pw.SizedBox(height: 8), - - // Item list header - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Expanded( - flex: 3, - child: pw.Text( - 'ITEM', - style: pw.TextStyle(fontWeight: pw.FontWeight.bold), - ), - ), - pw.Expanded( - flex: 1, - child: pw.Text( - 'QTY', - style: pw.TextStyle(fontWeight: pw.FontWeight.bold), - textAlign: pw.TextAlign.center, - ), - ), - pw.Expanded( - flex: 2, - child: pw.Text( - 'HARGA', - style: pw.TextStyle(fontWeight: pw.FontWeight.bold), - textAlign: pw.TextAlign.right, - ), - ), - pw.Expanded( - flex: 2, - child: pw.Text( - 'TOTAL', - style: pw.TextStyle(fontWeight: pw.FontWeight.bold), - textAlign: pw.TextAlign.right, - ), - ), - ], - ), - pw.SizedBox(height: 4), - pw.Divider(), - pw.SizedBox(height: 8), - - // Items - ...items.map( - (item) => pw.Padding( - padding: const pw.EdgeInsets.only(bottom: 4.0), - child: pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Expanded( - flex: 3, - child: pw.Text( - item.description, - style: const pw.TextStyle(fontSize: 14), - ), - ), - pw.Expanded( - flex: 1, - child: pw.Text( - item.quantity.toString(), - textAlign: pw.TextAlign.center, - style: const pw.TextStyle(fontSize: 14), - ), - ), - pw.Expanded( - flex: 2, - child: pw.Text( - item.price.toStringAsFixed(0), - textAlign: pw.TextAlign.right, - style: const pw.TextStyle(fontSize: 14), - ), - ), - pw.Expanded( - flex: 2, - child: pw.Text( - item.total.toStringAsFixed(0), - textAlign: pw.TextAlign.right, - style: const pw.TextStyle(fontSize: 14), - ), - ), - ], - ), - ), - ), - - pw.SizedBox(height: 8), - pw.Divider(), - pw.SizedBox(height: 8), - - // Total - pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text( - 'TOTAL:', - style: pw.TextStyle( - fontSize: 16, - fontWeight: pw.FontWeight.bold, - ), - ), - pw.Text( - items.fold(0.0, (sum, item) => sum + item.total).toStringAsFixed(0), - style: pw.TextStyle( - fontSize: 16, - fontWeight: pw.FontWeight.bold, - ), - ), - ], - ), - ], - ), - ), - ); - - // Dapatkan direktori dokumen - final dir = await getApplicationDocumentsDirectory(); - final file = File("${dir.path}/receipt.pdf"); - final pdfBytes = await pdf.save(); - await file.writeAsBytes(pdfBytes); - - return file.path; - } catch (e) { - // Handle error - return null; - } - } - - /// Membuka file PDF yang telah dibuat - static Future openPdf(String filePath) async { - try { - await OpenFile.open(filePath); - } catch (e) { - // Handle error - } - } -} \ No newline at end of file diff --git a/lib/services/struk_text_generator.dart b/lib/services/struk_text_generator.dart deleted file mode 100644 index 5b9c292..0000000 --- a/lib/services/struk_text_generator.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:cashumit/models/receipt_item.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:intl/intl.dart'; - -/// Class untuk menghasilkan struk dalam format teks untuk printer thermal -class StrukTextGenerator { - /// Fungsi untuk mengatur teks ke tengah - static String centerText(String text, int width, [String padChar = ' ']) { - if (text.length >= width) return text; - int padLeft = (width - text.length) ~/ 2; - int padRight = width - text.length - padLeft; - return padChar * padLeft + text + padChar * padRight; - } - - /// Fungsi untuk membuat garis pemisah - static String createSeparator(int width, String char) { - return char * width; - } - - /// Menghasilkan struk dalam format teks berdasarkan data transaksi - static Future generateStrukText({ - required List items, - required DateTime transactionDate, - required String cashierId, - required String transactionId, - }) async { - print('Memulai generateStrukText...'); - print('Jumlah item: ${items.length}'); - print('Tanggal transaksi: $transactionDate'); - print('ID kasir: $cashierId'); - print('ID transaksi: $transactionId'); - - // Load store info from shared preferences - final prefs = await SharedPreferences.getInstance(); - final storeName = prefs.getString('store_name') ?? 'TOKO SEMBAKO MURAH'; - final storeAddress = prefs.getString('store_address') ?? 'Jl. Merdeka No. 123'; - final adminName = prefs.getString('admin_name') ?? 'Budi Santoso'; - final adminPhone = prefs.getString('admin_phone') ?? '08123456789'; - - print('Nama toko: $storeName'); - print('Alamat toko: $storeAddress'); - print('Nama admin: $adminName'); - print('Telepon admin: $adminPhone'); - - // Format tanggal - final dateFormatter = DateFormat('dd/MM/yyyy'); - final formattedDate = dateFormatter.format(transactionDate); - print('Tanggal yang diformat: $formattedDate'); - - // Format angka ke rupiah - String formatRupiah(double amount) { - final formatter = NumberFormat("#,##0", "id_ID"); - return "Rp ${formatter.format(amount)}"; - } - - // Bangun struk dalam format teks - final buffer = StringBuffer(); - - // Header toko - menggunakan lebar 32 karakter untuk kompatibilitas printer termal - buffer.writeln(centerText(storeName, 32)); - buffer.writeln(centerText(storeAddress, 32)); - buffer.writeln(centerText('Admin: $adminName', 32)); - buffer.writeln(centerText('Telp: $adminPhone', 32)); - buffer.writeln(centerText(' $formattedDate ', 32)); - buffer.writeln(createSeparator(32, '=')); - - // Header tabel - disesuaikan lebar kolom untuk printer termal - buffer.writeln('ITEM QTY HARGA TOTAL'); - buffer.writeln(createSeparator(32, '-')); - - // Item list - print('Memulai iterasi item...'); - for (int i = 0; i < items.length; i++) { - var item = items[i]; - print('Item $i: ${item.description}, qty: ${item.quantity}, price: ${item.price}, total: ${item.total}'); - - // Nama item (potong jika terlalu panjang) - maks 12 karakter - String itemName = item.description.length > 12 - ? '${item.description.substring(0, 9)}...' - : item.description.padRight(12); - - // Quantity (3 karakter) - String qty = item.quantity.toString().padRight(3); - - // Harga (7 karakter) - String price = formatRupiah(item.price).padLeft(7); - - // Total (7 karakter) - String total = formatRupiah(item.total).padLeft(7); - - buffer.writeln('$itemName $qty $price $total'); - } - print('Selesai iterasi item'); - - // Garis pemisah sebelum total - buffer.writeln(createSeparator(32, '-')); - - // Total - final totalAmount = items.fold(0.0, (sum, item) => sum + item.total); - print('Total amount: $totalAmount'); - buffer.writeln('TOTAL: ${formatRupiah(totalAmount)}'.padLeft(32)); - - // Garis pemisah setelah total - buffer.writeln(createSeparator(32, '=')); - - // Memuat teks kustom dari shared preferences - final customDisclaimer = prefs.getString('store_disclaimer_text') ?? - 'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. ' - 'Harap periksa kembali struk belanja Anda sebelum meninggalkan toko.'; - final customThankYou = prefs.getString('thank_you_text') ?? '*** TERIMA KASIH ***'; - final customPantun = prefs.getString('pantun_text') ?? - 'Belanja di toko kami, hemat dan nyaman,\n' - 'Dengan penuh semangat, kami siap melayani,\n' - 'Harapan kami, Anda selalu puas,\n' - 'Sampai jumpa lagi, selamat tinggal.'; - - // Menambahkan disclaimer - buffer.writeln(''); - // Memecah disclaimer menjadi beberapa baris jika terlalu panjang - final disclaimerLines = customDisclaimer.split(' '); - final List wrappedDisclaimer = []; - String currentLine = ''; - - for (final word in disclaimerLines) { - if ((currentLine + word).length > 32) { - wrappedDisclaimer.add(currentLine.trim()); - currentLine = word + ' '; - } else { - currentLine += word + ' '; - } - } - if (currentLine.trim().isNotEmpty) { - wrappedDisclaimer.add(currentLine.trim()); - } - - for (final line in wrappedDisclaimer) { - buffer.writeln(centerText(line, 32)); - } - - // Menambahkan ucapan terima kasih - buffer.writeln(''); - buffer.writeln(centerText(customThankYou, 32)); - - // Menambahkan pantun - buffer.writeln(''); - final pantunLines = customPantun.split('\n'); - for (final line in pantunLines) { - buffer.writeln(centerText(line, 32)); - } - - // Spasi di akhir - buffer.writeln(''); - buffer.writeln(''); - - final result = buffer.toString(); - print('Teks struk yang dihasilkan:'); - print(result); - - return result; - } -} diff --git a/lib/utils/image_validator.dart b/lib/utils/image_validator.dart deleted file mode 100644 index d446501..0000000 --- a/lib/utils/image_validator.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:io'; -import 'dart:typed_data'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show instantiateImageCodec; -import 'package:path_provider/path_provider.dart'; - -/// Utility to validate image files -class ImageValidator { - /// Validate if a file is a valid image by trying to decode it - static Future validateImageFile(String filePath) async { - try { - final file = File(filePath); - if (!await file.exists()) { - print('File does not exist: $filePath'); - return false; - } - - final bytes = await file.readAsBytes(); - print('File size: ${bytes.length} bytes'); - - if (bytes.isEmpty) { - print('File is empty: $filePath'); - return false; - } - - // Try to decode as image - final codec = await instantiateImageCodec(bytes); - final frameInfo = await codec.getNextFrame(); - final image = frameInfo.image; - - print('Image dimensions: ${image.width}x${image.height}'); - await image.dispose(); - await codec.dispose(); - - return true; - } catch (e) { - print('Failed to validate image file $filePath: $e'); - return false; - } - } - - /// Validate images in the documents directory - static Future validateStoredImages() async { - try { - final dir = await getApplicationDocumentsDirectory(); - final logoDir = Directory('${dir.path}/logos'); - - if (!await logoDir.exists()) { - print('Logo directory does not exist'); - return; - } - - final files = logoDir.listSync(); - print('Found ${files.length} files in logo directory'); - - for (final file in files) { - if (file is File) { - print('Validating ${file.path}...'); - final isValid = await validateImageFile(file.path); - print('Validation result: $isValid'); - } - } - } catch (e) { - print('Error validating stored images: $e'); - } - } -} \ No newline at end of file diff --git a/lib/widgets/firefly_transaction_info.dart b/lib/widgets/firefly_transaction_info.dart deleted file mode 100644 index 47f5fb8..0000000 --- a/lib/widgets/firefly_transaction_info.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; - -/// Widget untuk menampilkan informasi transaksi Firefly -/// Menampilkan sumber (kiri), panah (tengah), dan destinasi (kanan) -class FireflyTransactionInfo extends StatelessWidget { - final String sourceAccount; - final String destinationAccount; - - const FireflyTransactionInfo({ - super.key, - required this.sourceAccount, - required this.destinationAccount, - }); - - @override - Widget build(BuildContext context) { - // Mencoba menggunakan Google Fonts Courier Prime, jika gagal gunakan font sistem - TextStyle courierPrime; - try { - courierPrime = GoogleFonts.courierPrime( - textStyle: const TextStyle( - fontSize: 14, - height: 1.2, - ), - ); - } catch (e) { - // Fallback ke font sistem jika Google Fonts tidak tersedia - courierPrime = const TextStyle( - fontFamily: 'CourierPrime, Courier, monospace', - fontSize: 14, - height: 1.2, - ); - } - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(8.0), - color: Colors.white, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Sumber akun - Expanded( - flex: 3, - child: Text( - sourceAccount, - style: courierPrime, - textAlign: TextAlign.left, - overflow: TextOverflow.ellipsis, - ), - ), - // Panah kanan - const Expanded( - flex: 1, - child: Icon( - Icons.arrow_forward, - size: 16, - ), - ), - // Destinasi akun - Expanded( - flex: 3, - child: Text( - destinationAccount, - style: courierPrime, - textAlign: TextAlign.right, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/receipt_speed_dial.dart b/lib/widgets/receipt_speed_dial.dart index fad4374..8474654 100644 --- a/lib/widgets/receipt_speed_dial.dart +++ b/lib/widgets/receipt_speed_dial.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:cashumit/services/bluetooth_service.dart'; -import 'package:cashumit/screens/local_receipts_screen.dart'; import 'package:provider/provider.dart'; import 'package:cashumit/providers/receipt_provider.dart'; diff --git a/lib/widgets/receipt_widgets.dart b/lib/widgets/receipt_widgets.dart deleted file mode 100644 index ed4b24e..0000000 --- a/lib/widgets/receipt_widgets.dart +++ /dev/null @@ -1,294 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:cashumit/models/receipt_item.dart'; - -/// Widget untuk header struk -class ReceiptHeader extends StatelessWidget { - final String storeName; - final String storeAddress; - final String storePhone; - - const ReceiptHeader({ - super.key, - required this.storeName, - required this.storeAddress, - required this.storePhone, - }); - - @override - Widget build(BuildContext context) { - final courierPrime = GoogleFonts.courierPrime( - textStyle: const TextStyle( - fontSize: 14, - height: 1.2, - ), - ); - - return Column( - children: [ - 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), - ], - ); - } -} - -/// Widget untuk informasi transaksi (tanggal, kasir, nota) -class ReceiptTransactionInfo extends StatelessWidget { - final DateTime transactionDate; - final String cashierId; - final String transactionId; - - const ReceiptTransactionInfo({ - super.key, - required this.transactionDate, - required this.cashierId, - required this.transactionId, - }); - - @override - Widget build(BuildContext context) { - final courierPrime = GoogleFonts.courierPrime( - textStyle: const TextStyle( - fontSize: 14, - height: 1.2, - ), - ); - - return Column( - children: [ - 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), - ], - ); - } -} - -/// Widget untuk header daftar item -class ReceiptItemHeader extends StatelessWidget { - const ReceiptItemHeader({super.key}); - - @override - Widget build(BuildContext context) { - return const Column( - children: [ - SizedBox(height: 4), - 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, - ), - ), - ], - ), - SizedBox(height: 2), - Divider(thickness: 1, height: 1, color: Colors.black), - SizedBox(height: 4), - ], - ); - } -} - -/// Widget untuk menampilkan satu item dalam struk -class ReceiptItemRow extends StatelessWidget { - final ReceiptItem item; - - const ReceiptItemRow({ - super.key, - required this.item, - }); - - @override - Widget build(BuildContext context) { - final courierPrime = GoogleFonts.courierPrime( - textStyle: const TextStyle( - fontSize: 14, - height: 1.2, - ), - ); - - return Padding( - padding: const EdgeInsets.only(bottom: 2.0), - 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, - ), - ), - ], - ), - ); - } -} - -/// Widget untuk menampilkan total -class ReceiptTotal extends StatelessWidget { - final double total; - - const ReceiptTotal({ - super.key, - required this.total, - }); - - @override - Widget build(BuildContext context) { - final courierPrime = GoogleFonts.courierPrime( - textStyle: const TextStyle( - fontSize: 14, - height: 1.2, - ), - ); - - return Column( - children: [ - const SizedBox(height: 4), - const Divider(thickness: 1, height: 1, color: Colors.black), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'TOTAL:', - style: courierPrime.copyWith( - fontWeight: FontWeight.bold, - ), - ), - Text( - total.toStringAsFixed(0), - style: courierPrime.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ], - ); - } -} - -/// Widget untuk footer struk -class ReceiptFooter extends StatelessWidget { - const ReceiptFooter({super.key}); - - @override - Widget build(BuildContext context) { - final courierPrime = GoogleFonts.courierPrime( - textStyle: const TextStyle( - fontSize: 14, - height: 1.2, - ), - ); - - return Column( - children: [ - 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, - ), - ], - ); - } -} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 92acd71..02400e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,38 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + url: "https://pub.dev" + source: hosted + version: "47.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "0c284758213de797488b570a8e0feaa6546b0e8dd189be6128d49bb0413d021a" + url: "https://pub.dev" + source: hosted + version: "0.10.0" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: @@ -128,6 +160,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_code_metrics: + dependency: "direct dev" + description: + name: dart_code_metrics + sha256: "3a01766c3925b884a171a396f115f48977e367fe656de065ed7012f3d9540273" + url: "https://pub.dev" + source: hosted + version: "4.19.2" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" + source: hosted + version: "2.2.4" fake_async: dependency: transitive description: @@ -148,10 +196,10 @@ packages: dependency: transitive description: name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "6.1.4" file_selector_linux: dependency: transitive description: @@ -247,6 +295,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" google_fonts: dependency: "direct main" description: @@ -519,6 +575,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -623,6 +687,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" qr: dependency: transitive description: @@ -772,6 +844,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.5" + watcher: + dependency: transitive + description: + name: watcher + sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249 + url: "https://pub.dev" + source: hosted + version: "1.2.0" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cc1d983..833d74a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^4.0.0 flutter_launcher_icons: ^0.11.0 + dart_code_metrics: ^4.19.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec