Files
motor-de-encuestas/app/admin/responses/page.tsx
2026-06-10 08:59:50 -03:00

127 lines
4.0 KiB
TypeScript

import { createClient } from '@supabase/supabase-js'
// Server Component sin auth — herramienta interna de diagnóstico (Etapa 3V).
// Usa SUPABASE_SERVICE_ROLE_KEY: vive solo acá, server-side, nunca al cliente.
export const dynamic = 'force-dynamic'
const MAX_ROWS = 200
interface ResponseRow {
id: string
submitted_at: string
qi_zone: string | null
qi_sector: string | null
qi_age_bucket: string | null
ip_hash: string | null
surveys: { title: string } | { title: string }[] | null
answers: { count: number }[]
}
function getSupabaseAdmin() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://placeholder.supabase.co'
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'placeholder'
return createClient(url, serviceKey, { auth: { persistSession: false } })
}
function formatDate(iso: string): string {
const d = new Date(iso)
const pad = (n: number) => String(n).padStart(2, '0')
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
function dash(value: string | null): string {
return value ?? '—'
}
export default async function AdminResponsesPage() {
const supabase = getSupabaseAdmin()
const { data, error } = await supabase
.from('responses')
.select(
`
id,
submitted_at,
qi_zone,
qi_sector,
qi_age_bucket,
ip_hash,
surveys ( title ),
answers ( count )
`
)
.order('submitted_at', { ascending: false })
.limit(MAX_ROWS)
if (error) {
return (
<div className="admin-page" role="main">
<h1 className="admin-title">Control de respuestas</h1>
<p className="admin-error">No fue posible conectarse a la base de datos.</p>
<pre className="admin-error-detail">{error.message}</pre>
</div>
)
}
const { count: totalCount } = await supabase
.from('responses')
.select('*', { count: 'exact', head: true })
const rows = (data ?? []) as unknown as ResponseRow[]
return (
<div className="admin-page" role="main">
<header className="admin-header">
<div>
<h1 className="admin-title">Control de respuestas</h1>
<p className="admin-subtitle">
{totalCount ?? 0} respuestas en total · últimas {rows.length} ·
actualizado {formatDate(new Date().toISOString())}
</p>
</div>
<a href="/admin/responses" className="admin-refresh">Actualizar</a>
</header>
<div className="admin-table-wrapper">
<table className="admin-table">
<thead>
<tr>
<th>#</th>
<th>Fecha</th>
<th>Encuesta</th>
<th>Departamento</th>
<th className="admin-col-optional">Sector</th>
<th className="admin-col-optional">Rango etario</th>
<th>Respuestas</th>
<th className="admin-col-optional">IP hash</th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => {
const survey = Array.isArray(row.surveys) ? row.surveys[0] : row.surveys
const answerCount = row.answers?.[0]?.count ?? 0
return (
<tr key={row.id}>
<td>{i + 1}</td>
<td>{formatDate(row.submitted_at)}</td>
<td>{survey?.title ?? '—'}</td>
<td>{dash(row.qi_zone)}</td>
<td className="admin-col-optional">{dash(row.qi_sector)}</td>
<td className="admin-col-optional">{dash(row.qi_age_bucket)}</td>
<td>{answerCount}</td>
<td className="admin-col-optional admin-iphash">
{row.ip_hash ? `${row.ip_hash.slice(0, 8)}...` : '—'}
</td>
</tr>
)
})}
</tbody>
</table>
{rows.length === 0 && (
<p className="admin-empty">Sin respuestas registradas todavía.</p>
)}
</div>
</div>
)
}