diff --git a/assets/translations/de.json b/assets/translations/de.json index e0b73f70..79364d6f 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -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...", @@ -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", diff --git a/assets/translations/en.json b/assets/translations/en.json index 870a7898..a8b5dfda 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -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...", diff --git a/assets/translations/es.json b/assets/translations/es.json index aa22e2c3..0ac67126 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -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...", @@ -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", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index a4c1b9ee..c6f442f7 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -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...", @@ -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", diff --git a/assets/translations/it.json b/assets/translations/it.json index ed48dd2c..7f48f729 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -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...", @@ -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", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index f984069f..d05c0628 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -171,6 +171,8 @@ "export": "Экспорт", "toPdf": "В PDF", "toExcel": "В Excel", + "exportSaved": "{name} сохранён", + "exportSaveFailed": "Не удалось сохранить файл.", "exportRequiresAccount": "Войдите в аккаунт, чтобы экспортировать. Файлы создаются на сервере.", "exportAwaitingSync": "Сначала синхронизируйте изменения, чтобы они попали в экспорт.", "exportPreparing": "Подготовка экспорта...", diff --git a/lib/core/error/error_handler.dart b/lib/core/error/error_handler.dart index a29894c5..e01017a9 100644 --- a/lib/core/error/error_handler.dart +++ b/lib/core/error/error_handler.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; @@ -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: @@ -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) return data; + try { + return jsonDecode(utf8.decode(data)); + } catch (_) { + return null; + } + } + static void _recordApiError(DioException err) { if (_crashReportingService == null) return; diff --git a/lib/core/sync/sync_dependency_manager.dart b/lib/core/sync/sync_dependency_manager.dart index ae542ef7..abe66700 100644 --- a/lib/core/sync/sync_dependency_manager.dart +++ b/lib/core/sync/sync_dependency_manager.dart @@ -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'; @@ -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: {}, diff --git a/lib/data/datasources/budget/dtos/budget_complete_dto.dart b/lib/data/datasources/budget/dtos/budget_complete_dto.dart index ed46a943..6ef17eae 100644 --- a/lib/data/datasources/budget/dtos/budget_complete_dto.dart +++ b/lib/data/datasources/budget/dtos/budget_complete_dto.dart @@ -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(), }; diff --git a/lib/data/datasources/export/export_remote_datasource.dart b/lib/data/datasources/export/export_remote_datasource.dart index c2f00ef9..af850905 100644 --- a/lib/data/datasources/export/export_remote_datasource.dart +++ b/lib/data/datasources/export/export_remote_datasource.dart @@ -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 { @@ -43,16 +44,18 @@ class ExportRemoteDataSourceImpl implements ExportRemoteDataSource { List walletIds = const [], List categoryIds = const [], }) async { - final response = await dio.get>( - '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>( + '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 ?? []); diff --git a/lib/data/sync/budget_sync_handler.dart b/lib/data/sync/budget_sync_handler.dart index 17c523da..6cee62a7 100644 --- a/lib/data/sync/budget_sync_handler.dart +++ b/lib/data/sync/budget_sync_handler.dart @@ -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 @@ -18,10 +20,12 @@ class BudgetSyncHandler BudgetSyncHandler( this.db, this.remoteDataSource, + this.localDataSource, ); final AppDatabase db; final BudgetRemoteDataSource remoteDataSource; + final BudgetLocalDataSource localDataSource; TableInfo get table => db.budgets; @@ -56,7 +60,26 @@ class BudgetSyncHandler } @override - Future shouldPersistRemote(BudgetCompleteDto entity) async => true; + Future 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> _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> restGetAllRemote({ @@ -85,10 +108,15 @@ class BudgetSyncHandler @override Future 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); } } @@ -151,7 +179,10 @@ class BudgetSyncHandler if (row == null) { throw Exception('Budget not found'); } - return BudgetCompleteDto(budget: row); + return BudgetCompleteDto( + budget: row, + targets: await _resolvedTargets(row.clientId), + ); } @override @@ -159,7 +190,10 @@ class BudgetSyncHandler 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; } diff --git a/lib/di/injection.config.dart b/lib/di/injection.config.dart index 54881fe2..5ea1ab99 100644 --- a/lib/di/injection.config.dart +++ b/lib/di/injection.config.dart @@ -318,8 +318,8 @@ _i174.GetIt $initGetIt( gh.factory<_i624.OAuthService>(() => _i624.OAuthService()); gh.factory<_i1041.SyncCubit>(() => _i1041.SyncCubit()); gh.factory<_i363.StatisticsFilterCubit>(() => _i363.StatisticsFilterCubit()); - gh.singleton<_i196.FeatureRemoteConfig>(() => _i196.FeatureRemoteConfig()); gh.singleton<_i957.SyncService>(() => _i957.SyncService()); + gh.singleton<_i196.FeatureRemoteConfig>(() => _i196.FeatureRemoteConfig()); gh.lazySingleton<_i877.SyncDependencyManagerBase>( () => syncModule.provideSyncDependencyManager()); gh.lazySingleton<_i627.ThemeCubit>(() => _i627.ThemeCubit()); @@ -457,12 +457,12 @@ _i174.GetIt $initGetIt( () => _i738.StatsRemoteDataSourceImpl(dio: gh<_i361.Dio>())); gh.factory<_i961.GetCategoriesUseCase>( () => _i961.GetCategoriesUseCase(gh<_i410.CategoryRepository>())); - gh.factory<_i292.DeleteCategoryUseCase>( - () => _i292.DeleteCategoryUseCase(gh<_i410.CategoryRepository>())); gh.factory<_i986.UpdateCategoryUseCase>( () => _i986.UpdateCategoryUseCase(gh<_i410.CategoryRepository>())); gh.factory<_i445.AddCategoryUseCase>( () => _i445.AddCategoryUseCase(gh<_i410.CategoryRepository>())); + gh.factory<_i292.DeleteCategoryUseCase>( + () => _i292.DeleteCategoryUseCase(gh<_i410.CategoryRepository>())); gh.singleton<_i11.CloudBenefitRepository>(() => _i415.CloudBenefitRepositoryImpl( gh<_i61.CloudBenefitRemoteDataSource>())); @@ -545,20 +545,20 @@ _i174.GetIt $initGetIt( gh<_i961.GetCategoriesUseCase>(), gh<_i500.ListenToCategoriesUseCase>(), )); - gh.factory<_i444.StreamAuthStatus>( - () => _i444.StreamAuthStatus(gh<_i800.AuthRepository>())); - gh.factory<_i880.GetLoggedInUser>( - () => _i880.GetLoggedInUser(gh<_i800.AuthRepository>())); gh.factory<_i723.LoginWithPhonePassword>( () => _i723.LoginWithPhonePassword(gh<_i800.AuthRepository>())); - gh.factory<_i768.LoginWithEmailPassword>( - () => _i768.LoginWithEmailPassword(gh<_i800.AuthRepository>())); - gh.factory<_i42.LoginByEmailUsecase>( - () => _i42.LoginByEmailUsecase(gh<_i800.AuthRepository>())); gh.factory<_i2.OnboardingCompleted>( () => _i2.OnboardingCompleted(gh<_i800.AuthRepository>())); gh.factory<_i498.LoginByPhoneUsecase>( () => _i498.LoginByPhoneUsecase(gh<_i800.AuthRepository>())); + gh.factory<_i880.GetLoggedInUser>( + () => _i880.GetLoggedInUser(gh<_i800.AuthRepository>())); + gh.factory<_i768.LoginWithEmailPassword>( + () => _i768.LoginWithEmailPassword(gh<_i800.AuthRepository>())); + gh.factory<_i42.LoginByEmailUsecase>( + () => _i42.LoginByEmailUsecase(gh<_i800.AuthRepository>())); + gh.factory<_i444.StreamAuthStatus>( + () => _i444.StreamAuthStatus(gh<_i800.AuthRepository>())); gh.factory<_i828.IsOnboardingCompleted>( () => _i828.IsOnboardingCompleted(gh<_i800.AuthRepository>())); gh.lazySingleton<_i161.BudgetPeriodStateSyncHandler>( @@ -566,29 +566,25 @@ _i174.GetIt $initGetIt( gh<_i704.AppDatabase>(), gh<_i760.BudgetRemoteDataSource>(), )); - gh.lazySingleton<_i918.BudgetSyncHandler>(() => _i918.BudgetSyncHandler( - gh<_i704.AppDatabase>(), - gh<_i760.BudgetRemoteDataSource>(), - )); gh.factory<_i26.GetFinancialPositionUseCase>(() => _i26.GetFinancialPositionUseCase( gh<_i581.FinancialPositionRepository>())); + gh.factory<_i929.GetImportSessionsUseCase>( + () => _i929.GetImportSessionsUseCase(gh<_i32.ImportRepository>())); + gh.factory<_i36.ConfirmSessionUseCase>( + () => _i36.ConfirmSessionUseCase(gh<_i32.ImportRepository>())); gh.factory<_i661.GetImportSessionUseCase>( () => _i661.GetImportSessionUseCase(gh<_i32.ImportRepository>())); gh.factory<_i60.AnalyzeDocumentUseCase>( () => _i60.AnalyzeDocumentUseCase(gh<_i32.ImportRepository>())); - gh.factory<_i36.ConfirmSessionUseCase>( - () => _i36.ConfirmSessionUseCase(gh<_i32.ImportRepository>())); - gh.factory<_i929.GetImportSessionsUseCase>( - () => _i929.GetImportSessionsUseCase(gh<_i32.ImportRepository>())); gh.lazySingleton<_i3.ExportRepository>( () => _i739.ExportRepositoryImpl(gh<_i875.ExportRemoteDataSource>())); - gh.factory<_i911.UpdatePartyUseCase>( - () => _i911.UpdatePartyUseCase(gh<_i661.PartyRepository>())); - gh.factory<_i84.AddPartyUseCase>( - () => _i84.AddPartyUseCase(gh<_i661.PartyRepository>())); gh.factory<_i56.DeletePartyUseCase>( () => _i56.DeletePartyUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i84.AddPartyUseCase>( + () => _i84.AddPartyUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i911.UpdatePartyUseCase>( + () => _i911.UpdatePartyUseCase(gh<_i661.PartyRepository>())); gh.factory<_i12.GetPartiesUseCase>( () => _i12.GetPartiesUseCase(gh<_i661.PartyRepository>())); gh.factory<_i714.ListenToPartiesUseCase>( @@ -598,24 +594,24 @@ _i174.GetIt $initGetIt( gh<_i1057.HoldingLocalDataSource>(), gh<_i683.PreferenceManager>(), )); - gh.factory<_i995.CreateSessionUseCase>( - () => _i995.CreateSessionUseCase(gh<_i542.AiRepository>())); - gh.factory<_i391.RejectActionUseCase>( - () => _i391.RejectActionUseCase(gh<_i542.AiRepository>())); - gh.factory<_i752.GetSessionUseCase>( - () => _i752.GetSessionUseCase(gh<_i542.AiRepository>())); - gh.factory<_i605.CheckHealthUseCase>( - () => _i605.CheckHealthUseCase(gh<_i542.AiRepository>())); - gh.factory<_i505.ListSessionsUseCase>( - () => _i505.ListSessionsUseCase(gh<_i542.AiRepository>())); gh.factory<_i82.ConfirmActionUseCase>( () => _i82.ConfirmActionUseCase(gh<_i542.AiRepository>())); gh.factory<_i633.UploadFilesUseCase>( () => _i633.UploadFilesUseCase(gh<_i542.AiRepository>())); + gh.factory<_i995.CreateSessionUseCase>( + () => _i995.CreateSessionUseCase(gh<_i542.AiRepository>())); gh.factory<_i236.DeleteSessionUseCase>( () => _i236.DeleteSessionUseCase(gh<_i542.AiRepository>())); + gh.factory<_i752.GetSessionUseCase>( + () => _i752.GetSessionUseCase(gh<_i542.AiRepository>())); gh.factory<_i308.SendMessageUseCase>( () => _i308.SendMessageUseCase(gh<_i542.AiRepository>())); + gh.factory<_i605.CheckHealthUseCase>( + () => _i605.CheckHealthUseCase(gh<_i542.AiRepository>())); + gh.factory<_i391.RejectActionUseCase>( + () => _i391.RejectActionUseCase(gh<_i542.AiRepository>())); + gh.factory<_i505.ListSessionsUseCase>( + () => _i505.ListSessionsUseCase(gh<_i542.AiRepository>())); gh.factory<_i559.AppUpdateCubit>( () => _i559.AppUpdateCubit(gh<_i150.CheckAppUpdateUseCase>())); gh.factory<_i538.ImportCubit>(() => _i538.ImportCubit( @@ -633,36 +629,36 @@ _i174.GetIt $initGetIt( )); gh.factory<_i88.BenefitsCubit>( () => _i88.BenefitsCubit(gh<_i61.FetchBenefits>())); - gh.factory<_i684.DeleteAccountUseCase>( - () => _i684.DeleteAccountUseCase(gh<_i800.AuthRepository>())); gh.factory<_i640.LogoutUsecase>( () => _i640.LogoutUsecase(gh<_i800.AuthRepository>())); + gh.factory<_i684.DeleteAccountUseCase>( + () => _i684.DeleteAccountUseCase(gh<_i800.AuthRepository>())); gh.lazySingleton<_i368.WalletRepository>(() => _i305.WalletRepositoryImpl( syncHandler: gh<_i849.WalletSyncHandler>(), localDataSource: gh<_i849.WalletLocalDataSource>(), db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); + gh.factory<_i679.UpdateHoldingUseCase>( + () => _i679.UpdateHoldingUseCase(gh<_i645.HoldingRepository>())); + gh.factory<_i301.DeleteHoldingUseCase>( + () => _i301.DeleteHoldingUseCase(gh<_i645.HoldingRepository>())); gh.factory<_i583.RepriceHoldingsUseCase>( () => _i583.RepriceHoldingsUseCase(gh<_i645.HoldingRepository>())); - gh.factory<_i83.GetHoldingsUseCase>( - () => _i83.GetHoldingsUseCase(gh<_i645.HoldingRepository>())); - gh.factory<_i573.WatchHoldingsUseCase>( - () => _i573.WatchHoldingsUseCase(gh<_i645.HoldingRepository>())); gh.factory<_i15.SearchCoinsUseCase>( () => _i15.SearchCoinsUseCase(gh<_i645.HoldingRepository>())); - gh.factory<_i679.UpdateHoldingUseCase>( - () => _i679.UpdateHoldingUseCase(gh<_i645.HoldingRepository>())); gh.factory<_i684.CreateHoldingUseCase>( () => _i684.CreateHoldingUseCase(gh<_i645.HoldingRepository>())); - gh.factory<_i301.DeleteHoldingUseCase>( - () => _i301.DeleteHoldingUseCase(gh<_i645.HoldingRepository>())); + gh.factory<_i83.GetHoldingsUseCase>( + () => _i83.GetHoldingsUseCase(gh<_i645.HoldingRepository>())); + gh.factory<_i573.WatchHoldingsUseCase>( + () => _i573.WatchHoldingsUseCase(gh<_i645.HoldingRepository>())); gh.factory<_i261.FinancialPositionCubit>(() => _i261.FinancialPositionCubit( getFinancialPositionUseCase: gh<_i26.GetFinancialPositionUseCase>())); - gh.factory<_i837.MarkNotificationAsReadUseCase>(() => - _i837.MarkNotificationAsReadUseCase(gh<_i965.NotificationRepository>())); gh.factory<_i422.GetNotificationsUseCase>( () => _i422.GetNotificationsUseCase(gh<_i965.NotificationRepository>())); + gh.factory<_i837.MarkNotificationAsReadUseCase>(() => + _i837.MarkNotificationAsReadUseCase(gh<_i965.NotificationRepository>())); gh.lazySingleton<_i957.GroupRepository>(() => _i875.GroupRepositoryImpl( syncHandler: gh<_i235.GroupSyncHandler>(), localDataSource: gh<_i873.GroupLocalDataSource>(), @@ -678,12 +674,12 @@ _i174.GetIt $initGetIt( repriceHoldingsUseCase: gh<_i583.RepriceHoldingsUseCase>(), searchCoinsUseCase: gh<_i15.SearchCoinsUseCase>(), )); - gh.factory<_i62.DeleteWalletUseCase>( - () => _i62.DeleteWalletUseCase(gh<_i368.WalletRepository>())); - gh.factory<_i418.UpdateWalletUseCase>( - () => _i418.UpdateWalletUseCase(gh<_i368.WalletRepository>())); gh.factory<_i80.AddWalletUseCase>( () => _i80.AddWalletUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i418.UpdateWalletUseCase>( + () => _i418.UpdateWalletUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i62.DeleteWalletUseCase>( + () => _i62.DeleteWalletUseCase(gh<_i368.WalletRepository>())); gh.factory<_i713.GetWalletsUseCase>( () => _i713.GetWalletsUseCase(gh<_i368.WalletRepository>())); gh.factory<_i314.FetchSubscriptionPlans>( @@ -702,16 +698,16 @@ _i174.GetIt $initGetIt( db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.factory<_i436.UpdateConfigUseCase>( - () => _i436.UpdateConfigUseCase(gh<_i899.ConfigRepository>())); + gh.factory<_i132.GetConfigsUseCase>( + () => _i132.GetConfigsUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i608.ListenToConfigsUseCase>( () => _i608.ListenToConfigsUseCase(gh<_i899.ConfigRepository>())); - gh.factory<_i833.SaveConfigUseCase>( - () => _i833.SaveConfigUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i933.GetConfigUseCase>( () => _i933.GetConfigUseCase(gh<_i899.ConfigRepository>())); - gh.factory<_i132.GetConfigsUseCase>( - () => _i132.GetConfigsUseCase(gh<_i899.ConfigRepository>())); + gh.factory<_i833.SaveConfigUseCase>( + () => _i833.SaveConfigUseCase(gh<_i899.ConfigRepository>())); + gh.factory<_i436.UpdateConfigUseCase>( + () => _i436.UpdateConfigUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i536.DeleteConfigUseCase>( () => _i536.DeleteConfigUseCase(gh<_i899.ConfigRepository>())); gh.singleton<_i1057.ExchangeRateRepository>( @@ -720,10 +716,10 @@ _i174.GetIt $initGetIt( localDataSource: gh<_i900.ExchangeRateLocalDataSource>(), configRepository: gh<_i899.ConfigRepository>(), )); - gh.factory<_i82.ListenToWalletsUseCase>( - () => _i82.ListenToWalletsUseCase(gh<_i368.WalletRepository>())); gh.factory<_i225.EnsureDefaultWalletExistsUseCase>(() => _i225.EnsureDefaultWalletExistsUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i82.ListenToWalletsUseCase>( + () => _i82.ListenToWalletsUseCase(gh<_i368.WalletRepository>())); gh.lazySingleton<_i415.AiChatCubit>(() => _i415.AiChatCubit( listSessionsUseCase: gh<_i505.ListSessionsUseCase>(), getSessionUseCase: gh<_i752.GetSessionUseCase>(), @@ -734,20 +730,20 @@ _i174.GetIt $initGetIt( rejectActionUseCase: gh<_i391.RejectActionUseCase>(), uploadFilesUseCase: gh<_i633.UploadFilesUseCase>(), )); + gh.factory<_i524.LoginWithEmailUseCase>( + () => _i524.LoginWithEmailUseCase(gh<_i800.AuthRepository>())); gh.factory<_i705.RegisterUseCase>( () => _i705.RegisterUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i400.LoginWithPhoneUseCase>( - () => _i400.LoginWithPhoneUseCase(gh<_i800.AuthRepository>())); gh.factory<_i402.GetOtpCodeUseCase>( () => _i402.GetOtpCodeUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i400.LoginWithPhoneUseCase>( + () => _i400.LoginWithPhoneUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i542.PasswordResetCodeUseCase>( + () => _i542.PasswordResetCodeUseCase(gh<_i800.AuthRepository>())); gh.factory<_i494.PasswordResetUseCase>( () => _i494.PasswordResetUseCase(gh<_i800.AuthRepository>())); gh.factory<_i100.VerifyEmailUseCase>( () => _i100.VerifyEmailUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i542.PasswordResetCodeUseCase>( - () => _i542.PasswordResetCodeUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i524.LoginWithEmailUseCase>( - () => _i524.LoginWithEmailUseCase(gh<_i800.AuthRepository>())); gh.factory<_i841.PartyCubit>(() => _i841.PartyCubit( getPartiesUseCase: gh<_i12.GetPartiesUseCase>(), addPartyUseCase: gh<_i84.AddPartyUseCase>(), @@ -801,12 +797,12 @@ _i174.GetIt $initGetIt( )); gh.factory<_i397.ListenExchangeRate>( () => _i397.ListenExchangeRate(gh<_i1057.ExchangeRateRepository>())); - gh.factory<_i146.ListenToGroupsUseCase>( - () => _i146.ListenToGroupsUseCase(gh<_i957.GroupRepository>())); - gh.factory<_i982.GetGroupsUseCase>( - () => _i982.GetGroupsUseCase(gh<_i957.GroupRepository>())); gh.factory<_i759.DeleteGroupUseCase>( () => _i759.DeleteGroupUseCase(gh<_i957.GroupRepository>())); + gh.factory<_i982.GetGroupsUseCase>( + () => _i982.GetGroupsUseCase(gh<_i957.GroupRepository>())); + gh.factory<_i146.ListenToGroupsUseCase>( + () => _i146.ListenToGroupsUseCase(gh<_i957.GroupRepository>())); gh.factory<_i353.AddGroupUseCase>( () => _i353.AddGroupUseCase(gh<_i957.GroupRepository>())); gh.factory<_i820.UpdateGroupUseCase>( @@ -816,20 +812,20 @@ _i174.GetIt $initGetIt( gh<_i704.AppDatabase>(), gh<_i1057.ExchangeRateRepository>(), )); - gh.factory<_i781.UpdateReminderUseCase>( - () => _i781.UpdateReminderUseCase(gh<_i868.ReminderRepository>())); - gh.factory<_i656.GetRemindersUseCase>( - () => _i656.GetRemindersUseCase(gh<_i868.ReminderRepository>())); + gh.factory<_i694.ListenToRemindersUseCase>( + () => _i694.ListenToRemindersUseCase(gh<_i868.ReminderRepository>())); gh.factory<_i405.DeleteReminderUseCase>( () => _i405.DeleteReminderUseCase(gh<_i868.ReminderRepository>())); gh.factory<_i913.SnoozeReminderUseCase>( () => _i913.SnoozeReminderUseCase(gh<_i868.ReminderRepository>())); - gh.factory<_i694.ListenToRemindersUseCase>( - () => _i694.ListenToRemindersUseCase(gh<_i868.ReminderRepository>())); gh.factory<_i268.PauseReminderUseCase>( () => _i268.PauseReminderUseCase(gh<_i868.ReminderRepository>())); + gh.factory<_i656.GetRemindersUseCase>( + () => _i656.GetRemindersUseCase(gh<_i868.ReminderRepository>())); gh.factory<_i485.CreateReminderUseCase>( () => _i485.CreateReminderUseCase(gh<_i868.ReminderRepository>())); + gh.factory<_i781.UpdateReminderUseCase>( + () => _i781.UpdateReminderUseCase(gh<_i868.ReminderRepository>())); gh.factory<_i305.ResumeReminderUseCase>( () => _i305.ResumeReminderUseCase(gh<_i868.ReminderRepository>())); gh.factory<_i408.ConfigCubit>(() => _i408.ConfigCubit( @@ -882,14 +878,14 @@ _i174.GetIt $initGetIt( gh<_i704.AppDatabase>(), gh<_i176.BudgetProgressRecomputer>(), )); - gh.factory<_i706.DeleteMediaUseCase>( - () => _i706.DeleteMediaUseCase(gh<_i442.MediaRepository>())); - gh.factory<_i843.AddMediaToTransactionUseCase>( - () => _i843.AddMediaToTransactionUseCase(gh<_i442.MediaRepository>())); gh.factory<_i150.GetFileContentUseCase>( () => _i150.GetFileContentUseCase(gh<_i442.MediaRepository>())); + gh.factory<_i706.DeleteMediaUseCase>( + () => _i706.DeleteMediaUseCase(gh<_i442.MediaRepository>())); gh.factory<_i1026.GetMediaForTransactionUseCase>( () => _i1026.GetMediaForTransactionUseCase(gh<_i442.MediaRepository>())); + gh.factory<_i843.AddMediaToTransactionUseCase>( + () => _i843.AddMediaToTransactionUseCase(gh<_i442.MediaRepository>())); gh.factory<_i484.CurrencyCubit>(() => _i484.CurrencyCubit( gh<_i933.GetConfigUseCase>(), gh<_i833.SaveConfigUseCase>(), @@ -898,13 +894,6 @@ _i174.GetIt $initGetIt( )); gh.factory<_i311.ExchangeRateCubit>( () => _i311.ExchangeRateCubit(gh<_i397.ListenExchangeRate>())); - gh.lazySingleton<_i340.BudgetRepository>(() => _i81.BudgetRepositoryImpl( - syncHandler: gh<_i918.BudgetSyncHandler>(), - localDataSource: gh<_i293.BudgetLocalDataSource>(), - remoteDataSource: gh<_i760.BudgetRemoteDataSource>(), - db: gh<_i704.AppDatabase>(), - requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), - )); gh.lazySingleton<_i118.TransactionRepository>(() => _i114.TransactionRepositoryImpl( syncHandler: gh<_i893.TransactionSyncHandler>(), @@ -913,33 +902,18 @@ _i174.GetIt $initGetIt( db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.factory<_i947.GetAllTransactionsUseCase>( - () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i163.DeleteTransactionUseCase>( + () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i973.ListenToTransactionsUseCase>(() => _i973.ListenToTransactionsUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i322.UnmarkTransactionRefundUseCase>(() => + _i322.UnmarkTransactionRefundUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i241.UpdateTransactionUseCase>( () => _i241.UpdateTransactionUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i468.MarkTransactionRefundUseCase>(() => _i468.MarkTransactionRefundUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i163.DeleteTransactionUseCase>( - () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i322.UnmarkTransactionRefundUseCase>(() => - _i322.UnmarkTransactionRefundUseCase(gh<_i118.TransactionRepository>())); - gh.lazySingleton>>( - () => syncModule.provideSyncTypeHandlers( - gh<_i463.CategorySyncHandler>(), - gh<_i480.ConfigSyncHandler>(), - gh<_i849.WalletSyncHandler>(), - gh<_i280.PartySyncHandler>(), - gh<_i235.GroupSyncHandler>(), - gh<_i217.NotificationSyncHandler>(), - gh<_i893.TransactionSyncHandler>(), - gh<_i225.TransferSyncHandler>(), - gh<_i382.MediaSyncHandler>(), - gh<_i918.BudgetSyncHandler>(), - gh<_i161.BudgetPeriodStateSyncHandler>(), - gh<_i139.ReminderSyncHandler>(), - )); + gh.factory<_i947.GetAllTransactionsUseCase>( + () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i669.CreateTransactionUseCase>( () => _i669.CreateTransactionUseCase( gh<_i118.TransactionRepository>(), @@ -969,24 +943,63 @@ _i174.GetIt $initGetIt( listenToTransactionsUseCase: gh<_i1022.ListenToTransactionsUseCase>(), getWalletsUseCase: gh<_i713.GetWalletsUseCase>(), )); - gh.factory<_i617.UpdateBudgetUseCase>( - () => _i617.UpdateBudgetUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i363.InsertBudgetUseCase>( - () => _i363.InsertBudgetUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i884.GetAllBudgetsUseCase>( - () => _i884.GetAllBudgetsUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i893.CloseBudgetPeriodUseCase>( - () => _i893.CloseBudgetPeriodUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i377.ListenToBudgetsUseCase>( - () => _i377.ListenToBudgetsUseCase(gh<_i340.BudgetRepository>())); + gh.lazySingleton<_i918.BudgetSyncHandler>(() => _i918.BudgetSyncHandler( + gh<_i704.AppDatabase>(), + gh<_i760.BudgetRemoteDataSource>(), + gh<_i293.BudgetLocalDataSource>(), + )); + gh.factory<_i1004.CreateTransferWithTransactionsUseCase>(() => + _i1004.CreateTransferWithTransactionsUseCase( + gh<_i55.TransferRepository>())); + gh.factory<_i453.AddTransferUseCase>( + () => _i453.AddTransferUseCase(gh<_i55.TransferRepository>())); + gh.factory<_i744.ListenToTransfersUseCase>( + () => _i744.ListenToTransfersUseCase(gh<_i55.TransferRepository>())); + gh.lazySingleton<_i340.BudgetRepository>(() => _i81.BudgetRepositoryImpl( + syncHandler: gh<_i918.BudgetSyncHandler>(), + localDataSource: gh<_i293.BudgetLocalDataSource>(), + remoteDataSource: gh<_i760.BudgetRemoteDataSource>(), + db: gh<_i704.AppDatabase>(), + requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), + )); + gh.factory<_i611.TransferCubit>(() => _i611.TransferCubit( + createTransferWithTransactionsUseCase: + gh<_i1004.CreateTransferWithTransactionsUseCase>(), + listenToTransfersUseCase: gh<_i744.ListenToTransfersUseCase>(), + )); + gh.lazySingleton>>( + () => syncModule.provideSyncTypeHandlers( + gh<_i463.CategorySyncHandler>(), + gh<_i480.ConfigSyncHandler>(), + gh<_i849.WalletSyncHandler>(), + gh<_i280.PartySyncHandler>(), + gh<_i235.GroupSyncHandler>(), + gh<_i217.NotificationSyncHandler>(), + gh<_i893.TransactionSyncHandler>(), + gh<_i225.TransferSyncHandler>(), + gh<_i382.MediaSyncHandler>(), + gh<_i918.BudgetSyncHandler>(), + gh<_i161.BudgetPeriodStateSyncHandler>(), + gh<_i139.ReminderSyncHandler>(), + )); gh.factory<_i920.ListenToPeriodStatesUseCase>( () => _i920.ListenToPeriodStatesUseCase(gh<_i340.BudgetRepository>())); gh.factory<_i102.GetBudgetUseCase>( () => _i102.GetBudgetUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i748.DeleteBudgetUseCase>( - () => _i748.DeleteBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i377.ListenToBudgetsUseCase>( + () => _i377.ListenToBudgetsUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i893.CloseBudgetPeriodUseCase>( + () => _i893.CloseBudgetPeriodUseCase(gh<_i340.BudgetRepository>())); gh.factory<_i442.ListenToTargetsUseCase>( () => _i442.ListenToTargetsUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i617.UpdateBudgetUseCase>( + () => _i617.UpdateBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i363.InsertBudgetUseCase>( + () => _i363.InsertBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i748.DeleteBudgetUseCase>( + () => _i748.DeleteBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i884.GetAllBudgetsUseCase>( + () => _i884.GetAllBudgetsUseCase(gh<_i340.BudgetRepository>())); gh.lazySingleton<_i646.SynchAppDatabase>(() => _i646.SynchAppDatabase( appDatabase: gh<_i704.AppDatabase>(), typeHandlers: @@ -996,18 +1009,6 @@ _i174.GetIt $initGetIt( logger: gh<_i877.SyncLogger>(), crashReporter: gh<_i877.SyncCrashReporter>(), )); - gh.factory<_i453.AddTransferUseCase>( - () => _i453.AddTransferUseCase(gh<_i55.TransferRepository>())); - gh.factory<_i744.ListenToTransfersUseCase>( - () => _i744.ListenToTransfersUseCase(gh<_i55.TransferRepository>())); - gh.factory<_i1004.CreateTransferWithTransactionsUseCase>(() => - _i1004.CreateTransferWithTransactionsUseCase( - gh<_i55.TransferRepository>())); - gh.factory<_i611.TransferCubit>(() => _i611.TransferCubit( - createTransferWithTransactionsUseCase: - gh<_i1004.CreateTransferWithTransactionsUseCase>(), - listenToTransfersUseCase: gh<_i744.ListenToTransfersUseCase>(), - )); gh.factory<_i1064.BudgetCubit>(() => _i1064.BudgetCubit( getAllBudgetsUseCase: gh<_i884.GetAllBudgetsUseCase>(), insertBudgetUseCase: gh<_i363.InsertBudgetUseCase>(), diff --git a/lib/gen/translations/codegen_loader.g.dart b/lib/gen/translations/codegen_loader.g.dart index 4621e866..6683e1c3 100644 --- a/lib/gen/translations/codegen_loader.g.dart +++ b/lib/gen/translations/codegen_loader.g.dart @@ -46,15 +46,6 @@ abstract class LocaleKeys { static const parties = 'parties'; static const partyAddParty = 'partyAddParty'; static const partyCreateParty = 'partyCreateParty'; - static const partyEditParty = 'partyEditParty'; - static const partyName = 'partyName'; - static const partyNameRequired = 'partyNameRequired'; - static const partyDescription = 'partyDescription'; - static const partyUpdate = 'partyUpdate'; - static const partyAdd = 'partyAdd'; - static const partyDeleteParty = 'partyDeleteParty'; - static const partyDeletePartyConfirm = 'partyDeletePartyConfirm'; - static const partyNoParties = 'partyNoParties'; static const partyPartyName = 'partyPartyName'; static const partyEnterPartyName = 'partyEnterPartyName'; static const partyPartyDescription = 'partyPartyDescription'; @@ -125,7 +116,6 @@ abstract class LocaleKeys { static const selectLanguage = 'selectLanguage'; static const seeAll = 'seeAll'; static const phoneNumber = 'phoneNumber'; - static const phoneNumberHint = 'phoneNumberHint'; static const payment = 'payment'; static const notifications = 'notifications'; static const langEnglish = 'langEnglish'; @@ -162,6 +152,15 @@ abstract class LocaleKeys { static const selectCurrency = 'selectCurrency'; static const defaultWalletName = 'defaultWalletName'; static const defaultWalletDescription = 'defaultWalletDescription'; + static const partyEditParty = 'partyEditParty'; + static const partyName = 'partyName'; + static const partyNameRequired = 'partyNameRequired'; + static const partyDescription = 'partyDescription'; + static const partyUpdate = 'partyUpdate'; + static const partyAdd = 'partyAdd'; + static const partyDeleteParty = 'partyDeleteParty'; + static const partyDeletePartyConfirm = 'partyDeletePartyConfirm'; + static const partyNoParties = 'partyNoParties'; static const groupUpdate = 'groupUpdate'; static const defaultGroupName = 'defaultGroupName'; static const accountInfo = 'accountInfo'; @@ -175,11 +174,14 @@ abstract class LocaleKeys { static const export = 'export'; static const toPdf = 'toPdf'; static const toExcel = 'toExcel'; + static const exportSaved = 'exportSaved'; + static const exportSaveFailed = 'exportSaveFailed'; static const exportRequiresAccount = 'exportRequiresAccount'; static const exportAwaitingSync = 'exportAwaitingSync'; static const exportPreparing = 'exportPreparing'; static const family = 'family'; static const searchHint = 'searchHint'; + static const totalIncome = 'totalIncome'; static const displaySettings = 'displaySettings'; static const transactionFormDisplayMode = 'transactionFormDisplayMode'; static const themeMode = 'themeMode'; @@ -193,7 +195,6 @@ abstract class LocaleKeys { static const anonymous = 'anonymous'; static const logOut = 'logOut'; static const logoutConfirm = 'logoutConfirm'; - static const alreadyHaveAccount = 'alreadyHaveAccount'; static const dontHaveAccount = 'dontHaveAccount'; static const benefitsAccount = 'benefitsAccount'; static const createAccountNow = 'createAccountNow'; @@ -231,6 +232,10 @@ abstract class LocaleKeys { static const to = 'to'; static const party = 'party'; static const category = 'category'; + static const description = 'description'; + static const attachment = 'attachment'; + static const date = 'date'; + static const time = 'time'; static const editCategory = 'editCategory'; static const addCategory = 'addCategory'; static const noCategoriesFound = 'noCategoriesFound'; @@ -239,14 +244,11 @@ abstract class LocaleKeys { static const addWallet = 'addWallet'; static const name = 'name'; static const nameIsRequired = 'nameIsRequired'; - static const description = 'description'; - static const attachment = 'attachment'; - static const date = 'date'; - static const time = 'time'; static const createSaving = 'createSaving'; static const pleaseSelectCurrency = 'pleaseSelectCurrency'; static const confirm = 'confirm'; static const categories = 'categories'; + static const edit = 'edit'; static const savings = 'savings'; static const addSaving = 'addSaving'; static const snapPicture = 'snapPicture'; @@ -307,6 +309,7 @@ abstract class LocaleKeys { static const selectCategory = 'selectCategory'; static const categoryIsRequired = 'categoryIsRequired'; static const pleaseSelectWallet = 'pleaseSelectWallet'; + static const phoneNumberHint = 'phoneNumberHint'; static const noWalletsYet = 'noWalletsYet'; static const deleteCategory = 'deleteCategory'; static const deleteCategoryConfirm = 'deleteCategoryConfirm'; @@ -320,8 +323,6 @@ abstract class LocaleKeys { static const transactionsIn = 'transactionsIn'; static const wallets = 'wallets'; static const noData = 'noData'; - static const totalIncome = 'totalIncome'; - static const totalExpense = 'totalExpense'; static const thisMonth = 'thisMonth'; static const thisWeek = 'thisWeek'; static const lastThreeMonths = 'lastThreeMonths'; @@ -336,7 +337,6 @@ abstract class LocaleKeys { static const categoryNameAlreadyExists = 'categoryNameAlreadyExists'; static const deleteWallet = 'deleteWallet'; static const deleteWalletConfirm = 'deleteWalletConfirm'; - static const edit = 'edit'; static const defaultName = 'defaultName'; static const officeElements = 'officeElements'; static const officeElementsDesc = 'officeElementsDesc'; @@ -504,6 +504,8 @@ abstract class LocaleKeys { static const createDefaultCategoriesDesc = 'createDefaultCategoriesDesc'; static const skipCategories = 'skipCategories'; static const skipCategoriesDesc = 'skipCategoriesDesc'; + static const alreadyHaveAccount = 'alreadyHaveAccount'; + static const totalExpense = 'totalExpense'; static const advanced = 'advanced'; static const synchronization = 'synchronization'; static const syncHistoryDesc = 'syncHistoryDesc'; @@ -659,6 +661,55 @@ abstract class LocaleKeys { static const partiesDesc = 'partiesDesc'; static const walletsDesc = 'walletsDesc'; static const cancelled = 'cancelled'; + static const imports = 'imports'; + static const importsDesc = 'importsDesc'; + static const importScanDocument = 'importScanDocument'; + static const importScanDocumentDesc = 'importScanDocumentDesc'; + static const importAnalyze = 'importAnalyze'; + static const importAmount = 'importAmount'; + static const importType = 'importType'; + static const importDescription = 'importDescription'; + static const importParty = 'importParty'; + static const importWallet = 'importWallet'; + static const importCategory = 'importCategory'; + static const importDate = 'importDate'; + static const importDocTypeLabel = 'importDocTypeLabel'; + static const importDocBankStatement = 'importDocBankStatement'; + static const importDocReceipt = 'importDocReceipt'; + static const importDocInvoice = 'importDocInvoice'; + static const importDocPayStub = 'importDocPayStub'; + static const importDocUtilityBill = 'importDocUtilityBill'; + static const importReviewSuggestions = 'importReviewSuggestions'; + static const importAnalyzing = 'importAnalyzing'; + static const importExtracting = 'importExtracting'; + static const importEnriching = 'importEnriching'; + static const importCheckingDuplicates = 'importCheckingDuplicates'; + static const importAnalysisFailed = 'importAnalysisFailed'; + static const importAnalysisFailedHint = 'importAnalysisFailedHint'; + static const importNoSuggestions = 'importNoSuggestions'; + static const importNoSuggestionsTitle = 'importNoSuggestionsTitle'; + static const importNoTransactionsFound = 'importNoTransactionsFound'; + static const importNoSuggestionsClose = 'importNoSuggestionsClose'; + static const importNoAcceptedSuggestions = 'importNoAcceptedSuggestions'; + static const importCreatedCount = 'importCreatedCount'; + static const importAutoCreateWallets = 'importAutoCreateWallets'; + static const importAutoCreateParties = 'importAutoCreateParties'; + static const importAutoCreateCategories = 'importAutoCreateCategories'; + static const importConfirm = 'importConfirm'; + static const importRecentActivity = 'importRecentActivity'; + static const importNoActivity = 'importNoActivity'; + static const importSourceLabel = 'importSourceLabel'; + static const importSourceCamera = 'importSourceCamera'; + static const importSourceCameraDesc = 'importSourceCameraDesc'; + static const importSourceGallery = 'importSourceGallery'; + static const importSourceGalleryDesc = 'importSourceGalleryDesc'; + static const importSourceFile = 'importSourceFile'; + static const importSourceFileDesc = 'importSourceFileDesc'; + static const importAiSuggested = 'importAiSuggested'; + static const importConfirmSheetTitle = 'importConfirmSheetTitle'; + static const importCreateTransactions = 'importCreateTransactions'; + static const importAutoCreateMissingHint = 'importAutoCreateMissingHint'; + static const importAutoCreateNoneNeeded = 'importAutoCreateNoneNeeded'; static const budget = 'budget'; static const budgets = 'budgets'; static const addBudget = 'addBudget'; @@ -683,55 +734,6 @@ abstract class LocaleKeys { static const budgetRefunds = 'budgetRefunds'; static const budgetRollover = 'budgetRollover'; static const budgetRolloverIn = 'budgetRolloverIn'; - static const importAiSuggested = 'importAiSuggested'; - static const importAmount = 'importAmount'; - static const importAnalysisFailed = 'importAnalysisFailed'; - static const importAnalysisFailedHint = 'importAnalysisFailedHint'; - static const importAnalyze = 'importAnalyze'; - static const importAnalyzing = 'importAnalyzing'; - static const importAutoCreateCategories = 'importAutoCreateCategories'; - static const importAutoCreateMissingHint = 'importAutoCreateMissingHint'; - static const importAutoCreateNoneNeeded = 'importAutoCreateNoneNeeded'; - static const importAutoCreateParties = 'importAutoCreateParties'; - static const importAutoCreateWallets = 'importAutoCreateWallets'; - static const importCategory = 'importCategory'; - static const importCheckingDuplicates = 'importCheckingDuplicates'; - static const importConfirm = 'importConfirm'; - static const importConfirmSheetTitle = 'importConfirmSheetTitle'; - static const importCreateTransactions = 'importCreateTransactions'; - static const importCreatedCount = 'importCreatedCount'; - static const importDate = 'importDate'; - static const importDescription = 'importDescription'; - static const importDocBankStatement = 'importDocBankStatement'; - static const importDocInvoice = 'importDocInvoice'; - static const importDocPayStub = 'importDocPayStub'; - static const importDocReceipt = 'importDocReceipt'; - static const importDocTypeLabel = 'importDocTypeLabel'; - static const importDocUtilityBill = 'importDocUtilityBill'; - static const importEnriching = 'importEnriching'; - static const importExtracting = 'importExtracting'; - static const importNoAcceptedSuggestions = 'importNoAcceptedSuggestions'; - static const importNoActivity = 'importNoActivity'; - static const importNoSuggestions = 'importNoSuggestions'; - static const importNoSuggestionsClose = 'importNoSuggestionsClose'; - static const importNoSuggestionsTitle = 'importNoSuggestionsTitle'; - static const importNoTransactionsFound = 'importNoTransactionsFound'; - static const importParty = 'importParty'; - static const importRecentActivity = 'importRecentActivity'; - static const importReviewSuggestions = 'importReviewSuggestions'; - static const importScanDocument = 'importScanDocument'; - static const importScanDocumentDesc = 'importScanDocumentDesc'; - static const importSourceCamera = 'importSourceCamera'; - static const importSourceCameraDesc = 'importSourceCameraDesc'; - static const importSourceFile = 'importSourceFile'; - static const importSourceFileDesc = 'importSourceFileDesc'; - static const importSourceGallery = 'importSourceGallery'; - static const importSourceGalleryDesc = 'importSourceGalleryDesc'; - static const importSourceLabel = 'importSourceLabel'; - static const importType = 'importType'; - static const importWallet = 'importWallet'; - static const imports = 'imports'; - static const importsDesc = 'importsDesc'; static const budgetPeriodOpen = 'budgetPeriodOpen'; static const closePeriodMessage = 'closePeriodMessage'; static const closePeriodTitle = 'closePeriodTitle'; diff --git a/lib/presentation/history_screen.dart b/lib/presentation/history_screen.dart index 842aae1e..a732482a 100644 --- a/lib/presentation/history_screen.dart +++ b/lib/presentation/history_screen.dart @@ -1,4 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -111,13 +112,7 @@ class _HistoryScreenState extends State { if (state.file != null) { final file = state.file!; context.read().clearFile(); - Share.shareXFiles([ - XFile.fromData( - file.bytes, - name: file.name, - mimeType: file.mimeType, - ), - ], fileNameOverrides: [file.name]); + _showExportActions(file); return; } @@ -136,6 +131,72 @@ class _HistoryScreenState extends State { } } + /// The file is ready; let the user decide whether it should be kept on the + /// device or handed to another app. + void _showExportActions(ExportedFile file) { + showModalBottomSheet( + context: context, + builder: (sheetContext) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + file.name, + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14.sp), + ), + ), + ListTile( + leading: const Icon(Icons.download_outlined), + title: Text(LocaleKeys.save.tr()), + onTap: () { + Navigator.pop(sheetContext); + _saveFile(file); + }, + ), + ListTile( + leading: const Icon(Icons.share_outlined), + title: Text(LocaleKeys.share.tr()), + onTap: () { + Navigator.pop(sheetContext); + _shareFile(file); + }, + ), + ], + ), + ), + ); + } + + void _shareFile(ExportedFile file) { + Share.shareXFiles([ + XFile.fromData( + file.bytes, + name: file.name, + mimeType: file.mimeType, + ), + ], fileNameOverrides: [file.name]); + } + + /// Writes the export through the system file picker, so the user chooses + /// where it lands and ends up with a copy they can find again. + Future _saveFile(ExportedFile file) async { + try { + final path = await FilePicker.platform.saveFile( + dialogTitle: LocaleKeys.export.tr(), + fileName: file.name, + bytes: file.bytes, + ); + if (path == null) return; + showSnackBar( + message: LocaleKeys.exportSaved.tr(namedArgs: {'name': file.name}), + isSuccess: true, + ); + } catch (_) { + showSnackBar(message: LocaleKeys.exportSaveFailed.tr()); + } + } + /// Server-side exports only cover synced records, so a filter pinned to a /// wallet or category that has not reached the server yet cannot be honoured. void _export(BuildContext context, ExportFormat format) { diff --git a/test/unit/budget_target_sync_test.dart b/test/unit/budget_target_sync_test.dart new file mode 100644 index 00000000..87c9b80d --- /dev/null +++ b/test/unit/budget_target_sync_test.dart @@ -0,0 +1,203 @@ +import 'package:drift/drift.dart' hide isNull, isNotNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:trakli/data/database/app_database.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/services/budget/budget_progress_recomputer.dart'; +import 'package:trakli/data/sync/budget_sync_handler.dart'; +import 'package:trakli/domain/repositories/exchange_rate_repository.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +class _MockBudgetRemote extends Mock implements BudgetRemoteDataSource {} + +class _MockExchangeRateRepository extends Mock + implements ExchangeRateRepository {} + +Budget _budget({int? id}) => Budget( + id: id, + clientId: 'b-1', + name: 'Grow budget', + slug: 'grow-budget', + ownerType: 'user', + amount: 20.0, + currency: 'USD', + periodType: BudgetPeriodType.monthly, + startDate: DateTime.utc(2026, 8, 1), + rolloverEnabled: true, + thresholdPercent: 85, + forecastAlertsEnabled: false, + isActive: true, + createdAt: DateTime.utc(2026, 8, 1, 10), + updatedAt: DateTime.utc(2026, 8, 1, 10), + ); + +void main() { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + + group('BudgetCompleteDto.toServerJson targets', () { + test('an unresolved target goes out as client_generated_id', () { + // The server validates targets.*.client_generated_id; sending the same + // value under 'client_id' failed required_without with a 422. + final json = BudgetCompleteDto( + budget: _budget(), + targets: const [ + BudgetTargetDto( + type: BudgetTargetType.category, + clientId: 'device:cat-1', + ), + ], + ).toServerJson(); + + final target = (json['targets'] as List).single as Map; + expect(target['client_generated_id'], 'device:cat-1'); + expect(target.containsKey('client_id'), isFalse); + expect(target['type'], 'category'); + }); + + test('a resolved target carries its server id', () { + final json = BudgetCompleteDto( + budget: _budget(), + targets: const [ + BudgetTargetDto( + type: BudgetTargetType.category, + id: 501, + clientId: 'device:cat-1', + ), + ], + ).toServerJson(); + + final target = (json['targets'] as List).single as Map; + expect(target['id'], 501); + expect(target['client_generated_id'], 'device:cat-1'); + }); + }); + + group('BudgetSyncHandler target resolution', () { + late AppDatabase db; + late _MockBudgetRemote remote; + late BudgetSyncHandler handler; + + setUpAll(() { + registerFallbackValue(BudgetCompleteDto(budget: _budget())); + }); + + setUp(() async { + db = AppDatabase(NativeDatabase.memory()); + remote = _MockBudgetRemote(); + handler = BudgetSyncHandler( + db, + remote, + BudgetLocalDataSourceImpl( + db, + BudgetProgressRecomputer(db, _MockExchangeRateRepository()), + ), + ); + + await db.budgets.insertOne(BudgetsCompanion.insert( + name: 'Grow budget', + slug: 'grow-budget', + amount: 20.0, + currency: 'USD', + periodType: BudgetPeriodType.monthly, + startDate: DateTime.utc(2026, 8, 1), + clientId: const Value('b-1'), + )); + // Both target categories were created offline, so neither has a + // server id yet — the exact state that produced the 422. + await db.categories.insertOne(CategoriesCompanion.insert( + name: 'offline Income cat', + slug: 'offline-income-cat', + type: TransactionType.income, + clientId: const Value('device:cat-1'), + )); + await db.categories.insertOne(CategoriesCompanion.insert( + name: 'offline expence cat', + slug: 'offline-expence-cat', + type: TransactionType.expense, + clientId: const Value('device:cat-2'), + )); + for (final categoryClientId in ['device:cat-1', 'device:cat-2']) { + await db.budgetTargets.insertOne(BudgetTargetsCompanion.insert( + budgetClientId: 'b-1', + targetType: BudgetTargetType.category, + targetClientId: categoryClientId, + )); + } + }); + + tearDown(() async { + await db.close(); + }); + + Future assignServerId(String clientId, int id) async { + await (db.update(db.categories) + ..where((c) => c.clientId.equals(clientId))) + .write(CategoriesCompanion(id: Value(id))); + } + + test('push is deferred while any target lacks a server id', () async { + final entity = await handler.getLocalByClientId('b-1'); + + expect(await handler.shouldPersistRemote(entity), isFalse); + + await assignServerId('device:cat-1', 501); + expect(await handler.shouldPersistRemote(entity), isFalse); + + await assignServerId('device:cat-2', 502); + expect(await handler.shouldPersistRemote(entity), isTrue); + }); + + test('a stale queued payload is re-resolved before it is pushed', + () async { + await assignServerId('device:cat-1', 501); + await assignServerId('device:cat-2', 502); + + // The queued snapshot was marshaled before the categories synced, so + // its targets still carry null ids. + final queued = BudgetCompleteDto( + budget: _budget(), + targets: const [ + BudgetTargetDto( + type: BudgetTargetType.category, + clientId: 'device:cat-1', + ), + BudgetTargetDto( + type: BudgetTargetType.category, + clientId: 'device:cat-2', + ), + ], + ); + when(() => remote.insertBudget(any())) + .thenAnswer((inv) async => inv.positionalArguments.first + as BudgetCompleteDto); + + await handler.restPutRemote(queued); + + final sent = verify(() => remote.insertBudget(captureAny())) + .captured + .single as BudgetCompleteDto; + expect(sent.targets.map((t) => t.id), [501, 502]); + + final targets = (sent.toServerJson()['targets'] as List) + .cast>(); + expect(targets.map((t) => t['id']), [501, 502]); + }); + + test('local reads carry the budget targets', () async { + await assignServerId('device:cat-1', 501); + await assignServerId('device:cat-2', 502); + + final byClientId = await handler.getLocalByClientId('b-1'); + + expect(byClientId.targets, hasLength(2)); + expect( + byClientId.targets.map((t) => t.clientId), + containsAll(['device:cat-1', 'device:cat-2']), + ); + }); + }); +} diff --git a/test/unit/export_error_message_test.dart b/test/unit/export_error_message_test.dart new file mode 100644 index 00000000..63a3912f --- /dev/null +++ b/test/unit/export_error_message_test.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:trakli/core/error/error_handler.dart'; +import 'package:trakli/core/error/exceptions.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/error/repository_error_handler.dart'; + +/// Builds the failure a download endpoint returns: a JSON body delivered as +/// bytes, because the request asked for [ResponseType.bytes]. +DioException _bytesFailure(int statusCode, Map body) { + final options = RequestOptions( + path: 'transactions/export', + responseType: ResponseType.bytes, + ); + return DioException( + requestOptions: options, + type: DioExceptionType.badResponse, + response: Response>( + requestOptions: options, + statusCode: statusCode, + data: utf8.encode(jsonEncode(body)), + ), + ); +} + +void main() { + const rowLimitMessage = + 'This export covers 318 transactions, over the limit of 100 for PDF ' + 'files. Narrow the date range or wallets, or pick a format that holds ' + 'more.'; + + group('byte-bodied error responses', () { + test('a 422 keeps the server message instead of becoming unknown', () { + final exception = ErrorHandler.handleDioException( + _bytesFailure(422, { + 'success': false, + 'message': rowLimitMessage, + 'errors': {'count': 318, 'max_rows': 100, 'format': 'pdf'}, + }), + ); + + expect(exception, isA()); + expect(exception.message, rowLimitMessage); + }); + + test('the message survives all the way to a displayable failure', () async { + final result = await RepositoryErrorHandler.handleApiCall( + () async => throw ErrorHandler.handleDioException( + _bytesFailure(422, { + 'success': false, + 'message': rowLimitMessage, + 'errors': {'count': 318, 'max_rows': 100}, + }), + ), + ); + + final failure = result.getLeft().toNullable(); + expect(failure, isA()); + expect(failure!.customMessage, rowLimitMessage); + }); + + test('a body that is not JSON falls back rather than throwing', () { + final options = RequestOptions( + path: 'transactions/export', + responseType: ResponseType.bytes, + ); + final exception = ErrorHandler.handleDioException( + DioException( + requestOptions: options, + type: DioExceptionType.badResponse, + response: Response>( + requestOptions: options, + statusCode: 500, + data: const [0xff, 0xfe, 0xfd], + ), + ), + ); + + expect(exception, isA()); + }); + + test('a normal decoded body still works', () { + final options = RequestOptions(path: 'budgets'); + final exception = ErrorHandler.handleDioException( + DioException( + requestOptions: options, + type: DioExceptionType.badResponse, + response: Response>( + requestOptions: options, + statusCode: 422, + data: const {'message': 'The name field is required.'}, + ), + ), + ); + + expect(exception, isA()); + expect(exception.message, 'The name field is required.'); + }); + }); +}