This commit is contained in:
natib21
2026-06-20 13:31:29 +00:00
parent 35b057da1d
commit 3a86a63215

View File

@@ -186,6 +186,133 @@ const BookingInfo = ({ job }: { job: LastMileJob }) => (
</Card>
);
const tripSlipRows = (job: LastMileJob): [string, string][] => [
["Customer", job.customer],
["Service", job.serviceType],
["Origin yard", job.originYard],
["Destination", job.destination],
["Cargo", job.cargo],
["Weight", job.weight],
["Vehicle", job.assignedVehicle ?? "Unassigned"],
["Contact", `${job.contactName} · ${job.contactPhone}`],
["Requested date", job.requestedDate],
["Delivery status", DELIVERY_STATUS_META[job.deliveryStatus].label],
];
const SignatureBlock = ({ title }: { title: string }) => (
<Stack gap="sm" style={{ flex: 1 }}>
<Text fw={600} size="sm">
{title}
</Text>
<Group gap="xs" align="flex-end">
<Text size="sm" c="dimmed">
Name:
</Text>
<Box style={{ flex: 1, borderBottom: "1px solid var(--mantine-color-gray-5)", height: 18 }} />
</Group>
<Group gap="xs" align="flex-end">
<Text size="sm" c="dimmed">
Signature:
</Text>
<Box style={{ flex: 1, borderBottom: "1px solid var(--mantine-color-gray-5)", height: 18 }} />
</Group>
<Box
style={{
marginTop: 4,
height: 90,
borderRadius: 8,
border: "1px dashed var(--mantine-color-gray-4)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
Stamp
</Text>
</Box>
</Stack>
);
const TripSlipDocument = ({ job }: { job: LastMileJob }) => (
<Stack gap="md">
<Stack gap={2} align="center">
<Text fw={700}>EDR Freight</Text>
<Text size="sm" c="dimmed" tt="uppercase" fw={600}>
Last Mile Trip Slip
</Text>
</Stack>
<Group justify="space-between">
<Text size="sm" fw={600}>
{job.bookingRef}
</Text>
<Text size="sm" c="dimmed">
{job.requestedDate}
</Text>
</Group>
<Divider />
<SimpleGrid cols={2} spacing="xs">
{tripSlipRows(job).map(([label, value]) => (
<InfoRow key={label} label={label} value={value} />
))}
</SimpleGrid>
<Divider label="Acknowledgement" labelPosition="center" />
<Group align="flex-start" gap="xl" wrap="nowrap">
<SignatureBlock title="Driver" />
<SignatureBlock title="Operator" />
</Group>
</Stack>
);
const escapeHtml = (value: string) =>
value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const buildTripSlipHtml = (job: LastMileJob) => {
const rows = tripSlipRows(job)
.map(
([label, value]) =>
`<tr><td class="lbl">${escapeHtml(label)}</td><td>${escapeHtml(value)}</td></tr>`,
)
.join("");
const signature = (title: string) => `
<div class="sign-col">
<div class="sign-title">${title}</div>
<div class="sign-field"><span>Name:</span><span class="line"></span></div>
<div class="sign-field"><span>Signature:</span><span class="line"></span></div>
<div class="stamp">STAMP</div>
</div>`;
return `<!doctype html><html><head><meta charset="utf-8" />
<title>Trip Slip ${escapeHtml(job.bookingRef)}</title>
<style>
* { box-sizing: border-box; }
body { font-family: Arial, Helvetica, sans-serif; color: #111; margin: 32px; }
.head { text-align: center; margin-bottom: 16px; }
.head h1 { font-size: 18px; margin: 0; }
.head p { font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: #555; margin: 2px 0 0; }
.meta { display: flex; justify-content: space-between; font-size: 13px; font-weight: 600; margin: 8px 0; }
table { width: 100%; border-collapse: collapse; font-size: 13px; margin: 8px 0 20px; }
td { padding: 5px 6px; border-bottom: 1px solid #eee; vertical-align: top; }
td.lbl { color: #666; text-transform: uppercase; font-size: 11px; font-weight: 700; width: 40%; }
.ack { text-align: center; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #777; margin: 16px 0 8px; }
.signs { display: flex; gap: 32px; }
.sign-col { flex: 1; }
.sign-title { font-weight: 700; font-size: 13px; margin-bottom: 12px; }
.sign-field { display: flex; gap: 6px; align-items: flex-end; font-size: 12px; color: #666; margin-bottom: 10px; }
.sign-field .line { flex: 1; border-bottom: 1px solid #888; height: 16px; }
.stamp { margin-top: 6px; height: 96px; border: 1px dashed #aaa; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 11px; letter-spacing: 1px; color: #999; }
</style></head>
<body onload="window.print()">
<div class="head"><h1>EDR Freight</h1><p>Last Mile Trip Slip</p></div>
<div class="meta"><span>${escapeHtml(job.bookingRef)}</span><span>${escapeHtml(job.requestedDate)}</span></div>
<table>${rows}</table>
<div class="ack">Acknowledgement</div>
<div class="signs">${signature("Driver")}${signature("Operator")}</div>
</body></html>`;
};
const LastMilePage = () => {
const { toast } = useToast();
const { pagination, setPagination } = usePagination({ pageSize: 10 });
@@ -196,6 +323,8 @@ const LastMilePage = () => {
const [assignOpen, setAssignOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
const [tripSlipOpen, setTripSlipOpen] = useState(false);
const [tripSlipJob, setTripSlipJob] = useState<LastMileJob | null>(null);
const [activeJobId, setActiveJobId] = useState<string | null>(null);
const [vehicleValue, setVehicleValue] = useState<string | null>(null);
@@ -315,8 +444,23 @@ const LastMilePage = () => {
};
const handlePrintTripSlip = (job: LastMileJob) => {
// Placeholder — wire to the trip-slip document service when available.
toast({ title: "Printing trip slip", description: job.bookingRef });
setTripSlipJob(job);
setTripSlipOpen(true);
};
const printTripSlip = () => {
if (!tripSlipJob) return;
const win = window.open("", "_blank", "width=820,height=920");
if (!win) {
toast({
title: "Pop-up blocked",
description: "Allow pop-ups to print the trip slip.",
variant: "destructive",
});
return;
}
win.document.write(buildTripSlipHtml(tripSlipJob));
win.document.close();
};
const columns = useMemo((): ColumnDef<LastMileJob>[] => {
@@ -582,6 +726,28 @@ const LastMilePage = () => {
</Group>
</Stack>
</Modal>
<Modal
opened={tripSlipOpen}
onClose={() => setTripSlipOpen(false)}
title={<Text fw={600}>Trip Slip</Text>}
size="lg"
radius="lg"
centered
>
<Stack gap="md">
{tripSlipJob && <TripSlipDocument job={tripSlipJob} />}
<Divider />
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={() => setTripSlipOpen(false)}>
Close
</Button>
<Button leftSection={<Printer size={16} />} onClick={printTripSlip}>
Print
</Button>
</Group>
</Stack>
</Modal>
</Stack>
);
};