parent
631066024c
commit
f1ca6db22c
|
|
@ -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)}";
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -142,86 +142,6 @@ class _LocalReceiptsScreenState extends State<LocalReceiptsScreen> {
|
|||
),
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<ReceiptScreen> {
|
|||
transactionDate: state.transactionDate,
|
||||
context: context,
|
||||
bluetoothService: _bluetoothService,
|
||||
paymentAmount: state.paymentAmount,
|
||||
isTip: state.isTip,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
|
|
|||
|
|
@ -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<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _urlController = TextEditingController();
|
||||
final _tokenController = TextEditingController();
|
||||
bool _isTestingConnection = false;
|
||||
bool _isTestingAuth = false;
|
||||
|
||||
// Bluetooth printer variables
|
||||
BluetoothPrint bluetoothPrint = BluetoothPrint.instance;
|
||||
bool _isScanning = false;
|
||||
List<BluetoothDevice> _devices = [];
|
||||
BluetoothDevice? _selectedDevice;
|
||||
bool _connected = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
_initBluetooth();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_urlController.dispose();
|
||||
_tokenController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _openCustomTextConfig() async {
|
||||
final result = await showDialog<bool>(
|
||||
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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ class EscPosPrintService {
|
|||
static Future<List<int>> generateEscPosBytes({
|
||||
required List<ReceiptItem> 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');
|
||||
|
|
|
|||
|
|
@ -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<String?> generateReceiptPdf(
|
||||
List<ReceiptItem> 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<void> openPdf(String filePath) async {
|
||||
try {
|
||||
await OpenFile.open(filePath);
|
||||
} catch (e) {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> generateStrukText({
|
||||
required List<ReceiptItem> 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<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<bool> 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<void> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
84
pubspec.lock
84
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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue