471 lines
16 KiB
Dart
471 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:cashumit/models/receipt_item.dart';
|
|
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 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';
|
|
import 'package:cashumit/screens/webview_screen.dart';
|
|
import 'package:cashumit/widgets/receipt_speed_dial.dart';
|
|
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 provider
|
|
import 'package:provider/provider.dart';
|
|
import 'package:cashumit/providers/receipt_provider.dart';
|
|
|
|
// Import untuk penanganan error
|
|
import 'dart:io';
|
|
import 'package:flutter/services.dart';
|
|
|
|
class ReceiptScreen extends StatefulWidget {
|
|
const ReceiptScreen({super.key});
|
|
|
|
@override
|
|
State<ReceiptScreen> createState() => _ReceiptScreenState();
|
|
}
|
|
|
|
class _ReceiptScreenState extends State<ReceiptScreen> {
|
|
// Bluetooth service
|
|
final BluetoothService _bluetoothService = BluetoothService();
|
|
|
|
// Printing status
|
|
bool _isPrinting = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
print('Inisialisasi ReceiptScreen...');
|
|
_initBluetooth();
|
|
_loadSavedBluetoothDevice();
|
|
print('Selesai inisialisasi ReceiptScreen');
|
|
|
|
// Panggil inisialisasi provider setelah widget dibuat
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final receiptProvider = context.read<ReceiptProvider>();
|
|
receiptProvider.initialize();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
}
|
|
|
|
/// Memuat device bluetooth yang tersimpan
|
|
Future<void> _loadSavedBluetoothDevice() async {
|
|
await _bluetoothService.loadSavedDevice();
|
|
}
|
|
|
|
/// Memeriksa status koneksi Bluetooth printer secara real-time
|
|
Future<bool> _checkBluetoothConnection() async {
|
|
return await _bluetoothService.checkConnection();
|
|
}
|
|
|
|
/// Inisialisasi Bluetooth printer
|
|
Future<void> _initBluetooth() async {
|
|
// Inisialisasi BluetoothService
|
|
await _bluetoothService.initialize();
|
|
|
|
// Listen to bluetooth state changes
|
|
_bluetoothService.state.listen((state) {
|
|
if (mounted) {
|
|
switch (state) {
|
|
case BluetoothPrint.connected:
|
|
setState(() {
|
|
// Status koneksi akan dikelola oleh BluetoothService
|
|
});
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Printer terhubung')),
|
|
);
|
|
}
|
|
break;
|
|
case BluetoothPrint.disconnected:
|
|
setState(() {
|
|
// Status koneksi akan dikelola oleh BluetoothService
|
|
});
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Printer terputus')),
|
|
);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Cetak struk ke printer thermal
|
|
Future<void> _printToThermalPrinter() async {
|
|
final receiptProvider = context.read<ReceiptProvider>();
|
|
final state = receiptProvider.state;
|
|
|
|
try {
|
|
// Cek dan reconnect jika perlu
|
|
final isConnected = await _bluetoothService.reconnectIfNeeded();
|
|
if (!isConnected) {
|
|
if (!mounted) return;
|
|
if (_bluetoothService.connectedDevice != null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Gagal menyambungkan ke printer')),
|
|
);
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Harap hubungkan printer terlebih dahulu')),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
await EscPosPrintService.printToThermalPrinter(
|
|
items: state.items,
|
|
transactionDate: state.transactionDate,
|
|
context: context,
|
|
bluetoothService: _bluetoothService,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Perintah cetak dikirim ke printer')),
|
|
);
|
|
} on SocketException catch (e) {
|
|
// Tangani error koneksi secara spesifik
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Koneksi ke printer terputus: ${e.message}')),
|
|
);
|
|
} on PlatformException catch (e) {
|
|
// Tangani error platform secara spesifik
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Error printer: ${e.message}')),
|
|
);
|
|
} catch (e) {
|
|
// Tangani error umum
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Gagal mencetak struk: $e')),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Methods to control printing status
|
|
void _startPrinting() {
|
|
setState(() {
|
|
_isPrinting = true;
|
|
});
|
|
}
|
|
|
|
void _endPrinting() {
|
|
setState(() {
|
|
_isPrinting = false;
|
|
});
|
|
}
|
|
|
|
void _addItem() async {
|
|
final newItem = await showDialog<ReceiptItem>(
|
|
context: context,
|
|
builder: (context) => const AddItemScreen(), // Tampilkan sebagai dialog
|
|
);
|
|
|
|
if (newItem != null) {
|
|
// Menambahkan item melalui provider
|
|
final receiptProvider = context.read<ReceiptProvider>();
|
|
receiptProvider.addItem(newItem);
|
|
}
|
|
}
|
|
|
|
void _editItem(int index) async {
|
|
final receiptProvider = context.read<ReceiptProvider>();
|
|
final state = receiptProvider.state;
|
|
|
|
// Pastikan index valid sebelum mengakses item
|
|
if (index < 0 || index >= state.items.length) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text("Gagal mengedit item: Index tidak valid.")),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
final originalItem =
|
|
state.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) {
|
|
// Memperbarui item melalui provider
|
|
receiptProvider.editItem(index, editedItem);
|
|
}
|
|
// Jika editedItem null (dibatalkan), tidak ada perubahan pada items[index]
|
|
}
|
|
|
|
/// Menampilkan dialog untuk memilih akun sumber (revenue)
|
|
Future<void> _selectSourceAccount() async {
|
|
final receiptProvider = context.read<ReceiptProvider>();
|
|
final state = receiptProvider.state;
|
|
|
|
final selectedAccount = await AccountDialogService.showSourceAccountDialog(
|
|
context,
|
|
state.accounts
|
|
);
|
|
|
|
if (selectedAccount != null) {
|
|
// Memperbarui state melalui provider
|
|
receiptProvider.selectSourceAccount(
|
|
selectedAccount['id'].toString(),
|
|
selectedAccount['name'].toString()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Menampilkan dialog untuk memilih akun tujuan (asset)
|
|
Future<void> _selectDestinationAccount() async {
|
|
final receiptProvider = context.read<ReceiptProvider>();
|
|
final state = receiptProvider.state;
|
|
|
|
final selectedAccount = await AccountDialogService.showDestinationAccountDialog(
|
|
context,
|
|
state.accounts
|
|
);
|
|
|
|
if (selectedAccount != null) {
|
|
// Memperbarui state melalui provider
|
|
receiptProvider.selectDestinationAccount(
|
|
selectedAccount['id'].toString(),
|
|
selectedAccount['name'].toString()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Mengirim transaksi ke Firefly III.
|
|
Future<void> _sendToFirefly() async {
|
|
final receiptProvider = context.read<ReceiptProvider>();
|
|
final state = receiptProvider.state;
|
|
|
|
try {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Mengirim transaksi ke Firefly III...')),
|
|
);
|
|
|
|
final transactionId = await receiptProvider.submitTransaction();
|
|
|
|
if (!mounted) return;
|
|
if (transactionId != null && transactionId != "success") {
|
|
// Navigasi ke WebViewScreen untuk menampilkan transaksi
|
|
if (state.fireflyUrl != null) {
|
|
final transactionUrl = '${state.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.')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Gagal mengirim transaksi: $e')),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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(() {
|
|
// Trigger rebuild untuk 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(() {
|
|
// Trigger rebuild untuk memuat ulang data
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Membuka layar konfigurasi.
|
|
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) {
|
|
final receiptProvider = context.read<ReceiptProvider>();
|
|
receiptProvider.loadCredentialsAndAccounts();
|
|
}
|
|
}
|
|
|
|
final TextStyle baseTextStyle = const TextStyle(
|
|
fontFamily: 'Courier', // Gunakan font courier jika tersedia
|
|
fontSize: 14,
|
|
height: 1.2,
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
children: [
|
|
Scaffold(
|
|
backgroundColor:
|
|
Colors.grey[300], // Latar belakang abu-abu untuk efek struk
|
|
floatingActionButton: Consumer<ReceiptProvider>(
|
|
builder: (context, receiptProvider, child) {
|
|
final state = receiptProvider.state;
|
|
return ReceiptSpeedDial(
|
|
bluetoothService: _bluetoothService,
|
|
onCheckConnection: _checkBluetoothConnection,
|
|
onPrint: _printToThermalPrinter,
|
|
onSettings: _openSettings,
|
|
onReloadAccounts: receiptProvider.loadAccounts,
|
|
hasItems: state.items.isNotEmpty,
|
|
hasSourceAccount: state.sourceAccountId != null,
|
|
hasDestinationAccount: state.destinationAccountId != null,
|
|
onSendToFirefly: _sendToFirefly,
|
|
onPrintingStart: _startPrinting,
|
|
onPrintingEnd: _endPrinting,
|
|
);
|
|
}
|
|
),
|
|
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: [
|
|
SizedBox(height: 15), // Jarak atas yang lebih besar
|
|
ReceiptTearTop(), // Efek kertas struk tersobek di bagian atas
|
|
],
|
|
),
|
|
),
|
|
|
|
// Konten struk
|
|
Consumer<ReceiptProvider>(
|
|
builder: (context, receiptProvider, child) {
|
|
final state = receiptProvider.state;
|
|
return ReceiptBody(
|
|
items: state.items,
|
|
sourceAccountName: state.sourceAccountName,
|
|
destinationAccountName: state.destinationAccountName,
|
|
onOpenStoreInfoConfig: _openStoreInfoConfig,
|
|
onSelectSourceAccount: () => _selectSourceAccount(), // Memanggil fungsi langsung
|
|
onSelectDestinationAccount: () => _selectDestinationAccount(), // Memanggil fungsi langsung
|
|
onOpenCustomTextConfig: _openCustomTextConfig,
|
|
total: state.total,
|
|
onEditItem: (index) => _editItem(index), // Memanggil fungsi langsung
|
|
onRemoveItem: (index) {
|
|
// Validasi index untuk mencegah error
|
|
if (index >= 0 && index < state.items.length) {
|
|
// Hapus item dari daftar melalui provider
|
|
receiptProvider.removeItem(index);
|
|
} else {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
"Gagal menghapus item: Index tidak valid.")),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
onAddItem: () => _addItem(), // Memanggil fungsi langsung
|
|
);
|
|
}
|
|
),
|
|
|
|
// 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: [
|
|
ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah
|
|
SizedBox(height: 15), // Jarak bawah yang lebih besar
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
PrintingStatusCard(
|
|
isVisible: _isPrinting,
|
|
onDismiss: () {
|
|
setState(() {
|
|
_isPrinting = false;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |