cleaning dan menambahkan pembayaran, kembalian, dan tip di struk cetak

master v1.4.1
a2nr 2025-12-28 10:51:15 +07:00
parent 631066024c
commit f1ca6db22c
18 changed files with 149 additions and 3338 deletions

View File

@ -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)}";
}
}

View File

@ -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,
);
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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;

View File

@ -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'),
),
),
],
),
),
),
);
}
}

View File

@ -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 {

View File

@ -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');

View File

@ -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
}
}
}

View File

@ -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;
}
}

View File

@ -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');
}
}
}

View File

@ -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,
),
),
],
),
);
}
}

View File

@ -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';

View File

@ -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,
),
],
);
}
}

View File

@ -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:

View File

@ -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