pelagia-portal/App/lib/pagination.ts
Claude (auto-fix) 5cefe8f7ed
All checks were successful
PR checks / checks (pull_request) Successful in 44s
PR checks / integration (pull_request) Successful in 32s
feat(history): paginate PO history with items-per-page control
The /history page fetched a fixed first 200 POs in one flat table with no
way to page further and no control over page size. Replace that with real
pagination:

- page.tsx: read page/perPage from searchParams; clamp perPage to 25/50/100
  (default 25) and page to [1, totalPages] via a new shared resolvePagination
  helper. Swap the fixed take:200 for skip/take + a count() for totals.
  Replace the "first 200 results" notice with a footer ("Showing X-Y of N",
  Prev/Next, page indicator) that preserves all filters. Export PDF/CSV links
  stay on the full filtered set.
- history-filters.tsx: add a Per-page dropdown; changing it or any filter
  resets to page 1 while preserving perPage in the URL.
- lib/pagination.ts: dependency-free clamp/skip/take helper, unit-tested.

Verified: type-check clean, 272 unit tests pass (9 new), skip/take windows
and clamping checked against the test DB.

Fixes #104
2026-06-24 03:26:47 +05:30

47 lines
1.6 KiB
TypeScript

// Shared, dependency-free pagination math used by list pages (e.g. PO History).
// Keeps page-size validation and out-of-range page clamping in one testable place.
export interface PaginationInput {
/** Raw `perPage` value from the query string (may be undefined / invalid). */
perPageParam?: string | number;
/** Raw `page` value from the query string (may be undefined / invalid). */
pageParam?: string | number;
/** Total number of rows matching the current filter. */
total: number;
/** Allowed page sizes; anything else falls back to `defaultPerPage`. */
options: number[];
defaultPerPage: number;
}
export interface Pagination {
perPage: number;
page: number;
totalPages: number;
/** Rows to skip for the current page (Prisma `skip`). */
skip: number;
/** Rows to take for the current page (Prisma `take`). */
take: number;
}
/**
* Resolve a safe page size and (1-based) page number from untrusted query
* params. `perPage` is clamped to the allowed `options`; `page` is clamped to
* `[1, totalPages]` so an out-of-range or non-numeric page never paginates past
* the last page.
*/
export function resolvePagination({
perPageParam,
pageParam,
total,
options,
defaultPerPage,
}: PaginationInput): Pagination {
const perPage = options.includes(Number(perPageParam)) ? Number(perPageParam) : defaultPerPage;
const totalPages = Math.max(1, Math.ceil(total / perPage));
const requested = Number(pageParam);
const page = Math.min(
Math.max(1, Number.isFinite(requested) && requested > 0 ? Math.floor(requested) : 1),
totalPages,
);
return { perPage, page, totalPages, skip: (page - 1) * perPage, take: perPage };
}