import { useEffect, useState, type ReactNode } from "react"; import type { DynamicField, PrRow } from "../api/form"; import type { PurchaseFormSnapshot } from "../types/purchasePayload"; import { buildPrLines, fetchFormSchema } from "../api/form"; import { Icon } from "./Icon"; const FALLBACK_INTERVALS = [ "Daily", "Weekly", "Monthly", "Bi-Monthly", "Quarterly", "Bi-Annual", "Annual", ]; /** Aligns label baselines for side-by-side fields; controls share one height. */ const CTRL = "h-11 w-full box-border rounded border border-outline-variant bg-surface-container-low px-2.5 text-body-sm"; const LABEL_CLASS = "min-h-[3rem] flex items-end text-[11px] font-bold text-secondary leading-snug"; /** Unit suffix for number fields when schema omits `unit` (cached / legacy schemas). */ function inferredNumberUnit(label: string): string | undefined { const L = label.toLowerCase(); if (/\b(kilograms?|kg)\b/i.test(label)) return "kg"; if (/\b(weight|mass|load capacity|weight capacity)\b/.test(L)) return "kg"; if (/\b(pounds?|lbs?)\b/i.test(L)) return "lb"; if ( /\b(millimeters?|millimetres?|mm)\b/i.test(L) || /\b(seat depth|seat width|seat height)\b/.test(L) ) return "mm"; if (/\b(centimeters?|centimetres?|cm)\b/i.test(L)) return "cm"; if (/\b(inches|inch)\b|\bin\.\b/i.test(L)) return "in"; if (/\b(meters?|metres?)\b/i.test(L)) return "m"; if (/\bpercent\b|%/i.test(L)) return "%"; return undefined; } function unitBadgeText(unit: string): string { const u = unit.trim(); if (!u) return u; // Short measurement tokens: show uppercase (kg → KG); leave longer symbols alone. return u.length <= 6 ? u.toUpperCase() : u; } type Props = { commodityCode: number; intro: string; catalogPath?: string; codeSummary?: string; onCommitPurchase: (payload: { rows: PrRow[]; snapshot: PurchaseFormSnapshot; }) => void; onAddNew: () => void; }; function FieldCell({ label, children, className = "col-span-12 md:col-span-6", }: { label: string; children: ReactNode; className?: string; }) { return (
{label} {children}
); } export function ProcurementFormBlock({ commodityCode, intro, catalogPath, codeSummary, onCommitPurchase, onAddNew, }: Props) { const [schemaLoading, setSchemaLoading] = useState(true); const [schemaError, setSchemaError] = useState(null); const [fields, setFields] = useState([]); const [intervalChoices, setIntervalChoices] = useState(FALLBACK_INTERVALS); const [deliveries, setDeliveries] = useState(4); const [interval, setInterval] = useState("Quarterly"); const [year, setYear] = useState(2026); const [otherSpec, setOtherSpec] = useState(""); const [dynamicValues, setDynamicValues] = useState>({}); const [confirmBusy, setConfirmBusy] = useState(false); const [buildError, setBuildError] = useState(null); const [justAdded, setJustAdded] = useState(false); /** Enables "Add new" only after this block has successfully committed lines to the PR table. */ const [hasCommittedLines, setHasCommittedLines] = useState(false); useEffect(() => { if (!intervalChoices.length) return; if (!intervalChoices.includes(interval)) { setInterval(intervalChoices[0] ?? "Quarterly"); } }, [intervalChoices, interval]); useEffect(() => { let cancelled = false; setSchemaLoading(true); setSchemaError(null); fetchFormSchema(commodityCode) .then((s) => { if (cancelled) return; if (s.error) setSchemaError(s.error); setFields(s.fields ?? []); if (s.interval_options?.length) setIntervalChoices(s.interval_options); const init: Record = {}; for (const f of s.fields ?? []) init[f.id] = ""; setDynamicValues(init); }) .catch((e) => setSchemaError(e instanceof Error ? e.message : String(e)) ) .finally(() => { if (!cancelled) setSchemaLoading(false); }); return () => { cancelled = true; }; }, [commodityCode]); const setDyn = (id: string, v: string) => { setDynamicValues((prev) => ({ ...prev, [id]: v })); }; const renderDynamicField = (f: DynamicField) => { const v = dynamicValues[f.id] ?? ""; if (f.type === "select" && f.options?.length) { return ( ); } if (f.type === "chips" && f.options?.length) { return (
{f.options.map((o) => ( ))}
); } if (f.type === "number") { const unit = (f.unit?.trim() || inferredNumberUnit(f.label)) ?? ""; return (
setDyn(f.id, e.target.value)} aria-describedby={unit ? `${f.id}-unit-hint` : undefined} /> {unit ? ( <> Enter value in {unitBadgeText(unit)} {unitBadgeText(unit)} ) : null}
); } if (f.type === "textarea") { return (