mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
feat(bookings): enhance contract viewer and backoffice UI consistency
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -21,6 +21,7 @@ export default function BookingContractPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [signOpen, setSignOpen] = useState(false);
|
||||
const [signerName, setSignerName] = useState("");
|
||||
const [signatureData, setSignatureData] = useState<string | null>(null);
|
||||
@@ -58,6 +59,12 @@ export default function BookingContractPage() {
|
||||
}
|
||||
}, [id, data?.reference]);
|
||||
|
||||
const handlePrint = useCallback(() => {
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.print();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[40vh] items-center justify-center">
|
||||
@@ -77,8 +84,6 @@ export default function BookingContractPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const bodyHtml = extractBodyHtml(data.html);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-muted/30 p-4 md:p-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
@@ -88,7 +93,7 @@ export default function BookingContractPage() {
|
||||
Back to booking
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => window.print()}>
|
||||
<Button variant="outline" size="sm" onClick={handlePrint}>
|
||||
<Printer className="mr-2 size-4" />
|
||||
Print
|
||||
</Button>
|
||||
@@ -105,9 +110,12 @@ export default function BookingContractPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article
|
||||
className="contract-document rounded-lg border bg-white p-6 shadow-sm print:shadow-none md:p-10"
|
||||
dangerouslySetInnerHTML={{ __html: bodyHtml }}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={data.html}
|
||||
className="w-full rounded-lg border bg-white shadow-sm"
|
||||
style={{ minHeight: "80vh" }}
|
||||
title="Contract document"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -158,8 +166,3 @@ export default function BookingContractPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function extractBodyHtml(fullHtml: string): string {
|
||||
const match = fullHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
return match ? match[1] : fullHtml;
|
||||
}
|
||||
|
||||
@@ -844,28 +844,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{(booking.status === "CONTRACT_READY" ||
|
||||
booking.status === "SIGNED_CUSTOMER" ||
|
||||
booking.status === "FULLY_EXECUTED") && (
|
||||
<Card className="border-primary/30 bg-primary/5">
|
||||
<CardContent className="flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">Contract ready</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Review the agreement and apply your digital signature.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
onClick={() => navigate(`/bookings/${booking.id}/contract`)}
|
||||
>
|
||||
<FileSignature className="size-4" />
|
||||
View & sign contract
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{renderContractCard(booking, navigate)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -1218,6 +1197,78 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderContractCard(
|
||||
booking: Freight.IBooking,
|
||||
navigate: ReturnType<typeof useNavigate>,
|
||||
) {
|
||||
const s = booking.status;
|
||||
if (
|
||||
s !== "APPROVED_PENDING_SIGNATURE" &&
|
||||
s !== "CONTRACT_READY" &&
|
||||
s !== "SIGNED_CUSTOMER" &&
|
||||
s !== "FULLY_EXECUTED"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config: Record<
|
||||
string,
|
||||
{ title: string; description: string; buttonLabel?: string }
|
||||
> = {
|
||||
APPROVED_PENDING_SIGNATURE: {
|
||||
title: "Contract being prepared",
|
||||
description:
|
||||
"Your booking has been approved. The contract is being generated and will be available shortly.",
|
||||
},
|
||||
CONTRACT_READY: {
|
||||
title: "Contract ready for signature",
|
||||
description:
|
||||
"Review the agreement and apply your digital signature.",
|
||||
buttonLabel: "View & sign contract",
|
||||
},
|
||||
SIGNED_CUSTOMER: {
|
||||
title: "You have signed the contract",
|
||||
description:
|
||||
"Your signature has been submitted. Awaiting staff signature to finalize.",
|
||||
buttonLabel: "View contract",
|
||||
},
|
||||
FULLY_EXECUTED: {
|
||||
title: "Contract fully executed",
|
||||
description:
|
||||
"The contract has been fully signed and executed by all parties.",
|
||||
buttonLabel: "View contract",
|
||||
},
|
||||
};
|
||||
|
||||
const c = config[s];
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"border-primary/30 bg-primary/5",
|
||||
s === "FULLY_EXECUTED" && "border-emerald-300 bg-emerald-50",
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">{c.title}</p>
|
||||
<p className="text-sm text-muted-foreground">{c.description}</p>
|
||||
</div>
|
||||
{c.buttonLabel && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
onClick={() => navigate(`/bookings/${booking.id}/contract`)}
|
||||
>
|
||||
<FileSignature className="size-4" />
|
||||
{c.buttonLabel}
|
||||
</button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function RouteEndpoint({
|
||||
label,
|
||||
station,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
UseQueryOptions,
|
||||
QueryObserverOptions,
|
||||
} from "@tanstack/react-query";
|
||||
import { UseQueryOptions, QueryObserverOptions } from "@tanstack/react-query";
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
|
||||
import { URL_CONSTANTS } from "@/constants/URLS";
|
||||
|
||||
@@ -23,11 +20,15 @@ function setCookie(name: string, value: string, days: number) {
|
||||
}
|
||||
|
||||
function clearAuthCookies() {
|
||||
["auth-token", "refresh-token", "auth-user", "current-position-id", "selected-position-id"].forEach(
|
||||
(name) => {
|
||||
document.cookie = `${name}=; Max-Age=0; path=/`;
|
||||
},
|
||||
);
|
||||
[
|
||||
"auth-token",
|
||||
"refresh-token",
|
||||
"auth-user",
|
||||
"current-position-id",
|
||||
"selected-position-id",
|
||||
].forEach((name) => {
|
||||
document.cookie = `${name}=; Max-Age=0; path=/`;
|
||||
});
|
||||
}
|
||||
|
||||
// Attach auth token to every request
|
||||
@@ -96,17 +97,13 @@ client.interceptors.response.use(
|
||||
if (!refreshToken) {
|
||||
isRefreshing = false;
|
||||
clearAuthCookies();
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await client.post<{ data: { token: string; refreshToken: string } }>(
|
||||
URL_CONSTANTS.AUTH.REFRESH_TOKEN,
|
||||
{ refreshToken },
|
||||
);
|
||||
const { data } = await client.post<{
|
||||
data: { token: string; refreshToken: string };
|
||||
}>(URL_CONSTANTS.AUTH.REFRESH_TOKEN, { refreshToken });
|
||||
const { token, refreshToken: newRefreshToken } = data.data;
|
||||
setCookie("auth-token", token, 7);
|
||||
setCookie("refresh-token", newRefreshToken, 7);
|
||||
|
||||
Reference in New Issue
Block a user