Files
motor-de-encuestas/sprints/sprint-5/ETAPA_ADMIN_PROMPT.md
2026-06-28 01:36:45 -03:00

6.1 KiB

ETAPA ADMIN — Vista de respuestas mejorada + vista detallada

1. Objetivo

Mejorar /admin/responses para que sea una interfaz de administración real, y crear /admin/responses/[id] como vista detallada de una respuesta individual. Todo detrás del middleware de auth existente.

2. Contexto

  • Estado actual: app/admin/responses/page.tsx (Server Component, 249 líneas) + app/admin/responses/responses-table.tsx (Client Component, 98 líneas).
  • Auth: ya implementada — getSupabaseSession() con anon key + cookies, respeta RLS.
  • Exclusión de sensibles: is_sensitive=false y type != 'text_long' en query de questions — NO tocar, mantener exactamente igual.
  • SURVEY_OPTIONS hardcodeadas con UUIDs literales — reemplazar por query dinámica.
  • LIMIT 500 sin paginación real — reemplazar por paginación con page size de 50.

3. DECISIONES YA TOMADAS — no consultar

  • Paginación: page size 50, parámetro ?page=N en la URL (Server Component, recarga completa — no client-side infinite scroll).
  • Filtro de survey: <select> que carga surveys desde la DB (solo las que el usuario puede ver según RLS), no hardcodeado.
  • Vista detallada: ruta /admin/responses/[id] — Server Component, muestra todas las respuestas no sensibles de esa submission, agrupadas por sección.
  • Diseño: usar clases Tailwind o clases .admin-* existentes — sin nuevo sistema.
  • Sin librería de tablas externa.
  • El expand/collapse por submission en la tabla se conserva.

4. Tareas

4.1 Vista lista mejorada — app/admin/responses/page.tsx

Filtro de survey dinámico: Reemplazar SURVEY_OPTIONS hardcodeadas por query a surveys table:

const { data: surveys } = await supabase
  .from('surveys')
  .select('id, title')
  .order('title')

Renderizar como <select> con onChange que navega a ?survey_id=<id>. Opción "Todos los surveys" con value=''.

Paginación:

  • Leer searchParams.page (default 1).
  • Query de responses: .range((page-1)*50, page*50-1) en vez de .limit(500).
  • Mostrar: "Página N de M · X respuestas totales".
  • Links Anterior / Siguiente que modifican ?page=N en la URL.
  • Si hay menos de 50 resultados, no mostrar paginación.

Columna de acciones: Agregar columna "Ver" en la fila cabecera de cada submission (isFirstInGroup=true) con link a /admin/responses/[response_id].

Header informativo: Mostrar claramente: total de submissions, total de answers, survey seleccionado.

4.2 Vista detallada — app/admin/responses/[id]/page.tsx

Server Component. Ruta: /admin/responses/[id] donde id = response_id (uuid).

Query principal:

// 1. Metadata de la respuesta
const { data: response } = await supabase
  .from('responses')
  .select('id, submitted_at, qi_zone, qi_sector, qi_age_bucket, survey_id')
  .eq('id', id)
  .single()

// 2. Preguntas permitidas (mismo filtro que la lista)
const { data: questions } = await supabase
  .from('questions')
  .select('id, code, prompt, type, position, section_number, section_title')
  .eq('is_sensitive', false)
  .neq('type', 'text_long')
  .eq('survey_id', response.survey_id)
  .order('position')

// 3. Answers de esta respuesta
const { data: answers } = await supabase
  .from('answers')
  .select('question_id, value, instance_id')
  .eq('response_id', id)
  .in('question_id', questions.map(q => q.id))

Layout:

  • Header: fecha, zona, sector, edad — datos de qi_* de la respuesta.
  • Link "← Volver a la lista" que preserva el ?survey_id y ?page anteriores (pasar como searchParam o usar referrer).
  • Respuestas agrupadas por sección (section_number + section_title).
  • Dentro de cada sección: tabla código | pregunta | valor.
  • Instancias (instance_id != 'default'): mostrar agrupadas bajo "Familiar N" con badge visual.
  • Valores nulos: badge "sin respuesta" (igual que la lista).
  • Si id no existe o RLS bloquea: mostrar "Respuesta no encontrada" — no crashear.

4.3 Tabla de respuestas — app/admin/responses/responses-table.tsx

Agregar la columna "Ver" en las filas isFirstInGroup con <Link> a /admin/responses/[submissionId]. Pasar surveyId y page como props para construir el link de "← Volver" en la vista detallada.

5. DoD antes de reportar

  • git log --name-only -1 — listar todos los archivos tocados
  • Filtro de survey carga desde DB, no hardcodeado
  • Paginación funciona: page=1 muestra primeros 50, page=2 los siguientes
  • Link "Ver" en cada submission lleva a /admin/responses/[id]
  • Vista detallada muestra respuestas agrupadas por sección
  • Instancias agrupadas bajo "Familiar N"
  • RLS respetado: misma exclusión de sensibles que la lista
  • "Respuesta no encontrada" si el id no existe
  • Build sin errores: npm run build
  • git log --name-only -1
  • Commit + push a Gitea

6. Qué reportar

Verificado:
- Filtro survey: ✓ — query a tabla surveys, no hardcodeado
- Paginación: ✓ — page size 50, range correcto
- Link "Ver" → /admin/responses/[id]: ✓
- Vista detallada: ✓ — agrupada por sección, instancias bajo "Familiar N"
- RLS/exclusión sensibles: ✓ — mismo filtro que antes
- "No encontrada" si id inválido: ✓
- Build: ✓
- git log --name-only -1: [lista completa]
- Commit: [hash]

Decisiones de diseño tomadas:
- [lo que haya]

Hallazgos:
- [lo que aparezca, especialmente sobre questions.section_number/section_title
  — si esas columnas no existen, reportarlo antes de inventar una solución]

7. Qué NO hacer

  • No tocar el filtro is_sensitive=false ni type != 'text_long'
  • No usar service_role — solo getSupabaseSession() con RLS
  • No agregar librerías de tablas o UI externas
  • No implementar búsqueda por texto (fuera de alcance)
  • No hacer deploy — solo commit y push a Gitea
  • Si section_number o section_title no existen en questions, reportarlo y NO inventar columnas — esperar instrucción

8. Versionado

feat(admin): vista respuestas mejorada + vista detallada [admin]

/admin/responses: filtro survey dinámico, paginación 50/página,
link a vista detallada. /admin/responses/[id]: respuestas por sección,
instancias bajo "Familiar N". RLS y exclusión de sensibles intactos.