diff --git a/backend/app/api/eta.py b/backend/app/api/eta.py index f993ffc..0258b9d 100644 --- a/backend/app/api/eta.py +++ b/backend/app/api/eta.py @@ -1,40 +1,54 @@ -from fastapi import APIRouter, HTTPException -from typing import Optional +from fastapi import APIRouter, Depends, HTTPException 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") -def get_eta(colonia: Optional[str] = None, routeId: Optional[str] = None): - # Resolver routeId a partir de colonia si es necesario - if routeId is None: - if colonia is None: - raise HTTPException(status_code=400, detail="colonia or routeId required") - mapping = simulation.get_colonias() - match = next((c for c in mapping if c.get("colonia","").lower() == colonia.lower()), None) - if not match: - raise HTTPException(status_code=404, detail="colonia not found") - routeId = match["routeId"] +def get_eta( + address_id: str, + current_user: dict = Depends(get_current_user), +): + """ + ETA para el ciudadano. + - Recibe `address_id` (domicilio del usuario autenticado). + - Valida que el domicilio pertenezca al usuario (túnel de privacidad). + - Devuelve SOLO texto: nunca coordenadas ni routeId completo. + """ + 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: - raise HTTPException(status_code=404, detail="route not found") + raise HTTPException(status_code=503, detail="Simulación no disponible") if pos < 4: mensaje = "El camión va en camino a tu sector" elif pos == 4: - mensaje = "Llega en aproximadamente 15 minutos" + mensaje = "Llega en aproximadamente 15 minutos — saca tus bolsas" elif pos < 8: - mensaje = "Está atendiendo tu zona; saca tus bolsas" + mensaje = "Está atendiendo tu zona; prepara tus bolsas" else: - mensaje = "Servicio del día finalizado" + mensaje = "Servicio del día finalizado. Mañana continuamos" - return {"mensaje": mensaje, "status": status, "routeId": routeId} - - -@router.post("/simulate/tick") -def simulate_tick(): - events = simulation.tick() - return {"events": events} + # ⚠️ Nunca devolver coordenadas ni el routeId al ciudadano + return {"mensaje": mensaje, "status": status} diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py new file mode 100644 index 0000000..f538cb4 --- /dev/null +++ b/backend/app/api/simulation.py @@ -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()} diff --git a/backend/app/db/seed.py b/backend/app/db/seed.py index 876cc29..d255df5 100644 --- a/backend/app/db/seed.py +++ b/backend/app/db/seed.py @@ -1,40 +1,28 @@ import json import os +from pathlib import Path +from dotenv import load_dotenv from supabase import create_client, Client -# Configuración de directorios base -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DATA_DIR = os.path.join(BASE_DIR, "data") -ENV_PATH = os.path.join(os.path.dirname(BASE_DIR), ".env") +BASE_DIR = Path(__file__).parent.parent # backend/app/ +DATA_DIR = BASE_DIR / "data" +ENV_PATH = BASE_DIR.parent / ".env" # backend/.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): - path = os.path.join(DATA_DIR, filename) - with open(path, "r", encoding="utf-8") as f: + with open(DATA_DIR / filename, encoding="utf-8") as f: return json.load(f) + def main(): 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") KEY = os.environ.get("SUPABASE_SERVICE_ROLE_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) @@ -99,7 +87,7 @@ def main(): "turno": c["turno"], "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.") diff --git a/backend/app/db/tables.sql b/backend/app/db/tables.sql new file mode 100644 index 0000000..94c97f2 --- /dev/null +++ b/backend/app/db/tables.sql @@ -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); diff --git a/backend/main.py b/backend/main.py index 6425803..1ace17f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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.addresses import router as addresses_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 scheduler = AsyncIOScheduler() @@ -57,6 +58,7 @@ app.include_router(auth_router) app.include_router(addresses_router) app.include_router(eta_router) app.include_router(colonias_router) +app.include_router(simulation_router) @app.get("/") diff --git a/recolecta_app/lib/core/models/address_create_request.dart b/recolecta_app/lib/core/models/address_create_request.dart new file mode 100644 index 0000000..bdbfb8e --- /dev/null +++ b/recolecta_app/lib/core/models/address_create_request.dart @@ -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 json) { + return AddressCreateRequest( + label: _pickString([json['label'], json['alias']]) ?? '', + street: _pickString([json['calle'], json['street']]) ?? '', + colonia: _pickString([json['colonia'], json['colony']]) ?? '', + ); + } + + Map toJson() { + return { + '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 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; + } +} diff --git a/recolecta_app/lib/core/models/address_response.dart b/recolecta_app/lib/core/models/address_response.dart new file mode 100644 index 0000000..4078e52 --- /dev/null +++ b/recolecta_app/lib/core/models/address_response.dart @@ -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 json) { + final payload = json['data'] is Map + ? json['data'] as Map + : json; + + return AddressResponse( + id: _pickString([payload['id'], json['id']]) ?? '', + userId: + _pickString([ + payload['user_id'], + payload['userId'], + json['user_id'], + json['userId'], + ]) ?? + '', + label: _pickString([payload['label'], json['label']]) ?? '', + street: + _pickString([ + payload['calle'], + payload['street'], + json['calle'], + json['street'], + ]) ?? + '', + colonia: + _pickString([ + payload['colonia'], + payload['colony'], + json['colonia'], + json['colony'], + ]) ?? + '', + routeId: + _pickString([ + payload['route_id'], + payload['routeId'], + json['route_id'], + json['routeId'], + ]) ?? + '', + verified: + _pickBool([payload['verified'], json['verified']]) ?? false, + verifiedMethod: _pickString([ + payload['verified_method'], + payload['verifiedMethod'], + json['verified_method'], + json['verifiedMethod'], + ]), + verifiedAt: _pickString([ + payload['verified_at'], + payload['verifiedAt'], + json['verified_at'], + json['verifiedAt'], + ]), + createdAt: _pickString([ + payload['created_at'], + payload['createdAt'], + json['created_at'], + json['createdAt'], + ]), + ); + } + + Map toJson() { + return { + '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 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 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; + } +} diff --git a/recolecta_app/lib/core/models/colonia.dart b/recolecta_app/lib/core/models/colonia.dart index e098ab3..e5f419b 100644 --- a/recolecta_app/lib/core/models/colonia.dart +++ b/recolecta_app/lib/core/models/colonia.dart @@ -14,24 +14,50 @@ class Colonia { final String? turno; factory Colonia.fromJson(Map json) { - final rawId = - json['id'] ?? - json['routeId'] ?? - json['route_id'] ?? - json['nombre'] ?? - json['name']; - final rawNombre = json['nombre'] ?? json['name'] ?? rawId; + final rawNombre = _pickString([ + json['nombre'], + json['colonia'], + json['name'], + ]); + final rawRouteId = _pickString([ + json['routeId'], + json['route_id'], + ]); + final rawId = _pickString([json['id'], rawRouteId, rawNombre]); return Colonia( - id: rawId?.toString() ?? rawNombre?.toString() ?? '', - nombre: rawNombre?.toString() ?? '', - routeId: (json['routeId'] ?? json['route_id'])?.toString(), - horarioEstimado: - (json['horario_estimado'] ?? - json['horarioEstimado'] ?? - json['schedule']) - ?.toString(), - turno: (json['turno'] ?? json['shift'])?.toString(), + id: rawId ?? '', + nombre: rawNombre ?? rawId ?? '', + routeId: rawRouteId, + horarioEstimado: _pickString([ + json['horario_estimado'], + json['horarioEstimado'], + json['schedule'], + ]), + turno: _pickString([json['turno'], json['shift']]), ); } + + Map toJson() { + return { + 'id': id, + 'nombre': nombre, + 'routeId': routeId, + 'horario_estimado': horarioEstimado, + 'turno': turno, + }; + } + + static String? _pickString(Iterable 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; + } } diff --git a/recolecta_app/lib/core/models/login_response.dart b/recolecta_app/lib/core/models/login_response.dart new file mode 100644 index 0000000..b1d353b --- /dev/null +++ b/recolecta_app/lib/core/models/login_response.dart @@ -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 json) { + final payload = json['data'] is Map + ? json['data'] as Map + : json; + + final userJson = payload['user'] is Map + ? payload['user'] as Map + : null; + + return LoginResponse( + accessToken: + _pickString([ + payload['access_token'], + payload['token'], + payload['jwt'], + json['access_token'], + json['token'], + json['jwt'], + ]) ?? + '', + tokenType: + _pickString([ + payload['token_type'], + payload['tokenType'], + json['token_type'], + json['tokenType'], + ]) ?? + 'bearer', + userId: + _pickString([ + payload['user_id'], + payload['userId'], + payload['id'], + json['user_id'], + json['userId'], + json['id'], + ]) ?? + '', + role: + _pickString([ + payload['role'], + payload['user_role'], + payload['userRole'], + json['role'], + json['user_role'], + json['userRole'], + ]) ?? + 'citizen', + routeId: _pickString([ + payload['route_id'], + payload['routeId'], + json['route_id'], + json['routeId'], + ]), + user: userJson == null ? null : User.fromJson(userJson), + ); + } + + Map toJson() { + return { + 'access_token': accessToken, + 'token_type': tokenType, + 'user_id': userId, + 'role': role, + 'route_id': routeId, + if (user != null) 'user': user!.toJson(), + }; + } + + static String? _pickString(Iterable 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; + } +} diff --git a/recolecta_app/lib/core/models/models.dart b/recolecta_app/lib/core/models/models.dart new file mode 100644 index 0000000..b3a7275 --- /dev/null +++ b/recolecta_app/lib/core/models/models.dart @@ -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'; diff --git a/recolecta_app/lib/core/models/user.dart b/recolecta_app/lib/core/models/user.dart new file mode 100644 index 0000000..57cc3bc --- /dev/null +++ b/recolecta_app/lib/core/models/user.dart @@ -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 json) { + return User( + id: + _pickString([json['id'], json['user_id'], json['userId']]) ?? + '', + email: _pickString([json['email'], json['mail']]), + phone: _pickString([json['phone'], json['telefono']]), + role: _pickString([json['role'], json['rol']]) ?? 'citizen', + routeId: _pickString([json['routeId'], json['route_id']]), + ); + } + + Map toJson() { + return { + 'id': id, + 'email': email, + 'phone': phone, + 'role': role, + 'routeId': routeId, + }; + } + + static String? _pickString(Iterable 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; + } +} diff --git a/recolecta_app/lib/features/addresses/new_address_page.dart b/recolecta_app/lib/features/addresses/new_address_page.dart index 5398699..4c08f92 100644 --- a/recolecta_app/lib/features/addresses/new_address_page.dart +++ b/recolecta_app/lib/features/addresses/new_address_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.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 'colonias_selector.dart'; @@ -36,10 +36,10 @@ class _NewAddressPageState extends ConsumerState { return; } - final address = AddressModel( + final address = AddressCreateRequest( label: _labelController.text.trim(), street: _streetController.text.trim(), - colonia: _selectedColonia!, + colonia: _selectedColonia!.nombre, ); ScaffoldMessenger.of(context).showSnackBar( diff --git a/recolecta_app/lib/features/auth/login_page.dart b/recolecta_app/lib/features/auth/login_page.dart index d3df2ad..ffe09c4 100644 --- a/recolecta_app/lib/features/auth/login_page.dart +++ b/recolecta_app/lib/features/auth/login_page.dart @@ -39,20 +39,6 @@ class _LoginPageState extends ConsumerState { if (!mounted) { 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'); } catch (error) { if (!mounted) { diff --git a/recolecta_app/lib/features/auth/register_page.dart b/recolecta_app/lib/features/auth/register_page.dart index 869b123..72ca14c 100644 --- a/recolecta_app/lib/features/auth/register_page.dart +++ b/recolecta_app/lib/features/auth/register_page.dart @@ -44,20 +44,6 @@ class _RegisterPageState extends ConsumerState { if (!mounted) { 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'); } catch (error) { if (!mounted) {