backend y front trabajar por version de historial de cambios
This commit is contained in:
@@ -1148,6 +1148,142 @@ def update_answer(
|
||||
return db_answer
|
||||
|
||||
|
||||
# ============= AUDIT LOG ENDPOINTS =============
|
||||
@app.get("/api/inspections/{inspection_id}/audit-log", response_model=List[schemas.AuditLog])
|
||||
def get_inspection_audit_log(
|
||||
inspection_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
"""Obtener el historial de cambios de una inspección"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Solo administradores pueden ver el historial")
|
||||
|
||||
logs = db.query(models.InspectionAuditLog).filter(
|
||||
models.InspectionAuditLog.inspection_id == inspection_id
|
||||
).order_by(models.InspectionAuditLog.created_at.desc()).all()
|
||||
|
||||
# Agregar nombre de usuario a cada log
|
||||
result = []
|
||||
for log in logs:
|
||||
log_dict = {
|
||||
"id": log.id,
|
||||
"inspection_id": log.inspection_id,
|
||||
"answer_id": log.answer_id,
|
||||
"user_id": log.user_id,
|
||||
"action": log.action,
|
||||
"entity_type": log.entity_type,
|
||||
"field_name": log.field_name,
|
||||
"old_value": log.old_value,
|
||||
"new_value": log.new_value,
|
||||
"comment": log.comment,
|
||||
"created_at": log.created_at,
|
||||
"user_name": None
|
||||
}
|
||||
|
||||
user = db.query(models.User).filter(models.User.id == log.user_id).first()
|
||||
if user:
|
||||
log_dict["user_name"] = user.full_name or user.username
|
||||
|
||||
result.append(schemas.AuditLog(**log_dict))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@app.put("/api/answers/{answer_id}/admin-edit", response_model=schemas.Answer)
|
||||
def admin_edit_answer(
|
||||
answer_id: int,
|
||||
answer_edit: schemas.AnswerEdit,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
"""Editar una respuesta (solo admin) con registro de auditoría"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Solo administradores pueden editar respuestas")
|
||||
|
||||
db_answer = db.query(models.Answer).filter(models.Answer.id == answer_id).first()
|
||||
|
||||
if not db_answer:
|
||||
raise HTTPException(status_code=404, detail="Respuesta no encontrada")
|
||||
|
||||
# Registrar cambios en el log de auditoría
|
||||
changes = []
|
||||
|
||||
if answer_edit.answer_value is not None and answer_edit.answer_value != db_answer.answer_value:
|
||||
changes.append({
|
||||
"field_name": "answer_value",
|
||||
"old_value": db_answer.answer_value,
|
||||
"new_value": answer_edit.answer_value
|
||||
})
|
||||
db_answer.answer_value = answer_edit.answer_value
|
||||
|
||||
if answer_edit.status is not None and answer_edit.status != db_answer.status:
|
||||
changes.append({
|
||||
"field_name": "status",
|
||||
"old_value": db_answer.status,
|
||||
"new_value": answer_edit.status
|
||||
})
|
||||
|
||||
# Recalcular puntos
|
||||
question = db.query(models.Question).filter(
|
||||
models.Question.id == db_answer.question_id
|
||||
).first()
|
||||
|
||||
old_points = db_answer.points_earned
|
||||
if answer_edit.status == "ok":
|
||||
db_answer.points_earned = question.points
|
||||
elif answer_edit.status == "warning":
|
||||
db_answer.points_earned = int(question.points * 0.5)
|
||||
else:
|
||||
db_answer.points_earned = 0
|
||||
|
||||
if old_points != db_answer.points_earned:
|
||||
changes.append({
|
||||
"field_name": "points_earned",
|
||||
"old_value": str(old_points),
|
||||
"new_value": str(db_answer.points_earned)
|
||||
})
|
||||
|
||||
db_answer.status = answer_edit.status
|
||||
|
||||
if answer_edit.comment is not None and answer_edit.comment != db_answer.comment:
|
||||
changes.append({
|
||||
"field_name": "comment",
|
||||
"old_value": db_answer.comment or "",
|
||||
"new_value": answer_edit.comment
|
||||
})
|
||||
db_answer.comment = answer_edit.comment
|
||||
|
||||
if answer_edit.is_flagged is not None and answer_edit.is_flagged != db_answer.is_flagged:
|
||||
changes.append({
|
||||
"field_name": "is_flagged",
|
||||
"old_value": str(db_answer.is_flagged),
|
||||
"new_value": str(answer_edit.is_flagged)
|
||||
})
|
||||
db_answer.is_flagged = answer_edit.is_flagged
|
||||
|
||||
# Crear registros de auditoría para cada cambio
|
||||
for change in changes:
|
||||
audit_log = models.InspectionAuditLog(
|
||||
inspection_id=db_answer.inspection_id,
|
||||
answer_id=answer_id,
|
||||
user_id=current_user.id,
|
||||
action="updated",
|
||||
entity_type="answer",
|
||||
field_name=change["field_name"],
|
||||
old_value=change["old_value"],
|
||||
new_value=change["new_value"],
|
||||
comment=answer_edit.edit_comment or "Editado por administrador"
|
||||
)
|
||||
db.add(audit_log)
|
||||
|
||||
db_answer.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(db_answer)
|
||||
|
||||
return db_answer
|
||||
|
||||
|
||||
# ============= MEDIA FILE ENDPOINTS =============
|
||||
@app.post("/api/answers/{answer_id}/upload", response_model=schemas.MediaFile)
|
||||
async def upload_photo(
|
||||
|
||||
@@ -201,3 +201,25 @@ class ChecklistPermission(Base):
|
||||
# Relationships
|
||||
checklist = relationship("Checklist", back_populates="permissions")
|
||||
mechanic = relationship("User")
|
||||
|
||||
|
||||
class InspectionAuditLog(Base):
|
||||
"""Registro de auditoría para cambios en inspecciones y respuestas"""
|
||||
__tablename__ = "inspection_audit_log"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
inspection_id = Column(Integer, ForeignKey("inspections.id", ondelete="CASCADE"), nullable=False)
|
||||
answer_id = Column(Integer, ForeignKey("answers.id", ondelete="CASCADE"), nullable=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
action = Column(String(50), nullable=False) # created, updated, deleted, status_changed
|
||||
entity_type = Column(String(50), nullable=False) # inspection, answer
|
||||
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
|
||||
inspection = relationship("Inspection")
|
||||
answer = relationship("Answer")
|
||||
user = relationship("User")
|
||||
|
||||
@@ -289,3 +289,36 @@ class InspectionListItem(BaseModel):
|
||||
flagged_items: int
|
||||
started_at: Optional[datetime]
|
||||
completed_at: Optional[datetime]
|
||||
|
||||
|
||||
# Audit Log Schemas
|
||||
class AuditLogBase(BaseModel):
|
||||
action: str
|
||||
entity_type: str
|
||||
field_name: Optional[str] = None
|
||||
old_value: Optional[str] = None
|
||||
new_value: Optional[str] = None
|
||||
comment: Optional[str] = None
|
||||
|
||||
class AuditLog(AuditLogBase):
|
||||
id: int
|
||||
inspection_id: int
|
||||
answer_id: Optional[int] = None
|
||||
user_id: int
|
||||
user_name: Optional[str] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class AnswerEdit(BaseModel):
|
||||
answer_value: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
comment: Optional[str] = None
|
||||
is_flagged: Optional[bool] = None
|
||||
edit_comment: Optional[str] = None # Comentario del admin sobre por qué editó
|
||||
|
||||
max_score: Optional[int]
|
||||
flagged_items: int
|
||||
started_at: Optional[datetime]
|
||||
completed_at: Optional[datetime]
|
||||
|
||||
Reference in New Issue
Block a user