176 lines
7.2 KiB
Python
176 lines
7.2 KiB
Python
"""
|
|
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"
|
|
),
|
|
}
|