Skip to content

Flujo Dashboard de producción

Base URL: /api/v1. Backend: camaroneras_backend (mismo módulo reportes, controller ReportesController) ✅. Admin: camaroneras_admin (feature dashboard-produccion) ✅. Mobile: no aplica (igual que el reporte semanal, ver referencia/produccion-flow.md). Noveno módulo de negocio; agrega la misma sábana que 08-reporte-semanal-flow.md en kpis globales, biomasa por sector (donut), filas por piscina (barras/tabla) y serie semanal de peso por piscina (línea), inspirado en dashboards de Power BI de referencia del socio.

Requests y responses 2xx: el shape de la respuesta exitosa está 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

No agrega tablas propias ni un query nuevo: reutiliza el mismo fetch Prisma y el mismo buildRow por piscina que getWeekly (ver weekly-report.aggregate.ts/.calc.ts) — y como buildRow ya aplica el motor de scoring de 10 · Parámetros KPI (indiceSalud, prioridad, pesoActG), las agregaciones ejecutivas de abajo no recalculan nada, solo agrupan. Suma cinco cosas encima de la sábana (dashboard.calc.ts, funciones puras testeadas aparte):

  • kpis: agregados globales de la organización a la fecha de corte (computeDashboardKpis), incluidos índice de salud promedio y conteos de prioridad.
  • sectors: hectáreas, biomasa, índice de salud promedio, atención prioritaria y FCA por sector (computeSectorSummaries).
  • fcrPorTalla: FCA sectorizado por talla (computeFcrPorTalla) — ver más abajo.
  • topPools: las 5 piscinas de peor índice de salud (computeTopPools).
  • series: por piscina con ciclo activo, puntos semanales de peso/incremento desde la siembra hasta el corte (weekly-report.aggregate.tsbuildWeeklySeries), derivados de los Sampling reales — no hay tabla de snapshots semanales, igual que en la sábana.

Kpis agregados (dashboard.calc.ts)

Los kpis son el agregado de toda la organización a la fecha de corte — no responden a los filtros del cliente (año/ciclo/sector/piscina/rango de peso), que solo recortan la vista de sectores/piscinas/tabla. Evita duplicar las fórmulas de agregación en el frontend (una sola fuente de verdad en el backend).

📐 Las fórmulas de agregación están en la referencia de fórmulas (fuente única), con el origen de cada dato de entrada. Acá, qué expone cada campo y sobre qué filas se calcula:

CampoSobre qué filas
haTotal, biomasaLb, animalesSembrados, animalesVivostodas las piscinas (con y sin ciclo)
lbPorHaActual (sin raleo), lbPorHaTotal (con raleo)todas
survivalPct — ponderado por cantidad sembradacon ciclo y ambos datos
survivalEstimadaPct — misma ponderación, fuente independientecon ciclo y ambos datos
fcrtodas
crecimientoDiaG, promedioIncrementosG — promedio simplecon ciclo y dato disponible
densidadHalas que tienen larvaeCount (ver nota)
poolCount, poolsConCicloCounttotales, para el estado vacío del admin
indiceSaludPromedio — promedio simple; null si ninguna tiene ciclocon ciclo
prioridadCounts y atencionPrioritariaCount (= urgente + alta)con ciclo
  • lbPorHaTotal es el que alimenta el semáforo de carga, no lbPorHaActual.
  • survivalEstimadaPct es informativa (censo por consumo, ver 08 · Reporte semanal → "Sobrevivencia: real vs. estimada"): no alimenta ningún otro kpi ni semáforo.
  • El denominador de densidadHa son las hectáreas de las filas con siembra (larvaeCount no nulo), no las de todas las que tienen ciclo. Hoy coincide, porque todo ciclo se crea junto con su siembra, pero así el kpi no se diluye si alguna vez existe un ciclo activo sin siembra.

⚠️ Índice/prioridad se agregan solo entre piscinas con ciclo, nunca sobre todas: una piscina vacía evalúa sus 5 criterios en sin_dato (penaliza 0), así que siempre da índice 100 — incluirla infla el promedio y esconde atención prioritaria real. Mismo criterio en sectors[].indiceSaludPromedio/.atencionPrioritariaCount y en topPools.

Resumen por sector (sectors, computeSectorSummaries)

Por sector: hectares, biomasaLb (suma simple), indiceSaludPromedio y atencionPrioritariaCount (mismo criterio que los kpis globales, pero acotado a las filas del sector) y fcrla misma fórmula del FCA consolidado global, aplicada solo a las filas del sector, no un promedio de los FCR por piscina (detalle).

FCA por talla (fcrPorTalla, computeFcrPorTalla)

🆕 Pedido de Winston (2026-08-04), fuera del Excel v4: poder ver el FCA sectorizado por talla de producción. La talla de cada piscina es el tramo vigente del criterio fca del KpiThresholdSet de la organización (mismo findBracket() que ya usa su semáforo de FCA, ver 10 · Parámetros KPI) — no hay tabla de tallas propia. Dentro de cada grupo se aplica la misma fórmula del FCA consolidado (alimento total ÷ biomasa total del grupo, sin ponderar); el método de Winston no cambia, solo el subconjunto de filas al que se aplica. Ver ADR-0059.

  • Cada grupo trae label, minWeightG (cota inferior exclusiva, null en el primer tramo), maxWeightG (cota superior inclusiva), poolCount, biomasaLb, feedAccumTotalKg y fcr (null si el grupo no tiene biomasa).
  • Las piscinas sin pesoActG (sin muestreo, o sin ciclo) van a un grupo sin_dato aparte (minWeightG/maxWeightG ambos null) — a diferencia de findBracket(), que manda los pesos null al último tramo (correcto para un semáforo, que necesita algún valor; acá inflaría el tramo de mayor peso con piscinas que no se pudieron tallar).
  • ⚠️ El bloque cambia si el admin edita los umbrales de FCA en KpiThresholdsPage — es la misma fuente de verdad que el semáforo, no una inconsistencia.
  • El último tramo se etiqueta "> N g", nunca con el sentinel 9999 (UNBOUNDED_BRACKET_WEIGHT_G).

Top-5 (topPools, computeTopPools)

Las 5 piscinas con ciclo de peor índice de salud (indiceSalud asc.). Desempate: prioridad más severa primero (urgente > alta > media > normal), luego código de piscina — orden estable y determinista ante empates exactos.

Serie semanal por piscina (buildWeeklySeries)

Un punto cada 7 días desde la siembra del ciclo activo hasta el corte, más un punto final exacto en el corte si no cae en un límite semanal exacto. pesoG es el peso del muestreo vigente a ese punto (null si aún no hay muestreo); incrementoG compara contra el punto anterior de la serie (no otro muestreo), y también es null hasta que hay dos pesos reales consecutivos. Piscinas sin ciclo activo devuelven points: [].

RBAC

  • Módulo dashboard (label "Dashboard de producción") en el catálogo, clients ["web"]permiso propio, no reutiliza reportes:ver (para poder dar/quitar acceso al dashboard sin tocar el permiso de la sábana).
  • Defaults: Administrador y Técnico ver. Bodeguero no lo tiene por defecto.

Endpoints (protegidos: AccessJwtGuard + PermissionGuard; tenant del JWT)

GET /reports/dashboard — (dashboard:ver)

Kpis + biomasa por sector + filas por piscina + serie semanal, a una fecha de corte.

Query: date? (ISO 8601; default hoy).

json
// response 400 (fecha de corte inválida)
{ "error": "...", "message": "Fecha de corte inválida", "statusCode": 400 }

Flujo admin (camaroneras_admin, feature dashboard-produccion)

  • Ruta: /produccion — protegida (ProtectedRoute module="dashboard" action="ver"). No se montó en /dashboard: esa ruta es "Inicio", el fallback de ProtectedRoute para cualquier acceso denegado y el destino de /; gatearla con un permiso propio crearía un loop de redirect para roles sin dashboard:ver. Nav propio en "Operación".
  • Code-splitting: la página se carga con React.lazy en el router (recharts es la primera librería de charts del admin, pesa ~400kB — ver router.tsx).
  • Selector de fecha de corte (default hoy) recalcula todo (['reports','dashboard',date]).
  • Gauges (Sobrevivencia, Sobrev. estimada, FCR, Crecimiento/día, Prom. incremento semanal, Índice de salud): RadialBarChart de recharts con techos fijos de escala (100 / 150 / 2 / 1g / 10g / 100) — son solo el límite visual del arco, no metas de negocio; ver KpiSection.tsx. El de "Sobrev. estimada" usa techo 150 (vs. 100 de la real) porque puede legítimamente superar 100% (sobrealimentación) — el arco se clampea al techo, pero el número mostrado nunca se recorta.
  • Filtros client-side (año, N° ciclo, estado, sector, piscina, rango de peso g): sobre la respuesta ya cargada (useMemo, sin refetch), afectan el donut, las barras y la tabla — no los kpis (ver arriba). "Año" y "N° Ciclo" filtran seasonYear/seasonNumber del ciclo activo (fila del Excel, ver ADR-0058) y son independientes de "Estado" (cycleStatus: activa/liberada/cerrada) — antes un solo select "Ciclo" filtraba en realidad por estado y "Año" se derivaba client-side de sowingDate.slice(0,4); ambos workarounds quedaron resueltos con la temporada real del ciclo.
  • Selector de piscina del gráfico de línea: independiente del filtro general "Piscina" — el line chart solo puede mostrar una piscina a la vez (evita el anti-patrón de demasiadas series), default a la primera con ciclo activo.
  • Tabla resumen: una fila por piscina (subset de columnas de la sábana completa + columna Sector), no reemplaza /reportes (la sábana completa con desviación).
  • Resumen por sector, Top-5 y FCA por talla: bloques ejecutivos nuevos — SectorSummaryTable, TopPoolsCard, FcrPorTallaTable. Los dos primeros son la foto completa sin filtros (mismo criterio que los kpis globales: no responden a los filtros client-side de la tabla/gráficos); grupos de talla con menos de 3 piscinas se marcan "(pocas piscinas)" para no leer un FCA ruidoso como representativo.
  • Badges de estado/prioridad compartidos: EstadoBadge/PrioridadBadge (shared/components/KpiBadges.tsx) — antes vivían solo en la feature reportes; se extrajeron a shared/ al necesitarlos también acá y en Prioridades (ver ADR-0059).
  • Tema: tokens --chart-1..--chart-5 existentes (src/index.css, hoy en escala de grises porque el tema base es neutral) — no se tocó la paleta compartida por una sola página.
  • Estados límite: cargando, sin piscinas en el corte, token expirado (flujo de refresh existente).

Backlog / a futuro

  • Costo por libra: requiere el módulo futuro tratamientos + costos (fuera de v1).
  • Filtro multi-finca: equivale a sector/organización en este modelo (no aplica).
  • Export de la vista (xlsx/csv/PDF) — no implementado, igual que en /reportes.
  • Mobile: fuera de alcance (dashboard principalmente web).