Compare commits

24 Commits

Author SHA1 Message Date
markosbenitez
31172cc91d chore(v2.1.0): snapshot de base de datos de producción 2026-06-22 09:29:43 -03:00
markosbenitez
5c17a9846a feat(admin): filtro por empresa + contadores filtrados 2026-06-17 10:01:59 -03:00
markosbenitez
b9a3118c84 Filtro en listado de respuestas admin 2026-06-17 09:25:18 -03:00
markosbenitez
cfd2eb0624 docs: estado del proyecto 11 junio 2026 2026-06-11 16:56:17 -03:00
markosbenitez
edf82bf7fd feat(dashboard): selector de empresa + flag p_all en _dash_responses 2026-06-11 16:35:03 -03:00
markosbenitez
60c9c81224 estado del proyecto 2026-06-10 21:42:14 -03:00
markosbenitez
1bab6874a9 feat(admin): colapsar/expandir submissions + eliminar columna Tipo 2026-06-10 21:41:47 -03:00
markosbenitez
933d00ad58 Etapa 2G: C.8 promovido a estable + AP.10/AP.11 documentados con casos reales 2026-06-10 21:36:33 -03:00
markosbenitez
059dfa071e Etapa 3C parte 2: orgs y surveys piloto InQuality y Fundación Solidaridad
Agrega organizations + surveys para los dos pilotos en espacio de pruebas
real, con su propio set de 44 preguntas (mismo instrumento que el survey
demo, IDs namespaced 02xx/03xx). Survey demo (...0001) intacto.
2026-06-10 21:05:06 -03:00
markosbenitez
2d13982d92 Pin next a 15.3.8 sin caret
Mantiene el estilo de pin exacto que tenía next antes de CVE-2025-66478,
evitando que un futuro npm install resuelva versiones 15.x posteriores
sin pasar por revisión.
2026-06-10 20:49:43 -03:00
markosbenitez
92a7923a26 Con las encuestas para 3 empresas 2026-06-10 16:55:41 -03:00
markosbenitez
af0ec65be5 Documentar deuda técnica: /admin/responses sin autenticación
Registra TECH-DEBT-008 como bloqueante para ampliar la vista de
control a preguntas is_sensitive=true (salud/discapacidad).
2026-06-10 16:30:42 -03:00
markosbenitez
157e108322 Los triggers 1.1, 1.6, 1.13 muestran solo Sí · No (sin "Prefiero no decir")
Al responder "No" en las tres, el formulario salta a 6.4 (no muestra secciones 2-5 ni 6.1-6.3)
La pregunta 6.6 aparece antes del cierre
Las preguntas multiple_choice muestran el hint "Podés marcar más de una opción"
2026-06-10 15:49:05 -03:00
markosbenitez
bf963e8894 Con el listado de encuestas cargadas/respondidas 2026-06-10 08:59:50 -03:00
markosbenitez
2f39ce555c solucionado los build args 2026-06-08 20:44:46 -03:00
markosbenitez
52295a78d0 Con los números de preguntas y secciones 2026-06-08 20:25:23 -03:00
markosbenitez
307a54dabc refactor completo + truncate de base de datos + reingenieria dashboard 2026-06-05 17:13:45 -03:00
markosbenitez
a17e810ea8 Con marca y dashboards 2026-06-05 07:18:10 -03:00
markosbenitez
b813622e30 Con la marca incorporada primera versión 2026-06-04 22:49:25 -03:00
markosbenitez
b1ca0e0795 Validaciones mandatorias — Sección 8 — IDs concretos 2026-06-04 21:52:39 -03:00
markosbenitez
d91ea6b2ff Instrumento v2: 44 preguntas + widget text_short 2026-06-04 14:30:28 -03:00
markosbenitez
10c665d25c Fix build time: env var placeholders + sacar .next del repo
- lib/supabase.ts: ?? placeholders para que el módulo cargue en build
  time sin las env vars; force-dynamic garantiza que nunca se llame
  en build time.
- next.config.ts: env fallbacks vacíos para que Next.js no aborte
  al no encontrar NEXT_PUBLIC_SUPABASE_* en build time.
- .gitignore: agrega .next/ (artefactos de build no van a git).
- git rm --cached .next/**: saca los artefactos ya trackeados.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 21:22:55 -03:00
markosbenitez
df2c33f2d9 Fix para deploy en dokploy 2026-06-02 20:10:02 -03:00
markosbenitez
ad35c46a7a Terminado sprint 2A 2026-06-02 15:50:59 -03:00
180 changed files with 11069 additions and 1198 deletions

BIN
.DS_Store vendored

Binary file not shown.

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
.git
.next
node_modules
.env
.env.*
!.env.example
*.md
sprints/
supabase/
.claude/

View File

@@ -6,3 +6,9 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key-publica>
# NUNCA agregar SUPABASE_SERVICE_ROLE_KEY como variable NEXT_PUBLIC_ ni en el cliente # NUNCA agregar SUPABASE_SERVICE_ROLE_KEY como variable NEXT_PUBLIC_ ni en el cliente
# La service_role bypasa RLS — solo para Edge Functions / backend server-side # La service_role bypasa RLS — solo para Edge Functions / backend server-side
# Usada por app/admin/responses (Server Component) para listar respuestas sin RLS
SUPABASE_SERVICE_ROLE_KEY=
# IP hash — variable server-only (NUNCA NEXT_PUBLIC_)
# Usada para deduplicación anónima. Generá un string aleatorio largo.
IP_HASH_SALT=

3
.gitignore vendored
View File

@@ -14,6 +14,9 @@ supabase/.temp/
# Node # Node
node_modules/ node_modules/
# Next.js build output
.next/
# Supabase — claves y credenciales # Supabase — claves y credenciales
*.key *.key
service_role* service_role*

View File

@@ -1,3 +0,0 @@
{
"pages": {}
}

View File

@@ -1,17 +0,0 @@
{
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [
"static/development/_buildManifest.js",
"static/development/_ssgManifest.js"
],
"rootMainFiles": [],
"rootMainFilesTree": {},
"pages": {
"/_app": []
},
"ampFirstPages": []
}

View File

@@ -1 +0,0 @@
{"encryption.key":"rCrwV8YThn7yWBHzp4+yH4ABfW8goSONtUiZDNhogWc=","encryption.expire_at":1781610552095}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +0,0 @@
{"type": "commonjs"}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"

View File

@@ -1,19 +0,0 @@
globalThis.__BUILD_MANIFEST = {
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [],
"rootMainFiles": [],
"rootMainFilesTree": {},
"pages": {
"/_app": []
},
"ampFirstPages": []
};
globalThis.__BUILD_MANIFEST.lowPriorityFiles = [
"/static/" + process.env.__NEXT_BUILD_ID + "/_buildManifest.js",
,"/static/" + process.env.__NEXT_BUILD_ID + "/_ssgManifest.js",
];

View File

@@ -1,6 +0,0 @@
{
"version": 3,
"middleware": {},
"functions": {},
"sortedMiddleware": []
}

View File

@@ -1 +0,0 @@
self.__REACT_LOADABLE_MANIFEST="{}"

View File

@@ -1 +0,0 @@
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"

View File

@@ -1 +0,0 @@
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}"

View File

@@ -1,5 +0,0 @@
{
"node": {},
"edge": {},
"encryptionKey": "rCrwV8YThn7yWBHzp4+yH4ABfW8goSONtUiZDNhogWc="
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
self.__BUILD_MANIFEST = (function(a){return {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},__routerFilterStatic:a,__routerFilterDynamic:a,sortedPages:["\u002F_app"]}}(void 0));self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()

View File

@@ -1 +0,0 @@
self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()

File diff suppressed because one or more lines are too long

View File

@@ -1,141 +0,0 @@
// Type definitions for Next.js cacheLife configs
declare module 'next/cache' {
export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache'
export {
revalidateTag,
revalidatePath,
unstable_expireTag,
unstable_expirePath,
} from 'next/dist/server/web/spec-extension/revalidate'
export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store'
/**
* Cache this `"use cache"` for a timespan defined by the `"default"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 900 seconds (15 minutes)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 15 minutes, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "default"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"seconds"` profile.
* ```
* stale: 0 seconds
* revalidate: 1 seconds
* expire: 60 seconds (1 minute)
* ```
*
* This cache may be stale on clients for 0 seconds before checking with the server.
* If the server receives a new request after 1 seconds, start revalidating new values in the background.
* If this entry has no traffic for 1 minute it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "seconds"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"minutes"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 60 seconds (1 minute)
* expire: 3600 seconds (1 hour)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 minute, start revalidating new values in the background.
* If this entry has no traffic for 1 hour it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "minutes"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"hours"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 3600 seconds (1 hour)
* expire: 86400 seconds (1 day)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 hour, start revalidating new values in the background.
* If this entry has no traffic for 1 day it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "hours"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"days"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 86400 seconds (1 day)
* expire: 604800 seconds (1 week)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 day, start revalidating new values in the background.
* If this entry has no traffic for 1 week it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "days"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"weeks"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 604800 seconds (1 week)
* expire: 2592000 seconds (30 days)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 week, start revalidating new values in the background.
* If this entry has no traffic for 30 days it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "weeks"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"max"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 2592000 seconds (30 days)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 30 days, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "max"): void
/**
* Cache this `"use cache"` using a custom timespan.
* ```
* stale: ... // seconds
* revalidate: ... // seconds
* expire: ... // seconds
* ```
*
* This is similar to Cache-Control: max-age=`stale`,s-max-age=`revalidate`,stale-while-revalidate=`expire-revalidate`
*
* If a value is left out, the lowest of other cacheLife() calls or the default, is used instead.
*/
export function unstable_cacheLife(profile: {
/**
* This cache may be stale on clients for ... seconds before checking with the server.
*/
stale?: number,
/**
* If the server receives a new request after ... seconds, start revalidating new values in the background.
*/
revalidate?: number,
/**
* If this entry has no traffic for ... seconds it will expire. The next request will recompute it.
*/
expire?: number
}): void
export { cacheTag as unstable_cacheTag } from 'next/dist/server/use-cache/cache-tag'
}

View File

@@ -1 +0,0 @@
{"type": "module"}

67
BACKLOG.md Normal file
View File

@@ -0,0 +1,67 @@
# BACKLOG.md
> Ítems pendientes que no entran en el sprint actual. El agente NO los ejecuta
> sin aprobación del director (Nivel 3 salvo indicación contraria).
---
## Seguridad
### [SEC-001] Actualizar Next.js — CVE pendiente en 15.3.3
- **Estado:** Pendiente
- **Prioridad:** Alta
- **Contexto:** Next.js 15.3.3 tiene un CVE conocido. La versión no se actualizó
en la tarea del 2026-06-02 para no mezclar cambios con el fix de producción de
chromedriver. Actualizar en el próximo ciclo de mantenimiento.
- **Acción:** `npm install next@latest` + verificar que el build y la app sigan
funcionando antes de deployar.
- **Nota:** revisar changelog de Next.js por breaking changes antes de actualizar.
---
## Técnico / Deuda
### [TECH-001] `npm audit` reporta 2 vulnerabilidades en deps de producción
- **Estado:** Pendiente de revisión
- **Contexto:** `npm audit` señala 1 moderate + 1 critical. Evaluar con
`npm audit --json` si afectan el runtime de la app o solo herramientas de build.
- **Acción:** `npm audit fix` (o fix manual) y re-verificar build.
### [TECH-002] Mockup `formulario_mapeo.html` no encontrado en el árbol de trabajo
- **Estado:** Pendiente
- **Contexto:** El BRIEF Sprint 2 lo cita como "contrato visual". El agente diseñó
la UI basándose en SEED_SPEC.md. Requiere validación visual del director contra
el mockup original.
### [TECH-003] Auditoría a11y interactiva pendiente (foco, aria-expanded, widgets)
- **Estado:** Pendiente
- **Contexto:** La auditoría de Etapa 2A se corrió sobre el HTML SSR estático
(jsdom). La verificación de componentes interactivos (foco al abrir bloque
condicional, aria-expanded en triggers, operación solo con teclado) requiere
auditoría en browser real.
- **Acción:** Correr axe-cli con Chrome 149+ (o actualizar Chrome a 149) y
hacer test manual de navegación por teclado.
---
## Features
*(Sprint 3 y posteriores — ver ARCHITECTURE.md y BRIEF por sprint)*
### [BACKLOG-001] Consentimiento inicial editable desde admin
- **Estado:** hoy el texto del consentimiento está hardcoded en `consent-screen.tsx`.
- **Necesidad:** cuando se construya el motor de formularios (builder), el texto del
consentimiento debe ser editable por encuesta desde la administración, para reflejar
cambios en política de privacidad o ajustes específicos por tipo de relevamiento.
- **Bloquea:** implementación del builder visual.
- **Decisión cerrada:** la edición es desde admin, no desde el código.
---
- Dashboard / reportes (Sprint 3)
- Formulario de consentimiento + persistencia de respuestas (Etapa 2B)
- Builder visual de encuestas (post-validación del caso fijo)
- Integración LLM para análisis de texto libre (diferenciador)
- Modo B / controller_role dual (reservado en esquema)
- Columnas `qi_gender` / `qi_modality` en `responses` — decisión Nivel 3 abierta
(ver hallazgo en SEED_SPEC.md)

43
Dockerfile Normal file
View File

@@ -0,0 +1,43 @@
FROM node:20-alpine AS base
# ── Dependencias ──────────────────────────────────────────────────────────────
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# ── Build ─────────────────────────────────────────────────────────────────────
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Las variables NEXT_PUBLIC_* se hornean en el bundle en build time.
# Pasarlas como build args desde Dokploy (Environment → Build Args).
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
RUN npm run build
# ── Runner ────────────────────────────────────────────────────────────────────
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

384
INSTALL_SUPABASE.md Normal file
View File

@@ -0,0 +1,384 @@
# INSTALL_SUPABASE.md — Supabase self-hosted en Dokploy v0.29.4
> Guía operativa basada en la instalación real del proyecto "Datos País sobre el Cuidado" (junio 2026).
> Registra los pasos exactos, errores conocidos y sus soluciones.
---
## Decisiones de arquitectura
- **Supabase self-hosted completo** (Postgres + Auth + Storage + Realtime + Studio + Kong).
- **Partida limpia** — no se migran datos desde Cloud, se aplican las migraciones del repo.
- **Gestión vía Git** — el compose y los volumes viven en un repo dedicado; Dokploy clona ese repo.
Las variables de entorno viven en Dokploy (no en el repo).
- **Backups locales** diarios, retención 30 días. Remoto: TECH-DEBT-006.
- **Studio** no expuesto a internet. Acceso solo vía SSH tunnel.
---
## Requisitos previos
- VPS Ubuntu 22.04+ con Docker 29+ instalado.
- Dokploy v0.29.4 instalado y accesible.
- Repo Git disponible (Gitea o GitHub) con acceso desde el VPS y desde Dokploy.
- Acceso SSH al VPS.
- Supabase CLI en la máquina de desarrollo local.
---
## PARTE 1 — Preparar el repo de Supabase
### 1.1 Descargar el stack oficial en el VPS
```bash
curl -fsSL https://supabase.link/setup.sh | sh
```
Crea `~/supabase-project/` con esta estructura:
```
~/supabase-project/
├── docker-compose.yml
├── volumes/
│ ├── api/
│ │ ├── kong.yml <- API gateway (EDITAR: ver 1.2)
│ │ └── kong-entrypoint.sh
│ ├── db/
│ │ ├── _supabase.sql <- CRITICO: crea la base _supabase
│ │ ├── roles.sql
│ │ ├── jwt.sql
│ │ ├── realtime.sql
│ │ ├── pooler.sql
│ │ ├── webhooks.sql
│ │ ├── logs.sql
│ │ └── init/data.sql
│ └── pooler/
│ └── pooler.exs
```
> Estos archivos son los scripts de inicializacion que Postgres ejecuta la primera vez.
> Sin ellos la base _supabase no se crea y el stack no funciona.
> Por eso NO se puede usar "docker raw" en Dokploy — ese modo no tiene contexto del directorio.
### 1.2 Ajustar kong.yml
El kong.yml define las credenciales del API gateway. Verificar que use los mismos
nombres de variables que se definen en Dokploy:
```bash
grep -n "keyauth" ~/supabase-project/volumes/api/kong.yml
```
Deben quedar dos bloques con UNA sola entrada cada uno:
```yaml
keyauth_credentials:
- key: $ANON_KEY # nombre exacto de la variable en Dokploy
keyauth_credentials:
- key: $SERVICE_ROLE_KEY # nombre exacto de la variable en Dokploy
```
ERROR comun: si los dos bloques resuelven al mismo valor JWT, Kong falla con
"uniqueness violation". Verificar que ANON_KEY y SERVICE_ROLE_KEY son distintos.
### 1.3 Crear .gitignore
```bash
cat > ~/supabase-project/.gitignore << 'EOF'
.env
.env.local
volumes/db/data/
volumes/storage/
volumes/logs/
*.log
EOF
```
### 1.4 Push al repo Git
```bash
cd ~/supabase-project
git init
git add docker-compose.yml volumes/api/ volumes/db/ volumes/pooler/ .gitignore
git commit -m "chore: supabase self-hosted stack inicial"
git remote add origin https://TU_GIT/usuario/supabase-repo.git
git push -u origin master
```
El .env con los secrets NO se sube (esta en .gitignore).
---
## PARTE 2 — Generar los secrets
Usar siempre openssl rand -hex (solo hex, sin caracteres especiales que rompan el escaping de Docker).
### 2.1 Valores base (en el VPS)
```bash
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"
echo "JWT_SECRET=$(openssl rand -hex 40)"
echo "DASHBOARD_PASSWORD=$(openssl rand -hex 16)"
echo "SECRET_KEY_BASE=$(openssl rand -hex 40)"
echo "VAULT_ENC_KEY=$(openssl rand -hex 20)"
echo "PG_META_CRYPTO_KEY=$(openssl rand -hex 24)"
```
Guardar todos en un gestor de contrasenas antes de continuar.
### 2.2 Generar ANON_KEY y SERVICE_ROLE_KEY
Con el JWT_SECRET generado, usar Node.js en el VPS:
```bash
node -e "
const crypto = require('crypto');
const secret = 'PEGAR_AQUI_JWT_SECRET';
function b64url(obj) {
return Buffer.from(JSON.stringify(obj)).toString('base64url');
}
function makeJWT(role) {
const h = b64url({alg:'HS256',typ:'JWT'});
const p = b64url({role,iss:'supabase',iat:1700000000,exp:2000000000});
const sig = crypto.createHmac('sha256',secret).update(h+'.'+p).digest('base64url');
return h+'.'+p+'.'+sig;
}
console.log('ANON_KEY='+makeJWT('anon'));
console.log('SERVICE_ROLE_KEY='+makeJWT('service_role'));
"
```
---
## PARTE 3 — Configurar el servicio en Dokploy
### 3.1 Crear el servicio
1. Projects -> seleccionar proyecto -> Create Service -> Docker Compose.
2. Provider: Gitea (o GitHub) -> seleccionar el repo del paso 1.4.
3. Branch: master | Compose Path: ./docker-compose.yml
4. IMPORTANTE: desactivar Autodeploy (toggle OFF).
### 3.2 Variables de entorno (pestaña Environment)
Pegar el bloque completo reemplazando los valores:
```env
POSTGRES_PASSWORD=<openssl rand -hex 24>
POSTGRES_HOST=db
POSTGRES_DB=postgres
POSTGRES_PORT=5432
JWT_SECRET=<openssl rand -hex 40>
JWT_EXPIRY=3600
ANON_KEY=<JWT anon>
SERVICE_ROLE_KEY=<JWT service_role>
SUPABASE_ANON_KEY=<igual a ANON_KEY>
SUPABASE_SERVICE_KEY=<igual a SERVICE_ROLE_KEY>
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=<openssl rand -hex 16>
KONG_HTTP_PORT=8000
KONG_HTTPS_PORT=8443
SECRET_KEY_BASE=<openssl rand -hex 40>
VAULT_ENC_KEY=<openssl rand -hex 20>
PG_META_CRYPTO_KEY=<openssl rand -hex 24>
SITE_URL=http://localhost:3000
API_EXTERNAL_URL=http://localhost:8000
SUPABASE_PUBLIC_URL=http://localhost:8000
STUDIO_DEFAULT_ORGANIZATION=NombreOrg
STUDIO_DEFAULT_PROJECT=NombreProyecto
SMTP_ADMIN_EMAIL=admin@example.com
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_SENDER_NAME=
ENABLE_EMAIL_SIGNUP=false
ENABLE_EMAIL_AUTOCONFIRM=false
ENABLE_PHONE_SIGNUP=false
ENABLE_PHONE_AUTOCONFIRM=false
DISABLE_SIGNUP=true
ENABLE_ANONYMOUS_USERS=false
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false
MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify
MAILER_URLPATHS_INVITE=/auth/v1/verify
MAILER_URLPATHS_RECOVERY=/auth/v1/verify
MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify
ADDITIONAL_REDIRECT_URLS=
POOLER_TENANT_ID=pooler
POOLER_DEFAULT_POOL_SIZE=20
POOLER_MAX_CLIENT_CONN=100
POOLER_PROXY_PORT_TRANSACTION=6543
POOLER_DB_POOL_SIZE=10
STORAGE_TENANT_ID=default
GLOBAL_S3_BUCKET=
REGION=
S3_PROTOCOL_ACCESS_KEY_ID=
S3_PROTOCOL_ACCESS_KEY_SECRET=
LOGFLARE_LOGGER_BACKEND_API_KEY=logflare-local-key
LOGFLARE_API_KEY=logflare-local-key
FUNCTIONS_VERIFY_JWT=true
IMGPROXY_ENABLE_WEBP_DETECTION=true
IMGPROXY_AUTO_WEBP=true
PGRST_DB_SCHEMAS=public,storage,graphql_public
```
### 3.3 Deploy y validacion
Click en Deploy. Cuando termine verificar:
```bash
docker ps --format "table {{.Names}}\t{{.Status}}" | grep supabase
docker exec supabase-db psql -U postgres -c 'SELECT version();'
curl -s http://localhost:8000/rest/v1/ -H "apikey: <ANON_KEY>" | head -c 200
docker exec supabase-db psql -U postgres -c '\l' | grep supabase
```
Contenedores esperados healthy: supabase-db, supabase-kong, supabase-studio,
supabase-auth, supabase-rest, supabase-storage, supabase-meta, supabase-pooler,
supabase-imgproxy, realtime-dev.supabase-realtime.
supabase-edge-functions puede quedar en Restarting si no se usa Edge Functions (no critico).
---
## PARTE 4 — Aplicar las migraciones del proyecto
El puerto 5432 no esta expuesto externamente. Las migraciones se aplican desde el VPS.
### 4.1 Copiar migraciones al VPS (desde la maquina de desarrollo)
```bash
ssh administrator@<VPS_IP> "mkdir -p ~/migrations"
scp -r supabase/migrations/ administrator@<VPS_IP>:~/migrations/
```
### 4.2 Aplicar en orden
```bash
for f in $(ls ~/migrations/*.sql | sort); do
echo "Aplicando: $f"
docker exec -i supabase-db psql -U postgres -d postgres < "$f"
echo "---"
done
```
### 4.3 Verificar
```bash
docker exec supabase-db psql -U postgres -d postgres -c \
"SELECT tablename, policyname FROM pg_policies WHERE schemaname='public' ORDER BY tablename;"
```
### Nota: auth.jwt() en GoTrue v2.189+
GoTrue v2.189+ elimino auth.jwt(). Las funciones disponibles son auth.uid(), auth.role().
Las politicas que usen custom claims JWT (app_role, org_id) fallaran. Ver TECH-DEBT-007.
No es bloqueante para el piloto porque el dashboard usa service_role que bypasa RLS.
---
## PARTE 5 — Backups locales
### 5.1 Crear el script
```bash
sudo mkdir -p /var/backups/supabase-prod
sudo chmod 700 /var/backups/supabase-prod
sudo tee /usr/local/bin/supabase-backup.sh > /dev/null << 'SCRIPT'
#!/usr/bin/bash
set -euo pipefail
TS=$(date +%Y%m%d_%H%M%S)
OUT=/var/backups/supabase-prod/supabase_$TS.sql.gz
docker exec supabase-db pg_dump -U postgres postgres | gzip > "$OUT"
chmod 600 "$OUT"
find /var/backups/supabase-prod -name "supabase_*.sql.gz" -mtime +30 -delete
echo "Backup OK: $OUT ($(du -h "$OUT" | cut -f1))"
SCRIPT
sudo chmod +x /usr/local/bin/supabase-backup.sh
```
### 5.2 Probar y configurar cron
```bash
sudo bash /usr/local/bin/supabase-backup.sh
sudo crontab -e
# Agregar: 0 3 * * * /usr/bin/bash /usr/local/bin/supabase-backup.sh >> /var/log/supabase-backup.log 2>&1
```
### 5.3 Probar restore (obligatorio)
```bash
docker exec supabase-db createdb -U postgres restore_test
gunzip < $(ls /var/backups/supabase-prod/*.sql.gz | tail -1) | \
docker exec -i supabase-db psql -U postgres restore_test
docker exec supabase-db psql -U postgres -d restore_test -c \
"SELECT count(*) FROM pg_tables WHERE schemaname='public';"
docker exec supabase-db dropdb -U postgres restore_test
```
---
## PARTE 6 — Dominio y SSL (pendiente)
En Dokploy, pestaña Domains del servicio supabase:
- Agregar dominio db.dominio.com -> apuntar a supabase-kong:8000 -> activar SSL.
- Studio NO tiene dominio publico. Acceso solo via SSH tunnel:
ssh -L 3001:localhost:3001 administrator@VPS_IP -> abrir http://localhost:3001
Actualizar variables de entorno:
```
API_EXTERNAL_URL=https://db.dominio.com
SUPABASE_PUBLIC_URL=https://db.dominio.com
```
---
## PARTE 7 — Conectar la app Next.js
Actualizar variables de entorno de la app Next.js en Dokploy:
```
NEXT_PUBLIC_SUPABASE_URL=https://db.dominio.com
NEXT_PUBLIC_SUPABASE_ANON_KEY=<ANON_KEY>
SUPABASE_SERVICE_ROLE_KEY=<SERVICE_ROLE_KEY>
```
---
## Errores conocidos y soluciones
| Error | Causa | Solucion |
|---|---|---|
| password authentication failed for supabase_storage_admin | POSTGRES_PASSWORD con caracteres especiales | Usar openssl rand -hex 24 |
| database "_supabase" does not exist | volumes/db/ no accesible | Usar modo Git en Dokploy, no docker raw |
| keyauth_credentials uniqueness violation | ANON_KEY = SERVICE_ROLE_KEY o duplicados en kong.yml | Una sola entrada por bloque en kong.yml |
| exec: kong-entrypoint.sh is a directory | Volumen corrupto | docker volume rm del volumen afectado |
| GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: converting to bool | Variable vacia | Agregar GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false |
| GOTRUE_SMTP_PORT: converting to int | SMTP_PORT vacio | Agregar SMTP_PORT=587 |
| auth.jwt() does not exist | GoTrue v2.189+ lo elimino | Ver TECH-DEBT-007 |
| Conflicto de nombres de contenedores | Stack manual y Dokploy corriendo en paralelo | docker compose down desde ~/supabase-project |
---
## Deuda tecnica
- TECH-DEBT-006: Backup remoto (S3) — bloqueante antes del primer dato real de empresa.
- TECH-DEBT-007: Migrar auth.jwt() a alternativa compatible con GoTrue v2.189+.
---
## Datos operativos de esta instalacion
- VPS: inq-mart-2 - IP VPN: 172.20.5.11
- Dokploy: https://172.20.5.11 (acceso via VPN)
- Repo Supabase: https://git.inquality.com.py/marcos/supabase-motor-fomularios
- Backups: /var/backups/supabase-prod/ (diarios, retencion 30 dias)
- Studio: ssh -L 3001:localhost:3001 administrator@172.20.5.11 -> http://localhost:3001

89
OBSERVATIONS.md Normal file
View File

@@ -0,0 +1,89 @@
# OBSERVATIONS.md — Observaciones y breaking changes
> Registro de cambios al instrumento que rompen compatibilidad con datos existentes
> o que requieren atención del director antes/después de re-seedear. No bloquean el
> trabajo del agente — se documentan para que el director decida qué hacer con los
> datos ya capturados.
---
## Etapa 3I2 — Revisión del instrumento v2.1 (feedback equipo médico)
### 1. Eliminación de "Prefiero no decir" (Cambio 1)
Afecta: A5, A6, 1.1, 1.5, 1.6, 1.13, 2.5, 3.1, 3.4.
Si existen respuestas guardadas con el valor `"Prefiero no decir"` en alguna de estas
preguntas, esos valores quedan **huérfanos**: ya no aparecen como opción en el
formulario ni en `seed.sql`, pero permanecen en `responses.answers` tal cual fueron
guardados. No se borran ni se migran automáticamente.
**Impacto en triggers (1.1, 1.6, 1.13):** estas preguntas ahora tienen solo 2 opciones
(Sí/No). La lógica condicional (incluyendo el nuevo salto-triple, Cambio 4a) solo
reacciona a `"Sí"`. Una respuesta histórica `"Prefiero no decir"` en un trigger se
trataría como "no Sí" a efectos de la lógica condicional si se re-evaluara, pero no
hay re-evaluación retroactiva de respuestas ya enviadas.
### 2. 2.1 cambia de `single_choice` a `multiple_choice` (Cambio 5)
Respuestas existentes para 2.1 están guardadas como un string único (`"Yo soy el/la
principal"`, etc.). El formulario nuevo espera un array de strings para preguntas
`multiple_choice`. Las respuestas históricas de 2.1 quedan en formato de string,
inconsistente con el nuevo esquema esperado para esa pregunta.
Ver `TECH_DEBT.md` → TECH-DEBT-006 (deuda de ranking pendiente sobre esta misma
pregunta).
### 3. Eliminación de la pregunta 2.4 (Cambio 6)
"¿Contás con apoyo de otras personas para el cuidado?" se elimina del seed por ser
similar/redundante con 2.1 (feedback del equipo médico).
El `DELETE FROM public.questions WHERE survey_id = ...` al inicio de `seed.sql` borra
la fila de la pregunta 2.4 (id `40000000-0000-0000-0000-000000000027`). Si existen
filas en `responses` cuyo `answers` referencian `question_id` = 2.4 (por
`question_id`, no por código embebido en JSON — depende del esquema de `responses`),
esas referencias quedan apuntando a un `question_id` que ya no existe en `questions`
tras el re-seed.
**No re-seedear la base de producción sin que el director confirme que no hay
respuestas reales que dependan de 2.4**, o decidir conservar la fila de 2.4 marcada
como `archived`/inactiva en lugar de borrarla (cambio de diseño — Nivel 3, no
ejecutado en esta etapa).
### 4. Cambio de texto en opción de 3.3 (Cambio 8)
"Es difícil sostenerlo con mis ingresos" → "No puedo sostenerlo con mis ingresos".
Respuestas existentes que eligieron la opción anterior quedan guardadas con el texto
literal `"Es difícil sostenerlo con mis ingresos"`, que ya no coincide con ninguna
opción del seed actual. Cualquier agregación por valor exacto de opción debe
contemplar ambos strings como equivalentes para no perder esos datos en reportes.
### 5. División de la primera franja salarial en 3.4 (Cambio 9)
"Sueldo mínimo Gs. 5.000.000" se reemplaza por dos opciones nuevas: "Sueldo mínimo"
y "Gs. 3.000.000 a 5.000.000".
Respuestas existentes con el valor `"Sueldo mínimo Gs. 5.000.000"` quedan con ese
valor, que ya no existe como opción. No hay forma automática de reasignar esas
respuestas a una de las dos franjas nuevas (la franja exacta del respondente no se
puede inferir del valor agregado anterior).
### 6. Cambio de texto en última opción de 6.1 (Cambio 10)
"No, nunca se ha hablado del tema" → "No, no se puede hablar del tema".
Respuestas existentes con el valor anterior quedan con ese texto literal, que ya no
coincide con ninguna opción del seed actual.
---
## Resumen para el director
- Ningún cambio de esta etapa borra datos directamente, salvo el `DELETE` estándar de
`questions` por `survey_id` (mecanismo de idempotencia ya existente en `seed.sql`,
no nuevo de esta etapa) — pero ese `DELETE` ahora también elimina la fila de 2.4.
- Los cambios 1, 8, 9 y 10 dejan **valores de texto huérfanos** en respuestas
históricas (ya no matchean ninguna opción vigente). No afecta el funcionamiento del
formulario hacia adelante, pero sí cualquier reporte/agregación que dependa de
matchear el string exacto de la opción.
- El cambio 5 (2.1) deja respuestas históricas en un formato (string) distinto al que
el formulario nuevo produce (array). Si se construye un reporte sobre 2.1, debe
manejar ambos formatos o normalizarse en una migración de datos aparte.
- **No se ejecutó ningún re-seed sobre la base real** — esto aplica solo cuando el
director apruebe re-seedear con `seed.sql` v2.1.

View File

@@ -1,105 +1,152 @@
# SEED_SPEC.md — Contrato del instrumento "Mapeo de cuidadores" # SEED_SPEC.md — Contrato del instrumento "Mapeo de Cuidadores" v2.1
> Inventario completo y autoritativo de la encuesta, extraído del mockup real > Versión 2.1 — incorpora feedback del equipo médico, Etapa 3I2 (Jun 2026). Reemplaza la v2.
> (`formulario_mapeo.html`). Reemplaza el seed inventado de la Etapa 1 (que tenía 9 de 34 preguntas y > 44 preguntas (se elimina 2.4, se agrega 6.6) · 8 secciones · Códigos únicos corregidos.
> el supuesto incorrecto C1→G4). Claude Code reescribe `seed.sql` a partir de ESTE contrato. > Claude Code reescribe `seed.sql` a partir de ESTE contrato.
## Convenciones (decisiones cerradas) ## Convenciones
- **Mapeo de tipos al esquema:** radio→`single_choice`, checkbox→`multiple_choice`, escala 1-5→ - Tipos: radio→`single_choice`, checkbox→`multiple_choice`, escala 1-5→`rating`,
`rating`, textarea`text_long`. texto libre largo→`text_long`, texto corto`text_short`.
- **`conditional_rules` viven en la pregunta DEPENDIENTE** (cada pregunta condicional declara su - **`conditional_rules` en la pregunta DEPENDIENTE** (cada pregunta declara su propia condición).
propia condición de visibilidad: `{if_question, op:'eq', value, action:'show'}`). El motor, al - Los 3 triggers (1.1, 1.6, 1.13) son `single_choice` de 2 opciones (Sí · No); solo **"Sí"** dispara
decidir si renderiza X, mira las reglas de X. (Resuelve la ambigüedad que el agente había dejado.) el bloque correspondiente.
- **B1/C1/D1 son `single_choice` de 3 opciones** (Sí / No / Prefiero no decir), **NO `yes_no`**. - `is_sensitive=true` en preguntas de salud/discapacidad/diagnóstico (marcadas ✅).
Solo el valor **"Sí"** dispara el bloque condicional; "No" y "Prefiero no decir" lo ocultan. - `3.4` (salary) — `is_sensitive=true` + analyst-only (ADR-001 addendum): nunca en vistas org_admin.
- **`is_sensitive = true`** en toda pregunta que elicita salud/discapacidad/diagnóstico (marcadas - RE-INDEX: las preguntas marcadas son candidatas a índice compuesto (metodología TBD Sprint 3).
abajo). La encuesta entera es `data_sensitivity = 'sensible'`. - En preguntas `multiple_choice` el formulario muestra el subtítulo "Podés marcar más de una opción".
- "Prefiero no decir" eliminada de todas las preguntas (Cambio 1, Etapa 3I2).
## Estructura condicional (3 bloques, el resto siempre visible) ## Estructura condicional (4 bloques)
- **cond-B** — visible si `B1 = 'Sí'`contiene B2, B3, B4, B5 - **cond-disc-propia** — 1.1='Sí' → muestra 1.2, 1.3, 1.4, 1.5
- **cond-C** — visible si `C1 = 'Sí'`contiene C2, C3, C4, C5 - **cond-familiar** — 1.6='Sí' → muestra 1.7, 1.8, 1.9, 1.10, 1.11, 1.12
- **cond-D** — visible si `D1 = 'Sí'`contiene D2, D3, D4, D5 - **cond-adulto-mayor** — 1.13='Sí' → muestra 1.14, 1.15, 1.16
- Secciones A (datos generales), E (rol cuidador), F (económico), G (bienestar), H (necesidades): - **salto-triple** — si 1.1='No' Y 1.6='No' Y 1.13='No' (es decir, ninguno de los tres dio
**top-level, siempre visibles.** 'Sí'), se ocultan completas las secciones 2, 3, 4, 5 y las preguntas 6.1, 6.2, 6.3. El
formulario salta directo a 6.4. Implementado como `conditional_rules`
`{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}` en cada
pregunta del bloque (equivalente: se muestra si AL MENOS uno de los tres es 'Sí').
- Sección A, 6.4-6.6 y Sección 7 (cierre): **top-level, siempre visibles.**
--- ---
## Sección 0 — Datos Generales (top-level, cuasi-identificadores) ## Sección A — Datos Generales (top-level)
| Code | Tipo | Pregunta | Opciones | | Code | Tipo | Pregunta | Opciones | Notas |
|---|---|---|---| |---|---|---|---|---|
| A1 | single_choice | ¿En qué área o gerencia trabajás? | Operaciones/Producción · Comercial/Ventas · Administración/Finanzas · RRHH/Cultura · Tecnología/Sistemas · Logística/Distribución · Otra área | | A1 | single_choice | ¿En qué departamento de Paraguay vivís?* | Asunción (Capital) · Alto Paraguay · Alto Paraná · Amambay · Boquerón · Caaguazú · Caazapá · Canindeyú · Central · Concepción · Cordillera · Guairá · Itapúa · Misiones · Ñeembucú · Paraguarí · Presidente Hayes · San Pedro | qi_zone |
| A2 | single_choice | ¿Cuántos años tenés? | Menos de 25 · 25-34 · 35-44 · 45-54 · 55+ · Prefiero no decir | | A2 | single_choice | ¿En qué ciudad vivís?* | 219 ciudades (GeoSelectWidget: dependiente de A1) | qi_city — Sprint 3 |
| A3 | single_choice | ¿Con qué género te identificás? | Femenino · Masculino · No binario/Otro · Prefiero no decir | | A3 | text_short | ¿En qué barrio vivís? | — (texto libre, opcional) | |
| A4 | single_choice | ¿Cuál es tu modalidad de trabajo actual? | Presencial · Remoto · Híbrido | | A4 | single_choice | ¿En qué área o gerencia trabajás?* | Directorio · Gerencia · Operaciones/Producción · Comercial/Ventas · Administración/Finanzas · Recursos Humanos/Cultura · Tecnología/Sistemas · Logística/Distribución · Otra área | qi_sector |
| A5 | single_choice | ¿Cuántos años tenés? | Menos de 25 años · 25 a 34 años · 35 a 44 años · 45 a 54 años · 55 años o más | qi_age_bucket |
| A6 | single_choice | ¿Con qué género te identificás? | Femenino · Masculino · No binario/Otro | |
| A7 | single_choice | ¿Cuál es tu modalidad de trabajo actual? | Presencial · Remoto (home office) · Híbrido | |
## Sección 1 — ¿Vivís con discapacidad? *(sensible)* ## Sección 1 — Prevalencia: cuidado y discapacidad *(sensible)*
| Code | Tipo | Cond. | Pregunta | Opciones | is_sensitive | | Code | Tipo | Cond. | Pregunta | Opciones | is_sensitive |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| B1 | single_choice | trigger | ¿Vos mismo/a tenés alguna discapacidad o condición de salud…? | Sí · No · Prefiero no decir | ✅ | | 1.1 | single_choice | trigger | ¿Vos mismo/a vivís con alguna discapacidad o condición de salud que afecte tu funcionamiento cotidiano?* | Sí · No | ✅ |
| B2 | multiple_choice | B1=Sí | ¿Qué tipo de discapacidad tenés? (más de una) | Física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial · Condición crónica · Otra | ✅ | | 1.2 | multiple_choice | 1.1=Sí | ¿Qué tipo de discapacidad o condición tenés? | Discapacidad física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial/salud mental · Condición crónica que genera limitaciones · Otra | ✅ |
| B3 | single_choice | B1=Sí | ¿Contás con certificado oficial de discapacidad? | Sí · En trámite · No | ✅ | | 1.3 | multiple_choice | 1.1=Sí | ¿Usás algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utilizo | ✅ |
| B4 | single_choice | B1=Sí | ¿Tu discapacidad afecta tu desempeño o comodidad en el trabajo? | Sí, significativamente · Sí, aspectos puntuales · No, bien gestionada · Prefiero no decir | ✅ | | 1.4 | single_choice | 1.1=Sí | ¿Contás con certificado oficial de discapacidad? | Sí · No, pero estoy en trámite · No | ✅ |
| B5 | single_choice | B1=Sí | ¿La empresa conoce tu situación? | Sí, con apoyo · Sí, sin apoyo · No, privado | ✅ | | 1.5 | single_choice | 1.1=Sí | ¿Tu condición afecta tu desempeño o comodidad en el trabajo? | Sí, de forma significativa · Sí, pero en aspectos puntuales · No, está bien gestionada | ✅ |
| 1.6 | single_choice | trigger | ¿Tenés algún familiar cercano (hijo/a, pareja, hermano/a, padre/madre) que viva con discapacidad?* | Sí · No | ✅ |
| 1.7 | multiple_choice | 1.6=Sí | ¿Cuál es tu vínculo con esa persona? | Hijo/a · Cónyuge/pareja · Padre o madre · Hermano/a · Otro familiar | ✅ |
| 1.8 | multiple_choice | 1.6=Sí | ¿Qué tipo de discapacidad tiene? | Física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial/salud mental · Condición crónica severa · Otra | ✅ |
| 1.9 | multiple_choice | 1.6=Sí | ¿Qué tipo de apoyo o acompañamiento necesita principalmente? | Movilidad física · Comunicación · Apoyo escolar/educativo · Inclusión laboral · Terapias/rehabilitación · Apoyo emocional/psicosocial | ✅ |
| 1.10 | multiple_choice | 1.6=Sí | ¿Usa algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utiliza | ✅ |
| 1.11 | single_choice | 1.6=Sí | Si es niño/a o adolescente en edad escolar: ¿está escolarizado/a y enfrenta barreras de aprendizaje? | Escolarizado/a, sin barreras importantes · Escolarizado/a, enfrenta barreras o faltan ajustes · No está escolarizado/a · No aplica | ✅ |
| 1.12 | single_choice | 1.6=Sí | ¿La persona con discapacidad se encuentra trabajando actualmente? | Sí, está trabajando · No, y está buscando empleo · No, y no está buscando · No aplica | ✅ |
| 1.13 | single_choice | trigger | ¿Tenés algún adulto mayor (60+ años) a tu cuidado o bajo tu responsabilidad?* | Sí · No | |
| 1.14 | single_choice | 1.13=Sí | ¿Qué nivel de dependencia tiene? (la más representativa) | Independiente, pero necesita supervisión · Dependencia leve · Dependencia moderada · Dependencia severa | ✅ |
| 1.15 | multiple_choice | 1.13=Sí | ¿Tiene una condición de salud que genere mayor necesidad de cuidado? | Demencia/Alzheimer · ACV/Parálisis · Enfermedad crónica grave · Movilidad reducida · Deterioro cognitivo · Sin condición específica | ✅ |
| 1.16 | single_choice | 1.13=Sí | ¿Vive con vos o en otra vivienda? | Vive en mi hogar · Vive en otra vivienda · Está en residencia/institución | |
## Sección 2 — Familiares con Discapacidad *(sensible)* ## Sección 2 — Intensidad y rol del cuidado (top-level)
| Code | Tipo | Cond. | Pregunta | Opciones | is_sensitive | > Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
|---|---|---|---|---|---|
| C1 | single_choice | trigger | ¿Tenés algún familiar cercano que viva con discapacidad? | Sí · No · Prefiero no decir | ✅ |
| C2 | multiple_choice | C1=Sí | ¿Cuál es el vínculo? (más de una) | Hijo/a · Cónyuge/pareja · Padre/madre · Hermano/a · Otro familiar | ✅ |
| C3 | multiple_choice | C1=Sí | ¿Qué tipo de discapacidad tiene? (más de una) | Física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial · Condición crónica | ✅ |
| C4 | single_choice | C1=Sí | ¿Esta persona requiere apoyo o cuidado cotidiano? | Alta dependencia · Apoyo parcial · Autónomo/a | ✅ |
| C5 | single_choice | C1=Sí | ¿Quién es el responsable principal del cuidado? | Yo principal · Compartido · Cuidador pagado · Institución | |
## Sección 3 — Adultos Mayores a Cargo *(sensible)*
| Code | Tipo | Cond. | Pregunta | Opciones | is_sensitive |
|---|---|---|---|---|---|
| D1 | single_choice | trigger | ¿Tenés algún adulto mayor (60+) a tu cuidado? | Sí · No · Prefiero no decir | |
| D2 | single_choice | D1=Sí | ¿Cuántos adultos mayores están bajo tu cuidado? | 1 · 2 · 3+ | |
| D3 | single_choice | D1=Sí | ¿Qué nivel de dependencia tiene/n? | Supervisión · Dependencia leve · Dependencia moderada · Dependencia severa | ✅ |
| D4 | multiple_choice | D1=Sí | ¿Tiene condición diagnosticada? (más de una) | Demencia/Alzheimer · ACV/Parálisis · Enf. crónica · Movilidad reducida · Deterioro cognitivo · Ninguna específica | ✅ |
| D5 | single_choice | D1=Sí | ¿La persona vive con vos o en otra vivienda? | Vive conmigo · Otra vivienda · Institución | |
## Sección 4 — Tu Rol como Cuidador/a (top-level)
| Code | Tipo | Pregunta | Opciones | | Code | Tipo | Pregunta | Opciones |
|---|---|---|---| |---|---|---|---|
| E1 | single_choice | ¿Cuántas horas por día dedicás, en promedio, al cuidado directo? | Menos de 1h · 1-3h · 3-6h · Más de 6h · Variable | | 2.1 | multiple_choice ⚠️ TECH-DEBT | ¿Quién es el/la responsable principal del cuidado? | Yo soy el/la principal · Comparto con otro familiar · Hay cuidador/a pagado/a · Está en institución/centro de día |
| E2 | multiple_choice | ¿En qué momentos realizás principalmente el cuidado? (más de una) | Antes del trabajo · Durante horario laboral · Al llegar · Durante la noche · Fines de semana · Continuo | | 2.2 | single_choice | ¿Cuántas horas por día dedicás, en promedio, al cuidado directo? | Menos de 1h · Entre 1 y 3h · Entre 3 y 6h · Más de 6h · Varía mucho |
| E3 | single_choice | ¿Contás con apoyo de otras personas para el cuidado? | Apoyo familiar suficiente · Apoyo insuficiente · Único responsable · Apoyo contratado | | 2.3 | multiple_choice | ¿En qué momentos del día cuidás principalmente? | Antes del trabajo · Durante horario laboral (llamadas/emergencias) · Al llegar del trabajo · Durante la noche · Los fines de semana · Es continuo sin horario fijo |
| E4 | single_choice | ¿Tuviste que faltar al trabajo por el cuidado en el último año? | Nunca · Pocas veces (1-3) · Varias veces (4-10) · Con frecuencia (10+) | | 2.5 | single_choice | Como cuidador/a primario, ¿presentás signos de agotamiento o estrés crónico? | Sí, frecuentemente · A veces · No, lo llevo bien |
> ⚠️ TECH-DEBT (2.1): pasó de `single_choice` a `multiple_choice` — selección múltiple sin orden
> de prioridad. Deuda: implementar ranking (Principal cuidador soy yo > secundario instituto >
> principal pagado > secundario familiar). Ver `TECH_DEBT.md`.
>
> **2.4 eliminada** (Cambio 6, Etapa 3I2): "¿Contás con apoyo de otras personas para el cuidado?"
> — feedback del equipo médico: pregunta similar/redundante con 2.1. Ver `OBSERVATIONS.md`.
## Sección 3 — Impacto económico del cuidado (top-level)
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
> 3.4: is_sensitive=true + analyst-only (no exponer a org_admin).
| Code | Tipo | Pregunta | Opciones | Notas |
|---|---|---|---|---|
| 3.1 | single_choice | ¿Cuánto estimás que gastás por mes en el cuidado? | Menos de Gs. 500.000 · Gs. 500.0001.500.000 · Gs. 1.500.0003.000.000 · Gs. 3.000.0006.000.000 · Más de Gs. 6.000.000 | |
| 3.2 | multiple_choice | ¿Qué servicios o apoyos utilizás actualmente? | Terapias especializadas · Cuidador/a particular · Centro de día/institución · Transporte adaptado · Medicación alto costo · Equipamiento médico/tecnología asistiva · No utilizo servicios formales | |
| 3.3 | multiple_choice | ¿Cómo sostenés los gastos y responsabilidades del cuidado? | Con mi empleo de tiempo completo · Con mi empleo de tiempo parcial · Con trabajo independiente/emprendimiento · Me ayudan mis familiares · No puedo sostenerlo con mis ingresos | |
| 3.4 | single_choice | ¿En qué franja salarial mensual te ubicás? *(CONFIDENCIAL — analyst-only)* | Sueldo mínimo · Gs. 3.000.000 a 5.000.000 · Gs. 5.000.0018.000.000 · Gs. 8.000.00112.000.000 · Gs. 12.000.00115.000.000 · Gs. 15.000.00118.000.000 · Gs. 18.000.00125.000.000 · Gs. 25.000.00150.000.000 · Gs. 50.000.001 o más | is_sensitive=true; never in org_admin views |
| 3.5 | rating | ¿Los gastos de cuidado impactan en tu situación económica personal? | 1=Sin impacto · 2=Leve · 3=Moderado · 4=Alto · 5=Muy alto | RE-INDEX |
> **Cambio 7** (3.1): se elimina "Sin gastos significativos" además de "Prefiero no decir".
> **Cambio 8** (3.3): "Es difícil sostenerlo con mis ingresos" → "No puedo sostenerlo con mis
> ingresos". Breaking change para datos existentes — ver `OBSERVATIONS.md`.
> **Cambio 9** (3.4): "Sueldo mínimo Gs. 5.000.000" se divide en "Sueldo mínimo" y
> "Gs. 3.000.000 a 5.000.000". Breaking change para datos existentes — ver `OBSERVATIONS.md`.
## Sección 4 — Tiempo y descanso (top-level)
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
## Sección 5 — Impacto Económico (top-level)
| Code | Tipo | Pregunta | Opciones | | Code | Tipo | Pregunta | Opciones |
|---|---|---|---| |---|---|---|---|
| F1 | single_choice | ¿Cuánto estimás que gastás mensualmente en el cuidado? | Sin gastos significativos · Menos de 500k · 500k-1.5M · 1.5M-3M · 3M-6M · Más de 6M · Prefiero no decir | | 4.1 | single_choice | ¿Cómo describirías tu nivel de descanso habitual? | Descanso bien, me siento recuperado/a · Descanso de forma irregular, pero lo manejo · Me cuesta descansar, me siento cansado/a con frecuencia · Descanso muy poco, el agotamiento afecta mi día a día | RE-INDEX |
| F2 | multiple_choice | ¿Qué servicios o apoyos utilizás actualmente? (más de una) | Terapias especializadas · Cuidador particular · Centro de día · Transporte adaptado · Medicación alto costo · Tecnología asistiva · Apoyo ONG · Sin servicios formales | | 4.2 | multiple_choice | ¿Qué aspectos de tu vida se ven más afectados por tu rol de cuidador/a? | Tiempo libre y ocio · Sueño y descanso · Vida social y amistades · Vida en pareja/familiar · Salud física personal · Salud emocional/mental · Desarrollo profesional · Situación económica | |
| F3 | rating | ¿Los gastos de cuidado impactan en tu situación económica personal? | Escala 1 (Sin impacto) → 5 (Muy alto) | | 4.3 | rating | En una escala del 1 al 5, ¿cómo evaluás tu bienestar general en este momento? | 1=Muy mal · 2=Mal · 3=Regular · 4=Bien · 5=Muy bien | RE-INDEX |
## Sección 5 — Impacto en el trabajo (top-level)
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
## Sección 6 — Bienestar y Descanso (top-level)
| Code | Tipo | Pregunta | Opciones | | Code | Tipo | Pregunta | Opciones |
|---|---|---|---| |---|---|---|---|
| G1 | single_choice | ¿Cómo describirías tu nivel de descanso habitual? | Descanso bien · Descanso irregular · Me cuesta descansar · Muy poco descanso | | 5.1 | single_choice | En los últimos 3 meses, ¿con qué frecuencia sentiste que el cuidado afectó tu concentración o rendimiento? | Nunca o casi nunca · Algunas veces al mes · Varias veces por semana · Casi todos los días | RE-INDEX |
| G2 | single_choice | En los últimos 3 meses, ¿con qué frecuencia el cuidado afectó tu concentración o rendimiento laboral? | Nunca/casi nunca · Algunas veces al mes · Varias veces por semana · Casi todos los días | | 5.2 | single_choice | En el último año, ¿faltaste al trabajo por situaciones relacionadas al cuidado? | Nunca · Pocas veces (13 veces al año) · Varias veces (410 veces al año) · Con frecuencia (más de 10 veces al año) | |
| G3 | multiple_choice | ¿Qué aspectos de tu vida se ven más afectados? (más de una) | Tiempo libre · Sueño y descanso · Vida social · Vida en pareja/familia · Salud física · Salud emocional · Desarrollo profesional · Situación económica |
| G4 | rating | ¿Cómo evaluás tu bienestar general en este momento? | Escala 1 (Muy mal) → 5 (Muy bien) |
| G5 | single_choice | ¿Sentís que tu empresa comprende la realidad de ser cuidador/a? | Sí, acompañado/a · Parcialmente · No, invisible · Nunca lo supo |
## Sección 7Necesidades (top-level) ## Sección 6Conciencia organizacional (top-level)
| Code | Tipo | Pregunta | Opciones / nota | > Cond. (6.1-6.3): **salto-triple** — se ocultan si 1.1='No' Y 1.6='No' Y 1.13='No'.
> 6.4-6.6: siempre visibles (destino del salto).
| Code | Tipo | Pregunta | Opciones | Notas |
|---|---|---|---|---|
| 6.1 | single_choice | ¿Sentís que tu empresa conoce o comprende la realidad del cuidado y la discapacidad de su gente?* | Sí, me siento acompañado/a · Parcialmente, hay cierta comprensión · No, es algo bastante invisible · No, no se puede hablar del tema | RE-INDEX. Cambio 10: última opción reescrita (era "No, nunca se ha hablado del tema"). Breaking change — ver `OBSERVATIONS.md`. |
| 6.2 | multiple_choice (**max_select=3**) | ¿Qué apoyos de la empresa te ayudarían más? Elegí hasta 3 | Flexibilidad horaria o trabajo remoto · Apoyo económico o convenios/descuentos · Acceso a terapias o salud · Contención/apoyo emocional o psicológico · Capacitación y autocuidado · Redes de apoyo/grupos de pares · Información sobre recursos y servicios disponibles | ⚠️ Ranking → multiple_choice para v1. Ranking widget al backlog. |
| 6.3 | single_choice | ¿Estarías dispuesto/a a participar en un grupo de apoyo para cuidadores dentro de la empresa? | Sí, definitivamente · Tal vez, si hay más personas en la misma situación · No por ahora · Prefiero mantenerlo en privado | |
| 6.4 | single_choice | ¿Conocés a algún compañero/a de trabajo que esté al cuidado de una persona con discapacidad o dependiente? | Sí, conozco a varias personas · Sí, conozco a una persona · No conozco a nadie · No estoy seguro/a | Código corregido (era 6.2 duplicado) |
| 6.5 | single_choice | ¿Considerás que la empresa debería ofrecer apoyos a quienes cuidan a una persona dependiente? | Sí, definitivamente · Tal vez, dependería del caso · No lo considero necesario · No lo sé | Código corregido (era 6.3 duplicado) |
| 6.6 | single_choice | ¿Estarías dispuesto/a a ser voluntario/a en iniciativas y/o redes de apoyo a personas con discapacidad y/o cuidadores? | Sí · No · Tal vez | Nueva (Cambio 4b, Etapa 3I2) |
## Sección 7 — Para cerrar (top-level)
| Code | Tipo | Pregunta | Notas |
|---|---|---|---| |---|---|---|---|
| H1 | multiple_choice (**max_select=3**) | ¿Qué tipo de apoyo de tu empresa te ayudaría más? (hasta 3) | Flexibilidad horaria/remoto · Info recursos y servicios · Apoyo psicológico · Espacio de escucha · Convenios descuentos · Grupos de pares · Capacitación autocuidado · Nada, estoy bien | | 7.1 | text_long | ¿Te gustaría dejar un comentario o sugerencia especial? (opcional) | No entra al store analítico crudo (regla 4 CLAUDE.md) |
| H2 | single_choice | ¿Estarías dispuesto/a a participar en un grupo de apoyo para cuidadores? | Sí, definitivamente · Tal vez · No por ahora · Prefiero privacidad |
| H3 | text_long | ¿Querés compartir algo más sobre tu experiencia? (opcional) | Texto libre, opcional. **No entra al store analítico crudo** (regla 4 CLAUDE.md). |
--- ---
## Mapeo a cuasi-identificadores (`responses.qi_*`) ## Mapeo a cuasi-identificadores (`responses.qi_*`)
- `qi_age_bucket`**A2** (ya viene en bucket — bien). - `qi_age_bucket`A5 (ya en bucket)
- `qi_sector`**A1** (área). - `qi_sector`A4 (área)
- `qi_zone`**no aplica en este instrumento** (no se pregunta zona geográfica; es intra-empresa). Va NULL. - `qi_zone`A1 (departamento — poblado en `submit-survey.ts` desde Etapa 2E)
- `qi_city` ← A2: **Sprint 3** — se agrega como columna qi en migration futura.
Por ahora A2 se captura en `answers` como single_choice.
## ⚠️ Hallazgo para el director (decisión de modelo de datos, Nivel 3 — NO ejecutar) ## ⚠️ Decisiones pendientes para el director (NO ejecutar sin aprobación)
El dashboard original quería filtrar por **género (A3)** y **modalidad (A4)**, pero `agg_segment` - **Ranking widget (6.2):** actual es multiple_choice max_select=3. Cuando se implemente el
solo tiene en su whitelist `qi_age_bucket / qi_sector / qi_zone`. Para segmentar/filtrar por género o widget de ranking (drag o 3 dropdowns), 6.2 pasa a tipo `ranking` con JSONB `{rank1, rank2, rank3}`.
modalidad en el dashboard (Sprint 3), hay que decidir: ¿se agregan columnas `qi_gender` / `qi_modality` - **qi_city:** migration futura para agregar columna en responses y extender whitelist de agg_segment.
(y a la whitelist de `agg_segment`), o se resuelven por otra vía? Es decisión de esquema → consultar Se hace antes de Sprint 3 (dashboard con filtro por ciudad).
antes de Sprint 3, no resolver en el seed. - **Salary analytics:** metodología para calcular "carga económica del cuidado como % del ingreso"
es trabajo de metodología de investigación (fuera del scope del código).
- **RE-INDEX scoring:** algoritmo de composición de los índices (descanso, bienestar, rendimiento,
conciencia) — decisión antes de construir los reportes del dashboard.
- **Formulario de empresa (Relevamiento Empresa):** instrumento separado con PII de contacto.
Ejecutar offline para las primeras empresas. Diseño del modelo de datos en el sprint del builder.

87
TECH_DEBT.md Normal file
View File

@@ -0,0 +1,87 @@
# TECH_DEBT.md — Deuda técnica registrada
> Ítems identificados durante el desarrollo. Cada ítem documenta qué falta, por qué
> no se hizo ahora, y qué trigger habilitaría su resolución.
---
## [TECH-DEBT-001] Ranking widget para 6.2
- **Estado:** 6.2 es actualmente `multiple_choice` max_select=3. El widget muestra
frecuencia simple (top 8).
- **Deuda:** cuando el tipo pase a `ranking` con JSONB `{rank1, rank2, rank3}`, el
widget debe reescribirse para mostrar ponderación por posición.
- **Trigger:** decisión del director sobre el tipo de 6.2 en próxima versión del
instrumento (anotada en SEED_SPEC.md § Decisiones pendientes).
---
## [TECH-DEBT-002] KPI NPS Cuidador
- **Estado:** el instrumento actual no tiene pregunta NPS. El KPI fue eliminado del
dashboard en Etapa 2E.
- **Para implementar:** agregar en próxima versión del instrumento:
"En una escala de 0 a 10, ¿qué tan probable es que recomiendes esta empresa a otra
persona cuidadora?"
- **Trigger:** decisión de incluir NPS en el instrumento v2.
---
## [TECH-DEBT-003] Brecha de políticas (oferta vs. demanda)
- **Estado:** el instrumento captura solo demanda (6.2 = apoyos que ayudarían).
No captura qué políticas la empresa ya ofrece. W6 muestra solo demanda.
- **Para implementar:** agregar en próxima versión del instrumento:
"¿Qué apoyos para el cuidado ofrece tu empresa hoy?" (multi-select con las mismas
opciones de 6.2 + "Ninguno" + "No lo sé").
- **Trigger:** decisión de agregar la pregunta de oferta al instrumento.
---
## [TECH-DEBT-004] Intención directa de salida laboral
- **Estado:** el instrumento no pregunta directamente por intención de dejar el
empleo. W8 usa indicador compuesto (4.3 + 5.1 + 5.2) renombrado a "Saturación".
- **Para implementar:** agregar en próxima versión:
"En los últimos 6 meses, ¿pensaste en dejar este empleo por razones relacionadas
al cuidado?" (Sí/Frecuentemente · Sí/A veces · No · Prefiero no decir).
- **Trigger:** validación metodológica de que el indicador compuesto no es suficiente
para medir intención de salida.
---
## [TECH-DEBT-005] qi_city / qi_country — migración pendiente
- **Estado:** A1 (Departamento) y A2 (Ciudad) se guardan en `answers`. Las columnas
`qi_city` y `qi_country` en `responses` son NULL. Sprint 3 las necesita para
segmentación avanzada.
- **Para implementar:** migration futura que agregue extracción de A1/A2 en el insert
y extienda la whitelist de `agg_segment`.
- **Trigger:** inicio de Sprint 3 (dashboard avanzado con filtros de ciudad).
---
## [TECH-DEBT-006] Ranking widget para 2.1
- **Estado:** Etapa 3I2 (Cambio 5) cambió 2.1 ("¿Quién es el/la responsable principal
del cuidado?") de `single_choice` a `multiple_choice`. Permite selección múltiple
pero sin orden de prioridad — pierde la noción de "responsable principal" cuando
el respondente marca más de una opción.
- **Deuda:** implementar widget de ranking para 2.1 con el orden:
Principal cuidador soy yo > secundario instituto > principal pagado > secundario
familiar (mismo patrón que TECH-DEBT-001 para 6.2).
- **Trigger:** decisión del director sobre el tipo de 2.1 en próxima versión del
instrumento.
---
## [TECH-DEBT-008] /admin/responses sin autenticación
- **Estado:** ABIERTO.
- **Descripción:** la vista de control `/admin/responses` no tiene auth. Por diseño
explícito, excluye preguntas `is_sensitive=true` (14 preguntas de salud/
discapacidad) y `text_long`. Para mostrar esas preguntas en la vista, primero
implementar un gate de autenticación para staff (Basic Auth con env var como
mínimo viable).
- **Impacto actual:** ninguno en producción — la URL no está publicada.
- **Bloqueante para:** ampliar visibilidad de datos en la vista de control.

View File

@@ -0,0 +1,111 @@
'use server'
import { headers } from 'next/headers'
import { createHash, randomUUID } from 'crypto'
import { createClient as createSupabaseClient } from '@supabase/supabase-js'
export type SubmitResult =
| { success: true }
| { success: false; error: string }
export async function submitSurvey(payload: {
surveyId: string
answers: Record<string, unknown>
questionMap: Record<string, string> // código → question_id UUID
consentOperativo: boolean
consentMacroN1: boolean
}): Promise<SubmitResult> {
// 1. ip_hash: server-only, nunca en texto plano
const headersList = await headers()
const rawIp =
headersList.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
const salt = process.env.IP_HASH_SALT ?? ''
const ipHash = createHash('sha256').update(rawIp + salt).digest('hex')
// 2. Cliente Supabase con anon key (RLS policies permiten INSERT a anon)
// NUNCA usar service_role — el anon key respeta RLS
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ''
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? ''
const supabase = createSupabaseClient(supabaseUrl, supabaseAnonKey)
// 3. Leer survey para obtener org_id y consent_version_id
const { data: survey, error: sErr } = await supabase
.from('surveys')
.select('org_id, consent_version_id')
.eq('id', payload.surveyId)
.single()
if (sErr || !survey) return { success: false, error: 'survey_not_found' }
// 4. Pre-generar UUIDs server-side para evitar .select().single() post-INSERT.
//
// DIAGNÓSTICO: el Supabase JS client usa RETURNING * en .select().single(), lo que
// requiere SELECT policy para anon en consent_records/responses. Esa policy no existe
// (correcto — anon no debe leer consent_records de otros respondentes).
// Solución: pre-generar UUIDs con randomUUID() y NO hacer SELECT after INSERT.
const consentId = randomUUID()
const responseId = randomUUID()
// 5. INSERT consent_record (inmutable, ADR-003)
// Sin .select() para no activar RETURNING y evitar el error RLS 42501.
const { error: cErr } = await supabase
.from('consent_records')
.insert({
id: consentId,
consent_version_id: survey.consent_version_id,
granted_scopes: {
operativo: payload.consentOperativo,
macro_n1: payload.consentMacroN1,
},
granted_at: new Date().toISOString(),
})
if (cErr) return { success: false, error: `consent_insert_failed: ${cErr.message}` }
// 6. Extraer qi_* de las respuestas
const qi_age_bucket =
typeof payload.answers['A5'] === 'string' ? payload.answers['A5'] : null
const qi_sector =
typeof payload.answers['A4'] === 'string' ? payload.answers['A4'] : null
const qi_zone =
typeof payload.answers['A1'] === 'string' ? payload.answers['A1'] : null
// 7. INSERT response (respondent_id=null — encuesta anónima, regla 2 CLAUDE.md)
const { error: rErr } = await supabase
.from('responses')
.insert({
id: responseId,
survey_id: payload.surveyId,
company_id: survey.org_id,
respondent_id: null,
ip_hash: ipHash,
consent_record_id: consentId,
qi_age_bucket: qi_age_bucket || null,
qi_sector: qi_sector || null,
qi_zone: qi_zone || null,
})
if (rErr) return { success: false, error: `response_insert_failed: ${rErr.message}` }
// 8. INSERT answers — una fila por pregunta respondida
const answerRows = Object.entries(payload.answers)
.map(([code, value]) => ({
response_id: responseId,
question_id: payload.questionMap[code],
value,
}))
.filter(
(row) =>
row.question_id &&
row.value !== null &&
row.value !== undefined &&
row.value !== '' &&
!(Array.isArray(row.value) && row.value.length === 0)
)
if (answerRows.length > 0) {
const { error: aErr } = await supabase.from('answers').insert(answerRows)
if (aErr) return { success: false, error: `answers_insert_failed: ${aErr.message}` }
}
return { success: true }
}

View File

@@ -0,0 +1,228 @@
import { createClient } from '@supabase/supabase-js'
import { ResponsesTable, type DisplayRow } from './responses-table'
// Server Component sin auth — herramienta interna de diagnóstico (Etapa 3V-B).
// Usa SUPABASE_SERVICE_ROLE_KEY: vive solo acá, server-side, nunca al cliente.
export const dynamic = 'force-dynamic'
const MAX_SUBMISSIONS = 500
const PROMPT_MAX_LEN = 60
const SURVEY_OPTIONS = [
{ label: 'Todos', id: null },
{ label: 'InQuality', id: '30000000-0000-0000-0000-000000000002' },
{ label: 'Fundación Solidaridad', id: '30000000-0000-0000-0000-000000000003' },
{ label: 'Demo', id: '30000000-0000-0000-0000-000000000001' },
] as const
interface AllowedQuestion {
id: string
code: string
prompt: string
type: string
position: number
}
interface AnswerRow {
value: unknown
question_id: string
}
interface ResponseRow {
id: string
submitted_at: string
qi_zone: string | null
qi_sector: string | null
answers: AnswerRow[]
}
function getSupabaseAdmin() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://placeholder.supabase.co'
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'placeholder'
return createClient(url, serviceKey, { auth: { persistSession: false } })
}
function formatDate(iso: string): string {
const d = new Date(iso)
const pad = (n: number) => String(n).padStart(2, '0')
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
function truncate(text: string, max = PROMPT_MAX_LEN): string {
return text.length > max ? `${text.slice(0, max)}...` : text
}
// Spec 4.3 — null/undefined → null (renderizado como badge "sin dato")
function renderValue(value: unknown, type: string): string | null {
if (value === null || value === undefined) return null
switch (type) {
case 'single_choice':
case 'text_short':
return String(value).replace(/^"|"$/g, '')
case 'multiple_choice':
if (Array.isArray(value)) return value.join(', ')
return String(value)
case 'rating':
return String(value)
default:
return String(value)
}
}
export default async function AdminResponsesPage({
searchParams,
}: {
searchParams: Promise<{ survey_id?: string }>
}) {
const { survey_id: surveyId } = await searchParams
const supabase = getSupabaseAdmin()
// 1. Preguntas permitidas — text_long e is_sensitive=true se excluyen ACÁ,
// antes de tocar la tabla answers. Sus valores nunca se consultan.
const { data: questionsData, error: questionsError } = await supabase
.from('questions')
.select('id, code, prompt, type, position')
.eq('is_sensitive', false)
.neq('type', 'text_long')
.order('position', { ascending: true })
if (questionsError) {
return (
<div className="admin-page" role="main">
<h1 className="admin-title">Control de respuestas</h1>
<p className="admin-error">No fue posible conectarse a la base de datos.</p>
<pre className="admin-error-detail">{questionsError.message}</pre>
</div>
)
}
const allowedQuestions = (questionsData ?? []) as AllowedQuestion[]
const allowedIds = allowedQuestions.map((q) => q.id)
const questionMap = new Map(allowedQuestions.map((q) => [q.id, q]))
// 2. Submissions con sus respuestas — answers!inner + .in() filtra el ARRAY
// embebido a solo question_id permitidos (consulta, no post-filtro).
let responsesQuery = supabase
.from('responses')
.select(
`
id,
submitted_at,
qi_zone,
qi_sector,
answers!inner ( value, question_id )
`
)
.in('answers.question_id', allowedIds)
if (surveyId) {
responsesQuery = responsesQuery.eq('survey_id', surveyId)
}
const { data: responsesData, error: responsesError } =
allowedIds.length > 0
? await responsesQuery.order('submitted_at', { ascending: false }).limit(MAX_SUBMISSIONS)
: { data: [] as ResponseRow[], error: null }
if (responsesError) {
return (
<div className="admin-page" role="main">
<h1 className="admin-title">Control de respuestas</h1>
<p className="admin-error">No fue posible conectarse a la base de datos.</p>
<pre className="admin-error-detail">{responsesError.message}</pre>
</div>
)
}
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 }
// 4. Aplanar: una fila por answer, ordenadas por position dentro de cada submission
const rows: DisplayRow[] = []
responses.forEach((response, idx) => {
const answers = response.answers
.filter((a) => questionMap.has(a.question_id))
.sort(
(a, b) =>
questionMap.get(a.question_id)!.position - questionMap.get(b.question_id)!.position
)
answers.forEach((a, ai) => {
const q = questionMap.get(a.question_id)!
const isFirstInGroup = ai === 0
rows.push({
key: `${response.id}-${a.question_id}`,
submissionNum: idx + 1,
isFirstInGroup,
date: isFirstInGroup ? formatDate(response.submitted_at) : '',
code: q.code,
prompt: truncate(q.prompt),
rendered: renderValue(a.value, q.type),
zone: isFirstInGroup ? (response.qi_zone ?? '—') : '',
sector: isFirstInGroup ? (response.qi_sector ?? '—') : '',
})
})
})
return (
<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())}
</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>
</div>
<a href="/admin/responses" className="admin-refresh">Actualizar</a>
</header>
<div className="admin-table-wrapper">
<ResponsesTable rows={rows} />
{rows.length === 0 && (
<p className="admin-empty">Sin respuestas registradas todavía.</p>
)}
</div>
</div>
)
}

View File

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

View File

@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { queryToFilters, filtersToRpc } from '@/lib/dashboard-utils'
// Route Handler server-side: llama a las RPC con el cliente Supabase.
// El service_role nunca llega al browser — vive solo en este servidor.
// Las RPC son SECURITY DEFINER → aplican k-anonimato y excluyen 3.4/7.1 internamente.
const RPC_MAP: Record<string, string> = {
kpis: 'rpc_dashboard_kpis',
prevalencia: 'rpc_prevalencia_por_depto',
heatmap: 'rpc_heatmap_intensidad',
distribucion: 'rpc_perfil_poblacion',
apoyos: 'rpc_apoyos_demandados',
score: 'rpc_score_organizacional',
piramide: 'rpc_piramide_demografica',
intensidad: 'rpc_intensidad_vs_bienestar',
saturacion: 'rpc_saturacion_cuidador',
}
function getSupabaseServer() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL!
// Usar anon key — las RPC son SECURITY DEFINER y aplican sus propias reglas
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
return createClient(url, key)
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ widget: string }> }
) {
const { widget } = await params
const rpcName = RPC_MAP[widget]
if (!rpcName) {
return NextResponse.json({ error: 'Widget desconocido' }, { status: 404 })
}
const sp = request.nextUrl.searchParams
const surveyId = sp.get('survey_id')
if (!surveyId) {
return NextResponse.json({ error: 'survey_id requerido' }, { status: 400 })
}
const filters = queryToFilters(sp)
const supabase = getSupabaseServer()
const { data, error } = await supabase.rpc(rpcName, {
p_survey_id: surveyId,
p_filters: filtersToRpc(filters),
})
if (error) {
console.error(`[dashboard/${widget}] RPC error:`, error.message)
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json(data)
}

15
app/dashboard/loading.tsx Normal file
View File

@@ -0,0 +1,15 @@
export default function DashboardLoading() {
return (
<div className="db-loading-page" aria-busy="true" aria-label="Cargando dashboard…">
<div className="db-skeleton-block" style={{ height: 60, marginBottom: 16 }} />
<div className="db-grid">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="db-skeleton-block" style={{ gridColumn: 'span 3', height: 96 }} />
))}
{Array.from({ length: 6 }).map((_, i) => (
<div key={i+4} className="db-skeleton-block" style={{ gridColumn: 'span 6', height: 240 }} />
))}
</div>
</div>
)
}

100
app/dashboard/page.tsx Normal file
View File

@@ -0,0 +1,100 @@
'use client'
import { useState } from 'react'
import { useSearchParams } from 'next/navigation'
import { KpiCards } from '@/components/dashboard/kpi-cards'
import { ParaguayMap } from '@/components/dashboard/paraguay-map'
import { DonutCuidados } from '@/components/dashboard/donut-cuidados'
import { GaugeOrganizacional } from '@/components/dashboard/gauge-organizacional'
import { HeatmapWidget } from '@/components/dashboard/heatmap-widget'
import { PiramideDemografica } from '@/components/dashboard/piramide-demografica'
import { ApoyosBar } from '@/components/dashboard/apoyos-bar'
import { IntensidadProductividad } from '@/components/dashboard/intensidad-productividad'
import { TablaSaturacion } from '@/components/dashboard/tabla-saturacion'
import { FilterSidebar } from '@/components/dashboard/filter-sidebar'
import type { DashboardFilters } from '@/lib/dashboard-types'
// El survey_id se pasa como query param: /dashboard?survey_id=UUID
// '00000000-0000-0000-0000-000000000000' = flag p_all (vista agregada de todos los surveys)
const SURVEY_OPTIONS = [
{ label: 'Todos', id: '00000000-0000-0000-0000-000000000000' },
{ 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' },
]
export default function DashboardPage() {
const searchParams = useSearchParams()
const surveyId = searchParams.get('survey_id') ?? '30000000-0000-0000-0000-000000000001'
const [filters, setFilters] = useState<DashboardFilters>({})
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
function handleDeptoClick(depto: string) {
setFilters(f => ({ ...f, depto: depto || undefined }))
}
return (
<div className="db-page">
<FilterSidebar
filters={filters}
onChange={setFilters}
collapsed={sidebarCollapsed}
onToggle={() => setSidebarCollapsed(c => !c)}
/>
<div className="db-main">
<header className="db-page-header">
<h1 className="db-page-title">Economía del Cuidado Paraguay</h1>
<p className="db-page-subtitle">
Datos agregados · k-anonimato k5 · Encuesta Mapeo de Cuidadores 2026
{filters.depto && <span className="db-active-filter"> · Filtro: {filters.depto}</span>}
</p>
<nav className="db-survey-nav" aria-label="Selección de empresa">
{SURVEY_OPTIONS.map((opt) => (
<a
key={opt.id}
href={`/dashboard?survey_id=${opt.id}`}
className={
opt.id === surveyId
? 'db-survey-link db-survey-link-active'
: 'db-survey-link'
}
aria-current={opt.id === surveyId ? 'page' : undefined}
>
{opt.label}
</a>
))}
</nav>
</header>
{/* Fila 1: KPIs (span 12) */}
<section className="db-section" aria-label="Indicadores clave">
<KpiCards surveyId={surveyId} filters={filters} />
</section>
{/* Fila 2: Mapa (6) + Donut (3) + Gauge (3) */}
<section className="db-grid db-section" aria-label="Distribución geográfica y tipos">
<ParaguayMap surveyId={surveyId} filters={filters} onDeptoClick={handleDeptoClick} />
<DonutCuidados surveyId={surveyId} filters={filters} />
<GaugeOrganizacional surveyId={surveyId} filters={filters} />
</section>
{/* Fila 3: Heatmap (6) + Pirámide (6) */}
<section className="db-grid db-section" aria-label="Intensidad y demografía">
<HeatmapWidget surveyId={surveyId} filters={filters} />
<PiramideDemografica surveyId={surveyId} filters={filters} />
</section>
{/* Fila 4: Apoyos demandados (6) + Hexbin intensidad/bienestar (6) */}
<section className="db-grid db-section" aria-label="Apoyos e impacto">
<ApoyosBar surveyId={surveyId} filters={filters} />
<IntensidadProductividad surveyId={surveyId} filters={filters} />
</section>
{/* Fila 5: Saturación del cuidador (12) */}
<section className="db-grid db-section" aria-label="Saturación del cuidador">
<TablaSaturacion surveyId={surveyId} filters={filters} />
</section>
</div>
</div>
)
}

View File

@@ -2,18 +2,27 @@
/* ── Tokens ── */ /* ── Tokens ── */
:root { :root {
/* ── Marca RE ── */
--color-re-teal: #43BBC8;
--color-re-dark: #494F4F;
--color-re-orange: #F8AD13;
--color-re-orange-intense: #EE761A;
--color-re-light: #E7E6E5;
--color-re-white: #FFFFFF;
/* ── Sistema (primario = teal RE) ── */
--color-bg: #f9fafb; --color-bg: #f9fafb;
--color-surface: #ffffff; --color-surface: #ffffff;
--color-text: #111827; --color-text: #111827;
--color-text-muted: #6b7280; --color-text-muted: #6b7280;
--color-primary: #1d4ed8; --color-primary: #43BBC8;
--color-primary-hover: #1e40af; --color-primary-hover: #3DAAB6;
--color-primary-light: #dbeafe; --color-primary-light: #e8f8fa;
--color-accent: #2563eb; --color-accent: #43BBC8;
--color-error: #b91c1c; --color-error: #b91c1c;
--color-error-bg: #fef2f2; --color-error-bg: #fef2f2;
--color-border: #d1d5db; --color-border: #d1d5db;
--color-border-focus: #2563eb; --color-border-focus: #43BBC8;
--color-sensitive: #fef3c7; --color-sensitive: #fef3c7;
--color-sensitive-border: #f59e0b; --color-sensitive-border: #f59e0b;
--radius: 8px; --radius: 8px;
@@ -23,6 +32,7 @@
/* ── Reset & base ── */ /* ── Reset & base ── */
*, *::before, *::after { box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }
html { font-size: 16px; scroll-behavior: smooth; } html { font-size: 16px; scroll-behavior: smooth; }
html, body { height: 100%; }
body { body {
margin: 0; margin: 0;
background: var(--color-bg); background: var(--color-bg);
@@ -30,7 +40,12 @@ body {
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif; font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.6; line-height: 1.6;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
display: flex;
flex-direction: column;
min-height: 100vh;
} }
/* El contenido entre header y footer crece para empujar el footer al fondo */
.layout-content { flex: 1; }
/* ── Skip link ── */ /* ── Skip link ── */
.skip-link { .skip-link {
@@ -132,16 +147,6 @@ body {
border-radius: 0 var(--radius) var(--radius) 0; border-radius: 0 var(--radius) var(--radius) 0;
color: var(--color-primary-hover); color: var(--color-primary-hover);
} }
.section-sensitive-note {
font-size: 0.8125rem;
color: #92400e;
background: var(--color-sensitive);
border: 1px solid var(--color-sensitive-border);
border-radius: var(--radius);
padding: 8px 12px;
margin: 0;
}
/* ── Question wrapper ── */ /* ── Question wrapper ── */
.question-wrapper { .question-wrapper {
background: var(--color-surface); background: var(--color-surface);
@@ -168,6 +173,12 @@ body {
line-height: 1.4; line-height: 1.4;
} }
.required-mark { color: var(--color-error); } .required-mark { color: var(--color-error); }
.q-code {
font-size: 0.78em;
font-weight: 700;
color: #7A8FA0;
letter-spacing: 0.02em;
}
/* ── Options list ── */ /* ── Options list ── */
.options-list { display: flex; flex-direction: column; gap: 8px; } .options-list { display: flex; flex-direction: column; gap: 8px; }
@@ -254,6 +265,26 @@ body {
.rating-step:hover .rating-dot { border-color: var(--color-accent); background: var(--color-primary-light); } .rating-step:hover .rating-dot { border-color: var(--color-accent); background: var(--color-primary-light); }
.rating-step:has(input:checked):hover .rating-dot { background: var(--color-primary-hover); border-color: var(--color-primary-hover); color: #fff; } .rating-step:has(input:checked):hover .rating-dot { background: var(--color-primary-hover); border-color: var(--color-primary-hover); color: #fff; }
/* ── Text short (single-line input) ── */
.text-short-input {
display: block;
width: 100%;
padding: 10px 12px;
border: 1.5px solid var(--color-border);
border-radius: var(--radius);
font-size: 1rem;
font-family: inherit;
line-height: 1.5;
transition: border-color 0.15s;
color: var(--color-text);
background: var(--color-surface);
}
.text-short-input:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgba(37,99,235,0.15);
}
/* ── Textarea ── */ /* ── Textarea ── */
.text-area { .text-area {
display: block; display: block;
@@ -378,6 +409,61 @@ body {
justify-content: flex-start; justify-content: flex-start;
} }
/* ── Geo select widget ── */
.geo-select-wrapper { display: flex; flex-direction: column; gap: 16px; }
.geo-select-group { display: flex; flex-direction: column; gap: 8px; }
.geo-select {
display: block; width: 100%;
padding: 10px 12px;
border: 1.5px solid var(--color-border); border-radius: var(--radius);
font-size: 1rem; font-family: inherit; background: var(--color-surface);
color: var(--color-text); cursor: pointer; appearance: auto;
transition: border-color 0.15s;
}
.geo-select:focus { outline: none; border-color: var(--color-border-focus); box-shadow: 0 0 0 3px rgba(37,99,235,0.15); }
.geo-select-disabled { opacity: 0.5; cursor: not-allowed; background: #f3f4f6; }
.geo-label-disabled { color: var(--color-text-muted); }
.geo-disabled-hint { font-size: 0.8125rem; font-weight: 400; color: var(--color-text-muted); }
/* ── Brand header ── */
.brand-header {
background: var(--color-primary-light); border-left: 4px solid var(--color-primary);
border-radius: 0 var(--radius) var(--radius) 0;
padding: 10px 14px; margin: 16px 0 0;
}
.brand-text { margin: 0; font-size: 0.875rem; color: var(--color-primary-hover); line-height: 1.5; }
/* ── Consent screen ── */
.consent-container { max-width: 720px; margin: 0 auto; padding: 32px 16px 80px; }
.consent-title { font-size: 1.5rem; font-weight: 700; margin: 0 0 24px; }
.consent-section { margin-bottom: 24px; }
.consent-section-title { font-size: 1.0625rem; font-weight: 700; margin: 0 0 10px; }
.consent-list { margin: 8px 0 0; padding-left: 20px; line-height: 1.7; display: flex; flex-direction: column; gap: 8px; }
.consent-anon-box {
background: var(--color-sensitive); border: 1px solid var(--color-sensitive-border);
border-radius: var(--radius); padding: 14px 16px; margin-bottom: 24px;
font-size: 0.9375rem; line-height: 1.6;
}
.consent-checkboxes { margin-bottom: 28px; display: flex; flex-direction: column; gap: 14px; }
.consent-checkbox-label {
display: flex; align-items: flex-start; gap: 12px;
padding: 14px 16px; border: 1.5px solid var(--color-border); border-radius: var(--radius);
cursor: pointer; transition: border-color 0.15s, background 0.15s; line-height: 1.5;
}
.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-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); }
.btn-disabled { opacity: 0.5; cursor: not-allowed; }
/* ── Thank you / error screens ── */
.thank-you-container, .error-container { max-width: 560px; margin: 80px auto; padding: 0 16px; }
.thank-you-title, .error-title { font-size: 1.5rem; font-weight: 700; margin: 0 0 16px; }
.thank-you-body { color: var(--color-text-muted); line-height: 1.7; margin: 0 0 12px; }
/* ── No survey state ── */ /* ── No survey state ── */
.no-survey { .no-survey {
max-width: 480px; max-width: 480px;
@@ -419,6 +505,314 @@ body {
.btn { width: 100%; } .btn { width: 100%; }
} }
/* ── Survey header (marca RE) ── */
.survey-header-bar {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 24px;
position: sticky;
top: 0;
z-index: 20;
}
.survey-header-logo { display: block; height: 48px; width: auto; flex-shrink: 0; }
.survey-header-title {
color: var(--color-re-white);
font-weight: 700;
font-size: 1rem;
letter-spacing: 0.08em;
text-transform: uppercase;
line-height: 1.2;
}
/* ── Survey footer ── */
.survey-footer-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px;
margin-top: 40px;
}
.survey-footer-group {
display: flex;
align-items: center;
gap: 8px;
}
.survey-footer-label {
color: rgba(255,255,255,0.7);
font-size: 0.75rem;
white-space: nowrap;
}
.survey-footer-logo { height: 32px; width: auto; display: block; }
/* ══════════════════════════════════════════════════════════════
DASHBOARD — tema oscuro premium
Fondo: #0F1923 · Widgets: #1A2535 · Teal: #43BBC8
══════════════════════════════════════════════════════════════ */
.db-page {
display: flex; min-height: 100vh;
background: #0F1923; color: #E7E6E5;
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
}
.db-main { flex: 1; padding: 24px; overflow-x: hidden; }
.db-page-header { margin-bottom: 20px; }
.db-page-title { font-size: 1.375rem; font-weight: 700; color: #fff; margin: 0 0 4px; }
.db-page-subtitle { font-size: 0.8125rem; color: #9ca3af; margin: 0; }
.db-active-filter { color: var(--color-re-teal); font-weight: 600; }
.db-survey-nav { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.db-survey-link {
font-size: 0.75rem; font-weight: 600; padding: 4px 12px; border-radius: 999px;
border: 1px solid rgba(67,187,200,0.3); color: #9ca3af; text-decoration: none;
transition: all 0.2s ease;
}
.db-survey-link:hover { color: #E7E6E5; border-color: var(--color-re-teal); }
.db-survey-link-active { background: var(--color-re-teal); color: #0F1923; border-color: var(--color-re-teal); }
.db-section { margin-bottom: 16px; }
.db-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 12px;
}
/* ── Widget shell ── */
.db-widget {
background: #1A2535;
border: 1px solid rgba(67,187,200,0.2);
border-radius: 10px;
overflow: hidden;
transition: box-shadow 0.3s ease;
}
.db-widget:hover { box-shadow: 0 0 20px rgba(67,187,200,0.18); }
.db-widget-header {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 16px 8px;
}
.db-widget-title { font-size: 0.875rem; font-weight: 600; color: #E7E6E5; margin: 0; }
.db-widget-n { font-size: 0.75rem; color: #9ca3af; }
.db-widget-body { padding: 0 16px 16px; }
/* ── Suppressed / loading states ── */
.db-suppressed {
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 100px; text-align: center; color: #9ca3af; gap: 6px;
}
.db-suppressed-icon { font-size: 1.5rem; }
.db-suppressed-hint { font-size: 0.75rem; color: #6b7280; }
.db-suppressed-card {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 4px; color: #6b7280;
}
.db-skeleton-block { background: #1e2d42; border-radius: 6px; animation: pulse 1.4s ease-in-out infinite; }
.db-error { color: #ef4444; font-size: 0.875rem; padding: 12px; }
.db-loading-page { padding: 24px; background: #0F1923; min-height: 100vh; }
/* ── KPI row ── */
.db-kpi-row {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
padding: 0;
}
.db-kpi-card {
background: #1A2535; border: 1px solid rgba(67,187,200,0.2);
border-radius: 10px; padding: 16px; text-align: center;
display: flex; flex-direction: column; gap: 4px;
transition: box-shadow 0.3s;
}
.db-kpi-card:hover { box-shadow: 0 0 20px rgba(67,187,200,0.18); }
/* Título de KPI: mayúsculas, bold, siempre visible */
.db-kpi-title { font-size: 0.8125rem; font-weight: 700; color: #E7E6E5; text-transform: uppercase; letter-spacing: 0.04em; }
/* Subtítulo de KPI: código de pregunta + descripción */
.db-kpi-subtitle { font-size: 0.6875rem; color: #7A8FA0; font-weight: 400; line-height: 1.4; margin-bottom: 4px; }
.db-kpi-value {
font-size: 2rem; font-weight: 700; color: var(--color-re-teal);
font-variant-numeric: tabular-nums; margin-top: 4px;
}
.db-kpi-value-suppressed { font-size: 1.25rem; color: #6b7280; }
.db-kpi-note { font-size: 0.75rem; color: #9ca3af; }
.db-kpi-suppressed-block { display: flex; flex-direction: column; align-items: center; gap: 4px; margin-top: 8px; }
/* Compat — se puede quitar si ya no se usa */
.db-kpi-label { font-size: 0.8125rem; color: #E7E6E5; }
.db-kpi-sub { font-size: 0.75rem; color: #9ca3af; }
/* Subtítulo de widget-shell */
.db-widget-title-group { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.db-widget-subtitle { font-size: 0.75rem; color: #7A8FA0; font-weight: 400; margin: 0; line-height: 1.4; }
/* ── Mapa tile ── */
.db-map-wrapper { position: relative; }
.db-map-tooltip {
position: absolute; top: -28px; left: 50%; transform: translateX(-50%);
background: #0F1923; color: #E7E6E5; font-size: 0.75rem;
padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(67,187,200,0.3);
white-space: nowrap; z-index: 10; pointer-events: none;
}
.db-tile-grid {
display: grid; grid-template-columns: repeat(5, 1fr); gap: 3px;
grid-template-rows: repeat(6, 1fr); min-height: 220px;
}
.db-tile {
border: none; border-radius: 4px; cursor: pointer; padding: 4px 2px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
transition: filter 0.15s, outline 0.15s;
min-height: 36px;
}
.db-tile:hover { filter: brightness(1.2); }
.db-tile:focus-visible { outline: 2px solid var(--color-re-teal); outline-offset: 1px; }
.db-tile-selected { outline: 2px solid #fff; }
.db-tile-suppressed { background-image: repeating-linear-gradient(
45deg, transparent, transparent 4px, rgba(255,255,255,0.06) 4px, rgba(255,255,255,0.06) 8px
) !important; }
.db-tile-name { font-size: 0.5rem; color: #fff; text-align: center; line-height: 1.2; font-weight: 600; }
.db-tile-pct { font-size: 0.6rem; color: rgba(255,255,255,0.85); font-weight: 700; }
/* ── Donut ── */
.db-donut-wrapper { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.db-donut-svg { width: 130px; height: 130px; flex-shrink: 0; }
.db-donut-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.db-legend-item { display: flex; align-items: center; gap: 6px; font-size: 0.75rem; }
.db-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.db-legend-val { margin-left: auto; font-weight: 600; color: var(--color-re-teal); }
/* ── Gauge ── */
.db-gauge-wrapper { text-align: center; }
.db-gauge-svg { width: 100%; max-width: 160px; }
.db-gauge-zona { font-size: 0.9rem; font-weight: 700; margin: 4px 0 2px; }
.db-gauge-hint { font-size: 0.7rem; color: #6b7280; margin: 0; }
/* ── Bar chart ── */
.db-bar-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.db-bar-item { display: flex; align-items: center; gap: 8px; }
.db-bar-label { font-size: 0.75rem; color: #E7E6E5; width: 180px; flex-shrink: 0; line-height: 1.3; }
.db-bar-track { flex: 1; background: #2a3547; border-radius: 4px; height: 14px; overflow: hidden; }
.db-bar-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; }
.db-bar-pct { font-size: 0.75rem; color: var(--color-re-teal); width: 40px; text-align: right; font-weight: 600; }
/* ── Heatmap / hexbin shared ── */
.db-heatmap-scroll { overflow-x: auto; }
.db-heatmap-grid { display: grid; gap: 2px; }
.db-heatmap-header {
font-size: 0.625rem; color: #9ca3af; text-align: center; padding: 2px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.db-heatmap-rowlabel { font-size: 0.625rem; color: #9ca3af; padding: 2px 4px; line-height: 1.2; }
.db-heatmap-cell {
border-radius: 3px; min-height: 32px; display: flex; align-items: center; justify-content: center;
font-size: 0.7rem; color: rgba(255,255,255,0.8); transition: filter 0.15s;
}
.db-heatmap-cell:hover { filter: brightness(1.2); }
.db-hexbin-note { font-size: 0.75rem; color: #9ca3af; margin-bottom: 8px; }
/* ── Pirámide ── */
.db-piramide { display: flex; flex-direction: column; gap: 6px; }
.db-piramide-legend { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.db-piramide-row { display: flex; align-items: center; gap: 6px; }
.db-piramide-label { font-size: 0.7rem; color: #9ca3af; width: 90px; flex-shrink: 0; text-align: right; }
.db-piramide-bars { flex: 1; display: flex; gap: 2px; }
.db-piramide-bar-wrap { display: flex; flex-direction: column; }
.db-piramide-bar { height: 20px; border-radius: 3px; min-width: 4px; transition: width 0.4s; }
.db-piramide-n { font-size: 0.7rem; color: #9ca3af; width: 24px; text-align: right; }
/* ── Tabla riesgo ── */
.db-table-wrapper { overflow-x: auto; }
.db-table-note { font-size: 0.75rem; color: #9ca3af; margin-bottom: 10px; }
.db-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
.db-table th { color: #9ca3af; font-weight: 600; text-align: left; padding: 8px 12px; border-bottom: 1px solid #2a3547; }
.db-table td { padding: 10px 12px; border-bottom: 1px solid #1e2d42; color: #E7E6E5; }
.db-table tr:last-child td { border-bottom: none; }
.db-risk-badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 0.75rem; font-weight: 700; color: #fff; }
/* ── Sidebar ── */
.db-sidebar {
width: 220px; flex-shrink: 0; background: #14202e;
border-right: 1px solid rgba(67,187,200,0.15);
padding: 12px; transition: width 0.3s ease;
display: flex; flex-direction: column; gap: 8px;
}
.db-sidebar-collapsed { width: 52px; }
.db-sidebar-toggle {
background: none; border: 1px solid rgba(67,187,200,0.3);
color: var(--color-re-teal); border-radius: 6px; padding: 6px;
cursor: pointer; font-size: 0.75rem; white-space: nowrap;
}
.db-sidebar-content { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; }
.db-filter-group { display: flex; flex-direction: column; gap: 4px; }
.db-filter-label { font-size: 0.75rem; font-weight: 600; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em; }
.db-filter-select {
background: #1e2d42; border: 1px solid #2a3547; color: #E7E6E5;
border-radius: 6px; padding: 6px 8px; font-size: 0.8125rem; width: 100%;
}
.db-filter-check { display: flex; align-items: center; gap: 6px; font-size: 0.75rem; color: #E7E6E5; cursor: pointer; }
.db-filter-check input { accent-color: var(--color-re-teal); width: 14px; height: 14px; }
.db-filter-reset {
background: rgba(67,187,200,0.1); border: 1px solid rgba(67,187,200,0.3);
color: var(--color-re-teal); border-radius: 6px; padding: 8px;
cursor: pointer; font-size: 0.8125rem; font-weight: 600;
transition: background 0.15s;
}
.db-filter-reset:hover { background: rgba(67,187,200,0.2); }
/* ── Admin: control de respuestas (Etapa 3V) ── */
.admin-page { max-width: 1100px; margin: 0 auto; padding: 24px; }
.admin-header {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 16px; margin-bottom: 16px; flex-wrap: wrap;
}
.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-table-wrapper {
overflow-x: auto; background: var(--color-surface); border: 1px solid var(--color-border);
border-radius: var(--radius); box-shadow: var(--shadow-card);
}
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
.admin-table th {
position: sticky; top: 0; background: var(--color-re-dark); color: #fff;
font-weight: 600; text-align: left; padding: 10px 12px; white-space: nowrap;
}
.admin-table td { padding: 8px 12px; border-bottom: 1px solid var(--color-border); color: var(--color-text); vertical-align: top; }
.admin-table tbody tr:last-child td { border-bottom: none; }
.admin-table tbody tr.admin-group-even { background: var(--color-bg); }
.admin-table tbody tr.admin-group-start td { border-top: 2px solid var(--color-re-dark); }
.admin-table tbody tr:hover { background: var(--color-primary-light); }
.admin-prompt { color: var(--color-text); }
.admin-row-toggle { cursor: pointer; }
.admin-toggle-icon {
display: inline-block; width: 14px; margin-right: 4px;
color: var(--color-text-muted); font-size: 0.7rem;
}
.admin-nodata {
display: inline-block; padding: 1px 8px; border-radius: 10px;
background: var(--color-re-light); color: var(--color-re-dark);
opacity: 0.6; font-size: 0.7rem; font-style: italic;
}
.admin-empty { padding: 24px; text-align: center; color: var(--color-text-muted); }
.admin-error { color: var(--color-error); font-weight: 600; }
.admin-error-detail {
background: var(--color-error-bg); border-radius: var(--radius);
padding: 12px; font-family: 'Courier New', monospace; font-size: 0.75rem;
overflow-x: auto;
}
/* ── Responsive ── */
@media (max-width: 768px) {
.db-kpi-row { grid-template-columns: repeat(2,1fr); }
.db-sidebar { width: 100%; border-right: none; border-bottom: 1px solid rgba(67,187,200,0.15); }
.db-page { flex-direction: column; }
.admin-col-optional { display: none; }
}
/* ── Reduced motion ── */ /* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }

View File

@@ -1,5 +1,7 @@
import type { Metadata, Viewport } from 'next' import type { Metadata, Viewport } from 'next'
import './globals.css' import './globals.css'
import { SurveyHeader } from '@/components/survey-header'
import { SurveyFooter } from '@/components/survey-footer'
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Mapeo de Cuidadores', title: 'Mapeo de Cuidadores',
@@ -17,7 +19,11 @@ export const viewport: Viewport = {
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="es"> <html lang="es">
<body>{children}</body> <body>
<SurveyHeader />
<div className="layout-content">{children}</div>
<SurveyFooter />
</body>
</html> </html>
) )
} }

View File

@@ -1,21 +1,27 @@
import { createClient } from '@/lib/supabase' import { createClient } from '@/lib/supabase'
import type { Question, Survey } from '@/lib/types' import type { BrandConfig, Question, Survey } from '@/lib/types'
import { SurveyForm } from '@/components/survey-form' import { SurveyForm } from '@/components/survey-form'
// Force dynamic rendering — page fetches from Supabase at request time. // Force dynamic rendering — page fetches from Supabase at request time.
// Never statically generated: credentials live in .env.local (runtime only).
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
// Server Component — fetches survey with anon key; RLS ensures only status='live' is returned. // In Next.js 15, searchParams is a Promise and must be awaited.
export default async function Page() { interface PageProps {
searchParams: Promise<{ s?: string }>
}
export default async function Page({ searchParams }: PageProps) {
const { s: surveyId } = await searchParams
const supabase = createClient() const supabase = createClient()
// 1. Fetch the live survey // 1. Fetch survey — by ?s=UUID if provided, otherwise first live survey
const { data: surveyRow, error: surveyError } = await supabase const surveyQuery = supabase
.from('surveys') .from('surveys')
.select('id, title, status, is_anonymous, k_threshold') .select('id, org_id, title, status, is_anonymous, k_threshold, consent_version_id, brand_config')
.eq('status', 'live')
.maybeSingle() const { data: surveyRow, error: surveyError } = surveyId
? await surveyQuery.eq('id', surveyId).maybeSingle()
: await surveyQuery.eq('status', 'live').maybeSingle()
if (surveyError) { if (surveyError) {
return ( return (
@@ -32,10 +38,11 @@ export default async function Page() {
<div className="no-survey" role="main"> <div className="no-survey" role="main">
<h1 className="no-survey-title">No hay encuesta activa</h1> <h1 className="no-survey-title">No hay encuesta activa</h1>
<p> <p>
Para ver el formulario, la encuesta debe estar en estado <code>live</code>. Para ver el formulario, la encuesta debe estar en estado <code>live</code>,
o pasá el UUID por parámetro: <code>?s=UUID</code>.
</p> </p>
<pre className="no-survey-hint"> <pre className="no-survey-hint">
{`-- Activar la encuesta en Supabase Cloud:\nUPDATE surveys\nSET status = 'live'\nWHERE id = '30000000-0000-0000-0000-000000000001';`} {`-- Activar en Supabase Cloud:\nUPDATE surveys SET status = 'live'\nWHERE id = '30000000-0000-0000-0000-000000000001';`}
</pre> </pre>
</div> </div>
) )
@@ -61,20 +68,19 @@ export default async function Page() {
const questions = questionRows as Question[] const questions = questionRows as Question[]
// 3. Log question count for validation (visible in server logs and browser Network tab) console.log(`[2B] Survey loaded: id=${surveyRow.id} | questions=${questions.length}`)
console.log(
`[2A] Survey loaded: id=${surveyRow.id} | questions=${questions.length}`
)
const survey: Survey = { const survey: Survey = {
...(surveyRow as Omit<Survey, 'questions'>), id: surveyRow.id,
org_id: surveyRow.org_id,
title: surveyRow.title,
status: surveyRow.status,
is_anonymous: surveyRow.is_anonymous,
k_threshold: surveyRow.k_threshold,
consent_version_id: surveyRow.consent_version_id,
brand_config: (surveyRow.brand_config as BrandConfig) ?? null,
questions, questions,
} }
return ( return <SurveyForm survey={survey} questions={questions} />
<SurveyForm
survey={survey}
questions={questions}
/>
)
} }

BIN
assets/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/fundacion/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,380 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="535.614px" height="308.44px" viewBox="0 0 535.614 308.44" enable-background="new 0 0 535.614 308.44"
xml:space="preserve">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#129539" d="M181.184,99.452c-0.34-0.962-0.875-1.782-1.596-2.409
c-0.729-0.643-1.628-1.059-2.608-1.183c-2.093-0.275-4.58,0.708-6.829,3.731c-1.76,1.805-4.482,5.509-7.164,9.176
c-2.851,3.915-5.667,7.738-7.12,9.029c-0.727,0.664-1.556,0.967-2.206,0.945c-0.281-0.027-0.548-0.098-0.732-0.238h-0.021
c-0.165-0.14-0.303-0.323-0.4-0.588c-0.167-0.584-0.046-1.432,0.492-2.549c2.257-4.683,6.057-12.303,9.529-19.074
c2.979-5.827,5.676-11.001,6.939-13.166c2.279-3.803,2.374-7.205,1.248-9.457c-0.494-0.939-1.199-1.711-2.065-2.182
c-0.843-0.491-1.855-0.723-2.933-0.604c-2.441,0.232-5.228,2.177-7.482,6.47c-2.673,5.163-5.185,9.78-7.674,14.381
c-2.981,5.466-5.913,10.86-8.935,16.779c-0.521,0.815-1.193,1.426-1.828,1.701c-0.289,0.124-0.524,0.194-0.737,0.151
c-0.162-0.027-0.319-0.124-0.44-0.308c-0.259-0.324-0.421-0.913-0.421-1.831c-0.027-3.052,3.124-12.577,5.627-20.154
c1.037-3.191,1.971-6.021,2.536-7.933c0.891-3.116,2.374-8.484,3.521-12.945c1.032-3.937,1.828-7.177,1.828-7.625
c-0.021-2.23-0.516-3.916-1.291-5.114c-0.748-1.129-1.709-1.81-2.819-2.128c-1.075-0.34-2.252-0.286-3.391,0.043
c-1.512,0.47-2.962,1.432-4.094,2.787c-1.755,2.139-4.102,10.217-6.045,16.968l-0.608,2.058c-0.542,1.879-1.574,5.59-2.724,9.764
c-2.09,7.646-4.623,16.871-5.422,19.031c-0.373,1.053-1.01,1.798-1.685,2.149c-0.275,0.146-0.591,0.216-0.851,0.194
c-0.232-0.027-0.438-0.124-0.632-0.308c-0.395-0.378-0.702-1.097-0.748-2.252c-0.327-5.654,0.256-13.118,0.845-20.506
c0.491-6.194,0.958-12.313,0.915-17.319c0.235-3.32-0.842-5.643-2.468-6.934c-0.775-0.632-1.685-1.01-2.646-1.156
c-0.942-0.135-1.928-0.021-2.86,0.324c-2.068,0.756-3.896,2.685-4.601,5.676c-1.269,5.455-1.925,13.686-2.533,21.311
c-0.588,7.463-1.15,14.332-2.16,17.222c-0.448,1.215-1.199,3.24-2.136,4.854c-0.821,1.383-1.736,2.506-2.536,2.35
c-2.533-0.48-2.859-2.587-3.14-4.396c-0.046-0.389-0.119-0.793-0.189-1.16c-0.235-1.912-0.603-3.904-1.078-5.801
c-0.448-1.922-1.007-3.774-1.593-5.4c-1.064-2.835-3.005-5.551-5.239-7.344c-1.08-0.838-2.249-1.496-3.416-1.831
c-1.226-0.318-2.468-0.318-3.664,0.118c-1.013,0.373-1.974,1.086-2.841,2.166c-2.136,2.674-0.632,6.658,1.288,11.849
c1.95,5.228,4.393,11.751,3.832,19.042c-0.127,1.522-0.24,3.057-0.383,4.53c-0.762,9.202-1.38,17.33-0.148,26.965
c1.045,0.34,2.092,0.681,3.181,1.074c0,0-0.256-0.967,1.248-11.789c2.787-20.073,27.423-29.054,45.787-18.923
c12.418,6.854,17.333,18.696,18.124,31.619c3.98-6.604,6.618-12.285,7.59-15.429c0.443-1.501,2.959-5.396,6-9.808
c4.577-6.62,10.398-14.321,12.461-16.8C181.532,104.62,181.951,101.683,181.184,99.452z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#163F75" d="M153.942,161.811c-0.454,0-0.856,0-1.302,0
c-0.934,8.527-3.151,17-5.765,24.361c-8.868,25.03-8.111,45.039-45.396,37.861c-11.679-2.246-24.604-4.73-36.032-8.883
c14.938,42.295,79.458,45.109,90.397,6.248C159.669,207.789,161.961,181.332,153.942,161.811z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#E00914" d="M91.254,160.644c3.381,2.273-0.47-4.86,2.627-17.275
c4.412-17.567,20.135-29.179,36.528-20.128c21.699,11.968,17.783,40.968,10.744,60.857c-7.925,22.357-7.272,40.238-40.563,33.833
c-26.197-5.065-59.521-11.405-60.733-39.601C39.311,165.963,57.051,133.426,91.254,160.644L91.254,160.644z"/>
<path fill="#E00914" d="M199.191,221.652c4.323,0,7.555-1.291,9.834-3.916c1.733-1.943,2.6-4.504,2.6-7.604
c0-3.37-1.288-6.658-3.823-9.872c-1.167-1.458-2.487-2.884-3.848-4.271c-3.378-3.327-5.77-6.118-7.131-8.489
c-1.547-2.582-2.301-5.298-2.301-8.182c0-3.041,0.548-5.471,1.577-7.302c1.245-2.208,3.07-3.326,5.422-3.326
c3.256,0,5.365,1.998,6.421,5.908c0.467,1.755,0.678,3.71,0.678,5.789c0,2.819-0.278,5.54-0.842,8.235
c-0.07,0.384-0.116,0.751-0.116,1.129c0,1.388,0.681,2.09,2.063,2.09c1.528,0,2.605-1.388,3.243-4.175
c0.488-2.024,0.721-4.46,0.721-7.279c0-11.276-4.404-16.888-13.25-16.888c-4.226,0-7.625,1.167-10.158,3.56
c-1.312,1.22-2.344,2.689-3.075,4.438c-1.007,2.463-1.526,5.282-1.526,8.377c0,4.525,1.196,8.678,3.621,12.383
c1.658,2.581,4.072,5.465,7.345,8.635c1.142,1.178,2.228,2.279,3.227,3.381c2.182,2.42,3.238,4.672,3.238,6.756
c0,3.262-1.48,4.903-4.429,4.903c-2.651,0-4.366-1.388-5.076-4.196c-0.33-1.145-0.486-2.327-0.486-3.45
c0-1.858-0.751-2.792-2.252-2.792c-1.645,0-2.671,1.571-2.671,3.64c0,3.656,0.772,6.54,2.387,8.646
C192.557,220.361,195.419,221.652,199.191,221.652L199.191,221.652z M234.038,216.235c-3.008,0-5.238-1.755-6.71-5.249
c-1.296-3.121-1.947-7.345-1.947-12.691c0-10.963,3.138-16.428,9.402-16.428c2.889,0,4.95,1.782,6.267,5.373
c1.013,2.781,1.499,6.243,1.499,10.461c0,5.093-0.635,9.321-1.877,12.669C239.192,214.291,236.989,216.235,234.038,216.235
L234.038,216.235z M231.667,221.587c5.819,0,10.134-2.278,12.853-6.848c2.39-4.062,3.591-9.612,3.591-16.703
c0-5.563-0.872-10.164-2.632-13.749c-2.417-4.926-6.189-7.394-11.395-7.394c-6.011,0-10.487,2.273-13.425,6.874
c-2.603,3.991-3.918,9.451-3.918,16.348c0,5.897,1.034,10.66,3.124,14.365C222.519,219.238,226.48,221.587,231.667,221.587
L231.667,221.587z M263.008,221.728c0.889,0,1.974-0.141,3.211-0.491c1.218-0.33,2.233-0.768,2.983-1.41
c0.554-0.47,0.861-0.988,0.861-1.62c0-0.794-0.486-1.198-1.499-1.198c-0.324,0-0.821,0.075-1.477,0.265
c-0.426,0.118-0.918,0.184-1.474,0.184c-1.855,0-2.187-1.523-2.187-9.219v-38.548c0-1.62-0.284-2.9-0.77-3.915
c-0.732-1.523-2.001-2.274-3.751-2.274c-3.103,0-4.137,1.707-4.137,6.497v33.126c0,2.933,0.076,5.697,0.262,8.214
c0.21,2.717,0.772,4.925,1.663,6.681C257.958,220.485,260.049,221.728,263.008,221.728L263.008,221.728z M279.279,174.36
c1.653,0,2.825-0.61,3.594-1.755c0.545-0.777,0.805-1.717,0.805-2.797c0-1.193-0.4-2.317-1.205-3.327
c-0.767-1.01-1.831-1.534-3.122-1.534c-2.53,0-4.331,2.215-4.331,5.331C275.021,173,276.447,174.36,279.279,174.36L279.279,174.36z
M279.123,221.652c3.019,0,4.556-2.08,4.556-6.238v-32.023c0-4.828-1.129-6.497-4.129-6.497c-2.983,0-4.528,2.16-4.528,6.199v32.02
c0,1.102,0.089,2.133,0.232,3.143C275.583,220.556,276.849,221.652,279.123,221.652L279.123,221.652z M309.905,193.721
c0,2.9-0.237,5.936-0.683,9.073c-0.421,3.1-1.08,5.746-1.947,7.884c-1.571,3.965-3.542,5.936-5.935,5.936
c-3.029,0.162-3.988-6.729-3.988-14.009c0-4.963,1.482-20.43,8.587-20.43c1.949,0,3.138,0.47,3.634,1.447
c0.211,0.476,0.332,1.037,0.332,1.642V193.721z M318.535,169.997c0-3.208-0.138-6.497-4.131-6.497c-3.467,0-4.499,3.009-4.499,6.189
v7.458c0,0.572-0.078,0.875-0.259,0.875c-0.189,0-0.5-0.119-0.867-0.303c-1.083-0.572-2.279-0.826-3.521-0.826
c-4.698,0-8.352,2.673-11.006,8.073c-2.481,5.109-3.729,11.806-3.729,20.035c0,8.83,3.238,16.796,9.602,16.65
c3.375,0,6.186-1.756,8.435-5.229c0.662-1.068,1.04-1.642,1.153-1.642s0.191,0.232,0.191,0.632c0,3.191,1.032,6.238,4.499,6.238
c3.994,0,4.131-3.278,4.131-6.54V169.997z M348.777,221.652c3.964,0.075,4.061-3.322,4.129-6.54v-31.722
c-0.068-3.139-0.165-6.563-4.129-6.497c-1.385,0-2.443,0.427-3.075,1.226c-0.208,0.281-0.462,0.416-0.815,0.416
c-0.241,0-0.6-0.107-0.97-0.329c-1.501-0.864-2.951-1.313-4.355-1.313c-4.72,0-8.387,2.673-11.03,8.073
c-2.484,5.109-3.729,11.806-3.729,20.035c0,8.83,3.265,16.796,9.597,16.65c3.794,0,6.826-2.08,9.121-6.26
c0.259-0.496,0.448-0.729,0.567-0.729c0.116,0,0.167,0.275,0.167,0.75C344.254,218.605,345.299,221.652,348.777,221.652
L348.777,221.652z M335.616,216.613c-3.027,0.162-3.991-6.729-3.991-14.009c0-4.963,1.477-20.43,8.586-20.43
c2.7,0,4.042,0.967,4.042,2.911v10.342c0,1.879-0.216,4.336-0.664,7.366c-0.467,3.029-1.128,5.681-2.042,7.884
C339.999,214.643,338.006,216.613,335.616,216.613L335.616,216.613z M365.996,221.652c3.027,0,4.525-2.08,4.525-6.238v-9.996
c0-6.988,0.889-12.664,2.649-17c1.763-4.391,3.78-6.551,5.986-6.551c0.651,0,1.212,0.211,1.712,0.653
c0.632,0.572,1.24,0.87,1.828,0.87c1.62,0,2.887-1.664,2.887-3.44c0-1.977-2.322-3.057-5.16-3.057c-3.705,0-6.731,2.068-9.081,6.199
c-0.259,0.486-0.489,0.751-0.629,0.751c-0.122,0-0.192-0.265-0.192-0.805c0-4.104-1.499-6.146-4.525-6.146
c-3.894,0-4.128,3.262-4.128,6.448v31.771c0,1.069,0.097,2.133,0.235,3.143C362.429,220.556,363.739,221.652,365.996,221.652
L365.996,221.652z M393.254,174.36c1.645,0,2.841-0.61,3.616-1.755c0.516-0.777,0.799-1.717,0.799-2.797
c0-1.193-0.427-2.317-1.207-3.327c-0.789-1.01-1.823-1.534-3.138-1.534c-2.533,0-4.315,2.215-4.315,5.331
C389.01,173,390.414,174.36,393.254,174.36L393.254,174.36z M393.12,221.652c2.997,0,4.55-2.08,4.55-6.238v-32.023
c0-4.828-1.134-6.497-4.137-6.497c-2.997,0-4.523,2.16-4.523,6.199v32.02c0,1.102,0.067,2.133,0.229,3.143
C389.571,220.556,390.838,221.652,393.12,221.652L393.12,221.652z M423.867,193.721c0,2.9-0.205,5.936-0.656,9.073
c-0.418,3.1-1.077,5.746-1.95,7.884c-1.566,3.965-3.534,5.936-5.951,5.936c-3.029,0.162-3.999-6.729-3.999-14.009
c0-4.963,1.51-20.43,8.619-20.43c1.917,0,3.122,0.47,3.613,1.447c0.235,0.476,0.324,1.037,0.324,1.642V193.721z M432.529,169.997
c0-3.208-0.167-6.497-4.128-6.497c-3.473,0-4.534,3.009-4.534,6.189v7.458c0,0.572-0.043,0.875-0.254,0.875
c-0.184,0-0.467-0.119-0.867-0.303c-1.083-0.572-2.255-0.826-3.494-0.826c-4.72,0-8.378,2.673-11.003,8.073
c-2.511,5.109-3.731,11.806-3.731,20.035c0,8.83,3.237,16.796,9.575,16.65c3.4,0,6.187-1.756,8.444-5.229
c0.675-1.068,1.053-1.642,1.169-1.642c0.119,0,0.162,0.232,0.162,0.632c0,3.191,1.061,6.238,4.534,6.238
c3.961,0,4.128-3.278,4.128-6.54V169.997z M462.744,221.652c3.985,0.075,4.058-3.322,4.15-6.54v-31.722
c-0.092-3.139-0.165-6.563-4.15-6.497c-1.364,0-2.417,0.427-3.049,1.226c-0.21,0.281-0.494,0.416-0.823,0.416
c-0.257,0-0.607-0.107-0.961-0.329c-1.501-0.864-2.957-1.313-4.391-1.313c-4.714,0-8.373,2.673-10.995,8.073
c-2.492,5.109-3.737,11.806-3.737,20.035c0,8.83,3.24,16.796,9.578,16.65c3.82,0,6.848-2.08,9.121-6.26
c0.259-0.496,0.454-0.729,0.567-0.729c0.119,0,0.178,0.275,0.178,0.75C458.232,218.605,459.271,221.652,462.744,221.652
L462.744,221.652z M449.602,216.613c-3.024,0.162-4.001-6.729-4.001-14.009c0-4.963,1.499-20.43,8.605-20.43
c2.697,0,4.026,0.967,4.026,2.911v10.342c0,1.879-0.203,4.336-0.648,7.366c-0.467,3.029-1.131,5.681-2.044,7.884
C453.974,214.643,452,216.613,449.602,216.613L449.602,216.613z M492.913,193.721c0,2.9-0.208,5.936-0.656,9.073
c-0.427,3.1-1.08,5.746-1.95,7.884c-1.563,3.965-3.54,5.936-5.935,5.936c-3.027,0.162-4.013-6.729-4.013-14.009
c0-4.963,1.507-20.43,8.614-20.43c1.944,0,3.143,0.47,3.616,1.447c0.232,0.476,0.324,1.037,0.324,1.642V193.721z M501.573,169.997
c0-3.208-0.148-6.497-4.137-6.497c-3.461,0-4.523,3.009-4.523,6.189v7.458c0,0.572-0.048,0.875-0.237,0.875
c-0.203,0-0.492-0.119-0.867-0.303c-1.102-0.572-2.274-0.826-3.519-0.826c-4.717,0-8.373,2.673-11.003,8.073
c-2.484,5.109-3.729,11.806-3.729,20.035c0,8.83,3.24,16.796,9.57,16.65c3.405,0,6.224-1.756,8.449-5.229
c0.681-1.068,1.053-1.642,1.169-1.642c0.119,0,0.167,0.232,0.167,0.632c0,3.191,1.062,6.238,4.523,6.238
c3.988,0,4.137-3.278,4.137-6.54V169.997z"/>
<path fill="#2A2A2B" d="M190.7,230.309v11.546h-1.399v-11.546H190.7z M194.639,236.648v5.206h-1.334v-8.651h1.288v1.361h0.122
c0.213-0.454,0.516-0.805,0.913-1.086c0.424-0.265,0.964-0.395,1.62-0.395c0.864,0,1.574,0.275,2.111,0.816
c0.54,0.518,0.821,1.365,0.821,2.462v5.492h-1.336v-5.4c0-0.702-0.184-1.215-0.54-1.587c-0.348-0.4-0.824-0.589-1.458-0.589
c-0.632,0-1.145,0.21-1.569,0.637C194.853,235.314,194.639,235.903,194.639,236.648L194.639,236.648z M206.377,233.203v1.123h-1.855
v5.045c0,0.399,0.07,0.652,0.165,0.842c0.122,0.194,0.256,0.335,0.427,0.378c0.181,0.07,0.367,0.098,0.556,0.098
c0.138,0,0.281,0,0.375-0.027c0.092-0.026,0.162-0.026,0.213-0.043l0.281,1.188c-0.097,0.048-0.211,0.069-0.381,0.118
c-0.159,0.021-0.373,0.043-0.629,0.043c-0.378,0-0.721-0.064-1.099-0.237c-0.356-0.156-0.656-0.41-0.894-0.751
c-0.24-0.318-0.351-0.739-0.351-1.231v-5.422h-1.293v-1.123h1.293v-2.073h1.337v2.073H206.377z M211.915,242.044
c-0.826,0-1.55-0.189-2.144-0.557c-0.607-0.389-1.075-0.896-1.41-1.556c-0.324-0.675-0.486-1.457-0.486-2.344
c0-0.863,0.162-1.663,0.486-2.349c0.335-0.675,0.781-1.199,1.364-1.566c0.594-0.41,1.267-0.589,2.071-0.589
c0.442,0,0.885,0.07,1.336,0.232c0.418,0.141,0.845,0.4,1.199,0.724c0.346,0.33,0.653,0.772,0.861,1.34
c0.208,0.545,0.308,1.22,0.308,2.041v0.562h-6.284c0.046,0.918,0.303,1.62,0.789,2.111c0.494,0.497,1.134,0.751,1.909,0.751
c0.535,0,0.978-0.113,1.361-0.324c0.37-0.237,0.651-0.562,0.815-1.036l1.293,0.372c-0.216,0.664-0.61,1.172-1.218,1.583
C213.551,241.833,212.803,242.044,211.915,242.044L211.915,242.044z M209.217,236.838h4.95c0-0.734-0.219-1.334-0.665-1.831
c-0.416-0.47-0.982-0.729-1.706-0.729c-0.524,0-0.967,0.113-1.315,0.351c-0.375,0.238-0.688,0.567-0.894,0.939
C209.377,235.969,209.263,236.395,209.217,236.838L209.217,236.838z M217.54,241.854v-8.651h1.291v1.307h0.076
c0.154-0.416,0.435-0.767,0.861-1.053c0.427-0.254,0.87-0.395,1.41-0.395c0.089,0,0.208,0,0.351,0
c0.143,0.021,0.262,0.021,0.334,0.021v1.367c-0.021-0.033-0.143-0.033-0.311-0.06c-0.146-0.021-0.327-0.044-0.494-0.044
c-0.632,0-1.145,0.189-1.569,0.589c-0.429,0.378-0.604,0.87-0.604,1.458v5.46H217.54z M230.797,233.203l-3.189,8.651h-1.363
l-3.219-8.651h1.455l2.393,6.891h0.097l2.395-6.891H230.797z M235.857,242.044c-0.807,0-1.539-0.189-2.155-0.557
c-0.583-0.389-1.056-0.896-1.382-1.556c-0.33-0.675-0.491-1.457-0.491-2.344c0-0.863,0.162-1.663,0.491-2.349
c0.327-0.675,0.772-1.199,1.361-1.566c0.589-0.41,1.269-0.589,2.068-0.589c0.443,0,0.886,0.07,1.334,0.232
c0.416,0.141,0.848,0.4,1.193,0.724c0.354,0.33,0.665,0.772,0.87,1.34c0.21,0.545,0.305,1.22,0.305,2.041v0.562h-6.286
c0.046,0.918,0.302,1.62,0.796,2.111c0.492,0.497,1.129,0.751,1.896,0.751c0.545,0,0.988-0.113,1.364-0.324
c0.375-0.237,0.659-0.562,0.826-1.036l1.285,0.372c-0.216,0.664-0.607,1.172-1.215,1.583
C237.5,241.833,236.751,242.044,235.857,242.044L235.857,242.044z M233.166,236.838h4.952c0-0.734-0.21-1.334-0.664-1.831
c-0.419-0.47-1.007-0.729-1.704-0.729c-0.521,0-0.967,0.113-1.315,0.351c-0.375,0.238-0.686,0.567-0.891,0.939
C233.333,235.969,233.211,236.395,233.166,236.838L233.166,236.838z M242.83,236.648v5.206h-1.337v-8.651h1.293v1.361h0.089
c0.213-0.454,0.516-0.805,0.939-1.086c0.397-0.265,0.945-0.395,1.596-0.395c0.87,0,1.596,0.275,2.136,0.816
c0.535,0.518,0.793,1.365,0.793,2.462v5.492h-1.333v-5.4c0-0.702-0.162-1.215-0.516-1.587c-0.354-0.4-0.839-0.589-1.453-0.589
c-0.634,0-1.177,0.21-1.593,0.637C243.013,235.314,242.83,235.903,242.83,236.648L242.83,236.648z M254.303,242.044
c-0.821,0-1.526-0.189-2.117-0.578c-0.556-0.384-1.023-0.896-1.334-1.577c-0.327-0.68-0.467-1.452-0.467-2.301
c0-0.891,0.14-1.663,0.467-2.349c0.333-0.675,0.802-1.199,1.383-1.566c0.588-0.41,1.269-0.589,2.049-0.589
c0.607,0,1.14,0.119,1.636,0.352c0.492,0.205,0.889,0.535,1.199,0.935c0.324,0.399,0.519,0.896,0.581,1.437h-1.333
c-0.089-0.405-0.327-0.751-0.659-1.064c-0.346-0.291-0.815-0.464-1.404-0.464c-0.778,0-1.412,0.308-1.874,0.891
c-0.494,0.583-0.729,1.388-0.729,2.371c0,0.982,0.235,1.798,0.708,2.392c0.464,0.616,1.096,0.913,1.896,0.913
c0.513,0,0.964-0.135,1.336-0.394c0.378-0.287,0.613-0.638,0.727-1.129h1.333c-0.062,0.518-0.256,0.988-0.556,1.388
c-0.283,0.421-0.688,0.756-1.15,0.978C255.499,241.925,254.935,242.044,254.303,242.044L254.303,242.044z M259.652,241.854v-8.651
h1.312v8.651H259.652z M260.33,231.767c-0.281,0-0.494-0.092-0.678-0.28c-0.184-0.162-0.284-0.379-0.284-0.633
c0-0.253,0.1-0.469,0.284-0.637c0.184-0.184,0.397-0.248,0.678-0.248c0.256,0,0.472,0.064,0.664,0.248
c0.184,0.168,0.275,0.384,0.275,0.637c0,0.254-0.092,0.471-0.275,0.633C260.802,231.675,260.586,231.767,260.33,231.767
L260.33,231.767z M266.923,242.044c-0.775,0-1.453-0.189-2.039-0.557c-0.591-0.389-1.059-0.896-1.385-1.556
c-0.332-0.675-0.492-1.457-0.492-2.344c0-0.912,0.16-1.685,0.492-2.37c0.327-0.681,0.794-1.199,1.385-1.577
c0.586-0.378,1.264-0.557,2.039-0.557c0.772,0,1.477,0.179,2.065,0.557c0.589,0.378,1.029,0.896,1.364,1.577
c0.324,0.686,0.491,1.458,0.491,2.37c0,0.887-0.167,1.669-0.491,2.344c-0.335,0.659-0.775,1.167-1.364,1.556
C268.4,241.854,267.695,242.044,266.923,242.044L266.923,242.044z M266.923,240.845c0.588,0,1.077-0.156,1.477-0.438
c0.375-0.313,0.651-0.729,0.848-1.226c0.187-0.491,0.284-1.026,0.284-1.594c0-0.588-0.097-1.134-0.284-1.619
c-0.197-0.519-0.473-0.913-0.848-1.227c-0.4-0.291-0.889-0.464-1.477-0.464c-0.586,0-1.075,0.173-1.453,0.464
c-0.408,0.313-0.683,0.708-0.875,1.227c-0.187,0.485-0.256,1.031-0.256,1.619c0,0.567,0.07,1.103,0.256,1.594
c0.192,0.497,0.467,0.912,0.875,1.226C265.849,240.688,266.337,240.845,266.923,240.845L266.923,240.845z M266.313,232.031
l1.382-2.635h1.553l-1.763,2.635H266.313z M274.222,236.648v5.206h-1.342v-8.651h1.288v1.361h0.122
c0.189-0.454,0.491-0.805,0.918-1.086c0.424-0.265,0.937-0.395,1.596-0.395c0.889,0,1.593,0.275,2.128,0.816
c0.542,0.518,0.805,1.365,0.805,2.462v5.492h-1.318v-5.4c0-0.702-0.192-1.215-0.54-1.587c-0.356-0.4-0.848-0.589-1.453-0.589
c-0.629,0-1.172,0.21-1.569,0.637C274.427,235.314,274.222,235.903,274.222,236.648L274.222,236.648z M287.45,245.214
c-0.208,0-0.416,0-0.605-0.049c-0.17-0.021-0.283-0.07-0.359-0.092l0.329-1.177c0.492,0.118,0.894,0.118,1.223-0.027
c0.327-0.113,0.608-0.508,0.84-1.145l0.284-0.821l-3.191-8.7h1.431l2.398,6.891h0.089l2.4-6.891h1.423l-3.702,10.066
C289.51,244.576,288.671,245.214,287.45,245.214L287.45,245.214z M303.8,233.203v1.123h-1.917v7.528h-1.342v-7.528h-1.401v-1.123
h1.401v-1.199c0-0.491,0.122-0.912,0.351-1.242c0.238-0.329,0.516-0.566,0.894-0.729c0.375-0.189,0.775-0.265,1.196-0.265
c0.33,0,0.586,0.032,0.797,0.075c0.216,0.076,0.356,0.125,0.467,0.162l-0.392,1.156c-0.076-0.032-0.173-0.07-0.287-0.098
c-0.119-0.021-0.256-0.049-0.472-0.049c-0.416,0-0.748,0.092-0.935,0.33c-0.189,0.216-0.278,0.54-0.278,0.988v0.869H303.8z
M308.854,242.044c-0.802,0-1.488-0.189-2.071-0.557c-0.589-0.389-1.053-0.896-1.388-1.556c-0.324-0.675-0.486-1.457-0.486-2.344
c0-0.912,0.162-1.685,0.486-2.37c0.335-0.681,0.799-1.199,1.388-1.577c0.583-0.378,1.269-0.557,2.071-0.557
c0.767,0,1.445,0.179,2.031,0.557s1.056,0.896,1.385,1.577c0.334,0.686,0.497,1.458,0.497,2.37c0,0.887-0.162,1.669-0.497,2.344
c-0.33,0.659-0.799,1.167-1.385,1.556C310.299,241.854,309.622,242.044,308.854,242.044L308.854,242.044z M308.854,240.845
c0.575,0,1.072-0.156,1.445-0.438c0.378-0.313,0.653-0.729,0.842-1.226c0.194-0.491,0.286-1.026,0.286-1.594
c0-0.588-0.092-1.134-0.286-1.619c-0.189-0.519-0.464-0.913-0.842-1.227c-0.373-0.291-0.87-0.464-1.445-0.464
c-0.624,0-1.11,0.173-1.488,0.464c-0.37,0.313-0.659,0.708-0.84,1.227c-0.194,0.485-0.283,1.031-0.283,1.619
c0,0.567,0.089,1.103,0.283,1.594c0.181,0.497,0.47,0.912,0.84,1.226C307.745,240.688,308.231,240.845,308.854,240.845
L308.854,240.845z M314.782,241.854v-8.651h1.285v1.307h0.1c0.165-0.416,0.443-0.767,0.867-1.053
c0.396-0.254,0.864-0.395,1.385-0.395c0.095,0,0.211,0,0.375,0c0.135,0.021,0.262,0.021,0.33,0.021v1.367
c-0.049-0.033-0.143-0.033-0.305-0.06c-0.167-0.021-0.33-0.044-0.516-0.044c-0.632,0-1.15,0.189-1.55,0.589
c-0.421,0.378-0.634,0.87-0.634,1.458v5.46H314.782z M320.689,241.854v-8.651h1.299v1.361h0.113c0.195-0.471,0.473-0.821,0.87-1.086
s0.891-0.395,1.445-0.395c0.569,0,1.063,0.13,1.439,0.395s0.678,0.615,0.891,1.086h0.1c0.208-0.454,0.532-0.821,0.977-1.086
c0.448-0.265,0.988-0.395,1.604-0.395c0.767,0,1.404,0.249,1.896,0.734c0.494,0.481,0.746,1.237,0.746,2.242v5.794h-1.334v-5.794
c0-0.633-0.165-1.102-0.519-1.361c-0.351-0.281-0.767-0.421-1.239-0.421c-0.61,0-1.083,0.188-1.41,0.562
c-0.33,0.356-0.513,0.826-0.513,1.383v5.632h-1.336v-5.93c0-0.497-0.165-0.896-0.491-1.183c-0.308-0.291-0.735-0.464-1.223-0.464
c-0.356,0-0.681,0.092-0.988,0.286c-0.278,0.178-0.538,0.442-0.702,0.75c-0.189,0.33-0.286,0.702-0.286,1.14v5.4H320.689z
M337.044,242.065c-0.537,0-1.031-0.119-1.474-0.335c-0.454-0.173-0.821-0.48-1.083-0.886c-0.259-0.394-0.396-0.886-0.396-1.452
c0-0.492,0.121-0.887,0.308-1.193c0.184-0.309,0.443-0.567,0.772-0.729c0.332-0.189,0.699-0.303,1.104-0.399
c0.37-0.092,0.77-0.168,1.194-0.211c0.513-0.064,0.942-0.119,1.271-0.162c0.321-0.021,0.583-0.098,0.721-0.194
c0.165-0.07,0.235-0.227,0.235-0.442v-0.044c0-0.566-0.165-0.988-0.47-1.29c-0.284-0.309-0.751-0.476-1.364-0.476
c-0.629,0-1.129,0.141-1.499,0.427c-0.351,0.281-0.602,0.588-0.748,0.891l-1.272-0.442c0.238-0.54,0.516-0.945,0.918-1.247
c0.378-0.281,0.772-0.486,1.223-0.616c0.464-0.108,0.886-0.179,1.331-0.179c0.286,0,0.594,0.038,0.967,0.092
c0.351,0.065,0.702,0.217,1.05,0.422c0.332,0.194,0.616,0.496,0.851,0.912c0.232,0.405,0.329,0.962,0.329,1.643v5.702h-1.317v-1.166
h-0.07c-0.094,0.178-0.229,0.394-0.448,0.604c-0.21,0.216-0.489,0.395-0.842,0.54C337.981,241.995,337.561,242.065,337.044,242.065
L337.044,242.065z M337.255,240.866c0.518,0,0.961-0.092,1.339-0.297c0.348-0.216,0.632-0.476,0.823-0.799
c0.187-0.335,0.278-0.687,0.278-1.037v-1.215c-0.07,0.069-0.189,0.113-0.373,0.161c-0.189,0.065-0.405,0.119-0.659,0.168
c-0.232,0.016-0.472,0.064-0.705,0.086c-0.235,0.027-0.421,0.049-0.562,0.076c-0.354,0.043-0.686,0.113-0.958,0.211
c-0.313,0.097-0.548,0.254-0.732,0.447c-0.184,0.217-0.281,0.471-0.281,0.795c0,0.469,0.162,0.82,0.51,1.058
C336.294,240.753,336.712,240.866,337.255,240.866L337.255,240.866z M346.971,242.044c-0.799,0-1.507-0.189-2.084-0.578
c-0.6-0.384-1.04-0.896-1.367-1.577c-0.302-0.68-0.467-1.452-0.467-2.301c0-0.891,0.165-1.663,0.491-2.349
c0.332-0.675,0.775-1.199,1.361-1.566c0.589-0.41,1.267-0.589,2.039-0.589c0.613,0,1.172,0.119,1.666,0.352
c0.473,0.205,0.873,0.535,1.194,0.935c0.308,0.399,0.5,0.896,0.591,1.437h-1.337c-0.097-0.405-0.324-0.751-0.678-1.064
c-0.348-0.291-0.823-0.464-1.409-0.464c-0.772,0-1.413,0.308-1.879,0.891c-0.475,0.583-0.705,1.388-0.705,2.371
c0,0.982,0.23,1.798,0.705,2.392c0.467,0.616,1.107,0.913,1.879,0.913c0.516,0,0.958-0.135,1.333-0.394
c0.378-0.287,0.635-0.638,0.753-1.129h1.337c-0.092,0.518-0.284,0.988-0.564,1.388c-0.302,0.421-0.678,0.756-1.169,0.978
C348.161,241.925,347.626,242.044,346.971,242.044L346.971,242.044z M352.317,241.854v-8.651h1.336v8.651H352.317z M352.995,231.767
c-0.254,0-0.486-0.092-0.678-0.28c-0.165-0.162-0.262-0.379-0.262-0.633c0-0.253,0.097-0.469,0.262-0.637
c0.192-0.184,0.424-0.248,0.678-0.248c0.259,0,0.5,0.064,0.659,0.248c0.195,0.168,0.287,0.384,0.287,0.637
c0,0.254-0.092,0.471-0.287,0.633C353.494,231.675,353.254,231.767,352.995,231.767L352.995,231.767z M359.613,242.044
c-0.799,0-1.482-0.189-2.06-0.557c-0.589-0.389-1.059-0.896-1.388-1.556c-0.329-0.675-0.491-1.457-0.491-2.344
c0-0.912,0.162-1.685,0.491-2.37c0.33-0.681,0.799-1.199,1.388-1.577c0.578-0.378,1.261-0.557,2.06-0.557
c0.778,0,1.453,0.179,2.042,0.557c0.591,0.378,1.056,0.896,1.38,1.577c0.334,0.686,0.497,1.458,0.497,2.37
c0,0.887-0.162,1.669-0.497,2.344c-0.324,0.659-0.789,1.167-1.38,1.556C361.065,241.854,360.391,242.044,359.613,242.044
L359.613,242.044z M359.613,240.845c0.591,0,1.08-0.156,1.453-0.438c0.405-0.313,0.681-0.729,0.845-1.226
c0.192-0.491,0.284-1.026,0.284-1.594c0-0.588-0.092-1.134-0.284-1.619c-0.165-0.519-0.44-0.913-0.845-1.227
c-0.373-0.291-0.861-0.464-1.453-0.464c-0.607,0-1.077,0.173-1.482,0.464c-0.37,0.313-0.651,0.708-0.837,1.227
c-0.189,0.485-0.281,1.031-0.281,1.619c0,0.567,0.092,1.103,0.281,1.594c0.186,0.497,0.467,0.912,0.837,1.226
C358.536,240.688,359.005,240.845,359.613,240.845L359.613,240.845z M359.005,232.031l1.385-2.635h1.544l-1.758,2.635H359.005z
M366.887,236.648v5.206h-1.342v-8.651h1.293v1.361h0.119c0.213-0.454,0.513-0.805,0.937-1.086c0.399-0.265,0.937-0.395,1.596-0.395
c0.869,0,1.571,0.275,2.114,0.816c0.535,0.518,0.818,1.365,0.818,2.462v5.492h-1.339v-5.4c0-0.702-0.157-1.215-0.535-1.587
c-0.351-0.4-0.827-0.589-1.456-0.589c-0.637,0-1.147,0.21-1.577,0.637C367.101,235.314,366.887,235.903,366.887,236.648
L366.887,236.648z M379.322,245.116v-11.913h1.291v1.383h0.162c0.094-0.168,0.208-0.356,0.402-0.589
c0.156-0.237,0.418-0.442,0.745-0.638c0.33-0.184,0.775-0.275,1.339-0.275c0.731,0,1.385,0.179,1.922,0.557
c0.567,0.356,1.007,0.869,1.313,1.556c0.302,0.659,0.472,1.452,0.472,2.37c0,0.908-0.17,1.713-0.472,2.365
c-0.305,0.687-0.746,1.199-1.288,1.556c-0.562,0.367-1.199,0.557-1.917,0.557c-0.57,0-0.991-0.098-1.348-0.281
c-0.324-0.188-0.583-0.399-0.767-0.632c-0.165-0.232-0.308-0.442-0.402-0.61h-0.116v4.596H379.322z M380.628,237.54
c0,0.966,0.219,1.761,0.643,2.392c0.416,0.616,1.029,0.913,1.85,0.913c0.535,0,1.016-0.135,1.391-0.438
c0.37-0.286,0.651-0.686,0.837-1.198c0.189-0.497,0.287-1.054,0.287-1.669c0-0.616-0.097-1.178-0.287-1.669
c-0.186-0.491-0.467-0.864-0.815-1.145c-0.396-0.309-0.851-0.448-1.412-0.448c-0.821,0-1.434,0.308-1.85,0.891
C380.847,235.785,380.628,236.557,380.628,237.54L380.628,237.54z M391.542,242.065c-0.542,0-1.061-0.119-1.504-0.335
c-0.445-0.173-0.799-0.48-1.056-0.886c-0.259-0.394-0.397-0.886-0.397-1.452c0-0.492,0.097-0.887,0.305-1.193
c0.186-0.309,0.445-0.567,0.77-0.729c0.33-0.189,0.678-0.303,1.088-0.399c0.4-0.092,0.794-0.168,1.191-0.211
c0.54-0.064,0.961-0.119,1.288-0.162c0.33-0.021,0.57-0.098,0.732-0.194c0.143-0.07,0.208-0.227,0.208-0.442v-0.044
c0-0.566-0.135-0.988-0.442-1.29c-0.308-0.309-0.751-0.476-1.361-0.476c-0.629,0-1.155,0.141-1.504,0.427
c-0.351,0.281-0.61,0.588-0.748,0.891l-1.269-0.442c0.213-0.54,0.516-0.945,0.891-1.247c0.378-0.281,0.802-0.486,1.25-0.616
c0.437-0.108,0.888-0.179,1.328-0.179c0.289,0,0.589,0.038,0.964,0.092c0.356,0.065,0.712,0.217,1.061,0.422
c0.324,0.194,0.602,0.496,0.84,0.912c0.216,0.405,0.332,0.962,0.332,1.643v5.702h-1.342v-1.166h-0.04
c-0.095,0.178-0.265,0.394-0.473,0.604c-0.208,0.216-0.489,0.395-0.824,0.54C392.479,241.995,392.059,242.065,391.542,242.065
L391.542,242.065z M391.756,240.866c0.513,0,0.956-0.092,1.334-0.297c0.356-0.216,0.635-0.476,0.824-0.799
c0.189-0.335,0.253-0.687,0.253-1.037v-1.215c-0.04,0.069-0.154,0.113-0.345,0.161c-0.189,0.065-0.427,0.119-0.656,0.168
c-0.265,0.016-0.494,0.064-0.732,0.086c-0.216,0.027-0.397,0.049-0.562,0.076c-0.33,0.043-0.664,0.113-0.958,0.211
c-0.286,0.097-0.551,0.254-0.732,0.447c-0.162,0.217-0.254,0.471-0.254,0.795c0,0.469,0.162,0.82,0.511,1.058
C390.792,240.753,391.208,240.866,391.756,240.866L391.756,240.866z M397.945,241.854v-8.651h1.296v1.307h0.086
c0.148-0.416,0.424-0.767,0.848-1.053c0.424-0.254,0.891-0.395,1.404-0.395c0.243,0,0.448,0.038,0.637,0.092
c0.17,0.049,0.33,0.108,0.47,0.238l-0.448,1.117c-0.089-0.043-0.211-0.092-0.33-0.118c-0.138-0.021-0.278-0.044-0.438-0.044
c-0.64,0-1.158,0.189-1.577,0.589c-0.395,0.378-0.608,0.87-0.608,1.458v5.46H397.945z M406.58,242.065
c-0.538,0-1.029-0.119-1.475-0.335c-0.451-0.173-0.826-0.48-1.08-0.886c-0.262-0.394-0.402-0.886-0.402-1.452
c0-0.492,0.119-0.887,0.305-1.193c0.192-0.309,0.448-0.567,0.772-0.729c0.33-0.189,0.708-0.303,1.102-0.399
c0.378-0.092,0.802-0.168,1.202-0.211c0.51-0.064,0.937-0.119,1.267-0.162c0.329-0.021,0.583-0.098,0.726-0.194
c0.165-0.07,0.235-0.227,0.235-0.442v-0.044c0-0.566-0.162-0.988-0.47-1.29c-0.281-0.309-0.753-0.476-1.361-0.476
c-0.637,0-1.124,0.141-1.501,0.427c-0.348,0.281-0.61,0.588-0.753,0.891l-1.266-0.442c0.24-0.54,0.521-0.945,0.916-1.247
c0.373-0.281,0.777-0.486,1.223-0.616c0.47-0.108,0.888-0.179,1.339-0.179c0.278,0,0.613,0.038,0.967,0.092
c0.349,0.065,0.694,0.217,1.048,0.422c0.327,0.194,0.61,0.496,0.851,0.912c0.229,0.405,0.321,0.962,0.321,1.643v5.702h-1.313v-1.166
h-0.07c-0.092,0.178-0.232,0.394-0.446,0.604c-0.213,0.216-0.494,0.395-0.845,0.54C407.514,241.995,407.099,242.065,406.58,242.065
L406.58,242.065z M406.791,240.866c0.519,0,0.964-0.092,1.337-0.297c0.354-0.216,0.634-0.476,0.826-0.799
c0.186-0.335,0.278-0.687,0.278-1.037v-1.215c-0.07,0.069-0.189,0.113-0.378,0.161c-0.181,0.065-0.397,0.119-0.651,0.168
c-0.232,0.016-0.475,0.064-0.705,0.086c-0.235,0.027-0.424,0.049-0.564,0.076c-0.354,0.043-0.678,0.113-0.964,0.211
c-0.302,0.097-0.535,0.254-0.726,0.447c-0.192,0.217-0.281,0.471-0.281,0.795c0,0.469,0.165,0.82,0.513,1.058
C405.827,240.753,406.256,240.866,406.791,240.866L406.791,240.866z M418.809,230.309v11.546h-1.345v-11.546H418.809z
M423.777,242.065c-0.54,0-1.061-0.119-1.506-0.335c-0.438-0.173-0.792-0.48-1.056-0.886c-0.253-0.394-0.394-0.886-0.394-1.452
c0-0.492,0.097-0.887,0.308-1.193c0.184-0.309,0.44-0.567,0.77-0.729c0.329-0.189,0.683-0.303,1.077-0.399
c0.399-0.092,0.802-0.168,1.196-0.211c0.545-0.064,0.961-0.119,1.296-0.162c0.324-0.021,0.559-0.098,0.727-0.194
c0.146-0.07,0.213-0.227,0.213-0.442v-0.044c0-0.566-0.146-0.988-0.451-1.29c-0.294-0.309-0.748-0.476-1.358-0.476
c-0.635,0-1.145,0.141-1.499,0.427c-0.383,0.281-0.613,0.588-0.777,0.891l-1.24-0.442c0.208-0.54,0.513-0.945,0.886-1.247
c0.378-0.281,0.802-0.486,1.242-0.616c0.451-0.108,0.896-0.179,1.342-0.179c0.281,0,0.58,0.038,0.958,0.092
c0.348,0.065,0.705,0.217,1.058,0.422c0.33,0.194,0.613,0.496,0.845,0.912c0.206,0.405,0.321,0.962,0.321,1.643v5.702h-1.328v-1.166
h-0.049c-0.097,0.178-0.262,0.394-0.47,0.604c-0.208,0.216-0.497,0.395-0.821,0.54C424.717,241.995,424.271,242.065,423.777,242.065
L423.777,242.065z M423.988,240.866c0.516,0,0.961-0.092,1.339-0.297c0.354-0.216,0.629-0.476,0.821-0.799
c0.165-0.335,0.259-0.687,0.259-1.037v-1.215c-0.049,0.069-0.165,0.113-0.356,0.161c-0.192,0.065-0.413,0.119-0.659,0.168
c-0.259,0.016-0.486,0.064-0.721,0.086c-0.24,0.027-0.429,0.049-0.564,0.076c-0.33,0.043-0.653,0.113-0.961,0.211
c-0.313,0.097-0.537,0.254-0.724,0.447c-0.165,0.217-0.267,0.471-0.267,0.795c0,0.469,0.167,0.82,0.521,1.058
C423.002,240.753,423.454,240.866,423.988,240.866L423.988,240.866z M434.641,241.854v-8.651h1.336v8.651H434.641z M435.321,231.767
c-0.259,0-0.47-0.092-0.656-0.28c-0.194-0.162-0.286-0.379-0.286-0.633c0-0.253,0.092-0.469,0.286-0.637
c0.187-0.184,0.397-0.248,0.656-0.248c0.256,0,0.492,0.064,0.678,0.248c0.192,0.168,0.284,0.384,0.284,0.637
c0,0.254-0.092,0.471-0.284,0.633C435.813,231.675,435.577,231.767,435.321,231.767L435.321,231.767z M439.754,236.648v5.206h-1.342
v-8.651h1.291v1.361h0.122c0.187-0.454,0.492-0.805,0.916-1.086c0.421-0.265,0.937-0.395,1.593-0.395
c0.896,0,1.596,0.275,2.136,0.816c0.535,0.518,0.799,1.365,0.799,2.462v5.492h-1.313v-5.4c0-0.702-0.192-1.215-0.54-1.587
c-0.356-0.4-0.851-0.589-1.461-0.589c-0.634,0-1.164,0.21-1.571,0.637C439.965,235.314,439.754,235.903,439.754,236.648
L439.754,236.648z M451.228,242.044c-0.826,0-1.506-0.189-2.087-0.578c-0.589-0.384-1.062-0.896-1.364-1.577
c-0.305-0.68-0.47-1.452-0.47-2.301c0-0.891,0.165-1.663,0.491-2.349c0.305-0.675,0.786-1.199,1.364-1.566
c0.586-0.41,1.264-0.589,2.039-0.589c0.613,0,1.15,0.119,1.642,0.352c0.492,0.205,0.891,0.535,1.199,0.935
c0.321,0.399,0.516,0.896,0.588,1.437h-1.32c-0.116-0.405-0.327-0.751-0.678-1.064c-0.348-0.291-0.818-0.464-1.404-0.464
c-0.772,0-1.412,0.308-1.874,0.891c-0.476,0.583-0.71,1.388-0.71,2.371c0,0.982,0.213,1.798,0.71,2.392
c0.461,0.616,1.072,0.913,1.874,0.913c0.516,0,0.958-0.135,1.329-0.394c0.378-0.287,0.616-0.638,0.753-1.129h1.32
c-0.073,0.518-0.267,0.988-0.54,1.388c-0.311,0.421-0.705,0.756-1.177,0.978C452.418,241.925,451.857,242.044,451.228,242.044
L451.228,242.044z M457.911,230.309v11.546h-1.336v-11.546H457.911z M465.792,238.317v-5.114h1.339v8.651h-1.339v-1.447h-0.089
c-0.192,0.411-0.521,0.794-0.945,1.102c-0.418,0.324-0.985,0.459-1.617,0.459c-0.815,0-1.499-0.253-2.02-0.794
c-0.513-0.539-0.794-1.36-0.794-2.462v-5.509h1.342v5.417c0,0.637,0.184,1.128,0.535,1.501c0.356,0.373,0.794,0.567,1.361,0.567
c0.327,0,0.656-0.07,1.013-0.26c0.345-0.167,0.632-0.427,0.869-0.777C465.682,239.301,465.792,238.857,465.792,238.317
L465.792,238.317z"/>
<path fill="#2A2A2B" d="M475.694,235.147l-1.202,0.324c-0.113-0.303-0.3-0.589-0.581-0.843c-0.289-0.259-0.699-0.399-1.291-0.399
c-0.519,0-0.961,0.118-1.313,0.378c-0.354,0.232-0.543,0.54-0.543,0.907c0,0.341,0.138,0.589,0.378,0.777
c0.24,0.211,0.604,0.356,1.128,0.503l1.285,0.297c1.526,0.378,2.298,1.177,2.298,2.344c0,0.523-0.14,0.945-0.418,1.339
c-0.281,0.399-0.686,0.713-1.202,0.913c-0.516,0.237-1.104,0.356-1.76,0.356c-0.891,0-1.615-0.189-2.204-0.578
c-0.586-0.384-0.934-0.945-1.099-1.674l1.266-0.329c0.235,0.944,0.916,1.403,2.015,1.403c0.615,0,1.104-0.135,1.479-0.394
c0.354-0.26,0.538-0.584,0.538-0.967c0-0.605-0.421-1-1.286-1.21l-1.437-0.335c-0.793-0.184-1.38-0.491-1.733-0.891
c-0.378-0.373-0.559-0.896-0.559-1.48c0-0.491,0.138-0.912,0.395-1.29c0.283-0.373,0.656-0.675,1.15-0.892
c0.472-0.205,1.012-0.324,1.62-0.324c0.861,0,1.55,0.179,2.014,0.557C475.132,234.019,475.483,234.51,475.694,235.147
L475.694,235.147z M477.854,241.854v-8.651h1.336v8.651H477.854z M478.535,231.767c-0.262,0-0.47-0.092-0.659-0.28
c-0.192-0.162-0.284-0.379-0.284-0.633c0-0.253,0.092-0.469,0.284-0.637c0.189-0.184,0.397-0.248,0.659-0.248
c0.254,0,0.492,0.064,0.681,0.248c0.186,0.168,0.286,0.384,0.286,0.637c0,0.254-0.1,0.471-0.286,0.633
C479.026,231.675,478.789,231.767,478.535,231.767L478.535,231.767z M485.147,242.044c-0.796,0-1.474-0.189-2.068-0.557
c-0.581-0.389-1.026-0.896-1.35-1.556c-0.335-0.675-0.505-1.457-0.505-2.344c0-0.912,0.17-1.685,0.505-2.37
c0.324-0.681,0.77-1.199,1.35-1.577c0.594-0.378,1.272-0.557,2.068-0.557c0.778,0,1.456,0.179,2.044,0.557
c0.608,0.378,1.056,0.896,1.38,1.577c0.327,0.686,0.494,1.458,0.494,2.37c0,0.887-0.167,1.669-0.494,2.344
c-0.324,0.659-0.772,1.167-1.38,1.556C486.603,241.854,485.925,242.044,485.147,242.044L485.147,242.044z M485.147,240.845
c0.589,0,1.083-0.156,1.456-0.438c0.399-0.313,0.678-0.729,0.867-1.226c0.165-0.491,0.262-1.026,0.262-1.594
c0-0.588-0.097-1.134-0.262-1.619c-0.189-0.519-0.467-0.913-0.867-1.227c-0.373-0.291-0.867-0.464-1.456-0.464
c-0.61,0-1.077,0.173-1.474,0.464c-0.381,0.313-0.662,0.708-0.848,1.227c-0.189,0.485-0.278,1.031-0.278,1.619
c0,0.567,0.089,1.103,0.278,1.594c0.187,0.497,0.467,0.912,0.848,1.226C484.07,240.688,484.537,240.845,485.147,240.845
L485.147,240.845z M484.537,232.031l1.388-2.635h1.544l-1.755,2.635H484.537z M492.425,236.648v5.206h-1.315v-8.651h1.288v1.361
h0.095c0.213-0.454,0.516-0.805,0.937-1.086c0.402-0.265,0.939-0.395,1.596-0.395c0.867,0,1.59,0.275,2.117,0.816
c0.535,0.518,0.818,1.365,0.818,2.462v5.492h-1.345v-5.4c0-0.702-0.162-1.215-0.508-1.587c-0.356-0.4-0.848-0.589-1.455-0.589
c-0.637,0-1.172,0.21-1.599,0.637C492.63,235.314,492.425,235.903,492.425,236.648L492.425,236.648z"/>
<path fill="#163F75" d="M250.517,155.119c3.775,0,3.982-3.133,3.982-6.216v-30.609c0-3.148-0.208-6.318-3.982-6.318
c-3.402,0-4.35,2.98-4.35,5.983v7.484c0,3.641-0.205,7.162-0.626,10.58c-0.429,3.424-1.059,6.27-1.971,8.543
c-1.499,3.872-3.402,5.768-5.751,5.768c-2.552,0-3.818-3.71-3.818-11.092c0-7.037,0.259-13.35,0.753-18.961
c0.235-2.273,0.354-4.077,0.354-5.417c0-1.971-1.085-2.959-3.243-2.959c-1.201,0-2,0.454-2.459,1.313
c-1.296,2.44-1.95,10.893-1.95,25.322c0,11.26,3.005,16.579,9.194,16.579c3.259,0,5.983-1.663,8.098-4.995
c0.721-1.103,1.142-1.686,1.261-1.686c0.094,0,0.159,0.238,0.159,0.745C246.167,152.192,247.115,155.119,250.517,155.119
L250.517,155.119z M287.159,155.119c1.169,0,1.971-0.427,2.443-1.285c1.24-2.484,1.896-10.903,1.896-25.307
c0-3.435-0.259-6.275-0.772-8.619c-1.177-5.352-3.964-8.003-8.395-8.003c-3.664,0-6.572,1.928-8.708,5.772
c-0.281,0.551-0.521,0.778-0.659,0.778c-0.138,0-0.216-0.205-0.216-0.616c0-3.958-1.426-5.935-4.334-5.935
c-1.55,0-2.625,0.523-3.194,1.464c-0.532,0.912-0.796,2.506-0.796,4.779v30.598c0,1.054,0.07,2.063,0.21,3.02
c0.335,2.209,1.599,3.289,3.78,3.289c1.855,0,3.122-0.896,3.797-2.663c0.356-0.95,0.537-2.058,0.537-3.348v-7.928
c0-8.182,0.875-14.382,2.582-18.681c1.555-3.823,3.478-5.746,5.805-5.746c1.677,0,2.86,1.647,3.394,4.839
c0.333,1.923,0.476,4.315,0.476,7.178c-0.021,6.054-0.284,12.064-0.826,18.063c-0.232,2.274-0.354,4.078-0.354,5.4
c0,1.988,1.11,2.949,3.292,2.949H287.159z M316.507,128.123c0,2.808-0.21,5.719-0.634,8.748c-0.397,3.003-1.059,5.557-1.874,7.599
c-1.526,3.823-3.429,5.724-5.725,5.724c-2.935,0.141-3.85-6.475-3.85-13.512c0-4.784,1.439-19.679,8.284-19.679
c1.855,0,3.027,0.47,3.47,1.404c0.237,0.476,0.329,1.01,0.329,1.577V128.123z M324.837,105.268c0-3.094-0.146-6.264-3.964-6.264
c-3.362,0-4.366,2.916-4.366,5.983v7.177c0,0.54-0.064,0.848-0.235,0.848c-0.186,0-0.473-0.118-0.842-0.308
c-1.056-0.54-2.185-0.799-3.381-0.799c-4.525,0-8.065,2.608-10.606,7.793c-2.392,4.931-3.585,11.373-3.585,19.284
c0,8.517,3.121,16.212,9.221,16.072c3.284,0,5.979-1.696,8.139-5.05c0.656-1.031,1.01-1.566,1.123-1.566
c0.103,0,0.167,0.206,0.167,0.605c0,3.072,1.004,6.011,4.366,6.011c3.818,0,3.964-3.176,3.964-6.309V105.268z M354.839,155.055
c3.851,0.064,3.918-3.197,3.988-6.309v-30.565c-0.07-3.035-0.138-6.324-3.988-6.275c-1.307,0-2.317,0.405-2.935,1.198
c-0.205,0.266-0.467,0.405-0.796,0.405c-0.229,0-0.583-0.097-0.932-0.329c-1.434-0.82-2.841-1.274-4.229-1.274
c-4.528,0-8.047,2.608-10.601,7.793c-2.39,4.931-3.588,11.373-3.588,19.284c0,8.517,3.119,16.212,9.245,16.072
c3.653,0,6.589-2.021,8.768-6.033c0.257-0.469,0.448-0.701,0.567-0.701c0.094,0,0.156,0.254,0.156,0.724
C350.494,152.116,351.507,155.055,354.839,155.055L354.839,155.055z M342.175,150.193c-2.911,0.141-3.855-6.475-3.855-13.512
c0-4.784,1.437-19.679,8.284-19.679c2.605,0,3.891,0.935,3.891,2.813v9.947c0,1.831-0.2,4.197-0.626,7.107
c-0.445,2.938-1.083,5.486-1.971,7.599C346.396,148.293,344.495,150.193,342.175,150.193L342.175,150.193z M379.405,154.979
c2.762,0,5.02-0.75,6.802-2.252c1.785-1.507,3.024-3.461,3.64-5.881c0.114-0.324,0.138-0.572,0.138-0.751
c0-1.128-0.827-1.804-1.807-1.804c-0.821,0-1.434,0.416-1.809,1.329c-1.191,2.954-2.765,4.434-4.736,4.434
c-2.563,0-4.458-1.853-5.754-5.558c-0.843-2.419-1.264-5.146-1.264-8.273c0-7.544,1.013-12.961,3.027-16.266
c1.107-1.83,2.328-2.749,3.683-2.749c1.785,0,2.954,1.199,3.567,3.575c0.332,1.404,0.519,3.208,0.519,5.39
c0,0.988-0.068,1.923-0.262,2.911c-0.064,0.442-0.094,1.145-0.094,2.014c0,1.453,0.17,2.814,1.906,2.814c1.993,0,3-2.744,3-8.188
c0-9.191-3.151-13.793-9.378-13.793c-3.265,0-6.081,1.221-8.408,3.662c-4.01,4.223-6.027,11.098-6.027,20.629
c0,3.791,0.47,6.999,1.361,9.607c0.985,2.847,2.533,5.114,4.623,6.832C374.035,154.212,376.465,154.979,379.405,154.979
L379.405,154.979z M401.434,109.47c1.566,0,2.724-0.562,3.464-1.69c0.502-0.756,0.761-1.637,0.761-2.7
c0-1.14-0.378-2.225-1.158-3.181c-0.748-0.988-1.761-1.485-3-1.485c-2.465,0-4.174,2.111-4.174,5.136
C397.327,108.179,398.69,109.47,401.434,109.47L401.434,109.47z M401.263,155.055c2.917,0,4.396-2.021,4.396-6.011v-30.863
c0-4.649-1.083-6.275-3.969-6.275c-2.881,0-4.363,2.095-4.363,5.989v30.852c0,1.054,0.067,2.063,0.216,3.02
C397.861,153.975,399.084,155.055,401.263,155.055L401.263,155.055z M429.821,149.843c-2.89,0-5.047-1.712-6.475-5.093
c-1.245-2.997-1.855-7.053-1.855-12.215c0-10.531,3.024-15.846,9.051-15.846c2.778,0,4.763,1.739,6.032,5.195
c0.958,2.668,1.428,6.017,1.428,10.083c0,4.903-0.61,8.965-1.801,12.221C434.792,147.937,432.653,149.843,429.821,149.843
L429.821,149.843z M427.542,154.979c5.582,0,9.737-2.203,12.386-6.583c2.301-3.926,3.451-9.288,3.451-16.099
c0-5.368-0.821-9.813-2.527-13.252c-2.328-4.736-5.986-7.14-11.009-7.14c-5.773,0-10.088,2.214-12.926,6.643
c-2.511,3.845-3.775,9.104-3.775,15.764c0,5.659,1.004,10.255,3.019,13.819C418.728,152.705,422.527,154.979,427.542,154.979
L427.542,154.979z M425.924,108.67c1.288,0,3.213-0.799,5.773-2.349c2.549-1.545,3.869-3.116,3.869-4.661
c0-1.339-0.659-1.998-1.974-1.998c-1.239,0-3.051,1.129-5.395,3.402c-2.322,2.23-3.47,3.732-3.47,4.574
C424.728,108.319,425.122,108.67,425.924,108.67L425.924,108.67z M472.821,155.119c1.19,0,1.968-0.427,2.441-1.285
c1.239-2.484,1.898-10.903,1.898-25.307c0-3.435-0.259-6.275-0.756-8.619c-1.188-5.352-3.98-8.003-8.419-8.003
c-3.662,0-6.545,1.928-8.703,5.772c-0.278,0.551-0.491,0.778-0.629,0.778c-0.143,0-0.213-0.205-0.213-0.616
c0-3.958-1.458-5.935-4.364-5.935c-1.525,0-2.608,0.523-3.188,1.464c-0.545,0.912-0.799,2.506-0.799,4.779v30.598
c0,1.054,0.097,2.063,0.232,3.02c0.308,2.209,1.574,3.289,3.756,3.289c1.879,0,3.116-0.896,3.829-2.663
c0.343-0.95,0.535-2.058,0.535-3.348v-7.928c0-8.182,0.842-14.382,2.56-18.681c1.566-3.823,3.473-5.746,5.79-5.746
c1.717,0,2.886,1.647,3.429,4.839c0.327,1.923,0.464,4.315,0.464,7.178c-0.046,6.054-0.278,12.064-0.84,18.063
c-0.235,2.274-0.332,4.078-0.332,5.4c0,1.988,1.085,2.949,3.281,2.949H472.821z"/>
<g>
<path fill="#163F75" d="M206.898,156.113c3.626,0,4.701-3.09,4.701-6.249v-28.687c0-1.08,0.634-1.625,1.812-1.625l7.788,0.092
c1.807,0.092,2.806-0.908,2.806-2.085c0-1.086-0.362-1.814-1.094-2.263c-0.632-0.362-1.537-0.546-2.892-0.546
c-2.079,0-4.158,0.184-6.237,0.184c-1.456,0-2.182-0.453-2.182-2.711v-5.427c0-1.998,0.91-2.992,2.725-2.992
c0.996,0,2.352,0,3.523,0.098c1.094,0,8.689,0,9.502,0c2.714,0,3.988-1.27,3.988-2.452c0-1.62-1.091-2.533-3.076-2.803
c-0.913-0.184-1.636-0.184-2.362-0.184h-9.224h-8.238c-2.986,0-4.79,1.183-5.519,3.629c-0.27,1.172-0.448,2.532-0.448,4.158v43.252
c0,1.091,0.086,2.176,0.27,3.17C203.099,154.936,204.46,156.113,206.898,156.113L206.898,156.113z"/>
</g>
<g>
<defs>
<rect id="SVGID_23_" x="-476.587" y="-28.438" width="597.06" height="844.021"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_23_" overflow="visible"/>
</clipPath>
</g>
</svg>

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 62 KiB

BIN
assets/re/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

1
assets/re/RE - SVG.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More