From 0917d24029433e85663ebd9a89799af8bbd43933 Mon Sep 17 00:00:00 2001 From: ronalds Date: Wed, 19 Nov 2025 22:25:40 -0300 Subject: [PATCH] backend actualizado para dashboard --- backend/app/main.py | 283 ++++++++++++++++++++++++++++++++++++++++- backend/app/schemas.py | 41 ++++++ 2 files changed, 323 insertions(+), 1 deletion(-) diff --git a/backend/app/main.py b/backend/app/main.py index e1c4f68..4d94dce 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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(): diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 72ab026..4b8cd86 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -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]