# Plan: Add PagedResult Pagination ## Goal Make all list endpoints return `PagedResult` with `page` / `pageSize` query params, bounded by a config-driven max, matching the stwaddle stack API convention. Update the frontend to consume paginated responses. **Prerequisites:** - Plan #10 (project split) — DTOs and request types will live in `Budget.Core`. - Plan #12 (TanStack Query) — query hooks need to be updated to pass page params and handle paginated responses. Doing this before #12 means rewriting the hooks twice. ## Scope Paginate all collection endpoints: - `GET /api/budgets` - `GET /api/budgets/{id}/incomes` - `GET /api/budgets/{id}/outgos` - `GET /api/budgets/{id}/shares` `GET /api/budgets/{id}/summary` is a single-object endpoint — not paginated. `GET /api/budgets/{id}/outgos/categories` and `/payment-sources` return small distinct-value lists — not paginated. ## Steps ### Phase 1 — Backend: shared types and config > If plan #10 is done, add these to `Budget.Core`. Otherwise add to `Budget.Api/DTOs/`. 1. Add `PagedResult` record: ```csharp public record PagedResult(IReadOnlyList Items, int Page, int PageSize, int TotalCount) { public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); } ``` 2. Add pagination config to `appsettings.json`: ```json "Pagination": { "DefaultPageSize": 25, "MaxPageSize": 100 } ``` 3. Register `IOptions` in Program.cs: ```csharp builder.Services.Configure(builder.Configuration.GetSection("Pagination")); ``` with a `PaginationOptions` record/class containing `DefaultPageSize` and `MaxPageSize`. ### Phase 2 — Backend: update list endpoints 4. Inject `IOptions` into each controller that has a list endpoint, or pass it via a helper. 5. Add `int page = 1, int pageSize = 0` parameters to each list action. If `pageSize` is 0 or not provided, use `DefaultPageSize`. Clamp `pageSize` to `MaxPageSize`. 6. Update each list query to use `.Skip((page - 1) * pageSize).Take(pageSize)` and return `PagedResult`: ```csharp var total = await query.CountAsync(); var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); return Ok(new PagedResult(items, page, pageSize, total)); ``` 7. Affected actions: `BudgetsController.List`, `IncomesController.List`, `OutgosController.List`, `SharesController.List`. ### Phase 3 — Frontend: update types 8. Add `PagedResult` to `src/Budget.Client/src/types/index.ts`: ```ts export interface PagedResult { items: T[]; page: number; pageSize: number; totalCount: number; totalPages: number; } ``` ### Phase 4 — Frontend: update query hooks > If plan #12 is done, update the hook files. Otherwise update the `useEffect` fetch calls. 9. Update each list hook to accept a `page` param (default `1`) and return `PagedResult`: ```ts export function useBudgets(page = 1) { return useQuery({ queryKey: ['budgets', page], queryFn: () => api.get>(`/api/budgets?page=${page}`), }); } ``` ### Phase 5 — Frontend: add pagination UI 10. Create a reusable `Paginator` component: ```tsx interface PaginatorProps { page: number; totalPages: number; onPageChange: (p: number) => void; } ``` Renders Prev / page number / Next buttons; disables Prev on page 1, Next on last page. 11. Add `const [page, setPage] = useState(1)` to each list page. 12. Wire `` below each list, passing `data.totalPages` and `setPage`. 13. Reset to page 1 when a new item is created or deleted (in the mutation `onSuccess`). ### Phase 6 — Build and verify 14. `dotnet build` (backend) — zero errors. 15. `npm run build` (frontend) — zero TypeScript errors. 16. Verify with more than `DefaultPageSize` items that next/prev navigation works. ## Key decisions - `page` is 1-indexed throughout (both API and UI), consistent with the stack doc. - `pageSize` can be omitted from the query string; the server uses `DefaultPageSize`. No client-side page size picker in this plan. - The `Incomes` and `Outgos` lists are ordered by `SortOrder` — pagination must preserve this ordering. The `Skip/Take` applies after the `OrderBy`, so this is automatic. - Reorder endpoints (`PUT .../order`) send the full ordered ID list — they are unaffected by pagination since they operate on the whole set. If lists grow very large this becomes a concern, but it is out of scope here. ## Files affected **Backend:** - New: `Budget.Core/DTOs/PagedResult.cs` (or `Budget.Api/DTOs/`) - New: `Budget.Infrastructure/Options/PaginationOptions.cs` (or `Budget.Api/`) - `appsettings.json` - `Program.cs` - `Budget.Api/Controllers/BudgetsController.cs` - `Budget.Api/Controllers/IncomesController.cs` - `Budget.Api/Controllers/OutgosController.cs` - `Budget.Api/Controllers/SharesController.cs` **Frontend:** - `src/Budget.Client/src/types/index.ts` - `src/Budget.Client/src/api/budgets.ts` (or `BudgetsPage.tsx` if #12 not done) - `src/Budget.Client/src/api/incomes.ts` - `src/Budget.Client/src/api/outgos.ts` - `src/Budget.Client/src/api/shares.ts` - New: `src/Budget.Client/src/components/Paginator.tsx` - `src/Budget.Client/src/pages/BudgetsPage.tsx` - `src/Budget.Client/src/pages/IncomePage.tsx` - `src/Budget.Client/src/pages/OutgoPage.tsx` - `src/Budget.Client/src/pages/SettingsPage.tsx`