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 */}