mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 17:50:54 +00:00
Merge pull request #1422 from Tria-plc/freight_feature/usermanagement
feat: refine loading window validation and improve error messaging fo…
This commit is contained in:
@@ -2902,27 +2902,17 @@ export class TrainSchedulingService {
|
||||
schedule = reloaded;
|
||||
}
|
||||
}
|
||||
// Loading is tracked per station: dispatching with cargo still to board at
|
||||
// the origin marks it loaded (checklist + auto-load below), so the origin's
|
||||
// loading time window must have been started first — same gate the
|
||||
// per-booking load endpoint enforces.
|
||||
const originBoarders = await this.unloadedOriginBoarderIds(
|
||||
scheduleId,
|
||||
schedule.originStationId,
|
||||
);
|
||||
const boardersToLoad = dto.loadedBookingIds
|
||||
? originBoarders.filter((id) => new Set(dto.loadedBookingIds).has(id))
|
||||
: originBoarders;
|
||||
// Dispatch requires the origin's loading window to be COMPLETE: started
|
||||
// and ended. Not started or still open both block — a train departs only
|
||||
// after loading was formally opened and closed.
|
||||
const originLoadingLog =
|
||||
schedule.stationWorkLogs?.[schedule.originStationId]?.loading;
|
||||
if (boardersToLoad.length && !originLoadingLog?.startedAt) {
|
||||
if (!originLoadingLog?.startedAt) {
|
||||
throw new BadRequestException(
|
||||
'Start loading at the origin station before dispatching with cargo to load',
|
||||
'Start (and end) the loading window at the origin station before dispatching',
|
||||
);
|
||||
}
|
||||
// A train never departs mid-loading: once the origin's loading window was
|
||||
// opened (or there is cargo to load), it must be ENDED before dispatch.
|
||||
if ((boardersToLoad.length || originLoadingLog?.startedAt) && !originLoadingLog?.endedAt) {
|
||||
if (!originLoadingLog?.endedAt) {
|
||||
throw new BadRequestException(
|
||||
'End the loading window at the origin station before dispatching',
|
||||
);
|
||||
|
||||
@@ -36,6 +36,13 @@ export function DoCollectionDateFields({
|
||||
const outOfOrder =
|
||||
Boolean(value.vesselArrival && value.doCollected) && !doDatesComplete(value);
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const doMin =
|
||||
value.vesselArrival && value.vesselArrival > today
|
||||
? value.vesselArrival
|
||||
: today;
|
||||
|
||||
return (
|
||||
<Group grow align="flex-start" gap="sm" wrap="wrap">
|
||||
<DateInput
|
||||
@@ -45,7 +52,7 @@ export function DoCollectionDateFields({
|
||||
onChange={(v) =>
|
||||
onChange({ ...value, vesselArrival: v ? new Date(v) : null })
|
||||
}
|
||||
maxDate={new Date()}
|
||||
minDate={today}
|
||||
size="sm"
|
||||
required
|
||||
withAsterisk
|
||||
@@ -57,8 +64,7 @@ export function DoCollectionDateFields({
|
||||
onChange={(v) =>
|
||||
onChange({ ...value, doCollected: v ? new Date(v) : null })
|
||||
}
|
||||
minDate={value.vesselArrival ?? undefined}
|
||||
maxDate={new Date()}
|
||||
minDate={doMin}
|
||||
size="sm"
|
||||
required
|
||||
withAsterisk
|
||||
|
||||
@@ -2055,9 +2055,10 @@ export default function GlCreateBookingForm() {
|
||||
? bulkErrors.quantity
|
||||
: undefined
|
||||
}
|
||||
onChange={(e) =>
|
||||
setBulk((b) => ({ ...b, cargoWeightTons: e.currentTarget.value }))
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
setBulk((b) => ({ ...b, cargoWeightTons: value }));
|
||||
}}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
/>
|
||||
@@ -2074,9 +2075,10 @@ export default function GlCreateBookingForm() {
|
||||
? bulkErrors.quantity
|
||||
: undefined
|
||||
}
|
||||
onChange={(e) =>
|
||||
setBulk((b) => ({ ...b, itemCount: e.currentTarget.value }))
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
setBulk((b) => ({ ...b, itemCount: value }));
|
||||
}}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
/>
|
||||
@@ -2091,12 +2093,10 @@ export default function GlCreateBookingForm() {
|
||||
step={1}
|
||||
value={bulk.requestedWagons}
|
||||
error={showErrors ? bulkErrors.wagons : undefined}
|
||||
onChange={(e) =>
|
||||
setBulk((b) => ({
|
||||
...b,
|
||||
requestedWagons: e.currentTarget.value,
|
||||
}))
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
setBulk((b) => ({ ...b, requestedWagons: value }));
|
||||
}}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
/>
|
||||
@@ -2110,12 +2110,10 @@ export default function GlCreateBookingForm() {
|
||||
step={1}
|
||||
value={bulk.hazardousQuantity}
|
||||
error={showErrors ? bulkErrors.hazardous : undefined}
|
||||
onChange={(e) =>
|
||||
setBulk((b) => ({
|
||||
...b,
|
||||
hazardousQuantity: e.currentTarget.value,
|
||||
}))
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
setBulk((b) => ({ ...b, hazardousQuantity: value }));
|
||||
}}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
/>
|
||||
@@ -2129,12 +2127,10 @@ export default function GlCreateBookingForm() {
|
||||
step={1}
|
||||
value={bulk.reeferQuantity}
|
||||
error={showErrors ? bulkErrors.reefer : undefined}
|
||||
onChange={(e) =>
|
||||
setBulk((b) => ({
|
||||
...b,
|
||||
reeferQuantity: e.currentTarget.value,
|
||||
}))
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
setBulk((b) => ({ ...b, reeferQuantity: value }));
|
||||
}}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Divider, Group, Modal, SimpleGrid, Stack, Text, Textarea } from "@mantine/core";
|
||||
import { Button, Group, Modal, Stack, Text, Textarea } from "@mantine/core";
|
||||
import { DateTimePicker } from "@mantine/dates";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -67,6 +67,10 @@ export function CheckpointTimeModal({
|
||||
const isSmallScreen = useMediaQuery("(max-width: 48em)");
|
||||
const [at, setAt] = useState<Date | null>(null);
|
||||
const [note, setNote] = useState("");
|
||||
// The four station-work stamps are no longer edited HERE — the track page's
|
||||
// "Loading & unloading windows" section owns start/end with its own
|
||||
// permissions. The modal still carries any existing stamps through
|
||||
// unchanged on submit, so editing a checkpoint never wipes them.
|
||||
const [handling, setHandling] = useState<HandlingState>(EMPTY_HANDLING);
|
||||
useEffect(() => {
|
||||
if (!opened) return;
|
||||
@@ -121,34 +125,6 @@ export function CheckpointTimeModal({
|
||||
radius="md"
|
||||
/>
|
||||
|
||||
<Divider
|
||||
label="Station work (optional)"
|
||||
labelPosition="left"
|
||||
styles={{ label: { fontWeight: 600 } }}
|
||||
/>
|
||||
<Text size="xs" c="dimmed" mt={-8}>
|
||||
Loading and unloading times for this stop. Total handling is unloading start to
|
||||
loading finish; the rest of the stay reports as other activity.
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
|
||||
{HANDLING_FIELDS.map(([field, label]) => (
|
||||
<DateTimePicker
|
||||
key={field}
|
||||
label={label}
|
||||
value={handling[field]}
|
||||
onChange={(v) =>
|
||||
setHandling((prev) => ({ ...prev, [field]: v ? new Date(v) : null }))
|
||||
}
|
||||
maxDate={new Date()}
|
||||
dropdownType={isSmallScreen ? "modal" : "popover"}
|
||||
popoverProps={{ withinPortal: true }}
|
||||
valueFormat="DD MMM YYYY HH:mm"
|
||||
clearable
|
||||
radius="md"
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Textarea
|
||||
label="Note"
|
||||
placeholder="Optional"
|
||||
|
||||
@@ -500,15 +500,10 @@ export default function TrainScheduleV2DetailPage() {
|
||||
: undefined;
|
||||
const originLoadingStarted = Boolean(originLoadingLog?.startedAt);
|
||||
const originLoadingEnded = Boolean(originLoadingLog?.endedAt);
|
||||
const dispatchBoardersKept = pendingOriginBoarders.some(
|
||||
(b) => b.isGovernment || dispatchLoadedIds.has(b.id),
|
||||
);
|
||||
const dispatchNeedsLoadingStart = dispatchBoardersKept && !originLoadingStarted;
|
||||
// A train never departs mid-loading: once the window opened (or cargo is to
|
||||
// board), it must be ENDED before dispatch — same gate the server enforces.
|
||||
const dispatchNeedsLoadingEnd =
|
||||
(dispatchBoardersKept || originLoadingStarted) && !originLoadingEnded;
|
||||
const dispatchBlockedByLoading = dispatchNeedsLoadingStart || dispatchNeedsLoadingEnd;
|
||||
// Dispatch requires the origin's loading window to be COMPLETE (started AND
|
||||
// ended): not started → disabled, in progress → disabled, ended → active.
|
||||
// Same gate the server enforces.
|
||||
const dispatchBlockedByLoading = !originLoadingEnded;
|
||||
|
||||
const finalizeStep = hasContainerStep ? 3 : 2;
|
||||
const canModifyBookings = canEditBookings && !["DISPATCHED", "ARRIVED"].includes(schedule.status);
|
||||
@@ -967,15 +962,11 @@ export default function TrainScheduleV2DetailPage() {
|
||||
phase="loading"
|
||||
log={originLoadingLog}
|
||||
/>
|
||||
{dispatchNeedsLoadingStart ? (
|
||||
{dispatchBlockedByLoading ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
Start loading before dispatching — the ticked bookings are marked
|
||||
loaded at dispatch, which needs an open loading window.
|
||||
</Text>
|
||||
) : dispatchNeedsLoadingEnd ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
End the loading window before dispatching — a train never departs
|
||||
mid-loading.
|
||||
{originLoadingStarted
|
||||
? "End the loading window before dispatching — a train never departs mid-loading."
|
||||
: "Start and end the loading window before dispatching — dispatch needs a completed loading window."}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
@@ -1711,9 +1702,9 @@ export default function TrainScheduleV2DetailPage() {
|
||||
</Button>
|
||||
<Tooltip
|
||||
label={
|
||||
dispatchNeedsLoadingStart
|
||||
? "Start loading at the origin station first — dispatch marks the ticked bookings loaded"
|
||||
: "End the loading window at the origin station first — a train never departs mid-loading"
|
||||
originLoadingStarted
|
||||
? "End the loading window at the origin station first — a train never departs mid-loading"
|
||||
: "Start and end the loading window at the origin station first — dispatch needs a completed loading window"
|
||||
}
|
||||
disabled={!dispatchBlockedByLoading}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user