Etapa completada (local) — widgets de Mirada Empresa
This commit is contained in:
@@ -4,21 +4,43 @@
|
||||
import type { DashboardFilters } from './dashboard-types'
|
||||
import { filtersToQuery } from './dashboard-utils'
|
||||
|
||||
async function fetchWidget<T>(widget: string, surveyId: string, filters: DashboardFilters): Promise<T> {
|
||||
async function fetchWidget<T>(
|
||||
widget: string,
|
||||
surveyId: string,
|
||||
filters: DashboardFilters,
|
||||
extra?: Record<string, string>
|
||||
): Promise<T> {
|
||||
const qs = filtersToQuery(surveyId, filters)
|
||||
const res = await fetch(`/api/dashboard/${widget}?${qs}`, { cache: 'no-store' })
|
||||
const extraQs = extra ? `&${new URLSearchParams(extra).toString()}` : ''
|
||||
const res = await fetch(`/api/dashboard/${widget}?${qs}${extraQs}`, { cache: 'no-store' })
|
||||
if (!res.ok) throw new Error(`Widget ${widget}: ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// Mirada Empresa — una entrada por métrica (E1-E10 + estrella).
|
||||
export const queries = {
|
||||
kpis: (sid: string, f: DashboardFilters) => fetchWidget('kpis', sid, f),
|
||||
prevalencia: (sid: string, f: DashboardFilters) => fetchWidget('prevalencia', sid, f),
|
||||
heatmap: (sid: string, f: DashboardFilters) => fetchWidget('heatmap', sid, f),
|
||||
distribucion: (sid: string, f: DashboardFilters) => fetchWidget('distribucion', sid, f),
|
||||
apoyos: (sid: string, f: DashboardFilters) => fetchWidget('apoyos', sid, f),
|
||||
score: (sid: string, f: DashboardFilters) => fetchWidget('score', sid, f),
|
||||
piramide: (sid: string, f: DashboardFilters) => fetchWidget('piramide', sid, f),
|
||||
intensidad: (sid: string, f: DashboardFilters) => fetchWidget('intensidad', sid, f),
|
||||
saturacion: (sid: string, f: DashboardFilters) => fetchWidget('saturacion', sid, f),
|
||||
mePrevalencia: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-prevalencia', sid, f),
|
||||
meRiesgoRotacion: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-riesgo-rotacion', sid, f),
|
||||
meAusentismo: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-ausentismo', sid, f),
|
||||
mePresentismo: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-presentismo', sid, f),
|
||||
mePuntoCiego: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-punto-ciego', sid, f),
|
||||
meApoyos: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-apoyos', sid, f),
|
||||
// p_umbral_senales es parametrizable en la RPC — el widget puede pasarlo,
|
||||
// si no se pasa nada la RPC usa su propio default (2 de 3).
|
||||
meTalentoRiesgo: (sid: string, f: DashboardFilters, umbral?: number) =>
|
||||
fetchWidget('me-talento-riesgo', sid, f, umbral !== undefined ? { umbral: String(umbral) } : undefined),
|
||||
meCarreraCongelada: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-carrera-congelada', sid, f),
|
||||
meIntensidadCarga: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-intensidad-carga', sid, f),
|
||||
meSegmentacion: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-segmentacion', sid, f),
|
||||
meDisposicionRed: (sid: string, f: DashboardFilters) =>
|
||||
fetchWidget('me-disposicion-red', sid, f),
|
||||
}
|
||||
|
||||
@@ -9,51 +9,6 @@ export interface DashboardFilters {
|
||||
// Estado de cada widget
|
||||
export type WidgetStatus = 'loading' | 'ok' | 'suppressed' | 'error'
|
||||
|
||||
// Payloads de cada RPC
|
||||
export interface KpiData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
pct_cuidadores: number
|
||||
score_bienestar: number | null // avg raw 4.3 (1.0–5.0)
|
||||
pct_ausencias: number
|
||||
}
|
||||
|
||||
export interface DeptoData {
|
||||
depto: string
|
||||
n: number | null
|
||||
pct_cuidadores: number | null
|
||||
suppressed: boolean
|
||||
}
|
||||
export interface PrevalenciaData { status: 'ok'; n_total: number; data: DeptoData[] }
|
||||
|
||||
export interface HeatmapCell { horas: string; ausencias: string; n: number | null; suppressed: boolean }
|
||||
export interface HeatmapData { status: 'ok'; n_total: number; data: HeatmapCell[] }
|
||||
|
||||
export interface DistribucionData {
|
||||
status: 'ok'; n: number
|
||||
disc_propia: number; familiar: number; adulto_mayor: number; ninguno: number
|
||||
}
|
||||
|
||||
export interface ApoyoItem { politica: string; n: number; pct: number }
|
||||
export interface ApoyosData { status: 'ok'; n_total: number; data: ApoyoItem[] }
|
||||
|
||||
export interface ScoreData { status: 'ok'; n: number; score: number; zona: 'baja' | 'desarrollo' | 'consciente' }
|
||||
|
||||
export interface PiramideCell { etario: string; genero: string; n: number | null; suppressed: boolean }
|
||||
export interface PiramideData { status: 'ok'; n_total: number; data: PiramideCell[] }
|
||||
|
||||
export interface IntensidadCell { horas: string; bienestar: string; n: number | null; suppressed: boolean }
|
||||
export interface IntensidadData { status: 'ok'; n_total: number; data: IntensidadCell[] }
|
||||
|
||||
export interface SaturacionSegmento {
|
||||
segmento: string
|
||||
n: number | null
|
||||
pct: number | null
|
||||
apoyo: string | null
|
||||
suppressed: boolean
|
||||
}
|
||||
export interface SaturacionData { status: 'ok'; n_total: number; segmentos: SaturacionSegmento[] }
|
||||
|
||||
export type SuppressedPayload = { status: 'suppressed'; min_n: number; n: number }
|
||||
|
||||
export type RpcResult<T> = T | SuppressedPayload
|
||||
@@ -61,3 +16,123 @@ export type RpcResult<T> = T | SuppressedPayload
|
||||
export function isSuppressed(r: unknown): r is SuppressedPayload {
|
||||
return typeof r === 'object' && r !== null && (r as Record<string, unknown>)['status'] === 'suppressed'
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// Mirada Empresa — shapes tal cual los devuelven las rpc_me_*
|
||||
// (migración 20260625000002_dashboard_mirada_empresa.sql, verificada
|
||||
// contra Postgres real). NO copiar del SPEC_MIRADA_EMPRESA.md a ciegas:
|
||||
// el SQL real difiere un poco del shape ilustrativo de la spec (ej. E6
|
||||
// agrega apoyo_percibido_suppressed; E7 usa "apoyos", no "data").
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
// E1 — Prevalencia de cuidadores
|
||||
export interface PrevalenciaCuidadoresData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
pct_cuidadores: number
|
||||
}
|
||||
|
||||
// E2 — Riesgo de rotación
|
||||
export interface RiesgoRotacionData {
|
||||
status: 'ok'
|
||||
n_cuidadores: number
|
||||
pct_riesgo: number
|
||||
desglose: { algo: number; muy: number }
|
||||
}
|
||||
|
||||
// E3 — Ausentismo
|
||||
export interface AusentismoData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
pct_ausentismo_recurrente: number
|
||||
desglose: { nunca: number; pocas: number; varias: number; frecuente: number }
|
||||
}
|
||||
|
||||
// E4 — Presentismo
|
||||
export interface PresentismoData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
pct_presentismo: number
|
||||
desglose: {
|
||||
nunca_o_casi_nunca: number
|
||||
algunas_al_mes: number
|
||||
varias_por_semana: number
|
||||
casi_todos_los_dias: number
|
||||
}
|
||||
}
|
||||
|
||||
// E6 — Punto ciego organizacional
|
||||
// apoyo_percibido_pct puede ser null con status='ok' si SU población (9.1)
|
||||
// no llega a 5, aunque el total (carga_real) sí — dos poblaciones, dos
|
||||
// supresiones independientes (ver comentario en la RPC).
|
||||
export interface PuntoCiegoData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
carga_real_pct: number
|
||||
apoyo_percibido_pct: number | null
|
||||
apoyo_percibido_suppressed: boolean
|
||||
brecha_pp: number | null
|
||||
}
|
||||
|
||||
// E7 — Apoyos demandados
|
||||
export interface ApoyoMEItem { apoyo: string; n: number; pct: number }
|
||||
export interface ApoyosMEData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
apoyos: ApoyoMEItem[]
|
||||
}
|
||||
|
||||
// ★ Talento en riesgo
|
||||
export interface TalentoRiesgoData {
|
||||
status: 'ok'
|
||||
n_cuidadores: number
|
||||
umbral_senales: number
|
||||
pct_talento_riesgo: number
|
||||
criterios_promedio: number
|
||||
}
|
||||
|
||||
// E5 — Carrera congelada [SEC]
|
||||
export interface CarreraCongeladaItem { opcion: string; n: number; pct: number }
|
||||
export interface CarreraCongeladaData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
pct_carrera_congelada: number
|
||||
top_opciones: CarreraCongeladaItem[]
|
||||
}
|
||||
|
||||
// E8 — Intensidad de carga [SEC]
|
||||
export interface IntensidadCargaCell {
|
||||
dias_semana: string
|
||||
horas_dia: string
|
||||
n: number | null
|
||||
suppressed: boolean
|
||||
}
|
||||
export interface IntensidadCargaData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
distribucion_intensidad: IntensidadCargaCell[]
|
||||
pct_guardia_permanente: number
|
||||
}
|
||||
|
||||
// E9 — Segmentación por nivel y modalidad [SEC]
|
||||
export interface SegmentoNivelItem { nivel: string; n: number; pct: number }
|
||||
export interface SegmentoModalidadItem { modalidad: string; n: number; pct: number }
|
||||
export interface SegmentacionData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
por_nivel: SegmentoNivelItem[]
|
||||
por_modalidad: SegmentoModalidadItem[]
|
||||
}
|
||||
|
||||
// E10 — Disposición a red de apoyo [SEC]
|
||||
// Shape "suppressed" de esta RPC difiere del genérico (usa
|
||||
// n_grupo_apoyo/n_voluntariado, no min_n/n) — isSuppressed() solo mira
|
||||
// `status`, así que sigue funcionando; no leer .n de un resultado
|
||||
// suprimido de esta RPC en particular.
|
||||
export interface DisposicionRedData {
|
||||
status: 'ok'
|
||||
n_grupo_apoyo: number
|
||||
pct_grupo_apoyo: number | null
|
||||
n_voluntariado: number
|
||||
pct_voluntariado: number | null
|
||||
}
|
||||
|
||||
@@ -33,18 +33,4 @@ export function filtersToRpc(filters: DashboardFilters): Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export const HORAS_ORDER = ['Menos de 1h','Entre 1 y 3h','Entre 3 y 6h','Más de 6h','Varía mucho']
|
||||
export const AUSENCIAS_ORDER = ['Nunca','Pocas veces (1–3 veces al año)','Varias veces (4–10 veces al año)','Con frecuencia (más de 10 veces al año)']
|
||||
export const ETARIO_ORDER = ['Menos de 25 años','25 a 34 años','35 a 44 años','45 a 54 años','55 años o más','Prefiero no decir']
|
||||
|
||||
export function sortByOrder<T extends Record<string, unknown>>(
|
||||
arr: T[],
|
||||
key: keyof T,
|
||||
order: string[]
|
||||
): T[] {
|
||||
return [...arr].sort((a, b) => {
|
||||
const ia = order.indexOf(a[key] as string)
|
||||
const ib = order.indexOf(b[key] as string)
|
||||
return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user