From 157e108322b241bec53d5b9ef22dc09ee8e419ae Mon Sep 17 00:00:00 2001 From: markosbenitez Date: Wed, 10 Jun 2026 15:49:05 -0300 Subject: [PATCH] =?UTF-8?q?Los=20triggers=201.1,=201.6,=201.13=20muestran?= =?UTF-8?q?=20solo=20S=C3=AD=20=C2=B7=20No=20(sin=20"Prefiero=20no=20decir?= =?UTF-8?q?")=20Al=20responder=20"No"=20en=20las=20tres,=20el=20formulario?= =?UTF-8?q?=20salta=20a=206.4=20(no=20muestra=20secciones=202-5=20ni=206.1?= =?UTF-8?q?-6.3)=20La=20pregunta=206.6=20aparece=20antes=20del=20cierre=20?= =?UTF-8?q?Las=20preguntas=20multiple=5Fchoice=20muestran=20el=20hint=20"P?= =?UTF-8?q?od=C3=A9s=20marcar=20m=C3=A1s=20de=20una=20opci=C3=B3n"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OBSERVATIONS.md | 89 ++++++++ SEED_SPEC.md | 75 +++++-- TECH_DEBT.md | 14 ++ app/admin/responses/page.tsx | 204 ++++++++++++++----- app/globals.css | 19 +- components/survey-form.tsx | 4 +- components/widgets/checkbox-group.tsx | 4 +- lib/conditional.ts | 16 +- lib/types.ts | 4 +- migrate-claude/ESTADO_PROYECTO_JUNIO_2026.md | 178 ++++++++++++++++ migrate-claude/PROMPT_NUEVA_SESION.md | 70 +++++++ migrate-claude/TECH_DEBT_ACTUALIZADO.md | 48 +++++ sprints/sprint-3-infra/ETAPA_3I2_PROMPT.md | 174 ++++++++++++++++ sprints/sprint-3-infra/ETAPA_3V_B_PROMPT.md | 175 ++++++++++++++++ supabase/seed.sql | 183 +++++++++-------- 15 files changed, 1096 insertions(+), 161 deletions(-) create mode 100644 OBSERVATIONS.md create mode 100644 migrate-claude/ESTADO_PROYECTO_JUNIO_2026.md create mode 100644 migrate-claude/PROMPT_NUEVA_SESION.md create mode 100644 migrate-claude/TECH_DEBT_ACTUALIZADO.md create mode 100644 sprints/sprint-3-infra/ETAPA_3I2_PROMPT.md create mode 100644 sprints/sprint-3-infra/ETAPA_3V_B_PROMPT.md diff --git a/OBSERVATIONS.md b/OBSERVATIONS.md new file mode 100644 index 0000000..6b068a9 --- /dev/null +++ b/OBSERVATIONS.md @@ -0,0 +1,89 @@ +# OBSERVATIONS.md — Observaciones y breaking changes + +> Registro de cambios al instrumento que rompen compatibilidad con datos existentes +> o que requieren atención del director antes/después de re-seedear. No bloquean el +> trabajo del agente — se documentan para que el director decida qué hacer con los +> datos ya capturados. + +--- + +## Etapa 3I2 — Revisión del instrumento v2.1 (feedback equipo médico) + +### 1. Eliminación de "Prefiero no decir" (Cambio 1) +Afecta: A5, A6, 1.1, 1.5, 1.6, 1.13, 2.5, 3.1, 3.4. + +Si existen respuestas guardadas con el valor `"Prefiero no decir"` en alguna de estas +preguntas, esos valores quedan **huérfanos**: ya no aparecen como opción en el +formulario ni en `seed.sql`, pero permanecen en `responses.answers` tal cual fueron +guardados. No se borran ni se migran automáticamente. + +**Impacto en triggers (1.1, 1.6, 1.13):** estas preguntas ahora tienen solo 2 opciones +(Sí/No). La lógica condicional (incluyendo el nuevo salto-triple, Cambio 4a) solo +reacciona a `"Sí"`. Una respuesta histórica `"Prefiero no decir"` en un trigger se +trataría como "no Sí" a efectos de la lógica condicional si se re-evaluara, pero no +hay re-evaluación retroactiva de respuestas ya enviadas. + +### 2. 2.1 cambia de `single_choice` a `multiple_choice` (Cambio 5) +Respuestas existentes para 2.1 están guardadas como un string único (`"Yo soy el/la +principal"`, etc.). El formulario nuevo espera un array de strings para preguntas +`multiple_choice`. Las respuestas históricas de 2.1 quedan en formato de string, +inconsistente con el nuevo esquema esperado para esa pregunta. + +Ver `TECH_DEBT.md` → TECH-DEBT-006 (deuda de ranking pendiente sobre esta misma +pregunta). + +### 3. Eliminación de la pregunta 2.4 (Cambio 6) +"¿Contás con apoyo de otras personas para el cuidado?" se elimina del seed por ser +similar/redundante con 2.1 (feedback del equipo médico). + +El `DELETE FROM public.questions WHERE survey_id = ...` al inicio de `seed.sql` borra +la fila de la pregunta 2.4 (id `40000000-0000-0000-0000-000000000027`). Si existen +filas en `responses` cuyo `answers` referencian `question_id` = 2.4 (por +`question_id`, no por código embebido en JSON — depende del esquema de `responses`), +esas referencias quedan apuntando a un `question_id` que ya no existe en `questions` +tras el re-seed. + +**No re-seedear la base de producción sin que el director confirme que no hay +respuestas reales que dependan de 2.4**, o decidir conservar la fila de 2.4 marcada +como `archived`/inactiva en lugar de borrarla (cambio de diseño — Nivel 3, no +ejecutado en esta etapa). + +### 4. Cambio de texto en opción de 3.3 (Cambio 8) +"Es difícil sostenerlo con mis ingresos" → "No puedo sostenerlo con mis ingresos". + +Respuestas existentes que eligieron la opción anterior quedan guardadas con el texto +literal `"Es difícil sostenerlo con mis ingresos"`, que ya no coincide con ninguna +opción del seed actual. Cualquier agregación por valor exacto de opción debe +contemplar ambos strings como equivalentes para no perder esos datos en reportes. + +### 5. División de la primera franja salarial en 3.4 (Cambio 9) +"Sueldo mínimo – Gs. 5.000.000" se reemplaza por dos opciones nuevas: "Sueldo mínimo" +y "Gs. 3.000.000 a 5.000.000". + +Respuestas existentes con el valor `"Sueldo mínimo – Gs. 5.000.000"` quedan con ese +valor, que ya no existe como opción. No hay forma automática de reasignar esas +respuestas a una de las dos franjas nuevas (la franja exacta del respondente no se +puede inferir del valor agregado anterior). + +### 6. Cambio de texto en última opción de 6.1 (Cambio 10) +"No, nunca se ha hablado del tema" → "No, no se puede hablar del tema". + +Respuestas existentes con el valor anterior quedan con ese texto literal, que ya no +coincide con ninguna opción del seed actual. + +--- + +## Resumen para el director + +- Ningún cambio de esta etapa borra datos directamente, salvo el `DELETE` estándar de + `questions` por `survey_id` (mecanismo de idempotencia ya existente en `seed.sql`, + no nuevo de esta etapa) — pero ese `DELETE` ahora también elimina la fila de 2.4. +- Los cambios 1, 8, 9 y 10 dejan **valores de texto huérfanos** en respuestas + históricas (ya no matchean ninguna opción vigente). No afecta el funcionamiento del + formulario hacia adelante, pero sí cualquier reporte/agregación que dependa de + matchear el string exacto de la opción. +- El cambio 5 (2.1) deja respuestas históricas en un formato (string) distinto al que + el formulario nuevo produce (array). Si se construye un reporte sobre 2.1, debe + manejar ambos formatos o normalizarse en una migración de datos aparte. +- **No se ejecutó ningún re-seed sobre la base real** — esto aplica solo cuando el + director apruebe re-seedear con `seed.sql` v2.1. diff --git a/SEED_SPEC.md b/SEED_SPEC.md index 8b48b28..67387dd 100644 --- a/SEED_SPEC.md +++ b/SEED_SPEC.md @@ -1,23 +1,31 @@ -# SEED_SPEC.md — Contrato del instrumento "Mapeo de Cuidadores" v2 +# SEED_SPEC.md — Contrato del instrumento "Mapeo de Cuidadores" v2.1 -> Versión 2 — incorpora revisión de la colega (Jun 2026). Reemplaza la v1 (34 preguntas). -> 44 preguntas · 8 secciones · Códigos únicos corregidos. +> Versión 2.1 — incorpora feedback del equipo médico, Etapa 3I2 (Jun 2026). Reemplaza la v2. +> 44 preguntas (se elimina 2.4, se agrega 6.6) · 8 secciones · Códigos únicos corregidos. > Claude Code reescribe `seed.sql` a partir de ESTE contrato. ## Convenciones - Tipos: radio→`single_choice`, checkbox→`multiple_choice`, escala 1-5→`rating`, texto libre largo→`text_long`, texto corto→`text_short`. - **`conditional_rules` en la pregunta DEPENDIENTE** (cada pregunta declara su propia condición). -- Los 3 triggers (1.1, 1.6, 1.13) son `single_choice` de 3 opciones; solo **"Sí"** dispara el bloque. +- Los 3 triggers (1.1, 1.6, 1.13) son `single_choice` de 2 opciones (Sí · No); solo **"Sí"** dispara + el bloque correspondiente. - `is_sensitive=true` en preguntas de salud/discapacidad/diagnóstico (marcadas ✅). - `3.4` (salary) — `is_sensitive=true` + analyst-only (ADR-001 addendum): nunca en vistas org_admin. - RE-INDEX: las preguntas marcadas son candidatas a índice compuesto (metodología TBD Sprint 3). +- En preguntas `multiple_choice` el formulario muestra el subtítulo "Podés marcar más de una opción". +- "Prefiero no decir" eliminada de todas las preguntas (Cambio 1, Etapa 3I2). -## Estructura condicional (3 bloques) +## Estructura condicional (4 bloques) - **cond-disc-propia** — 1.1='Sí' → muestra 1.2, 1.3, 1.4, 1.5 - **cond-familiar** — 1.6='Sí' → muestra 1.7, 1.8, 1.9, 1.10, 1.11, 1.12 - **cond-adulto-mayor** — 1.13='Sí' → muestra 1.14, 1.15, 1.16 -- Secciones A, 2, 3, 4, 5, 6, cierre: **top-level, siempre visibles.** +- **salto-triple** — si 1.1='No' Y 1.6='No' Y 1.13='No' (es decir, ninguno de los tres dio + 'Sí'), se ocultan completas las secciones 2, 3, 4, 5 y las preguntas 6.1, 6.2, 6.3. El + formulario salta directo a 6.4. Implementado como `conditional_rules` + `{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}` en cada + pregunta del bloque (equivalente: se muestra si AL MENOS uno de los tres es 'Sí'). +- Sección A, 6.4-6.6 y Sección 7 (cierre): **top-level, siempre visibles.** --- @@ -28,49 +36,68 @@ | A2 | single_choice | ¿En qué ciudad vivís?* | 219 ciudades (GeoSelectWidget: dependiente de A1) | qi_city — Sprint 3 | | A3 | text_short | ¿En qué barrio vivís? | — (texto libre, opcional) | | | A4 | single_choice | ¿En qué área o gerencia trabajás?* | Directorio · Gerencia · Operaciones/Producción · Comercial/Ventas · Administración/Finanzas · Recursos Humanos/Cultura · Tecnología/Sistemas · Logística/Distribución · Otra área | qi_sector | -| A5 | single_choice | ¿Cuántos años tenés? | 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 | qi_age_bucket | -| A6 | single_choice | ¿Con qué género te identificás? | Femenino · Masculino · No binario/Otro · Prefiero no decir | | +| A5 | single_choice | ¿Cuántos años tenés? | 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 | qi_age_bucket | +| A6 | single_choice | ¿Con qué género te identificás? | Femenino · Masculino · No binario/Otro | | | A7 | single_choice | ¿Cuál es tu modalidad de trabajo actual? | Presencial · Remoto (home office) · Híbrido | | ## Sección 1 — Prevalencia: cuidado y discapacidad *(sensible)* | Code | Tipo | Cond. | Pregunta | Opciones | is_sensitive | |---|---|---|---|---|---| -| 1.1 | single_choice | trigger | ¿Vos mismo/a vivís con alguna discapacidad o condición de salud que afecte tu funcionamiento cotidiano?* | Sí · No · Prefiero no decir | ✅ | +| 1.1 | single_choice | trigger | ¿Vos mismo/a vivís con alguna discapacidad o condición de salud que afecte tu funcionamiento cotidiano?* | Sí · No | ✅ | | 1.2 | multiple_choice | 1.1=Sí | ¿Qué tipo de discapacidad o condición tenés? | Discapacidad física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial/salud mental · Condición crónica que genera limitaciones · Otra | ✅ | -| 1.3 | multiple_choice | 1.1=Sí | ¿Usás algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · No utilizo | ✅ | +| 1.3 | multiple_choice | 1.1=Sí | ¿Usás algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utilizo | ✅ | | 1.4 | single_choice | 1.1=Sí | ¿Contás con certificado oficial de discapacidad? | Sí · No, pero estoy en trámite · No | ✅ | -| 1.5 | single_choice | 1.1=Sí | ¿Tu condición afecta tu desempeño o comodidad en el trabajo? | Sí, de forma significativa · Sí, pero en aspectos puntuales · No, está bien gestionada · Prefiero no decir | ✅ | -| 1.6 | single_choice | trigger | ¿Tenés algún familiar cercano (hijo/a, pareja, hermano/a, padre/madre) que viva con discapacidad?* | Sí · No · Prefiero no decir | ✅ | +| 1.5 | single_choice | 1.1=Sí | ¿Tu condición afecta tu desempeño o comodidad en el trabajo? | Sí, de forma significativa · Sí, pero en aspectos puntuales · No, está bien gestionada | ✅ | +| 1.6 | single_choice | trigger | ¿Tenés algún familiar cercano (hijo/a, pareja, hermano/a, padre/madre) que viva con discapacidad?* | Sí · No | ✅ | | 1.7 | multiple_choice | 1.6=Sí | ¿Cuál es tu vínculo con esa persona? | Hijo/a · Cónyuge/pareja · Padre o madre · Hermano/a · Otro familiar | ✅ | | 1.8 | multiple_choice | 1.6=Sí | ¿Qué tipo de discapacidad tiene? | Física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial/salud mental · Condición crónica severa · Otra | ✅ | | 1.9 | multiple_choice | 1.6=Sí | ¿Qué tipo de apoyo o acompañamiento necesita principalmente? | Movilidad física · Comunicación · Apoyo escolar/educativo · Inclusión laboral · Terapias/rehabilitación · Apoyo emocional/psicosocial | ✅ | -| 1.10 | multiple_choice | 1.6=Sí | ¿Usa algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · No utiliza | ✅ | +| 1.10 | multiple_choice | 1.6=Sí | ¿Usa algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utiliza | ✅ | | 1.11 | single_choice | 1.6=Sí | Si es niño/a o adolescente en edad escolar: ¿está escolarizado/a y enfrenta barreras de aprendizaje? | Escolarizado/a, sin barreras importantes · Escolarizado/a, enfrenta barreras o faltan ajustes · No está escolarizado/a · No aplica | ✅ | | 1.12 | single_choice | 1.6=Sí | ¿La persona con discapacidad se encuentra trabajando actualmente? | Sí, está trabajando · No, y está buscando empleo · No, y no está buscando · No aplica | ✅ | -| 1.13 | single_choice | trigger | ¿Tenés algún adulto mayor (60+ años) a tu cuidado o bajo tu responsabilidad?* | Sí · No · Prefiero no decir | | +| 1.13 | single_choice | trigger | ¿Tenés algún adulto mayor (60+ años) a tu cuidado o bajo tu responsabilidad?* | Sí · No | | | 1.14 | single_choice | 1.13=Sí | ¿Qué nivel de dependencia tiene? (la más representativa) | Independiente, pero necesita supervisión · Dependencia leve · Dependencia moderada · Dependencia severa | ✅ | | 1.15 | multiple_choice | 1.13=Sí | ¿Tiene una condición de salud que genere mayor necesidad de cuidado? | Demencia/Alzheimer · ACV/Parálisis · Enfermedad crónica grave · Movilidad reducida · Deterioro cognitivo · Sin condición específica | ✅ | | 1.16 | single_choice | 1.13=Sí | ¿Vive con vos o en otra vivienda? | Vive en mi hogar · Vive en otra vivienda · Está en residencia/institución | | ## Sección 2 — Intensidad y rol del cuidado (top-level) +> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'. + | Code | Tipo | Pregunta | Opciones | |---|---|---|---| -| 2.1 | single_choice | ¿Quién es el/la responsable principal del cuidado? | Yo soy el/la principal · Comparto con otro familiar · Hay cuidador/a pagado/a · Está en institución/centro de día | +| 2.1 | multiple_choice ⚠️ TECH-DEBT | ¿Quién es el/la responsable principal del cuidado? | Yo soy el/la principal · Comparto con otro familiar · Hay cuidador/a pagado/a · Está en institución/centro de día | | 2.2 | single_choice | ¿Cuántas horas por día dedicás, en promedio, al cuidado directo? | Menos de 1h · Entre 1 y 3h · Entre 3 y 6h · Más de 6h · Varía mucho | | 2.3 | multiple_choice | ¿En qué momentos del día cuidás principalmente? | Antes del trabajo · Durante horario laboral (llamadas/emergencias) · Al llegar del trabajo · Durante la noche · Los fines de semana · Es continuo sin horario fijo | -| 2.4 | single_choice | ¿Contás con apoyo de otras personas para el cuidado? | Sí, apoyo familiar suficiente · Algo de apoyo, no suficiente · No, soy el/la único/a responsable · Tengo apoyo contratado | -| 2.5 | single_choice | Como cuidador/a primario, ¿presentás signos de agotamiento o estrés crónico? | Sí, frecuentemente · A veces · No, lo llevo bien · Prefiero no decir | +| 2.5 | single_choice | Como cuidador/a primario, ¿presentás signos de agotamiento o estrés crónico? | Sí, frecuentemente · A veces · No, lo llevo bien | + +> ⚠️ TECH-DEBT (2.1): pasó de `single_choice` a `multiple_choice` — selección múltiple sin orden +> de prioridad. Deuda: implementar ranking (Principal cuidador soy yo > secundario instituto > +> principal pagado > secundario familiar). Ver `TECH_DEBT.md`. +> +> **2.4 eliminada** (Cambio 6, Etapa 3I2): "¿Contás con apoyo de otras personas para el cuidado?" +> — feedback del equipo médico: pregunta similar/redundante con 2.1. Ver `OBSERVATIONS.md`. ## Sección 3 — Impacto económico del cuidado (top-level) +> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'. +> 3.4: is_sensitive=true + analyst-only (no exponer a org_admin). + | Code | Tipo | Pregunta | Opciones | Notas | |---|---|---|---|---| -| 3.1 | single_choice | ¿Cuánto estimás que gastás por mes en el cuidado? | Sin gastos significativos · Menos de Gs. 500.000 · Gs. 500.000–1.500.000 · Gs. 1.500.000–3.000.000 · Gs. 3.000.000–6.000.000 · Más de Gs. 6.000.000 · Prefiero no decir | | +| 3.1 | single_choice | ¿Cuánto estimás que gastás por mes en el cuidado? | Menos de Gs. 500.000 · Gs. 500.000–1.500.000 · Gs. 1.500.000–3.000.000 · Gs. 3.000.000–6.000.000 · Más de Gs. 6.000.000 | | | 3.2 | multiple_choice | ¿Qué servicios o apoyos utilizás actualmente? | Terapias especializadas · Cuidador/a particular · Centro de día/institución · Transporte adaptado · Medicación alto costo · Equipamiento médico/tecnología asistiva · No utilizo servicios formales | | -| 3.3 | multiple_choice | ¿Cómo sostenés los gastos y responsabilidades del cuidado? | Con mi empleo de tiempo completo · Con mi empleo de tiempo parcial · Con trabajo independiente/emprendimiento · Me ayudan mis familiares · Es difícil sostenerlo con mis ingresos | | -| 3.4 | single_choice | ¿En qué franja salarial mensual te ubicás? *(CONFIDENCIAL — analyst-only)* | Sueldo mínimo – Gs. 5.000.000 · Gs. 5.000.001–8.000.000 · Gs. 8.000.001–12.000.000 · Gs. 12.000.001–15.000.000 · Gs. 15.000.001–18.000.000 · Gs. 18.000.001–25.000.000 · Gs. 25.000.001–50.000.000 · Gs. 50.000.001 o más · Prefiero no decir | is_sensitive=true; never in org_admin views | +| 3.3 | multiple_choice | ¿Cómo sostenés los gastos y responsabilidades del cuidado? | Con mi empleo de tiempo completo · Con mi empleo de tiempo parcial · Con trabajo independiente/emprendimiento · Me ayudan mis familiares · No puedo sostenerlo con mis ingresos | | +| 3.4 | single_choice | ¿En qué franja salarial mensual te ubicás? *(CONFIDENCIAL — analyst-only)* | Sueldo mínimo · Gs. 3.000.000 a 5.000.000 · Gs. 5.000.001–8.000.000 · Gs. 8.000.001–12.000.000 · Gs. 12.000.001–15.000.000 · Gs. 15.000.001–18.000.000 · Gs. 18.000.001–25.000.000 · Gs. 25.000.001–50.000.000 · Gs. 50.000.001 o más | is_sensitive=true; never in org_admin views | | 3.5 | rating | ¿Los gastos de cuidado impactan en tu situación económica personal? | 1=Sin impacto · 2=Leve · 3=Moderado · 4=Alto · 5=Muy alto | RE-INDEX | +> **Cambio 7** (3.1): se elimina "Sin gastos significativos" además de "Prefiero no decir". +> **Cambio 8** (3.3): "Es difícil sostenerlo con mis ingresos" → "No puedo sostenerlo con mis +> ingresos". Breaking change para datos existentes — ver `OBSERVATIONS.md`. +> **Cambio 9** (3.4): "Sueldo mínimo – Gs. 5.000.000" se divide en "Sueldo mínimo" y +> "Gs. 3.000.000 a 5.000.000". Breaking change para datos existentes — ver `OBSERVATIONS.md`. + ## Sección 4 — Tiempo y descanso (top-level) +> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'. + | Code | Tipo | Pregunta | Opciones | |---|---|---|---| | 4.1 | single_choice | ¿Cómo describirías tu nivel de descanso habitual? | Descanso bien, me siento recuperado/a · Descanso de forma irregular, pero lo manejo · Me cuesta descansar, me siento cansado/a con frecuencia · Descanso muy poco, el agotamiento afecta mi día a día | RE-INDEX | @@ -78,19 +105,25 @@ | 4.3 | rating | En una escala del 1 al 5, ¿cómo evaluás tu bienestar general en este momento? | 1=Muy mal · 2=Mal · 3=Regular · 4=Bien · 5=Muy bien | RE-INDEX | ## Sección 5 — Impacto en el trabajo (top-level) +> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'. + | Code | Tipo | Pregunta | Opciones | |---|---|---|---| | 5.1 | single_choice | En los últimos 3 meses, ¿con qué frecuencia sentiste que el cuidado afectó tu concentración o rendimiento? | Nunca o casi nunca · Algunas veces al mes · Varias veces por semana · Casi todos los días | RE-INDEX | | 5.2 | single_choice | En el último año, ¿faltaste al trabajo por situaciones relacionadas al cuidado? | 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) | | ## Sección 6 — Conciencia organizacional (top-level) +> Cond. (6.1-6.3): **salto-triple** — se ocultan si 1.1='No' Y 1.6='No' Y 1.13='No'. +> 6.4-6.6: siempre visibles (destino del salto). + | Code | Tipo | Pregunta | Opciones | Notas | |---|---|---|---|---| -| 6.1 | single_choice | ¿Sentís que tu empresa conoce o comprende la realidad del cuidado y la discapacidad de su gente?* | Sí, me siento acompañado/a · Parcialmente, hay cierta comprensión · No, es algo bastante invisible · No, nunca se ha hablado del tema | RE-INDEX | +| 6.1 | single_choice | ¿Sentís que tu empresa conoce o comprende la realidad del cuidado y la discapacidad de su gente?* | Sí, me siento acompañado/a · Parcialmente, hay cierta comprensión · No, es algo bastante invisible · No, no se puede hablar del tema | RE-INDEX. Cambio 10: última opción reescrita (era "No, nunca se ha hablado del tema"). Breaking change — ver `OBSERVATIONS.md`. | | 6.2 | multiple_choice (**max_select=3**) | ¿Qué apoyos de la empresa te ayudarían más? Elegí hasta 3 | Flexibilidad horaria o trabajo remoto · Apoyo económico o convenios/descuentos · Acceso a terapias o salud · Contención/apoyo emocional o psicológico · Capacitación y autocuidado · Redes de apoyo/grupos de pares · Información sobre recursos y servicios disponibles | ⚠️ Ranking → multiple_choice para v1. Ranking widget al backlog. | | 6.3 | single_choice | ¿Estarías dispuesto/a a participar en un grupo de apoyo para cuidadores dentro de la empresa? | Sí, definitivamente · Tal vez, si hay más personas en la misma situación · No por ahora · Prefiero mantenerlo en privado | | | 6.4 | single_choice | ¿Conocés a algún compañero/a de trabajo que esté al cuidado de una persona con discapacidad o dependiente? | Sí, conozco a varias personas · Sí, conozco a una persona · No conozco a nadie · No estoy seguro/a | Código corregido (era 6.2 duplicado) | | 6.5 | single_choice | ¿Considerás que la empresa debería ofrecer apoyos a quienes cuidan a una persona dependiente? | Sí, definitivamente · Tal vez, dependería del caso · No lo considero necesario · No lo sé | Código corregido (era 6.3 duplicado) | +| 6.6 | single_choice | ¿Estarías dispuesto/a a ser voluntario/a en iniciativas y/o redes de apoyo a personas con discapacidad y/o cuidadores? | Sí · No · Tal vez | Nueva (Cambio 4b, Etapa 3I2) | ## Sección 7 — Para cerrar (top-level) | Code | Tipo | Pregunta | Notas | diff --git a/TECH_DEBT.md b/TECH_DEBT.md index bc8eaf8..f7e57cf 100644 --- a/TECH_DEBT.md +++ b/TECH_DEBT.md @@ -58,3 +58,17 @@ - **Para implementar:** migration futura que agregue extracción de A1/A2 en el insert y extienda la whitelist de `agg_segment`. - **Trigger:** inicio de Sprint 3 (dashboard avanzado con filtros de ciudad). + +--- + +## [TECH-DEBT-006] Ranking widget para 2.1 + +- **Estado:** Etapa 3I2 (Cambio 5) cambió 2.1 ("¿Quién es el/la responsable principal + del cuidado?") de `single_choice` a `multiple_choice`. Permite selección múltiple + pero sin orden de prioridad — pierde la noción de "responsable principal" cuando + el respondente marca más de una opción. +- **Deuda:** implementar widget de ranking para 2.1 con el orden: + Principal cuidador soy yo > secundario instituto > principal pagado > secundario + familiar (mismo patrón que TECH-DEBT-001 para 6.2). +- **Trigger:** decisión del director sobre el tipo de 2.1 en próxima versión del + instrumento. diff --git a/app/admin/responses/page.tsx b/app/admin/responses/page.tsx index 6f64b09..19734d1 100644 --- a/app/admin/responses/page.tsx +++ b/app/admin/responses/page.tsx @@ -1,20 +1,44 @@ import { createClient } from '@supabase/supabase-js' -// Server Component sin auth — herramienta interna de diagnóstico (Etapa 3V). +// Server Component sin auth — herramienta interna de diagnóstico (Etapa 3V-B). // Usa SUPABASE_SERVICE_ROLE_KEY: vive solo acá, server-side, nunca al cliente. export const dynamic = 'force-dynamic' -const MAX_ROWS = 200 +const MAX_SUBMISSIONS = 500 +const PROMPT_MAX_LEN = 60 + +interface AllowedQuestion { + id: string + code: string + prompt: string + type: string + position: number +} + +interface AnswerRow { + value: unknown + question_id: string +} interface ResponseRow { id: string submitted_at: string qi_zone: string | null qi_sector: string | null - qi_age_bucket: string | null - ip_hash: string | null - surveys: { title: string } | { title: string }[] | null - answers: { count: number }[] + answers: AnswerRow[] +} + +interface FlatRow { + key: string + submissionNum: number + isFirstInGroup: boolean + submitted_at: string + qi_zone: string | null + qi_sector: string | null + code: string + prompt: string + type: string + value: unknown } function getSupabaseAdmin() { @@ -29,45 +53,125 @@ function formatDate(iso: string): string { return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}` } -function dash(value: string | null): string { - return value ?? '—' +function truncate(text: string, max = PROMPT_MAX_LEN): string { + return text.length > max ? `${text.slice(0, max)}...` : text +} + +// Spec 4.3 — null/undefined → null (renderizado como badge "sin dato") +function renderValue(value: unknown, type: string): string | null { + if (value === null || value === undefined) return null + + switch (type) { + case 'single_choice': + case 'text_short': + return String(value).replace(/^"|"$/g, '') + case 'multiple_choice': + if (Array.isArray(value)) return value.join(', ') + return String(value) + case 'rating': + return String(value) + default: + return String(value) + } } export default async function AdminResponsesPage() { const supabase = getSupabaseAdmin() - const { data, error } = await supabase - .from('responses') - .select( - ` - id, - submitted_at, - qi_zone, - qi_sector, - qi_age_bucket, - ip_hash, - surveys ( title ), - answers ( count ) - ` - ) - .order('submitted_at', { ascending: false }) - .limit(MAX_ROWS) + // 1. Preguntas permitidas — text_long e is_sensitive=true se excluyen ACÁ, + // antes de tocar la tabla answers. Sus valores nunca se consultan. + const { data: questionsData, error: questionsError } = await supabase + .from('questions') + .select('id, code, prompt, type, position') + .eq('is_sensitive', false) + .neq('type', 'text_long') + .order('position', { ascending: true }) - if (error) { + if (questionsError) { return (

Control de respuestas

No fue posible conectarse a la base de datos.

-
{error.message}
+
{questionsError.message}
) } - const { count: totalCount } = await supabase + const allowedQuestions = (questionsData ?? []) as AllowedQuestion[] + const allowedIds = allowedQuestions.map((q) => q.id) + const questionMap = new Map(allowedQuestions.map((q) => [q.id, q])) + + // 2. Submissions con sus respuestas — answers!inner + .in() filtra el ARRAY + // embebido a solo question_id permitidos (consulta, no post-filtro). + const { data: responsesData, error: responsesError } = + allowedIds.length > 0 + ? await supabase + .from('responses') + .select( + ` + id, + submitted_at, + qi_zone, + qi_sector, + answers!inner ( value, question_id ) + ` + ) + .in('answers.question_id', allowedIds) + .order('submitted_at', { ascending: false }) + .limit(MAX_SUBMISSIONS) + : { data: [] as ResponseRow[], error: null } + + if (responsesError) { + return ( +
+

Control de respuestas

+

No fue posible conectarse a la base de datos.

+
{responsesError.message}
+
+ ) + } + + const responses = (responsesData ?? []) as unknown as ResponseRow[] + + // 3. Contadores totales (independientes del límite de 500 submissions) + const { count: totalSubmissions } = await supabase .from('responses') .select('*', { count: 'exact', head: true }) - const rows = (data ?? []) as unknown as ResponseRow[] + const { count: totalAnswers } = + allowedIds.length > 0 + ? await supabase + .from('answers') + .select('*', { count: 'exact', head: true }) + .in('question_id', allowedIds) + : { count: 0 } + + // 4. Aplanar: una fila por answer, ordenadas por position dentro de cada submission + const rows: FlatRow[] = [] + responses.forEach((response, idx) => { + const answers = response.answers + .filter((a) => questionMap.has(a.question_id)) + .sort( + (a, b) => + questionMap.get(a.question_id)!.position - questionMap.get(b.question_id)!.position + ) + + answers.forEach((a, ai) => { + const q = questionMap.get(a.question_id)! + rows.push({ + key: `${response.id}-${a.question_id}`, + submissionNum: idx + 1, + isFirstInGroup: ai === 0, + submitted_at: response.submitted_at, + qi_zone: response.qi_zone, + qi_sector: response.qi_sector, + code: q.code, + prompt: q.prompt, + type: q.type, + value: a.value, + }) + }) + }) return (
@@ -75,7 +179,7 @@ export default async function AdminResponsesPage() {

Control de respuestas

- {totalCount ?? 0} respuestas en total · últimas {rows.length} · + {totalSubmissions ?? 0} submissions · {totalAnswers ?? 0} respuestas individuales · actualizado {formatDate(new Date().toISOString())}

@@ -88,29 +192,35 @@ export default async function AdminResponsesPage() { # Fecha - Encuesta - Departamento + Código + Pregunta + Tipo + Valor + Departamento Sector - Rango etario - Respuestas - IP hash - {rows.map((row, i) => { - const survey = Array.isArray(row.surveys) ? row.surveys[0] : row.surveys - const answerCount = row.answers?.[0]?.count ?? 0 + {rows.map((row) => { + const groupClass = + row.submissionNum % 2 === 0 ? 'admin-group-even' : 'admin-group-odd' + const trClass = row.isFirstInGroup ? `${groupClass} admin-group-start` : groupClass + const rendered = renderValue(row.value, row.type) return ( - - {i + 1} - {formatDate(row.submitted_at)} - {survey?.title ?? '—'} - {dash(row.qi_zone)} - {dash(row.qi_sector)} - {dash(row.qi_age_bucket)} - {answerCount} - - {row.ip_hash ? `${row.ip_hash.slice(0, 8)}...` : '—'} + + {row.submissionNum} + {row.isFirstInGroup ? formatDate(row.submitted_at) : ''} + {row.code} + {truncate(row.prompt)} + {row.type} + + {rendered === null ? sin dato : rendered} + + + {row.isFirstInGroup ? (row.qi_zone ?? '—') : ''} + + + {row.isFirstInGroup ? (row.qi_sector ?? '—') : ''} ) diff --git a/app/globals.css b/app/globals.css index 2f7cafb..ff1eb46 100644 --- a/app/globals.css +++ b/app/globals.css @@ -765,11 +765,22 @@ body { position: sticky; top: 0; background: var(--color-re-dark); color: #fff; font-weight: 600; text-align: left; padding: 10px 12px; white-space: nowrap; } -.admin-table td { padding: 10px 12px; border-bottom: 1px solid var(--color-border); color: var(--color-text); } -.admin-table tbody tr:nth-child(even) { background: var(--color-bg); } -.admin-table tbody tr:hover { background: var(--color-primary-light); } +.admin-table td { padding: 8px 12px; border-bottom: 1px solid var(--color-border); color: var(--color-text); vertical-align: top; } .admin-table tbody tr:last-child td { border-bottom: none; } -.admin-iphash { font-family: 'Courier New', monospace; color: var(--color-text-muted); } +.admin-table tbody tr.admin-group-even { background: var(--color-bg); } +.admin-table tbody tr.admin-group-start td { border-top: 2px solid var(--color-re-dark); } +.admin-table tbody tr:hover { background: var(--color-primary-light); } +.admin-prompt { color: var(--color-text); } +.admin-type-badge { + display: inline-block; padding: 1px 8px; border-radius: 10px; + background: var(--color-primary-light); color: var(--color-primary-hover); + font-size: 0.7rem; font-weight: 600; white-space: nowrap; +} +.admin-nodata { + display: inline-block; padding: 1px 8px; border-radius: 10px; + background: var(--color-re-light); color: var(--color-re-dark); + opacity: 0.6; font-size: 0.7rem; font-style: italic; +} .admin-empty { padding: 24px; text-align: center; color: var(--color-text-muted); } .admin-error { color: var(--color-error); font-weight: 600; } .admin-error-detail { diff --git a/components/survey-form.tsx b/components/survey-form.tsx index 50563b9..d670522 100644 --- a/components/survey-form.tsx +++ b/components/survey-form.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState, useTransition } from 'react' import type { FormAnswers, Question, Survey } from '@/lib/types' import { clearDraft, loadDraft, saveDraft } from '@/lib/storage' -import { getHiddenCodes, isQuestionVisible } from '@/lib/conditional' +import { getHiddenCodes, isQuestionVisible, ruleQuestionCodes } from '@/lib/conditional' import { submitSurvey } from '@/app/actions/submit-survey' import { ConsentScreen } from './consent-screen' import { ProgressBar } from './progress-bar' @@ -71,7 +71,7 @@ export function SurveyForm({ survey, questions }: SurveyFormProps) { } else { const appeared = questions.some( (q) => - q.conditional_rules?.some((r) => r.if_question === code) && + q.conditional_rules?.some((r) => ruleQuestionCodes(r).includes(code)) && isQuestionVisible(q, updated) && !isQuestionVisible(q, prev) ) diff --git a/components/widgets/checkbox-group.tsx b/components/widgets/checkbox-group.tsx index 9c3c7f7..461b08a 100644 --- a/components/widgets/checkbox-group.tsx +++ b/components/widgets/checkbox-group.tsx @@ -40,11 +40,13 @@ export function CheckboxGroup({ {required && } {required && (obligatorio)} - {maxSelect !== null && ( + {maxSelect !== null ? (

Elegí hasta {maxSelect} opciones {value.length > 0 && ` · ${value.length}/${maxSelect} seleccionadas`}

+ ) : ( +

Podés marcar más de una opción

)}
{options.map((opt) => { diff --git a/lib/conditional.ts b/lib/conditional.ts index 22f961f..0353900 100644 --- a/lib/conditional.ts +++ b/lib/conditional.ts @@ -11,9 +11,17 @@ export function isQuestionVisible( return rules.every((rule) => evaluateRule(rule, answers)) } +/** Normalizes if_question to an array of codes regardless of rule shape. */ +export function ruleQuestionCodes(rule: ConditionalRule): string[] { + return Array.isArray(rule.if_question) ? rule.if_question : [rule.if_question] +} + function evaluateRule(rule: ConditionalRule, answers: FormAnswers): boolean { if (rule.op === 'eq') { - return answers[rule.if_question] === rule.value + return answers[rule.if_question as string] === rule.value + } + if (rule.op === 'eq_any') { + return ruleQuestionCodes(rule).some((code) => answers[code] === rule.value) } return false } @@ -32,8 +40,8 @@ export function getHiddenCodes( return questions .filter((q) => { if (!q.conditional_rules?.length) return false - const dependsOnTrigger = q.conditional_rules.some( - (r) => r.if_question === triggerCode + const dependsOnTrigger = q.conditional_rules.some((r) => + ruleQuestionCodes(r).includes(triggerCode) ) if (!dependsOnTrigger) return false return !isQuestionVisible(q, projected) @@ -46,7 +54,7 @@ export function getTriggerCodes(questions: Question[]): Set { const triggers = new Set() for (const q of questions) { for (const rule of q.conditional_rules ?? []) { - triggers.add(rule.if_question) + for (const code of ruleQuestionCodes(rule)) triggers.add(code) } } return triggers diff --git a/lib/types.ts b/lib/types.ts index 6d9fcc2..bcd1a51 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -19,8 +19,8 @@ export interface RatingOptions { } export interface ConditionalRule { - if_question: string - op: 'eq' + if_question: string | string[] + op: 'eq' | 'eq_any' value: string action: 'show' } diff --git a/migrate-claude/ESTADO_PROYECTO_JUNIO_2026.md b/migrate-claude/ESTADO_PROYECTO_JUNIO_2026.md new file mode 100644 index 0000000..4d80148 --- /dev/null +++ b/migrate-claude/ESTADO_PROYECTO_JUNIO_2026.md @@ -0,0 +1,178 @@ +# ESTADO DEL PROYECTO — Datos País sobre el Cuidado +## Documento de continuidad — junio 2026 + +> Generado al cierre de una sesión extendida de dirección técnica. +> Propósito: minimizar pérdida de contexto al abrir una nueva conversación. +> Director: Marcos (Roy) · Metodología: Maria Mersan · Contacto Fundación: Valeria Arce + +--- + +## 1. Qué es el proyecto + +Plataforma soberana de encuestas sobre economía del cuidado en Paraguay. +- **Producto:** formulario anónimo + dashboard analítico con k-anonimato. +- **Marca:** RE (Responsabilidad y Empatía Empresarial) · Fundación Solidaridad · InQuality. +- **URL producción:** https://economia-cuidado.inqualityhq.com +- **Survey demo:** https://economia-cuidado.inqualityhq.com/?s=30000000-0000-0000-0000-000000000001 + +--- + +## 2. Infraestructura actual — OPERATIVA ✅ + +### Stack +- **App:** Next.js 15.3.3 + TypeScript + Tailwind CSS +- **DB:** Supabase self-hosted en Dokploy (stack oficial clonado) +- **Deploy:** Dokploy v0.29.4 en VPS inq-mart-2 (IP VPN: 172.20.5.11) +- **Repos:** + - App: `git.inquality.com.py/marcos/motor-de-encuestas` (migrado desde GitHub InqGit) + - Supabase stack: `git.inquality.com.py/marcos/supabase-motor-fomularios` +- **Dominio API Supabase:** `db.inqualityhq.com` (Kong expuesto con SSL via Traefik/Let's Encrypt) + +### Supabase self-hosted — detalles técnicos +- Stack corriendo desde repo Gitea (modo Git, NO docker raw — docker raw no tiene ./volumes/db/) +- Kong conectado a `dokploy-network` (persistente en docker-compose.yml del repo) +- Fix aplicado: `LUA_AUTH_EXPR` en `kong-entrypoint.sh` con prefijo `Bearer ` para PostgREST JWT propagation +- Variables de entorno en Dokploy: ANON_KEY, SERVICE_ROLE_KEY, SUPABASE_ANON_KEY, SUPABASE_SERVICE_KEY (nombres que usa kong.yml) +- SMTP_PORT=587 requerido aunque SMTP esté desactivado (GoTrue lo exige como int) +- GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false (GoTrue lo exige como bool explícito) +- Backups: script GFS en /usr/local/bin/supabase-backup.sh, cron 3AM root, retención 3 días diarios + domingos 14 días +- Studio: solo accesible via SSH tunnel `ssh -L 3001:localhost:3001 administrator@172.20.5.11` + +### Variables de entorno de la app Next.js (Build Arguments en Dokploy) +- NEXT_PUBLIC_SUPABASE_URL=https://db.inqualityhq.com +- NEXT_PUBLIC_SUPABASE_ANON_KEY= +- SUPABASE_SERVICE_ROLE_KEY= (Environment Setting, no Build Arg) + +--- + +## 3. Estado del código — Sprint 2 COMPLETADO, Sprint 3 COMPLETADO + +### Migraciones aplicadas (12 en total, en orden) +1. 20260531000001_create_tables.sql — 7 tablas +2. 20260531000002_enable_rls.sql +3. 20260531000003_rls_policies.sql — PARCIAL (ver TECH-DEBT-007) +4. 20260531000004_constraints_triggers.sql +5. 20260531000005_agg_segment.sql +6. 20260531000006_grants.sql +7. 20260602111851_revoke_consent_select.sql +8. 20260604200932_fix_answers_insert_policy.sql +9. 20260605093948_dashboard_rpc.sql +10. 20260605093949_rls_deny_sensitive.sql +11. 20260605140000_011_truncate_testing_data.sql +12. 20260605140001_012_dashboard_rpc_v2.sql — 11 RPCs v2 del dashboard + +### Políticas RLS activas (7) +- answers_insert, deny_sensitive_answers +- consent_records_insert, consent_versions_select_all +- questions_select_live, responses_insert +- surveys_select_live_anon + +### Políticas RLS que fallaron (TECH-DEBT-007) +- org_select_own, surveys_select_auth, questions_select_auth +- Causa: auth.jwt() deprecado en GoTrue v2.189+ — no es bloqueante para el piloto + +### Dashboard — widgets y códigos reales del instrumento +| Widget | Códigos reales | Subtítulo | +|---|---|---| +| K1 Cuidadores activos | 1.6 + 1.13 | % plantilla con responsabilidades de cuidado | +| K2 Frecuencia ausentismo | 5.2 | % con 4+ ausencias al año | +| K3 Bienestar | 4.3 (rating 1-5) | autorreporte bienestar general | +| W1 Prevalencia por depto | A1 (Departamento) | % cuidadores activos por departamento | +| W2 Perfil población | 1.1+1.6+1.13 | composición no excluyente | +| W3 Madurez organizacional | 6.1(×0.7)+6.5(×0.3) | score 0-100 | +| W4 Horas×Ausentismo | 2.2×5.2 | correlación intensidad/ausentismo | +| W5 Pirámide demográfica | A5×A6 | etario por género (k≥5) | +| W6 Apoyos demandados | 6.2 | top 8 multi-select | +| W7 Intensidad vs Bienestar | 2.2×4.3 | bins k≥5 | +| W8 Saturación cuidador | 4.3+5.1+5.2 | indicador compuesto | + +--- + +## 4. Deuda técnica registrada + +| ID | Descripción | Bloqueante | +|---|---|---| +| TECH-DEBT-006 | Backup remoto S3 (Backblaze B2 o Wasabi ~$5/mes) | **SÍ — antes del primer dato real** | +| TECH-DEBT-007 | auth.jwt() deprecado en GoTrue v2.189+ — current_app_role() y current_org_id() no funcionales | No (service_role bypasa RLS) | +| CVE-2025-66478 | Next.js 15.3.3 con vulnerabilidad de seguridad crítica | **SÍ — antes del primer dato real** | + +--- + +## 5. Prompts pendientes de ejecutar en Claude Code + +### ETAPA 2G — Cierre Sprint 2 (método) +**Archivo:** `sprints/sprint-2/ETAPA_2G_PROMPT.md` +**Qué hace:** promueve Patrón C.8 a estable + documenta AP.10 y AP.11 en method/ANTI_PATTERNS.md con casos reales del proyecto. + +### ETAPA 3V — Vista de control /admin/responses +**Archivo:** `sprints/sprint-3-infra/ETAPA_3V_PROMPT.md` +**Qué hace:** crea ruta Next.js /admin/responses que muestra tabla de todas las respuestas ordenadas por fecha descendente, sin auth, usando service_role. + +--- + +## 6. Contexto comercial + +- **2 empresas comprometidas** para piloto (no en producción aún) +- **Cronograma deck:** JUN 2026 inicio · JUL-AGO relevamiento · SEP análisis · OCT primer informe + evento +- **Meta:** 20 empresas pioneras +- **Precios:** Gs. 4.5M (≤50) · 7.5M (≤200) · 12M (≤400) +- **GAPs deck vs. producto:** + - RE Index no implementado (fórmula: Bienestar×0.35 + Concentración×0.25 + Descanso×0.25 + Apoyo×0.15) + - Informe descargable prometido en el deck, no existe todavía +- **Contacto Fundación:** Valeria Arce — ddff@fundacionsolidaridad.org.py · +595 991 854333 + +--- + +## 7. Documentos producidos en estas sesiones + +| Archivo | Descripción | +|---|---| +| INSTALL_SUPABASE.md | Guía instalación self-hosted (384 líneas) — con tabla de errores conocidos | +| BACKUP_STRATEGY.md | Estrategia backup GFS (335 líneas) — con procedimiento de restore | +| DOCUMENTACION_PLATAFORMA.md | Doc conceptual con 3 diagramas Mermaid | +| DatoPais_DocumentacionConceptual.docx | Word con 3 PNGs de diagramas embebidos | +| DatoPais_DimensionamientoEconomico.docx | Word con 5 charts económicos | +| DatoPais_ValidacionInstrumento.xlsx | Planilla colaborativa 4 hojas | +| sprints/sprint-2/ETAPA_2G_PROMPT.md | Cierre Sprint 2 — PENDIENTE ejecución | +| sprints/sprint-3-infra/ETAPA_3V_PROMPT.md | Vista /admin/responses — PENDIENTE ejecución | + +--- + +## 8. Decisiones clave tomadas (no reabrir) + +1. **docker raw de Dokploy es incompatible con Supabase** — usar siempre modo Git; el compose oficial necesita ./volumes/db/ que docker raw no tiene. +2. **POSTGRES_PASSWORD siempre con openssl rand -hex** — base64 tiene caracteres especiales que rompen el escaping de Docker Compose. +3. **kong.yml: una sola entrada por bloque keyauth_credentials** — dos entradas con el mismo valor JWT causa uniqueness violation. +4. **SMTP_PORT=587 siempre explícito** aunque SMTP esté desactivado — GoTrue lo parsea como int y falla si está vacío. +5. **GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false siempre explícito** — GoTrue lo parsea como bool y falla si está vacío. +6. **LUA_AUTH_EXPR con prefijo "Bearer "** en kong-entrypoint.sh rama legacy — sin el prefijo PostgREST recibe JWT vacío. +7. **kong.yml usa $ANON_KEY y $SERVICE_ROLE_KEY** — alineado con los nombres de variables en Dokploy; SUPABASE_ANON_KEY y SUPABASE_SERVICE_KEY son aliases adicionales. +8. **App Next.js conecta a Supabase por URL pública** (https://db.inqualityhq.com) — las variables NEXT_PUBLIC_* se embeben en el bundle en build time y el navegador no puede resolver nombres Docker internos. +9. **NEXT_PUBLIC_SUPABASE_URL va como Build Argument** en Dokploy, no como Environment Setting — se embebe en el bundle de JavaScript durante el build. +10. **Repos en Gitea** (migrado desde GitHub InqGit) — VS Code configurado apuntando a git.inquality.com.py. + +--- + +## 9. Patrones y anti-patrones del método (pendiente formalización en 2G) + +### C.8 — Verificación de códigos contra fuente única (PROMOVER A ESTABLE) +Todos los widget subtitles y RPC mappings deben verificarse contra el instrumento real (SEED_SPEC.md). El drift silencioso entre instrumento e implementación es un failure mode de alto costo. + +### AP.10 — Propagación silenciosa de inconsistencia entre fuentes +Caso real: Etapa 2D tenía subtítulos con códigos incorrectos que no se detectaron hasta auditoría explícita. Los errores de mapeo de códigos no generan errores en tiempo de compilación. + +### AP.11 — Contrato desactualizado por código +Caso real: Etapa 1D dejó SEED_SPEC.md desactualizado respecto al seed.sql. El documento de referencia divergió del código sin señal visible. + +--- + +## 10. Próximos pasos en orden de prioridad + +1. **TECH-DEBT-006** — Backup remoto S3 (bloqueante antes del primer dato real) +2. **CVE-2025-66478** — Actualizar Next.js antes del primer dato real +3. **ETAPA 2G** — Ejecutar en Claude Code (método, no infraestructura) +4. **ETAPA 3V** — Ejecutar en Claude Code (vista /admin/responses) +5. **RE Index** — Implementar como widget real (confirmar mapeo G1/G2/G4/G5 → códigos seed con Marcos primero) +6. **Informe descargable** — Prometido en deck comercial, no existe +7. **Onboarding 2 empresas comprometidas** — cuando validación esté lista +8. **Validación instrumento** — Respuestas equipo de salud en DatoPais_ValidacionInstrumento.xlsx (12 preguntas metodológicas abiertas) diff --git a/migrate-claude/PROMPT_NUEVA_SESION.md b/migrate-claude/PROMPT_NUEVA_SESION.md new file mode 100644 index 0000000..d3e7c35 --- /dev/null +++ b/migrate-claude/PROMPT_NUEVA_SESION.md @@ -0,0 +1,70 @@ +# PROMPT DE CONTINUIDAD — nueva sesión de dirección técnica +## Datos País sobre el Cuidado + +> Pegá este prompt al inicio de una nueva conversación con Claude. +> Actualizado: junio 2026. + +--- + +Sos el copiloto técnico del proyecto "Datos País sobre el Cuidado" — una plataforma soberana de encuestas sobre economía del cuidado en Paraguay. Ya tenemos múltiples sesiones de trabajo juntos. Este mensaje te pone al día antes de continuar. + +## Contexto del proyecto + +Plataforma de dos caras: formulario web anónimo + dashboard analítico con k-anonimato. Marca RE, Fundación Solidaridad, powered by InQuality. + +- **Stack:** Next.js 15.3.3 + TypeScript + Tailwind CSS + Supabase self-hosted +- **Deploy:** Dokploy v0.29.4 en VPS (IP VPN: 172.20.5.11) +- **URL producción:** https://economia-cuidado.inqualityhq.com +- **Repos:** app en `git.inquality.com.py/marcos/motor-de-encuestas`, Supabase stack en `git.inquality.com.py/marcos/supabase-motor-fomularios` +- **API Supabase:** https://db.inqualityhq.com (Kong con SSL) + +## Estado actual — Sprint 3 COMPLETADO + +El formulario y el dashboard están operativos con Supabase self-hosted. Las 12 migraciones están aplicadas. Backups locales configurados con retención GFS. El formulario carga y permite completar la encuesta end-to-end. + +## Deuda técnica abierta (ver TECH_DEBT.md en el repo) + +- **TECH-DEBT-006** — Backup remoto S3 — BLOQUEANTE antes del primer dato real de empresa +- **TECH-DEBT-007** — auth.jwt() deprecado en GoTrue v2.189+ — no bloqueante para el piloto +- **CVE-2025-66478** — Next.js 15.3.3 con vulnerabilidad crítica — BLOQUEANTE antes del primer dato real + +## Decisiones tomadas — no reabrir + +1. Supabase en Dokploy siempre via modo Git (docker raw no tiene ./volumes/db/) +2. POSTGRES_PASSWORD siempre con openssl rand -hex (evita caracteres problemáticos) +3. kong.yml: una sola entrada por bloque keyauth_credentials +4. SMTP_PORT=587 siempre explícito aunque SMTP esté desactivado +5. GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false siempre explícito +6. LUA_AUTH_EXPR con prefijo "Bearer " en kong-entrypoint.sh rama legacy +7. NEXT_PUBLIC_SUPABASE_URL via Build Argument en Dokploy (no Environment Setting) +8. App Next.js conecta a Supabase por URL pública https://db.inqualityhq.com + +## Prompts pendientes de ejecutar en Claude Code + +1. **ETAPA 2G** — `sprints/sprint-2/ETAPA_2G_PROMPT.md` — cierre Sprint 2, formaliza patrones C.8, AP.10, AP.11 en el método +2. **ETAPA 3V** — `sprints/sprint-3-infra/ETAPA_3V_PROMPT.md` — vista /admin/responses para control operativo + +## Próximos pasos en orden + +1. TECH-DEBT-006 (S3 backup) — bloqueante +2. CVE-2025-66478 (Next.js update) — bloqueante +3. Ejecutar ETAPA 2G en Claude Code +4. Ejecutar ETAPA 3V en Claude Code +5. RE Index — confirmar mapeo G1/G2/G4/G5 → códigos seed antes de implementar +6. Informe descargable — prometido en deck comercial, no existe aún + +## Método de trabajo + +Usamos niveles de autonomía explícitos en los prompts (Nivel 1 = reportar antes de hacer, Nivel 3 = ejecutar con validación al final). Los prompts de Claude Code están en `sprints/`. Las decisiones arquitecturales se documentan como ADRs en `docs/adr/`. Las reglas no negociables del proyecto están en `CLAUDE.md` del repo. + +## Preguntas frecuentes de contexto + +**¿Por qué Supabase self-hosted y no Cloud?** Soberanía del dato — datos de cuidadores paraguayos no deben vivir en servidores de terceros fuera de control del proyecto. Decisión tomada antes de ingresar el primer dato real. + +**¿Por qué k-anonimato k≥5?** Regla no negociable del sistema. En empresas chicas, un cruce demográfico de menos de 5 personas puede reidentificar a un individuo aunque el nombre nunca se haya guardado. + +**¿Por qué el dashboard usa service_role?** Porque las políticas de org_admin con JWT custom claims no están implementadas (TECH-DEBT-007). Para el piloto, el dashboard es herramienta interna y service_role es aceptable. Cuando haya org_admin real, hay que resolver TECH-DEBT-007 primero. + +--- + +¿Listo para continuar? El próximo paso es [COMPLETAR CON LO QUE VAYAS A HACER]. diff --git a/migrate-claude/TECH_DEBT_ACTUALIZADO.md b/migrate-claude/TECH_DEBT_ACTUALIZADO.md new file mode 100644 index 0000000..85fa714 --- /dev/null +++ b/migrate-claude/TECH_DEBT_ACTUALIZADO.md @@ -0,0 +1,48 @@ +# TECH_DEBT.md — actualización junio 2026 +## Para aplicar en el repo del proyecto + +> Reemplazar el contenido existente de TECH_DEBT.md en el repo con este bloque, +> o mergear los ítems nuevos si ya existe el archivo con otros ítems previos. + +--- + +## Deuda técnica abierta + +### TECH-DEBT-006 — Backup remoto S3 +**Estado:** ABIERTO — BLOQUEANTE antes del primer dato real de empresa +**Descripción:** Los backups locales están configurados y funcionando (GFS, 3AM diario, retención 3 días + domingos 14 días). Pero los backups viven solo en el mismo VPS. Si el VPS muere, se pierden datos Y backups. +**Solución:** rsync nocturno a S3-compatible (Backblaze B2 o Wasabi, ~USD 5/mes) usando rclone. +**Referencia:** BACKUP_STRATEGY.md sección 5 — TECH-DEBT-006. + +### TECH-DEBT-007 — auth.jwt() deprecado en GoTrue v2.189+ +**Estado:** ABIERTO — no bloqueante para el piloto +**Descripción:** Las funciones helper `current_app_role()` y `current_org_id()` en migration 003 usan `auth.jwt()` que fue eliminado en GoTrue v2.189+. Las políticas que dependen de estas funciones no se crearon: +- `org_select_own` en organizations +- `surveys_select_auth` en surveys +- `questions_select_auth` en questions +**Impacto actual:** ninguno — el dashboard usa service_role que bypasa RLS. Los respondentes usan anon que tiene las políticas básicas de INSERT. +**Impacto futuro:** org_admin no podrá autenticarse con custom claims (app_role, org_id) hasta resolver esto. +**Solución:** migrar a auth.session() o implementar JWT claims via hook de GoTrue v2. +**NO resolver hasta tener caso de uso real de org_admin autenticado.** + +### CVE-2025-66478 — Next.js 15.3.3 con vulnerabilidad de seguridad +**Estado:** ABIERTO — BLOQUEANTE antes del primer dato real de empresa +**Descripción:** Next.js 15.3.3 tiene una vulnerabilidad de seguridad crítica. El build lo reporta explícitamente: "This version has a security vulnerability. Please upgrade to a patched version." +**Solución:** actualizar next en package.json a la versión parcheada disponible, correr npm install, hacer redeploy. +**Referencia:** https://nextjs.org/blog/CVE-2025-66478 + +--- + +## Deuda técnica cerrada en Sprint 3 + +### TECH-DEBT-002 — KPI NPS eliminado ✅ +KPI NPS eliminado — instrumento no tiene pregunta NPS. + +### TECH-DEBT-003 — Brecha de políticas ✅ +Widget W6 redefinido como demanda-only (6.2), sin oferta. Documentado. + +### TECH-DEBT-004 — Intención directa de salida no capturada +Intención directa de salida laboral no capturada en el instrumento v2. No resuelto pero documentado como gap conocido. + +### TECH-DEBT-005 — A1 País vs A1b ✅ +Cerrado: A1 ya es Departamento (18 opciones Paraguay) en el instrumento final. diff --git a/sprints/sprint-3-infra/ETAPA_3I2_PROMPT.md b/sprints/sprint-3-infra/ETAPA_3I2_PROMPT.md new file mode 100644 index 0000000..50bf7fd --- /dev/null +++ b/sprints/sprint-3-infra/ETAPA_3I2_PROMPT.md @@ -0,0 +1,174 @@ +# ETAPA 3I2 — Revisión del instrumento v2.1 (feedback equipo médico) + +## 1. Objetivo +Aplicar 10 cambios al instrumento "Mapeo de Cuidadores" resultado del feedback del equipo +de salud. Los cambios afectan SEED_SPEC.md, seed.sql, y el código del formulario Next.js. + +--- + +## 2. Contexto previo +- Instrumento actual: v2, 44 preguntas, 8 secciones, en `SEED_SPEC.md`. +- La DB tiene al menos 1 respuesta real. Algunos cambios son breaking para datos existentes + (opciones renombradas, tipo cambiado) — documentar en OBSERVATIONS.md, no bloquear. +- El formulario Next.js usa un sistema de conditional_rules en el JSON de cada pregunta. +- Los 3 triggers (1.1, 1.6, 1.13) controlan bloques condicionales enteros. + +--- + +## 3. DECISIONES YA TOMADAS — no consultar de nuevo +- Cambio 4: el salto a 6.4 ocurre cuando las TRES preguntas 1.1, 1.6, 1.13 dan "No" + simultáneamente. Si al menos una da "Sí", las secciones 2-5 y 6.1-6.3 se muestran normalmente. +- Cambio 4: las secciones 2, 3, 4, 5, 6.1, 6.2, 6.3 se saltean completamente — ir directo a 6.4. +- Cambio 5: 2.1 pasa a multiple_choice. Es breaking para datos existentes — documentar, no bloquear. +- Cambio 6: 2.4 se elimina del seed. Si hay respuestas con question_id de 2.4, se pierden + al re-seedear. Documentar en OBSERVATIONS.md. +- Cambio 4: pregunta 6.6 opciones: Sí · No · Tal vez. +- Cambio 3: "Tecnología asistiva" va en penúltima posición (antes de "No utilizo"/"No utiliza"). +- "Prefiero no decir" se elimina de TODAS las preguntas que lo tengan. + +--- + +## 4. Los 10 cambios — especificación exacta + +### Cambio 1 — Eliminar "Prefiero no decir" de todo el instrumento +Preguntas afectadas: A5, A6, 1.1, 1.5, 1.6, 1.13, 2.5, 3.1, 3.4. +En cada una, eliminar la opción "Prefiero no decir" del array de opciones en seed.sql. +En SEED_SPEC.md, eliminarla de la columna Opciones de cada pregunta. + +⚠️ Los triggers 1.1, 1.6, 1.13 quedan con solo 2 opciones: Sí · No. +La lógica condicional no cambia — solo "Sí" dispara el bloque. + +### Cambio 2 — Subtítulo hint en todas las preguntas multiple_choice +En el componente del formulario que renderiza preguntas de tipo `multiple_choice`, +agregar un subtítulo/hint debajo del prompt: "Podés marcar más de una opción". +Preguntas afectadas: 1.2, 1.3, 1.7, 1.8, 1.9, 1.10, 1.15, 2.3, 3.2, 3.3, 4.2, 6.2. +Es un cambio de UI — no afecta seed.sql ni SEED_SPEC.md. + +### Cambio 3 — Agregar "Tecnología asistiva" en 1.3 y 1.10 +1.3 opciones actuales: Silla de ruedas · Bastón · Andador · Muletas · Prótesis · No utilizo +1.3 opciones nuevas: Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utilizo + +1.10 opciones actuales: Silla de ruedas · Bastón · Andador · Muletas · Prótesis · No utiliza +1.10 opciones nuevas: Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utiliza + +### Cambio 4 — Salto condicional triple + nueva pregunta 6.6 + +#### 4a. Lógica de salto +Cuando las tres responden "No": 1.1="No" AND 1.6="No" AND 1.13="No", +las secciones 2, 3, 4, 5 completas + preguntas 6.1, 6.2, 6.3 se ocultan. +El formulario salta directamente a 6.4. + +Implementación sugerida: agregar una regla condicional en las preguntas de las secciones +2-5 y 6.1-6.3 que las oculte cuando se cumple esa condición triple. La forma exacta de +implementarlo (conditional_rules en cada pregunta vs. lógica en el componente de sección) +la decide Claude Code según la arquitectura existente del formulario. + +#### 4b. Nueva pregunta 6.6 +Agregar al final de la Sección 6 (después de 6.5, antes de Sección 7): +- Code: 6.6 +- Tipo: single_choice +- Pregunta: "¿Estarías dispuesto/a a ser voluntario/a en iniciativas y/o redes de apoyo + a personas con discapacidad y/o cuidadores?" +- Opciones: Sí · No · Tal vez +- is_sensitive: false +- top-level (siempre visible) + +Actualizar position de 7.1 para que quede después de 6.6. +Actualizar el conteo total de preguntas en SEED_SPEC.md (pasa de 44 a 45, o 44 si se +compensa con la eliminación de 2.4 — verificar el conteo final). + +### Cambio 5 — 2.1 pasa a multiple_choice +Cambiar tipo de `single_choice` a `multiple_choice` en seed.sql y SEED_SPEC.md. +Agregar nota en SEED_SPEC.md: +"⚠️ TECH-DEBT: múltiple selección sin orden de prioridad. Deuda: implementar ranking +(Principal cuidador soy yo > secundario instituto > principal pagado > secundario familiar)." + +### Cambio 6 — Eliminar 2.4 +Eliminar la pregunta 2.4 ("¿Contás con apoyo de otras personas para el cuidado?") +del seed.sql y SEED_SPEC.md. +Renumerar positions de las preguntas posteriores si es necesario. +Documentar en OBSERVATIONS.md: "2.4 eliminada por feedback equipo médico (similar a 2.1)." + +### Cambio 7 — En 3.1 eliminar "Sin gastos significativos" +El cambio 1 ya elimina "Prefiero no decir" de 3.1. +Adicionalmente, eliminar "Sin gastos significativos" de las opciones de 3.1. +3.1 opciones finales: Menos de Gs. 500.000 · Gs. 500.000–1.500.000 · Gs. 1.500.000–3.000.000 + · Gs. 3.000.000–6.000.000 · Más de Gs. 6.000.000 + +### Cambio 8 — En 3.3 cambiar texto de opción +Cambiar: "Es difícil sostenerlo con mis ingresos" +Por: "No puedo sostenerlo con mis ingresos" +En seed.sql y SEED_SPEC.md. +Documentar en OBSERVATIONS.md: "Respuestas existentes con el texto anterior quedan +con valor 'Es difícil sostenerlo con mis ingresos' — breaking change en datos existentes." + +### Cambio 9 — En 3.4 reestructurar primera franja salarial +Opción actual: "Sueldo mínimo – Gs. 5.000.000" +Reemplazar por dos opciones: + 1. "Sueldo mínimo" + 2. "Gs. 3.000.000 a 5.000.000" +Las demás franjas no cambian. +Documentar en OBSERVATIONS.md: "Respuestas existentes con 'Sueldo mínimo – Gs. 5.000.000' +quedan con valor anterior — breaking change en datos existentes." + +### Cambio 10 — En 6.1 cambiar texto de última opción +Cambiar: "No, nunca se ha hablado del tema" +Por: "No, no se puede hablar del tema" +En seed.sql y SEED_SPEC.md. +Documentar en OBSERVATIONS.md: "Breaking change en datos existentes para esta opción." + +--- + +## 5. Archivos a modificar + +| Archivo | Cambios | +|---|---| +| `SEED_SPEC.md` | Todos los cambios de opciones, tipos, preguntas nuevas/eliminadas | +| `supabase/seed.sql` | Todos los cambios — es la fuente de verdad para re-seedear | +| Componente formulario (multiple_choice) | Cambio 2: agregar hint | +| Lógica condicional del formulario | Cambio 4a: salto triple | +| `OBSERVATIONS.md` o `TECH_DEBT.md` | Documentar breaking changes y nueva deuda | + +--- + +## 6. Entregables (DoD) + +- [ ] SEED_SPEC.md actualizado con todos los cambios. +- [ ] seed.sql actualizado — re-seedeable desde cero produce el instrumento v2.1. +- [ ] Componente multiple_choice muestra hint "Podés marcar más de una opción". +- [ ] Lógica de salto 1.1+1.6+1.13 todos "No" → salta a 6.4. +- [ ] Pregunta 6.6 visible en el formulario. +- [ ] "Prefiero no decir" ausente en todas las preguntas. +- [ ] 2.4 eliminada del seed. +- [ ] Breaking changes documentados en OBSERVATIONS.md. +- [ ] Build exitoso. +- [ ] `git diff --name-only HEAD` en scope. + +## 7. Qué reportar +``` +Verificado: +- SEED_SPEC.md actualizado ✓ +- seed.sql actualizado ✓ +- Conteo final de preguntas: [N] +- Hint en multiple_choice: ✓ +- Salto triple implementado: ✓ (describir cómo) +- 6.6 agregada ✓ +- "Prefiero no decir" eliminado de: [lista de preguntas] +- 2.4 eliminada ✓ +- Build exitoso ✓ +- Diff en scope ✓ + +Breaking changes documentados en OBSERVATIONS.md: +- [lista] + +Asumido: +- [decisiones de implementación] +``` + +## 8. Qué NO hacer +- ❌ No re-seedear la DB de producción — solo actualizar seed.sql. + Re-seedear producción requiere aprobación explícita del director. +- ❌ No cambiar la estructura de tablas ni agregar columnas — solo opciones y lógica. +- ❌ No tocar el dashboard ni las RPCs. +- ❌ No resolver ítems de TECH_DEBT que no estén listados acá. +- ❌ No cambiar el diseño visual del formulario más allá del hint de multiple_choice. \ No newline at end of file diff --git a/sprints/sprint-3-infra/ETAPA_3V_B_PROMPT.md b/sprints/sprint-3-infra/ETAPA_3V_B_PROMPT.md new file mode 100644 index 0000000..e1e8ea0 --- /dev/null +++ b/sprints/sprint-3-infra/ETAPA_3V_B_PROMPT.md @@ -0,0 +1,175 @@ +# ETAPA 3V-B — Vista de control: tabla plana de respuestas individuales + +## 1. Objetivo +Reemplazar la vista `/admin/responses` actual (una fila por submission) por una tabla plana +con **una fila por respuesta individual** (una fila por cada pregunta respondida), mostrando +todos los campos explícitamente incluyendo nulos marcados visualmente. + +--- + +## 2. Contexto previo +- La Etapa 3V-A implementó la vista de metadatos de submissions (una fila por response). +- El director quiere ver las respuestas individuales a las preguntas, no solo los metadatos. +- El esquema relevante: `responses` → `answers` (via response_id) → `questions` (via question_id). +- `answers.value` es JSONB. Los tipos de preguntas existentes en el instrumento son: + - `single_choice` → value es string: `"Asunción (Capital)"` + - `multiple_choice` → value es array: `["opción1","opción2"]` + - `rating` → value es número: `3` + - `text_short` → value es string corto + - `text_long` → **NUNCA mostrar** — regla no negociable (RLS + privacidad) + +--- + +## 3. DECISIONES YA TOMADAS — no consultar de nuevo +- Ruta: `/admin/responses` — reemplaza la página anterior completamente. +- Sin auth (herramienta interna de control). +- Sin paginación por ahora — máximo 500 filas (limit en la query). +- Ordenado por `responses.submitted_at DESC`, luego por `questions.position ASC` dentro de cada submission. +- Valores nulos mostrados con badge visual "sin dato" — fondo `--color-re-light` (#E7E6E5), texto `--color-re-dark` (#494F4F) opacidad 0.6. +- text_long excluido completamente — ni la columna ni el valor aparecen. +- Convención CSS: `.admin-*` en `app/globals.css`, igual que la etapa anterior. +- Cliente service-role inline en el Server Component, igual que en la versión anterior. + +--- + +## 4. Estructura de la tabla + +### 4.1 Encabezado de página +- Título: "Control de respuestas" +- Subtítulo: total de submissions + total de respuestas individuales + fecha/hora actualización. +- Botón "Actualizar" (``). + +### 4.2 Columnas de la tabla (una fila por answer) + +| Columna | Fuente | Notas | +|---|---|---| +| # submission | índice de la submission (1, 2, 3...) | mismo número para todas las filas de esa submission | +| Fecha | `responses.submitted_at` | DD/MM/YYYY HH:mm — solo en la primera fila de cada submission | +| Código | `questions.code` | A1, A2, 1.1, 1.6, etc. | +| Pregunta | `questions.prompt` | texto de la pregunta, truncado a 60 chars con "..." | +| Tipo | `questions.type` | badge con el tipo | +| Valor | `answers.value` | renderizado según tipo (ver 4.3) | +| Departamento | `responses.qi_zone` | solo en primera fila de cada submission | +| Sector | `responses.qi_sector` | solo en primera fila de cada submission | + +### 4.3 Renderizado del valor según tipo + +```typescript +function renderValue(value: unknown, type: string): string { + if (value === null || value === undefined) return null // → badge "sin dato" + + switch(type) { + case 'single_choice': + case 'text_short': + return String(value).replace(/^"|"$/g, '') // quitar comillas del JSONB string + + case 'multiple_choice': + if (Array.isArray(value)) return value.join(', ') + return String(value) + + case 'rating': + return String(value) // número, opcionalmente con ★ repetidas + + case 'text_long': + return null // NUNCA mostrar — excluir de la query directamente + + default: + return String(value) + } +} +``` + +### 4.4 Agrupación visual de submissions +Las filas de una misma submission deben estar visualmente agrupadas: +- Separador visual entre submissions (border-top más pronunciado o fondo alternado por submission). +- Fecha, Departamento y Sector solo se muestran en la primera fila de cada submission. +- Las filas siguientes de la misma submission muestran esas celdas vacías (o con colspan si es HTML puro). + +--- + +## 5. Query recomendada + +```typescript +const { data } = await supabase + .from('responses') + .select(` + id, + submitted_at, + qi_zone, + qi_sector, + qi_age_bucket, + ip_hash, + surveys ( title ), + answers ( + value, + questions ( + code, + prompt, + type, + position, + is_sensitive + ) + ) + `) + .order('submitted_at', { ascending: false }) + .limit(500) +``` + +Después en el componente, aplanar el resultado: +```typescript +// Para cada response, para cada answer (excluir text_long e is_sensitive): +// generar una fila con los datos de la response + los datos de la answer/question +const rows = responses.flatMap((response, responseIdx) => + response.answers + .filter(a => a.questions?.type !== 'text_long' && !a.questions?.is_sensitive) + .sort((a, b) => (a.questions?.position ?? 0) - (b.questions?.position ?? 0)) + .map((answer, answerIdx) => ({ + responseIdx: responseIdx + 1, + isFirstInGroup: answerIdx === 0, + submitted_at: response.submitted_at, + qi_zone: response.qi_zone, + qi_sector: response.qi_sector, + ip_hash: response.ip_hash, + survey_title: response.surveys?.title, + code: answer.questions?.code, + prompt: answer.questions?.prompt, + type: answer.questions?.type, + value: answer.value, + })) +) +``` + +--- + +## 6. Entregables (DoD) + +- [ ] Ruta `/admin/responses` renderiza con tabla plana (HTTP 200). +- [ ] Una fila por answer, agrupadas visualmente por submission. +- [ ] Columnas: # submission, Fecha, Código, Pregunta (truncada), Tipo, Valor, Departamento, Sector. +- [ ] `text_long` e `is_sensitive=true` excluidos. +- [ ] Valores nulos muestran badge "sin dato" con estilo tenue. +- [ ] Fecha, Departamento, Sector solo en primera fila de cada grupo. +- [ ] Build exitoso. +- [ ] `git diff --name-only HEAD` solo en `app/admin/` y `app/globals.css`. + +## 7. Qué reportar +``` +Verificado: +- Ruta /admin/responses accesible ✓ +- Total submissions: [N] · Total filas en tabla: [N] +- Tipos de preguntas visibles: [lista] +- text_long excluido ✓ / is_sensitive excluido ✓ +- Badge "sin dato" visible en campos nulos ✓ / no hay nulos en los datos actuales +- Build exitoso ✓ +- Diff en scope ✓ + +Asumido: +- [decisiones cosméticas] +``` + +## 8. Qué NO hacer +- ❌ No mostrar text_long ni is_sensitive bajo ningún concepto. +- ❌ No exponer SUPABASE_SERVICE_ROLE_KEY al cliente. +- ❌ No tocar el formulario ni el dashboard. +- ❌ No agregar dependencias npm. +- ❌ No paginar todavía — limit 500 es suficiente para el piloto. \ No newline at end of file diff --git a/supabase/seed.sql b/supabase/seed.sql index 3a1e5b7..b3f3c29 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -1,13 +1,15 @@ -- ============================================================ --- seed.sql — Instrumento "Mapeo de Cuidadores" v2 — 44 preguntas --- Fuente autoritativa: SEED_SPEC.md v2 (Jun 2026) +-- seed.sql — Instrumento "Mapeo de Cuidadores" v2.1 — 44 preguntas +-- Fuente autoritativa: SEED_SPEC.md v2.1 (Jun 2026) — Etapa 3I2 -- Reemplaza v1 (34 preguntas con códigos A-H) -- -- Estructura condicional: -- cond-disc-propia: 1.1='Sí' → 1.2–1.5 (4 dependientes) -- cond-familiar: 1.6='Sí' → 1.7–1.12 (6 dependientes) -- cond-adulto-mayor: 1.13='Sí' → 1.14–1.16 (3 dependientes) --- Resto: top-level, siempre visibles +-- salto-triple (Cambio 4a): si 1.1, 1.6 y 1.13 son TODAS 'No', se ocultan +-- las secciones 2-5 completas y 6.1-6.3 (eq_any 'Sí' sobre 1.1/1.6/1.13) +-- Resto (incl. 6.4-6.6 y sección 7): top-level, siempre visibles -- -- is_sensitive=true: 1.1-1.12, 1.14, 1.15, 3.4 (15 preguntas) -- required=false en todas (equipo decidió captura completa; obligatoriedad se define luego) @@ -16,7 +18,8 @@ -- org: 10000000-0000-0000-0000-000000000001 -- cv-v1: 20000000-0000-0000-0000-000000000001 -- survey: 30000000-0000-0000-0000-000000000001 --- preguntas 40000000-0000-0000-0000-00000000000{01..44} +-- preguntas 40000000-0000-0000-0000-00000000000{01..45} excepto 027 (2.4, eliminada) +-- → 44 preguntas activas -- ============================================================ -- ── Organización demo ───────────────────────────────────────── @@ -137,16 +140,14 @@ VALUES ('40000000-0000-0000-0000-000000000005','30000000-0000-0000-0000-00000000 {"value":"25 a 34 años","label":"25 a 34 años"}, {"value":"35 a 44 años","label":"35 a 44 años"}, {"value":"45 a 54 años","label":"45 a 54 años"}, - {"value":"55 años o más","label":"55 años o más"}, - {"value":"Prefiero no decir","label":"Prefiero no decir"}]'::jsonb); + {"value":"55 años o más","label":"55 años o más"}]'::jsonb); INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) VALUES ('40000000-0000-0000-0000-000000000006','30000000-0000-0000-0000-000000000001', 'A6','single_choice','¿Con qué género te identificás?',6,false,false, '[{"value":"Femenino","label":"Femenino"}, {"value":"Masculino","label":"Masculino"}, - {"value":"No binario/Otro","label":"No binario/Otro"}, - {"value":"Prefiero no decir","label":"Prefiero no decir"}]'::jsonb); + {"value":"No binario/Otro","label":"No binario/Otro"}]'::jsonb); INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) VALUES ('40000000-0000-0000-0000-000000000007','30000000-0000-0000-0000-000000000001', @@ -168,8 +169,7 @@ VALUES ('40000000-0000-0000-0000-000000000008','30000000-0000-0000-0000-00000000 '¿Vos mismo/a vivís con alguna discapacidad o condición de salud que afecte tu funcionamiento cotidiano?', 8,true,false, '[{"value":"Sí","label":"Sí"}, - {"value":"No","label":"No"}, - {"value":"Prefiero no decir","label":"Prefiero no decir"}]'::jsonb); + {"value":"No","label":"No"}]'::jsonb); -- cond-disc-propia: 1.2–1.5 se muestran si 1.1='Sí' INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) @@ -210,8 +210,7 @@ VALUES ('40000000-0000-0000-0000-000000000012','30000000-0000-0000-0000-00000000 '1.5','single_choice','¿Tu condición afecta tu desempeño o comodidad en el trabajo?',12,true,false, '[{"value":"Sí, de forma significativa","label":"Sí, de forma significativa"}, {"value":"Sí, pero en aspectos puntuales","label":"Sí, pero en aspectos puntuales"}, - {"value":"No, está bien gestionada","label":"No, está bien gestionada"}, - {"value":"Prefiero no decir","label":"Prefiero no decir"}]'::jsonb, + {"value":"No, está bien gestionada","label":"No, está bien gestionada"}]'::jsonb, '[{"if_question":"1.1","op":"eq","value":"Sí","action":"show"}]'::jsonb); -- 1.6 trigger — is_sensitive=true @@ -221,8 +220,7 @@ VALUES ('40000000-0000-0000-0000-000000000013','30000000-0000-0000-0000-00000000 '¿Tenés algún familiar cercano (hijo/a, pareja, hermano/a, padre/madre) que viva con discapacidad?', 13,true,false, '[{"value":"Sí","label":"Sí"}, - {"value":"No","label":"No"}, - {"value":"Prefiero no decir","label":"Prefiero no decir"}]'::jsonb); + {"value":"No","label":"No"}]'::jsonb); -- cond-familiar: 1.7–1.12 se muestran si 1.6='Sí' INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) @@ -267,6 +265,7 @@ VALUES ('40000000-0000-0000-0000-000000000017','30000000-0000-0000-0000-00000000 {"value":"Andador","label":"Andador"}, {"value":"Muletas","label":"Muletas"}, {"value":"Prótesis","label":"Prótesis"}, + {"value":"Tecnología asistiva","label":"Tecnología asistiva"}, {"value":"No utiliza","label":"No utiliza"}]'::jsonb, '[{"if_question":"1.6","op":"eq","value":"Sí","action":"show"}]'::jsonb); @@ -296,8 +295,7 @@ VALUES ('40000000-0000-0000-0000-000000000020','30000000-0000-0000-0000-00000000 '1.13','single_choice','¿Tenés algún adulto mayor (60+ años) a tu cuidado o bajo tu responsabilidad?', 20,false,false, '[{"value":"Sí","label":"Sí"}, - {"value":"No","label":"No"}, - {"value":"Prefiero no decir","label":"Prefiero no decir"}]'::jsonb); + {"value":"No","label":"No"}]'::jsonb); -- cond-adulto-mayor: 1.14–1.16 se muestran si 1.13='Sí' INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) @@ -331,26 +329,30 @@ VALUES ('40000000-0000-0000-0000-000000000023','30000000-0000-0000-0000-00000000 -- ══════════════════════════════════════════════════════════════ -- SECCIÓN 2 — Intensidad y rol del cuidado (top-level) +-- Cambio 4a: visible si 1.1='Sí' OR 1.6='Sí' OR 1.13='Sí' (eq_any) -- ══════════════════════════════════════════════════════════════ -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +-- 2.1: Cambio 5 — pasa a multiple_choice (TECH-DEBT: ranking, ver TECH_DEBT.md) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000024','30000000-0000-0000-0000-000000000001', - '2.1','single_choice','¿Quién es el/la responsable principal del cuidado?',24,false,false, + '2.1','multiple_choice','¿Quién es el/la responsable principal del cuidado?',24,false,false, '[{"value":"Yo soy el/la principal","label":"Yo soy el/la principal"}, {"value":"Comparto con otro familiar","label":"Comparto con otro familiar"}, {"value":"Hay cuidador/a pagado/a","label":"Hay cuidador/a pagado/a"}, - {"value":"Está en institución/centro de día","label":"Está en institución/centro de día"}]'::jsonb); + {"value":"Está en institución/centro de día","label":"Está en institución/centro de día"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000025','30000000-0000-0000-0000-000000000001', '2.2','single_choice','¿Cuántas horas por día dedicás, en promedio, al cuidado directo?',25,false,false, '[{"value":"Menos de 1h","label":"Menos de 1h"}, {"value":"Entre 1 y 3h","label":"Entre 1 y 3h"}, {"value":"Entre 3 y 6h","label":"Entre 3 y 6h"}, {"value":"Más de 6h","label":"Más de 6h"}, - {"value":"Varía mucho","label":"Varía mucho"}]'::jsonb); + {"value":"Varía mucho","label":"Varía mucho"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000026','30000000-0000-0000-0000-000000000001', '2.3','multiple_choice','¿En qué momentos del día cuidás principalmente?',26,false,false, '[{"value":"Antes del trabajo","label":"Antes del trabajo"}, @@ -358,98 +360,99 @@ VALUES ('40000000-0000-0000-0000-000000000026','30000000-0000-0000-0000-00000000 {"value":"Al llegar del trabajo","label":"Al llegar del trabajo"}, {"value":"Durante la noche","label":"Durante la noche"}, {"value":"Los fines de semana","label":"Los fines de semana"}, - {"value":"Es continuo sin horario fijo","label":"Es continuo sin horario fijo"}]'::jsonb); + {"value":"Es continuo sin horario fijo","label":"Es continuo sin horario fijo"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) -VALUES ('40000000-0000-0000-0000-000000000027','30000000-0000-0000-0000-000000000001', - '2.4','single_choice','¿Contás con apoyo de otras personas para el cuidado?',27,false,false, - '[{"value":"Sí, apoyo familiar suficiente","label":"Sí, apoyo familiar suficiente"}, - {"value":"Algo de apoyo, no suficiente","label":"Algo de apoyo, no suficiente"}, - {"value":"No, soy el/la único/a responsable","label":"No, soy el/la único/a responsable"}, - {"value":"Tengo apoyo contratado","label":"Tengo apoyo contratado"}]'::jsonb); +-- 2.4 eliminada (Cambio 6 — feedback equipo médico, similar a 2.1). Ver OBSERVATIONS.md. -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000028','30000000-0000-0000-0000-000000000001', '2.5','single_choice', - 'Como cuidador/a primario, ¿presentás signos de agotamiento o estrés crónico?',28,false,false, + 'Como cuidador/a primario, ¿presentás signos de agotamiento o estrés crónico?',27,false,false, '[{"value":"Sí, frecuentemente","label":"Sí, frecuentemente"}, {"value":"A veces","label":"A veces"}, - {"value":"No, lo llevo bien","label":"No, lo llevo bien"}, - {"value":"Prefiero no decir","label":"Prefiero no decir"}]'::jsonb); + {"value":"No, lo llevo bien","label":"No, lo llevo bien"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -- ══════════════════════════════════════════════════════════════ -- SECCIÓN 3 — Impacto económico del cuidado (top-level) -- 3.4: is_sensitive=true + analyst-only (no exponer a org_admin) +-- Cambio 4a: visible si 1.1='Sí' OR 1.6='Sí' OR 1.13='Sí' (eq_any) -- ══════════════════════════════════════════════════════════════ -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000029','30000000-0000-0000-0000-000000000001', - '3.1','single_choice','¿Cuánto estimás que gastás por mes en el cuidado?',29,false,false, - '[{"value":"Sin gastos significativos","label":"Sin gastos significativos"}, - {"value":"Menos de Gs. 500.000","label":"Menos de Gs. 500.000"}, + '3.1','single_choice','¿Cuánto estimás que gastás por mes en el cuidado?',28,false,false, + '[{"value":"Menos de Gs. 500.000","label":"Menos de Gs. 500.000"}, {"value":"Gs. 500.000–1.500.000","label":"Gs. 500.000–1.500.000"}, {"value":"Gs. 1.500.000–3.000.000","label":"Gs. 1.500.000–3.000.000"}, {"value":"Gs. 3.000.000–6.000.000","label":"Gs. 3.000.000–6.000.000"}, - {"value":"Más de Gs. 6.000.000","label":"Más de Gs. 6.000.000"}, - {"value":"Prefiero no decir","label":"Prefiero no decir"}]'::jsonb); + {"value":"Más de Gs. 6.000.000","label":"Más de Gs. 6.000.000"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000030','30000000-0000-0000-0000-000000000001', - '3.2','multiple_choice','¿Qué servicios o apoyos utilizás actualmente?',30,false,false, + '3.2','multiple_choice','¿Qué servicios o apoyos utilizás actualmente?',29,false,false, '[{"value":"Terapias especializadas","label":"Terapias especializadas"}, {"value":"Cuidador/a particular","label":"Cuidador/a particular"}, {"value":"Centro de día/institución","label":"Centro de día/institución"}, {"value":"Transporte adaptado","label":"Transporte adaptado"}, {"value":"Medicación alto costo","label":"Medicación alto costo"}, {"value":"Equipamiento médico/tecnología asistiva","label":"Equipamiento médico/tecnología asistiva"}, - {"value":"No utilizo servicios formales","label":"No utilizo servicios formales"}]'::jsonb); + {"value":"No utilizo servicios formales","label":"No utilizo servicios formales"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000031','30000000-0000-0000-0000-000000000001', - '3.3','multiple_choice','¿Cómo sostenés los gastos y responsabilidades del cuidado?',31,false,false, + '3.3','multiple_choice','¿Cómo sostenés los gastos y responsabilidades del cuidado?',30,false,false, '[{"value":"Con mi empleo de tiempo completo","label":"Con mi empleo de tiempo completo"}, {"value":"Con mi empleo de tiempo parcial","label":"Con mi empleo de tiempo parcial"}, {"value":"Con trabajo independiente/emprendimiento","label":"Con trabajo independiente/emprendimiento"}, {"value":"Me ayudan mis familiares","label":"Me ayudan mis familiares"}, - {"value":"Es difícil sostenerlo con mis ingresos","label":"Es difícil sostenerlo con mis ingresos"}]'::jsonb); + {"value":"No puedo sostenerlo con mis ingresos","label":"No puedo sostenerlo con mis ingresos"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -- 3.4: CONFIDENCIAL — is_sensitive=true, analyst-only -- No exponer en ninguna vista accesible a org_admin (ADR-001 addendum) -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000032','30000000-0000-0000-0000-000000000001', - '3.4','single_choice','¿En qué franja salarial mensual te ubicás?',32,true,false, - '[{"value":"Sueldo mínimo – Gs. 5.000.000","label":"Sueldo mínimo – Gs. 5.000.000"}, + '3.4','single_choice','¿En qué franja salarial mensual te ubicás?',31,true,false, + '[{"value":"Sueldo mínimo","label":"Sueldo mínimo"}, + {"value":"Gs. 3.000.000 a 5.000.000","label":"Gs. 3.000.000 a 5.000.000"}, {"value":"Gs. 5.000.001–8.000.000","label":"Gs. 5.000.001–8.000.000"}, {"value":"Gs. 8.000.001–12.000.000","label":"Gs. 8.000.001–12.000.000"}, {"value":"Gs. 12.000.001–15.000.000","label":"Gs. 12.000.001–15.000.000"}, {"value":"Gs. 15.000.001–18.000.000","label":"Gs. 15.000.001–18.000.000"}, {"value":"Gs. 18.000.001–25.000.000","label":"Gs. 18.000.001–25.000.000"}, {"value":"Gs. 25.000.001–50.000.000","label":"Gs. 25.000.001–50.000.000"}, - {"value":"Gs. 50.000.001 o más","label":"Gs. 50.000.001 o más"}, - {"value":"Prefiero no decir","label":"Prefiero no decir"}]'::jsonb); + {"value":"Gs. 50.000.001 o más","label":"Gs. 50.000.001 o más"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000033','30000000-0000-0000-0000-000000000001', - '3.5','rating','¿Los gastos de cuidado impactan en tu situación económica personal?',33,false,false, - '{"min":1,"max":5,"min_label":"Sin impacto","max_label":"Muy alto impacto"}'::jsonb); + '3.5','rating','¿Los gastos de cuidado impactan en tu situación económica personal?',32,false,false, + '{"min":1,"max":5,"min_label":"Sin impacto","max_label":"Muy alto impacto"}'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -- ══════════════════════════════════════════════════════════════ -- SECCIÓN 4 — Tiempo y descanso (top-level) +-- Cambio 4a: visible si 1.1='Sí' OR 1.6='Sí' OR 1.13='Sí' (eq_any) -- ══════════════════════════════════════════════════════════════ -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000034','30000000-0000-0000-0000-000000000001', - '4.1','single_choice','¿Cómo describirías tu nivel de descanso habitual?',34,false,false, + '4.1','single_choice','¿Cómo describirías tu nivel de descanso habitual?',33,false,false, '[{"value":"Descanso bien, me siento recuperado/a","label":"Descanso bien, me siento recuperado/a"}, {"value":"Descanso de forma irregular, pero lo manejo","label":"Descanso de forma irregular, pero lo manejo"}, {"value":"Me cuesta descansar, me siento cansado/a con frecuencia","label":"Me cuesta descansar, me siento cansado/a con frecuencia"}, - {"value":"Descanso muy poco, el agotamiento afecta mi día a día","label":"Descanso muy poco, el agotamiento afecta mi día a día"}]'::jsonb); + {"value":"Descanso muy poco, el agotamiento afecta mi día a día","label":"Descanso muy poco, el agotamiento afecta mi día a día"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000035','30000000-0000-0000-0000-000000000001', - '4.2','multiple_choice','¿Qué aspectos de tu vida se ven más afectados por tu rol de cuidador/a?',35,false,false, + '4.2','multiple_choice','¿Qué aspectos de tu vida se ven más afectados por tu rol de cuidador/a?',34,false,false, '[{"value":"Tiempo libre y ocio","label":"Tiempo libre y ocio"}, {"value":"Sueño y descanso","label":"Sueño y descanso"}, {"value":"Vida social y amistades","label":"Vida social y amistades"}, @@ -457,80 +460,91 @@ VALUES ('40000000-0000-0000-0000-000000000035','30000000-0000-0000-0000-00000000 {"value":"Salud física personal","label":"Salud física personal"}, {"value":"Salud emocional/mental","label":"Salud emocional/mental"}, {"value":"Desarrollo profesional","label":"Desarrollo profesional"}, - {"value":"Situación económica","label":"Situación económica"}]'::jsonb); + {"value":"Situación económica","label":"Situación económica"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000036','30000000-0000-0000-0000-000000000001', - '4.3','rating','En una escala del 1 al 5, ¿cómo evaluás tu bienestar general en este momento?',36,false,false, - '{"min":1,"max":5,"min_label":"Muy mal","max_label":"Muy bien"}'::jsonb); + '4.3','rating','En una escala del 1 al 5, ¿cómo evaluás tu bienestar general en este momento?',35,false,false, + '{"min":1,"max":5,"min_label":"Muy mal","max_label":"Muy bien"}'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -- ══════════════════════════════════════════════════════════════ -- SECCIÓN 5 — Impacto en el trabajo (top-level) +-- Cambio 4a: visible si 1.1='Sí' OR 1.6='Sí' OR 1.13='Sí' (eq_any) -- ══════════════════════════════════════════════════════════════ -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000037','30000000-0000-0000-0000-000000000001', '5.1','single_choice', 'En los últimos 3 meses, ¿con qué frecuencia sentiste que el cuidado afectó tu concentración o rendimiento?', - 37,false,false, + 36,false,false, '[{"value":"Nunca o casi nunca","label":"Nunca o casi nunca"}, {"value":"Algunas veces al mes","label":"Algunas veces al mes"}, {"value":"Varias veces por semana","label":"Varias veces por semana"}, - {"value":"Casi todos los días","label":"Casi todos los días"}]'::jsonb); + {"value":"Casi todos los días","label":"Casi todos los días"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000038','30000000-0000-0000-0000-000000000001', '5.2','single_choice', - 'En el último año, ¿faltaste al trabajo por situaciones relacionadas al cuidado?',38,false,false, + 'En el último año, ¿faltaste al trabajo por situaciones relacionadas al cuidado?',37,false,false, '[{"value":"Nunca","label":"Nunca"}, {"value":"Pocas veces (1–3 veces al año)","label":"Pocas veces (1–3 veces al año)"}, {"value":"Varias veces (4–10 veces al año)","label":"Varias veces (4–10 veces al año)"}, - {"value":"Con frecuencia (más de 10 veces al año)","label":"Con frecuencia (más de 10 veces al año)"}]'::jsonb); + {"value":"Con frecuencia (más de 10 veces al año)","label":"Con frecuencia (más de 10 veces al año)"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -- ══════════════════════════════════════════════════════════════ -- SECCIÓN 6 — Conciencia organizacional (top-level) +-- 6.1-6.3: Cambio 4a — visible si 1.1='Sí' OR 1.6='Sí' OR 1.13='Sí' (eq_any) +-- 6.4-6.6: siempre visibles (destino del salto triple) -- 6.2: multiple_choice max_select=3 (ranking en backlog) -- 6.4/6.5: códigos corregidos (eran 6.2/6.3 duplicados en el HTML original) +-- 6.6: nueva pregunta (Cambio 4b) -- ══════════════════════════════════════════════════════════════ -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000039','30000000-0000-0000-0000-000000000001', '6.1','single_choice', '¿Sentís que tu empresa conoce o comprende la realidad del cuidado y la discapacidad de su gente?', - 39,false,false, + 38,false,false, '[{"value":"Sí, me siento acompañado/a","label":"Sí, me siento acompañado/a"}, {"value":"Parcialmente, hay cierta comprensión","label":"Parcialmente, hay cierta comprensión"}, {"value":"No, es algo bastante invisible","label":"No, es algo bastante invisible"}, - {"value":"No, nunca se ha hablado del tema","label":"No, nunca se ha hablado del tema"}]'::jsonb); + {"value":"No, no se puede hablar del tema","label":"No, no se puede hablar del tema"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,max_select,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,max_select,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000040','30000000-0000-0000-0000-000000000001', - '6.2','multiple_choice','¿Qué apoyos de la empresa te ayudarían más? Elegí hasta 3',40,false,false,3, + '6.2','multiple_choice','¿Qué apoyos de la empresa te ayudarían más? Elegí hasta 3',39,false,false,3, '[{"value":"Flexibilidad horaria o trabajo remoto","label":"Flexibilidad horaria o trabajo remoto"}, {"value":"Apoyo económico o convenios/descuentos","label":"Apoyo económico o convenios/descuentos"}, {"value":"Acceso a terapias o salud","label":"Acceso a terapias o salud"}, {"value":"Contención/apoyo emocional o psicológico","label":"Contención/apoyo emocional o psicológico"}, {"value":"Capacitación y autocuidado","label":"Capacitación y autocuidado"}, {"value":"Redes de apoyo/grupos de pares","label":"Redes de apoyo/grupos de pares"}, - {"value":"Información sobre recursos y servicios disponibles","label":"Información sobre recursos y servicios disponibles"}]'::jsonb); + {"value":"Información sobre recursos y servicios disponibles","label":"Información sobre recursos y servicios disponibles"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); -INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options,conditional_rules) VALUES ('40000000-0000-0000-0000-000000000041','30000000-0000-0000-0000-000000000001', '6.3','single_choice', '¿Estarías dispuesto/a a participar en un grupo de apoyo para cuidadores dentro de la empresa?', - 41,false,false, + 40,false,false, '[{"value":"Sí, definitivamente","label":"Sí, definitivamente"}, {"value":"Tal vez, si hay más personas en la misma situación","label":"Tal vez, si hay más personas en la misma situación"}, {"value":"No por ahora","label":"No por ahora"}, - {"value":"Prefiero mantenerlo en privado","label":"Prefiero mantenerlo en privado"}]'::jsonb); + {"value":"Prefiero mantenerlo en privado","label":"Prefiero mantenerlo en privado"}]'::jsonb, + '[{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}]'::jsonb); INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) VALUES ('40000000-0000-0000-0000-000000000042','30000000-0000-0000-0000-000000000001', '6.4','single_choice', '¿Conocés a algún compañero/a de trabajo que esté al cuidado de una persona con discapacidad o dependiente?', - 42,false,false, + 41,false,false, '[{"value":"Sí, conozco a varias personas","label":"Sí, conozco a varias personas"}, {"value":"Sí, conozco a una persona","label":"Sí, conozco a una persona"}, {"value":"No conozco a nadie","label":"No conozco a nadie"}, @@ -540,12 +554,21 @@ INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitiv VALUES ('40000000-0000-0000-0000-000000000043','30000000-0000-0000-0000-000000000001', '6.5','single_choice', '¿Considerás que la empresa debería ofrecer apoyos a quienes cuidan a una persona dependiente?', - 43,false,false, + 42,false,false, '[{"value":"Sí, definitivamente","label":"Sí, definitivamente"}, {"value":"Tal vez, dependería del caso","label":"Tal vez, dependería del caso"}, {"value":"No lo considero necesario","label":"No lo considero necesario"}, {"value":"No lo sé","label":"No lo sé"}]'::jsonb); +INSERT INTO public.questions (id,survey_id,code,type,prompt,position,is_sensitive,required,options) +VALUES ('40000000-0000-0000-0000-000000000045','30000000-0000-0000-0000-000000000001', + '6.6','single_choice', + '¿Estarías dispuesto/a a ser voluntario/a en iniciativas y/o redes de apoyo a personas con discapacidad y/o cuidadores?', + 43,false,false, + '[{"value":"Sí","label":"Sí"}, + {"value":"No","label":"No"}, + {"value":"Tal vez","label":"Tal vez"}]'::jsonb); + -- ══════════════════════════════════════════════════════════════ -- SECCIÓN 7 — Para cerrar (top-level)