'use client' import { useEffect, useLayoutEffect, useRef, useState, useTransition } from 'react' import type { BrandConfig, Survey } from '@/lib/types' import type { AnswerValue, FormAnswers, FormSchema, Screen } from '@/lib/form-engine.types' import { answerKey, applyAnswer, expandInstances, initialState, isRequiredAndUnanswered, next, back, resolveScreens, } from '@/lib/form-engine' import { loadDraft, saveDraft, clearDraft } from '@/lib/storage' import { submitSurvey } from '@/app/actions/submit-survey' import { ConsentScreen } from './consent-screen' import { ProgressBar } from './progress-bar' import { QuestionRenderer } from './question-renderer' type Stage = 'consent' | 'form' | 'success' | 'error' interface WizardFormProps { survey: Survey schema: FormSchema } function isEmpty(v: AnswerValue): boolean { return v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0) } function valueFor(screen: Screen, answers: FormAnswers): AnswerValue { if (!screen.question) return null const v = answers[answerKey(screen.question.code, screen.instanceId ?? undefined)] return v === undefined ? null : v } /** Resume en la primera pantalla sin responder (o la sección que la precede). */ function resumeScreenId(schema: FormSchema, answers: FormAnswers): string { const screens = resolveScreens(schema, answers) const firstUnanswered = screens.findIndex( (s) => s.kind === 'question' && isEmpty(valueFor(s, answers)) ) if (firstUnanswered <= 0) return screens[0]?.id ?? 'end' const prev = screens[firstUnanswered - 1] return prev.kind === 'section' ? prev.id : screens[firstUnanswered].id } export function WizardForm({ survey, schema }: WizardFormProps) { const [stage, setStage] = useState('consent') const [consentOperativo, setConsentOperativo] = useState(false) const [consentMacroN1, setConsentMacroN1] = useState(false) const [state, setState] = useState(() => initialState(schema)) const [errors, setErrors] = useState>({}) const [hydrated, setHydrated] = useState(false) const [a11yAnnounce, setA11yAnnounce] = useState('') const answerAreaRef = useRef(null) const [hasOverflow, setHasOverflow] = useState(false) const [submitError, setSubmitError] = useState(null) const [isPending, startTransition] = useTransition() useEffect(() => { const draft = loadDraft(survey.id) if (Object.keys(draft).length > 0) { setState({ answers: draft, currentScreenId: resumeScreenId(schema, draft) }) } setHydrated(true) // eslint-disable-next-line react-hooks/exhaustive-deps }, [survey.id]) useEffect(() => { if (!hydrated) return saveDraft(survey.id, state.answers) }, [state.answers, hydrated, survey.id]) const screens = resolveScreens(schema, state.answers) const idx = screens.findIndex((s) => s.id === state.currentScreenId) const screen = screens[idx] ?? screens[0] useEffect(() => { if (!screen) return if (screen.kind === 'section') setA11yAnnounce(`Sección: ${screen.question?.prompt}`) else if (screen.kind === 'question') setA11yAnnounce(`Pregunta ${screen.question?.code}: ${screen.question?.prompt}`) else setA11yAnnounce('Fin del formulario') }, [screen?.id]) // El fade de wizard-answer-area solo debe aparecer si hay overflow real — // CSS no puede expresar esa condición (no hay selector para "este elemento // scrollea"), así que se mide el DOM. Se remide al cambiar de pantalla // (cambia todo el contenido) y al resize (puede cambiar cuántas opciones entran). useLayoutEffect(() => { const el = answerAreaRef.current setHasOverflow(el ? el.scrollHeight > el.clientHeight : false) }, [screen?.id]) useEffect(() => { function recalc() { const el = answerAreaRef.current setHasOverflow(el ? el.scrollHeight > el.clientHeight : false) } window.addEventListener('resize', recalc) return () => window.removeEventListener('resize', recalc) }, []) if (stage === 'consent') { return ( { setConsentOperativo(op) setConsentMacroN1(macro) setStage('form') }} /> ) } if (stage === 'success') { return (

¡Gracias por participar!

Tu respuesta fue registrada correctamente. Recordá que la encuesta es anónima: no hay forma de vincular tus respuestas con tu identidad.

Los resultados agregados ayudarán a {survey.brand_config?.org_name ?? 'tu empresa'} a diseñar mejores apoyos para las personas cuidadoras.

) } if (stage === 'error') { return (

No se pudo enviar la encuesta

Hubo un error al guardar tus respuestas. Podés intentarlo de nuevo.

{submitError &&
{submitError}
}
) } if (!screen) return null const currentValue = valueFor(screen, state.answers) const questionScreens = screens.filter((s) => s.kind !== 'end') const ordinal = Math.min(idx + 1, questionScreens.length) const sectionScreens = screens.filter((s) => s.kind === 'section') const sectionOrdinal = screen.kind === 'section' ? sectionScreens.findIndex((s) => s.id === screen.id) + 1 : null // Contexto de instancia ("Familiar X de N") — derivado, no decide visibilidad ni navegación. let instanceCtx: { label: string; ordinal: number; total: number } | null = null if (screen.instanceId && screen.question?.instance_of) { const generator = schema.questions.find((q) => q.code === screen.question!.instance_of) if (generator) { const instances = expandInstances(generator, state.answers) const ord = instances.findIndex((i) => i.instanceId === screen.instanceId) if (ord >= 0) { instanceCtx = { label: instances[ord].optionValue, ordinal: ord + 1, total: instances.length } } } } function handleChange(value: AnswerValue) { if (!screen.question) return const code = screen.question.code const instanceId = screen.instanceId ?? undefined setState((prev) => applyAnswer(prev, schema, code, value, instanceId)) setErrors((prev) => { if (!prev[code]) return prev const { [code]: _, ...rest } = prev return rest }) } function handleNext() { if (isRequiredAndUnanswered(screen, state.answers)) { setErrors((e) => ({ ...e, [screen.question!.code]: 'Este campo es obligatorio.' })) return } setState((s) => next(s, schema)) } function handleBack() { setState((s) => back(s, schema)) } function handleSubmit() { if (isPending) return // state.answers usa claves "code:instanceId" — se descompone una sola vez // acá, al borde cliente/servidor; el validador (lib/submit-validation.ts) // recibe la forma explícita {code, instanceId, value}[]. const answers = Object.entries(state.answers).map(([key, value]) => { const sep = key.indexOf(':') return { code: key.slice(0, sep), instanceId: key.slice(sep + 1), value } }) startTransition(async () => { try { const result = await submitSurvey({ surveyId: survey.id, answers, consentOperativo, consentMacroN1, }) if (result.success) { clearDraft(survey.id) setStage('success') } else { setSubmitError(result.error) setStage('error') } } catch { // Falla de red al invocar el Server Action (fetch failed) — no es un // error que submitSurvey pueda devolver como result, hay que capturarlo acá. setSubmitError('No pudimos enviar tu respuesta. Verificá tu conexión e intentá de nuevo.') setStage('error') } }) } const bc: BrandConfig | null = survey.brand_config return (
{a11yAnnounce}
{bc?.org_name && (

Encuesta de {bc.org_name}

)}
{screen.kind === 'section' && screen.question && (

Sección {sectionOrdinal} de {sectionScreens.length}

{screen.question.prompt}

{screen.question.hint &&

{screen.question.hint}

}
)} {screen.kind === 'question' && screen.question && ( <>
{instanceCtx && ( {instanceCtx.label} · familiar {instanceCtx.ordinal} de {instanceCtx.total} )} {screen.question.code}

{screen.question.prompt}

{screen.question.hint &&

{screen.question.hint}

}
)} {screen.kind === 'end' && (

Llegaste al final

Revisá tus respuestas con "Anterior" si querés corregir algo, o enviá la encuesta cuando estés listo/a.

)}
{screen.kind !== 'end' ? ( ) : ( )}
) }