mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 09:28:19 +00:00
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:
@@ -0,0 +1,43 @@
|
||||
import { Button, Group, Modal, Stack, Text, ThemeIcon } from "@mantine/core";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
|
||||
export interface ContractSignSuccessModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
reference: string;
|
||||
message?: string;
|
||||
confirmLabel?: string;
|
||||
}
|
||||
|
||||
export function ContractSignSuccessModal({
|
||||
opened,
|
||||
onClose,
|
||||
reference,
|
||||
message = "Your signature has been recorded on the contract.",
|
||||
confirmLabel = "Back to contract",
|
||||
}: ContractSignSuccessModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title="Contract signed successfully"
|
||||
centered
|
||||
radius="lg"
|
||||
>
|
||||
<Stack gap="md" align="center" ta="center">
|
||||
<ThemeIcon size={56} radius="xl" color="edr-green" variant="light">
|
||||
<CheckCircle2 size={28} />
|
||||
</ThemeIcon>
|
||||
<Text fw={600}>{reference}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{message}
|
||||
</Text>
|
||||
<Group justify="center" mt="xs">
|
||||
<Button color="edr-green" onClick={onClose}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -118,6 +118,7 @@ export const URL_CONSTANTS = {
|
||||
CONFIRM_SUBMIT: (id: string) => `/api/contracts/${id}/confirm-submit`,
|
||||
CONTRACT_GENERATE: (id: string) => `/api/contracts/${id}/contract/generate`,
|
||||
CONTRACT_VIEW: (id: string) => `/api/contracts/${id}/contract/view`,
|
||||
CONTRACT_DOCUMENT: (id: string) => `/api/contracts/${id}/contract/document`,
|
||||
CONTRACT_SIGN: (id: string) => `/api/contracts/${id}/contract/sign`,
|
||||
RENEW: (id: string) => `/api/contracts/${id}/renew`,
|
||||
CLEARANCE: (id: string) => `/api/contracts/${id}/clearance`,
|
||||
|
||||
@@ -48,8 +48,10 @@ import { useDisclosure } from "@mantine/hooks";
|
||||
import { isViewable, type ViewableFile } from "@edr/ui-common";
|
||||
import type { Freight } from "@edr/types";
|
||||
import { api } from "@/services/api";
|
||||
import { contractsService } from "@/services/contracts.service";
|
||||
import { fileViewUrl } from "@/constants/apiConfig";
|
||||
import { useFileViewer } from "@/hooks/useFileViewer";
|
||||
import toast from "react-hot-toast";
|
||||
import { labelForDocCode } from "@/pages/bookings/resubmit";
|
||||
import { ContractClearancePanel } from "./ContractClearancePanel";
|
||||
import { formatRateUnit } from "./new-contract-form/unit-rates";
|
||||
@@ -223,6 +225,22 @@ export default function ContractDetailPage() {
|
||||
// The generated contract PDF — surfaced via a dedicated "View contract" button
|
||||
// in the header (it's excluded from the Documents tab groups).
|
||||
const contractPdf = files.find((f) => f.code === "contract");
|
||||
const hasContractDocument = Boolean(contractPdf || contract.contractGeneratedAt);
|
||||
|
||||
const downloadContractPdf = async () => {
|
||||
if (!contract?.id) return;
|
||||
try {
|
||||
const blob = await contractsService.downloadContractDocument(contract.id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = contractPdf?.name ?? `contract-${contract.reference}.pdf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
toast.error("Could not download contract PDF.");
|
||||
}
|
||||
};
|
||||
|
||||
const canSign = contract.status === "CONTRACT_READY";
|
||||
const customsPath = contract.customsClearingEnabled;
|
||||
@@ -303,6 +321,17 @@ export default function ContractDetailPage() {
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{hasContractDocument && (
|
||||
<Button
|
||||
variant="default"
|
||||
radius="md"
|
||||
size="md"
|
||||
leftSection={<Download size={16} />}
|
||||
onClick={() => void downloadContractPdf()}
|
||||
>
|
||||
Download PDF
|
||||
</Button>
|
||||
)}
|
||||
{canBookShipment && (
|
||||
<Button
|
||||
color="edr-green"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
@@ -13,18 +15,20 @@ 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 { api } from "@/services/api";
|
||||
|
||||
const CONSENT_TEXT =
|
||||
"I have read the entire contract and agree to its terms.";
|
||||
|
||||
/**
|
||||
* Customer contract preview + sign. Customers must open and read the generated
|
||||
* contract here before signing — there is no sign action on the detail page or
|
||||
* the contract list. Signing is only possible once the contract is generated
|
||||
* and ready (CONTRACT_READY).
|
||||
* Customer contract preview + sign. Customers must scroll through the full
|
||||
* contract and accept the terms before signing.
|
||||
*/
|
||||
export default function ContractViewPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -33,11 +37,12 @@ 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 saved signature for approval first; the customer can draw a fresh
|
||||
// one instead.
|
||||
const [drawNew, setDrawNew] = useState(false);
|
||||
const [hasScrolledToBottom, setHasScrolledToBottom] = useState(false);
|
||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
||||
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ["contract-view", id],
|
||||
@@ -47,6 +52,48 @@ export default function ContractViewPage() {
|
||||
|
||||
const savedSignatureImage = data?.savedSignature?.signatureImageUrl ?? null;
|
||||
const usingSaved = Boolean(savedSignatureImage) && !drawNew;
|
||||
const canProceedToSign = hasScrolledToBottom && agreedToTerms;
|
||||
|
||||
const checkScrollBottom = useCallback(() => {
|
||||
try {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
if (!win?.document?.documentElement) return;
|
||||
const el = win.document.documentElement;
|
||||
const threshold = 48;
|
||||
if (el.scrollHeight <= el.clientHeight + threshold) {
|
||||
setHasScrolledToBottom(true);
|
||||
return;
|
||||
}
|
||||
if (el.scrollTop + el.clientHeight >= el.scrollHeight - threshold) {
|
||||
setHasScrolledToBottom(true);
|
||||
}
|
||||
} catch {
|
||||
/* srcDoc is same-origin; ignore edge cases */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
checkScrollBottom();
|
||||
try {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
win?.addEventListener("scroll", checkScrollBottom);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
iframeRef.current?.contentWindow?.removeEventListener(
|
||||
"scroll",
|
||||
checkScrollBottom,
|
||||
);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
}, [checkScrollBottom]);
|
||||
|
||||
const signMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
@@ -56,11 +103,11 @@ export default function ContractViewPage() {
|
||||
? (savedSignatureImage as string)
|
||||
: (signatureData as string),
|
||||
signerDisplayName: signerName.trim(),
|
||||
consentText: "I agree to the terms of this contract.",
|
||||
consentText: CONSENT_TEXT,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success("Contract signed successfully");
|
||||
setSignOpen(false);
|
||||
setSuccessOpen(true);
|
||||
void refetch();
|
||||
void qc.invalidateQueries({
|
||||
queryKey: api.contracts.get.queryKey({ id: id! }),
|
||||
@@ -70,6 +117,7 @@ export default function ContractViewPage() {
|
||||
});
|
||||
|
||||
const openSign = () => {
|
||||
if (!canProceedToSign) return;
|
||||
setSignerName(data?.savedSignature?.signerDisplayName ?? "");
|
||||
setSignatureData(null);
|
||||
setDrawNew(false);
|
||||
@@ -85,6 +133,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]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Group justify="center" mih="40vh" align="center">
|
||||
@@ -105,7 +168,7 @@ export default function ContractViewPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p={{ base: "md", md: "xl" }}>
|
||||
<Box p={{ base: "md", md: "xl" }} pb={data.canSignCustomer ? 120 : undefined}>
|
||||
<Box maw={920} mx="auto">
|
||||
<Group justify="space-between" wrap="wrap" gap="sm" mb="md">
|
||||
<Button
|
||||
@@ -124,23 +187,28 @@ export default function ContractViewPage() {
|
||||
>
|
||||
Print
|
||||
</Button>
|
||||
{data.canSignCustomer && (
|
||||
<Button
|
||||
color="edr-green"
|
||||
leftSection={<FileSignature size={16} />}
|
||||
onClick={openSign}
|
||||
>
|
||||
{usingSaved ? "Approve & sign" : "Sign contract"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
leftSection={<Download size={16} />}
|
||||
onClick={() => void downloadPdf()}
|
||||
>
|
||||
Download PDF
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{data.canSignCustomer && !hasScrolledToBottom && (
|
||||
<Alert color="blue" variant="light" radius="md" mb="md">
|
||||
Please scroll through the entire contract before signing.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Paper withBorder radius="lg" p={0} style={{ overflow: "hidden" }}>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={data.html}
|
||||
title="Contract document"
|
||||
onLoad={handleIframeLoad}
|
||||
style={{
|
||||
width: "100%",
|
||||
minHeight: "80vh",
|
||||
@@ -151,6 +219,49 @@ export default function ContractViewPage() {
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{data.canSignCustomer && hasScrolledToBottom && (
|
||||
<Paper
|
||||
withBorder
|
||||
radius="lg"
|
||||
p="md"
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 100,
|
||||
borderTop: "1px solid var(--mantine-color-gray-3)",
|
||||
background: "var(--mantine-color-body)",
|
||||
}}
|
||||
>
|
||||
<Box maw={920} mx="auto">
|
||||
<Stack gap="sm">
|
||||
<Checkbox
|
||||
checked={agreedToTerms}
|
||||
onChange={(e) => setAgreedToTerms(e.currentTarget.checked)}
|
||||
disabled={!hasScrolledToBottom}
|
||||
label={CONSENT_TEXT}
|
||||
description={
|
||||
hasScrolledToBottom
|
||||
? "You may now sign the contract."
|
||||
: "Read the full contract above before you can agree and sign."
|
||||
}
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
color="edr-green"
|
||||
leftSection={<FileSignature size={16} />}
|
||||
disabled={!canProceedToSign}
|
||||
onClick={openSign}
|
||||
>
|
||||
{usingSaved ? "Approve & sign" : "Sign contract"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
opened={signOpen}
|
||||
onClose={() => setSignOpen(false)}
|
||||
@@ -217,6 +328,16 @@ export default function ContractViewPage() {
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<ContractSignSuccessModal
|
||||
opened={successOpen}
|
||||
reference={data.reference}
|
||||
message="Your signature has been recorded. EDR staff will counter-sign to complete the contract."
|
||||
onClose={() => {
|
||||
setSuccessOpen(false);
|
||||
navigate(`/contracts/${id}`);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,9 +39,11 @@ import {
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
import {
|
||||
CONTRACT_STEPS,
|
||||
EDIT_CONTRACT_STEPS,
|
||||
ContractFormInputValues,
|
||||
contractFormSchema,
|
||||
contractStepFields,
|
||||
editContractStepFields,
|
||||
initialContractFormValues,
|
||||
OPERATION_TYPES,
|
||||
type ContractFormValues,
|
||||
@@ -212,7 +214,7 @@ export default function NewContractPage({
|
||||
clearContractDraft();
|
||||
setPriceModalMode(null);
|
||||
queryClient.invalidateQueries({ queryKey: api.contracts.list.queryKey() });
|
||||
navigate(`/contracts/${priceContractId}`);
|
||||
navigate("/contracts");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -226,7 +228,7 @@ export default function NewContractPage({
|
||||
setPriceChangeResult(null);
|
||||
setPriceModalMode(null);
|
||||
queryClient.invalidateQueries({ queryKey: api.contracts.list.queryKey() });
|
||||
navigate(`/contracts/${priceContractId}`);
|
||||
navigate("/contracts");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -282,7 +284,10 @@ export default function NewContractPage({
|
||||
const destinationYard = form.watch("destinationYard");
|
||||
const operationType = form.watch("operationType");
|
||||
|
||||
const visibleSteps = useMemo(() => CONTRACT_STEPS, []);
|
||||
const visibleSteps = useMemo(
|
||||
() => (isEdit ? EDIT_CONTRACT_STEPS : CONTRACT_STEPS),
|
||||
[isEdit],
|
||||
);
|
||||
const visibleStepIds = useMemo<number[]>(
|
||||
() => visibleSteps.map((s) => s.id),
|
||||
[visibleSteps],
|
||||
@@ -463,10 +468,24 @@ export default function NewContractPage({
|
||||
}, [auth.company, auth.activeCompanyProfileId]);
|
||||
|
||||
async function handleContinue() {
|
||||
const valid = await form.trigger(contractStepFields[step], {
|
||||
shouldFocus: true,
|
||||
});
|
||||
if (!valid) return;
|
||||
const stepFields = isEdit ? editContractStepFields : contractStepFields;
|
||||
const fields = stepFields[step];
|
||||
if (fields.length > 0) {
|
||||
const valid = await form.trigger(fields, { shouldFocus: true });
|
||||
if (!valid) return;
|
||||
}
|
||||
if (isEdit && step === 2 && editContract) {
|
||||
const missing = missingRequiredDocKeys(
|
||||
editDocSettingQuery.data,
|
||||
editContract,
|
||||
editDocuments,
|
||||
);
|
||||
if (missing.length > 0) {
|
||||
setShowDocErrors(true);
|
||||
return;
|
||||
}
|
||||
setShowDocErrors(false);
|
||||
}
|
||||
goToStep(1);
|
||||
}
|
||||
|
||||
@@ -753,9 +772,34 @@ export default function NewContractPage({
|
||||
</StepCard>
|
||||
)}
|
||||
|
||||
{/* Step 2 — Documents. */}
|
||||
{/* Step 2 — Review & Submit. */}
|
||||
{step === 2 && (
|
||||
{/* Step 2 (edit) — Documents. */}
|
||||
{step === 2 && isEdit && editContract && (
|
||||
<StepCard>
|
||||
<StepHeader
|
||||
title="Contract Documents"
|
||||
description="Upload or replace the documents required for this contract before resubmitting."
|
||||
/>
|
||||
<ContractDocsEditor
|
||||
contract={editContract}
|
||||
value={editDocuments}
|
||||
onChange={setEditDocuments}
|
||||
errors={
|
||||
showDocErrors
|
||||
? Object.fromEntries(
|
||||
missingRequiredDocKeys(
|
||||
editDocSettingQuery.data,
|
||||
editContract,
|
||||
editDocuments,
|
||||
).map((k) => [k, "Required"]),
|
||||
)
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
</StepCard>
|
||||
)}
|
||||
|
||||
{/* Step 2 (create) / Step 3 (edit) — Review & Submit. */}
|
||||
{((step === 2 && !isEdit) || (step === 3 && isEdit)) && (
|
||||
<Step8Review
|
||||
form={form}
|
||||
setStep={setStep}
|
||||
@@ -781,26 +825,6 @@ export default function NewContractPage({
|
||||
persistAndPriceMutation.variables?.mode === "submit"
|
||||
}
|
||||
isEdit={isEdit}
|
||||
documentsEditor={
|
||||
isEdit && editContract ? (
|
||||
<ContractDocsEditor
|
||||
contract={editContract}
|
||||
value={editDocuments}
|
||||
onChange={setEditDocuments}
|
||||
errors={
|
||||
showDocErrors
|
||||
? Object.fromEntries(
|
||||
missingRequiredDocKeys(
|
||||
editDocSettingQuery.data,
|
||||
editContract,
|
||||
editDocuments,
|
||||
).map((k) => [k, "Required"]),
|
||||
)
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -11,6 +11,14 @@ export const CONTRACT_STEPS = [
|
||||
{ id: 2, label: "Review & Submit", short: "Review" },
|
||||
] as const;
|
||||
|
||||
/** Edit flow (CHANGES_REQUESTED): documents on step 2, review on step 3. */
|
||||
export const EDIT_CONTRACT_STEPS = [
|
||||
{ id: 0, label: "Setup", short: "Setup" },
|
||||
{ id: 1, label: "Cargo & Route", short: "Cargo & Route" },
|
||||
{ id: 2, label: "Documents", short: "Documents" },
|
||||
{ id: 3, label: "Review & Submit", short: "Review" },
|
||||
] as const;
|
||||
|
||||
export const OPERATION_TYPES = [
|
||||
"import",
|
||||
"export",
|
||||
@@ -317,3 +325,15 @@ export const contractStepFields: Record<
|
||||
// company profile documents are attached to the contract automatically.)
|
||||
2: ["notes"],
|
||||
};
|
||||
|
||||
/** Field validation per step when editing a CHANGES_REQUESTED contract. */
|
||||
export const editContractStepFields: Record<
|
||||
number,
|
||||
Array<Path<ContractFormValues>>
|
||||
> = {
|
||||
0: contractStepFields[0],
|
||||
1: contractStepFields[1],
|
||||
// Step 2 — Documents: validated via missingRequiredDocKeys in the wizard.
|
||||
2: [],
|
||||
3: ["notes"],
|
||||
};
|
||||
|
||||
@@ -245,29 +245,40 @@ export function Step2ServiceType({
|
||||
const lastMileEnabled = form.watch("lastMile.enabled");
|
||||
|
||||
const prevServiceType = useRef(serviceType);
|
||||
useEffect(() => {
|
||||
form.setValue(
|
||||
"firstMile",
|
||||
{ enabled: false, pickUpAddress: "", exactLocation: "", lat: null, lng: null },
|
||||
{ shouldValidate: true },
|
||||
);
|
||||
}, [includesFirstMile]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue(
|
||||
"lastMile",
|
||||
{ enabled: false, deliveryAddress: "", exactLocation: "", lat: null, lng: null },
|
||||
{ shouldValidate: true },
|
||||
);
|
||||
}, [includesLastMile]);
|
||||
|
||||
// Customs clearance bundling is driven by the service: includesCustoms → the
|
||||
// contract follows Path B (clearance docs after sign); otherwise Path A.
|
||||
useEffect(() => {
|
||||
const prev = prevServiceType.current;
|
||||
prevServiceType.current = serviceType;
|
||||
if (!prev || prev === serviceType) return;
|
||||
|
||||
if (!includesFirstMile) {
|
||||
form.setValue(
|
||||
"firstMile",
|
||||
{
|
||||
enabled: false,
|
||||
pickUpAddress: "",
|
||||
exactLocation: "",
|
||||
lat: null,
|
||||
lng: null,
|
||||
},
|
||||
{ shouldValidate: true },
|
||||
);
|
||||
}
|
||||
if (!includesLastMile) {
|
||||
form.setValue(
|
||||
"lastMile",
|
||||
{
|
||||
enabled: false,
|
||||
deliveryAddress: "",
|
||||
exactLocation: "",
|
||||
lat: null,
|
||||
lng: null,
|
||||
},
|
||||
{ shouldValidate: true },
|
||||
);
|
||||
}
|
||||
|
||||
// Customs clearance bundling is driven by the service: includesCustoms → the
|
||||
// contract follows Path B (clearance docs after sign); otherwise Path A.
|
||||
if (includesCustoms) {
|
||||
form.setValue("customsClearingEnabled", true, { shouldDirty: true });
|
||||
form.setValue("customsClearingAgent", "", { shouldDirty: true });
|
||||
@@ -275,7 +286,7 @@ export function Step2ServiceType({
|
||||
form.setValue("customsClearingEnabled", false, { shouldDirty: true });
|
||||
form.setValue("customsClearingAgent", "", { shouldDirty: true });
|
||||
}
|
||||
}, [serviceTypeId, form]);
|
||||
}, [serviceType, includesFirstMile, includesLastMile, includesCustoms, form]);
|
||||
|
||||
const showServiceSections =
|
||||
serviceType != null || includesFirstMile || includesLastMile;
|
||||
|
||||
@@ -73,13 +73,6 @@ export function Step3CargoScope({
|
||||
}
|
||||
}, [parentId, form]);
|
||||
|
||||
// Clear reefer when switching to container (container reefer is per-booking).
|
||||
useEffect(() => {
|
||||
if (cargoType !== "bulk" && form.getValues("isRefrigerated")) {
|
||||
form.setValue("isRefrigerated", false);
|
||||
}
|
||||
}, [cargoType, form]);
|
||||
|
||||
const freightTypeGroups = useMemo(() => {
|
||||
if (!referenceData?.cargo_type) return [];
|
||||
return referenceData.cargo_type.filter(
|
||||
|
||||
@@ -409,6 +409,16 @@ export function Step8Review({
|
||||
: "Not requested"
|
||||
}
|
||||
/>
|
||||
<SummaryItem
|
||||
icon={<Package size={18} />}
|
||||
label="Hazardous cargo"
|
||||
value={values.isHazardous ? "Yes" : "No"}
|
||||
/>
|
||||
<SummaryItem
|
||||
icon={<Package size={18} />}
|
||||
label="Refrigerated"
|
||||
value={values.isRefrigerated ? "Yes" : "No"}
|
||||
/>
|
||||
<SummaryItem
|
||||
icon={<FileText size={18} />}
|
||||
label="Documents"
|
||||
|
||||
@@ -203,6 +203,13 @@ export const contractsService = {
|
||||
return data.data ?? data;
|
||||
},
|
||||
|
||||
downloadContractDocument: async (id: string): Promise<Blob> => {
|
||||
const { data } = await client.get(C.CONTRACT_DOCUMENT(id), {
|
||||
responseType: "blob",
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
signContract: async (
|
||||
id: string,
|
||||
payload: SignContractPayload,
|
||||
|
||||
Reference in New Issue
Block a user