Implement CategoryScreen, ImportBankStatementScreen, SettingsScreen, and ExportAction helper #7

Closed
opened 2026-06-28 08:44:57 +00:00 by admin · 0 comments
Owner

Overview

Implements four pieces:

  1. ui/components/ExportAction.kt — shared export helper (date range dialog + SAF CreateDocument launcher)
  2. ui/screens/category/CategoryScreen.kt — category list with add/edit/delete
  3. ui/screens/import_bank_statement/ImportBankStatementScreen.kt — 1 Voyager Screen, 1 ScreenModel, 2 composables
  4. ui/screens/settings/SettingsScreen.kt — uses PreferenceScreen

Depends on #2, #3, #4.

Prerequisites

Issues #1, #2, #3, #4 must all be merged first.

What to do

1. ui/components/ExportAction.kt — shared export helper

A composable that:

  • Holds a Boolean state for the date range dialog.
  • Holds an ActivityResultLauncher<String> for ActivityResultContracts.CreateDocument("text/csv").
  • Renders an IconButton (Export icon) that, on click, opens the date range dialog.
  • The dialog has 2 DatePicker fields (start, end). On confirm, calls launcher.launch("ledgerr-export-YYYY-MM-DD.csv") (or similar default filename).
  • When the launcher returns a Uri, calls the active ScreenModel's export method with that URI.
  • The composable takes a callback onExportConfirmed: (DateRange, Uri) -> Unit (the screen model implements the actual ExportExpensesToCsv.await(...) call).

Signature:

@Composable
fun ExportAction(
    onExportConfirmed: (DateRange, Uri) -> Unit,
    modifier: Modifier = Modifier,
)

Used by HomeScreen (issue #5), ExpenseListScreen (issue #6), and SettingsScreen (this issue).

2. ui/screens/category/CategoryScreen.kt + CategoryScreenModel.kt

  • List of categories from getCategories.subscribeAll().
  • Tap row → opens an edit dialog (name, color picker, icon picker — keep simple, color picker can be a small grid of preset ARGB swatches).
  • Long-press row (or trailing delete icon) → delete confirmation dialog → deleteCategory.await(id). The reassignment-to-Uncategorized logic happens inside DeleteCategory.
  • Floating "Add" button → opens the same edit dialog with empty fields.
  • Categories with isDefault = true show a lock icon and cannot be deleted.

3. ui/screens/import_bank_statement/ImportBankStatementScreen.kt + ImportBankStatementScreenModel.kt

One Voyager Screen, one ScreenModel, two internal composables switched by StateFlow<ImportState>:

sealed interface ImportState {
    data class BankPicker(val importers: List<BankStatementImporter>) : ImportState
    data class Processing(val bank: String) : ImportState
    data class Confirmation(
        val bank: String,
        val rows: List<PendingImportExpense>,
    ) : ImportState
}

ImportBankStatementPickerContent — rendered when state is BankPicker or Processing:

  • List of banks (one row per importer, showing bankName).
  • Tap a bank → launches SAF ActivityResultContracts.OpenDocument(arrayOf("application/pdf")) via rememberLauncherForActivityResult. On URI returned, sets state to Processing and triggers screenModel.processPdf(uri, importer).
  • When state is Processing, render a CircularProgressIndicator overlay on top.

ImportBankStatementConfirmationContent — rendered when state is Confirmation:

  • List of rows. Each row: checkbox (isSelected), amount, date, description, category dropdown (driven by getCategories.subscribeAll()).
  • "Import X items" button at the bottom. On click:
    • Filter rows where isSelected == true.
    • For each, build Expense(amount, categoryId ?: defaultCategoryId, date, note = description, ...) and call insertExpenses.awaitAll(...).
    • navigator.pop().

ScreenModel injects: getImporters: List<BankStatementImporter> = inject(), getCategories, insertExpenses, defaultCategory (resolved via getCategories.awaitDefault() lazily).

The 3 bank stubs from #4 will return NotImplementedError; the ScreenModel should catch that and surface a snackbar / error message — not crash.

4. ui/screens/settings/SettingsScreen.kt + SettingsScreenModel.kt

Uses the PreferenceScreen composable from ui/components/preference/:

PreferenceScreen(
    itemsProvider = {
        listOf(
            Preference.PreferenceItem.ListPreference(
                title = "Theme",
                preference = appPreference.appTheme(),
                entries = mapOf(
                    "Light" to AppTheme.LIGHT.name,
                    "Dark" to AppTheme.DARK.name,
                    "System" to AppTheme.SYSTEM.name,
                ),
            ),
            Preference.PreferenceItem.TextPreference(
                title = "Export CSV",
                onClick = { /* show date range dialog via ExportAction */ },
            ),
            Preference.PreferenceItem.AlertDialogPreference(
                title = "Clear all data",
                message = "This will permanently delete all expenses, categories, and recurring entries. The 8 default categories will be re-seeded.",
                confirmText = "Clear",
                onConfirm = { screenModel.clearData() },
            ),
        )
    }
)

SettingsScreenModel injects clearData: ClearAllData, seedDefaultCategories: SeedDefaultCategories:

suspend fun clearData() {
    clearData.await()
    seedDefaultCategories.await()
}

For Export, the simplest is to embed ExportAction directly as a row in PreferenceScreen (or call its onClick from the TextPreference). The ExportAction composable provides the dialog + SAF launcher; the ScreenModel calls exportExpensesToCsv.await(range, uri).

Acceptance

  • ExportAction composable is reusable across screens
  • CategoryScreen allows add/edit/delete; default categories cannot be deleted
  • ImportBankStatementScreen shows bank picker, processing overlay, and confirmation view
  • ImportBankStatementScreen handles NotImplementedError gracefully (snackbar/error)
  • SettingsScreen exposes theme, export, and clear-data preferences
  • ClearAllData + re-seed works end-to-end from the settings dialog
  • ./gradlew assembleDebug succeeds

Implementation rule

Per AGENTS.md — do not start implementation without explicit user sign-off on this issue. When working, check for related issues in the remote repo first.

## Overview Implements four pieces: 1. `ui/components/ExportAction.kt` — shared export helper (date range dialog + SAF `CreateDocument` launcher) 2. `ui/screens/category/CategoryScreen.kt` — category list with add/edit/delete 3. `ui/screens/import_bank_statement/ImportBankStatementScreen.kt` — 1 Voyager Screen, 1 ScreenModel, 2 composables 4. `ui/screens/settings/SettingsScreen.kt` — uses `PreferenceScreen` Depends on #2, #3, #4. ## Prerequisites Issues #1, #2, #3, #4 must all be merged first. ## What to do ### 1. `ui/components/ExportAction.kt` — shared export helper A composable that: - Holds a `Boolean` state for the date range dialog. - Holds an `ActivityResultLauncher<String>` for `ActivityResultContracts.CreateDocument("text/csv")`. - Renders an `IconButton` (Export icon) that, on click, opens the date range dialog. - The dialog has 2 `DatePicker` fields (start, end). On confirm, calls `launcher.launch("ledgerr-export-YYYY-MM-DD.csv")` (or similar default filename). - When the launcher returns a `Uri`, calls the active ScreenModel's export method with that URI. - The composable takes a callback `onExportConfirmed: (DateRange, Uri) -> Unit` (the screen model implements the actual `ExportExpensesToCsv.await(...)` call). Signature: ```kotlin @Composable fun ExportAction( onExportConfirmed: (DateRange, Uri) -> Unit, modifier: Modifier = Modifier, ) ``` Used by `HomeScreen` (issue #5), `ExpenseListScreen` (issue #6), and `SettingsScreen` (this issue). ### 2. `ui/screens/category/CategoryScreen.kt` + `CategoryScreenModel.kt` - List of categories from `getCategories.subscribeAll()`. - Tap row → opens an edit dialog (name, color picker, icon picker — keep simple, color picker can be a small grid of preset ARGB swatches). - Long-press row (or trailing delete icon) → delete confirmation dialog → `deleteCategory.await(id)`. The reassignment-to-Uncategorized logic happens inside `DeleteCategory`. - Floating "Add" button → opens the same edit dialog with empty fields. - Categories with `isDefault = true` show a lock icon and cannot be deleted. ### 3. `ui/screens/import_bank_statement/ImportBankStatementScreen.kt` + `ImportBankStatementScreenModel.kt` One Voyager `Screen`, one `ScreenModel`, two internal composables switched by `StateFlow<ImportState>`: ```kotlin sealed interface ImportState { data class BankPicker(val importers: List<BankStatementImporter>) : ImportState data class Processing(val bank: String) : ImportState data class Confirmation( val bank: String, val rows: List<PendingImportExpense>, ) : ImportState } ``` **`ImportBankStatementPickerContent`** — rendered when state is `BankPicker` or `Processing`: - List of banks (one row per importer, showing `bankName`). - Tap a bank → launches SAF `ActivityResultContracts.OpenDocument(arrayOf("application/pdf"))` via `rememberLauncherForActivityResult`. On URI returned, sets state to `Processing` and triggers `screenModel.processPdf(uri, importer)`. - When state is `Processing`, render a `CircularProgressIndicator` overlay on top. **`ImportBankStatementConfirmationContent`** — rendered when state is `Confirmation`: - List of rows. Each row: checkbox (`isSelected`), amount, date, description, category dropdown (driven by `getCategories.subscribeAll()`). - "Import X items" button at the bottom. On click: - Filter rows where `isSelected == true`. - For each, build `Expense(amount, categoryId ?: defaultCategoryId, date, note = description, ...)` and call `insertExpenses.awaitAll(...)`. - `navigator.pop()`. ScreenModel injects: `getImporters: List<BankStatementImporter> = inject()`, `getCategories`, `insertExpenses`, `defaultCategory` (resolved via `getCategories.awaitDefault()` lazily). The 3 bank stubs from #4 will return `NotImplementedError`; the ScreenModel should catch that and surface a snackbar / error message — not crash. ### 4. `ui/screens/settings/SettingsScreen.kt` + `SettingsScreenModel.kt` Uses the `PreferenceScreen` composable from `ui/components/preference/`: ```kotlin PreferenceScreen( itemsProvider = { listOf( Preference.PreferenceItem.ListPreference( title = "Theme", preference = appPreference.appTheme(), entries = mapOf( "Light" to AppTheme.LIGHT.name, "Dark" to AppTheme.DARK.name, "System" to AppTheme.SYSTEM.name, ), ), Preference.PreferenceItem.TextPreference( title = "Export CSV", onClick = { /* show date range dialog via ExportAction */ }, ), Preference.PreferenceItem.AlertDialogPreference( title = "Clear all data", message = "This will permanently delete all expenses, categories, and recurring entries. The 8 default categories will be re-seeded.", confirmText = "Clear", onConfirm = { screenModel.clearData() }, ), ) } ) ``` `SettingsScreenModel` injects `clearData: ClearAllData`, `seedDefaultCategories: SeedDefaultCategories`: ```kotlin suspend fun clearData() { clearData.await() seedDefaultCategories.await() } ``` For Export, the simplest is to embed `ExportAction` directly as a row in `PreferenceScreen` (or call its `onClick` from the `TextPreference`). The `ExportAction` composable provides the dialog + SAF launcher; the ScreenModel calls `exportExpensesToCsv.await(range, uri)`. ## Acceptance - [ ] `ExportAction` composable is reusable across screens - [ ] `CategoryScreen` allows add/edit/delete; default categories cannot be deleted - [ ] `ImportBankStatementScreen` shows bank picker, processing overlay, and confirmation view - [ ] `ImportBankStatementScreen` handles `NotImplementedError` gracefully (snackbar/error) - [ ] `SettingsScreen` exposes theme, export, and clear-data preferences - [ ] `ClearAllData` + re-seed works end-to-end from the settings dialog - [ ] `./gradlew assembleDebug` succeeds ## Implementation rule Per `AGENTS.md` — do not start implementation without explicit user sign-off on this issue. When working, check for related issues in the remote repo first.
admin closed this issue 2026-06-28 12:06:48 +00:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/ledgerr#7