import { useCallback, useState } from "react"; import { VendorPortalFeedback } from "@/app/v/_components/vendor-portal-feedback"; import { vendorPortalApi } from "@/domain/vendor-portal/api/vendor-portal-api"; import { createUpliftRequestKey } from "@/domain/vendor-portal/lib/request-key"; import type { VendorPortalDocumentStatus, VendorPortalUpliftRequest, } from "@/domain/vendor-portal/types/vendor-portal"; import { UpliftRequestForm } from "@/app/v/[token]/dispatch/_components/uplift-request-form"; import type { UpliftFormValues } from "@/domain/vendor-portal/lib/uplift-validation"; import { UpliftRequestCard } from "@/app/v/[token]/dispatch/_components/uplift-request-card"; import { useVendorUpliftEvidence, type EvidenceScanState, } from "@/app/v/[token]/dispatch/_components/use-uplift-evidence"; const ACTIVE_STATUSES = new Set(["Pending", "ChangesRequested"]); type UpliftRequestsSectionProps = { token: string; dispatchId: number; currentNte?: number; upliftRequests: VendorPortalUpliftRequest[]; locked: boolean; onChanged: () => Promise; pollDocument: (documentId: number) => Promise; }; type UseVendorUpliftActionsArgs = { token: string; dispatchId: number; currentNte?: number; onChanged: () => Promise; evidence: ReturnType; }; function scanErrorMessage(scan: EvidenceScanState): string { if (scan === "Rejected") { return "The evidence file was rejected by the security scan."; } return "Security scan timed out. Please try again once the file finishes scanning."; } function useVendorUpliftActions({ token, dispatchId, currentNte, onChanged, evidence, }: UseVendorUpliftActionsArgs) { const [submitting, setSubmitting] = useState(false); const [formError, setFormError] = useState(""); const [infoMessage, setInfoMessage] = useState(""); const [requestError, setRequestError] = useState(""); const [errorRequestId, setErrorRequestId] = useState(null); const uploadAndScan = useCallback( async (file: File): Promise<{ documentId: number; scan: EvidenceScanState }> => { const document = await evidence.uploadEvidence({ token, dispatchId, file }); const scan = await evidence.waitForScan(document.id); evidence.setScanState(scan); return { documentId: document.id, scan }; }, [evidence, token, dispatchId], ); const handleCreate = async (values: UpliftFormValues) => { setFormError(""); setInfoMessage(""); setSubmitting(true); try { const requiresApproval = currentNte == null || values.amount > (currentNte ?? 0); let evidenceDocumentId: number | undefined; if (requiresApproval) { if (!values.evidenceFile) { setFormError( "Attach PDF, JPG, or PNG evidence — approval-required requests need evidence.", ); return; } const result = await uploadAndScan(values.evidenceFile); if (result.scan !== "Passed") { setFormError(scanErrorMessage(result.scan)); return; } evidenceDocumentId = result.documentId; } const outcome = await vendorPortalApi.requestUplift(token, dispatchId, { requestedNTE: values.amount, reason: values.reason, requestKey: createUpliftRequestKey(), evidenceDocumentId, }); evidence.reset(); await onChanged(); if (outcome.outcome === "NoApprovalRequired") { setInfoMessage("No approval required — the existing NTE already covers this amount."); } } catch { setFormError("Unable to request the NTE uplift. Please try again."); } finally { setSubmitting(false); } }; const handleWithdraw = async (request: VendorPortalUpliftRequest) => { if (!window.confirm("Withdraw this uplift request?")) return; setErrorRequestId(request.id); setRequestError(""); setSubmitting(true); try { await vendorPortalApi.withdrawUplift(token, dispatchId, request.id); await onChanged(); } catch { setRequestError("Unable to withdraw the uplift request. Please try again."); } finally { setSubmitting(false); } }; const handleRevise = async (request: VendorPortalUpliftRequest, values: UpliftFormValues) => { setErrorRequestId(request.id); setRequestError(""); setSubmitting(true); try { const requiresApproval = request.currentNTE == null || values.amount > (request.currentNTE ?? 0); let evidenceDocumentId = request.evidenceDocumentId; if (requiresApproval && values.evidenceFile) { const result = await uploadAndScan(values.evidenceFile); if (result.scan !== "Passed") { setRequestError(scanErrorMessage(result.scan)); return; } evidenceDocumentId = result.documentId; } await vendorPortalApi.reviseUplift(token, dispatchId, request.id, { requestedNTE: values.amount, reason: values.reason, evidenceDocumentId, }); evidence.reset(); await onChanged(); } catch { setRequestError("Unable to revise the uplift request. Please try again."); } finally { setSubmitting(false); } }; return { submitting, formError, infoMessage, requestError, errorRequestId, handleCreate, handleWithdraw, handleRevise, }; } export function UpliftRequestsSection({ token, dispatchId, currentNte, upliftRequests, locked, onChanged, pollDocument, }: UpliftRequestsSectionProps) { const evidence = useVendorUpliftEvidence({ pollDocument }); const { submitting, formError, infoMessage, requestError, errorRequestId, handleCreate, handleWithdraw, handleRevise, } = useVendorUpliftActions({ token, dispatchId, currentNte, onChanged, evidence }); const activeRequest = upliftRequests.find((request) => ACTIVE_STATUSES.has(request.status)); const canShowForm = !locked && !activeRequest; return ( <> {upliftRequests.length === 0 && !activeRequest && ( No uplift requests yet. Use the form below if the work will exceed the current NTE. )} {Boolean(infoMessage) && {infoMessage}} {upliftRequests.map((request) => ( void handleWithdraw(r)} onRevise={(r, values) => void handleRevise(r, values)} onCancelRevise={evidence.reset} /> ))} {canShowForm && ( void handleCreate(values)} /> )} ); }