"use client"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { apiFetch } from "@/lib/api"; // ─── Types ──────────────────────────────────────────────────────────────────── type EarlyBirdTier = { deadline: string; price: number; order?: number; variantId?: string | null }; type OptionVariant = { id: string; name: string; price: number | null; stockLimit?: number; availableCount?: number | null }; type EventOption = { id: string; name: string; price: number; isMainTicket?: boolean; earlyBirdTiers?: EarlyBirdTier[]; variants?: OptionVariant[]; }; type FormField = { id: string; type: string; label: string; isRequired: boolean; order: number; helpText?: string | null; options?: any; }; type EventForm = { id: string; isRequired: boolean; fields: FormField[]; }; type KioskEvent = { id: string; title: string; description?: string; startDate: string; endDate: string; picture?: string; eventOptions?: EventOption[]; form?: EventForm | null; }; type Screen = "setup" | "form" | "eventform" | "confirmation"; // ─── Helpers ────────────────────────────────────────────────────────────────── function effectiveUnit(opt: EventOption): number { const base = opt.price || 0; const tiers = (Array.isArray(opt.earlyBirdTiers) ? opt.earlyBirdTiers : []).filter(t => !t.variantId); if (tiers.length === 0) return base; const now = new Date(); const applicable = tiers .map((t) => ({ ...t, deadline: new Date(t.deadline) })) .filter((t) => now < t.deadline) .sort((a, b) => a.deadline.getTime() - b.deadline.getTime() || (a.order || 0) - (b.order || 0) || a.price - b.price); return applicable.length > 0 ? applicable[0].price : base; } function effectiveVariantUnit(opt: EventOption, variant: OptionVariant): number { const base = variant.price !== null && variant.price !== undefined ? variant.price : opt.price || 0; const allTiers = Array.isArray(opt.earlyBirdTiers) ? opt.earlyBirdTiers : []; const variantTiers = allTiers.filter(t => t.variantId === variant.id); const tiers = variantTiers.length > 0 ? variantTiers : allTiers.filter(t => !t.variantId); if (tiers.length === 0) return base; const now = new Date(); const applicable = tiers .map((t) => ({ ...t, deadline: new Date(t.deadline) })) .filter((t) => now < t.deadline) .sort((a, b) => a.deadline.getTime() - b.deadline.getTime() || (a.order || 0) - (b.order || 0) || a.price - b.price); return applicable.length > 0 ? applicable[0].price : base; } function fmtDate(dateStr: string) { try { return new Date(dateStr).toLocaleDateString(undefined, { weekday: "short", year: "numeric", month: "short", day: "numeric", }); } catch { return dateStr; } } function fmtCurrency(val: number) { return val === 0 ? "Free" : `R${val.toFixed(2)}`; } function prefLabel(pref: "email" | "whatsapp" | "both") { return pref === "whatsapp" ? "WhatsApp" : pref === "both" ? "Email & WhatsApp" : "Email"; } // Loose "does this look like a mobile number" check — covers 0821234567 (10, leading 0), // 821234567 (9, no leading 0), and 27821234567 / +27821234567 (11 digits, country code). function looksLikePhone(s: string): boolean { const digits = s.replace(/\D/g, ""); return digits.length >= 9 && digits.length <= 11; } // Show/hide toggle rendered inside a password input — absolutely positioned on its // right edge, so callers must wrap the input in a `relative` container and add // enough right padding (`pr-12`) for it not to overlap the typed text. function PasswordToggleButton({ shown, onToggle }: { shown: boolean; onToggle: () => void }) { return ( ); } // ─── Kiosk Page ─────────────────────────────────────────────────────────────── export default function SelfServicePage() { const [screen, setScreen] = useState("setup"); // ── Setup state ───────────────────────────────────────────────── const [setupEmail, setSetupEmail] = useState(""); const [setupPassword, setSetupPassword] = useState(""); const [showSetupPassword, setShowSetupPassword] = useState(false); const [setupLoading, setSetupLoading] = useState(false); const [setupError, setSetupError] = useState(null); const [supervisorToken, setSupervisorToken] = useState(null); const [events, setEvents] = useState([]); const [selectedEventId, setSelectedEventId] = useState(""); const [eventsLoading, setEventsLoading] = useState(false); // ── Change-event modal ─────────────────────────────────────────── const [showChangeModal, setShowChangeModal] = useState(false); const [changePassword, setChangePassword] = useState(""); const [showChangePassword, setShowChangePassword] = useState(false); const [changeError, setChangeError] = useState(null); const [changeLoading, setChangeLoading] = useState(false); // ── Visitor form state ─────────────────────────────────────────── const [visitorName, setVisitorName] = useState(""); const [visitorEmail, setVisitorEmail] = useState(""); const [visitorPhone, setVisitorPhone] = useState(""); const [notificationPref, setNotificationPref] = useState<"email" | "whatsapp" | "both">("email"); const [createAccount, setCreateAccount] = useState(false); const [visitorPassword, setVisitorPassword] = useState(""); const [showVisitorPassword, setShowVisitorPassword] = useState(false); const [quantities, setQuantities] = useState>({}); // ── Account lookup (automatic, triggered when email/phone is entered) ── // Email and phone are resolved to an existing account independently. If they // resolve to the SAME account, that account is "matched". If they resolve to // two DIFFERENT accounts, that's a conflict — surfaced to the operator instead // of silently overwriting one field's details with the other's account. type MatchedAccount = { id: string; name: string; email: string | null; phoneNumber: string | null; notificationPreference: "email" | "whatsapp" | "both" }; const [emailMatch, setEmailMatch] = useState(null); const [phoneMatch, setPhoneMatch] = useState(null); const accountConflict = !!(emailMatch && phoneMatch && emailMatch.id !== phoneMatch.id); const matchedAccount = accountConflict ? null : (emailMatch || phoneMatch); const accountExists = !!matchedAccount; const [lookupLoading, setLookupLoading] = useState(false); const [lookupMessage, setLookupMessage] = useState(null); const [showUpdateConfirm, setShowUpdateConfirm] = useState(false); const [formLoading, setFormLoading] = useState(false); const [formError, setFormError] = useState(null); // ── Event form (attendee form) state ──────────────────────────── const [currentRegistrationId, setCurrentRegistrationId] = useState(null); const [currentUserId, setCurrentUserId] = useState(null); const [currentTicketIndex, setCurrentTicketIndex] = useState(0); const [allFormAnswers, setAllFormAnswers] = useState[]>([]); const [currentTicketAnswers, setCurrentTicketAnswers] = useState>({}); const [formSubmitLoading, setFormSubmitLoading] = useState(false); const [formSubmitError, setFormSubmitError] = useState(null); // ── Confirmation state ─────────────────────────────────────────── const [confirmationName, setConfirmationName] = useState(""); const [registrationIsFree, setRegistrationIsFree] = useState(false); const [countdown, setCountdown] = useState(10); const countdownRef = useRef | null>(null); // ─── Derived ──────────────────────────────────────────────────── const selectedEvent = useMemo( () => events.find((e) => e.id === selectedEventId) || null, [events, selectedEventId] ); const total = useMemo(() => { if (!selectedEvent) return 0; return (selectedEvent.eventOptions || []).reduce((sum, o) => { if ((o.variants || []).length > 0) { return sum + (o.variants || []).reduce((vs, v) => vs + (quantities[`${o.id}::${v.id}`] || 0) * effectiveVariantUnit(o, v), 0); } return sum + (quantities[o.id] || 0) * effectiveUnit(o); }, 0); }, [selectedEvent, quantities]); // Does the selected event have a required form? const eventHasRequiredForm = useMemo(() => { return !!(selectedEvent?.form?.isRequired && (selectedEvent.form.fields || []).some(f => f.type !== 'statement')); }, [selectedEvent]); // Answerable form fields (exclude statement-type display fields) const answerableFields = useMemo(() => { if (!selectedEvent?.form) return []; return (selectedEvent.form.fields || []) .filter(f => f.type !== 'statement') .sort((a, b) => (a.order || 0) - (b.order || 0)); }, [selectedEvent]); // Number of main tickets selected (for form count) const mainTicketCount = useMemo(() => { if (!selectedEvent) return 0; return (selectedEvent.eventOptions || []) .filter(o => o.isMainTicket) .reduce((sum, o) => { if ((o.variants || []).length > 0) { return sum + (o.variants || []).reduce((vs, v) => vs + (quantities[`${o.id}::${v.id}`] || 0), 0); } return sum + (quantities[o.id] || 0); }, 0); }, [selectedEvent, quantities]); // Differences between what's on file for the matched account and what's currently // typed in the form — shown to the operator for confirmation before anything is saved. const pendingChanges = useMemo(() => { if (!matchedAccount) return []; const changes: { field: string; from: string; to: string }[] = []; const name = visitorName.trim(); const email = visitorEmail.trim(); const phone = visitorPhone.trim(); if (name && name !== matchedAccount.name) { changes.push({ field: "Name", from: matchedAccount.name, to: name }); } if (email && email !== (matchedAccount.email || "")) { changes.push({ field: "Email", from: matchedAccount.email || "(none on file)", to: email }); } if (phone && phone !== (matchedAccount.phoneNumber || "")) { changes.push({ field: "Phone", from: matchedAccount.phoneNumber || "(none on file)", to: phone }); } if (notificationPref !== matchedAccount.notificationPreference) { changes.push({ field: "Send tickets via", from: prefLabel(matchedAccount.notificationPreference), to: prefLabel(notificationPref), }); } return changes; }, [matchedAccount, visitorName, visitorEmail, visitorPhone, notificationPref]); // ─── Load events after supervisor login ───────────────────────── const loadEvents = useCallback(async (token: string) => { setEventsLoading(true); try { const data: KioskEvent[] = await apiFetch( "/api/events/all?includePast=false&includeInactive=false&excludeClosed=true", { authToken: token } ); setEvents(data); } catch { setEvents([]); } finally { setEventsLoading(false); } }, []); // ─── Setup: supervisor login ───────────────────────────────────── async function handleSetupLogin(e: React.FormEvent) { e.preventDefault(); setSetupError(null); setSetupLoading(true); try { const data = await apiFetch("/api/users/login", { method: "POST", body: { email: setupEmail, password: setupPassword }, }); const role: string = data.role || ""; if (!["admin", "supervisor"].includes(role)) { setSetupError("Only admins and supervisors can set up the kiosk."); setSetupLoading(false); return; } setSupervisorToken(data.token); await loadEvents(data.token); } catch (err: any) { setSetupError(err?.message || "Login failed. Please check your credentials."); } finally { setSetupLoading(false); } } function handleStartKiosk() { if (!selectedEventId || !selectedEvent) return; const initial: Record = {}; (selectedEvent.eventOptions || []).forEach((o) => { if ((o.variants || []).length > 0) { (o.variants || []).forEach((v) => (initial[`${o.id}::${v.id}`] = 0)); } else { initial[o.id] = 0; } }); setQuantities(initial); setScreen("form"); } // ─── Change event modal ────────────────────────────────────────── async function handleChangeEvent(e: React.FormEvent) { e.preventDefault(); setChangeError(null); setChangeLoading(true); try { const data = await apiFetch("/api/users/login", { method: "POST", body: { email: setupEmail, password: changePassword }, }); const role: string = data.role || ""; if (!["admin", "supervisor"].includes(role)) { setChangeError("Only admins and supervisors can change the event."); setChangeLoading(false); return; } setSupervisorToken(data.token); await loadEvents(data.token); setChangePassword(""); setChangeError(null); setShowChangeModal(false); setScreen("setup"); setSelectedEventId(""); resetForm(); } catch (err: any) { setChangeError(err?.message || "Incorrect password."); } finally { setChangeLoading(false); } } // ─── Visitor registration ──────────────────────────────────────── // Validates the form and, if the typed name/email/phone differ from the matched // account's details, shows a confirmation modal before anything is saved. function handleRegisterSubmit(e: React.FormEvent) { e.preventDefault(); setFormError(null); if (!visitorName.trim() || (!visitorEmail.trim() && !visitorPhone.trim())) { setFormError("Name and at least an email or phone number are required."); return; } if (createAccount && visitorPassword.trim().length < 6) { setFormError("Password must be at least 6 characters."); return; } if (!selectedEventId) { setFormError("No event selected."); return; } if (accountConflict) { setFormError("The email and phone number entered belong to two different existing accounts. Please check and correct one of them before continuing."); return; } if (pendingChanges.length > 0) { setShowUpdateConfirm(true); return; } handleRegister(); } async function handleRegister() { setFormLoading(true); try { const payload: any = { eventId: selectedEventId, options: Object.entries(quantities) .filter(([, qty]) => qty > 0) .map(([key, quantity]) => { const [eventOptionId, variantId] = key.split("::"); return { eventOptionId, quantity, ...(variantId ? { variantId } : {}) }; }), user: { name: visitorName.trim(), ...(visitorEmail.trim() ? { email: visitorEmail.trim() } : {}), ...(visitorPhone.trim() ? { phoneNumber: visitorPhone.trim() } : {}), }, guestOnly: !createAccount, notificationPreference: notificationPref, }; if (createAccount && visitorPassword) { payload.user.password = visitorPassword; } const result: any = await apiFetch("/api/registrations/manual", { method: "POST", body: payload, authToken: supervisorToken, }); setCurrentRegistrationId(result?.id || null); setCurrentUserId(result?.userId || null); setRegistrationIsFree(result?.status === "paid" || total === 0); setConfirmationName(visitorName.trim()); // If event has a required form and there are main tickets, show form step if (eventHasRequiredForm && mainTicketCount > 0 && result?.id) { setCurrentTicketIndex(0); setAllFormAnswers([]); setCurrentTicketAnswers({}); setFormSubmitError(null); setScreen("eventform"); } else { setScreen("confirmation"); startCountdown(); } } catch (err: any) { setFormError(err?.message || "Registration failed. Please try again."); } finally { setFormLoading(false); } } // ─── Event form submission (per-ticket) ────────────────────────── async function handleFormSubmit(e: React.FormEvent) { e.preventDefault(); setFormSubmitError(null); // Validate required fields for current ticket for (const field of answerableFields) { if (field.isRequired && !currentTicketAnswers[field.id]?.trim()) { setFormSubmitError(`"${field.label}" is required.`); return; } } if (!currentRegistrationId) { setFormSubmitError("No registration found."); return; } const collectedAnswers = [...allFormAnswers, currentTicketAnswers]; // If more tickets remain, advance to the next one if (currentTicketIndex < mainTicketCount - 1) { setAllFormAnswers(collectedAnswers); setCurrentTicketAnswers({}); setCurrentTicketIndex((i) => i + 1); return; } // Last ticket — submit all responses setFormSubmitLoading(true); try { const responses = collectedAnswers.map((answers) => ({ answers })); await apiFetch(`/api/registrations/${currentRegistrationId}/forms/responses`, { method: "POST", body: { responses }, authToken: supervisorToken, }); setScreen("confirmation"); startCountdown(); } catch (err: any) { setFormSubmitError(err?.message || "Form submission failed. Please try again."); } finally { setFormSubmitLoading(false); } } // ─── Confirmation countdown ────────────────────────────────────── function startCountdown() { setCountdown(10); if (countdownRef.current) clearInterval(countdownRef.current); countdownRef.current = setInterval(() => { setCountdown((c) => { if (c <= 1) { clearInterval(countdownRef.current!); resetAndGoToForm(); return 0; } return c - 1; }); }, 1000); } function resetAndGoToForm() { if (countdownRef.current) clearInterval(countdownRef.current); resetForm(); setScreen("form"); const initial: Record = {}; (selectedEvent?.eventOptions || []).forEach((o) => { if ((o.variants || []).length > 0) { (o.variants || []).forEach((v) => (initial[`${o.id}::${v.id}`] = 0)); } else { initial[o.id] = 0; } }); setQuantities(initial); } function resetForm() { setVisitorName(""); setVisitorEmail(""); setVisitorPhone(""); setNotificationPref("email"); setCreateAccount(false); setVisitorPassword(""); setShowVisitorPassword(false); setEmailMatch(null); setPhoneMatch(null); appliedMatchIdRef.current = null; setShowUpdateConfirm(false); setLookupMessage(null); setFormError(null); setCurrentRegistrationId(null); setCurrentUserId(null); setCurrentTicketIndex(0); setAllFormAnswers([]); setCurrentTicketAnswers({}); setFormSubmitError(null); } useEffect(() => { return () => { if (countdownRef.current) clearInterval(countdownRef.current); }; }, []); // ─── Automatic account lookup — runs once the operator finishes entering the // email or phone field (on blur), never on keystroke. Matches exactly against // that one value and only ever returns that one matched account (or nothing) — // never a broader/fuzzy match. Email and phone are tracked as two independent // matches (emailMatch/phoneMatch, above) rather than being merged into a single // "last found account" — that's what previously let changing the phone number // silently pull in and overwrite the form with an unrelated account's details. ── async function performLookup(kind: "email" | "phone", rawValue: string) { const value = rawValue.trim(); const setMatch = kind === "email" ? setEmailMatch : setPhoneMatch; if (!value || !supervisorToken) { setMatch(null); return; } if (kind === "email" ? !value.includes("@") : !looksLikePhone(value)) { setMatch(null); return; } setLookupLoading(true); setLookupMessage(null); try { const params = new URLSearchParams(); if (kind === "email") params.set("email", value); else params.set("phone", value); const data = await apiFetch<{ exists: boolean; user?: MatchedAccount | null; }>(`/api/users/check-exists?${params.toString()}`, { authToken: supervisorToken }); if (data?.exists && data.user) { setMatch(data.user); setLookupMessage("Account found — details filled in below."); } else { setMatch(null); setLookupMessage(null); } } catch { setLookupMessage("Lookup failed. Please try again."); } finally { setLookupLoading(false); } } function handleEmailBlur() { performLookup("email", visitorEmail); } function handlePhoneBlur() { performLookup("phone", visitorPhone); } // Autofill Name/Email/Phone/preference from the matched account — but only once per // distinct account id. Re-focusing elsewhere on the page (e.g. tapping a ticket // quantity button) blurs whatever field was last active and re-runs its lookup; // that returns the same account as a new object each time, and keying this off // object identity instead of id made it re-fire and stomp every field — including // ones the operator had already deliberately edited — back to the account's // original values on every unrelated tap. const appliedMatchIdRef = useRef(null); useEffect(() => { if (!matchedAccount || appliedMatchIdRef.current === matchedAccount.id) return; appliedMatchIdRef.current = matchedAccount.id; setVisitorName(matchedAccount.name); if (matchedAccount.email) setVisitorEmail(matchedAccount.email); if (matchedAccount.phoneNumber) setVisitorPhone(matchedAccount.phoneNumber); setNotificationPref(matchedAccount.notificationPreference); }, [matchedAccount]); // Existing accounts are linked automatically — never show the "create account" toggle for them useEffect(() => { if (accountExists && createAccount) { setCreateAccount(false); setVisitorPassword(""); } }, [accountExists, createAccount]); // ─── Setup screen ──────────────────────────────────────────────── const isLoggedIn = !!supervisorToken; // ─── Render ────────────────────────────────────────────────────── return (
{/* Change Event button */} {screen === "form" && (
)} {/* ── Change Event Modal ─────────────────────────────────────── */} {showChangeModal && (

Change Event

Re-enter your password to change the event.

setChangePassword(e.target.value)} className="w-full border border-gray-300 rounded-lg px-4 py-3 pr-12 text-base focus:outline-none focus:ring-2 focus:ring-brand-500" placeholder="Your password" autoFocus required /> setShowChangePassword((v) => !v)} />
{changeError &&

{changeError}

}
)} {/* ── Setup Screen ───────────────────────────────────────────── */} {screen === "setup" && (

Self-Service Kiosk

Supervisor setup required

{!isLoggedIn && (
setSetupEmail(e.target.value)} className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-brand-500" placeholder="supervisor@example.com" required autoComplete="email" />
setSetupPassword(e.target.value)} className="w-full border border-gray-300 rounded-xl px-4 py-3 pr-12 text-base focus:outline-none focus:ring-2 focus:ring-brand-500" placeholder="••••••••" required autoComplete="current-password" /> setShowSetupPassword((v) => !v)} />
{setupError &&

{setupError}

}
)} {isLoggedIn && (

Signed in as supervisor. Select an event to begin.

{eventsLoading ? (

Loading events…

) : (
)} {selectedEvent && (

{selectedEvent.title}

{fmtDate(selectedEvent.startDate)} — {fmtDate(selectedEvent.endDate)}

{selectedEvent.form?.isRequired && (

This event requires an attendee form to be completed at registration.

)}
)}
)}
)} {/* ── Registration Form Screen ───────────────────────────────── */} {screen === "form" && selectedEvent && (

Registering for

{selectedEvent.title}

{fmtDate(selectedEvent.startDate)}

{ const v = e.target.value; setVisitorEmail(v); // Auto-derive preference when only one contact channel is present if (v.trim() && !visitorPhone.trim()) setNotificationPref("email"); else if (!v.trim() && visitorPhone.trim()) setNotificationPref("whatsapp"); }} onBlur={handleEmailBlur} className="w-full border border-gray-300 rounded-xl px-4 py-3.5 text-lg focus:outline-none focus:ring-2 focus:ring-brand-500" placeholder="john@example.com" />
{ const v = e.target.value; setVisitorPhone(v); if (v.trim() && !visitorEmail.trim()) setNotificationPref("whatsapp"); else if (!v.trim() && visitorEmail.trim()) setNotificationPref("email"); }} onBlur={handlePhoneBlur} className="w-full border border-gray-300 rounded-xl px-4 py-3.5 text-lg focus:outline-none focus:ring-2 focus:ring-brand-500" placeholder="+27 82 000 0000" />
{lookupLoading && (

Checking for an existing account…

)} {!lookupLoading && accountConflict && emailMatch && phoneMatch && (
This email matches an existing account for {emailMatch.name}, but this phone number matches a different existing account for {phoneMatch.name}. Please check and correct one of these fields before continuing.
)} {!lookupLoading && !accountConflict && lookupMessage && (

{lookupMessage}

)}
setVisitorName(e.target.value)} className="w-full border border-gray-300 rounded-xl px-4 py-3.5 text-lg focus:outline-none focus:ring-2 focus:ring-brand-500" placeholder="John Smith" required />
{/* Preference selector — only shown when both channels are available */} {visitorEmail.trim() && visitorPhone.trim() && (
{(["email", "whatsapp", "both"] as const).map((p) => ( ))}
)} {/* Ticket options */} {(selectedEvent.eventOptions || []).length > 0 && (
{(selectedEvent.eventOptions || []).map((opt) => { const hasVariants = (opt.variants || []).length > 0; if (hasVariants) { return (

{opt.name}

{(opt.variants || []).map((v) => { const unit = effectiveVariantUnit(opt, v); const baseVariantPrice = v.price !== null && v.price !== undefined ? v.price : opt.price; const key = `${opt.id}::${v.id}`; return (

{v.name}

{fmtCurrency(unit)} {unit !== baseVariantPrice && baseVariantPrice > 0 ? ` (was ${fmtCurrency(baseVariantPrice)})` : ""}

{quantities[key] || 0}
); })}
); } const unit = effectiveUnit(opt); return (

{opt.name}

{fmtCurrency(unit)}{unit !== opt.price && opt.price > 0 ? ` (was ${fmtCurrency(opt.price)})` : ""}

{quantities[opt.id] || 0}
); })}
{total > 0 && (
Total: {fmtCurrency(total)}
)}
)} {/* Account creation toggle — hidden once we know an account already exists */} {accountExists ? (
An account with this email/number already exists — you'll be registered using that account.
) : (
{createAccount && (
setVisitorPassword(e.target.value)} className="w-full border border-gray-300 rounded-xl px-4 py-3 pr-12 text-base focus:outline-none focus:ring-2 focus:ring-brand-500" placeholder="Min. 6 characters" autoComplete="new-password" /> setShowVisitorPassword((v) => !v)} />
)}
)} {formError && (
{formError}
)}
)} {/* ── Confirm Account Changes Modal ──────────────────────────── */} {showUpdateConfirm && (

Confirm account changes

These details differ from what's on file for this account. Confirm to update them.

{pendingChanges.map((c) => (

{c.field}

{c.from}

{c.to}

))}
)} {/* ── Event Form Screen ──────────────────────────────────────── */} {screen === "eventform" && selectedEvent?.form && (

{selectedEvent.title}

Attendee Information

{mainTicketCount > 1 && (

Attendee {currentTicketIndex + 1} of {mainTicketCount}

{Array.from({ length: mainTicketCount }).map((_, i) => (
))}
)} {mainTicketCount <= 1 && (

This form is required to complete your registration.

)}
{answerableFields.map((field) => (
{field.helpText && (

{field.helpText}

)} {field.type === "yes_no" ? (
{["Yes", "No"].map((opt) => ( ))}
) : field.type === "numeric" ? ( setCurrentTicketAnswers(a => ({ ...a, [field.id]: e.target.value }))} required={field.isRequired} className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-brand-500" /> ) : field.type === "date" ? ( setCurrentTicketAnswers(a => ({ ...a, [field.id]: e.target.value }))} required={field.isRequired} className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-brand-500" /> ) : field.type === "paragraph" ? (