Merge pull request #446 from Tria-plc/freight/feature/first_mile_invoice

fix
This commit is contained in:
yaschalew10
2026-07-04 04:59:55 +03:00
committed by GitHub

View File

@@ -202,21 +202,48 @@ const BookingInfo = ({ record }: { record: FirstMileRecord }) => {
); );
}; };
const tripSlipRows = (record: FirstMileRecord): [string, string][] => [ type TripSlipVehicle = NonNullable<FirstMileRecord["vehicleAssignments"]>[number];
["Customer", customerName(record)],
["Service", serviceTypeName(record)], const tripSlipRows = (
["Pickup location", pickupLocation(record)], record: FirstMileRecord,
["Destination yard", destinationYardName(record)], vehicle?: TripSlipVehicle | null,
["Cargo", cargoDesc(record)], ): [string, string][] => {
["Advanced Payment", formatPrice(record.advancedPayment, currencyOf(record))], // Per-vehicle block when a specific truck is chosen (its own driver, container
["Post Payment", formatPrice(record.remainingPayment, currencyOf(record))], // and distance); else fall back to the record-level vehicle summary.
["Vehicle", vehicleLabel(record) ?? "Unassigned"], const vehicleRows: [string, string][] = vehicle
["Contact", `${contactPersonName(record)} · ${contactPhone(record)}`], ? [
["Requested date", requestedDate(record)], [
["Est. Distance (KM)", record.estimatedKm != null ? String(record.estimatedKm) : "—"], "Vehicle",
["Actual Distance (KM)", record.exactKm != null ? String(record.exactKm) : "—"], vehicle.vehicle
["Status", STATUS_META[record.status].label], ? [vehicle.vehicle.code, vehicle.vehicle.plateNumber].filter(Boolean).join(" · ")
]; : vehicle.vehicleId,
],
["Driver", vehicle.vehicle?.assignedDriverName || "—"],
[
"Container(s)",
vehicle.containerNumber || bookingContainerNumbers(record).join(", ") || "—",
],
["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 location", pickupLocation(record)],
["Destination yard", destinationYardName(record)],
["Cargo", cargoDesc(record)],
["Advanced Payment", formatPrice(record.advancedPayment, currencyOf(record))],
["Post Payment", formatPrice(record.remainingPayment, currencyOf(record))],
...vehicleRows,
["Contact", `${contactPersonName(record)} · ${contactPhone(record)}`],
["Requested date", requestedDate(record)],
["Est. Distance (KM)", record.estimatedKm != null ? String(record.estimatedKm) : "—"],
["Status", STATUS_META[record.status].label],
];
};
const SampleStamp = () => ( const SampleStamp = () => (
<Box style={{ height: 96, display: "flex", alignItems: "center" }}> <Box style={{ height: 96, display: "flex", alignItems: "center" }}>
@@ -282,7 +309,13 @@ const SignatureBlock = ({ title, stamp }: { title: string; stamp?: ReactNode })
</Stack> </Stack>
); );
const TripSlipDocument = ({ record }: { record: FirstMileRecord }) => ( const TripSlipDocument = ({
record,
vehicle,
}: {
record: FirstMileRecord;
vehicle?: TripSlipVehicle | null;
}) => (
<Stack gap="md"> <Stack gap="md">
<Stack gap={2} align="center"> <Stack gap={2} align="center">
<Text fw={700}>EDR Freight</Text> <Text fw={700}>EDR Freight</Text>
@@ -294,7 +327,7 @@ const TripSlipDocument = ({ record }: { record: FirstMileRecord }) => (
</Group> </Group>
<Divider /> <Divider />
<SimpleGrid cols={2} spacing="xs"> <SimpleGrid cols={2} spacing="xs">
{tripSlipRows(record).map(([label, value]) => ( {tripSlipRows(record, vehicle).map(([label, value]) => (
<InfoRow key={label} label={label} value={value} /> <InfoRow key={label} label={label} value={value} />
))} ))}
</SimpleGrid> </SimpleGrid>
@@ -309,8 +342,8 @@ const TripSlipDocument = ({ record }: { record: FirstMileRecord }) => (
const escapeHtml = (v: string) => const escapeHtml = (v: string) =>
v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const buildTripSlipHtml = (record: FirstMileRecord) => { const buildTripSlipHtml = (record: FirstMileRecord, vehicle?: TripSlipVehicle | null) => {
const rows = tripSlipRows(record) const rows = tripSlipRows(record, vehicle)
.map(([l, v]) => `<tr><td class="lbl">${escapeHtml(l)}</td><td>${escapeHtml(v)}</td></tr>`) .map(([l, v]) => `<tr><td class="lbl">${escapeHtml(l)}</td><td>${escapeHtml(v)}</td></tr>`)
.join(""); .join("");
const sig = (title: string, withStamp: boolean) => ` const sig = (title: string, withStamp: boolean) => `
@@ -369,6 +402,9 @@ const FirstMilePage = () => {
const [detailOpen, setDetailOpen] = useState(false); const [detailOpen, setDetailOpen] = useState(false);
const [tripSlipOpen, setTripSlipOpen] = useState(false); const [tripSlipOpen, setTripSlipOpen] = useState(false);
const [tripSlipRecord, setTripSlipRecord] = useState<FirstMileRecord | null>(null); const [tripSlipRecord, setTripSlipRecord] = useState<FirstMileRecord | 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); const [activeId, setActiveId] = useState<string | null>(null);
// Multi-vehicle assign: one row per truck — vehicle + the container it carries. // Multi-vehicle assign: one row per truck — vehicle + the container it carries.
const [vehicleRows, setVehicleRows] = useState< const [vehicleRows, setVehicleRows] = useState<
@@ -801,10 +837,27 @@ const FirstMilePage = () => {
}; };
const handlePrintTripSlip = (record: FirstMileRecord) => { const handlePrintTripSlip = (record: FirstMileRecord) => {
// Always open the picker so the operator chooses which truck to print.
setTripSlipRecord(record); setTripSlipRecord(record);
setTripSlipVehicleId(null);
setTripSlipSelectOpen(true);
};
const printBookingSlip = () => {
setTripSlipVehicleId(null);
setTripSlipSelectOpen(false);
setTripSlipOpen(true); setTripSlipOpen(true);
}; };
const chooseTripSlipVehicle = (vehicleId: string) => {
setTripSlipVehicleId(vehicleId);
setTripSlipSelectOpen(false);
setTripSlipOpen(true);
};
const tripSlipVehicle =
tripSlipRecord?.vehicleAssignments?.find((a) => a.vehicleId === tripSlipVehicleId) ?? null;
const printTripSlip = () => { const printTripSlip = () => {
if (!tripSlipRecord) return; if (!tripSlipRecord) return;
const win = window.open("", "_blank", "width=820,height=920"); const win = window.open("", "_blank", "width=820,height=920");
@@ -812,8 +865,17 @@ const FirstMilePage = () => {
toast({ title: "Pop-up blocked", description: "Allow pop-ups to print the trip slip.", variant: "destructive" }); toast({ title: "Pop-up blocked", description: "Allow pop-ups to print the trip slip.", variant: "destructive" });
return; return;
} }
win.document.write(buildTripSlipHtml(tripSlipRecord)); win.document.write(buildTripSlipHtml(tripSlipRecord, tripSlipVehicle));
win.document.close(); win.document.close();
win.focus();
// Explicit print after the doc paints (onload can miss with document.write).
setTimeout(() => {
try {
win.print();
} catch {
/* window may have been closed */
}
}, 250);
}; };
const columns = useMemo((): ColumnDef<FirstMileRecord>[] => { const columns = useMemo((): ColumnDef<FirstMileRecord>[] => {
@@ -1264,6 +1326,61 @@ const FirstMilePage = () => {
</Stack> </Stack>
</Modal> </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) : ""} pick a truck to print its slip.
</Text>
{(tripSlipRecord?.vehicleAssignments ?? []).map((a) => {
const v = a.vehicle;
const label = v ? [v.code, v.plateNumber].filter(Boolean).join(" · ") : a.vehicleId;
return (
<Card
key={a.id}
withBorder
radius="md"
padding="sm"
onClick={() => chooseTripSlipVehicle(a.vehicleId)}
style={{ cursor: "pointer" }}
className="hover:bg-gray-50"
>
<Group justify="space-between" wrap="nowrap" align="center">
<Stack gap={2} style={{ minWidth: 0 }}>
<Group gap="xs" wrap="nowrap">
<Truck size={15} />
<Text size="sm" fw={600} truncate>{label}</Text>
</Group>
<Text size="xs" c="dimmed" truncate>
Driver: {v?.assignedDriverName || "—"}
</Text>
<Text size="xs" c="dimmed" truncate>
Container: {a.containerNumber || "—"}
</Text>
</Stack>
<ActionIcon variant="light" color="edr-green" size="lg" aria-label="Print">
<Printer size={16} />
</ActionIcon>
</Group>
</Card>
);
})}
{(tripSlipRecord?.vehicleAssignments?.length ?? 0) === 0 && (
<Text size="sm" c="dimmed" ta="center" py="xs">No vehicles assigned yet.</Text>
)}
<Divider label="or" labelPosition="center" />
<Button variant="subtle" size="sm" onClick={printBookingSlip}>
Print booking slip (no vehicle)
</Button>
</Stack>
</Modal>
{/* Trip slip modal */} {/* Trip slip modal */}
<Modal <Modal
opened={tripSlipOpen} opened={tripSlipOpen}
@@ -1274,7 +1391,7 @@ const FirstMilePage = () => {
centered centered
> >
<Stack gap="md"> <Stack gap="md">
{tripSlipRecord && <TripSlipDocument record={tripSlipRecord} />} {tripSlipRecord && <TripSlipDocument record={tripSlipRecord} vehicle={tripSlipVehicle} />}
<Divider /> <Divider />
<Group justify="flex-end" gap="sm"> <Group justify="flex-end" gap="sm">
<Button variant="default" onClick={() => setTripSlipOpen(false)}>Close</Button> <Button variant="default" onClick={() => setTripSlipOpen(false)}>Close</Button>