mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
Fix:
Added assertCapacity() checks before saving (matches single receive) Added applyCapacityDelta() after save to increment counters Now validates warehouse → yard → zone capacity hierarchy Single receive already had both checks; bulk receive was gap.
This commit is contained in:
@@ -205,6 +205,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
label: `Last-mile · ${truckPrefill.truckPlateNumber}`,
|
||||
trailerPlate: truckPrefill.trailerPlateNumber ?? '',
|
||||
driverName: truckPrefill.driverName ?? '',
|
||||
driverLicense: truckPrefill.driverLicense ?? '',
|
||||
driverPhone: truckPrefill.driverPhone ?? '',
|
||||
truckType: truckPrefill.truckType ?? '',
|
||||
containerNumbers: splitContainerNumbers(truckPrefill.containerNumber),
|
||||
@@ -218,6 +219,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
label: `Customer · ${t.plateNumber} — ${t.driverName}`,
|
||||
trailerPlate: '',
|
||||
driverName: t.driverName,
|
||||
driverLicense: '',
|
||||
driverPhone: '',
|
||||
truckType: t.truckType,
|
||||
containerNumbers: (t.containers ?? []).map((c) => c.containerNumber).filter(Boolean),
|
||||
@@ -231,6 +233,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
label: `Last-mile · ${t.truckPlateNumber ?? ''}${t.driverName ? ` — ${t.driverName}` : ''}`,
|
||||
trailerPlate: t.trailerPlateNumber ?? '',
|
||||
driverName: t.driverName ?? '',
|
||||
driverLicense: t.driverLicense ?? '',
|
||||
driverPhone: t.driverPhone ?? '',
|
||||
truckType: t.truckType ?? '',
|
||||
containerNumbers: splitContainerNumbers(t.containerNumber),
|
||||
@@ -281,6 +284,11 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
// way. A walk-in truck (typed plate, no assignment) stays editable at arrival.
|
||||
const isTruckIdentityLocked = isEntranceLocked || Boolean(selectedOption);
|
||||
const isDriverNameLocked = isEntranceLocked || Boolean(selectedOption?.driverName);
|
||||
// The freight order's truck details are the customer's / fleet's record — the
|
||||
// gate may FILL blanks (walk-in license, phone) but never edit shown values.
|
||||
const isTrailerLocked = isEntranceLocked || Boolean(selectedOption?.trailerPlate);
|
||||
const isDriverLicenseLocked = isEntranceLocked || Boolean(selectedOption?.driverLicense);
|
||||
const isDriverPhoneLocked = isEntranceLocked || Boolean(selectedOption?.driverPhone);
|
||||
const referenceLocked = Boolean(item?.releaseOrderReference) || savedBlocks.length > 0;
|
||||
|
||||
/** Load a truck into the form: its saved block if any, else its assignment. */
|
||||
@@ -293,7 +301,7 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
setTruckPlateNumber(plate);
|
||||
setTrailerPlateNumber(block?.trailerPlateNumber || option?.trailerPlate || '');
|
||||
setDriverName(block?.driverName || option?.driverName || '');
|
||||
setDriverLicense(block?.driverLicense || '');
|
||||
setDriverLicense(block?.driverLicense || option?.driverLicense || '');
|
||||
setDriverPhone(block?.driverPhone || option?.driverPhone || '');
|
||||
setTruckType(block?.truckType || option?.truckType || '');
|
||||
const loaded = block
|
||||
@@ -611,15 +619,15 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
label="Trailer plate number"
|
||||
value={trailerPlateNumber}
|
||||
onChange={(e) => setTrailerPlateNumber(e.currentTarget.value)}
|
||||
readOnly={isEntranceLocked}
|
||||
readOnly={isTrailerLocked}
|
||||
/>
|
||||
</Group>
|
||||
<Group grow>
|
||||
<TextInput label="Driver name" required value={driverName} onChange={(e) => setDriverName(e.currentTarget.value)} readOnly={isDriverNameLocked} />
|
||||
<TextInput label="Driver license" value={driverLicense} onChange={(e) => setDriverLicense(e.currentTarget.value)} readOnly={isEntranceLocked} />
|
||||
<TextInput label="Driver license" value={driverLicense} onChange={(e) => setDriverLicense(e.currentTarget.value)} readOnly={isDriverLicenseLocked} />
|
||||
</Group>
|
||||
<Group grow>
|
||||
<TextInput label="Driver phone" value={driverPhone} onChange={(e) => setDriverPhone(e.currentTarget.value)} readOnly={isEntranceLocked} />
|
||||
<TextInput label="Driver phone" value={driverPhone} onChange={(e) => setDriverPhone(e.currentTarget.value)} readOnly={isDriverPhoneLocked} />
|
||||
<TextInput label="Truck type" value={truckType} onChange={(e) => setTruckType(e.currentTarget.value)} readOnly={isTruckIdentityLocked} />
|
||||
</Group>
|
||||
<Group grow align="flex-start">
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Group,
|
||||
Loader,
|
||||
@@ -27,6 +28,19 @@ import { BulkTruckUploadModal } from "./BulkTruckUploadModal";
|
||||
|
||||
const TRUCK_TYPES = ["Flatbed", "Container Chassis", "Lowboy", "Box Truck", "Tipper"];
|
||||
|
||||
// Waybill-style selectable copies (indexes 1-8 in the API catalog). The 2 gate
|
||||
// copies (Port Operations, Gate Security & Carrier) are always printed.
|
||||
const FREIGHT_ORDER_COPIES = [
|
||||
{ index: 1, label: "Original 1 (for Issuing Carrier)" },
|
||||
{ index: 2, label: "Original 2 (for Consignee)" },
|
||||
{ index: 3, label: "Original 3 (for Shipper)" },
|
||||
{ index: 4, label: "Copy 4 (Delivery Receipt)" },
|
||||
{ index: 5, label: "Copy 5 (Extra Copy)" },
|
||||
{ index: 6, label: "Copy 6 (Extra Copy)" },
|
||||
{ index: 7, label: "Copy 7 (Extra Copy)" },
|
||||
{ index: 8, label: "Copy 8 (for Agent)" },
|
||||
];
|
||||
|
||||
const downloadBlob = (blob: Blob, filename: string) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
@@ -138,8 +152,11 @@ export function CustomerTruckAssignmentCard({
|
||||
});
|
||||
|
||||
const downloadMutation = useMutation(api.bookings.downloadCustomerTruckFreightOrder.mutationOptions());
|
||||
const [selectedCopies, setSelectedCopies] = useState<number[]>(
|
||||
FREIGHT_ORDER_COPIES.map((c) => c.index),
|
||||
);
|
||||
const downloadFreightOrder = async () => {
|
||||
const blob = await downloadMutation.mutateAsync({ id: booking.id });
|
||||
const blob = await downloadMutation.mutateAsync({ id: booking.id, copies: selectedCopies });
|
||||
downloadBlob(blob, `freight-order-${booking.reference}.pdf`);
|
||||
};
|
||||
|
||||
@@ -322,17 +339,52 @@ export function CustomerTruckAssignmentCard({
|
||||
)}
|
||||
|
||||
{trucks.length > 0 && (
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<Download size={16} />}
|
||||
color="edr-green"
|
||||
onClick={downloadFreightOrder}
|
||||
loading={downloadMutation.isPending}
|
||||
>
|
||||
Generate Freight Order Copies
|
||||
</Button>
|
||||
</Group>
|
||||
<Stack gap="xs">
|
||||
<Text fz="13px" fw={600}>Copies</Text>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="default"
|
||||
onClick={() => setSelectedCopies(FREIGHT_ORDER_COPIES.map((c) => c.index))}
|
||||
>
|
||||
8 copies
|
||||
</Button>
|
||||
<Button size="compact-xs" variant="default" onClick={() => setSelectedCopies([])}>
|
||||
clear selection
|
||||
</Button>
|
||||
</Group>
|
||||
<SimpleGrid cols={2} spacing={6} verticalSpacing={6}>
|
||||
{FREIGHT_ORDER_COPIES.map((c) => (
|
||||
<Checkbox
|
||||
key={c.index}
|
||||
size="xs"
|
||||
label={c.label}
|
||||
checked={selectedCopies.includes(c.index)}
|
||||
onChange={(e) =>
|
||||
setSelectedCopies((prev) =>
|
||||
e.currentTarget.checked
|
||||
? [...prev, c.index].sort((a, b) => a - b)
|
||||
: prev.filter((i) => i !== c.index),
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fz="11.5px" c="#9AA8B5">
|
||||
Port Operations and Gate Security copies are always included.
|
||||
</Text>
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<Download size={16} />}
|
||||
color="edr-green"
|
||||
onClick={downloadFreightOrder}
|
||||
loading={downloadMutation.isPending}
|
||||
>
|
||||
Generate Freight Order Copies
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -308,10 +308,10 @@ export const api = {
|
||||
bookingsService.assignCustomerTruck(id, payload),
|
||||
),
|
||||
|
||||
downloadCustomerTruckFreightOrder: endpoint<{ id: string }, Blob>(
|
||||
downloadCustomerTruckFreightOrder: endpoint<{ id: string; copies?: number[] }, Blob>(
|
||||
"bookings",
|
||||
"downloadCustomerTruckFreightOrder",
|
||||
({ id }) => bookingsService.downloadCustomerTruckFreightOrder(id),
|
||||
({ id, copies }) => bookingsService.downloadCustomerTruckFreightOrder(id, copies),
|
||||
),
|
||||
|
||||
downloadHandoverDocument: endpoint<{ inventoryId: string }, Blob>(
|
||||
|
||||
@@ -212,10 +212,10 @@ export const bookingsService = {
|
||||
);
|
||||
return data.data;
|
||||
},
|
||||
downloadCustomerTruckFreightOrder: async (id: string): Promise<Blob> => {
|
||||
downloadCustomerTruckFreightOrder: async (id: string, copies?: number[]): Promise<Blob> => {
|
||||
const { data } = await client.get(
|
||||
`/api/bookings/${id}/customer-truck-assignment/freight-order`,
|
||||
{ responseType: "blob" },
|
||||
{ responseType: "blob", params: copies?.length ? { copies: copies.join(",") } : undefined },
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user