cashumit/lib/screens/receipt_screen.dart

1222 lines
43 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cashumit/models/receipt_item.dart';
import 'package:cashumit/screens/add_item_screen.dart';
import 'package:cashumit/screens/settings_screen.dart';
import 'package:cashumit/screens/printer_setup_screen.dart';
import 'package:cashumit/services/pdf_export_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';
class ReceiptScreen extends StatefulWidget {
const ReceiptScreen({super.key});
@override
State<ReceiptScreen> createState() => _ReceiptScreenState();
}
class _ReceiptScreenState extends State<ReceiptScreen> {
List<ReceiptItem> items = [
ReceiptItem(description: 'Apel', quantity: 3, price: 5000),
ReceiptItem(description: 'Roti', quantity: 2, price: 12000),
ReceiptItem(description: 'Susu', quantity: 1, price: 15000),
];
final String storeName = 'TOKO SEMBAKO MURAH';
final String storeAddress = 'Jl. Merdeka No. 123';
final String storePhone = 'Telp: 021-12345678';
final String cashierId = 'KSR001';
final String transactionId = 'TXN202508200001';
// Fields for transaction settings directly in main UI
late DateTime _transactionDate;
List<Map<String, dynamic>> _accounts = [];
String? _sourceAccountId;
String? _sourceAccountName;
String? _destinationAccountId;
String? _destinationAccountName;
// Controllers for manual account input
final TextEditingController _sourceAccountController = TextEditingController();
final TextEditingController _destinationAccountController = TextEditingController();
// Bluetooth printer variables
BluetoothPrint bluetoothPrint = BluetoothPrint.instance;
bool _bluetoothConnected = false;
BluetoothDevice? _bluetoothDevice;
bool _bluetoothScanning = false;
@override
void initState() {
super.initState();
_transactionDate = DateTime.now();
_loadCredentialsAndAccounts();
_initBluetooth();
_loadSavedBluetoothDevice();
}
@override
void dispose() {
_sourceAccountController.dispose();
_destinationAccountController.dispose();
super.dispose();
}
String? _fireflyUrl;
String? _accessToken;
/// 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 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;
setState(() {
_bluetoothDevice = 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 ?? '');
}
/// Memuat daftar akun sumber (revenue) dan tujuan (asset) dari API.
Future<void> _loadAccounts() async {
if (_fireflyUrl == null || _accessToken == null) {
return;
}
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;
});
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal memuat akun: $error')),
);
}
}
}
/// Inisialisasi Bluetooth printer
Future<void> _initBluetooth() async {
// Memeriksa status koneksi Bluetooth
final isConnected = await bluetoothPrint.isConnected ?? false;
if (isConnected) {
setState(() {
_bluetoothConnected = true;
});
}
// Listen to bluetooth state changes
bluetoothPrint.state.listen((state) {
if (mounted) {
switch (state) {
case BluetoothPrint.CONNECTED:
setState(() {
_bluetoothConnected = true;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Printer terhubung')),
);
}
break;
case BluetoothPrint.DISCONNECTED:
setState(() {
_bluetoothConnected = false;
// Jangan set _bluetoothDevice ke null, biarkan device yang sudah dipilih
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Printer terputus')),
);
}
break;
default:
break;
}
}
});
}
/// Memeriksa ulang status koneksi bluetooth
Future<void> _checkBluetoothStatus() async {
final isConnected = await bluetoothPrint.isConnected ?? false;
if (mounted) {
setState(() {
_bluetoothConnected = isConnected;
});
if (isConnected) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Printer terhubung')),
);
} else {
// Jika ada device yang sudah dipilih sebelumnya, coba sambungkan kembali
if (_bluetoothDevice != null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Mencoba menyambungkan kembali ke printer...')),
);
_reconnectToBluetoothDevice();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Printer tidak terhubung. Silakan sambungkan melalui menu Setup Printer atau ikon Bluetooth di atas.')),
);
}
}
}
}
/// Menyambungkan kembali ke printer bluetooth yang sudah dipilih
Future<void> _reconnectToBluetoothDevice() async {
if (_bluetoothDevice != null) {
try {
await bluetoothPrint.connect(_bluetoothDevice!);
if (mounted) {
setState(() {
_bluetoothConnected = true;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Berhasil terhubung kembali ke printer')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal menyambungkan kembali ke printer: $e')),
);
}
}
}
}
/// Scan perangkat Bluetooth
Future<void> _scanBluetoothDevices() async {
setState(() {
_bluetoothScanning = true;
});
try {
// Mulai scan perangkat Bluetooth
await bluetoothPrint.startScan(timeout: const Duration(seconds: 4));
if (mounted) {
final selectedDevice = await showDialog<BluetoothDevice?>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Pilih Perangkat Printer'),
content: StreamBuilder<List<BluetoothDevice>>(
stream: bluetoothPrint.scanResults,
initialData: const [],
builder: (c, snapshot) {
if (snapshot.data!.isEmpty) {
return const Text('Tidak ada perangkat ditemukan');
}
return SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final device = snapshot.data![index];
return ListTile(
title: Text(device.name ?? 'Unknown Device'),
subtitle: Text(device.address ?? ''),
onTap: () {
Navigator.of(context).pop(device);
},
);
},
),
);
},
),
);
},
);
if (selectedDevice != null) {
setState(() {
_bluetoothDevice = selectedDevice;
});
// Simpan device yang dipilih
await _saveBluetoothDevice(selectedDevice);
// Hubungkan ke perangkat yang dipilih
await bluetoothPrint.connect(selectedDevice);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal memindai perangkat: $e')),
);
}
} finally {
setState(() {
_bluetoothScanning = false;
});
}
}
/// Cetak struk ke printer thermal
Future<void> _printToThermalPrinter() async {
print('=== MULAI MENCETAK KE PRINTER THERMAL ===');
// Periksa koneksi printer
final isConnected = await bluetoothPrint.isConnected ?? false;
if (!isConnected) {
print('ERROR: Printer tidak terhubung');
if (mounted) {
// Coba sambungkan kembali jika ada device yang tersimpan
if (_bluetoothDevice != null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Mencoba menyambungkan kembali ke printer...')),
);
try {
await bluetoothPrint.connect(_bluetoothDevice!);
// Perbarui status koneksi
setState(() {
_bluetoothConnected = true;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal menyambungkan kembali ke printer: $e')),
);
return;
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Harap hubungkan printer terlebih dahulu')),
);
return;
}
} else {
return;
}
}
print('Printer terhubung, mencoba mencetak...');
print('Device address: ${_bluetoothDevice?.address}');
print('Device name: ${_bluetoothDevice?.name}');
try {
// Konfigurasi untuk mencetak struk
Map<String, dynamic> config = {};
// Data yang akan dicetak
List<LineText> list = [];
print('Membuat data struk untuk dicetak...');
// Header struk
list.add(LineText(type: LineText.TYPE_TEXT, content: storeName, weight: 1, align: LineText.ALIGN_CENTER, fontZoom: 2, linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: storeAddress, align: LineText.ALIGN_CENTER, linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: storePhone, align: LineText.ALIGN_CENTER, linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1));
// Info transaksi
list.add(LineText(type: LineText.TYPE_TEXT, content: 'TANGGAL: ${_transactionDate.day.toString().padLeft(2, '0')}/${_transactionDate.month.toString().padLeft(2, '0')}/${_transactionDate.year}', linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: 'KASIR: $cashierId', linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: 'NOTA: $transactionId', linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1));
// Header item
list.add(LineText(type: LineText.TYPE_TEXT, content: 'ITEM Q HARGA TOTAL', weight: 1, linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1));
// Item list
for (var item in items) {
// Nama item (potong jika terlalu panjang)
String itemName = item.description.length > 12
? item.description.substring(0, 12)
: item.description.padRight(12);
// Quantity (3 karakter)
String qty = item.quantity.toString().padLeft(3);
// Harga (8 karakter)
String price = item.price.toStringAsFixed(0).padLeft(8);
// Total (8 karakter)
String total = item.total.toStringAsFixed(0).padLeft(8);
list.add(LineText(type: LineText.TYPE_TEXT, content: '$itemName $qty $price $total', linefeed: 1));
}
list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1));
// Total
list.add(LineText(type: LineText.TYPE_TEXT, content: 'TOTAL: ${_calculateTotal().toStringAsFixed(0).padLeft(8)}', weight: 1, linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: '--------------------------------', align: LineText.ALIGN_CENTER, linefeed: 1));
// Footer
list.add(LineText(type: LineText.TYPE_TEXT, content: '*** TERIMA KASIH ***', align: LineText.ALIGN_CENTER, fontZoom: 1, linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: 'Barang yang sudah dibeli', align: LineText.ALIGN_CENTER, linefeed: 1));
list.add(LineText(type: LineText.TYPE_TEXT, content: 'tidak dapat dikembalikan/ditukar', align: LineText.ALIGN_CENTER, linefeed: 1));
// Garis kosong di akhir
list.add(LineText(linefeed: 3));
print('Data struk berhasil dibuat, mengirim ke printer...');
print('Jumlah baris: ${list.length}');
// Kirim perintah cetak ke printer
await bluetoothPrint.printReceipt(config, list);
print('Perintah cetak dikirim ke printer');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Struk berhasil dicetak')),
);
}
} catch (e, stackTrace) {
print('ERROR saat mencetak struk: $e');
print('Stack trace: $stackTrace');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal mencetak struk: $e')),
);
}
}
print('=== SELESAI MENCETAK KE PRINTER THERMAL ===');
}
void _addItem() async {
final newItem = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AddItemScreen()),
);
if (newItem != null) {
setState(() {
items.add(newItem);
});
}
}
void _editItem(int index) async {
final editedItem = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddItemScreen.fromItem(items[index]),
),
);
if (editedItem != null) {
setState(() {
items[index] = editedItem;
});
}
}
void _removeItem(int index) {
setState(() {
items.removeAt(index);
});
}
Future<void> _printReceipt() async {
// Generate PDF
final pdfPath = await PdfExportService.generateReceiptPdf(
items,
storeName,
storeAddress,
storePhone,
);
if (pdfPath != null) {
// Menampilkan pesan sukses
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PDF berhasil dibuat')),
);
}
// Membuka file PDF
await PdfExportService.openPdf(pdfPath);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Gagal membuat file PDF')),
);
}
}
}
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) {
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) {
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;
try {
final account = _accounts.firstWhere(
(account) => account['name'].toString().toLowerCase() == name.toLowerCase(),
);
return account['id'] as String?;
} catch (e) {
// Jika tidak ditemukan, kembalikan null
return null;
}
}
Future<void> _sendToFirefly() async {
if (items.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tidak ada item untuk dikirim')),
);
}
return;
}
// Cek apakah user memasukkan akun secara manual
if (_sourceAccountId == null && _sourceAccountName != null && _sourceAccountName!.isNotEmpty) {
_sourceAccountId = _findAccountIdByName(_sourceAccountName!);
// Jika tidak ditemukan, coba cari dengan pendekatan yang lebih fleksibel
if (_sourceAccountId == null) {
for (var account in _accounts) {
if (account['name'].toString().toLowerCase().contains(_sourceAccountName!.toLowerCase())) {
_sourceAccountId = account['id'] as String?;
_sourceAccountName = account['name'] as String?;
_sourceAccountController.text = _sourceAccountName!;
break;
}
}
}
}
if (_destinationAccountId == null && _destinationAccountName != null && _destinationAccountName!.isNotEmpty) {
_destinationAccountId = _findAccountIdByName(_destinationAccountName!);
// Jika tidak ditemukan, coba cari dengan pendekatan yang lebih fleksibel
if (_destinationAccountId == null) {
for (var account in _accounts) {
if (account['name'].toString().toLowerCase().contains(_destinationAccountName!.toLowerCase())) {
_destinationAccountId = account['id'] as String?;
_destinationAccountName = account['name'] as String?;
_destinationAccountController.text = _destinationAccountName!;
break;
}
}
}
}
// Validasi input
if (_sourceAccountId == null || _destinationAccountId == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Silakan pilih atau masukkan akun sumber dan tujuan yang valid. '
'Anda bisa memilih dari daftar atau mengetik nama akun yang sesuai.'
),
duration: const Duration(seconds: 5),
),
);
}
return;
}
if (_sourceAccountId == _destinationAccountId) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Akun sumber dan tujuan tidak boleh sama')),
);
}
return;
}
// Validasi apakah akun benar-benar ada di Firefly III
bool sourceAccountExists = false;
bool destinationAccountExists = false;
for (var account in _accounts) {
if (account['id'].toString() == _sourceAccountId) {
sourceAccountExists = true;
}
if (account['id'].toString() == _destinationAccountId) {
destinationAccountExists = true;
}
}
if (!sourceAccountExists) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Akun sumber tidak ditemukan di Firefly III. Silakan pilih akun yang valid.')),
);
}
return;
}
if (!destinationAccountExists) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Akun tujuan tidak ditemukan di Firefly III. Silakan pilih akun yang valid.')),
);
}
return;
}
final total = _calculateTotal();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Mengirim transaksi ke Firefly III...')),
);
}
print('=== MULAI MENGIRIM TRANSAKSI ===');
print('Jumlah item: ${items.length}');
print('Total: $total');
print('Source Account ID: $_sourceAccountId');
print('Destination Account ID: $_destinationAccountId');
print('Transaction Date: $_transactionDate');
for (var i = 0; i < items.length; i++) {
var item = items[i];
print(' Item ${i+1}: ${item.description} - ${item.quantity} x ${item.price} = ${item.total}');
}
bool success = false;
if (_fireflyUrl != null && _accessToken != null) {
success = await FireflyApiService.submitDummyTransaction(
baseUrl: _fireflyUrl!,
accessToken: _accessToken!,
sourceId: _sourceAccountId!,
destinationId: _destinationAccountId!,
type: 'deposit',
description: 'Transaksi Struk Belanja',
date: '${_transactionDate.year}-${_transactionDate.month.toString().padLeft(2, '0')}-${_transactionDate.day.toString().padLeft(2, '0')}',
amount: total.toStringAsFixed(2),
);
}
if (mounted) {
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Transaksi berhasil dikirim ke Firefly III')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Gagal mengirim transaksi ke Firefly III. Periksa log untuk detail kesalahan.')),
);
}
}
print('=== SELESAI MENGIRIM TRANSAKSI ===');
}
void _openSettings() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsScreen()),
);
}
void _openPrinterSetup() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PrinterSetupScreen()),
);
}
@override
Widget build(BuildContext context) {
// Gunakan font Courier Prime untuk menyerupai font struk fisik
final courierPrime = GoogleFonts.courierPrime(
textStyle: const TextStyle(
fontSize: 14,
height: 1.2,
),
);
return Scaffold(
appBar: AppBar(
title: const Text('Struk Belanja'),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: _openSettings,
),
IconButton(
icon: const Icon(Icons.print),
onPressed: _printReceipt,
),
IconButton(
icon: Icon(_bluetoothConnected ? Icons.bluetooth_connected : Icons.bluetooth),
onPressed: _scanBluetoothDevices,
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _checkBluetoothStatus,
),
IconButton(
icon: const Icon(Icons.cloud_upload),
onPressed: _sendToFirefly,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
children: [
// Receipt paper style container - desain ulang untuk menyerupai struk fisik
Container(
width: 320, // Lebar tetap untuk menyerupai struk fisik
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
color: Colors.white,
),
child: Padding(
padding: const EdgeInsets.all(8.0), // Kurangi padding
child: Column(
children: [
// Header struk
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),
const SizedBox(height: 4),
// Info transaksi tambahan
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),
const SizedBox(height: 4),
// Item list header
const 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,
),
),
],
),
const SizedBox(height: 2),
const Divider(thickness: 1, height: 1, color: Colors.black),
const SizedBox(height: 4),
// Item list
...items.map((item) {
return Padding(
padding: const EdgeInsets.only(bottom: 2.0), // Kurangi jarak antar item
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,
),
),
],
),
);
}).toList(),
const SizedBox(height: 4),
const Divider(thickness: 1, height: 1, color: Colors.black),
const SizedBox(height: 4),
// Total
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'TOTAL:',
style: courierPrime.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
_calculateTotal().toStringAsFixed(0),
style: courierPrime.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
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,
),
],
),
),
),
const SizedBox(height: 16),
// Account selection section - tetap di luar area struk
Container(
width: 320, // Sesuaikan lebar dengan area struk
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
color: Colors.grey.shade50,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pengaturan Transaksi',
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
// Source account
const Text(
'Akun Sumber:',
style: TextStyle(fontWeight: FontWeight.bold),
),
_sourceAccountId != null
? ListTile(
title: Text(_sourceAccountName ?? ''),
contentPadding: EdgeInsets.zero,
dense: true,
trailing: const Icon(Icons.edit, size: 18),
onTap: _selectSourceAccount,
)
: TextField(
controller: _sourceAccountController,
decoration: const InputDecoration(
hintText: 'Ketik nama akun atau pilih dari daftar',
contentPadding: EdgeInsets.symmetric(horizontal: 8),
isDense: true,
border: OutlineInputBorder(),
),
onChanged: _onSourceAccountChanged,
),
if (_sourceAccountId == null)
ElevatedButton(
onPressed: _selectSourceAccount,
child: const Text('Pilih Akun Sumber'),
),
const SizedBox(height: 8),
// Destination account
const Text(
'Akun Tujuan:',
style: TextStyle(fontWeight: FontWeight.bold),
),
_destinationAccountId != null
? ListTile(
title: Text(_destinationAccountName ?? ''),
contentPadding: EdgeInsets.zero,
dense: true,
trailing: const Icon(Icons.edit, size: 18),
onTap: _selectDestinationAccount,
)
: TextField(
controller: _destinationAccountController,
decoration: const InputDecoration(
hintText: 'Ketik nama akun atau pilih dari daftar',
contentPadding: EdgeInsets.symmetric(horizontal: 8),
isDense: true,
border: OutlineInputBorder(),
),
onChanged: _onDestinationAccountChanged,
),
if (_destinationAccountId == null)
ElevatedButton(
onPressed: _selectDestinationAccount,
child: const Text('Pilih Akun Tujuan'),
),
],
),
),
),
const SizedBox(height: 16),
// Action buttons
SizedBox(
width: 320, // Sesuaikan lebar dengan area struk
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _addItem,
child: const Text('Tambah Item'),
),
ElevatedButton(
onPressed: items.isNotEmpty ? _sendToFirefly : null,
child: const Text('Kirim ke Firefly'),
),
],
),
),
const SizedBox(height: 16),
// Additional action buttons
SizedBox(
width: 320, // Sesuaikan lebar dengan area struk
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
onPressed: _openPrinterSetup,
child: const Text('Setup Printer', textAlign: TextAlign.center),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
onPressed: _printReceipt,
child: const Text('Cetak PDF', textAlign: TextAlign.center),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
onPressed: _bluetoothConnected ? _printToThermalPrinter : null,
child: const Text('Cetak Struk', textAlign: TextAlign.center),
),
),
),
],
),
),
if (!_bluetoothConnected)
const Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text(
'⚠️ Printer tidak terhubung. Hubungkan printer melalui menu Setup Printer atau ikon Bluetooth di atas.',
style: TextStyle(
color: Colors.orange,
fontSize: 12,
),
textAlign: TextAlign.center,
),
),
if (_sourceAccountId == null || _destinationAccountId == null)
const Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text(
'⚠️ Pilih akun sumber dan tujuan untuk mengaktifkan tombol kirim',
style: TextStyle(
color: Colors.orange,
fontSize: 12,
),
textAlign: TextAlign.center,
),
),
],
),
),
),
);
}
double _calculateTotal() {
return items.fold(0.0, (sum, item) => sum + item.total);
}
}