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 }) {
-
+ />
@@ -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' && (
-