mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
- Add TransferFulfillModal for fulfilling wagon transfer requests. - Create TransferRequestFormModal for filing new wagon transfer requests. - Introduce TransferCloseShortModal for closing requests that cannot be fully fulfilled. - Develop WagonTransfersPage to manage and display wagon transfer requests. - Implement utility functions for handling wagon transfer request data and UI components. - Enhance UI with Mantine components for better user experience.
223 lines
8.1 KiB
TypeScript
223 lines
8.1 KiB
TypeScript
import type { Freight, PaginatedResponse } from "@edr/types";
|
|
|
|
import { api as apiClient } from "../auth/http";
|
|
|
|
export interface Wagon {
|
|
id: string;
|
|
wagonNumber: string;
|
|
wagonTypeId: string;
|
|
trainId: string | null;
|
|
sequenceNumber: number | null;
|
|
currentLocationYardId: string | null;
|
|
currentLocationYard?: {
|
|
id: string;
|
|
code: string;
|
|
label: string;
|
|
country?: string;
|
|
} | null;
|
|
/** Owns this wagon's spec — tare, capacity, length are read from here, never off the wagon. */
|
|
wagonType?: {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
supportedLoadTypes?: string[];
|
|
tareWeightTons?: number;
|
|
capacityTons?: number;
|
|
lengthMeters?: number;
|
|
} | null;
|
|
status: Freight.WagonStatus;
|
|
currentYardId: string | null;
|
|
currentYard?: { id: string; label?: string; code?: string } | null;
|
|
/** Odd EXPORT run (Ethiopia → Djibouti); null when the wagon is not on a run. */
|
|
exportTrainNumber?: string | null;
|
|
/** Even IMPORT run (Djibouti → Ethiopia); always the export run + 1. */
|
|
importTrainNumber?: string | null;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface WagonListFilters {
|
|
search?: string;
|
|
status?: Freight.WagonStatus;
|
|
currentYardId?: string;
|
|
wagonTypeId?: string;
|
|
trainId?: string;
|
|
/** Run number — matches a wagon whose export OR import run equals it. */
|
|
trainNumber?: string;
|
|
}
|
|
|
|
/**
|
|
* One row of the wagon_movements ledger: every physical relocation between
|
|
* yards — a booking's loaded leg, an empty reposition ride, or a manual staff
|
|
* correction. Returned newest first by the API.
|
|
*/
|
|
export interface WagonMovementRecord {
|
|
id: string;
|
|
wagonId: string;
|
|
fromYardId: string | null;
|
|
toYardId: string;
|
|
fromYard?: { id?: string; label?: string; code?: string } | null;
|
|
toYard?: { id?: string; label?: string; code?: string } | null;
|
|
trainScheduleId: string | null;
|
|
bookingId: string | null;
|
|
kind: Freight.WagonMovementKind;
|
|
movedByUserId: string | null;
|
|
/** The transfer request this move fulfilled, when one drove it. */
|
|
transferRequestId: string | null;
|
|
occurredAt: string;
|
|
note: string | null;
|
|
createdAt: string;
|
|
wagon?: { id: string; wagonNumber?: string } | null;
|
|
}
|
|
|
|
export const wagonService = {
|
|
getAll: (filters: WagonListFilters = {}) => {
|
|
const params = new URLSearchParams();
|
|
if (filters.search?.trim()) params.set('search', filters.search.trim());
|
|
if (filters.status) params.set('status', filters.status);
|
|
if (filters.currentYardId) params.set('currentYardId', filters.currentYardId);
|
|
if (filters.wagonTypeId) params.set('wagonTypeId', filters.wagonTypeId);
|
|
if (filters.trainId) params.set('trainId', filters.trainId);
|
|
if (filters.trainNumber) params.set('trainNumber', filters.trainNumber);
|
|
const qs = params.toString();
|
|
return apiClient.get<Wagon[]>(`/wagons${qs ? `?${qs}` : ''}`);
|
|
},
|
|
getById: (id: string) => apiClient.get<Wagon>(`/wagons/${id}`),
|
|
getMovements: (id: string) =>
|
|
apiClient.get<WagonMovementRecord[]>(`/wagons/${id}/movements`),
|
|
getByTrain: (trainId: string) => apiClient.get<Wagon[]>(`/wagons?trainId=${trainId}`),
|
|
assignToTrain: (wagonId: string, trainId: string, sequenceNumber?: number) =>
|
|
apiClient.post(`/wagons/${wagonId}/assign-train`, { trainId, sequenceNumber }),
|
|
unassign: (wagonId: string) => apiClient.post(`/wagons/${wagonId}/unassign-train`),
|
|
create: (data: Partial<Wagon>) => apiClient.post('/wagons', data),
|
|
update: (id: string, data: Partial<Wagon>) => apiClient.patch(`/wagons/${id}`, data),
|
|
delete: (id: string) => apiClient.delete(`/wagons/${id}`),
|
|
/** Relocate many wagons to one yard in a single call (writes movement ledger). */
|
|
bulkTransfer: (wagonIds: string[], toYardId: string) =>
|
|
apiClient.post<{ moved: number }>('/wagons/bulk-transfer', { wagonIds, toYardId }),
|
|
/** Set the same status on many wagons in a single call. */
|
|
bulkSetStatus: (wagonIds: string[], status: Freight.WagonStatus) =>
|
|
apiClient.post<{ updated: number }>('/wagons/bulk-status', { wagonIds, status }),
|
|
};
|
|
|
|
/**
|
|
* A two-person wagon-transfer request: a requester asks for N wagons of a type
|
|
* to move between yards (count only); OCC hand-picks the wagons and fulfils it.
|
|
*/
|
|
export interface WagonTransferRequest {
|
|
id: string;
|
|
fromYardId: string;
|
|
toYardId: string;
|
|
wagonTypeId: string;
|
|
quantity: number;
|
|
/** How many have actually moved so far — OCC delivers in instalments. */
|
|
fulfilledQuantity: number;
|
|
status: Freight.WagonTransferRequestStatus;
|
|
requestedByUserId: string | null;
|
|
fulfilledByUserId: string | null;
|
|
/** When the LAST instalment ran, not necessarily the full count. */
|
|
fulfilledAt: string | null;
|
|
closedShortAt?: string | null;
|
|
closedShortByUserId?: string | null;
|
|
/** Why the wagons are needed — required for new requests, shown on the queue. */
|
|
reason?: string | null;
|
|
note: string | null;
|
|
fromYard?: { id: string; label?: string; code?: string } | null;
|
|
toYard?: { id: string; label?: string; code?: string } | null;
|
|
wagonType?: { id: string; code?: string; name?: string } | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface CreateTransferRequestPayload {
|
|
fromYardId: string;
|
|
toYardId: string;
|
|
wagonTypeId: string;
|
|
quantity: number;
|
|
/** Mandatory: why the wagons are needed. */
|
|
reason: string;
|
|
note?: string;
|
|
}
|
|
|
|
/** Bulk accept-and-execute result: what ran, what stayed open and why. */
|
|
export interface BulkFulfillResult {
|
|
fulfilled: WagonTransferRequest[];
|
|
skipped: Array<{ id: string; reason: string }>;
|
|
}
|
|
|
|
/** Per-user activity: requests filed/fulfilled + the wagons physically moved. */
|
|
export interface TransferHistory {
|
|
requests: WagonTransferRequest[];
|
|
movements: WagonMovementRecord[];
|
|
meta: {
|
|
page: number;
|
|
pageSize: number;
|
|
requestsTotal: number;
|
|
movementsTotal: number;
|
|
totalPages: number;
|
|
};
|
|
}
|
|
|
|
/** Desk list filters — `status` may be a comma-separated set ("open" tab). */
|
|
export interface TransferRequestListFilter {
|
|
status?: string;
|
|
fromYardId?: string;
|
|
toYardId?: string;
|
|
wagonTypeId?: string;
|
|
search?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}
|
|
|
|
const listQuery = (filter: TransferRequestListFilter = {}): string => {
|
|
const params = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(filter)) {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
params.set(key, String(value));
|
|
}
|
|
}
|
|
const qs = params.toString();
|
|
return qs ? `?${qs}` : '';
|
|
};
|
|
|
|
export const wagonTransferRequestService = {
|
|
list: (filter: TransferRequestListFilter = {}) =>
|
|
apiClient.get<PaginatedResponse<WagonTransferRequest>>(
|
|
`/wagon-transfer-requests${listQuery(filter)}`,
|
|
),
|
|
/** The caller's own history (both roles: requests they filed and fulfilled). */
|
|
myHistory: (page = 1, pageSize = 20) =>
|
|
apiClient.get<TransferHistory>(
|
|
`/wagon-transfer-requests/history?page=${page}&pageSize=${pageSize}`,
|
|
),
|
|
/** Admin: any/all staff's history (optional userId filter). */
|
|
allHistory: (userId?: string, page = 1, pageSize = 20) =>
|
|
apiClient.get<TransferHistory>(
|
|
`/wagon-transfer-requests/history/all?page=${page}&pageSize=${pageSize}` +
|
|
`${userId ? `&userId=${userId}` : ''}`,
|
|
),
|
|
getById: (id: string) =>
|
|
apiClient.get<WagonTransferRequest>(`/wagon-transfer-requests/${id}`),
|
|
create: (data: CreateTransferRequestPayload) =>
|
|
apiClient.post<WagonTransferRequest>('/wagon-transfer-requests', data),
|
|
/** OCC: accept-and-execute a subset of pending requests (auto-picked wagons). */
|
|
bulkFulfill: (requestIds: string[]) =>
|
|
apiClient.post<BulkFulfillResult>('/wagon-transfer-requests/bulk-fulfill', {
|
|
requestIds,
|
|
}),
|
|
/** OCC: execute the transfer with the hand-picked wagons. */
|
|
fulfill: (id: string, wagonIds: string[]) =>
|
|
apiClient.post<WagonTransferRequest>(
|
|
`/wagon-transfer-requests/${id}/fulfill`,
|
|
{ wagonIds },
|
|
),
|
|
cancel: (id: string) =>
|
|
apiClient.post<WagonTransferRequest>(
|
|
`/wagon-transfer-requests/${id}/cancel`,
|
|
),
|
|
/** OCC: end the request short — the source yard has no more to give. */
|
|
closeShort: (id: string, note?: string) =>
|
|
apiClient.post<WagonTransferRequest>(
|
|
`/wagon-transfer-requests/${id}/close-short`,
|
|
{ note },
|
|
),
|
|
};
|