Con marca y dashboards
This commit is contained in:
24
lib/dashboard-queries.ts
Normal file
24
lib/dashboard-queries.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// El cliente NUNCA consulta Supabase directamente.
|
||||
// Toda la comunicación va a través de los Route Handlers /api/dashboard/[widget].
|
||||
|
||||
import type { DashboardFilters } from './dashboard-types'
|
||||
import { filtersToQuery } from './dashboard-utils'
|
||||
|
||||
async function fetchWidget<T>(widget: string, surveyId: string, filters: DashboardFilters): Promise<T> {
|
||||
const qs = filtersToQuery(surveyId, filters)
|
||||
const res = await fetch(`/api/dashboard/${widget}?${qs}`, { cache: 'no-store' })
|
||||
if (!res.ok) throw new Error(`Widget ${widget}: ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export const queries = {
|
||||
kpis: (sid: string, f: DashboardFilters) => fetchWidget('kpis', sid, f),
|
||||
prevalencia: (sid: string, f: DashboardFilters) => fetchWidget('prevalencia', sid, f),
|
||||
heatmap: (sid: string, f: DashboardFilters) => fetchWidget('heatmap', sid, f),
|
||||
distribucion: (sid: string, f: DashboardFilters) => fetchWidget('distribucion', sid, f),
|
||||
politicas: (sid: string, f: DashboardFilters) => fetchWidget('politicas', sid, f),
|
||||
score: (sid: string, f: DashboardFilters) => fetchWidget('score', sid, f),
|
||||
piramide: (sid: string, f: DashboardFilters) => fetchWidget('piramide', sid, f),
|
||||
intensidad: (sid: string, f: DashboardFilters) => fetchWidget('intensidad', sid, f),
|
||||
riesgo: (sid: string, f: DashboardFilters) => fetchWidget('riesgo', sid, f),
|
||||
}
|
||||
57
lib/dashboard-types.ts
Normal file
57
lib/dashboard-types.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// Filtros globales del dashboard (sidebar)
|
||||
export interface DashboardFilters {
|
||||
depto?: string
|
||||
genero?: string[]
|
||||
etario?: string[]
|
||||
modalidad?: string[]
|
||||
}
|
||||
|
||||
// Estado de cada widget
|
||||
export type WidgetStatus = 'loading' | 'ok' | 'suppressed' | 'error'
|
||||
|
||||
// Payloads de cada RPC
|
||||
export interface KpiData {
|
||||
status: 'ok'
|
||||
n: number
|
||||
pct_cuidadores: number
|
||||
score_bienestar: number | null
|
||||
pct_ausencias: number
|
||||
}
|
||||
|
||||
export interface DeptoData {
|
||||
depto: string
|
||||
n: number | null
|
||||
pct_cuidadores: number | null
|
||||
suppressed: boolean
|
||||
}
|
||||
export interface PrevalenciaData { status: 'ok'; n_total: number; data: DeptoData[] }
|
||||
|
||||
export interface HeatmapCell { horas: string; ausencias: string; n: number | null; suppressed: boolean }
|
||||
export interface HeatmapData { status: 'ok'; n_total: number; data: HeatmapCell[] }
|
||||
|
||||
export interface DistribucionData {
|
||||
status: 'ok'; n: number
|
||||
disc_propia: number; familiar: number; adulto_mayor: number; ninguno: number
|
||||
}
|
||||
|
||||
export interface PoliticaItem { politica: string; n: number; pct: number }
|
||||
export interface PoliticasData { status: 'ok'; n_total: number; data: PoliticaItem[] }
|
||||
|
||||
export interface ScoreData { status: 'ok'; n: number; score: number; zona: 'baja' | 'desarrollo' | 'consciente' }
|
||||
|
||||
export interface PiramideCell { etario: string; genero: string; n: number | null; suppressed: boolean }
|
||||
export interface PiramideData { status: 'ok'; n_total: number; data: PiramideCell[] }
|
||||
|
||||
export interface IntensidadCell { horas: string; bienestar: string; n: number | null; suppressed: boolean }
|
||||
export interface IntensidadData { status: 'ok'; n_total: number; data: IntensidadCell[] }
|
||||
|
||||
export interface RiesgoSegmento { segmento: string; n: number | null; pct: number | null; politica: string | null; suppressed: boolean }
|
||||
export interface RiesgoData { status: 'ok'; n_total: number; segmentos: RiesgoSegmento[] }
|
||||
|
||||
export type SuppressedPayload = { status: 'suppressed'; min_n: number; n: number }
|
||||
|
||||
export type RpcResult<T> = T | SuppressedPayload
|
||||
|
||||
export function isSuppressed(r: unknown): r is SuppressedPayload {
|
||||
return typeof r === 'object' && r !== null && (r as Record<string, unknown>)['status'] === 'suppressed'
|
||||
}
|
||||
50
lib/dashboard-utils.ts
Normal file
50
lib/dashboard-utils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { DashboardFilters } from './dashboard-types'
|
||||
|
||||
/** Convierte filtros a query string para las API routes */
|
||||
export function filtersToQuery(surveyId: string, filters: DashboardFilters): string {
|
||||
const p = new URLSearchParams({ survey_id: surveyId })
|
||||
if (filters.depto) p.set('depto', filters.depto)
|
||||
if (filters.genero?.length) p.set('genero', filters.genero.join(','))
|
||||
if (filters.etario?.length) p.set('etario', filters.etario.join(','))
|
||||
if (filters.modalidad?.length) p.set('modalidad', filters.modalidad.join(','))
|
||||
return p.toString()
|
||||
}
|
||||
|
||||
/** Parsea query string a objeto DashboardFilters */
|
||||
export function queryToFilters(searchParams: URLSearchParams): DashboardFilters {
|
||||
const f: DashboardFilters = {}
|
||||
if (searchParams.has('depto')) f.depto = searchParams.get('depto')!
|
||||
const g = searchParams.get('genero')
|
||||
if (g) f.genero = g.split(',').filter(Boolean)
|
||||
const e = searchParams.get('etario')
|
||||
if (e) f.etario = e.split(',').filter(Boolean)
|
||||
const m = searchParams.get('modalidad')
|
||||
if (m) f.modalidad = m.split(',').filter(Boolean)
|
||||
return f
|
||||
}
|
||||
|
||||
/** Normaliza filtros a JSONB-compatible object para las RPC */
|
||||
export function filtersToRpc(filters: DashboardFilters): Record<string, unknown> {
|
||||
return {
|
||||
depto: filters.depto ?? null,
|
||||
genero: filters.genero ?? [],
|
||||
etario: filters.etario ?? [],
|
||||
modalidad: filters.modalidad ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
export const HORAS_ORDER = ['Menos de 1h','Entre 1 y 3h','Entre 3 y 6h','Más de 6h','Varía mucho']
|
||||
export const AUSENCIAS_ORDER = ['Nunca','Pocas veces (1–3 veces al año)','Varias veces (4–10 veces al año)','Con frecuencia (más de 10 veces al año)']
|
||||
export const ETARIO_ORDER = ['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','Prefiero no decir']
|
||||
|
||||
export function sortByOrder<T extends Record<string, unknown>>(
|
||||
arr: T[],
|
||||
key: keyof T,
|
||||
order: string[]
|
||||
): T[] {
|
||||
return [...arr].sort((a, b) => {
|
||||
const ia = order.indexOf(a[key] as string)
|
||||
const ib = order.indexOf(b[key] as string)
|
||||
return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user