Apariencia
Flujo Eventos/Eventualidades
Base URL: /api/v1. Backend: camaroneras_backend (módulo eventos) ✅. Admin: camaroneras_admin (feature eventos) ✅. Mobile: camaroneras_mobile (feature eventos, offline-first) ✅. Sexto módulo de negocio; depende de piscinas (y opcionalmente ciclos). Registro de mortalidades y contratiempos productivos, visibles de inmediato a gerencia (vía RBAC). Captura por piscina con vínculo opcional al ciclo.
Requests y responses 2xx: bodies (tipos, opcionales, enums) y el shape de las respuestas exitosas están en la API Reference, generada desde el spec OpenAPI. Acá quedan solo las respuestas de error (4xx/5xx) y las reglas de negocio/flujo.
Modelo
Pool ──< Event >── Cycle? (evento por piscina, ciclo opcional)- Event:
poolId(obligatorio),cycleId(opcional),date,category,description,affectedCount?(animales afectados, p. ej. mortalidad),notes?. - category (enum):
mortalidad | enfermedad | clima | equipo | otro. onDelete: Pool → Cascade; Cycle → SetNull (no se pierde el histórico si el ciclo se borra).- Sin derivados: pura captura.
RBAC
- Módulo
eventos(label "Eventos/Eventualidades") en el catálogo, clients["web","mobile"]. - Defaults: Administrador todo; Técnico
ver/crear/editar; Bodeguerover. Backfill automático.
Endpoints (protegidos: AccessJwtGuard + PermissionGuard; tenant del JWT)
GET /pools/:poolId/events — (eventos:ver)
Lista los eventos de una piscina (más reciente primero).
POST /pools/:poolId/events — (eventos:crear)
Registra un evento. category y description obligatorios; cycleId (si viene) debe pertenecer a esa piscina.
json
// response 400 (categoría inválida)
{ "error": "...", "message": "category must be one of the following values: ...", "statusCode": 400 }json
// response 404 (piscina no existe / cycleId no pertenece a la piscina)
{ "error": "...", "message": "Ciclo no encontrado para esta piscina", "statusCode": 404 }PATCH /events/:id — (eventos:editar)
Edita un evento (campos parciales). Si se cambia cycleId, se revalida contra la piscina.
DELETE /events/:id — (eventos:eliminar)
json
// response 404 (no existe o de otra organización)
{ "error": "...", "message": "Evento no encontrado", "statusCode": 404 }Flujo admin (camaroneras_admin, feature eventos)
- Ruta:
/eventos— protegida (ProtectedRoute module="eventos" action="ver"). Sin sesión →/login; sin permisoeventos:ver→ bloqueada y sin item de nav. - Selector de piscina (
FormCombobox) desdeGET /pools. Sin selección → aviso; al elegir aparece la tabla + acciones. - Tabla: Fecha · Categoría · Descripción · Afectados · Acciones (estados loading/empty).
- Diálogo crear/editar: fecha, categoría (combo: mortalidad/enfermedad/clima/equipo/ otro), descripción (requerida), ciclo opcional (combo con "Ninguno" + ciclos de la piscina vía
GET /cycles?poolId=), animales afectados (opcional), notas. Botón deshabilitado hasta fecha + descripción. - Gating RBAC: "Nuevo evento" con
eventos:crear; "Editar" coneventos:editar; "Eliminar" coneventos:eliminar.
Flujo mobile (camaroneras_mobile, feature eventos, offline-first)
- Rutas:
/eventos(picker de piscina) →/eventos/:poolId(eventos). Card en Home solo concanVerEventos. La app ya exige sesión (go_router →/login). - Offline-first: tabla Drift
Events(schema v8). Lectura siempre desde Drift;sync()refresca desde red y conserva el cache si no hay conexión. El picker leePoolsy hace su propiosyncPools(). El combo de ciclo lee la tablaCyclescacheada (sin red). - Escrituras online: crear/editar/eliminar requieren red (
_netError→ "Sin conexión…"); tras cada escritura se re-sincroniza la piscina. - Sheet: categoría (combo, default mortalidad), descripción (requerida), ciclo opcional ("Ninguno" + ciclos de la piscina), animales afectados (opcional), notas.
- Gating RBAC: FAB "Registrar" con
canCrearEventos; editar/eliminar con los suyos. - Tests: repository (Drift in-memory: mapeo + replace borra ausentes) + gating del FAB.