Apariencia
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.ts→buildWeeklySeries), derivados de losSamplingreales — 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:
| Campo | Sobre qué filas |
|---|---|
haTotal, biomasaLb, animalesSembrados, animalesVivos | todas las piscinas (con y sin ciclo) |
lbPorHaActual (sin raleo), lbPorHaTotal (con raleo) | todas |
survivalPct — ponderado por cantidad sembrada | con ciclo y ambos datos |
survivalEstimadaPct — misma ponderación, fuente independiente | con ciclo y ambos datos |
fcr | todas |
crecimientoDiaG, promedioIncrementosG — promedio simple | con ciclo y dato disponible |
densidadHa | las que tienen larvaeCount (ver nota) |
poolCount, poolsConCicloCount | totales, para el estado vacío del admin |
indiceSaludPromedio — promedio simple; null si ninguna tiene ciclo | con ciclo |
prioridadCounts y atencionPrioritariaCount (= urgente + alta) | con ciclo |
lbPorHaTotales el que alimenta el semáforo de carga, nolbPorHaActual.survivalEstimadaPctes informativa (censo por consumo, ver 08 · Reporte semanal → "Sobrevivencia: real vs. estimada"): no alimenta ningún otro kpi ni semáforo.- El denominador de
densidadHason las hectáreas de las filas con siembra (larvaeCountno 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 fcr — la 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,nullen el primer tramo),maxWeightG(cota superior inclusiva),poolCount,biomasaLb,feedAccumTotalKgyfcr(nullsi el grupo no tiene biomasa). - Las piscinas sin
pesoActG(sin muestreo, o sin ciclo) van a un gruposin_datoaparte (minWeightG/maxWeightGambosnull) — a diferencia defindBracket(), que manda los pesosnullal ú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 sentinel9999(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 reutilizareportes: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 deProtectedRoutepara cualquier acceso denegado y el destino de/; gatearla con un permiso propio crearía un loop de redirect para roles sindashboard:ver. Nav propio en "Operación". - Code-splitting: la página se carga con
React.lazyen el router (recharts es la primera librería de charts del admin, pesa ~400kB — verrouter.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):
RadialBarChartde recharts con techos fijos de escala (100 / 150 / 2 / 1g / 10g / 100) — son solo el límite visual del arco, no metas de negocio; verKpiSection.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" filtranseasonYear/seasonNumberdel 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 desowingDate.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 featurereportes; se extrajeron ashared/al necesitarlos también acá y enPrioridades(ver ADR-0059). - Tema: tokens
--chart-1..--chart-5existentes (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).