shoc-frontend-new/src/app/v/[token]/dispatch/_components/uplift-requests-section.tsx
2026-08-11 08:57:38 -03:00

223 lines
7.1 KiB
TypeScript

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<void>;
pollDocument: (documentId: number) => Promise<VendorPortalDocumentStatus | undefined>;
};
type UseVendorUpliftActionsArgs = {
token: string;
dispatchId: number;
currentNte?: number;
onChanged: () => Promise<void>;
evidence: ReturnType<typeof useVendorUpliftEvidence>;
};
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<string | number | null>(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 && (
<VendorPortalFeedback>
No uplift requests yet. Use the form below if the work will exceed the current NTE.
</VendorPortalFeedback>
)}
{Boolean(infoMessage) && <VendorPortalFeedback>{infoMessage}</VendorPortalFeedback>}
{upliftRequests.map((request) => (
<UpliftRequestCard
key={request.id}
request={request}
locked={locked}
canAct={!locked}
uploadingPercent={evidence.uploadingPercent}
submitting={submitting}
errorMessage={errorRequestId === request.id ? requestError : ""}
onWithdraw={(r) => void handleWithdraw(r)}
onRevise={(r, values) => void handleRevise(r, values)}
onCancelRevise={evidence.reset}
/>
))}
{canShowForm && (
<UpliftRequestForm
mode="create"
currentNte={currentNte}
submitting={submitting}
uploadingPercent={evidence.uploadingPercent}
errorMessage={formError}
onSubmit={(values) => void handleCreate(values)}
/>
)}
</>
);
}