cashumit/lib/providers/receipt_provider.dart

500 lines
16 KiB
Dart

// lib/providers/receipt_provider.dart
import 'package:flutter/material.dart';
import 'package:cashumit/providers/receipt_state.dart';
import 'package:cashumit/models/receipt_item.dart';
import 'package:cashumit/services/receipt_service.dart'; // Pastikan ReceiptService sudah dibuat
import 'package:cashumit/models/local_receipt.dart';
import 'package:cashumit/services/local_receipt_service.dart';
import 'package:cashumit/services/account_cache_service.dart';
import 'dart:math';
class ReceiptProvider with ChangeNotifier {
late ReceiptState _state;
ReceiptState get state => _state;
ReceiptProvider() {
_state = ReceiptState();
}
/// Initialize state and load credentials and accounts
Future<void> initialize() async {
await loadCredentialsAndAccounts();
// Additional initialization can be added here if needed
}
/// Memuat kredensial dan akun
Future<void> loadCredentialsAndAccounts() async {
final credentials = await ReceiptService.loadCredentials();
if (credentials == null) {
// Jika tidak ada kredensial, kita tetap perlu memberitahu listener bahwa state berubah
// (misalnya untuk menampilkan pesan error di UI)
_state = _state.copyWith(
fireflyUrl: null,
accessToken: null,
accounts: [], // Kosongkan akun juga
);
notifyListeners();
return;
}
// Periksa apakah kredensial berubah
final credentialsChanged = _state.fireflyUrl != credentials['url'] ||
_state.accessToken != credentials['token'];
_state = _state.copyWith(
fireflyUrl: credentials['url'],
accessToken: credentials['token'],
);
notifyListeners();
// Jika kredensial ada dan berubah, lanjutkan untuk memuat akun
if (credentialsChanged) {
await loadAccounts();
// Juga perbarui mirror akun dari server
try {
await ReceiptService.updateAccountMirror(
baseUrl: _state.fireflyUrl!,
accessToken: _state.accessToken!,
);
} catch (e) {
print('Gagal memperbarui mirror akun: $e');
}
} else if (_state.accounts.isEmpty) {
// Jika akun belum pernah dimuat, muat sekarang
await loadAccounts();
}
}
/// Memuat daftar akun dengan prioritas: server > cache > fallback
Future<void> loadAccounts() async {
if (_state.fireflyUrl == null || _state.accessToken == null) {
// Jika kredensial tidak ada, coba ambil dari cache
final cachedAccounts = await AccountCacheService.getCachedAccounts();
if (cachedAccounts.isNotEmpty) {
_state = _state.copyWith(accounts: cachedAccounts);
notifyListeners();
}
return;
}
// Gunakan fungsi untuk mendapatkan akun dengan fallback mekanisme
final allAccounts = await _getAccountsWithFallback();
_state = _state.copyWith(accounts: allAccounts);
notifyListeners();
}
/// Fungsi untuk mendapatkan akun dengan fallback mekanisme
Future<List<Map<String, dynamic>>> _getAccountsWithFallback() async {
if (_state.fireflyUrl == null || _state.accessToken == null) {
return [];
}
try {
// Coba ambil dari server terlebih dahulu
final serverAccounts = await ReceiptService.loadAccounts(
baseUrl: _state.fireflyUrl!,
accessToken: _state.accessToken!,
);
// Simpan ke mirror jika berhasil ambil dari server
await ReceiptService.saveAccountsToMirror(serverAccounts);
return serverAccounts;
} catch (serverError) {
print('Gagal memuat akun dari server: $serverError');
// Jika gagal dari server, coba dari mirror
try {
final mirroredAccounts = await ReceiptService.getMirroredAccounts();
if (mirroredAccounts.isNotEmpty) {
return mirroredAccounts;
}
} catch (mirrorError) {
print('Gagal memuat dari mirror: $mirrorError');
}
// Jika semua fallback gagal, kembalikan list kosong
return [];
}
}
/// Menambahkan item ke receipt
void addItem(ReceiptItem item) {
_state = _state.copyWith(
items: [..._state.items, item],
);
notifyListeners();
}
/// Mengedit item di receipt
void editItem(int index, ReceiptItem newItem) {
if (index < 0 || index >= _state.items.length) return;
final updatedItems = List<ReceiptItem>.from(_state.items);
updatedItems[index] = newItem;
_state = _state.copyWith(
items: updatedItems,
);
notifyListeners();
}
/// Menghapus item dari receipt
void removeItem(int index) {
if (index < 0 || index >= _state.items.length) return;
final updatedItems = List<ReceiptItem>.from(_state.items)..removeAt(index);
_state = _state.copyWith(
items: updatedItems,
);
notifyListeners();
}
/// Memilih akun sumber
void selectSourceAccount(String id, String name) {
_state = _state.copyWith(
sourceAccountId: id,
sourceAccountName: name,
);
// Tidak menyimpan default account mapping lagi
notifyListeners();
}
/// Memilih akun tujuan
void selectDestinationAccount(String id, String name) {
_state = _state.copyWith(
destinationAccountId: id,
destinationAccountName: name,
);
// Tidak menyimpan default account mapping lagi
notifyListeners();
}
/// Mencari ID akun berdasarkan nama dan tipe
String? findAccountIdByName(String name, String expectedType) {
return ReceiptService.findAccountIdByName(
name: name,
expectedType: expectedType,
accounts: _state.accounts,
);
}
/// Mengirim transaksi (ini akan memanggil ReceiptService dan mungkin memperbarui state setelahnya)
Future<String?> submitTransaction() async {
if (_state.items.isEmpty) {
throw Exception('Tidak ada item untuk dikirim');
}
if (_state.fireflyUrl == null || _state.accessToken == null) {
throw Exception('Kredensial Firefly III tidak ditemukan');
}
// Pastikan akun sumber dan tujuan dipilih
// Logika untuk mencari ID berdasarkan nama jika diperlukan bisa ditambahkan di sini
// atau diharapkan UI sudah menangani ini sebelum memanggil submitTransaction
if (_state.sourceAccountId == null || _state.destinationAccountId == null) {
throw Exception('Akun sumber dan tujuan harus dipilih');
}
final transactionId = await ReceiptService.submitTransaction(
items: _state.items,
transactionDate: _state.transactionDate,
sourceAccountId: _state.sourceAccountId!,
destinationAccountId: _state.destinationAccountId!,
accounts: _state.accounts,
baseUrl: _state.fireflyUrl!,
accessToken: _state.accessToken!,
paymentAmount: _state.paymentAmount,
isTip: _state.isTip,
);
return transactionId;
}
/// Simpan transaksi ke penyimpanan lokal (ini akan menyimpan transaksi lokal bukan langsung submit ke server)
Future<String?> saveTransactionLocally() async {
if (_state.items.isEmpty) {
throw Exception('Tidak ada item untuk disimpan');
}
if (_state.sourceAccountId == null || _state.destinationAccountId == null) {
throw Exception('Akun sumber dan tujuan harus dipilih');
}
// Buat ID unik untuk receipt lokal
final receiptId =
'receipt_${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(1000)}';
// Generate transaction description
final transactionDescription =
_generateTransactionDescription(_state.items);
// Buat objek LocalReceipt
final localReceipt = LocalReceipt(
id: receiptId,
items: _state.items,
sourceAccountId: _state.sourceAccountId,
sourceAccountName: _state.sourceAccountName,
destinationAccountId: _state.destinationAccountId,
destinationAccountName: _state.destinationAccountName,
transactionDate: _state.transactionDate,
transactionDescription: transactionDescription,
createdAt: DateTime.now(),
paymentAmount: _state.paymentAmount,
isTip: _state.isTip,
);
// Simpan ke penyimpanan lokal
await LocalReceiptService.saveReceipt(localReceipt);
// Kosongkan items di state setelah disimpan
_state = _state.copyWith(
items: [],
sourceAccountId: null,
sourceAccountName: null,
destinationAccountId: null,
destinationAccountName: null,
paymentAmount: 0.0,
isTip: false,
);
notifyListeners();
return receiptId;
}
/// Submit semua receipt lokal yang belum terkirim ke server
Future<Map<String, dynamic>> submitAllLocalReceipts() async {
if (_state.fireflyUrl == null || _state.accessToken == null) {
throw Exception('Kredensial Firefly III tidak ditemukan');
}
// Submit semua receipt lokal yang belum terkirim
final result = await LocalReceiptService.submitAllUnsubmittedReceipts(
_state.fireflyUrl!,
_state.accessToken!,
);
notifyListeners(); // Update UI jika ada perubahan
return result;
}
/// Generate transaction description from items
String _generateTransactionDescription(List<ReceiptItem> items) {
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(', ');
}
/// Set payment amount
void setPaymentAmount(double amount) {
_state = _state.copyWith(
paymentAmount: amount,
);
notifyListeners();
}
/// Toggle tip status
void toggleTipStatus(bool isTip) {
_state = _state.copyWith(
isTip: isTip,
);
notifyListeners();
}
/// Get change amount
double get changeAmount => _state.changeAmount;
/// Get payment amount
double get paymentAmount => _state.paymentAmount;
/// Get tip status
bool get isTip => _state.isTip;
/// Load receipt data to current state for editing
void loadReceiptForEdit(LocalReceipt receipt) {
_state = _state.copyWith(
items: receipt.items,
sourceAccountId: receipt.sourceAccountId,
sourceAccountName: receipt.sourceAccountName,
destinationAccountId: receipt.destinationAccountId,
destinationAccountName: receipt.destinationAccountName,
transactionDate: receipt.transactionDate,
paymentAmount: receipt.paymentAmount,
isTip: receipt.isTip,
);
notifyListeners();
}
/// Show payment and tip dialog
Future<void> showPaymentTipDialog(BuildContext context) async {
await showDialog(
context: context,
builder: (BuildContext context) {
double localPaymentAmount = _state.paymentAmount;
bool localIsTip = _state.isTip;
return AlertDialog(
title: const Text('Pembayaran dan Tip'),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Total info
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'TOTAL:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
_state.total.toStringAsFixed(0).replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},'),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 16),
TextField(
decoration: const InputDecoration(
labelText: 'Jumlah Bayar',
hintText: 'Masukkan jumlah uang yang dibayar',
labelStyle: TextStyle(fontSize: 14),
hintStyle: TextStyle(fontSize: 14, color: Colors.grey),
),
keyboardType: TextInputType.number,
style: const TextStyle(fontSize: 14),
controller: TextEditingController(
text: localPaymentAmount > 0
? localPaymentAmount.toString()
: '',
),
onChanged: (value) {
localPaymentAmount = double.tryParse(value) ?? 0.0;
},
),
const SizedBox(height: 16),
Row(
children: [
const Text(
'Sebagai Tip: ',
style: TextStyle(fontSize: 14),
),
Switch(
value: localIsTip,
onChanged: (value) {
setState(() {
localIsTip = value;
});
},
),
],
),
if (localPaymentAmount > 0 &&
localPaymentAmount >= _state.total)
const SizedBox(height: 8),
if (localPaymentAmount > 0 &&
localPaymentAmount >= _state.total)
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: (localPaymentAmount - _state.total) >= 0
? Colors.green[50]
: Colors.red[50],
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: (localPaymentAmount - _state.total) >= 0
? Colors.green
: Colors.red,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'KEMBALI:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
(localPaymentAmount - _state.total)
.toStringAsFixed(0)
.replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},'),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: (localPaymentAmount - _state.total) >= 0
? Colors.green
: Colors.red,
),
),
],
),
),
],
);
},
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Batal'),
),
TextButton(
onPressed: () {
setPaymentAmount(localPaymentAmount);
toggleTipStatus(localIsTip);
Navigator.of(context).pop();
},
child: const Text('Simpan'),
),
],
);
},
);
}
// Tambahkan metode lain sesuai kebutuhan, seperti untuk memperbarui transactionDate jika diperlukan
}