087fbdd176
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>
146 lines
5.4 KiB
Markdown
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`
|