cashumit/lib/screens/receipt_screen.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;
});
},
),
],
);
}
}