feat: implement wagon transfer management modals and page

- 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.
This commit is contained in:
Marshal
2026-07-26 15:11:50 +00:00
parent 9a1c8e5603
commit 9b13fa2ac6
40 changed files with 2584 additions and 809 deletions

View File

@@ -1,4 +1,4 @@
import type { Freight } from "@edr/types";
import type { Freight, PaginatedResponse } from "@edr/types";
import { api as apiClient } from "../auth/http";
@@ -109,10 +109,15 @@ export interface WagonTransferRequest {
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;
@@ -132,7 +137,7 @@ export interface CreateTransferRequestPayload {
note?: string;
}
/** Bulk accept-and-execute result: what ran, what stayed PENDING and why. */
/** Bulk accept-and-execute result: what ran, what stayed open and why. */
export interface BulkFulfillResult {
fulfilled: WagonTransferRequest[];
skipped: Array<{ id: string; reason: string }>;
@@ -142,20 +147,52 @@ export interface BulkFulfillResult {
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: (status?: Freight.WagonTransferRequestStatus) =>
apiClient.get<WagonTransferRequest[]>(
`/wagon-transfer-requests${status ? `?status=${status}` : ''}`,
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: () =>
apiClient.get<TransferHistory>('/wagon-transfer-requests/history'),
/** Admin: any/all staff's history (optional userId filter). */
allHistory: (userId?: string) =>
myHistory: (page = 1, pageSize = 20) =>
apiClient.get<TransferHistory>(
`/wagon-transfer-requests/history/all${userId ? `?userId=${userId}` : ''}`,
`/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}`),
@@ -176,4 +213,10 @@ export const wagonTransferRequestService = {
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 },
),
};