Cambios Grandes, editro nuevo de preguntas, logica nueva con mas opciones de pregutnas con preguntas hijos hasta 5 niveles
This commit is contained in:
302
frontend/src/QuestionAnswerInput.jsx
Normal file
302
frontend/src/QuestionAnswerInput.jsx
Normal 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
|
||||
Reference in New Issue
Block a user