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][] => [
["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))],
["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<FirstMileRecord["vehicleAssignments"]>[number];
const tripSlipRows = (
record: FirstMileRecord,
vehicle?: TripSlipVehicle | null,
): [string, string][] => {
// Per-vehicle block when a specific truck is chosen (its own driver, container
// 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 || 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 = () => (
<Box style={{ height: 96, display: "flex", alignItems: "center" }}>
@@ -282,7 +309,13 @@ const SignatureBlock = ({ title, stamp }: { title: string; stamp?: ReactNode })
</Stack>
);
const TripSlipDocument = ({ record }: { record: FirstMileRecord }) => (
const TripSlipDocument = ({
record,
vehicle,
}: {
record: FirstMileRecord;
vehicle?: TripSlipVehicle | null;
}) => (
<Stack gap="md">
<Stack gap={2} align="center">
<Text fw={700}>EDR Freight</Text>
@@ -294,7 +327,7 @@ const TripSlipDocument = ({ record }: { record: FirstMileRecord }) => (
</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>
@@ -309,8 +342,8 @@ const TripSlipDocument = ({ record }: { record: FirstMileRecord }) => (
const escapeHtml = (v: string) =>
v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const buildTripSlipHtml = (record: FirstMileRecord) => {
const rows = tripSlipRows(record)
const buildTripSlipHtml = (record: FirstMileRecord, 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) => `
@@ -369,6 +402,9 @@ const FirstMilePage = () => {
const [detailOpen, setDetailOpen] = useState(false);
const [tripSlipOpen, setTripSlipOpen] = useState(false);
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);
// Multi-vehicle assign: one row per truck — vehicle + the container it carries.
const [vehicleRows, setVehicleRows] = useState<
@@ -801,10 +837,27 @@ const FirstMilePage = () => {
};
const handlePrintTripSlip = (record: FirstMileRecord) => {
// Always open the picker so the operator chooses which truck to print.
setTripSlipRecord(record);
setTripSlipVehicleId(null);
setTripSlipSelectOpen(true);
};
const printBookingSlip = () => {
setTripSlipVehicleId(null);
setTripSlipSelectOpen(false);
setTripSlipOpen(true);
};
const chooseTripSlipVehicle = (vehicleId: string) => {
setTripSlipVehicleId(vehicleId);
setTripSlipSelectOpen(false);
setTripSlipOpen(true);
};
const tripSlipVehicle =
tripSlipRecord?.vehicleAssignments?.find((a) => a.vehicleId === tripSlipVehicleId) ?? null;
const printTripSlip = () => {
if (!tripSlipRecord) return;
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" });
return;
}
win.document.write(buildTripSlipHtml(tripSlipRecord));
win.document.write(buildTripSlipHtml(tripSlipRecord, tripSlipVehicle));
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>[] => {
@@ -1264,6 +1326,61 @@ const FirstMilePage = () => {
</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) : ""} 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 */}
<Modal
opened={tripSlipOpen}
@@ -1274,7 +1391,7 @@ const FirstMilePage = () => {
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>