420 lines
14 KiB
Dart
420 lines
14 KiB
Dart
// lib/services/firefly_api_service.dart
|
|
|
|
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import '../models/firefly_account.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../models/report_transaction.dart';
|
|
|
|
class FireflyApiService {
|
|
/// Mengambil daftar akun dari Firefly III API.
|
|
///
|
|
/// [baseUrl] adalah URL dasar instance Firefly III (e.g., https://firefly.yourdomain.com).
|
|
/// [accessToken] adalah Personal Access Token pengguna.
|
|
/// [type] adalah filter opsional untuk tipe akun (e.g., 'asset', 'revenue').
|
|
static Future<List<FireflyAccount>> fetchAccounts({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
String? type,
|
|
}) async {
|
|
final uri = type != null
|
|
? Uri.parse('$baseUrl/api/v1/accounts?type=$type')
|
|
: Uri.parse('$baseUrl/api/v1/accounts');
|
|
|
|
final response = await http.get(
|
|
uri,
|
|
headers: {
|
|
'Authorization': 'Bearer $accessToken',
|
|
'Accept': 'application/json',
|
|
},
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final dynamic responseBody = json.decode(response.body);
|
|
|
|
if (responseBody is Map<String, dynamic> &&
|
|
responseBody.containsKey('data')) {
|
|
final List accountsJson = responseBody['data'] as List;
|
|
|
|
final List<FireflyAccount> accounts = accountsJson
|
|
.map((accountJson) => FireflyAccount.fromJson(accountJson))
|
|
.toList();
|
|
|
|
return accounts;
|
|
} else {
|
|
throw Exception('Format respons API tidak sesuai');
|
|
}
|
|
} else {
|
|
// Handle error - misalnya, lempar exception
|
|
throw Exception('Gagal mengambil akun: ${response.statusCode}');
|
|
}
|
|
}
|
|
|
|
/// Mengambil daftar tag dari Firefly III API.
|
|
static Future<List<String>> fetchTags({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/api/v1/tags');
|
|
|
|
final response = await http.get(
|
|
uri,
|
|
headers: {
|
|
'Authorization': 'Bearer $accessToken',
|
|
'Accept': 'application/json',
|
|
},
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final dynamic responseBody = json.decode(response.body);
|
|
|
|
if (responseBody is Map<String, dynamic> &&
|
|
responseBody.containsKey('data')) {
|
|
final List tagsJson = responseBody['data'] as List;
|
|
|
|
final List<String> tags = tagsJson
|
|
.map((tagObj) {
|
|
if (tagObj is Map<String, dynamic> &&
|
|
tagObj.containsKey('attributes')) {
|
|
final attributes = tagObj['attributes'] as Map<String, dynamic>;
|
|
return attributes['tag']?.toString();
|
|
}
|
|
return null;
|
|
})
|
|
.whereType<String>()
|
|
.where((tag) => tag.isNotEmpty)
|
|
.toList();
|
|
|
|
return tags;
|
|
} else {
|
|
throw Exception('Format respons API tidak sesuai');
|
|
}
|
|
} else {
|
|
throw Exception('Gagal mengambil tag: ${response.statusCode}');
|
|
}
|
|
}
|
|
|
|
/// Menguji koneksi ke server Firefly III
|
|
static Future<bool> testServerConnection({required String baseUrl}) async {
|
|
try {
|
|
final response = await http.get(Uri.parse(baseUrl));
|
|
return response.statusCode == 200;
|
|
} catch (e) {
|
|
print('Gagal menguji koneksi: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Mengirim transaksi dummy ke Firefly III API.
|
|
///
|
|
/// [baseUrl] adalah URL dasar instance Firefly III.
|
|
/// [accessToken] adalah Personal Access Token pengguna.
|
|
/// [sourceId] dan [destinationId] adalah ID akun yang valid.
|
|
/// [type] adalah tipe transaksi (default 'deposit').
|
|
static Future<String?> submitDummyTransaction({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
required String sourceId,
|
|
required String destinationId,
|
|
String type = 'deposit',
|
|
String description = 'Pengeluaran Dummy via Flutter App',
|
|
String date = '2025-08-19', // Gunakan format ISO 8601
|
|
String amount = '50.00',
|
|
List<String>? tags,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/api/v1/transactions');
|
|
final payload = jsonEncode({
|
|
"transactions": [
|
|
{
|
|
"type": type,
|
|
"date": date,
|
|
"amount": amount,
|
|
"description": description,
|
|
"source_id": sourceId,
|
|
"destination_id": destinationId,
|
|
if (tags != null && tags.isNotEmpty) "tags": tags,
|
|
}
|
|
]
|
|
});
|
|
|
|
try {
|
|
final response = await http.post(
|
|
uri,
|
|
headers: {
|
|
'Authorization': 'Bearer $accessToken',
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: payload,
|
|
);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
// Parse response to get transaction ID
|
|
try {
|
|
final dynamic responseBody = json.decode(response.body);
|
|
|
|
if (responseBody is Map<String, dynamic> &&
|
|
responseBody.containsKey('data')) {
|
|
final dynamic data = responseBody['data'];
|
|
|
|
// Handle case where data is a single object with an 'id' field
|
|
if (data is Map<String, dynamic> && data.containsKey('id')) {
|
|
final transactionId = data['id'].toString();
|
|
return transactionId;
|
|
}
|
|
|
|
// Handle case where data is a list with at least one object
|
|
if (data is List && data.isNotEmpty) {
|
|
final firstItem = data[0];
|
|
if (firstItem is Map<String, dynamic>) {
|
|
if (firstItem.containsKey('id')) {
|
|
final transactionId = firstItem['id'].toString();
|
|
return transactionId;
|
|
} else if (firstItem.containsKey('transaction_id')) {
|
|
final transactionId = firstItem['transaction_id'].toString();
|
|
return transactionId;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Don't crash if parsing fails, just return success
|
|
}
|
|
|
|
// If we can't parse the ID, return a default success indicator
|
|
return "success";
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Mengunggah lampiran langsung ke transaksi di Firefly III.
|
|
///
|
|
/// [baseUrl] adalah URL dasar instance Firefly III.
|
|
/// [accessToken] adalah Personal Access Token pengguna.
|
|
/// [transactionId] adalah ID transaksi yang akan dilampiri.
|
|
/// [fileBytes] adalah byte data dari file lampiran.
|
|
static Future<bool> uploadAttachment({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
required String transactionId,
|
|
required List<int> fileBytes,
|
|
}) async {
|
|
final uri = Uri.parse('$baseUrl/api/v1/attachments/$transactionId/upload');
|
|
|
|
try {
|
|
// Kirim file sebagai raw binary data dengan Content-Type: application/octet-stream
|
|
final response = await http.post(
|
|
uri,
|
|
headers: {
|
|
'Authorization': 'Bearer $accessToken',
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/octet-stream',
|
|
},
|
|
body: fileBytes, // Kirim byte array langsung sebagai body
|
|
);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 204) {
|
|
return true;
|
|
} else {
|
|
print('Upload attachment failed with status: ${response.statusCode}');
|
|
print('Response body: ${response.body}');
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
print('Upload attachment failed with exception: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Menguji koneksi ke Firefly III.
|
|
static Future<bool> testConnection({required String baseUrl}) async {
|
|
try {
|
|
final response = await http.get(Uri.parse(baseUrl));
|
|
return response.statusCode == 200;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Menguji autentikasi dengan token.
|
|
static Future<bool> testAuthentication(
|
|
{required String baseUrl, required String accessToken}) async {
|
|
try {
|
|
final response = await http.get(
|
|
Uri.parse('$baseUrl/api/v1/about/user'),
|
|
headers: {
|
|
'Authorization': 'Bearer $accessToken',
|
|
'Accept': 'application/json',
|
|
},
|
|
);
|
|
return response.statusCode == 200;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Mengambil daftar transaksi akun untuk periode tertentu (keperluan Laporan).
|
|
static Future<List<ReportTransaction>> fetchAccountTransactions({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
required String accountId,
|
|
required DateTime startDate,
|
|
required DateTime endDate,
|
|
}) async {
|
|
final startStr = DateFormat('yyyy-MM-dd').format(startDate);
|
|
final endStr = DateFormat('yyyy-MM-dd').format(endDate);
|
|
final uri = Uri.parse(
|
|
'$baseUrl/api/v1/accounts/$accountId/transactions?start=$startStr&end=$endStr&type=all');
|
|
|
|
final response = await http.get(
|
|
uri,
|
|
headers: {
|
|
'Authorization': 'Bearer $accessToken',
|
|
'Accept': 'application/json',
|
|
},
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final dynamic body = json.decode(response.body);
|
|
if (body is Map<String, dynamic> && body.containsKey('data')) {
|
|
final List dataList = body['data'] as List;
|
|
final List<ReportTransaction> result = [];
|
|
|
|
for (final item in dataList) {
|
|
if (item is Map<String, dynamic> && item.containsKey('attributes')) {
|
|
final attrs = item['attributes'] as Map<String, dynamic>;
|
|
final String groupId = (item['id'] ?? '').toString();
|
|
final txList = attrs['transactions'];
|
|
if (txList is List) {
|
|
for (final tx in txList) {
|
|
if (tx is Map<String, dynamic>) {
|
|
final String journalId =
|
|
(tx['transaction_journal_id'] ?? '').toString();
|
|
final String txId =
|
|
journalId.isNotEmpty ? journalId : groupId;
|
|
final DateTime date =
|
|
DateTime.tryParse(tx['date']?.toString() ?? '') ?? startDate;
|
|
final String desc = tx['description']?.toString() ?? '-';
|
|
final double amt =
|
|
double.tryParse(tx['amount']?.toString() ?? '0') ?? 0.0;
|
|
final String sourceId = tx['source_id']?.toString() ?? '';
|
|
final String destId = tx['destination_id']?.toString() ?? '';
|
|
final String sourceName =
|
|
tx['source_name']?.toString() ?? 'Kas/Sumber';
|
|
final String destName =
|
|
tx['destination_name']?.toString() ?? 'Tujuan';
|
|
final String txType =
|
|
tx['type']?.toString().toLowerCase() ?? '';
|
|
|
|
List<String> tags = [];
|
|
if (tx['tags'] is List) {
|
|
tags = (tx['tags'] as List).map((e) => e.toString()).toList();
|
|
}
|
|
|
|
String direction;
|
|
String counterparty;
|
|
|
|
if (sourceId == accountId) {
|
|
direction = 'pengeluaran';
|
|
counterparty = destName;
|
|
} else if (destId == accountId) {
|
|
direction = 'pemasukan';
|
|
counterparty = sourceName;
|
|
} else if (txType == 'withdrawal') {
|
|
direction = 'pengeluaran';
|
|
counterparty = destName;
|
|
} else if (txType == 'deposit') {
|
|
direction = 'pemasukan';
|
|
counterparty = sourceName;
|
|
} else {
|
|
direction = 'pengeluaran';
|
|
counterparty = destName;
|
|
}
|
|
|
|
result.add(ReportTransaction(
|
|
id: txId,
|
|
date: date,
|
|
description: desc,
|
|
amount: amt.abs(),
|
|
direction: direction,
|
|
counterpartyName: counterparty,
|
|
tags: tags,
|
|
transactionGroupId: groupId,
|
|
transactionJournalId: journalId.isNotEmpty ? journalId : null,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Urutkan berdasarkan tanggal ascending
|
|
result.sort((a, b) => a.date.compareTo(b.date));
|
|
return result;
|
|
}
|
|
return [];
|
|
} else {
|
|
throw Exception('Gagal mengambil transaksi: ${response.statusCode}');
|
|
}
|
|
}
|
|
|
|
/// Memperbarui transaksi di Firefly III melalui endpoint PUT /api/v1/transactions/{id}.
|
|
/// Mengembalikan null jika sukses, atau String pesan error jika gagal.
|
|
static Future<String?> updateTransaction({
|
|
required String baseUrl,
|
|
required String accessToken,
|
|
required String transactionGroupId,
|
|
String? transactionJournalId,
|
|
required DateTime date,
|
|
required String description,
|
|
required double amount,
|
|
required List<String> tags,
|
|
}) async {
|
|
try {
|
|
final uri = Uri.parse('$baseUrl/api/v1/transactions/$transactionGroupId');
|
|
final txData = <String, dynamic>{
|
|
if (transactionJournalId != null && transactionJournalId.isNotEmpty)
|
|
'transaction_journal_id': transactionJournalId,
|
|
'date': date.toIso8601String(),
|
|
'description': description,
|
|
'amount': amount.toStringAsFixed(2),
|
|
'tags': tags,
|
|
};
|
|
|
|
final payload = jsonEncode({
|
|
'apply_rules': false,
|
|
'fire_webhooks': false,
|
|
'transactions': [txData],
|
|
});
|
|
|
|
final response = await http.put(
|
|
uri,
|
|
headers: {
|
|
'Authorization': 'Bearer $accessToken',
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: payload,
|
|
);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 204) {
|
|
return null;
|
|
} else {
|
|
try {
|
|
final errorBody = json.decode(response.body);
|
|
if (errorBody is Map<String, dynamic> && errorBody.containsKey('message')) {
|
|
return errorBody['message'].toString();
|
|
}
|
|
} catch (_) {}
|
|
return 'Gagal memperbarui transaksi di Firefly III (Kode HTTP ${response.statusCode})';
|
|
}
|
|
} catch (e) {
|
|
return 'Koneksi error: $e';
|
|
}
|
|
}
|
|
}
|