854 lines
27 KiB
Dart
854 lines
27 KiB
Dart
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<Item> items;
|
|
|
|
const TransactionScreen({
|
|
super.key,
|
|
required this.items,
|
|
});
|
|
|
|
@override
|
|
State<TransactionScreen> createState() => _TransactionScreenState();
|
|
}
|
|
|
|
class _TransactionScreenState extends State<TransactionScreen> {
|
|
final BluetoothPrint bluetoothPrint = BluetoothPrint.instance;
|
|
List<BluetoothDevice> _devices = [];
|
|
BluetoothDevice? _selectedDevice;
|
|
bool _isScanning = false;
|
|
final List<Map<String, dynamic>> _cart = [];
|
|
int _total = 0;
|
|
String _paymentMethod = 'Tunai';
|
|
final TextEditingController _searchController = TextEditingController();
|
|
List<Item> _filteredItems = [];
|
|
|
|
// Transaction settings directly in this screen
|
|
late DateTime _transactionDate;
|
|
List<Map<String, dynamic>> _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<void> _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<void> _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 = <Map<String, dynamic>>[];
|
|
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<void> _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<void> _selectSourceAccount() async {
|
|
if (_accounts.isEmpty) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Daftar akun belum dimuat')),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
final selectedAccount = await showDialog<Map<String, dynamic>?>(
|
|
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<void> _selectDestinationAccount() async {
|
|
if (_accounts.isEmpty) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Daftar akun belum dimuat')),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
final selectedAccount = await showDialog<Map<String, dynamic>?>(
|
|
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<void> _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<bool>(
|
|
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<BluetoothDevice>(
|
|
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<String>(
|
|
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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
} |