Expenses screen: replace inline search with SearchToolbar #26

Closed
opened 2026-06-28 13:41:24 +00:00 by admin · 0 comments
Owner

Enhancement / bug

The Expenses screen implements search via an OutlinedTextField placed as the first item inside the LazyColumn (app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt:259-272). This is wrong on two counts:

  1. The field scrolls away with the list, so the user loses it once they've scrolled.
  2. It does not use the shared SearchToolbar composable defined in app/src/main/java/dev/achmad/ledgerr/ui/components/AppBar.kt:306.

Expected behavior

Replace the inline search field with the SearchToolbar composable:

  • Wire SearchToolbar into the topBar slot of the Scaffold in ExpenseListScreen (replacing the current AppBar for the Expenses/Recurring tab content).
  • Pass searchQuery = screenModel.searchQuery.collectAsState().value and onChangeSearchQuery = screenModel::setSearchQuery.
  • Keep the existing toolbar actions (currently ExportAction) via the actions = { … } parameter of SearchToolbar.
  • Remove the inline OutlinedTextField from ExpensesTabContent and remove the now-unused OutlinedTextField import. (Icons.Outlined.Search is not removed — it is still used by SearchToolbar.)
  • The search input should also be debounced using the existing SEARCH_DEBOUNCE_MILLIS constant in AppBar.kt:65 (wire via the screen model so a fast typist doesn't re-query on every keystroke — confirm whether the current setSearchQuery already debounces; if not, add it).

Search semantics

The search query must filter both tabs:

  • Expenses tab — filter expenses by searchQuery matching (case-insensitive, substring) against the category name and the expense note. (The current inline search already does this — keep that behavior.)
  • Recurring tab — filter recurring by searchQuery matching (case-insensitive, substring) against the category name and the recurring note. (Add the filter to RecurringTabContent / the screen model — note that recurring may not currently have a note column; if not, filter on category name only.)
  • The date-range filter (DateRangeFilter) continues to apply only to the Expenses tab; it does not affect Recurring.
  • The search query is shared across both tabs (it's one SearchToolbar); switching tabs preserves the active query and the filtered list on the other tab.

Acceptance criteria

  • Tapping the search icon in the top bar toggles a search field in the toolbar.
  • Typing a query filters the visible list on whichever tab is currently selected, in real time (or after debounce).
  • Switching tabs while a query is active shows the filtered list for the other tab — the query is not reset.
  • The badge counts (see #25) reflect the filtered counts on the currently visible tab. If both tabs are filtered by the same query, the Expenses badge shows the filtered expense count and the Recurring badge shows the filtered recurring count.
  • Tapping the close (X) icon clears the query, restores both lists to their unfiltered state, and exits search mode.
  • When the filtered list is empty, the existing empty-state copy is shown (per-tab copy if available, otherwise a generic "no results").
**Enhancement / bug** The Expenses screen implements search via an `OutlinedTextField` placed as the first item inside the `LazyColumn` (`app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt:259-272`). This is wrong on two counts: 1. The field scrolls away with the list, so the user loses it once they've scrolled. 2. It does not use the shared `SearchToolbar` composable defined in `app/src/main/java/dev/achmad/ledgerr/ui/components/AppBar.kt:306`. **Expected behavior** Replace the inline search field with the `SearchToolbar` composable: - Wire `SearchToolbar` into the `topBar` slot of the `Scaffold` in `ExpenseListScreen` (replacing the current `AppBar` for the Expenses/Recurring tab content). - Pass `searchQuery = screenModel.searchQuery.collectAsState().value` and `onChangeSearchQuery = screenModel::setSearchQuery`. - Keep the existing toolbar actions (currently `ExportAction`) via the `actions = { … }` parameter of `SearchToolbar`. - Remove the inline `OutlinedTextField` from `ExpensesTabContent` and remove the now-unused `OutlinedTextField` import. (`Icons.Outlined.Search` is **not** removed — it is still used by `SearchToolbar`.) - The search input should also be debounced using the existing `SEARCH_DEBOUNCE_MILLIS` constant in `AppBar.kt:65` (wire via the screen model so a fast typist doesn't re-query on every keystroke — confirm whether the current `setSearchQuery` already debounces; if not, add it). **Search semantics** The search query must filter **both** tabs: - **Expenses tab** — filter `expenses` by `searchQuery` matching (case-insensitive, substring) against the category name and the expense note. (The current inline search already does this — keep that behavior.) - **Recurring tab** — filter `recurring` by `searchQuery` matching (case-insensitive, substring) against the category name and the recurring note. (Add the filter to `RecurringTabContent` / the screen model — note that recurring may not currently have a `note` column; if not, filter on category name only.) - The date-range filter (`DateRangeFilter`) continues to apply only to the **Expenses** tab; it does not affect `Recurring`. - The search query is **shared** across both tabs (it's one `SearchToolbar`); switching tabs preserves the active query and the filtered list on the other tab. **Acceptance criteria** - Tapping the search icon in the top bar toggles a search field in the toolbar. - Typing a query filters the visible list on whichever tab is currently selected, in real time (or after debounce). - Switching tabs while a query is active shows the filtered list for the other tab — the query is not reset. - The badge counts (see #25) reflect the **filtered** counts on the currently visible tab. If both tabs are filtered by the same query, the Expenses badge shows the filtered expense count and the Recurring badge shows the filtered recurring count. - Tapping the close (X) icon clears the query, restores both lists to their unfiltered state, and exits search mode. - When the filtered list is empty, the existing empty-state copy is shown (per-tab copy if available, otherwise a generic "no results").
admin closed this issue 2026-06-28 14:34:21 +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#26