mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 07:38:10 +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][] => [
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user