import { ArrowLeft, ExternalLink, RefreshCw, Save, Send } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useParams } from "react-router"; import { Button, ConfirmDialog, DismissibleAlert, LoadingIndicator, PageScrollViewport, StatusBadge, ToggleSwitch, hasScope, useGuardedNavigate, type PlatformRouteContext } from "@govoplan/core-webui"; import { getFormDefinition, getFormInstance, getFormInstanceEvents, getFormInstanceHistory, listFormHandoffs, startFormHandoff, actOnFormHandoff, compensateFormHandoff, saveFormDraft, submitFormInstance, type FormDefinition, type FormFieldDefinition, type FormInstance, type FormInstanceEvent, type FormHandoff, type ValidationResult } from "../../api/formsRuntime"; export default function FormInstancePage({ settings, auth }: PlatformRouteContext) { const { instanceId = "" } = useParams(); const navigate = useGuardedNavigate(); const [instance, setInstance] = useState(null); const [definition, setDefinition] = useState(null); const [history, setHistory] = useState([]); const [events, setEvents] = useState([]); const [handoffs, setHandoffs] = useState([]); const [values, setValues] = useState>({}); const [changeReason, setChangeReason] = useState(""); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [handoffBusy, setHandoffBusy] = useState(false); const [handoffKind, setHandoffKind] = useState<"case" | "workflow">("case"); const [handoffBinding, setHandoffBinding] = useState(""); const [compensating, setCompensating] = useState(null); const load = useCallback(async (signal?: AbortSignal) => { setLoading(true); setError(""); try { const nextInstance = await getFormInstance(settings, instanceId, signal); const [nextDefinition, nextHistory, nextEvents, nextHandoffs] = await Promise.all([ getFormDefinition(settings, instanceId, signal), getFormInstanceHistory(settings, instanceId, signal), getFormInstanceEvents(settings, instanceId, signal), listFormHandoffs(settings, instanceId, signal) ]); setInstance(nextInstance); setDefinition(nextDefinition); setHistory(nextHistory.revisions); setEvents(nextEvents.events); setHandoffs(nextHandoffs.handoffs); setValues(nextInstance.values); setChangeReason(""); } finally { setLoading(false); } }, [instanceId, settings]); useEffect(() => { const controller = new AbortController(); load(controller.signal).catch((reason) => { if ((reason as Error).name !== "AbortError") { setError(reason instanceof Error ? reason.message : "The Form could not be loaded."); } }); return () => controller.abort(); }, [load]); useEffect(() => { if (!definition) return; if (!definition.handoff_kinds.includes(handoffKind)) { setHandoffKind(definition.handoff_kinds.includes("case") ? "case" : "workflow"); } }, [definition, handoffKind]); const editable = instance?.status === "started" || instance?.status === "draft"; const canSave = instance?.status === "draft" && definition?.allow_drafts; const changed = useMemo( () => Boolean(instance && JSON.stringify(values) !== JSON.stringify(instance.values)), [instance, values] ); const diagnostics = useMemo(() => { const grouped = new Map(); for (const item of instance?.validation_results ?? []) { const key = item.field ?? ""; grouped.set(key, [...(grouped.get(key) ?? []), item]); } return grouped; }, [instance]); const localized = useMemo( () => localizeDefinition(definition), [definition] ); const groups = useMemo( () => definition ? visibleGroups(definition, values) : [], [definition, values] ); const mayHandoff = Boolean(instance && ["submitted", "validated", "needs_review", "accepted"].includes(instance.status) && instance.service_ref); const canAdmin = hasScope(auth, "forms_runtime:workspace:admin"); async function save() { if (!instance || !canSave || !changed || !changeReason.trim()) return; setSaving(true); setError(""); try { await saveFormDraft(settings, instance, values, changeReason.trim()); await load(); } catch (reason) { setError(reason instanceof Error ? reason.message : "The draft could not be saved."); } finally { setSaving(false); } } async function submit() { if (!instance || !editable) return; setSaving(true); setError(""); try { await submitFormInstance(settings, instance, values); await load(); } catch (reason) { setError(reason instanceof Error ? reason.message : "The Form could not be submitted."); } finally { setSaving(false); } } async function startHandoff() { if (!instance || !mayHandoff) return; setHandoffBusy(true); setError(""); try { await startFormHandoff(settings, instance, handoffKind, handoffBinding); await load(); } catch (reason) { setError(reason instanceof Error ? reason.message : "The handoff could not be started."); } finally { setHandoffBusy(false); } } async function handoffAction(item: FormHandoff, action: "retry" | "reconcile") { setHandoffBusy(true); setError(""); try { await actOnFormHandoff(settings, item.instance_id, item.effect_id, action); await load(); } catch (reason) { setError(reason instanceof Error ? reason.message : "The handoff could not be updated."); } finally { setHandoffBusy(false); } } async function compensate() { if (!compensating) return; setHandoffBusy(true); setError(""); try { await compensateFormHandoff(settings, compensating.instance_id, compensating.effect_id, "Operator confirmed that no target effect exists."); setCompensating(null); await load(); } catch (reason) { setError(reason instanceof Error ? reason.message : "The handoff could not be compensated."); } finally { setHandoffBusy(false); } } return (
{definition && {localized.title}} {instance && }
{error && {error} } {loading && } {!loading && instance && definition &&

{localized.title}

{localized.description &&

{localized.description}

}
{groups.map((group) =>
{(groups.length > 1 || definition.pages?.length) &&

{localized.sectionTitles[group.sectionKey] ?? group.sectionTitle}

{group.description &&

{group.description}

}
}
{group.fields.map((field) => setValues((current) => { const next = { ...current }; if (value === undefined || value === "") delete next[field.key]; else next[field.key] = value; return next; })} /> )}
)} {(instance.attachment_refs.length > 0 || instance.signature_refs.length > 0) &&
{instance.attachment_refs.length} attachments {instance.signature_refs.length} signatures
} {editable &&
{canSave && } {canSave && }
} {!editable && instance.receipt_id &&
Submission receipt {instance.receipt_id}
} {!editable && definition.handoff_kinds.length > 0 &&

Case and workflow handoffs

{mayHandoff &&
setHandoffBinding(event.target.value)} placeholder="Target binding (optional)" aria-label="Exact target binding" />
}
{handoffs.length === 0 &&

No handoff has been requested.

}
{handoffs.map((item) =>
{humanize(item.binding_kind)}{item.binding_reference} {item.last_error && {item.last_error}} {item.href && } {item.state === "rejected" && } {item.state === "outcome_unknown" && } {canAdmin && (item.state === "rejected" || item.state === "outcome_unknown") && }
)}
}
}
setCompensating(null)} onConfirm={() => void compensate()} />
); } function FormField({ field, value, disabled, diagnostics, optionLabels, onChange }: { field: FormFieldDefinition; value: unknown; disabled: boolean; diagnostics: ValidationResult[]; optionLabels: Record; onChange: (value: unknown) => void; }) { const describedBy = diagnostics.length > 0 ? `form-field-${field.key}-messages` : undefined; if (field.value_type === "boolean") { return (
); } return ( ); } function renderInput( field: FormFieldDefinition, value: unknown, disabled: boolean, describedBy: string | undefined, onChange: (value: unknown) => void, optionLabels: Record ) { const common = { disabled, required: field.required, "aria-describedby": describedBy }; if (field.value_type === "multiline_text" || field.value_type === "object" || field.value_type === "list") { return (