Files
checklist/frontend/src/QuestionAnswerInput.jsx
ronalds a0927131c4 Actualizada la versión del frontend de 1.3.9 a 1.4.0 ✓
Cambios en esta versión:

Botón "Verificación de Incidencia" (renombrado desde "Consultar Asistente")
Botón "Finalizar Verificación" que aparece después del primer mensaje del asistente
El chat permanece abierto permitiendo continuar la conversación
Eliminado texto informativo innecesario del tipo de pregunta ai_assistant
2025-12-09 01:23:05 -03:00

319 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
/>
)
}
// PHOTO_ONLY (solo foto, sin campo de respuesta)
if (questionType === 'photo_only') {
return (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800">
📸 Esta pregunta solo requiere fotografías. Adjunta las imágenes en la sección de fotos abajo.
</p>
</div>
)
}
// AI_ASSISTANT (Chat con asistente) - No requiere UI aquí, el botón está en App.jsx
if (questionType === 'ai_assistant') {
return null
}
// 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