feat(admin): colapsar/expandir submissions + eliminar columna Tipo

This commit is contained in:
markosbenitez
2026-06-10 21:41:47 -03:00
parent 933d00ad58
commit 1bab6874a9
3 changed files with 112 additions and 65 deletions

View File

@@ -1,4 +1,5 @@
import { createClient } from '@supabase/supabase-js'
import { ResponsesTable, type DisplayRow } from './responses-table'
// 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.
@@ -28,19 +29,6 @@ interface ResponseRow {
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() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://placeholder.supabase.co'
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'placeholder'
@@ -147,7 +135,7 @@ export default async function AdminResponsesPage() {
: { count: 0 }
// 4. Aplanar: una fila por answer, ordenadas por position dentro de cada submission
const rows: FlatRow[] = []
const rows: DisplayRow[] = []
responses.forEach((response, idx) => {
const answers = response.answers
.filter((a) => questionMap.has(a.question_id))
@@ -158,17 +146,17 @@ export default async function AdminResponsesPage() {
answers.forEach((a, ai) => {
const q = questionMap.get(a.question_id)!
const isFirstInGroup = ai === 0
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,
isFirstInGroup,
date: isFirstInGroup ? formatDate(response.submitted_at) : '',
code: q.code,
prompt: q.prompt,
type: q.type,
value: a.value,
prompt: truncate(q.prompt),
rendered: renderValue(a.value, q.type),
zone: isFirstInGroup ? (response.qi_zone ?? '—') : '',
sector: isFirstInGroup ? (response.qi_sector ?? '—') : '',
})
})
})
@@ -187,46 +175,7 @@ export default async function AdminResponsesPage() {
</header>
<div className="admin-table-wrapper">
<table className="admin-table">
<thead>
<tr>
<th>#</th>
<th>Fecha</th>
<th>Código</th>
<th>Pregunta</th>
<th>Tipo</th>
<th>Valor</th>
<th className="admin-col-optional">Departamento</th>
<th className="admin-col-optional">Sector</th>
</tr>
</thead>
<tbody>
{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 (
<tr key={row.key} className={trClass}>
<td>{row.submissionNum}</td>
<td>{row.isFirstInGroup ? formatDate(row.submitted_at) : ''}</td>
<td>{row.code}</td>
<td className="admin-prompt">{truncate(row.prompt)}</td>
<td><span className="admin-type-badge">{row.type}</span></td>
<td>
{rendered === null ? <span className="admin-nodata">sin dato</span> : rendered}
</td>
<td className="admin-col-optional">
{row.isFirstInGroup ? (row.qi_zone ?? '—') : ''}
</td>
<td className="admin-col-optional">
{row.isFirstInGroup ? (row.qi_sector ?? '—') : ''}
</td>
</tr>
)
})}
</tbody>
</table>
<ResponsesTable rows={rows} />
{rows.length === 0 && (
<p className="admin-empty">Sin respuestas registradas todavía.</p>
)}

View File

@@ -0,0 +1,98 @@
'use client'
import { useState } from 'react'
export interface DisplayRow {
key: string
submissionNum: number
isFirstInGroup: boolean
date: string
code: string
prompt: string
rendered: string | null
zone: string
sector: string
}
export function ResponsesTable({ rows }: { rows: DisplayRow[] }) {
const [expanded, setExpanded] = useState<Set<number>>(new Set())
function toggle(submissionNum: number) {
setExpanded((prev) => {
const next = new Set(prev)
if (next.has(submissionNum)) next.delete(submissionNum)
else next.add(submissionNum)
return next
})
}
return (
<table className="admin-table">
<thead>
<tr>
<th>#</th>
<th>Fecha</th>
<th>Código</th>
<th>Pregunta</th>
<th>Valor</th>
<th className="admin-col-optional">Departamento</th>
<th className="admin-col-optional">Sector</th>
</tr>
</thead>
<tbody>
{rows
.filter((row) => row.isFirstInGroup || expanded.has(row.submissionNum))
.map((row) => {
const groupClass =
row.submissionNum % 2 === 0 ? 'admin-group-even' : 'admin-group-odd'
const isExpanded = expanded.has(row.submissionNum)
const trClass = row.isFirstInGroup
? `${groupClass} admin-group-start admin-row-toggle`
: groupClass
return (
<tr
key={row.key}
className={trClass}
onClick={row.isFirstInGroup ? () => toggle(row.submissionNum) : undefined}
onKeyDown={
row.isFirstInGroup
? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
toggle(row.submissionNum)
}
}
: undefined
}
role={row.isFirstInGroup ? 'button' : undefined}
tabIndex={row.isFirstInGroup ? 0 : undefined}
aria-expanded={row.isFirstInGroup ? isExpanded : undefined}
>
<td>
{row.isFirstInGroup && (
<span className="admin-toggle-icon" aria-hidden="true">
{isExpanded ? '▼' : '▶'}
</span>
)}
{row.submissionNum}
</td>
<td>{row.date}</td>
<td>{row.code}</td>
<td className="admin-prompt">{row.prompt}</td>
<td>
{row.rendered === null ? (
<span className="admin-nodata">sin dato</span>
) : (
row.rendered
)}
</td>
<td className="admin-col-optional">{row.zone}</td>
<td className="admin-col-optional">{row.sector}</td>
</tr>
)
})}
</tbody>
</table>
)
}