Files
2026-06-05 07:18:10 -03:00

292 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROMPT: Dashboard "Economía del Cuidado" — Análisis Empresarial (v2)
> **Cambio clave respecto a v1:** toda la agregación y los filtros de privacidad
> corren **server-side** (RPC de Postgres). El cliente NUNCA consulta `answers` crudo.
> El k-anonimato (k≥5) y la exclusión de campos sensibles son **estructurales**, no
> dependen de recordar un filtro en cada query.
---
## Contexto de negocio
Dashboard analítico de la plataforma "Datos País sobre el Cuidado" (Paraguay). Es el producto de valor para las empresas participantes: entender el impacto de las responsabilidades de cuidado en su fuerza laboral y tomar decisiones basadas en evidencia.
**Audiencia:** directores de RRHH, sostenibilidad, gerencia general de empresas paraguayas.
---
## Stack técnico
- **Frontend:** Next.js 15 (App Router), TypeScript, Tailwind CSS
- **Base de datos:** Supabase (PostgreSQL) — misma instancia que el formulario
- **Gráficos:** Recharts (ya instalado)
- **Mapa:** SVG inline de Paraguay (NO librería externa — ver Widget 1). Esto evita
incompatibilidades de peer-deps con React 19 y no agrega dependencias (alineado con
la política de gobernanza del método).
- **NO agregar nuevas dependencias npm.**
- **Paleta RE:** `#43BBC8` (teal), `#494F4F` (oscuro), `#F8AD13` (naranja), `#EE761A` (naranja intenso), `#E7E6E5` (gris claro), blanco
---
## ⚠️ REGLAS DE PRIVACIDAD — NO NEGOCIABLES (leer primero)
Estas reglas son del proyecto (CLAUDE.md) y tienen prioridad sobre cualquier widget.
Si un widget no puede cumplirlas, NO se construye ese widget — se reporta el conflicto.
1. **k-anonimato k≥5 por construcción.** Toda celda, barra, punto, segmento o
departamento que represente menos de 5 respondentes se SUPRIME (se muestra como
"Datos insuficientes" o se agrega a un grupo mayor). No es post-filtro: las funciones
RPC ya devuelven datos con la supresión aplicada. El cliente nunca ve un N<5.
2. **`3.4` (salario) jamás sale de la base.** Protegido por RLS server-side que niega
`question_code='3.4'` al rol usado por el dashboard. El dashboard NO usa este dato
en ningún cálculo ni widget. No alcanza con filtrarlo en el cliente.
3. **`7.1` (texto libre) jamás llega al dashboard.** Mismo tratamiento que `3.4`:
excluido en la capa de datos, no en el cliente.
4. **El cliente NUNCA consulta la tabla `answers` directamente.** Todo pasa por
funciones RPC que devuelven agregados ya suprimidos. Si el browser pudiera pedir
filas crudas, la privacidad sería inspeccionable/evitable desde la consola.
---
## Arquitectura de datos (server-side)
### Capa 1 — Funciones RPC en Postgres (Supabase)
Crear funciones SQL (`SECURITY DEFINER` con search_path fijado) que:
- Reciben `survey_id` + filtros opcionales (departamento, género, etario, contrato)
- Calculan el agregado de cada widget
- **Aplican supresión k≥5 ANTES de devolver**
- Nunca tocan `3.4` ni `7.1`
- Devuelven JSON tipado
Las funciones van en una migración nueva: `migrations/0XX_dashboard_rpc.sql`.
Listar las funciones a crear (una por widget o agrupadas por afinidad):
```
rpc_dashboard_kpis(survey_id, filters) → KPIs + N total
rpc_prevalencia_por_depto(survey_id, filters) → mapa (con supresión por depto)
rpc_heatmap_intensidad(survey_id, filters) → celdas con N≥5
rpc_distribucion_cuidado(survey_id, filters) → donut
rpc_politicas_gap(survey_id, filters) → barras (demandadas vs existentes)
rpc_score_organizacional(survey_id, filters) → gauge
rpc_piramide_demografica(survey_id, filters) → pirámide (celdas con N≥5)
rpc_riesgo_retencion(survey_id, filters) → tabla (segmentos con N≥5)
rpc_intensidad_vs_productividad(survey_id, filters) → reemplazo del scatter (ver Widget 8)
```
### Capa 2 — Route Handlers (Next.js)
`app/api/dashboard/[widget]/route.ts` — llaman a las RPC con el cliente server-side
de Supabase (service_role NUNCA expuesto al browser; vive solo en el servidor).
Validan que `survey_id` exista y esté `live`.
### Capa 3 — Cliente
Los componentes hacen `fetch('/api/dashboard/...')`. Reciben datos ya agregados y
ya suprimidos. No tienen acceso a Supabase directo.
### Casteo de tipos
`answer_value` es TEXT. Las RPC deben castear explícitamente y manejar no-numéricos:
```sql
-- ejemplo dentro de una RPC
AVG(NULLIF(regexp_replace(answer_value, '[^0-9.]', '', 'g'), '')::numeric)
FILTER (WHERE answer_value ~ '^[0-9.]+$')
```
Valores no parseables o nulos se excluyen del cálculo, no se cuentan como 0.
---
## Esquema de base de datos (referencia)
```sql
surveys (id UUID PK, title TEXT, status TEXT, brand_config JSONB, created_at TIMESTAMP)
responses (id UUID PK, survey_id UUID FK, ip_hash TEXT, created_at TIMESTAMP)
answers (id UUID PK, response_id UUID FK, question_code TEXT, answer_value TEXT, created_at TIMESTAMP)
consent_records (id UUID PK, response_id UUID, operational_consent BOOL, macro_consent BOOL, created_at TIMESTAMP)
```
### Códigos de preguntas (instrumento de 44)
- **A1** Departamento (18 deptos) · **A2** Ciudad · **A3** Barrio (texto) · **A4** Género · **A5** Rango etario · **A6** Educativo · **A7** Contrato
- **1.1** Discapacidad propia (→1.2-1.5) · **1.6** Cuida familiar (→1.7-1.12) · **1.13** Cuida adulto mayor (→1.14-1.16)
- **2.1-2.5** Intensidad (horas, tipo, red de apoyo)
- **3.1** Ausencias días/mes · **3.2** Tardanzas · **3.3** Reducción horas · **3.4** Salario 🔒 · **3.5** Gasto en cuidados
- **4.1-4.3** Sueño, tiempo libre, salud
- **5.1** Productividad (1-5) · **5.2** Consideró dejar el trabajo (Sí/No)
- **6.1** Empresa conoce (Sí/No/Parcial) · **6.2** Políticas que conoce (multi, max 3) · **6.3** Satisfacción (1-5) · **6.4** Políticas que necesita (multi) · **6.5** NPS (0-10)
- **7.1** Texto libre 🔒
---
## Ruta y estructura
`/dashboard?survey_id=UUID`
- Header `SurveyHeader` existente (marca RE)
- Sidebar izquierdo colapsable — filtros globales
- Grid principal de widgets
- Footer `SurveyFooter` existente
### Filtros globales (sidebar)
Multi-select Departamento · checkbox Género · slider dual Rango etario · checkbox Contrato · botón Resetear.
Los filtros se pasan como query params a los Route Handlers; las RPC los aplican server-side.
**Importante:** al combinar filtros, el N puede caer bajo 5 → el widget muestra "Datos insuficientes para esta combinación de filtros" en lugar de datos.
---
## WIDGETS
### Estado de N insuficiente
TODO widget tiene 3 estados: `loading` (skeleton) · `ok` (datos) · `suppressed`
("Datos insuficientes — se necesitan al menos 5 respuestas en este corte"). El estado
`suppressed` lo determina la RPC, no el cliente.
### 🗺️ WIDGET 1 — Mapa de Paraguay (OBLIGATORIO)
**Qué muestra:** prevalencia de cuidadores por departamento (% con Sí en `1.6` o `1.13`).
**Implementación SVG inline:**
- Crear `public/geo/paraguay-departamentos.svg` o un componente con los paths de los
18 departamentos. Fuente del contorno: GeoJSON público de límites administrativos de
Paraguay (nivel ADM1), simplificado. Si no se consigue un SVG fiel, usar una grilla
esquemática de 18 celdas etiquetadas con los departamentos (cuadrícula tipo "tile map")
— es aceptable como fallback y mantiene la interactividad.
- Color por departamento: escala `#E7E6E5` (baja) → `#43BBC8` (alta).
- **Departamentos con N<5 se pintan en gris neutro con patrón rayado y tooltip "Datos
insuficientes"** — NO muestran porcentaje.
- Hover: tooltip con nombre + % + N (solo si N≥5).
- Click: filtra el dashboard a ese departamento.
### 📊 WIDGET 2 — KPI Cards
1. **% Cuidadores activos** — `1.1=Sí` OR `1.6=Sí` OR `1.13=Sí`
2. **Prom. días ausencia/mes** — media de `3.1` (casteo numérico)
3. **Score bienestar** — media de `5.1` (1-5 → 0-100%)
4. **NPS Cuidador** — de `6.5`. **Solo se muestra si N≥10**; si no, "N insuficiente para NPS".
Card: número grande teal, label, animación de conteo al cargar.
### 🔥 WIDGET 3 — Heatmap intensidad vs. ausentismo
X = horas semanales (`2.1`, en bins) · Y = días ausencia (`3.1`, en bins).
Celdas coloreadas por densidad. **Celdas con N<5 se muestran vacías/neutras**, no con
su conteo real. Insight: correlación carga↔ausentismo.
### 🍩 WIDGET 4 — Donut: tipo de cuidado
% discapacidad propia (`1.1`) / cuidador familiar (`1.6`) / cuidador adulto mayor (`1.13`).
Solapamiento calculado (categorías no excluyentes). Agregado simple, sin riesgo de k
salvo que el total del survey sea <5 (entonces todo el widget es `suppressed`).
### 📈 WIDGET 5 — Barras: gap de políticas
Top 8 de `6.4` (necesitadas) vs `6.2` (existentes). Teal = necesidad, naranja = ya existe.
Insight: brecha demanda/oferta de políticas.
### 🎯 WIDGET 6 — Gauge: madurez organizacional
Compuesto: `6.1` (Sí=1/Parcial=0.5/No=0) + `6.3` normalizado + `6.5` normalizado.
Score 0-100. Zonas: 0-33 rojo "Baja madurez" / 34-66 naranja "En desarrollo" / 67-100 teal "Consciente".
### 👥 WIDGET 7 — Pirámide demográfica
Rango etario (`A5`) × género (`A4`). Mujeres izquierda (`#EE761A`), hombres derecha
(`#43BBC8`), no binario (`#F8AD13`). **Cada celda (etario×género) con N<5 se colapsa**
al rango contiguo o se omite con nota al pie.
### 💸 WIDGET 8 — Intensidad de cuidado vs. productividad (REEMPLAZA al scatter)
> El scatter original graficaba un punto por persona = dato individual, reidentificable.
> Se reemplaza por un **hexbin / binned heatmap agregado**:
- X = horas de cuidado (`2.1`, bins) · Y = productividad (`5.1`, 1-5)
- Color de cada bin = cantidad de respondentes (solo bins con N≥5)
- Línea de tendencia calculada sobre los bins, no sobre individuos
- Insight idéntico al scatter (relación carga↔productividad) sin exponer personas
- **NO usa `3.5` (gasto) como en v1**, porque cruzar gasto+productividad+género+horas en
un punto individual era el vector de reidentificación más fuerte.
### 📋 WIDGET 9 — Tabla de riesgo de retención
Segmentos: Alto (`5.2=Sí` AND `6.3≤2`) / Medio (`5.2=Sí` OR `6.3≤2`) / Bajo (resto).
Columnas: Segmento · N · % del total · Política más demandada del segmento.
> **Cambio vs v1:** se ELIMINA la columna "Depto. más frecuente". Cruzar segmento de
> riesgo × departamento producía celdas de N=1-2 reidentificables en el piloto.
> **Cada segmento con N<5 se muestra como "—" (suprimido)**, no con su conteo.
---
## Diseño visual — "deslumbrante"
- Fondo `#0F1923` (oscuro premium) · widgets `#1A2535` con borde teal 20% opacity
- Texto principal blanco, secundario `#E7E6E5`
- CSS Grid 12 columnas, responsive
- KPI con counter animation (requestAnimationFrame), `tabular-nums`
- Hover en widget: `box-shadow: 0 0 20px #43BBC830`
- Transición de filtro 300ms ease
**Layout (12 cols):**
```
Fila 1: [KPI×4] cada uno 3 cols
Fila 2: [MAPA 6 cols][DONUT 3 cols][GAUGE 3 cols]
Fila 3: [HEATMAP 6 cols][PIRÁMIDE 6 cols]
Fila 4: [BARRAS 6 cols][INTENSIDAD/PRODUCTIVIDAD 6 cols]
Fila 5: [TABLA RIESGO 12 cols]
```
---
## Archivos a crear
```
migrations/
0XX_dashboard_rpc.sql — funciones RPC con supresión k≥5
0XX_rls_deny_sensitive.sql — RLS que niega 3.4 y 7.1 al rol del dashboard
app/dashboard/
page.tsx
loading.tsx
app/api/dashboard/
[widget]/route.ts — o un route.ts por widget
components/dashboard/
kpi-cards.tsx · paraguay-map.tsx · heatmap-widget.tsx · donut-cuidados.tsx
politicas-bar.tsx · gauge-organizacional.tsx · piramide-demografica.tsx
intensidad-productividad.tsx · tabla-riesgo.tsx · filter-sidebar.tsx · widget-shell.tsx
lib/
dashboard-queries.ts — fetch a los Route Handlers (NO Supabase directo)
dashboard-types.ts
dashboard-utils.ts — NPS, score organizacional, regresión sobre bins
```
---
## Entregables (DoD)
- [ ] Migración de RPC aplicada; cada función aplica supresión k≥5 internamente
- [ ] Migración RLS que niega `3.4` y `7.1` aplicada y verificada
- [ ] El cliente NO importa el cliente de Supabase para datos — solo hace fetch a `/api/dashboard`
- [ ] `/dashboard?survey_id=UUID` carga con datos reales
- [ ] Los 9 widgets renderizan en uno de los 3 estados (loading/ok/suppressed)
- [ ] Mapa de Paraguay interactivo (hover + click filtra)
- [ ] Filtros globales afectan todos los widgets via query params server-side
- [ ] Widget 8 es agregado (hexbin), NO scatter individual
- [ ] Tabla de riesgo SIN columna de departamento
- [ ] Test manual: con un survey de N=3, todos los widgets muestran "suppressed"
- [ ] `grep -r "3.4" app/ components/ lib/` no encuentra el código usándolo como dato
- [ ] Build sin errores (`next build`)
- [ ] `git diff --name-only HEAD` — solo archivos del scope
## Qué NO hacer
- ❌ Cliente consultando `answers` directo (todo via RPC/Route Handler)
- ❌ Filtros de privacidad en el cliente como única protección
- ❌ Exponer `3.4` o `7.1` en cualquier capa
- ❌ Widgets que grafiquen individuos (scatter de personas, tablas con cruces de N<5)
- ❌ service_role en código de cliente o en `NEXT_PUBLIC_*`
- ❌ Agregar dependencias npm
- ❌ Tocar el formulario, `app/actions/` de submit, o RLS existente de escritura
## Qué reportar
```
Verificado:
- migración RPC aplicada: [funciones creadas]
- migración RLS aplicada: 3.4 y 7.1 negados — confirmado con query de prueba
- cliente sin acceso directo a answers: confirmado (grep)
- build exitoso
- archivos creados: [lista]
- widgets en estado ok / suppressed con datos actuales: [lista]
- prueba k-anonimato (survey N<5 → suppressed): [resultado]
- mapa interactivo: [descripción]
Asumido:
- validación visual pendiente del director
Decisiones proactivas (Nivel 2):
- [cambios fuera de scope exacto]
Conflictos de scope (si git diff mostró algo no autorizado):
- [archivo + qué se hizo]
```