This commit is contained in:
natib21
2026-07-03 21:45:54 +00:00
parent bc62ce3a8e
commit 9422e5588e
2 changed files with 158 additions and 49 deletions

View File

@@ -365,20 +365,28 @@ export class LastMileService {
}
/**
* Free every vehicle held by this record (direct assignment + container
* allocations), unless still in use by another active trip.
* Free every vehicle held by this record — junction assignments, the legacy
* direct vehicle, and container allocations unless still used by another
* active trip.
*/
private async releaseVehicles(record: LastMile): Promise<void> {
const recordAllocations = await this.dataSource.manager.find(
LastMileContainerAllocation,
{ where: { lastMileId: record.id } },
);
const vehicleIds = recordAllocations
.map((a) => a.vehicleId)
.filter((id): id is string => Boolean(id));
if (record.vehicleId) {
vehicleIds.push(record.vehicleId);
}
const [assignments, recordAllocations] = await Promise.all([
this.dataSource.manager.find(LastMileVehicleAssignment, {
where: { lastMileId: record.id },
}),
this.dataSource.manager.find(LastMileContainerAllocation, {
where: { lastMileId: record.id },
}),
]);
const vehicleIds = [
...new Set(
[
...assignments.map((a) => a.vehicleId),
...recordAllocations.map((a) => a.vehicleId),
record.vehicleId ?? null,
].filter((id): id is string => Boolean(id)),
),
];
await this.vehiclesService.releaseIfUnused(vehicleIds);
}

View File

@@ -36,6 +36,7 @@ import {
Stack,
Text,
TextInput,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import type { ArrivalQueueItem, ImportUnloadedItem, WarehouseInventoryItem } from "@/types/warehouse";
@@ -306,21 +307,43 @@ const BookingInfo = ({ record }: { record: LastMileRecord }) => {
);
};
const tripSlipRows = (record: LastMileRecord): [string, string][] => [
["Customer", customerName(record)],
["Service", serviceTypeName(record)],
["Pickup (origin yard)", originYardName(record)],
["Destination", deliveryLocation(record)],
["Cargo", cargoDesc(record)],
["Advanced Payment", formatPrice(record.advancedPayment)],
["Post Payment", formatPrice(record.remainingPayment)],
["Vehicle", vehicleLabel(record) ?? "Unassigned"],
["Contact", `${contactPersonName(record)} · ${contactPhone(record)}`],
["Requested date", requestedDate(record)],
["Est. Distance (KM)", record.estimatedKm != null ? String(record.estimatedKm) : "—"],
["Actual Distance (KM)", record.exactKm != null ? String(record.exactKm) : "—"],
["Status", STATUS_META[record.status].label],
];
type TripSlipVehicle = NonNullable<LastMileRecord["vehicleAssignments"]>[number];
const tripSlipRows = (
record: LastMileRecord,
vehicle?: TripSlipVehicle | null,
): [string, string][] => {
// Per-vehicle block when a specific truck is chosen (its own driver, container(s)
// and distance); else fall back to the record-level vehicle summary.
const vehicleRows: [string, string][] = vehicle
? [
[
"Vehicle",
vehicle.vehicle
? [vehicle.vehicle.code, vehicle.vehicle.plateNumber].filter(Boolean).join(" · ")
: vehicle.vehicleId,
],
["Driver", vehicle.vehicle?.assignedDriverName || "—"],
["Container(s)", vehicle.containerNumber || "—"],
["Distance (KM)", vehicle.distanceKm != null ? String(vehicle.distanceKm) : "—"],
]
: [
["Vehicle", vehicleLabel(record) ?? "Unassigned"],
["Actual Distance (KM)", record.exactKm != null ? String(record.exactKm) : "—"],
];
return [
["Customer", customerName(record)],
["Service", serviceTypeName(record)],
["Pickup (origin yard)", originYardName(record)],
["Destination", deliveryLocation(record)],
["Cargo", cargoDesc(record)],
["Post Payment", formatPrice(record.remainingPayment)],
...vehicleRows,
["Contact", `${contactPersonName(record)} · ${contactPhone(record)}`],
["Requested date", requestedDate(record)],
["Status", STATUS_META[record.status].label],
];
};
const SampleStamp = () => (
<Box style={{ height: 96, display: "flex", alignItems: "center" }}>
@@ -378,7 +401,13 @@ const SignatureBlock = ({ title, stamp }: { title: string; stamp?: ReactNode })
</Stack>
);
const TripSlipDocument = ({ record }: { record: LastMileRecord }) => (
const TripSlipDocument = ({
record,
vehicle,
}: {
record: LastMileRecord;
vehicle?: TripSlipVehicle | null;
}) => (
<Stack gap="md">
<Stack gap={2} align="center">
<Text fw={700}>EDR Freight</Text>
@@ -390,7 +419,7 @@ const TripSlipDocument = ({ record }: { record: LastMileRecord }) => (
</Group>
<Divider />
<SimpleGrid cols={2} spacing="xs">
{tripSlipRows(record).map(([label, value]) => (
{tripSlipRows(record, vehicle).map(([label, value]) => (
<InfoRow key={label} label={label} value={value} />
))}
</SimpleGrid>
@@ -405,8 +434,8 @@ const TripSlipDocument = ({ record }: { record: LastMileRecord }) => (
const escapeHtml = (v: string) =>
v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const buildTripSlipHtml = (record: LastMileRecord) => {
const rows = tripSlipRows(record)
const buildTripSlipHtml = (record: LastMileRecord, vehicle?: TripSlipVehicle | null) => {
const rows = tripSlipRows(record, vehicle)
.map(([l, v]) => `<tr><td class="lbl">${escapeHtml(l)}</td><td>${escapeHtml(v)}</td></tr>`)
.join("");
const sig = (title: string, withStamp: boolean) => `
@@ -465,6 +494,9 @@ const LastMilePage = () => {
const [detailOpen, setDetailOpen] = useState(false);
const [tripSlipOpen, setTripSlipOpen] = useState(false);
const [tripSlipRecord, setTripSlipRecord] = useState<LastMileRecord | null>(null);
// Which vehicle the trip slip is for (per-truck), + the pre-print picker.
const [tripSlipVehicleId, setTripSlipVehicleId] = useState<string | null>(null);
const [tripSlipSelectOpen, setTripSlipSelectOpen] = useState(false);
const [activeId, setActiveId] = useState<string | null>(null);
// Multi-vehicle assign: one row per truck — vehicle + the container it carries.
const [vehicleRows, setVehicleRows] = useState<
@@ -915,6 +947,20 @@ const LastMilePage = () => {
const handlePrintTripSlip = (record: LastMileRecord) => {
setTripSlipRecord(record);
const assigns = record.vehicleAssignments ?? [];
if (assigns.length > 1) {
// Multiple trucks → let the operator pick which one to print.
setTripSlipVehicleId(null);
setTripSlipSelectOpen(true);
} else {
setTripSlipVehicleId(assigns[0]?.vehicleId ?? null);
setTripSlipOpen(true);
}
};
const chooseTripSlipVehicle = (vehicleId: string) => {
setTripSlipVehicleId(vehicleId);
setTripSlipSelectOpen(false);
setTripSlipOpen(true);
};
@@ -958,6 +1004,9 @@ const LastMilePage = () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
};
const tripSlipVehicle =
tripSlipRecord?.vehicleAssignments?.find((a) => a.vehicleId === tripSlipVehicleId) ?? null;
const printTripSlip = () => {
if (!tripSlipRecord) return;
const win = window.open("", "_blank", "width=820,height=920");
@@ -965,7 +1014,7 @@ const LastMilePage = () => {
toast({ title: "Pop-up blocked", description: "Allow pop-ups to print the trip slip.", variant: "destructive" });
return;
}
win.document.write(buildTripSlipHtml(tripSlipRecord));
win.document.write(buildTripSlipHtml(tripSlipRecord, tripSlipVehicle));
win.document.close();
};
@@ -1019,17 +1068,32 @@ const LastMilePage = () => {
cell: ({ row }) => {
const assigns = row.original.vehicleAssignments ?? [];
if (assigns.length > 1) {
const first = assigns[0]?.vehicle;
const firstLabel = first
? [first.code, first.plateNumber].filter(Boolean).join(" · ")
: "Vehicle";
const labelFor = (a: (typeof assigns)[number]) => {
const v = a.vehicle;
const l = v ? [v.code, v.plateNumber].filter(Boolean).join(" · ") : a.vehicleId;
return a.containerNumber ? `${l} · ${a.containerNumber}` : l;
};
return (
<Group gap={4} wrap="nowrap" style={{ whiteSpace: "nowrap" }}>
<Text size="sm" style={{ whiteSpace: "nowrap" }}>{firstLabel}</Text>
<Badge size="sm" variant="light" color="blue">
+{assigns.length - 1}
</Badge>
</Group>
<Tooltip
withArrow
multiline
label={
<div style={{ whiteSpace: "pre-line" }}>
{assigns.map(labelFor).join("\n")}
</div>
}
>
<Group gap={4} wrap="nowrap" style={{ whiteSpace: "nowrap" }}>
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
{assigns[0].vehicle
? [assigns[0].vehicle.code, assigns[0].vehicle.plateNumber].filter(Boolean).join(" · ")
: assigns[0].vehicleId}
</Text>
<Badge size="sm" variant="light" color="blue">
+{assigns.length - 1}
</Badge>
</Group>
</Tooltip>
);
}
return vehicleLabel(row.original) ?? <Text c="dimmed">Unassigned</Text>;
@@ -1114,7 +1178,12 @@ const LastMilePage = () => {
Boolean(releaseRow?.releaseOrderReference) && !releaseRow?.releaseDate && !pastTransit;
return (
<Group gap={4} justify="flex-end" wrap="nowrap">
<Menu position="bottom-end" width={200} withinPortal>
<Menu
position="bottom-end"
width={200}
withinPortal
styles={{ dropdown: { maxHeight: 320, overflowY: "auto" } }}
>
<Menu.Target>
<ActionIcon variant="subtle" color="gray" aria-label="Delivery actions">
<MoreHorizontal size={16} />
@@ -1661,7 +1730,7 @@ const LastMilePage = () => {
centered
>
<Stack gap="md">
{tripSlipRecord && <TripSlipDocument record={tripSlipRecord} />}
{tripSlipRecord && <TripSlipDocument record={tripSlipRecord} vehicle={tripSlipVehicle} />}
<Divider />
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={() => setTripSlipOpen(false)}>Close</Button>
@@ -1670,6 +1739,42 @@ const LastMilePage = () => {
</Stack>
</Modal>
{/* Trip slip — pick a vehicle (multi-truck) */}
<Modal
opened={tripSlipSelectOpen}
onClose={() => setTripSlipSelectOpen(false)}
title={<Text fw={600}>Print trip slip select vehicle</Text>}
radius="lg"
centered
>
<Stack gap="sm">
<Text size="sm" c="dimmed">
{tripSlipRecord ? bookingRef(tripSlipRecord) : ""} has multiple trucks choose one.
</Text>
{(tripSlipRecord?.vehicleAssignments ?? []).map((a) => {
const v = a.vehicle;
const label = v ? [v.code, v.plateNumber].filter(Boolean).join(" · ") : a.vehicleId;
return (
<Button
key={a.id}
variant="default"
justify="space-between"
rightSection={<Printer size={15} />}
onClick={() => chooseTripSlipVehicle(a.vehicleId)}
>
<Stack gap={0} align="flex-start">
<Text size="sm" fw={600}>{label}</Text>
<Text size="xs" c="dimmed">
{v?.assignedDriverName || "No driver"}
{a.containerNumber ? ` · ${a.containerNumber}` : ""}
</Text>
</Stack>
</Button>
);
})}
</Stack>
</Modal>
{/* Add Actual Distance modal */}
<Modal
opened={distanceOpen}
@@ -1782,11 +1887,7 @@ const LastMilePage = () => {
loading={generateInvoiceMutation.isPending}
onClick={() =>
generateInvoiceMutation.mutate(invoiceConfirm.id, {
onSuccess: (res) => {
setInvoiceConfirm(null);
const invoiceId = res?.data?.id;
if (invoiceId) navigate(`/dashboard/invoices/${invoiceId}`);
},
onSuccess: () => setInvoiceConfirm(null),
})
}
>