Compare commits
20 Commits
b3f039fbd5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf09346f29 | ||
|
|
9461ee1e2e | ||
|
|
3bf92f015c | ||
|
|
67042b0941 | ||
|
|
7efab390da | ||
|
|
4611fb2363 | ||
|
|
d211382e4d | ||
|
|
e29752644a | ||
|
|
704d2407e8 | ||
|
|
5472043a33 | ||
|
|
0fdee073a6 | ||
|
|
6a09ce9ee5 | ||
|
|
af751d771d | ||
|
|
cd87ddb18e | ||
|
|
6d9cf918f9 | ||
|
|
a0db888f2e | ||
|
|
a0253b4f90 | ||
|
|
a660928085 | ||
|
|
30cd83ffc1 | ||
|
|
ca3af1a750 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,6 +14,11 @@ supabase/.temp/
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# Playwright — reportes/snapshots, pueden contener credenciales de .env.test en el DOM
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
|
||||
# Next.js build output
|
||||
.next/
|
||||
|
||||
|
||||
58
TECH_DEBT.md
58
TECH_DEBT.md
@@ -111,3 +111,61 @@
|
||||
- **Trigger:** evaluar si la Mirada País/Humana también necesitan segmentar por
|
||||
modalidad con frecuencia — si varias RPCs repiten el mismo JOIN, conviene
|
||||
promoverla de una vez en lugar de cada RPC reimplementando el JOIN.
|
||||
|
||||
---
|
||||
|
||||
## [TECH-DEBT-011] Sección de pregunta inferida por posición, no modelada como dato
|
||||
|
||||
- **Estado:** ABIERTO — decisión consciente, no un descuido. Surgió en
|
||||
ETAPA_ADMIN (vista detallada `/admin/responses/[id]`, 27 jun 2026): el prompt
|
||||
de etapa asumía columnas `questions.section_number`/`section_title` que NO
|
||||
existen. Verificado contra el esquema real (`supabase/migrations/
|
||||
20260531000001_create_tables.sql` — única definición de `questions`,
|
||||
`20260620000002_form_schema_v3.sql` solo agrega comentarios, ninguna
|
||||
migración agrega esas columnas).
|
||||
- **Descripción:** hoy `questions` no tiene ningún campo que declare "esta
|
||||
pregunta pertenece a la sección X". La sección es una fila más en la misma
|
||||
tabla, con `type='section'`, intercalada por `position` entre las preguntas
|
||||
reales — el código (`lib/form-engine.ts:146-148` en el wizard, y la vista
|
||||
detallada de admin) infiere el agrupamiento recorriendo las filas en orden
|
||||
de `position` y "abriendo" una sección nueva cada vez que encuentra una fila
|
||||
`type='section'`. El título de la sección es el `prompt` de esa fila — no
|
||||
hay un campo `title` separado.
|
||||
- **Por qué es deuda, no solo una curiosidad de modelado:** el motor está
|
||||
pensado para ser un "motor de formularios de encuestas" genérico — no toda
|
||||
encuesta futura va a tener secciones, y las que las tengan no necesariamente
|
||||
van a querer modelarlas como filas-marcador en la misma tabla que las
|
||||
preguntas. Hoy CUALQUIER consumidor que necesite agrupar por sección
|
||||
(wizard, admin, y a futuro cualquier export/reporte) tiene que reimplementar
|
||||
el mismo recorrido secuencial con el mismo supuesto implícito ("la fila
|
||||
`type='section'` más cercana hacia atrás en `position` es la sección de esta
|
||||
pregunta"). Si esa lógica se necesita en un tercer lugar, ya es momento de
|
||||
promoverla a una función compartida — y si el motor crece a encuestas con
|
||||
estructuras más ricas (subsecciones, secciones condicionales, secciones que
|
||||
no son "preguntas" en absoluto), el modelo actual no escala sin reescribir
|
||||
cada consumidor.
|
||||
- **Alternativa más genérica y escalable (no implementada, propuesta para
|
||||
evaluar cuando se priorice):** sacar la sección de `questions.type` y
|
||||
modelarla como entidad propia — opción simple: `questions.section_id uuid
|
||||
NULL REFERENCES sections(id)` + tabla `sections(id, survey_id, title,
|
||||
position)`; opción más liviana sin tabla nueva: `questions.section_title
|
||||
text NULL` + `questions.section_position int NULL` poblados directamente en
|
||||
cada pregunta (denormalizado, sin fila-marcador `type='section'` separada).
|
||||
Cualquiera de las dos deja "pertenece a la sección X" como un hecho
|
||||
consultable con una columna, no como un cálculo derivado de recorrer
|
||||
`position` — y ninguna asume que toda encuesta tiene secciones (el campo es
|
||||
nullable). Migrar implicaría: (1) nueva(s) columna(s)/tabla, (2) backfill
|
||||
desde las filas `type='section'` existentes, (3) actualizar
|
||||
`lib/form-engine.ts` y la vista detallada de admin para leer el dato en vez
|
||||
de inferirlo, (4) decidir si `type='section'` se deprecia o convive.
|
||||
- **Acción tomada mientras tanto:** la vista detallada de admin
|
||||
(`app/admin/responses/[id]/page.tsx`) replica la misma lógica de
|
||||
`lib/form-engine.ts` (recorrido secuencial por `position`, abre sección en
|
||||
cada `type='section'`) — no inventa columnas nuevas, no diverge del modelo
|
||||
actual.
|
||||
- **Trigger:** si una tercera feature necesita el mismo agrupamiento, extraer
|
||||
la lógica a un helper compartido en `lib/` en vez de triplicarla. Si el
|
||||
producto efectivamente evoluciona a soportar surveys con estructuras de
|
||||
sección más complejas que el caso actual, evaluar la migración a columna/
|
||||
tabla explícita antes de seguir apilando consumidores sobre el supuesto
|
||||
implícito.
|
||||
|
||||
106
app/actions/invite-user.ts
Normal file
106
app/actions/invite-user.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
'use server'
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
import { getSupabaseAdmin } from '@/lib/supabase'
|
||||
|
||||
export type InviteUserResult = { success: true } | { success: false; error: string }
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
// Cliente ligado a la sesión (cookies) — anon key, respeta RLS. Se usa solo
|
||||
// para identificar a quien llama y leer SU PROPIA fila de user_roles
|
||||
// (policy user_roles_select_own: user_id = auth.uid()). Nunca para las
|
||||
// escrituras de esta acción — esas requieren service_role (ver getSupabaseAdmin).
|
||||
async function getSessionClient() {
|
||||
const cookieStore = await cookies()
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options)
|
||||
)
|
||||
} catch {
|
||||
// Server Action no siempre puede mutar cookies — se ignora
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Invita a un usuario de empresa y lo asocia a su organización con rol org_admin.
|
||||
// Seguridad: quien llama debe ser platform_admin — se verifica leyendo user_roles
|
||||
// en la DB con la sesión real (auth.uid()), nunca confiando en datos del formData.
|
||||
// El rol del nuevo usuario es siempre 'org_admin', fijo en código — no es un
|
||||
// valor que el formulario pueda controlar para escalar privilegios.
|
||||
export async function inviteUser(formData: FormData): Promise<InviteUserResult> {
|
||||
const sessionClient = await getSessionClient()
|
||||
|
||||
// auth.getUser() valida el token contra el servidor de Auth — no decodifica
|
||||
// la cookie localmente (mismo patrón que middleware.ts).
|
||||
const { data: { user: caller } } = await sessionClient.auth.getUser()
|
||||
if (!caller) return { success: false, error: 'no_autenticado' }
|
||||
|
||||
// Verificación de autorización — lee la fila real de user_roles del llamante.
|
||||
const { data: callerRole, error: roleErr } = await sessionClient
|
||||
.from('user_roles')
|
||||
.select('app_role')
|
||||
.eq('user_id', caller.id)
|
||||
.maybeSingle()
|
||||
|
||||
if (roleErr || callerRole?.app_role !== 'platform_admin') {
|
||||
return { success: false, error: 'forbidden' }
|
||||
}
|
||||
|
||||
const email = String(formData.get('email') ?? '').trim().toLowerCase()
|
||||
const orgId = String(formData.get('orgId') ?? '').trim()
|
||||
|
||||
if (!EMAIL_RE.test(email)) return { success: false, error: 'email_invalido' }
|
||||
if (!orgId) return { success: false, error: 'organizacion_requerida' }
|
||||
|
||||
const admin = getSupabaseAdmin()
|
||||
|
||||
// La organización debe existir — admin client porque ya está autorizado el
|
||||
// llamante (paso anterior) y queremos evitar depender del claim app_role del
|
||||
// JWT (puede estar stale si el rol cambió desde la última emisión de token).
|
||||
const { data: org } = await admin
|
||||
.from('organizations')
|
||||
.select('id')
|
||||
.eq('id', orgId)
|
||||
.maybeSingle()
|
||||
|
||||
if (!org) return { success: false, error: 'organizacion_no_encontrada' }
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? ''
|
||||
const { data: inviteData, error: inviteErr } = await admin.auth.admin.inviteUserByEmail(
|
||||
email,
|
||||
{ redirectTo: `${siteUrl}/auth/confirm` }
|
||||
)
|
||||
|
||||
if (inviteErr || !inviteData?.user) {
|
||||
return { success: false, error: `invite_failed: ${inviteErr?.message ?? 'sin detalle'}` }
|
||||
}
|
||||
|
||||
const newUserId = inviteData.user.id
|
||||
|
||||
// Insert inmediato — antes de que el usuario confirme nada. user_roles no
|
||||
// tiene policy de INSERT (RLS deniega por defecto), por eso requiere admin.
|
||||
const { error: roleInsertErr } = await admin
|
||||
.from('user_roles')
|
||||
.insert({ user_id: newUserId, app_role: 'org_admin', org_id: orgId })
|
||||
|
||||
if (roleInsertErr) {
|
||||
// Rollback — no dejar un usuario en auth.users sin rol asignado.
|
||||
await admin.auth.admin.deleteUser(newUserId)
|
||||
return { success: false, error: `role_insert_failed: ${roleInsertErr.message}` }
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
274
app/admin/responses/[id]/page.tsx
Normal file
274
app/admin/responses/[id]/page.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
import Link from 'next/link'
|
||||
import { formatDate, renderValue } from '../admin-format'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface QuestionRow {
|
||||
id: string
|
||||
code: string
|
||||
prompt: string
|
||||
type: string
|
||||
position: number
|
||||
instance_of: string | null
|
||||
}
|
||||
|
||||
interface AnswerRow {
|
||||
question_id: string
|
||||
value: unknown
|
||||
instance_id: string
|
||||
}
|
||||
|
||||
interface ResponseMeta {
|
||||
id: string
|
||||
submitted_at: string
|
||||
qi_zone: string | null
|
||||
qi_sector: string | null
|
||||
qi_age_bucket: string | null
|
||||
survey_id: string
|
||||
}
|
||||
|
||||
interface DetailRow {
|
||||
code: string
|
||||
prompt: string
|
||||
rendered: string | null
|
||||
}
|
||||
|
||||
interface InstanceGroup {
|
||||
instanceId: string
|
||||
rows: DetailRow[]
|
||||
}
|
||||
|
||||
interface SectionGroup {
|
||||
title: string | null
|
||||
instances: InstanceGroup[]
|
||||
}
|
||||
|
||||
async function getSupabaseSession() {
|
||||
const cookieStore = await cookies()
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options)
|
||||
)
|
||||
} catch {
|
||||
// Server Component — no puede mutar cookies, se ignora
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function backHref(surveyId: string | undefined, page: string | undefined): string {
|
||||
const params = new URLSearchParams()
|
||||
if (surveyId) params.set('survey_id', surveyId)
|
||||
if (page) params.set('page', page)
|
||||
const qs = params.toString()
|
||||
return `/admin/responses${qs ? `?${qs}` : ''}`
|
||||
}
|
||||
|
||||
export default async function ResponseDetailPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
searchParams: Promise<{ survey_id?: string; page?: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const { survey_id: backSurveyId, page: backPage } = await searchParams
|
||||
const back = backHref(backSurveyId, backPage)
|
||||
const supabase = await getSupabaseSession()
|
||||
|
||||
const { data: response, error: responseError } = await supabase
|
||||
.from('responses')
|
||||
.select('id, submitted_at, qi_zone, qi_sector, qi_age_bucket, survey_id')
|
||||
.eq('id', id)
|
||||
.maybeSingle()
|
||||
|
||||
if (responseError || !response) {
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<Link href={back} className="admin-back-link">← Volver a la lista</Link>
|
||||
<h1 className="admin-title">Respuesta no encontrada</h1>
|
||||
<p className="admin-empty">
|
||||
No existe esta respuesta o no tenés permiso para verla.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta = response as ResponseMeta
|
||||
|
||||
// Mismo filtro de sensibles que la lista (regla no negociable #4 CLAUDE.md) — NO diverge.
|
||||
const { data: questionsData, error: questionsError } = await supabase
|
||||
.from('questions')
|
||||
.select('id, code, prompt, type, position, instance_of')
|
||||
.eq('survey_id', meta.survey_id)
|
||||
.eq('is_sensitive', false)
|
||||
.neq('type', 'text_long')
|
||||
.order('position', { ascending: true })
|
||||
|
||||
if (questionsError) {
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<Link href={back} className="admin-back-link">← Volver a la lista</Link>
|
||||
<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">{questionsError.message}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const questions = (questionsData ?? []) as QuestionRow[]
|
||||
const answerQuestionIds = questions.filter((q) => q.type !== 'section').map((q) => q.id)
|
||||
|
||||
const { data: answersData, error: answersError } =
|
||||
answerQuestionIds.length > 0
|
||||
? await supabase
|
||||
.from('answers')
|
||||
.select('question_id, value, instance_id')
|
||||
.eq('response_id', id)
|
||||
.in('question_id', answerQuestionIds)
|
||||
: { data: [] as AnswerRow[], error: null }
|
||||
|
||||
if (answersError) {
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<Link href={back} className="admin-back-link">← Volver a la lista</Link>
|
||||
<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">{answersError.message}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const answers = (answersData ?? []) as AnswerRow[]
|
||||
const answersByQuestion = new Map<string, AnswerRow[]>()
|
||||
answers.forEach((a) => {
|
||||
const list = answersByQuestion.get(a.question_id) ?? []
|
||||
list.push(a)
|
||||
answersByQuestion.set(a.question_id, list)
|
||||
})
|
||||
|
||||
// Agrupar por sección. No existe section_number/section_title en `questions`
|
||||
// (verificado — ver TECH_DEBT.md). Las secciones hoy son filas type='section'
|
||||
// intercaladas por position, mismo modelo que usa el wizard en
|
||||
// lib/form-engine.ts:146-148 — se replica esa lógica acá, no se inventa una
|
||||
// columna nueva.
|
||||
const sections: SectionGroup[] = []
|
||||
let current: SectionGroup = { title: null, instances: [] }
|
||||
let hasContent = false
|
||||
|
||||
function instanceGroupFor(section: SectionGroup, instanceId: string): InstanceGroup {
|
||||
let group = section.instances.find((g) => g.instanceId === instanceId)
|
||||
if (!group) {
|
||||
group = { instanceId, rows: [] }
|
||||
section.instances.push(group)
|
||||
}
|
||||
return group
|
||||
}
|
||||
|
||||
questions.forEach((q) => {
|
||||
if (q.type === 'section') {
|
||||
if (hasContent || current.title !== null) sections.push(current)
|
||||
current = { title: q.prompt, instances: [] }
|
||||
hasContent = false
|
||||
return
|
||||
}
|
||||
|
||||
const qAnswers = answersByQuestion.get(q.id)
|
||||
if (qAnswers && qAnswers.length > 0) {
|
||||
qAnswers.forEach((a) => {
|
||||
const group = instanceGroupFor(current, a.instance_id)
|
||||
group.rows.push({ code: q.code, prompt: q.prompt, rendered: renderValue(a.value, q.type) })
|
||||
})
|
||||
hasContent = true
|
||||
} else if (!q.instance_of) {
|
||||
// Pregunta normal sin respuesta — placeholder "sin respuesta" visible.
|
||||
// Si es instance_of y no hay respuestas, no se generaron instancias
|
||||
// (igual que el wizard no muestra pantallas de instancia sin generador
|
||||
// disparado) — no se fabrica un grupo "Familiar" vacío.
|
||||
const group = instanceGroupFor(current, 'default')
|
||||
group.rows.push({ code: q.code, prompt: q.prompt, rendered: null })
|
||||
hasContent = true
|
||||
}
|
||||
})
|
||||
if (hasContent || current.title !== null) sections.push(current)
|
||||
|
||||
sections.forEach((s) => {
|
||||
s.instances.sort((a, b) => {
|
||||
if (a.instanceId === 'default') return -1
|
||||
if (b.instanceId === 'default') return 1
|
||||
return a.instanceId.localeCompare(b.instanceId, undefined, { numeric: true })
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<Link href={back} className="admin-back-link">← Volver a la lista</Link>
|
||||
|
||||
<header className="admin-header">
|
||||
<div>
|
||||
<h1 className="admin-title">Respuesta {meta.id.slice(0, 8)}</h1>
|
||||
<p className="admin-subtitle">
|
||||
{formatDate(meta.submitted_at)} · Departamento: {meta.qi_zone ?? '—'} · Sector:{' '}
|
||||
{meta.qi_sector ?? '—'} · Edad: {meta.qi_age_bucket ?? '—'}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{sections.map((section, si) => (
|
||||
<section key={si} className="admin-detail-section">
|
||||
{section.title && <h2 className="admin-detail-section-title">{section.title}</h2>}
|
||||
{section.instances.map((group) => (
|
||||
<div key={group.instanceId} className="admin-detail-instance">
|
||||
{group.instanceId !== 'default' && (
|
||||
<span className="admin-instance-badge">
|
||||
Familiar {group.instanceId.replace('familiar_', '')}
|
||||
</span>
|
||||
)}
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Pregunta</th>
|
||||
<th>Valor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{group.rows.map((row, ri) => (
|
||||
<tr key={ri}>
|
||||
<td>{row.code}</td>
|
||||
<td className="admin-prompt">{row.prompt}</td>
|
||||
<td>
|
||||
{row.rendered === null ? (
|
||||
<span className="admin-nodata">sin respuesta</span>
|
||||
) : (
|
||||
row.rendered
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
|
||||
{sections.length === 0 && (
|
||||
<p className="admin-empty">Esta respuesta no tiene preguntas visibles para mostrar.</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
app/admin/responses/admin-format.ts
Normal file
27
app/admin/responses/admin-format.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// Compartido entre la lista (page.tsx) y la vista detallada ([id]/page.tsx) —
|
||||
// deben renderizar fecha y valor exactamente igual; antes vivían duplicados
|
||||
// en los dos archivos y ya habían divergido.
|
||||
|
||||
export 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())}`
|
||||
}
|
||||
|
||||
// null/undefined → null (renderizado como badge "sin dato"/"sin respuesta" por el caller)
|
||||
export 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)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,15 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
import { ResponsesTable, type DisplayRow } from './responses-table'
|
||||
import { SignOutButton } from '../sign-out-button'
|
||||
import { SurveySelect } from './survey-select'
|
||||
import { AdminNavBar } from '@/components/admin-nav-bar'
|
||||
import { formatDate, renderValue } from './admin-format'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const MAX_SUBMISSIONS = 500
|
||||
const PAGE_SIZE = 50
|
||||
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
|
||||
@@ -36,6 +31,11 @@ interface ResponseRow {
|
||||
answers: AnswerRow[]
|
||||
}
|
||||
|
||||
interface SurveyOption {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
async function getSupabaseSession() {
|
||||
const cookieStore = await cookies()
|
||||
return createServerClient(
|
||||
@@ -60,50 +60,48 @@ async function getSupabaseSession() {
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
function pageHref(surveyId: string | undefined, page: number): string {
|
||||
const params = new URLSearchParams()
|
||||
if (surveyId) params.set('survey_id', surveyId)
|
||||
params.set('page', String(page))
|
||||
return `/admin/responses?${params.toString()}`
|
||||
}
|
||||
|
||||
export default async function AdminResponsesPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ survey_id?: string }>
|
||||
searchParams: Promise<{ survey_id?: string; page?: string }>
|
||||
}) {
|
||||
const { survey_id: surveyId } = await searchParams
|
||||
const { survey_id: surveyId, page: pageParam } = await searchParams
|
||||
const page = Math.max(1, Number(pageParam) || 1)
|
||||
const supabase = await getSupabaseSession()
|
||||
|
||||
// 0 y 1 no dependen entre sí — en paralelo.
|
||||
// 0. Surveys para el filtro — dinámico, no hardcodeado. Solo trae lo que
|
||||
// RLS deja ver al usuario autenticado. Si falla, el filtro se degrada a
|
||||
// "sin opciones" en vez de tirar abajo la página entera — la lista de
|
||||
// respuestas no depende de esta query.
|
||||
// 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 })
|
||||
const [
|
||||
{ data: surveysData, error: surveysError },
|
||||
{ data: questionsData, error: questionsError },
|
||||
] = await Promise.all([
|
||||
supabase.from('surveys').select('id, title').order('title'),
|
||||
supabase
|
||||
.from('questions')
|
||||
.select('id, code, prompt, type, position')
|
||||
.eq('is_sensitive', false)
|
||||
.neq('type', 'text_long')
|
||||
.order('position', { ascending: true }),
|
||||
])
|
||||
|
||||
const surveys = (surveysData ?? []) as SurveyOption[]
|
||||
const selectedSurvey = surveys.find((s) => s.id === surveyId)
|
||||
|
||||
if (questionsError) {
|
||||
return (
|
||||
@@ -121,6 +119,7 @@ export default async function AdminResponsesPage({
|
||||
|
||||
// 2. Submissions con sus respuestas — answers!inner + .in() filtra el ARRAY
|
||||
// embebido a solo question_id permitidos (consulta, no post-filtro).
|
||||
// Paginado con .range() — page size 50, no LIMIT fijo sin paginación.
|
||||
let responsesQuery = supabase
|
||||
.from('responses')
|
||||
.select(
|
||||
@@ -138,10 +137,44 @@ export default async function AdminResponsesPage({
|
||||
responsesQuery = responsesQuery.eq('survey_id', surveyId)
|
||||
}
|
||||
|
||||
const { data: responsesData, error: responsesError } =
|
||||
// 3. Contadores totales (independientes de la página actual). submissionsCount
|
||||
// usa el MISMO answers!inner + .in() que responsesQuery — si contara todas
|
||||
// las responses sin ese filtro, una response sin ningún answer permitido
|
||||
// inflaría el total y totalPages quedaría desalineado con lo que .range()
|
||||
// realmente puede devolver (página "fantasma" al final).
|
||||
let submissionsCountQuery = supabase
|
||||
.from('responses')
|
||||
.select('*, answers!inner(question_id)', { count: 'exact', head: true })
|
||||
.in('answers.question_id', allowedIds)
|
||||
|
||||
if (surveyId) {
|
||||
submissionsCountQuery = submissionsCountQuery.eq('survey_id', surveyId)
|
||||
}
|
||||
|
||||
let answersCountQuery = supabase
|
||||
.from('answers')
|
||||
.select('*, responses!inner(survey_id)', { count: 'exact', head: true })
|
||||
.in('question_id', allowedIds)
|
||||
|
||||
if (surveyId) {
|
||||
answersCountQuery = answersCountQuery.eq('responses.survey_id', surveyId)
|
||||
}
|
||||
|
||||
// Las tres son independientes entre sí — en paralelo.
|
||||
const [
|
||||
{ data: responsesData, error: responsesError },
|
||||
{ count: totalSubmissions },
|
||||
{ count: totalAnswers },
|
||||
] =
|
||||
allowedIds.length > 0
|
||||
? await responsesQuery.order('submitted_at', { ascending: false }).limit(MAX_SUBMISSIONS)
|
||||
: { data: [] as ResponseRow[], error: null }
|
||||
? await Promise.all([
|
||||
responsesQuery
|
||||
.order('submitted_at', { ascending: false })
|
||||
.range((page - 1) * PAGE_SIZE, page * PAGE_SIZE - 1),
|
||||
submissionsCountQuery,
|
||||
answersCountQuery,
|
||||
])
|
||||
: [{ data: [] as ResponseRow[], error: null }, { count: 0 }, { count: 0 }]
|
||||
|
||||
if (responsesError) {
|
||||
return (
|
||||
@@ -154,30 +187,7 @@ export default async function AdminResponsesPage({
|
||||
}
|
||||
|
||||
const responses = (responsesData ?? []) as unknown as ResponseRow[]
|
||||
|
||||
// 3. Contadores totales (independientes del límite de 500 submissions),
|
||||
// filtrados por survey_id cuando hay filtro activo.
|
||||
let submissionsCountQuery = supabase
|
||||
.from('responses')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
|
||||
if (surveyId) {
|
||||
submissionsCountQuery = submissionsCountQuery.eq('survey_id', surveyId)
|
||||
}
|
||||
|
||||
const { count: totalSubmissions } = await submissionsCountQuery
|
||||
|
||||
let answersCountQuery = supabase
|
||||
.from('answers')
|
||||
.select('*, responses!inner(survey_id)', { count: 'exact', head: true })
|
||||
.in('question_id', allowedIds)
|
||||
|
||||
if (surveyId) {
|
||||
answersCountQuery = answersCountQuery.eq('responses.survey_id', surveyId)
|
||||
}
|
||||
|
||||
const { count: totalAnswers } =
|
||||
allowedIds.length > 0 ? await answersCountQuery : { count: 0 }
|
||||
const totalPages = Math.max(1, Math.ceil((totalSubmissions ?? 0) / PAGE_SIZE))
|
||||
|
||||
// 4. Aplanar: una fila por answer, ordenadas por position dentro de cada submission
|
||||
const rows: DisplayRow[] = []
|
||||
@@ -194,7 +204,8 @@ export default async function AdminResponsesPage({
|
||||
const isFirstInGroup = ai === 0
|
||||
rows.push({
|
||||
key: `${response.id}-${a.question_id}`,
|
||||
submissionNum: idx + 1,
|
||||
responseId: response.id,
|
||||
submissionNum: (page - 1) * PAGE_SIZE + idx + 1,
|
||||
isFirstInGroup,
|
||||
date: isFirstInGroup ? formatDate(response.submitted_at) : '',
|
||||
code: q.code,
|
||||
@@ -207,43 +218,57 @@ export default async function AdminResponsesPage({
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<>
|
||||
<AdminNavBar active="responses" />
|
||||
<div className="admin-page" role="main">
|
||||
<header className="admin-header">
|
||||
<div>
|
||||
<h1 className="admin-title">Control de respuestas</h1>
|
||||
<p className="admin-subtitle">
|
||||
{totalSubmissions ?? 0} submissions · {totalAnswers ?? 0} respuestas individuales ·
|
||||
actualizado {formatDate(new Date().toISOString())}
|
||||
{totalSubmissions ?? 0} submissions · {totalAnswers ?? 0} respuestas individuales ·{' '}
|
||||
{selectedSurvey ? selectedSurvey.title : 'Todos los surveys'} · actualizado{' '}
|
||||
{formatDate(new Date().toISOString())}
|
||||
</p>
|
||||
<nav className="admin-survey-nav" aria-label="Selección de empresa">
|
||||
{SURVEY_OPTIONS.map((opt) => (
|
||||
<a
|
||||
key={opt.label}
|
||||
href={opt.id ? `/admin/responses?survey_id=${opt.id}` : '/admin/responses'}
|
||||
className={
|
||||
opt.id === (surveyId ?? null)
|
||||
? 'admin-survey-link admin-survey-link-active'
|
||||
: 'admin-survey-link'
|
||||
}
|
||||
aria-current={opt.id === (surveyId ?? null) ? 'page' : undefined}
|
||||
>
|
||||
{opt.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<p className="admin-subtitle">
|
||||
Página {page} de {totalPages} · {totalSubmissions ?? 0} submissions totales
|
||||
</p>
|
||||
<div className="admin-survey-nav">
|
||||
<SurveySelect surveys={surveys} value={surveyId ?? ''} />
|
||||
</div>
|
||||
{surveysError && (
|
||||
<p className="admin-error" role="alert">
|
||||
No se pudo cargar la lista de surveys para el filtro — se muestra "Todos".
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="admin-header-actions">
|
||||
<a href="/admin/responses" className="admin-refresh">Actualizar</a>
|
||||
<SignOutButton />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="admin-table-wrapper">
|
||||
<ResponsesTable rows={rows} />
|
||||
<ResponsesTable rows={rows} surveyId={surveyId} page={page} />
|
||||
{rows.length === 0 && (
|
||||
<p className="admin-empty">Sin respuestas registradas todavía.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(totalSubmissions ?? 0) > PAGE_SIZE && (
|
||||
<nav className="admin-pagination" aria-label="Paginación de respuestas">
|
||||
{page > 1 ? (
|
||||
<a href={pageHref(surveyId, page - 1)} className="admin-pagination-link">← Anterior</a>
|
||||
) : (
|
||||
<span className="admin-pagination-link admin-pagination-disabled" aria-disabled="true">← Anterior</span>
|
||||
)}
|
||||
<span className="admin-pagination-current">Página {page} de {totalPages}</span>
|
||||
{page < totalPages ? (
|
||||
<a href={pageHref(surveyId, page + 1)} className="admin-pagination-link">Siguiente →</a>
|
||||
) : (
|
||||
<span className="admin-pagination-link admin-pagination-disabled" aria-disabled="true">Siguiente →</span>
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export interface DisplayRow {
|
||||
key: string
|
||||
responseId: string
|
||||
submissionNum: number
|
||||
isFirstInGroup: boolean
|
||||
date: string
|
||||
@@ -14,7 +16,15 @@ export interface DisplayRow {
|
||||
sector: string
|
||||
}
|
||||
|
||||
export function ResponsesTable({ rows }: { rows: DisplayRow[] }) {
|
||||
export function ResponsesTable({
|
||||
rows,
|
||||
surveyId,
|
||||
page,
|
||||
}: {
|
||||
rows: DisplayRow[]
|
||||
surveyId?: string
|
||||
page: number
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState<Set<number>>(new Set())
|
||||
|
||||
function toggle(submissionNum: number) {
|
||||
@@ -26,6 +36,13 @@ export function ResponsesTable({ rows }: { rows: DisplayRow[] }) {
|
||||
})
|
||||
}
|
||||
|
||||
function detailHref(responseId: string): string {
|
||||
const params = new URLSearchParams()
|
||||
if (surveyId) params.set('survey_id', surveyId)
|
||||
params.set('page', String(page))
|
||||
return `/admin/responses/${responseId}?${params.toString()}`
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
@@ -37,6 +54,7 @@ export function ResponsesTable({ rows }: { rows: DisplayRow[] }) {
|
||||
<th>Valor</th>
|
||||
<th className="admin-col-optional">Departamento</th>
|
||||
<th className="admin-col-optional">Sector</th>
|
||||
<th>Ver</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -54,28 +72,32 @@ export function ResponsesTable({ rows }: { rows: DisplayRow[] }) {
|
||||
<tr
|
||||
key={row.key}
|
||||
className={trClass}
|
||||
// Conveniencia para mouse — sin semántica ARIA en la fila (evita
|
||||
// anidar un control interactivo real, el Link "Ver", dentro de un
|
||||
// elemento expuesto como role="button" a lectores de pantalla).
|
||||
// El toggle accesible por teclado vive en el <button> de abajo.
|
||||
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.isFirstInGroup ? (
|
||||
<button
|
||||
type="button"
|
||||
className="admin-row-toggle-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggle(row.submissionNum)
|
||||
}}
|
||||
aria-expanded={isExpanded}
|
||||
aria-label={isExpanded ? 'Colapsar respuesta' : 'Expandir respuesta'}
|
||||
>
|
||||
<span className="admin-toggle-icon" aria-hidden="true">
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
{row.submissionNum}
|
||||
</button>
|
||||
) : (
|
||||
row.submissionNum
|
||||
)}
|
||||
{row.submissionNum}
|
||||
</td>
|
||||
<td>{row.date}</td>
|
||||
<td>{row.code}</td>
|
||||
@@ -89,6 +111,13 @@ export function ResponsesTable({ rows }: { rows: DisplayRow[] }) {
|
||||
</td>
|
||||
<td className="admin-col-optional">{row.zone}</td>
|
||||
<td className="admin-col-optional">{row.sector}</td>
|
||||
<td onClick={(e) => e.stopPropagation()}>
|
||||
{row.isFirstInGroup && (
|
||||
<Link href={detailHref(row.responseId)} className="admin-action-link">
|
||||
Ver
|
||||
</Link>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
|
||||
31
app/admin/responses/survey-select.tsx
Normal file
31
app/admin/responses/survey-select.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
interface SurveyOption {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export function SurveySelect({ surveys, value }: { surveys: SurveyOption[]; value: string }) {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<select
|
||||
className="admin-survey-select"
|
||||
aria-label="Selección de encuesta"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const id = e.target.value
|
||||
router.push(id ? `/admin/responses?survey_id=${id}` : '/admin/responses')
|
||||
}}
|
||||
>
|
||||
<option value="">Todos los surveys</option>
|
||||
{surveys.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
89
app/admin/usuarios/invite-form.tsx
Normal file
89
app/admin/usuarios/invite-form.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
import { useActionState, useEffect, useState } from 'react'
|
||||
import { inviteUser, type InviteUserResult } from '@/app/actions/invite-user'
|
||||
|
||||
interface OrgOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const INITIAL_STATE: InviteUserResult = { success: false, error: '' }
|
||||
const BANNER_VISIBLE_MS = 4000
|
||||
|
||||
const ERROR_MESSAGES: Record<string, string> = {
|
||||
no_autenticado: 'Tu sesión expiró — volvé a iniciar sesión.',
|
||||
forbidden: 'No tenés permisos para invitar usuarios.',
|
||||
email_invalido: 'El email no es válido.',
|
||||
organizacion_requerida: 'Elegí una organización.',
|
||||
organizacion_no_encontrada: 'La organización seleccionada no existe.',
|
||||
}
|
||||
|
||||
function describeError(code: string): string {
|
||||
if (code.startsWith('invite_failed'))
|
||||
return `No se pudo enviar la invitación: ${code.slice('invite_failed: '.length)}`
|
||||
if (code.startsWith('role_insert_failed'))
|
||||
return `Error interno al asignar el rol: ${code.slice('role_insert_failed: '.length)}`
|
||||
return ERROR_MESSAGES[code] ?? 'Ocurrió un error inesperado.'
|
||||
}
|
||||
|
||||
export function InviteForm({ organizations }: { organizations: OrgOption[] }) {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
async (_prev: InviteUserResult, formData: FormData) => inviteUser(formData),
|
||||
INITIAL_STATE
|
||||
)
|
||||
const [bannerVisible, setBannerVisible] = useState(false)
|
||||
|
||||
// Banner visible al menos BANNER_VISIBLE_MS tras cada resultado nuevo, o
|
||||
// hasta que el usuario vuelva a tocar el formulario (lo que pase primero).
|
||||
useEffect(() => {
|
||||
const hasMessage = state.success || (!state.success && state.error !== '')
|
||||
setBannerVisible(hasMessage)
|
||||
if (!hasMessage) return
|
||||
const timer = setTimeout(() => setBannerVisible(false), BANNER_VISIBLE_MS)
|
||||
return () => clearTimeout(timer)
|
||||
}, [state])
|
||||
|
||||
return (
|
||||
<form
|
||||
action={formAction}
|
||||
className="invite-form"
|
||||
onChange={() => setBannerVisible(false)}
|
||||
>
|
||||
<div className="invite-form-row">
|
||||
<div className="login-field">
|
||||
<label htmlFor="email" className="login-label">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
disabled={isPending}
|
||||
className="text-short-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="login-field">
|
||||
<label htmlFor="orgId" className="login-label">Organización</label>
|
||||
<select id="orgId" name="orgId" required disabled={isPending} className="admin-survey-select">
|
||||
<option value="">Seleccioná una organización</option>
|
||||
{organizations.map((org) => (
|
||||
<option key={org.id} value={org.id}>{org.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" disabled={isPending} className="login-submit">
|
||||
{isPending ? 'Enviando…' : 'Enviar invitación'}
|
||||
</button>
|
||||
</div>
|
||||
{bannerVisible && !state.success && state.error && (
|
||||
<p role="alert" className="invite-banner invite-banner-error">
|
||||
{describeError(state.error)}
|
||||
</p>
|
||||
)}
|
||||
{bannerVisible && state.success && (
|
||||
<p role="status" className="invite-banner invite-banner-success">
|
||||
Invitación enviada correctamente.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
195
app/admin/usuarios/page.tsx
Normal file
195
app/admin/usuarios/page.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
import { getSupabaseAdmin } from '@/lib/supabase'
|
||||
import { formatDate } from '../responses/admin-format'
|
||||
import { AdminNavBar } from '@/components/admin-nav-bar'
|
||||
import { InviteForm } from './invite-form'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface OrgOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface RoleRow {
|
||||
user_id: string
|
||||
app_role: string
|
||||
org_id: string | null
|
||||
organizations: { name: string } | null
|
||||
}
|
||||
|
||||
interface UserDisplayRow {
|
||||
userId: string
|
||||
email: string
|
||||
orgName: string
|
||||
role: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
async function getSupabaseSession() {
|
||||
const cookieStore = await cookies()
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options)
|
||||
)
|
||||
} catch {
|
||||
// Server Component — no puede mutar cookies, se ignora
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// auth.users no está expuesto vía PostgREST (no es schema público) — no hay
|
||||
// forma de hacer un .from('auth.users') ni un join SQL directo desde
|
||||
// supabase-js. listUsers() es la API admin soportada para resolver el email;
|
||||
// pagina hasta agotar resultados (tope defensivo de 2000 usuarios).
|
||||
async function fetchAllUsersMap(admin: ReturnType<typeof getSupabaseAdmin>) {
|
||||
const map = new Map<string, { email: string; createdAt: string }>()
|
||||
const perPage = 200
|
||||
for (let page = 1; page <= 10; page++) {
|
||||
const { data, error } = await admin.auth.admin.listUsers({ page, perPage })
|
||||
if (error || !data) break
|
||||
data.users.forEach((u) => {
|
||||
map.set(u.id, { email: u.email ?? '—', createdAt: u.created_at })
|
||||
})
|
||||
if (data.users.length < perPage) break
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export default async function AdminUsuariosPage() {
|
||||
const supabase = await getSupabaseSession()
|
||||
|
||||
// 1. Identificar a quien llama — defensivo, el middleware ya debería haber
|
||||
// redirigido a /login si no hay sesión.
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<h1 className="admin-title">Acceso restringido</h1>
|
||||
<p className="admin-error">No se encontró una sesión activa.</p>
|
||||
<a href="/login" className="admin-back-link">← Iniciar sesión</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Verificar platform_admin leyendo la fila REAL en user_roles — esta
|
||||
// página usa getSupabaseAdmin() más abajo (bypasa RLS por completo), así
|
||||
// que el gate de autorización tiene que ir acá, no delegarse a las policies.
|
||||
const { data: callerRole } = await supabase
|
||||
.from('user_roles')
|
||||
.select('app_role')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle()
|
||||
|
||||
if (callerRole?.app_role !== 'platform_admin') {
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<h1 className="admin-title">Acceso restringido</h1>
|
||||
<p className="admin-error">
|
||||
Esta sección es exclusiva para administradores de plataforma.
|
||||
</p>
|
||||
<a href="/admin/responses" className="admin-back-link">← Volver</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Datos — admin client para todo a partir de acá. La autorización ya
|
||||
// quedó probada en el paso 2 con una lectura directa a la DB; usar admin
|
||||
// para el resto evita depender de policies/claims del JWT que podrían
|
||||
// haber quedado stale.
|
||||
const admin = getSupabaseAdmin()
|
||||
|
||||
const [{ data: orgsData, error: orgsError }, { data: rolesData, error: rolesError }] =
|
||||
await Promise.all([
|
||||
admin.from('organizations').select('id, name').order('name'),
|
||||
admin.from('user_roles').select('user_id, app_role, org_id, organizations(name)'),
|
||||
])
|
||||
|
||||
if (orgsError || rolesError) {
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<h1 className="admin-title">Usuarios</h1>
|
||||
<p className="admin-error">No fue posible conectarse a la base de datos.</p>
|
||||
<pre className="admin-error-detail">
|
||||
{(orgsError ?? rolesError)?.message}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const organizations = (orgsData ?? []) as OrgOption[]
|
||||
const roles = (rolesData ?? []) as unknown as RoleRow[]
|
||||
const usersMap = await fetchAllUsersMap(admin)
|
||||
|
||||
const userRows: UserDisplayRow[] = roles.map((r) => {
|
||||
const u = usersMap.get(r.user_id)
|
||||
return {
|
||||
userId: r.user_id,
|
||||
email: u?.email ?? '—',
|
||||
orgName: r.organizations?.name ?? '—',
|
||||
role: r.app_role,
|
||||
createdAt: u?.createdAt ? formatDate(u.createdAt) : '—',
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminNavBar active="usuarios" />
|
||||
<div className="admin-page" role="main">
|
||||
<header className="admin-header">
|
||||
<div>
|
||||
<h1 className="admin-title">Usuarios</h1>
|
||||
<p className="admin-subtitle">{userRows.length} usuarios registrados</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="admin-detail-section">
|
||||
<h2 className="admin-detail-section-title">Invitar usuario</h2>
|
||||
<InviteForm organizations={organizations} />
|
||||
</section>
|
||||
|
||||
<section className="admin-detail-section">
|
||||
<h2 className="admin-detail-section-title">Usuarios existentes</h2>
|
||||
<div className="admin-table-wrapper">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Organización</th>
|
||||
<th>Rol</th>
|
||||
<th>Fecha de creación</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userRows.map((row) => (
|
||||
<tr key={row.userId}>
|
||||
<td>{row.email}</td>
|
||||
<td>{row.orgName}</td>
|
||||
<td>{row.role}</td>
|
||||
<td>{row.createdAt}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{userRows.length === 0 && (
|
||||
<p className="admin-empty">Todavía no hay usuarios invitados.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
191
app/auth/confirm/page.tsx
Normal file
191
app/auth/confirm/page.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
type Stage = 'validating' | 'ready' | 'submitting' | 'error'
|
||||
|
||||
const DEFAULT_LINK_ERROR = 'Pedile al administrador que te envíe una nueva invitación.'
|
||||
|
||||
// GoTrue redirige con #error=...&error_code=...&error_description=... cuando
|
||||
// el token ya fue usado, venció, o es inválido — en vez de emitir PASSWORD_RECOVERY.
|
||||
// Mapeo a mensajes accionables; default cubre códigos no contemplados acá.
|
||||
function describeLinkError(errorCode: string | null): string {
|
||||
switch (errorCode) {
|
||||
case 'otp_expired':
|
||||
return 'Este link ya fue usado o venció. ' + DEFAULT_LINK_ERROR
|
||||
case 'access_denied':
|
||||
return 'El acceso fue denegado para este link. ' + DEFAULT_LINK_ERROR
|
||||
default:
|
||||
return 'El link de invitación no es válido. ' + DEFAULT_LINK_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
export default function ConfirmPage() {
|
||||
const [stage, setStage] = useState<Stage>('validating')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [linkErrorMessage, setLinkErrorMessage] = useState(DEFAULT_LINK_ERROR)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const supabase = createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// GoTrue ya nos dice en el fragmento si el token es inválido/vencido/usado
|
||||
// (#error=...&error_code=...) — leerlo evita esperar el timeout genérico.
|
||||
const hash = window.location.hash.startsWith('#')
|
||||
? window.location.hash.slice(1)
|
||||
: window.location.hash
|
||||
const hashParams = new URLSearchParams(hash)
|
||||
const errorCode = hashParams.get('error_code')
|
||||
|
||||
if (errorCode) {
|
||||
setLinkErrorMessage(describeLinkError(errorCode))
|
||||
setStage('error')
|
||||
return
|
||||
}
|
||||
|
||||
// Supabase emite PASSWORD_RECOVERY tanto para recovery como para invite
|
||||
// cuando el token de la URL (#access_token) es válido.
|
||||
const { data: listener } = supabase.auth.onAuthStateChange((event) => {
|
||||
if (event === 'PASSWORD_RECOVERY') setStage('ready')
|
||||
})
|
||||
|
||||
// Carrera posible: si el token ya se procesó antes de montar el listener,
|
||||
// ya hay sesión activa aunque el evento no se vuelva a emitir.
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setStage((s) => (s === 'validating' && session ? 'ready' : s))
|
||||
})
|
||||
|
||||
// Sin evento ni sesión tras unos segundos = token inválido/vencido/ausente
|
||||
// sin que GoTrue haya mandado un error explícito en el fragmento.
|
||||
const timeout = setTimeout(() => {
|
||||
setStage((s) => (s === 'validating' ? 'error' : s))
|
||||
}, 8000)
|
||||
|
||||
return () => {
|
||||
listener.subscription.unsubscribe()
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [supabase])
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
const fd = new FormData(e.currentTarget)
|
||||
const password = String(fd.get('password') ?? '')
|
||||
const confirmPassword = String(fd.get('confirmPassword') ?? '')
|
||||
|
||||
if (password.length < 8) {
|
||||
setError('La contraseña debe tener al menos 8 caracteres.')
|
||||
return
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
setError('Las contraseñas no coinciden.')
|
||||
return
|
||||
}
|
||||
|
||||
setError(null)
|
||||
setStage('submitting')
|
||||
|
||||
const { error: updateErr } = await supabase.auth.updateUser({ password })
|
||||
|
||||
if (updateErr) {
|
||||
setError('No pudimos guardar tu contraseña. Probá de nuevo.')
|
||||
setStage('ready')
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/dashboard')
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
if (stage === 'validating') {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-card">
|
||||
<h1 className="login-title">Validando tu invitación...</h1>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (stage === 'error') {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-card">
|
||||
<h1 className="login-title">Link inválido o vencido</h1>
|
||||
<p role="alert" className="field-error login-error">
|
||||
{linkErrorMessage}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-card">
|
||||
<h1 className="login-title">Creá tu contraseña</h1>
|
||||
<form onSubmit={handleSubmit} className="login-form" noValidate>
|
||||
<div className="login-field">
|
||||
<label htmlFor="password" className="login-label">Contraseña</label>
|
||||
<div className="login-password-wrapper">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
minLength={8}
|
||||
disabled={stage === 'submitting'}
|
||||
className="text-short-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="login-password-toggle"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
aria-label={showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="login-field">
|
||||
<label htmlFor="confirmPassword" className="login-label">Confirmar contraseña</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
minLength={8}
|
||||
disabled={stage === 'submitting'}
|
||||
className="text-short-input"
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p role="alert" className="field-error login-error">{error}</p>
|
||||
)}
|
||||
<button type="submit" disabled={stage === 'submitting'} className="login-submit">
|
||||
{stage === 'submitting' ? 'Guardando…' : 'Crear contraseña'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { AdminNavBar } from '@/components/admin-nav-bar'
|
||||
import { FilterSidebar } from '@/components/dashboard/filter-sidebar'
|
||||
import { TalentoRiesgoCard } from '@/components/dashboard/talento-riesgo-card'
|
||||
import { PrevalenciaCuidadoresCard } from '@/components/dashboard/prevalencia-cuidadores-card'
|
||||
@@ -46,7 +47,9 @@ export default function DashboardPage() {
|
||||
const [lens, setLens] = useState<Lens>('empresa')
|
||||
|
||||
return (
|
||||
<div className="db-page">
|
||||
<>
|
||||
<AdminNavBar active="dashboard" />
|
||||
<div className="db-page">
|
||||
<FilterSidebar
|
||||
filters={filters}
|
||||
onChange={setFilters}
|
||||
@@ -144,6 +147,7 @@ export default function DashboardPage() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
122
app/globals.css
122
app/globals.css
@@ -295,6 +295,7 @@ body {
|
||||
/* position: fixed + top/left/width set by JS via getBoundingClientRect()
|
||||
so the list escapes any overflow:auto ancestor (wizard-answer-area) */
|
||||
position: fixed;
|
||||
max-width: calc(100vw - 32px);
|
||||
background: var(--color-surface);
|
||||
border: 1.5px solid var(--color-border-focus);
|
||||
border-radius: var(--radius);
|
||||
@@ -316,7 +317,7 @@ body {
|
||||
.barrio-autocomplete-item:hover,
|
||||
.barrio-autocomplete-item--active {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
color: #494F4F;
|
||||
}
|
||||
|
||||
/* ── Textarea ── */
|
||||
@@ -377,7 +378,7 @@ body {
|
||||
.btn-primary:hover { background: var(--color-primary-hover); }
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
color: #494F4F;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.btn-secondary:hover { background: var(--color-primary-light); }
|
||||
@@ -428,7 +429,7 @@ body {
|
||||
}
|
||||
.summary-code {
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
color: #494F4F;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.summary-answer {
|
||||
@@ -487,7 +488,7 @@ body {
|
||||
.consent-checkbox-label:has(input:checked) { border-color: var(--color-primary); background: var(--color-primary-light); }
|
||||
.consent-checkbox-input { width: 20px; height: 20px; flex-shrink: 0; margin-top: 1px; accent-color: var(--color-primary); cursor: pointer; }
|
||||
.consent-checkbox-text { flex: 1; }
|
||||
.consent-required-badge { display: inline-block; font-size: 0.75rem; font-weight: 600; color: var(--color-primary); background: var(--color-primary-light); padding: 1px 6px; border-radius: 4px; }
|
||||
.consent-required-badge { display: inline-block; font-size: 0.75rem; font-weight: 600; color: #494F4F; background: var(--color-primary-light); padding: 1px 6px; border-radius: 4px; }
|
||||
.consent-optional-badge { display: inline-block; font-size: 0.75rem; font-weight: 600; color: var(--color-text-muted); background: #f3f4f6; padding: 1px 6px; border-radius: 4px; }
|
||||
.consent-footer { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }
|
||||
.consent-required-hint { margin: 0; font-size: 0.875rem; color: var(--color-text-muted); }
|
||||
@@ -566,7 +567,7 @@ body {
|
||||
.wizard-section-number {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
color: #494F4F;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.wizard-section-title {
|
||||
@@ -583,9 +584,8 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Enunciado: bloque con altura reservada constante, mismo lugar pantalla tras pantalla. */
|
||||
/* Enunciado: crece orgánicamente con el contenido, sin altura mínima fija. */
|
||||
.wizard-prompt-block {
|
||||
min-height: 116px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -659,6 +659,7 @@ body {
|
||||
padding: 16px 0;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -673,6 +674,11 @@ body {
|
||||
.wizard-prompt-text { font-size: 1.1875rem; }
|
||||
.wizard-section-title { font-size: 1.5rem; }
|
||||
.wizard-answer-area { max-height: 400px; }
|
||||
/* column-reverse: el botón primario (Siguiente/Enviar) queda arriba —
|
||||
el pulgar llega más fácil al centro de la pantalla que a la base. */
|
||||
.wizard-bottombar { flex-direction: column-reverse; }
|
||||
.wizard-bottombar .btn { width: 100%; }
|
||||
.wizard-bottombar-hint { order: 3; }
|
||||
}
|
||||
|
||||
/* ── Loading skeleton ── */
|
||||
@@ -965,7 +971,7 @@ body {
|
||||
.db-widget-star {
|
||||
border-color: var(--color-re-teal);
|
||||
}
|
||||
.db-widget-star .db-widget-title { color: var(--color-re-teal); }
|
||||
.db-widget-star .db-widget-title { color: #494F4F; }
|
||||
|
||||
/* ── Big stat (KPI de una sola cifra, dentro de WidgetShell) ── */
|
||||
.db-big-stat { font-size: 2.375rem; font-weight: 700; color: var(--color-re-teal); line-height: 1; }
|
||||
@@ -992,6 +998,21 @@ body {
|
||||
.db-placeholder-title { font-size: 1rem; font-weight: 600; color: #494F4F; }
|
||||
.db-placeholder-hint { font-size: 0.8125rem; max-width: 420px; line-height: 1.5; }
|
||||
|
||||
/* ── Nav compartido (dashboard / respuestas / usuarios) ── */
|
||||
.admin-nav-bar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 16px; padding: 10px 24px; background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border); flex-wrap: wrap;
|
||||
}
|
||||
.admin-nav { display: flex; gap: 4px; }
|
||||
.admin-nav-link {
|
||||
padding: 6px 14px; border-radius: var(--radius); font-size: 0.875rem;
|
||||
font-weight: 600; color: var(--color-text-muted); text-decoration: none;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.admin-nav-link:hover { background: var(--color-primary-light); color: var(--color-text); }
|
||||
.admin-nav-link-active { background: var(--color-primary-light); color: var(--color-primary-hover); }
|
||||
|
||||
/* ── Admin: control de respuestas (Etapa 3V) ── */
|
||||
.admin-page { max-width: 1100px; margin: 0 auto; padding: 24px; }
|
||||
.admin-header {
|
||||
@@ -1001,19 +1022,46 @@ body {
|
||||
.admin-title { font-size: 1.375rem; font-weight: 700; color: var(--color-text); margin: 0 0 4px; }
|
||||
.admin-subtitle { font-size: 0.8125rem; color: var(--color-text-muted); margin: 0; }
|
||||
.admin-survey-nav { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
||||
.admin-survey-link {
|
||||
font-size: 0.75rem; font-weight: 600; padding: 4px 12px; border-radius: 999px;
|
||||
border: 1px solid var(--color-border); color: var(--color-text-muted); text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.admin-survey-link:hover { color: var(--color-text); border-color: var(--color-primary); }
|
||||
.admin-survey-link-active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
|
||||
.admin-refresh {
|
||||
display: inline-block; padding: 8px 16px; border-radius: var(--radius);
|
||||
background: var(--color-primary); color: #fff; font-size: 0.8125rem;
|
||||
font-weight: 600; text-decoration: none; white-space: nowrap;
|
||||
}
|
||||
.admin-refresh:hover { background: var(--color-primary-hover); }
|
||||
.admin-survey-select {
|
||||
font-size: 0.8125rem; padding: 6px 10px; border-radius: var(--radius);
|
||||
border: 1px solid var(--color-border); color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
.admin-action-link {
|
||||
font-size: 0.75rem; font-weight: 600; color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.admin-action-link:hover { text-decoration: underline; }
|
||||
.admin-pagination {
|
||||
display: flex; align-items: center; justify-content: center; gap: 16px;
|
||||
margin-top: 16px; font-size: 0.8125rem;
|
||||
}
|
||||
.admin-pagination-link { color: var(--color-primary); text-decoration: none; font-weight: 600; }
|
||||
.admin-pagination-link:hover { text-decoration: underline; }
|
||||
.admin-pagination-disabled { color: var(--color-text-muted); cursor: default; }
|
||||
.admin-pagination-current { color: var(--color-text-muted); }
|
||||
.admin-back-link {
|
||||
display: inline-block; margin-bottom: 12px; font-size: 0.8125rem;
|
||||
font-weight: 600; color: var(--color-primary); text-decoration: none;
|
||||
}
|
||||
.admin-back-link:hover { text-decoration: underline; }
|
||||
.admin-detail-section { margin-bottom: 24px; }
|
||||
.admin-detail-section-title {
|
||||
font-size: 1.0625rem; font-weight: 700; color: var(--color-text);
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.admin-detail-instance { margin-bottom: 14px; }
|
||||
.admin-instance-badge {
|
||||
display: inline-block; font-size: 0.75rem; font-weight: 600;
|
||||
color: var(--color-primary-hover); background: var(--color-primary-light);
|
||||
padding: 3px 10px; border-radius: 999px; margin-bottom: 6px;
|
||||
}
|
||||
.admin-table-wrapper {
|
||||
overflow-x: auto; background: var(--color-surface); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius); box-shadow: var(--shadow-card);
|
||||
@@ -1034,6 +1082,10 @@ body {
|
||||
display: inline-block; width: 14px; margin-right: 4px;
|
||||
color: var(--color-text-muted); font-size: 0.7rem;
|
||||
}
|
||||
.admin-row-toggle-btn {
|
||||
background: none; border: none; padding: 0; margin: 0;
|
||||
font: inherit; color: inherit; cursor: pointer;
|
||||
}
|
||||
.admin-nodata {
|
||||
display: inline-block; padding: 1px 8px; border-radius: 10px;
|
||||
background: var(--color-re-light); color: var(--color-re-dark);
|
||||
@@ -1126,6 +1178,46 @@ body {
|
||||
}
|
||||
.login-submit:hover:not(:disabled) { background: var(--color-primary-hover); }
|
||||
.login-submit:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.login-back-button { margin-top: 16px; }
|
||||
.login-forgot-link {
|
||||
align-self: center;
|
||||
margin-top: 4px;
|
||||
padding: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.login-forgot-link:hover { color: var(--color-primary-hover); }
|
||||
|
||||
/* ── Invite form (/admin/usuarios) ── */
|
||||
.invite-form { display: flex; flex-direction: column; gap: 12px; }
|
||||
.invite-form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
.invite-banner {
|
||||
margin: 0;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.invite-banner-success {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
.invite-banner-error {
|
||||
background: var(--color-error-bg);
|
||||
color: var(--color-error);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.invite-form-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ── Admin sign-out ── */
|
||||
.admin-header-actions { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
@@ -4,10 +4,14 @@ import { useState } from 'react'
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
type Mode = 'login' | 'reset' | 'reset-sent'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [mode, setMode] = useState<Mode>('login')
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [resetEmail, setResetEmail] = useState('')
|
||||
const router = useRouter()
|
||||
|
||||
const supabase = createBrowserClient(
|
||||
@@ -36,6 +40,89 @@ export default function LoginPage() {
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
// Reusa /auth/confirm: ya escucha PASSWORD_RECOVERY (lo armamos para el
|
||||
// flujo de invitación, pero Supabase emite el mismo evento acá).
|
||||
async function handleReset(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
setIsPending(true)
|
||||
setError(null)
|
||||
|
||||
const fd = new FormData(e.currentTarget)
|
||||
const email = String(fd.get('email') ?? '')
|
||||
|
||||
const { error: resetError } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/confirm`,
|
||||
})
|
||||
|
||||
setIsPending(false)
|
||||
|
||||
if (resetError) {
|
||||
setError('No pudimos enviar el link. Probá de nuevo.')
|
||||
return
|
||||
}
|
||||
|
||||
setResetEmail(email)
|
||||
setMode('reset-sent')
|
||||
}
|
||||
|
||||
if (mode === 'reset-sent') {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-card">
|
||||
<h1 className="login-title">Revisá tu email</h1>
|
||||
<p className="admin-subtitle">
|
||||
Si <strong>{resetEmail}</strong> tiene una cuenta, te enviamos un link para crear una
|
||||
contraseña nueva.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="login-submit login-back-button"
|
||||
onClick={() => { setMode('login'); setError(null) }}
|
||||
>
|
||||
Volver al login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (mode === 'reset') {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-card">
|
||||
<h1 className="login-title">Recuperar contraseña</h1>
|
||||
<form onSubmit={handleReset} className="login-form" noValidate>
|
||||
<div className="login-field">
|
||||
<label htmlFor="reset-email" className="login-label">Email</label>
|
||||
<input
|
||||
id="reset-email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
disabled={isPending}
|
||||
className="text-short-input"
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p role="alert" className="field-error login-error">{error}</p>
|
||||
)}
|
||||
<button type="submit" disabled={isPending} className="login-submit">
|
||||
{isPending ? 'Enviando…' : 'Enviar link de recuperación'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="login-forgot-link"
|
||||
onClick={() => { setMode('login'); setError(null) }}
|
||||
>
|
||||
← Volver al login
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-card">
|
||||
@@ -96,6 +183,13 @@ export default function LoginPage() {
|
||||
>
|
||||
{isPending ? 'Ingresando…' : 'Ingresar'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="login-forgot-link"
|
||||
onClick={() => { setMode('reset'); setError(null) }}
|
||||
>
|
||||
¿Olvidaste tu contraseña?
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
34
components/admin-nav-bar.tsx
Normal file
34
components/admin-nav-bar.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { SignOutButton } from '@/app/admin/sign-out-button'
|
||||
|
||||
interface AdminNavBarProps {
|
||||
active: 'dashboard' | 'responses' | 'usuarios'
|
||||
}
|
||||
|
||||
const LINKS = [
|
||||
{ id: 'dashboard', href: '/dashboard', label: 'Dashboard' },
|
||||
{ id: 'responses', href: '/admin/responses', label: 'Respuestas' },
|
||||
{ id: 'usuarios', href: '/admin/usuarios', label: 'Usuarios' },
|
||||
] as const
|
||||
|
||||
// Nav compartido entre /dashboard, /admin/responses y /admin/usuarios — las
|
||||
// tres pantallas detrás del login. Sin esto, cada una era una isla y el único
|
||||
// modo de llegar a otra era escribir la URL a mano.
|
||||
export function AdminNavBar({ active }: AdminNavBarProps) {
|
||||
return (
|
||||
<div className="admin-nav-bar">
|
||||
<nav className="admin-nav" aria-label="Navegación admin">
|
||||
{LINKS.map((link) => (
|
||||
<a
|
||||
key={link.id}
|
||||
href={link.href}
|
||||
className={`admin-nav-link${active === link.id ? ' admin-nav-link-active' : ''}`}
|
||||
aria-current={active === link.id ? 'page' : undefined}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<SignOutButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react'
|
||||
|
||||
type GeoData = Record<string, Record<string, string[]>>
|
||||
import { getGeoData, type GeoData } from '@/lib/geo-cache'
|
||||
|
||||
interface BarrioAutocompleteFieldProps {
|
||||
name: string
|
||||
@@ -25,16 +24,16 @@ export function BarrioAutocompleteField({
|
||||
error,
|
||||
}: BarrioAutocompleteFieldProps) {
|
||||
const [geoData, setGeoData] = useState<GeoData | null>(null)
|
||||
const [geoStatus, setGeoStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [activeIdx, setActiveIdx] = useState(-1)
|
||||
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({})
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/geo_paraguay_full.json')
|
||||
.then((r) => r.json())
|
||||
.then(setGeoData)
|
||||
.catch(() => {})
|
||||
getGeoData()
|
||||
.then((data) => { setGeoData(data); setGeoStatus('ready') })
|
||||
.catch(() => setGeoStatus('error'))
|
||||
}, [])
|
||||
|
||||
// Close dropdown on scroll so the fixed-position list doesn't drift from the input
|
||||
@@ -126,6 +125,12 @@ export function BarrioAutocompleteField({
|
||||
autoComplete="off"
|
||||
className="text-short-input"
|
||||
ref={inputRef}
|
||||
disabled={geoStatus === 'loading'}
|
||||
placeholder={
|
||||
geoStatus === 'loading' ? 'Cargando opciones...' :
|
||||
geoStatus === 'error' ? 'No se pudieron cargar las opciones. Podés escribir el nombre manualmente.' :
|
||||
undefined
|
||||
}
|
||||
onChange={(e) => handleInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }}
|
||||
@@ -147,6 +152,7 @@ export function BarrioAutocompleteField({
|
||||
aria-selected={i === activeIdx}
|
||||
className={`barrio-autocomplete-item${i === activeIdx ? ' barrio-autocomplete-item--active' : ''}`}
|
||||
onMouseDown={() => select(barrio)}
|
||||
onTouchEnd={(e) => { e.preventDefault(); select(barrio) }}
|
||||
>
|
||||
{barrio}
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react'
|
||||
|
||||
type GeoData = Record<string, Record<string, string[]>>
|
||||
import { getGeoData, type GeoData } from '@/lib/geo-cache'
|
||||
|
||||
interface CiudadSelectFieldProps {
|
||||
name: string
|
||||
@@ -23,6 +22,7 @@ export function CiudadSelectField({
|
||||
error,
|
||||
}: CiudadSelectFieldProps) {
|
||||
const [geoData, setGeoData] = useState<GeoData | null>(null)
|
||||
const [geoStatus, setGeoStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [activeIdx, setActiveIdx] = useState(-1)
|
||||
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({})
|
||||
@@ -35,10 +35,9 @@ export function CiudadSelectField({
|
||||
const prevDeptoRef = useRef(departamento)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/geo_paraguay_full.json')
|
||||
.then((r) => r.json())
|
||||
.then(setGeoData)
|
||||
.catch(() => {})
|
||||
getGeoData()
|
||||
.then((data) => { setGeoData(data); setGeoStatus('ready') })
|
||||
.catch(() => setGeoStatus('error'))
|
||||
}, [])
|
||||
|
||||
// Clear city when departamento changes so no orphan value remains
|
||||
@@ -130,6 +129,12 @@ export function CiudadSelectField({
|
||||
autoComplete="off"
|
||||
className="text-short-input"
|
||||
ref={inputRef}
|
||||
disabled={geoStatus === 'loading'}
|
||||
placeholder={
|
||||
geoStatus === 'loading' ? 'Cargando opciones...' :
|
||||
geoStatus === 'error' ? 'No se pudieron cargar las opciones. Podés escribir el nombre manualmente.' :
|
||||
undefined
|
||||
}
|
||||
onChange={(e) => handleInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }}
|
||||
@@ -151,6 +156,7 @@ export function CiudadSelectField({
|
||||
aria-selected={i === activeIdx}
|
||||
className={`barrio-autocomplete-item${i === activeIdx ? ' barrio-autocomplete-item--active' : ''}`}
|
||||
onMouseDown={() => select(ciudad)}
|
||||
onTouchEnd={(e) => { e.preventDefault(); select(ciudad) }}
|
||||
>
|
||||
{ciudad}
|
||||
</li>
|
||||
|
||||
@@ -70,7 +70,8 @@ export function WizardForm({ survey, schema }: WizardFormProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) return
|
||||
saveDraft(survey.id, state.answers)
|
||||
const timer = setTimeout(() => saveDraft(survey.id, state.answers), 500)
|
||||
return () => clearTimeout(timer)
|
||||
}, [state.answers, hydrated, survey.id])
|
||||
|
||||
const screens = resolveScreens(schema, state.answers)
|
||||
@@ -192,10 +193,12 @@ export function WizardForm({ survey, schema }: WizardFormProps) {
|
||||
setErrors((e) => ({ ...e, [screen.question!.code]: 'Este campo es obligatorio.' }))
|
||||
return
|
||||
}
|
||||
saveDraft(survey.id, state.answers)
|
||||
setState((s) => next(s, schema))
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
saveDraft(survey.id, state.answers)
|
||||
setState((s) => back(s, schema))
|
||||
}
|
||||
|
||||
@@ -210,17 +213,24 @@ export function WizardForm({ survey, schema }: WizardFormProps) {
|
||||
})
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await submitSurvey({
|
||||
surveyId: survey.id,
|
||||
answers,
|
||||
consentOperativo,
|
||||
consentMacroN1,
|
||||
})
|
||||
if (result.success) {
|
||||
clearDraft(survey.id)
|
||||
setStage('success')
|
||||
} else {
|
||||
setSubmitError(result.error)
|
||||
try {
|
||||
const result = await submitSurvey({
|
||||
surveyId: survey.id,
|
||||
answers,
|
||||
consentOperativo,
|
||||
consentMacroN1,
|
||||
})
|
||||
if (result.success) {
|
||||
clearDraft(survey.id)
|
||||
setStage('success')
|
||||
} else {
|
||||
setSubmitError(result.error)
|
||||
setStage('error')
|
||||
}
|
||||
} catch {
|
||||
// Falla de red al invocar el Server Action (fetch failed) — no es un
|
||||
// error que submitSurvey pueda devolver como result, hay que capturarlo acá.
|
||||
setSubmitError('No pudimos enviar tu respuesta. Verificá tu conexión e intentá de nuevo.')
|
||||
setStage('error')
|
||||
}
|
||||
})
|
||||
|
||||
971
docs/dashboard/seed_prueba_50.sql
Normal file
971
docs/dashboard/seed_prueba_50.sql
Normal file
@@ -0,0 +1,971 @@
|
||||
-- SEED DE PRUEBA — 50 respuestas + modalidad (2.7). Determinista seed=42.
|
||||
-- E1=72.0% E2=38.9% E3=36.1% E4=41.7% · estrella umbral2=30.6% umbral1=88.9%
|
||||
--
|
||||
-- Resuelve los question_id por code DENTRO de un bloque DO $$ — nunca
|
||||
-- hardcodeados. Antes este archivo tenía los UUIDs literales y se
|
||||
-- desactualizaba cada vez que la DB se reinicializaba (gen_random_uuid()
|
||||
-- en supabase/seed.sql genera UUIDs nuevos en cada corrida). Con esto,
|
||||
-- el seed funciona contra cualquier instancia sin importar cuándo se
|
||||
-- recreó, siempre que el catálogo de preguntas (supabase/seed.sql) ya
|
||||
-- esté aplicado.
|
||||
--
|
||||
-- Idempotente: limpia SOLO las filas que este script genera (ip_hash con el
|
||||
-- prefijo 'seed-prueba-'), no un TRUNCATE de las tablas completas — answers/
|
||||
-- responses/consent_records pueden tener datos reales de otras encuestas
|
||||
-- (InQuality, Fundación Solidaridad) que este script no debe tocar.
|
||||
|
||||
-- Único bloque DO $$ — sin statements sueltos antes/después que psql
|
||||
-- pueda seguir ejecutando como filas independientes si algo de acá adentro
|
||||
-- falla. Si un PERFORM revienta, todo el bloque aborta y se ve UN error,
|
||||
-- no cincuenta.
|
||||
DO $$
|
||||
DECLARE
|
||||
s_id uuid := '30000000-0000-0000-0000-000000000001';
|
||||
q_2_7 uuid;
|
||||
q_4_1 uuid;
|
||||
q_6_1 uuid;
|
||||
q_6_2 uuid;
|
||||
q_6_4 uuid;
|
||||
q_7_2 uuid;
|
||||
q_7_3 uuid;
|
||||
q_7_5 uuid;
|
||||
q_7_6 uuid;
|
||||
q_9_1 uuid;
|
||||
q_9_2 uuid;
|
||||
q_9_3 uuid;
|
||||
q_9_4 uuid;
|
||||
BEGIN
|
||||
-- Resolver UUIDs de questions por code, en el momento de ejecutarse.
|
||||
SELECT id INTO q_2_7 FROM public.questions
|
||||
WHERE code = '2.7' AND survey_id = s_id;
|
||||
SELECT id INTO q_4_1 FROM public.questions
|
||||
WHERE code = '4.1' AND survey_id = s_id;
|
||||
SELECT id INTO q_6_1 FROM public.questions
|
||||
WHERE code = '6.1' AND survey_id = s_id;
|
||||
SELECT id INTO q_6_2 FROM public.questions
|
||||
WHERE code = '6.2' AND survey_id = s_id;
|
||||
SELECT id INTO q_6_4 FROM public.questions
|
||||
WHERE code = '6.4' AND survey_id = s_id;
|
||||
SELECT id INTO q_7_2 FROM public.questions
|
||||
WHERE code = '7.2' AND survey_id = s_id;
|
||||
SELECT id INTO q_7_3 FROM public.questions
|
||||
WHERE code = '7.3' AND survey_id = s_id;
|
||||
SELECT id INTO q_7_5 FROM public.questions
|
||||
WHERE code = '7.5' AND survey_id = s_id;
|
||||
SELECT id INTO q_7_6 FROM public.questions
|
||||
WHERE code = '7.6' AND survey_id = s_id;
|
||||
SELECT id INTO q_9_1 FROM public.questions
|
||||
WHERE code = '9.1' AND survey_id = s_id;
|
||||
SELECT id INTO q_9_2 FROM public.questions
|
||||
WHERE code = '9.2' AND survey_id = s_id;
|
||||
SELECT id INTO q_9_3 FROM public.questions
|
||||
WHERE code = '9.3' AND survey_id = s_id;
|
||||
SELECT id INTO q_9_4 FROM public.questions
|
||||
WHERE code = '9.4' AND survey_id = s_id;
|
||||
|
||||
-- Si alguna no se resolvió, frenar acá con un mensaje claro en vez de
|
||||
-- dejar que rpc_submit_response falle más abajo con invalid_answer_reference.
|
||||
IF q_2_7 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '2.7';
|
||||
END IF;
|
||||
IF q_4_1 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '4.1';
|
||||
END IF;
|
||||
IF q_6_1 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '6.1';
|
||||
END IF;
|
||||
IF q_6_2 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '6.2';
|
||||
END IF;
|
||||
IF q_6_4 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '6.4';
|
||||
END IF;
|
||||
IF q_7_2 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '7.2';
|
||||
END IF;
|
||||
IF q_7_3 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '7.3';
|
||||
END IF;
|
||||
IF q_7_5 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '7.5';
|
||||
END IF;
|
||||
IF q_7_6 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '7.6';
|
||||
END IF;
|
||||
IF q_9_1 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '9.1';
|
||||
END IF;
|
||||
IF q_9_2 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '9.2';
|
||||
END IF;
|
||||
IF q_9_3 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '9.3';
|
||||
END IF;
|
||||
IF q_9_4 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '9.4';
|
||||
END IF;
|
||||
|
||||
DELETE FROM public.responses
|
||||
WHERE survey_id = s_id
|
||||
AND ip_hash LIKE 'seed-prueba-%';
|
||||
-- answers cae por ON DELETE CASCADE (responses.id). consent_records de
|
||||
-- estas filas queda huérfano a propósito — es evidencia de consentimiento
|
||||
-- (ADR-003), no se borra aunque la response que lo usaba se borre.
|
||||
|
||||
UPDATE surveys SET status='live' WHERE id = s_id;
|
||||
|
||||
-- seed-prueba-000
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-000', '55 años o más', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-001
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-001', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo adulto con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Reducir tu jornada o pasar a medio tiempo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-002
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-002', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud', 'Apoyo económico o convenios/descuentos', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-003
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-003', '25 a 34 años', 'Gerencial', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-004
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-004', '55 años o más', 'Jefatura', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar mayor con discapacidad', 'Hijo adulto con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-005
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-005', '55 años o más', 'Auxiliar', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Reducir tu jornada o pasar a medio tiempo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios', 'Acceso a beneficios para terapias o salud']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-006
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-006', '25 a 34 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar', 'Hijo menor sin discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios', 'Redes de apoyo / grupos de pares', 'Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-007
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-007', '35 a 44 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-008
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-008', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad', 'Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Contención/apoyo emocional o psicológico', 'Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-009
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-009', 'Menos de 25 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Flexibilidad horaria o trabajo remoto', 'Información sobre recursos y servicios', 'Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-010
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-010', '25 a 34 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Declinar viajes, horas extra o capacitaciones']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios', 'Capacitación y autocuidado', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-011
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-011', '35 a 44 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Dejar un trabajo anterior']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-012
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-012', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor sin discapacidad', 'Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares', 'Acceso a beneficios para terapias o salud', 'Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-013
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-013', '25 a 34 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-014
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-014', '35 a 44 años', 'Gerencial', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado', 'Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-015
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-015', '35 a 44 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Contención/apoyo emocional o psicológico', 'Capacitación y autocuidado', 'Flexibilidad horaria o trabajo remoto']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-016
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-016', '25 a 34 años', 'Directivo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-017
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-017', '55 años o más', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Declinar viajes, horas extra o capacitaciones']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-018
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-018', '45 a 54 años', 'Auxiliar', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios', 'Flexibilidad horaria o trabajo remoto']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-019
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-019', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor sin discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Contención/apoyo emocional o psicológico', 'Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-020
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-020', '55 años o más', 'Jefatura', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor sin discapacidad', 'Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Declinar viajes, horas extra o capacitaciones']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud', 'Capacitación y autocuidado', 'Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-021
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-021', '45 a 54 años', 'Jefatura', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado', 'Redes de apoyo / grupos de pares', 'Acceso a beneficios para terapias o salud']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-022
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-022', '45 a 54 años', 'Jefatura', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-023
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-023', '25 a 34 años', 'Operativo', 'Itapúa',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad', 'Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-024
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-024', '25 a 34 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado', 'Redes de apoyo / grupos de pares']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-025
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-025', '25 a 34 años', 'Auxiliar', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud', 'Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-026
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-026', '25 a 34 años', 'Jefatura', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor sin discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Rechazar un ascenso o nueva responsabilidad']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Flexibilidad horaria o trabajo remoto', 'Capacitación y autocuidado', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-027
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-027', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Pensar seriamente en dejar el trabajo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-028
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-028', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad', 'Familiar menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud', 'Flexibilidad horaria o trabajo remoto']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-029
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-029', '55 años o más', 'Auxiliar', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Reducir tu jornada o pasar a medio tiempo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Flexibilidad horaria o trabajo remoto', 'Capacitación y autocuidado', 'Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-030
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-030', '35 a 44 años', 'Operativo', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-031
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-031', '35 a 44 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo adulto con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Dejar un trabajo anterior']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-032
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-032', '25 a 34 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-033
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-033', 'Menos de 25 años', 'Auxiliar', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad', 'Hijo adulto con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Declinar viajes, horas extra o capacitaciones']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-034
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-034', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad', 'Hijo menor sin discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Pensar seriamente en dejar el trabajo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Contención/apoyo emocional o psicológico', 'Redes de apoyo / grupos de pares']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-035
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-035', '45 a 54 años', 'Jefatura', 'Itapúa',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares', 'Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-036
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-036', '25 a 34 años', 'Jefatura', 'Itapúa',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-037
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-037', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-038
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-038', '35 a 44 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-039
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-039', '35 a 44 años', 'Auxiliar', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-040
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-040', '45 a 54 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-041
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-041', '35 a 44 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-042
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-042', '25 a 34 años', 'Jefatura', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-043
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-043', 'Menos de 25 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-044
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-044', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-045
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-045', '25 a 34 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-046
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-046', '25 a 34 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-047
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-047', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-048
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-048', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-049
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-049', '35 a 44 años', 'Auxiliar', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
|
||||
-- Survey queda en 'live' (ya lo estaba desde el UPDATE de arriba; este
|
||||
-- segundo UPDATE es explícito para que quede claro al releer el script,
|
||||
-- no depende de que nada de arriba lo haya dejado sin tocar).
|
||||
UPDATE surveys SET status='live' WHERE id = s_id;
|
||||
END $$;
|
||||
|
||||
-- Para limpiar manualmente este lote de prueba sin tocar otras encuestas:
|
||||
-- DELETE FROM public.responses WHERE survey_id = '30000000-0000-0000-0000-000000000001' AND ip_hash LIKE 'seed-prueba-%';
|
||||
975
docs/dashboard/seed_prueba_fundacion_solidaridad.sql
Normal file
975
docs/dashboard/seed_prueba_fundacion_solidaridad.sql
Normal file
@@ -0,0 +1,975 @@
|
||||
-- SEED DE PRUEBA — Fundación Solidaridad — 50 respuestas + modalidad (2.7).
|
||||
-- Clon de docs/dashboard/seed_prueba_50.sql (mismo patrón, mismas distribuciones
|
||||
-- determinísticas seed=42) apuntado a survey_id de Fundación Solidaridad
|
||||
-- ('30000000-0000-0000-0000-000000000003') en vez de Empresa Demo.
|
||||
-- DATOS SINTÉTICOS — no son respuestas reales de cuidadores de la Fundación.
|
||||
-- E1=72.0% E2=38.9% E3=36.1% E4=41.7% · estrella umbral2=30.6% umbral1=88.9%
|
||||
--
|
||||
-- Resuelve los question_id por code DENTRO de un bloque DO $$ — nunca
|
||||
-- hardcodeados. Antes este archivo tenía los UUIDs literales y se
|
||||
-- desactualizaba cada vez que la DB se reinicializaba (gen_random_uuid()
|
||||
-- en supabase/seed.sql genera UUIDs nuevos en cada corrida). Con esto,
|
||||
-- el seed funciona contra cualquier instancia sin importar cuándo se
|
||||
-- recreó, siempre que el catálogo de preguntas (supabase/seed.sql) ya
|
||||
-- esté aplicado.
|
||||
--
|
||||
-- Idempotente: limpia SOLO las filas que este script genera (ip_hash con el
|
||||
-- prefijo 'seed-prueba-fs-'), no un TRUNCATE de las tablas completas — answers/
|
||||
-- responses/consent_records pueden tener datos reales de otras encuestas
|
||||
-- (Empresa Demo, InQuality) que este script no debe tocar.
|
||||
|
||||
-- Único bloque DO $$ — sin statements sueltos antes/después que psql
|
||||
-- pueda seguir ejecutando como filas independientes si algo de acá adentro
|
||||
-- falla. Si un PERFORM revienta, todo el bloque aborta y se ve UN error,
|
||||
-- no cincuenta.
|
||||
DO $$
|
||||
DECLARE
|
||||
s_id uuid := '30000000-0000-0000-0000-000000000003';
|
||||
q_2_7 uuid;
|
||||
q_4_1 uuid;
|
||||
q_6_1 uuid;
|
||||
q_6_2 uuid;
|
||||
q_6_4 uuid;
|
||||
q_7_2 uuid;
|
||||
q_7_3 uuid;
|
||||
q_7_5 uuid;
|
||||
q_7_6 uuid;
|
||||
q_9_1 uuid;
|
||||
q_9_2 uuid;
|
||||
q_9_3 uuid;
|
||||
q_9_4 uuid;
|
||||
BEGIN
|
||||
-- Resolver UUIDs de questions por code, en el momento de ejecutarse.
|
||||
SELECT id INTO q_2_7 FROM public.questions
|
||||
WHERE code = '2.7' AND survey_id = s_id;
|
||||
SELECT id INTO q_4_1 FROM public.questions
|
||||
WHERE code = '4.1' AND survey_id = s_id;
|
||||
SELECT id INTO q_6_1 FROM public.questions
|
||||
WHERE code = '6.1' AND survey_id = s_id;
|
||||
SELECT id INTO q_6_2 FROM public.questions
|
||||
WHERE code = '6.2' AND survey_id = s_id;
|
||||
SELECT id INTO q_6_4 FROM public.questions
|
||||
WHERE code = '6.4' AND survey_id = s_id;
|
||||
SELECT id INTO q_7_2 FROM public.questions
|
||||
WHERE code = '7.2' AND survey_id = s_id;
|
||||
SELECT id INTO q_7_3 FROM public.questions
|
||||
WHERE code = '7.3' AND survey_id = s_id;
|
||||
SELECT id INTO q_7_5 FROM public.questions
|
||||
WHERE code = '7.5' AND survey_id = s_id;
|
||||
SELECT id INTO q_7_6 FROM public.questions
|
||||
WHERE code = '7.6' AND survey_id = s_id;
|
||||
SELECT id INTO q_9_1 FROM public.questions
|
||||
WHERE code = '9.1' AND survey_id = s_id;
|
||||
SELECT id INTO q_9_2 FROM public.questions
|
||||
WHERE code = '9.2' AND survey_id = s_id;
|
||||
SELECT id INTO q_9_3 FROM public.questions
|
||||
WHERE code = '9.3' AND survey_id = s_id;
|
||||
SELECT id INTO q_9_4 FROM public.questions
|
||||
WHERE code = '9.4' AND survey_id = s_id;
|
||||
|
||||
-- Si alguna no se resolvió, frenar acá con un mensaje claro en vez de
|
||||
-- dejar que rpc_submit_response falle más abajo con invalid_answer_reference.
|
||||
IF q_2_7 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '2.7';
|
||||
END IF;
|
||||
IF q_4_1 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '4.1';
|
||||
END IF;
|
||||
IF q_6_1 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '6.1';
|
||||
END IF;
|
||||
IF q_6_2 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '6.2';
|
||||
END IF;
|
||||
IF q_6_4 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '6.4';
|
||||
END IF;
|
||||
IF q_7_2 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '7.2';
|
||||
END IF;
|
||||
IF q_7_3 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '7.3';
|
||||
END IF;
|
||||
IF q_7_5 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '7.5';
|
||||
END IF;
|
||||
IF q_7_6 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '7.6';
|
||||
END IF;
|
||||
IF q_9_1 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '9.1';
|
||||
END IF;
|
||||
IF q_9_2 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '9.2';
|
||||
END IF;
|
||||
IF q_9_3 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '9.3';
|
||||
END IF;
|
||||
IF q_9_4 IS NULL THEN
|
||||
RAISE EXCEPTION 'UUID no encontrado para code %', '9.4';
|
||||
END IF;
|
||||
|
||||
DELETE FROM public.responses
|
||||
WHERE survey_id = s_id
|
||||
AND ip_hash LIKE 'seed-prueba-fs-%';
|
||||
-- answers cae por ON DELETE CASCADE (responses.id). consent_records de
|
||||
-- estas filas queda huérfano a propósito — es evidencia de consentimiento
|
||||
-- (ADR-003), no se borra aunque la response que lo usaba se borre.
|
||||
|
||||
UPDATE surveys SET status='live' WHERE id = s_id;
|
||||
|
||||
-- seed-prueba-fs-000
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-000', '55 años o más', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-001
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-001', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo adulto con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Reducir tu jornada o pasar a medio tiempo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-002
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-002', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud', 'Apoyo económico o convenios/descuentos', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-003
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-003', '25 a 34 años', 'Gerencial', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-004
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-004', '55 años o más', 'Jefatura', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar mayor con discapacidad', 'Hijo adulto con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-005
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-005', '55 años o más', 'Auxiliar', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Reducir tu jornada o pasar a medio tiempo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios', 'Acceso a beneficios para terapias o salud']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-006
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-006', '25 a 34 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar', 'Hijo menor sin discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios', 'Redes de apoyo / grupos de pares', 'Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-007
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-007', '35 a 44 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-008
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-008', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad', 'Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Contención/apoyo emocional o psicológico', 'Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-009
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-009', 'Menos de 25 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Flexibilidad horaria o trabajo remoto', 'Información sobre recursos y servicios', 'Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-010
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-010', '25 a 34 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Declinar viajes, horas extra o capacitaciones']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios', 'Capacitación y autocuidado', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-011
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-011', '35 a 44 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Dejar un trabajo anterior']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-012
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-012', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor sin discapacidad', 'Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares', 'Acceso a beneficios para terapias o salud', 'Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-013
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-013', '25 a 34 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-014
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-014', '35 a 44 años', 'Gerencial', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado', 'Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-015
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-015', '35 a 44 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Contención/apoyo emocional o psicológico', 'Capacitación y autocuidado', 'Flexibilidad horaria o trabajo remoto']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-016
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-016', '25 a 34 años', 'Directivo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-017
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-017', '55 años o más', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Declinar viajes, horas extra o capacitaciones']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-018
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-018', '45 a 54 años', 'Auxiliar', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios', 'Flexibilidad horaria o trabajo remoto']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-019
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-019', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor sin discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Contención/apoyo emocional o psicológico', 'Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-020
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-020', '55 años o más', 'Jefatura', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor sin discapacidad', 'Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Nunca o casi nunca'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Declinar viajes, horas extra o capacitaciones']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud', 'Capacitación y autocuidado', 'Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-021
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-021', '45 a 54 años', 'Jefatura', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado', 'Redes de apoyo / grupos de pares', 'Acceso a beneficios para terapias o salud']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-022
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-022', '45 a 54 años', 'Jefatura', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-023
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-023', '25 a 34 años', 'Operativo', 'Itapúa',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad', 'Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Información sobre recursos y servicios']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-024
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-024', '25 a 34 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado', 'Redes de apoyo / grupos de pares']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-025
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-025', '25 a 34 años', 'Auxiliar', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'De forma permanente / sin horario'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud', 'Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-026
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-026', '25 a 34 años', 'Jefatura', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor sin discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Rechazar un ascenso o nueva responsabilidad']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Flexibilidad horaria o trabajo remoto', 'Capacitación y autocuidado', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-027
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-027', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Sí, definitivamente'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Pensar seriamente en dejar el trabajo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-028
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-028', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad', 'Familiar menor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Algo probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Parcialmente, hay cierta comprensión'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Acceso a beneficios para terapias o salud', 'Flexibilidad horaria o trabajo remoto']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-029
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-029', '55 años o más', 'Auxiliar', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Reducir tu jornada o pasar a medio tiempo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Flexibilidad horaria o trabajo remoto', 'Capacitación y autocuidado', 'Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-030
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-030', '35 a 44 años', 'Operativo', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Otro / Especificar']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Faltar o llegar tarde de forma reiterada']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-031
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-031', '35 a 44 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo adulto con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Tal vez, si hay más personas en la misma situación'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Dejar un trabajo anterior']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-032
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-032', '25 a 34 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar mayor con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', 'Todos los días'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', 'Más de 6 horas'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Pocas veces (1–3 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Muy probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Sí, me siento acompañado/a'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos', 'Contención/apoyo emocional o psicológico']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-033
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-033', 'Menos de 25 años', 'Auxiliar', 'Alto Paraná',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Hijo menor con discapacidad', 'Hijo adulto con discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Casi todos los días'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Varias veces (4–10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'Nunca hemos tocado el tema'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'Prefiero mantenerlo en privado'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Declinar viajes, horas extra o capacitaciones']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Apoyo económico o convenios/descuentos']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-034
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-034', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Familiar menor con discapacidad', 'Hijo menor sin discapacidad']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Algunas veces al mes'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Nunca'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Poco probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'No'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Pensar seriamente en dejar el trabajo']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Contención/apoyo emocional o psicológico', 'Redes de apoyo / grupos de pares']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-035
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-035', '45 a 54 años', 'Jefatura', 'Itapúa',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Padre/madre con movilidad reducida o 100% dependiente']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial'),
|
||||
jsonb_build_object('question_id', q_6_1, 'instance_id', 'default', 'value', '3–4'),
|
||||
jsonb_build_object('question_id', q_6_2, 'instance_id', 'default', 'value', '5–6'),
|
||||
jsonb_build_object('question_id', q_6_4, 'instance_id', 'default', 'value', '1–2'),
|
||||
jsonb_build_object('question_id', q_7_2, 'instance_id', 'default', 'value', 'Varias veces por semana'),
|
||||
jsonb_build_object('question_id', q_7_3, 'instance_id', 'default', 'value', 'Con frecuencia (más de 10 al año)'),
|
||||
jsonb_build_object('question_id', q_7_6, 'instance_id', 'default', 'value', 'Nada probable'),
|
||||
jsonb_build_object('question_id', q_9_1, 'instance_id', 'default', 'value', 'No, es algo bastante invisible'),
|
||||
jsonb_build_object('question_id', q_9_3, 'instance_id', 'default', 'value', 'No por ahora'),
|
||||
jsonb_build_object('question_id', q_9_4, 'instance_id', 'default', 'value', 'Sí'),
|
||||
jsonb_build_object('question_id', q_7_5, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores']::text[]),
|
||||
jsonb_build_object('question_id', q_9_2, 'instance_id', 'default', 'value', ARRAY['Redes de apoyo / grupos de pares', 'Capacitación y autocuidado']::text[])
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-036
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-036', '25 a 34 años', 'Jefatura', 'Itapúa',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-037
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-037', '35 a 44 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-038
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-038', '35 a 44 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-039
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-039', '35 a 44 años', 'Auxiliar', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-040
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-040', '45 a 54 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Híbrido')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-041
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-041', '35 a 44 años', 'Operativo', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-042
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-042', '25 a 34 años', 'Jefatura', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-043
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-043', 'Menos de 25 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-044
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-044', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-045
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-045', '25 a 34 años', 'Auxiliar', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-046
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-046', '25 a 34 años', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-047
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-047', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Presencial')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-048
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-048', '55 años o más', 'Operativo', 'Central',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
-- seed-prueba-fs-049
|
||||
PERFORM public.rpc_submit_response(
|
||||
s_id, 'seed-prueba-fs-049', '35 a 44 años', 'Auxiliar', 'Asunción (Capital)',
|
||||
true, false,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('question_id', q_4_1, 'instance_id', 'default', 'value', ARRAY['Ninguna de las anteriores / No tengo vinculación de cuidado']::text[]),
|
||||
jsonb_build_object('question_id', q_2_7, 'instance_id', 'default', 'value', 'Remoto (home office)')
|
||||
)
|
||||
);
|
||||
|
||||
-- Survey queda en 'live' (ya lo estaba desde el UPDATE de arriba; este
|
||||
-- segundo UPDATE es explícito para que quede claro al releer el script,
|
||||
-- no depende de que nada de arriba lo haya dejado sin tocar).
|
||||
UPDATE surveys SET status='live' WHERE id = s_id;
|
||||
END $$;
|
||||
|
||||
-- Para limpiar manualmente este lote de prueba sin tocar otras encuestas:
|
||||
-- DELETE FROM public.responses WHERE survey_id = '30000000-0000-0000-0000-000000000003' AND ip_hash LIKE 'seed-prueba-fs-%';
|
||||
169
docs/estado/ESTADO_PROYECTO_26JUN2026.md
Normal file
169
docs/estado/ESTADO_PROYECTO_26JUN2026.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# ESTADO DEL PROYECTO — Datos País sobre el Cuidado
|
||||
## Documento de continuidad — 26 junio 2026
|
||||
|
||||
> Director (Product Owner): Marcos · Claude (Projects) = arquitecto/copiloto · Claude Code = implementación
|
||||
> Metodología: Maria Mersan · Contacto Fundación: Valeria Arce
|
||||
|
||||
---
|
||||
|
||||
## 1. Versionado y ramas
|
||||
- **Producción:** **3.0.0** — wizard v3 + dashboard tres miradas desplegado · tag `v3.0.0` · commit `fcf93ea`
|
||||
- **Preservación:** `v2.1.0-stable` + tag `v2.1.0`
|
||||
- **3 surveys en `live`** — reactivados al desplegar v3 (26 jun 2026)
|
||||
- SemVer + CHANGELOG + tags. ADRs en `docs/adr/`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sprint 4 — Wizard v3 — COMPLETO Y EN PRODUCCIÓN
|
||||
|
||||
| Etapa | Estado |
|
||||
|---|---|
|
||||
| 4.0 Preservación v2 | ✅ |
|
||||
| 4A instance_id | ✅ |
|
||||
| 4B esquema declarativo + seed v3 | ✅ |
|
||||
| 4C motor de flujo (`lib/form-engine.ts`, puro, 20+ tests) | ✅ |
|
||||
| 4D wizard renderer (layout estable, header/footer fijos) | ✅ |
|
||||
| 4E instancias familiares (mini-wizard emergente del motor) | ✅ |
|
||||
| 4F autocomplete barrios + filtro ciudad por departamento | ✅ |
|
||||
| 4G submit + validación server-side (RPC `rpc_submit_response`) | ✅ |
|
||||
| 4I validación e2e + tag v3.0.0 + deploy + surveys live | ✅ |
|
||||
|
||||
### Decisiones arquitectónicas clave (no reabrir)
|
||||
- **ADR-005:** lógica de flujo en datos (esquema), nunca en frontend.
|
||||
- Motor puro en cliente; reusado server-side para validación de submit.
|
||||
- Único camino de escritura: RPC `rpc_submit_response` (SECURITY DEFINER).
|
||||
- `instance_id` por answer — instancias familiares persistidas correctamente.
|
||||
- Validación server-side: 7 checks (preguntas visibles, valores válidos, instancias legítimas,
|
||||
consentimiento, requeridas presentes). Rechaza el submit completo si algo falla.
|
||||
|
||||
### Autocomplete geo (4F)
|
||||
- `public/data/geo_paraguay_full.json` — 8152 barrios, jerarquía depto → ciudad → barrios.
|
||||
- Pregunta 2.3 (barrio): autocomplete con `position: fixed` + `getBoundingClientRect()`.
|
||||
- Pregunta 2.2 (ciudad): filtra por departamento (2.1) usando el mismo JSON. Limpia valor
|
||||
al cambiar departamento. Texto libre siempre permitido como fallback.
|
||||
|
||||
---
|
||||
|
||||
## 3. Dashboard v3 — Mirada Empresa en producción
|
||||
|
||||
**Decisión vigente:** NO migrar RPCs v2. Dashboard nuevo de tres miradas
|
||||
(OIT 5R, CEPAL, PNCUPA Paraguay).
|
||||
|
||||
### Estado
|
||||
- **Mirada Empresa:** ✅ COMPLETA EN PRODUCCIÓN
|
||||
- 11 RPCs (`rpc_me_*`) + helper `_dash_es_cuidador` verificados contra 50 respuestas reales.
|
||||
- 9 widgets funcionando: Talento en riesgo, Prevalencia, Punto ciego, Señales de impacto
|
||||
(presentismo 41.7%, ausentismo 36.1%, riesgo 38.9%, carrera congelada 47.2%),
|
||||
Apoyos demandados (ordenados por demanda), Segmentación (nivel y modalidad),
|
||||
Disposición a red de apoyo.
|
||||
- Tema claro marca Re: fondo `#E7E6E5`, cards `#FFFFFF`, texto `#494F4F`.
|
||||
- Migraciones aplicadas: `20260625000001` (fix `_dash_responses`) +
|
||||
`20260625000002` (11 RPCs) + `20260626000001` (fix bug `cnt` Opción B).
|
||||
- **Mirada País y Mirada Humana:** propuesta + mockup listos, implementación DIFERIDA
|
||||
a sprint de dashboard post-deadline.
|
||||
|
||||
### Bug conocido abierto
|
||||
- `rpc_me_carrera_congelada`: "Ninguna de las anteriores" aparece en `top_opciones`
|
||||
a pesar del filtro en el subquery. El filtro opera sobre el array completo, no sobre
|
||||
los elementos individuales del `jsonb_array_elements_text`. No bloqueante para el
|
||||
release — registrado para próximo sprint.
|
||||
|
||||
### Documentos (en `docs/dashboard/`)
|
||||
- `PROPUESTA_DASHBOARD_v3.md` — las 3 miradas, ~30 métricas, marcos citados
|
||||
- `dashboard_tres_miradas_mockup.html` — mockup funcional 3 pestañas
|
||||
- `SPEC_MIRADA_EMPRESA.md` — contrato técnico 10 métricas + estrella
|
||||
|
||||
### Decisiones del dashboard (no reabrir)
|
||||
- CUIDADOR_ACTIVO incluye "hijo menor sin discapacidad" (criterio UNFPA, validado por Maria jun 2026).
|
||||
- Mirada Empresa NO expone datos sensibles (3.x, 4.2, 4.3).
|
||||
- k-anonimato k≥5 sobre `count(DISTINCT response_id)` — también en métricas por instancia.
|
||||
- 2 gaps de datos para sprint dashboard: horas cuidado no remunerado (País),
|
||||
escolarización niñez (Humana).
|
||||
|
||||
---
|
||||
|
||||
## 4. Datos de prueba en VPS
|
||||
- **51 respuestas** en survey Demo (`30000000-0000-0000-0000-000000000001`) — 50 del seed
|
||||
determinista (seed=42) + 1 del e2e de 4I.
|
||||
- Survey Demo en `live`. Datos NO limpiados — el director los necesita para mostrar al equipo.
|
||||
- Script de limpieza cuando corresponda:
|
||||
```bash
|
||||
docker exec supabase-db psql -U postgres -d postgres -c "
|
||||
TRUNCATE answers, responses, consent_records CASCADE;
|
||||
UPDATE surveys SET status='draft' WHERE id='30000000-0000-0000-0000-000000000001';"
|
||||
```
|
||||
- ⚠️ El seed corrió dos veces en sesión anterior (detectado y corregido). Agregar guard
|
||||
anti-duplicación al `seed_prueba_50.sql` — anotado en BACKLOG.
|
||||
|
||||
---
|
||||
|
||||
## 5. Deuda técnica y seguridad
|
||||
|
||||
| ID | Descripción | Prioridad |
|
||||
|---|---|---|
|
||||
| BUG-001 | "Ninguna de las anteriores" aparece en `rpc_me_carrera_congelada` | Próximo sprint dashboard |
|
||||
| TECH-DEBT-006 | Backup remoto (S3 o VPS alternativo o Google Drive) | Antes de datos reales no desechables |
|
||||
| TECH-DEBT-009 | Rotar secrets post-CVE | Antes de datos reales no desechables |
|
||||
| TECH-DEBT-010 | E9 lee modalidad por JOIN a answers (2.7) en vez de `qi_modalidad` | Baja |
|
||||
| SEC-002 | Sprint compliance (audit log, hashing, cifrado en reposo) | Sprint dedicado |
|
||||
| SEC-003 | TRUNCATE evade inmutabilidad ADR-003 | Antes de datos reales no desechables |
|
||||
| SEC-004 | RPC bypass REST (coherencia de flujo en Server Action) | Sprint futuro |
|
||||
| BACKLOG-001 | `seed_prueba_50.sql` idempotente (guard anti-duplicación) | Baja |
|
||||
| BACKLOG-002 | Fase 2: motor genérico de formularios | Post-deadline |
|
||||
|
||||
**Riesgo aceptado explícitamente (26 jun 2026):** TECH-DEBT-006, TECH-DEBT-009 y SEC-003
|
||||
aplican cuando haya datos reales no desechables. Los datos actuales son de empresas voluntarias
|
||||
y grupos de control — desechables si es necesario.
|
||||
|
||||
---
|
||||
|
||||
## 6. Infraestructura
|
||||
|
||||
- Next.js 15.3.8 · Supabase self-hosted · Dokploy v0.29.4 · VPS inq-mart-2 (172.20.5.11)
|
||||
- App: `economia-cuidado.inqualityhq.com` · API: `db.inqualityhq.com` (Kong + SSL)
|
||||
- Repo: `git.inquality.com.py/marcos/motor-de-encuestas` (rama `main`)
|
||||
- Repo local: `/Users/marcos/InQ/Motor de encuestas`
|
||||
- Migraciones SIEMPRE vía CLI del VPS:
|
||||
`docker exec -i supabase-db psql -U postgres -d postgres < archivo.sql`
|
||||
- Deploy: Dokploy → Rebuild + "Clean cache"
|
||||
- Anon key Kong (diagnóstico):
|
||||
`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjIwMDAwMDAwMDB9.6D79NWu1KfeOzEZXT6Amf-NmG0T9uk8SK9xNd32oLaY`
|
||||
|
||||
---
|
||||
|
||||
## 7. Contexto comercial
|
||||
- 2 clientes inminentes. Mirada Empresa es la que le habla al cliente que paga.
|
||||
- InQuality y Fundación Solidaridad en joint venture.
|
||||
- Datos actuales: empresas voluntarias y grupos de control (desechables).
|
||||
- Datos reales no desechables: avisará el director — en ese momento activar deuda de seguridad.
|
||||
|
||||
---
|
||||
|
||||
## 8. Próximos sprints sugeridos
|
||||
|
||||
1. **Sprint dashboard** — Miradas País y Humana, fix BUG-001 carrera congelada,
|
||||
preguntas faltantes (horas no remuneradas, escolarización), decisión `qi_modalidad`.
|
||||
2. **Sprint compliance** — SEC-002 (audit log, hashing, cifrado), SEC-003, TECH-DEBT-006/009.
|
||||
3. **Sprint motor genérico** — BACKLOG-002 Fase 2, extracción del intérprete.
|
||||
|
||||
---
|
||||
|
||||
## 9. Prompt para nueva sesión
|
||||
|
||||
```
|
||||
Sos el arquitecto/copiloto técnico de "Datos País sobre el Cuidado", motor de formularios
|
||||
soberano (Paraguay). Yo soy el product owner (Marcos).
|
||||
Claude Code (VS Code, contexto separado) hace la implementación; vos planificás,
|
||||
especificás y auditás.
|
||||
|
||||
Estado completo: docs/estado/ESTADO_PROYECTO_26JUN2026.md
|
||||
|
||||
Producción: v3.0.0 desplegada · 3 surveys en live · tag v3.0.0 (commit fcf93ea)
|
||||
Formulario v3: COMPLETO (motor, wizard, instancias, submit server-side, autocomplete geo)
|
||||
Dashboard: Mirada Empresa funcional en producción · País/Humana diferidas
|
||||
Datos en VPS: 51 respuestas en survey Demo (no limpiar — para mostrar al equipo)
|
||||
|
||||
Bug abierto: "Ninguna de las anteriores" en rpc_me_carrera_congelada (no bloqueante)
|
||||
|
||||
Próximo paso: [COMPLETAR]
|
||||
```
|
||||
142
docs/guias/GUIA_PRUEBA_FORMULARIO_Y_DASHBOARD.md
Normal file
142
docs/guias/GUIA_PRUEBA_FORMULARIO_Y_DASHBOARD.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Guía — Probar el formulario y leer el dashboard
|
||||
## Datos País sobre el Cuidado
|
||||
|
||||
> Dos públicos en un documento: **Sección 1** es para el equipo técnico que va a
|
||||
> probar el formulario. **Sección 2** es para Maria Mersan (metodología) — cómo
|
||||
> leer el dashboard de Mirada Empresa.
|
||||
|
||||
---
|
||||
|
||||
## Sección 1 — Plan de pruebas del formulario (equipo técnico)
|
||||
|
||||
### El link
|
||||
|
||||
```
|
||||
https://economia-cuidado.inqualityhq.com/?s=30000000-0000-0000-0000-000000000001
|
||||
```
|
||||
|
||||
Esta es la encuesta **"Empresa Demo S.A."** — un sandbox pensado exactamente para
|
||||
esto. **Pueden responderla las veces que quieran.** No es la encuesta real de
|
||||
ningún cliente: nada de lo que carguen ahí se mezcla con datos reales.
|
||||
|
||||
> ⚠️ No usen el link de Fundación Solidaridad ni el de InQuality para pruebas.
|
||||
> El sistema es **anónimo por diseño** (no guarda qué respuesta es de quién), así
|
||||
> que una respuesta de prueba enviada a una encuesta real queda mezclada para
|
||||
> siempre con datos reales, sin forma de distinguirla ni borrarla después.
|
||||
|
||||
### Qué van a ver al entrar
|
||||
|
||||
1. **Pantalla de consentimiento** — checkbox operativo (obligatorio) + checkbox de
|
||||
estudio país (opcional). Sin esto no se entra al formulario.
|
||||
2. **El wizard** — 10 secciones, 40 preguntas. La mayoría son **obligatorias**
|
||||
(recién activado) excepto la pregunta de barrio (2.3), que es texto libre
|
||||
opcional.
|
||||
3. **Pantalla final** — botón "Enviar encuesta".
|
||||
|
||||
### Checklist de cosas a probar
|
||||
|
||||
- [ ] Completar el formulario entero y enviarlo — confirmar que llega la pantalla
|
||||
de agradecimiento.
|
||||
- [ ] Intentar avanzar sin responder una pregunta obligatoria — debe bloquear con
|
||||
mensaje "Este campo es obligatorio."
|
||||
- [ ] Dejar el campo de barrio (2.3) vacío y avanzar — **no** debe bloquear (es
|
||||
opcional).
|
||||
- [ ] Escribir un barrio que no exista en la lista — debe aceptarse como texto
|
||||
libre, sin error.
|
||||
- [ ] Seleccionar Departamento → confirmar que Ciudad se filtra por ese
|
||||
departamento.
|
||||
- [ ] Ir hacia atrás con "Anterior" y adelante de nuevo — los valores que ya
|
||||
cargaron deben seguir ahí.
|
||||
- [ ] Cerrar la pestaña a mitad de formulario, volver a abrir el link — debe
|
||||
ofrecer retomar donde quedaron (guardado automático).
|
||||
- [ ] Probar en el celular (o achicando la ventana del navegador) — los botones y
|
||||
el texto no se deben cortar ni superponer.
|
||||
- [ ] Si pueden, prueben con el wifi cortado a mitad de un envío — debe mostrar
|
||||
un mensaje de error claro, no una pantalla rota.
|
||||
|
||||
Cualquier cosa rara (texto que no carga, botón que no responde, algo que se ve
|
||||
mal en el celular) — repórtenlo con captura de pantalla y en qué pregunta pasó.
|
||||
|
||||
### Dónde ver lo que enviaron
|
||||
|
||||
- **Lista de respuestas:** `/admin/responses` (filtro por encuesta "Empresa Demo
|
||||
S.A."). Necesita login.
|
||||
- **Dashboard:** `/dashboard?survey_id=30000000-0000-0000-0000-000000000001`
|
||||
(ver Sección 2 para cómo leerlo). Con pocas respuestas de prueba, varios
|
||||
números van a aparecer suprimidos — es el umbral de privacidad k≥5 funcionando
|
||||
como debe (ver más abajo), no un bug.
|
||||
|
||||
---
|
||||
|
||||
## Sección 2 — Cómo leer el dashboard (para Maria)
|
||||
|
||||
### Acceso
|
||||
|
||||
`https://economia-cuidado.inqualityhq.com/dashboard` — requiere login.
|
||||
|
||||
**Importante sobre roles, antes de entrar:** hoy el sistema tiene dos roles
|
||||
posibles — `platform_admin` (ve todas las organizaciones, incluido el selector
|
||||
"Todos") y `org_admin` (ve solo la organización a la que está asociado). El
|
||||
flujo de invitación que ya existe (`/admin/usuarios`) **solo puede crear
|
||||
usuarios `org_admin`** — no hay forma, hoy, de autoinvitarse como
|
||||
`platform_admin`. Si necesitás ver **varias** organizaciones desde tu propia
|
||||
cuenta (no solo Fundación Solidaridad), avisale a Marcos para que te dé acceso
|
||||
`platform_admin` directo en la base — el self-service todavía no lo cubre.
|
||||
|
||||
### Qué mirar primero
|
||||
|
||||
Arriba del dashboard hay un selector de organización. Para ver los datos que
|
||||
acabamos de cargar:
|
||||
|
||||
```
|
||||
/dashboard?survey_id=30000000-0000-0000-0000-000000000003
|
||||
```
|
||||
|
||||
(o click en "Fundación Solidaridad" en la barra superior).
|
||||
|
||||
> 🧪 **Estos son datos sintéticos, no respuestas reales de cuidadores de la
|
||||
> Fundación.** Se generaron para que el dashboard tenga algo que mostrar
|
||||
> mientras se valida el diseño — no son representativos de la población real
|
||||
> de la Fundación todavía. Cuando empecemos a recolectar datos reales, te
|
||||
> avisamos explícitamente (así lo acordamos con Marcos).
|
||||
|
||||
Las pestañas **Mirada País** y **Mirada Humana** todavía no están construidas
|
||||
(dicen "Disponible próximamente") — hoy solo **Mirada Empresa** tiene datos.
|
||||
|
||||
### Los 7 indicadores de Mirada Empresa
|
||||
|
||||
Cada tarjeta está anclada a una o más preguntas del instrumento (el número de
|
||||
pregunta aparece como subtítulo en cada tarjeta, para que puedan rastrear de
|
||||
dónde sale el número).
|
||||
|
||||
| Tarjeta (como aparece en pantalla) | Pregunta(s) | Qué responde | Cómo leerla |
|
||||
|---|---|---|---|
|
||||
| **Talento en riesgo** | 7.6 + 7.3 + 7.5 | Métrica compuesta: % de la plantilla cuidadora con señales combinadas de fuga (probabilidad de irse/reducir + ausentismo + carrera frenada). | Es el número "traductor a negocio" — cuanto más alto, más costo de reemplazo en riesgo. Pensado para abrir la conversación con RRHH/dirección, no como diagnóstico aislado. |
|
||||
| **Cuidadores en la plantilla** | 4.1 | % de empleados con algún vínculo de cuidado activo (incluye hijo/a menor sin discapacidad, criterio UNFPA). | Es la prevalencia base — el "cuánta gente de mi plantilla está en esto" antes de mirar cualquier otro indicador. |
|
||||
| **Punto ciego organizacional** | 9.1 vs. 4.1 | Brecha entre la carga real de cuidado (prevalencia) y la percepción de que la empresa lo nota/apoya. | Si la brecha es grande, hay gente cuidando sin que la organización lo sepa — es el hallazgo central que justifica el dashboard (ver marco conceptual en `docs/dashboard/PROPUESTA_DASHBOARD_v3.md`). |
|
||||
| **Señales de impacto laboral** | 7.2, 7.3, 7.6, 7.5 | Desglose de presentismo, ausentismo, probabilidad de salida y carrera frenada — los componentes de "Talento en riesgo" mostrados por separado. | Útil para ver CUÁL señal pesa más en este segmento (¿es ausentismo o es que están rechazando ascensos?). |
|
||||
| **Apoyos más demandados** | 9.2 | Qué beneficios pedirían (hasta 3 opciones por persona). | Es el insumo directo para diseñar beneficios — léelo como un ranking, no como un % que suma 100%. |
|
||||
| **Dónde se concentra la carga** | 4.1 cruzado con 2.6 (nivel) y 2.7 (modalidad) | % de cuidadores por segmento (jerarquía × modalidad de trabajo). | Mirar si la carga se concentra en niveles operativos vs. gerenciales, y presencial vs. remoto — relevante para política de flexibilidad. |
|
||||
| **Disposición a red de apoyo** | 9.3 + 9.4 | % dispuesto a sumarse a un grupo de apoyo interno / hacer voluntariado. | Mide apetito para iniciativas comunitarias internas, no necesidad — alguien puede necesitar apoyo y no querer un grupo. |
|
||||
|
||||
### El umbral de privacidad (k≥5)
|
||||
|
||||
Todo el dashboard aplica **k-anonimato k≥5**: ningún segmento con menos de 5
|
||||
personas se muestra desagregado (aparece suprimido). Esto es una regla de
|
||||
diseño no negociable del proyecto (protege a quien respondió, especialmente
|
||||
tratándose de datos de salud/discapacidad) — si ves un segmento "vacío" o con
|
||||
guion, probablemente es esto, no un error de carga. Con el lote de 50
|
||||
respuestas sintéticas la mayoría de los cortes generales van a tener datos,
|
||||
pero cruces muy específicos (ej. un departamento + un nivel jerárquico puntual)
|
||||
pueden quedar por debajo del umbral.
|
||||
|
||||
### Decisión metodológica pendiente (marcada para vos en la propuesta original)
|
||||
|
||||
El documento `docs/dashboard/PROPUESTA_DASHBOARD_v3.md` (sección "Decisión
|
||||
metodológica pendiente — para Maria") deja abierta una definición: las métricas
|
||||
de vínculo de cuidado (4.1) se pueden leer **por persona** ("% de empleados con
|
||||
al menos un familiar con discapacidad X") o **por instancia** ("del total de
|
||||
familiares cuidados, % con discapacidad X"). Hoy el dashboard usa la lectura
|
||||
por persona. Si para tu análisis necesitás la lectura por instancia, es un
|
||||
cambio de método de cálculo a definir y construir — avisame cuando lo hayas
|
||||
decidido.
|
||||
16
lib/geo-cache.ts
Normal file
16
lib/geo-cache.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// departamento → ciudad → barrios[]
|
||||
export type GeoData = Record<string, Record<string, string[]>>
|
||||
|
||||
let geoData: GeoData | null = null
|
||||
let geoPromise: Promise<GeoData> | null = null
|
||||
|
||||
export async function getGeoData(): Promise<GeoData> {
|
||||
if (geoData) return geoData
|
||||
if (!geoPromise) {
|
||||
geoPromise = fetch('/data/geo_paraguay_full.json')
|
||||
.then((r) => r.json())
|
||||
.then((data: GeoData) => { geoData = data; return data })
|
||||
.catch((err) => { geoPromise = null; throw err })
|
||||
}
|
||||
return geoPromise
|
||||
}
|
||||
@@ -11,3 +11,14 @@ export function createClient() {
|
||||
|
||||
return createSupabaseClient(url, anonKey)
|
||||
}
|
||||
|
||||
// service_role — bypasa RLS por completo (regla 9 CLAUDE.md). Solo se importa
|
||||
// desde Server Components / Server Actions ('use server'), nunca desde código
|
||||
// que se bundlea al cliente. No exportar este cliente ni su resultado hacia props
|
||||
// de un Client Component.
|
||||
export function getSupabaseAdmin() {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'https://placeholder.supabase.co'
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? 'placeholder'
|
||||
|
||||
return createSupabaseClient(url, serviceKey)
|
||||
}
|
||||
|
||||
63
package-lock.json
generated
63
package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/cli": "^4.11.3",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
@@ -1041,6 +1042,22 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
@@ -2135,6 +2152,20 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -3002,6 +3033,38 @@
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/cli": "^4.11.3",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
|
||||
14
playwright.config.ts
Normal file
14
playwright.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
// .env.test nunca se commitea (ver .gitignore) — credenciales de un usuario
|
||||
// dedicado de e2e, nunca las del director.
|
||||
process.loadEnvFile('.env.test')
|
||||
|
||||
export default defineConfig({
|
||||
testDir: 'tests/e2e',
|
||||
use: {
|
||||
baseURL: process.env.E2E_BASE_URL,
|
||||
},
|
||||
// Local contra producción — no corre en CI (backlog).
|
||||
reporter: 'list',
|
||||
})
|
||||
20
scripts/apply_all_migrations.sh
Executable file
20
scripts/apply_all_migrations.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Aplica todas las migraciones de supabase/migrations/ en orden de timestamp.
|
||||
# Uso: scripts/apply_all_migrations.sh
|
||||
set -e
|
||||
|
||||
MIGRATIONS_DIR="$(dirname "$0")/../supabase/migrations"
|
||||
|
||||
for f in $(ls "$MIGRATIONS_DIR"/*.sql | sort); do
|
||||
echo "Applying: $f"
|
||||
# -v ON_ERROR_STOP=1: sin esto, psql sigue ejecutando statements después de un
|
||||
# error SQL y termina con exit code 0 igual — set -e nunca vería el fallo.
|
||||
if docker exec -i supabase-db psql -v ON_ERROR_STOP=1 -U postgres -d postgres < "$f"; then
|
||||
echo "Done: $f"
|
||||
else
|
||||
echo "ERROR applying: $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All migrations applied."
|
||||
45
scripts/setup_all.sh
Executable file
45
scripts/setup_all.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# Recuperación completa de la DB tras una reinicialización (deploy que vació
|
||||
# supabase-db). Orden:
|
||||
# 1. Todas las migraciones (scripts/apply_all_migrations.sh)
|
||||
# 2. Catálogo de encuestas (supabase/seed.sql) — sin esto, las migraciones
|
||||
# dejan organizations/surveys/questions vacías y nada de lo que sigue
|
||||
# tiene contra qué referenciar (setup_first_admin.sql sí funcionaría,
|
||||
# pero seed_prueba_50.sql fallaría: rpc_submit_response necesita
|
||||
# questions.id reales, que solo existen después de este paso).
|
||||
# 3. Usuario platform_admin (scripts/setup_first_admin.sql)
|
||||
# 4. Datos de prueba (docs/dashboard/seed_prueba_50.sql)
|
||||
#
|
||||
# Uso:
|
||||
# ADMIN_EMAIL='admin@inquality.com.py' ADMIN_PASSWORD='...' scripts/setup_all.sh
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
ROOT_DIR="$SCRIPT_DIR/.."
|
||||
|
||||
if [ -z "$ADMIN_EMAIL" ] || [ -z "$ADMIN_PASSWORD" ]; then
|
||||
echo "ERROR: ADMIN_EMAIL y ADMIN_PASSWORD son obligatorios (variables de entorno)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== 1/4: Aplicando todas las migraciones =="
|
||||
"$SCRIPT_DIR/apply_all_migrations.sh"
|
||||
|
||||
echo "== 2/4: Sembrando catálogo de encuestas (supabase/seed.sql) =="
|
||||
# ADVERTENCIA propia del archivo: "SOLO ENTORNOS DE DESARROLLO — nunca ejecutar
|
||||
# en producción". Esa advertencia es para NO re-correrlo contra una DB con
|
||||
# datos reales (DELETE+INSERT de questions con UUIDs nuevos puede chocar con
|
||||
# answers existentes via FK). En este escenario de recuperación la DB está
|
||||
# vacía (reinicializada por el deploy) — no hay answers/responses que puedan
|
||||
# quedar huérfanas. Si la DB NO está realmente vacía, frenar antes de este paso.
|
||||
docker exec -i supabase-db psql -v ON_ERROR_STOP=1 -U postgres -d postgres < "$ROOT_DIR/supabase/seed.sql"
|
||||
|
||||
echo "== 3/4: Creando usuario platform_admin =="
|
||||
docker exec -i supabase-db psql -v ON_ERROR_STOP=1 -U postgres -d postgres \
|
||||
-v email="$ADMIN_EMAIL" -v password="$ADMIN_PASSWORD" \
|
||||
< "$ROOT_DIR/scripts/setup_first_admin.sql"
|
||||
|
||||
echo "== 4/4: Sembrando datos de prueba (seed_prueba_50.sql) =="
|
||||
docker exec -i supabase-db psql -v ON_ERROR_STOP=1 -U postgres -d postgres < "$ROOT_DIR/docs/dashboard/seed_prueba_50.sql"
|
||||
|
||||
echo "Setup completo."
|
||||
@@ -1,6 +1,13 @@
|
||||
-- ============================================================
|
||||
-- Script: crear primer usuario platform_admin
|
||||
--
|
||||
-- Verificado contra GoTrue v2.189.0 (supabase/gotrue:v2.189.0)
|
||||
-- Schema de auth.users puede variar entre versiones de GoTrue.
|
||||
-- Si falla con "column does not exist", verificar el schema real:
|
||||
-- SELECT column_name FROM information_schema.columns
|
||||
-- WHERE table_schema='auth' AND table_name='users'
|
||||
-- ORDER BY ordinal_position;
|
||||
--
|
||||
-- USO:
|
||||
-- docker exec -i supabase-db psql -U postgres -d postgres \
|
||||
-- -v email='admin@ejemplo.com' -v password='CAMBIAR' \
|
||||
@@ -16,43 +23,23 @@
|
||||
|
||||
WITH new_user AS (
|
||||
INSERT INTO auth.users (
|
||||
id,
|
||||
instance_id,
|
||||
aud,
|
||||
role,
|
||||
email,
|
||||
encrypted_password,
|
||||
email_confirmed_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
raw_app_meta_data,
|
||||
raw_user_meta_data,
|
||||
confirmation_token,
|
||||
recovery_token,
|
||||
email_change_token_new,
|
||||
email_change,
|
||||
id, instance_id, aud, role, email, encrypted_password,
|
||||
email_confirmed_at, created_at, updated_at,
|
||||
raw_app_meta_data, raw_user_meta_data,
|
||||
confirmation_token, recovery_token,
|
||||
email_change_token_new, email_change,
|
||||
is_super_admin
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'authenticated',
|
||||
'authenticated',
|
||||
'authenticated', 'authenticated',
|
||||
:'email',
|
||||
extensions.crypt(:'password', extensions.gen_salt('bf')),
|
||||
now(),
|
||||
now(),
|
||||
now(),
|
||||
now(), now(), now(),
|
||||
'{"provider":"email","providers":["email"]}',
|
||||
'{}',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
false
|
||||
)
|
||||
RETURNING id, email
|
||||
'{}', '', '', '', '', false
|
||||
) RETURNING id
|
||||
)
|
||||
INSERT INTO public.user_roles (user_id, app_role, org_id)
|
||||
SELECT id, 'platform_admin', '10000000-0000-0000-0000-000000000002'
|
||||
FROM new_user
|
||||
RETURNING (SELECT email FROM new_user), user_id;
|
||||
FROM new_user;
|
||||
|
||||
113
sprints/sprint-4/ETAPA_4I_PROMPT.md
Normal file
113
sprints/sprint-4/ETAPA_4I_PROMPT.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# ETAPA 4I — Validación e2e + Release v3.0.0
|
||||
|
||||
## 1. Objetivo
|
||||
Validar el wizard v3 end-to-end y hacer el release a producción. Los 3 surveys
|
||||
pasan de `draft` a `live`. Esta etapa es irreversible en varios pasos — seguir
|
||||
el orden estricto de fases y detenerse si algo falla.
|
||||
|
||||
## 2. Estado de entrada
|
||||
- Último commit: `3e7109a0` (4F ciudad-select)
|
||||
- Commits no desplegados desde último deploy:
|
||||
`8dea04c` fix cnt RPCs · `dbcc60a` tema claro · `3ec5da0` autocomplete barrios ·
|
||||
`3e7109a0` ciudad-select
|
||||
- VPS: seed de 50 respuestas activo (Demo en `live`) — NO limpiar, el director
|
||||
quiere mostrar el dashboard al equipo con esos datos
|
||||
- Los 3 surveys están en `draft`
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar
|
||||
- Deploy vía Dokploy (rebuild + clean cache)
|
||||
- Migraciones SIEMPRE vía CLI del VPS — ninguna migración pendiente en esta etapa
|
||||
- Los 3 surveys se reactivan a `live` solo DESPUÉS de verificar producción
|
||||
- Tag `v3.0.0` solo DESPUÉS de reactivar surveys
|
||||
- No hay migración de datos v2 → v3 (arranque limpio)
|
||||
|
||||
## 4. Fases — orden estricto, detenerse si algo falla
|
||||
|
||||
### FASE 1 — Validación e2e local (antes de tocar producción)
|
||||
Correr el wizard completo localmente y verificar:
|
||||
- [ ] Consentimiento → flujo completo → submit exitoso
|
||||
- [ ] Una respuesta con al menos 1 familiar (instancia) persiste con instance_id
|
||||
- [ ] El motor no muestra preguntas que el flujo oculta
|
||||
- [ ] El autocomplete de barrio funciona (dropdown visible, no tapado)
|
||||
- [ ] El filtro ciudad-departamento funciona (2.1 → 2.2 filtra, cambiar 2.1 limpia 2.2)
|
||||
- [ ] Submit devuelve confirmación, no error
|
||||
|
||||
Si CUALQUIER item falla: DETENER. Reportar el fallo y esperar instrucción.
|
||||
|
||||
### FASE 2 — Deploy en Dokploy
|
||||
- Trigger rebuild en Dokploy con "Clean cache"
|
||||
- Verificar que el BUILD_ID nuevo aparece en los logs
|
||||
- Verificar que la app responde en `economia-cuidado.inqualityhq.com`
|
||||
- Verificar que el dashboard sigue mostrando los 50 datos sembrados
|
||||
- Verificar que el wizard abre y llega a la pregunta 2.3 sin errores de consola
|
||||
|
||||
Si CUALQUIER item falla: DETENER. Reportar el fallo y esperar instrucción.
|
||||
|
||||
### FASE 3 — Reactivar surveys + Tag
|
||||
Solo ejecutar si Fase 1 y Fase 2 están completas y verificadas.
|
||||
|
||||
Reactivar los 3 surveys en el VPS:
|
||||
```sql
|
||||
UPDATE surveys SET status = 'live'
|
||||
WHERE id IN (
|
||||
SELECT id FROM surveys WHERE status = 'draft'
|
||||
);
|
||||
```
|
||||
|
||||
Verificar que quedaron en `live`:
|
||||
```sql
|
||||
SELECT id, name, status FROM surveys ORDER BY name;
|
||||
```
|
||||
|
||||
Tag de release en el repo local y push:
|
||||
```bash
|
||||
git tag -a v3.0.0 -m "Release v3.0.0 — wizard declarativo + dashboard tres miradas"
|
||||
git push origin v3.0.0
|
||||
```
|
||||
|
||||
Actualizar CHANGELOG.md con las etapas 4C→4I completadas.
|
||||
|
||||
## 5. DoD completo
|
||||
- [ ] `git log --name-only -1` antes de empezar — confirmar commit de entrada
|
||||
- [ ] Fase 1 completa sin fallos
|
||||
- [ ] Fase 2 completa — app en producción respondiendo
|
||||
- [ ] Surveys en `live` — verificado con SELECT
|
||||
- [ ] Tag `v3.0.0` pusheado a Gitea
|
||||
- [ ] CHANGELOG.md actualizado
|
||||
- [ ] `git log --name-only -1` del commit de CHANGELOG
|
||||
|
||||
## 6. Qué reportar
|
||||
Por fase, en orden:
|
||||
FASE 1 — e2e local:
|
||||
|
||||
Submit con familiar: [response_id y instance_id del registro insertado]
|
||||
Items del checklist: ✓/✗ cada uno
|
||||
|
||||
FASE 2 — Deploy:
|
||||
|
||||
BUILD_ID nuevo: [valor]
|
||||
App responde: ✓/✗
|
||||
Dashboard con datos: ✓/✗
|
||||
Wizard sin errores de consola: ✓/✗
|
||||
|
||||
FASE 3 — Release:
|
||||
|
||||
Surveys en live: [output del SELECT]
|
||||
Tag v3.0.0: [output de git tag]
|
||||
CHANGELOG: [secciones agregadas]
|
||||
|
||||
## 7. Qué NO hacer
|
||||
- ❌ No reactivar surveys antes de verificar producción (Fase 2 primero)
|
||||
- ❌ No taggear antes de reactivar surveys (Fase 3 es la última)
|
||||
- ❌ No limpiar el seed de 50 respuestas — el director los necesita para mostrar al equipo
|
||||
- ❌ No aplicar migraciones — no hay ninguna pendiente en esta etapa
|
||||
- ❌ No tocar `v2.1.0-stable`
|
||||
- ❌ No continuar si algo falla — detenerse y reportar
|
||||
|
||||
## 8. Versionado
|
||||
chore(release): CHANGELOG v3.0.0 — wizard declarativo + dashboard tres miradas
|
||||
Etapas 4C→4I completadas. Motor de flujo puro, wizard renderer,
|
||||
|
||||
instancias familiares, submit con validación server-side, dashboard
|
||||
|
||||
tres miradas (Mirada Empresa funcional), autocomplete barrios/ciudad.
|
||||
148
sprints/sprint-4/ETAPA_AUTH_PROMPT.md
Normal file
148
sprints/sprint-4/ETAPA_AUTH_PROMPT.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# ETAPA AUTH — Supabase Auth + protección /admin
|
||||
|
||||
## 1. Objetivo
|
||||
Implementar autenticación email/password con Supabase Auth y proteger
|
||||
la ruta /admin/*. El dashboard y las vistas de admin quedan detrás de login.
|
||||
Sin esta etapa no se avanza a vista admin mejorada ni vista detallada.
|
||||
|
||||
## 2. Contexto
|
||||
- `current_app_role()` y `current_org_id()` ya leen del JWT — el esquema está listo.
|
||||
- No hay middleware.ts ni sistema de auth hoy — /admin/responses está abierto.
|
||||
- Organizations existentes: Empresa Demo (…0001), InQuality (…0002),
|
||||
Fundación Solidaridad (…0003).
|
||||
- No hay usuarios en auth.users — hay que crear el primero como parte de esta etapa.
|
||||
- La app usa anon key para el formulario público — eso NO cambia.
|
||||
- /admin/* usa service_role hoy — eso SÍ cambia: pasa a usar sesión del usuario.
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar
|
||||
- Supabase Auth nativo, email/password. Sin OAuth por ahora.
|
||||
- Dos roles: `platform_admin` (acceso total) y `org_admin` (acceso a su org).
|
||||
- Roles van como custom claims en el JWT: `app_role` y `org_id`.
|
||||
- Middleware Next.js protege /admin/* — redirige a /login si no hay sesión válida.
|
||||
- Página de login en /login — simple, solo email/password, sin registro público.
|
||||
- El formulario público (/encuesta/*) NO se toca — sigue con anon key.
|
||||
- Las RPCs del dashboard usan current_app_role() y current_org_id() del JWT —
|
||||
cuando el usuario esté autenticado, RLS funciona automáticamente.
|
||||
|
||||
## 4. Tareas
|
||||
|
||||
### 4.1 Diagnóstico previo — mecanismo de custom claims
|
||||
ANTES de implementar, reportar qué mecanismo de JWT customization está disponible
|
||||
en esta versión de Supabase self-hosted. Puede ser:
|
||||
- Hook SQL en `auth.hooks`
|
||||
- Override de función `auth.jwt()`
|
||||
- Edge Function hook
|
||||
No implementar nada hasta confirmar el mecanismo y reportarlo.
|
||||
|
||||
### 4.2 Migración — tabla user_roles + custom claims hook
|
||||
Migración `20260627000001_auth_user_roles.sql`:
|
||||
|
||||
```sql
|
||||
-- Tabla de roles
|
||||
CREATE TABLE public.user_roles (
|
||||
user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
app_role text NOT NULL CHECK (app_role IN ('platform_admin','org_admin')),
|
||||
org_id uuid REFERENCES public.organizations(id)
|
||||
);
|
||||
|
||||
-- RLS: solo el propio usuario puede ver su rol (y platform_admin ve todos)
|
||||
ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY;
|
||||
```
|
||||
|
||||
Implementar el hook de custom claims según el mecanismo disponible confirmado en 4.1.
|
||||
El JWT resultante debe incluir:
|
||||
```json
|
||||
{ "app_role": "platform_admin", "org_id": "10000000-0000-0000-0000-000000000002" }
|
||||
```
|
||||
|
||||
### 4.3 Script de setup — primer usuario admin
|
||||
Script operativo (NO migración) en `scripts/setup_first_admin.sql`:
|
||||
- Crea el usuario en auth.users vía INSERT directo o Supabase admin API
|
||||
- Inserta en user_roles con app_role='platform_admin' y org_id de InQuality
|
||||
- NO hardcodear credenciales — usar variables `:email` y `:password` con instrucciones
|
||||
de cómo ejecutarlo:
|
||||
```bash
|
||||
docker exec -i supabase-db psql -U postgres -d postgres \
|
||||
-v email='admin@ejemplo.com' -v password='CAMBIAR' \
|
||||
< scripts/setup_first_admin.sql
|
||||
```
|
||||
- Documentar en el script cómo cambiar la contraseña después del primer login
|
||||
|
||||
### 4.4 Middleware Next.js
|
||||
`middleware.ts` en la raíz del proyecto:
|
||||
- Protege `/admin/*` y `/dashboard/*`
|
||||
- Si no hay sesión válida → redirect a `/login`
|
||||
- Si hay sesión → deja pasar
|
||||
- `/`, `/encuesta/*`, `/login` — sin restricción
|
||||
|
||||
### 4.5 Página de login
|
||||
`app/login/page.tsx` — Client Component:
|
||||
- Formulario email/password
|
||||
- Llama `supabase.auth.signInWithPassword()`
|
||||
- En éxito: redirect a `/admin/responses`
|
||||
- En error: mensaje claro ("Email o contraseña incorrectos"), nunca trace crudo
|
||||
- Sin registro público
|
||||
- Usar clases Tailwind existentes — sin nuevo sistema de diseño
|
||||
|
||||
### 4.6 Actualizar /admin/responses
|
||||
- Reemplazar `getSupabaseAdmin()` (service_role) por cliente de sesión del usuario
|
||||
- RLS de Postgres filtra según app_role y org_id del JWT automáticamente
|
||||
- Agregar botón "Cerrar sesión" (`supabase.auth.signOut()` → redirect a `/login`)
|
||||
|
||||
## 5. DoD antes de reportar
|
||||
- [ ] `git log --name-only -1` — listar todos los archivos tocados
|
||||
- [ ] Diagnóstico de mecanismo de custom claims reportado (4.1) antes de implementar
|
||||
- [ ] Migración `20260627000001` aplicada en VPS — verificar con:
|
||||
```bash
|
||||
docker exec -i supabase-db psql -U postgres -d postgres -c "\d user_roles"
|
||||
```
|
||||
- [ ] Script `scripts/setup_first_admin.sql` existe y está documentado
|
||||
- [ ] Hook JWT verificado — el token incluye `app_role` (cómo verificarlo: reportar el método)
|
||||
- [ ] `/login` funciona — email/password → sesión activa → redirect a /admin/responses
|
||||
- [ ] `/admin/responses` sin sesión → redirect a `/login`
|
||||
- [ ] `/admin/responses` con sesión → carga datos correctamente
|
||||
- [ ] Formulario público sin cambios (verificar que /encuesta/* sigue funcionando)
|
||||
- [ ] Build sin errores: `npm run build`
|
||||
- [ ] Commit + push a Gitea
|
||||
|
||||
## 6. Qué reportar
|
||||
```
|
||||
Diagnóstico previo (4.1):
|
||||
- Mecanismo de custom claims disponible: [cuál]
|
||||
- Alternativas descartadas: [por qué]
|
||||
|
||||
Verificado:
|
||||
- Migración user_roles aplicada: ✓ — output de \d user_roles
|
||||
- Hook JWT: app_role presente en token ✓/✗ — método de verificación usado
|
||||
- /login funciona: ✓/✗
|
||||
- /admin sin sesión → /login: ✓/✗
|
||||
- /admin con sesión → datos: ✓/✗
|
||||
- Formulario público sin cambios: ✓/✗
|
||||
- Build: ✓/✗
|
||||
- git log --name-only -1: [lista completa de archivos]
|
||||
|
||||
Asumido / decisiones:
|
||||
- [mecanismo de hook y razonamiento]
|
||||
|
||||
Hallazgos:
|
||||
- [lo que aparezca]
|
||||
```
|
||||
|
||||
## 7. Qué NO hacer
|
||||
- ❌ No tocar el formulario público ni la anon key
|
||||
- ❌ No hardcodear credenciales en ningún archivo commiteado
|
||||
- ❌ No implementar registro público de usuarios
|
||||
- ❌ No usar service_role en rutas que ya tienen sesión de usuario
|
||||
- ❌ No avanzar a vista admin mejorada — eso es la etapa siguiente
|
||||
- ❌ No hacer deploy — solo commit y push a Gitea
|
||||
- ❌ No implementar el hook sin reportar primero el diagnóstico de 4.1
|
||||
|
||||
## 8. Versionado
|
||||
```
|
||||
feat(auth): Supabase Auth email/password + middleware + login [auth]
|
||||
|
||||
Protege /admin/* con Supabase Auth. Tabla user_roles con custom claims
|
||||
(app_role, org_id) en JWT. Middleware Next.js redirige a /login sin sesión.
|
||||
Página /login con email/password. /admin/responses migrado de service_role
|
||||
a sesión de usuario autenticado.
|
||||
```
|
||||
154
sprints/sprint-5/ETAPA_ADMIN_PROMPT.md
Normal file
154
sprints/sprint-5/ETAPA_ADMIN_PROMPT.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# ETAPA ADMIN — Vista de respuestas mejorada + vista detallada
|
||||
|
||||
## 1. Objetivo
|
||||
Mejorar `/admin/responses` para que sea una interfaz de administración real,
|
||||
y crear `/admin/responses/[id]` como vista detallada de una respuesta individual.
|
||||
Todo detrás del middleware de auth existente.
|
||||
|
||||
## 2. Contexto
|
||||
- Estado actual: `app/admin/responses/page.tsx` (Server Component, 249 líneas) +
|
||||
`app/admin/responses/responses-table.tsx` (Client Component, 98 líneas).
|
||||
- Auth: ya implementada — getSupabaseSession() con anon key + cookies, respeta RLS.
|
||||
- Exclusión de sensibles: `is_sensitive=false` y `type != 'text_long'` en query de
|
||||
questions — NO tocar, mantener exactamente igual.
|
||||
- SURVEY_OPTIONS hardcodeadas con UUIDs literales — reemplazar por query dinámica.
|
||||
- LIMIT 500 sin paginación real — reemplazar por paginación con page size de 50.
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar
|
||||
- Paginación: page size 50, parámetro `?page=N` en la URL (Server Component,
|
||||
recarga completa — no client-side infinite scroll).
|
||||
- Filtro de survey: `<select>` que carga surveys desde la DB (solo las que el
|
||||
usuario puede ver según RLS), no hardcodeado.
|
||||
- Vista detallada: ruta `/admin/responses/[id]` — Server Component, muestra todas
|
||||
las respuestas no sensibles de esa submission, agrupadas por sección.
|
||||
- Diseño: usar clases Tailwind o clases `.admin-*` existentes — sin nuevo sistema.
|
||||
- Sin librería de tablas externa.
|
||||
- El expand/collapse por submission en la tabla se conserva.
|
||||
|
||||
## 4. Tareas
|
||||
|
||||
### 4.1 Vista lista mejorada — `app/admin/responses/page.tsx`
|
||||
|
||||
**Filtro de survey dinámico:**
|
||||
Reemplazar SURVEY_OPTIONS hardcodeadas por query a `surveys` table:
|
||||
```ts
|
||||
const { data: surveys } = await supabase
|
||||
.from('surveys')
|
||||
.select('id, title')
|
||||
.order('title')
|
||||
```
|
||||
Renderizar como `<select>` con onChange que navega a `?survey_id=<id>`.
|
||||
Opción "Todos los surveys" con value=''.
|
||||
|
||||
**Paginación:**
|
||||
- Leer `searchParams.page` (default 1).
|
||||
- Query de responses: `.range((page-1)*50, page*50-1)` en vez de `.limit(500)`.
|
||||
- Mostrar: "Página N de M · X respuestas totales".
|
||||
- Links Anterior / Siguiente que modifican `?page=N` en la URL.
|
||||
- Si hay menos de 50 resultados, no mostrar paginación.
|
||||
|
||||
**Columna de acciones:**
|
||||
Agregar columna "Ver" en la fila cabecera de cada submission (isFirstInGroup=true)
|
||||
con link a `/admin/responses/[response_id]`.
|
||||
|
||||
**Header informativo:**
|
||||
Mostrar claramente: total de submissions, total de answers, survey seleccionado.
|
||||
|
||||
### 4.2 Vista detallada — `app/admin/responses/[id]/page.tsx`
|
||||
|
||||
Server Component. Ruta: `/admin/responses/[id]` donde `id` = `response_id` (uuid).
|
||||
|
||||
**Query principal:**
|
||||
```ts
|
||||
// 1. Metadata de la respuesta
|
||||
const { data: response } = await supabase
|
||||
.from('responses')
|
||||
.select('id, submitted_at, qi_zone, qi_sector, qi_age_bucket, survey_id')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
// 2. Preguntas permitidas (mismo filtro que la lista)
|
||||
const { data: questions } = await supabase
|
||||
.from('questions')
|
||||
.select('id, code, prompt, type, position, section_number, section_title')
|
||||
.eq('is_sensitive', false)
|
||||
.neq('type', 'text_long')
|
||||
.eq('survey_id', response.survey_id)
|
||||
.order('position')
|
||||
|
||||
// 3. Answers de esta respuesta
|
||||
const { data: answers } = await supabase
|
||||
.from('answers')
|
||||
.select('question_id, value, instance_id')
|
||||
.eq('response_id', id)
|
||||
.in('question_id', questions.map(q => q.id))
|
||||
```
|
||||
|
||||
**Layout:**
|
||||
- Header: fecha, zona, sector, edad — datos de qi_* de la respuesta.
|
||||
- Link "← Volver a la lista" que preserva el `?survey_id` y `?page` anteriores
|
||||
(pasar como searchParam o usar `referrer`).
|
||||
- Respuestas agrupadas por sección (section_number + section_title).
|
||||
- Dentro de cada sección: tabla código | pregunta | valor.
|
||||
- Instancias (instance_id != 'default'): mostrar agrupadas bajo "Familiar N"
|
||||
con badge visual.
|
||||
- Valores nulos: badge "sin respuesta" (igual que la lista).
|
||||
- Si `id` no existe o RLS bloquea: mostrar "Respuesta no encontrada" —
|
||||
no crashear.
|
||||
|
||||
### 4.3 Tabla de respuestas — `app/admin/responses/responses-table.tsx`
|
||||
Agregar la columna "Ver" en las filas isFirstInGroup con `<Link>` a
|
||||
`/admin/responses/[submissionId]`. Pasar `surveyId` y `page` como props
|
||||
para construir el link de "← Volver" en la vista detallada.
|
||||
|
||||
## 5. DoD antes de reportar
|
||||
- [ ] `git log --name-only -1` — listar todos los archivos tocados
|
||||
- [ ] Filtro de survey carga desde DB, no hardcodeado
|
||||
- [ ] Paginación funciona: page=1 muestra primeros 50, page=2 los siguientes
|
||||
- [ ] Link "Ver" en cada submission lleva a `/admin/responses/[id]`
|
||||
- [ ] Vista detallada muestra respuestas agrupadas por sección
|
||||
- [ ] Instancias agrupadas bajo "Familiar N"
|
||||
- [ ] RLS respetado: misma exclusión de sensibles que la lista
|
||||
- [ ] "Respuesta no encontrada" si el id no existe
|
||||
- [ ] Build sin errores: `npm run build`
|
||||
- [ ] `git log --name-only -1`
|
||||
- [ ] Commit + push a Gitea
|
||||
|
||||
## 6. Qué reportar
|
||||
```
|
||||
Verificado:
|
||||
- Filtro survey: ✓ — query a tabla surveys, no hardcodeado
|
||||
- Paginación: ✓ — page size 50, range correcto
|
||||
- Link "Ver" → /admin/responses/[id]: ✓
|
||||
- Vista detallada: ✓ — agrupada por sección, instancias bajo "Familiar N"
|
||||
- RLS/exclusión sensibles: ✓ — mismo filtro que antes
|
||||
- "No encontrada" si id inválido: ✓
|
||||
- Build: ✓
|
||||
- git log --name-only -1: [lista completa]
|
||||
- Commit: [hash]
|
||||
|
||||
Decisiones de diseño tomadas:
|
||||
- [lo que haya]
|
||||
|
||||
Hallazgos:
|
||||
- [lo que aparezca, especialmente sobre questions.section_number/section_title
|
||||
— si esas columnas no existen, reportarlo antes de inventar una solución]
|
||||
```
|
||||
|
||||
## 7. Qué NO hacer
|
||||
- ❌ No tocar el filtro `is_sensitive=false` ni `type != 'text_long'`
|
||||
- ❌ No usar service_role — solo getSupabaseSession() con RLS
|
||||
- ❌ No agregar librerías de tablas o UI externas
|
||||
- ❌ No implementar búsqueda por texto (fuera de alcance)
|
||||
- ❌ No hacer deploy — solo commit y push a Gitea
|
||||
- ❌ Si `section_number` o `section_title` no existen en questions,
|
||||
reportarlo y NO inventar columnas — esperar instrucción
|
||||
|
||||
## 8. Versionado
|
||||
```
|
||||
feat(admin): vista respuestas mejorada + vista detallada [admin]
|
||||
|
||||
/admin/responses: filtro survey dinámico, paginación 50/página,
|
||||
link a vista detallada. /admin/responses/[id]: respuestas por sección,
|
||||
instancias bajo "Familiar N". RLS y exclusión de sensibles intactos.
|
||||
```
|
||||
124
sprints/sprint-5/ETAPA_AUDIT_LOG_PROMPT.md
Normal file
124
sprints/sprint-5/ETAPA_AUDIT_LOG_PROMPT.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# ETAPA AUDIT LOG — Registro de auditoría append-only (Sprint 5, SEC-002)
|
||||
|
||||
## 1. Objetivo
|
||||
Implementar un registro de auditoría append-only en Postgres que loggee eventos
|
||||
de configuración crítica: cambios en `user_roles` (altas/bajas/modificaciones de
|
||||
acceso) y cambios de `status` en `surveys` (activación/desactivación de encuestas).
|
||||
Opcionalmente, si la implementación es simple y sin fricción, agregar también un
|
||||
trigger DDL para loggear migraciones aplicadas.
|
||||
|
||||
## 2. Contexto
|
||||
- Migración de referencia para patrones: `20260627000001_auth_user_roles.sql`
|
||||
(patrón SECURITY DEFINER + GRANT puntual a reutilizar).
|
||||
- Helpers RLS existentes a reusar: `public.current_app_role()` y
|
||||
`public.current_org_id()` — no reinventar.
|
||||
- Gap conocido y aceptado (SEC-003): esto NO protege contra TRUNCATE/DROP ejecutado
|
||||
por superusuario directo en el VPS. Documentar en comentario de la migración, no resolver.
|
||||
- Acceso a datos sensibles: NO loggear en esta etapa — hoy no existe código que
|
||||
lea columnas `is_sensitive=true`. Deferido a cuando ese código exista.
|
||||
- Naming de migración: después de `20260627000001` — usar timestamp del día de
|
||||
ejecución, formato `YYYYMMDDHHMMSS`.
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar
|
||||
- Tabla nueva: `public.audit_log`, append-only real.
|
||||
- Ningún rol (`anon`, `authenticated`, `service_role` vía API) tiene INSERT/UPDATE/DELETE
|
||||
directo en `audit_log`. Solo escriben funciones SECURITY DEFINER de los triggers.
|
||||
- Segunda barrera: trigger `BEFORE UPDATE OR DELETE ON audit_log` que aborta con
|
||||
excepción — defensa en profundidad.
|
||||
- Lectura de audit_log: RLS, solo `platform_admin` via `current_app_role()`.
|
||||
- Patrón de función trigger: SECURITY DEFINER, propiedad de `postgres`,
|
||||
GRANT EXECUTE solo al rol que lo necesita (no a PUBLIC).
|
||||
- Columnas de audit_log:
|
||||
```sql
|
||||
id bigserial PRIMARY KEY,
|
||||
occurred_at timestamptz NOT NULL DEFAULT now(),
|
||||
actor_role text,
|
||||
actor_user_id uuid,
|
||||
action text NOT NULL,
|
||||
target_table text NOT NULL,
|
||||
target_id text,
|
||||
detail jsonb
|
||||
```
|
||||
- Triggers obligatorios (Prioridad 1):
|
||||
- AFTER INSERT/UPDATE/DELETE en `user_roles`
|
||||
- AFTER UPDATE en `surveys` cuando cambia `status`
|
||||
- Trigger DDL (Prioridad 2 — opcional): solo implementar si es simple y sin fricción.
|
||||
Si requiere extensión nueva o configuración de infraestructura, NO incluir y reportarlo.
|
||||
|
||||
## 4. Tareas
|
||||
|
||||
### 4.1 Diagnóstico previo — DDL trigger
|
||||
Antes de implementar, verificar si el DDL trigger (`ddl_command_end`) está disponible
|
||||
sin extensiones adicionales en esta versión de Postgres self-hosted:
|
||||
```bash
|
||||
docker exec -i supabase-db psql -U postgres -d postgres -c "
|
||||
SELECT event_object_schema, trigger_name
|
||||
FROM information_schema.triggers
|
||||
WHERE trigger_name LIKE '%ddl%' LIMIT 5;"
|
||||
```
|
||||
Reportar si es viable sin fricción. Si requiere configuración extra, documentarlo
|
||||
y omitirlo de esta etapa.
|
||||
|
||||
### 4.2 Migración principal
|
||||
Archivo: `supabase/migrations/YYYYMMDDHHMMSS_audit_log.sql`
|
||||
|
||||
Contenido en orden:
|
||||
1. Crear tabla `audit_log` con columnas definidas arriba.
|
||||
2. RLS en `audit_log`: solo `platform_admin` puede SELECT.
|
||||
3. Trigger `BEFORE UPDATE OR DELETE ON audit_log` → función que lanza excepción
|
||||
(append-only enforcement).
|
||||
4. Función `log_user_roles_change()` SECURITY DEFINER → INSERT en audit_log.
|
||||
5. Trigger AFTER INSERT/UPDATE/DELETE en `user_roles` → llama a la función.
|
||||
6. Función `log_survey_status_change()` SECURITY DEFINER → INSERT en audit_log
|
||||
solo cuando `NEW.status IS DISTINCT FROM OLD.status`.
|
||||
7. Trigger AFTER UPDATE en `surveys` → llama a la función.
|
||||
8. Si DDL trigger es viable (4.1): función + trigger DDL.
|
||||
9. Comentario al final documentando el gap SEC-003.
|
||||
|
||||
### 4.3 Verificación en VPS
|
||||
NO aplicar la migración — solo generar el archivo y reportar el SQL completo para
|
||||
revisión del director. El director aplica manualmente después de aprobar.
|
||||
|
||||
## 5. DoD antes de reportar
|
||||
- [ ] `git log --name-only -1` — solo el archivo de migración
|
||||
- [ ] SQL completo de la migración en el reporte (no resumido — el director
|
||||
necesita leerlo antes de aplicar)
|
||||
- [ ] Diagnóstico DDL trigger reportado (4.1)
|
||||
- [ ] Build sin errores: `npm run build`
|
||||
- [ ] Commit + push a Gitea
|
||||
- [ ] NO aplicado en VPS — esperar aprobación del director
|
||||
|
||||
## 6. Qué reportar
|
||||
```
|
||||
Diagnóstico DDL trigger (4.1):
|
||||
- ¿Viable sin extensiones? ✓/✗
|
||||
- Si no: razón y alternativa
|
||||
|
||||
SQL completo de la migración:
|
||||
[pegar el SQL íntegro — no resumido]
|
||||
|
||||
Verificado:
|
||||
- Build: ✓/✗
|
||||
- git log --name-only -1: [lista]
|
||||
- Commit: [hash]
|
||||
|
||||
NO aplicado en VPS — pendiente aprobación del director.
|
||||
```
|
||||
|
||||
## 7. Qué NO hacer
|
||||
- ❌ No aplicar la migración en el VPS sin aprobación explícita del director
|
||||
- ❌ No implementar logging de accesos a datos sensibles (deferido)
|
||||
- ❌ No agregar extensiones nuevas a Postgres
|
||||
- ❌ No tocar el formulario público, las RPCs de dashboard, ni el middleware de auth
|
||||
- ❌ No usar GRANT EXECUTE a PUBLIC — solo al rol específico que necesita cada función
|
||||
- ❌ No omitir el comentario del gap SEC-003 en la migración
|
||||
|
||||
## 8. Versionado
|
||||
```
|
||||
feat(compliance): audit_log append-only — user_roles y surveys.status [SEC-002]
|
||||
|
||||
Tabla audit_log con RLS (solo platform_admin), trigger BEFORE UPDATE/DELETE
|
||||
que aborta (append-only enforcement), triggers AFTER en user_roles y
|
||||
surveys.status. Patrón SECURITY DEFINER reutilizado de auth_user_roles.
|
||||
Gap SEC-003 (TRUNCATE por superusuario) documentado y aceptado.
|
||||
```
|
||||
123
sprints/sprint-5/ETAPA_CALIDAD_PROMPT.md
Normal file
123
sprints/sprint-5/ETAPA_CALIDAD_PROMPT.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# ETAPA CALIDAD — Contraste, mobile, conectividad, touch (Sprint 5)
|
||||
|
||||
## 1. Objetivo
|
||||
Resolver los 4 problemas de calidad identificados en la auditoría del 27 jun 2026,
|
||||
en orden de impacto. Un solo commit al terminar todo.
|
||||
|
||||
## 2. Contexto
|
||||
Diagnóstico base: auditoría de solidez/usabilidad (27 jun 2026, sin modificaciones).
|
||||
Todos los problemas tienen archivo y línea exacta identificados.
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar
|
||||
- **Contraste teal:** Opción B — `#43BBC8` queda SOLO para decoración (fondos,
|
||||
bordes, barras, elementos gráficos). Cuando se usa como COLOR DE TEXTO, reemplazar
|
||||
por `#494F4F` (carbón de marca). No crear variante oscura del teal.
|
||||
- **Mobile bottombar:** flex-wrap en el contenedor, no width fijo en los botones.
|
||||
- **Submit sin try/catch:** agregar manejo de error de red que muestre el estado
|
||||
'error' propio del wizard, no la pantalla cruda de React.
|
||||
- **Autocomplete touch:** agregar `onTouchEnd` como fallback junto al `onMouseDown`
|
||||
existente — no reemplazarlo.
|
||||
- Sin librerías nuevas en ningún fix.
|
||||
|
||||
## 4. Tareas en orden
|
||||
|
||||
### P1 — Contraste teal como texto (CRÍTICO — sistemático)
|
||||
En `app/globals.css`, reemplazar `color: var(--color-re-teal)` por
|
||||
`color: #494F4F` en estas clases específicas:
|
||||
- `.btn-secondary` (botón "Anterior") — línea ~380
|
||||
- `.wizard-section-number` ("Sección X de Y") — línea ~569
|
||||
- `.summary-code` — línea ~431
|
||||
- `.barrio-autocomplete-item--active` (texto del ítem activo) — línea ~319
|
||||
- `.consent-required-badge` — línea ~490
|
||||
|
||||
NO tocar usos de `var(--color-re-teal)` como `background-color`, `border-color`,
|
||||
`fill`, o en las clases `.db-*` del dashboard — esos son usos decorativos correctos.
|
||||
|
||||
Verificar después del cambio que no quedó ningún `color: var(--color-re-teal)` en
|
||||
clases del wizard (grep en globals.css sección wizard):
|
||||
```bash
|
||||
grep -n "color: var(--color-re-teal)" app/globals.css
|
||||
```
|
||||
Los únicos resultados aceptables son en sección `.db-*` del dashboard.
|
||||
|
||||
### P2 — Submit sin try/catch (red se cae al enviar)
|
||||
En `components/wizard-form.tsx`, función `handleSubmit` (líneas ~212-226):
|
||||
Envolver el `await submitSurvey(...)` en try/catch. Si el catch recibe un error
|
||||
de red (fetch failed, network error), setear el estado del wizard a 'error' con
|
||||
mensaje accionable: "No pudimos enviar tu respuesta. Verificá tu conexión e
|
||||
intentá de nuevo." El botón de reintento ya existe en el estado 'error'
|
||||
(wizard-form.tsx:136-152) — solo hay que llegar a ese estado correctamente.
|
||||
No crear `app/error.tsx` — el estado de error propio del wizard es suficiente.
|
||||
|
||||
### P3 — Botones bottombar en mobile ≤480px
|
||||
En `app/globals.css`, sección `.wizard-bottombar` (~línea 653):
|
||||
Agregar `flex-wrap: wrap` al contenedor `.wizard-bottombar`.
|
||||
En el breakpoint `@media (max-width: 480px)`:
|
||||
- `.wizard-bottombar` → `flex-direction: column-reverse` (Siguiente arriba, Anterior abajo)
|
||||
- `.btn` dentro del bottombar → `width: 100%`
|
||||
- El span hint entre los botones → `order: 3` para que quede al final
|
||||
`column-reverse` porque el botón primario (Siguiente/Enviar) debe estar arriba
|
||||
en mobile — el pulgar llega más fácil al centro que a la base.
|
||||
|
||||
### P4 — Autocomplete touch frágil
|
||||
En `components/widgets/barrio-autocomplete-field.tsx` y
|
||||
`components/widgets/ciudad-select-field.tsx`:
|
||||
En el handler de selección de ítem del dropdown, agregar `onTouchEnd` junto
|
||||
al `onMouseDown` existente (no reemplazarlo). El `onTouchEnd` debe llamar
|
||||
`e.preventDefault()` antes de ejecutar la selección para evitar el blur
|
||||
sintético que cierra la lista antes de que se procese la selección.
|
||||
|
||||
## 5. DoD antes de reportar
|
||||
- [ ] `git log --name-only -1` — listar todos los archivos tocados
|
||||
- [ ] grep de verificación P1:
|
||||
```bash
|
||||
grep -n "color: var(--color-re-teal)" app/globals.css
|
||||
```
|
||||
Solo resultados en sección `.db-*`
|
||||
- [ ] Build sin errores: `npm run build`
|
||||
- [ ] P2 verificado: en `handleSubmit`, el catch existe y setea estado 'error'
|
||||
con mensaje accionable (pegar el bloque try/catch completo en el reporte)
|
||||
- [ ] P3 verificado: describir el CSS resultante del bottombar en mobile
|
||||
- [ ] P4 verificado: describir el handler resultante de selección en autocomplete
|
||||
- [ ] Commit: `fix(quality): contraste teal, submit red, bottombar mobile, autocomplete touch`
|
||||
- [ ] Push a Gitea
|
||||
|
||||
## 6. Qué reportar
|
||||
```
|
||||
P1 — Contraste:
|
||||
- Clases modificadas: [lista con línea antes → después]
|
||||
- grep verificación: [output]
|
||||
|
||||
P2 — Submit try/catch:
|
||||
- Bloque handleSubmit completo después del fix: [código]
|
||||
|
||||
P3 — Bottombar mobile:
|
||||
- CSS resultante de .wizard-bottombar y el breakpoint: [código]
|
||||
|
||||
P4 — Autocomplete touch:
|
||||
- Handler de selección resultante (onMouseDown + onTouchEnd): [código]
|
||||
|
||||
Verificado:
|
||||
- Build: ✓/✗
|
||||
- git log --name-only -1: [lista]
|
||||
- Commit: [hash]
|
||||
```
|
||||
|
||||
## 7. Qué NO hacer
|
||||
- ❌ No tocar `var(--color-re-teal)` en usos decorativos (background, border, fill)
|
||||
- ❌ No tocar clases `.db-*` del dashboard
|
||||
- ❌ No crear `app/error.tsx`
|
||||
- ❌ No reemplazar `onMouseDown` — agregar `onTouchEnd` junto a él
|
||||
- ❌ No agregar librerías nuevas
|
||||
- ❌ No hacer deploy — solo commit y push a Gitea
|
||||
- ❌ No mezclar con los fixes rápidos de la etapa anterior
|
||||
|
||||
## 8. Versionado
|
||||
```
|
||||
fix(quality): contraste teal, submit red, bottombar mobile, autocomplete touch
|
||||
|
||||
P1: color de texto teal → #494F4F en 5 clases del wizard (WCAG AA).
|
||||
P2: try/catch en handleSubmit — error de red muestra estado error del wizard.
|
||||
P3: flex-wrap + column-reverse en bottombar ≤480px.
|
||||
P4: onTouchEnd fallback en autocomplete barrio y ciudad.
|
||||
```
|
||||
89
sprints/sprint-5/ETAPA_E2E_PROMPT.md
Normal file
89
sprints/sprint-5/ETAPA_E2E_PROMPT.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# ETAPA E2E — Tests Playwright: verificación deploy Sprint 5
|
||||
|
||||
## Antes de tocar código, leé:
|
||||
1. `CLAUDE.md`
|
||||
2. `docs/estado/ESTADO_PROYECTO_26JUN2026.md` — estado general del proyecto
|
||||
|
||||
Confirmame el plan antes de implementar.
|
||||
|
||||
## Credenciales de test (usuario dedicado — nunca las de Marcos)
|
||||
- URL: `https://economia-cuidado.inqualityhq.com`
|
||||
- Email: `test-e2e@inquality.com.py`
|
||||
- Password: `TestE2E2026!#Cuidado`
|
||||
- Rol: `platform_admin`, org: `InQuality`
|
||||
|
||||
Estas credenciales van en `.env.test` en la raíz (agregar a `.gitignore` si no está).
|
||||
**Nunca hardcodeadas en los specs.**
|
||||
|
||||
## Verificar Playwright antes de instalar nada
|
||||
|
||||
```bash
|
||||
npx playwright --version 2>/dev/null && echo "instalado" || echo "no instalado"
|
||||
ls ~/.cache/ms-playwright 2>/dev/null && echo "browsers presentes" || echo "browsers ausentes"
|
||||
```
|
||||
|
||||
Reportar resultado antes de continuar. Si ya está instalado con browsers, agregar solo
|
||||
`@playwright/test` como devDependency sin correr `npx playwright install`.
|
||||
Si no está: `npx playwright install --with-deps chromium` (solo Chromium).
|
||||
|
||||
## Los 5 tests a implementar
|
||||
|
||||
**Archivo:** `tests/e2e/sprint5-verificacion.spec.ts`
|
||||
|
||||
### Test 1 — Login y redirect
|
||||
- Abrir `/admin/responses` sin sesión
|
||||
- Verificar redirect a `/login`
|
||||
- El DOM de `/login` contiene el campo `email`, el campo `password`, y el botón de toggle
|
||||
de contraseña (buscar `aria-label` que contenga "contraseña")
|
||||
|
||||
### Test 2 — Login funcional
|
||||
- Hacer login con las credenciales de test en `/login`
|
||||
- Verificar redirect a `/admin/responses`
|
||||
- El DOM contiene `<select>` para filtro de survey (no links hardcodeados)
|
||||
- El DOM contiene al menos una fila de respuesta (hay 50 sembradas)
|
||||
|
||||
### Test 3 — Vista detallada de respuesta
|
||||
- Desde `/admin/responses`, hacer click en el primer link "Ver" disponible
|
||||
- Verificar que la URL cambia a `/admin/responses/[uuid]`
|
||||
- El DOM contiene al menos una sección (buscar texto que indique agrupación por sección)
|
||||
- No hay error 404 ni texto "Respuesta no encontrada"
|
||||
|
||||
### Test 4 — Dashboard con datos
|
||||
- Navegar a `/dashboard` (o la ruta del dashboard — verificar cuál es)
|
||||
- Verificar que al menos un widget muestra un porcentaje (buscar `%` en el DOM)
|
||||
- No aparece "Datos insuficientes" en todos los widgets (puede aparecer en algunos
|
||||
segmentos con k<5, pero no en todos)
|
||||
|
||||
### Test 5 — Formulario público accesible sin auth
|
||||
- Navegar a `/` o `/encuesta` (verificar ruta correcta del formulario)
|
||||
- Verificar que NO redirige a `/login` (el formulario es público)
|
||||
- El DOM contiene el wizard o la pantalla de consentimiento
|
||||
|
||||
## Decisiones ya tomadas
|
||||
- Playwright en local apuntando a producción, no en CI (backlog)
|
||||
- `playwright.config.ts` en raíz, `testDir: 'tests/e2e'`
|
||||
- `.env.test` para credenciales, en `.gitignore`
|
||||
- Si un test falla, reportar el error exacto y consultarme — no intentar workarounds
|
||||
|
||||
## Archivos autorizados
|
||||
`playwright.config.ts`, `tests/e2e/sprint5-verificacion.spec.ts`, `.env.test`
|
||||
(no se commitea), `package.json` (devDependency), `package-lock.json`.
|
||||
Cualquier otro en `git diff --name-only HEAD` → revertir y reportar.
|
||||
|
||||
## DoD antes de reportar
|
||||
|
||||
```bash
|
||||
npx playwright test tests/e2e/sprint5-verificacion.spec.ts --reporter=list
|
||||
git log --name-only -1
|
||||
git diff --name-only HEAD # solo archivos autorizados, sin .env.test
|
||||
grep -r "TestE2E2026" . # debe dar vacío — contraseña no commiteada
|
||||
```
|
||||
|
||||
## Qué reportar
|
||||
- Resultado de checks de Playwright previos (versión, browsers)
|
||||
- Resultado de cada test: pass/fail con mensaje exacto si falla
|
||||
- Ruta real del dashboard y del formulario público (las que encontró en el código)
|
||||
- `git log --name-only -1` y `git diff --name-only HEAD`
|
||||
- Hallazgos no esperados
|
||||
|
||||
**No hacer deploy ni modificar nada en producción. No continuar sin mi OK si algo falla.**
|
||||
47
sprints/sprint-5/ETAPA_FIXES_RAPIDOS_PROMPT.md
Normal file
47
sprints/sprint-5/ETAPA_FIXES_RAPIDOS_PROMPT.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# ETAPA — Fixes rápidos: toggle password + card Talento en riesgo
|
||||
|
||||
## 1. Objetivo
|
||||
Dos fixes visuales independientes y quirúrgicos. Scope estricto.
|
||||
|
||||
## 2. DECISIONES YA TOMADAS — no consultar
|
||||
- Toggle de contraseña: solo en el campo password del login, sin librería nueva.
|
||||
- Card Talento en riesgo: eliminar el gradiente, quedar igual que los demás cards
|
||||
(background blanco, sin distinción especial de fondo).
|
||||
|
||||
## 3. Tareas
|
||||
|
||||
### Fix A — Toggle "ver contraseña" en /login
|
||||
En `app/login/page.tsx`:
|
||||
- Agregar estado `showPassword` (useState).
|
||||
- El input password alterna entre `type="password"` y `type="text"`.
|
||||
- Botón inline dentro del campo (position absolute o flex) con ícono SVG
|
||||
simple de ojo (inline, sin librería). Accesible: `aria-label="Mostrar contraseña"` /
|
||||
`aria-label="Ocultar contraseña"` según estado. `type="button"` para no submitear.
|
||||
- Estilo consistente con el formulario existente.
|
||||
|
||||
### Fix B — Card Talento en riesgo sin gradiente
|
||||
En `app/globals.css`, clase `.db-widget-star`:
|
||||
- Eliminar la propiedad `background` (el gradiente actual).
|
||||
- El card hereda `background: #FFFFFF` de `.db-widget` — mismo que los demás.
|
||||
- NO tocar `border-color: var(--color-re-teal)` — mantener el borde distintivo.
|
||||
- NO tocar `.db-widget-star .db-widget-title` — mantener el título en teal.
|
||||
|
||||
## 4. DoD antes de reportar
|
||||
- [ ] `git log --name-only -1` — solo `app/login/page.tsx` y `app/globals.css`
|
||||
- [ ] Toggle password funciona: click muestra/oculta, aria-label cambia
|
||||
- [ ] Card Talento en riesgo: fondo blanco igual que los demás cards
|
||||
- [ ] Build sin errores: `npm run build`
|
||||
- [ ] Commit: `fix(ui): toggle password en login + fondo blanco card talento`
|
||||
- [ ] Push a Gitea
|
||||
|
||||
## 5. Qué reportar
|
||||
- `git log --name-only -1`
|
||||
- Línea exacta eliminada en `.db-widget-star`
|
||||
- Build: ✓/✗
|
||||
|
||||
## 6. Qué NO hacer
|
||||
- ❌ No tocar otros campos del formulario de login
|
||||
- ❌ No tocar otros widgets del dashboard
|
||||
- ❌ No agregar librerías de iconos
|
||||
- ❌ No modificar el borde teal ni el título teal del card estrella
|
||||
- ❌ No hacer deploy — solo commit y push
|
||||
106
sprints/sprint-5/INSUMOS_AUDIT_LOG.md
Normal file
106
sprints/sprint-5/INSUMOS_AUDIT_LOG.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# INSUMOS — Etapa Audit Log append-only (Sprint compliance, SEC-002)
|
||||
|
||||
> Material de contexto para que Claude (Projects, arquitecto) redacte el ETAPA_PROMPT.md
|
||||
> de esta etapa. No es el prompt en sí — es lo que el arquitecto necesita para escribirlo
|
||||
> bien, sin tener que releer todo el repo.
|
||||
|
||||
---
|
||||
|
||||
## 1. Por qué esta etapa, y por qué este alcance
|
||||
|
||||
`BACKLOG.md` (SEC-002) y `COMPLIANCE.md` §5.1 piden, dentro del sprint de compliance,
|
||||
un "registro de auditoría append-only (log inmutable de operaciones sensibles)". El
|
||||
director priorizó esta pieza primero, antes de hashing de integridad, trazabilidad de
|
||||
cambios o cifrado en reposo (esas quedan para etapas futuras del mismo sprint).
|
||||
|
||||
COMPLIANCE.md §5.1 pide loggear 3 cosas: accesos a datos sensibles, cambios de
|
||||
configuración, y ejecución de migraciones. Evalué las 3 y propongo **acotar el alcance
|
||||
de esta etapa a 2 de las 3**:
|
||||
|
||||
1. **Migraciones (DDL)** — factible con un event trigger nativo de Postgres
|
||||
(`ddl_command_end`), sin tocar el flujo de deploy.
|
||||
2. **Cambios de configuración** — triggers `AFTER INSERT/UPDATE/DELETE` en
|
||||
`user_roles` (altas/bajas/cambios de `app_role`/`org_id`) y en `surveys` cuando
|
||||
cambia `status` (activación/desactivación de encuestas).
|
||||
3. **Acceso a datos sensibles — propongo DEFERIR explícitamente.** Verificado en
|
||||
COMPLIANCE.md §3.2: hoy ningún camino de código (ni `/admin/responses` ni las RPCs
|
||||
`rpc_me_*`) lee columnas `is_sensitive=true`. Instrumentar "acceso" sin que exista
|
||||
un acceso real sería simular cobertura. Cuando se construya el primer camino que sí
|
||||
lea sensibles, ESE PR debe agregar su propio log de acceso junto con el código que
|
||||
lo necesita — no antes.
|
||||
|
||||
El arquitecto puede confirmar o ajustar este recorte; lo presento como punto de partida,
|
||||
no como cerrado (esto todavía no se aprobó con el director, se está armando el prompt).
|
||||
|
||||
---
|
||||
|
||||
## 2. Decisiones de diseño que ya tienen forma (para que el prompt las fije)
|
||||
|
||||
- **Tabla nueva:** `public.audit_log` — append-only real:
|
||||
- Ningún rol (`anon`, `authenticated`, ni `service_role` vía API) tiene
|
||||
`INSERT`/`UPDATE`/`DELETE` directo. Solo escriben funciones `SECURITY DEFINER`
|
||||
de los triggers, propiedad de `postgres` (mismo patrón que
|
||||
`custom_access_token_hook`, ver `supabase/migrations/20260627000001_auth_user_roles.sql`).
|
||||
- Trigger `BEFORE UPDATE OR DELETE ON audit_log` que aborta con excepción — segunda
|
||||
barrera, defensa en profundidad (no a prueba de un operador con `psql -U postgres`
|
||||
en el VPS — ver gap conocido abajo).
|
||||
- Lectura: RLS, solo `platform_admin` (reusar `current_app_role()`, ver
|
||||
`supabase/migrations/20260531000003_rls_policies.sql:17`).
|
||||
- **Columnas sugeridas (a validar por el arquitecto):** `id bigserial pk`,
|
||||
`occurred_at timestamptz default now()`, `actor_role text`, `actor_user_id uuid`,
|
||||
`action text`, `target_table text`, `target_id text`, `detail jsonb`.
|
||||
- **Gap conocido a documentar, no a resolver en esta etapa:** esto no protege contra
|
||||
`TRUNCATE`/`DROP` ejecutado por un superusuario directo en el VPS — mismo gap ya
|
||||
registrado en `SEC-003` (BACKLOG.md). Append-only a nivel de aplicación/RLS, no a
|
||||
prueba de acceso root a la base.
|
||||
|
||||
---
|
||||
|
||||
## 3. Convenciones del repo que el prompt debe respetar
|
||||
|
||||
- **Migraciones:** `supabase/migrations/`, naming `YYYYMMDDHHMMSS_descripcion.sql`.
|
||||
Última migración aplicada: `20260627000001_auth_user_roles.sql`. La próxima debe
|
||||
fecharse después de esa.
|
||||
- **Patrón SECURITY DEFINER + GRANT EXECUTE puntual:** ver
|
||||
`custom_access_token_hook` en `20260627000001_auth_user_roles.sql` — función
|
||||
`SECURITY DEFINER STABLE`, `GRANT EXECUTE ... TO supabase_auth_admin` (rol
|
||||
específico, no `PUBLIC`). Mismo patrón a replicar para las funciones de trigger
|
||||
de `audit_log`.
|
||||
- **Helpers RLS existentes a reusar, no reinventar:** `public.current_app_role()`
|
||||
y `public.current_org_id()` (`20260531000003_rls_policies.sql:17-30`).
|
||||
- **Verificación obligatoria antes de aplicar en VPS:** mostrar el SQL final completo
|
||||
y esperar OK explícito del director antes de correrlo en producción (ya es práctica
|
||||
establecida en este proyecto — ver incidente VPS del 27 jun, recuperación documentada
|
||||
en `docs/estado/ESTADO_PROYECTO_JUNIO_2026.md` §2.1).
|
||||
- **Formato esperado del archivo de etapa:** seguir la estructura de
|
||||
`sprints/sprint-4/ETAPA_AUTH_PROMPT.md` o `ETAPA_4G_PROMPT.md` — Objetivo,
|
||||
Decisiones ya tomadas (no consultar), Tareas, DoD, Qué reportar (datos concretos,
|
||||
no "funcionó"), Qué NO hacer, Versionado (mensaje de commit sugerido con tag
|
||||
`[compliance]` o similar).
|
||||
|
||||
---
|
||||
|
||||
## 4. Qué NO debería pedir el prompt (fuera de alcance de esta etapa)
|
||||
|
||||
- Hashing de integridad SHA-256 (siguiente pieza del mismo sprint, etapa separada).
|
||||
- Cifrado en reposo (es trabajo de VPS/infraestructura, no de migración SQL).
|
||||
- Trazabilidad histórica completa de cambios de datos (triggers de historial / WAL) —
|
||||
etapa separada también.
|
||||
- Logging de acceso a datos sensibles (ver §1, punto 3 — deferido a cuando exista
|
||||
código que lea sensibles).
|
||||
- pgaudit o cualquier extensión nueva — no está en el Supabase self-hosted actual,
|
||||
agregar una extensión es Nivel 3 aparte (afecta infraestructura/dependencias).
|
||||
|
||||
---
|
||||
|
||||
## 5. Referencias
|
||||
|
||||
- `COMPLIANCE.md` §5 (roadmap auditabilidad), §5.2 (por qué no blockchain), §3.2
|
||||
(qué es sensible y cómo se protege hoy).
|
||||
- `BACKLOG.md` → SEC-002, SEC-003 (gap de TRUNCATE), SEC-004 (bypass REST, contexto
|
||||
de otro riesgo residual ya aceptado, mismo espíritu de "documentar en vez de
|
||||
sobre-construir").
|
||||
- `TECH_DEBT.md` → TECH-DEBT-008 (resuelto 27 jun, ejemplo de cómo se cierra un ítem).
|
||||
- `docs/estado/ESTADO_PROYECTO_JUNIO_2026.md` — estado general post-release v3.0.0
|
||||
+ incidente VPS, para que el arquitecto tenga el panorama completo antes de escribir
|
||||
el prompt.
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Activar required en todas las preguntas del formulario de cuidadores
|
||||
-- excepto barrio (2.3) y secciones (type = 'section')
|
||||
UPDATE public.questions
|
||||
SET required = true
|
||||
WHERE survey_id = '30000000-0000-0000-0000-000000000001'
|
||||
AND code != '2.3'
|
||||
AND type != 'section';
|
||||
|
||||
-- Verificar: debe mostrar todas las preguntas con required=true excepto 2.3
|
||||
SELECT code, type, required
|
||||
FROM public.questions
|
||||
WHERE survey_id = '30000000-0000-0000-0000-000000000001'
|
||||
AND type != 'section'
|
||||
ORDER BY position;
|
||||
70
tests/e2e/sprint5-verificacion.spec.ts
Normal file
70
tests/e2e/sprint5-verificacion.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { test, expect, type Page, type BrowserContext } from '@playwright/test'
|
||||
|
||||
// Credenciales SIEMPRE desde .env.test (playwright.config.ts las carga) — nunca acá.
|
||||
const EMAIL = process.env.E2E_EMAIL!
|
||||
const PASSWORD = process.env.E2E_PASSWORD!
|
||||
|
||||
// Survey Demo — la única con datos sembrados (51 respuestas, ver
|
||||
// docs/estado/ESTADO_PROYECTO_26JUN2026.md §4). Necesaria como ?s= en el
|
||||
// formulario público: con 3 surveys en `live`, `/` sin parámetro es ambiguo
|
||||
// (app/page.tsx usa .maybeSingle() sobre status='live').
|
||||
const DEMO_SURVEY_ID = '30000000-0000-0000-0000-000000000001'
|
||||
|
||||
test.describe.serial('Sprint 5 — verificación post-deploy', () => {
|
||||
let context: BrowserContext
|
||||
let page: Page
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await context.close()
|
||||
})
|
||||
|
||||
test('1. /admin/responses sin sesión redirige a /login', async () => {
|
||||
await page.goto('/admin/responses')
|
||||
await expect(page).toHaveURL(/\/login/)
|
||||
await expect(page.getByRole('textbox', { name: 'Email' })).toBeVisible()
|
||||
await expect(page.getByRole('textbox', { name: 'Contraseña' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /contraseña/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('2. login funcional redirige a /admin/responses con filtro y datos', async () => {
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(EMAIL)
|
||||
await page.getByRole('textbox', { name: 'Contraseña' }).fill(PASSWORD)
|
||||
await page.getByRole('button', { name: 'Ingresar' }).click()
|
||||
await expect(page).toHaveURL(/\/admin\/responses/)
|
||||
await expect(page.locator('select.admin-survey-select')).toBeVisible()
|
||||
await expect(page.locator('table.admin-table tbody tr').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('3. vista detallada de una respuesta', async () => {
|
||||
await page.locator('a.admin-action-link', { hasText: 'Ver' }).first().click()
|
||||
await expect(page).toHaveURL(/\/admin\/responses\/[0-9a-f-]{36}/)
|
||||
await expect(page.locator('.admin-detail-section').first()).toBeVisible()
|
||||
await expect(page.getByText('Respuesta no encontrada')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('4. dashboard muestra datos reales (no todo suprimido por k<5)', async () => {
|
||||
await page.goto('/dashboard')
|
||||
await expect(page.locator('.db-widget').first()).toBeVisible()
|
||||
await expect(page.locator('body')).toContainText('%')
|
||||
const totalWidgets = await page.locator('.db-widget').count()
|
||||
const suppressed = await page.locator('.db-suppressed').count()
|
||||
expect(totalWidgets).toBeGreaterThan(0)
|
||||
expect(suppressed).toBeLessThan(totalWidgets)
|
||||
})
|
||||
|
||||
test('5. formulario público accesible sin auth', async ({ browser }) => {
|
||||
const anonContext = await browser.newContext()
|
||||
const anonPage = await anonContext.newPage()
|
||||
await anonPage.goto(`/?s=${DEMO_SURVEY_ID}`)
|
||||
await expect(anonPage).not.toHaveURL(/\/login/)
|
||||
await expect(
|
||||
anonPage.locator('.consent-container, .wizard-shell').first()
|
||||
).toBeVisible()
|
||||
await anonContext.close()
|
||||
})
|
||||
})
|
||||
211
tests/e2e/sprint6-usabilidad.spec.ts
Normal file
211
tests/e2e/sprint6-usabilidad.spec.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { test, expect, type BrowserContext, type Page } from '@playwright/test'
|
||||
|
||||
const DEMO_SURVEY_ID = '30000000-0000-0000-0000-000000000001'
|
||||
const SURVEY_URL = `/?s=${DEMO_SURVEY_ID}`
|
||||
|
||||
// Devuelve el código de la pregunta visible (ej: '2.3'), o null si estamos en una sección.
|
||||
async function currentCode(page: Page): Promise<string | null> {
|
||||
const el = page.locator('.wizard-prompt-code')
|
||||
if (await el.count() === 0) return null
|
||||
return (await el.textContent())?.trim() ?? null
|
||||
}
|
||||
|
||||
// Navega hacia adelante hasta que la pregunta con `targetCode` sea visible.
|
||||
// Maneja preguntas required (Q1.1) y screens de sección intermedias.
|
||||
async function navigateUntil(page: Page, targetCode: string, maxSteps = 30): Promise<void> {
|
||||
for (let i = 0; i < maxSteps; i++) {
|
||||
if (await currentCode(page) === targetCode) return
|
||||
// Si hay error de validación (required sin respuesta), responder primero
|
||||
if (await page.locator('.field-error').count() > 0) {
|
||||
await page.locator('.option-label').first().click()
|
||||
await page.waitForTimeout(150)
|
||||
continue
|
||||
}
|
||||
await page.getByRole('button', { name: 'Siguiente' }).click()
|
||||
// Esperar a que React aplique el state update y re-renderice
|
||||
await page.waitForTimeout(150)
|
||||
}
|
||||
const code = await currentCode(page)
|
||||
if (code !== targetCode) throw new Error(`navigateUntil: llegué a ${code}, esperaba ${targetCode}`)
|
||||
}
|
||||
|
||||
// Acepta el consent y espera a que el wizard esté visible.
|
||||
async function acceptConsent(page: Page): Promise<void> {
|
||||
await page.check('#consent-operativo')
|
||||
await page.getByRole('button', { name: 'Comenzar la encuesta' }).click()
|
||||
await page.locator('.wizard-shell').waitFor()
|
||||
}
|
||||
|
||||
test.describe.serial('Sprint 6 — usabilidad y cache geo', () => {
|
||||
let context: BrowserContext
|
||||
let page: Page
|
||||
const geoRequests: string[] = []
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
// Interceptar requests geo antes de navegar (para Test 1)
|
||||
page.on('request', (req) => {
|
||||
if (req.url().includes('geo_paraguay_full.json')) geoRequests.push(req.url())
|
||||
})
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await context.close()
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Test 1 — Cache geo: un solo fetch para barrio y ciudad
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
test('1. cache geo: un solo fetch para barrio y ciudad', async () => {
|
||||
await page.goto(SURVEY_URL)
|
||||
await acceptConsent(page)
|
||||
|
||||
// Navegar hasta Q2.2 (CiudadSelectField monta → geo request se dispara aquí)
|
||||
await navigateUntil(page, '2.2')
|
||||
expect(await currentCode(page)).toBe('2.2')
|
||||
|
||||
// Avanzar a Q2.3 (BarrioAutocompleteField monta → debe usar cache, no nueva request)
|
||||
await page.getByRole('button', { name: 'Siguiente' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
expect(await currentCode(page)).toBe('2.3')
|
||||
|
||||
// Solo debe haber habido 1 request al JSON de geo (Q2.2 lo cargó, Q2.3 usó el cache)
|
||||
expect(geoRequests.length).toBe(1)
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Test 2 — Estado loading del campo geo
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
test('2. estado loading del campo geo', async () => {
|
||||
// Estamos en Q2.3 (del test anterior). Volver a Q2.2 para observar el estado del campo.
|
||||
await page.getByRole('button', { name: 'Anterior' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
expect(await currentCode(page)).toBe('2.2')
|
||||
|
||||
// CiudadSelectField: input con clase .text-short-input
|
||||
const input = page.locator('input.text-short-input').first()
|
||||
await expect(input).toBeVisible()
|
||||
|
||||
const isDisabled = await input.isDisabled()
|
||||
if (isDisabled) {
|
||||
// Estado loading capturado — verificar placeholder correcto
|
||||
const placeholder = await input.getAttribute('placeholder')
|
||||
expect(placeholder).toBe('Cargando opciones...')
|
||||
}
|
||||
// En cualquier caso, el campo debe estar (o volverse) habilitado
|
||||
await expect(input).toBeEnabled({ timeout: 5000 })
|
||||
|
||||
// Volver a Q2.3 para el siguiente test
|
||||
await page.getByRole('button', { name: 'Siguiente' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
expect(await currentCode(page)).toBe('2.3')
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Test 3 — Texto libre funciona en campo barrio
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
test('3. texto libre funciona en campo barrio', async () => {
|
||||
// Estamos en Q2.3 (BarrioAutocompleteField)
|
||||
expect(await currentCode(page)).toBe('2.3')
|
||||
|
||||
const barrioCampo = page.locator('input.text-short-input')
|
||||
await expect(barrioCampo).toBeVisible()
|
||||
await expect(barrioCampo).toBeEnabled()
|
||||
|
||||
// Escribir un barrio que no existe en el JSON (texto libre)
|
||||
await barrioCampo.fill('Barrio Inexistente XYZ')
|
||||
await expect(barrioCampo).toHaveValue('Barrio Inexistente XYZ')
|
||||
|
||||
// Avanzar — campo es opcional, texto libre debe aceptarse sin error
|
||||
await page.getByRole('button', { name: 'Siguiente' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.field-error')).toHaveCount(0)
|
||||
await expect(page.locator('.wizard-shell')).toBeVisible()
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Test 4 — Draft persiste tras recarga
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
test('4. draft persiste tras recarga', async () => {
|
||||
// Volver a Q2.3 para sobreescribir el draft con un valor reconocible
|
||||
await page.getByRole('button', { name: 'Anterior' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
expect(await currentCode(page)).toBe('2.3')
|
||||
|
||||
const barrioCampo = page.locator('input.text-short-input')
|
||||
await barrioCampo.fill('Barrio Test Draft')
|
||||
await expect(barrioCampo).toHaveValue('Barrio Test Draft')
|
||||
|
||||
// Avanzar — handleNext guarda el draft inmediatamente (no debounce)
|
||||
await page.getByRole('button', { name: 'Siguiente' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Verificar que el draft fue guardado antes de recargar
|
||||
const draftRaw = await page.evaluate(
|
||||
(id) => localStorage.getItem(`survey_draft_${id}`),
|
||||
DEMO_SURVEY_ID,
|
||||
)
|
||||
expect(draftRaw, 'draft debe existir en localStorage antes del reload').not.toBeNull()
|
||||
expect(draftRaw, 'draft debe contener el valor del barrio').toContain('Barrio Test Draft')
|
||||
|
||||
// Recargar
|
||||
await page.reload()
|
||||
|
||||
// Verificar que el draft sobrevive el reload
|
||||
const draftAfterReload = await page.evaluate(
|
||||
(id) => localStorage.getItem(`survey_draft_${id}`),
|
||||
DEMO_SURVEY_ID,
|
||||
)
|
||||
expect(draftAfterReload, 'draft debe persistir después del reload').not.toBeNull()
|
||||
|
||||
await acceptConsent(page)
|
||||
|
||||
// El indicador de restauración es el valor de Q2.3 — navegamos hasta ahí
|
||||
// (navigateUntil funciona desde cualquier pantalla de inicio, incluyendo S1)
|
||||
await navigateUntil(page, '2.3')
|
||||
await expect(page.locator('input.text-short-input')).toHaveValue('Barrio Test Draft')
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Test 5 — Submit con error de red muestra estado error del wizard
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
test('5. submit con error de red muestra estado error del wizard', async ({ browser }) => {
|
||||
const ctx5 = await browser.newContext()
|
||||
const page5 = await ctx5.newPage()
|
||||
|
||||
// Pre-rellenar Q1.1 (única pregunta required) para no bloquearnos al avanzar
|
||||
await page5.goto(SURVEY_URL)
|
||||
await page5.evaluate((id) => {
|
||||
localStorage.setItem(`survey_draft_${id}`, JSON.stringify({ '1.1:default': 'true' }))
|
||||
}, DEMO_SURVEY_ID)
|
||||
|
||||
await acceptConsent(page5)
|
||||
|
||||
// Navegar hasta el end screen: todas las preguntas restantes son opcionales
|
||||
let iters = 0
|
||||
while (
|
||||
(await page5.locator('button', { hasText: 'Enviar encuesta' }).count()) === 0 &&
|
||||
iters < 70
|
||||
) {
|
||||
await page5.getByRole('button', { name: 'Siguiente' }).click()
|
||||
await page5.waitForTimeout(100)
|
||||
iters++
|
||||
}
|
||||
|
||||
await expect(page5.getByRole('button', { name: 'Enviar encuesta' })).toBeVisible()
|
||||
|
||||
// Poner el contexto offline para simular error de red en el submit
|
||||
await page5.context().setOffline(true)
|
||||
|
||||
await page5.getByRole('button', { name: 'Enviar encuesta' }).click()
|
||||
|
||||
// El wizard debe mostrar su propio estado de error (no un React error boundary)
|
||||
await expect(page5.locator('.error-container')).toBeVisible({ timeout: 15000 })
|
||||
await expect(page5.getByText(/Application error/i)).toHaveCount(0)
|
||||
await expect(page5.getByRole('button', { name: /Volver e intentar/i })).toBeVisible()
|
||||
|
||||
await page5.context().setOffline(false)
|
||||
await ctx5.close()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user