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
319 lines
9.8 KiB
JavaScript
319 lines
9.8 KiB
JavaScript
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
|