Files
2026-05-23 09:30:38 -06:00
..
2026-05-22 14:33:12 -06:00
2026-05-22 14:33:12 -06:00
2026-05-23 05:00:54 -06:00
2026-05-22 14:33:12 -06:00
2026-05-23 09:30:38 -06:00
2026-05-22 14:33:12 -06:00
2026-05-22 14:33:12 -06:00
2026-05-23 07:41:56 -06:00
2026-05-22 14:33:12 -06:00
2026-05-23 00:07:00 -06:00
2026-05-22 14:33:12 -06:00
2026-05-23 07:34:21 -06:00
2026-05-23 07:34:21 -06:00
2026-05-23 09:30:38 -06:00
2026-05-22 14:33:12 -06:00

Frontend — OptiRuta

App móvil en Expo SDK 56 + React Native 0.85 + TypeScript con expo-router.

Para el README general, ver ../README.md. Para la guía de integración con el backend (compañeros), ver INTEGRATION.md.


Tabla de contenidos


Stack

  • Expo SDK 56 (Sept 2025)
  • React Native 0.85 + React 19
  • TypeScript estricto
  • expo-router 56 — navegación basada en archivos
  • @expo/vector-icons — iconos Ionicons
  • expo-image — imágenes optimizadas
  • Sin librerías de estado externas (solo Context)
  • Sin librerías de UI externas (StyleSheet nativo)

Estructura

frontend/
├── app.json                    ← config de Expo (name="OptiRuta", slug, etc.)
├── tsconfig.json               ← paths "@/*" → "./src/*"
├── assets/
│   ├── images/                 ← icons del SO, splash
│   └── illustrations/          ← ilustraciones del UI (camión, botes, etc.)
└── src/
    ├── app/                    ← rutas de expo-router (cada archivo = pantalla)
    │   ├── _layout.tsx         ← Tabs + AppProvider + Toast
    │   ├── index.tsx           ← Home (ETA + estado de ruta)
    │   ├── login.tsx
    │   ├── register.tsx
    │   ├── addresses.tsx       ← validación de domicilio
    │   ├── alerts.tsx          ← timeline de notificaciones
    │   ├── feedback.tsx        ← buzón de retro
    │   ├── guide.tsx           ← calendario + guía de separación
    │   ├── admin.tsx           ← panel admin
    │   └── profile.tsx         ← perfil + ajustes
    ├── components/
    │   ├── EtaCard.tsx
    │   ├── Alertltem.tsx       ← (sic) typo histórico, no romper
    │   ├── InputField.tsx
    │   ├── PrimaryButton.tsx
    │   ├── QuickAction.tsx
    │   ├── SectionTitle.tsx
    │   ├── StatusBadge.tsx
    │   ├── CollectionCalendar.tsx
    │   └── NotificationToast.tsx
    ├── config/
    │   └── api.ts              ← URL del backend (cambia según dispositivo)
    ├── constants/
    │   ├── colors.ts
    │   └── theme.ts
    ├── context/
    │   └── AppContext.tsx      ← estado global + polling
    ├── data/
    │   └── mocks/
    │       └── routes.mock.ts  ← catálogo de routeId/name (para el calendario)
    ├── hooks/
    ├── lib/
    │   ├── api.ts              ← apiFetch (wrapper de fetch + auth token)
    │   ├── notifications.ts    ← (preparado para Dev Build; no se importa en Go)
    │   └── notification-mapper.ts
    └── services/
        ├── auth.service.ts
        ├── tracking.service.ts
        ├── addresses.service.ts
        ├── feedback.service.ts
        └── admin.service.ts

Cómo correr

cd frontend
npm install
npx expo start -c        # -c limpia caché de Metro

Después:

  • a → Android emulator
  • i → iOS simulator (solo Mac)
  • w → web
  • Escanear QR con Expo Go en celular físico

Configurar la URL del backend

Edita src/config/api.ts:

// Para Android emulator local:
export const API_URL = "http://10.0.2.2:8080";

// Para celular físico (mismo Wi-Fi o hotspot):
// export const API_URL = "http://192.168.X.X:8080";
Entorno URL
Android Studio emulator http://10.0.2.2:8080
iOS Simulator http://localhost:8080
Celular físico Android/iOS (mismo Wi-Fi) http://<IP-LAN>:8080
Celular físico (hotspot) http://<IP-LAN-hotspot>:8080
Web http://localhost:8080

Si estás en red de escuela/oficina con AP isolation, usa hotspot del celular.


Pantallas

Ruta Archivo Rol Descripción
/ index.tsx USER Home: título + hero del camión + card ETA + banner estado de ruta + banner preventivo + 4 acciones rápidas + consejo
/login login.tsx público Login con email + password
/register register.tsx público Registro con nombre, email, password ≥ 6 caracteres
/addresses addresses.tsx USER Lista de colonias + selector + guardar (PUT /me)
/alerts alerts.tsx USER Hero + estado de ruta + timeline vertical con dots de color por severidad
/guide guide.tsx USER Calendario semanal + 4 cards (Orgánicos, Reciclables, Sanitarios, Especiales) + Recuerda
/feedback feedback.tsx USER Tipo de mensaje (4 opciones) + estrellas si es rating + textarea
/profile profile.tsx USER Avatar circular + cards Tu zona/Ruta + opciones (Mi domicilio, Buzón, Reiniciar demo, Ayuda, Cerrar sesión) + Tu impacto
/admin admin.tsx ADMIN Reportes del día + lista de rutas + cancelar/reanudar

Estado global (AppContext)

src/context/AppContext.tsx

Proveedor único que expone:

interface AppContextValue {
  user: AuthUser | null;            // { id, name, email, role }
  eta: EtaResult | null;
  notifications: InboxNotification[];
  route: RouteState | null;
  loading: boolean;
  toast: InboxNotification | null;
  dismissToast: () => void;
  login: (email, password) => Promise<void>;
  register: (name, email, password) => Promise<void>;
  logout: () => void;
  refreshStatus: () => Promise<void>;
}

Polling

  • Arranca cuando user ≠ null Y user.role ≠ "ADMIN"
  • Cada 30 segundos llama GET /api/tracking/status
  • Limpia el interval cuando el user cierra sesión
  • Detecta notificaciones nuevas (vía Set de IDs ya vistos) y dispara el toast

Logout

  • Borra token con setAuthToken(null)
  • Resetea user, eta, route, notifications, toast
  • Limpia el Set de IDs ya vistos

Comunicación con el backend

lib/api.ts

let authToken: string | null = null;

export const setAuthToken = (token: string | null) => { authToken = token; };

export const apiFetch = async <T>(path: string, options: RequestInit = {}): Promise<T> => {
  const headers = { "Content-Type": "application/json", ...options.headers };
  if (authToken) headers.Authorization = `Bearer ${authToken}`;
  const res = await fetch(`${API_URL}${path}`, { ...options, headers });
  const body = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
  return body as T;
};

El token se setea automáticamente tras login o register (dentro de auth.service.ts).

Services (capa fina sobre apiFetch)

  • auth.service.ts — register, login, getMe
  • tracking.service.ts — getMyStatus, sendGpsUpdate, resetDemo
  • addresses.service.ts — listColonias, getMyAddress, setMyAddress
  • feedback.service.ts — submitFeedback, listMyFeedback
  • admin.service.ts — listAllRoutes, cancelRoute, resumeRoute

Componentes reutilizables

Componente Para qué
EtaCard Card grande del ETA con "X min" gigante + ventana
AlertItem (Alertltem.tsx) Item de notificación con icon + título + body + tiempo + badge por tipo
InputField Wrap de TextInput con estilos del proyecto
PrimaryButton Botón principal verde
QuickAction Card cuadrada con icon circular + título (usada en home)
SectionTitle Título grande de sección
StatusBadge (no usado actualmente)
CollectionCalendar Calendario mensual con colores por día según ruta del user
NotificationToast Banner verde animado que cae cuando llega una notif nueva

Roles y navegación

USER

  • Ve las tabs: Inicio, Alertas, Guía, Perfil
  • Polling activo
  • Al loguearse, lo redirige a / (Home)
  • Si entra a / sin login → redirect a /login
  • Si entra a una pantalla protegida sin domicilio validado → algunas muestran CTA "Validar domicilio"

ADMIN

  • Ve solo la tab: Admin
  • Las demás tabs del user están ocultas (href: null)
  • NO hace polling de /status
  • Al loguearse desde /login aterriza en / y desde ahí redirect a /admin
  • Sus acciones (cancelar / reanudar) refrescan el listado cada 15 s

Notificaciones

En foreground (Toast in-app)

  • Cuando llega una notificación nueva del polling, se setea toast en el AppContext
  • El componente NotificationToast anima un banner verde que cae desde arriba
  • Se autodescarta a los 5 segundos
  • Funciona en cualquier pantalla porque vive en _layout.tsx

Push del SO con app cerrada

  • No funciona en Expo Go SDK 53+ — Expo removió el soporte
  • El módulo expo-notifications está instalado y el helper lib/notifications.ts está listo
  • Para activarlo en producción → crear Development Build con EAS

Troubleshooting

Síntoma Causa Fix
Network request failed API_URL apunta a dirección inalcanzable desde el dispositivo Usa 10.0.2.2:8080 (emulador) o IP LAN (físico)
Failed to fetch en navegador web CORS Activar cors() middleware en el backend
401 al cargar /status Token vencido o no enviado Logout + login otra vez
"Cannot find module ../config/api.js" Importaste con .js (estilo backend) En frontend, importa sin extensión: from "../config/api"
Unable to resolve module Metro caché stale npx expo start -c
App muestra "12 min / En Camino" hardcoded Te perdiste de actualizar EtaCard con props del backend Verificar que home pasa minutes y status
Versión de SDK incompatible Expo Go diferente al del proyecto (56) Actualizar Expo Go en App Store / Play Store
Tab Admin no aparece Login devolvió role ≠ "ADMIN" Verifica logueado con admin@test.com / admin123

Para extender

  • Nueva pantalla: crea archivo en src/app/X.tsx. expo-router la detecta. Si no quieres que aparezca en el tab bar, agrégala con options={{ href: null }} en _layout.tsx.
  • Nuevo service: crea services/X.service.ts que use apiFetch. Importa donde lo necesites.
  • Nuevo dato global: agrega al AppContext y expón en el value.
  • Imágenes nuevas: ponlas en assets/illustrations/ y úsalas con require().

Frase de arquitectura para la exposición

"El frontend hace polling controlado cada 30 s al endpoint /api/tracking/status. Esto evita sobrecargar el servidor y es predecible. El AppContext detecta notificaciones nuevas con un Set de IDs ya vistos y dispara un Toast in-app. Para push del SO con app cerrada migraríamos a Development Build y expo-notifications real, pero la decisión de Expo Go nos limita en esta etapa."