'use client' import { useEffect, useState } from 'react' import type { BrandConfig, Survey } from '@/lib/types' import type { AnswerValue, FormAnswers, FormSchema, Screen } from '@/lib/form-engine.types' import { answerKey, expandInstances, initialState, next, back, resolveScreens, setAnswer, } from '@/lib/form-engine' import { loadDraft, saveDraft } from '@/lib/storage' import { ConsentScreen } from './consent-screen' import { ProgressBar } from './progress-bar' import { QuestionRenderer } from './question-renderer' type Stage = 'consent' | 'form' 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('') 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]) if (stage === 'consent') { return ( { setConsentOperativo(op) setConsentMacroN1(macro) setStage('form') }} /> ) } 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) => { const updated = setAnswer(prev, code, value, instanceId) // Limpia respuestas de pantallas que dejaron de ser visibles tras este cambio. const before = resolveScreens(schema, prev.answers) const after = resolveScreens(schema, updated.answers) const afterIds = new Set(after.map((s) => s.id)) const disappeared = before.filter((s) => !afterIds.has(s.id) && s.question) if (disappeared.length === 0) return updated const clearedAnswers = { ...updated.answers } for (const s of disappeared) { delete clearedAnswers[answerKey(s.question!.code, s.instanceId ?? undefined)] } return { ...updated, answers: clearedAnswers } }) setErrors((prev) => { if (!prev[code]) return prev const { [code]: _, ...rest } = prev return rest }) } function handleNext() { if (screen.kind === 'question' && screen.question?.required && isEmpty(currentValue)) { setErrors((e) => ({ ...e, [screen.question!.code]: 'Este campo es obligatorio.' })) return } setState((s) => next(s, schema)) } function handleBack() { setState((s) => back(s, schema)) } 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

El envío de la encuesta se habilita en una próxima etapa. Por ahora podés revisar tus respuestas con "Anterior".

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