Implement CategoryScreen, ImportBankStatementScreen, SettingsScreen, and ExportAction helper (#7) #17
Reference in New Issue
Block a user
Delete Branch "feat/7-implement-screens-export"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
ui/components/ExportAction.kt— shared date-range dialog + SAFCreateDocumentlauncher, withExportAction(icon button) andExportPreferenceAction(text row) variants backed by the same controllerCategoryScreen(add/edit/delete with color picker; default categories are locked) +CategoryScreenModelImportBankStatementScreen(1 Voyager Screen + 2 composables) +ImportBankStatementScreenModelwith sealedImportState;NotImplementedErrorfrom bank stubs surfaces as a snackbar and resets the pickerSettingsScreen(theme + export + clear-data) usingPreferenceScreen, andSettingsScreenModelthat chainsClearAllData+ re-seedsSeedDefaultCategoriesDispatchers.IOinstead ofDispatchers.Main.immediate(flow sources use.flowOn(Dispatchers.IO); suspend calls usewithContext(Dispatchers.IO))Test plan
./gradlew :app:assembleDebugsucceeds./gradlew :app:testDebugUnitTestsucceedsCloses #7
Summary
Verdict: has one blocking concern, plus non-blocking notes.
The PR is well-structured and follows the spec from issue #7: the four pieces (
ExportAction,CategoryScreen,ImportBankStatementScreen,SettingsScreen) are all in place, the Voyager + Koin ScreenModel pattern is used correctly, no ScreenModel is registered in DI, no repository layer is introduced, no manifest permissions are added, and no chart library is used. TheLazyVerticalGrid+LazyColumnco-existence inCategoryScreenis handled with an aliased import, which is the right call../gradlew assembleDebugand:app:testDebugUnitTestare reported green. All cross-references (Category.DEFAULT_COLOR_*,ui.util.onClickInput,Preference.PreferenceItem.CustomPreference/AlertDialogPreference,AppBar,DateRange.thisMonth()) resolve to existing code.Blocking
ExportAction.kt:199—DateFieldmixesZoneId.systemDefault()(line 188, initial seed) andZoneId.of("UTC")(line 199, pick →LocalDate). For any user in a non-UTC zone, the picker and the field display different days, and confirming without re-picking silently shifts the export range. See the inline comment for the fix.Non-blocking notes (out of scope for #7, not blocking merge)
ImportBankStatementScreenModel.kt:69, 77, 116— three snackbar strings are hardcoded English ("No transactions found in PDF", theerror::class.simpleNamefallback, "No items selected") while the rest of the file usesstringResource. Worth aligning for localization parity.ImportBankStatementScreenModel.kt:77— falling back toerror::class.simpleNamecan surface Kotlin class names (e.g.NotImplementedError) to the user. A generic "Import failed" fallback is friendlier.SettingsScreenModel.kt:21, 30—exportToCsvandclearDatatake a(Result<Unit>) -> Unitcallback rather than returning a suspendResult<Unit>or aSharedFlow<Result<Unit>>. It works, but it's the only callback-style ScreenModel API in the codebase. If a second screen starts needing the same pattern, aFlow-based result channel would be more consistent.Test plan
The PR body correctly notes that the three manual test items (Categories CRUD, Import flow, Settings flow) are left unchecked — the implementor has no device available, and the user triages those at review/merge time. The
assembleDebug+testDebugUnitTestchecks being pre-ticked is the right call given the diff is mostly Compose UI + state plumbing.@@ -0,0 +196,4 @@onClick = {datePickerState.selectedDateMillis?.let { millis ->val picked = Instant.ofEpochMilli(millis).atZone(ZoneId.of("UTC"))Blocking:
DateFieldis inconsistent about time zones. Line 188 seedsinitialMilliswithZoneId.systemDefault()(so the OutlinedTextField shows the date in the user's zone), but line 199 converts the pick back withZoneId.of("UTC"). The ComposeDatePickerState.selectedDateMillisis documented as "UTC milliseconds at the start of the day for the date in the user's time zone" — i.e. the picker encodes the picked day as midnight UTC of that day. Concretely, for a user in UTC+7 with the field seeded to2025-01-15:initialMillis = 2025-01-15T00:00:00+07:00 = 2025-01-14T17:00:00ZatZone(UTC).toLocalDate()returns 2025-01-14startDate/endDateare written as 2025-01-14, silently shifting the export range by a day in the common case.Fix: pick one zone and apply it on both sides. The simpler change is
ZoneId.of("UTC")on line 188 instead ofZoneId.systemDefault()— that way both seed and read agree on UTC and the picker/field always match. (UsingsystemDefault()on the read side would work too, but introduces a second zone dependency that's harder to reason about.)DateField seeded the picker with ZoneId.systemDefault() but converted the selectedDateMillis back with ZoneId.of("UTC"). For any non-UTC user, the field and the picker displayed different days, and confirming without re-picking silently shifted the export range by a day. Use UTC on both sides (the DatePickerState contract is that selectedDateMillis is UTC midnight of the picked day).