Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>
Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.githu b.com> correcion de errores en llenado de tablas, primeras vistas frontend
This commit is contained in:
@@ -1,40 +1,54 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from typing import Optional
|
|
||||||
from app.services import simulation
|
from app.services import simulation
|
||||||
|
from app.core.deps import get_current_user
|
||||||
|
from app.core.supabase_client import supabase_admin
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(tags=["eta"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/eta")
|
@router.get("/eta")
|
||||||
def get_eta(colonia: Optional[str] = None, routeId: Optional[str] = None):
|
def get_eta(
|
||||||
# Resolver routeId a partir de colonia si es necesario
|
address_id: str,
|
||||||
if routeId is None:
|
current_user: dict = Depends(get_current_user),
|
||||||
if colonia is None:
|
):
|
||||||
raise HTTPException(status_code=400, detail="colonia or routeId required")
|
"""
|
||||||
mapping = simulation.get_colonias()
|
ETA para el ciudadano.
|
||||||
match = next((c for c in mapping if c.get("colonia","").lower() == colonia.lower()), None)
|
- Recibe `address_id` (domicilio del usuario autenticado).
|
||||||
if not match:
|
- Valida que el domicilio pertenezca al usuario (túnel de privacidad).
|
||||||
raise HTTPException(status_code=404, detail="colonia not found")
|
- Devuelve SOLO texto: nunca coordenadas ni routeId completo.
|
||||||
routeId = match["routeId"]
|
"""
|
||||||
|
result = (
|
||||||
|
supabase_admin.table("addresses")
|
||||||
|
.select("route_id, user_id")
|
||||||
|
.eq("id", address_id)
|
||||||
|
.maybe_single()
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result.data:
|
||||||
|
raise HTTPException(status_code=404, detail="Domicilio no encontrado")
|
||||||
|
|
||||||
|
address = result.data
|
||||||
|
|
||||||
|
# Túnel: ciudadano solo puede consultar sus propios domicilios
|
||||||
|
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"]
|
||||||
|
pos = simulation.get_route_position(route_id)
|
||||||
|
status = simulation.get_route_status(route_id)
|
||||||
|
|
||||||
pos = simulation.get_route_position(routeId)
|
|
||||||
status = simulation.get_route_status(routeId)
|
|
||||||
if pos is None:
|
if pos is None:
|
||||||
raise HTTPException(status_code=404, detail="route not found")
|
raise HTTPException(status_code=503, detail="Simulación no disponible")
|
||||||
|
|
||||||
if pos < 4:
|
if pos < 4:
|
||||||
mensaje = "El camión va en camino a tu sector"
|
mensaje = "El camión va en camino a tu sector"
|
||||||
elif pos == 4:
|
elif pos == 4:
|
||||||
mensaje = "Llega en aproximadamente 15 minutos"
|
mensaje = "Llega en aproximadamente 15 minutos — saca tus bolsas"
|
||||||
elif pos < 8:
|
elif pos < 8:
|
||||||
mensaje = "Está atendiendo tu zona; saca tus bolsas"
|
mensaje = "Está atendiendo tu zona; prepara tus bolsas"
|
||||||
else:
|
else:
|
||||||
mensaje = "Servicio del día finalizado"
|
mensaje = "Servicio del día finalizado. Mañana continuamos"
|
||||||
|
|
||||||
return {"mensaje": mensaje, "status": status, "routeId": routeId}
|
# ⚠️ Nunca devolver coordenadas ni el routeId al ciudadano
|
||||||
|
return {"mensaje": mensaje, "status": status}
|
||||||
|
|
||||||
@router.post("/simulate/tick")
|
|
||||||
def simulate_tick():
|
|
||||||
events = simulation.tick()
|
|
||||||
return {"events": events}
|
|
||||||
|
|||||||
64
backend/app/api/simulation.py
Normal file
64
backend/app/api/simulation.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from app.services import simulation
|
||||||
|
from app.core.deps import require_role
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/simulation", tags=["simulation"])
|
||||||
|
|
||||||
|
# Solo admin puede ver el estado real de la simulación (incluye routeId y posición)
|
||||||
|
_require_admin = require_role("admin")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/state")
|
||||||
|
def get_state(_: dict = Depends(_require_admin)):
|
||||||
|
"""
|
||||||
|
Estado actual de la simulación: positionId y status de cada ruta.
|
||||||
|
⚠️ No devuelve coordenadas — solo el índice (1–8) y el status de texto.
|
||||||
|
"""
|
||||||
|
colonias = simulation.get_colonias()
|
||||||
|
route_ids = {c["routeId"] for c in colonias}
|
||||||
|
|
||||||
|
state = []
|
||||||
|
for route_id in sorted(route_ids):
|
||||||
|
pos = simulation.get_route_position(route_id)
|
||||||
|
status = simulation.get_route_status(route_id)
|
||||||
|
|
||||||
|
# Derivar mensaje ETA sin coordenadas
|
||||||
|
if pos is None:
|
||||||
|
mensaje = "Ruta no cargada"
|
||||||
|
elif pos < 4:
|
||||||
|
mensaje = "En camino al sector"
|
||||||
|
elif pos == 4:
|
||||||
|
mensaje = "Llega en ~15 min"
|
||||||
|
elif pos < 8:
|
||||||
|
mensaje = "Atendiendo la zona"
|
||||||
|
else:
|
||||||
|
mensaje = "Servicio finalizado"
|
||||||
|
|
||||||
|
state.append({
|
||||||
|
"routeId": route_id,
|
||||||
|
"positionId": pos,
|
||||||
|
"status": status,
|
||||||
|
"etaMensaje": mensaje,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"routes": state, "totalRoutes": len(state)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/tick")
|
||||||
|
def manual_tick(_: dict = Depends(_require_admin)):
|
||||||
|
"""Avanza manualmente un tick en todas las rutas (útil para la demo)."""
|
||||||
|
events = simulation.tick()
|
||||||
|
return {"events": events, "total": len(events)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reset")
|
||||||
|
def reset_simulation(_: dict = Depends(_require_admin)):
|
||||||
|
"""Reinicia todas las rutas a positionId=1 (para repetir la demo)."""
|
||||||
|
simulation.start_simulation_state()
|
||||||
|
return {"message": "Simulación reiniciada. Todas las rutas están en posición 1."}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/events")
|
||||||
|
def get_events(_: dict = Depends(_require_admin)):
|
||||||
|
"""Últimos 20 eventos disparados por la simulación."""
|
||||||
|
return {"events": simulation.get_last_events()}
|
||||||
@@ -1,40 +1,28 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
from supabase import create_client, Client
|
from supabase import create_client, Client
|
||||||
|
|
||||||
# Configuración de directorios base
|
BASE_DIR = Path(__file__).parent.parent # backend/app/
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
DATA_DIR = BASE_DIR / "data"
|
||||||
DATA_DIR = os.path.join(BASE_DIR, "data")
|
ENV_PATH = BASE_DIR.parent / ".env" # backend/.env
|
||||||
ENV_PATH = os.path.join(os.path.dirname(BASE_DIR), ".env")
|
|
||||||
|
|
||||||
def load_env(path: str):
|
|
||||||
"""Carga variables de entorno de forma manual sin depender de python-dotenv"""
|
|
||||||
if not os.path.exists(path):
|
|
||||||
print(f"Advertencia: No se encontró el archivo {path}")
|
|
||||||
return
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith('#'):
|
|
||||||
# Separa por el primer '=' y limpia espacios y comillas
|
|
||||||
key, val = line.split('=', 1)
|
|
||||||
os.environ[key.strip()] = val.strip().strip("'").strip('"')
|
|
||||||
|
|
||||||
def load_json(filename: str):
|
def load_json(filename: str):
|
||||||
path = os.path.join(DATA_DIR, filename)
|
with open(DATA_DIR / filename, encoding="utf-8") as f:
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("Iniciando proceso de seeding...")
|
print("Iniciando proceso de seeding...")
|
||||||
load_env(ENV_PATH)
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
# Es crucial usar SUPABASE_SERVICE_ROLE_KEY para saltar el RLS durante el Seed
|
|
||||||
URL = os.environ.get("SUPABASE_URL")
|
URL = os.environ.get("SUPABASE_URL")
|
||||||
KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
|
KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
|
||||||
|
|
||||||
if not URL or not KEY:
|
if not URL or not KEY:
|
||||||
raise ValueError("Error: Asegúrate de tener SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY en el .env")
|
raise ValueError("Falta SUPABASE_URL o SUPABASE_SERVICE_ROLE_KEY en el .env")
|
||||||
|
|
||||||
supabase: Client = create_client(URL, KEY)
|
supabase: Client = create_client(URL, KEY)
|
||||||
|
|
||||||
@@ -99,7 +87,7 @@ def main():
|
|||||||
"turno": c["turno"],
|
"turno": c["turno"],
|
||||||
"horario_estimado": c["horario_estimado"]
|
"horario_estimado": c["horario_estimado"]
|
||||||
})
|
})
|
||||||
supabase.table("colonias").upsert(colonias_to_insert).execute()
|
supabase.table("colonias").upsert(colonias_to_insert, on_conflict="nombre").execute()
|
||||||
|
|
||||||
print("✅ Seed completado con éxito. Base de datos operativa para la app.")
|
print("✅ Seed completado con éxito. Base de datos operativa para la app.")
|
||||||
|
|
||||||
|
|||||||
93
backend/app/db/tables.sql
Normal file
93
backend/app/db/tables.sql
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Tablas del dominio — Sistema de Recolección Inteligente
|
||||||
|
-- Ejecutar ANTES que rls_policies.sql
|
||||||
|
-- Supabase > SQL Editor
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 1. UNITS (camiones recolectores)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.units (
|
||||||
|
id INT PRIMARY KEY, -- truckId del JSON (101, 103, …)
|
||||||
|
plate TEXT,
|
||||||
|
status TEXT DEFAULT 'active'
|
||||||
|
CHECK (status IN ('active', 'inactive', 'maintenance'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 2. ROUTES (6 rutas MVP)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.routes (
|
||||||
|
id TEXT PRIMARY KEY, -- 'RUTA-01' … 'RUTA-13'
|
||||||
|
name TEXT,
|
||||||
|
truck_id INT REFERENCES public.units(id),
|
||||||
|
turno TEXT CHECK (turno IN ('matutino', 'vespertino', 'Matutino', 'Vespertino')),
|
||||||
|
status TEXT DEFAULT 'pendiente'
|
||||||
|
CHECK (status IN ('pendiente','en_ruta','completada','diferida','reasignada')),
|
||||||
|
current_position_id INT DEFAULT 1 CHECK (current_position_id BETWEEN 1 AND 8)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 3. ROUTE_POSITIONS (8 posiciones por ruta — solo admin)
|
||||||
|
-- ⚠️ Contiene lat/lng. NUNCA se devuelve al ciudadano.
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.route_positions (
|
||||||
|
route_id TEXT REFERENCES public.routes(id) ON DELETE CASCADE,
|
||||||
|
position_id INT,
|
||||||
|
lat DOUBLE PRECISION,
|
||||||
|
lng DOUBLE PRECISION,
|
||||||
|
speed INT,
|
||||||
|
ts TIMESTAMPTZ,
|
||||||
|
PRIMARY KEY (route_id, position_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 4. COLONIAS (mapeo colonia → ruta; sin coordenadas)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.colonias (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
nombre TEXT NOT NULL,
|
||||||
|
route_id TEXT REFERENCES public.routes(id),
|
||||||
|
turno TEXT,
|
||||||
|
horario_estimado TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 5. DRIVERS (chofer → unidad; 1:1)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.drivers (
|
||||||
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
user_id UUID REFERENCES public.users(id) ON DELETE CASCADE,
|
||||||
|
unit_id INT REFERENCES public.units(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 6. COLLECTION_EVENTS (registro de recolección por domicilio)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.collection_events (
|
||||||
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
route_id TEXT REFERENCES public.routes(id),
|
||||||
|
address_id UUID REFERENCES public.addresses(id) ON DELETE CASCADE,
|
||||||
|
status TEXT,
|
||||||
|
collected_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 7. ROUTE_CHANGES (historial de reasignaciones / retrasos)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.route_changes (
|
||||||
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
route_id TEXT REFERENCES public.routes(id),
|
||||||
|
from_unit INT REFERENCES public.units(id),
|
||||||
|
to_unit INT REFERENCES public.units(id),
|
||||||
|
reason TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Índices de rendimiento
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_route_positions_route ON public.route_positions(route_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_colonias_route ON public.colonias(route_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_drivers_user ON public.drivers(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_collection_route ON public.collection_events(route_id);
|
||||||
@@ -8,6 +8,7 @@ from app.api.eta import router as eta_router
|
|||||||
from app.api.auth import router as auth_router
|
from app.api.auth import router as auth_router
|
||||||
from app.api.addresses import router as addresses_router
|
from app.api.addresses import router as addresses_router
|
||||||
from app.api.colonias import router as colonias_router
|
from app.api.colonias import router as colonias_router
|
||||||
|
from app.api.simulation import router as simulation_router
|
||||||
from app.services import simulation, notifications
|
from app.services import simulation, notifications
|
||||||
|
|
||||||
scheduler = AsyncIOScheduler()
|
scheduler = AsyncIOScheduler()
|
||||||
@@ -57,6 +58,7 @@ app.include_router(auth_router)
|
|||||||
app.include_router(addresses_router)
|
app.include_router(addresses_router)
|
||||||
app.include_router(eta_router)
|
app.include_router(eta_router)
|
||||||
app.include_router(colonias_router)
|
app.include_router(colonias_router)
|
||||||
|
app.include_router(simulation_router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|||||||
52
recolecta_app/lib/core/models/address_create_request.dart
Normal file
52
recolecta_app/lib/core/models/address_create_request.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
class AddressCreateRequest {
|
||||||
|
const AddressCreateRequest({
|
||||||
|
required this.label,
|
||||||
|
required this.street,
|
||||||
|
required this.colonia,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String street;
|
||||||
|
final String colonia;
|
||||||
|
|
||||||
|
factory AddressCreateRequest.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AddressCreateRequest(
|
||||||
|
label: _pickString(<dynamic>[json['label'], json['alias']]) ?? '',
|
||||||
|
street: _pickString(<dynamic>[json['calle'], json['street']]) ?? '',
|
||||||
|
colonia: _pickString(<dynamic>[json['colonia'], json['colony']]) ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'label': label,
|
||||||
|
'calle': street,
|
||||||
|
'colonia': colonia,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AddressCreateRequest copyWith({
|
||||||
|
String? label,
|
||||||
|
String? street,
|
||||||
|
String? colonia,
|
||||||
|
}) {
|
||||||
|
return AddressCreateRequest(
|
||||||
|
label: label ?? this.label,
|
||||||
|
street: street ?? this.street,
|
||||||
|
colonia: colonia ?? this.colonia,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _pickString(Iterable<dynamic> candidates) {
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
if (candidate is String && candidate.isNotEmpty) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if (candidate != null && candidate.toString().isNotEmpty) {
|
||||||
|
return candidate.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
135
recolecta_app/lib/core/models/address_response.dart
Normal file
135
recolecta_app/lib/core/models/address_response.dart
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
class AddressResponse {
|
||||||
|
const AddressResponse({
|
||||||
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
required this.label,
|
||||||
|
required this.street,
|
||||||
|
required this.colonia,
|
||||||
|
required this.routeId,
|
||||||
|
required this.verified,
|
||||||
|
this.verifiedMethod,
|
||||||
|
this.verifiedAt,
|
||||||
|
this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String userId;
|
||||||
|
final String label;
|
||||||
|
final String street;
|
||||||
|
final String colonia;
|
||||||
|
final String routeId;
|
||||||
|
final bool verified;
|
||||||
|
final String? verifiedMethod;
|
||||||
|
final String? verifiedAt;
|
||||||
|
final String? createdAt;
|
||||||
|
|
||||||
|
factory AddressResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
final payload = json['data'] is Map<String, dynamic>
|
||||||
|
? json['data'] as Map<String, dynamic>
|
||||||
|
: json;
|
||||||
|
|
||||||
|
return AddressResponse(
|
||||||
|
id: _pickString(<dynamic>[payload['id'], json['id']]) ?? '',
|
||||||
|
userId:
|
||||||
|
_pickString(<dynamic>[
|
||||||
|
payload['user_id'],
|
||||||
|
payload['userId'],
|
||||||
|
json['user_id'],
|
||||||
|
json['userId'],
|
||||||
|
]) ??
|
||||||
|
'',
|
||||||
|
label: _pickString(<dynamic>[payload['label'], json['label']]) ?? '',
|
||||||
|
street:
|
||||||
|
_pickString(<dynamic>[
|
||||||
|
payload['calle'],
|
||||||
|
payload['street'],
|
||||||
|
json['calle'],
|
||||||
|
json['street'],
|
||||||
|
]) ??
|
||||||
|
'',
|
||||||
|
colonia:
|
||||||
|
_pickString(<dynamic>[
|
||||||
|
payload['colonia'],
|
||||||
|
payload['colony'],
|
||||||
|
json['colonia'],
|
||||||
|
json['colony'],
|
||||||
|
]) ??
|
||||||
|
'',
|
||||||
|
routeId:
|
||||||
|
_pickString(<dynamic>[
|
||||||
|
payload['route_id'],
|
||||||
|
payload['routeId'],
|
||||||
|
json['route_id'],
|
||||||
|
json['routeId'],
|
||||||
|
]) ??
|
||||||
|
'',
|
||||||
|
verified:
|
||||||
|
_pickBool(<dynamic>[payload['verified'], json['verified']]) ?? false,
|
||||||
|
verifiedMethod: _pickString(<dynamic>[
|
||||||
|
payload['verified_method'],
|
||||||
|
payload['verifiedMethod'],
|
||||||
|
json['verified_method'],
|
||||||
|
json['verifiedMethod'],
|
||||||
|
]),
|
||||||
|
verifiedAt: _pickString(<dynamic>[
|
||||||
|
payload['verified_at'],
|
||||||
|
payload['verifiedAt'],
|
||||||
|
json['verified_at'],
|
||||||
|
json['verifiedAt'],
|
||||||
|
]),
|
||||||
|
createdAt: _pickString(<dynamic>[
|
||||||
|
payload['created_at'],
|
||||||
|
payload['createdAt'],
|
||||||
|
json['created_at'],
|
||||||
|
json['createdAt'],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'user_id': userId,
|
||||||
|
'label': label,
|
||||||
|
'calle': street,
|
||||||
|
'colonia': colonia,
|
||||||
|
'route_id': routeId,
|
||||||
|
'verified': verified,
|
||||||
|
'verified_method': verifiedMethod,
|
||||||
|
'verified_at': verifiedAt,
|
||||||
|
'created_at': createdAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _pickString(Iterable<dynamic> candidates) {
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
if (candidate is String && candidate.isNotEmpty) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if (candidate != null && candidate.toString().isNotEmpty) {
|
||||||
|
return candidate.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool? _pickBool(Iterable<dynamic> candidates) {
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
if (candidate is bool) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if (candidate is String) {
|
||||||
|
final normalized = candidate.toLowerCase();
|
||||||
|
if (normalized == 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (normalized == 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,24 +14,50 @@ class Colonia {
|
|||||||
final String? turno;
|
final String? turno;
|
||||||
|
|
||||||
factory Colonia.fromJson(Map<String, dynamic> json) {
|
factory Colonia.fromJson(Map<String, dynamic> json) {
|
||||||
final rawId =
|
final rawNombre = _pickString(<dynamic>[
|
||||||
json['id'] ??
|
json['nombre'],
|
||||||
json['routeId'] ??
|
json['colonia'],
|
||||||
json['route_id'] ??
|
json['name'],
|
||||||
json['nombre'] ??
|
]);
|
||||||
json['name'];
|
final rawRouteId = _pickString(<dynamic>[
|
||||||
final rawNombre = json['nombre'] ?? json['name'] ?? rawId;
|
json['routeId'],
|
||||||
|
json['route_id'],
|
||||||
|
]);
|
||||||
|
final rawId = _pickString(<dynamic>[json['id'], rawRouteId, rawNombre]);
|
||||||
|
|
||||||
return Colonia(
|
return Colonia(
|
||||||
id: rawId?.toString() ?? rawNombre?.toString() ?? '',
|
id: rawId ?? '',
|
||||||
nombre: rawNombre?.toString() ?? '',
|
nombre: rawNombre ?? rawId ?? '',
|
||||||
routeId: (json['routeId'] ?? json['route_id'])?.toString(),
|
routeId: rawRouteId,
|
||||||
horarioEstimado:
|
horarioEstimado: _pickString(<dynamic>[
|
||||||
(json['horario_estimado'] ??
|
json['horario_estimado'],
|
||||||
json['horarioEstimado'] ??
|
json['horarioEstimado'],
|
||||||
json['schedule'])
|
json['schedule'],
|
||||||
?.toString(),
|
]),
|
||||||
turno: (json['turno'] ?? json['shift'])?.toString(),
|
turno: _pickString(<dynamic>[json['turno'], json['shift']]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'nombre': nombre,
|
||||||
|
'routeId': routeId,
|
||||||
|
'horario_estimado': horarioEstimado,
|
||||||
|
'turno': turno,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _pickString(Iterable<dynamic> candidates) {
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
if (candidate is String && candidate.isNotEmpty) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if (candidate != null && candidate.toString().isNotEmpty) {
|
||||||
|
return candidate.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
recolecta_app/lib/core/models/login_response.dart
Normal file
101
recolecta_app/lib/core/models/login_response.dart
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import 'user.dart';
|
||||||
|
|
||||||
|
class LoginResponse {
|
||||||
|
const LoginResponse({
|
||||||
|
required this.accessToken,
|
||||||
|
this.tokenType = 'bearer',
|
||||||
|
required this.userId,
|
||||||
|
required this.role,
|
||||||
|
this.routeId,
|
||||||
|
this.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String accessToken;
|
||||||
|
final String tokenType;
|
||||||
|
final String userId;
|
||||||
|
final String role;
|
||||||
|
final String? routeId;
|
||||||
|
final User? user;
|
||||||
|
|
||||||
|
factory LoginResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
final payload = json['data'] is Map<String, dynamic>
|
||||||
|
? json['data'] as Map<String, dynamic>
|
||||||
|
: json;
|
||||||
|
|
||||||
|
final userJson = payload['user'] is Map<String, dynamic>
|
||||||
|
? payload['user'] as Map<String, dynamic>
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return LoginResponse(
|
||||||
|
accessToken:
|
||||||
|
_pickString(<dynamic>[
|
||||||
|
payload['access_token'],
|
||||||
|
payload['token'],
|
||||||
|
payload['jwt'],
|
||||||
|
json['access_token'],
|
||||||
|
json['token'],
|
||||||
|
json['jwt'],
|
||||||
|
]) ??
|
||||||
|
'',
|
||||||
|
tokenType:
|
||||||
|
_pickString(<dynamic>[
|
||||||
|
payload['token_type'],
|
||||||
|
payload['tokenType'],
|
||||||
|
json['token_type'],
|
||||||
|
json['tokenType'],
|
||||||
|
]) ??
|
||||||
|
'bearer',
|
||||||
|
userId:
|
||||||
|
_pickString(<dynamic>[
|
||||||
|
payload['user_id'],
|
||||||
|
payload['userId'],
|
||||||
|
payload['id'],
|
||||||
|
json['user_id'],
|
||||||
|
json['userId'],
|
||||||
|
json['id'],
|
||||||
|
]) ??
|
||||||
|
'',
|
||||||
|
role:
|
||||||
|
_pickString(<dynamic>[
|
||||||
|
payload['role'],
|
||||||
|
payload['user_role'],
|
||||||
|
payload['userRole'],
|
||||||
|
json['role'],
|
||||||
|
json['user_role'],
|
||||||
|
json['userRole'],
|
||||||
|
]) ??
|
||||||
|
'citizen',
|
||||||
|
routeId: _pickString(<dynamic>[
|
||||||
|
payload['route_id'],
|
||||||
|
payload['routeId'],
|
||||||
|
json['route_id'],
|
||||||
|
json['routeId'],
|
||||||
|
]),
|
||||||
|
user: userJson == null ? null : User.fromJson(userJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'access_token': accessToken,
|
||||||
|
'token_type': tokenType,
|
||||||
|
'user_id': userId,
|
||||||
|
'role': role,
|
||||||
|
'route_id': routeId,
|
||||||
|
if (user != null) 'user': user!.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _pickString(Iterable<dynamic> candidates) {
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
if (candidate is String && candidate.isNotEmpty) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if (candidate != null && candidate.toString().isNotEmpty) {
|
||||||
|
return candidate.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
recolecta_app/lib/core/models/models.dart
Normal file
8
recolecta_app/lib/core/models/models.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export 'address.dart';
|
||||||
|
export 'address_create_request.dart';
|
||||||
|
export 'address_response.dart';
|
||||||
|
export 'auth_session.dart';
|
||||||
|
export 'auth_state.dart';
|
||||||
|
export 'colonia.dart';
|
||||||
|
export 'login_response.dart';
|
||||||
|
export 'user.dart';
|
||||||
50
recolecta_app/lib/core/models/user.dart
Normal file
50
recolecta_app/lib/core/models/user.dart
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
class User {
|
||||||
|
const User({
|
||||||
|
required this.id,
|
||||||
|
this.email,
|
||||||
|
this.phone,
|
||||||
|
required this.role,
|
||||||
|
this.routeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String? email;
|
||||||
|
final String? phone;
|
||||||
|
final String role;
|
||||||
|
final String? routeId;
|
||||||
|
|
||||||
|
factory User.fromJson(Map<String, dynamic> json) {
|
||||||
|
return User(
|
||||||
|
id:
|
||||||
|
_pickString(<dynamic>[json['id'], json['user_id'], json['userId']]) ??
|
||||||
|
'',
|
||||||
|
email: _pickString(<dynamic>[json['email'], json['mail']]),
|
||||||
|
phone: _pickString(<dynamic>[json['phone'], json['telefono']]),
|
||||||
|
role: _pickString(<dynamic>[json['role'], json['rol']]) ?? 'citizen',
|
||||||
|
routeId: _pickString(<dynamic>[json['routeId'], json['route_id']]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'email': email,
|
||||||
|
'phone': phone,
|
||||||
|
'role': role,
|
||||||
|
'routeId': routeId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _pickString(Iterable<dynamic> candidates) {
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
if (candidate is String && candidate.isNotEmpty) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if (candidate != null && candidate.toString().isNotEmpty) {
|
||||||
|
return candidate.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../core/models/address.dart';
|
import '../../core/models/address_create_request.dart';
|
||||||
import '../../core/models/colonia.dart';
|
import '../../core/models/colonia.dart';
|
||||||
import 'colonias_selector.dart';
|
import 'colonias_selector.dart';
|
||||||
|
|
||||||
@@ -36,10 +36,10 @@ class _NewAddressPageState extends ConsumerState<NewAddressPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final address = AddressModel(
|
final address = AddressCreateRequest(
|
||||||
label: _labelController.text.trim(),
|
label: _labelController.text.trim(),
|
||||||
street: _streetController.text.trim(),
|
street: _streetController.text.trim(),
|
||||||
colonia: _selectedColonia!,
|
colonia: _selectedColonia!.nombre,
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|||||||
@@ -39,20 +39,6 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final authState = ref.read(authControllerProvider).asData?.value;
|
|
||||||
if (authState?.userRole == 'admin') {
|
|
||||||
context.go('/admin');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (authState?.userRole == 'driver') {
|
|
||||||
context.go('/driver');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final routeId = authState?.routeId;
|
|
||||||
if (routeId != null && routeId.isNotEmpty) {
|
|
||||||
context.go('/home?routeId=$routeId');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.go('/home');
|
context.go('/home');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
|
|||||||
@@ -44,20 +44,6 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final authState = ref.read(authControllerProvider).asData?.value;
|
|
||||||
if (authState?.userRole == 'admin') {
|
|
||||||
context.go('/admin');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (authState?.userRole == 'driver') {
|
|
||||||
context.go('/driver');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final routeId = authState?.routeId;
|
|
||||||
if (routeId != null && routeId.isNotEmpty) {
|
|
||||||
context.go('/home?routeId=$routeId');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.go('/home');
|
context.go('/home');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
|
|||||||
Reference in New Issue
Block a user