Cambios Grandes, editro nuevo de preguntas, logica nueva con mas opciones de pregutnas con preguntas hijos hasta 5 niveles

This commit is contained in:
2025-11-25 22:23:21 -03:00
parent 99f0952378
commit 1ef07ad2c5
8 changed files with 1771 additions and 135 deletions

View File

@@ -0,0 +1,302 @@
import React from 'react'
/**
* Renderizador Dinámico de Campos de Respuesta
* Renderiza el input apropiado según la configuración de la pregunta
*/
export function QuestionAnswerInput({ question, value, onChange, onSave }) {
const config = question.options || {}
const questionType = config.type || question.type
// BOOLEAN (2 opciones)
if (questionType === 'boolean' && config.choices?.length === 2) {
const [choice1, choice2] = config.choices
return (
<div className="flex gap-4">
<label className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50">
<input
type="radio"
value={choice1.value}
checked={value === choice1.value}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
className="mr-3"
/>
<span className={`font-medium ${choice1.status === 'ok' ? 'text-green-600' : 'text-red-600'}`}>
{choice1.status === 'ok' ? '✓' : '✗'} {choice1.label}
</span>
</label>
<label className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50">
<input
type="radio"
value={choice2.value}
checked={value === choice2.value}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
className="mr-3"
/>
<span className={`font-medium ${choice2.status === 'ok' ? 'text-green-600' : 'text-red-600'}`}>
{choice2.status === 'ok' ? '✓' : '✗'} {choice2.label}
</span>
</label>
</div>
)
}
// SINGLE CHOICE (selección única)
if (questionType === 'single_choice' && config.choices) {
return (
<div className="space-y-2">
{config.choices.map((choice, idx) => (
<label
key={idx}
className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50"
>
<input
type="radio"
value={choice.value}
checked={value === choice.value}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
className="mr-3"
/>
<span className="flex-1 font-medium">{choice.label}</span>
{choice.points > 0 && (
<span className="text-sm text-blue-600">+{choice.points} pts</span>
)}
</label>
))}
{config.allow_other && (
<div className="pl-7">
<label className="flex items-center">
<input
type="radio"
value="__other__"
checked={value && !config.choices.find(c => c.value === value)}
onChange={(e) => onChange('')}
className="mr-3"
/>
<span>Otro:</span>
<input
type="text"
value={value && !config.choices.find(c => c.value === value) ? value : ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
className="ml-2 flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Especificar..."
/>
</label>
</div>
)}
</div>
)
}
// MULTIPLE CHOICE (selección múltiple)
if (questionType === 'multiple_choice' && config.choices) {
const selectedValues = value ? (Array.isArray(value) ? value : value.split(',')) : []
const handleToggle = (choiceValue) => {
let newValues
if (selectedValues.includes(choiceValue)) {
newValues = selectedValues.filter(v => v !== choiceValue)
} else {
newValues = [...selectedValues, choiceValue]
}
onChange(newValues.join(','))
onSave?.()
}
return (
<div className="space-y-2">
{config.choices.map((choice, idx) => (
<label
key={idx}
className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50"
>
<input
type="checkbox"
checked={selectedValues.includes(choice.value)}
onChange={() => handleToggle(choice.value)}
className="mr-3 w-4 h-4"
/>
<span className="flex-1 font-medium">{choice.label}</span>
{choice.points > 0 && (
<span className="text-sm text-blue-600">+{choice.points} pts</span>
)}
</label>
))}
</div>
)
}
// SCALE (escala numérica)
if (questionType === 'scale') {
const min = config.min || 1
const max = config.max || 5
const step = config.step || 1
const labels = config.labels || {}
const options = []
for (let i = min; i <= max; i += step) {
options.push(i)
}
return (
<div className="space-y-3">
<div className="flex justify-between text-sm text-gray-600 mb-2">
{labels.min && <span>{labels.min}</span>}
{labels.max && <span>{labels.max}</span>}
</div>
<div className="flex gap-2 justify-center">
{options.map(num => (
<button
key={num}
type="button"
onClick={() => {
onChange(String(num))
onSave?.()
}}
className={`w-12 h-12 rounded-full font-bold transition ${
value === String(num)
? 'bg-blue-600 text-white scale-110'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{num}
</button>
))}
</div>
<div className="text-center">
{value && (
<div className="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 rounded-lg">
<span className="text-sm text-gray-600">Seleccionado:</span>
<span className="font-bold text-blue-600 text-lg">{value}</span>
<span className="text-sm text-gray-600">/ {max}</span>
</div>
)}
</div>
</div>
)
}
// TEXT (texto libre)
if (questionType === 'text') {
const multiline = config.multiline !== false
const maxLength = config.max_length || 500
if (multiline) {
return (
<div>
<textarea
value={value || ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
maxLength={maxLength}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
rows="4"
placeholder="Ingrese su respuesta..."
/>
<div className="text-xs text-gray-500 mt-1 text-right">
{(value?.length || 0)} / {maxLength} caracteres
</div>
</div>
)
} else {
return (
<input
type="text"
value={value || ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
maxLength={maxLength}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Ingrese su respuesta..."
/>
)
}
}
// NUMBER (valor numérico)
if (questionType === 'number') {
const min = config.min ?? 0
const max = config.max ?? 100
const unit = config.unit || ''
return (
<div className="flex items-center gap-2">
<input
type="number"
value={value || ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
min={min}
max={max}
step="any"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder={`${min} - ${max}`}
/>
{unit && <span className="text-gray-600 font-medium">{unit}</span>}
</div>
)
}
// DATE (fecha)
if (questionType === 'date') {
return (
<input
type="date"
value={value || ''}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
min={config.min_date}
max={config.max_date}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
)
}
// TIME (hora)
if (questionType === 'time') {
return (
<input
type="time"
value={value || ''}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
)
}
// Fallback para tipos desconocidos
return (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
Tipo de pregunta no reconocido: <code>{questionType}</code>
</p>
<input
type="text"
value={value || ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
className="mt-2 w-full px-3 py-2 border border-gray-300 rounded-lg"
placeholder="Respuesta de texto libre..."
/>
</div>
)
}
export default QuestionAnswerInput