Skip to content

feat(transactions): Add server-side pagination and filter bar#70

Merged
nfebe merged 3 commits intodevfrom
feat/transactions-filtering-pagination
Apr 6, 2026
Merged

feat(transactions): Add server-side pagination and filter bar#70
nfebe merged 3 commits intodevfrom
feat/transactions-filtering-pagination

Conversation

@nfebe
Copy link
Copy Markdown
Contributor

@nfebe nfebe commented Apr 6, 2026

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.

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.
@sourceant
Copy link
Copy Markdown

sourceant bot commented Apr 6, 2026

Code Review Summary

This 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

  • Replaced 100-item limit with server-side pagination.
  • Centralized filtering logic in TransactionFilters.vue.
  • Implemented a race-condition guard using request IDs in useTransactions.ts.
  • Added support for multi-select wallet and category filtering.

💡 Minor Suggestions

  • Deduplicate filter button styles.
  • Ensure deep cloning of filter props to avoid reference sharing.

Copy link
Copy Markdown

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.


function handleWalletChange(value: unknown) {
if (Array.isArray(value)) {
localFilters.wallet_ids = value.map((w: any) => (typeof w === 'object' ? w.id : w));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }'.

Suggested change
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 () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 6, 2026

Deploying trakli-dev with  Cloudflare Pages  Cloudflare Pages

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

View logs

Copy link
Copy Markdown

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

const month = now.getMonth();

switch (preset) {
case 'this-month':
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 6, 2026

Deploying webui with  Cloudflare Pages  Cloudflare Pages

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

View logs

- 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
Copy link
Copy Markdown

@sourceant sourceant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

<div class="filter-group filter-group--type">
<label class="filter-label">{{ t('Type') }}</label>
<div class="type-toggle">
<button
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
<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 });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}>({ ...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
});

@nfebe nfebe merged commit 1f0c7c6 into dev Apr 6, 2026
5 checks passed
@nfebe nfebe deleted the feat/transactions-filtering-pagination branch April 6, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant