diff --git a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts index 8fcb23674..71153c547 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts @@ -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', ); diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/DoCollectionDateFields.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/DoCollectionDateFields.tsx index 365b8e404..ecc2a6104 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/DoCollectionDateFields.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/DoCollectionDateFields.tsx @@ -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 ( 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 diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx index ff59b1b65..2e31d2a8c 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx @@ -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} /> diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointTimeModal.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointTimeModal.tsx index c024cb0e9..1f8f915de 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointTimeModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointTimeModal.tsx @@ -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(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(EMPTY_HANDLING); useEffect(() => { if (!opened) return; @@ -121,34 +125,6 @@ export function CheckpointTimeModal({ radius="md" /> - - - Loading and unloading times for this stop. Total handling is unloading start to - loading finish; the rest of the stay reports as other activity. - - - {HANDLING_FIELDS.map(([field, label]) => ( - - 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" - /> - ))} - -