Commit Graph

37 Commits

Author SHA1 Message Date
Achmad Setyabudi Susilo b7c9c39862 fix(#41,#42,#43): numeric amount input, clickable DateField, search-based category picker
#41: pipe amount input through sanitizeAmountInput() (digits + single
decimal) in both AddEditExpenseScreenModel and AddEditRecurringScreenModel
so KeyboardType.Decimal is no longer bypassable via paste or hardware keys.

#42: wrap DateField's OutlinedTextField in a Box.clickable so the whole
row (label, text, icon) opens the date picker, not just the trailing icon.

#43: replace CategoryDropdownField with a new CategoryPickerField that
opens a generic ListSearchDialog (search bar, scrollable list, swatch,
checkmark on selected row), decoupled from the Preference machinery.
ListSearchPreferenceWidget is unchanged.
2026-06-28 21:55:33 +07:00
admin 7963ac55e2 Merge pull request 'fix(#25,#26,#28): Expenses screen polish — scrim, tab badges, shared SearchToolbar' (#37) from fix/25-26-28-expenses-screen-polish into main
Reviewed-on: #37
2026-06-28 14:33:43 +00:00
Achmad Setyabudi Susilo d5c0d395c5 Merge origin/main into fix/25-26-28-expenses-screen-polish
Main moved ahead with the home-polish (#29/#32/#33), category HSV color
picker (#30), and category lock-icon fix (#31) PRs. The only conflicts
were in ExpenseListScreen.kt:

- Imports: main added AppBar + EmptyStateIllustration. The branch had
  already removed AppBar (replaced by SearchToolbar) and replaced the
  inline text empty state. Resolution: keep the branch's
  AppBarTitle/SearchToolbar/TabText set, drop AppBar (no longer used),
  add EmptyStateIllustration.
- ExpensesTabContent empty state: main replaced the Box+Text
  'No expenses yet' with EmptyStateIllustration. Resolution: use
  EmptyStateIllustration (consistent with main's new design) but keep
  the branch's three-way message selection (no results vs no data).

For visual consistency between the two tabs, also swap
RecurringTabContent's empty state to EmptyStateIllustration. No
behavioral change for the recurring case — same per-tab vs no-results
message selection, same strings, just rendered through the shared
component.

All other files (build.gradle.kts, libs.versions.toml, HomeScreen,
SettingsScreen, CategoryScreen, EmptyStateIllustration.kt, strings.xml)
auto-merged cleanly.
2026-06-28 21:32:23 +07:00
Achmad Setyabudi Susilo 2f8368d4ef fix(#23): wire AppTheme preference in MainActivity 2026-06-28 21:30:54 +07:00
Achmad Setyabudi Susilo dca79df056 fix(#24): format user-visible dates as dd MMM yyyy 2026-06-28 21:30:32 +07:00
Achmad Setyabudi Susilo 903cd3a70d fix(#27): replace "Pick" TextButton with trailing calendar icon in DateField 2026-06-28 21:30:10 +07:00
admin 5f6de30958 Merge pull request 'feat(#29,#32,#33): home polish — Manage Categories to Settings, View All, empty-state illustration' (#36) from feat/home-polish-29-32-33 into main
Reviewed-on: #36
2026-06-28 14:25:57 +00:00
Achmad Setyabudi Susilo 16236c6d6c fix(#26): replace inline search with SearchToolbar (debounced, shared)
Replace the inline OutlinedTextField inside ExpensesTabContent with
the shared SearchToolbar in the topBar. The query is shared across
both tabs; closing the toolbar (X / navigate-up) deactivates search
and clears the filter.

In ExpenseListScreenModel:
- searchQuery becomes StateFlow<String?> (null = inactive, "" = active
  empty, "foo" = active query)
- setSearchQuery now accepts String? to match SearchToolbar's
  onChangeSearchQuery signature
- expenses and recurring combine on
  _searchQuery.debounce(SEARCH_DEBOUNCE_MILLIS).distinctUntilChanged()
  so fast typing does not re-filter on every keystroke
- recurring is now filtered by query against category name and
  recurring note (case-insensitive substring)

In ExpenseListScreen:
- Remove the inline search field, the date-range-filter search hint
  label, and the now-unused OutlinedTextField / Icons.Outlined.Search
  imports
- Both ExpensesTabContent and RecurringTabContent now take a nullable
  searchQuery and show a generic 'No results' message when the active
  filter empties the list, or the per-tab empty copy otherwise
- Add the expense_list_no_results string
2026-06-28 21:23:13 +07:00
Achmad Setyabudi Susilo f0803431f9 feat(#29,#32,#33): home polish — move Manage Categories to Settings, View All button, empty-state illustration 2026-06-28 21:22:32 +07:00
Achmad Setyabudi Susilo 5953111897 fix(#25): render expense tab counts via TabText
Replace the plain Text inside PrimaryTabRow's Tab with the shared
TabText composable. Pass the current size of the expenses and
recurring state flows as badgeCount so each tab shows a small pill
with the (post-filter) list size. Counts update reactively as
items are added, deleted, or filtered by search/date.
2026-06-28 21:22:09 +07:00
admin 38baeef401 Merge pull request 'feat(#30): replace category swatch grid with HSV color picker' (#35) from feat/30-category-rgb-color-picker into main
Reviewed-on: #35
2026-06-28 14:21:44 +00:00
Achmad Setyabudi Susilo b698f5084f fix(#28): add scrim when ExpandedFab is open
Add ExpandedFabScrim composable that renders a Material 3 scrim
overlay fading in/out in sync with the mini-FABs. Tapping the scrim
dismisses the FAB.

Move the ExpandedFab out of Scaffold's floatingActionButton slot and
into the body inside a Box, so the scrim can match the body size via
matchParentSize() and stack above the list but below the FAB. Add a
BackHandler that dismisses the FAB on system back while it is open.
2026-06-28 21:21:36 +07:00
Achmad Setyabudi Susilo b39ca61cfb feat(#30): replace category swatch grid with HSV color picker 2026-06-28 21:17:21 +07:00
Achmad Setyabudi Susilo 86bea46c30 fix(#31): vertical-align lock icon with trash icon in category rows 2026-06-28 21:09:33 +07:00
admin ba99eac4be Merge pull request 'fix(#21): move DB I/O to Dispatchers.IO in AddEdit screen models' (#22) from feat/21-move-db-io-to-dispatchers-io into main
Reviewed-on: #22
2026-06-28 13:30:20 +00:00
Achmad Setyabudi Susilo 0bf47d5c94 fix(#21): move DB I/O to Dispatchers.IO in AddEdit screen models
Wrap getExpenses.awaitOne, upsertExpense.await, getRecurringExpenses.awaitOne
and upsertRecurringExpense.await in withContext(Dispatchers.IO) { ... } so
the suspending Room calls run off the main thread. State-flow updates stay
inside screenModelScope.launch, which is Main-bound, and execute after the
withContext block returns.
2026-06-28 20:28:46 +07:00
Achmad Setyabudi Susilo 3ddfaa0a22 fix(#5): address PR review — share ExpandedFab, inject GetExpenseSummary, column-chart spec
- Add ui/components/ExpandedFab.kt with ExpandedFab + MiniFab helpers; HomeScreen and ExpenseListScreen both consume it, the tab-collapsing LaunchedEffect in ExpenseListScreen is hoisted to its Content()
- Inject GetExpenseSummary in HomeScreenModel; drive summary via dateRange.flatMapLatest { getExpenseSummary.await(it) } (fixes the period-filter total-card flicker) and drop the inline combine(expenses, dateRange) recomputation
- Hoist isFabExpanded out of HomeScreenModel into HomeScreen.Content() so the FAB state is local to the composable
- Convert HomeScreenModel.exportToCsv from a callback to a suspend fun returning Result<Unit>; the screen does the snackbar dispatch on the coroutineScope
- Consolidate DateRangeOption label mapping to a single DateRangeOption.labelRes() / .labelText() pair (one source of truth)
- Rename FAB string keys to shared fab_manual / fab_import and drop the home_fab_* duplicates
- Update docs/04-implementation-plan.md and .opencode/agent/implementor.md Charts sections to reflect the Vico 2.0.0 column-chart-with-legend substitution (Vico 2.0.0 has no pie layer)
2026-06-28 20:25:53 +07:00
Achmad Setyabudi Susilo a0ccf22e67 feat(#5): implement HomeScreen with Vico dashboard
- Add HomeScreenModel with expenses/summary/recurring-banner/fab state flows and a getExpenses + processDueRecurring + exportExpensesToCsv + expensePreference constructor
- Replace the HomeScreen stub with a Material 3 dashboard: AppBar (Export + Settings), total card, period filter, Vico ColumnCartesianLayer chart with per-category legend, manage-categories/see-all actions, recent expenses, and an expanded FAB exposing Manual + Import sub-actions
- Add home strings and a home_recurring_banner plurals resource
2026-06-28 20:11:01 +07:00
admin 8ce0dcc678 Merge pull request 'Implement ExpenseListScreen, AddEditExpenseScreen, AddEditRecurringScreen (#6)' (#18) from feat/6-implement-expense-list-add-edit-screens into main
Reviewed-on: #18
2026-06-28 12:57:17 +00:00
Achmad Setyabudi Susilo ed8b3577dc chore(#9): enable Room exportSchema and configure schemaLocation 2026-06-28 19:51:33 +07:00
Achmad Setyabudi Susilo e8c7a14c75 fix(#6): address PR review
- matchesQuery: use %.2f format to match the display (was Double.toString, which can use scientific notation and disagree with the row's %.2f)
- Remove unused Switch import in AddEditRecurringScreen (the active toggle uses ToggleItem)
- Extract CategoryDropdownField to ui/components/CategoryDropdownField.kt so both add/edit screens share one implementation; takes label as a parameter
- Remove unused recurring_list_active string
2026-06-28 19:49:02 +07:00
Achmad Setyabudi Susilo b0a62bedf0 feat(#6): implement ExpenseListScreen, AddEditExpenseScreen, AddEditRecurringScreen 2026-06-28 19:36:20 +07:00
Achmad Setyabudi Susilo 22863bfcd6 fix(#7): emit snackbar as resource id, resolve with stringResource in UI
Drop the Context injection from the ScreenModel. Emit a sealed
ImportSnackbarMessage carrying either a @StringRes id or a dynamic
text (for the error.message case where the bank importer surfaces a
useful 'BRI import not yet implemented' string). The screen resolves
resource ids via stringResource() in Composable scope and shows the
snackbar; the original error.message info is preserved via the
Dynamic variant instead of being dropped.
2026-06-28 18:46:17 +07:00
Achmad Setyabudi Susilo 5893ffc955 fix(#7): localize import snackbar strings and drop class.simpleName fallback
The three snackbar strings in ImportBankStatementScreenModel were
hardcoded English while the rest of the app uses stringResource. Move
them to res/values/strings.xml. Also drop the
error::class.simpleName fallback in the failure branch — it surfaced
Kotlin class names like NotImplementedError to end users. Now falls
back to the localized 'Import failed' string.
2026-06-28 18:32:58 +07:00
Achmad Setyabudi Susilo ce01c175df fix(#7): use UTC for DateField initial seed to match pick conversion
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).
2026-06-28 18:27:49 +07:00
Achmad Setyabudi Susilo 7782df8b36 fix(#7): move DB/IO off main thread in ScreenModels
screenModelScope is backed by PlatformMainDispatcher (Main.immediate),
so direct interactor calls run DB queries and file I/O on the UI
thread. Switch reactive flows with .flowOn(Dispatchers.IO) and wrap
suspend calls in withContext(Dispatchers.IO).
2026-06-28 18:10:35 +07:00
Achmad Setyabudi Susilo f6860544e4 feat(#7): implement CategoryScreen, ImportBankStatementScreen, SettingsScreen, and ExportAction helper 2026-06-28 17:55:22 +07:00
Achmad Setyabudi Susilo 94d40d4216 feat(#4): implement bankstatement, export, and data interactors 2026-06-28 17:37:04 +07:00
Achmad Setyabudi Susilo ca2992fceb Merge branch 'main' of https://git.achmad.dev/admin/ledgerr into feat/2-implement-expense-interactors
# Conflicts:
#	app/src/main/java/dev/achmad/ledgerr/di/DomainModule.kt
2026-06-28 16:57:58 +07:00
Achmad Setyabudi Susilo 63bfe2a6b5 fix(#2): address PR review — use row mapper, require id==0, drop ignoreCase on amount, doc drift, orphan-category note 2026-06-28 16:52:42 +07:00
Achmad Setyabudi Susilo 7bb65025a2 fix(#3): make ProcessDueRecurringExpenses atomic via withTransaction
Address review: insert + advance pair must run in one DB transaction to
prevent duplicate expenses if the process is killed mid-loop.
2026-06-28 16:52:07 +07:00
Achmad Setyabudi Susilo 6a11284212 feat(#3): implement recurring interactors 2026-06-28 16:33:41 +07:00
Achmad Setyabudi Susilo 46f882b3c3 feat(#2): implement expense interactors 2026-06-28 16:33:39 +07:00
Achmad Setyabudi Susilo 547343992a fix(#1): address PR review — split upsert into insert/update, add @Transaction, runBlocking seed, trailing newline
Per review on PR #8 (#8):

- Split @Upsert into @Insert(OnConflictStrategy.REPLACE) + @Update in all 3 DAOs.
  @Upsert returns -1 on the update path, so callers wanting the row ID would
  get a junk value. Interactors now call insert vs update based on id == 0.
  UpsertCategory returns category.id explicitly for the id != 0 branch.
- Add @Transaction to the 3 @Relation queries (ExpenseDao.subscribeAll,
  ExpenseDao.subscribeByDateRange, RecurringExpenseDao.subscribeAll). This
  silences the KSP warnings the PR body mentioned and makes the intent
  explicit.
- Switch MainApplication seeding from a fire-and-forget CoroutineScope to
  runBlocking(Dispatchers.IO). A fast first-tap on HomeScreen could otherwise
  call GetCategories.awaitDefault() before seeding completed and crash.
- Add documenting comment on CategoryDao.getDefault() noting that the
  'only one isDefault = 1' invariant is maintained at the interactor layer
  (partial unique index would be a v2 migration).
- Add trailing newline to app/build.gradle.kts.
- File follow-up issue #9 for flipping exportSchema to true before any v2
  migration lands.
2026-06-28 16:09:15 +07:00
Achmad Setyabudi Susilo 8e7a6cfe3f feat(#1): implement category feature and wire DI foundation
- Add Room 2.7.1, PDFBox-Android 2.0.27.0, Vico 2.0.0 dependencies
- Bump compileSdk 36 -> 37 (transitive deps require it)
- Add LocalDateConverter + 3 entities + 3 DAOs with @Relation rows + mappers + AppDatabase v1
- Add all domain models (expense, category, recurring, bankstatement, preference)
- Implement 4 category interactors: GetCategories, UpsertCategory, DeleteCategory, SeedDefaultCategories
- Add minimal ReassignExpenseCategory (full expense interactors come in #2)
- Wire CoreModule (SharedPreferences + AndroidPreferenceStore), DataModule (Room + 3 DAOs), PreferenceModule, DomainModule (category factories)
- Create MainApplication with Koin start and SeedDefaultCategories trigger
- Register MainApplication in AndroidManifest
- Add missing string resources for pre-existing copied UI components (confirm, cancel, ok, general_selected, general_not_selected, disabled)
2026-06-28 15:53:58 +07:00
Achmad Setyabudi Susilo 3e30423083 docs: add architecture docs, AGENTS.md, CLAUDE.md, and copy UI components
- Add docs/01-04 covering data model, interfaces, function TODOs, and implementation plan
- Add AGENTS.md with project conventions, architecture rules, feature workflow, and git policy
- Add CLAUDE.md pointing to AGENTS.md
- Copy UI components and utils from info-krl-android (preference widgets, scrollbars, etc.)
- Delete obsolete UiModule.kt
2026-06-28 15:08:37 +07:00
Achmad Setyabudi Susilo dfca375a9b chore: setup project 2026-06-28 13:21:57 +07:00