/** * CreateFormPanel — shared by Feedback, Registration, Other, and Web check-in forms. * Pass mode="feedback" | "registration" | "other" | "web-check-in", eventId, optional formId, onSuccess. */ import React, { useState, useEffect, useCallback } from "react"; import { Plus, Trash2, GripVertical } from "lucide-react"; import { PanelShell, AccordionSection, useAccordion, Label, Field, Grid, RadioGroup, HelperText, IS, SS, focusIn, focusOut } from "./PanelShell"; import { useAuth } from "../../contexts/AuthContext"; import { eventsApi, formsApi, participantListsApi, portfolioListOrganizationId } from "../../../services/api"; import type { Form, FormField, ParticipantListRecord } from "../../../services/api"; import { assignUniqueFieldNames } from "../../utils/formFieldNames"; import { DropdownOtherConfigEditor } from "../forms/DropdownOtherConfigEditor"; type FormMode = "feedback" | "registration" | "other" | "web-check-in"; function eventIdFromFormRef(ev: Form["event"] | undefined): string { if (!ev) return ""; if (typeof ev === "string") return ev; return String((ev as { _id?: string })._id ?? ""); } const FEEDBACK_Q_TYPES = ["Star Rating", "NPS (0–10)", "Multiple Choice", "Radio", "Short Text", "Long Text", "Checkboxes", "Yes / No", "Section"]; const REG_FIELD_TYPES = ["Text", "Email", "Phone", "Dropdown", "Radio", "Checkboxes", "Yes / No", "Date", "Time", "File Upload", "Country State", "Section"]; /** Other forms: union of feedback + registration field labels (order preserved, no duplicates). */ const OTHER_Q_TYPES = [...new Set([...FEEDBACK_Q_TYPES, ...REG_FIELD_TYPES])]; const SECTION_FIELD_TYPE = "Section"; function isSectionFieldType(type: string): boolean { return type === SECTION_FIELD_TYPE; } function patchForFieldTypeChange(next: string, q: Question): Partial { const nextPatch: Partial = { type: next }; if (isSectionFieldType(next)) { nextPatch.required = "No"; nextPatch.options = []; nextPatch.mapsTo = ""; nextPatch.customMapsKey = ""; return nextPatch; } if (OPTION_FIELD_TYPES.has(next)) { nextPatch.options = q.options?.length ? q.options : defaultChoiceOptions(); } else { nextPatch.options = []; } if (next !== "Dropdown") { nextPatch.allowOther = false; nextPatch.otherLabel = undefined; } return nextPatch; } /** Types that need a configurable list of choices (dropdown / MC / radio / multi-checkbox). */ const OPTION_FIELD_TYPES = new Set(["Multiple Choice", "Dropdown", "Radio", "Checkboxes"]); const FORM_Q_DRAG = "application/x-eventbuzz-form-q"; function reorderList(items: T[], from: number, to: number): T[] { if (from === to || from < 0 || to < 0 || from >= items.length || to >= items.length) return items; const next = [...items]; const [moved] = next.splice(from, 1); next.splice(to, 0, moved); return next; } function DragGrip({ index, disabled }: { index: number; disabled?: boolean }) { return (
{ if (disabled) return; e.stopPropagation(); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData(FORM_Q_DRAG, String(index)); e.dataTransfer.setData("text/plain", String(index)); }} style={{ color: "var(--muted)", paddingTop: "6px", flexShrink: 0, cursor: disabled ? "default" : "grab", opacity: disabled ? 0.35 : 1, touchAction: "none", }} title={disabled ? undefined : "Drag to reorder"} aria-label={disabled ? undefined : "Drag to reorder"} >
); } function DraggableRowShell({ index, onReorder, disabled, children, style, }: { index: number; onReorder: (from: number, to: number) => void; disabled?: boolean; children: React.ReactNode; style?: React.CSSProperties; }) { const [dragOver, setDragOver] = useState(false); return (
{ if (disabled) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { if (disabled) return; e.preventDefault(); setDragOver(false); const raw = e.dataTransfer.getData(FORM_Q_DRAG) || e.dataTransfer.getData("text/plain"); const from = parseInt(raw, 10); if (Number.isNaN(from) || from === index) return; onReorder(from, index); }} style={{ ...style, outline: dragOver && !disabled ? "2px solid var(--primary)" : undefined, outlineOffset: dragOver && !disabled ? 2 : undefined, }} > {children}
); } const REG_TYPE_TO_API: Record = { Text: "text", Email: "email", Phone: "phone", Dropdown: "select", Radio: "radio", Checkboxes: "checkboxes", "Yes / No": "checkbox", Date: "date", Time: "time", "File Upload": "file", "Country State": "country_state", }; const REG_EXTRA_MAP_OPTIONS: { value: string; label: string }[] = [ { value: "", label: "Don't map (submission only)" }, { value: "phone", label: "Phone" }, { value: "organization", label: "Organization" }, { value: "designation", label: "Designation" }, { value: "notes", label: "Notes" }, { value: "displayName", label: "Display name" }, { value: "custom", label: "Custom field (key below)" }, ]; interface Question { id: string; label: string; /** Unique submission key (derived from label; not shown to respondents). */ name?: string; type: string; required: "Yes" | "No"; /** Choice labels for Multiple Choice, Dropdown, Radio, Checkboxes. */ options?: string[]; /** Dropdown only: include an Other option with a custom label. */ allowOther?: boolean; otherLabel?: string; mapsTo?: string; customMapsKey?: string; fixed?: boolean; /** Optional note shown below the field on the public form. */ remarks?: string; } function normalizedOptions(q: Question): string[] { return (q.options ?? []).map((s) => String(s).trim()).filter(Boolean); } function fieldChoicesAreValid(q: Question): boolean { if (!OPTION_FIELD_TYPES.has(q.type)) return true; return normalizedOptions(q).length >= 2; } function defaultChoiceOptions(): string[] { return ["Option 1", "Option 2"]; } function FieldRemarksEditor({ value, onChange }: { value?: string; onChange: (v: string) => void }) { return (
Remarks onChange(e.target.value)} onFocus={focusIn} onBlur={focusOut} />
); } function FieldOptionsEditor({ options, onChange }: { options: string[]; onChange: (opts: string[]) => void }) { const rows = options.length ? options : ["", ""]; const setAt = (i: number, v: string) => { const next = [...rows]; next[i] = v; onChange(next); }; const addRow = () => onChange([...rows, ""]); const removeRow = (i: number) => { if (rows.length <= 1) return; onChange(rows.filter((_, j) => j !== i)); }; return (
Choices {rows.map((opt, i) => (
setAt(i, e.target.value)} onFocus={focusIn} onBlur={focusOut} />
))}
); } function initialRegQuestions(): Question[] { return [ { id: "reg-core-fn", label: "First name", type: "Text", required: "Yes", mapsTo: "firstName", fixed: true }, { id: "reg-core-ln", label: "Last name", type: "Text", required: "Yes", mapsTo: "lastName", fixed: true }, { id: "reg-core-em", label: "Email", type: "Email", required: "Yes", mapsTo: "email", fixed: true }, ]; } function initialWebCheckInQuestions(): Question[] { return [ { id: "wc-core-fn", label: "First name", type: "Text", required: "Yes", mapsTo: "firstName", fixed: true }, { id: "wc-core-ln", label: "Last name", type: "Text", required: "Yes", mapsTo: "lastName", fixed: true }, { id: "wc-core-em", label: "Email", type: "Email", required: "Yes", mapsTo: "email", fixed: true }, { id: "wc-doc", label: "Upload document", type: "File Upload", required: "Yes", fixed: true }, ]; } function QuestionRow({ q, types, onPatch, onRemove, idx, canRemove, dragIndex, onReorder, onLabelCommit }: { q: Question; types: string[]; onPatch: (patch: Partial) => void; onRemove: () => void; idx: number; canRemove: boolean; dragIndex: number; onReorder: (from: number, to: number) => void; onLabelCommit: () => void; }) { const showChoices = OPTION_FIELD_TYPES.has(q.type); const isSection = isSectionFieldType(q.type); const rowStyle = { display: "flex" as const, gap: "8px", alignItems: "flex-start" as const, padding: "10px 12px", background: isSection ? "var(--accent)" : "var(--secondary)", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", }; return (
{isSection ? ( Section divider · not submitted ) : null}
onPatch({ label: e.target.value })} onFocus={focusIn} onBlur={() => { focusOut(); onLabelCommit(); }} />
{canRemove && ( )}
{showChoices && ( onPatch({ options: opts })} /> )} {q.type === "Dropdown" && ( onPatch(patch)} /> )} {!isSection ? (
) : null} {!isSection ? ( onPatch({ remarks })} /> ) : null}
); } function RegCoreFieldRow({ q, idx, onPatch, onLabelCommit }: { q: Question; idx: number; onPatch: (patch: Partial) => void; onLabelCommit: () => void }) { return (
Core · maps to {q.mapsTo}
onPatch({ label: e.target.value })} onFocus={focusIn} onBlur={() => { focusOut(); onLabelCommit(); }} /> onPatch({ remarks })} />
); } function RegExtraFieldRow({ q, types, onPatch, onRemove, idx, dragIndex, onReorder, onLabelCommit }: { q: Question; types: string[]; onPatch: (patch: Partial) => void; onRemove: () => void; idx: number; dragIndex: number; onReorder: (from: number, to: number) => void; onLabelCommit: () => void; }) { const showChoices = OPTION_FIELD_TYPES.has(q.type); const isSection = isSectionFieldType(q.type); const rowStyle = { display: "flex" as const, flexDirection: "column" as const, gap: "8px", padding: "10px 12px", background: isSection ? "var(--accent)" : "var(--secondary)", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", }; return (
{isSection ? ( Section divider · not submitted ) : null} {showChoices && ( onPatch({ options: opts })} /> )} {q.type === "Dropdown" && ( onPatch(patch)} /> )} {!isSection ? ( <> {q.mapsTo === "custom" && ( )} ) : null} {!isSection ? ( onPatch({ remarks })} /> ) : null}
); } interface FForm { name: string; description: string; questions: Question[]; showThankYou: "Yes" | "No"; thankYouMsg: string; collectEmail: "Yes" | "No"; anonymous: "Yes" | "No"; publish: "Draft" | "Published"; paymentEnabled: "Yes" | "No"; amount: string; confirmationEmail: "Yes" | "No"; onSubmitTags: string; onSubmitListId: string; } const newQ = (types: string[]): Question => { const first = types[0]; return { id: Math.random().toString(36).slice(2), label: "", type: first, required: "No", options: OPTION_FIELD_TYPES.has(first) ? defaultChoiceOptions() : [], mapsTo: "", customMapsKey: "", fixed: false, }; }; const initialForm = (qTypes: string[], isFeedback: boolean): FForm => ({ name: "", description: "", questions: [newQ(qTypes)], showThankYou: "Yes", thankYouMsg: "Thank you for your feedback!", collectEmail: "No", anonymous: isFeedback ? "Yes" : "No", publish: "Draft", paymentEnabled: "No", amount: "", confirmationEmail: "Yes", onSubmitTags: "", onSubmitListId: "", }); function typeFromApi( t: string, opts: string[] | undefined, qTypes: string[], mode: FormMode, uiType?: string, ): string { const savedUi = uiType?.trim(); if (savedUi && qTypes.includes(savedUi)) return savedUi; if (t === "stars") return "Star Rating"; if (t === "select" && opts?.length === 5 && opts.every((v, i) => v === String(i + 1))) return "Star Rating"; if (t === "select" && opts?.length === 11) return "NPS (0–10)"; if (t === "radio") return "Radio"; if (t === "checkboxes") return "Checkboxes"; if (t === "select") { if (mode === "registration") return "Dropdown"; if (mode === "feedback") return "Multiple Choice"; return qTypes.includes("Dropdown") ? "Dropdown" : "Multiple Choice"; } if (t === "checkbox") return qTypes.includes("Yes / No") ? "Yes / No" : "Checkboxes"; if (t === "textarea") return mode === "registration" ? "Text" : "Long Text"; if (t === "text") { if (mode === "registration" || mode === "other") return qTypes.includes("Text") ? "Text" : "Short Text"; return "Short Text"; } if (t === "email") return "Email"; if (t === "phone") return "Phone"; if (t === "date") return "Date"; if (t === "time") return "Time"; if (t === "file") return "File Upload"; if (t === "section") return "Section"; if (t === "country_state") return "Country State"; return qTypes[0] ?? "Short Text"; } function optionsFromApi(f: FormField): string[] { if (!Array.isArray(f.options)) return []; return f.options.map((s) => String(s)); } function withUiType(field: FormField, uiType: string | undefined, persistUiType: boolean): FormField { if (!persistUiType || !uiType?.trim()) return field; return { ...field, uiType: uiType.trim() }; } function withFieldRemarks(field: FormField, remarks?: string): FormField { const trimmed = remarks?.trim(); return trimmed ? { ...field, remarks: trimmed } : field; } function withSelectOther(field: FormField, q: Question): FormField { if (q.type !== "Dropdown") return field; if (!q.allowOther) return field; const otherLabel = q.otherLabel?.trim() || "Other"; return { ...field, allowOther: true, otherLabel }; } function toApiField(q: Question, mode: FormMode): FormField { const label = q.label?.trim() || "Untitled"; const name = q.name?.trim() || label; const required = q.required === "Yes"; const choiceOpts = normalizedOptions(q); const persistUiType = mode === "other"; if (isSectionFieldType(q.type)) { return withUiType({ label, type: "section", required: false }, q.type, persistUiType); } const withName = (field: FormField) => withFieldRemarks({ ...field, name }, q.remarks); const isFeedbackOrOther = mode === "feedback" || mode === "other"; if (isFeedbackOrOther) { if (q.type === "Star Rating") { return withName(withUiType({ label, type: "stars", required, options: ["1", "2", "3", "4", "5"] }, q.type, persistUiType)); } if (q.type === "NPS (0–10)") { return withName(withUiType( { label, type: "select", required, options: Array.from({ length: 11 }, (_, i) => String(i)) }, q.type, persistUiType, )); } if (q.type === "Multiple Choice") { return withName(withUiType({ label, type: "select", required, options: choiceOpts }, q.type, persistUiType)); } if (q.type === "Dropdown") { return withName( withSelectOther( withUiType({ label, type: "select", required, options: choiceOpts }, q.type, persistUiType), q, ), ); } if (q.type === "Radio") { return withName(withUiType({ label, type: "radio", required, options: choiceOpts }, q.type, persistUiType)); } if (q.type === "Checkboxes") { return withName(withUiType({ label, type: "checkboxes", required, options: choiceOpts }, q.type, persistUiType)); } if (q.type === "Short Text") { return withName(withUiType({ label, type: "text", required }, q.type, persistUiType)); } if (q.type === "Long Text") { return withName(withUiType({ label, type: "textarea", required }, q.type, persistUiType)); } if (q.type === "Yes / No") { return withName(withUiType({ label, type: "checkbox", required }, q.type, persistUiType)); } if (q.type === "Country State") { return withName(withUiType({ label, type: "country_state", required }, q.type, persistUiType)); } const regType = REG_TYPE_TO_API[q.type]; if (regType) { if (regType === "select" || regType === "radio" || regType === "checkboxes") { const base = withUiType({ label, type: regType, required, options: choiceOpts }, q.type, persistUiType); return withName(q.type === "Dropdown" ? withSelectOther(base, q) : base); } return withName(withUiType({ label, type: regType, required }, q.type, persistUiType)); } return withName(withUiType({ label, type: "text", required }, q.type, persistUiType)); } const type = (REG_TYPE_TO_API[q.type] ?? "text") as FormField["type"]; if (type === "select" || type === "radio" || type === "checkboxes") { const base = { label, name, type, required, options: choiceOpts }; return withFieldRemarks(q.type === "Dropdown" ? withSelectOther(base, q) : base, q.remarks); } return withFieldRemarks({ label, name, type, required }, q.remarks); } function isCoreRegFieldMapsTo(mapsTo: string | undefined): boolean { return mapsTo === "firstName" || mapsTo === "lastName" || mapsTo === "email"; } function toApiFieldReg(q: Question): FormField { const base = toApiField(q, "registration"); const required = q.fixed === true || isCoreRegFieldMapsTo(q.mapsTo) ? true : base.required; return { ...base, required, mapsTo: q.mapsTo || undefined, customMapsKey: q.mapsTo === "custom" && q.customMapsKey?.trim() ? q.customMapsKey.trim() : undefined, isFixed: q.fixed === true, }; } function requiredFromApiRegField(f: FormField): "Yes" | "No" { if (f.isFixed === true || isCoreRegFieldMapsTo(f.mapsTo)) return "Yes"; return f.required ? "Yes" : "No"; } function fromApiFields(fields: FormField[] | undefined, qTypes: string[], mode: FormMode): Question[] { if (!fields?.length) return [newQ(qTypes)]; const questions = fields.map((f, i) => { const typ = typeFromApi(f.type ?? "text", f.options, qTypes, mode, f.uiType); return { id: Math.random().toString(36).slice(2) + String(i), label: f.label ?? "", name: f.name ?? f.label ?? "", type: typ, required: f.required ? "Yes" : "No", options: OPTION_FIELD_TYPES.has(typ) ? optionsFromApi(f) : [], allowOther: typ === "Dropdown" ? !!f.allowOther : undefined, otherLabel: typ === "Dropdown" ? (f.otherLabel?.trim() || "Other") : undefined, mapsTo: f.mapsTo, customMapsKey: f.customMapsKey, fixed: f.isFixed, remarks: f.remarks ?? "", }; }); return assignUniqueFieldNames(questions); } function fromApiFieldSingle(f: FormField, i: number, qTypes: string[], mode: FormMode): Question { const typ = typeFromApi(f.type ?? "text", f.options, qTypes, mode, f.uiType); return { id: `imp-${i}-${Math.random().toString(36).slice(2)}`, label: f.label ?? "", name: f.name ?? f.label ?? "", type: typ, required: requiredFromApiRegField(f), options: OPTION_FIELD_TYPES.has(typ) ? optionsFromApi(f) : [], allowOther: typ === "Dropdown" ? !!f.allowOther : undefined, otherLabel: typ === "Dropdown" ? (f.otherLabel?.trim() || "Other") : undefined, mapsTo: f.mapsTo, customMapsKey: f.customMapsKey, fixed: f.isFixed === true, remarks: f.remarks ?? "", }; } function mergeRegFieldsFromApi(fields: FormField[] | undefined, qTypes: string[]): Question[] { const base = initialRegQuestions(); if (!fields?.length) return base; const converted = fields.map((f, i) => fromApiFieldSingle(f, i, qTypes, "registration")); const merged = !converted.some((q) => q.mapsTo === "firstName") ? [...base, ...converted] : converted; return assignUniqueFieldNames(merged); } export function CreateFormPanel({ eventId, formId, mode, defaultFormName, onClose, onSuccess, }: { eventId?: string; formId?: string; mode: FormMode; /** Prefill name when creating a new form (no formId). */ defaultFormName?: string; onClose: () => void; onSuccess: () => void; }) { const { user } = useAuth(); const listOrgId = portfolioListOrganizationId(user); const isFeedback = mode === "feedback"; const isReg = mode === "registration"; const isOther = mode === "other"; const isWebCheckIn = mode === "web-check-in"; const TOTAL = isReg ? 5 : 3; const qTypes = isReg || isWebCheckIn ? OTHER_Q_TYPES : isOther ? OTHER_Q_TYPES : FEEDBACK_Q_TYPES; const isFeedbackOrOther = isFeedback || isOther || isWebCheckIn; const acc = useAccordion(TOTAL); const [form, setForm] = useState(() => { const base = initialForm(qTypes, isFeedbackOrOther); if (isReg) return { ...base, questions: initialRegQuestions() }; if (isWebCheckIn) return { ...base, questions: initialWebCheckInQuestions() }; return base; }); const [fixedLists, setFixedLists] = useState([]); const [sectionErrors, setSectionErrors] = useState>({}); const [submitting, setSubmitting] = useState(false); const [loading, setLoading] = useState(!!formId); const [orgEvents, setOrgEvents] = useState<{ _id: string; name: string }[]>([]); const [selectedEventId, setSelectedEventId] = useState(""); useEffect(() => { if (!listOrgId) { setOrgEvents([]); return; } let cancelled = false; void eventsApi .list({ organization: listOrgId, recordKind: "event", limit: 500 }) .then((res) => { if (cancelled) return; const evs = res.data?.events ?? []; setOrgEvents(evs.map((e) => ({ _id: e._id, name: e.name?.trim() ? e.name : "Untitled" }))); }) .catch(() => { if (!cancelled) setOrgEvents([]); }); return () => { cancelled = true; }; }, [listOrgId]); useEffect(() => { if (!formId && eventId) setSelectedEventId(eventId); }, [formId, eventId]); useEffect(() => { if (formId || !defaultFormName?.trim()) return; setForm((prev) => (prev.name.trim() ? prev : { ...prev, name: defaultFormName.trim() })); }, [formId, defaultFormName]); useEffect(() => { if (!isReg) return; if (!listOrgId) { setFixedLists([]); return; } participantListsApi.list({ organization: listOrgId }).then((r) => { if (r.data?.lists) setFixedLists(r.data.lists.filter((L) => L.type === "fixed")); }).catch(() => setFixedLists([])); }, [isReg, listOrgId]); useEffect(() => { if (!formId) return; let cancelled = false; (async () => { try { const data = await formsApi.get(formId); if (cancelled) return; const loadMode: FormMode = isReg ? "registration" : isOther ? "other" : "feedback"; const questions = data.type === "registration" ? mergeRegFieldsFromApi(data.fields, qTypes) : fromApiFields(data.fields, qTypes, loadMode); const s = data.settings ?? {}; const tags = Array.isArray(s.onSubmitAddTags) ? s.onSubmitAddTags.join(", ") : ""; setForm({ name: data.name ?? "", description: data.description ?? "", questions: questions.length ? questions : (data.type === "registration" ? initialRegQuestions() : [newQ(qTypes)]), showThankYou: s.showThankYou === false ? "No" : "Yes", thankYouMsg: s.thankYouMessage?.trim() ? s.thankYouMessage : "Thank you for your feedback!", collectEmail: s.collectEmail ? "Yes" : "No", anonymous: s.allowAnonymous ? "Yes" : (isFeedbackOrOther ? "Yes" : "No"), publish: s.isActive !== false ? "Published" : "Draft", paymentEnabled: s.paymentEnabled ? "Yes" : "No", amount: s.paymentAmount != null && s.paymentAmount !== "" ? String(s.paymentAmount) : "", confirmationEmail: s.sendConfirmationEmail === false ? "No" : "Yes", onSubmitTags: tags, onSubmitListId: s.onSubmitParticipantListId ?? "", }); setSelectedEventId(eventIdFromFormRef(data.event)); } catch { if (!cancelled) setLoading(false); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [formId, qTypes.join(","), isFeedbackOrOther]); const set = (k: K, v: FForm[K]) => setForm(p => ({ ...p, [k]: v })); const addQ = () => setForm(p => ({ ...p, questions: [...p.questions, newQ(qTypes)] })); const removeQ = (id: string) => setForm(p => ({ ...p, questions: p.questions.filter(q => !q.fixed && q.id !== id) })); const updateQuestion = (id: string, patch: Partial) => setForm(p => ({ ...p, questions: p.questions.map(q => (q.id === id ? { ...q, ...patch } : q)) })); const commitQuestionLabel = useCallback(() => { setForm((p) => ({ ...p, questions: assignUniqueFieldNames(p.questions) })); }, []); const reorderQuestions = useCallback((from: number, to: number) => { setForm((p) => ({ ...p, questions: reorderList(p.questions, from, to) })); }, []); const reorderRegExtras = useCallback((from: number, to: number) => { setForm((p) => { const core = p.questions.filter((q) => q.fixed); const extras = p.questions.filter((q) => !q.fixed); return { ...p, questions: [...core, ...reorderList(extras, from, to)] }; }); }, []); const validateSection = useCallback((f: FForm, n: number): boolean => { if (n === 1) return f.name.trim().length > 0 && selectedEventId.trim().length > 0; if (n === 2) { if (isReg || isWebCheckIn) { const coreOk = f.questions.filter((q) => q.fixed).every((q) => q.label.trim().length > 0); const extras = f.questions.filter((q) => !q.fixed); const extrasOk = extras.every((q) => { if (!q.label.trim()) return false; if (isSectionFieldType(q.type)) return true; if (q.mapsTo === "custom" && !q.customMapsKey?.trim()) return false; if (!fieldChoicesAreValid(q)) return false; return true; }); return coreOk && extrasOk; } const anyLabel = f.questions.some(q => q.label.trim().length > 0); const choicesOk = f.questions.every(q => isSectionFieldType(q.type) || !q.label.trim() || fieldChoicesAreValid(q)); return anyLabel && choicesOk; } if (n === 3 && isReg) return true; if (n === 4 && isReg) return true; if (n === (isReg ? 5 : 3)) return true; return true; }, [isReg, isWebCheckIn, selectedEventId]); const tryAdvance = useCallback((n: number) => { const valid = validateSection(form, n); setSectionErrors(prev => ({ ...prev, [n]: valid ? "" : "Please complete required fields before continuing." })); if (valid) acc.advance(n); }, [form, validateSection, acc]); const buildPayload = useCallback(() => { const apiType = isReg ? "registration" : isOther ? "other" : isWebCheckIn ? "web-check-in" : "feedback"; const saveMode: FormMode = apiType; const uniqueQuestions = assignUniqueFieldNames(form.questions); const fields = isReg ? uniqueQuestions.filter(q => q.label.trim()).map(q => toApiFieldReg(q)) : uniqueQuestions.filter(q => q.label.trim()).map(q => toApiField(q, saveMode)); const isActive = form.publish === "Published"; const tagArr = form.onSubmitTags.split(/[,;\n]+/).map(t => t.trim()).filter(Boolean); return { name: form.name.trim(), description: form.description.trim(), type: apiType, fields, settings: { isActive, thankYouMessage: form.thankYouMsg.trim(), showThankYou: form.showThankYou === "Yes", collectEmail: form.collectEmail === "Yes", allowAnonymous: form.anonymous === "Yes", paymentEnabled: form.paymentEnabled === "Yes", paymentAmount: form.paymentEnabled === "Yes" && form.amount.trim() ? form.amount.trim() : "", sendConfirmationEmail: form.confirmationEmail === "Yes", ...(isReg ? { onSubmitAddTags: tagArr, onSubmitParticipantListId: form.onSubmitListId.trim() || null, } : {}), }, }; }, [form, isReg, isOther, isWebCheckIn, isFeedbackOrOther]); const handleSave = useCallback(async () => { if (!form.name.trim() || !selectedEventId.trim()) { if (!selectedEventId.trim()) setSectionErrors((prev) => ({ ...prev, 1: "Select an event for this form." })); return; } setSubmitting(true); setSectionErrors({}); try { const payload = buildPayload(); if (formId) { await formsApi.update(formId, { ...payload, event: selectedEventId }); } else { await formsApi.create({ ...payload, event: selectedEventId }); } onSuccess(); onClose(); } catch { setSectionErrors(prev => ({ ...prev, 1: "Failed to save. Please try again." })); } finally { setSubmitting(false); } }, [form.name, formId, selectedEventId, buildPayload, onSuccess, onClose]); const onSaveLast = useCallback(() => { const lastN = isReg ? 5 : 3; if (!validateSection(form, lastN)) { setSectionErrors(prev => ({ ...prev, [lastN]: "Please complete required fields." })); return; } setSectionErrors(prev => ({ ...prev, [lastN]: "" })); handleSave(); }, [form, isReg, validateSection, handleSave]); const breadcrumb = isFeedback ? "Forms & Workflow · Feedback" : isReg ? "Forms & Workflow · Registration" : isWebCheckIn ? "Forms & Workflow · Web check-in" : "Forms & Workflow · Other Forms"; const title = formId ? (isFeedback ? "Edit Feedback Form" : isReg ? "Edit Registration Form" : isWebCheckIn ? "Edit Web check-in Form" : "Edit Form") : (isFeedback ? "Create Feedback Form" : isReg ? "Create Registration Form" : isWebCheckIn ? "Create Web check-in Form" : "Create Other Form"); const panelDescription = isFeedback ? "Design a feedback form to collect responses from your attendees." : isReg ? "Build a registration form; each submission creates or updates a participant." : isWebCheckIn ? "Create a web check-in form with the required personal details and document upload fields." : "Build custom forms (other / general-purpose) for your event."; if (loading) { return (
Loading form…
); } return ( acc.toggle(1)} onSave={() => tryAdvance(1)} nextLabel={isFeedbackOrOther ? "Questions" : "Fields"} sectionError={sectionErrors[1]} >