backend actualizado para dashboard

This commit is contained in:
2025-11-19 22:25:40 -03:00
parent 250758963c
commit 0917d24029
2 changed files with 323 additions and 1 deletions

View File

@@ -2,7 +2,8 @@ from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session, joinedload
from typing import List
from sqlalchemy import func, case
from typing import List, Optional
import os
import shutil
from datetime import datetime, timedelta
@@ -1273,6 +1274,286 @@ Responde en formato JSON:
}
# ============= REPORTS =============
@app.get("/api/reports/dashboard", response_model=schemas.DashboardData)
def get_dashboard_data(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
mechanic_id: Optional[int] = None,
current_user: models.User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Obtener datos del dashboard de informes"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Solo administradores pueden acceder a reportes")
# Construir query base
query = db.query(models.Inspection)
# Aplicar filtros de fecha
if start_date:
start = datetime.fromisoformat(start_date)
query = query.filter(models.Inspection.started_at >= start)
if end_date:
end = datetime.fromisoformat(end_date)
query = query.filter(models.Inspection.started_at <= end)
# Filtro por mecánico
if mechanic_id:
query = query.filter(models.Inspection.mechanic_id == mechanic_id)
# Solo inspecciones activas
query = query.filter(models.Inspection.is_active == True)
# ESTADÍSTICAS GENERALES
total = query.count()
completed = query.filter(models.Inspection.status == "completed").count()
pending = total - completed
# Score promedio
avg_score_result = query.filter(
models.Inspection.score.isnot(None),
models.Inspection.max_score.isnot(None),
models.Inspection.max_score > 0
).with_entities(
func.avg(models.Inspection.score * 100.0 / models.Inspection.max_score)
).scalar()
avg_score = round(avg_score_result, 2) if avg_score_result else 0.0
# Items señalados
flagged_items = db.query(func.count(models.Answer.id))\
.filter(models.Answer.is_flagged == True)\
.join(models.Inspection)\
.filter(models.Inspection.is_active == True)
if start_date:
start = datetime.fromisoformat(start_date)
flagged_items = flagged_items.filter(models.Inspection.started_at >= start)
if end_date:
end = datetime.fromisoformat(end_date)
flagged_items = flagged_items.filter(models.Inspection.started_at <= end)
if mechanic_id:
flagged_items = flagged_items.filter(models.Inspection.mechanic_id == mechanic_id)
total_flagged = flagged_items.scalar() or 0
stats = schemas.InspectionStats(
total_inspections=total,
completed_inspections=completed,
pending_inspections=pending,
completion_rate=round((completed / total * 100) if total > 0 else 0, 2),
avg_score=avg_score,
total_flagged_items=total_flagged
)
# RANKING DE MECÁNICOS
mechanic_stats = db.query(
models.User.id,
models.User.full_name,
func.count(models.Inspection.id).label('total'),
func.avg(
case(
(models.Inspection.max_score > 0, models.Inspection.score * 100.0 / models.Inspection.max_score),
else_=None
)
).label('avg_score'),
func.count(case((models.Inspection.status == 'completed', 1))).label('completed')
).join(models.Inspection, models.Inspection.mechanic_id == models.User.id)\
.filter(models.User.role.in_(['mechanic', 'mecanico']))\
.filter(models.User.is_active == True)\
.filter(models.Inspection.is_active == True)
if start_date:
start = datetime.fromisoformat(start_date)
mechanic_stats = mechanic_stats.filter(models.Inspection.started_at >= start)
if end_date:
end = datetime.fromisoformat(end_date)
mechanic_stats = mechanic_stats.filter(models.Inspection.started_at <= end)
mechanic_stats = mechanic_stats.group_by(models.User.id, models.User.full_name)\
.order_by(func.count(models.Inspection.id).desc())\
.all()
mechanic_ranking = [
schemas.MechanicRanking(
mechanic_id=m.id,
mechanic_name=m.full_name,
total_inspections=m.total,
avg_score=round(m.avg_score, 2) if m.avg_score else 0.0,
completion_rate=round((m.completed / m.total * 100) if m.total > 0 else 0, 2)
)
for m in mechanic_stats
]
# ESTADÍSTICAS POR CHECKLIST
checklist_stats_query = db.query(
models.Checklist.id,
models.Checklist.name,
func.count(models.Inspection.id).label('total'),
func.avg(
case(
(models.Inspection.max_score > 0, models.Inspection.score * 100.0 / models.Inspection.max_score),
else_=None
)
).label('avg_score')
).join(models.Inspection)\
.filter(models.Inspection.is_active == True)\
.filter(models.Checklist.is_active == True)
if start_date:
start = datetime.fromisoformat(start_date)
checklist_stats_query = checklist_stats_query.filter(models.Inspection.started_at >= start)
if end_date:
end = datetime.fromisoformat(end_date)
checklist_stats_query = checklist_stats_query.filter(models.Inspection.started_at <= end)
if mechanic_id:
checklist_stats_query = checklist_stats_query.filter(models.Inspection.mechanic_id == mechanic_id)
checklist_stats_query = checklist_stats_query.group_by(models.Checklist.id, models.Checklist.name)
checklist_stats_data = checklist_stats_query.all()
checklist_stats = [
schemas.ChecklistStats(
checklist_id=c.id,
checklist_name=c.name,
total_inspections=c.total,
avg_score=round(c.avg_score, 2) if c.avg_score else 0.0
)
for c in checklist_stats_data
]
# INSPECCIONES POR FECHA (últimos 30 días)
end_date_obj = datetime.fromisoformat(end_date) if end_date else datetime.now()
start_date_obj = datetime.fromisoformat(start_date) if start_date else end_date_obj - timedelta(days=30)
inspections_by_date_query = db.query(
func.date(models.Inspection.started_at).label('date'),
func.count(models.Inspection.id).label('count')
).filter(
models.Inspection.started_at.between(start_date_obj, end_date_obj),
models.Inspection.is_active == True
)
if mechanic_id:
inspections_by_date_query = inspections_by_date_query.filter(
models.Inspection.mechanic_id == mechanic_id
)
inspections_by_date_data = inspections_by_date_query.group_by(
func.date(models.Inspection.started_at)
).all()
inspections_by_date = {
str(d.date): d.count for d in inspections_by_date_data
}
# RATIO PASS/FAIL
pass_fail_data = db.query(
models.Answer.answer_value,
func.count(models.Answer.id).label('count')
).join(models.Inspection)\
.filter(models.Inspection.is_active == True)\
.filter(models.Answer.answer_value.in_(['pass', 'fail', 'good', 'bad', 'regular']))
if start_date:
start = datetime.fromisoformat(start_date)
pass_fail_data = pass_fail_data.filter(models.Inspection.started_at >= start)
if end_date:
end = datetime.fromisoformat(end_date)
pass_fail_data = pass_fail_data.filter(models.Inspection.started_at <= end)
if mechanic_id:
pass_fail_data = pass_fail_data.filter(models.Inspection.mechanic_id == mechanic_id)
pass_fail_data = pass_fail_data.group_by(models.Answer.answer_value).all()
pass_fail_ratio = {d.answer_value: d.count for d in pass_fail_data}
return schemas.DashboardData(
stats=stats,
mechanic_ranking=mechanic_ranking,
checklist_stats=checklist_stats,
inspections_by_date=inspections_by_date,
pass_fail_ratio=pass_fail_ratio
)
@app.get("/api/reports/inspections")
def get_inspections_report(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
mechanic_id: Optional[int] = None,
checklist_id: Optional[int] = None,
status: Optional[str] = None,
limit: int = 100,
current_user: models.User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Obtener lista de inspecciones con filtros"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Solo administradores pueden acceder a reportes")
# Query base con select_from explícito
query = db.query(
models.Inspection.id,
models.Inspection.vehicle_plate,
models.Checklist.name.label('checklist_name'),
models.User.full_name.label('mechanic_name'),
models.Inspection.status,
models.Inspection.score,
models.Inspection.max_score,
models.Inspection.started_at,
models.Inspection.completed_at,
func.coalesce(
func.count(case((models.Answer.is_flagged == True, 1))),
0
).label('flagged_items')
).select_from(models.Inspection)\
.join(models.Checklist, models.Inspection.checklist_id == models.Checklist.id)\
.join(models.User, models.Inspection.mechanic_id == models.User.id)\
.outerjoin(models.Answer, models.Answer.inspection_id == models.Inspection.id)\
.filter(models.Inspection.is_active == True)
# Aplicar filtros
if start_date:
start = datetime.fromisoformat(start_date)
query = query.filter(models.Inspection.started_at >= start)
if end_date:
end = datetime.fromisoformat(end_date)
query = query.filter(models.Inspection.started_at <= end)
if mechanic_id:
query = query.filter(models.Inspection.mechanic_id == mechanic_id)
if checklist_id:
query = query.filter(models.Inspection.checklist_id == checklist_id)
if status:
query = query.filter(models.Inspection.status == status)
# Group by y order
query = query.group_by(
models.Inspection.id,
models.Checklist.name,
models.User.full_name
).order_by(models.Inspection.started_at.desc())\
.limit(limit)
results = query.all()
return [
{
"id": r.id,
"vehicle_plate": r.vehicle_plate,
"checklist_name": r.checklist_name,
"mechanic_name": r.mechanic_name,
"status": r.status,
"score": r.score,
"max_score": r.max_score,
"flagged_items": r.flagged_items,
"started_at": r.started_at.isoformat() if r.started_at else None,
"completed_at": r.completed_at.isoformat() if r.completed_at else None
}
for r in results
]
# ============= HEALTH CHECK =============
@app.get("/")
def root():

View File

@@ -236,3 +236,44 @@ class AIModelInfo(BaseModel):
name: str
provider: str
description: Optional[str] = None
# Reports Schemas
class InspectionStats(BaseModel):
total_inspections: int
completed_inspections: int
pending_inspections: int
completion_rate: float
avg_score: float
total_flagged_items: int
class MechanicRanking(BaseModel):
mechanic_id: int
mechanic_name: str
total_inspections: int
avg_score: float
completion_rate: float
class ChecklistStats(BaseModel):
checklist_id: int
checklist_name: str
total_inspections: int
avg_score: float
class DashboardData(BaseModel):
stats: InspectionStats
mechanic_ranking: List[MechanicRanking]
checklist_stats: List[ChecklistStats]
inspections_by_date: dict
pass_fail_ratio: dict
class InspectionListItem(BaseModel):
id: int
vehicle_plate: str
checklist_name: str
mechanic_name: str
status: str
score: Optional[int]
max_score: Optional[int]
flagged_items: int
started_at: Optional[datetime]
completed_at: Optional[datetime]