Con las mejoras visuales
This commit is contained in:
@@ -1,274 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { useCallback, useEffect, useRef, useState, useTransition } from 'react'
|
|
||||||
import type { FormAnswers, Question, Survey } from '@/lib/types'
|
|
||||||
import { clearDraft, loadDraft, saveDraft } from '@/lib/storage'
|
|
||||||
import { getHiddenCodes, isQuestionVisible, ruleQuestionCodes } from '@/lib/conditional'
|
|
||||||
import { submitSurvey } from '@/app/actions/submit-survey'
|
|
||||||
import { ConsentScreen } from './consent-screen'
|
|
||||||
import { ProgressBar } from './progress-bar'
|
|
||||||
import { QuestionRenderer } from './question-renderer'
|
|
||||||
|
|
||||||
// Section metadata (code prefix → display title)
|
|
||||||
const SECTIONS: { prefix: string; title: string }[] = [
|
|
||||||
{ prefix: 'A', title: 'Datos Generales' },
|
|
||||||
{ prefix: '1', title: 'Prevalencia: cuidado y discapacidad' },
|
|
||||||
{ prefix: '2', title: 'Intensidad y rol del cuidado' },
|
|
||||||
{ prefix: '3', title: 'Impacto económico del cuidado' },
|
|
||||||
{ prefix: '4', title: 'Tiempo y descanso' },
|
|
||||||
{ prefix: '5', title: 'Impacto en el trabajo' },
|
|
||||||
{ prefix: '6', title: 'Conciencia organizacional' },
|
|
||||||
{ prefix: '7', title: 'Para cerrar' },
|
|
||||||
]
|
|
||||||
|
|
||||||
function getSectionPrefix(code: string): string {
|
|
||||||
const m = code.match(/^([A-Za-z]+|\d+)/)
|
|
||||||
return m ? m[1] : code
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormStage = 'consent' | 'form' | 'success' | 'error'
|
|
||||||
|
|
||||||
interface SurveyFormProps {
|
|
||||||
survey: Survey
|
|
||||||
questions: Question[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
|
||||||
const [stage, setStage] = useState<FormStage>('consent')
|
|
||||||
const [consentOperativo, setConsentOperativo] = useState(false)
|
|
||||||
const [consentMacroN1, setConsentMacroN1] = useState(false)
|
|
||||||
const [answers, setAnswers] = useState<FormAnswers>({})
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
|
||||||
const [isPending, startTransition] = useTransition()
|
|
||||||
const [a11yAnnounce, setA11yAnnounce] = useState('')
|
|
||||||
const [hydrated, setHydrated] = useState(false)
|
|
||||||
const prevAnswers = useRef<FormAnswers>({})
|
|
||||||
|
|
||||||
// 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 after hydration
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hydrated) return
|
|
||||||
saveDraft(survey.id, answers)
|
|
||||||
prevAnswers.current = answers
|
|
||||||
}, [answers, hydrated, survey.id])
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(code: string, value: FormAnswers[string]) => {
|
|
||||||
setAnswers((prev) => {
|
|
||||||
const updated = { ...prev, [code]: value }
|
|
||||||
const toHide = getHiddenCodes(code, value, questions, prev)
|
|
||||||
for (const hiddenCode of toHide) {
|
|
||||||
updated[hiddenCode] = undefined as unknown as FormAnswers[string]
|
|
||||||
}
|
|
||||||
if (toHide.length > 0) {
|
|
||||||
setA11yAnnounce('Se ocultaron preguntas relacionadas y se limpiaron sus respuestas.')
|
|
||||||
} else {
|
|
||||||
const appeared = questions.some(
|
|
||||||
(q) =>
|
|
||||||
q.conditional_rules?.some((r) => ruleQuestionCodes(r).includes(code)) &&
|
|
||||||
isQuestionVisible(q, updated) &&
|
|
||||||
!isQuestionVisible(q, prev)
|
|
||||||
)
|
|
||||||
if (appeared) setA11yAnnounce('Aparecieron nuevas preguntas según tu respuesta.')
|
|
||||||
}
|
|
||||||
return updated
|
|
||||||
})
|
|
||||||
setErrors((prev) => {
|
|
||||||
if (!prev[code]) return prev
|
|
||||||
const { [code]: _, ...rest } = prev
|
|
||||||
return rest
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[questions]
|
|
||||||
)
|
|
||||||
|
|
||||||
function validate(): boolean {
|
|
||||||
const newErrors: Record<string, string> = {}
|
|
||||||
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 (isPending) return
|
|
||||||
if (!validate()) {
|
|
||||||
const firstErrorCode = Object.keys(errors)[0]
|
|
||||||
if (firstErrorCode) document.getElementById(firstErrorCode)?.focus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const questionMap = Object.fromEntries(questions.map((q) => [q.code, q.id]))
|
|
||||||
|
|
||||||
startTransition(async () => {
|
|
||||||
const result = await submitSurvey({
|
|
||||||
surveyId: survey.id,
|
|
||||||
answers: answers as Record<string, unknown>,
|
|
||||||
questionMap,
|
|
||||||
consentOperativo,
|
|
||||||
consentMacroN1,
|
|
||||||
})
|
|
||||||
if (result.success) {
|
|
||||||
clearDraft(survey.id)
|
|
||||||
setStage('success')
|
|
||||||
} else {
|
|
||||||
setSubmitError(result.error)
|
|
||||||
setStage('error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Stage: consent ──────────────────────────────────────────
|
|
||||||
if (stage === 'consent') {
|
|
||||||
return (
|
|
||||||
<ConsentScreen
|
|
||||||
brandConfig={survey.brand_config}
|
|
||||||
onAccept={(op, macro) => {
|
|
||||||
setConsentOperativo(op)
|
|
||||||
setConsentMacroN1(macro)
|
|
||||||
setStage('form')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Stage: success ──────────────────────────────────────────
|
|
||||||
if (stage === 'success') {
|
|
||||||
return (
|
|
||||||
<div className="survey-wrapper">
|
|
||||||
<div className="thank-you-container" role="main">
|
|
||||||
<h1 className="thank-you-title">¡Gracias por participar!</h1>
|
|
||||||
<p className="thank-you-body">
|
|
||||||
Tu respuesta fue registrada correctamente. Recordá que la encuesta es anónima: no hay
|
|
||||||
forma de vincular tus respuestas con tu identidad.
|
|
||||||
</p>
|
|
||||||
<p className="thank-you-body">
|
|
||||||
Los resultados agregados ayudarán a {survey.brand_config?.org_name ?? 'tu empresa'} a
|
|
||||||
diseñar mejores apoyos para las personas cuidadoras.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Stage: error ────────────────────────────────────────────
|
|
||||||
if (stage === 'error') {
|
|
||||||
return (
|
|
||||||
<div className="survey-wrapper">
|
|
||||||
<div className="error-container" role="main">
|
|
||||||
<h1 className="error-title">No se pudo enviar la encuesta</h1>
|
|
||||||
<p>Hubo un error al guardar tus respuestas ({submitError}). Podés intentarlo de nuevo.</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={() => { setSubmitError(null); setStage('form') }}
|
|
||||||
>
|
|
||||||
Volver e intentar de nuevo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Stage: form ─────────────────────────────────────────────
|
|
||||||
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
|
|
||||||
|
|
||||||
type Section = { prefix: string; title: string; questions: Question[] }
|
|
||||||
const sectionMap = new Map<string, Section>()
|
|
||||||
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, questions: [] })
|
|
||||||
}
|
|
||||||
sectionMap.get(prefix)!.questions.push(q)
|
|
||||||
}
|
|
||||||
|
|
||||||
const bc = survey.brand_config
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="survey-wrapper">
|
|
||||||
<a href="#survey-form" className="skip-link">Ir al formulario</a>
|
|
||||||
|
|
||||||
<div aria-live="polite" aria-atomic="true" className="sr-only" role="status">
|
|
||||||
{a11yAnnounce}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Header contextual con org/branch si está configurado */}
|
|
||||||
{bc?.org_name && (
|
|
||||||
<div className="brand-header" role="note" aria-label="Información de la empresa">
|
|
||||||
<p className="brand-text">
|
|
||||||
Estás respondiendo esto porque pertenecés a <strong>{bc.org_name}</strong>
|
|
||||||
{bc.branch_name && (
|
|
||||||
<> y participás en las oficinas de <strong>{bc.branch_name}</strong>
|
|
||||||
{bc.branch_city && <> en <strong>{bc.branch_city}</strong></>}.</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<header className="survey-header">
|
|
||||||
<h1 className="survey-title">{survey.title}</h1>
|
|
||||||
<p className="survey-anon-note">Esta encuesta es anónima. Tus respuestas no te identifican.</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="survey-progress-sticky" aria-label="Barra de progreso">
|
|
||||||
<ProgressBar answered={answeredCount} total={visibleQuestions.length} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main id="survey-form">
|
|
||||||
<form onSubmit={handleSubmit} noValidate aria-label={`Encuesta: ${survey.title}`}>
|
|
||||||
{Array.from(sectionMap.values()).map((section) => (
|
|
||||||
<section
|
|
||||||
key={section.prefix}
|
|
||||||
className="survey-section"
|
|
||||||
aria-labelledby={`section-${section.prefix}`}
|
|
||||||
>
|
|
||||||
<div className="section-header">
|
|
||||||
<h2 id={`section-${section.prefix}`} className="section-title">Sección {section.prefix} · {section.title}</h2>
|
|
||||||
</div>
|
|
||||||
{section.questions.map((q) => (
|
|
||||||
<div key={q.id} className="question-wrapper" data-code={q.code}>
|
|
||||||
<QuestionRenderer
|
|
||||||
question={q}
|
|
||||||
answers={answers}
|
|
||||||
onChange={handleChange}
|
|
||||||
errors={errors}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="form-footer">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-primary"
|
|
||||||
disabled={isPending}
|
|
||||||
aria-disabled={isPending}
|
|
||||||
>
|
|
||||||
{isPending ? 'Enviando…' : 'Enviar encuesta'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user