paginate wagons, locomotives and routes lists server-side

This commit is contained in:
Marshal
2026-07-27 21:27:36 +00:00
parent 496c66017f
commit 481878e553
7 changed files with 49 additions and 44 deletions

View File

@@ -72,16 +72,7 @@ export class ListWagonsQueryDto {
@Min(1) @Min(1)
page?: number; page?: number;
@ApiPropertyOptional({ minimum: 1, maximum: 500 }) @ApiPropertyOptional({ default: 10, minimum: 1, maximum: 100 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(500)
limit?: number;
/** Page size for `GET /wagons/paged`; the legacy `limit` still drives `GET /wagons`. */
@ApiPropertyOptional({ default: 20, minimum: 1, maximum: 100 })
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsInt() @IsInt()

View File

@@ -39,19 +39,13 @@ export class WagonsController {
@Get() @Get()
@StaffReference() @StaffReference()
@ApiOperation({ summary: 'List all wagons' }) @ApiOperation({
summary: 'List wagons, paginated ({items, meta}) — 10 per page by default',
})
findAll(@Query() query: ListWagonsQueryDto) { findAll(@Query() query: ListWagonsQueryDto) {
return this.wagonsService.findAll(query); return this.wagonsService.findAll(query);
} }
// Must be declared before @Get(':id') so the path isn't captured as an id.
@Get('paged')
@StaffReference()
@ApiOperation({ summary: 'List wagons, paginated ({items, meta})' })
findAllPaged(@Query() query: ListWagonsQueryDto) {
return this.wagonsService.findAllPaged(query);
}
@Get(':id') @Get(':id')
@StaffReference() @StaffReference()
@ApiOperation({ summary: 'Get a wagon by ID' }) @ApiOperation({ summary: 'Get a wagon by ID' })

View File

@@ -115,20 +115,13 @@ export class WagonsService {
return qb; return qb;
} }
async findAll(query: ListWagonsQueryDto = {}): Promise<Wagon[]> { /**
const qb = this.buildListQuery(query); * The wagon list is always a page. Callers that genuinely need every row
* (yard workspace, coupling pickers) walk the pages client-side — see
if (query.page && query.limit) { * `wagonService.listAll` in the backoffice.
qb.skip((Number(query.page) - 1) * Number(query.limit)); */
} findAll(query: ListWagonsQueryDto = {}): Promise<PaginatedResponse<Wagon>> {
if (query.limit) qb.take(Number(query.limit)); return paginateQuery(this.buildListQuery(query), query, { defaultPageSize: 10 });
return qb.getMany();
}
/** Same filters as `findAll`, on the shared `{items, meta}` list envelope. */
findAllPaged(query: ListWagonsQueryDto = {}): Promise<PaginatedResponse<Wagon>> {
return paginateQuery(this.buildListQuery(query), query);
} }
async findById(id: string): Promise<Wagon> { async findById(id: string): Promise<Wagon> {

View File

@@ -155,9 +155,15 @@ const FleetResourcePage = () => {
const { data: cargoTypes = [], isLoading: cargoTypesLoading } = useQuery( const { data: cargoTypes = [], isLoading: cargoTypesLoading } = useQuery(
api.cargoTypes.list.queryOptions({ staleTime: Infinity }), api.cargoTypes.list.queryOptions({ staleTime: Infinity }),
); );
const { data: wagons = [], isLoading: wagonsLoading } = useQuery( // Whole-fleet list for the "Wagon" form select — page-walked, so only fetch it
api.wagons.list.queryOptions({ input: {} }), // where a form actually offers that select (containers), not on every slug.
const needsWagonOptions = Boolean(
config?.formFields.some((field) => field.dynamicOptions === "wagons"),
); );
const { data: wagons = [], isLoading: wagonsLoading } = useQuery({
...api.wagons.list.queryOptions({ input: {} }),
enabled: needsWagonOptions,
});
const { data: containers = [], isLoading: containersLoading } = useQuery( const { data: containers = [], isLoading: containersLoading } = useQuery(
api.containers.list.queryOptions(), api.containers.list.queryOptions(),
); );

View File

@@ -1632,17 +1632,25 @@ export const api = {
}, },
wagons: { wagons: {
/** Every match, page-walked — for pickers and yard views. Lists use `listPaged`. */
list: endpoint<{ filters?: WagonListFilters }, Wagon[]>( list: endpoint<{ filters?: WagonListFilters }, Wagon[]>(
"wagons", "wagons",
"list", "list",
({ filters }) => wagonService.getAll(filters ?? {}).then((r) => r.data), ({ filters }) => wagonService.listAll(filters ?? {}),
({ filters }) => ["wagons", "list", filters ?? {}], ({ filters }) => ["wagons", "list", filters ?? {}],
), ),
listPaged: endpoint<{ filters?: WagonListFilters }, PaginatedResponse<Wagon>>(
"wagons",
"listPaged",
({ filters }) => wagonService.getAll(filters ?? {}).then((r) => r.data),
({ filters }) => ["wagons", "listPaged", filters ?? {}],
),
listByTrain: endpoint<{ trainId: string }, Wagon[]>( listByTrain: endpoint<{ trainId: string }, Wagon[]>(
"wagons", "wagons",
"listByTrain", "listByTrain",
({ trainId }) => wagonService.getByTrain(trainId).then((r) => r.data), ({ trainId }) => wagonService.getByTrain(trainId),
({ trainId }) => ["wagons", "train", trainId], ({ trainId }) => ["wagons", "train", trainId],
), ),

View File

@@ -23,7 +23,7 @@ const listHandlers: Record<
> = { > = {
locomotives: (filters) => locomotivesService.getAll(filters ?? {}).then((r) => r.data), locomotives: (filters) => locomotivesService.getAll(filters ?? {}).then((r) => r.data),
trains: () => trainService.getAll().then((r) => r.data), trains: () => trainService.getAll().then((r) => r.data),
wagons: (filters) => wagonService.getAll(filters ?? {}).then((r) => r.data), wagons: (filters) => wagonService.listAll(filters ?? {}),
containers: () => containerService.getAll().then((r) => r.data), containers: () => containerService.getAll().then((r) => r.data),
cargoes: () => cargoService.getAll().then((r) => r.data), cargoes: () => cargoService.getAll().then((r) => r.data),
vehicles: (filters) => vehiclesService.getAll(filters ?? {}).then((r) => r.data), vehicles: (filters) => vehiclesService.getAll(filters ?? {}).then((r) => r.data),
@@ -38,7 +38,7 @@ const pagedHandlers: Partial<
Record<FleetResourceSlug, (filters: FleetListFilters) => Promise<PaginatedResponse<FleetRecord>>> Record<FleetResourceSlug, (filters: FleetListFilters) => Promise<PaginatedResponse<FleetRecord>>>
> = { > = {
locomotives: (filters) => locomotivesService.getPaged(filters).then((r) => r.data), locomotives: (filters) => locomotivesService.getPaged(filters).then((r) => r.data),
wagons: (filters) => wagonService.getPaged(filters).then((r) => r.data), wagons: (filters) => wagonService.getAll(filters).then((r) => r.data),
}; };
export const isFleetServerPaginated = (slug: FleetResourceSlug): boolean => export const isFleetServerPaginated = (slug: FleetResourceSlug): boolean =>

View File

@@ -95,15 +95,28 @@ export interface WagonMovementRecord {
} }
export const wagonService = { export const wagonService = {
/** One page ({items, meta}); 10 rows unless `pageSize` says otherwise. */
getAll: (filters: WagonListFilters = {}) => getAll: (filters: WagonListFilters = {}) =>
apiClient.get<Wagon[]>(`/wagons${wagonListQuery(filters)}`), apiClient.get<PaginatedResponse<Wagon>>(`/wagons${wagonListQuery(filters)}`),
/** Same filters as `getAll`, server-paginated ({items, meta}). */ /**
getPaged: (filters: WagonListFilters = {}) => * Every matching wagon, page-walked at the API's 100-row cap. For the pickers
apiClient.get<PaginatedResponse<Wagon>>(`/wagons/paged${wagonListQuery(filters)}`), * and yard views that filter the whole fleet in the browser — a list page
* should use `getAll` and show the real page controls instead.
*/
listAll: async (filters: WagonListFilters = {}): Promise<Wagon[]> => {
const pageSize = 100;
const first = await wagonService.getAll({ ...filters, page: 1, pageSize });
const items = [...first.data.items];
for (let page = 2; page <= (first.data.meta.totalPages ?? 1); page += 1) {
const next = await wagonService.getAll({ ...filters, page, pageSize });
items.push(...next.data.items);
}
return items;
},
getById: (id: string) => apiClient.get<Wagon>(`/wagons/${id}`), getById: (id: string) => apiClient.get<Wagon>(`/wagons/${id}`),
getMovements: (id: string) => getMovements: (id: string) =>
apiClient.get<WagonMovementRecord[]>(`/wagons/${id}/movements`), apiClient.get<WagonMovementRecord[]>(`/wagons/${id}/movements`),
getByTrain: (trainId: string) => apiClient.get<Wagon[]>(`/wagons?trainId=${trainId}`), getByTrain: (trainId: string) => wagonService.listAll({ trainId }),
assignToTrain: (wagonId: string, trainId: string, sequenceNumber?: number) => assignToTrain: (wagonId: string, trainId: string, sequenceNumber?: number) =>
apiClient.post(`/wagons/${wagonId}/assign-train`, { trainId, sequenceNumber }), apiClient.post(`/wagons/${wagonId}/assign-train`, { trainId, sequenceNumber }),
unassign: (wagonId: string) => apiClient.post(`/wagons/${wagonId}/unassign-train`), unassign: (wagonId: string) => apiClient.post(`/wagons/${wagonId}/unassign-train`),