Rafael Lopez

entidades

......@@ -242,6 +242,8 @@
color: #B8B5AD;
cursor: pointer;
transition: all 0.2s ease;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.nav-search-btn:hover {
......@@ -291,6 +293,8 @@
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: #B8B5AD;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
cursor: pointer;
transition: all 0.2s ease;
}
......@@ -330,6 +334,8 @@
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: #B8B5AD;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
......@@ -363,6 +369,8 @@
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: #B8B5AD;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
cursor: pointer;
transition: all 0.2s ease;
}
......
<script>
import { onMount, tick } from 'svelte';
import { tick } from 'svelte';
import { goto } from '$app/navigation';
import {
query,
results,
isLoading,
indexLoaded,
error,
selectedIndex,
initIndex,
performSearch,
clearSearch,
navigateResults
} from '$lib/stores/searchStore';
import { get } from 'svelte/store';
import { landingSearchMode, landingSelectedClassifiers } from '$lib/stores/landingSearchState';
let { open = $bindable(false) } = $props();
let searchInput = $state(null);
let searchVal = $state('');
let isMac = $state(false);
// Search filters
let searchFilters = $state({
entidad: true,
objeto_gasto: true,
rubro: true,
finfun: true,
organismo: true,
fuente: true
let isMac = $state(typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform));
let searchLoading = $state(false);
let searchResults = $state([]);
let selectedIdx = $state(-1);
let debounceTimer;
// Mapeo de clasificadores landing → filtros modal
const classifierToFilter = {
entidad: 'entidad',
objeto: 'objeto',
rubro: 'rubro',
finfun: 'finfun',
organismo: 'organismo',
fuente: 'fuente'
};
function getInitialFilters() {
const mode = get(landingSearchMode);
const classifiers = get(landingSelectedClassifiers);
if (mode === 'clasificadores' && classifiers.length > 0) {
const filters = { entidad: false, objeto_gasto: false, rubro: false, finfun: false, organismo: false, fuente: false };
classifiers.forEach(c => {
const key = classifierToFilter[c];
if (key === 'objeto') filters.objeto_gasto = true;
else if (key && filters[key] !== undefined) filters[key] = true;
});
return filters;
}
return { entidad: true, objeto_gasto: true, rubro: true, finfun: true, organismo: true, fuente: true };
}
// Filtered results based on active filters
let filteredResults = $derived(
$results.filter(item => searchFilters[item.tipo])
);
let searchFilters = $state(getInitialFilters());
function toggleFilter(tipo) {
searchFilters[tipo] = !searchFilters[tipo];
// Re-buscar con filtros actualizados
if (searchVal.length >= 2) doSearch(searchVal);
}
// Filtros activos → class_ param para Typesense
function getActiveClasses() {
const map = { entidad: 'entidad', objeto_gasto: 'objeto', finfun: 'finfun' };
return Object.entries(searchFilters)
.filter(([_, v]) => v)
.map(([k]) => map[k] || k)
.filter(Boolean);
}
onMount(() => {
isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
initIndex();
function parseMetadatos(meta) {
if (!meta) return {};
if (typeof meta === 'object') return meta;
try { return JSON.parse(meta); } catch { return {}; }
}
async function doSearch(query) {
searchLoading = true;
try {
const classes = getActiveClasses();
const params = new URLSearchParams({
q: query,
is_class: 'true',
per_page: '30'
});
if (classes.length > 0 && classes.length < 6) {
params.set('class_', classes.join(','));
}
const res = await fetch(`/api/search?${params}`);
if (!res.ok) throw new Error();
const data = await res.json();
searchResults = (data.hits || []).map(hit => {
const meta = parseMetadatos(hit.document.metadatos);
const cls = hit.document.class_;
let tipo = cls;
let codigo = '';
let nombre = hit.document.texto;
let highlight = hit.highlights?.[0]?.snippet || nombre;
if (cls === 'entidad') {
const esDA = !!meta.da;
codigo = esDA ? `${meta.entidad}.${meta.da}` : String(meta.entidad);
tipo = 'entidad';
} else if (cls === 'objeto') {
codigo = meta.objeto_subpartida || meta.objeto_partida || meta.objeto_subgrupo || meta.objeto_grupo || '';
tipo = 'objeto_gasto';
} else if (cls === 'finfun') {
const fin = String(meta.finfun_finalidad || '');
if (meta.finfun_funcion !== undefined && meta.finfun_grpfuncion !== undefined) {
codigo = `${fin}.${meta.finfun_grpfuncion}.${meta.finfun_funcion}`;
} else if (meta.finfun_grpfuncion !== undefined) {
codigo = `${fin}.${meta.finfun_grpfuncion}`;
} else {
codigo = fin;
}
tipo = 'finfun';
}
// Focus input when modal opens
return { tipo, codigo, nombre, highlight };
});
selectedIdx = -1;
} catch {
searchResults = [];
}
searchLoading = false;
}
let filteredResults = $derived(searchResults);
// Focus input and sync filters when modal opens
$effect(() => {
if (open) {
searchFilters = getInitialFilters();
tick().then(() => {
searchInput?.focus();
});
} else {
// Clear on close
searchVal = '';
clearSearch();
searchResults = [];
selectedIdx = -1;
}
});
......@@ -63,10 +135,11 @@
function handleInput(e) {
searchVal = e.target.value;
clearTimeout(debounceTimer);
if (searchVal.length >= 2) {
performSearch(searchVal);
debounceTimer = setTimeout(() => doSearch(searchVal), 200);
} else {
clearSearch();
searchResults = [];
}
}
......@@ -75,13 +148,13 @@
closeModal();
} else if (e.key === 'ArrowDown') {
e.preventDefault();
navigateResults('down', filteredResults.length);
selectedIdx = selectedIdx < filteredResults.length - 1 ? selectedIdx + 1 : 0;
} else if (e.key === 'ArrowUp') {
e.preventDefault();
navigateResults('up', filteredResults.length);
} else if (e.key === 'Enter' && $selectedIndex >= 0) {
selectedIdx = selectedIdx > 0 ? selectedIdx - 1 : filteredResults.length - 1;
} else if (e.key === 'Enter' && selectedIdx >= 0) {
e.preventDefault();
goToResult(filteredResults[$selectedIndex]);
goToResult(filteredResults[selectedIdx]);
}
}
......@@ -91,10 +164,10 @@
goto(`/entidad/${item.codigo}`);
} else if (item.tipo === 'objeto_gasto') {
goto(`/objeto/${item.codigo}`);
} else if (item.tipo === 'rubro') {
goto(`/rubro/${item.codigo}`);
} else if (item.tipo === 'finfun') {
goto(`/finfun/${item.codigo}`);
} else if (item.tipo === 'rubro') {
goto(`/rubro/${item.codigo}`);
} else if (item.tipo === 'organismo') {
goto(`/organismo/${item.codigo}`);
} else if (item.tipo === 'fuente') {
......@@ -248,9 +321,7 @@
<!-- Results -->
<div class="search-modal-results">
{#if !$indexLoaded}
<div class="search-modal-status">Cargando índice...</div>
{:else if $isLoading}
{#if searchLoading}
<div class="search-modal-status">Buscando...</div>
{:else if searchVal.length < 2}
<div class="search-modal-empty"></div>
......@@ -260,18 +331,15 @@
{#each filteredResults as item, i}
<button
class="search-result-item"
class:result-selected={$selectedIndex === i}
class:result-selected={selectedIdx === i}
onclick={() => goToResult(item)}
onmouseenter={() => selectedIndex.set(i)}
onmouseenter={() => { selectedIdx = i; }}
>
<div class="result-main">
<span class="result-type" style="color:{typeConfig[item.tipo]?.color || '#888'}">
{typeConfig[item.tipo]?.label || item.tipo}
</span>
<span class="result-name">{item.nombre}</span>
{#if item.descripcion}
<span class="result-desc">{item.descripcion}</span>
{/if}
<span class="result-name">{@html item.highlight}</span>
</div>
<span class="result-code">{item.codigo}</span>
</button>
......
import { writable } from 'svelte/store';
// Persiste el estado de búsqueda del landing entre navegaciones
export const landingSearchMode = writable('programas'); // 'programas' | 'clasificadores'
export const landingSelectedClassifiers = writable([]); // ['entidad', 'objeto', ...]
export const landingSearchQuery = writable('');
This diff is collapsed. Click to expand it.
......@@ -2,24 +2,64 @@ import { supabase } from '$lib/supabase';
import { error } from '@sveltejs/kit';
export async function load({ params }) {
const codigo = parseInt(params.codigo);
const codigo = params.codigo;
const isDA = codigo.includes('.');
const entidadCode = isDA ? codigo.split('.')[0] : codigo;
const entidadNum = parseInt(entidadCode);
if (isNaN(codigo)) {
if (isNaN(entidadNum)) {
throw error(400, 'Código de entidad inválido');
}
const { data, error: dbError } = await supabase
// Cargar metadata y resumen en paralelo
const [entidadRes, resumenRes] = await Promise.all([
supabase
.schema('ppto')
.from('clas_institucional')
.select('*')
.eq('entidad', codigo)
.single();
.eq('entidad', entidadNum)
.single(),
if (dbError || !data) {
supabase
.schema('ppto')
.from('entidad_resumen')
.select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking')
.eq('codigo', codigo)
]);
if (entidadRes.error || !entidadRes.data) {
throw error(404, 'Entidad no encontrada');
}
// Determinar última gestión disponible
const gestiones = [...new Set((resumenRes.data || []).map(d => d.gestion))].sort((a, b) => b - a);
const ultimaGestion = gestiones[0] || 2025;
// Cargar distribuciones solo de la última gestión
const distRes = await supabase
.schema('ppto')
.from('entidad_distribuciones')
.select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado')
.eq('codigo', codigo)
.eq('gestion', ultimaGestion);
// Resolver nombre de DA desde el resumen
let nombreDA = null;
let nombreEntidadMadre = null;
if (isDA && resumenRes.data?.length > 0) {
const firstRow = resumenRes.data[0];
nombreDA = firstRow.desc || null;
nombreEntidadMadre = firstRow.desc_padre || null;
}
return {
entidad: data
entidad: entidadRes.data,
isDA,
nombreDA,
nombreEntidadMadre,
codigoEntidadPadre: isDA ? entidadCode : null,
resumenData: resumenRes.data || [],
distribucionesData: distRes.data || [],
gestionInicial: ultimaGestion
};
}
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
......@@ -377,6 +377,70 @@ Sector → Subsector → Área → Subárea → Entidad
---
## Tablas de Vista de Entidad
### 13. `entidad_resumen` - Resumen histórico por entidad/DA
Alimenta los gráficos de historia (area plots) y rankings en la vista de entidad (`/entidad/[codigo]`).
| Columna | Tipo | Descripción |
|---------|------|-------------|
| tipo | TEXT | Tipo de flujo: 'gastos' o 'ingresos' |
| tipo_codigo | TEXT | Tipo de código: 'entidad', 'entidad_da', 'municipio_ubigeo', etc. |
| codigo | TEXT | Código de la entidad o DA (ej: "1901", "1901.6") |
| desc | TEXT | Nombre de la entidad o DA |
| desc_padre | TEXT | Nombre de la entidad madre (para DAs) |
| gestion | INTEGER | Año fiscal |
| devengado | NUMERIC | Monto devengado en Bs |
| ranking | INTEGER | Posición en ranking por monto dentro de su tipo y gestión |
**Primary key:** `(tipo, tipo_codigo, codigo, gestion)`
**Índices:**
```sql
CREATE INDEX idx_entidad_resumen_codigo ON ppto.entidad_resumen (codigo);
CREATE INDEX idx_entidad_resumen_gestion ON ppto.entidad_resumen (gestion);
CREATE INDEX idx_entidad_resumen_tipo_codigo ON ppto.entidad_resumen (tipo_codigo);
```
**Notas:**
- ~53K filas
- `codigo` es TEXT para soportar entidades ("1901") y DAs ("1901.6")
- `desc` y `desc_padre` permiten resolver nombres sin joins ni búsquedas externas
- Una fila por combinación tipo + tipo_codigo + código + año
---
### 14. `entidad_distribuciones` - Distribución del gasto/ingreso por clasificador
Alimenta las barras de composición (clasificadores de gasto e ingreso) en la vista de entidad.
| Columna | Tipo | Descripción |
|---------|------|-------------|
| id | SERIAL (PK) | Identificador autoincremental |
| tipo | TEXT | Tipo de flujo: 'gastos' o 'ingresos' |
| tipo_codigo | TEXT | Tipo de código: 'entidad', 'entidad_da', etc. |
| codigo | TEXT | Código de la entidad o DA |
| dimension | TEXT | Clasificador: 'objeto', 'finfun', 'acteco', 'rubro', 'organismo' |
| gestion | INTEGER | Año fiscal |
| padre | TEXT | Código del grupo padre en la dimensión |
| desc_padre | TEXT | Descripción del grupo padre |
| hijo | TEXT | Código del ítem hijo |
| desc_hijo | TEXT | Descripción del ítem hijo |
| devengado | NUMERIC | Monto devengado en Bs |
**Índices:**
```sql
CREATE INDEX idx_entidad_dist_codigo ON ppto.entidad_distribuciones (codigo);
CREATE INDEX idx_entidad_dist_gestion ON ppto.entidad_distribuciones (gestion);
CREATE INDEX idx_entidad_dist_codigo_gestion ON ppto.entidad_distribuciones (codigo, gestion);
```
**Notas:**
- ~3.6M filas
- Usa `id SERIAL` como PK porque un mismo `hijo` puede tener distintas descripciones dentro de la misma combinación padre/gestión
- Cada fila es un segmento de las barras de composición: cuánto gastó/ingresó una entidad en un ítem específico de un clasificador en un año
---
*Documento generado para el equipo de sistemas - Marzo 2026*
......