mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
Merge pull request #640 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
This commit is contained in:
@@ -20,6 +20,8 @@ import {
|
||||
CreateDropdownSettingDto,
|
||||
DropdownOption,
|
||||
DropdownSetting,
|
||||
DropdownSettingListQuery,
|
||||
PaginatedDropdownSettings,
|
||||
UpdateDropdownOptionDto,
|
||||
UpdateDropdownSettingDto,
|
||||
} from "@/types/dropdownSettings";
|
||||
@@ -61,7 +63,8 @@ import type {
|
||||
StaffBookingWindow,
|
||||
TrainScheduleDetail,
|
||||
TrainScheduleFilters,
|
||||
TrainScheduleListItem,
|
||||
TrainScheduleListFilters,
|
||||
TrainScheduleListResponse,
|
||||
UpdateScheduleWindowRulePayload,
|
||||
TrainSchedulePreviewPayload,
|
||||
TrainSchedulePreviewResponse,
|
||||
@@ -214,13 +217,14 @@ export const api = {
|
||||
trainScheduling: {
|
||||
// ── Queries ────────────────────────────────────────────────────────────
|
||||
scheduleList: endpoint<
|
||||
{ freightType?: FreightType },
|
||||
TrainScheduleListItem[]
|
||||
{ freightType?: FreightType; filters?: TrainScheduleListFilters },
|
||||
TrainScheduleListResponse
|
||||
>(
|
||||
"train-scheduling",
|
||||
"schedules",
|
||||
({ freightType }) => trainSchedulingService.listSchedules(freightType),
|
||||
() => QUERY_KEYS.TRAIN_SCHEDULING.schedules(),
|
||||
({ freightType, filters }) =>
|
||||
trainSchedulingService.listSchedules(freightType, filters),
|
||||
({ filters }) => QUERY_KEYS.TRAIN_SCHEDULING.schedules(filters),
|
||||
),
|
||||
|
||||
batchBoard: endpoint<
|
||||
@@ -1398,7 +1402,7 @@ export const api = {
|
||||
yards: endpoint<void, YardRef[]>(
|
||||
"routes",
|
||||
"yards",
|
||||
() => routesService.getYards().then((r) => r.data.data),
|
||||
() => routesService.getYards(),
|
||||
() => ["routes", "yards"],
|
||||
),
|
||||
|
||||
@@ -1970,6 +1974,15 @@ export const api = {
|
||||
dropdownSettingsService.list,
|
||||
),
|
||||
|
||||
listPaged: endpoint<
|
||||
{ query: DropdownSettingListQuery },
|
||||
PaginatedDropdownSettings
|
||||
>(
|
||||
"dropdown-settings",
|
||||
"listPaged",
|
||||
({ query }) => dropdownSettingsService.listPaged(query),
|
||||
),
|
||||
|
||||
getById: endpoint<{ id: string }, DropdownSetting>(
|
||||
"dropdown-settings",
|
||||
"getById",
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface BookingListFilter {
|
||||
freightType?: string;
|
||||
/** ONE_TIME | GENERAL_CONTRACT — the booking-kind tab filter. */
|
||||
bookingType?: string;
|
||||
/** 'true' → customs bookings, 'false' → self-clearance (non-customs). */
|
||||
customsClearingEnabled?: "true" | "false";
|
||||
tradeDirection?: string;
|
||||
paymentCurrency?: string;
|
||||
paymentStatus?: string;
|
||||
@@ -34,6 +36,8 @@ export interface BookingListFilter {
|
||||
destinationYardId?: string;
|
||||
/** "true" = government bookings only, "false" = private only. */
|
||||
isGovernment?: "true" | "false";
|
||||
/** Free-text search: booking reference, customer name, contract reference (server-side). */
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
@@ -183,6 +187,9 @@ export const bookingsService = {
|
||||
if (filter.originYardId) params.originYardId = filter.originYardId;
|
||||
if (filter.destinationYardId) params.destinationYardId = filter.destinationYardId;
|
||||
if (filter.isGovernment) params.isGovernment = filter.isGovernment;
|
||||
if (filter.customsClearingEnabled)
|
||||
params.customsClearingEnabled = filter.customsClearingEnabled;
|
||||
if (filter.search) params.search = filter.search;
|
||||
}
|
||||
const response = await client.get<PaginatedBookings>(B.BASE, {
|
||||
params,
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { api } from "../auth/http";
|
||||
|
||||
type ListResponse<T> = T[] | { data: T[] };
|
||||
|
||||
const asList = <T>(payload: ListResponse<T>): T[] =>
|
||||
Array.isArray(payload) ? payload : payload.data;
|
||||
import { ruleEngineService } from "./ruleEngine/ruleEngine.service";
|
||||
|
||||
export const cargoTypesService = {
|
||||
/** All active cargo types (page-walked — the API caps pageSize at 100). */
|
||||
async getCargoTypes() {
|
||||
const response = await api.get<ListResponse<unknown>>('/cargo-types', {
|
||||
params: { isActive: true, pageSize: 500 },
|
||||
});
|
||||
return asList(response.data);
|
||||
return ruleEngineService.listAll("cargo-types", { isActive: true });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { api } from "../auth/http";
|
||||
|
||||
type ListResponse<T> = T[] | { data: T[] };
|
||||
|
||||
const asList = <T>(payload: ListResponse<T>): T[] =>
|
||||
Array.isArray(payload) ? payload : payload.data;
|
||||
import { ruleEngineService } from "./ruleEngine/ruleEngine.service";
|
||||
|
||||
export const containerTypesService = {
|
||||
/** All active container types (page-walked — the API caps pageSize at 100). */
|
||||
async getContainerTypes() {
|
||||
const response = await api.get<ListResponse<unknown>>('/container-types', {
|
||||
params: { isActive: true, pageSize: 500 },
|
||||
});
|
||||
return asList(response.data);
|
||||
return ruleEngineService.listAll("container-types", { isActive: true });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface ContractListFilter {
|
||||
tradeDirection?: string;
|
||||
contractKind?: string;
|
||||
paymentCurrency?: string;
|
||||
/** Server-side free-text search (contract reference, company name). */
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
@@ -58,6 +60,10 @@ export interface ShipmentValidation {
|
||||
pairingErrors: string[];
|
||||
/** Lines above the container type's hard max capacity — booking cannot be created. */
|
||||
capacityErrors?: string[];
|
||||
/** Containers already on another active booking for the same day + route — booking cannot be created. */
|
||||
containerClashErrors?: string[];
|
||||
/** EXPORT only: no single open train on the chosen day can carry the whole booking — booking cannot be created. */
|
||||
spaceErrors?: string[];
|
||||
lineItems?: ShipmentPriceLine[];
|
||||
totalAmount?: number;
|
||||
}
|
||||
@@ -123,6 +129,7 @@ function buildListParams(filter?: ContractListFilter) {
|
||||
if (filter) {
|
||||
if (filter.statuses) params.statuses = filter.statuses;
|
||||
else if (filter.status) params.status = filter.status;
|
||||
if (filter.search) params.search = filter.search;
|
||||
if (filter.page != null) params.page = filter.page;
|
||||
if (filter.pageSize != null) params.pageSize = filter.pageSize;
|
||||
if (filter.sortBy) params.sortBy = filter.sortBy;
|
||||
@@ -450,9 +457,14 @@ export const contractsService = {
|
||||
},
|
||||
|
||||
// ── Path A self-clearance (Operations review) ──
|
||||
getOpsClearanceQueue: async (): Promise<PaginatedContracts> => {
|
||||
getOpsClearanceQueue: async (filter?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
}): Promise<PaginatedContracts> => {
|
||||
const response = await client.get<PaginatedContracts>(
|
||||
C.OPS_CLEARANCE_QUEUE,
|
||||
{ params: filter },
|
||||
);
|
||||
const data = unwrap(response.data);
|
||||
return {
|
||||
@@ -467,8 +479,15 @@ export const contractsService = {
|
||||
return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 };
|
||||
},
|
||||
|
||||
getOpsClearanceHistory: async (): Promise<PaginatedContracts> => {
|
||||
const response = await client.get<PaginatedContracts>(C.OPS_CLEARANCE_HISTORY);
|
||||
getOpsClearanceHistory: async (filter?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
}): Promise<PaginatedContracts> => {
|
||||
const response = await client.get<PaginatedContracts>(
|
||||
C.OPS_CLEARANCE_HISTORY,
|
||||
{ params: filter },
|
||||
);
|
||||
const data = unwrap(response.data);
|
||||
return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 };
|
||||
},
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
CreateDropdownSettingDto,
|
||||
DropdownOption,
|
||||
DropdownSetting,
|
||||
DropdownSettingListQuery,
|
||||
PaginatedDropdownSettings,
|
||||
UpdateDropdownOptionDto,
|
||||
UpdateDropdownSettingDto,
|
||||
} from "@/types/dropdownSettings";
|
||||
@@ -19,6 +21,16 @@ export const dropdownSettingsService = {
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
listPaged: async (
|
||||
query: DropdownSettingListQuery,
|
||||
): Promise<PaginatedDropdownSettings> => {
|
||||
const response = await client.get<ApiResponse<PaginatedDropdownSettings>>(
|
||||
`${BASE}/paged`,
|
||||
{ params: query },
|
||||
);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<DropdownSetting> => {
|
||||
const response = await client.get<ApiResponse<DropdownSetting>>(
|
||||
URL_CONSTANTS.DROPDOWN_SETTINGS.BY_ID(id),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { api as apiClient } from '../auth/http';
|
||||
|
||||
import { URL_CONSTANTS } from '@/constants/URLS';
|
||||
import { ruleEngineService } from './ruleEngine/ruleEngine.service';
|
||||
|
||||
export type RouteStatus = 'AVAILABLE' | 'MAINTENANCE' | 'DAMAGED' | 'STOP_WORKING';
|
||||
|
||||
@@ -76,10 +77,6 @@ export const ROUTE_STATUS_OPTIONS: Array<{ value: RouteStatus; label: string }>
|
||||
{ value: 'STOP_WORKING', label: 'Stop working' },
|
||||
];
|
||||
|
||||
interface YardListResponse {
|
||||
data: YardRef[];
|
||||
}
|
||||
|
||||
export const routesService = {
|
||||
getAll: (params?: { status?: RouteStatus; search?: string }) =>
|
||||
apiClient.get<RouteRecord[]>(URL_CONSTANTS.ROUTES.BASE, { params }),
|
||||
@@ -88,8 +85,9 @@ export const routesService = {
|
||||
update: (id: string, data: Partial<SaveRoutePayload>) =>
|
||||
apiClient.patch(URL_CONSTANTS.ROUTES.BY_ID(id), data),
|
||||
deactivate: (id: string) => apiClient.delete(URL_CONSTANTS.ROUTES.BY_ID(id)),
|
||||
getYards: () =>
|
||||
apiClient.get<YardListResponse>(URL_CONSTANTS.RULE_ENGINE.YARDS, {
|
||||
params: { isActive: true, pageSize: 200 },
|
||||
}),
|
||||
/** All active yards (page-walked — the yards list API caps pageSize at 100). */
|
||||
getYards: async (): Promise<YardRef[]> => {
|
||||
const rows = await ruleEngineService.listAll("yards", { isActive: true });
|
||||
return rows as unknown as YardRef[];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -68,50 +68,63 @@ const defaultMeta = (
|
||||
dataLength: number,
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
): RuleEngineListMeta => ({
|
||||
total: dataLength,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.max(1, Math.ceil(dataLength / pageSize)),
|
||||
});
|
||||
): RuleEngineListMeta => {
|
||||
const totalPages = Math.max(1, Math.ceil(dataLength / pageSize));
|
||||
return {
|
||||
total: dataLength,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPreviousPage: page > 1,
|
||||
};
|
||||
};
|
||||
|
||||
const isPaginatedListResult = <T extends RuleEngineRecord>(
|
||||
/** Standard envelope from the shared pagination toolkit: `{ items, meta }`. */
|
||||
const isItemsEnvelope = <T extends RuleEngineRecord>(
|
||||
value: unknown,
|
||||
): value is RuleEngineListResult<T> =>
|
||||
Boolean(value) &&
|
||||
typeof value === "object" &&
|
||||
"data" in (value ?? {}) &&
|
||||
Array.isArray((value as RuleEngineListResult<T>).data);
|
||||
Array.isArray((value as { items?: unknown }).items);
|
||||
|
||||
/** Legacy envelope (`{ data, meta }`) — still returned by wagon-types. */
|
||||
const isLegacyEnvelope = <T extends RuleEngineRecord>(
|
||||
value: unknown,
|
||||
): value is { data: T[]; meta?: RuleEngineListMeta } =>
|
||||
Boolean(value) &&
|
||||
typeof value === "object" &&
|
||||
Array.isArray((value as { data?: unknown }).data);
|
||||
|
||||
const normalizeList = <T extends RuleEngineRecord>(
|
||||
payload: unknown,
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
): RuleEngineListResult<T> => {
|
||||
if (isPaginatedListResult<T>(payload)) {
|
||||
return {
|
||||
data: payload.data,
|
||||
meta: payload.meta ?? defaultMeta(payload.data.length, page, pageSize),
|
||||
};
|
||||
const candidates: unknown[] = [payload, unwrap(payload as { data: unknown })];
|
||||
|
||||
for (const body of candidates) {
|
||||
if (isItemsEnvelope<T>(body)) {
|
||||
return {
|
||||
items: body.items,
|
||||
meta: body.meta ?? defaultMeta(body.items.length, page, pageSize),
|
||||
};
|
||||
}
|
||||
if (isLegacyEnvelope<T>(body)) {
|
||||
return {
|
||||
items: body.data,
|
||||
meta: body.meta ?? defaultMeta(body.data.length, page, pageSize),
|
||||
};
|
||||
}
|
||||
if (Array.isArray(body)) {
|
||||
return {
|
||||
items: body as T[],
|
||||
meta: defaultMeta(body.length, page, pageSize),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const body = unwrap(payload as { data: unknown }) as unknown;
|
||||
|
||||
if (isPaginatedListResult<T>(body)) {
|
||||
return {
|
||||
data: body.data,
|
||||
meta: body.meta ?? defaultMeta(body.data.length, page, pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(body)) {
|
||||
return {
|
||||
data: body as T[],
|
||||
meta: defaultMeta(body.length, page, pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
return { data: [], meta: defaultMeta(0, page, pageSize) };
|
||||
return { items: [], meta: defaultMeta(0, page, pageSize) };
|
||||
};
|
||||
|
||||
const normalizeEntity = <T extends RuleEngineRecord>(payload: unknown): T => {
|
||||
@@ -140,6 +153,34 @@ export const ruleEngineService = {
|
||||
return normalizeList<T>(response.data, page, pageSize);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch every row of a resource by walking the pages. The API caps pageSize
|
||||
* at 100, so option/dropdown consumers that used to ask for 200-500 rows in
|
||||
* one shot go through here instead of getting silently capped (or a 400).
|
||||
*/
|
||||
listAll: async <T extends RuleEngineRecord>(
|
||||
resource: RuleEngineResourceSlug,
|
||||
params?: Omit<RuleEngineListParams, "page" | "pageSize">,
|
||||
): Promise<T[]> => {
|
||||
const pageSize = 100;
|
||||
const first = await ruleEngineService.list<T>(resource, {
|
||||
...params,
|
||||
page: 1,
|
||||
pageSize,
|
||||
});
|
||||
const items = [...first.items];
|
||||
const totalPages = first.meta.totalPages ?? 1;
|
||||
for (let page = 2; page <= totalPages; page += 1) {
|
||||
const next = await ruleEngineService.list<T>(resource, {
|
||||
...params,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
items.push(...next.items);
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
getById: async <T extends RuleEngineRecord>(
|
||||
resource: RuleEngineResourceSlug,
|
||||
id: string,
|
||||
|
||||
@@ -30,7 +30,8 @@ import type {
|
||||
TrainScheduleDetail,
|
||||
UpdateScheduleWindowRulePayload,
|
||||
TrainScheduleFilters,
|
||||
TrainScheduleListItem,
|
||||
TrainScheduleListFilters,
|
||||
TrainScheduleListResponse,
|
||||
TrainSchedulePreviewPayload,
|
||||
TrainSchedulePreviewResponse,
|
||||
TrainSchedulingGlobalRules,
|
||||
@@ -94,9 +95,22 @@ export const trainSchedulingService = {
|
||||
|
||||
listSchedules: async (
|
||||
freightType: FreightType = "CONTAINER",
|
||||
): Promise<TrainScheduleListItem[]> => {
|
||||
const response = await client.get<TrainScheduleListItem[]>(
|
||||
filters: TrainScheduleListFilters = {},
|
||||
): Promise<TrainScheduleListResponse> => {
|
||||
const params: Record<string, string | number> = {};
|
||||
if (filters.page) params.page = filters.page;
|
||||
if (filters.pageSize) params.pageSize = filters.pageSize;
|
||||
if (filters.search?.trim()) params.search = filters.search.trim();
|
||||
if (filters.status) params.status = filters.status;
|
||||
if (filters.freightType) params.freightType = filters.freightType;
|
||||
if (filters.originStationId) params.originStationId = filters.originStationId;
|
||||
if (filters.destinationStationId)
|
||||
params.destinationStationId = filters.destinationStationId;
|
||||
if (filters.sortBy) params.sortBy = filters.sortBy;
|
||||
if (filters.sortOrder) params.sortOrder = filters.sortOrder;
|
||||
const response = await client.get<TrainScheduleListResponse>(
|
||||
pathsFor(freightType === "MIXED" ? undefined : freightType).SCHEDULES,
|
||||
{ params },
|
||||
);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
@@ -219,7 +219,11 @@ export const warehouseService = {
|
||||
apiClient.post<Warehouse>(URL_CONSTANTS.WAREHOUSES.BASE, payload),
|
||||
update: (id: string, payload: Partial<SaveWarehousePayload>) =>
|
||||
apiClient.patch<Warehouse>(URL_CONSTANTS.WAREHOUSES.BY_ID(id), payload),
|
||||
listFacilities: () => apiClient.get<WarehouseFacility[]>(URL_CONSTANTS.RULE_ENGINE.YARDS),
|
||||
// Yards list now returns the standard paginated envelope ({ items, meta }).
|
||||
listFacilities: () =>
|
||||
apiClient.get<{ items: WarehouseFacility[] }>(URL_CONSTANTS.RULE_ENGINE.YARDS, {
|
||||
params: { pageSize: 100 },
|
||||
}),
|
||||
|
||||
// ── Yards ────────────────────────────────────────────────────────────────
|
||||
listYards: (warehouseId: string) =>
|
||||
|
||||
Reference in New Issue
Block a user