# backend/: Laravel + Inertia (React)

Este proyecto contiene **tres cosas**:

1. El backend en Laravel.
2. La **API móvil** (`routes/api.php`, prefijo `/api/v1`), documentada en Swagger.
3. El **sistema web** en React vía Inertia (`resources/js`).

Especificación funcional: `../docs/SAI_Analisis_Requerimientos.md`.

## Stack

- **Laravel** (la versión de `composer.json` manda) + **Inertia.js** + **React**, en **JavaScript** con `.jsx` para componentes.
  - No se usa TypeScript salvo que se decida explícitamente.
  - Base de autenticación web: **Laravel Breeze, stack React, en JavaScript** (sin `--typescript`). Es una **decisión tomada**. No uses el starter kit oficial de React (TypeScript/shadcn) ni sugieras migrar a TypeScript.
- **MySQL**.
- **Laravel Sanctum**: tokens Bearer para la API móvil. La web usa la sesión normal de Inertia.
- **Laravel Reverb**: WebSockets (cola del monitor, chat, ubicación en vivo, presencia de operadores).
- **Colas y scheduler**: escalamiento de alertas, SMS, push y exportaciones. Nada pesado se ejecuta en el request.
- **Swagger/OpenAPI**: `dedoc/scramble`. Documenta con FormRequests, API Resources y docblocks, sin escribir YAML a mano.
- **Paquetes de backend**:
  - `spatie/laravel-permission`: roles y permisos; un usuario puede tener varios roles.
  - `spatie/laravel-activitylog`: auditoría.
  - `maatwebsite/excel`: exportación a Excel y CSV (en cola).
  - `barryvdh/laravel-dompdf`: exportación a PDF (PHP puro, sin Chromium; en cola).
  - `laravel-lang/common` (**solo `--dev`**): traducciones al español en `lang/es` (validación, auth, contraseñas). Los archivos publicados en `lang/` son los que usa la app.
- **Servicios externos**:

| Servicio | Uso | Notas |
|---|---|---|
| Google Maps | Mapas | — |
| Firebase (FCM) | Push | Con driver `log`/`fake` en local/demo. Se envía por **token de registro** (`citizen_devices.fcm_token`, FCM HTTP v1 `message.token`). La app también manda el **FID** (`citizen_devices.fid`), que se guarda pero no se usa: la migración del envío a FID queda **pendiente hasta que el SDK de PHP lo soporte oficialmente** |
| Brevo | SMS y correo | Con driver `log` en local/demo |
| Captcha | Solo en el login web | Cloudflare Turnstile, verificado en el servidor. Driver `log` (no valida) en local/demo: `CAPTCHA_DRIVER` |

- **Archivos**: siempre con `Storage::disk(...)` en un disco **privado** (fotos de ciudadanos, chat, fichas) y URLs firmadas temporales.
  - Por ahora se guardan en el storage local. La migración a S3 debe ser solo un cambio de `.env`.
  - **Nunca** guardes estos archivos en `public/`.

## Comandos

```bash
composer run dev            # servidor + queue + logs + vite (si existe el script)
php artisan reverb:start    # WebSockets
php artisan queue:work --queue=realtime,default --sleep=1   # si no corre en composer run dev (realtime = broadcast del Monitor)
php artisan migrate:fresh --seed
php artisan test
npm run build
php artisan sai:simulate-route {request_id}   # simula ubicación en movimiento (demo)
```

Swagger UI: `/docs/api` (ruta por defecto de Scramble).

Contrato para la app: `composer run openapi` exporta a `../docs/api/openapi.json`. Re-expórtalo cada vez que cambie `/api/v1`.

- **Errores documentados:** `app/OpenApi/DocumentApiErrors.php` documenta en cada endpoint sus errores reales con el formato `{ message, code, errors?, retry_after? }` y un ejemplo por `code`. Los deduce del middleware (`auth` → 401, `citizen`/`citizen.active` → 403, `module:*` → 404, `throttle:*` → 429), de los parámetros de la ruta (404 `not_found`), de la validación (422) y de `ROUTE_ERRORS` (errores propios de cada acción). **Si agregas un `ApiException` nuevo en un endpoint, regístralo en `ROUTE_ERRORS`**; `tests/Feature/Foundation/ApiDocsTest.php` verifica el formato.

## Frontend: estructura de `resources/js`

```
resources/js/
├── Components/
│   ├── InertiaComponents/   # Componentes que trae Breeze, aislados. No usarlos en código nuevo.
│   ├── Structure/           # Sidebar, logo, avatares, headers (piezas de los layouts)
│   └── UI/                  # Reutilizables organizados por tipo: Cards/, Grids/, Dialogs/, Maps/, ...
├── Hooks/                   # Hooks personalizados (useAuth.js, usePermissions.js, useEcho.js, ...)
├── Layouts/                 # AppLayout.jsx (logueado), GuestLayout.jsx (sin sesión), ...
├── Pages/
│   ├── SuperAdmin/          # Configuración de la instancia (branding, módulos visibles)
│   ├── Admin/               # Vistas de los roles administrativos, una carpeta por módulo:
│   │   ├── Dashboard/  MonitorSAI/  Citizens/  CitizenMailbox/  MissingPets/
│   │   ├── MissingPersons/  Notifications/  Collaborators/  Catalogs/  Settings/  Reports/
│   └── Auth/                # Login, recuperación de contraseña
├── Schemas/                 # Esquemas Zod + hints por formulario (citizenSchema.js, ...)
├── Services/                # Llamadas HTTP con axios (citizenService.js, ...)
└── Utils/                   # Formateadores puros (dates.js, ...)
```

Reglas:

- **Reutiliza antes de crear.** Si un componente se usa o se usará en más de un lugar, va en `Components/UI/<Tipo>/`.
- **Hooks**: la lógica JS compartida va en `Hooks/`. Patrón de referencia:

  ```js
  import { usePage } from "@inertiajs/react";
  export default function useAuth() {
    const { auth } = usePage().props;
    const user = auth?.user ?? null;
    const hasRole = (role) => !!user && user.roles.includes(role);
    const hasAnyRole = (roles) => !!user && roles.some((r) => user.roles.includes(r));
    return { user, hasRole, hasAnyRole };
  }
  ```

  Para mostrar u ocultar UI, usa **permisos** (`can('requests.take')`), no nombres de rol, porque existen roles compuestos. La autorización real siempre está en el backend.
- **Schemas**: un archivo por entidad, que exporta el schema y sus hints:

  ```js
  import { z } from "zod";
  export const citizenSchema = z.object({
    first_name: z.string().min(1, "El nombre es requerido"),
    email: z.email("Correo inválido"),
    phone: z.string().regex(/^\d{10}$/, "Debe tener 10 dígitos"),
  });
  export const citizenHints = {
    first_name: "Nombre(s)",
    email: "correo@ejemplo.com",
    phone: "10 dígitos sin espacios",
  };
  ```

- **Services**: un objeto por recurso, y es el **único lugar donde se importa axios**:

  ```js
  import axios from "axios";
  const citizenService = {
    async list(params = {}) { const res = await axios.get("/admin/citizens/data", { params }); return res.data; },
    approve(id) { return axios.post(`/admin/citizens/${id}/approve`); },
  };
  export default citizenService;
  ```

  Navegación y props de página con Inertia (`router`, `usePage`). Peticiones de datos o acciones AJAX con `Services/`.
- **Utils**: funciones puras de formato con `date-fns`, con `try/catch` que devuelve el valor original si falla (`dateToDMY`, `dateToYMD`, `timeToHM`). Para la zona horaria se usa `Intl` con `America/Mexico_City`.

## Librerías del frontend (obligatorias)

| Uso | Librería | Regla |
|---|---|---|
| Componentes UI | `@mui/material`, `@mui/icons-material`, `@emotion/react`, `@emotion/styled` | Primera opción para cualquier componente visual. |
| MUI adicionales | `@mui/lab` | Solo para lo que no existe en `@mui/material`. **No usar `LoadingButton`**: en MUI 7 se usa la prop `loading` de `Button`. |
| Fechas en formularios | `@mui/x-date-pickers` con `AdapterDateFns` y locale `es` | Todos los selectores de fecha y hora. |
| Tablas | `ag-grid-community`, `ag-grid-react`, `@ag-grid-community/locale` | **Única** librería de tablas, en español. **No usar `@mui/x-data-grid`.** |
| Formularios | `react-hook-form`, `@hookform/resolvers`, `zod` (v4) | Todo formulario. **No usar `yup`** ni el `useForm` de Inertia en formularios con validación. |
| Sidebar | `react-pro-sidebar` | Sidebar de `AppLayout`. |
| Fechas (lógica) | `date-fns` 4 | Parseo y formato. Para la zona horaria, `Intl`; `temporal-polyfill` solo si hace falta aritmética con zona. |
| Tipografía | `@fontsource/inter` | Fuente única, importada en `app.jsx`. |
| HTTP | `axios` | Solo dentro de `Services/`. |
| Layout y estilos | MUI (`Box`, `Stack`, `Grid`, `Container`, `sx`, `styled`) | Todo el layout, espaciado y responsive, con los breakpoints del tema MUI. |
| Mapas | Google Maps JS | El dibujo de círculos usa Terra Draw si la Drawing Library de Google no está disponible; verificar antes. Los comités no tienen polígono. |
| Tiempo real | `laravel-echo` + `pusher-js` (cliente de Reverb) | Encapsular en `Hooks/useEcho.js` o similar. |
| Gráficas | `recharts` | Única librería de gráficas (Dashboard y reportes). Colores desde el tema MUI. |

- **No instalar** FullCalendar: no hay calendario en SAI.
- **Tailwind:** Breeze lo trae. Solo puede quedar dentro de `InertiaComponents/`; todo lo nuevo usa MUI.
- **Tema:** el branding de cada municipio (colores, logo, nombre) viene de la tabla `settings` como prop compartida de Inertia, y se aplica con `createTheme` en tiempo de ejecución. No va fijo en el código.
- **Centro del mapa (`settings.map_center`):** punto de partida de los mapas sin otra ubicación. Lo edita el Superadmin en Configuración, se comparte en Inertia (`mapCenter`, lo usa `CirclePicker`) y la app lo recibe en `GET /api/v1/app-config`. `null` = sin configurar (la web cae a un respaldo fijo; la app, a la vista de México).
- **URLs temporales de archivos:** `PrivateFiles::url()` castea a `string` porque Scramble no infiere `temporaryUrl()` y documentaría la URL como `null`.

## Backend: convenciones

### Base de datos
- Todo en **inglés**.
- **Migraciones de Laravel**, nunca SQL crudo. Los catálogos se llenan con seeders.
- PK `id` (`$table->id()`) y FKs `xxx_id` con `foreignId()->constrained()`. **No usar `id_xxx`.**
- Catálogos con prefijo `cat_`, con columna `code` (inglés, para la lógica) y `name` (español, para mostrar). **Nunca compares contra `name`.** Usa `code` o PHP Enums.
- Índices en FKs y en columnas que se filtran (teléfono, estatus, fechas). `softDeletes()` en entidades de negocio.
- `request_coordinates` es una tabla de alto volumen: índice `(request_id, captured_at)` y **sin** unique sobre lat/lng.

### Código
- **Controladores delgados.** La validación va en FormRequests y la lógica de negocio en Actions/Services (`app/Actions/...`).
- **Autorización** con Policies y permisos de Spatie (`requests.take`, `notifications.send.committee`, ...). El admin de comité solo ve los datos de **sus** comités: aplícalo con scopes o policies, **nunca solo en el front**.
- **Eventos de broadcast** para todo lo de tiempo real: `RequestCreated`, `RequestEscalated`, `RequestOperatorChanged`, `RequestLocationUpdated`, `RequestMessageSent`, `RequestClosed`.
- **Canales**: `monitor.queue`, `committee.{id}.queue` y `request.{id}` (presence) para la web, con sesión en `/broadcasting/auth`.
  - `citizen.{id}` (privado) para la app: solo el propio ciudadano, autorizado con Bearer en `POST /api/broadcasting/auth`. Eventos propios en `app/Events/Citizen/` (`CitizenMessageReceived`, `CitizenOperatorChanged`, `CitizenRequestClosed`, `CitizenNotificationReceived`) con payload de lista cerrada: **nunca** `request_events`, `closing_notes`, ids de operadores ni datos de terceros. Los reenvía `RelayRequestEventToCitizen` desde los eventos del Monitor.
- **Auditoría** con activitylog en las acciones sensibles: activar o inactivar cuenta, aprobar o rechazar fichas, tomar o cerrar peticiones, exportar y cambiar permisos.
- **Rate limiting** en login, OTP y creación de peticiones.

### API móvil (`/api/v1`)
- Sanctum Bearer. Respuestas con **API Resources**. Errores con un formato consistente: `{ "message": "...", "code": "account_not_active", "errors": {...} }`.
- Las cuentas `unverified` o `inactive` **pueden** hacer `GET`, pero **no** crear ni publicar nada. Responden 403 `account_not_active` mediante el middleware `EnsureCitizenIsActive`.
- **El ciudadano no puede cerrar ni cancelar peticiones.** Solo los usuarios web pueden cerrarlas.
- **No rompas el contrato** de `/api/v1`. Cada endpoint debe verse bien documentado en Swagger, con ejemplos.
- En `APP_ENV=local` o `demo`, los OTP, SMS y push van al log, para poder demostrar todo desde Swagger.

### Reglas de negocio críticas (resumen; el detalle está en docs)
- Petición: `sent` → `in_progress` → `finished` | `false_alarm`. "Escalada" es un **nivel** (`committee|municipal`), no un estatus.
- **Un solo operador responsable** por petición (el único que chatea); los demás ven en solo lectura. Tomarla requiere `requests.take`, confirmación y aviso al anterior. Todo queda en `request_events`.
- El escalamiento comité → municipio es un job con delay de `escalation_minutes` (por defecto 3, rango 0–60). El municipio ve las alertas de comité desde el inicio y puede tomarlas.
- **Menor de edad** (< 18, calculado desde `born_date`): tutor obligatorio. Al emitir una alerta se envía push al tutor si está registrado, o SMS si no lo está (una sola vez por petición). En el Monitor aparece la etiqueta "MENOR DE EDAD" con los datos del tutor.
- **Cierre** con nota obligatoria, `closed_by` y `closed_at`, y notificación al ciudadano. El detalle muestra el **recorrido** completo en el mapa.

## Tests
- Feature tests para cada endpoint de `/api/v1` y para las reglas críticas: ruteo comité/municipio, escalamiento, toma de atención, permisos del admin de comité, cuenta no activa y aviso al tutor.
- Usa `Event::fake()`, `Queue::fake()`, `Notification::fake()` y `Storage::fake()`. No llames servicios externos en los tests.
