Los triggers 1.1, 1.6, 1.13 muestran solo Sí · No (sin "Prefiero no decir")
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"
This commit is contained in:
175
sprints/sprint-3-infra/ETAPA_3V_B_PROMPT.md
Normal file
175
sprints/sprint-3-infra/ETAPA_3V_B_PROMPT.md
Normal file
@@ -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" (`<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
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user