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
|
||||
# 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
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ def send_completed_inspection_to_n8n(inspection, db):
|
||||
# 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)
|
||||
|
||||
# S3/MinIO configuration
|
||||
@@ -780,13 +780,17 @@ def get_checklists(
|
||||
|
||||
@app.get("/api/checklists/{checklist_id}", response_model=schemas.ChecklistWithQuestions)
|
||||
def get_checklist(checklist_id: int, db: Session = Depends(get_db)):
|
||||
checklist = db.query(models.Checklist).options(
|
||||
joinedload(models.Checklist.questions)
|
||||
).filter(models.Checklist.id == checklist_id).first()
|
||||
checklist = db.query(models.Checklist).filter(models.Checklist.id == checklist_id).first()
|
||||
|
||||
if not checklist:
|
||||
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
|
||||
permissions = db.query(models.ChecklistPermission.mechanic_id).filter(
|
||||
models.ChecklistPermission.checklist_id == checklist.id
|
||||
@@ -1050,6 +1054,21 @@ def delete_question(
|
||||
if not db_question:
|
||||
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
|
||||
audit_log = models.QuestionAuditLog(
|
||||
question_id=question_id,
|
||||
@@ -1061,9 +1080,16 @@ def delete_question(
|
||||
)
|
||||
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()
|
||||
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])
|
||||
|
||||
@@ -83,7 +83,11 @@ class Question(Base):
|
||||
# AI Analysis
|
||||
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())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
checklist = relationship("Checklist", back_populates="questions")
|
||||
|
||||
@@ -126,6 +126,7 @@ class QuestionBase(BaseModel):
|
||||
parent_question_id: Optional[int] = None
|
||||
show_if_answer: Optional[str] = None
|
||||
ai_prompt: Optional[str] = None
|
||||
is_deleted: bool = False
|
||||
|
||||
class QuestionCreate(QuestionBase):
|
||||
checklist_id: int
|
||||
@@ -137,6 +138,7 @@ class Question(QuestionBase):
|
||||
id: int
|
||||
checklist_id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "checklist-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.69",
|
||||
"version": "1.0.70",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1050,8 +1050,6 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
||||
}
|
||||
|
||||
const handleEditQuestion = (question) => {
|
||||
console.log('Editando pregunta:', question)
|
||||
console.log('AI Prompt de la pregunta:', question.ai_prompt)
|
||||
setEditingQuestion(question)
|
||||
setShowCreateForm(false)
|
||||
setFormData({
|
||||
@@ -1152,12 +1150,20 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
||||
|
||||
if (response.ok) {
|
||||
loadQuestions()
|
||||
alert('✅ Pregunta eliminada exitosamente')
|
||||
} 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) {
|
||||
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