mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #446 from Tria-plc/freight/feature/first_mile_invoice
fix
This commit is contained in:
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user