From 1ef07ad2c55e00ae43447d481d1a7070c79918da Mon Sep 17 00:00:00 2001 From: ronalds Date: Tue, 25 Nov 2025 22:23:21 -0300 Subject: [PATCH] Cambios Grandes, editro nuevo de preguntas, logica nueva con mas opciones de pregutnas con preguntas hijos hasta 5 niveles --- backend/app/models.py | 7 +- backend/app/schemas.py | 19 +- frontend/src/App.jsx | 255 +++++----- frontend/src/QuestionAnswerInput.jsx | 302 ++++++++++++ frontend/src/QuestionTypeEditor.jsx | 530 +++++++++++++++++++++ migrations/add_flexible_question_types.sql | 153 ++++++ migrations/add_nested_subquestions.sql | 172 +++++++ migrations/migrate_question_types.py | 468 ++++++++++++++++++ 8 files changed, 1771 insertions(+), 135 deletions(-) create mode 100644 frontend/src/QuestionAnswerInput.jsx create mode 100644 frontend/src/QuestionTypeEditor.jsx create mode 100644 migrations/add_flexible_question_types.sql create mode 100644 migrations/add_nested_subquestions.sql create mode 100644 migrations/migrate_question_types.py diff --git a/backend/app/models.py b/backend/app/models.py index a88659d..6379462 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -65,18 +65,19 @@ class Question(Base): checklist_id = Column(Integer, ForeignKey("checklists.id"), nullable=False) section = Column(String(100)) # Sistema eléctrico, Frenos, etc text = Column(Text, nullable=False) - type = Column(String(30), nullable=False) # pass_fail, good_bad, text, etc + type = Column(String(30), nullable=False) # boolean, single_choice, multiple_choice, scale, text, number, date, time points = Column(Integer, default=1) - options = Column(JSON) # Para multiple choice + options = Column(JSON) # Configuración flexible según tipo de pregunta order = Column(Integer, default=0) allow_photos = Column(Boolean, default=True) max_photos = Column(Integer, default=3) requires_comment_on_fail = Column(Boolean, default=False) send_notification = Column(Boolean, default=False) - # Conditional logic + # Conditional logic - Subpreguntas anidadas hasta 5 niveles parent_question_id = Column(Integer, ForeignKey("questions.id"), nullable=True) show_if_answer = Column(String(50), nullable=True) # Valor que dispara esta pregunta + depth_level = Column(Integer, default=0) # 0=principal, 1-5=subpreguntas anidadas # AI Analysis ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 5ba49b2..24a44cb 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -94,12 +94,27 @@ class Checklist(ChecklistBase): # Question Schemas +# Tipos de preguntas soportados: +# - boolean: Dos opciones personalizables (ej: Sí/No, Pasa/Falla) +# - single_choice: Selección única con N opciones +# - multiple_choice: Selección múltiple +# - scale: Escala numérica (1-5, 1-10, etc.) +# - text: Texto libre +# - number: Valor numérico +# - date: Fecha +# - time: Hora + class QuestionBase(BaseModel): section: Optional[str] = None text: str - type: str + type: str # boolean, single_choice, multiple_choice, scale, text, number, date, time points: int = 1 - options: Optional[dict] = None + options: Optional[dict] = None # Configuración flexible según tipo + # Estructura de options: + # Boolean: {"type": "boolean", "choices": [{"value": "yes", "label": "Sí", "points": 1, "status": "ok"}, ...]} + # Single/Multiple Choice: {"type": "single_choice", "choices": [{"value": "opt1", "label": "Opción 1", "points": 2}, ...]} + # Scale: {"type": "scale", "min": 1, "max": 5, "step": 1, "labels": {"min": "Muy malo", "max": "Excelente"}} + # Text: {"type": "text", "multiline": true, "max_length": 500} order: int = 0 allow_photos: bool = True max_photos: int = 3 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index eddf82c..33cf08c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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 }) {
- 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 - > - - - - - + />
@@ -1097,9 +1109,27 @@ function QuestionsManagerModal({ checklist, onClose }) { /> - {/* Pregunta Condicional */} + {/* Configuración del Tipo de Pregunta */} +
+

📝 Configuración de la Pregunta

+ { + setFormData({ + ...formData, + type: config.type, + options: config + }) + }} + maxPoints={formData.points} + /> +
+ + {/* Pregunta Condicional - Subpreguntas Anidadas hasta 5 niveles */}
-

⚡ Pregunta Condicional (opcional)

+

+ ⚡ Pregunta Condicional - Subpreguntas Anidadas (hasta 5 niveles) +

@@ -1140,25 +1181,58 @@ function QuestionsManagerModal({ checklist, onClose }) { {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) => ( + + )) + } + + // Compatibilidad con tipos antiguos + if (parentType === 'pass_fail') { return [ , ] - } else if (parentQ?.type === 'good_bad') { + } else if (parentType === 'good_bad') { return [ , ] } - return null + + return })()}

- La pregunta solo se mostrará con esta respuesta + La pregunta solo se mostrará con esta respuesta específica

+ + {/* 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 ( +
= 5 ? 'bg-red-50 border border-red-200' : 'bg-blue-100'}`}> +

+ 📊 Profundidad: Nivel {newDepth} de 5 máximo + {newDepth >= 5 && ' ⚠️ Máximo alcanzado'} +

+
+ ) + })()}
{/* 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 * - {currentQuestion.type === 'pass_fail' && ( -
- - -
- )} - - {currentQuestion.type === 'good_bad' && ( - - )} - - {currentQuestion.type === 'numeric' && ( - setAnswers(prev => ({ + { + 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' && ( - - )} - - {currentQuestion.type === 'text' && ( -