2025-08-20 15:15:20 +07:00
import ' package:flutter/material.dart ' ;
import ' package:shared_preferences/shared_preferences.dart ' ;
2025-08-23 16:29:32 +07:00
// import 'dart:io'; // Add this import for file operations // PDF Removed
2025-08-23 22:44:16 +07:00
import ' dart:typed_data ' ; // Untuk Uint8List
2025-08-23 16:29:32 +07:00
import ' package:intl/intl.dart ' ; // Untuk format angka
2025-08-20 15:15:20 +07:00
import ' package:cashumit/models/receipt_item.dart ' ;
import ' package:cashumit/screens/add_item_screen.dart ' ;
2025-08-23 16:29:32 +07:00
// import 'package:cashumit/services/pdf_export_service.dart'; // PDF Removed
2025-08-20 15:15:20 +07:00
import ' package:cashumit/services/firefly_api_service.dart ' ;
2025-08-23 16:29:32 +07:00
import ' package:cashumit/services/struk_text_generator.dart ' ; // Tambahkan import ini
2025-08-23 22:44:16 +07:00
import ' package:cashumit/services/esc_pos_print_service.dart ' ; // Tambahkan import ini
2025-08-20 15:15:20 +07:00
import ' package:bluetooth_print/bluetooth_print.dart ' ;
import ' package:bluetooth_print/bluetooth_print_model.dart ' ;
2025-08-23 16:29:32 +07:00
import ' package:flutter_speed_dial/flutter_speed_dial.dart ' ; // Tambahkan import ini
// Import widget komponen struk baru
import ' package:cashumit/widgets/receipt_tear_effect.dart ' ;
import ' package:cashumit/widgets/store_info_widget.dart ' ;
import ' package:cashumit/widgets/dotted_line.dart ' ;
import ' package:cashumit/widgets/receipt_item_widget.dart ' ;
import ' package:cashumit/widgets/add_item_button.dart ' ;
import ' package:cashumit/widgets/store_disclaimer.dart ' ;
import ' package:cashumit/widgets/thank_you_pantun.dart ' ;
import ' package:cashumit/widgets/store_info_config_dialog.dart ' ;
import ' package:cashumit/widgets/custom_text_config_dialog.dart ' ;
import ' package:cashumit/widgets/account_settings_widget.dart ' ;
import ' package:cashumit/widgets/horizontal_divider.dart ' ; // Tambahkan import ini
// import 'package:cashumit/widgets/struk_printer.dart'; // DIHAPUS
import ' package:cashumit/screens/webview_screen.dart ' ;
// import 'package:google_fonts/google_fonts.dart'; // DIHAPUS karena tidak digunakan di file ini
2025-08-20 15:15:20 +07:00
class ReceiptScreen extends StatefulWidget {
2025-08-23 16:29:32 +07:00
const ReceiptScreen ( { super . key } ) ;
2025-08-20 15:15:20 +07:00
2025-08-23 16:29:32 +07:00
@ override
State < ReceiptScreen > createState ( ) = > _ReceiptScreenState ( ) ;
2025-08-20 15:15:20 +07:00
}
class _ReceiptScreenState extends State < ReceiptScreen > {
2025-08-23 16:29:32 +07:00
List < ReceiptItem > items = [ ] ;
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
late BluetoothPrint bluetoothPrint ;
bool _bluetoothConnected = false ;
BluetoothDevice ? _bluetoothDevice ;
2025-08-23 22:44:16 +07:00
bool _isPrinting = false ; // State variable to track printing status
2025-08-23 16:29:32 +07:00
@ override
void initState ( ) {
super . initState ( ) ;
print ( ' Inisialisasi ReceiptScreen... ' ) ;
bluetoothPrint = BluetoothPrint . instance ;
_transactionDate = DateTime . now ( ) ;
_loadCredentialsAndAccounts ( ) ;
_initBluetooth ( ) ;
_loadSavedBluetoothDevice ( ) ;
print ( ' Selesai inisialisasi ReceiptScreen ' ) ;
}
@ override
void didChangeDependencies ( ) {
super . didChangeDependencies ( ) ;
// Memuat ulang kredensial dan akun setiap kali ada perubahan dependencies
// Ini memastikan akun dimuat ulang ketika pengguna kembali dari ConfigScreen
_loadCredentialsAndAccounts ( ) ;
}
@ override
void dispose ( ) {
_sourceAccountController . dispose ( ) ;
_destinationAccountController . dispose ( ) ;
super . dispose ( ) ;
}
String ? _fireflyUrl ;
String ? _accessToken ;
/// Memuat kredensial dari shared preferences dan kemudian memuat akun.
///
/// Metode ini dipanggil saat:
/// 1. Widget pertama kali dibuat (initState)
/// 2. Pengguna kembali dari ConfigScreen (melalui _openSettings)
/// 3. Pengguna mengetuk tombol "Muat Ulang Akun"
///
/// Jika kredensial berubah atau daftar akun kosong, metode ini akan memanggil
/// _loadAccounts untuk memperbarui daftar akun dari API Firefly III.
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. Silakan atur di menu Pengaturan. ' ) ) ,
) ;
}
return ;
}
// Periksa apakah kredensial berubah
final credentialsChanged = _fireflyUrl ! = url | | _accessToken ! = token ;
setState ( ( ) {
_fireflyUrl = url ;
_accessToken = token ;
} ) ;
// Jika kredensial ada dan berubah, lanjutkan untuk memuat akun
if ( credentialsChanged ) {
_loadAccounts ( ) ;
} else if ( _accounts . isEmpty ) {
// Jika akun belum pernah dimuat, muat sekarang
_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 ;
} ) ;
}
}
/// Memeriksa status koneksi Bluetooth printer secara real-time
Future < bool > _checkBluetoothConnection ( ) async {
try {
final isConnected = await bluetoothPrint . isConnected ? ? false ;
print ( ' Status koneksi Bluetooth printer (real-time): $ isConnected ' ) ;
// Update state jika perlu
if ( isConnected ! = _bluetoothConnected ) {
setState ( ( ) {
_bluetoothConnected = isConnected ;
} ) ;
print ( ' Status koneksi Bluetooth diperbarui di state: $ isConnected ' ) ;
}
return isConnected ;
} catch ( e ) {
print ( ' Error saat memeriksa koneksi Bluetooth: $ e ' ) ;
return false ;
}
}
/// Memuat daftar akun sumber (revenue) dan tujuan (asset) dari API.
///
/// Metode ini mengambil akun revenue dan asset secara terpisah dari API Firefly III,
/// kemudian menggabungkannya ke dalam satu daftar `_accounts` yang digunakan
/// untuk mengisi dropdown akun sumber dan tujuan.
///
/// Jika berhasil memuat akun, metode ini akan menampilkan pesan jumlah akun yang dimuat.
/// Jika tidak ada akun yang dimuat, metode ini akan menampilkan pesan peringatan.
/// Jika terjadi kesalahan saat memuat akun, metode ini akan menampilkan pesan kesalahan.
Future < void > _loadAccounts ( ) async {
if ( _fireflyUrl = = null | | _accessToken = = null ) {
return ;
}
try {
// Tampilkan pesan loading
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( ' Memuat daftar akun dari Firefly III... ' ) ) ,
) ;
}
// 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 ;
} ) ;
if ( mounted ) {
if ( allAccounts . isEmpty ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
' Tidak ada akun yang ditemukan. Pastikan kredensial dan koneksi internet Anda benar. ' ) ) ,
) ;
} else {
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar (
content: Text (
' Berhasil memuat ${ allAccounts . length } akun dari Firefly III ' ) ) ,
) ;
}
}
} 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 ;
}
}
} ) ;
}
/// Cetak struk ke printer thermal
Future < void > _printToThermalPrinter ( ) async {
print ( ' === FUNGSI _printToThermalPrinter DIPANGGIL === ' ) ;
print ( ' Memulai proses pencetakan struk... ' ) ;
print ( ' Jumlah item: ${ items . length } ' ) ;
print ( ' Tanggal transaksi: $ _transactionDate ' ) ;
print ( ' ID kasir: $ cashierId ' ) ;
print ( ' ID transaksi: $ transactionId ' ) ;
// Periksa koneksi printer secara real-time
bool isConnected = await _checkBluetoothConnection ( ) ;
print ( ' Status koneksi printer awal (real-time): $ isConnected ' ) ;
if ( ! isConnected ) {
print ( ' Printer tidak terhubung ' ) ;
if ( ! mounted ) return ;
// Coba sambungkan kembali jika ada device yang tersimpan
if ( _bluetoothDevice ! = null ) {
print ( ' Mencoba menyambungkan kembali ke printer: ${ _bluetoothDevice ! . name } ' ) ;
print ( ' Alamat printer: ${ _bluetoothDevice ! . address } ' ) ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( ' Mencoba menyambungkan kembali ke printer... ' ) ) ,
) ;
try {
// Putuskan koneksi yang mungkin tersisa
try {
await bluetoothPrint . disconnect ( ) ;
print ( ' Berhasil memutuskan koneksi yang tersisa ' ) ;
} catch ( disconnectError ) {
print ( ' Tidak ada koneksi yang perlu diputuskan: $ disconnectError ' ) ;
}
// Tunggu sebentar sebelum menyambungkan kembali
await Future . delayed ( const Duration ( milliseconds: 500 ) ) ;
// Sambungkan kembali
await bluetoothPrint . connect ( _bluetoothDevice ! ) ;
print ( ' Berhasil menyambungkan kembali ke printer ' ) ;
// Tunggu sebentar untuk memastikan koneksi stabil
await Future . delayed ( const Duration ( milliseconds: 500 ) ) ;
// Periksa koneksi lagi
isConnected = await _checkBluetoothConnection ( ) ;
print ( ' Status koneksi printer setelah reconnect (real-time): $ isConnected ' ) ;
if ( isConnected ) {
// Perbarui status koneksi
setState ( ( ) {
_bluetoothConnected = true ;
} ) ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar ( content: Text ( ' Printer berhasil terhubung kembali ' ) ) ,
) ;
} else {
throw Exception ( ' Koneksi tidak stabil setelah reconnect ' ) ;
}
} catch ( e ) {
print ( ' Gagal menyambungkan kembali ke printer: $ e ' ) ;
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar (
content: Text ( ' Gagal menyambungkan kembali ke printer: $ e ' ) ) ,
) ;
return ;
}
} else {
print ( ' Tidak ada device printer yang tersimpan ' ) ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( ' Harap hubungkan printer terlebih dahulu ' ) ) ,
) ;
return ;
}
} else {
print ( ' Printer sudah terhubung ' ) ;
}
try {
2025-08-23 22:44:16 +07:00
print ( ' Menghasilkan byte array ESC/POS menggunakan flutter_esc_pos_utils... ' ) ;
2025-08-23 16:29:32 +07:00
print ( ' Jumlah item: ${ items . length } ' ) ;
print ( ' Tanggal transaksi: $ _transactionDate ' ) ;
print ( ' ID kasir: $ cashierId ' ) ;
print ( ' ID transaksi: $ transactionId ' ) ;
2025-08-23 22:44:16 +07:00
// Generate struk dalam format byte array menggunakan EscPosPrintService
final bytes = await EscPosPrintService . generateEscPosBytes (
2025-08-23 16:29:32 +07:00
items: items ,
transactionDate: _transactionDate ,
cashierId: cashierId ,
transactionId: transactionId ,
2025-08-23 22:44:16 +07:00
) ;
2025-08-23 16:29:32 +07:00
2025-08-23 22:44:16 +07:00
print ( ' Byte array ESC/POS berhasil dihasilkan ' ) ;
print ( ' Jumlah byte: ${ bytes . length } ' ) ;
// Tampilkan byte array untuk debugging (dalam format hex)
print ( ' Isi byte array (hex): ' ) ;
if ( bytes . length < = 1000 ) { // Batasi tampilan untuk mencegah output terlalu panjang
print ( bytes . map ( ( b ) = > b . toRadixString ( 16 ) . padLeft ( 2 , ' 0 ' ) ) . join ( ' ' ) ) ;
} else {
print ( ' Terlalu banyak byte untuk ditampilkan ( ${ bytes . length } bytes) ' ) ;
2025-08-23 16:29:32 +07:00
}
// Verifikasi koneksi sebelum mencetak
final isConnectedBeforePrint = await bluetoothPrint . isConnected ? ? false ;
if ( ! isConnectedBeforePrint ) {
print ( ' Printer tidak terhubung saat akan mencetak ' ) ;
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar ( content: Text ( ' Printer tidak terhubung saat akan mencetak ' ) ) ,
) ;
return ;
}
2025-08-23 22:44:16 +07:00
print ( ' Mengirim byte array ke printer... ' ) ;
2025-08-23 16:29:32 +07:00
bool printSuccess = false ;
String printError = ' ' ;
try {
2025-08-23 22:44:16 +07:00
// Coba kirim byte array beberapa kali jika gagal
2025-08-23 16:29:32 +07:00
for ( int attempt = 1 ; attempt < = 3 ; attempt + + ) {
print ( ' Percobaan cetak ke- $ attempt ' ) ;
try {
2025-08-23 22:44:16 +07:00
// Konversi List<int> ke Uint8List
final Uint8List data = Uint8List . fromList ( bytes ) ;
await bluetoothPrint . printRawData ( data ) ;
2025-08-23 16:29:32 +07:00
print ( ' Perintah cetak berhasil dikirim pada percobaan ke- $ attempt ' ) ;
printSuccess = true ;
break ;
} catch ( e ) {
print ( ' Error saat mengirim perintah cetak pada percobaan ke- $ attempt : $ e ' ) ;
printError = e . toString ( ) ;
if ( attempt < 3 ) {
// Tunggu sebentar sebelum mencoba lagi
await Future . delayed ( const Duration ( milliseconds: 500 ) ) ;
}
}
}
if ( printSuccess ) {
print ( ' Perintah cetak berhasil dikirim ' ) ;
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar ( content: Text ( ' Perintah cetak dikirim ke printer ' ) ) ,
) ;
} else {
print ( ' Gagal mengirim perintah cetak setelah 3 percobaan: $ printError ' ) ;
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar ( content: Text ( ' Gagal mengirim perintah cetak: $ printError ' ) ) ,
) ;
return ;
}
} catch ( printError ) {
print ( ' Error saat mengirim perintah cetak ke printer: $ printError ' ) ;
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar ( content: Text ( ' Gagal mengirim perintah cetak: $ printError ' ) ) ,
) ;
return ;
}
} catch ( e , stackTrace ) {
print ( ' Error saat mencetak struk: $ e ' ) ;
print ( ' Stack trace: $ stackTrace ' ) ;
// Cetak detail error tambahan
print ( ' Tipe error: ${ e . runtimeType } ' ) ;
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
2025-08-23 22:44:16 +07:00
SnackBar ( content: Text ( ' Gagal mencetak struk: $ e ' ) ) ,
) ;
return ;
2025-08-23 16:29:32 +07:00
}
}
void _addItem ( ) async {
final newItem = await showDialog < ReceiptItem > (
context: context ,
builder: ( context ) = > const AddItemScreen ( ) , // Tampilkan sebagai dialog
) ;
if ( newItem ! = null ) {
setState ( ( ) {
items . add ( newItem ) ;
} ) ;
}
}
void _editItem ( int index ) async {
// Pastikan index valid sebelum mengakses item
if ( index < 0 | | index > = items . length ) {
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( " Gagal mengedit item: Index tidak valid. " ) ) ,
) ;
}
return ;
}
final originalItem =
items [ index ] ; // Simpan item asli untuk fallback jika diperlukan
final editedItem = await showDialog < ReceiptItem > (
context: context ,
builder: ( context ) = >
AddItemScreen . fromItem ( originalItem ) , // Tampilkan sebagai dialog
) ;
// Hanya update jika item tidak null (bukan hasil dari 'Batal')
if ( editedItem ! = null ) {
setState ( ( ) {
items [ index ] = editedItem ;
} ) ;
}
// Jika editedItem null (dibatalkan), tidak ada perubahan pada items[index]
}
/// Menampilkan dialog konfirmasi sebelum menghapus item
///
/// Mengembalikan Future<bool?> yang menunjukkan apakah pengguna mengkonfirmasi penghapusan.
/// - true: Pengguna mengkonfirmasi.
/// - false: Pengguna membatalkan.
/// - null: Dialog ditutup tanpa memilih opsi.
Future < bool ? > _confirmRemoveItem ( int index ) async {
// Validasi index untuk mencegah error
if ( index < 0 | | index > = items . length ) {
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( " Gagal menghapus item: Index tidak valid. " ) ) ,
) ;
}
return false ; // Atau throw exception tergantung kebijakan error handling
}
// Simpan referensi item sebelum dialog muncul
final itemToRemove = items [ index ] ;
final bool ? shouldRemove = await showDialog < bool > (
context: context ,
builder: ( context ) = > AlertDialog (
title: const Text ( ' Konfirmasi Hapus ' ) ,
content: Text (
' Apakah Anda yakin ingin menghapus item ${ itemToRemove . description } ' ) ,
actions: [
TextButton (
onPressed: ( ) = > Navigator . of ( context ) . pop ( false ) , // Batal
child: const Text ( ' Batal ' ) ,
) ,
TextButton (
onPressed: ( ) = > Navigator . of ( context ) . pop ( true ) , // Hapus
child: const Text ( ' Hapus ' ) ,
) ,
] ,
) ,
) ;
// Tampilkan snackbar informasi berdasarkan hasil konfirmasi
if ( shouldRemove = = true ) {
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar (
content: Text (
" Item ' ${ itemToRemove . description } ' akan dihapus setelah gestur selesai. " ) ) ,
) ;
}
} else if ( shouldRemove = = false ) {
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar (
content: Text (
" Penghapusan item ' ${ itemToRemove . description } ' dibatalkan. " ) ) ,
) ;
}
}
// Jika shouldRemove == null, pengguna menutup dialog, tidak perlu snackbar tambahan
return shouldRemove ;
}
/// Menampilkan dialog untuk memilih akun sumber (revenue).
///
/// Metode ini memfilter daftar akun untuk hanya menampilkan akun dengan tipe 'revenue',
/// kemudian menampilkan dialog dengan daftar akun tersebut. Ketika pengguna memilih
/// akun, metode ini akan memperbarui state `_sourceAccountId` dan `_sourceAccountName`.
Future < void > _selectSourceAccount ( ) async {
if ( _accounts . isEmpty ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
' Daftar akun kosong. Pastikan kredensial sudah diatur dan akun telah dimuat. Klik "Muat Ulang Akun" untuk mencoba lagi. ' ) ) ,
) ;
return ;
}
// Filter akun sumber (revenue)
final revenueAccounts =
_accounts . where ( ( account ) = > account [ ' type ' ] = = ' revenue ' ) . toList ( ) ;
if ( revenueAccounts . isEmpty ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
' Tidak ada akun sumber (revenue) yang ditemukan. Klik "Muat Ulang Akun" untuk mencoba lagi atau periksa akun Anda di Firefly III. ' ) ) ,
) ;
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: revenueAccounts . length ,
itemBuilder: ( context , index ) {
final account = revenueAccounts [ 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 ' ] ;
} ) ;
}
}
/// Menampilkan dialog untuk memilih akun tujuan (asset).
///
/// Metode ini memfilter daftar akun untuk hanya menampilkan akun dengan tipe 'asset',
/// kemudian menampilkan dialog dengan daftar akun tersebut. Ketika pengguna memilih
/// akun, metode ini akan memperbarui state `_destinationAccountId` dan `_destinationAccountName`.
Future < void > _selectDestinationAccount ( ) async {
if ( _accounts . isEmpty ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
' Daftar akun kosong. Pastikan kredensial sudah diatur dan akun telah dimuat. Klik "Muat Ulang Akun" untuk mencoba lagi. ' ) ) ,
) ;
return ;
}
// Filter akun tujuan (asset)
final assetAccounts =
_accounts . where ( ( account ) = > account [ ' type ' ] = = ' asset ' ) . toList ( ) ;
if ( assetAccounts . isEmpty ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
' Tidak ada akun tujuan (asset) yang ditemukan. Klik "Muat Ulang Akun" untuk mencoba lagi atau periksa akun Anda di Firefly III. ' ) ) ,
) ;
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: assetAccounts . length ,
itemBuilder: ( context , index ) {
final account = assetAccounts [ 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 ' ] ;
} ) ;
}
}
/// Mencari ID akun berdasarkan nama dan tipe akun.
///
/// Metode ini digunakan ketika pengguna memasukkan nama akun secara manual
/// (bukan memilih dari dropdown). Metode ini akan mencari akun dengan nama
/// yang cocok dan tipe yang diharapkan (revenue untuk sumber, asset untuk tujuan).
///
/// Jika tidak ditemukan akun yang cocok, metode ini akan mencoba pencarian
/// yang lebih fleksibel dengan mencari apakah nama akun mengandung teks yang dimasukkan.
///
/// Parameter:
/// - [name]: Nama akun yang dicari
/// - [expectedType]: Tipe akun yang diharapkan (revenue atau asset)
///
/// Mengembalikan ID akun jika ditemukan, atau null jika tidak ditemukan.
String ? _findAccountIdByName ( String name , String expectedType ) {
if ( name . isEmpty ) return null ;
try {
// Cari akun dengan nama yang cocok dan tipe yang diharapkan
final account = _accounts . firstWhere (
( account ) = >
account [ ' name ' ] . toString ( ) . toLowerCase ( ) = = name . toLowerCase ( ) & &
account [ ' type ' ] = = expectedType ,
) ;
return account [ ' id ' ] as String ? ;
} catch ( e ) {
// Jika tidak ditemukan, coba pencarian yang lebih fleksibel
for ( var account in _accounts ) {
if ( account [ ' type ' ] = = expectedType & &
account [ ' name ' ]
. toString ( )
. toLowerCase ( )
. contains ( name . toLowerCase ( ) ) ) {
return account [ ' id ' ] as String ? ;
}
}
// Jika masih tidak ditemukan, kembalikan null
return null ;
}
}
/// Generates a transaction description based on item names
String _generateTransactionDescription ( ) {
if ( items . isEmpty ) {
return ' Transaksi Struk Belanja ' ;
}
// Take the first 5 item descriptions
final itemNames = items . take ( 5 ) . map ( ( item ) = > item . description ) . toList ( ) ;
// If there are more than 5 items, append ', dll' to the last item
if ( items . length > 5 ) {
itemNames [ 4 ] + = ' , dll ' ;
}
// Join the item names with ', '
return itemNames . join ( ' , ' ) ;
}
/// Mengirim transaksi ke Firefly III.
///
/// Sebelum mengirim transaksi, metode ini melakukan beberapa validasi:
/// 1. Memastikan ada item yang akan dikirim
/// 2. Memastikan akun sumber dan tujuan telah dipilih atau dimasukkan
/// 3. Memastikan akun sumber dan tujuan berbeda
/// 4. Memastikan akun sumber dan tujuan ada di daftar akun yang dimuat
/// 5. Memastikan tipe akun sesuai (sumber: revenue, tujuan: asset)
///
/// Jika semua validasi lolos, metode ini akan mengirim transaksi ke API Firefly III
/// dan menampilkan pesan hasilnya.
Future < void > _sendToFirefly ( ) async {
if ( items . isEmpty ) {
if ( ! mounted ) return ;
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 ! , ' revenue ' ) ;
// Jika tidak ditemukan, coba cari dengan pendekatan yang lebih fleksibel
if ( _sourceAccountId = = null ) {
for ( var account in _accounts ) {
if ( account [ ' type ' ] = = ' revenue ' & &
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 ! , ' asset ' ) ;
// Jika tidak ditemukan, coba cari dengan pendekatan yang lebih fleksibel
if ( _destinationAccountId = = null ) {
for ( var account in _accounts ) {
if ( account [ ' type ' ] = = ' asset ' & &
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 ) return ;
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 ) return ;
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 ;
// Cari detail akun untuk validasi tipe akun
Map < String , dynamic > ? sourceAccountDetails ;
Map < String , dynamic > ? destinationAccountDetails ;
for ( var account in _accounts ) {
if ( account [ ' id ' ] . toString ( ) = = _sourceAccountId ) {
sourceAccountExists = true ;
sourceAccountDetails = account ;
}
if ( account [ ' id ' ] . toString ( ) = = _destinationAccountId ) {
destinationAccountExists = true ;
destinationAccountDetails = account ;
}
}
if ( ! sourceAccountExists ) {
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
' Akun sumber tidak ditemukan di daftar akun yang dimuat. Klik "Muat Ulang Akun" dan coba lagi. ' ) ) ,
) ;
return ;
}
if ( ! destinationAccountExists ) {
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
' Akun tujuan tidak ditemukan di daftar akun yang dimuat. Klik "Muat Ulang Akun" dan coba lagi. ' ) ) ,
) ;
return ;
}
// Validasi tipe akun (sumber harus revenue, tujuan harus asset)
if ( sourceAccountDetails ! = null & &
sourceAccountDetails [ ' type ' ] ! = ' revenue ' ) {
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar (
content: Text (
' Peringatan: Akun sumber sebaiknya bertipe revenue, tetapi akun ini bertipe ${ sourceAccountDetails [ ' type ' ] } . Transaksi mungkin tidak akan berhasil. ' ) ) ,
) ;
}
if ( destinationAccountDetails ! = null & &
destinationAccountDetails [ ' type ' ] ! = ' asset ' ) {
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar (
content: Text (
' Peringatan: Akun tujuan sebaiknya bertipe asset, tetapi akun ini bertipe ${ destinationAccountDetails [ ' type ' ] } . Transaksi mungkin tidak akan berhasil. ' ) ) ,
) ;
}
final total = _calculateTotal ( ) ;
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar ( content: Text ( ' Mengirim transaksi ke Firefly III... ' ) ) ,
) ;
// Generate transaction description
final transactionDescription = _generateTransactionDescription ( ) ;
String ? transactionId ; // To store the transaction ID for attachment
if ( _fireflyUrl ! = null & & _accessToken ! = null ) {
transactionId = await FireflyApiService . submitDummyTransaction (
baseUrl: _fireflyUrl ! ,
accessToken: _accessToken ! ,
sourceId: _sourceAccountId ! ,
destinationId: _destinationAccountId ! ,
type: ' deposit ' ,
description: transactionDescription , // Use the generated description
date:
' ${ _transactionDate . year } - ${ _transactionDate . month . toString ( ) . padLeft ( 2 , ' 0 ' ) } - ${ _transactionDate . day . toString ( ) . padLeft ( 2 , ' 0 ' ) } ' ,
amount: total . toStringAsFixed ( 2 ) ,
) ;
}
if ( ! mounted ) return ;
if ( transactionId ! = null & & transactionId ! = " success " ) {
// PDF generation and upload removed as per request
// Navigasi ke WebViewScreen untuk menampilkan transaksi
if ( _fireflyUrl ! = null ) {
final transactionUrl = ' $ _fireflyUrl /transactions/show/ $ transactionId ' ;
if ( mounted ) {
Navigator . push (
context ,
MaterialPageRoute (
builder: ( context ) = > WebViewScreen (
url: transactionUrl ,
title: ' Transaksi Firefly III ' ,
) ,
) ,
) ;
}
}
} else if ( transactionId = = " success " ) {
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content:
Text ( ' Transaksi berhasil dikirim ke Firefly III (tanpa ID) ' ) ) ,
) ;
} else {
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
' Gagal mengirim transaksi ke Firefly III. Periksa log untuk detail kesalahan. ' ) ) ,
) ;
}
}
/// Membuka dialog konfigurasi informasi toko
Future < void > _openStoreInfoConfig ( ) async {
final result = await showDialog < bool > (
context: context ,
builder: ( context ) = > const StoreInfoConfigDialog ( ) ,
) ;
// Jika informasi toko berhasil disimpan, refresh widget
if ( result = = true ) {
setState ( ( ) {
// setState akan memicu rebuild, sehingga StoreInfoWidget akan memuat ulang data
} ) ;
}
}
/// Membuka dialog konfigurasi teks kustom (disclaimer, thank you, pantun)
Future < void > _openCustomTextConfig ( ) async {
final result = await showDialog < bool > (
context: context ,
builder: ( context ) = > const CustomTextConfigDialog ( ) ,
) ;
// Jika teks berhasil disimpan, refresh widget
if ( result = = true ) {
setState ( ( ) {
// setState akan memicu rebuild, sehingga StoreDisclaimer dan ThankYouPantun akan memuat ulang data
} ) ;
}
}
/// Membuka layar konfigurasi.
///
/// Jika pengguna menyimpan konfigurasi di ConfigScreen, metode ini akan
/// menerima hasil `true` dan memanggil `_loadCredentialsAndAccounts` untuk
/// memuat ulang kredensial dan akun dengan pengaturan baru.
void _openSettings ( ) async {
final result = await Navigator . pushNamed ( context , ' /config ' ) ;
// Jika pengguna kembali dari ConfigScreen dengan hasil true, muat ulang kredensial dan akun
if ( result = = true ) {
_loadCredentialsAndAccounts ( ) ;
}
}
/// Memformat angka menjadi format mata uang Rupiah
/// Memformat angka menjadi format mata uang Rupiah
String _formatRupiah ( double amount ) {
// Menggunakan NumberFormat untuk memformat angka
final formatter = NumberFormat ( " #,##0 " , " id_ID " ) ;
return " Rp ${ formatter . format ( amount ) } " ;
}
final TextStyle baseTextStyle = const TextStyle (
fontFamily: ' Courier ' , // Gunakan font courier jika tersedia
fontSize: 14 ,
height: 1.2 ,
) ;
@ override
Widget build ( BuildContext context ) {
return Scaffold (
backgroundColor:
Colors . grey [ 300 ] , // Latar belakang abu-abu untuk efek struk
floatingActionButton: SpeedDial (
icon: Icons . menu ,
activeIcon: Icons . close ,
spacing: 3 ,
spaceBetweenChildren: 4 ,
children: [
SpeedDialChild (
child: const Icon ( Icons . send ) ,
label: ' Kirim ke Firefly ' ,
onTap: items . isNotEmpty & &
_sourceAccountId ! = null & &
_destinationAccountId ! = null
? _sendToFirefly
: ( ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
' Pilih akun sumber dan tujuan terlebih dahulu ' ) ,
duration: Duration ( seconds: 2 ) ,
) ,
) ;
} ,
backgroundColor: items . isNotEmpty & &
_sourceAccountId ! = null & &
_destinationAccountId ! = null
? Colors . blue
: Colors . grey ,
) ,
SpeedDialChild (
child: const Icon ( Icons . refresh ) ,
label: ' Muat Ulang Akun ' ,
onTap: _loadAccounts ,
backgroundColor: Colors . orange ,
) ,
SpeedDialChild (
child: const Icon ( Icons . settings ) ,
label: ' Pengaturan ' ,
onTap: _openSettings ,
backgroundColor: Colors . green ,
) ,
// SpeedDialChild for PDF printing removed as per request
SpeedDialChild (
2025-08-23 22:44:16 +07:00
child: _isPrinting
? const CircularProgressIndicator ( color: Colors . white , strokeWidth: 2 )
: const Icon ( Icons . receipt ) ,
2025-08-23 16:29:32 +07:00
label: ' Cetak Struk ' ,
2025-08-23 22:44:16 +07:00
onTap: _isPrinting
? null
: ( ) async {
// Periksa koneksi secara real-time
final isConnected = await _checkBluetoothConnection ( ) ;
if ( isConnected ) {
_printToThermalPrinter ( ) ;
} else {
// Coba sambungkan kembali jika ada device yang tersimpan
if ( _bluetoothDevice ! = null ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( ' Mencoba menyambungkan ke printer... ' ) ) ,
) ;
try {
await bluetoothPrint . connect ( _bluetoothDevice ! ) ;
// Tunggu sebentar untuk memastikan koneksi stabil
await Future . delayed ( const Duration ( milliseconds: 500 ) ) ;
// Periksa koneksi lagi
final isConnectedAfterConnect = await _checkBluetoothConnection ( ) ;
if ( isConnectedAfterConnect ) {
_printToThermalPrinter ( ) ;
} else {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( ' Gagal menyambungkan ke printer ' ) ) ,
) ;
}
} catch ( e ) {
2025-08-23 16:29:32 +07:00
ScaffoldMessenger . of ( context ) . showSnackBar (
2025-08-23 22:44:16 +07:00
SnackBar (
content: Text ( ' Gagal menyambungkan ke printer: $ e ' ) ) ,
2025-08-23 16:29:32 +07:00
) ;
}
2025-08-23 22:44:16 +07:00
} else {
2025-08-23 16:29:32 +07:00
ScaffoldMessenger . of ( context ) . showSnackBar (
2025-08-23 22:44:16 +07:00
const SnackBar (
content: Text ( ' Hubungkan printer terlebih dahulu ' ) ) ,
2025-08-23 16:29:32 +07:00
) ;
}
}
2025-08-23 22:44:16 +07:00
} ,
2025-08-23 16:29:32 +07:00
backgroundColor: Colors . purple ,
) ,
] ,
) ,
body: SafeArea (
child: Center (
// Membungkus dengan widget Center untuk memastikan struk berada di tengah
child: SingleChildScrollView (
child: Column (
crossAxisAlignment: CrossAxisAlignment
. center , // Memusatkan konten secara horizontal
children: [
// Background untuk efek kertas struk tersobek di bagian atas
Container (
width: 360 ,
color: const Color ( 0xFFE0E0E0 ) , // Warna latar belakang sesuai dengan latar belakang layar
child: const Column (
children: [
const SizedBox ( height: 15 ) , // Jarak atas yang lebih besar
const ReceiptTearTop ( ) , // Efek kertas struk tersobek di bagian atas
] ,
) ,
) ,
// Konten struk
Container (
width: 360 , // Lebar tetap untuk menyerupai struk fisik
decoration: const BoxDecoration (
color: Colors . white ,
) ,
child: Column (
children: [
// Informasi toko dengan widget yang dapat diedit
StoreInfoWidget (
onTap: _openStoreInfoConfig , // Buka dialog konfigurasi saat di-tap
) ,
// Garis pembatas
const DottedLine ( ) ,
const SizedBox ( height: 8 ) ,
// Pengaturan akun sumber dan destinasi
AccountSettingsWidget (
sourceAccount: _sourceAccountName ? ? ' Pilih Sumber ' ,
destinationAccount:
_destinationAccountName ? ? ' Pilih Tujuan ' ,
onSelectSource: _selectSourceAccount ,
onSelectDestination: _selectDestinationAccount ,
) ,
const SizedBox ( height: 8 ) ,
// Garis pemisah
const HorizontalDivider ( ) ,
// Baris tabel keterangan
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Expanded (
flex: 4 ,
child: Text (
' ITEM ' ,
style: baseTextStyle . copyWith (
fontWeight: FontWeight . bold ) ,
textAlign: TextAlign . left ,
) ,
) ,
Expanded (
flex: 1 ,
child: Text (
' QTY ' ,
style: baseTextStyle . copyWith (
fontWeight: FontWeight . bold ) ,
textAlign: TextAlign . center ,
) ,
) ,
Expanded (
flex: 2 ,
child: Text (
' HARGA ' ,
style: baseTextStyle . copyWith (
fontWeight: FontWeight . bold ) ,
textAlign: TextAlign . right ,
) ,
) ,
Expanded (
flex: 2 ,
child: Text (
' TOTAL ' ,
style: baseTextStyle . copyWith (
fontWeight: FontWeight . bold ) ,
textAlign: TextAlign . right ,
) ,
) ,
] ,
) ,
// Garis pembatas
const HorizontalDivider ( ) ,
// Daftar item dengan Dismissible untuk swipe-to-delete
. . . items . asMap ( ) . entries . map ( ( entry ) {
int index = entry . key ;
ReceiptItem item = entry . value ;
return Dismissible (
key: Key (
' ${ item . hashCode } _ ${ index } ' ) , // Unique key combining item hash and index for stability
direction: DismissDirection
. horizontal , // Geser ke kiri atau kanan
background: Container (
color: Colors
. red , // Latar belakang merah saat digeser ke kanan
alignment: Alignment . centerLeft ,
padding: const EdgeInsets . only ( left: 20.0 ) ,
child:
const Icon ( Icons . delete , color: Colors . white ) ,
) ,
secondaryBackground: Container (
color: Colors
. red , // Latar belakang merah saat digeser ke kiri
alignment: Alignment . centerRight ,
padding: const EdgeInsets . only ( right: 20.0 ) ,
child:
const Icon ( Icons . delete , color: Colors . white ) ,
) ,
confirmDismiss: ( direction ) async {
// Tampilkan dialog konfirmasi sebelum menghapus dan kembalikan hasilnya
final bool ? shouldRemove =
await _confirmRemoveItem ( index ) ;
// Jika shouldRemove adalah null (dialog ditutup), perlakukan sebagai pembatalan (false)
return shouldRemove ? ? false ;
} ,
onDismissed: ( direction ) {
// Validasi index untuk mencegah error
if ( index > = 0 & & index < items . length ) {
// Simpan deskripsi item untuk pesan snackbar
final itemDescription = items [ index ] . description ;
// Hapus item dari daftar
items . removeAt ( index ) ;
// Update UI
if ( mounted ) {
setState ( ( ) {
// items sudah diupdate oleh removeAt di atas
} ) ;
// Tampilkan snackbar bahwa item telah dihapus
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar (
content: Text (
" Item ' $ itemDescription ' telah dihapus. " ) ) ,
) ;
}
} else {
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text (
" Gagal menghapus item: Index tidak valid setelah dismiss. " ) ) ,
) ;
}
}
} ,
child: GestureDetector (
onTap: ( ) = > _editItem ( index ) , // Tap untuk edit
// onLongPress dihapus karena fungsi utamanya sudah digantikan oleh Dismissible
child: ReceiptItemWidget (
item: item ,
) ,
) ,
) ;
} ) , // .toList() dihapus karena spread operator tidak memerlukannya
// Tombol tambah item
AddItemButton ( onTap: _addItem ) ,
// Garis pembatas
const HorizontalDivider ( ) ,
// Total harga keseluruhan
Container (
width: double . infinity ,
padding: const EdgeInsets . all ( 8.0 ) ,
color: Colors . white ,
child: Column (
children: [
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Expanded (
flex: 4 ,
child: Text (
' TOTAL: ' ,
style: TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
) ,
) ,
) ,
Expanded (
flex: 4 ,
child: Text (
_formatRupiah ( _calculateTotal ( ) ) ,
style: const TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
) ,
textAlign: TextAlign . right ,
) ,
) ,
] ,
) ,
] ,
) ,
) ,
const SizedBox ( height: 8 ) ,
// Garis pembatas
const DottedLine ( ) ,
// Disclaimer toko
StoreDisclaimer ( onTap: _openCustomTextConfig ) ,
// Ucapan terima kasih dan pantun
ThankYouPantun ( onTap: _openCustomTextConfig ) ,
const SizedBox (
height: 16 ) , // Add some space at the very bottom
] ,
) ,
) ,
// Background untuk efek kertas struk tersobek di bagian bawah
Container (
width: 360 ,
color: const Color ( 0xFFE0E0E0 ) , // Warna latar belakang sesuai dengan latar belakang layar
child: const Column (
children: [
const ReceiptTearBottom ( ) , // Efek kertas struk tersobek di bagian bawah
const SizedBox (
height: 15 ) , // Jarak bawah yang lebih besar
] ,
) ,
) ,
] ,
) ,
) ,
) ,
) ,
) ;
}
/// Menghitung total harga semua item
double _calculateTotal ( ) {
return items . fold ( 0.0 , ( sum , item ) = > sum + item . total ) ;
}
}