Al responder "No" en las tres, el formulario salta a 6.4 (no muestra secciones 2-5 ni 6.1-6.3) La pregunta 6.6 aparece antes del cierre Las preguntas multiple_choice muestran el hint "Podés marcar más de una opción"
6.3 KiB
6.3 KiB
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.valuees 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:3text_short→ value es string cortotext_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 porquestions.position ASCdentro 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-*enapp/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" (
<a href="/admin/responses">).
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
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
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:
// 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/responsesrenderiza 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_longeis_sensitive=trueexcluidos.- 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 HEADsolo enapp/admin/yapp/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.