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:
@@ -60,7 +60,7 @@ data class Category(
|
||||
val name: String,
|
||||
val color: Int, // ARGB
|
||||
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 |
|
||||
| Shopping | `0xFFE91E63` (pink) | 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>>
|
||||
suspend fun awaitOne(id: Long): 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,
|
||||
) {
|
||||
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>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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)`
|
||||
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.
|
||||
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).
|
||||
7. Close sink.
|
||||
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]
|
||||
room = "2.7.1"
|
||||
pdfbox_android = "2.0.27.0"
|
||||
vico = "2.0.0" # Compose-native chart library (Apache 2.0)
|
||||
|
||||
[libraries]
|
||||
room-runtime = { group = "androidx.room", name = "room-runtime", 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" }
|
||||
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`:
|
||||
@@ -25,9 +29,13 @@ ksp(libs.room.compiler)
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
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.
|
||||
|
||||
---
|
||||
@@ -122,6 +130,10 @@ dev.achmad.ledgerr/
|
||||
│ └── interactor/
|
||||
│ └── ExportExpensesToCsv.kt
|
||||
│
|
||||
│ └── data/
|
||||
│ └── interactor/
|
||||
│ └── ClearAllData.kt
|
||||
│
|
||||
└── ui/
|
||||
├── base/
|
||||
│ ├── MainApplication.kt
|
||||
@@ -168,6 +180,8 @@ dev.achmad.ledgerr/
|
||||
│ │ └── ExpenseListScreen.kt - 2-tab screen (Expenses | Recurring)
|
||||
│ ├── add_edit_expense/
|
||||
│ │ └── AddEditExpenseScreen.kt - shared add & edit
|
||||
│ ├── add_edit_recurring/
|
||||
│ │ └── AddEditRecurringScreen.kt - shared add & edit for recurring templates
|
||||
│ ├── category/
|
||||
│ │ └── CategoryScreen.kt
|
||||
│ ├── import_bank_statement/
|
||||
@@ -246,15 +260,19 @@ HomeScreen (root)
|
||||
├── Export icon ────────────────────────────── date range dialog → SAF CreateDocument → writes CSV
|
||||
├── "See all" / "View expenses" button ──────── → ExpenseListScreen
|
||||
│ ├── Tab 1: Expenses list (search + filter)
|
||||
│ │ └── FAB (expanded) → "Manual" → AddEditExpenseScreen
|
||||
│ │ → "Import" → ImportBankStatementScreen
|
||||
│ │ └── FAB (expanded, tab-aware) → "Manual" → AddEditExpenseScreen
|
||||
│ │ → "Import Bank Statement" → ImportBankStatementScreen
|
||||
│ └── Tab 2: Recurring list
|
||||
│ └── FAB (expanded, tab-aware) → "Add Recurring" → AddEditRecurringScreen
|
||||
├── "Manage Categories" button (dashboard) ──── → CategoryScreen
|
||||
└── Settings icon ──────────────────────────── → SettingsScreen
|
||||
|
||||
AddEditExpenseScreen
|
||||
└── "Manage Categories" button ───────────────── → CategoryScreen
|
||||
|
||||
AddEditRecurringScreen
|
||||
(no secondary navigation)
|
||||
|
||||
ImportBankStatementScreen (1 Voyager Screen, 1 ScreenModel, 2 composable functions)
|
||||
├── ImportBankStatementPickerContent — rendered when state is BankPicker or Processing
|
||||
│ • 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()
|
||||
```
|
||||
|
||||
**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:
|
||||
- **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.
|
||||
- **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`.
|
||||
- **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`. 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.
|
||||
- **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
|
||||
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
|
||||
|
||||
1. Domain models (all features)
|
||||
1. Domain models (all features, including `data` feature placeholder if needed)
|
||||
2. Domain preference classes
|
||||
3. Data: entities, DAOs, `LocalDateConverter`, `AppDatabase`
|
||||
4. Mappers
|
||||
5. Domain interactors (category → expense → recurring → bank stubs → export)
|
||||
5. Domain interactors (category → expense → recurring → bank stubs → export → data)
|
||||
6. DI modules
|
||||
7. `MainApplication` + manifest
|
||||
8. Screens: `HomeScreen` → `ExpenseListScreen` → `AddEditExpenseScreen` → `CategoryScreen` → `ImportBankStatementScreen` → `SettingsScreen`
|
||||
9. UI components extracted as needed during screen work
|
||||
8. Screens: `HomeScreen` → `ExpenseListScreen` (with tab-aware FAB) → `AddEditExpenseScreen` → `AddEditRecurringScreen` → `CategoryScreen` → `ImportBankStatementScreen` → `SettingsScreen`
|
||||
9. Shared UI components extracted as needed during screen work (including `ExportAction` helper)
|
||||
|
||||
Reference in New Issue
Block a user