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

@@ -2,6 +2,8 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-d
import { useState, useEffect, useRef } from 'react'
import SignatureCanvas from 'react-signature-canvas'
import Sidebar from './Sidebar'
import QuestionTypeEditor from './QuestionTypeEditor'
import QuestionAnswerInput from './QuestionAnswerInput'
function App() {
const [user, setUser] = useState(null)
@@ -903,8 +905,15 @@ function QuestionsManagerModal({ checklist, onClose }) {
const [formData, setFormData] = useState({
section: '',
text: '',
type: 'pass_fail',
type: 'boolean',
points: 1,
options: {
type: 'boolean',
choices: [
{ value: 'pass', label: 'Pasa', points: 1, status: 'ok' },
{ value: 'fail', label: 'Falla', points: 0, status: 'critical' }
]
},
allow_photos: true,
max_photos: 3,
requires_comment_on_fail: false,
@@ -962,8 +971,15 @@ function QuestionsManagerModal({ checklist, onClose }) {
setFormData({
section: '',
text: '',
type: 'pass_fail',
type: 'boolean',
points: 1,
options: {
type: 'boolean',
choices: [
{ value: 'pass', label: 'Pasa', points: 1, status: 'ok' },
{ value: 'fail', label: 'Falla', points: 0, status: 'critical' }
]
},
allow_photos: true,
max_photos: 3,
requires_comment_on_fail: false,
@@ -1067,19 +1083,15 @@ function QuestionsManagerModal({ checklist, onClose }) {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tipo de pregunta *
Puntos
</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
<input
type="number"
min="0"
value={formData.points}
onChange={(e) => setFormData({ ...formData, points: parseInt(e.target.value) || 1 })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
required
>
<option value="pass_fail">Pasa/Falla</option>
<option value="good_bad">Bueno/Malo</option>
<option value="text">Texto libre</option>
<option value="number">Número</option>
</select>
/>
</div>
</div>
@@ -1097,9 +1109,27 @@ function QuestionsManagerModal({ checklist, onClose }) {
/>
</div>
{/* Pregunta Condicional */}
{/* Configuración del Tipo de Pregunta */}
<div className="bg-white border-2 border-purple-300 rounded-lg p-4">
<h4 className="text-sm font-semibold text-purple-900 mb-3">📝 Configuración de la Pregunta</h4>
<QuestionTypeEditor
value={formData.options || null}
onChange={(config) => {
setFormData({
...formData,
type: config.type,
options: config
})
}}
maxPoints={formData.points}
/>
</div>
{/* Pregunta Condicional - Subpreguntas Anidadas hasta 5 niveles */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-sm font-semibold text-blue-900 mb-3"> Pregunta Condicional (opcional)</h4>
<h4 className="text-sm font-semibold text-blue-900 mb-3">
Pregunta Condicional - Subpreguntas Anidadas (hasta 5 niveles)
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
@@ -1109,22 +1139,33 @@ function QuestionsManagerModal({ checklist, onClose }) {
value={formData.parent_question_id || ''}
onChange={(e) => setFormData({
...formData,
parent_question_id: e.target.value ? parseInt(e.target.value) : null
parent_question_id: e.target.value ? parseInt(e.target.value) : null,
show_if_answer: '' // Reset al cambiar padre
})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 bg-white"
>
<option value="">Ninguna (pregunta principal)</option>
{questions
.filter(q => (q.type === 'pass_fail' || q.type === 'good_bad') && !q.parent_question_id)
.map(q => (
<option key={q.id} value={q.id}>
#{q.id} - {q.text.substring(0, 50)}{q.text.length > 50 ? '...' : ''}
</option>
))
.filter(q => {
// Permitir cualquier pregunta que no sea esta misma
// y que tenga depth_level < 5 (para no exceder límite)
const depth = q.depth_level || 0
return depth < 5
})
.map(q => {
const depth = q.depth_level || 0
const indent = ' '.repeat(depth)
const levelLabel = depth > 0 ? ` [Nivel ${depth}]` : ''
return (
<option key={q.id} value={q.id}>
{indent}#{q.id} - {q.text.substring(0, 40)}{q.text.length > 40 ? '...' : ''}{levelLabel}
</option>
)
})
}
</select>
<p className="text-xs text-gray-500 mt-1">
Esta pregunta aparecerá solo si se responde la pregunta padre
Esta pregunta aparecerá solo si se cumple la condición de la pregunta padre
</p>
</div>
<div>
@@ -1140,25 +1181,58 @@ function QuestionsManagerModal({ checklist, onClose }) {
<option value="">Seleccione...</option>
{formData.parent_question_id && (() => {
const parentQ = questions.find(q => q.id === formData.parent_question_id)
if (parentQ?.type === 'pass_fail') {
if (!parentQ) return null
// Leer opciones del nuevo formato
const config = parentQ.options || {}
const parentType = config.type || parentQ.type
// Para boolean o single_choice, mostrar las opciones configuradas
if ((parentType === 'boolean' || parentType === 'single_choice') && config.choices) {
return config.choices.map((choice, idx) => (
<option key={idx} value={choice.value}>
{choice.label}
</option>
))
}
// Compatibilidad con tipos antiguos
if (parentType === 'pass_fail') {
return [
<option key="pass" value="pass"> Pasa</option>,
<option key="fail" value="fail"> Falla</option>
]
} else if (parentQ?.type === 'good_bad') {
} else if (parentType === 'good_bad') {
return [
<option key="good" value="good"> Bueno</option>,
<option key="bad" value="bad"> Malo</option>
]
}
return null
return <option disabled>Tipo de pregunta no compatible</option>
})()}
</select>
<p className="text-xs text-gray-500 mt-1">
La pregunta solo se mostrará con esta respuesta
La pregunta solo se mostrará con esta respuesta específica
</p>
</div>
</div>
{/* Indicador de profundidad */}
{formData.parent_question_id && (() => {
const parentQ = questions.find(q => q.id === formData.parent_question_id)
const parentDepth = parentQ?.depth_level || 0
const newDepth = parentDepth + 1
return (
<div className={`mt-3 p-2 rounded ${newDepth >= 5 ? 'bg-red-50 border border-red-200' : 'bg-blue-100'}`}>
<p className="text-xs">
📊 <strong>Profundidad:</strong> Nivel {newDepth} de 5 máximo
{newDepth >= 5 && ' ⚠️ Máximo alcanzado'}
</p>
</div>
)
})()}
</div>
{/* AI Prompt - Solo visible si el checklist tiene IA habilitada */}
@@ -2765,11 +2839,22 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
const token = localStorage.getItem('token')
const API_URL = import.meta.env.VITE_API_URL || ''
// Determine status based on answer value
// Determine status based on answer value and question config
let status = 'ok'
if (question.type === 'pass_fail') {
const config = question.options || {}
const questionType = config.type || question.type
if (questionType === 'boolean' && config.choices) {
const selectedChoice = config.choices.find(c => c.value === answer.value)
status = selectedChoice?.status || 'ok'
} else if (questionType === 'single_choice' && config.choices) {
const selectedChoice = config.choices.find(c => c.value === answer.value)
status = selectedChoice?.status || 'ok'
} else if (questionType === 'pass_fail') {
// Compatibilidad hacia atrás
status = answer.value === 'pass' ? 'ok' : 'critical'
} else if (question.type === 'good_bad') {
} else if (questionType === 'good_bad') {
// Compatibilidad hacia atrás
if (answer.value === 'good') status = 'ok'
else if (answer.value === 'regular') status = 'warning'
else if (answer.value === 'bad') status = 'critical'
@@ -3360,107 +3445,17 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
Respuesta *
</label>
{currentQuestion.type === 'pass_fail' && (
<div className="flex gap-4">
<label className="flex items-center">
<input
type="radio"
value="pass"
checked={answers[currentQuestion.id]?.value === 'pass'}
onChange={(e) => {
const newValue = e.target.value
setAnswers(prev => ({
...prev,
[currentQuestion.id]: { ...prev[currentQuestion.id], value: newValue }
}))
setTimeout(() => saveAnswer(currentQuestion.id), 500)
}}
className="mr-2"
/>
<span className="text-green-600"> Pasa</span>
</label>
<label className="flex items-center">
<input
type="radio"
value="fail"
checked={answers[currentQuestion.id]?.value === 'fail'}
onChange={(e) => {
const newValue = e.target.value
setAnswers(prev => ({
...prev,
[currentQuestion.id]: { ...prev[currentQuestion.id], value: newValue }
}))
setTimeout(() => saveAnswer(currentQuestion.id), 500)
}}
className="mr-2"
/>
<span className="text-red-600"> Falla</span>
</label>
</div>
)}
{currentQuestion.type === 'good_bad' && (
<select
value={answers[currentQuestion.id]?.value}
onChange={(e) => {
const newValue = e.target.value
setAnswers(prev => ({
...prev,
[currentQuestion.id]: { ...prev[currentQuestion.id], value: newValue }
}))
setTimeout(() => saveAnswer(currentQuestion.id), 500)
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">Seleccionar...</option>
<option value="good">Bueno</option>
<option value="regular">Regular</option>
<option value="bad">Malo</option>
</select>
)}
{currentQuestion.type === 'numeric' && (
<input
type="number"
step="0.01"
value={answers[currentQuestion.id]?.value}
onChange={(e) => setAnswers(prev => ({
<QuestionAnswerInput
question={currentQuestion}
value={answers[currentQuestion.id]?.value}
onChange={(newValue) => {
setAnswers(prev => ({
...prev,
[currentQuestion.id]: { ...prev[currentQuestion.id], value: e.target.value }
}))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Ingrese un valor numérico"
/>
)}
{currentQuestion.type === 'status' && (
<select
value={answers[currentQuestion.id]?.value}
onChange={(e) => setAnswers(prev => ({
...prev,
[currentQuestion.id]: { ...prev[currentQuestion.id], value: e.target.value }
}))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">Seleccionar...</option>
<option value="ok">OK</option>
<option value="warning">Advertencia</option>
<option value="critical">Crítico</option>
</select>
)}
{currentQuestion.type === 'text' && (
<textarea
value={answers[currentQuestion.id]?.value}
onChange={(e) => setAnswers(prev => ({
...prev,
[currentQuestion.id]: { ...prev[currentQuestion.id], value: e.target.value }
}))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
rows="3"
placeholder="Ingrese su respuesta"
/>
)}
[currentQuestion.id]: { ...prev[currentQuestion.id], value: newValue }
}))
}}
onSave={() => setTimeout(() => saveAnswer(currentQuestion.id), 500)}
/>
</div>
{/* Observations */}

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

View File

@@ -0,0 +1,530 @@
import React, { useState, useEffect } from 'react'
/**
* Editor de Tipos de Preguntas Configurables (estilo Google Forms)
*
* Tipos soportados:
* - boolean: Dos opciones personalizables
* - single_choice: Selección única con N opciones
* - multiple_choice: Selección múltiple
* - scale: Escala numérica
* - text: Texto libre
* - number: Valor numérico
* - date: Fecha
* - time: Hora
*/
const QUESTION_TYPES = [
{ value: 'boolean', label: '✓✗ Booleana (2 opciones)', icon: '🔘' },
{ value: 'single_choice', label: '◎ Selección Única', icon: '⚪' },
{ value: 'multiple_choice', label: '☑ Selección Múltiple', icon: '✅' },
{ value: 'scale', label: '⭐ Escala Numérica', icon: '📊' },
{ value: 'text', label: '📝 Texto Libre', icon: '✏️' },
{ value: 'number', label: '🔢 Número', icon: '#️⃣' },
{ value: 'date', label: '📅 Fecha', icon: '📆' },
{ value: 'time', label: '🕐 Hora', icon: '⏰' }
]
const STATUS_OPTIONS = [
{ value: 'ok', label: 'OK (Verde)', color: 'green' },
{ value: 'warning', label: 'Advertencia (Amarillo)', color: 'yellow' },
{ value: 'critical', label: 'Crítico (Rojo)', color: 'red' },
{ value: 'info', label: 'Informativo (Azul)', color: 'blue' }
]
// Plantillas predefinidas para tipos booleanos
const BOOLEAN_TEMPLATES = [
{
name: 'Pasa/Falla',
choices: [
{ value: 'pass', label: 'Pasa', points: 1, status: 'ok' },
{ value: 'fail', label: 'Falla', points: 0, status: 'critical' }
]
},
{
name: 'Sí/No',
choices: [
{ value: 'yes', label: 'Sí', points: 1, status: 'ok' },
{ value: 'no', label: 'No', points: 0, status: 'critical' }
]
},
{
name: 'Bueno/Malo',
choices: [
{ value: 'good', label: 'Bueno', points: 1, status: 'ok' },
{ value: 'bad', label: 'Malo', points: 0, status: 'critical' }
]
},
{
name: 'Aprobado/Rechazado',
choices: [
{ value: 'approved', label: 'Aprobado', points: 1, status: 'ok' },
{ value: 'rejected', label: 'Rechazado', points: 0, status: 'critical' }
]
},
{
name: 'Funciona/No Funciona',
choices: [
{ value: 'works', label: 'Funciona', points: 1, status: 'ok' },
{ value: 'not_works', label: 'No Funciona', points: 0, status: 'critical' }
]
},
{
name: 'Personalizado',
choices: [
{ value: 'option1', label: 'Opción 1', points: 1, status: 'ok' },
{ value: 'option2', label: 'Opción 2', points: 0, status: 'critical' }
]
}
]
export function QuestionTypeEditor({ value, onChange, maxPoints = 1 }) {
const [config, setConfig] = useState(value || {
type: 'boolean',
choices: BOOLEAN_TEMPLATES[0].choices
})
useEffect(() => {
if (value && value.type) {
setConfig(value)
}
}, [value])
const handleTypeChange = (newType) => {
let newConfig = { type: newType }
// Inicializar con valores por defecto según el tipo
switch (newType) {
case 'boolean':
newConfig.choices = [...BOOLEAN_TEMPLATES[0].choices]
break
case 'single_choice':
case 'multiple_choice':
newConfig.choices = [
{ value: 'option1', label: 'Opción 1', points: maxPoints },
{ value: 'option2', label: 'Opción 2', points: Math.floor(maxPoints / 2) },
{ value: 'option3', label: 'Opción 3', points: 0 }
]
newConfig.allow_other = false
break
case 'scale':
newConfig.min = 1
newConfig.max = 5
newConfig.step = 1
newConfig.labels = { min: 'Muy malo', max: 'Excelente' }
newConfig.points_per_level = maxPoints / 5
break
case 'text':
newConfig.multiline = true
newConfig.max_length = 500
break
case 'number':
newConfig.min = 0
newConfig.max = 100
newConfig.unit = ''
break
case 'date':
newConfig.min_date = null
newConfig.max_date = null
break
case 'time':
newConfig.format = '24h'
break
}
setConfig(newConfig)
onChange(newConfig)
}
const updateConfig = (updates) => {
const newConfig = { ...config, ...updates }
setConfig(newConfig)
onChange(newConfig)
}
const updateChoice = (index, field, value) => {
const newChoices = [...config.choices]
newChoices[index] = { ...newChoices[index], [field]: value }
updateConfig({ choices: newChoices })
}
const addChoice = () => {
const newChoices = [...config.choices, {
value: `option${config.choices.length + 1}`,
label: `Opción ${config.choices.length + 1}`,
points: 0
}]
updateConfig({ choices: newChoices })
}
const removeChoice = (index) => {
if (config.type === 'boolean' && config.choices.length <= 2) {
alert('Las preguntas booleanas deben tener exactamente 2 opciones')
return
}
const newChoices = config.choices.filter((_, i) => i !== index)
updateConfig({ choices: newChoices })
}
const applyBooleanTemplate = (template) => {
updateConfig({ choices: [...template.choices] })
}
return (
<div className="space-y-4">
{/* Selector de Tipo de Pregunta */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipo de Pregunta
</label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{QUESTION_TYPES.map(type => (
<button
key={type.value}
type="button"
onClick={() => handleTypeChange(type.value)}
className={`p-3 border-2 rounded-lg text-left transition ${
config.type === type.value
? 'border-purple-600 bg-purple-50'
: 'border-gray-300 hover:border-purple-300'
}`}
>
<div className="text-2xl mb-1">{type.icon}</div>
<div className="text-xs font-medium">{type.label}</div>
</button>
))}
</div>
</div>
{/* Configuración específica según tipo */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
{/* BOOLEAN */}
{config.type === 'boolean' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Plantilla Predefinida
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{BOOLEAN_TEMPLATES.map((template, idx) => (
<button
key={idx}
type="button"
onClick={() => applyBooleanTemplate(template)}
className="px-3 py-2 border border-gray-300 rounded-lg hover:border-purple-500 hover:bg-purple-50 text-sm transition"
>
{template.name}
</button>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{config.choices?.map((choice, idx) => (
<div key={idx} className="bg-white border border-gray-300 rounded-lg p-3">
<label className="block text-xs text-gray-600 mb-1">
Opción {idx + 1}
</label>
<input
type="text"
value={choice.label}
onChange={(e) => updateChoice(idx, 'label', e.target.value)}
className="w-full px-2 py-1 border border-gray-300 rounded mb-2 text-sm"
placeholder="Texto de la opción"
/>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-xs text-gray-500 mb-1">Puntos</label>
<input
type="number"
value={choice.points}
onChange={(e) => updateChoice(idx, 'points', parseInt(e.target.value) || 0)}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
min="0"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Estado</label>
<select
value={choice.status || 'info'}
onChange={(e) => updateChoice(idx, 'status', e.target.value)}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
>
{STATUS_OPTIONS.map(s => (
<option key={s.value} value={s.value}>{s.label}</option>
))}
</select>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* SINGLE CHOICE / MULTIPLE CHOICE */}
{(config.type === 'single_choice' || config.type === 'multiple_choice') && (
<div className="space-y-3">
<div className="flex justify-between items-center">
<label className="text-sm font-medium text-gray-700">
Opciones ({config.choices?.length || 0})
</label>
<button
type="button"
onClick={addChoice}
className="px-3 py-1 bg-purple-600 text-white rounded text-sm hover:bg-purple-700"
>
+ Agregar Opción
</button>
</div>
<div className="space-y-2">
{config.choices?.map((choice, idx) => (
<div key={idx} className="flex gap-2 items-start bg-white border border-gray-300 rounded-lg p-2">
<div className="flex-shrink-0 mt-2">
{config.type === 'single_choice' ? '⚪' : '☑️'}
</div>
<div className="flex-1 grid grid-cols-3 gap-2">
<div className="col-span-2">
<input
type="text"
value={choice.label}
onChange={(e) => updateChoice(idx, 'label', e.target.value)}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
placeholder={`Opción ${idx + 1}`}
/>
</div>
<div>
<input
type="number"
value={choice.points}
onChange={(e) => updateChoice(idx, 'points', parseInt(e.target.value) || 0)}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
placeholder="Puntos"
min="0"
/>
</div>
</div>
<button
type="button"
onClick={() => removeChoice(idx)}
className="flex-shrink-0 px-2 py-1 text-red-600 hover:bg-red-50 rounded"
>
🗑
</button>
</div>
))}
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="allow_other"
checked={config.allow_other || false}
onChange={(e) => updateConfig({ allow_other: e.target.checked })}
className="rounded border-gray-300"
/>
<label htmlFor="allow_other" className="text-sm text-gray-700">
Permitir opción "Otro" con texto libre
</label>
</div>
</div>
)}
{/* SCALE */}
{config.type === 'scale' && (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Mínimo</label>
<input
type="number"
value={config.min || 1}
onChange={(e) => updateConfig({ min: parseInt(e.target.value) || 1 })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Máximo</label>
<input
type="number"
value={config.max || 5}
onChange={(e) => updateConfig({ max: parseInt(e.target.value) || 5 })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Incremento</label>
<input
type="number"
value={config.step || 1}
onChange={(e) => updateConfig({ step: parseInt(e.target.value) || 1 })}
className="w-full px-2 py-1 border border-gray-300 rounded"
min="1"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Etiqueta Mínimo</label>
<input
type="text"
value={config.labels?.min || ''}
onChange={(e) => updateConfig({
labels: { ...config.labels, min: e.target.value }
})}
className="w-full px-2 py-1 border border-gray-300 rounded"
placeholder="Ej: Muy malo"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Etiqueta Máximo</label>
<input
type="text"
value={config.labels?.max || ''}
onChange={(e) => updateConfig({
labels: { ...config.labels, max: e.target.value }
})}
className="w-full px-2 py-1 border border-gray-300 rounded"
placeholder="Ej: Excelente"
/>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded p-3">
<p className="text-xs text-blue-800">
Vista previa: {config.min} {config.labels?.min} {config.max} {config.labels?.max}
</p>
</div>
</div>
)}
{/* TEXT */}
{config.type === 'text' && (
<div className="space-y-3">
<div className="flex items-center gap-4">
<input
type="checkbox"
id="multiline"
checked={config.multiline !== false}
onChange={(e) => updateConfig({ multiline: e.target.checked })}
className="rounded border-gray-300"
/>
<label htmlFor="multiline" className="text-sm text-gray-700">
Permitir múltiples líneas
</label>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">
Longitud máxima (caracteres)
</label>
<input
type="number"
value={config.max_length || 500}
onChange={(e) => updateConfig({ max_length: parseInt(e.target.value) || 500 })}
className="w-full px-2 py-1 border border-gray-300 rounded"
min="1"
max="5000"
/>
</div>
</div>
)}
{/* NUMBER */}
{config.type === 'number' && (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Mínimo</label>
<input
type="number"
value={config.min ?? 0}
onChange={(e) => updateConfig({ min: parseFloat(e.target.value) })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Máximo</label>
<input
type="number"
value={config.max ?? 100}
onChange={(e) => updateConfig({ max: parseFloat(e.target.value) })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Unidad</label>
<input
type="text"
value={config.unit || ''}
onChange={(e) => updateConfig({ unit: e.target.value })}
className="w-full px-2 py-1 border border-gray-300 rounded"
placeholder="Ej: km, kg, °C"
/>
</div>
</div>
</div>
)}
{/* DATE */}
{config.type === 'date' && (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Fecha mínima (opcional)</label>
<input
type="date"
value={config.min_date || ''}
onChange={(e) => updateConfig({ min_date: e.target.value })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Fecha máxima (opcional)</label>
<input
type="date"
value={config.max_date || ''}
onChange={(e) => updateConfig({ max_date: e.target.value })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
</div>
</div>
)}
{/* TIME */}
{config.type === 'time' && (
<div>
<label className="block text-sm text-gray-700 mb-2">Formato</label>
<div className="flex gap-4">
<label className="flex items-center gap-2">
<input
type="radio"
value="12h"
checked={config.format === '12h'}
onChange={(e) => updateConfig({ format: e.target.value })}
className="border-gray-300"
/>
<span className="text-sm">12 horas (AM/PM)</span>
</label>
<label className="flex items-center gap-2">
<input
type="radio"
value="24h"
checked={config.format !== '12h'}
onChange={(e) => updateConfig({ format: e.target.value })}
className="border-gray-300"
/>
<span className="text-sm">24 horas</span>
</label>
</div>
</div>
)}
</div>
</div>
)
}
export default QuestionTypeEditor