refactor completo + truncate de base de datos + reingenieria dashboard
This commit is contained in:
@@ -1,29 +1,29 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isSuppressed, type PoliticasData } from '@/lib/dashboard-types'
|
||||
import { isSuppressed, type ApoyosData } from '@/lib/dashboard-types'
|
||||
import { queries } from '@/lib/dashboard-queries'
|
||||
import type { DashboardFilters } from '@/lib/dashboard-types'
|
||||
import { WidgetShell } from './widget-shell'
|
||||
|
||||
export function PoliticasBar({ surveyId, filters }: { surveyId: string; filters: DashboardFilters }) {
|
||||
const [data, setData] = useState<PoliticasData | null>(null)
|
||||
export function ApoyosBar({ surveyId, filters }: { surveyId: string; filters: DashboardFilters }) {
|
||||
const [data, setData] = useState<ApoyosData | null>(null)
|
||||
const [sup, setSup] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setData(null); setSup(false)
|
||||
queries.politicas(surveyId, filters)
|
||||
.then(d => { if (isSuppressed(d)) setSup(true); else setData(d as PoliticasData) })
|
||||
queries.apoyos(surveyId, filters)
|
||||
.then(d => { if (isSuppressed(d)) setSup(true); else setData(d as ApoyosData) })
|
||||
.catch(() => {})
|
||||
}, [surveyId, JSON.stringify(filters)])
|
||||
|
||||
const status = !data && !sup ? 'loading' : sup ? 'suppressed' : 'ok'
|
||||
|
||||
return (
|
||||
<WidgetShell title="📈 Políticas más demandadas" status={status} n={data?.n_total} colSpan={6}>
|
||||
<WidgetShell title="Apoyos más demandados" subtitle="6.2 · apoyos de la empresa que los respondentes consideran que más los ayudarían" status={status} n={data?.n_total} colSpan={6}>
|
||||
{data && (
|
||||
<ul className="db-bar-list" aria-label="Políticas demandadas">
|
||||
<ul className="db-bar-list" aria-label="Apoyos más demandados">
|
||||
{data.data.length === 0 && (
|
||||
<li className="db-suppressed-hint">No hay datos suficientes por política (n<5 en todas).</li>
|
||||
<li className="db-suppressed-hint">No hay datos suficientes por apoyo (n<5 en todos).</li>
|
||||
)}
|
||||
{data.data.map(item => (
|
||||
<li key={item.politica} className="db-bar-item">
|
||||
@@ -41,7 +41,7 @@ export function DonutCuidados({ surveyId, filters }: { surveyId: string; filters
|
||||
}) : []
|
||||
|
||||
return (
|
||||
<WidgetShell title="🍩 Tipo de cuidado" status={status} n={data?.n} colSpan={3}>
|
||||
<WidgetShell title="Perfil de la población" subtitle="1.1 + 1.6 + 1.13 · discapacidad propia, cuidador familiar y de adulto mayor (no excluyentes)" status={status} n={data?.n} colSpan={3}>
|
||||
{data && (
|
||||
<div className="db-donut-wrapper">
|
||||
<svg viewBox="0 0 150 150" className="db-donut-svg" aria-hidden="true">
|
||||
|
||||
@@ -31,7 +31,7 @@ export function GaugeOrganizacional({ surveyId, filters }: { surveyId: string; f
|
||||
const color = data ? ZONA_COLOR[data.zona] : '#43BBC8'
|
||||
|
||||
return (
|
||||
<WidgetShell title="🎯 Madurez organizacional" status={status} n={data?.n} colSpan={3}>
|
||||
<WidgetShell title="Madurez organizacional" subtitle="6.1 + 6.5 · comprensión percibida y expectativa hacia la empresa · score 0–100" status={status} n={data?.n} colSpan={3}>
|
||||
{data && (
|
||||
<div className="db-gauge-wrapper">
|
||||
<svg viewBox="0 0 150 100" className="db-gauge-svg" aria-label={`Score: ${score}/100`}>
|
||||
|
||||
@@ -31,7 +31,7 @@ export function HeatmapWidget({ surveyId, filters }: { surveyId: string; filters
|
||||
const cellMap = new Map((data?.data ?? []).map(c => [`${c.horas}|${c.ausencias}`, c]))
|
||||
|
||||
return (
|
||||
<WidgetShell title="🔥 Horas de cuidado × Ausencias laborales" status={status} n={data?.n_total} colSpan={6}>
|
||||
<WidgetShell title="Horas de cuidado × Frecuencia de ausentismo" subtitle="2.2 × 5.2 · correlación entre intensidad de cuidado y ausentismo recurrente" status={status} n={data?.n_total} colSpan={6}>
|
||||
{data && (
|
||||
<div className="db-heatmap-scroll" role="table" aria-label="Heatmap de intensidad">
|
||||
<div className="db-heatmap-grid"
|
||||
|
||||
@@ -33,7 +33,7 @@ export function IntensidadProductividad({ surveyId, filters }: { surveyId: strin
|
||||
}
|
||||
|
||||
return (
|
||||
<WidgetShell title="💸 Intensidad de cuidado vs. Bienestar (bins N≥5)" status={status} n={data?.n_total} colSpan={6}>
|
||||
<WidgetShell title="Intensidad de cuidado vs. Bienestar" subtitle="2.2 × 4.3 · cruce agregado intensidad de cuidado × bienestar (bins con k≥5)" status={status} n={data?.n_total} colSpan={6}>
|
||||
{data && (
|
||||
<div className="db-hexbin-wrapper">
|
||||
<p className="db-hexbin-note">
|
||||
|
||||
@@ -4,14 +4,57 @@ import { isSuppressed, type KpiData, type SuppressedPayload } from '@/lib/dashbo
|
||||
import { queries } from '@/lib/dashboard-queries'
|
||||
import type { DashboardFilters } from '@/lib/dashboard-types'
|
||||
|
||||
interface KpiCardProps { label: string; value: string; sub?: string }
|
||||
const KPI_DEFS = [
|
||||
{
|
||||
title: 'CUIDADORES ACTIVOS',
|
||||
subtitle: '1.6 + 1.13 · % de la plantilla con responsabilidades de cuidado de otros',
|
||||
getValue: (d: KpiData) => `${d.pct_cuidadores}%`,
|
||||
getN: (d: KpiData) => `n = ${d.n}`,
|
||||
},
|
||||
{
|
||||
title: 'FRECUENCIA DE AUSENTISMO',
|
||||
subtitle: '5.2 · % con ausencias laborales recurrentes (4 o más veces al año)',
|
||||
getValue: (d: KpiData) => `${d.pct_ausencias}%`,
|
||||
getN: () => 'ausencias frecuentes',
|
||||
},
|
||||
{
|
||||
title: 'BIENESTAR',
|
||||
subtitle: '4.3 · autorreporte de bienestar general · escala 1–5',
|
||||
getValue: (d: KpiData) => d.score_bienestar !== null ? d.score_bienestar.toFixed(1) : '—',
|
||||
getN: () => '/ 5',
|
||||
},
|
||||
] as const
|
||||
|
||||
function KpiCard({ label, value, sub }: KpiCardProps) {
|
||||
interface KpiCardProps {
|
||||
title: string
|
||||
subtitle: string
|
||||
value: string
|
||||
valueNote: string
|
||||
suppressed: boolean
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
function KpiCard({ title, subtitle, value, valueNote, suppressed, loading }: KpiCardProps) {
|
||||
return (
|
||||
<div className="db-kpi-card">
|
||||
<span className="db-kpi-value" aria-live="polite">{value}</span>
|
||||
<span className="db-kpi-label">{label}</span>
|
||||
{sub && <span className="db-kpi-sub">{sub}</span>}
|
||||
<span className="db-kpi-title">{title}</span>
|
||||
<span className="db-kpi-subtitle">{subtitle}</span>
|
||||
|
||||
{loading && (
|
||||
<div className="db-skeleton-block" style={{ height: 36, margin: '10px 0 4px', borderRadius: 6 }} />
|
||||
)}
|
||||
{!loading && suppressed && (
|
||||
<div className="db-kpi-suppressed-block" role="status">
|
||||
<span className="db-suppressed-icon" aria-hidden="true">🔒</span>
|
||||
<span className="db-kpi-value db-kpi-value-suppressed">n < 5</span>
|
||||
</div>
|
||||
)}
|
||||
{!loading && !suppressed && (
|
||||
<>
|
||||
<span className="db-kpi-value" aria-live="polite">{value}</span>
|
||||
<span className="db-kpi-note">{valueNote}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -30,33 +73,24 @@ export function KpiCards({ surveyId, filters }: Props) {
|
||||
}, [surveyId, JSON.stringify(filters)])
|
||||
|
||||
if (error) return <p className="db-error">Error al cargar KPIs</p>
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="db-kpi-row">
|
||||
{[1,2,3,4].map(i => <div key={i} className="db-kpi-card db-skeleton-block" style={{height:96}} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (isSuppressed(data)) {
|
||||
return (
|
||||
<div className="db-kpi-row">
|
||||
{[1,2,3,4].map(i => (
|
||||
<div key={i} className="db-kpi-card db-suppressed-card">
|
||||
<span className="db-suppressed-icon">🔒</span>
|
||||
<span className="db-kpi-sub">n < 5</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const d = data as KpiData
|
||||
const loading = data === null
|
||||
const suppressed = !loading && isSuppressed(data)
|
||||
const ok = !loading && !suppressed
|
||||
|
||||
return (
|
||||
<div className="db-kpi-row">
|
||||
<KpiCard label="Cuidadores activos" value={`${d.pct_cuidadores}%`} sub={`n = ${d.n}`} />
|
||||
<KpiCard label="Score bienestar (0-100)" value={d.score_bienestar !== null ? `${d.score_bienestar}` : '—'} />
|
||||
<KpiCard label="Ausencias frecuentes" value={`${d.pct_ausencias}%`} />
|
||||
<KpiCard label="Total respuestas" value={d.n.toLocaleString('es-PY')} sub="en este corte" />
|
||||
{KPI_DEFS.map((def) => (
|
||||
<KpiCard
|
||||
key={def.title}
|
||||
title={def.title}
|
||||
subtitle={def.subtitle}
|
||||
value={ok ? def.getValue(data as KpiData) : '—'}
|
||||
valueNote={ok ? def.getN(data as KpiData) : ''}
|
||||
suppressed={suppressed}
|
||||
loading={loading}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function ParaguayMap({ surveyId, filters, onDeptoClick }: Props) {
|
||||
const n = !isSuppressed(data) && data ? (data as PrevalenciaData).n_total : undefined
|
||||
|
||||
return (
|
||||
<WidgetShell title="🗺️ Prevalencia de cuidadores por departamento" status={status} n={n} colSpan={6}>
|
||||
<WidgetShell title="Prevalencia de cuidadores por departamento" subtitle="A1 · % cuidadores activos por departamento" status={status} n={n} colSpan={6}>
|
||||
<div className="db-map-wrapper" role="img" aria-label="Mapa de Paraguay con prevalencia de cuidadores por departamento">
|
||||
{tooltip && (
|
||||
<div className="db-map-tooltip" aria-live="polite">
|
||||
|
||||
@@ -31,7 +31,7 @@ export function PiramideDemografica({ surveyId, filters }: { surveyId: string; f
|
||||
const generos = ['Femenino','Masculino','No binario/Otro']
|
||||
|
||||
return (
|
||||
<WidgetShell title="👥 Pirámide demográfica" status={status} n={data?.n_total} colSpan={6}>
|
||||
<WidgetShell title="Pirámide demográfica" subtitle="A5 × A6 · rango etario por género (celdas con k≥5)" status={status} n={data?.n_total} colSpan={6}>
|
||||
{data && (
|
||||
<div className="db-piramide">
|
||||
<div className="db-piramide-legend" aria-label="Leyenda">
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isSuppressed, type RiesgoData } from '@/lib/dashboard-types'
|
||||
import { isSuppressed, type SaturacionData } from '@/lib/dashboard-types'
|
||||
import { queries } from '@/lib/dashboard-queries'
|
||||
import type { DashboardFilters } from '@/lib/dashboard-types'
|
||||
import { WidgetShell } from './widget-shell'
|
||||
|
||||
const RIESGO_COLOR: Record<string, string> = {
|
||||
Alto: '#ef4444',
|
||||
Medio: '#F8AD13',
|
||||
Bajo: '#43BBC8',
|
||||
const SATURACION_COLOR: Record<string, string> = {
|
||||
'Alta saturación': '#ef4444',
|
||||
'Saturación media': '#F8AD13',
|
||||
'Baja saturación': '#43BBC8',
|
||||
}
|
||||
|
||||
export function TablaRiesgo({ surveyId, filters }: { surveyId: string; filters: DashboardFilters }) {
|
||||
const [data, setData] = useState<RiesgoData | null>(null)
|
||||
export function TablaSaturacion({ surveyId, filters }: { surveyId: string; filters: DashboardFilters }) {
|
||||
const [data, setData] = useState<SaturacionData | null>(null)
|
||||
const [sup, setSup] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setData(null); setSup(false)
|
||||
queries.riesgo(surveyId, filters)
|
||||
.then(d => { if (isSuppressed(d)) setSup(true); else setData(d as RiesgoData) })
|
||||
queries.saturacion(surveyId, filters)
|
||||
.then(d => { if (isSuppressed(d)) setSup(true); else setData(d as SaturacionData) })
|
||||
.catch(() => {})
|
||||
}, [surveyId, JSON.stringify(filters)])
|
||||
|
||||
const status = !data && !sup ? 'loading' : sup ? 'suppressed' : 'ok'
|
||||
|
||||
return (
|
||||
<WidgetShell title="📋 Tabla de riesgo de retención" status={status} n={data?.n_total} colSpan={12}>
|
||||
<WidgetShell title="Saturación del cuidador" subtitle="4.3 + 5.1 + 5.2 · indicador compuesto de saturación (bienestar bajo + rendimiento afectado + ausentismo)" status={status} n={data?.n_total} colSpan={12}>
|
||||
{data && (
|
||||
<div className="db-table-wrapper">
|
||||
<p className="db-table-note">
|
||||
Alto: ausencias frecuentes + empresa no comprende. Medio: una condición. Bajo: ninguna.
|
||||
La columna de departamento fue eliminada para evitar cruces reidentificables.
|
||||
Alta: bienestar ≤2, rendimiento afectado casi todos los días y ausentismo frecuente.
|
||||
Media: dos de los tres criterios. Baja: uno o ninguno.
|
||||
</p>
|
||||
<table className="db-table" aria-label="Tabla de riesgo de retención">
|
||||
<table className="db-table" aria-label="Tabla de saturación del cuidador">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Segmento</th>
|
||||
<th>N</th>
|
||||
<th>% del total</th>
|
||||
<th>Política más demandada</th>
|
||||
<th>Apoyo más demandado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -46,14 +46,16 @@ export function TablaRiesgo({ surveyId, filters }: { surveyId: string; filters:
|
||||
<tr key={seg.segmento}>
|
||||
<td>
|
||||
<span className="db-risk-badge"
|
||||
style={{ background: RIESGO_COLOR[seg.segmento] ?? '#6b7280' }}>
|
||||
style={{ background: SATURACION_COLOR[seg.segmento] ?? '#6b7280' }}>
|
||||
{seg.segmento}
|
||||
</span>
|
||||
</td>
|
||||
<td>{seg.suppressed ? '—' : seg.n}</td>
|
||||
<td>{seg.suppressed ? '—' : `${seg.pct}%`}</td>
|
||||
<td>{seg.suppressed ? <span className="db-suppressed-hint">Datos insuficientes (n<5)</span>
|
||||
: seg.politica ?? '—'}</td>
|
||||
<td>{seg.suppressed
|
||||
? <span className="db-suppressed-hint">Datos insuficientes (n<5)</span>
|
||||
: seg.apoyo ?? '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -3,17 +3,21 @@ import type { WidgetStatus } from '@/lib/dashboard-types'
|
||||
|
||||
interface WidgetShellProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
status: WidgetStatus
|
||||
n?: number
|
||||
children: React.ReactNode
|
||||
colSpan?: number
|
||||
}
|
||||
|
||||
export function WidgetShell({ title, status, n, children, colSpan = 6 }: WidgetShellProps) {
|
||||
export function WidgetShell({ title, subtitle, status, n, children, colSpan = 6 }: WidgetShellProps) {
|
||||
return (
|
||||
<div className={`db-widget db-col-${colSpan}`} style={{ gridColumn: `span ${colSpan}` }}>
|
||||
<div className="db-widget-header">
|
||||
<h3 className="db-widget-title">{title}</h3>
|
||||
<div className="db-widget-title-group">
|
||||
<h3 className="db-widget-title">{title}</h3>
|
||||
{subtitle && <p className="db-widget-subtitle">{subtitle}</p>}
|
||||
</div>
|
||||
{n !== undefined && status === 'ok' && (
|
||||
<span className="db-widget-n">n = {n.toLocaleString('es-PY')}</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user