Initial commit

This commit is contained in:
marianesaldana
2026-05-23 08:59:34 -06:00
commit 80dbd947e5
36446 changed files with 3729147 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
"""
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"
),
}