Con el listado de encuestas cargadas/respondidas
This commit is contained in:
126
app/admin/responses/page.tsx
Normal file
126
app/admin/responses/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user