docs + AGENTS: no-implementation rule, issue-driven workflow, Vico, Uncategorized, ClearAllData, AddEditRecurringScreen
- AGENTS.md: add No-implementation rule and Issue-driven workflow section - docs/01: rename default fallback category from 'Other' to 'Uncategorized' - docs/02, 03: update 'Other' references to 'Uncategorized' in delete/seed/default flows - docs/04: replace Canvas charts with Vico 2.x dep, add data feature folder, add AddEditRecurringScreen, document tab-aware FAB and shared ExportAction helper
This commit is contained in:
@@ -4,6 +4,22 @@ Personal Android expense tracking app. Single-module, Jetpack Compose + Material
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## No-implementation rule
|
||||||
|
|
||||||
|
**Do NOT implement code unless the user explicitly says so or has signed off on the work.** A "go ahead" on the high-level plan is NOT a sign-off to implement — it just means the plan is approved. After the planning steps (define structs, define interfaces, add TODOs, self-review, prompt for review) the agent must STOP and wait for the user to explicitly say "implement", "go", "proceed with implementation", or similar. The same rule applies to each subsequent vertical slice — stop after the planning step and wait. A system-reminder switching to "build mode" is not user authorization.
|
||||||
|
|
||||||
|
Renames, typo fixes, doc/AGENTS.md edits, and structural changes (Phase 0/1) may be done immediately as part of the planning step. Interactor implementations, DI wiring, `MainApplication` setup, manifest edits, and any screen code require explicit per-slice sign-off.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue-driven workflow
|
||||||
|
|
||||||
|
The work is split into issues in the remote Gitea repo: https://git.achmad.dev/admin/ledgerr. **Before starting any implementation, check the remote repo for related issues.** If a relevant issue exists, claim/work on that issue and open a PR when done. Do not duplicate work that is already tracked.
|
||||||
|
|
||||||
|
Tooling: the Gitea MCP tools are available (`gitea-mcp_list_issues`, `gitea-mcp_issue_read`, `gitea-mcp_pull_request_write`, etc.) — use them instead of the CLI when interacting with the remote.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Package structure
|
## Package structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -34,7 +50,9 @@ dev.achmad.ledgerr/
|
|||||||
│ ├── bankstatement/
|
│ ├── bankstatement/
|
||||||
│ │ ├── model/
|
│ │ ├── model/
|
||||||
│ │ └── interactor/
|
│ │ └── interactor/
|
||||||
│ └── export/
|
│ ├── export/
|
||||||
|
│ │ └── interactor/
|
||||||
|
│ └── data/
|
||||||
│ └── interactor/
|
│ └── interactor/
|
||||||
├── data/
|
├── data/
|
||||||
│ └── local/
|
│ └── local/
|
||||||
@@ -53,7 +71,9 @@ dev.achmad.ledgerr/
|
|||||||
│ ├── home/ HomeScreen.kt
|
│ ├── home/ HomeScreen.kt
|
||||||
│ ├── expenses/ ExpenseListScreen.kt
|
│ ├── expenses/ ExpenseListScreen.kt
|
||||||
│ ├── add_edit_expense/ AddEditExpenseScreen.kt
|
│ ├── add_edit_expense/ AddEditExpenseScreen.kt
|
||||||
|
│ ├── add_edit_recurring/ AddEditRecurringScreen.kt
|
||||||
│ ├── category/ CategoryScreen.kt
|
│ ├── category/ CategoryScreen.kt
|
||||||
|
│ ├── import_bank_statement/ ImportBankStatementScreen.kt
|
||||||
│ └── settings/ SettingsScreen.kt
|
│ └── settings/ SettingsScreen.kt
|
||||||
├── theme/
|
├── theme/
|
||||||
└── util/
|
└── util/
|
||||||
@@ -114,20 +134,25 @@ val screenModel = rememberScreenModel { HomeScreenModel() }
|
|||||||
├── Export icon → date range dialog → SAF CreateDocument → writes CSV
|
├── Export icon → date range dialog → SAF CreateDocument → writes CSV
|
||||||
├── "See all" → ExpenseListScreen
|
├── "See all" → ExpenseListScreen
|
||||||
│ ├── Tab: Expenses (search + filter)
|
│ ├── Tab: Expenses (search + filter)
|
||||||
│ │ └── FAB expand → same as HomeScreen FAB
|
│ │ └── FAB expand (tab-aware) → "Manual" → AddEditExpenseScreen
|
||||||
|
│ │ → "Import Bank Statement" → ImportBankStatementScreen
|
||||||
│ └── Tab: Recurring
|
│ └── Tab: Recurring
|
||||||
|
│ └── FAB expand (tab-aware) → "Add Recurring" → AddEditRecurringScreen
|
||||||
├── "Manage Categories" button (dashboard body) → CategoryScreen
|
├── "Manage Categories" button (dashboard body) → CategoryScreen
|
||||||
└── Settings icon → SettingsScreen
|
└── Settings icon → SettingsScreen
|
||||||
|
|
||||||
AddEditExpenseScreen → CategoryScreen (manage categories button)
|
AddEditExpenseScreen → CategoryScreen (manage categories button)
|
||||||
|
|
||||||
|
AddEditRecurringScreen (no secondary navigation)
|
||||||
|
|
||||||
ImportBankStatementScreen (1 Voyager Screen, 1 ScreenModel, 2 composable functions)
|
ImportBankStatementScreen (1 Voyager Screen, 1 ScreenModel, 2 composable functions)
|
||||||
├── ImportBankStatementPickerContent — state is BankPicker or Processing
|
├── ImportBankStatementPickerContent — state is BankPicker or Processing
|
||||||
│ select bank → SAF OpenDocument → loading overlay while parsing
|
│ select bank → SAF OpenDocument → loading overlay while parsing
|
||||||
└── ImportBankStatementConfirmationContent — state is Confirmation
|
└── ImportBankStatementConfirmationContent — state is Confirmation
|
||||||
toggle/edit per row → InsertExpenses → navigator.pop()
|
toggle/edit per row → InsertExpenses → navigator.pop()
|
||||||
```
|
```
|
||||||
- **Export** has no dedicated screen. Icon in `HomeScreen` and `ExpenseListScreen` top bars; also a `TextPreference` in `SettingsScreen`. Tapping shows a date range dialog first, then opens SAF `CreateDocument`. CSV uses ISO 8601 dates (`yyyy-MM-dd`) with UTF-8 BOM.
|
- **Tab-aware FAB.** `ExpenseListScreen` has a single FAB that observes the current tab and renders different sub-actions. Expenses tab mirrors `HomeScreen`'s FAB (Manual + Import). Recurring tab shows only "Add Recurring" → `AddEditRecurringScreen`. The expansion/collapse UI is shared; only the actions list is tab-driven.
|
||||||
|
- **Export** has no dedicated screen. Icon in `HomeScreen` and `ExpenseListScreen` top bars; also a `TextPreference` in `SettingsScreen`. Tapping shows a date range dialog first, then opens SAF `CreateDocument`. CSV uses ISO 8601 dates (`yyyy-MM-dd`) with UTF-8 BOM. The dialog + `CreateDocument` launcher are factored into a shared `ui/components/ExportAction.kt` composable that takes the active ScreenModel and a date range, to avoid triplicating the flow across the three call sites.
|
||||||
- **Import** goes through `ImportBankStatementScreen` — one Voyager Screen, one ScreenModel, two internal composables switched by `StateFlow<ImportState>`. `ImportBankStatementPickerContent` handles bank selection and processing; `ImportBankStatementConfirmationContent` handles review, edit, and confirm. On confirm calls `InsertExpenses.awaitAll()` then pops.
|
- **Import** goes through `ImportBankStatementScreen` — one Voyager Screen, one ScreenModel, two internal composables switched by `StateFlow<ImportState>`. `ImportBankStatementPickerContent` handles bank selection and processing; `ImportBankStatementConfirmationContent` handles review, edit, and confirm. On confirm calls `InsertExpenses.awaitAll()` then pops.
|
||||||
|
|
||||||
### Settings screen
|
### Settings screen
|
||||||
@@ -170,7 +195,8 @@ class AppPreference(private val store: PreferenceStore) {
|
|||||||
|
|
||||||
- `LocalDate` stored as `Long` epoch day via `LocalDateConverter` (`LocalDate.toEpochDay()` / `LocalDate.ofEpochDay()`).
|
- `LocalDate` stored as `Long` epoch day via `LocalDateConverter` (`LocalDate.toEpochDay()` / `LocalDate.ofEpochDay()`).
|
||||||
- Use `@Upsert` Room annotation for insert-or-update operations.
|
- Use `@Upsert` Room annotation for insert-or-update operations.
|
||||||
- `AppDatabase.Callback.onCreate` seeds default categories using `CoroutineScope(Dispatchers.IO)` — Koin is not ready inside Room callbacks.
|
- Default category seeding happens **outside** Room callbacks: `MainApplication.onCreate` calls `inject<SeedDefaultCategories>().await()` on a `CoroutineScope(Dispatchers.IO)` after `startKoin` completes. Koin is not available inside `AppDatabase.Callback.onCreate`.
|
||||||
|
- `ClearAllData` interactor calls `database.clearAllTables()` inside `withTransaction { }`. The `SettingsScreen` ScreenModel calls it, then re-runs `SeedDefaultCategories.await()` so the app retains the 8 default categories.
|
||||||
- minSdk 26, so `java.time.*` is available without desugaring.
|
- minSdk 26, so `java.time.*` is available without desugaring.
|
||||||
|
|
||||||
### Bank statement import
|
### Bank statement import
|
||||||
@@ -184,6 +210,13 @@ class AppPreference(private val store: PreferenceStore) {
|
|||||||
|
|
||||||
- `ExportExpensesToCsv` interactor writes UTF-8 BOM CSV via Okio to a SAF URI.
|
- `ExportExpensesToCsv` interactor writes UTF-8 BOM CSV via Okio to a SAF URI.
|
||||||
- SAF launchers (`ActivityResultContracts.OpenDocument`, `ActivityResultContracts.CreateDocument`) are registered in the Screen composable; URIs are passed to the ScreenModel.
|
- SAF launchers (`ActivityResultContracts.OpenDocument`, `ActivityResultContracts.CreateDocument`) are registered in the Screen composable; URIs are passed to the ScreenModel.
|
||||||
|
- Shared `ui/components/ExportAction.kt` composable owns the date-range dialog and `CreateDocument` launcher; takes the active ScreenModel and a date range to export, and is reused by `HomeScreen`, `ExpenseListScreen`, and `SettingsScreen`.
|
||||||
|
|
||||||
|
### Charts
|
||||||
|
|
||||||
|
- **Vico** (`com.patrykandpatrick.vico:compose`, `compose-m3`, `core`) is the only charting library used.
|
||||||
|
- `HomeScreen` dashboard renders a pie chart from `ExpenseSummary.byCategory` via Vico's `Chart` composable. No Canvas drawing for charts.
|
||||||
|
- Apache 2.0 license, Compose-native, no AndroidView wrapper.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -195,8 +228,8 @@ Follow these steps in order every time. Do not skip ahead to implementation with
|
|||||||
2. **Define interfaces** — add or update interactor class signatures and method stubs in `domain/<feature>/interactor/`. If a new feature has no existing feature folder, create one.
|
2. **Define interfaces** — add or update interactor class signatures and method stubs in `domain/<feature>/interactor/`. If a new feature has no existing feature folder, create one.
|
||||||
3. **Add TODOs** — fill method bodies with `TODO("...")` comments describing the intended behavior, edge cases, and any invariants.
|
3. **Add TODOs** — fill method bodies with `TODO("...")` comments describing the intended behavior, edge cases, and any invariants.
|
||||||
4. **Self-review** — think through the full implementation. If anything requires going outside or beyond what was defined in steps 1–3 (new models, new methods, changed signatures), go back to the relevant step and update it before continuing.
|
4. **Self-review** — think through the full implementation. If anything requires going outside or beyond what was defined in steps 1–3 (new models, new methods, changed signatures), go back to the relevant step and update it before continuing.
|
||||||
5. **Prompt for review** — stop and present a summary of what was defined. Wait for an explicit go-ahead before writing any real implementation code.
|
5. **Prompt for review** — stop and present a summary of what was defined. Wait for an explicit go-ahead before writing any real implementation code. See the **No-implementation rule** at the top of this file.
|
||||||
6. **Implement** — only after approval: replace TODOs with real code, wire up DI in the relevant module, update any callers.
|
6. **Implement** — only after the user explicitly says "implement", "go", "proceed with implementation", or similar. A general "go ahead" on the plan is NOT sufficient.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -209,10 +242,9 @@ Follow these steps in order every time. Do not skip ahead to implementation with
|
|||||||
| Koin | 4.2.2 | DI |
|
| Koin | 4.2.2 | DI |
|
||||||
| Room | 2.7.1 | Local DB |
|
| Room | 2.7.1 | Local DB |
|
||||||
| PDFBox-Android | 2.0.27.0 | PDF text extraction |
|
| PDFBox-Android | 2.0.27.0 | PDF text extraction |
|
||||||
|
| Vico (compose / compose-m3 / core) | 2.x | Charts (pie / bar) |
|
||||||
| Okio | (transitive) | CSV write |
|
| Okio | (transitive) | CSV write |
|
||||||
|
|
||||||
No chart library — use Canvas (`drawArc` for pie, `drawRect` for bar).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Git
|
## Git
|
||||||
@@ -227,9 +259,10 @@ No chart library — use Canvas (`drawArc` for pie, `drawRect` for bar).
|
|||||||
- Do not register ScreenModels in Koin.
|
- Do not register ScreenModels in Koin.
|
||||||
- Do not create a repository layer between interactors and DAOs.
|
- Do not create a repository layer between interactors and DAOs.
|
||||||
- Do not add a `TabNavigator` at the root.
|
- Do not add a `TabNavigator` at the root.
|
||||||
- Do not create a dedicated screen for export — it is a direct action (date range dialog → SAF picker).
|
- Do not create a dedicated screen for export — it is a direct action (date range dialog → SAF picker). The flow is shared via `ui/components/ExportAction.kt`.
|
||||||
- Do not add manifest permissions for file access — SAF handles it.
|
- Do not add manifest permissions for file access — SAF handles it.
|
||||||
- Do not use an external chart library.
|
- Do not draw charts with Canvas — use Vico.
|
||||||
|
- Do not add a chart library other than Vico.
|
||||||
- Do not rename `MainApplication` back to `LedgerrApp`.
|
- Do not rename `MainApplication` back to `LedgerrApp`.
|
||||||
- `UiModule.kt` has been deleted — do not recreate it; the replacement is `PreferenceModule.kt`.
|
- `UiModule.kt` has been deleted — do not recreate it; the replacement is `PreferenceModule.kt`.
|
||||||
- Do not use `@KoinViewModel` or `viewModel {}` — this project uses Voyager `ScreenModel`.
|
- Do not use `@KoinViewModel` or `viewModel {}` — this project uses Voyager `ScreenModel`.
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ data class Category(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val color: Int, // ARGB
|
val color: Int, // ARGB
|
||||||
val iconName: String? = null, // Material icon name
|
val iconName: String? = null, // Material icon name
|
||||||
val isDefault: Boolean = false // non-deletable; "Other" is always true
|
val isDefault: Boolean = false // non-deletable; "Uncategorized" is always true
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -75,9 +75,9 @@ Default categories seeded on first install:
|
|||||||
| Entertainment | `0xFF9C27B0` (purple) | false |
|
| Entertainment | `0xFF9C27B0` (purple) | false |
|
||||||
| Shopping | `0xFFE91E63` (pink) | false |
|
| Shopping | `0xFFE91E63` (pink) | false |
|
||||||
| Education | `0xFF4CAF50` (green) | false |
|
| Education | `0xFF4CAF50` (green) | false |
|
||||||
| Other | `0xFF9E9E9E` (grey) | **true** |
|
| Uncategorized | `0xFF9E9E9E` (grey) | **true** |
|
||||||
|
|
||||||
`Other` is the permanent fallback when a user-defined category is deleted.
|
`Uncategorized` is the permanent fallback when a user-defined category is deleted — orphaned expenses are reassigned to it before the category is removed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -68,7 +68,7 @@ class GetCategories(dao: CategoryDao) {
|
|||||||
fun subscribeAll(): Flow<List<Category>>
|
fun subscribeAll(): Flow<List<Category>>
|
||||||
suspend fun awaitOne(id: Long): Category?
|
suspend fun awaitOne(id: Long): Category?
|
||||||
suspend fun awaitAll(): List<Category>
|
suspend fun awaitAll(): List<Category>
|
||||||
suspend fun awaitDefault(): Category // returns the isDefault=true "Other" category
|
suspend fun awaitDefault(): Category // returns the isDefault=true "Uncategorized" category
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ class DeleteCategory(
|
|||||||
getCategories: GetCategories,
|
getCategories: GetCategories,
|
||||||
) {
|
) {
|
||||||
suspend fun await(id: Long)
|
suspend fun await(id: Long)
|
||||||
// internally: reassign orphaned expenses to "Other", then delete
|
// internally: reassign orphaned expenses to "Uncategorized", then delete
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -183,3 +183,16 @@ class ExportExpensesToCsv(expenseDao: ExpenseDao, categoryDao: CategoryDao, cont
|
|||||||
suspend fun await(range: DateRange, outputUri: Uri): Result<Unit>
|
suspend fun await(range: DateRange, outputUri: Uri): Result<Unit>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature: data
|
||||||
|
|
||||||
|
Wipe-all-data administrative action. Triggered from `SettingsScreen` "Clear all data" `AlertDialogPreference`.
|
||||||
|
|
||||||
|
### `ClearAllData`
|
||||||
|
```kotlin
|
||||||
|
class ClearAllData(database: AppDatabase) {
|
||||||
|
suspend fun await() // deletes all rows from every table
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ Insert if `id == 0`, update otherwise. Returns the id. Does not allow changing `
|
|||||||
|
|
||||||
### `await(id: Long)`
|
### `await(id: Long)`
|
||||||
1. Guard: fetch category by id. If `isDefault = true`, throw `IllegalArgumentException("Cannot delete a default category")`.
|
1. Guard: fetch category by id. If `isDefault = true`, throw `IllegalArgumentException("Cannot delete a default category")`.
|
||||||
2. Find the fallback: call `getCategories.awaitDefault()` to get the "Other" category id.
|
2. Find the fallback: call `getCategories.awaitDefault()` to get the "Uncategorized" category id.
|
||||||
3. Call `reassignExpenseCategory.await(id, fallbackId)` to move orphaned expenses.
|
3. Call `reassignExpenseCategory.await(id, fallbackId)` to move orphaned expenses.
|
||||||
4. Delete the category from the DB.
|
4. Delete the category from the DB.
|
||||||
|
|
||||||
Steps 3 and 4 run sequentially in the interactor (not a DB transaction). If step 4 fails, expenses are already reassigned to Other — acceptable, retry is safe.
|
Steps 3 and 4 run sequentially in the interactor (not a DB transaction). If step 4 fails, expenses are already reassigned to Uncategorized — acceptable, retry is safe.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -172,3 +172,12 @@ Contract for all implementations:
|
|||||||
6. For each expense: `date` in ISO 8601 (`yyyy-MM-dd`), then `category.name`, `amount`, `"note"` (double-quote the note to handle embedded commas).
|
6. For each expense: `date` in ISO 8601 (`yyyy-MM-dd`), then `category.name`, `amount`, `"note"` (double-quote the note to handle embedded commas).
|
||||||
7. Close sink.
|
7. Close sink.
|
||||||
8. Return `Result.success(Unit)` or `Result.failure(exception)`.
|
8. Return `Result.success(Unit)` or `Result.failure(exception)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## data / ClearAllData
|
||||||
|
|
||||||
|
### `await()`
|
||||||
|
1. Call `database.clearAllTables()` (Room built-in) inside a `withTransaction { }` block on the database's executor.
|
||||||
|
2. No-op guard: this is destructive — the caller (`SettingsScreen` `AlertDialogPreference`) is responsible for showing a confirmation dialog before invoking.
|
||||||
|
3. After clearing, `SeedDefaultCategories` should be re-run so the app retains the 8 default categories. The `SettingsScreen` ScreenModel calls `inject<SeedDefaultCategories>().await()` after `ClearAllData.await()` returns successfully.
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ In `gradle/libs.versions.toml`:
|
|||||||
[versions]
|
[versions]
|
||||||
room = "2.7.1"
|
room = "2.7.1"
|
||||||
pdfbox_android = "2.0.27.0"
|
pdfbox_android = "2.0.27.0"
|
||||||
|
vico = "2.0.0" # Compose-native chart library (Apache 2.0)
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||||
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||||
pdfbox-android = { group = "com.tom-roush", name = "pdfbox-android", version.ref = "pdfbox_android" }
|
pdfbox-android = { group = "com.tom-roush", name = "pdfbox-android", version.ref = "pdfbox_android" }
|
||||||
|
vico-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico" }
|
||||||
|
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
|
||||||
|
vico-core = { group = "com.patrykandpatrick.vico", name = "core", version.ref = "vico" }
|
||||||
```
|
```
|
||||||
|
|
||||||
In `app/build.gradle.kts`:
|
In `app/build.gradle.kts`:
|
||||||
@@ -25,9 +29,13 @@ ksp(libs.room.compiler)
|
|||||||
implementation(libs.room.runtime)
|
implementation(libs.room.runtime)
|
||||||
implementation(libs.room.ktx)
|
implementation(libs.room.ktx)
|
||||||
implementation(libs.pdfbox.android)
|
implementation(libs.pdfbox.android)
|
||||||
|
implementation(libs.vico.compose)
|
||||||
|
implementation(libs.vico.compose.m3)
|
||||||
|
implementation(libs.vico.core)
|
||||||
```
|
```
|
||||||
|
|
||||||
**No chart library** — Canvas-based (`drawArc` for pie, `drawRect` for bars).
|
**Charts**: Vico 2.x Compose-native library. `HomeScreen` dashboard renders a pie chart from `ExpenseSummary.byCategory`. No custom Canvas drawing.
|
||||||
|
|
||||||
**No manifest permissions** — SAF handles file access.
|
**No manifest permissions** — SAF handles file access.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -122,6 +130,10 @@ dev.achmad.ledgerr/
|
|||||||
│ └── interactor/
|
│ └── interactor/
|
||||||
│ └── ExportExpensesToCsv.kt
|
│ └── ExportExpensesToCsv.kt
|
||||||
│
|
│
|
||||||
|
│ └── data/
|
||||||
|
│ └── interactor/
|
||||||
|
│ └── ClearAllData.kt
|
||||||
|
│
|
||||||
└── ui/
|
└── ui/
|
||||||
├── base/
|
├── base/
|
||||||
│ ├── MainApplication.kt
|
│ ├── MainApplication.kt
|
||||||
@@ -168,6 +180,8 @@ dev.achmad.ledgerr/
|
|||||||
│ │ └── ExpenseListScreen.kt - 2-tab screen (Expenses | Recurring)
|
│ │ └── ExpenseListScreen.kt - 2-tab screen (Expenses | Recurring)
|
||||||
│ ├── add_edit_expense/
|
│ ├── add_edit_expense/
|
||||||
│ │ └── AddEditExpenseScreen.kt - shared add & edit
|
│ │ └── AddEditExpenseScreen.kt - shared add & edit
|
||||||
|
│ ├── add_edit_recurring/
|
||||||
|
│ │ └── AddEditRecurringScreen.kt - shared add & edit for recurring templates
|
||||||
│ ├── category/
|
│ ├── category/
|
||||||
│ │ └── CategoryScreen.kt
|
│ │ └── CategoryScreen.kt
|
||||||
│ ├── import_bank_statement/
|
│ ├── import_bank_statement/
|
||||||
@@ -246,15 +260,19 @@ HomeScreen (root)
|
|||||||
├── Export icon ────────────────────────────── date range dialog → SAF CreateDocument → writes CSV
|
├── Export icon ────────────────────────────── date range dialog → SAF CreateDocument → writes CSV
|
||||||
├── "See all" / "View expenses" button ──────── → ExpenseListScreen
|
├── "See all" / "View expenses" button ──────── → ExpenseListScreen
|
||||||
│ ├── Tab 1: Expenses list (search + filter)
|
│ ├── Tab 1: Expenses list (search + filter)
|
||||||
│ │ └── FAB (expanded) → "Manual" → AddEditExpenseScreen
|
│ │ └── FAB (expanded, tab-aware) → "Manual" → AddEditExpenseScreen
|
||||||
│ │ → "Import" → ImportBankStatementScreen
|
│ │ → "Import Bank Statement" → ImportBankStatementScreen
|
||||||
│ └── Tab 2: Recurring list
|
│ └── Tab 2: Recurring list
|
||||||
|
│ └── FAB (expanded, tab-aware) → "Add Recurring" → AddEditRecurringScreen
|
||||||
├── "Manage Categories" button (dashboard) ──── → CategoryScreen
|
├── "Manage Categories" button (dashboard) ──── → CategoryScreen
|
||||||
└── Settings icon ──────────────────────────── → SettingsScreen
|
└── Settings icon ──────────────────────────── → SettingsScreen
|
||||||
|
|
||||||
AddEditExpenseScreen
|
AddEditExpenseScreen
|
||||||
└── "Manage Categories" button ───────────────── → CategoryScreen
|
└── "Manage Categories" button ───────────────── → CategoryScreen
|
||||||
|
|
||||||
|
AddEditRecurringScreen
|
||||||
|
(no secondary navigation)
|
||||||
|
|
||||||
ImportBankStatementScreen (1 Voyager Screen, 1 ScreenModel, 2 composable functions)
|
ImportBankStatementScreen (1 Voyager Screen, 1 ScreenModel, 2 composable functions)
|
||||||
├── ImportBankStatementPickerContent — rendered when state is BankPicker or Processing
|
├── ImportBankStatementPickerContent — rendered when state is BankPicker or Processing
|
||||||
│ • list of banks from List<BankStatementImporter>
|
│ • list of banks from List<BankStatementImporter>
|
||||||
@@ -267,13 +285,15 @@ ImportBankStatementScreen (1 Voyager Screen, 1 ScreenModel, 2 composable functi
|
|||||||
• "Import X items" → InsertExpenses.awaitAll() → navigator.pop()
|
• "Import X items" → InsertExpenses.awaitAll() → navigator.pop()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**FAB changes per tab.** `ExpenseListScreen` has a single FAB that observes the currently-selected tab and changes its label and sub-action list accordingly. Expenses tab mirrors the HomeScreen FAB (Manual + Import). Recurring tab exposes only "Add Recurring" → `AddEditRecurringScreen`. The expanded mini-FAB UI (sub-action buttons) is the same component, just with a different actions list driven by tab state.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- **ExpenseListScreen tabs** are plain Compose `TabRow` / `HorizontalPager` — not Voyager tabs.
|
- **ExpenseListScreen tabs** are plain Compose `TabRow` / `HorizontalPager` — not Voyager tabs.
|
||||||
- **FAB expansion** on both `HomeScreen` and `ExpenseListScreen`: tapping reveals two sub-actions; second tap or outside tap collapses it.
|
- **FAB expansion** on both `HomeScreen` and `ExpenseListScreen`: tapping reveals sub-actions; second tap or outside tap collapses it. On `ExpenseListScreen` the FAB is **tab-aware** — the list of sub-actions changes based on the currently-selected tab.
|
||||||
- **Export** — tapping the export icon shows a date range picker dialog (start + end date); after confirming the range the SAF `CreateDocument` picker opens. The ScreenModel writes the CSV (ISO 8601 dates, UTF-8 BOM) when the URI comes back. Export icon lives in `HomeScreen` top bar, `ExpenseListScreen` top bar, and as a `TextPreference` in `SettingsScreen`.
|
- **Export** — tapping the export icon shows a date range picker dialog (start + end date); after confirming the range the SAF `CreateDocument` picker opens. The ScreenModel writes the CSV (ISO 8601 dates, UTF-8 BOM) when the URI comes back. Export icon lives in `HomeScreen` top bar, `ExpenseListScreen` top bar, and as a `TextPreference` in `SettingsScreen`. The flow is identical across the three call sites, so the date-range dialog and `CreateDocument` launcher are factored into a shared composable (`ui/components/ExportAction.kt`) that takes the active ScreenModel and a date range to export.
|
||||||
- **Import** goes through `ImportBankStatementScreen` (one Voyager Screen, one ScreenModel). The ScreenModel owns a `StateFlow<ImportState>` that switches between two internal composables: `ImportBankStatementPickerContent` (BankPicker + Processing overlay) and `ImportBankStatementConfirmationContent` (review, edit, confirm). On confirmation the ScreenModel calls `InsertExpenses.awaitAll()` then pops.
|
- **Import** goes through `ImportBankStatementScreen` (one Voyager Screen, one ScreenModel). The ScreenModel owns a `StateFlow<ImportState>` that switches between two internal composables: `ImportBankStatementPickerContent` (BankPicker + Processing overlay) and `ImportBankStatementConfirmationContent` (review, edit, confirm). On confirmation the ScreenModel calls `InsertExpenses.awaitAll()` then pops.
|
||||||
- **CategoryScreen** reachable from the "Manage Categories" button on `HomeScreen` dashboard and from inside `AddEditExpenseScreen`.
|
- **CategoryScreen** reachable from the "Manage Categories" button on `HomeScreen` dashboard and from inside `AddEditExpenseScreen`.
|
||||||
- **SettingsScreen** uses `PreferenceScreen` component (same pattern as `MoreTab` in info-krl-android). Exposes: theme selector (`ListPreference`), export action (`TextPreference`), clear data (`AlertDialogPreference`).
|
- **SettingsScreen** uses `PreferenceScreen` component (same pattern as `MoreTab` in info-krl-android). Exposes: theme selector (`ListPreference`), export action (`TextPreference`), clear data (`AlertDialogPreference` — calls `ClearAllData.await()` then `SeedDefaultCategories.await()` so the app re-seeds the 8 default categories).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -337,6 +357,9 @@ val domainModule = module {
|
|||||||
|
|
||||||
// export
|
// export
|
||||||
factory { ExportExpensesToCsv(get(), get(), androidContext()) }
|
factory { ExportExpensesToCsv(get(), get(), androidContext()) }
|
||||||
|
|
||||||
|
// data
|
||||||
|
factory { ClearAllData(get()) }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -401,12 +424,12 @@ Launchers registered in the Screen composable; URIs passed to the ScreenModel.
|
|||||||
|
|
||||||
## Build Order
|
## Build Order
|
||||||
|
|
||||||
1. Domain models (all features)
|
1. Domain models (all features, including `data` feature placeholder if needed)
|
||||||
2. Domain preference classes
|
2. Domain preference classes
|
||||||
3. Data: entities, DAOs, `LocalDateConverter`, `AppDatabase`
|
3. Data: entities, DAOs, `LocalDateConverter`, `AppDatabase`
|
||||||
4. Mappers
|
4. Mappers
|
||||||
5. Domain interactors (category → expense → recurring → bank stubs → export)
|
5. Domain interactors (category → expense → recurring → bank stubs → export → data)
|
||||||
6. DI modules
|
6. DI modules
|
||||||
7. `MainApplication` + manifest
|
7. `MainApplication` + manifest
|
||||||
8. Screens: `HomeScreen` → `ExpenseListScreen` → `AddEditExpenseScreen` → `CategoryScreen` → `ImportBankStatementScreen` → `SettingsScreen`
|
8. Screens: `HomeScreen` → `ExpenseListScreen` (with tab-aware FAB) → `AddEditExpenseScreen` → `AddEditRecurringScreen` → `CategoryScreen` → `ImportBankStatementScreen` → `SettingsScreen`
|
||||||
9. UI components extracted as needed during screen work
|
9. Shared UI components extracted as needed during screen work (including `ExportAction` helper)
|
||||||
|
|||||||
Reference in New Issue
Block a user