mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
mile
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user