Backend v1.0.71:
- Implementado soft delete para preguntas
- Nuevas columnas: is_deleted (boolean), updated_at (timestamp)
- Migración SQL: add_soft_delete_to_questions.sql
- Endpoint DELETE marca preguntas como eliminadas en lugar de borrarlas
- GET /api/checklists/{id} filtra preguntas eliminadas (is_deleted=false)
- Validación de subpreguntas activas antes de eliminar
- Índices agregados para optimizar queries
- Mantiene integridad de respuestas históricas y PDFs generados
- Permite limpiar checklists sin afectar inspecciones completadas
This commit is contained in:
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@@ -1,5 +1,6 @@
|
|||||||
# No olvides dar lo comentarios de los cambios que se hicieron para el backend y el front en para los comentarios de git
|
# No olvides dar lo comentarios de los cambios que se hicieron para el backend y el front en para los comentarios de git
|
||||||
# Siempre actuliza la version del front y del back la version del front esta en el archivo package.json y la del backend en el archivo main.py en una variable llamada BACKEND_VERSION
|
# Siempre actuliza la version del front y del back la version del front esta en el archivo package.json y la del backend en el archivo main.py en una variable llamada BACKEND_VERSION
|
||||||
|
# Si el FrontEnd no sufre modificaciones no es necesario actualizar su version, al igual que el backend, solo poner en el comentario de git que no se hicieron cambios en el front o en el backend segun sea el caso
|
||||||
|
|
||||||
# Ayudetec - Intelligent Checklist System for Automotive Workshops
|
# Ayudetec - Intelligent Checklist System for Automotive Workshops
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ def send_completed_inspection_to_n8n(inspection, db):
|
|||||||
# No lanzamos excepción para no interrumpir el flujo normal
|
# No lanzamos excepción para no interrumpir el flujo normal
|
||||||
|
|
||||||
|
|
||||||
BACKEND_VERSION = "1.0.69"
|
BACKEND_VERSION = "1.0.71"
|
||||||
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
||||||
|
|
||||||
# S3/MinIO configuration
|
# S3/MinIO configuration
|
||||||
@@ -780,13 +780,17 @@ def get_checklists(
|
|||||||
|
|
||||||
@app.get("/api/checklists/{checklist_id}", response_model=schemas.ChecklistWithQuestions)
|
@app.get("/api/checklists/{checklist_id}", response_model=schemas.ChecklistWithQuestions)
|
||||||
def get_checklist(checklist_id: int, db: Session = Depends(get_db)):
|
def get_checklist(checklist_id: int, db: Session = Depends(get_db)):
|
||||||
checklist = db.query(models.Checklist).options(
|
checklist = db.query(models.Checklist).filter(models.Checklist.id == checklist_id).first()
|
||||||
joinedload(models.Checklist.questions)
|
|
||||||
).filter(models.Checklist.id == checklist_id).first()
|
|
||||||
|
|
||||||
if not checklist:
|
if not checklist:
|
||||||
raise HTTPException(status_code=404, detail="Checklist no encontrado")
|
raise HTTPException(status_code=404, detail="Checklist no encontrado")
|
||||||
|
|
||||||
|
# Cargar solo preguntas NO eliminadas
|
||||||
|
checklist.questions = db.query(models.Question).filter(
|
||||||
|
models.Question.checklist_id == checklist_id,
|
||||||
|
models.Question.is_deleted == False
|
||||||
|
).order_by(models.Question.order).all()
|
||||||
|
|
||||||
# Agregar allowed_mechanics
|
# Agregar allowed_mechanics
|
||||||
permissions = db.query(models.ChecklistPermission.mechanic_id).filter(
|
permissions = db.query(models.ChecklistPermission.mechanic_id).filter(
|
||||||
models.ChecklistPermission.checklist_id == checklist.id
|
models.ChecklistPermission.checklist_id == checklist.id
|
||||||
@@ -1050,6 +1054,21 @@ def delete_question(
|
|||||||
if not db_question:
|
if not db_question:
|
||||||
raise HTTPException(status_code=404, detail="Pregunta no encontrada")
|
raise HTTPException(status_code=404, detail="Pregunta no encontrada")
|
||||||
|
|
||||||
|
if db_question.is_deleted:
|
||||||
|
raise HTTPException(status_code=400, detail="La pregunta ya está eliminada")
|
||||||
|
|
||||||
|
# Verificar si tiene subpreguntas activas
|
||||||
|
active_subquestions = db.query(models.Question).filter(
|
||||||
|
models.Question.parent_question_id == question_id,
|
||||||
|
models.Question.is_deleted == False
|
||||||
|
).count()
|
||||||
|
|
||||||
|
if active_subquestions > 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"No se puede eliminar la pregunta porque tiene {active_subquestions} subpregunta(s) activa(s). Elimina primero las subpreguntas."
|
||||||
|
)
|
||||||
|
|
||||||
# Registrar auditoría antes de eliminar
|
# Registrar auditoría antes de eliminar
|
||||||
audit_log = models.QuestionAuditLog(
|
audit_log = models.QuestionAuditLog(
|
||||||
question_id=question_id,
|
question_id=question_id,
|
||||||
@@ -1061,9 +1080,16 @@ def delete_question(
|
|||||||
)
|
)
|
||||||
db.add(audit_log)
|
db.add(audit_log)
|
||||||
|
|
||||||
db.delete(db_question)
|
# SOFT DELETE: marcar como eliminada en lugar de borrar físicamente
|
||||||
|
db_question.is_deleted = True
|
||||||
|
db_question.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Pregunta eliminada"}
|
|
||||||
|
return {
|
||||||
|
"message": "Pregunta eliminada exitosamente",
|
||||||
|
"note": "Las respuestas históricas se mantienen intactas. La pregunta no aparecerá en nuevas inspecciones."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/questions/{question_id}/audit", response_model=List[schemas.QuestionAuditLog])
|
@app.get("/api/questions/{question_id}/audit", response_model=List[schemas.QuestionAuditLog])
|
||||||
|
|||||||
@@ -83,7 +83,11 @@ class Question(Base):
|
|||||||
# AI Analysis
|
# AI Analysis
|
||||||
ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta
|
ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta
|
||||||
|
|
||||||
|
# Soft Delete
|
||||||
|
is_deleted = Column(Boolean, default=False) # Soft delete: mantiene integridad de respuestas históricas
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
checklist = relationship("Checklist", back_populates="questions")
|
checklist = relationship("Checklist", back_populates="questions")
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class QuestionBase(BaseModel):
|
|||||||
parent_question_id: Optional[int] = None
|
parent_question_id: Optional[int] = None
|
||||||
show_if_answer: Optional[str] = None
|
show_if_answer: Optional[str] = None
|
||||||
ai_prompt: Optional[str] = None
|
ai_prompt: Optional[str] = None
|
||||||
|
is_deleted: bool = False
|
||||||
|
|
||||||
class QuestionCreate(QuestionBase):
|
class QuestionCreate(QuestionBase):
|
||||||
checklist_id: int
|
checklist_id: int
|
||||||
@@ -137,6 +138,7 @@ class Question(QuestionBase):
|
|||||||
id: int
|
id: int
|
||||||
checklist_id: int
|
checklist_id: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.69",
|
"version": "1.0.70",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1050,8 +1050,6 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleEditQuestion = (question) => {
|
const handleEditQuestion = (question) => {
|
||||||
console.log('Editando pregunta:', question)
|
|
||||||
console.log('AI Prompt de la pregunta:', question.ai_prompt)
|
|
||||||
setEditingQuestion(question)
|
setEditingQuestion(question)
|
||||||
setShowCreateForm(false)
|
setShowCreateForm(false)
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -1152,12 +1150,20 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
loadQuestions()
|
loadQuestions()
|
||||||
|
alert('✅ Pregunta eliminada exitosamente')
|
||||||
} else {
|
} else {
|
||||||
alert('Error al eliminar pregunta')
|
const errorData = await response.json().catch(() => ({ detail: 'Error desconocido' }))
|
||||||
|
|
||||||
|
if (response.status === 400) {
|
||||||
|
// Error de validación (pregunta con respuestas o subpreguntas)
|
||||||
|
alert(`⚠️ ${errorData.detail}`)
|
||||||
|
} else {
|
||||||
|
alert('❌ Error al eliminar pregunta')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error)
|
console.error('Error:', error)
|
||||||
alert('Error al eliminar pregunta')
|
alert('❌ Error de conexión al eliminar pregunta')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
migrations/add_soft_delete_to_questions.sql
Normal file
25
migrations/add_soft_delete_to_questions.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- Migración: Agregar soft delete a preguntas
|
||||||
|
-- Fecha: 2025-11-27
|
||||||
|
-- Descripción: Permite eliminar preguntas sin romper la integridad de respuestas históricas
|
||||||
|
|
||||||
|
-- Agregar columna is_deleted a la tabla questions
|
||||||
|
ALTER TABLE questions ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Agregar columna updated_at si no existe
|
||||||
|
ALTER TABLE questions ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW();
|
||||||
|
|
||||||
|
-- Crear índice para mejorar queries que filtran por is_deleted
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_questions_is_deleted ON questions(is_deleted);
|
||||||
|
|
||||||
|
-- Crear índice compuesto para mejorar queries de preguntas activas por checklist
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_questions_checklist_active ON questions(checklist_id, is_deleted);
|
||||||
|
|
||||||
|
-- Actualizar preguntas existentes como no eliminadas
|
||||||
|
UPDATE questions SET is_deleted = FALSE WHERE is_deleted IS NULL;
|
||||||
|
|
||||||
|
-- Actualizar updated_at en preguntas existentes
|
||||||
|
UPDATE questions SET updated_at = created_at WHERE updated_at IS NULL;
|
||||||
|
|
||||||
|
-- Comentarios en las columnas
|
||||||
|
COMMENT ON COLUMN questions.is_deleted IS 'Soft delete: marca pregunta como eliminada sin borrarla físicamente, manteniendo integridad de respuestas históricas';
|
||||||
|
COMMENT ON COLUMN questions.updated_at IS 'Timestamp de última actualización de la pregunta';
|
||||||
Reference in New Issue
Block a user