simulacion de estados y flujo de notificacion, modificacion de estilos en todas las vistas
This commit is contained in:
@@ -96,3 +96,61 @@ def get_address(
|
||||
raise HTTPException(status_code=403, detail="No tienes acceso a este domicilio")
|
||||
|
||||
return address
|
||||
|
||||
|
||||
@router.get("/{address_id}/unit")
|
||||
def get_address_unit(
|
||||
address_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Devuelve la unidad asignada al domicilio (a través de su ruta).
|
||||
Cadena: addresses.route_id → routes.truck_id → units.
|
||||
El ciudadano sólo puede consultar sus propios domicilios.
|
||||
"""
|
||||
addr_res = (
|
||||
supabase_admin.table("addresses")
|
||||
.select("id, user_id, route_id")
|
||||
.eq("id", address_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
if not addr_res.data:
|
||||
raise HTTPException(status_code=404, detail="Domicilio no encontrado")
|
||||
|
||||
address = addr_res.data
|
||||
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.get("route_id")
|
||||
if not route_id:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="El domicilio no tiene una ruta asociada",
|
||||
)
|
||||
|
||||
route_res = (
|
||||
supabase_admin.table("routes")
|
||||
.select("id, truck_id")
|
||||
.eq("id", route_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
if not route_res.data or route_res.data.get("truck_id") is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="La ruta asignada no tiene unidad activa",
|
||||
)
|
||||
|
||||
truck_id = route_res.data["truck_id"]
|
||||
unit_res = (
|
||||
supabase_admin.table("units")
|
||||
.select("id, plate, status")
|
||||
.eq("id", truck_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
if not unit_res.data:
|
||||
raise HTTPException(status_code=404, detail="Unidad no encontrada")
|
||||
|
||||
return unit_res.data
|
||||
|
||||
@@ -6,7 +6,7 @@ Operan directamente contra Supabase (RLS bypaseado por service_role).
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.core.deps import require_role
|
||||
from app.core.deps import get_current_user, require_role
|
||||
from app.core.supabase_client import supabase_admin
|
||||
from app.schemas.admin import (
|
||||
AdminUser,
|
||||
@@ -375,3 +375,156 @@ def delete_driver(driver_id: str):
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"Error al borrar el conductor: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ── Incidents por unidad ──────────────────────────────────────────────────────
|
||||
_INCIDENT_CATEGORIES = {
|
||||
"derrame",
|
||||
"dano_propiedad",
|
||||
"conducta",
|
||||
"no_recoleccion",
|
||||
"otro",
|
||||
}
|
||||
|
||||
|
||||
def _route_for_unit(unit_id: int) -> Optional[str]:
|
||||
"""Devuelve el route_id que actualmente tiene asignada la unidad
|
||||
(vía routes.truck_id), o None si no está asignada a ninguna ruta."""
|
||||
try:
|
||||
res = (
|
||||
supabase_admin.table("routes")
|
||||
.select("id")
|
||||
.eq("truck_id", unit_id)
|
||||
.limit(1)
|
||||
.execute()
|
||||
)
|
||||
rows = res.data or []
|
||||
return rows[0]["id"] if rows else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _driver_for_unit(unit_id: int, users_map: dict[str, dict]) -> Optional[str]:
|
||||
"""Devuelve el nombre del chofer asignado a la unidad (drivers.unit_id)."""
|
||||
try:
|
||||
res = (
|
||||
supabase_admin.table("drivers")
|
||||
.select("user_id")
|
||||
.eq("unit_id", unit_id)
|
||||
.limit(1)
|
||||
.execute()
|
||||
)
|
||||
rows = res.data or []
|
||||
if not rows:
|
||||
return None
|
||||
user_id = str(rows[0].get("user_id"))
|
||||
return (users_map.get(user_id) or {}).get("name")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _serialize_incident(row: dict, route_id: Optional[str], driver_name: Optional[str]) -> dict:
|
||||
"""Da la forma que espera el cliente Flutter (admin_incident.dart)."""
|
||||
return {
|
||||
"id": str(row.get("id")),
|
||||
"unit_id": row.get("unit_id"),
|
||||
"route_id": route_id,
|
||||
"type": row.get("category"),
|
||||
"description": row.get("description"),
|
||||
"driver_name": driver_name,
|
||||
"status": row.get("status") or "open",
|
||||
"photo_url": row.get("photo_url"),
|
||||
"created_at": row.get("created_at") and str(row["created_at"]),
|
||||
}
|
||||
|
||||
|
||||
@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")
|
||||
|
||||
try:
|
||||
res = (
|
||||
supabase_admin.table("incidents")
|
||||
.select("id, unit_id, user_id, category, description, status, photo_url, created_at")
|
||||
.eq("unit_id", unit_id)
|
||||
.order("created_at", desc=True)
|
||||
.execute()
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Error al listar incidencias: {e}")
|
||||
|
||||
rows = res.data or []
|
||||
route_id = _route_for_unit(unit_id)
|
||||
|
||||
# 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)
|
||||
|
||||
return [_serialize_incident(r, route_id, driver_name) for r in rows]
|
||||
|
||||
|
||||
@router.post("/units/{unit_id}/incidents", status_code=201)
|
||||
def create_unit_incident(
|
||||
unit_id: int,
|
||||
body: dict,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Crea una incidencia para esta unidad. El admin queda como reporter."""
|
||||
category = (body.get("type") or body.get("category") or "").strip()
|
||||
description = (body.get("description") or "").strip()
|
||||
|
||||
if category not in _INCIDENT_CATEGORIES:
|
||||
raise HTTPException(
|
||||
400,
|
||||
f"Categoría inválida. Use una de: {sorted(_INCIDENT_CATEGORIES)}",
|
||||
)
|
||||
if len(description) < 3:
|
||||
# La tabla requiere description NOT NULL y un mínimo razonable.
|
||||
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")
|
||||
|
||||
payload = {
|
||||
"user_id": current_user["user_id"],
|
||||
"unit_id": unit_id,
|
||||
"category": category,
|
||||
"description": description,
|
||||
}
|
||||
try:
|
||||
res = supabase_admin.table("incidents").insert(payload).execute()
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Error al crear la incidencia: {e}")
|
||||
|
||||
row = (res.data or [None])[0]
|
||||
if not row:
|
||||
raise HTTPException(500, "Supabase no devolvió la fila creada")
|
||||
|
||||
route_id = _route_for_unit(unit_id)
|
||||
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)
|
||||
return _serialize_incident(row, route_id, driver_name)
|
||||
|
||||
93
backend/app/api/feedback.py
Normal file
93
backend/app/api/feedback.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Endpoint de retroalimentación ciudadana.
|
||||
|
||||
Reglas:
|
||||
- El user_id se toma del token (nunca del body).
|
||||
- target_unit_id es la UNIDAD (camión), no el chofer.
|
||||
- La inserción usa supabase_admin (service_role) que bypassea RLS.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.core.deps import get_current_user
|
||||
from app.core.supabase_client import supabase_admin
|
||||
from app.schemas.feedback import FeedbackCreate, FeedbackOut
|
||||
|
||||
router = APIRouter(prefix="/feedback", tags=["feedback"])
|
||||
|
||||
|
||||
@router.post("", response_model=FeedbackOut, status_code=201)
|
||||
def create_feedback(
|
||||
body: FeedbackCreate,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
user_id = current_user["user_id"]
|
||||
|
||||
# Validar que la unidad exista si se envió
|
||||
if body.target_unit_id is not None:
|
||||
try:
|
||||
unit = (
|
||||
supabase_admin.table("units")
|
||||
.select("id")
|
||||
.eq("id", body.target_unit_id)
|
||||
.limit(1)
|
||||
.execute()
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error al validar unidad: {e}")
|
||||
if not unit.data:
|
||||
raise HTTPException(status_code=400, detail="Unidad inexistente")
|
||||
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"address_id": body.address_id,
|
||||
"type": body.type,
|
||||
"target_unit_id": body.target_unit_id,
|
||||
"message": body.message,
|
||||
"rating": body.rating,
|
||||
}
|
||||
|
||||
try:
|
||||
res = supabase_admin.table("feedback").insert(payload).execute()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error al guardar feedback: {e}")
|
||||
|
||||
row = (res.data or [{}])[0]
|
||||
return FeedbackOut(
|
||||
id=str(row.get("id")),
|
||||
user_id=str(row.get("user_id")),
|
||||
address_id=row.get("address_id") and str(row["address_id"]),
|
||||
type=row.get("type") or body.type,
|
||||
target_unit_id=row.get("target_unit_id"),
|
||||
message=row.get("message"),
|
||||
rating=row.get("rating"),
|
||||
created_at=row.get("created_at") and str(row["created_at"]),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me", response_model=list[FeedbackOut])
|
||||
def my_feedback(current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
res = (
|
||||
supabase_admin.table("feedback")
|
||||
.select("*")
|
||||
.eq("user_id", current_user["user_id"])
|
||||
.order("created_at", desc=True)
|
||||
.execute()
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error al listar feedback: {e}")
|
||||
|
||||
out: list[FeedbackOut] = []
|
||||
for row in res.data or []:
|
||||
out.append(
|
||||
FeedbackOut(
|
||||
id=str(row.get("id")),
|
||||
user_id=str(row.get("user_id")),
|
||||
address_id=row.get("address_id") and str(row["address_id"]),
|
||||
type=row.get("type") or "",
|
||||
target_unit_id=row.get("target_unit_id"),
|
||||
message=row.get("message"),
|
||||
rating=row.get("rating"),
|
||||
created_at=row.get("created_at") and str(row["created_at"]),
|
||||
)
|
||||
)
|
||||
return out
|
||||
Reference in New Issue
Block a user