mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
feat(last-mile): customer-signed LM contract gates the advance invoice
Approval now snapshots the rate estimate and generates a last-mile contract instead of invoicing immediately. The customer picks a delivery date on the confirm form, then reviews and signs the contract in the portal (saved signature or drawn); the signed PDF is stored as LM_<CustomerName>.pdf and only then is the advance invoice issued. Backoffice shows signature status and the contract download.
This commit is contained in:
@@ -94,6 +94,20 @@ export function LastMileRequestsPanel() {
|
||||
const invalidate = () =>
|
||||
qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE_REQUESTS.ROOT });
|
||||
|
||||
const downloadContract = async (r: LastMileRequest) => {
|
||||
try {
|
||||
const { data: blob } = await lastMileRequestsService.contractDocument(r.id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `LM_${r.booking?.company?.name?.replace(/[^A-Za-z0-9._-]+/g, "_") ?? r.id}.pdf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
toast({ title: "Contract PDF not available", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
const approve = useMutation({
|
||||
mutationFn: () =>
|
||||
lastMileRequestsService.approve(approveTarget!.id, Number(advanceAmount)),
|
||||
@@ -141,8 +155,16 @@ export function LastMileRequestsPanel() {
|
||||
id: "containers",
|
||||
header: () => <span>Requested Containers</span>,
|
||||
cell: ({ row }) => {
|
||||
const nums = row.original.requestedContainerNumbers;
|
||||
return <Text size="sm">{nums?.length ? nums.join(", ") : "—"}</Text>;
|
||||
const r = row.original;
|
||||
const nums = r.requestedContainerNumbers;
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
<Text size="sm">{nums?.length ? nums.join(", ") : "—"}</Text>
|
||||
{r.requestedDeliveryDate && (
|
||||
<Text size="xs" c="dimmed">Delivery: {r.requestedDeliveryDate}</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -162,6 +184,24 @@ export function LastMileRequestsPanel() {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "contract",
|
||||
header: () => <span>LM Contract</span>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
if (r.status !== "APPROVED") return <Text size="sm" c="dimmed">—</Text>;
|
||||
return (
|
||||
<Stack gap={4} align="flex-start">
|
||||
<Badge color={r.customerSignedAt ? "green" : "yellow"} variant="light" size="sm">
|
||||
{r.customerSignedAt ? "Signed" : "Awaiting signature"}
|
||||
</Badge>
|
||||
<Button size="compact-xs" variant="subtle" onClick={() => void downloadContract(r)}>
|
||||
Download PDF
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
...(canApprove
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -714,6 +714,7 @@ export const URL_CONSTANTS = {
|
||||
PRICE_ESTIMATE: (id: string) => `/last-mile-requests/${id}/price-estimate`,
|
||||
APPROVE: (id: string) => `/last-mile-requests/${id}/approve`,
|
||||
REJECT: (id: string) => `/last-mile-requests/${id}/reject`,
|
||||
CONTRACT_DOCUMENT: (id: string) => `/last-mile-requests/${id}/contract/document`,
|
||||
},
|
||||
|
||||
DRIVERS: {
|
||||
|
||||
@@ -28,6 +28,9 @@ export interface LastMileRequest {
|
||||
reviewedAt?: string | null;
|
||||
rejectionReason?: string | null;
|
||||
resultingLastMileId?: string | null;
|
||||
requestedDeliveryDate?: string | null;
|
||||
customerSignedAt?: string | null;
|
||||
signerDisplayName?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -58,4 +61,6 @@ export const lastMileRequestsService = {
|
||||
api.post<LastMileRequest>(LMR.APPROVE(id), { advanceAmount }),
|
||||
reject: (id: string, reason: string) =>
|
||||
api.post<LastMileRequest>(LMR.REJECT(id), { reason }),
|
||||
contractDocument: (id: string) =>
|
||||
api.get<Blob>(LMR.CONTRACT_DOCUMENT(id), { responseType: 'blob' }),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user