diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts index 13624691a..4a26236b0 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts @@ -2811,13 +2811,12 @@ export class TrainSchedulingService { return [ ` ${wagonCells} - EMPTY — no cargo allocated + EMPTY — no cargo allocated `, ]; } return allocations.map((allocation) => { const booking = allocation.booking ?? bookingById.get(allocation.bookingId); - const company = booking?.company as Record | null | undefined; const cargoType = (booking as unknown as { cargoType?: { name?: string; code?: string } } | undefined)?.cargoType; const containerItems = allocation.containerItems ?? []; const firstContainer = containerItems[0]; @@ -2826,8 +2825,6 @@ export class TrainSchedulingService { const chassisNumbers = containerItems.map((item) => item.chassisNumber).filter(Boolean).join(', '); return ` ${wagonCells} - ${esc(company?.name ?? company?.legalName ?? company?.tradeName ?? booking?.companyId)} - ${esc(booking?.companyId)} ${esc(cargoType?.name ?? cargoType?.code ?? allocation.loadType)} ${esc(containerNumbers || firstContainer?.containerNumber)} ${esc(chassisNumbers)} @@ -2909,8 +2906,6 @@ export class TrainSchedulingService { Equated Length Tare Weight Load Capacity - Customer Name - Customer ID Cargo Type Container No Chassis No @@ -2918,7 +2913,7 @@ export class TrainSchedulingService { - ${rows || 'No wagons on this train set.'} + ${rows || 'No wagons on this train set.'} diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx index 546bd0def..6591ffa7c 100644 --- a/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/DeliverCargoDialog.tsx @@ -9,6 +9,7 @@ import { Textarea } from '@/components/ui/textarea'; import { useMutation } from '@tanstack/react-query'; import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; +import { isBackdated, nowLocalDateTimeInput } from '@/lib/no-backdate'; /** * Customer Pickup + Proof of Delivery capture for a LOADED cargo. @@ -27,6 +28,10 @@ export function DeliverCargoDialog({ cargoId, onSuccess }: { cargoId: string; on toast({ title: 'Receiver name is required', variant: 'destructive' }); return; } + if (isBackdated(pickupDate)) { + toast({ title: 'Pickup date cannot be in the past', variant: 'destructive' }); + return; + } try { await deliver.mutateAsync({ id: cargoId, @@ -75,6 +80,7 @@ export function DeliverCargoDialog({ cargoId, onSuccess }: { cargoId: string; on setPickupDate(e.target.value)} /> diff --git a/apps/edr-freight-web/backoffice/src/components/operations/TruckDetentionModal.tsx b/apps/edr-freight-web/backoffice/src/components/operations/TruckDetentionModal.tsx index 2088db32d..c9748d80a 100644 --- a/apps/edr-freight-web/backoffice/src/components/operations/TruckDetentionModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/operations/TruckDetentionModal.tsx @@ -18,6 +18,7 @@ import { useEffect, useState } from 'react'; import { QUERY_KEYS } from '@/constants/QUERY_KEYS'; import { useToast } from '@/hooks/use-toast'; +import { isBackdated } from '@/lib/no-backdate'; import { lastMileService, type LastMileRecord } from '@/services/last-mile.service'; interface TruckDetentionModalProps { @@ -111,6 +112,7 @@ export function TruckDetentionModal({ opened, onClose, record }: TruckDetentionM description="Detention clock start" value={arrived} onChange={(v) => setArrived(v ? new Date(v) : null)} + minDate={new Date()} clearable /> setDelivered(v ? new Date(v) : null)} + minDate={new Date()} clearable /> - diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx index 9e66d9861..681f12ad5 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/ReleaseOrderModal.tsx @@ -6,6 +6,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { api } from '@/services/api'; import { useToast } from '@/hooks/use-toast'; +import { isBackdated, nowLocalDateTimeInput } from '@/lib/no-backdate'; import { warehouseService } from '@/services/warehouse.service'; import type { WarehouseInventoryItem } from '@/types/warehouse'; import { extractErrorMessage } from './options'; @@ -440,6 +441,12 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea }); return; } + // No backdating: gate times are recorded as they happen. The locked + // entrance (exit step) keeps its original past gate-in untouched. + if (!isEntranceLocked && isBackdated(gateInTime)) { + toast({ variant: 'destructive', title: 'Gate in time cannot be in the past' }); + return; + } if (isExitStep && (!gateOutTime || (!skipWeighing && grossWeight === ''))) { toast({ variant: 'destructive', @@ -447,6 +454,10 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea }); return; } + if (isExitStep && isBackdated(gateOutTime)) { + toast({ variant: 'destructive', title: 'Gate out time cannot be in the past' }); + return; + } if (isExitStep && hasContainerWeights && selectedContainerNumbers.length === 0) { toast({ variant: 'destructive', title: 'Select the containers loaded on this truck' }); return; @@ -646,7 +657,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea )} - setGateInTime(e.currentTarget.value)} readOnly={isEntranceLocked} /> + setGateInTime(e.currentTarget.value)} readOnly={isEntranceLocked} /> {hasContainerWeights && ( @@ -679,7 +690,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea Computed net: {computedNetWeight == null ? '-' : `${computedNetWeight.toLocaleString()} t`} - setGateOutTime(e.currentTarget.value)} disabled={!isExitStep || hasTruckLeft} /> + setGateOutTime(e.currentTarget.value)} disabled={!isExitStep || hasTruckLeft} /> {weightMismatch && ( } color="red" variant="light"> diff --git a/apps/edr-freight-web/backoffice/src/lib/no-backdate.ts b/apps/edr-freight-web/backoffice/src/lib/no-backdate.ts new file mode 100644 index 000000000..0ce0541ee --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/lib/no-backdate.ts @@ -0,0 +1,20 @@ +/** + * Backdating guard for operational time entries (gate in/out, mile truck + * times, delivery pickups): times must be recorded as they happen, never + * dated back. A one-hour grace covers real-world lag (weighbridge queue, + * operator finishing the form after the event). + */ +export const BACKDATE_GRACE_MS = 60 * 60 * 1000; + +/** Local-time "YYYY-MM-DDTHH:mm" for a datetime-local input's `min`. */ +export const nowLocalDateTimeInput = (): string => + new Date(Date.now() - new Date().getTimezoneOffset() * 60_000) + .toISOString() + .slice(0, 16); + +/** True when the value is more than the grace period in the past. */ +export const isBackdated = (value: string | Date | null | undefined): boolean => { + if (!value) return false; + const t = value instanceof Date ? value.getTime() : new Date(value).getTime(); + return Number.isFinite(t) && t < Date.now() - BACKDATE_GRACE_MS; +};