mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
Merge pull request #905 from Tria-plc/testfixes
Fixes Download Excel template with instructions Parse uploaded file, preview trucks Validate container assignments (1x40ft OR 2x20ft per truck) Commit bulk upload in one call
This commit is contained in:
@@ -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
|
||||
<Label>Pickup date</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
min={nowLocalDateTimeInput()}
|
||||
value={pickupDate}
|
||||
onChange={(e) => setPickupDate(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
<DateTimePicker
|
||||
@@ -118,11 +120,26 @@ export function TruckDetentionModal({ opened, onClose, record }: TruckDetentionM
|
||||
description="Clock end (blank = still out)"
|
||||
value={delivered}
|
||||
onChange={(v) => setDelivered(v ? new Date(v) : null)}
|
||||
minDate={new Date()}
|
||||
clearable
|
||||
/>
|
||||
</Group>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="light" loading={saveTimes.isPending} onClick={() => saveTimes.mutate()}>
|
||||
<Button
|
||||
variant="light"
|
||||
loading={saveTimes.isPending}
|
||||
onClick={() => {
|
||||
// No backdating: detention times are recorded as they happen.
|
||||
if (isBackdated(arrived) || isBackdated(delivered)) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Detention times cannot be in the past',
|
||||
});
|
||||
return;
|
||||
}
|
||||
saveTimes.mutate();
|
||||
}}
|
||||
>
|
||||
Save times
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -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
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
)}
|
||||
<TextInput label="Gate in time" type="datetime-local" value={gateInTime} onChange={(e) => setGateInTime(e.currentTarget.value)} readOnly={isEntranceLocked} />
|
||||
<TextInput label="Gate in time" type="datetime-local" min={isEntranceLocked ? undefined : nowLocalDateTimeInput()} value={gateInTime} onChange={(e) => setGateInTime(e.currentTarget.value)} readOnly={isEntranceLocked} />
|
||||
</Group>
|
||||
{hasContainerWeights && (
|
||||
<Group gap="md" align="center">
|
||||
@@ -679,7 +690,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
<Text size="sm" c={weightMismatch ? 'red' : 'dimmed'}>
|
||||
Computed net: <b>{computedNetWeight == null ? '-' : `${computedNetWeight.toLocaleString()} t`}</b>
|
||||
</Text>
|
||||
<TextInput label="Gate out time" type="datetime-local" value={gateOutTime} onChange={(e) => setGateOutTime(e.currentTarget.value)} disabled={!isExitStep || hasTruckLeft} />
|
||||
<TextInput label="Gate out time" type="datetime-local" min={nowLocalDateTimeInput()} value={gateOutTime} onChange={(e) => setGateOutTime(e.currentTarget.value)} disabled={!isExitStep || hasTruckLeft} />
|
||||
</Group>
|
||||
{weightMismatch && (
|
||||
<Alert icon={<Scale size={16} />} color="red" variant="light">
|
||||
|
||||
20
apps/edr-freight-web/backoffice/src/lib/no-backdate.ts
Normal file
20
apps/edr-freight-web/backoffice/src/lib/no-backdate.ts
Normal file
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user