500 lines
16 KiB
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
|
|
}
|