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
47 lines
1.6 KiB
TypeScript
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 };
|
|
}
|