294 lines
11 KiB
Markdown
294 lines
11 KiB
Markdown
# 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://<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:
|
|
|
|
```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<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`
|
|
|
|
```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."
|