Files
budget/.plans/19-pagination.md
Spencer Twaddle 087fbdd176 Split into Budget.Core / Budget.Infrastructure / Budget.Api projects
Budget.Core: entities, DTOs, enums, FrequencyCalculator (no EF/ASP.NET deps)
Budget.Infrastructure: AppDbContext, migrations, BudgetAuthorizationService
Budget.Api: controllers, middleware, Program.cs — references both projects

EF and Npgsql packages moved to Infrastructure; Api retains only JwtBearer,
HealthChecks, and EF.Design (needed for dotnet ef CLI). Dockerfile updated
to copy all three project directories before publishing. Migration namespaces
updated from Budget.Api.Data.* to Budget.Infrastructure.Data.* and model
type strings updated to Budget.Core.Models.* in the snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:30:31 -05:00

146 lines
5.4 KiB
Markdown

# Plan: Add PagedResult<T> Pagination
## Goal
Make all list endpoints return `PagedResult<T>` 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<T>` record:
```csharp
public record PagedResult<T>(IReadOnlyList<T> 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<PaginationOptions>` in Program.cs:
```csharp
builder.Services.Configure<PaginationOptions>(builder.Configuration.GetSection("Pagination"));
```
with a `PaginationOptions` record/class containing `DefaultPageSize` and `MaxPageSize`.
### Phase 2 — Backend: update list endpoints
4. Inject `IOptions<PaginationOptions>` 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<T>`:
```csharp
var total = await query.CountAsync();
var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
return Ok(new PagedResult<BudgetDto>(items, page, pageSize, total));
```
7. Affected actions: `BudgetsController.List`, `IncomesController.List`,
`OutgosController.List`, `SharesController.List`.
### Phase 3 — Frontend: update types
8. Add `PagedResult<T>` to `src/Budget.Client/src/types/index.ts`:
```ts
export interface PagedResult<T> {
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<T>`:
```ts
export function useBudgets(page = 1) {
return useQuery({
queryKey: ['budgets', page],
queryFn: () => api.get<PagedResult<BudgetDto>>(`/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 `<Paginator>` 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`