import { useEffect, useState } from 'react' import { Link, useNavigate, useParams } from 'react-router-dom' import { api, ApiError } from '../lib/api' import type { Form, QuestionInput, QuestionOption, QuestionType } from '../lib/types' const questionTypes: Array<{ value: QuestionType; label: string; hint: string }> = [ { value: 'text', label: 'Text', hint: 'Free-form text (single or multiline)' }, { value: 'boolean', label: 'True / false', hint: 'Yes/no style answer' }, { value: 'number', label: 'Number', hint: 'Numeric input with optional bounds' }, { value: 'select', label: 'Select', hint: 'Choose one from a list' }, { value: 'date', label: 'Date', hint: 'Calendar date in YYYY-MM-DD format' }, ] function emptyQuestion(): QuestionInput { return { key: `q${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`, label: '', type: 'text', required: false, multiline: false, } } function emptyOption(): QuestionOption { return { value: '', label: '' } } function slugKey(value: string): string { return ( value .toLowerCase() .replace(/[^a-z0-9]+/g, '_') .replace(/^_+|_+$/g, '') .slice(0, 64) || 'question' ) } export function FormBuilderPage() { const { formId } = useParams<{ formId?: string }>() const navigate = useNavigate() const isEdit = Boolean(formId) const [loading, setLoading] = useState(isEdit) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [introduction, setIntroduction] = useState('') const [requiresAccessCode, setRequiresAccessCode] = useState(false) const [signatureRequired, setSignatureRequired] = useState(false) const [externalReferenceRequired, setExternalReferenceRequired] = useState(false) const [externalReferenceLabel, setExternalReferenceLabel] = useState('') const [logoUrl, setLogoUrl] = useState('') const [pdfUrl, setPdfUrl] = useState('') const [companyName, setCompanyName] = useState('') const [contactEmail, setContactEmail] = useState('') const [contactPhone, setContactPhone] = useState('') const [contactUrl, setContactUrl] = useState('') const [questions, setQuestions] = useState([emptyQuestion()]) useEffect(() => { if (!formId) return let cancelled = false api .getForm(formId) .then((form: Form) => { if (cancelled) return setTitle(form.title) setDescription(form.description ?? '') setIntroduction(form.introduction ?? '') setRequiresAccessCode(form.requiresAccessCode) setSignatureRequired(Boolean(form.signatureRequired)) setExternalReferenceRequired(Boolean(form.externalReferenceRequired)) setExternalReferenceLabel(form.externalReferenceLabel ?? '') setLogoUrl(form.logoUrl ?? '') setPdfUrl(form.pdfUrl ?? '') setCompanyName(form.companyName ?? '') setContactEmail(form.contactEmail ?? '') setContactPhone(form.contactPhone ?? '') setContactUrl(form.contactUrl ?? '') setQuestions( form.questions.map((q) => ({ key: q.key, label: q.label, description: q.description, type: q.type, required: q.required, options: q.options ? q.options.map((o) => ({ ...o })) : undefined, minValue: q.minValue, maxValue: q.maxValue, placeholder: q.placeholder, multiline: q.multiline, })), ) }) .catch((err: unknown) => { if (!cancelled) setError(err instanceof ApiError ? err.message : 'Could not load the form') }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [formId]) function addQuestion() { setQuestions((current) => [...current, emptyQuestion()]) } function updateQuestion(index: number, patch: Partial) { setQuestions((current) => { const next = [...current] next[index] = { ...next[index], ...patch } return next }) } function suggestKey(index: number) { setQuestions((current) => { const next = [...current] next[index] = { ...next[index], key: slugKey(next[index].label) } return next }) } function removeQuestion(index: number) { setQuestions((current) => current.filter((_, i) => i !== index)) } function addOption(index: number) { setQuestions((current) => { const next = [...current] const options = next[index].options ? [...next[index].options] : [] options.push(emptyOption()) next[index] = { ...next[index], options } return next }) } function updateOption(qIndex: number, oIndex: number, patch: Partial) { setQuestions((current) => { const next = [...current] const options = next[qIndex].options ? [...next[qIndex].options] : [] options[oIndex] = { ...options[oIndex], ...patch } next[qIndex] = { ...next[qIndex], options } return next }) } function removeOption(qIndex: number, oIndex: number) { setQuestions((current) => { const next = [...current] const options = (next[qIndex].options ?? []).filter((_, i) => i !== oIndex) next[qIndex] = { ...next[qIndex], options } return next }) } async function save(event: React.FormEvent) { event.preventDefault() setError(null) if (title.trim() === '') { setError('Title is required.') return } if (questions.length === 0) { setError('Add at least one question.') return } const payload = { title: title.trim(), description: description.trim() || undefined, introduction: introduction.trim() || undefined, requiresAccessCode, signatureRequired, externalReferenceRequired, externalReferenceLabel: externalReferenceLabel.trim() || undefined, logoUrl: logoUrl.trim() || undefined, pdfUrl: pdfUrl.trim() || undefined, companyName: companyName.trim() || undefined, contactEmail: contactEmail.trim() || undefined, contactPhone: contactPhone.trim() || undefined, contactUrl: contactUrl.trim() || undefined, questions: questions.map((q) => { const out: QuestionInput = { key: q.key.trim(), label: q.label.trim(), type: q.type, required: q.required, description: q.description?.trim() || undefined, placeholder: q.placeholder?.trim() || undefined, } if (q.type === 'number') { out.minValue = q.minValue out.maxValue = q.maxValue } if (q.type === 'text') { out.multiline = q.multiline } if (q.type === 'select') { out.options = (q.options ?? []) .filter((o) => o.value.trim() && o.label.trim()) .map((o) => ({ value: o.value.trim(), label: o.label.trim() })) } return out }), } setSaving(true) try { if (isEdit && formId) { const updated = await api.updateForm(formId, payload) navigate(`/manage/forms/${updated.id}`) } else { const created = await api.createForm(payload) navigate(`/manage/forms/${created.id}`) } } catch (err) { setError(err instanceof ApiError ? err.message : 'Could not save the form') } finally { setSaving(false) } } if (loading) { return

Loading form…

} return (
Management / {isEdit ? 'Edit form' : 'Create form'}

{isEdit ? 'Edit form' : 'Create form'}

{isEdit ? 'Saving creates a new version; earlier submissions keep their original definition.' : 'Build a new versioned form.'}

Cancel
{error && (
{error}
)}

Form details

setTitle(e.target.value)} required />