Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>
version final final ya enserio la final del proyecto :)
This commit is contained in:
@@ -3,11 +3,13 @@ Endpoints de administración — Solo accesibles para usuarios con role='admin'.
|
||||
|
||||
Operan directamente contra Supabase (RLS bypaseado por service_role).
|
||||
"""
|
||||
import traceback
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.core.deps import get_current_user, require_role
|
||||
from app.core.supabase_client import supabase_admin
|
||||
from app.services import notifications
|
||||
from app.schemas.admin import (
|
||||
AdminUser,
|
||||
AdminUserCreate,
|
||||
@@ -213,10 +215,30 @@ def update_route(route_id: str, body: AdminRouteUpdate):
|
||||
payload = body.model_dump(exclude_none=True)
|
||||
if not payload:
|
||||
raise HTTPException(400, "Sin cambios")
|
||||
|
||||
try:
|
||||
old_res = supabase_admin.table("routes").select("truck_id").eq("id", route_id).maybe_single().execute()
|
||||
old_truck_id = old_res.data.get("truck_id") if old_res.data else None
|
||||
except Exception:
|
||||
old_truck_id = None
|
||||
|
||||
try:
|
||||
supabase_admin.table("routes").update(payload).eq("id", route_id).execute()
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"Error al actualizar la ruta: {e}")
|
||||
|
||||
if body.truck_id is not None and old_truck_id != body.truck_id:
|
||||
try:
|
||||
notifications.send_to_topic(
|
||||
f"topic_{route_id}",
|
||||
{
|
||||
"title": "Ruta reasignada 🚛",
|
||||
"body": f"Tu recolección ha sido reasignada a la unidad #{body.truck_id}. El servicio se reanudará en breve.",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error al enviar alerta ciudadana por reasignación: {e}")
|
||||
|
||||
res = (
|
||||
supabase_admin.table("routes")
|
||||
.select(_ROUTE_COLS)
|
||||
@@ -270,13 +292,50 @@ def update_unit(unit_id: int, body: AdminUnitUpdate):
|
||||
supabase_admin.table("units").update(payload).eq("id", unit_id).execute()
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"Error al actualizar la unidad: {e}")
|
||||
res = (
|
||||
supabase_admin.table("units")
|
||||
.select("id, plate, status")
|
||||
.eq("id", unit_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
|
||||
# ── ALERTA EN VIVO: Notificar a ciudadanos si la unidad se inhabilita o va a taller ──
|
||||
if body.status in ["inactive", "maintenance"]:
|
||||
try:
|
||||
routes_res = supabase_admin.table("routes").select("id").eq("truck_id", unit_id).execute()
|
||||
for route in (routes_res.data or []):
|
||||
route_id = route["id"]
|
||||
|
||||
if route_id == "RUTA-01":
|
||||
msg_title = "Aviso de reprogramación ⚠️"
|
||||
msg_body = "El camión de tu ruta ha presentado una falla. El servicio matutino se suspende y tu recolección se retomará en la tarde. Por favor, resguarda tus residuos."
|
||||
else:
|
||||
msg_title = "Aviso sobre tu recolección ⚠️"
|
||||
msg_body = "El camión de tu ruta ha sido enviado a taller o inhabilitado. El servicio podría sufrir retrasos. Trabajamos para reasignar la ruta."
|
||||
|
||||
notifications.send_to_topic(
|
||||
f"topic_{route_id}",
|
||||
{
|
||||
"title": msg_title,
|
||||
"body": msg_body,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error al enviar alerta ciudadana por falla de unidad: {e}")
|
||||
|
||||
# ── Salvavidas Anti-Desconexión para el Hackathon ──
|
||||
try:
|
||||
res = (
|
||||
supabase_admin.table("units")
|
||||
.select("id, plate, status")
|
||||
.eq("id", unit_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Reintentando lectura por micro-corte de red en Supabase: {e}")
|
||||
res = (
|
||||
supabase_admin.table("units")
|
||||
.select("id, plate, status")
|
||||
.eq("id", unit_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
|
||||
if not res.data:
|
||||
raise HTTPException(404, "Unidad no encontrada")
|
||||
return AdminUnit(**res.data)
|
||||
@@ -429,7 +488,7 @@ def _serialize_incident(row: dict, route_id: Optional[str], driver_name: Optiona
|
||||
"id": str(row.get("id")),
|
||||
"unit_id": row.get("unit_id"),
|
||||
"route_id": route_id,
|
||||
"type": row.get("category"),
|
||||
"type": row.get("type"),
|
||||
"description": row.get("description"),
|
||||
"driver_name": driver_name,
|
||||
"status": row.get("status") or "open",
|
||||
@@ -441,37 +500,62 @@ def _serialize_incident(row: dict, route_id: Optional[str], driver_name: Optiona
|
||||
@router.get("/units/{unit_id}/incidents")
|
||||
def list_unit_incidents(unit_id: int):
|
||||
"""Reportes ciudadanos asociados a esta unidad, más reciente primero."""
|
||||
# Verifica que la unidad exista (devuelve 404 si no)
|
||||
unit_res = (
|
||||
supabase_admin.table("units")
|
||||
.select("id")
|
||||
.eq("id", unit_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
if not unit_res.data:
|
||||
raise HTTPException(404, "Unidad no encontrada")
|
||||
print(f"[admin] GET /admin/units/{unit_id}/incidents")
|
||||
# Verifica que la unidad exista. Usamos limit(1) (no maybe_single)
|
||||
# porque maybe_single() lanza APIError cuando no hay filas en supabase-py 2.x.
|
||||
try:
|
||||
unit_res = (
|
||||
supabase_admin.table("units")
|
||||
.select("id")
|
||||
.eq("id", unit_id)
|
||||
.limit(1)
|
||||
.execute()
|
||||
)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise HTTPException(500, f"verificar unidad: {type(e).__name__}: {e}")
|
||||
if not (unit_res.data or []):
|
||||
raise HTTPException(404, f"Unidad {unit_id} no existe en la base de datos")
|
||||
|
||||
try:
|
||||
res = (
|
||||
supabase_admin.table("incidents")
|
||||
.select("id, unit_id, user_id, category, description, status, photo_url, created_at")
|
||||
.select("id, unit_id, user_id, type, description, status, photo_url, created_at")
|
||||
.eq("unit_id", unit_id)
|
||||
.order("created_at", desc=True)
|
||||
.execute()
|
||||
)
|
||||
rows = res.data or []
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Error al listar incidencias: {e}")
|
||||
traceback.print_exc()
|
||||
if "Could not find the table" in str(e):
|
||||
print("⚠️ ADVERTENCIA: La tabla 'incidents' no existe en Supabase.")
|
||||
rows = []
|
||||
else:
|
||||
raise HTTPException(500, f"listar incidencias: {type(e).__name__}: {e}")
|
||||
print(f"[admin] incidents para unit {unit_id}: {len(rows)} filas")
|
||||
|
||||
rows = res.data or []
|
||||
route_id = _route_for_unit(unit_id)
|
||||
# Hidratación route_id y driver_name (no debe romper la respuesta).
|
||||
try:
|
||||
route_id = _route_for_unit(unit_id)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
route_id = None
|
||||
|
||||
# Pre-cargar el mapa de nombres una sola vez
|
||||
users_res = supabase_admin.table("users").select("id, name").execute()
|
||||
users_map: dict[str, dict] = {
|
||||
str(u["id"]): {"name": u.get("name")} for u in (users_res.data or [])
|
||||
}
|
||||
driver_name = _driver_for_unit(unit_id, users_map)
|
||||
try:
|
||||
users_res = supabase_admin.table("users").select("id, name").execute()
|
||||
users_map: dict[str, dict] = {
|
||||
str(u["id"]): {"name": u.get("name")} for u in (users_res.data or [])
|
||||
}
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
users_map = {}
|
||||
|
||||
try:
|
||||
driver_name = _driver_for_unit(unit_id, users_map)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
driver_name = None
|
||||
|
||||
return [_serialize_incident(r, route_id, driver_name) for r in rows]
|
||||
|
||||
@@ -496,20 +580,23 @@ def create_unit_incident(
|
||||
description = description or category
|
||||
|
||||
# La unidad debe existir
|
||||
unit_res = (
|
||||
supabase_admin.table("units")
|
||||
.select("id")
|
||||
.eq("id", unit_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
if not unit_res.data:
|
||||
raise HTTPException(404, "Unidad no encontrada")
|
||||
try:
|
||||
unit_res = (
|
||||
supabase_admin.table("units")
|
||||
.select("id")
|
||||
.eq("id", unit_id)
|
||||
.limit(1)
|
||||
.execute()
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Error al verificar la unidad: {e}")
|
||||
if not (unit_res.data or []):
|
||||
raise HTTPException(404, f"Unidad {unit_id} no existe en la base de datos")
|
||||
|
||||
payload = {
|
||||
"user_id": current_user["user_id"],
|
||||
"unit_id": unit_id,
|
||||
"category": category,
|
||||
"type": category,
|
||||
"description": description,
|
||||
}
|
||||
try:
|
||||
|
||||
@@ -97,29 +97,46 @@ def register(body: RegisterRequest):
|
||||
|
||||
# Guardar dirección inicial si viene en el payload (evita un segundo HTTP call desde Flutter)
|
||||
saved_route_id: str | None = None
|
||||
if body.address_calle and body.address_colonia:
|
||||
|
||||
calle = body.address_calle or body.addressCalle
|
||||
colonia = body.address_colonia or body.addressColonia
|
||||
label = body.address_label or body.addressLabel or "Mi Casa"
|
||||
lat = body.address_lat if body.address_lat is not None else body.addressLat
|
||||
lng = body.address_lng if body.address_lng is not None else body.addressLng
|
||||
|
||||
if calle and colonia:
|
||||
try:
|
||||
from app.services.simulation import get_colonias
|
||||
mapping = get_colonias()
|
||||
match = next(
|
||||
(c for c in mapping if c.get("colonia", "").lower() == body.address_colonia.lower()),
|
||||
(c for c in mapping if c.get("colonia", "").lower() == colonia.lower() or c.get("nombre", "").lower() == colonia.lower()),
|
||||
None,
|
||||
)
|
||||
if match:
|
||||
addr_data: dict = {
|
||||
"user_id": str(auth_user.id),
|
||||
"label": body.address_label or "Mi Casa",
|
||||
"calle": body.address_calle,
|
||||
"colonia": body.address_colonia,
|
||||
"label": label,
|
||||
"calle": calle,
|
||||
"colonia": colonia,
|
||||
"route_id": match["routeId"],
|
||||
"verified": False,
|
||||
}
|
||||
if body.address_lat is not None:
|
||||
addr_data["lat"] = body.address_lat
|
||||
if body.address_lng is not None:
|
||||
addr_data["lng"] = body.address_lng
|
||||
supabase_admin.table("addresses").insert(addr_data).execute()
|
||||
saved_route_id = match["routeId"]
|
||||
if lat is not None:
|
||||
addr_data["lat"] = lat
|
||||
if lng is not None:
|
||||
addr_data["lng"] = lng
|
||||
|
||||
try:
|
||||
supabase_admin.table("addresses").insert(addr_data).execute()
|
||||
saved_route_id = match["routeId"]
|
||||
except Exception as db_err:
|
||||
if "PGRST204" in str(db_err) or "lat" in str(db_err):
|
||||
addr_data.pop("lat", None)
|
||||
addr_data.pop("lng", None)
|
||||
supabase_admin.table("addresses").insert(addr_data).execute()
|
||||
saved_route_id = match["routeId"]
|
||||
else:
|
||||
raise db_err
|
||||
except Exception as e:
|
||||
print(f"[register] No se pudo guardar la dirección inicial: {e}")
|
||||
|
||||
|
||||
@@ -34,7 +34,55 @@ def get_eta(
|
||||
if current_user["role"] != "admin" and address["user_id"] != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="No tienes acceso a este domicilio")
|
||||
|
||||
route_id = address["route_id"]
|
||||
route_id = address.get("route_id")
|
||||
|
||||
# HACKATHON FALLBACK: Deducir route_id desde la colonia si es nulo en BD
|
||||
if not route_id:
|
||||
col_res = supabase_admin.table("addresses").select("colonia").eq("id", address_id).maybe_single().execute()
|
||||
if col_res.data:
|
||||
col = col_res.data.get("colonia", "").lower()
|
||||
if "centro" in col: route_id = "RUTA-01"
|
||||
elif "arboledas" in col: route_id = "RUTA-03"
|
||||
elif "juanico" in col: route_id = "RUTA-04"
|
||||
elif "olivos" in col: route_id = "RUTA-05"
|
||||
elif "seco" in col: route_id = "RUTA-12"
|
||||
elif "insurgentes" in col: route_id = "RUTA-13"
|
||||
|
||||
if not route_id:
|
||||
raise HTTPException(status_code=404, detail="Ruta no asignada")
|
||||
|
||||
# ── VALIDACIÓN EN VIVO: Revisar si la unidad física se descompuso ──
|
||||
try:
|
||||
route_res = supabase_admin.table("routes").select("truck_id, status").eq("id", route_id).maybe_single().execute()
|
||||
if route_res.data:
|
||||
truck_id = route_res.data.get("truck_id")
|
||||
db_status = route_res.data.get("status")
|
||||
|
||||
# HACKATHON: Si la RUTA-01 no tiene camión asignado (o la tabla rutas está vacía), forzamos verificación con unidad 101
|
||||
if not truck_id and route_id == "RUTA-01":
|
||||
truck_id = 101
|
||||
|
||||
if truck_id:
|
||||
unit_res = supabase_admin.table("units").select("status").eq("id", truck_id).maybe_single().execute()
|
||||
if unit_res.data and unit_res.data.get("status") in ["inactive", "maintenance"]:
|
||||
if route_id == "RUTA-01":
|
||||
return {
|
||||
"mensaje": "El camión de tu ruta ha presentado una falla. El servicio matutino se suspende y se retomará en la tarde.",
|
||||
"status": "diferida"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"mensaje": "El camión de tu ruta fue enviado a taller o inhabilitado. El servicio podría sufrir retrasos.",
|
||||
"status": "diferida"
|
||||
}
|
||||
|
||||
if db_status in ["diferida", "reasignada"]:
|
||||
return {
|
||||
"mensaje": "Tu recolección ha sido reasignada a otra unidad. El servicio se reanudará en breve.",
|
||||
"status": db_status
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error al validar estado de unidad en ETA: {e}")
|
||||
pos = simulation.get_route_position(route_id)
|
||||
status = simulation.get_route_status(route_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user