shoc-frontend-new/src/app/v/[token]/dispatch/_components/uplift-request-form.tsx
2026-08-11 13:49:03 -03:00

148 lines
4.5 KiB
TypeScript

import { useState } from "react";
import { Text } from "@/components/ui/text";
import { validateVendorDocument } from "@/domain/vendor-portal/lib/document-validation";
import {
isApprovalRequired,
validateUpliftAmount,
type UpliftFormValues,
} from "@/domain/vendor-portal/lib/uplift-validation";
type UpliftRequestFormProps = {
mode: "create" | "revise";
currentNte?: number;
initialRequested?: string;
initialReason?: string;
hasExistingEvidence?: boolean;
submitting: boolean;
uploadingPercent: number | undefined;
errorMessage: string;
onSubmit: (values: UpliftFormValues) => void;
onCancel?: () => void;
};
export function UpliftRequestForm({
mode,
currentNte,
initialRequested = "",
initialReason = "",
hasExistingEvidence = false,
submitting,
uploadingPercent,
errorMessage,
onSubmit,
onCancel,
}: UpliftRequestFormProps) {
const [requested, setRequested] = useState(initialRequested);
const [reason, setReason] = useState(initialReason);
const [evidenceFile, setEvidenceFile] = useState<File | undefined>();
const [formError, setFormError] = useState("");
const showApprovalFields = mode === "revise" || isApprovalRequired(requested, currentNte);
const submit = (event: React.FormEvent) => {
event.preventDefault();
const amount = Number.parseFloat(requested);
const validationError = validateUpliftAmount(requested, currentNte, reason, {
file: evidenceFile,
hasExisting: hasExistingEvidence,
mode,
});
setFormError(validationError);
if (validationError) {
return;
}
onSubmit({ amount, reason: reason.trim(), evidenceFile });
};
const chooseFile = (file?: File) => {
const validationMessage = file ? validateVendorDocument(file) : undefined;
setFormError(validationMessage ?? "");
setEvidenceFile(file);
};
const shownError = errorMessage || formError;
const busy = submitting || uploadingPercent !== undefined;
return (
<form
onSubmit={(event) => void submit(event)}
className="vp-uplift-form"
aria-label={mode === "revise" ? "Revise uplift request" : "Request NTE uplift"}
>
{mode === "create" && (
<div className="vp-row">
<strong>Current NTE</strong>
<span>${Number(currentNte ?? 0).toFixed(2)}</span>
</div>
)}
<div className="vp-row">
<strong>
<label htmlFor={`uplift-amount-${mode}`}>New total NTE</label>
</strong>
<input
id={`uplift-amount-${mode}`}
type="number"
step="0.01"
min="0"
className="vp-input"
value={requested}
onChange={(event) => {
setRequested(event.target.value);
setFormError("");
}}
placeholder="0.00"
/>
</div>
{showApprovalFields && (
<>
<div className="vp-row">
<strong>
<label htmlFor={`uplift-reason-${mode}`}>Reason</label>
</strong>
<textarea
id={`uplift-reason-${mode}`}
className="vp-textarea"
value={reason}
onChange={(event) => {
setReason(event.target.value);
setFormError("");
}}
placeholder="Why is the uplift needed?"
/>
</div>
<div className="vp-row">
<strong>
<label htmlFor={`uplift-evidence-${mode}`}>Evidence (PDF/JPG/PNG)</label>
</strong>
<input
id={`uplift-evidence-${mode}`}
type="file"
className="vp-input"
accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png"
onChange={(event) => chooseFile(event.target.files?.[0])}
/>
</div>
</>
)}
{uploadingPercent !== undefined && (
<div className="vp-row" role="status" aria-live="polite">
<strong>Uploading</strong>
<span>{uploadingPercent}%</span>
</div>
)}
<Text variant="error" when={Boolean(shownError)}>
{shownError}
</Text>
<div className="vp-actions">
<button type="submit" className="vp-btn vp-btn-primary" disabled={busy}>
{busy ? "Submitting…" : mode === "revise" ? "Submit revision" : "Request uplift"}
</button>
{mode === "revise" && (
<button type="button" className="vp-btn vp-btn-ghost" onClick={onCancel} disabled={busy}>
Cancel
</button>
)}
</div>
</form>
);
}