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. export const dynamic = 'force-dynamic' const MAX_SUBMISSIONS = 500 const PROMPT_MAX_LEN = 60 const SURVEY_OPTIONS = [ { label: 'Todos', id: null }, { label: 'InQuality', id: '30000000-0000-0000-0000-000000000002' }, { label: 'Fundación Solidaridad', id: '30000000-0000-0000-0000-000000000003' }, { label: 'Demo', id: '30000000-0000-0000-0000-000000000001' }, ] as const interface AllowedQuestion { id: string code: string prompt: string type: string position: number } interface AnswerRow { value: unknown question_id: string } interface ResponseRow { id: string submitted_at: string qi_zone: string | null qi_sector: string | null answers: AnswerRow[] } 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 truncate(text: string, max = PROMPT_MAX_LEN): string { return text.length > max ? `${text.slice(0, max)}...` : text } // Spec 4.3 — null/undefined → null (renderizado como badge "sin dato") function renderValue(value: unknown, type: string): string | null { if (value === null || value === undefined) return null switch (type) { case 'single_choice': case 'text_short': return String(value).replace(/^"|"$/g, '') case 'multiple_choice': if (Array.isArray(value)) return value.join(', ') return String(value) case 'rating': return String(value) default: return String(value) } } export default async function AdminResponsesPage({ searchParams, }: { searchParams: Promise<{ survey_id?: string }> }) { const { survey_id: surveyId } = await searchParams const supabase = getSupabaseAdmin() // 1. Preguntas permitidas — text_long e is_sensitive=true se excluyen ACÁ, // antes de tocar la tabla answers. Sus valores nunca se consultan. const { data: questionsData, error: questionsError } = await supabase .from('questions') .select('id, code, prompt, type, position') .eq('is_sensitive', false) .neq('type', 'text_long') .order('position', { ascending: true }) if (questionsError) { return (

Control de respuestas

No fue posible conectarse a la base de datos.

{questionsError.message}
) } const allowedQuestions = (questionsData ?? []) as AllowedQuestion[] const allowedIds = allowedQuestions.map((q) => q.id) const questionMap = new Map(allowedQuestions.map((q) => [q.id, q])) // 2. Submissions con sus respuestas — answers!inner + .in() filtra el ARRAY // embebido a solo question_id permitidos (consulta, no post-filtro). let responsesQuery = supabase .from('responses') .select( ` id, submitted_at, qi_zone, qi_sector, answers!inner ( value, question_id ) ` ) .in('answers.question_id', allowedIds) if (surveyId) { responsesQuery = responsesQuery.eq('survey_id', surveyId) } const { data: responsesData, error: responsesError } = allowedIds.length > 0 ? await responsesQuery.order('submitted_at', { ascending: false }).limit(MAX_SUBMISSIONS) : { data: [] as ResponseRow[], error: null } if (responsesError) { return (

Control de respuestas

No fue posible conectarse a la base de datos.

{responsesError.message}
) } const responses = (responsesData ?? []) as unknown as ResponseRow[] // 3. Contadores totales (independientes del límite de 500 submissions) const { count: totalSubmissions } = await supabase .from('responses') .select('*', { count: 'exact', head: true }) const { count: totalAnswers } = allowedIds.length > 0 ? await supabase .from('answers') .select('*', { count: 'exact', head: true }) .in('question_id', allowedIds) : { count: 0 } // 4. Aplanar: una fila por answer, ordenadas por position dentro de cada submission const rows: DisplayRow[] = [] responses.forEach((response, idx) => { const answers = response.answers .filter((a) => questionMap.has(a.question_id)) .sort( (a, b) => questionMap.get(a.question_id)!.position - questionMap.get(b.question_id)!.position ) 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, date: isFirstInGroup ? formatDate(response.submitted_at) : '', code: q.code, prompt: truncate(q.prompt), rendered: renderValue(a.value, q.type), zone: isFirstInGroup ? (response.qi_zone ?? '—') : '', sector: isFirstInGroup ? (response.qi_sector ?? '—') : '', }) }) }) return (

Control de respuestas

{totalSubmissions ?? 0} submissions · {totalAnswers ?? 0} respuestas individuales · actualizado {formatDate(new Date().toISOString())}

Actualizar
{rows.length === 0 && (

Sin respuestas registradas todavía.

)}
) }