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;
+};