import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { Button, Modal, Stack, Text, Textarea } from "@mantine/core"; import { Check, Eye, FilePen, FileSignature, MessageSquareWarning, ShieldCheck, XCircle, Zap, } from "lucide-react"; import type { Freight } from "@edr/types"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions"; import { api } from "@/services/api"; import { contractsService } from "@/services/contracts.service"; import { SectionCard } from "@/components/bookings/detail/SectionCard"; import { ContractDocumentEditorModal } from "@/components/contracts/ContractDocumentEditorModal"; import { ContractPreviewModal } from "@/components/contracts/ContractPreviewModal"; import type { useContractMutations } from "@/hooks/contracts/useContracts"; /** Dropdown-settings code holding the admin-configured contract validity days. */ const CONTRACT_VALIDITY_PERIODS_CODE = "contract_validity_periods"; type Mutations = ReturnType; interface ContractActionsToolbarProps { contract: Freight.IContract; mutations: Mutations; /** Switch the detail page to its Clearance Review tab. */ onReviewClearance?: () => void; } // Contract is in the pre-booking clearance phase — staff can review the // customer's uploaded documents. const CLEARANCE_REVIEW_STATUSES = [ "AWAITING_CLEARANCE_DOCUMENTS", "CLEARANCE_UNDER_REVIEW", "CLEARANCE_READY_FOR_BOOKING", ]; /** Detail-page staff actions: accept / request changes / reject / generate / sign. */ export function ContractActionsToolbar({ contract, mutations, onReviewClearance, }: ContractActionsToolbarProps) { const navigate = useNavigate(); const { user } = useAuth(); const { status } = contract; // Intake permissions are split per freight type: an accept:bulk holder must // not see the accept button on a container contract (API enforces the same). const arm = contract.freightType === "BULK" ? "bulk" : "container"; const mayAccept = hasPermission(user, FREIGHT_PERMS.contracts.staffAccept[arm]); const mayRequestChanges = hasPermission( user, FREIGHT_PERMS.contracts.requestChanges[arm], ); const mayReject = hasPermission(user, FREIGHT_PERMS.contracts.reject[arm]); const [editorOpen, setEditorOpen] = useState(false); const [editorMode, setEditorMode] = useState<"accept" | "edit">("accept"); const [previewOpen, setPreviewOpen] = useState(false); const [changesOpen, setChangesOpen] = useState(false); const [changesNote, setChangesNote] = useState(""); const [rejectOpen, setRejectOpen] = useState(false); const [rejectReason, setRejectReason] = useState(""); // Whether the document is editable depends on WHO is viewing — only the // approver whose turn it is may edit — so the server decides, not the client. const { data: draft } = useQuery({ queryKey: ["contracts", contract.id, "document-draft"], queryFn: () => contractsService.getContractDocumentDraft(contract.id), enabled: contract.status === "PENDING_APPROVAL", staleTime: 0, }); // Admin-configured validity durations (days) for the accept dialog. Staff can // only pick one of these — no free-typing. Read-only setting, fetched once. const { data: validitySetting, isLoading: validityLoading } = useQuery({ ...api.dropdownSettings.getByCode.queryOptions({ input: { code: CONTRACT_VALIDITY_PERIODS_CODE }, }), retry: false, }); const validityOptions = useMemo( () => [...(validitySetting?.children ?? [])] .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) .map((o) => ({ value: String(o.value), label: o.label })), [validitySetting], ); if (["REJECTED", "CANCELLED", "EXPIRED", "CONTRACT_CLOSED"].includes(status)) { return null; } if (status === "CHANGES_REQUESTED") { return ( No staff actions until the customer resubmits the contract. ); } const canAccept = status === "SUBMITTED" && (mayAccept || mayRequestChanges || mayReject); // The document stays editable for the whole approval chain, but only by the // approver whose turn it is. The server resolves that against the caller's // position type; the client cannot derive it. const canEditDocument = Boolean(draft?.editableByMe); const inApproval = status === "PENDING_APPROVAL"; // Signing now happens on the contract VIEW page (staff must open and read the // generated contract before signing) — no sign button in this toolbar. const canViewContract = ["CONTRACT_READY", "SIGNED_CUSTOMER"].includes(status) && Boolean(contract.contractGeneratedAt); // Show "Review clearance" while the contract is in the document-review phase. // Reviewer = GL (Path B / customs) or Operations (Path A / no customs). const canReviewClearance = Boolean(onReviewClearance) && CLEARANCE_REVIEW_STATUSES.includes(status); const clearanceReviewer = contract.customsClearingEnabled ? "Review clearance (GL)" : "Review clearance (Ops)"; return ( Confirm each step before it is applied. {canAccept && ( <> {mayAccept && ( )} {mayRequestChanges && ( )} {mayReject && ( )} )} {inApproval && ( <> {canEditDocument ? "It is your turn to approve. You can edit the articles before approving — the PDF is generated automatically once the last approver approves." : draft?.nextApproverRole ? `Awaiting ${draft.nextApproverRole}. Only the current approver can edit the document.` : "Awaiting approval."} {canEditDocument && ( )} )} {canViewContract && ( )} {canReviewClearance && ( )} {/* GL "Create booking" removed for now — clearance ends at finalize and the customer creates the booking in the portal. */} {!canAccept && !inApproval && !canViewContract && !canReviewClearance && ( No staff actions available for this status. Monitor until the workflow advances. )} {/* Accept / edit — review + optionally edit this contract's articles */} setEditorOpen(false)} contractId={contract.id} mode={editorMode} validityOptions={validityOptions} validityLoading={validityLoading} accepting={mutations.staffAccept.isPending} saving={mutations.updateDocument.isPending} onAccept={(days, snapshot) => mutations.staffAccept.mutate( { validityDays: days, documentSnapshot: snapshot }, { onSuccess: () => setEditorOpen(false) }, ) } onSaveEdit={(snapshot) => mutations.updateDocument.mutate(snapshot, { onSuccess: () => setEditorOpen(false), }) } /> setPreviewOpen(false)} contractId={contract.id} /> {/* Request changes */} setChangesOpen(false)} title="Request changes" centered >