Files
motor-de-encuestas/TECH_DEBT.md
markosbenitez a0253b4f90 feat(admin): vista respuestas mejorada + vista detallada [admin]
/admin/responses: filtro survey dinámico (select, no hardcodeado),
paginación 50/página, link a vista detallada, conteos consistentes con
el filtro de preguntas permitidas (evita página fantasma al final).
/admin/responses/[id]: respuestas agrupadas por sección (derivado de
type='section', igual que el wizard — no existen section_number/
section_title, ver TECH-DEBT-011), instancias bajo "Familiar N".
RLS y exclusión de sensibles intactos en ambas vistas.

Fixes de revisión antes de cerrar: toggle expandir/colapsar movido a
un <button> real (evita anidar el link "Ver" dentro de un role=button),
formatDate/renderValue unificados en admin-format.ts, queries
independientes en paralelo (Promise.all).
2026-06-27 16:48:32 -03:00

9.2 KiB

TECH_DEBT.md — Deuda técnica registrada

Ítems identificados durante el desarrollo. Cada ítem documenta qué falta, por qué no se hizo ahora, y qué trigger habilitaría su resolución.


[TECH-DEBT-001] Ranking widget para 6.2

  • Estado: 6.2 es actualmente multiple_choice max_select=3. El widget muestra frecuencia simple (top 8).
  • Deuda: cuando el tipo pase a ranking con JSONB {rank1, rank2, rank3}, el widget debe reescribirse para mostrar ponderación por posición.
  • Trigger: decisión del director sobre el tipo de 6.2 en próxima versión del instrumento (anotada en SEED_SPEC.md § Decisiones pendientes).

[TECH-DEBT-002] KPI NPS Cuidador

  • Estado: el instrumento actual no tiene pregunta NPS. El KPI fue eliminado del dashboard en Etapa 2E.
  • Para implementar: agregar en próxima versión del instrumento: "En una escala de 0 a 10, ¿qué tan probable es que recomiendes esta empresa a otra persona cuidadora?"
  • Trigger: decisión de incluir NPS en el instrumento v2.

[TECH-DEBT-003] Brecha de políticas (oferta vs. demanda)

  • Estado: el instrumento captura solo demanda (6.2 = apoyos que ayudarían). No captura qué políticas la empresa ya ofrece. W6 muestra solo demanda.
  • Para implementar: agregar en próxima versión del instrumento: "¿Qué apoyos para el cuidado ofrece tu empresa hoy?" (multi-select con las mismas opciones de 6.2 + "Ninguno" + "No lo sé").
  • Trigger: decisión de agregar la pregunta de oferta al instrumento.

[TECH-DEBT-004] Intención directa de salida laboral

  • Estado: el instrumento no pregunta directamente por intención de dejar el empleo. W8 usa indicador compuesto (4.3 + 5.1 + 5.2) renombrado a "Saturación".
  • Para implementar: agregar en próxima versión: "En los últimos 6 meses, ¿pensaste en dejar este empleo por razones relacionadas al cuidado?" (Sí/Frecuentemente · Sí/A veces · No · Prefiero no decir).
  • Trigger: validación metodológica de que el indicador compuesto no es suficiente para medir intención de salida.

[TECH-DEBT-005] qi_city / qi_country — migración pendiente

  • Estado: A1 (Departamento) y A2 (Ciudad) se guardan en answers. Las columnas qi_city y qi_country en responses son NULL. Sprint 3 las necesita para segmentación avanzada.
  • 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.

[TECH-DEBT-008] /admin/responses sin autenticación

  • Estado: RESUELTO (27 jun 2026, ETAPA_AUTH).
  • Descripción: la vista de control /admin/responses no tenía auth. Se implementó login con Supabase Auth (@supabase/ssr), middleware.ts protegiendo /admin/:path* y /dashboard/:path* con redirect a /login sin sesión, y la página migrada de service_role a la sesión del usuario (anon key + RLS). Sigue excluyendo preguntas is_sensitive=true y text_long — ese diseño no cambió.
  • Nota: la vista no distingue roles dentro de staff todavía (todo usuario con fila en user_roles ve lo mismo). Diferenciar platform_admin de org_admin ahí sería una etapa nueva, no parte de este cierre.

[TECH-DEBT-010] E9 lee modalidad por JOIN a answers en vez de qi_modalidad

  • Estado: ABIERTO — decisión consciente, no un descuido.
  • Descripción: rpc_me_segmentacion (E9, Mirada Empresa) segmenta por nivel organizacional y por modalidad de trabajo. El nivel (2.6) sí se promovió a responses.qi_sector en el submit (Etapa 4G) — E9 lee de ahí directo. La modalidad (2.7) NO tiene una columna qi_modalidad equivalente — promoverla hubiera significado reabrir rpc_submit_response, ya verificada contra Postgres real en 4G. Para no reabrirla, E9 resuelve modalidad con un JOIN a answers/questions por code='2.7' en tiempo de consulta.
  • Costo del JOIN vs. la columna: una pasada extra por answers/questions en cada llamada a E9 — aceptable para el volumen actual (decenas/cientos de respuestas por encuesta), pero más lento que leer una columna denormalizada si el dataset crece mucho o si la segmentación por modalidad se usa con frecuencia en varios widgets.
  • Acción si se decide promover: agregar qi_modalidad text a responses, poblarla en rpc_submit_response (requiere re-verificar esa RPC contra Postgres real, igual que se hizo en 4G), y migrar E9 a leerla directo — mismo patrón que se usó para qi_sector.
  • Trigger: evaluar si la Mirada País/Humana también necesitan segmentar por modalidad con frecuencia — si varias RPCs repiten el mismo JOIN, conviene promoverla de una vez en lugar de cada RPC reimplementando el JOIN.

[TECH-DEBT-011] Sección de pregunta inferida por posición, no modelada como dato

  • Estado: ABIERTO — decisión consciente, no un descuido. Surgió en ETAPA_ADMIN (vista detallada /admin/responses/[id], 27 jun 2026): el prompt de etapa asumía columnas questions.section_number/section_title que NO existen. Verificado contra el esquema real (supabase/migrations/ 20260531000001_create_tables.sql — única definición de questions, 20260620000002_form_schema_v3.sql solo agrega comentarios, ninguna migración agrega esas columnas).
  • Descripción: hoy questions no tiene ningún campo que declare "esta pregunta pertenece a la sección X". La sección es una fila más en la misma tabla, con type='section', intercalada por position entre las preguntas reales — el código (lib/form-engine.ts:146-148 en el wizard, y la vista detallada de admin) infiere el agrupamiento recorriendo las filas en orden de position y "abriendo" una sección nueva cada vez que encuentra una fila type='section'. El título de la sección es el prompt de esa fila — no hay un campo title separado.
  • Por qué es deuda, no solo una curiosidad de modelado: el motor está pensado para ser un "motor de formularios de encuestas" genérico — no toda encuesta futura va a tener secciones, y las que las tengan no necesariamente van a querer modelarlas como filas-marcador en la misma tabla que las preguntas. Hoy CUALQUIER consumidor que necesite agrupar por sección (wizard, admin, y a futuro cualquier export/reporte) tiene que reimplementar el mismo recorrido secuencial con el mismo supuesto implícito ("la fila type='section' más cercana hacia atrás en position es la sección de esta pregunta"). Si esa lógica se necesita en un tercer lugar, ya es momento de promoverla a una función compartida — y si el motor crece a encuestas con estructuras más ricas (subsecciones, secciones condicionales, secciones que no son "preguntas" en absoluto), el modelo actual no escala sin reescribir cada consumidor.
  • Alternativa más genérica y escalable (no implementada, propuesta para evaluar cuando se priorice): sacar la sección de questions.type y modelarla como entidad propia — opción simple: questions.section_id uuid NULL REFERENCES sections(id) + tabla sections(id, survey_id, title, position); opción más liviana sin tabla nueva: questions.section_title text NULL + questions.section_position int NULL poblados directamente en cada pregunta (denormalizado, sin fila-marcador type='section' separada). Cualquiera de las dos deja "pertenece a la sección X" como un hecho consultable con una columna, no como un cálculo derivado de recorrer position — y ninguna asume que toda encuesta tiene secciones (el campo es nullable). Migrar implicaría: (1) nueva(s) columna(s)/tabla, (2) backfill desde las filas type='section' existentes, (3) actualizar lib/form-engine.ts y la vista detallada de admin para leer el dato en vez de inferirlo, (4) decidir si type='section' se deprecia o convive.
  • Acción tomada mientras tanto: la vista detallada de admin (app/admin/responses/[id]/page.tsx) replica la misma lógica de lib/form-engine.ts (recorrido secuencial por position, abre sección en cada type='section') — no inventa columnas nuevas, no diverge del modelo actual.
  • Trigger: si una tercera feature necesita el mismo agrupamiento, extraer la lógica a un helper compartido en lib/ en vez de triplicarla. Si el producto efectivamente evoluciona a soportar surveys con estructuras de sección más complejas que el caso actual, evaluar la migración a columna/ tabla explícita antes de seguir apilando consumidores sobre el supuesto implícito.