"use client"; import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import type { FormConfig, FormDataPayload, SubmitPayload } from "@/types/form"; import { evaluate } from "@/lib/conditional"; import { StageSection } from "./StageSection"; interface EngagementFormProps { config: FormConfig; cid: string; cs: string; } type LoadState = | { kind: "loading" } | { kind: "error"; message: string } | { kind: "ready"; data: FormDataPayload }; /** * Top-level form orchestrator. Responsible for: * - fetching prefill data + the org's current stage from /api/data * - hydrating react-hook-form with prefill values * - re-evaluating section visibility against live form values on every change * - submitting to /api/submit and surfacing success / error feedback */ export function EngagementForm({ config, cid, cs }: EngagementFormProps) { const [load, setLoad] = useState({ kind: "loading" }); const [submitState, setSubmitState] = useState< | { kind: "idle" } | { kind: "submitting" } | { kind: "success" } | { kind: "error"; message: string } >({ kind: "idle" }); const { register, handleSubmit, reset, watch, formState: { errors }, } = useForm({ mode: "onBlur" }); // Subscribe to all form values so section visibility re-evaluates live. const formValues = watch(); useEffect(() => { let cancelled = false; async function go() { try { const url = new URL("/api/data", window.location.origin); url.searchParams.set("cid", cid); url.searchParams.set("cs", cs); const res = await fetch(url.toString(), { cache: "no-store" }); if (!res.ok) { const text = await res.text(); if (!cancelled) { setLoad({ kind: "error", message: `Could not load form (${res.status}). ${text}` }); } return; } const data: FormDataPayload = await res.json(); if (cancelled) return; // Merge stage value + per-field prefill into a single defaults map. const defaults = { [config.stageField]: data.currentStage, ...data.prefill, }; reset(defaults); setLoad({ kind: "ready", data }); } catch (e) { if (!cancelled) { setLoad({ kind: "error", message: e instanceof Error ? e.message : "Unexpected error loading form.", }); } } } void go(); return () => { cancelled = true; }; }, [cid, cs, reset, config.stageField]); const sectionsToRender = useMemo(() => { return config.sections.map((s) => ({ section: s, sectionVisible: evaluate(s.visibleWhen, formValues), })); }, [config.sections, formValues]); // The "current" rank: highest section rank whose visibility rule matches // the current stage value. If nothing matches, fall back to 0 (Inquiry). const currentRank = useMemo(() => { const visibleRanks = sectionsToRender .filter((s) => s.sectionVisible) .map((s) => s.section.rank); return visibleRanks.length > 0 ? Math.max(...visibleRanks) : 0; }, [sectionsToRender]); if (load.kind === "loading") { return ; } if (load.kind === "error") { return ; } const onSubmit = async (values: Record) => { setSubmitState({ kind: "submitting" }); try { // Strip values for fields whose section or field rule isn't currently visible — // those aren't part of the user's intent in this submission. const visibleFieldNames = new Set(); for (const s of config.sections) { if (!evaluate(s.visibleWhen, values)) continue; for (const f of s.fields) { if (evaluate(f.visibleWhen, values)) { visibleFieldNames.add(f.name); } } } const filtered: Record = {}; for (const [k, v] of Object.entries(values)) { if (visibleFieldNames.has(k)) filtered[k] = v; } const payload: SubmitPayload = { cid, cs, values: filtered }; const res = await fetch("/api/submit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) { const text = await res.text(); setSubmitState({ kind: "error", message: `Submit failed (${res.status}). ${text}` }); return; } setSubmitState({ kind: "success" }); } catch (e) { setSubmitState({ kind: "error", message: e instanceof Error ? e.message : "Unexpected submit error.", }); } }; return (
{sectionsToRender.map(({ section, sectionVisible }) => { if (!sectionVisible) return null; return ( ); })}
); } function SubmissionContextHeader({ orgName, currentStage, }: { orgName: string; currentStage: string; }) { return (

Check-in for

{orgName}

Framework Stage:{" "} {currentStage || "—"}

); } function SubmitFeedback({ state, }: { state: | { kind: "idle" } | { kind: "submitting" } | { kind: "success" } | { kind: "error"; message: string }; }) { if (state.kind === "idle") { return

Your changes will be saved as a new check-in record.

; } if (state.kind === "submitting") { return (

Saving your check-in…

); } if (state.kind === "success") { return (

✓ Check-in saved. Thank you.

); } return (

✕ {state.message}

); } function LoadingState() { return (

Loading your check-in form…

); } function ErrorState({ message }: { message: string }) { return (

We couldn't load your check-in form.

{message}

If this problem persists, please contact your engagement coordinator and ask for a fresh link.

); }