diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..4c8d160e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,259 @@ +# AGENTS.md + +Guidance for AI coding assistants (and humans pairing with them) working in the **Trakli** Flutter app. The goal of this file is simple: **let you implement features quickly without breaking the parts of the codebase you didn't touch.** + +Read this before generating code. When in doubt, copy the pattern of the nearest existing feature rather than inventing a new one. + +> This file applies to the whole repo. Two companion docs go deeper: +> - [doc/bloc_cubit_patterns.md](doc/bloc_cubit_patterns.md) — state management patterns (read before touching any Cubit). +> - [doc/drift_sync_crash_reporting.md](doc/drift_sync_crash_reporting.md) — sync error reporting. +> - [CONTRIBUTING.md](CONTRIBUTING.md) — environment setup, commit/style rules. + +--- + +## 1. What this project is + +Trakli is an **offline-first** personal finance tracker (income/expenses, wallets, budgets, transfers, groups, categories, parties). It is a Flutter app built with **Clean Architecture** and a **local-first sync** model: the UI always reads/writes the local Drift (SQLite) database, and changes are synced to the server in the background via `drift_sync_core`. + +- **Flutter:** pinned to `3.38.9` via FVM (see [.fvmrc](.fvmrc)). Use `fvm flutter ...` if FVM is installed. +- **Dart SDK:** `>=3.4.3 <4.0.0`. + +--- + +## 2. Architecture — the layering rule (most important section) + +Code lives in three layers under `lib/`. **Dependencies point inward only.** Breaking this is the #1 way AI-generated code corrupts the codebase. + +``` +presentation/ ──depends on──> domain/ <──implemented by── data/ + (UI + Cubits) (pure contracts) (Drift, Dio, sync) + │ │ + └──────────────── never imports data/ directly ───────────┘ +``` + +### `lib/domain/` — pure business contracts (no Flutter, no Drift, no Dio) +- `entities/` — immutable `@freezed` domain models (e.g. `CategoryEntity`). UI and use cases speak in entities. +- `repositories/` — **abstract** repository interfaces returning `Future>` / `Stream>`. +- `usecases/` — one class per operation, implementing `UseCase` or `StreamUseCase` (see `lib/core/usecases/usecase.dart`). Use cases hold no logic beyond delegating to a repository. + +### `lib/data/` — implementation details +- `database/tables/` — Drift table definitions. `database/app_database.dart` — the database + generated `app_database.g.dart`. +- `datasources//` — `*_local_datasource.dart` (Drift queries) and `*_remote_datasource.dart` (Dio calls), plus `dtos/` for wire models. +- `repositories/*_impl.dart` — implement the domain repository interface; orchestrate local datasource + sync. +- `mappers/` — convert Drift row types ↔ domain entities (`CategoryMapper.toDomain`). +- `sync/*_sync_handler.dart` — `SyncTypeHandler` subclasses that drive `drift_sync_core`. + +### `lib/presentation//` +- `cubit/` — `Cubit` + `@freezed` state. Cubits depend **only on use cases**, never on repositories or datasources directly. +- screens + `widgets/`. + +### `lib/core/` — cross-cutting, framework-agnostic helpers +Errors (`error/`), DI base types, network, sync wiring, constants, extensions, utils. Anything shared across features. + +**Rules of thumb:** +- A Cubit imports use cases. It must **not** import anything from `lib/data/`. +- A use case imports a domain repository interface. It must **not** import an `*_impl` or a datasource. +- Drift row classes (e.g. `db.Category`) **stay in the data layer.** Convert to entities with a mapper before returning to domain. +- If you need a new operation, add it to the repository **interface** first, then implement it. + +--- + +## 3. The vertical-slice recipe (adding/extending a feature) + +To add a feature end-to-end, follow the existing **Category** slice as the canonical template. Touch files in this order: + +1. **Entity** — `lib/domain/entities/_entity.dart` (`@freezed`). +2. **Repository interface** — `lib/domain/repositories/_repository.dart` (returns `Either`). +3. **Use case(s)** — `lib/domain/usecases//__usecase.dart`, `@injectable`, one per operation, with a `...Params` class. +4. **Drift table** (if persisted) — `lib/data/database/tables/.dart` using `with SyncTable`; register it in `app_database.dart`. +5. **Local datasource** — `lib/data/datasources//_local_datasource.dart` (abstract + `@Injectable(as: ...)` impl). +6. **Remote datasource + DTO** (if synced) — `..._remote_datasource.dart`, `dtos/_dto.dart`. +7. **Sync handler** (if synced) — `lib/data/sync/_sync_handler.dart` extending `SyncTypeHandler`. +8. **Mapper** — `lib/data/mappers/_mapper.dart`. +9. **Repository impl** — `lib/data/repositories/_repository_impl.dart`, `@LazySingleton(as: Repository)`. +10. **Cubit + state** — `lib/presentation//cubit/`, `@injectable`. +11. **Wire the Cubit into the tree** — add a `BlocProvider(create: (_) => getIt())` in `lib/presentation/app_widget.dart` (or the feature's local provider) as appropriate. +12. **Run codegen** (Section 5) and **`flutter analyze`**. + +> **Adding a field to an existing feature** (vs. a whole new slice)? Trace the same chain, and don't forget the easy-to-miss edit points: the use-case **`...Params` classes** (e.g. `AddCategoryUseCaseParams`), the **mapper**, and **every** method of the sync handler (see §8). A field added to the entity/table but missed in any link is silently dropped at that boundary. + +> Reuse existing UI helpers instead of re-implementing them: `showSnackBar`, `showDeleteConfirmationDialog`, `showConfirmationDialog` (`lib/presentation/utils/helpers.dart`, `dialogs.dart`), and `AppNavigator` (`lib/presentation/utils/app_navigator.dart`). + +--- + +## 4. State management — Cubit + freezed (read doc/bloc_cubit_patterns.md) + +The app uses **flutter_bloc Cubits** (no events) with **freezed immutable state**. Non-negotiable rules: + +- State is a `@freezed` class with a `.initial()` factory and a `required Failure failure` field. +- **Never mutate state** — always `emit(state.copyWith(...))`. +- **Reset the failure at the start of every operation:** `emit(state.copyWith(isLoading: true, failure: const Failure.none()))`. +- Per-operation loading flags: `isLoading`, `isSaving`, `isDeleting`, etc. +- Fold the `Either` result; **store failures in state, never throw out of a Cubit.** +- **Cancel stream subscriptions** in `close()` — forgetting this is a memory leak. +- Side effects (navigation, snackbars) go in `BlocListener` with `listenWhen`; reactive UI goes in `BlocBuilder` with `buildWhen`. + +See the doc for the full Add/Update/Delete/listen patterns and anti-patterns. + +--- + +## 5. Code generation — DO NOT hand-edit generated files + +Large parts of the codebase are generated. Editing generated files by hand will be overwritten and will break builds. + +**Never edit files ending in:** `.g.dart`, `.freezed.dart`, `injection.config.dart`, `assets.gen.dart`, `codegen_loader.g.dart`, `locale_keys.g.dart`, `app_database.g.dart`. + +Instead, edit the **source** annotation file and regenerate. (Flutter/Dart are **fvm-pinned** here — see §1. If you run tools through FVM, prefix every command below with `fvm`, e.g. `fvm dart run build_runner ...`, `fvm flutter pub run ...`. The bare forms work only if the pinned SDK is your active `dart`/`flutter`.) + +```bash +# freezed / json_serializable / injectable / drift (run after changing any annotated source) +dart run build_runner build --delete-conflicting-outputs + +# assets (after adding/removing an image or SVG under assets/) +dart run flutter_gen_runner # or: fluttergen -c pubspec.yaml + +# localization (after editing assets/translations/*.json) +flutter pub run easy_localization:generate -S "assets/translations/" -O "lib/gen/translations" -o "codegen_loader.g.dart" -f keys +``` + +Reminders: +- After adding `@injectable`/`@LazySingleton`/`@Injectable` to a class, **run build_runner** so `injection.config.dart` picks it up. A new use case/repository/Cubit that isn't registered will fail at runtime with a `get_it` lookup error. +- After changing a Drift table, run build_runner **and** add a schema migration (see Section 8). + +--- + +## 6. Dependency injection (injectable + get_it) + +- The container is `getIt` (`lib/di/injection.dart`); `configureDependencies(env)` is called from `bootstrap.dart`. +- Annotate registrable classes: + - Repositories: `@LazySingleton(as: Repository)`. + - Datasources / sync handlers: `@Injectable(as: ...)` / `@lazySingleton`. + - Use cases & Cubits: `@injectable`. +- Resolve dependencies via **constructor injection**. Only resolve via `getIt()` directly at composition roots (e.g. `BlocProvider(create: (_) => getIt())`). +- Manual registrations and `ignoreUnregisteredTypes` live in `lib/di/injection.dart` — touch with care. + +--- + +## 7. Error handling — `Either` + +- Repository methods return `Future>`. Wrap datasource calls in `RepositoryErrorHandler.handleApiCall(() async { ... })` (`lib/core/error/repository_error_handler.dart`) — it maps exceptions → typed `Failure`s. +- `Failure` is a freezed union (`lib/core/error/failures/failures.dart`): `serverError`, `networkError`, `validationError`, `unauthorizedError`, `duplicate`, `notFound`, `none`, etc. +- Throw the matching exception from datasources (e.g. `DuplicateException`) — don't return failures from datasources. +- In the UI, use `state.failure.hasError` and `state.failure.customMessage` (already localized). Pass the `Failure` straight to `showSnackBar(message: state.failure)`. + +--- + +## 8. Offline-first sync (drift_sync) — handle with extra care + +This is the most fragile subsystem. Breaking it causes silent data loss or sync loops. + +- Every **synced** table mixes in `SyncTable` (`lib/data/database/tables/sync_table.dart`). That mixin supplies all sync columns and their server-side `@JsonKey` names — `clientId` (the **primary key**, locally generated), `id` (nullable server id), `userId`, `rev`, `createdAt` / `updatedAt` / `deletedAt`, `lastSyncedAt`. Just write `class Foo extends Table with SyncTable`; never redeclare those columns or change the primary key. +- **Two infrastructure tables back the sync engine — do not treat them as feature tables or touch them unless you are working on sync itself:** + - `LocalChanges` (`tables/local_changes.dart`) — the outbox queue of pending local mutations (entityType/entityId/rev/data/error/...), keyed by `(entityId, entityType)`. + - `SyncMetadata` (`tables/sync_meta_data.dart`) — per-entity-type `lastSyncedAt` cursor. + - `AppDatabase` implements the `drift_sync_core` hooks against these (`getPendingLocalChanges`, `insertLocalChange`, `concludeLocalChange`, `getLocalSyncMetadata`, …). Don't bypass them with ad-hoc reads/writes. +- **Client IDs:** generate local rows' `clientId` with `generateDeviceScopedId()` (`lib/core/utils/id_helper.dart`). Never reuse or hand-craft IDs. +- Repository write methods write **locally first**, then fire-and-forget the sync via `unawaited(post(...))` / `put(...)` / `delete(...)` (provided by `SyncEntityRepository`). Keep this pattern — the UI must not wait on the network. +- Each synced entity needs a `*SyncHandler` implementing marshal/unmarshal, local upsert/delete, and id resolution. Mirror an existing handler exactly. + - **When you add a field to a synced entity, audit the WHOLE handler — not just `upsertLocal`.** Some handler methods **hand-build the data class** instead of returning the Drift row (e.g. `CategorySyncHandler.getLocalByServerId` does `return Category(id: …, name: …, …)`). A hand-built constructor **silently drops any column you don't list**, so add your new field there too. `marshal`/`unmarshal` that delegate to the generated `toJson()`/`fromJson()` pick up new columns automatically after codegen; hand-built constructors do not. +- Sync triggers (lifecycle/connectivity/5-min timer) are wired in `bootstrap.dart`. Don't add ad-hoc sync calls. + +### Drift migrations (any table change is a schema change) + +The app migrates with drift's **`stepByStep`** strategy. The moving parts: + +- `lib/data/database/app_database.dart` — the `schemaVersion` getter and the **hand-written** `fromTo` callbacks (the `Migrations` extension at the bottom; see the existing `from4To5`). +- `lib/data/database/app_database.steps.dart` — generated per-version schema snapshots (`// GENERATED BY drift_dev, DO NOT MODIFY`). +- `drift_schemas/default/drift_schema_v.json` — exported schema per version. +- `test/drift/default/generated/schema_v.dart` — generated snapshots used by `test/drift/default/migration_test.dart`. + +> `default` is the database name from `build.yaml` (`databases: { default: ... }`). drift's default `schema_dir` (`drift_schemas/`) and `test_dir` (`test/drift/`) already match this repo, so no extra config is needed. + +**The tool scaffolds; you (with AI help) write the actual logic.** `drift_dev make-migrations` regenerates the schema snapshots, the `.steps.dart` file, and the test files — and adds an **empty** `fromTo` callback. It cannot infer your intent, so you fill in each callback body by hand (`createTable` / `addColumn` / data backfills). + +**Workflow when you add/alter/remove a table or column:** + +1. Edit the table under `lib/data/database/tables/` (register new tables in the `@DriftDatabase(tables: [...])` list in `app_database.dart`). +2. **Bump `schemaVersion`** (e.g. `5` → `6`) in `app_database.dart`. +3. Regenerate code + migration scaffolding: + ```bash + dart run build_runner build --delete-conflicting-outputs # app_database.g.dart, freezed, etc. + dart run drift_dev make-migrations # schema json + .steps.dart + test snapshots + ``` +4. **Fill in the new `fromTo` callback** in the `Migrations` extension in `app_database.dart`, using the `schema.*` snapshot — never the live tables. Example: + ```dart + from5To6: (Migrator m, Schema6 schema) async { + await m.createTable(schema.myNewTable); + // await m.addColumn(schema.transactions, schema.transactions.newColumn); + }, + ``` +5. Run `flutter test` (the migration tests live in `test/drift/default/migration_test.dart`). The simple-migration suite currently ships `skip: true`, and the data-integrity tests are TODO templates — for column type/constraint changes (not pure additions), fill in a data-integrity test so the migration is proven not to lose data. + +**Never** change a table without bumping the version and adding a step callback — existing users' on-device databases will fail to open or silently lose data. Also note `build.yaml` sets `store_date_time_values_as_text: true`, so `DateTime` columns persist as ISO text. + +--- + +## 9. Localization, assets & theming + +**Localization** +- **No hardcoded user-facing strings.** Add the key to every `assets/translations/.json` (en, de, es, fr, it, ru), regenerate (Section 5), then use `LocaleKeys.my_key.tr()`. + +**Assets** +- Reference via generated `Assets` (`Assets.images...`), not raw string paths. Regenerate after adding files. + +**Theming (light + dark — every screen must work in both)** +- Themes are defined in `lib/presentation/utils/theme.dart` as `AppTheme.lightTheme` / `AppTheme.darkTheme`, applied in `MaterialApp` and switched by `ThemeCubit` (mode is persisted in Config under `ConfigConstants.theme`). Component styling (buttons, cards, inputs, date/time pickers) is themed centrally there — prefer relying on the theme over per-widget overrides. +- **Use semantic design tokens, not raw colors.** Colors live in the `AppTones` theme extension (`lib/presentation/utils/design_tokens.dart`) and are read via the `context.tones` getter — e.g. `context.tones.brand.accent`, `context.tones.income`, `context.tones.textMuted`, `context.tones.tone(...)`. These resolve to the right light/dark value automatically. +- **Do NOT hardcode `Color(0xFF…)` in widgets**, and avoid the legacy flat globals in `lib/presentation/utils/colors.dart` and `lib/core/constants/colors.dart` — they predate `AppTones` and are being migrated away from. New or changed UI should use `context.tones` (and `Theme.of(context)` for text/component styles). Hardcoded colors break dark mode. +- ⚠️ **Some existing screens predate `AppTones` and still hardcode colors** (e.g. the Category add form hardcodes `Color(0xFFEB5757)` as its accent). When §3 says "copy the nearest feature," copy its *structure*, **not** its color usage — replace any hardcoded `Color(...)` with the matching `context.tones` token as you go. +- **Responsive sizing:** the app uses `flutter_screenutil` (design size 390×844, set in `bootstrap.dart`). Express font/icon sizes and dimensions with `.sp` (and `.w` / `.h` / `.r` where appropriate) — e.g. `fontSize: 13.sp`, `size: 16.sp` — not raw logical pixels. + +--- + +## 10. Build, run, test + +Three flavors, each with its own entrypoint and Firebase config: + +```bash +flutter run --flavor development --target lib/main_development.dart +flutter run --flavor staging --target lib/main_staging.dart +flutter run --flavor production --target lib/main_production.dart +``` +(VS Code launch configs `mobile:development|staging|production` are in `.vscode/launch.json`.) + +```bash +flutter pub get # after editing pubspec.yaml +flutter analyze # must pass — enforced by the pre-commit hook +flutter test # must pass — enforced by the pre-push hook +dart format . # formatting standard +``` + +Tests live in `test/` (`unit/`, `widget/`, `drift/`). Use `mocktail` and `bloc_test`. + +--- + +## 11. Git, commits, and hooks + +- **Branch** off `dev` (the main branch). Don't commit directly to `dev`. +- **Conventional Commits, sentence-case subject, ≤72 chars** (enforced by commitlint). Allowed types: `feat, fix, docs, style, refactor, test, chore, build, ci, enh, enhance, tweak, imp, improve`. +- Hooks (Husky) run automatically: + - **pre-commit:** `lint_staged` + `flutter analyze` (+ GitHub Actions validation). Commit is blocked if analysis fails. + - **pre-push:** `flutter test`. +- Don't bypass hooks (`--no-verify`) to land code. + +--- + +## 12. Guardrails — before you finish, verify you did NOT: + +- [ ] Import `lib/data/...` from a Cubit, screen, or use case. +- [ ] Edit a generated file (`*.g.dart`, `*.freezed.dart`, `injection.config.dart`, etc.) by hand. +- [ ] Add an `@injectable`/repository/Cubit without running build_runner (→ runtime `get_it` failure). +- [ ] Mutate Cubit state instead of `copyWith`, or forget to reset `failure` / cancel a subscription in `close()`. +- [ ] Return Drift row types from the domain layer instead of mapping to entities. +- [ ] Change a Drift table without bumping the schema version + adding a migration + updating migration tests. +- [ ] Hardcode a user-facing string, a raw asset path, or a `Color(0xFF…)` / raw pixel size (use `LocaleKeys`, `Assets`, `context.tones`, `.sp`) — and verify the screen still works in **dark mode**. +- [ ] Read/write the sync infrastructure tables (`LocalChanges`, `SyncMetadata`) directly, or redeclare `SyncTable` columns on a feature table. +- [ ] Make the UI block on a network/sync call (writes are local-first; sync is fire-and-forget). +- [ ] Leave `flutter analyze` or `flutter test` failing. + +When unsure how something should look, **find the equivalent in an existing feature (Category, Wallet, Budget) and match it.** diff --git a/lib/core/error/error_handler.dart b/lib/core/error/error_handler.dart index 23abe4cc..72aadb16 100644 --- a/lib/core/error/error_handler.dart +++ b/lib/core/error/error_handler.dart @@ -32,8 +32,10 @@ class ErrorHandler { } if (err.type == DioExceptionType.unknown) { + _recordApiError(err); return UnknownException('Unknown error'); } + final statusCode = err.response?.statusCode; final responseDataForMessage = err.response?.data; final message = responseDataForMessage is Map diff --git a/lib/data/datasources/budget/budget_local_datasource.dart b/lib/data/datasources/budget/budget_local_datasource.dart index f3805af4..5bce82d7 100644 --- a/lib/data/datasources/budget/budget_local_datasource.dart +++ b/lib/data/datasources/budget/budget_local_datasource.dart @@ -5,7 +5,6 @@ import 'package:trakli/core/utils/date_util.dart'; import 'package:trakli/core/utils/id_helper.dart'; import 'package:trakli/data/services/budget/budget_progress_recomputer.dart'; import 'package:trakli/data/database/app_database.dart'; -import 'package:trakli/domain/entities/budget_progress_entity.dart'; import 'package:trakli/presentation/utils/enums.dart'; class BudgetTargetInput { @@ -76,9 +75,6 @@ abstract class BudgetLocalDataSource { Future> getResolvedTargetsForBudget( String budgetClientId); - - Future updateBudgetProgressByServerId( - int id, BudgetProgressEntity progress); } @Injectable(as: BudgetLocalDataSource) @@ -335,13 +331,6 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { return out; } - @override - Future updateBudgetProgressByServerId( - int id, BudgetProgressEntity progress) async { - await (database.update(database.budgets)..where((b) => b.id.equals(id))) - .write(BudgetsCompanion(progress: Value(progress))); - } - @override Future deleteBudget(String clientId) async { final row = await (database.select(database.budgets) diff --git a/lib/data/datasources/budget/budget_remote_datasource.dart b/lib/data/datasources/budget/budget_remote_datasource.dart index 31e8ed80..051a47cd 100644 --- a/lib/data/datasources/budget/budget_remote_datasource.dart +++ b/lib/data/datasources/budget/budget_remote_datasource.dart @@ -4,8 +4,6 @@ import 'package:trakli/core/utils/date_util.dart'; import 'package:trakli/core/utils/json_defaults.dart'; import 'package:trakli/data/datasources/budget/dtos/budget_complete_dto.dart'; import 'package:trakli/data/datasources/budget/dtos/budget_period_state_dto.dart'; -import 'package:trakli/data/datasources/budget/dtos/budget_progress_dto.dart'; -import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; import 'package:trakli/data/datasources/core/api_response.dart'; import 'package:trakli/data/datasources/core/pagination_response.dart'; @@ -19,11 +17,6 @@ abstract class BudgetRemoteDataSource { Future insertBudget(BudgetCompleteDto dto); Future updateBudget(BudgetCompleteDto dto); Future deleteBudget(int id); - Future getBudgetProgress(int id); - Future getBudgetTransactions( - int id, { - int limit = 50, - }); Future closeBudgetPeriod(int id); Future> getAllPeriodStates({ DateTime? syncedSince, @@ -48,10 +41,10 @@ class BudgetRemoteDataSourceImpl implements BudgetRemoteDataSource { while (true) { final queryParams = {'page': currentPage}; - if (syncedSince != null) { - queryParams['synced_since'] = - formatServerIsoDateTimeString(syncedSince); - } + // if (syncedSince != null) { + // queryParams['synced_since'] = + // formatServerIsoDateTimeString(syncedSince); + // } if (noClientId != null) { queryParams['no_client_id'] = noClientId; } @@ -114,32 +107,6 @@ class BudgetRemoteDataSourceImpl implements BudgetRemoteDataSource { await dio.delete('budgets/$id'); } - @override - Future getBudgetProgress(int id) async { - final response = await dio.get('budgets/$id/progress'); - if (response.data == null) return null; - final apiResponse = ApiResponse.fromJson(response.data); - return BudgetProgressDto.fromJson( - apiResponse.data as Map, - ); - } - - @override - Future getBudgetTransactions( - int id, { - int limit = 50, - }) async { - final response = await dio.get( - 'budgets/$id/transactions', - queryParameters: {'limit': limit}, - ); - if (response.data == null) return null; - final apiResponse = ApiResponse.fromJson(response.data); - return BudgetTransactionsResponse.fromJson( - apiResponse.data as Map, - ); - } - @override Future closeBudgetPeriod(int id) async { final response = await dio.post('budgets/$id/close-period'); diff --git a/lib/data/datasources/budget/dtos/budget_transactions_response.dart b/lib/data/datasources/budget/dtos/budget_transactions_response.dart deleted file mode 100644 index bb8cded5..00000000 --- a/lib/data/datasources/budget/dtos/budget_transactions_response.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:trakli/data/database/app_database.dart'; -import 'package:trakli/core/utils/json_defaults.dart'; - -class BudgetTransactionsResponse { - final DateTime periodStart; - final DateTime periodEnd; - final List data; - - const BudgetTransactionsResponse({ - required this.periodStart, - required this.periodEnd, - required this.data, - }); - - factory BudgetTransactionsResponse.fromJson(Map json) { - final rawData = json['data']; - final txs = (rawData is List) - ? rawData - .whereType>() - .map((j) => Transaction.fromJson(JsonDefaultsHelper.addDefaults(j))) - .toList() - : []; - - return BudgetTransactionsResponse( - periodStart: DateTime.parse(json['period_start'] as String), - periodEnd: DateTime.parse(json['period_end'] as String), - data: txs, - ); - } -} diff --git a/lib/data/mappers/budget_mapper.dart b/lib/data/mappers/budget_mapper.dart index d1e2f0b2..40e10aab 100644 --- a/lib/data/mappers/budget_mapper.dart +++ b/lib/data/mappers/budget_mapper.dart @@ -1,6 +1,5 @@ import 'package:trakli/data/database/app_database.dart' as db; import 'package:trakli/data/datasources/budget/dtos/budget_progress_dto.dart'; -import 'package:trakli/data/datasources/budget/dtos/budget_target_dto.dart'; import 'package:trakli/domain/entities/budget_entity.dart'; import 'package:trakli/domain/entities/budget_period_state_entity.dart'; import 'package:trakli/domain/entities/budget_progress_entity.dart'; @@ -38,23 +37,6 @@ class BudgetMapper { ); } - static BudgetTargetEntity targetFromDb(db.BudgetTarget row, {String? name}) { - return BudgetTargetEntity( - type: row.targetType, - clientId: row.targetClientId, - name: name, - ); - } - - static BudgetTargetEntity targetFromDto(BudgetTargetDto dto) { - return BudgetTargetEntity( - type: dto.type, - id: dto.id, - clientId: dto.clientId, - name: dto.name, - ); - } - static BudgetPeriodStateEntity periodStateToDomain(db.BudgetPeriodState row) { return BudgetPeriodStateEntity( clientId: row.clientId, diff --git a/lib/data/repositories/budget_repository_impl.dart b/lib/data/repositories/budget_repository_impl.dart index ca5b211f..fd7c4401 100644 --- a/lib/data/repositories/budget_repository_impl.dart +++ b/lib/data/repositories/budget_repository_impl.dart @@ -10,12 +10,10 @@ 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/datasources/budget/dtos/budget_transactions_response.dart'; import 'package:trakli/data/mappers/budget_mapper.dart'; import 'package:trakli/data/sync/budget_sync_handler.dart'; import 'package:trakli/domain/entities/budget_entity.dart'; import 'package:trakli/domain/entities/budget_period_state_entity.dart'; -import 'package:trakli/domain/entities/budget_progress_entity.dart'; import 'package:trakli/domain/entities/budget_target_entity.dart'; import 'package:trakli/domain/repositories/budget_repository.dart'; import 'package:trakli/presentation/utils/enums.dart'; @@ -205,28 +203,6 @@ class BudgetRepositoryImpl }); } - @override - Future> fetchBudgetProgress(int id) { - return RepositoryErrorHandler.handleApiCall(() async { - final dto = await remoteDataSource.getBudgetProgress(id); - if (dto == null) return null; - final progress = BudgetMapper.progressFromDto(dto); - - await localDataSource.updateBudgetProgressByServerId(id, progress); - - return progress; - }); - } - - @override - Future> fetchBudgetTransactions( - int id, - {int limit = 50}) { - return RepositoryErrorHandler.handleApiCall(() async { - return remoteDataSource.getBudgetTransactions(id, limit: limit); - }); - } - @override Future> closeBudgetPeriod(int id) { return RepositoryErrorHandler.handleApiCall(() async { diff --git a/lib/data/services/budget/budget_progress_recomputer.dart b/lib/data/services/budget/budget_progress_recomputer.dart index 68ba3d86..81c0aeb5 100644 --- a/lib/data/services/budget/budget_progress_recomputer.dart +++ b/lib/data/services/budget/budget_progress_recomputer.dart @@ -8,8 +8,7 @@ import 'package:trakli/domain/entities/exchange_rate_entity.dart'; import 'package:trakli/domain/repositories/exchange_rate_repository.dart'; import 'package:trakli/presentation/utils/enums.dart'; -/// Computes and persists local budget progress, writing the result to the -/// `budgets.progress` column. Server reconciliation later overwrites it. +// Server reconciliation later overwrites the persisted progress. @lazySingleton class BudgetProgressRecomputer { BudgetProgressRecomputer(this._db, this._exchangeRateRepository); @@ -19,9 +18,7 @@ class BudgetProgressRecomputer { StreamSubscription? _fxSub; - /// Recomputes all budgets whenever a fresh rate is cached, re-folding - /// foreign-currency transactions that an earlier offline recompute excluded. - /// Call once at app boot; idempotent. + // FX self-heal: re-fold foreign-currency txns excluded while offline once a rate is cached. void attachFxSelfHeal() { if (_fxSub != null) return; _fxSub = _exchangeRateRepository.onExchangeRateUpdated.listen((_) { @@ -29,13 +26,11 @@ class BudgetProgressRecomputer { }); } - /// Cancels the self-heal subscription (mainly for tests). Future dispose() async { await _fxSub?.cancel(); _fxSub = null; } - /// Recompute progress for a single budget by its client id. Future recomputeFor(String budgetClientId) async { final budget = await (_db.select(_db.budgets) ..where((b) => b.clientId.equals(budgetClientId))) @@ -44,8 +39,6 @@ class BudgetProgressRecomputer { await _recomputeAndWrite(budget); } - /// Recompute every active budget affected by the given transaction (empty - /// targets = catch-all, else a target must match its wallet/group/category). Future recomputeAffectedBy({ required String walletClientId, String? groupClientId, @@ -74,7 +67,6 @@ class BudgetProgressRecomputer { } } - /// Recompute progress for every active budget. Future recomputeAll() async { final activeBudgets = await (_db.select(_db.budgets) ..where((b) => b.isActive.equals(true))) @@ -90,8 +82,6 @@ class BudgetProgressRecomputer { }) async { final ts = targets ?? await _targetsFor(budget.clientId); final txns = await _txnInputs(); - // Cache-only, offline-safe read; when absent, foreign-currency txns are - // excluded until a rate is cached (see attachFxSelfHeal). final exchangeRate = await _exchangeRateRepository.getCachedExchangeRate(); final progress = computeLocalProgress( budget: budget, @@ -112,7 +102,6 @@ class BudgetProgressRecomputer { .get(); } - /// Joins transactions with their category client ids and wallet currency. Future> _txnInputs() async { final txns = await _db.select(_db.transactions).get(); if (txns.isEmpty) return const []; @@ -128,7 +117,6 @@ class BudgetProgressRecomputer { .add(row.categoryClientId); } - // One-shot wallet → currency lookup so the per-txn cost stays O(1). final wallets = await _db.select(_db.wallets).get(); final currencyByWallet = { for (final w in wallets) w.clientId: w.currency, diff --git a/lib/data/services/budget/compute_local_progress.dart b/lib/data/services/budget/compute_local_progress.dart index 4f5afe9c..f93f542d 100644 --- a/lib/data/services/budget/compute_local_progress.dart +++ b/lib/data/services/budget/compute_local_progress.dart @@ -3,7 +3,6 @@ import 'package:trakli/domain/entities/budget_progress_entity.dart'; import 'package:trakli/domain/entities/exchange_rate_entity.dart'; import 'package:trakli/presentation/utils/enums.dart'; -/// A transaction enriched with the category client ids it is tagged with. class BudgetTxnInput { final TransactionType type; final double amount; @@ -74,7 +73,6 @@ BudgetProgressEntity computeLocalProgress({ ? null : _convert(t.amount, txnCurrency, budget.currency, exchangeRate); if (converted == null) { - // No usable rate — exclude until one is cached (see attachFxSelfHeal). continue; } grossSpent += converted; @@ -119,7 +117,6 @@ BudgetProgressEntity computeLocalProgress({ effectiveLimit: effectiveLimit, remaining: remaining, percentUsed: percentUsed, - // Projection/forecast are server-only signals; default locally. projectedSpend: netSpent, status: status, isThresholdCrossed: isThresholdCrossed, @@ -127,9 +124,7 @@ BudgetProgressEntity computeLocalProgress({ ); } -/// Converts [amount] from [from] to [to] via the base-relative snapshot -/// (`amount / rate(from) * rate(to)`). Returns `null` when a rate is missing -/// or the source rate is zero, so the caller can exclude the transaction. +// FX conversion via base-relative snapshot: amount / rate(from) * rate(to). double? _convert(double amount, String from, String to, ExchangeRateEntity fx) { if (from == to) return amount; final rateFrom = from == fx.baseCode ? 1.0 : fx.rates[from]; @@ -152,7 +147,6 @@ BudgetStatus _deriveStatus({ } (DateTime, DateTime) _periodWindow(Budget budget, DateTime now) { - // Clamp the reference to start_date so pre-start dates still yield a window. final ref = now.isBefore(budget.startDate) ? budget.startDate : now; switch (budget.periodType) { diff --git a/lib/data/services/budget/period_state_client_id.dart b/lib/data/services/budget/period_state_client_id.dart index e2f0a904..8eb7865a 100644 --- a/lib/data/services/budget/period_state_client_id.dart +++ b/lib/data/services/budget/period_state_client_id.dart @@ -1,5 +1,2 @@ -/// Deterministic local client id for a server-authored `BudgetPeriodState`, -/// minted from its immutable server `id` so re-syncs `insertOrReplace` -/// idempotently. The `bps:server-` prefix distinguishes these from real -/// device-scoped client ids. +// Local client id derived from the server id. String periodStateClientId(int serverId) => 'bps:server-$serverId'; diff --git a/lib/data/sync/budget_period_state_sync_handler.dart b/lib/data/sync/budget_period_state_sync_handler.dart index 5a900524..87a24e6b 100644 --- a/lib/data/sync/budget_period_state_sync_handler.dart +++ b/lib/data/sync/budget_period_state_sync_handler.dart @@ -7,9 +7,7 @@ import 'package:trakli/data/database/tables/budget_period_states.dart'; import 'package:trakli/data/datasources/budget/budget_remote_datasource.dart'; import 'package:trakli/data/datasources/budget/dtos/budget_period_state_dto.dart'; -/// Read-only sync handler for `BudgetPeriodState`. The backend is the sole -/// writer ([CloseBudgetPeriodJob]); the mobile never pushes. Server responses - +// Read-only: backend is the sole writer; mobile never pushes. @lazySingleton class BudgetPeriodStateSyncHandler extends SyncTypeHandler @@ -47,7 +45,6 @@ class BudgetPeriodStateSyncHandler @override Map marshal(BudgetPeriodStateDto entity) { - // Read-only entity from the client's perspective — never pushed upstream. return const {}; } @@ -59,8 +56,6 @@ class BudgetPeriodStateSyncHandler bool? noClientId, DateTime? syncedSince, }) { - // Budget period state doesn't have a clientId - // handles all our persistence. if (noClientId == true) { return Future.value(const []); } @@ -72,9 +67,6 @@ class BudgetPeriodStateSyncHandler @override Future restGetRemote(int id) async { - // Server exposes only a list endpoint; fall back to scanning the latest - // page. In practice the sync flow uses restGetAllRemote, so this is rarely - // exercised. final all = await remoteDataSource.getAllPeriodStates(); for (final dto in all) { if (dto.id == id) return dto; @@ -85,29 +77,20 @@ class BudgetPeriodStateSyncHandler @override Future restPutRemote( BudgetPeriodStateDto entity) async { - // Server is the sole writer for period states — no-op. return entity; } @override - Future restDeleteRemote(BudgetPeriodStateDto entity) async { - // Server is the sole writer for period states — no-op. - } + Future restDeleteRemote(BudgetPeriodStateDto entity) async {} @override Future assignClientId(BudgetPeriodStateDto item) async { final budgetClientId = await _resolveBudgetClientId(item); if (budgetClientId == null || budgetClientId.isEmpty) { - // Orphan period state — no local budget matches. Leave clientId empty; - // upsertLocal will skip it. Next sync will pick it up once the parent - // budget is also synced. return item.copyWith(clientId: ''); } final serverId = item.id; if (serverId == null) { - // Period states only originate server-side, so a missing server id is - // unexpected — defensively treat it as a skip and let the next sync - // pick it up once the server replays it with an id. return item.copyWith(clientId: ''); } return item.copyWith( @@ -135,16 +118,11 @@ class BudgetPeriodStateSyncHandler @override Future upsertLocal(BudgetPeriodStateDto dto) async { - // The framework's down-sync path (`_timeBasedPartialResync`) does NOT call - // `assignClientId` before persisting — and the server never sends a - // `client_generated_id` for period states. Assign one inline so the row - // gets persisted on every regular sync. final resolved = dto.clientId.isEmpty ? await assignClientId(dto) : dto; final budgetClientId = resolved.budgetClientGeneratedId; if (budgetClientId == null || budgetClientId.isEmpty) return; if (resolved.clientId.isEmpty) { - // orphan — parent budget not synced yet, or server id missing return; } diff --git a/lib/data/sync/budget_sync_handler.dart b/lib/data/sync/budget_sync_handler.dart index c2e60b55..1b7ac905 100644 --- a/lib/data/sync/budget_sync_handler.dart +++ b/lib/data/sync/budget_sync_handler.dart @@ -219,9 +219,6 @@ class BudgetSyncHandler } bool _shouldResetTargets(BudgetCompleteDto entity) { - // Server payloads always include the canonical target list (possibly empty). - // Locally-composed DTOs always populate it from the DB. Either way, the - // server-state of targets is authoritative. return true; } } diff --git a/lib/di/injection.config.dart b/lib/di/injection.config.dart index c8d61288..03f8f8ab 100644 --- a/lib/di/injection.config.dart +++ b/lib/di/injection.config.dart @@ -148,9 +148,6 @@ import '../domain/usecases/auth/stream_auth_status.dart' as _i444; import '../domain/usecases/auth/verify_email_usecase.dart' as _i100; import '../domain/usecases/budget/close_budget_period_usecase.dart' as _i893; import '../domain/usecases/budget/delete_budget_usecase.dart' as _i748; -import '../domain/usecases/budget/fetch_budget_progress_usecase.dart' as _i598; -import '../domain/usecases/budget/fetch_budget_transactions_usecase.dart' - as _i59; import '../domain/usecases/budget/get_all_budgets_usecase.dart' as _i884; import '../domain/usecases/budget/get_budget_usecase.dart' as _i102; import '../domain/usecases/budget/insert_budget_usecase.dart' as _i363; @@ -819,12 +816,8 @@ _i174.GetIt $initGetIt( () => _i617.UpdateBudgetUseCase(gh<_i340.BudgetRepository>())); gh.factory<_i363.InsertBudgetUseCase>( () => _i363.InsertBudgetUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i59.FetchBudgetTransactionsUseCase>( - () => _i59.FetchBudgetTransactionsUseCase(gh<_i340.BudgetRepository>())); gh.factory<_i748.DeleteBudgetUseCase>( () => _i748.DeleteBudgetUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i598.FetchBudgetProgressUseCase>( - () => _i598.FetchBudgetProgressUseCase(gh<_i340.BudgetRepository>())); gh.factory<_i884.GetAllBudgetsUseCase>( () => _i884.GetAllBudgetsUseCase(gh<_i340.BudgetRepository>())); gh.factory<_i920.ListenToPeriodStatesUseCase>( @@ -855,9 +848,6 @@ _i174.GetIt $initGetIt( insertBudgetUseCase: gh<_i363.InsertBudgetUseCase>(), updateBudgetUseCase: gh<_i617.UpdateBudgetUseCase>(), deleteBudgetUseCase: gh<_i748.DeleteBudgetUseCase>(), - fetchBudgetProgressUseCase: gh<_i598.FetchBudgetProgressUseCase>(), - fetchBudgetTransactionsUseCase: - gh<_i59.FetchBudgetTransactionsUseCase>(), closeBudgetPeriodUseCase: gh<_i893.CloseBudgetPeriodUseCase>(), listenToBudgetsUseCase: gh<_i377.ListenToBudgetsUseCase>(), listenToTargetsUseCase: gh<_i442.ListenToTargetsUseCase>(), diff --git a/lib/domain/repositories/budget_repository.dart b/lib/domain/repositories/budget_repository.dart index 9817975e..435fed24 100644 --- a/lib/domain/repositories/budget_repository.dart +++ b/lib/domain/repositories/budget_repository.dart @@ -1,9 +1,7 @@ import 'package:fpdart/fpdart.dart'; import 'package:trakli/core/error/failures/failures.dart'; -import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; import 'package:trakli/domain/entities/budget_entity.dart'; import 'package:trakli/domain/entities/budget_period_state_entity.dart'; -import 'package:trakli/domain/entities/budget_progress_entity.dart'; import 'package:trakli/domain/entities/budget_target_entity.dart'; import 'package:trakli/presentation/utils/enums.dart'; @@ -56,9 +54,5 @@ abstract class BudgetRepository { Stream>> listenToPeriodStates( String budgetClientId); - Future> fetchBudgetProgress(int id); - Future> fetchBudgetTransactions( - int id, - {int limit = 50}); Future> closeBudgetPeriod(int id); } diff --git a/lib/domain/usecases/budget/fetch_budget_progress_usecase.dart b/lib/domain/usecases/budget/fetch_budget_progress_usecase.dart deleted file mode 100644 index fa101bd0..00000000 --- a/lib/domain/usecases/budget/fetch_budget_progress_usecase.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:injectable/injectable.dart'; -import 'package:trakli/core/error/failures/failures.dart'; -import 'package:trakli/core/usecases/usecase.dart'; -import 'package:trakli/domain/entities/budget_progress_entity.dart'; -import 'package:trakli/domain/repositories/budget_repository.dart'; - -class FetchBudgetProgressParams { - final int id; - const FetchBudgetProgressParams({required this.id}); -} - -@injectable -class FetchBudgetProgressUseCase - implements UseCase { - final BudgetRepository _repository; - - FetchBudgetProgressUseCase(this._repository); - - @override - Future> call( - FetchBudgetProgressParams params) async { - return await _repository.fetchBudgetProgress(params.id); - } -} diff --git a/lib/domain/usecases/budget/fetch_budget_transactions_usecase.dart b/lib/domain/usecases/budget/fetch_budget_transactions_usecase.dart deleted file mode 100644 index 7f8ff7b4..00000000 --- a/lib/domain/usecases/budget/fetch_budget_transactions_usecase.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:injectable/injectable.dart'; -import 'package:trakli/core/error/failures/failures.dart'; -import 'package:trakli/core/usecases/usecase.dart'; -import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; -import 'package:trakli/domain/repositories/budget_repository.dart'; - -class FetchBudgetTransactionsParams { - final int id; - final int limit; - const FetchBudgetTransactionsParams({required this.id, this.limit = 50}); -} - -@injectable -class FetchBudgetTransactionsUseCase - implements - UseCase { - final BudgetRepository _repository; - - FetchBudgetTransactionsUseCase(this._repository); - - @override - Future> call( - FetchBudgetTransactionsParams params) async { - return await _repository.fetchBudgetTransactions(params.id, - limit: params.limit); - } -} diff --git a/lib/presentation/budget/add_budget_screen.dart b/lib/presentation/budget/add_budget_screen.dart index 93c1fd3e..515a7dfe 100644 --- a/lib/presentation/budget/add_budget_screen.dart +++ b/lib/presentation/budget/add_budget_screen.dart @@ -713,10 +713,6 @@ class _TargetTile extends StatelessWidget { } } -/// Tap-to-open currency picker bound to a `TextEditingController` so the -/// existing `_submit()` flow (which reads `_currency.text`) keeps working -/// unchanged. Restricted to the user's wallet currencies plus the app default -/// (and the current value) so a usable exchange rate exists for conversion. class _CurrencyPickerField extends StatelessWidget { final TextEditingController controller; final VoidCallback onChanged; diff --git a/lib/presentation/budget/budget_detail_screen.dart b/lib/presentation/budget/budget_detail_screen.dart index 9ca19340..aa119832 100644 --- a/lib/presentation/budget/budget_detail_screen.dart +++ b/lib/presentation/budget/budget_detail_screen.dart @@ -11,9 +11,9 @@ import 'package:trakli/presentation/budget/add_budget_screen.dart'; import 'package:trakli/presentation/budget/cubit/budget_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/design_tokens.dart'; -import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/dialogs.dart' show showDeleteConfirmationDialog, showConfirmationDialog; +import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/helpers.dart' show showSnackBar, formatDateYmd; @@ -31,10 +31,6 @@ class _BudgetDetailScreenState extends State { super.initState(); final cubit = context.read(); cubit.watchBudget(widget.budget.clientId); - final id = widget.budget.id; - if (id != null) { - cubit.fetchProgress(id); - } } BudgetEntity _currentBudget(BudgetState state) { @@ -52,7 +48,8 @@ class _BudgetDetailScreenState extends State { final confirm = await showDeleteConfirmationDialog( context, title: LocaleKeys.deleteBudget.tr(), - message: LocaleKeys.deleteBudgetConfirm.tr(namedArgs: {'name': budget.name}), + message: + LocaleKeys.deleteBudgetConfirm.tr(namedArgs: {'name': budget.name}), ); if (!confirm || !mounted) return; @@ -109,63 +106,49 @@ class _BudgetDetailScreenState extends State { ), body: SafeArea( child: BlocListener( - listenWhen: (prev, curr) { - final deletionCompleted = prev.isDeleting && !curr.isDeleting; - final closingCompleted = - prev.isClosingPeriod && !curr.isClosingPeriod; - return deletionCompleted || closingCompleted; - }, - listener: (context, state) { - if (state.isDeleting || state.isClosingPeriod) return; - - if (state.failure != const Failure.none()) { - showSnackBar( - message: state.isDeleting - ? LocaleKeys.deleteBudgetError.tr() - : LocaleKeys.closePeriodError.tr(), - ); - return; - } - - if (_budgetWasDeleted(state)) { - showSnackBar( - message: LocaleKeys.deleteBudgetSuccess.tr(), - isSuccess: true, - ); - AppNavigator.pop(context); - } else { - showSnackBar( - message: LocaleKeys.closePeriodSuccess.tr(), - isSuccess: true, - ); - } - }, - child: BlocBuilder( - builder: (context, state) { - final budget = _currentBudget(state); - // Prefer the locally-streamed value (kept fresh by the recomputer - // on edits and by the sync handler on server reconciliation). - // Fall back to the cubit's selectedBudgetProgress only if the - // budget row hasn't been written yet. - final progress = budget.progress ?? state.selectedBudgetProgress; - final targets = state.selectedBudgetTargets; - final periods = state.selectedBudgetPeriodStates; - return RefreshIndicator( - onRefresh: () { - final id = budget.id; - if (id != null) { - return context.read().fetchProgress(id); - } - return Future.value(); - }, - child: ListView( + listenWhen: (prev, curr) { + final deletionCompleted = prev.isDeleting && !curr.isDeleting; + final closingCompleted = + prev.isClosingPeriod && !curr.isClosingPeriod; + return deletionCompleted || closingCompleted; + }, + listener: (context, state) { + if (state.isDeleting || state.isClosingPeriod) return; + + if (state.failure != const Failure.none()) { + showSnackBar( + message: state.isDeleting + ? LocaleKeys.deleteBudgetError.tr() + : LocaleKeys.closePeriodError.tr(), + ); + return; + } + + if (_budgetWasDeleted(state)) { + showSnackBar( + message: LocaleKeys.deleteBudgetSuccess.tr(), + isSuccess: true, + ); + AppNavigator.pop(context); + } else { + showSnackBar( + message: LocaleKeys.closePeriodSuccess.tr(), + isSuccess: true, + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + final budget = _currentBudget(state); + final progress = budget.progress; + final targets = state.selectedBudgetTargets; + final periods = state.selectedBudgetPeriodStates; + return ListView( padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 24.h), children: [ _HeaderCard(budget: budget, progress: progress), SizedBox(height: 14.h), - if (state.isProgressLoading && progress == null) - const Center(child: CircularProgressIndicator()) - else if (progress != null) + if (progress != null) _KpiGrid(budget: budget, progress: progress) else if (budget.id == null) _InfoBanner( @@ -197,10 +180,9 @@ class _BudgetDetailScreenState extends State { ), ], ], - ), - ); - }, - ), + ); + }, + ), ), ), ); diff --git a/lib/presentation/budget/cubit/budget_cubit.dart b/lib/presentation/budget/cubit/budget_cubit.dart index 723a50a8..e9ec1d37 100644 --- a/lib/presentation/budget/cubit/budget_cubit.dart +++ b/lib/presentation/budget/cubit/budget_cubit.dart @@ -4,22 +4,18 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:trakli/core/error/failures/failures.dart'; -import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; import 'package:trakli/domain/entities/budget_entity.dart'; import 'package:trakli/domain/entities/budget_period_state_entity.dart'; -import 'package:trakli/domain/entities/budget_progress_entity.dart'; import 'package:trakli/domain/entities/budget_target_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; import 'package:trakli/domain/usecases/budget/close_budget_period_usecase.dart'; import 'package:trakli/domain/usecases/budget/delete_budget_usecase.dart'; -import 'package:trakli/domain/usecases/budget/fetch_budget_progress_usecase.dart'; -import 'package:trakli/domain/usecases/budget/fetch_budget_transactions_usecase.dart'; import 'package:trakli/domain/usecases/budget/get_all_budgets_usecase.dart'; import 'package:trakli/domain/usecases/budget/insert_budget_usecase.dart'; import 'package:trakli/domain/usecases/budget/listen_to_budgets_usecase.dart'; import 'package:trakli/domain/usecases/budget/listen_to_period_states_usecase.dart'; import 'package:trakli/domain/usecases/budget/listen_to_targets_usecase.dart'; import 'package:trakli/domain/usecases/budget/update_budget_usecase.dart'; -import 'package:trakli/domain/repositories/budget_repository.dart'; import 'package:trakli/presentation/utils/enums.dart'; part 'budget_cubit.freezed.dart'; @@ -31,8 +27,6 @@ class BudgetCubit extends Cubit { final InsertBudgetUseCase _insertBudgetUseCase; final UpdateBudgetUseCase _updateBudgetUseCase; final DeleteBudgetUseCase _deleteBudgetUseCase; - final FetchBudgetProgressUseCase _fetchBudgetProgressUseCase; - final FetchBudgetTransactionsUseCase _fetchBudgetTransactionsUseCase; final CloseBudgetPeriodUseCase _closeBudgetPeriodUseCase; final ListenToBudgetsUseCase _listenToBudgetsUseCase; final ListenToTargetsUseCase _listenToTargetsUseCase; @@ -48,8 +42,6 @@ class BudgetCubit extends Cubit { required InsertBudgetUseCase insertBudgetUseCase, required UpdateBudgetUseCase updateBudgetUseCase, required DeleteBudgetUseCase deleteBudgetUseCase, - required FetchBudgetProgressUseCase fetchBudgetProgressUseCase, - required FetchBudgetTransactionsUseCase fetchBudgetTransactionsUseCase, required CloseBudgetPeriodUseCase closeBudgetPeriodUseCase, required ListenToBudgetsUseCase listenToBudgetsUseCase, required ListenToTargetsUseCase listenToTargetsUseCase, @@ -58,8 +50,6 @@ class BudgetCubit extends Cubit { _insertBudgetUseCase = insertBudgetUseCase, _updateBudgetUseCase = updateBudgetUseCase, _deleteBudgetUseCase = deleteBudgetUseCase, - _fetchBudgetProgressUseCase = fetchBudgetProgressUseCase, - _fetchBudgetTransactionsUseCase = fetchBudgetTransactionsUseCase, _closeBudgetPeriodUseCase = closeBudgetPeriodUseCase, _listenToBudgetsUseCase = listenToBudgetsUseCase, _listenToTargetsUseCase = listenToTargetsUseCase, @@ -70,7 +60,8 @@ class BudgetCubit extends Cubit { Future loadBudgets({bool? active}) async { emit(state.copyWith(isLoading: true, failure: const Failure.none())); - final result = await _getAllBudgetsUseCase(GetAllBudgetsParams(active: active)); + final result = + await _getAllBudgetsUseCase(GetAllBudgetsParams(active: active)); result.fold( (failure) => emit(state.copyWith(isLoading: false, failure: failure)), (budgets) => emit(state.copyWith( @@ -84,7 +75,8 @@ class BudgetCubit extends Cubit { void listenToBudgets({bool? active}) { _budgetsSubscription?.cancel(); _budgetsSubscription = - _listenToBudgetsUseCase(ListenToBudgetsParams(active: active)).listen((either) { + _listenToBudgetsUseCase(ListenToBudgetsParams(active: active)) + .listen((either) { either.fold( (failure) => emit(state.copyWith(failure: failure)), (budgets) => emit(state.copyWith( @@ -201,7 +193,8 @@ class BudgetCubit extends Cubit { _targetsSubscription?.cancel(); _targetsSubscription = - _listenToTargetsUseCase(ListenToTargetsParams(budgetClientId: clientId)).listen((either) { + _listenToTargetsUseCase(ListenToTargetsParams(budgetClientId: clientId)) + .listen((either) { either.fold( (failure) => emit(state.copyWith(failure: failure)), (targets) => emit(state.copyWith( @@ -225,42 +218,6 @@ class BudgetCubit extends Cubit { }); } - Future fetchProgress(int serverId) async { - emit(state.copyWith(isProgressLoading: true)); - final result = await _fetchBudgetProgressUseCase( - FetchBudgetProgressParams(id: serverId), - ); - result.fold( - (failure) => emit(state.copyWith( - isProgressLoading: false, - failure: failure, - )), - (progress) => emit(state.copyWith( - isProgressLoading: false, - selectedBudgetProgress: progress, - failure: const Failure.none(), - )), - ); - } - - Future fetchPeriodTransactions(int serverId, {int limit = 50}) async { - emit(state.copyWith(isPeriodTransactionsLoading: true)); - final result = await _fetchBudgetTransactionsUseCase( - FetchBudgetTransactionsParams(id: serverId, limit: limit), - ); - result.fold( - (failure) => emit(state.copyWith( - isPeriodTransactionsLoading: false, - failure: failure, - )), - (response) => emit(state.copyWith( - isPeriodTransactionsLoading: false, - selectedBudgetTransactions: response, - failure: const Failure.none(), - )), - ); - } - Future closeBudgetPeriod(int serverId) async { emit(state.copyWith(isClosingPeriod: true)); final result = await _closeBudgetPeriodUseCase( @@ -271,13 +228,10 @@ class BudgetCubit extends Cubit { isClosingPeriod: false, failure: failure, )), - (_) async { - emit(state.copyWith( - isClosingPeriod: false, - failure: const Failure.none(), - )); - await fetchProgress(serverId); - }, + (_) => emit(state.copyWith( + isClosingPeriod: false, + failure: const Failure.none(), + )), ); } diff --git a/lib/presentation/budget/cubit/budget_cubit.freezed.dart b/lib/presentation/budget/cubit/budget_cubit.freezed.dart index e5cf746b..39139129 100644 --- a/lib/presentation/budget/cubit/budget_cubit.freezed.dart +++ b/lib/presentation/budget/cubit/budget_cubit.freezed.dart @@ -20,13 +20,7 @@ mixin _$BudgetState { bool get isLoading => throw _privateConstructorUsedError; bool get isSaving => throw _privateConstructorUsedError; bool get isDeleting => throw _privateConstructorUsedError; - bool get isProgressLoading => throw _privateConstructorUsedError; - bool get isPeriodTransactionsLoading => throw _privateConstructorUsedError; bool get isClosingPeriod => throw _privateConstructorUsedError; - BudgetProgressEntity? get selectedBudgetProgress => - throw _privateConstructorUsedError; - BudgetTransactionsResponse? get selectedBudgetTransactions => - throw _privateConstructorUsedError; List get selectedBudgetTargets => throw _privateConstructorUsedError; List get selectedBudgetPeriodStates => @@ -51,16 +45,11 @@ abstract class $BudgetStateCopyWith<$Res> { bool isLoading, bool isSaving, bool isDeleting, - bool isProgressLoading, - bool isPeriodTransactionsLoading, bool isClosingPeriod, - BudgetProgressEntity? selectedBudgetProgress, - BudgetTransactionsResponse? selectedBudgetTransactions, List selectedBudgetTargets, List selectedBudgetPeriodStates, Failure failure}); - $BudgetProgressEntityCopyWith<$Res>? get selectedBudgetProgress; $FailureCopyWith<$Res> get failure; } @@ -83,11 +72,7 @@ class _$BudgetStateCopyWithImpl<$Res, $Val extends BudgetState> Object? isLoading = null, Object? isSaving = null, Object? isDeleting = null, - Object? isProgressLoading = null, - Object? isPeriodTransactionsLoading = null, Object? isClosingPeriod = null, - Object? selectedBudgetProgress = freezed, - Object? selectedBudgetTransactions = freezed, Object? selectedBudgetTargets = null, Object? selectedBudgetPeriodStates = null, Object? failure = null, @@ -109,26 +94,10 @@ class _$BudgetStateCopyWithImpl<$Res, $Val extends BudgetState> ? _value.isDeleting : isDeleting // ignore: cast_nullable_to_non_nullable as bool, - isProgressLoading: null == isProgressLoading - ? _value.isProgressLoading - : isProgressLoading // ignore: cast_nullable_to_non_nullable - as bool, - isPeriodTransactionsLoading: null == isPeriodTransactionsLoading - ? _value.isPeriodTransactionsLoading - : isPeriodTransactionsLoading // ignore: cast_nullable_to_non_nullable - as bool, isClosingPeriod: null == isClosingPeriod ? _value.isClosingPeriod : isClosingPeriod // ignore: cast_nullable_to_non_nullable as bool, - selectedBudgetProgress: freezed == selectedBudgetProgress - ? _value.selectedBudgetProgress - : selectedBudgetProgress // ignore: cast_nullable_to_non_nullable - as BudgetProgressEntity?, - selectedBudgetTransactions: freezed == selectedBudgetTransactions - ? _value.selectedBudgetTransactions - : selectedBudgetTransactions // ignore: cast_nullable_to_non_nullable - as BudgetTransactionsResponse?, selectedBudgetTargets: null == selectedBudgetTargets ? _value.selectedBudgetTargets : selectedBudgetTargets // ignore: cast_nullable_to_non_nullable @@ -144,21 +113,6 @@ class _$BudgetStateCopyWithImpl<$Res, $Val extends BudgetState> ) as $Val); } - /// Create a copy of BudgetState - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $BudgetProgressEntityCopyWith<$Res>? get selectedBudgetProgress { - if (_value.selectedBudgetProgress == null) { - return null; - } - - return $BudgetProgressEntityCopyWith<$Res>(_value.selectedBudgetProgress!, - (value) { - return _then(_value.copyWith(selectedBudgetProgress: value) as $Val); - }); - } - /// Create a copy of BudgetState /// with the given fields replaced by the non-null parameter values. @override @@ -183,17 +137,11 @@ abstract class _$$BudgetStateImplCopyWith<$Res> bool isLoading, bool isSaving, bool isDeleting, - bool isProgressLoading, - bool isPeriodTransactionsLoading, bool isClosingPeriod, - BudgetProgressEntity? selectedBudgetProgress, - BudgetTransactionsResponse? selectedBudgetTransactions, List selectedBudgetTargets, List selectedBudgetPeriodStates, Failure failure}); - @override - $BudgetProgressEntityCopyWith<$Res>? get selectedBudgetProgress; @override $FailureCopyWith<$Res> get failure; } @@ -215,11 +163,7 @@ class __$$BudgetStateImplCopyWithImpl<$Res> Object? isLoading = null, Object? isSaving = null, Object? isDeleting = null, - Object? isProgressLoading = null, - Object? isPeriodTransactionsLoading = null, Object? isClosingPeriod = null, - Object? selectedBudgetProgress = freezed, - Object? selectedBudgetTransactions = freezed, Object? selectedBudgetTargets = null, Object? selectedBudgetPeriodStates = null, Object? failure = null, @@ -241,26 +185,10 @@ class __$$BudgetStateImplCopyWithImpl<$Res> ? _value.isDeleting : isDeleting // ignore: cast_nullable_to_non_nullable as bool, - isProgressLoading: null == isProgressLoading - ? _value.isProgressLoading - : isProgressLoading // ignore: cast_nullable_to_non_nullable - as bool, - isPeriodTransactionsLoading: null == isPeriodTransactionsLoading - ? _value.isPeriodTransactionsLoading - : isPeriodTransactionsLoading // ignore: cast_nullable_to_non_nullable - as bool, isClosingPeriod: null == isClosingPeriod ? _value.isClosingPeriod : isClosingPeriod // ignore: cast_nullable_to_non_nullable as bool, - selectedBudgetProgress: freezed == selectedBudgetProgress - ? _value.selectedBudgetProgress - : selectedBudgetProgress // ignore: cast_nullable_to_non_nullable - as BudgetProgressEntity?, - selectedBudgetTransactions: freezed == selectedBudgetTransactions - ? _value.selectedBudgetTransactions - : selectedBudgetTransactions // ignore: cast_nullable_to_non_nullable - as BudgetTransactionsResponse?, selectedBudgetTargets: null == selectedBudgetTargets ? _value._selectedBudgetTargets : selectedBudgetTargets // ignore: cast_nullable_to_non_nullable @@ -285,11 +213,7 @@ class _$BudgetStateImpl implements _BudgetState { required this.isLoading, required this.isSaving, required this.isDeleting, - required this.isProgressLoading, - required this.isPeriodTransactionsLoading, required this.isClosingPeriod, - this.selectedBudgetProgress, - this.selectedBudgetTransactions, required final List selectedBudgetTargets, required final List selectedBudgetPeriodStates, required this.failure}) @@ -312,15 +236,7 @@ class _$BudgetStateImpl implements _BudgetState { @override final bool isDeleting; @override - final bool isProgressLoading; - @override - final bool isPeriodTransactionsLoading; - @override final bool isClosingPeriod; - @override - final BudgetProgressEntity? selectedBudgetProgress; - @override - final BudgetTransactionsResponse? selectedBudgetTransactions; final List _selectedBudgetTargets; @override List get selectedBudgetTargets { @@ -344,7 +260,7 @@ class _$BudgetStateImpl implements _BudgetState { @override String toString() { - return 'BudgetState(budgets: $budgets, isLoading: $isLoading, isSaving: $isSaving, isDeleting: $isDeleting, isProgressLoading: $isProgressLoading, isPeriodTransactionsLoading: $isPeriodTransactionsLoading, isClosingPeriod: $isClosingPeriod, selectedBudgetProgress: $selectedBudgetProgress, selectedBudgetTransactions: $selectedBudgetTransactions, selectedBudgetTargets: $selectedBudgetTargets, selectedBudgetPeriodStates: $selectedBudgetPeriodStates, failure: $failure)'; + return 'BudgetState(budgets: $budgets, isLoading: $isLoading, isSaving: $isSaving, isDeleting: $isDeleting, isClosingPeriod: $isClosingPeriod, selectedBudgetTargets: $selectedBudgetTargets, selectedBudgetPeriodStates: $selectedBudgetPeriodStates, failure: $failure)'; } @override @@ -359,20 +275,8 @@ class _$BudgetStateImpl implements _BudgetState { other.isSaving == isSaving) && (identical(other.isDeleting, isDeleting) || other.isDeleting == isDeleting) && - (identical(other.isProgressLoading, isProgressLoading) || - other.isProgressLoading == isProgressLoading) && - (identical(other.isPeriodTransactionsLoading, - isPeriodTransactionsLoading) || - other.isPeriodTransactionsLoading == - isPeriodTransactionsLoading) && (identical(other.isClosingPeriod, isClosingPeriod) || other.isClosingPeriod == isClosingPeriod) && - (identical(other.selectedBudgetProgress, selectedBudgetProgress) || - other.selectedBudgetProgress == selectedBudgetProgress) && - (identical(other.selectedBudgetTransactions, - selectedBudgetTransactions) || - other.selectedBudgetTransactions == - selectedBudgetTransactions) && const DeepCollectionEquality() .equals(other._selectedBudgetTargets, _selectedBudgetTargets) && const DeepCollectionEquality().equals( @@ -388,11 +292,7 @@ class _$BudgetStateImpl implements _BudgetState { isLoading, isSaving, isDeleting, - isProgressLoading, - isPeriodTransactionsLoading, isClosingPeriod, - selectedBudgetProgress, - selectedBudgetTransactions, const DeepCollectionEquality().hash(_selectedBudgetTargets), const DeepCollectionEquality().hash(_selectedBudgetPeriodStates), failure); @@ -412,11 +312,7 @@ abstract class _BudgetState implements BudgetState { required final bool isLoading, required final bool isSaving, required final bool isDeleting, - required final bool isProgressLoading, - required final bool isPeriodTransactionsLoading, required final bool isClosingPeriod, - final BudgetProgressEntity? selectedBudgetProgress, - final BudgetTransactionsResponse? selectedBudgetTransactions, required final List selectedBudgetTargets, required final List selectedBudgetPeriodStates, required final Failure failure}) = _$BudgetStateImpl; @@ -430,16 +326,8 @@ abstract class _BudgetState implements BudgetState { @override bool get isDeleting; @override - bool get isProgressLoading; - @override - bool get isPeriodTransactionsLoading; - @override bool get isClosingPeriod; @override - BudgetProgressEntity? get selectedBudgetProgress; - @override - BudgetTransactionsResponse? get selectedBudgetTransactions; - @override List get selectedBudgetTargets; @override List get selectedBudgetPeriodStates; diff --git a/lib/presentation/budget/cubit/budget_state.dart b/lib/presentation/budget/cubit/budget_state.dart index 283355da..61b55a6d 100644 --- a/lib/presentation/budget/cubit/budget_state.dart +++ b/lib/presentation/budget/cubit/budget_state.dart @@ -7,11 +7,7 @@ class BudgetState with _$BudgetState { required bool isLoading, required bool isSaving, required bool isDeleting, - required bool isProgressLoading, - required bool isPeriodTransactionsLoading, required bool isClosingPeriod, - BudgetProgressEntity? selectedBudgetProgress, - BudgetTransactionsResponse? selectedBudgetTransactions, required List selectedBudgetTargets, required List selectedBudgetPeriodStates, required Failure failure, @@ -22,8 +18,6 @@ class BudgetState with _$BudgetState { isLoading: false, isSaving: false, isDeleting: false, - isProgressLoading: false, - isPeriodTransactionsLoading: false, isClosingPeriod: false, selectedBudgetTargets: [], selectedBudgetPeriodStates: [], diff --git a/pubspec.lock b/pubspec.lock index 43d0659f..93295228 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" charcode: dependency: transitive description: @@ -1276,10 +1276,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -2150,5 +2150,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0-0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.32.0"