mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +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;
|
schedule = reloaded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Loading is tracked per station: dispatching with cargo still to board at
|
// Dispatch requires the origin's loading window to be COMPLETE: started
|
||||||
// the origin marks it loaded (checklist + auto-load below), so the origin's
|
// and ended. Not started or still open both block — a train departs only
|
||||||
// loading time window must have been started first — same gate the
|
// after loading was formally opened and closed.
|
||||||
// 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;
|
|
||||||
const originLoadingLog =
|
const originLoadingLog =
|
||||||
schedule.stationWorkLogs?.[schedule.originStationId]?.loading;
|
schedule.stationWorkLogs?.[schedule.originStationId]?.loading;
|
||||||
if (boardersToLoad.length && !originLoadingLog?.startedAt) {
|
if (!originLoadingLog?.startedAt) {
|
||||||
throw new BadRequestException(
|
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
|
if (!originLoadingLog?.endedAt) {
|
||||||
// opened (or there is cargo to load), it must be ENDED before dispatch.
|
|
||||||
if ((boardersToLoad.length || originLoadingLog?.startedAt) && !originLoadingLog?.endedAt) {
|
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'End the loading window at the origin station before dispatching',
|
'End the loading window at the origin station before dispatching',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ export function DoCollectionDateFields({
|
|||||||
const outOfOrder =
|
const outOfOrder =
|
||||||
Boolean(value.vesselArrival && value.doCollected) && !doDatesComplete(value);
|
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 (
|
return (
|
||||||
<Group grow align="flex-start" gap="sm" wrap="wrap">
|
<Group grow align="flex-start" gap="sm" wrap="wrap">
|
||||||
<DateInput
|
<DateInput
|
||||||
@@ -45,7 +52,7 @@ export function DoCollectionDateFields({
|
|||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
onChange({ ...value, vesselArrival: v ? new Date(v) : null })
|
onChange({ ...value, vesselArrival: v ? new Date(v) : null })
|
||||||
}
|
}
|
||||||
maxDate={new Date()}
|
minDate={today}
|
||||||
size="sm"
|
size="sm"
|
||||||
required
|
required
|
||||||
withAsterisk
|
withAsterisk
|
||||||
@@ -57,8 +64,7 @@ export function DoCollectionDateFields({
|
|||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
onChange({ ...value, doCollected: v ? new Date(v) : null })
|
onChange({ ...value, doCollected: v ? new Date(v) : null })
|
||||||
}
|
}
|
||||||
minDate={value.vesselArrival ?? undefined}
|
minDate={doMin}
|
||||||
maxDate={new Date()}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
required
|
required
|
||||||
withAsterisk
|
withAsterisk
|
||||||
|
|||||||
@@ -2055,9 +2055,10 @@ export default function GlCreateBookingForm() {
|
|||||||
? bulkErrors.quantity
|
? bulkErrors.quantity
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setBulk((b) => ({ ...b, cargoWeightTons: e.currentTarget.value }))
|
const value = e.currentTarget.value;
|
||||||
}
|
setBulk((b) => ({ ...b, cargoWeightTons: value }));
|
||||||
|
}}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
/>
|
/>
|
||||||
@@ -2074,9 +2075,10 @@ export default function GlCreateBookingForm() {
|
|||||||
? bulkErrors.quantity
|
? bulkErrors.quantity
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setBulk((b) => ({ ...b, itemCount: e.currentTarget.value }))
|
const value = e.currentTarget.value;
|
||||||
}
|
setBulk((b) => ({ ...b, itemCount: value }));
|
||||||
|
}}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
/>
|
/>
|
||||||
@@ -2091,12 +2093,10 @@ export default function GlCreateBookingForm() {
|
|||||||
step={1}
|
step={1}
|
||||||
value={bulk.requestedWagons}
|
value={bulk.requestedWagons}
|
||||||
error={showErrors ? bulkErrors.wagons : undefined}
|
error={showErrors ? bulkErrors.wagons : undefined}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setBulk((b) => ({
|
const value = e.currentTarget.value;
|
||||||
...b,
|
setBulk((b) => ({ ...b, requestedWagons: value }));
|
||||||
requestedWagons: e.currentTarget.value,
|
}}
|
||||||
}))
|
|
||||||
}
|
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
/>
|
/>
|
||||||
@@ -2110,12 +2110,10 @@ export default function GlCreateBookingForm() {
|
|||||||
step={1}
|
step={1}
|
||||||
value={bulk.hazardousQuantity}
|
value={bulk.hazardousQuantity}
|
||||||
error={showErrors ? bulkErrors.hazardous : undefined}
|
error={showErrors ? bulkErrors.hazardous : undefined}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setBulk((b) => ({
|
const value = e.currentTarget.value;
|
||||||
...b,
|
setBulk((b) => ({ ...b, hazardousQuantity: value }));
|
||||||
hazardousQuantity: e.currentTarget.value,
|
}}
|
||||||
}))
|
|
||||||
}
|
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
/>
|
/>
|
||||||
@@ -2129,12 +2127,10 @@ export default function GlCreateBookingForm() {
|
|||||||
step={1}
|
step={1}
|
||||||
value={bulk.reeferQuantity}
|
value={bulk.reeferQuantity}
|
||||||
error={showErrors ? bulkErrors.reefer : undefined}
|
error={showErrors ? bulkErrors.reefer : undefined}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setBulk((b) => ({
|
const value = e.currentTarget.value;
|
||||||
...b,
|
setBulk((b) => ({ ...b, reeferQuantity: value }));
|
||||||
reeferQuantity: e.currentTarget.value,
|
}}
|
||||||
}))
|
|
||||||
}
|
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
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 { DateTimePicker } from "@mantine/dates";
|
||||||
import { useMediaQuery } from "@mantine/hooks";
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -67,6 +67,10 @@ export function CheckpointTimeModal({
|
|||||||
const isSmallScreen = useMediaQuery("(max-width: 48em)");
|
const isSmallScreen = useMediaQuery("(max-width: 48em)");
|
||||||
const [at, setAt] = useState<Date | null>(null);
|
const [at, setAt] = useState<Date | null>(null);
|
||||||
const [note, setNote] = useState("");
|
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);
|
const [handling, setHandling] = useState<HandlingState>(EMPTY_HANDLING);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!opened) return;
|
if (!opened) return;
|
||||||
@@ -121,34 +125,6 @@ export function CheckpointTimeModal({
|
|||||||
radius="md"
|
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
|
<Textarea
|
||||||
label="Note"
|
label="Note"
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
|
|||||||
@@ -500,15 +500,10 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const originLoadingStarted = Boolean(originLoadingLog?.startedAt);
|
const originLoadingStarted = Boolean(originLoadingLog?.startedAt);
|
||||||
const originLoadingEnded = Boolean(originLoadingLog?.endedAt);
|
const originLoadingEnded = Boolean(originLoadingLog?.endedAt);
|
||||||
const dispatchBoardersKept = pendingOriginBoarders.some(
|
// Dispatch requires the origin's loading window to be COMPLETE (started AND
|
||||||
(b) => b.isGovernment || dispatchLoadedIds.has(b.id),
|
// ended): not started → disabled, in progress → disabled, ended → active.
|
||||||
);
|
// Same gate the server enforces.
|
||||||
const dispatchNeedsLoadingStart = dispatchBoardersKept && !originLoadingStarted;
|
const dispatchBlockedByLoading = !originLoadingEnded;
|
||||||
// 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;
|
|
||||||
|
|
||||||
const finalizeStep = hasContainerStep ? 3 : 2;
|
const finalizeStep = hasContainerStep ? 3 : 2;
|
||||||
const canModifyBookings = canEditBookings && !["DISPATCHED", "ARRIVED"].includes(schedule.status);
|
const canModifyBookings = canEditBookings && !["DISPATCHED", "ARRIVED"].includes(schedule.status);
|
||||||
@@ -967,15 +962,11 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
phase="loading"
|
phase="loading"
|
||||||
log={originLoadingLog}
|
log={originLoadingLog}
|
||||||
/>
|
/>
|
||||||
{dispatchNeedsLoadingStart ? (
|
{dispatchBlockedByLoading ? (
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
Start loading before dispatching — the ticked bookings are marked
|
{originLoadingStarted
|
||||||
loaded at dispatch, which needs an open loading window.
|
? "End the loading window before dispatching — a train never departs mid-loading."
|
||||||
</Text>
|
: "Start and end the loading window before dispatching — dispatch needs a completed loading window."}
|
||||||
) : dispatchNeedsLoadingEnd ? (
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
End the loading window before dispatching — a train never departs
|
|
||||||
mid-loading.
|
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -1711,9 +1702,9 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={
|
label={
|
||||||
dispatchNeedsLoadingStart
|
originLoadingStarted
|
||||||
? "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"
|
||||||
: "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}
|
disabled={!dispatchBlockedByLoading}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user