Refactor ReceiptScreen: Memindahkan logika kompleks ke service terpisah dan membuat widget terpisah untuk SpeedDial

master
a2nr 2025-08-24 11:02:07 +07:00
parent 3e1c34d1ce
commit 3baa17e9a5
7 changed files with 907 additions and 1365 deletions

View File

@ -0,0 +1,9 @@
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)}";
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
class AccountDialogService {
/// Menampilkan dialog untuk memilih akun sumber (revenue)
static Future<Map<String, dynamic>?> showSourceAccountDialog(
BuildContext context,
List<Map<String, dynamic>> accounts,
) 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 null;
}
// 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 null;
}
return 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),
);
},
),
),
);
},
);
}
/// Menampilkan dialog untuk memilih akun tujuan (asset)
static Future<Map<String, dynamic>?> showDestinationAccountDialog(
BuildContext context,
List<Map<String, dynamic>> accounts,
) 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 null;
}
// 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 null;
}
return 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),
);
},
),
),
);
},
);
}
}

View File

@ -0,0 +1,140 @@
import 'package:bluetooth_print/bluetooth_print.dart';
import 'package:bluetooth_print/bluetooth_print_model.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:typed_data';
class BluetoothService {
late BluetoothPrint _bluetoothPrint;
BluetoothDevice? _connectedDevice;
bool _isConnected = false;
bool _isPrinting = false;
BluetoothService() {
_bluetoothPrint = BluetoothPrint.instance;
}
// Getter untuk status koneksi
bool get isConnected => _isConnected;
bool get isPrinting => _isPrinting;
BluetoothDevice? get connectedDevice => _connectedDevice;
/// Inisialisasi Bluetooth printer
Future<void> initialize() async {
// Memeriksa status koneksi Bluetooth
final isConnected = await _bluetoothPrint.isConnected ?? false;
_isConnected = isConnected;
}
/// Memuat device bluetooth yang tersimpan
Future<void> loadSavedDevice() 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) {
_connectedDevice = BluetoothDevice();
_connectedDevice!.name = deviceName;
_connectedDevice!.address = deviceAddress;
}
}
/// Memeriksa status koneksi Bluetooth printer secara real-time
Future<bool> checkConnection() async {
try {
final isConnected = await _bluetoothPrint.isConnected ?? false;
_isConnected = isConnected;
return isConnected;
} catch (e) {
return false;
}
}
/// Menghubungkan ke device bluetooth
Future<bool> connectToDevice(BluetoothDevice device) async {
try {
await _bluetoothPrint.connect(device);
_connectedDevice = device;
_isConnected = true;
// Simpan device ke SharedPreferences
final prefs = await SharedPreferences.getInstance();
await prefs.setString('bluetooth_device_address', device.address ?? '');
await prefs.setString('bluetooth_device_name', device.name ?? '');
return true;
} catch (e) {
return false;
}
}
/// Memutuskan koneksi dari device bluetooth
Future<void> disconnect() async {
try {
await _bluetoothPrint.disconnect();
_isConnected = false;
} catch (e) {
rethrow;
}
}
/// Mencetak struk ke printer thermal
Future<void> printReceipt(Uint8List data) async {
if (!_isConnected) {
throw Exception('Printer tidak terhubung');
}
_isPrinting = true;
try {
await _bluetoothPrint.printRawData(data);
} finally {
_isPrinting = false;
}
}
/// Mencoba menyambungkan kembali ke printer jika terputus
Future<bool> reconnectIfNeeded() async {
// Periksa koneksi printer secara real-time
bool isConnected = await checkConnection();
if (!isConnected) {
// Coba sambungkan kembali jika ada device yang tersimpan
if (_connectedDevice != null) {
try {
// Putuskan koneksi yang mungkin tersisa
try {
await disconnect();
} catch (disconnectError) {
// Tidak ada koneksi yang perlu diputuskan
}
// Tunggu sebentar sebelum menyambungkan kembali
await Future.delayed(const Duration(milliseconds: 500));
// Sambungkan kembali
bool connectResult = await connectToDevice(_connectedDevice!);
if (!connectResult) {
return false;
}
// Tunggu sebentar untuk memastikan koneksi stabil
await Future.delayed(const Duration(milliseconds: 500));
// Periksa koneksi lagi
isConnected = await checkConnection();
return isConnected;
} catch (e) {
return false;
}
} else {
return false;
}
} else {
return true;
}
}
/// Mendengarkan perubahan status bluetooth
Stream<dynamic> get state => _bluetoothPrint.state;
}

View File

@ -7,6 +7,7 @@ import 'package:image/image.dart' as img;
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// Service untuk menghasilkan perintah ESC/POS menggunakan flutter_esc_pos_utils
class EscPosPrintService {
@ -357,6 +358,77 @@ class EscPosPrintService {
return bytes;
}
/// Mencetak struk ke printer thermal
static Future<void> printToThermalPrinter({
required List<ReceiptItem> items,
required DateTime transactionDate,
required BuildContext context,
required dynamic bluetoothService, // Kita akan sesuaikan tipe ini nanti
}) async {
// Definisikan cashierId dan transactionId di sini karena tidak berubah
final String cashierId = 'KSR001';
final String transactionId = 'TXN202508200001';
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');
try {
print('Menghasilkan byte array ESC/POS menggunakan flutter_esc_pos_utils...');
print('Jumlah item: ${items.length}');
print('Tanggal transaksi: ${transactionDate}');
print('ID kasir: $cashierId');
print('ID transaksi: $transactionId');
// Generate struk dalam format byte array menggunakan EscPosPrintService
final bytes = await generateEscPosBytes(
items: items,
transactionDate: transactionDate,
cashierId: cashierId,
transactionId: transactionId,
);
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)');
}
// Verifikasi koneksi sebelum mencetak
final isConnectedBeforePrint = await bluetoothService.checkConnection();
if (!isConnectedBeforePrint) {
print('Printer tidak terhubung saat akan mencetak');
throw Exception('Printer tidak terhubung saat akan mencetak');
}
print('Mengirim byte array ke printer...');
try {
// Konversi List<int> ke Uint8List
final Uint8List data = Uint8List.fromList(bytes);
await bluetoothService.printReceipt(data);
print('Perintah cetak berhasil dikirim');
} catch (printError) {
print('Error saat mengirim perintah cetak ke printer: $printError');
throw Exception('Gagal mengirim perintah cetak: $printError');
}
} catch (e, stackTrace) {
print('Error saat mencetak struk: $e');
print('Stack trace: $stackTrace');
// Cetak detail error tambahan
print('Tipe error: ${e.runtimeType}');
throw Exception('Gagal mencetak struk: $e');
}
}
}
/// Data class to hold image processing parameters

View File

@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:cashumit/services/bluetooth_service.dart';
class ReceiptSpeedDial extends StatelessWidget {
final BluetoothService bluetoothService;
final Future<bool> Function() onCheckConnection;
final Future<void> Function() onPrint;
final VoidCallback onSettings;
final Future<void> Function() onReloadAccounts;
final bool hasItems;
final bool hasSourceAccount;
final bool hasDestinationAccount;
final Future<void> Function() onSendToFirefly;
const ReceiptSpeedDial({
Key? key,
required this.bluetoothService,
required this.onCheckConnection,
required this.onPrint,
required this.onSettings,
required this.onReloadAccounts,
required this.hasItems,
required this.hasSourceAccount,
required this.hasDestinationAccount,
required this.onSendToFirefly,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SpeedDial(
icon: Icons.menu,
activeIcon: Icons.close,
spacing: 3,
spaceBetweenChildren: 4,
children: [
SpeedDialChild(
child: const Icon(Icons.send),
label: 'Kirim ke Firefly',
onTap: hasItems && hasSourceAccount && hasDestinationAccount
? onSendToFirefly
: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Pilih akun sumber dan tujuan terlebih dahulu'),
duration: Duration(seconds: 2),
),
);
},
backgroundColor: hasItems &&
hasSourceAccount &&
hasDestinationAccount
? Colors.blue
: Colors.grey,
),
SpeedDialChild(
child: const Icon(Icons.refresh),
label: 'Muat Ulang Akun',
onTap: onReloadAccounts,
backgroundColor: Colors.orange,
),
SpeedDialChild(
child: const Icon(Icons.settings),
label: 'Pengaturan',
onTap: onSettings,
backgroundColor: Colors.green,
),
SpeedDialChild(
child: bluetoothService.isPrinting
? const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)
: const Icon(Icons.receipt),
label: 'Cetak Struk',
onTap: bluetoothService.isPrinting
? null
: () async {
// Periksa koneksi secara real-time
final isConnected = await onCheckConnection();
if (isConnected) {
onPrint();
} else {
// Coba sambungkan kembali jika ada device yang tersimpan
if (bluetoothService.connectedDevice != null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mencoba menyambungkan ke printer...')),
);
try {
bool connectResult = await bluetoothService.connectToDevice(bluetoothService.connectedDevice!);
if (!connectResult) {
throw Exception('Gagal menyambungkan ke printer');
}
// Tunggu sebentar untuk memastikan koneksi stabil
await Future.delayed(const Duration(milliseconds: 500));
// Periksa koneksi lagi
final isConnectedAfterConnect = await onCheckConnection();
if (isConnectedAfterConnect) {
onPrint();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal menyambungkan ke printer')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal menyambungkan ke printer: $e')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Hubungkan printer terlebih dahulu')),
);
}
}
},
backgroundColor: Colors.purple,
),
],
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:cashumit/extensions/double_extensions.dart';
class ReceiptTotal extends StatelessWidget {
final double total;
const ReceiptTotal({Key? key, required this.total}) : super(key: key);
@override
Widget build(BuildContext context) {
return 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(
total.toRupiah(),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.right,
),
),
],
),
],
),
);
}
}