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:
Hagernesh
2026-07-22 13:13:14 +00:00
parent 93cf4d1e2a
commit ec066f3d29
12 changed files with 289 additions and 24 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>(

View File

@@ -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;
},