# Frontend — OptiRuta App móvil en **Expo SDK 56 + React Native 0.85 + TypeScript** con **expo-router**. > Para el README general, ver [../README.md](../README.md). > Para la guía de integración con el backend (compañeros), ver [INTEGRATION.md](INTEGRATION.md). --- ## Tabla de contenidos - [Stack](#stack) - [Estructura](#estructura) - [Cómo correr](#cómo-correr) - [Configurar la URL del backend](#configurar-la-url-del-backend) - [Pantallas](#pantallas) - [Estado global (AppContext)](#estado-global-appcontext) - [Comunicación con el backend](#comunicación-con-el-backend) - [Componentes reutilizables](#componentes-reutilizables) - [Roles y navegación](#roles-y-navegación) - [Notificaciones](#notificaciones) - [Troubleshooting](#troubleshooting) --- ## 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 ```powershell 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`: ```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://:8080` | | Celular físico (hotspot) | `http://: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: ```ts 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; register: (name, email, password) => Promise; logout: () => void; refreshStatus: () => Promise; } ``` ### 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` ```ts let authToken: string | null = null; export const setAuthToken = (token: string | null) => { authToken = token; }; export const apiFetch = async (path: string, options: RequestInit = {}): Promise => { 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."