Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 145 additions & 78 deletions src/components/SurveyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import React, { useState, useEffect } from 'react';
import { Survey, Question, Answer, QuestionOption } from '../types';
import { Loader2, AlertTriangle, ArrowLeft } from 'lucide-react';

// ==== Interfaces que devuelve tu backend ====
interface DBQuestionOption {
id: number;
orden: number | null;
Expand All @@ -17,8 +16,8 @@ interface DBQuestionOption {
interface DBQuestion {
id: number;
texto: string;
tipo_escala?: string | null; // en tu modelo puede venir null
opciones?: DBQuestionOption[]; // aquí vienen las opciones
tipo_escala?: string | null;
opciones?: DBQuestionOption[];
}

interface SurveyFormProps {
Expand All @@ -39,7 +38,6 @@ export const SurveyForm: React.FC<SurveyFormProps> = ({ survey, onComplete, onCa
.replace(/\s+/g, ' ')
.trim();

// Normaliza opciones del backend -> QuestionOption[]
const toQuestionOptions = (dbOpts?: DBQuestionOption[]): QuestionOption[] => {
if (!dbOpts || dbOpts.length === 0) return [];
return [...dbOpts]
Expand All @@ -50,10 +48,22 @@ export const SurveyForm: React.FC<SurveyFormProps> = ({ survey, onComplete, onCa
typeof o.valor === 'number'
? o.valor
: (isNaN(Number(o.valor)) ? String(o.valor) : Number(o.valor)),
orden: o.orden ?? undefined,
group: o.subescala ?? undefined,
}));
};

// Carga de preguntas desde el backend
const isLSAS = (dbq: DBQuestion): boolean => {
const hasLsasType = dbq.opciones?.some(
(o) => (o.tipo_escala ?? '').toLowerCase() === 'lsas'
);
const hasLsasSub = dbq.opciones?.some((o) => {
const s = (o.subescala ?? '').toLowerCase();
return s === 'miedo' || s === 'evitacion' || s === 'evitación';
});
return Boolean(hasLsasType || hasLsasSub);
};

useEffect(() => {
const fetchQuestions = async () => {
setLoading(true);
Expand All @@ -66,7 +76,18 @@ export const SurveyForm: React.FC<SurveyFormProps> = ({ survey, onComplete, onCa
const mapped: Question[] = data.map((dbq) => {
const opts = toQuestionOptions(dbq.opciones);

// ✅ Si hay opciones, es multiple-choice
if (isLSAS(dbq)) {
return {
id: String(dbq.id),
text: sanitize(dbq.texto),
type: 'lsas',
required: true,
options: [],
scaleMin: 0,
scaleMax: 3,
};
}

if (opts.length > 0) {
return {
id: String(dbq.id),
Expand All @@ -77,7 +98,6 @@ export const SurveyForm: React.FC<SurveyFormProps> = ({ survey, onComplete, onCa
};
}

// Sin opciones -> text
return {
id: String(dbq.id),
text: sanitize(dbq.texto),
Expand Down Expand Up @@ -106,14 +126,13 @@ export const SurveyForm: React.FC<SurveyFormProps> = ({ survey, onComplete, onCa

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const finalAnswers: Answer[] = Object.keys(answers).map((questionId) => ({
questionId,
answer: answers[questionId],
const finalAnswers: Answer[] = Object.keys(answers).map((questionKey) => ({
questionId: questionKey,
answer: answers[questionKey],
}));
onComplete(survey.id, finalAnswers);
};

// ==== Estados de carga/error ====
if (loading) {
return (
<div className="text-center py-12">
Expand All @@ -139,84 +158,132 @@ export const SurveyForm: React.FC<SurveyFormProps> = ({ survey, onComplete, onCa
);
}

if (questions.length === 0) {
return (
<div className="text-center p-8 bg-yellow-100 border border-yellow-400 text-yellow-700 rounded">
<p className="font-bold">Información:</p>
<p>Esta encuesta no tiene preguntas asociadas aún.</p>
<button
onClick={onCancel}
className="mt-4 text-gray-600 hover:text-gray-900 transition-colors underline"
>
<ArrowLeft className="h-4 w-4 inline mr-1" /> Volver
</button>
</div>
);
}
const isLsasSurvey = questions.some((q) => q.type === 'lsas');

// ==== Render ====
return (
<div className="bg-white p-6 rounded-xl shadow-lg border-2 border-gray-200 min-w-0">
<h2 className="text-3xl font-extrabold text-gray-900 mb-2">{survey.title}</h2>
<p className="text-gray-600 mb-8">{survey.description}</p>
<p className="text-gray-600 mb-8">{survey.description || 'Cargando preguntas de la encuesta...'}</p>

{/* 🔶 Recuadro informativo solo para LSAS */}
{isLsasSurvey && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4 mb-6 text-gray-800">
<h3 className="font-bold text-lg mb-2">
Escala de Ansiedad Social de Liebowitz (LSAS)
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm leading-relaxed">
<div>
<p className="font-semibold">Miedo o ansiedad</p>
<p>0 = Nada de miedo o ansiedad</p>
<p>1 = Un poco de miedo o ansiedad</p>
<p>2 = Bastante miedo o ansiedad</p>
<p>3 = Mucho miedo o ansiedad</p>
</div>
<div>
<p className="font-semibold">Evitación</p>
<p>0 = Nunca lo evito (0%)</p>
<p>1 = En ocasiones lo evito (1–33%)</p>
<p>2 = Frecuentemente lo evito (33–67%)</p>
<p>3 = Habitualmente lo evito (67–100%)</p>
</div>
</div>
</div>
)}

<form onSubmit={handleSubmit} className="space-y-8 min-w-0">
{questions.map((q, index) => (
<div
key={q.id}
className="p-4 border-l-4 border-orange-500 bg-gray-50 rounded-r-lg shadow-sm min-w-0"
>
<label className="block text-lg font-semibold text-gray-800 mb-3">
{index + 1}. {q.text} {q.required && <span className="text-red-500">*</span>}
</label>

{q.type === 'multiple-choice' && q.options && q.options.length > 0 ? (
<div className="space-y-2 min-w-0">
{q.options.map((opt, i) => (
<label
key={`${q.id}-${i}`}
className="relative flex items-start gap-3 w-full min-w-0"
>
{questions.map((q, index) => {
const miedoKey = `${q.id}__miedo`;
const evitKey = `${q.id}__evitacion`;

return (
<div
key={q.id}
className="p-4 border-l-4 border-orange-500 bg-gray-50 rounded-r-lg shadow-sm min-w-0"
>
<label className="block text-lg font-semibold text-gray-800 mb-3">
{index + 1}. {q.text} {q.required && <span className="text-red-500">*</span>}
</label>

{q.type === 'multiple-choice' && q.options && q.options.length > 0 && (
<div className="space-y-2 min-w-0">
{q.options.map((opt, i) => (
<label
key={`${q.id}-${i}`}
className="relative flex items-start gap-3 w-full min-w-0"
>
<input
type="radio"
name={`q-${q.id}`}
value={opt.value}
checked={answers[q.id] === opt.value}
onChange={(e) => handleChange(q.id, e.target.value)}
className="h-4 w-4 text-orange-600 focus:ring-orange-500 border-gray-300 shrink-0 relative z-10"
required={q.required}
/>
<span
className="text-gray-800 flex-1 min-w-0 whitespace-pre-wrap break-words leading-normal relative z-10"
>
{opt.label}
</span>
</label>
))}
</div>
)}

{q.type === 'lsas' && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 min-w-0">
<div className="min-w-0">
<label className="block text-sm font-medium text-gray-700 mb-1">
Miedo/ansiedad (0–3)
</label>
<input
type="radio"
name={`q-${q.id}`}
value={opt.value}
checked={answers[q.id] === opt.value}
onChange={(e) => handleChange(q.id, e.target.value)}
className="h-4 w-4 text-orange-600 focus:ring-orange-500 border-gray-300 shrink-0 relative z-10"
type="number"
min={q.scaleMin ?? 0}
max={q.scaleMax ?? 3}
step={1}
value={answers[miedoKey] ?? ''}
onChange={(e) => {
const val = e.target.value === '' ? '' : Number(e.target.value);
handleChange(miedoKey, val);
}}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
required={q.required}
/>
</div>

{/* Texto de la opción */}
<span
className="
text-gray-800 flex-1 min-w-0
whitespace-pre-wrap break-words leading-normal
relative z-10
"
>
{opt.label}
</span>

{/* Capa de fondo para evitar overlays que tapen el texto */}
<span
aria-hidden
className="absolute inset-0 rounded bg-transparent z-0"
<div className="min-w-0">
<label className="block text-sm font-medium text-gray-700 mb-1">
Evitación (0–3)
</label>
<input
type="number"
min={q.scaleMin ?? 0}
max={q.scaleMax ?? 3}
step={1}
value={answers[evitKey] ?? ''}
onChange={(e) => {
const val = e.target.value === '' ? '' : Number(e.target.value);
handleChange(evitKey, val);
}}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
required={q.required}
/>
</label>
))}
</div>
) : (
<input
type="text"
value={answers[q.id] || ''}
onChange={(e) => handleChange(q.id, e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
required={q.required}
/>
)}
</div>
))}
</div>
</div>
)}

{q.type === 'text' && (
<input
type="text"
value={answers[q.id] || ''}
onChange={(e) => handleChange(q.id, e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
required={q.required}
/>
)}
</div>
);
})}

<div className="flex justify-between pt-4 border-t border-gray-200">
<button
Expand Down
2 changes: 1 addition & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface QuestionOption {
label: string;
value: number | string;
orden?: number;
group?: string; // para escalas con subcomponentes (e.g., lsas: miedo/evitacion)
group?: string; // para subescalas (e.g. LSAS: miedo/evitación)
}

export interface Question {
Expand Down