From cdd1b3507bc7b6c9f537156e2cdaf5163968cf3c Mon Sep 17 00:00:00 2001 From: ronalds Date: Thu, 27 Nov 2025 01:26:15 -0300 Subject: [PATCH] =?UTF-8?q?v1.0.63=20Backend=20/=20v1.0.57=20Frontend=20-?= =?UTF-8?q?=20Edici=C3=B3n=20y=20auditor=C3=ADa=20de=20preguntas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/main.py | 95 ++++++++++- backend/app/models.py | 21 +++ backend/app/schemas.py | 17 ++ backend/migrations/add_question_audit_log.sql | 44 ++++++ frontend/package.json | 2 +- frontend/src/App.jsx | 147 ++++++++++++++++++ 6 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 backend/migrations/add_question_audit_log.sql 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 }) {
+
+ + {/* Audit History Modal */} + {viewingAudit && ( +
+
+ {/* Header */} +
+

📜 Historial de Cambios - Pregunta #{viewingAudit}

+ +
+ + {/* Content */} +
+ {loadingAudit ? ( +
Cargando historial...
+ ) : auditHistory.length === 0 ? ( +
No hay cambios registrados
+ ) : ( +
+ {auditHistory.map((log) => ( +
+
+
+ + {log.action === 'created' ? '➕ Creado' : + log.action === 'updated' ? '✏️ Modificado' : + '🗑️ Eliminado'} + + {log.field_name && ( + + Campo: {log.field_name} + + )} +
+ + {new Date(log.created_at).toLocaleString('es-PY', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} + +
+ + {log.field_name && ( +
+
+
Valor anterior:
+
+ {log.old_value || '-'} +
+
+
+
Valor nuevo:
+
+ {log.new_value || '-'} +
+
+
+ )} + + {!log.field_name && (log.old_value || log.new_value) && ( +
+ {log.old_value || log.new_value} +
+ )} + + {log.comment && ( +
+ 💬 {log.comment} +
+ )} + + {log.user && ( +
+ Por: {log.user.full_name || log.user.username} +
+ )} +
+ ))} +
+ )} +
+ + {/* Footer */} +
+ +
+
+
+ )} ) }