Merge pull request #1025 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-07-30 16:16:52 +03:00
committed by GitHub
21 changed files with 591 additions and 46 deletions

View File

@@ -48,7 +48,7 @@ import {
X,
} from "lucide-react";
import type { Freight } from "@edr/types";
import { OperationDatePicker } from "@edr/ui-common";
import { ExportTrainPicker, OperationDatePicker } from "@edr/ui-common";
import { api } from "@/services/api";
import { PageContainer } from "@/components/page";
@@ -268,6 +268,8 @@ export default function GlCreateBookingForm() {
}, [bookingWindows]);
const [scheduledDate, setScheduledDate] = useState("");
// EXPORT rail completion: the specific train GL picks for the shipment day.
const [trainScheduleId, setTrainScheduleId] = useState("");
const [contractRouteId, setContractRouteId] = useState<string | null>(null);
const [notes, setNotes] = useState("");
// The customer states the billing currency on their shipment request — GL
@@ -578,6 +580,45 @@ export default function GlCreateBookingForm() {
enabled: cargoQuery !== null && !isIntercity,
});
// EXPORT completes pick the TRAIN, not just the day (portal parity). Only
// when completing an initiated instance — a fresh GL create goes through
// clearance and picks its train there.
const isExportPick =
contract?.tradeDirection === "EXPORT" && Boolean(completeBookingId);
const wagonsEstimate = useMemo(() => {
if (!isContainer) return undefined;
const ft20 = containerLines
.filter((l) => parseInt(l.containerSize, 10) === 20)
.reduce((s, l) => s + Number(l.quantity || 0), 0);
const ft40 = containerLines
.filter((l) => parseInt(l.containerSize, 10) === 40)
.reduce((s, l) => s + Number(l.quantity || 0), 0);
const wagons = Math.ceil(ft20 / 2) + ft40;
return wagons > 0 ? wagons : undefined;
}, [isContainer, containerLines]);
const exportTrainsQuery = useQuery({
...api.trainScheduling.exportTrains.queryOptions({
input: {
bookingId: completeBookingId ?? "",
date: scheduledDate,
cargo: {
containerSizes: isContainer
? containerLines
.filter((l) => Number(l.quantity || 0) >= 1)
.map((l) => l.containerSize)
: undefined,
cargoTypeCode: !isContainer
? (contract?.pricingBreakdown?.lineItems?.find(
(li) => li.cargoTypeCode,
)?.cargoTypeCode ?? undefined)
: undefined,
wagons: wagonsEstimate,
},
},
}),
enabled: isExportPick && Boolean(scheduledDate),
});
/**
* Line handling totals are a roll-up of the per-container switches — the
* count is however many containers ticked each service. Recomputed on every
@@ -846,6 +887,8 @@ export default function GlCreateBookingForm() {
...(scheduledDate
? { scheduledDate: new Date(scheduledDate).toISOString() }
: {}),
// EXPORT rail: lock the booking onto the picked train.
...(trainScheduleId ? { trainScheduleId } : {}),
...(notes.trim() ? { notes: notes.trim() } : {}),
// Equipment return: WITH_RETURN contracts derive it server-side from the
// per-line return quantities; only legacy contracts (no value chosen at
@@ -1667,7 +1710,11 @@ export default function GlCreateBookingForm() {
availableDays={availableDays ?? []}
isLoading={daysLoading}
value={scheduledDate}
onChange={setScheduledDate}
onChange={(d) => {
setScheduledDate(d);
// A new day invalidates the old train pick.
setTrainScheduleId("");
}}
/>
</Box>
{showErrors && dateError && (
@@ -1675,6 +1722,14 @@ export default function GlCreateBookingForm() {
{dateError}
</Text>
)}
{isExportPick && scheduledDate ? (
<ExportTrainPicker
options={exportTrainsQuery.data ?? []}
loading={exportTrainsQuery.isLoading}
value={trainScheduleId}
onChange={setTrainScheduleId}
/>
) : null}
</Box>
)}
</StepCard>

View File

@@ -1,4 +1,4 @@
import type { PaginatedResponse } from "@edr/types";
import type { Freight, PaginatedResponse } from "@edr/types";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import type { FleetResourceSlug } from "@/pages/fleet/config/resources";
@@ -438,6 +438,31 @@ export const api = {
],
),
exportTrains: endpoint<
{
bookingId: string;
date: string;
cargo?: {
containerSizes?: string[];
cargoTypeCode?: string;
wagons?: number;
};
},
Freight.ExportTrainOption[]
>(
"train-scheduling",
"export-trains",
({ bookingId, date, cargo }) =>
trainSchedulingService.getExportTrains(bookingId, date, cargo),
({ bookingId, date, cargo }) => [
...QUERY_KEYS.TRAIN_SCHEDULING.ROOT,
"export-trains",
bookingId,
date,
JSON.stringify(cargo ?? {}),
],
),
trainTrack: endpoint<{ id: string }, TrainTrackResponse>(
"train-scheduling",
"track",

View File

@@ -189,6 +189,29 @@ export const trainSchedulingService = {
// Cargo-aware day pool (matching wagons + open train capacity). `containers`
// is serialized as a JSON string param (the server parses it).
// Export train picker for a booking's shipment day; cargo params cover bare
// instances whose cargo only exists on the form so far.
getExportTrains: async (
bookingId: string,
date: string,
cargo?: { containerSizes?: string[]; cargoTypeCode?: string; wagons?: number },
): Promise<Freight.ExportTrainOption[]> => {
const response = await client.get<Freight.ExportTrainOption[]>(
`/bookings/${bookingId}/export-trains`,
{
params: {
date,
...(cargo?.containerSizes?.length
? { containerSizes: cargo.containerSizes.join(",") }
: {}),
...(cargo?.cargoTypeCode ? { cargoTypeCode: cargo.cargoTypeCode } : {}),
...(cargo?.wagons ? { wagons: cargo.wagons } : {}),
},
},
);
return unwrap(response.data);
},
getAvailableDaysForCargo: async (
query: Freight.AvailableDaysForCargoQuery,
): Promise<string[]> => {