""" Endpoints para EMPLEADOS operativos. - NO pueden gestionar reportes ciudadanos (eso es solo de ADMIN). - SÍ pueden levantar reportes operativos (problemas con el camión, ruta, etc.) - Reciben info de su horario, puntualidad y bonos para motivar el servicio. """ import uuid import hashlib from datetime import datetime, timedelta from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import desc, func from pydantic import BaseModel from ..database import get_db from ..models.user import User from ..models.report import OperationalReport from .deps import get_current_user router = APIRouter(prefix="/staff", tags=["staff"]) # ─── Helpers ───────────────────────────────────────────────────────────────── def _require_employee_or_admin(user: User): """Asegura que solo empleados o admins acceden. Ciudadanos: 403.""" if user.role not in ("EMPLEADO", "ADMIN"): raise HTTPException(status_code=403, detail="Acceso solo para personal") return user def _generate_folio() -> str: date = datetime.utcnow().strftime("%Y%m%d") short = str(uuid.uuid4())[:6].upper() return f"OP-{date}-{short}" # ─── Schemas ──────────────────────────────────────────────────────────────── class OperationalReportCreate(BaseModel): category: str description: Optional[str] = None severity: str = "MEDIA" route_id: Optional[str] = None truck_id: Optional[int] = None class OperationalReportOut(BaseModel): id: int folio: str category: str description: Optional[str] severity: str route_id: Optional[str] truck_id: Optional[int] status: str created_at: datetime class Config: from_attributes = True CATEGORY_LABELS = { "NO_ARRANQUE": "El camión no arrancó", "FALLA_MECANICA": "Falla mecánica en ruta", "ACCIDENTE": "Accidente vial", "OBSTACULO": "Obstáculo bloqueando la ruta", "TRAFICO": "Tráfico intenso / retraso", "COMBUSTIBLE": "Nivel bajo de combustible", "CLIMA": "Clima adverso", "OTRO": "Otro incidente", } # ─── 1. CATEGORÍAS DISPONIBLES ──────────────────────────────────────────────── @router.get("/categories") def list_categories(user=Depends(get_current_user)): _require_employee_or_admin(user) return [{"key": k, "label": v} for k, v in CATEGORY_LABELS.items()] # ─── 2. CREAR REPORTE OPERATIVO ────────────────────────────────────────────── @router.post("/operational-reports", response_model=OperationalReportOut, status_code=201) def create_op_report(data: OperationalReportCreate, db: Session = Depends(get_db), user=Depends(get_current_user)): _require_employee_or_admin(user) if data.category not in CATEGORY_LABELS: raise HTTPException(status_code=400, detail="Categoría inválida") if data.severity not in ("BAJA", "MEDIA", "ALTA"): raise HTTPException(status_code=400, detail="Severidad inválida") rep = OperationalReport( employee_id=user.id, folio=_generate_folio(), category=data.category, description=data.description, severity=data.severity, route_id=data.route_id, truck_id=data.truck_id, ) db.add(rep) db.commit() db.refresh(rep) return rep # ─── 3. LISTAR MIS REPORTES OPERATIVOS ─────────────────────────────────────── @router.get("/operational-reports", response_model=list[OperationalReportOut]) def list_my_op_reports(db: Session = Depends(get_db), user=Depends(get_current_user)): _require_employee_or_admin(user) return db.query(OperationalReport).filter( OperationalReport.employee_id == user.id ).order_by(desc(OperationalReport.created_at)).all() # ─── 4. HORARIO DE TRABAJO ─────────────────────────────────────────────────── @router.get("/schedule") def get_my_schedule(user=Depends(get_current_user)): _require_employee_or_admin(user) # Horario preestablecido (en producción vendría de una tabla) return { "shift_name": "Turno Matutino", "shift_start": "05:30", "shift_end": "09:00", "route_block": "06:00 - 08:00", "breaks": [ {"name": "Descanso técnico", "time": "07:00", "duration_min": 10, "icon": "coffee"}, {"name": "Pausa estiramiento", "time": "08:00", "duration_min": 5, "icon": "yoga"}, ], "days_per_week": "Lunes a sábado", "rest_day": "Domingo", "notes": "Llegada puntual a las 05:30 garantiza salida del camión a las 06:00 en tiempo.", } # ─── 5. PUNTUALIDAD Y BONOS (datos mock pero coherentes) ───────────────────── @router.get("/dashboard") def get_employee_dashboard(db: Session = Depends(get_db), user=Depends(get_current_user)): _require_employee_or_admin(user) # Generar valores determinísticos por employee_id (sin random, así no cambia entre llamadas) seed = int(hashlib.md5(str(user.id).encode()).hexdigest(), 16) streak_days = 7 + (seed % 18) # 7-24 días punctuality_pct = 88 + (seed % 12) # 88-99% bonus_mxn = streak_days * 50 # $50 por día puntual next_milestone_days = 30 - streak_days if streak_days < 30 else 60 - streak_days next_milestone_mxn = 500 # Total de reportes operativos generados por este empleado my_reports_count = db.query(func.count(OperationalReport.id)).filter( OperationalReport.employee_id == user.id ).scalar() or 0 # Mensaje motivacional rotativo (basado en día del año + user id) motivations = [ "¡Tu puntualidad permite que miles de familias planifiquen su día!", "Cada salida a tiempo es un acto de servicio que transforma a Celaya.", "Hoy también cuentan contigo. Manejen seguro, equipo.", "Una ruta puntual = vecinos contentos y ciudad más limpia.", "Tu trabajo refleja el orgullo del Gobierno de Celaya. ¡Gracias!", ] motivation = motivations[(datetime.utcnow().timetuple().tm_yday + user.id) % len(motivations)] return { "employee_name": user.full_name, "streak_days": streak_days, "punctuality_pct": punctuality_pct, "bonus_accumulated_mxn": bonus_mxn, "next_milestone_days": max(1, next_milestone_days), "next_milestone_mxn": next_milestone_mxn, "reports_generated": my_reports_count, "motivation_quote": motivation, "rating_label": ( "EXCELENTE" if punctuality_pct >= 95 else "MUY BUENO" if punctuality_pct >= 90 else "BUENO" ), }