import { useMemo, useState } from "react"; import { Check, ShieldCheck, X } from "lucide-react"; import { Stack, Group, Text, Badge, Button, Box, Modal, Select, Textarea, } from "@mantine/core"; import type { Freight } from "@edr/types"; import { formatContractApprovalProgress } from "@/features/contracts/contract-approval-progress"; import { SectionCard } from "@/components/bookings/detail/SectionCard"; import type { useContractMutations } from "@/hooks/contracts/useContracts"; type Mutations = ReturnType; interface ContractApprovalStepsCardProps { contract: Freight.IContract; mutations: Mutations; } /** Approval chain with inline approve on the next pending step. */ export function ContractApprovalStepsCard({ contract, mutations, }: ContractApprovalStepsCardProps) { const [confirmOpen, setConfirmOpen] = useState(false); const [pendingStep, setPendingStep] = useState(null); const [rejectOpen, setRejectOpen] = useState(false); const [rejectStepRow, setRejectStepRow] = useState(null); const [rejectReason, setRejectReason] = useState(""); // Where the rejection lands: "CUSTOMER" (terminal, resubmit) or the id of an // earlier APPROVED step to send the chain back to. First approver has no // choice — customer only. const [rejectTarget, setRejectTarget] = useState("CUSTOMER"); const steps = useMemo( () => [...(contract.approvalSteps ?? [])].sort( (a, b) => a.stepOrder - b.stepOrder, ), [contract.approvalSteps], ); const nextPending = steps.find((s) => s.status === "PENDING"); const summary = formatContractApprovalProgress(contract.status, steps); // The card also renders read-only trails (e.g. a REJECTED contract) — only // offer approve/reject while the backend accepts step actions. const actionable = contract.status === "PENDING_APPROVAL" || contract.status === "APPROVED_PENDING_SIGNATURE"; // Approvers review a live preview of the document; there is no PDF to // generate first — the final approval is what produces it. const openApprove = (step: Freight.IContractApprovalStep) => { setPendingStep(step); setConfirmOpen(true); }; const closeApprove = () => { setConfirmOpen(false); setPendingStep(null); }; const runApprove = () => { if (!pendingStep) return; mutations.approveStep.mutate( { stepId: pendingStep.id }, { onSuccess: () => closeApprove() }, ); }; const openReject = (step: Freight.IContractApprovalStep) => { setRejectStepRow(step); setRejectReason(""); setRejectTarget("CUSTOMER"); setRejectOpen(true); }; const closeReject = () => { setRejectOpen(false); setRejectStepRow(null); setRejectReason(""); setRejectTarget("CUSTOMER"); }; const trimmedReason = rejectReason.trim(); // Earlier stages this rejection can be returned to — only stages that have // already approved. Empty for the first approver, whose only target is the // customer. const returnableSteps = rejectStepRow ? steps.filter( (s) => s.stepOrder < rejectStepRow.stepOrder && s.status === "APPROVED", ) : []; const sendBack = rejectTarget !== "CUSTOMER"; const targetStep = sendBack ? returnableSteps.find((s) => s.id === rejectTarget) : undefined; const runReject = () => { if (!rejectStepRow || !trimmedReason) return; mutations.rejectStep.mutate( { stepId: rejectStepRow.id, reason: trimmedReason, returnToStepId: sendBack ? rejectTarget : undefined, }, { onSuccess: () => closeReject() }, ); }; const subtitle = summary.detail || (nextPending ? `Next: ${nextPending.requiredRole} · step ${nextPending.stepOrder}` : steps.length ? "All steps complete" : "Accept submission to begin"); return ( <> {steps.filter((s) => s.status === "APPROVED").length}/{steps.length} } > {subtitle} {steps.length === 0 ? ( Use Accept for approval in staff actions to instantiate steps. ) : ( {steps.map((step) => ( openApprove(step)} onReject={() => openReject(step)} /> ))} )} You are about to approve the{" "} {pendingStep?.requiredRole} {" "} step for contract{" "} {contract.reference} . This action cannot be undone from this screen. {returnableSteps.length > 0 && (