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>
5.4 KiB
5.4 KiB
Plan: Add PagedResult 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/budgetsGET /api/budgets/{id}/incomesGET /api/budgets/{id}/outgosGET /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 toBudget.Api/DTOs/.
- Add
PagedResult<T>record:public record PagedResult<T>(IReadOnlyList<T> Items, int Page, int PageSize, int TotalCount) { public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); } - Add pagination config to
appsettings.json:"Pagination": { "DefaultPageSize": 25, "MaxPageSize": 100 } - Register
IOptions<PaginationOptions>in Program.cs:with abuilder.Services.Configure<PaginationOptions>(builder.Configuration.GetSection("Pagination"));PaginationOptionsrecord/class containingDefaultPageSizeandMaxPageSize.
Phase 2 — Backend: update list endpoints
- Inject
IOptions<PaginationOptions>into each controller that has a list endpoint, or pass it via a helper. - Add
int page = 1, int pageSize = 0parameters to each list action. IfpageSizeis 0 or not provided, useDefaultPageSize. ClamppageSizetoMaxPageSize. - Update each list query to use
.Skip((page - 1) * pageSize).Take(pageSize)and returnPagedResult<T>: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)); - Affected actions:
BudgetsController.List,IncomesController.List,OutgosController.List,SharesController.List.
Phase 3 — Frontend: update types
- Add
PagedResult<T>tosrc/Budget.Client/src/types/index.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
useEffectfetch calls.
- Update each list hook to accept a
pageparam (default1) and returnPagedResult<T>: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
- Create a reusable
Paginatorcomponent:Renders Prev / page number / Next buttons; disables Prev on page 1, Next on last page.interface PaginatorProps { page: number; totalPages: number; onPageChange: (p: number) => void; } - Add
const [page, setPage] = useState(1)to each list page. - Wire
<Paginator>below each list, passingdata.totalPagesandsetPage. - Reset to page 1 when a new item is created or deleted (in the mutation
onSuccess).
Phase 6 — Build and verify
dotnet build(backend) — zero errors.npm run build(frontend) — zero TypeScript errors.- Verify with more than
DefaultPageSizeitems that next/prev navigation works.
Key decisions
pageis 1-indexed throughout (both API and UI), consistent with the stack doc.pageSizecan be omitted from the query string; the server usesDefaultPageSize. No client-side page size picker in this plan.- The
IncomesandOutgoslists are ordered bySortOrder— pagination must preserve this ordering. TheSkip/Takeapplies after theOrderBy, 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(orBudget.Api/DTOs/) - New:
Budget.Infrastructure/Options/PaginationOptions.cs(orBudget.Api/) appsettings.jsonProgram.csBudget.Api/Controllers/BudgetsController.csBudget.Api/Controllers/IncomesController.csBudget.Api/Controllers/OutgosController.csBudget.Api/Controllers/SharesController.cs
Frontend:
src/Budget.Client/src/types/index.tssrc/Budget.Client/src/api/budgets.ts(orBudgetsPage.tsxif #12 not done)src/Budget.Client/src/api/incomes.tssrc/Budget.Client/src/api/outgos.tssrc/Budget.Client/src/api/shares.ts- New:
src/Budget.Client/src/components/Paginator.tsx src/Budget.Client/src/pages/BudgetsPage.tsxsrc/Budget.Client/src/pages/IncomePage.tsxsrc/Budget.Client/src/pages/OutgoPage.tsxsrc/Budget.Client/src/pages/SettingsPage.tsx