diff --git a/backend/app/main.py b/backend/app/main.py index 5fcda03..a56aec1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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.28" +BACKEND_VERSION = "1.0.63" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) # S3/MinIO configuration @@ -966,6 +966,19 @@ def create_question( db.commit() db.refresh(db_question) + + # Registrar auditoría + audit_log = models.QuestionAuditLog( + question_id=db_question.id, + checklist_id=question.checklist_id, + user_id=current_user.id, + action="created", + new_value=f"Pregunta creada: {question.text}", + comment=f"Sección: {question.section}, Tipo: {question.type}, Puntos: {question.points}" + ) + db.add(audit_log) + db.commit() + return db_question @@ -983,11 +996,44 @@ def update_question( if not db_question: raise HTTPException(status_code=404, detail="Pregunta no encontrada") + # Guardar valores anteriores para auditoría + import json + changes = [] + for key, value in question.dict(exclude_unset=True).items(): - setattr(db_question, key, value) + old_value = getattr(db_question, key) + if old_value != value: + # Convertir a string para comparación y almacenamiento + old_str = json.dumps(old_value, ensure_ascii=False) if isinstance(old_value, (dict, list)) else str(old_value) + new_str = json.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value) + + changes.append({ + 'field': key, + 'old': old_str, + 'new': new_str + }) + setattr(db_question, key, value) db.commit() db.refresh(db_question) + + # Registrar auditoría para cada campo cambiado + for change in changes: + audit_log = models.QuestionAuditLog( + question_id=question_id, + checklist_id=db_question.checklist_id, + user_id=current_user.id, + action="updated", + field_name=change['field'], + old_value=change['old'], + new_value=change['new'], + comment=f"Campo '{change['field']}' modificado" + ) + db.add(audit_log) + + if changes: + db.commit() + return db_question @@ -1004,11 +1050,56 @@ def delete_question( if not db_question: raise HTTPException(status_code=404, detail="Pregunta no encontrada") + # Registrar auditoría antes de eliminar + audit_log = models.QuestionAuditLog( + question_id=question_id, + checklist_id=db_question.checklist_id, + user_id=current_user.id, + action="deleted", + old_value=f"Pregunta eliminada: {db_question.text}", + comment=f"Sección: {db_question.section}, Tipo: {db_question.type}, Puntos: {db_question.points}" + ) + db.add(audit_log) + db.delete(db_question) db.commit() return {"message": "Pregunta eliminada"} +@app.get("/api/questions/{question_id}/audit", response_model=List[schemas.QuestionAuditLog]) +def get_question_audit_history( + question_id: int, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + """Obtener historial de cambios de una pregunta""" + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="Solo administradores pueden ver el historial") + + audit_logs = db.query(models.QuestionAuditLog).filter( + models.QuestionAuditLog.question_id == question_id + ).order_by(models.QuestionAuditLog.created_at.desc()).all() + + return audit_logs + + +@app.get("/api/checklists/{checklist_id}/questions/audit", response_model=List[schemas.QuestionAuditLog]) +def get_checklist_questions_audit_history( + checklist_id: int, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + """Obtener historial de cambios de todas las preguntas de un checklist""" + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="Solo administradores pueden ver el historial") + + audit_logs = db.query(models.QuestionAuditLog).filter( + models.QuestionAuditLog.checklist_id == checklist_id + ).order_by(models.QuestionAuditLog.created_at.desc()).all() + + return audit_logs + + # ============= INSPECTION ENDPOINTS ============= @app.get("/api/inspections", response_model=List[schemas.Inspection]) def get_inspections( diff --git a/backend/app/models.py b/backend/app/models.py index 8072c79..30f7f38 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -208,6 +208,27 @@ class ChecklistPermission(Base): mechanic = relationship("User") +class QuestionAuditLog(Base): + """Registro de auditoría para cambios en preguntas de checklists""" + __tablename__ = "question_audit_log" + + id = Column(Integer, primary_key=True, index=True) + question_id = Column(Integer, ForeignKey("questions.id", ondelete="CASCADE"), nullable=False) + checklist_id = Column(Integer, ForeignKey("checklists.id", ondelete="CASCADE"), nullable=False) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + action = Column(String(50), nullable=False) # created, updated, deleted + field_name = Column(String(100), nullable=True) # Campo modificado + old_value = Column(Text, nullable=True) # Valor anterior + new_value = Column(Text, nullable=True) # Valor nuevo + comment = Column(Text, nullable=True) # Comentario del cambio + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + # Relationships + question = relationship("Question") + checklist = relationship("Checklist") + user = relationship("User") + + class InspectionAuditLog(Base): """Registro de auditoría para cambios en inspecciones y respuestas""" __tablename__ = "inspection_audit_log" diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 12d5a42..f3e20d8 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -142,6 +142,23 @@ class Question(QuestionBase): from_attributes = True +# Question Audit Schemas +class QuestionAuditLog(BaseModel): + id: int + question_id: int + checklist_id: int + user_id: int + action: str + field_name: Optional[str] = None + old_value: Optional[str] = None + new_value: Optional[str] = None + comment: Optional[str] = None + created_at: datetime + user: Optional['User'] = None + + class Config: + from_attributes = True + # Inspection Schemas class InspectionBase(BaseModel): diff --git a/backend/migrations/add_question_audit_log.sql b/backend/migrations/add_question_audit_log.sql new file mode 100644 index 0000000..6a319e0 --- /dev/null +++ b/backend/migrations/add_question_audit_log.sql @@ -0,0 +1,44 @@ +-- Migration: Add question_audit_log table +-- Date: 2025-11-27 +-- Description: Add audit logging for question changes + +CREATE TABLE IF NOT EXISTS question_audit_log ( + id SERIAL PRIMARY KEY, + question_id INTEGER NOT NULL, + checklist_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + action VARCHAR(50) NOT NULL, + field_name VARCHAR(100), + old_value TEXT, + new_value TEXT, + comment TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + -- Foreign keys + CONSTRAINT fk_question_audit_question + FOREIGN KEY (question_id) + REFERENCES questions(id) + ON DELETE CASCADE, + + CONSTRAINT fk_question_audit_checklist + FOREIGN KEY (checklist_id) + REFERENCES checklists(id) + ON DELETE CASCADE, + + CONSTRAINT fk_question_audit_user + FOREIGN KEY (user_id) + REFERENCES users(id) +); + +-- Create indexes for better query performance +CREATE INDEX idx_question_audit_question_id ON question_audit_log(question_id); +CREATE INDEX idx_question_audit_checklist_id ON question_audit_log(checklist_id); +CREATE INDEX idx_question_audit_created_at ON question_audit_log(created_at); +CREATE INDEX idx_question_audit_action ON question_audit_log(action); + +-- Add comment to table +COMMENT ON TABLE question_audit_log IS 'Registro de auditoría para cambios en preguntas de checklists'; +COMMENT ON COLUMN question_audit_log.action IS 'Tipo de acción: created, updated, deleted'; +COMMENT ON COLUMN question_audit_log.field_name IS 'Nombre del campo modificado (solo para updates)'; +COMMENT ON COLUMN question_audit_log.old_value IS 'Valor anterior del campo'; +COMMENT ON COLUMN question_audit_log.new_value IS 'Valor nuevo del campo'; diff --git a/frontend/package.json b/frontend/package.json index 6895a1b..924f996 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "checklist-frontend", "private": true, - "version": "1.0.0", + "version": "1.0.57", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e896132..fc87f67 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -917,6 +917,9 @@ function QuestionsManagerModal({ checklist, onClose }) { const [loading, setLoading] = useState(true) const [showCreateForm, setShowCreateForm] = useState(false) const [editingQuestion, setEditingQuestion] = useState(null) + const [viewingAudit, setViewingAudit] = useState(null) + const [auditHistory, setAuditHistory] = useState([]) + const [loadingAudit, setLoadingAudit] = useState(false) const [formData, setFormData] = useState({ section: '', text: '', @@ -962,6 +965,31 @@ function QuestionsManagerModal({ checklist, onClose }) { } } + const loadAuditHistory = async (questionId) => { + setLoadingAudit(true) + try { + const token = localStorage.getItem('token') + const API_URL = import.meta.env.VITE_API_URL || '' + + const response = await fetch(`${API_URL}/api/questions/${questionId}/audit`, { + headers: { 'Authorization': `Bearer ${token}` } + }) + + if (response.ok) { + const data = await response.json() + setAuditHistory(data) + setViewingAudit(questionId) + } else { + alert('Error al cargar historial') + } + } catch (error) { + console.error('Error loading audit history:', error) + alert('Error al cargar historial') + } finally { + setLoadingAudit(false) + } + } + const handleCreateQuestion = async (e) => { e.preventDefault() @@ -1539,6 +1567,13 @@ function QuestionsManagerModal({ checklist, onClose }) {