mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 06:02:08 +00:00
223 lines
7.1 KiB
TypeScript
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)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|