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; /** Drop wagons already coupled to a built train — only loose ones can be taken. */ unassigned?: boolean; /** Run number — matches a wagon whose export OR import run equals it. */ trainNumber?: string; /** Registration day range (YYYY-MM-DD), both ends inclusive. */ createdFrom?: string; createdTo?: string; /** Only read by `getPaged`. */ page?: number; pageSize?: number; } const wagonListQuery = (filters: WagonListFilters): string => { 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.unassigned) params.set('unassigned', 'true'); if (filters.trainNumber) params.set('trainNumber', filters.trainNumber); if (filters.createdFrom) params.set('createdFrom', filters.createdFrom); if (filters.createdTo) params.set('createdTo', filters.createdTo); if (filters.page) params.set('page', String(filters.page)); if (filters.pageSize) params.set('pageSize', String(filters.pageSize)); const qs = params.toString(); return qs ? `?${qs}` : ''; }; /** * 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 = { /** One page ({items, meta}); 10 rows unless `pageSize` says otherwise. */ getAll: (filters: WagonListFilters = {}) => apiClient.get>(`/wagons${wagonListQuery(filters)}`), /** * Every matching wagon, page-walked at the API's 100-row cap. For the pickers * 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 => { 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(`/wagons/${id}`), getMovements: (id: string) => apiClient.get(`/wagons/${id}/movements`), getByTrain: (trainId: string) => wagonService.listAll({ 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) => apiClient.post('/wagons', data), update: (id: string, data: Partial) => 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>( `/wagon-transfer-requests${listQuery(filter)}`, ), /** The caller's own history (both roles: requests they filed and fulfilled). */ myHistory: (page = 1, pageSize = 20) => apiClient.get( `/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( `/wagon-transfer-requests/history/all?page=${page}&pageSize=${pageSize}` + `${userId ? `&userId=${userId}` : ''}`, ), getById: (id: string) => apiClient.get(`/wagon-transfer-requests/${id}`), create: (data: CreateTransferRequestPayload) => apiClient.post('/wagon-transfer-requests', data), /** OCC: accept-and-execute a subset of pending requests (auto-picked wagons). */ bulkFulfill: (requestIds: string[]) => apiClient.post('/wagon-transfer-requests/bulk-fulfill', { requestIds, }), /** OCC: execute the transfer with the hand-picked wagons. */ fulfill: (id: string, wagonIds: string[]) => apiClient.post( `/wagon-transfer-requests/${id}/fulfill`, { wagonIds }, ), cancel: (id: string) => apiClient.post( `/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( `/wagon-transfer-requests/${id}/close-short`, { note }, ), };