Implement contract document download functionality and enhance clearance review status checks

- Added methods to assert clearance reviewable, finalizable, and uploadable statuses in the ContractClearanceService.
- Introduced a new endpoint in ContractsController for downloading contract PDFs.
- Implemented download functionality in the contracts service for both backoffice and portal applications.
- Updated UI components to include download buttons for contract PDFs in relevant pages.
- Enhanced contract request and view pages to support contract document downloads.
This commit is contained in:
marshal
2026-07-01 07:27:53 +03:00
parent 7654b18385
commit ccd5d6de31
24 changed files with 785 additions and 133 deletions

View File

@@ -1,4 +1,4 @@
import { useRef, 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 {
@@ -13,18 +13,17 @@ import {
Text,
TextInput,
} from "@mantine/core";
import { ArrowLeft, FileSignature, Printer } from "lucide-react";
import { ArrowLeft, Download, FileSignature, Printer } from "lucide-react";
import toast from "react-hot-toast";
import { ContractSignSuccessModal } from "@/components/contracts/ContractSignSuccessModal";
import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad";
import { contractsService } from "@/services/contracts.service";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
/**
* Staff contract preview + sign. Staff must open and read the generated
* contract here before signing — there is no sign action on the detail page or
* the list table. Signing as STAFF is only possible once the contract has been
* generated and is in CONTRACT_READY / SIGNED_CUSTOMER.
* contract here before signing.
*/
export default function ContractViewPage() {
const { id } = useParams<{ id: string }>();
@@ -33,9 +32,9 @@ export default function ContractViewPage() {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [signOpen, setSignOpen] = useState(false);
const [successOpen, setSuccessOpen] = useState(false);
const [signerName, setSignerName] = useState("");
const [signatureData, setSignatureData] = useState<string | null>(null);
// Offer the staff member's saved signature first; they can draw a fresh one.
const [drawNew, setDrawNew] = useState(false);
const { data, isLoading, isError, refetch } = useQuery({
@@ -58,8 +57,8 @@ export default function ContractViewPage() {
consentText: "I confirm this contract on behalf of EDR.",
}),
onSuccess: () => {
toast.success("Contract signed");
setSignOpen(false);
setSuccessOpen(true);
void refetch();
void qc.invalidateQueries({ queryKey: QUERY_KEYS.CONTRACTS.byId(id!) });
void qc.invalidateQueries({ queryKey: QUERY_KEYS.CONTRACTS.ROOT });
@@ -69,6 +68,21 @@ export default function ContractViewPage() {
const handlePrint = () => iframeRef.current?.contentWindow?.print();
const downloadPdf = useCallback(async () => {
if (!id) return;
try {
const blob = await contractsService.downloadContractDocument(id);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `contract-${data?.reference ?? id}.pdf`;
a.click();
URL.revokeObjectURL(url);
} catch {
toast.error("Could not download contract PDF.");
}
}, [id, data?.reference]);
const openSign = () => {
setSignerName(data?.savedSignature?.signerDisplayName ?? "");
setSignatureData(null);
@@ -124,6 +138,13 @@ export default function ContractViewPage() {
>
Print
</Button>
<Button
variant="default"
leftSection={<Download size={16} />}
onClick={() => void downloadPdf()}
>
Download PDF
</Button>
{data.canSignStaff && (
<Button
color="edr-green"
@@ -217,6 +238,16 @@ export default function ContractViewPage() {
</Group>
</Stack>
</Modal>
<ContractSignSuccessModal
opened={successOpen}
reference={data.reference}
message="The contract has been counter-signed. The customer will be notified of the next steps."
onClose={() => {
setSuccessOpen(false);
navigate(`/dashboard/contract-requests/${data.contractId}`);
}}
/>
</Box>
);
}