tambah fitur cache nota dan submit ketika server sudah aktif
parent
77b3f5bb79
commit
eece292526
|
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "cashumit",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cashumit (profile mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cashumit (release mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "plugins",
|
||||||
|
"cwd": "plugins",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "plugins (profile mode)",
|
||||||
|
"cwd": "plugins",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "plugins (release mode)",
|
||||||
|
"cwd": "plugins",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"cwd": "plugins/example",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "example (profile mode)",
|
||||||
|
"cwd": "plugins/example",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "example (release mode)",
|
||||||
|
"cwd": "plugins/example",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -2,13 +2,14 @@ import 'package:cashumit/screens/config_screen.dart';
|
||||||
import 'package:cashumit/screens/transaction_screen.dart';
|
import 'package:cashumit/screens/transaction_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cashumit/screens/receipt_screen.dart';
|
import 'package:cashumit/screens/receipt_screen.dart';
|
||||||
|
import 'package:cashumit/screens/local_receipts_screen.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:cashumit/providers/receipt_provider.dart';
|
import 'package:cashumit/providers/receipt_provider.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
// Ensure WidgetsFlutterBinding is initialized for async operations
|
// Ensure WidgetsFlutterBinding is initialized for async operations
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
|
@ -35,7 +36,8 @@ class MyApp extends StatelessWidget {
|
||||||
'/': (context) => const ReceiptScreen(),
|
'/': (context) => const ReceiptScreen(),
|
||||||
'/transaction': (context) => const TransactionScreen(),
|
'/transaction': (context) => const TransactionScreen(),
|
||||||
'/config': (context) => const ConfigScreen(),
|
'/config': (context) => const ConfigScreen(),
|
||||||
|
'/local-receipts': (context) => const LocalReceiptsScreen(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import 'package:cashumit/models/receipt_item.dart';
|
||||||
|
|
||||||
|
class LocalReceipt {
|
||||||
|
final String id;
|
||||||
|
final List<ReceiptItem> items;
|
||||||
|
final String? sourceAccountId;
|
||||||
|
final String? sourceAccountName;
|
||||||
|
final String? destinationAccountId;
|
||||||
|
final String? destinationAccountName;
|
||||||
|
final DateTime transactionDate;
|
||||||
|
final String?
|
||||||
|
transactionDescription; // Deskripsi transaksi untuk tampilan di daftar
|
||||||
|
final bool isSubmitted;
|
||||||
|
final String? submissionError;
|
||||||
|
final DateTime? submittedAt;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
LocalReceipt({
|
||||||
|
required this.id,
|
||||||
|
required this.items,
|
||||||
|
this.sourceAccountId,
|
||||||
|
this.sourceAccountName,
|
||||||
|
this.destinationAccountId,
|
||||||
|
this.destinationAccountName,
|
||||||
|
required this.transactionDate,
|
||||||
|
this.transactionDescription,
|
||||||
|
this.isSubmitted = false,
|
||||||
|
this.submissionError,
|
||||||
|
this.submittedAt,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
double get total => items.fold(0.0, (sum, item) => sum + item.total);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'items': items.map((item) => item.toJson()).toList(),
|
||||||
|
'sourceAccountId': sourceAccountId,
|
||||||
|
'sourceAccountName': sourceAccountName,
|
||||||
|
'destinationAccountId': destinationAccountId,
|
||||||
|
'destinationAccountName': destinationAccountName,
|
||||||
|
'transactionDate': transactionDate.toIso8601String(),
|
||||||
|
'transactionDescription': transactionDescription,
|
||||||
|
'isSubmitted': isSubmitted,
|
||||||
|
'submissionError': submissionError,
|
||||||
|
'submittedAt': submittedAt?.toIso8601String(),
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory LocalReceipt.fromJson(Map<String, dynamic> json) {
|
||||||
|
return LocalReceipt(
|
||||||
|
id: json['id'],
|
||||||
|
items: (json['items'] as List)
|
||||||
|
.map((item) => ReceiptItem.fromJson(item))
|
||||||
|
.toList(),
|
||||||
|
sourceAccountId: json['sourceAccountId'],
|
||||||
|
sourceAccountName: json['sourceAccountName'],
|
||||||
|
destinationAccountId: json['destinationAccountId'],
|
||||||
|
destinationAccountName: json['destinationAccountName'],
|
||||||
|
transactionDate: DateTime.parse(json['transactionDate']),
|
||||||
|
transactionDescription: json['transactionDescription'],
|
||||||
|
isSubmitted: json['isSubmitted'] ?? false,
|
||||||
|
submissionError: json['submissionError'],
|
||||||
|
submittedAt: json['submittedAt'] != null
|
||||||
|
? DateTime.parse(json['submittedAt'])
|
||||||
|
: null,
|
||||||
|
createdAt: DateTime.parse(json['createdAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalReceipt copyWith({
|
||||||
|
String? id,
|
||||||
|
List<ReceiptItem>? items,
|
||||||
|
String? sourceAccountId,
|
||||||
|
String? sourceAccountName,
|
||||||
|
String? destinationAccountId,
|
||||||
|
String? destinationAccountName,
|
||||||
|
DateTime? transactionDate,
|
||||||
|
String? transactionDescription,
|
||||||
|
bool? isSubmitted,
|
||||||
|
String? submissionError,
|
||||||
|
DateTime? submittedAt,
|
||||||
|
DateTime? createdAt,
|
||||||
|
}) {
|
||||||
|
return LocalReceipt(
|
||||||
|
id: id ?? this.id,
|
||||||
|
items: items ?? this.items,
|
||||||
|
sourceAccountId: sourceAccountId ?? this.sourceAccountId,
|
||||||
|
sourceAccountName: sourceAccountName ?? this.sourceAccountName,
|
||||||
|
destinationAccountId: destinationAccountId ?? this.destinationAccountId,
|
||||||
|
destinationAccountName:
|
||||||
|
destinationAccountName ?? this.destinationAccountName,
|
||||||
|
transactionDate: transactionDate ?? this.transactionDate,
|
||||||
|
transactionDescription:
|
||||||
|
transactionDescription ?? this.transactionDescription,
|
||||||
|
isSubmitted: isSubmitted ?? this.isSubmitted,
|
||||||
|
submissionError: submissionError ?? this.submissionError,
|
||||||
|
submittedAt: submittedAt ?? this.submittedAt,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,24 @@ class ReceiptItem {
|
||||||
|
|
||||||
double get total => quantity * price;
|
double get total => quantity * price;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'description': description,
|
||||||
|
'quantity': quantity,
|
||||||
|
'price': price,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ReceiptItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ReceiptItem(
|
||||||
|
description: json['description'],
|
||||||
|
quantity: json['quantity'].toDouble(),
|
||||||
|
price: json['price'].toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ReceiptItem(description: $description, quantity: $quantity, price: $price)';
|
return 'ReceiptItem(description: $description, quantity: $quantity, price: $price)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ import 'package:cashumit/providers/receipt_state.dart';
|
||||||
import 'package:cashumit/models/receipt_item.dart';
|
import 'package:cashumit/models/receipt_item.dart';
|
||||||
import 'package:cashumit/services/receipt_service.dart'; // Pastikan ReceiptService sudah dibuat
|
import 'package:cashumit/services/receipt_service.dart'; // Pastikan ReceiptService sudah dibuat
|
||||||
import 'package:cashumit/models/firefly_account.dart'; // Untuk tipe akun
|
import 'package:cashumit/models/firefly_account.dart'; // Untuk tipe akun
|
||||||
|
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 {
|
class ReceiptProvider with ChangeNotifier {
|
||||||
late ReceiptState _state;
|
late ReceiptState _state;
|
||||||
|
|
@ -24,7 +28,7 @@ class ReceiptProvider with ChangeNotifier {
|
||||||
/// Memuat kredensial dan akun
|
/// Memuat kredensial dan akun
|
||||||
Future<void> loadCredentialsAndAccounts() async {
|
Future<void> loadCredentialsAndAccounts() async {
|
||||||
final credentials = await ReceiptService.loadCredentials();
|
final credentials = await ReceiptService.loadCredentials();
|
||||||
|
|
||||||
if (credentials == null) {
|
if (credentials == null) {
|
||||||
// Jika tidak ada kredensial, kita tetap perlu memberitahu listener bahwa state berubah
|
// Jika tidak ada kredensial, kita tetap perlu memberitahu listener bahwa state berubah
|
||||||
// (misalnya untuk menampilkan pesan error di UI)
|
// (misalnya untuk menampilkan pesan error di UI)
|
||||||
|
|
@ -38,7 +42,8 @@ class ReceiptProvider with ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Periksa apakah kredensial berubah
|
// Periksa apakah kredensial berubah
|
||||||
final credentialsChanged = _state.fireflyUrl != credentials['url'] || _state.accessToken != credentials['token'];
|
final credentialsChanged = _state.fireflyUrl != credentials['url'] ||
|
||||||
|
_state.accessToken != credentials['token'];
|
||||||
|
|
||||||
_state = _state.copyWith(
|
_state = _state.copyWith(
|
||||||
fireflyUrl: credentials['url'],
|
fireflyUrl: credentials['url'],
|
||||||
|
|
@ -49,32 +54,47 @@ class ReceiptProvider with ChangeNotifier {
|
||||||
// Jika kredensial ada dan berubah, lanjutkan untuk memuat akun
|
// Jika kredensial ada dan berubah, lanjutkan untuk memuat akun
|
||||||
if (credentialsChanged) {
|
if (credentialsChanged) {
|
||||||
await loadAccounts();
|
await loadAccounts();
|
||||||
|
// Juga perbarui cache akun dari server
|
||||||
|
try {
|
||||||
|
await ReceiptService.updateAccountCache(
|
||||||
|
baseUrl: _state.fireflyUrl!,
|
||||||
|
accessToken: _state.accessToken!,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('Gagal memperbarui cache akun: $e');
|
||||||
|
}
|
||||||
} else if (_state.accounts.isEmpty) {
|
} else if (_state.accounts.isEmpty) {
|
||||||
// Jika akun belum pernah dimuat, muat sekarang
|
// Jika akun belum pernah dimuat, muat sekarang
|
||||||
await loadAccounts();
|
await loadAccounts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Memuat daftar akun
|
/// Memuat daftar akun dengan prioritas: server > cache > fallback
|
||||||
Future<void> loadAccounts() async {
|
Future<void> loadAccounts() async {
|
||||||
if (_state.fireflyUrl == null || _state.accessToken == null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Gunakan cache service untuk mendapatkan akun dengan prioritas
|
||||||
final allAccounts = await ReceiptService.loadAccounts(
|
final allAccounts =
|
||||||
|
await AccountCacheService.getAccountsWithFallback(() async {
|
||||||
|
final accounts = await ReceiptService.loadAccounts(
|
||||||
baseUrl: _state.fireflyUrl!,
|
baseUrl: _state.fireflyUrl!,
|
||||||
accessToken: _state.accessToken!,
|
accessToken: _state.accessToken!,
|
||||||
);
|
);
|
||||||
|
// Perbarui cache dengan data terbaru dari server
|
||||||
|
await ReceiptService.saveAccountsToCache(accounts);
|
||||||
|
return accounts;
|
||||||
|
});
|
||||||
|
|
||||||
_state = _state.copyWith(accounts: allAccounts);
|
_state = _state.copyWith(accounts: allAccounts);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (error) {
|
|
||||||
// Error handling bisa dilakukan di sini atau dibiarkan untuk ditangani oleh UI
|
|
||||||
print('Error in ReceiptProvider.loadAccounts: $error');
|
|
||||||
// Bisa memicu state error jika diperlukan
|
|
||||||
notifyListeners(); // Tetap notify untuk memperbarui UI (misalnya menampilkan pesan error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Menambahkan item ke receipt
|
/// Menambahkan item ke receipt
|
||||||
|
|
@ -88,10 +108,10 @@ class ReceiptProvider with ChangeNotifier {
|
||||||
/// Mengedit item di receipt
|
/// Mengedit item di receipt
|
||||||
void editItem(int index, ReceiptItem newItem) {
|
void editItem(int index, ReceiptItem newItem) {
|
||||||
if (index < 0 || index >= _state.items.length) return;
|
if (index < 0 || index >= _state.items.length) return;
|
||||||
|
|
||||||
final updatedItems = List<ReceiptItem>.from(_state.items);
|
final updatedItems = List<ReceiptItem>.from(_state.items);
|
||||||
updatedItems[index] = newItem;
|
updatedItems[index] = newItem;
|
||||||
|
|
||||||
_state = _state.copyWith(
|
_state = _state.copyWith(
|
||||||
items: updatedItems,
|
items: updatedItems,
|
||||||
);
|
);
|
||||||
|
|
@ -101,9 +121,9 @@ class ReceiptProvider with ChangeNotifier {
|
||||||
/// Menghapus item dari receipt
|
/// Menghapus item dari receipt
|
||||||
void removeItem(int index) {
|
void removeItem(int index) {
|
||||||
if (index < 0 || index >= _state.items.length) return;
|
if (index < 0 || index >= _state.items.length) return;
|
||||||
|
|
||||||
final updatedItems = List<ReceiptItem>.from(_state.items)..removeAt(index);
|
final updatedItems = List<ReceiptItem>.from(_state.items)..removeAt(index);
|
||||||
|
|
||||||
_state = _state.copyWith(
|
_state = _state.copyWith(
|
||||||
items: updatedItems,
|
items: updatedItems,
|
||||||
);
|
);
|
||||||
|
|
@ -150,7 +170,7 @@ class ReceiptProvider with ChangeNotifier {
|
||||||
// Pastikan akun sumber dan tujuan dipilih
|
// Pastikan akun sumber dan tujuan dipilih
|
||||||
// Logika untuk mencari ID berdasarkan nama jika diperlukan bisa ditambahkan di sini
|
// Logika untuk mencari ID berdasarkan nama jika diperlukan bisa ditambahkan di sini
|
||||||
// atau diharapkan UI sudah menangani ini sebelum memanggil submitTransaction
|
// atau diharapkan UI sudah menangani ini sebelum memanggil submitTransaction
|
||||||
|
|
||||||
if (_state.sourceAccountId == null || _state.destinationAccountId == null) {
|
if (_state.sourceAccountId == null || _state.destinationAccountId == null) {
|
||||||
throw Exception('Akun sumber dan tujuan harus dipilih');
|
throw Exception('Akun sumber dan tujuan harus dipilih');
|
||||||
}
|
}
|
||||||
|
|
@ -167,6 +187,87 @@ class ReceiptProvider with ChangeNotifier {
|
||||||
|
|
||||||
return transactionId;
|
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(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
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(', ');
|
||||||
|
}
|
||||||
|
|
||||||
// Tambahkan metode lain sesuai kebutuhan, seperti untuk memperbarui transactionDate jika diperlukan
|
// Tambahkan metode lain sesuai kebutuhan, seperti untuk memperbarui transactionDate jika diperlukan
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,402 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:cashumit/models/local_receipt.dart';
|
||||||
|
import 'package:cashumit/services/local_receipt_service.dart';
|
||||||
|
import 'package:cashumit/services/receipt_service.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class LocalReceiptsScreen extends StatefulWidget {
|
||||||
|
const LocalReceiptsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LocalReceiptsScreen> createState() => _LocalReceiptsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LocalReceiptsScreenState extends State<LocalReceiptsScreen> {
|
||||||
|
List<LocalReceipt> receipts = [];
|
||||||
|
bool isLoading = true;
|
||||||
|
bool isSubmitting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadReceipts();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadReceipts() async {
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final loadedReceipts = await LocalReceiptService.getReceipts();
|
||||||
|
setState(() {
|
||||||
|
receipts = loadedReceipts;
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Gagal memuat daftar nota: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submitAllReceipts() async {
|
||||||
|
final credentials = await ReceiptService.loadCredentials();
|
||||||
|
if (credentials == null) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Silakan konfigurasi kredensial FireFly III terlebih dahulu'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isSubmitting = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await LocalReceiptService.submitAllUnsubmittedReceipts(
|
||||||
|
credentials['url']!,
|
||||||
|
credentials['token']!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Berhasil: ${result['successCount']} berhasil, ${result['failureCount']} gagal dari ${result['totalCount']} total nota',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _loadReceipts(); // Refresh daftar setelah submit
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Gagal mengirim nota: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
isSubmitting = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteReceipt(String receiptId) async {
|
||||||
|
try {
|
||||||
|
await LocalReceiptService.removeReceipt(receiptId);
|
||||||
|
await _loadReceipts();
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Nota berhasil dihapus')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Gagal menghapus nota: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatCurrency(double amount) {
|
||||||
|
final formatter = NumberFormat.currency(
|
||||||
|
locale: 'id_ID',
|
||||||
|
symbol: 'Rp ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
);
|
||||||
|
return formatter.format(amount).replaceAll('.00', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Nota Tersimpan'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: isLoading ? null : _loadReceipts,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Summary card
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.3),
|
||||||
|
spreadRadius: 1,
|
||||||
|
blurRadius: 3,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
receipts
|
||||||
|
.where((r) => !r.isSubmitted)
|
||||||
|
.length
|
||||||
|
.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Menunggu',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
receipts
|
||||||
|
.where((r) => r.isSubmitted)
|
||||||
|
.length
|
||||||
|
.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Terkirim',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
receipts.length.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Total',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: isSubmitting ? null : _submitAllReceipts,
|
||||||
|
icon: isSubmitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor:
|
||||||
|
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.sync),
|
||||||
|
label:
|
||||||
|
Text(isSubmitting ? 'Mengirim...' : 'Kirim Semua Nota'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Receipts list
|
||||||
|
Expanded(
|
||||||
|
child: isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: receipts.isEmpty
|
||||||
|
? const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.receipt_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Belum ada nota tersimpan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: RefreshIndicator(
|
||||||
|
onRefresh: _loadReceipts,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
itemCount: receipts.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final receipt = receipts[index];
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(16),
|
||||||
|
leading: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: receipt.isSubmitted
|
||||||
|
? Colors.green.shade100
|
||||||
|
: Colors.orange.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
receipt.isSubmitted
|
||||||
|
? Icons.check_circle
|
||||||
|
: Icons.access_time,
|
||||||
|
color: receipt.isSubmitted
|
||||||
|
? Colors.green
|
||||||
|
: Colors.orange,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
// Transaction description di kiri
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Text(
|
||||||
|
receipt.transactionDescription ??
|
||||||
|
'Transaksi Struk Belanja',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// Total di kanan
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_formatCurrency(receipt.total),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Dibuat: ${DateFormat('dd/MM/yyyy HH:mm').format(receipt.createdAt)}',
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
if (receipt.isSubmitted)
|
||||||
|
Text(
|
||||||
|
'Dikirim: ${DateFormat('dd/MM/yyyy HH:mm').format(receipt.submittedAt ?? receipt.createdAt)}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (receipt.submissionError != null)
|
||||||
|
Text(
|
||||||
|
'Error: ${receipt.submissionError}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: PopupMenuButton<String>(
|
||||||
|
onSelected: (String action) {
|
||||||
|
if (action == 'delete') {
|
||||||
|
_showDeleteConfirmation(receipt.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
return [
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.delete, size: 18),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Hapus'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showDeleteConfirmation(String receiptId) async {
|
||||||
|
final result = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Konfirmasi Hapus'),
|
||||||
|
content: const Text('Apakah Anda yakin ingin menghapus nota ini?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: const Text('Hapus'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
_deleteReceipt(receiptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:cashumit/screens/add_item_screen.dart';
|
||||||
import 'package:bluetooth_print/bluetooth_print.dart';
|
import 'package:bluetooth_print/bluetooth_print.dart';
|
||||||
import 'package:cashumit/services/bluetooth_service.dart';
|
import 'package:cashumit/services/bluetooth_service.dart';
|
||||||
import 'package:cashumit/widgets/receipt_body.dart';
|
import 'package:cashumit/widgets/receipt_body.dart';
|
||||||
|
import 'package:cashumit/screens/local_receipts_screen.dart';
|
||||||
|
|
||||||
// Import widget komponen struk baru
|
// Import widget komponen struk baru
|
||||||
import 'package:cashumit/widgets/receipt_tear_effect.dart';
|
import 'package:cashumit/widgets/receipt_tear_effect.dart';
|
||||||
|
|
@ -35,7 +36,7 @@ class ReceiptScreen extends StatefulWidget {
|
||||||
class _ReceiptScreenState extends State<ReceiptScreen> {
|
class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
// Bluetooth service
|
// Bluetooth service
|
||||||
final BluetoothService _bluetoothService = BluetoothService();
|
final BluetoothService _bluetoothService = BluetoothService();
|
||||||
|
|
||||||
// Printing status
|
// Printing status
|
||||||
bool _isPrinting = false;
|
bool _isPrinting = false;
|
||||||
|
|
||||||
|
|
@ -46,7 +47,7 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
_initBluetooth();
|
_initBluetooth();
|
||||||
_loadSavedBluetoothDevice();
|
_loadSavedBluetoothDevice();
|
||||||
print('Selesai inisialisasi ReceiptScreen');
|
print('Selesai inisialisasi ReceiptScreen');
|
||||||
|
|
||||||
// Panggil inisialisasi provider setelah widget dibuat
|
// Panggil inisialisasi provider setelah widget dibuat
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final receiptProvider = context.read<ReceiptProvider>();
|
final receiptProvider = context.read<ReceiptProvider>();
|
||||||
|
|
@ -114,7 +115,7 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
Future<void> _printToThermalPrinter() async {
|
Future<void> _printToThermalPrinter() async {
|
||||||
final receiptProvider = context.read<ReceiptProvider>();
|
final receiptProvider = context.read<ReceiptProvider>();
|
||||||
final state = receiptProvider.state;
|
final state = receiptProvider.state;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Cek dan reconnect jika perlu
|
// Cek dan reconnect jika perlu
|
||||||
final isConnected = await _bluetoothService.reconnectIfNeeded();
|
final isConnected = await _bluetoothService.reconnectIfNeeded();
|
||||||
|
|
@ -122,25 +123,24 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (_bluetoothService.connectedDevice != null) {
|
if (_bluetoothService.connectedDevice != null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(content: Text('Gagal menyambungkan ke printer')),
|
||||||
content: Text('Gagal menyambungkan ke printer')),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Harap hubungkan printer terlebih dahulu')),
|
content: Text('Harap hubungkan printer terlebih dahulu')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await EscPosPrintService.printToThermalPrinter(
|
await EscPosPrintService.printToThermalPrinter(
|
||||||
items: state.items,
|
items: state.items,
|
||||||
transactionDate: state.transactionDate,
|
transactionDate: state.transactionDate,
|
||||||
context: context,
|
context: context,
|
||||||
bluetoothService: _bluetoothService,
|
bluetoothService: _bluetoothService,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Perintah cetak dikirim ke printer')),
|
const SnackBar(content: Text('Perintah cetak dikirim ke printer')),
|
||||||
|
|
@ -165,14 +165,14 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Methods to control printing status
|
/// Methods to control printing status
|
||||||
void _startPrinting() {
|
void _startPrinting() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isPrinting = true;
|
_isPrinting = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _endPrinting() {
|
void _endPrinting() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isPrinting = false;
|
_isPrinting = false;
|
||||||
|
|
@ -195,13 +195,13 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
void _editItem(int index) async {
|
void _editItem(int index) async {
|
||||||
final receiptProvider = context.read<ReceiptProvider>();
|
final receiptProvider = context.read<ReceiptProvider>();
|
||||||
final state = receiptProvider.state;
|
final state = receiptProvider.state;
|
||||||
|
|
||||||
// Pastikan index valid sebelum mengakses item
|
// Pastikan index valid sebelum mengakses item
|
||||||
if (index < 0 || index >= state.items.length) {
|
if (index < 0 || index >= state.items.length) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text("Gagal mengedit item: Index tidak valid.")),
|
content: Text("Gagal mengedit item: Index tidak valid.")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -228,18 +228,14 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
Future<void> _selectSourceAccount() async {
|
Future<void> _selectSourceAccount() async {
|
||||||
final receiptProvider = context.read<ReceiptProvider>();
|
final receiptProvider = context.read<ReceiptProvider>();
|
||||||
final state = receiptProvider.state;
|
final state = receiptProvider.state;
|
||||||
|
|
||||||
final selectedAccount = await AccountDialogService.showSourceAccountDialog(
|
final selectedAccount = await AccountDialogService.showSourceAccountDialog(
|
||||||
context,
|
context, state.accounts);
|
||||||
state.accounts
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedAccount != null) {
|
if (selectedAccount != null) {
|
||||||
// Memperbarui state melalui provider
|
// Memperbarui state melalui provider
|
||||||
receiptProvider.selectSourceAccount(
|
receiptProvider.selectSourceAccount(
|
||||||
selectedAccount['id'].toString(),
|
selectedAccount['id'].toString(), selectedAccount['name'].toString());
|
||||||
selectedAccount['name'].toString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,18 +243,15 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
Future<void> _selectDestinationAccount() async {
|
Future<void> _selectDestinationAccount() async {
|
||||||
final receiptProvider = context.read<ReceiptProvider>();
|
final receiptProvider = context.read<ReceiptProvider>();
|
||||||
final state = receiptProvider.state;
|
final state = receiptProvider.state;
|
||||||
|
|
||||||
final selectedAccount = await AccountDialogService.showDestinationAccountDialog(
|
final selectedAccount =
|
||||||
context,
|
await AccountDialogService.showDestinationAccountDialog(
|
||||||
state.accounts
|
context, state.accounts);
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedAccount != null) {
|
if (selectedAccount != null) {
|
||||||
// Memperbarui state melalui provider
|
// Memperbarui state melalui provider
|
||||||
receiptProvider.selectDestinationAccount(
|
receiptProvider.selectDestinationAccount(
|
||||||
selectedAccount['id'].toString(),
|
selectedAccount['id'].toString(), selectedAccount['name'].toString());
|
||||||
selectedAccount['name'].toString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,20 +259,21 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
Future<void> _sendToFirefly() async {
|
Future<void> _sendToFirefly() async {
|
||||||
final receiptProvider = context.read<ReceiptProvider>();
|
final receiptProvider = context.read<ReceiptProvider>();
|
||||||
final state = receiptProvider.state;
|
final state = receiptProvider.state;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Mengirim transaksi ke Firefly III...')),
|
const SnackBar(content: Text('Mengirim transaksi ke Firefly III...')),
|
||||||
);
|
);
|
||||||
|
|
||||||
final transactionId = await receiptProvider.submitTransaction();
|
final transactionId = await receiptProvider.submitTransaction();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (transactionId != null && transactionId != "success") {
|
if (transactionId != null && transactionId != "success") {
|
||||||
// Navigasi ke WebViewScreen untuk menampilkan transaksi
|
// Navigasi ke WebViewScreen untuk menampilkan transaksi
|
||||||
if (state.fireflyUrl != null) {
|
if (state.fireflyUrl != null) {
|
||||||
final transactionUrl = '${state.fireflyUrl}/transactions/show/$transactionId';
|
final transactionUrl =
|
||||||
|
'${state.fireflyUrl}/transactions/show/$transactionId';
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
@ -296,15 +290,15 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content:
|
content:
|
||||||
Text('Transaksi berhasil dikirim ke Firefly III (tanpa ID)')),
|
Text('Transaksi berhasil dikirim ke Firefly III (tanpa ID)')),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'Gagal mengirim transaksi ke Firefly III. Periksa log untuk detail kesalahan.')),
|
'Gagal mengirim transaksi ke Firefly III. Periksa log untuk detail kesalahan.')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -355,6 +349,12 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Membuka layar daftar receipt lokal.
|
||||||
|
void _openLocalReceipts() async {
|
||||||
|
// Gunakan route yang sudah didefinisikan di main.dart
|
||||||
|
await Navigator.pushNamed(context, '/local-receipts');
|
||||||
|
}
|
||||||
|
|
||||||
final TextStyle baseTextStyle = const TextStyle(
|
final TextStyle baseTextStyle = const TextStyle(
|
||||||
fontFamily: 'Courier', // Gunakan font courier jika tersedia
|
fontFamily: 'Courier', // Gunakan font courier jika tersedia
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|
@ -369,23 +369,23 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Colors.grey[300], // Latar belakang abu-abu untuk efek struk
|
Colors.grey[300], // Latar belakang abu-abu untuk efek struk
|
||||||
floatingActionButton: Consumer<ReceiptProvider>(
|
floatingActionButton: Consumer<ReceiptProvider>(
|
||||||
builder: (context, receiptProvider, child) {
|
builder: (context, receiptProvider, child) {
|
||||||
final state = receiptProvider.state;
|
final state = receiptProvider.state;
|
||||||
return ReceiptSpeedDial(
|
return ReceiptSpeedDial(
|
||||||
bluetoothService: _bluetoothService,
|
bluetoothService: _bluetoothService,
|
||||||
onCheckConnection: _checkBluetoothConnection,
|
onCheckConnection: _checkBluetoothConnection,
|
||||||
onPrint: _printToThermalPrinter,
|
onPrint: _printToThermalPrinter,
|
||||||
onSettings: _openSettings,
|
onSettings: _openSettings,
|
||||||
onReloadAccounts: receiptProvider.loadAccounts,
|
onReloadAccounts: receiptProvider.loadAccounts,
|
||||||
hasItems: state.items.isNotEmpty,
|
hasItems: state.items.isNotEmpty,
|
||||||
hasSourceAccount: state.sourceAccountId != null,
|
hasSourceAccount: state.sourceAccountId != null,
|
||||||
hasDestinationAccount: state.destinationAccountId != null,
|
hasDestinationAccount: state.destinationAccountId != null,
|
||||||
onSendToFirefly: _sendToFirefly,
|
onSendToFirefly: _sendToFirefly,
|
||||||
onPrintingStart: _startPrinting,
|
onPrintingStart: _startPrinting,
|
||||||
onPrintingEnd: _endPrinting,
|
onPrintingEnd: _endPrinting,
|
||||||
);
|
onOpenLocalReceipts: _openLocalReceipts,
|
||||||
}
|
);
|
||||||
),
|
}),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
// Membungkus dengan widget Center untuk memastikan struk berada di tengah
|
// Membungkus dengan widget Center untuk memastikan struk berada di tengah
|
||||||
|
|
@ -397,7 +397,8 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
// Background untuk efek kertas struk tersobek di bagian atas
|
// Background untuk efek kertas struk tersobek di bagian atas
|
||||||
Container(
|
Container(
|
||||||
width: 360,
|
width: 360,
|
||||||
color: const Color(0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar
|
color: const Color(
|
||||||
|
0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar
|
||||||
child: const Column(
|
child: const Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 15), // Jarak atas yang lebih besar
|
SizedBox(height: 15), // Jarak atas yang lebih besar
|
||||||
|
|
@ -408,42 +409,46 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
|
|
||||||
// Konten struk
|
// Konten struk
|
||||||
Consumer<ReceiptProvider>(
|
Consumer<ReceiptProvider>(
|
||||||
builder: (context, receiptProvider, child) {
|
builder: (context, receiptProvider, child) {
|
||||||
final state = receiptProvider.state;
|
final state = receiptProvider.state;
|
||||||
return ReceiptBody(
|
return ReceiptBody(
|
||||||
items: state.items,
|
items: state.items,
|
||||||
sourceAccountName: state.sourceAccountName,
|
sourceAccountName: state.sourceAccountName,
|
||||||
destinationAccountName: state.destinationAccountName,
|
destinationAccountName: state.destinationAccountName,
|
||||||
onOpenStoreInfoConfig: _openStoreInfoConfig,
|
onOpenStoreInfoConfig: _openStoreInfoConfig,
|
||||||
onSelectSourceAccount: () => _selectSourceAccount(), // Memanggil fungsi langsung
|
onSelectSourceAccount: () =>
|
||||||
onSelectDestinationAccount: () => _selectDestinationAccount(), // Memanggil fungsi langsung
|
_selectSourceAccount(), // Memanggil fungsi langsung
|
||||||
onOpenCustomTextConfig: _openCustomTextConfig,
|
onSelectDestinationAccount: () =>
|
||||||
total: state.total,
|
_selectDestinationAccount(), // Memanggil fungsi langsung
|
||||||
onEditItem: (index) => _editItem(index), // Memanggil fungsi langsung
|
onOpenCustomTextConfig: _openCustomTextConfig,
|
||||||
onRemoveItem: (index) {
|
total: state.total,
|
||||||
// Validasi index untuk mencegah error
|
onEditItem: (index) =>
|
||||||
if (index >= 0 && index < state.items.length) {
|
_editItem(index), // Memanggil fungsi langsung
|
||||||
// Hapus item dari daftar melalui provider
|
onRemoveItem: (index) {
|
||||||
receiptProvider.removeItem(index);
|
// Validasi index untuk mencegah error
|
||||||
} else {
|
if (index >= 0 && index < state.items.length) {
|
||||||
if (mounted) {
|
// Hapus item dari daftar melalui provider
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
receiptProvider.removeItem(index);
|
||||||
const SnackBar(
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
"Gagal menghapus item: Index tidak valid.")),
|
"Gagal menghapus item: Index tidak valid.")),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onAddItem: () => _addItem(), // Memanggil fungsi langsung
|
},
|
||||||
);
|
onAddItem: () =>
|
||||||
}
|
_addItem(), // Memanggil fungsi langsung
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
// Background untuk efek kertas struk tersobek di bagian bawah
|
// Background untuk efek kertas struk tersobek di bagian bawah
|
||||||
Container(
|
Container(
|
||||||
width: 360,
|
width: 360,
|
||||||
color: const Color(0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar
|
color: const Color(
|
||||||
|
0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar
|
||||||
child: const Column(
|
child: const Column(
|
||||||
children: [
|
children: [
|
||||||
ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah
|
ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah
|
||||||
|
|
@ -468,4 +473,4 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class AccountCacheService {
|
||||||
|
static const String _accountsKey = 'cached_accounts';
|
||||||
|
static const String _lastUpdatedKey = 'accounts_last_updated';
|
||||||
|
|
||||||
|
/// Simpan akun ke cache lokal
|
||||||
|
static Future<void> saveAccountsLocally(
|
||||||
|
List<Map<String, dynamic>> accounts) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
// Konversi akun ke JSON string
|
||||||
|
final accountsJson =
|
||||||
|
accounts.map((account) => json.encode(account)).toList();
|
||||||
|
|
||||||
|
await prefs.setStringList(_accountsKey, accountsJson);
|
||||||
|
await prefs.setInt(_lastUpdatedKey, DateTime.now().millisecondsSinceEpoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ambil akun dari cache lokal
|
||||||
|
static Future<List<Map<String, dynamic>>> getCachedAccounts() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final accountsJsonList = prefs.getStringList(_accountsKey) ?? [];
|
||||||
|
|
||||||
|
if (accountsJsonList.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountsJsonList
|
||||||
|
.map((jsonString) => json.decode(jsonString) as Map<String, dynamic>)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Periksa apakah cache akun masih valid (kurang dari 1 jam)
|
||||||
|
static Future<bool> isCacheValid() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final lastUpdated = prefs.getInt(_lastUpdatedKey) ?? 0;
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
// Cache valid selama 1 jam (360000 ms)
|
||||||
|
return (now - lastUpdated) < 360000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hapus cache akun
|
||||||
|
static Future<void> clearCache() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_accountsKey);
|
||||||
|
await prefs.remove(_lastUpdatedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update cache akun dari server
|
||||||
|
static Future<void> updateAccountsFromServer(
|
||||||
|
List<Map<String, dynamic>> accounts) async {
|
||||||
|
await saveAccountsLocally(accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dapatkan akun dengan prioritas: cache valid > data server > fallback kosong
|
||||||
|
static Future<List<Map<String, dynamic>>> getAccountsWithFallback(
|
||||||
|
Future<List<Map<String, dynamic>>> Function() serverFetchFunction,
|
||||||
|
) async {
|
||||||
|
// Coba ambil dari cache dulu
|
||||||
|
if (await isCacheValid()) {
|
||||||
|
final cachedAccounts = await getCachedAccounts();
|
||||||
|
if (cachedAccounts.isNotEmpty) {
|
||||||
|
return cachedAccounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika cache tidak valid atau kosong, coba ambil dari server
|
||||||
|
try {
|
||||||
|
final serverAccounts = await serverFetchFunction();
|
||||||
|
if (serverAccounts.isNotEmpty) {
|
||||||
|
// Simpan ke cache jika berhasil ambil dari server
|
||||||
|
await updateAccountsFromServer(serverAccounts);
|
||||||
|
return serverAccounts;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Gagal mengambil akun dari server: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika semua gagal, kembalikan cache terakhir (meskipun mungkin expired)
|
||||||
|
return await getCachedAccounts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:cashumit/models/local_receipt.dart';
|
||||||
|
import 'package:cashumit/models/receipt_item.dart';
|
||||||
|
import 'package:cashumit/services/firefly_api_service.dart';
|
||||||
|
|
||||||
|
class LocalReceiptService {
|
||||||
|
static const String _receiptsKey = 'local_receipts';
|
||||||
|
|
||||||
|
/// Save a receipt to local storage
|
||||||
|
static Future<void> saveReceipt(LocalReceipt receipt) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final receipts = await getReceipts();
|
||||||
|
|
||||||
|
// Check if receipt with the same ID already exists
|
||||||
|
final existingIndex = receipts.indexWhere((r) => r.id == receipt.id);
|
||||||
|
|
||||||
|
if (existingIndex != -1) {
|
||||||
|
// Replace existing receipt
|
||||||
|
receipts[existingIndex] = receipt;
|
||||||
|
} else {
|
||||||
|
// Add new receipt
|
||||||
|
receipts.add(receipt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert receipts to JSON strings
|
||||||
|
final receiptJsonList =
|
||||||
|
receipts.map((receipt) => json.encode(receipt.toJson())).toList();
|
||||||
|
|
||||||
|
await prefs.setStringList(_receiptsKey, receiptJsonList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all receipts from local storage
|
||||||
|
static Future<List<LocalReceipt>> getReceipts() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final receiptJsonList = prefs.getStringList(_receiptsKey) ?? [];
|
||||||
|
|
||||||
|
return receiptJsonList
|
||||||
|
.map((jsonString) => LocalReceipt.fromJson(json.decode(jsonString)))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get unsubmitted receipts from local storage
|
||||||
|
static Future<List<LocalReceipt>> getUnsubmittedReceipts() async {
|
||||||
|
final allReceipts = await getReceipts();
|
||||||
|
return allReceipts.where((receipt) => !receipt.isSubmitted).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get submitted receipts from local storage
|
||||||
|
static Future<List<LocalReceipt>> getSubmittedReceipts() async {
|
||||||
|
final allReceipts = await getReceipts();
|
||||||
|
return allReceipts.where((receipt) => receipt.isSubmitted).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a receipt's submission status
|
||||||
|
static Future<void> updateReceiptSubmissionStatus(String receiptId,
|
||||||
|
{bool? isSubmitted, String? submissionError}) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final receipts = await getReceipts();
|
||||||
|
|
||||||
|
final receiptIndex = receipts.indexWhere((r) => r.id == receiptId);
|
||||||
|
|
||||||
|
if (receiptIndex != -1) {
|
||||||
|
final receipt = receipts[receiptIndex];
|
||||||
|
receipts[receiptIndex] = receipt.copyWith(
|
||||||
|
isSubmitted: isSubmitted ?? receipt.isSubmitted,
|
||||||
|
submissionError: submissionError ?? receipt.submissionError,
|
||||||
|
submittedAt: isSubmitted == true ? DateTime.now() : receipt.submittedAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert receipts to JSON strings
|
||||||
|
final receiptJsonList =
|
||||||
|
receipts.map((receipt) => json.encode(receipt.toJson())).toList();
|
||||||
|
|
||||||
|
await prefs.setStringList(_receiptsKey, receiptJsonList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a receipt from local storage
|
||||||
|
static Future<void> removeReceipt(String receiptId) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final receipts = await getReceipts();
|
||||||
|
|
||||||
|
final filteredReceipts =
|
||||||
|
receipts.where((receipt) => receipt.id != receiptId).toList();
|
||||||
|
|
||||||
|
// Convert receipts to JSON strings
|
||||||
|
final receiptJsonList = filteredReceipts
|
||||||
|
.map((receipt) => json.encode(receipt.toJson()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
await prefs.setStringList(_receiptsKey, receiptJsonList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all receipts from local storage
|
||||||
|
static Future<void> clearAllReceipts() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_receiptsKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Submit a single receipt to FireFly III
|
||||||
|
static Future<bool> submitReceiptToServer(
|
||||||
|
LocalReceipt receipt,
|
||||||
|
String baseUrl,
|
||||||
|
String accessToken,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
// Check if the receipt is already submitted
|
||||||
|
if (receipt.isSubmitted) {
|
||||||
|
return true; // Already submitted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (receipt.sourceAccountId == null ||
|
||||||
|
receipt.destinationAccountId == null) {
|
||||||
|
await updateReceiptSubmissionStatus(receipt.id,
|
||||||
|
isSubmitted: false, submissionError: 'Missing account information');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit to FireFly III
|
||||||
|
final transactionId = await _submitToFireFly(
|
||||||
|
receipt: receipt,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
accessToken: accessToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (transactionId != null) {
|
||||||
|
// Update receipt status to submitted
|
||||||
|
await updateReceiptSubmissionStatus(receipt.id,
|
||||||
|
isSubmitted: true, submissionError: null);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
await updateReceiptSubmissionStatus(receipt.id,
|
||||||
|
isSubmitted: false, submissionError: 'Failed to submit to server');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await updateReceiptSubmissionStatus(receipt.id,
|
||||||
|
isSubmitted: false, submissionError: e.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Submit all unsubmitted receipts to FireFly III
|
||||||
|
static Future<Map<String, dynamic>> submitAllUnsubmittedReceipts(
|
||||||
|
String baseUrl,
|
||||||
|
String accessToken,
|
||||||
|
) async {
|
||||||
|
final unsubmittedReceipts = await getUnsubmittedReceipts();
|
||||||
|
final results = <String, bool>{};
|
||||||
|
int successCount = 0;
|
||||||
|
int failureCount = 0;
|
||||||
|
|
||||||
|
for (final receipt in unsubmittedReceipts) {
|
||||||
|
try {
|
||||||
|
final success =
|
||||||
|
await submitReceiptToServer(receipt, baseUrl, accessToken);
|
||||||
|
results[receipt.id] = success;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
failureCount++;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
results[receipt.id] = false;
|
||||||
|
failureCount++;
|
||||||
|
// Update the receipt with the error
|
||||||
|
await updateReceiptSubmissionStatus(receipt.id,
|
||||||
|
isSubmitted: false, submissionError: e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'results': results,
|
||||||
|
'successCount': successCount,
|
||||||
|
'failureCount': failureCount,
|
||||||
|
'totalCount': unsubmittedReceipts.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Private method to handle actual submission to FireFly III
|
||||||
|
static Future<String?> _submitToFireFly({
|
||||||
|
required LocalReceipt receipt,
|
||||||
|
required String baseUrl,
|
||||||
|
required String accessToken,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final transactionId = await FireflyApiService.submitDummyTransaction(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
accessToken: accessToken,
|
||||||
|
sourceId: receipt.sourceAccountId!,
|
||||||
|
destinationId: receipt.destinationAccountId!,
|
||||||
|
type: 'deposit', // Assuming deposit for receipts
|
||||||
|
description: receipt.transactionDescription ??
|
||||||
|
_generateTransactionDescription(receipt.items),
|
||||||
|
date:
|
||||||
|
'${receipt.transactionDate.year}-${receipt.transactionDate.month.toString().padLeft(2, '0')}-${receipt.transactionDate.day.toString().padLeft(2, '0')}',
|
||||||
|
amount: receipt.total.toStringAsFixed(2),
|
||||||
|
);
|
||||||
|
|
||||||
|
return transactionId;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error submitting receipt to FireFly III: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate transaction description
|
||||||
|
static 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(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:cashumit/models/firefly_account.dart';
|
import 'package:cashumit/models/firefly_account.dart';
|
||||||
import 'package:cashumit/models/receipt_item.dart';
|
import 'package:cashumit/models/receipt_item.dart';
|
||||||
import 'package:cashumit/services/firefly_api_service.dart';
|
import 'package:cashumit/services/firefly_api_service.dart';
|
||||||
|
import 'package:cashumit/services/account_cache_service.dart';
|
||||||
|
|
||||||
class ReceiptService {
|
class ReceiptService {
|
||||||
/// Memuat kredensial dari shared preferences.
|
/// Memuat kredensial dari shared preferences.
|
||||||
|
|
@ -212,4 +213,29 @@ class ReceiptService {
|
||||||
static double _calculateTotal(List<ReceiptItem> items) {
|
static double _calculateTotal(List<ReceiptItem> items) {
|
||||||
return items.fold(0.0, (sum, item) => sum + item.total);
|
return items.fold(0.0, (sum, item) => sum + item.total);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// Fungsi untuk menyimpan akun ke cache
|
||||||
|
static Future<void> saveAccountsToCache(
|
||||||
|
List<Map<String, dynamic>> accounts) async {
|
||||||
|
await AccountCacheService.saveAccountsLocally(accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fungsi untuk mengambil akun dari cache
|
||||||
|
static Future<List<Map<String, dynamic>>> getCachedAccounts() async {
|
||||||
|
return await AccountCacheService.getCachedAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fungsi untuk memperbarui cache akun dari server
|
||||||
|
static Future<void> updateAccountCache({
|
||||||
|
required String baseUrl,
|
||||||
|
required String accessToken,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final accounts =
|
||||||
|
await loadAccounts(baseUrl: baseUrl, accessToken: accessToken);
|
||||||
|
await AccountCacheService.updateAccountsFromServer(accounts);
|
||||||
|
} catch (e) {
|
||||||
|
print('Gagal memperbarui cache akun: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
||||||
import 'package:cashumit/services/bluetooth_service.dart';
|
import 'package:cashumit/services/bluetooth_service.dart';
|
||||||
|
import 'package:cashumit/screens/local_receipts_screen.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:cashumit/providers/receipt_provider.dart';
|
||||||
|
|
||||||
class ReceiptSpeedDial extends StatelessWidget {
|
class ReceiptSpeedDial extends StatelessWidget {
|
||||||
final BluetoothService bluetoothService;
|
final BluetoothService bluetoothService;
|
||||||
|
|
@ -14,6 +17,7 @@ class ReceiptSpeedDial extends StatelessWidget {
|
||||||
final Future<void> Function() onSendToFirefly;
|
final Future<void> Function() onSendToFirefly;
|
||||||
final VoidCallback onPrintingStart;
|
final VoidCallback onPrintingStart;
|
||||||
final VoidCallback onPrintingEnd;
|
final VoidCallback onPrintingEnd;
|
||||||
|
final VoidCallback onOpenLocalReceipts;
|
||||||
|
|
||||||
const ReceiptSpeedDial({
|
const ReceiptSpeedDial({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -28,6 +32,7 @@ class ReceiptSpeedDial extends StatelessWidget {
|
||||||
required this.onSendToFirefly,
|
required this.onSendToFirefly,
|
||||||
required this.onPrintingStart,
|
required this.onPrintingStart,
|
||||||
required this.onPrintingEnd,
|
required this.onPrintingEnd,
|
||||||
|
required this.onOpenLocalReceipts,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -38,6 +43,12 @@ class ReceiptSpeedDial extends StatelessWidget {
|
||||||
spacing: 3,
|
spacing: 3,
|
||||||
spaceBetweenChildren: 4,
|
spaceBetweenChildren: 4,
|
||||||
children: [
|
children: [
|
||||||
|
SpeedDialChild(
|
||||||
|
child: const Icon(Icons.list_alt),
|
||||||
|
label: 'Lihat Nota Tersimpan',
|
||||||
|
onTap: onOpenLocalReceipts,
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
),
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: const Icon(Icons.send),
|
child: const Icon(Icons.send),
|
||||||
label: 'Kirim ke Firefly',
|
label: 'Kirim ke Firefly',
|
||||||
|
|
@ -56,6 +67,48 @@ class ReceiptSpeedDial extends StatelessWidget {
|
||||||
? Colors.blue
|
? Colors.blue
|
||||||
: Colors.grey,
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
|
SpeedDialChild(
|
||||||
|
child: const Icon(Icons.save),
|
||||||
|
label: 'Simpan untuk Dikirim Nanti',
|
||||||
|
onTap: hasItems && hasSourceAccount && hasDestinationAccount
|
||||||
|
? () async {
|
||||||
|
final receiptProvider = context.read<ReceiptProvider>();
|
||||||
|
try {
|
||||||
|
final receiptId =
|
||||||
|
await receiptProvider.saveTransactionLocally();
|
||||||
|
if (receiptId != null) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Nota berhasil disimpan untuk dikirim nanti'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Gagal menyimpan nota: $e'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content:
|
||||||
|
Text('Pilih akun sumber dan tujuan terlebih dahulu'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
backgroundColor: hasItems && hasSourceAccount && hasDestinationAccount
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.grey,
|
||||||
|
),
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: const Icon(Icons.refresh),
|
child: const Icon(Icons.refresh),
|
||||||
label: 'Muat Ulang Akun',
|
label: 'Muat Ulang Akun',
|
||||||
|
|
@ -146,4 +199,3 @@ class ReceiptSpeedDial extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue