Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion assets/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@
"export": "Exportieren",
"toPdf": "Als PDF",
"toExcel": "Als Excel",
"exportSaved": "{name} gespeichert",
"exportSaveFailed": "Die Datei konnte nicht gespeichert werden.",
"exportRequiresAccount": "Melden Sie sich an, um zu exportieren. Exporte werden auf dem Server erstellt.",
"exportAwaitingSync": "Synchronisieren Sie zuerst, damit der Export Ihre Anderungen enthalt.",
"exportPreparing": "Export wird vorbereitet...",
Expand Down Expand Up @@ -297,7 +299,7 @@
"updateWallet": "Update wallet",
"createWallet": "Create wallet",
"phoneIsRequired": "Phone is required",
"save": "Save",
"save": "Speichern",
"selectCategory": "Select category",
"categoryIsRequired": "Category is required",
"pleaseSelectWallet": "Please select a wallet",
Expand Down
2 changes: 2 additions & 0 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@
"export": "Export",
"toPdf": "To PDF",
"toExcel": "To Excel",
"exportSaved": "Saved {name}",
"exportSaveFailed": "Could not save the file.",
"exportRequiresAccount": "Sign in to export. Exports are generated on the server.",
"exportAwaitingSync": "Sync your changes first so the export includes them.",
"exportPreparing": "Preparing your export...",
Expand Down
4 changes: 3 additions & 1 deletion assets/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@
"export": "Exportar",
"toPdf": "A PDF",
"toExcel": "A Excel",
"exportSaved": "{name} guardado",
"exportSaveFailed": "No se pudo guardar el archivo.",
"exportRequiresAccount": "Inicia sesion para exportar. Las exportaciones se generan en el servidor.",
"exportAwaitingSync": "Sincroniza tus cambios primero para que la exportacion los incluya.",
"exportPreparing": "Preparando la exportacion...",
Expand Down Expand Up @@ -300,7 +302,7 @@
"updateWallet": "Update wallet",
"createWallet": "Create wallet",
"phoneIsRequired": "Phone is required",
"save": "Save",
"save": "Guardar",
"selectCategory": "Select category",
"categoryIsRequired": "Category is required",
"pleaseSelectWallet": "Please select a wallet",
Expand Down
4 changes: 3 additions & 1 deletion assets/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@
"export": "Exporter",
"toPdf": "En PDF",
"toExcel": "En Excel",
"exportSaved": "{name} enregistré",
"exportSaveFailed": "Impossible d'enregistrer le fichier.",
"exportRequiresAccount": "Connectez-vous pour exporter. Les exports sont generes sur le serveur.",
"exportAwaitingSync": "Synchronisez vos modifications pour qu'elles figurent dans l'export.",
"exportPreparing": "Preparation de votre export...",
Expand Down Expand Up @@ -299,7 +301,7 @@
"updateWallet": "Update wallet",
"createWallet": "Create wallet",
"phoneIsRequired": "Phone is required",
"save": "Save",
"save": "Enregistrer",
"selectCategory": "Select category",
"categoryIsRequired": "Category is required",
"pleaseSelectWallet": "Please select a wallet",
Expand Down
4 changes: 3 additions & 1 deletion assets/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@
"export": "Esporta",
"toPdf": "In PDF",
"toExcel": "In Excel",
"exportSaved": "{name} salvato",
"exportSaveFailed": "Impossibile salvare il file.",
"exportRequiresAccount": "Accedi per esportare. Le esportazioni vengono generate sul server.",
"exportAwaitingSync": "Sincronizza prima le modifiche cosi l'esportazione le includera.",
"exportPreparing": "Preparazione dell'esportazione...",
Expand Down Expand Up @@ -298,7 +300,7 @@
"updateWallet": "Update wallet",
"createWallet": "Create wallet",
"phoneIsRequired": "Phone is required",
"save": "Save",
"save": "Salva",
"selectCategory": "Select category",
"categoryIsRequired": "Category is required",
"pleaseSelectWallet": "Please select a wallet",
Expand Down
2 changes: 2 additions & 0 deletions assets/translations/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@
"export": "Экспорт",
"toPdf": "В PDF",
"toExcel": "В Excel",
"exportSaved": "{name} сохранён",
"exportSaveFailed": "Не удалось сохранить файл.",
"exportRequiresAccount": "Войдите в аккаунт, чтобы экспортировать. Файлы создаются на сервере.",
"exportAwaitingSync": "Сначала синхронизируйте изменения, чтобы они попали в экспорт.",
"exportPreparing": "Подготовка экспорта...",
Expand Down
20 changes: 16 additions & 4 deletions lib/core/error/error_handler.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';

import 'package:dio/dio.dart';
Expand Down Expand Up @@ -37,11 +38,10 @@ class ErrorHandler {
}

final statusCode = err.response?.statusCode;
final responseDataForMessage = err.response?.data;
final message = responseDataForMessage is Map
? (responseDataForMessage['message'] as String?) ?? 'Unknown error'
final data = _decodeBody(err.response?.data);
final message = data is Map
? (data['message'] as String?) ?? 'Unknown error'
: 'Unknown error';
final data = err.response?.data;

switch (statusCode) {
case 400:
Expand Down Expand Up @@ -75,6 +75,18 @@ class ErrorHandler {
}
}

/// A failed file download still carries a JSON body, but it arrives as bytes
/// because the request asked for bytes. Decode it so the server's message is
/// not lost behind a generic error.
static dynamic _decodeBody(dynamic data) {
if (data is! List<int>) return data;
try {
return jsonDecode(utf8.decode(data));
} catch (_) {
return null;
}
}

static void _recordApiError(DioException err) {
if (_crashReportingService == null) return;

Expand Down
6 changes: 6 additions & 0 deletions lib/core/sync/sync_dependency_manager.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:drift_sync_core/drift_sync_core.dart';
import 'package:trakli/data/sync/budget_sync_handler.dart';
import 'package:trakli/data/sync/category_sync_handler.dart';
import 'package:trakli/data/sync/group_sync_handler.dart';
import 'package:trakli/data/sync/notification_sync_handler.dart';
Expand All @@ -24,6 +25,11 @@ class SyncDependencyManager extends DefaultSyncDependencyManager {
PartySyncHandler.entity,
GroupSyncHandler.entity,
},
BudgetSyncHandler.entity: {
CategorySyncHandler.entity,
WalletSyncHandler.entity,
GroupSyncHandler.entity,
},
CategorySyncHandler.entity: {},
WalletSyncHandler.entity: {},
PartySyncHandler.entity: {},
Expand Down
2 changes: 1 addition & 1 deletion lib/data/datasources/budget/dtos/budget_complete_dto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class BudgetCompleteDto {
'type': t.type.serverKey,
if (t.id != null) 'id': t.id,
if (t.clientId != null && t.clientId!.isNotEmpty)
'client_id': t.clientId,
'client_generated_id': t.clientId,
})
.toList(),
};
Expand Down
23 changes: 13 additions & 10 deletions lib/data/datasources/export/export_remote_datasource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import 'package:intl/intl.dart';
import 'package:trakli/core/error/error_handler.dart';

/// The file formats the export endpoints can produce.
enum ExportFormat {
Expand Down Expand Up @@ -43,16 +44,18 @@ class ExportRemoteDataSourceImpl implements ExportRemoteDataSource {
List<int> walletIds = const [],
List<int> categoryIds = const [],
}) async {
final response = await dio.get<List<int>>(
'transactions/export',
queryParameters: {
'format': format.key,
if (from != null) 'date_from': _ymd(from),
if (to != null) 'date_to': _ymd(to),
if (walletIds.isNotEmpty) 'wallet_ids': walletIds.join(','),
if (categoryIds.isNotEmpty) 'category_ids': categoryIds.join(','),
},
options: Options(responseType: ResponseType.bytes),
final response = await ErrorHandler.handleApiCall(
() => dio.get<List<int>>(
'transactions/export',
queryParameters: {
'format': format.key,
if (from != null) 'date_from': _ymd(from),
if (to != null) 'date_to': _ymd(to),
if (walletIds.isNotEmpty) 'wallet_ids': walletIds.join(','),
if (categoryIds.isNotEmpty) 'category_ids': categoryIds.join(','),
},
options: Options(responseType: ResponseType.bytes),
),
);

return Uint8List.fromList(response.data ?? []);
Expand Down
46 changes: 40 additions & 6 deletions lib/data/sync/budget_sync_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import 'package:trakli/core/utils/id_helper.dart';
import 'package:trakli/data/database/app_database.dart';
import 'package:trakli/data/database/tables/budgets.dart';
import 'package:trakli/data/database/tables/sync_table.dart';
import 'package:trakli/data/datasources/budget/budget_local_datasource.dart';
import 'package:trakli/data/datasources/budget/budget_remote_datasource.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_complete_dto.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_target_dto.dart';
import 'package:trakli/data/mappers/budget_mapper.dart';

@lazySingleton
Expand All @@ -18,10 +20,12 @@ class BudgetSyncHandler
BudgetSyncHandler(
this.db,
this.remoteDataSource,
this.localDataSource,
);

final AppDatabase db;
final BudgetRemoteDataSource remoteDataSource;
final BudgetLocalDataSource localDataSource;

TableInfo<Budgets, Budget> get table => db.budgets;

Expand Down Expand Up @@ -56,7 +60,26 @@ class BudgetSyncHandler
}

@override
Future<bool> shouldPersistRemote(BudgetCompleteDto entity) async => true;
Future<bool> shouldPersistRemote(BudgetCompleteDto entity) async {
final targets = await _resolvedTargets(entity.budget.clientId);
return targets.every((t) => t.id != null);
}

/// The queued payload is a snapshot taken when the budget was saved, so a
/// target created offline still carries a null id there even after its own
/// entity has synced. Re-read from local before pushing.
Future<List<BudgetTargetDto>> _resolvedTargets(String budgetClientId) async {
final resolved =
await localDataSource.getResolvedTargetsForBudget(budgetClientId);
return resolved
.map((r) => BudgetTargetDto(
type: r.type,
id: r.id,
clientId: r.clientId,
name: r.name,
))
.toList();
}

@override
Future<List<BudgetCompleteDto>> restGetAllRemote({
Expand Down Expand Up @@ -85,10 +108,15 @@ class BudgetSyncHandler

@override
Future<BudgetCompleteDto> restPutRemote(BudgetCompleteDto entity) async {
if (entity.budget.id == null) {
return remoteDataSource.insertBudget(entity);
final payload = BudgetCompleteDto(
budget: entity.budget,
targets: await _resolvedTargets(entity.budget.clientId),
progress: entity.progress,
);
if (payload.budget.id == null) {
return remoteDataSource.insertBudget(payload);
} else {
return remoteDataSource.updateBudget(entity);
return remoteDataSource.updateBudget(payload);
}
}

Expand Down Expand Up @@ -151,15 +179,21 @@ class BudgetSyncHandler
if (row == null) {
throw Exception('Budget not found');
}
return BudgetCompleteDto(budget: row);
return BudgetCompleteDto(
budget: row,
targets: await _resolvedTargets(row.clientId),
);
}

@override
Future<BudgetCompleteDto?> getLocalByServerId(int serverId) async {
try {
final row = await (db.select(table)..where((t) => t.id.equals(serverId)))
.getSingle();
return BudgetCompleteDto(budget: row);
return BudgetCompleteDto(
budget: row,
targets: await _resolvedTargets(row.clientId),
);
} catch (_) {
return null;
}
Expand Down
Loading
Loading