feat(transactions): Add server-side pagination and filter bar#70
Conversation
Replaces the 100-transaction hard limit with server-side pagination and adds a filter panel (type, date range with presets, wallets, categories, search) that auto-applies on change. The filter toggle sits next to the existing search input with a count badge for active filters. Totals now reflect the entire filtered set across all pages (server computed) rather than only the current page. Distinguishes between no-transactions-yet (onboarding) and no-results-for-filters empty states.
Code Review SummaryThis PR successfully transitions the transaction view from a limited client-side filter to a scalable server-side pagination and filtering system. The UI remains responsive while handling larger datasets. 🚀 Key Improvements
💡 Minor Suggestions
|
|
|
||
| function handleWalletChange(value: unknown) { | ||
| if (Array.isArray(value)) { | ||
| localFilters.wallet_ids = value.map((w: any) => (typeof w === 'object' ? w.id : w)); |
There was a problem hiding this comment.
Using 'any' type here bypasses TypeScript's safety. Since you have access to the 'wallets' from 'useSharedData', you should use the Wallet type or at least 'number | { id: number }'.
| localFilters.wallet_ids = value.map((w: any) => (typeof w === 'object' ? w.id : w)); | |
| localFilters.wallet_ids = value.map((w: { id: number } | number) => (typeof w === 'object' ? w.id : w)); |
|
|
||
| const updateSearch = (query: string) => { | ||
| if (searchDebounceTimer) clearTimeout(searchDebounceTimer); | ||
| searchDebounceTimer = setTimeout(async () => { |
There was a problem hiding this comment.
There is a potential race condition here. If multiple searches are triggered rapidly, older requests might resolve after newer ones. It's better to store a 'currentRequestPromise' or use an AbortController to cancel previous inflight API calls.
| searchDebounceTimer = setTimeout(async () => { | |
| searchDebounceTimer = setTimeout(async () => { | |
| filters.value = { ...filters.value, search: query || undefined }; | |
| currentPage.value = 1; | |
| // Implement abort logic or ensure only the latest request's results are applied | |
| await fetchTransactionsFromApi(); | |
| }, 400); |
Deploying trakli-dev with
|
| Latest commit: |
2618c9d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://041466e6.trakli-dev.pages.dev |
| Branch Preview URL: | https://feat-transactions-filtering.trakli-dev.pages.dev |
| const month = now.getMonth(); | ||
|
|
||
| switch (preset) { | ||
| case 'this-month': |
There was a problem hiding this comment.
The date preset logic relies on the local system time which can cause inconsistencies if the user and server are in significantly different timezones. Additionally, the month calculation for 'this-month' and 'last-3-months' could be simplified using a helper to avoid manual string padding.
| case 'this-month': | |
| case 'this-month': { | |
| const firstDay = new Date(year, month, 1); | |
| const lastDay = new Date(year, month + 1, 0); | |
| localFilters.date_from = firstDay.toISOString().split('T')[0]; | |
| localFilters.date_to = lastDay.toISOString().split('T')[0]; | |
| break; | |
| } | |
| case 'last-3-months': { | |
| const threeMonthsAgo = new Date(year, month - 2, 1); | |
| localFilters.date_from = threeMonthsAgo.toISOString().split('T')[0]; | |
| localFilters.date_to = now.toISOString().split('T')[0]; | |
| break; | |
| } |
Deploying webui with
|
| Latest commit: |
2618c9d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://64a85f6c.webui-9fh.pages.dev |
| Branch Preview URL: | https://feat-transactions-filtering.webui-9fh.pages.dev |
- Fix prettier formatting in TransactionFilters and TransactionsContentSection - Replace 'any' types in wallet/category change handlers with a shared IdOrObject type and extractIds helper - Simplify 'this-month' date preset and use a local-time YYYY-MM-DD formatter to avoid UTC drift near month/year boundaries - Centralize activeFilterCount as a computed in useTransactions instead of duplicating the logic in the parent component - Guard fetchTransactionsFromApi against race conditions by tagging each request with a monotonic id and discarding stale responses
| <div class="filter-group filter-group--type"> | ||
| <label class="filter-label">{{ t('Type') }}</label> | ||
| <div class="type-toggle"> | ||
| <button |
There was a problem hiding this comment.
The type filter buttons currently trigger an API call on every click even if the type hasn't changed. Adding a check to see if the value is different before emitting would prevent unnecessary network requests.
| <button | |
| <button | |
| class="type-btn" | |
| :class="{ active: !localFilters.type }" | |
| @click="localFilters.type !== undefined && setType(undefined)" | |
| > | |
| {{ t('All') }} | |
| </button> | |
| <button | |
| class="type-btn type-btn--income" | |
| :class="{ active: localFilters.type === 'income' }" | |
| @click="localFilters.type !== 'income' && setType('income')" | |
| > | |
| {{ t('Income') }} | |
| </button> | |
| <button | |
| class="type-btn type-btn--expense" | |
| :class="{ active: localFilters.type === 'expense' }" | |
| @click="localFilters.type !== 'expense' && setType('expense')" | |
| > | |
| {{ t('Expenses') }} | |
| </button> |
| wallet_ids?: number[]; | ||
| category_ids?: number[]; | ||
| search?: string; | ||
| }>({ ...props.filters }); |
There was a problem hiding this comment.
Using { ...props.filters } only performs a shallow copy. If wallet_ids or category_ids (arrays) are modified in localFilters, it might inadvertently mutate the parent state or cause reactivity issues if the reference is shared. A deep clone or explicit array spread is safer.
| }>({ ...props.filters }); | |
| }>({ | |
| ...props.filters, | |
| wallet_ids: props.filters.wallet_ids ? [...props.filters.wallet_ids] : undefined, | |
| category_ids: props.filters.category_ids ? [...props.filters.category_ids] : undefined | |
| }); |
Replaces the 100-transaction hard limit with server-side pagination and adds a filter panel (type, date range with presets, wallets, categories, search) that auto-applies on change. The filter toggle sits next to the existing search input with a count badge for active filters.
Totals now reflect the entire filtered set across all pages (server computed) rather than only the current page. Distinguishes between no-transactions-yet (onboarding) and no-results-for-filters empty states.