'use client' import { useCallback, useEffect, useRef, useState } from 'react' import type { FormAnswers, Question, Survey } from '@/lib/types' import { clearDraft, loadDraft, saveDraft } from '@/lib/storage' import { getHiddenCodes, isQuestionVisible } from '@/lib/conditional' import { ProgressBar } from './progress-bar' import { QuestionRenderer } from './question-renderer' import { SummaryScreen } from './summary-screen' // Section metadata (code prefix → display title) const SECTIONS: { prefix: string; title: string; sensitive?: boolean }[] = [ { prefix: 'A', title: 'Datos Generales' }, { prefix: 'B', title: '¿Vivís con discapacidad?', sensitive: true }, { prefix: 'C', title: 'Familiares con Discapacidad', sensitive: true }, { prefix: 'D', title: 'Adultos Mayores a Cargo', sensitive: true }, { prefix: 'E', title: 'Tu Rol como Cuidador/a' }, { prefix: 'F', title: 'Impacto Económico' }, { prefix: 'G', title: 'Bienestar y Descanso' }, { prefix: 'H', title: 'Necesidades' }, ] function getSectionPrefix(code: string) { return code.replace(/\d+$/, '') } interface SurveyFormProps { survey: Survey questions: Question[] } export function SurveyForm({ survey, questions }: SurveyFormProps) { const [answers, setAnswers] = useState({}) const [errors, setErrors] = useState>({}) const [submitted, setSubmitted] = useState(false) const [hydrated, setHydrated] = useState(false) // Announce when conditional blocks appear/disappear const [a11yAnnounce, setA11yAnnounce] = useState('') // Track which trigger codes had "Sí" on prev render to detect toggle const prevVisibility = useRef>({}) // Hydrate from localStorage on mount useEffect(() => { const draft = loadDraft(survey.id) if (Object.keys(draft).length > 0) setAnswers(draft) setHydrated(true) }, [survey.id]) // Persist to localStorage on every change (after hydration) useEffect(() => { if (!hydrated) return saveDraft(survey.id, answers) }, [answers, hydrated, survey.id]) const handleChange = useCallback( (code: string, value: FormAnswers[string]) => { setAnswers((prev) => { const updated = { ...prev, [code]: value } // Clear any dependent questions that become hidden const toHide = getHiddenCodes(code, value, questions, prev) for (const hiddenCode of toHide) { updated[hiddenCode] = undefined as unknown as FormAnswers[string] } // Announce block visibility change to screen readers if (toHide.length > 0) { setA11yAnnounce('Se ocultaron preguntas relacionadas y se limpiaron sus respuestas.') } else { // Check if a conditional block just became visible const appeared = questions.some( (q) => q.conditional_rules?.some((r) => r.if_question === code) && isQuestionVisible(q, updated) && !isQuestionVisible(q, prev) ) if (appeared) { setA11yAnnounce('Aparecieron nuevas preguntas según tu respuesta.') } } return updated }) // Clear validation error for this field setErrors((prev) => { if (!prev[code]) return prev const { [code]: _, ...rest } = prev return rest }) }, [questions] ) function validate(): boolean { const newErrors: Record = {} for (const q of questions) { if (!q.required) continue if (!isQuestionVisible(q, answers)) continue const val = answers[q.code] if ( val === null || val === undefined || val === '' || (Array.isArray(val) && val.length === 0) ) { newErrors[q.code] = 'Este campo es obligatorio.' } } setErrors(newErrors) return Object.keys(newErrors).length === 0 } function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!validate()) { // Move focus to first error const firstErrorCode = Object.keys(errors)[0] if (firstErrorCode) { document.getElementById(firstErrorCode)?.focus() } return } // 2A: no write to DB — just show summary clearDraft(survey.id) setSubmitted(true) } function handleStartOver() { setAnswers({}) setSubmitted(false) setErrors({}) } // Visible questions driven by conditional_rules const visibleQuestions = questions.filter((q) => isQuestionVisible(q, answers)) const answeredCount = visibleQuestions.filter((q) => { const v = answers[q.code] return v !== null && v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0) }).length if (submitted) { return ( ) } // Group visible questions by section prefix, in order type Section = { prefix: string; title: string; sensitive?: boolean; questions: Question[] } const sectionMap = new Map() for (const q of visibleQuestions) { const prefix = getSectionPrefix(q.code) if (!sectionMap.has(prefix)) { const meta = SECTIONS.find((s) => s.prefix === prefix) sectionMap.set(prefix, { prefix, title: meta?.title ?? prefix, sensitive: meta?.sensitive, questions: [], }) } sectionMap.get(prefix)!.questions.push(q) } return (
{/* Skip link for keyboard users */} Ir al formulario {/* Screen-reader live region for conditional announcements */}
{a11yAnnounce}

{survey.title}

Esta encuesta es anónima. Tus respuestas no te identifican.

{Array.from(sectionMap.values()).map((section) => (

{section.title}

{section.sensitive && (

Esta sección contiene preguntas sensibles sobre salud o discapacidad. Responder es completamente voluntario.

)}
{section.questions.map((q) => (
))}
))}
) }