Rafael Lopez

general

......@@ -9,7 +9,6 @@
"version": "0.0.1",
"dependencies": {
"@observablehq/plot": "^0.6.17",
"@supabase/supabase-js": "^2.98.0",
"@sveltejs/adapter-node": "^5.5.4",
"d3": "^7.9.0",
"fuse.js": "^7.1.0",
......@@ -939,86 +938,6 @@
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@supabase/auth-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.98.0.tgz",
"integrity": "sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.98.0.tgz",
"integrity": "sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.98.0.tgz",
"integrity": "sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.98.0.tgz",
"integrity": "sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.98.0.tgz",
"integrity": "sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz",
"integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.98.0",
"@supabase/functions-js": "2.98.0",
"@supabase/postgrest-js": "2.98.0",
"@supabase/realtime-js": "2.98.0",
"@supabase/storage-js": "2.98.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
......@@ -1415,21 +1334,6 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.3.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz",
"integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT"
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
......@@ -1442,15 +1346,6 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
......@@ -2114,15 +2009,6 @@
"integrity": "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w==",
"license": "MIT"
},
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
......@@ -2775,13 +2661,9 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
"dev": true,
"license": "0BSD",
"optional": true
},
"node_modules/vite": {
"version": "7.3.1",
......@@ -2876,27 +2758,6 @@
}
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/zimmerframe": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
......
......@@ -20,7 +20,6 @@
},
"dependencies": {
"@observablehq/plot": "^0.6.17",
"@supabase/supabase-js": "^2.98.0",
"@sveltejs/adapter-node": "^5.5.4",
"d3": "^7.9.0",
"fuse.js": "^7.1.0",
......
import Fuse from 'fuse.js';
import { supabase } from '$lib/supabase';
import { normalizeText } from '$lib/utils/normalize';
let fuseInstance = null;
let searchData = [];
/**
* Configuración de Fuse.js
*/
const fuseOptions = {
keys: [
{ name: 'nombre_normalizado', weight: 0.6 },
{ name: 'sigla_normalizada', weight: 0.2 },
{ name: 'codigo_normalizado', weight: 0.2 }
],
threshold: 0.4, // 0 = exacto, 1 = muy fuzzy
distance: 100,
includeScore: true,
includeMatches: true,
minMatchCharLength: 2,
ignoreLocation: true
};
/**
* Carga las entidades y objetos de gasto desde Supabase y construye el índice
*/
export async function initSearchIndex() {
if (fuseInstance) return fuseInstance;
// Cargar entidades
const { data: entidades, error: errorEntidades } = await supabase
.schema('ppto')
.from('clas_institucional')
.select('entidad, desc_entidad, sigla_entidad, desc_area, n_gestiones');
if (errorEntidades) {
console.error('Error cargando entidades:', errorEntidades);
throw errorEntidades;
}
// Cargar objetos de gasto
const { data: objetos, error: errorObjetos } = await supabase
.schema('ppto')
.from('clas_objetos')
.select('objeto, desc_objeto, nivel');
if (errorObjetos) {
console.error('Error cargando objetos:', errorObjetos);
throw errorObjetos;
}
// Cargar rubros
const { data: rubros, error: errorRubros } = await supabase
.schema('ppto')
.from('clas_rubros')
.select('rubro, desc_rubro, nivel');
if (errorRubros) {
console.error('Error cargando rubros:', errorRubros);
throw errorRubros;
}
// Cargar finalidad y función
const { data: finfun, error: errorFinfun } = await supabase
.schema('ppto')
.from('clas_finfun')
.select('finfun, desc_finfun, nivel');
if (errorFinfun) {
console.error('Error cargando finalidad/función:', errorFinfun);
throw errorFinfun;
}
// Cargar organismos (no bloquea si falla)
let organismos = [];
const { data: organismosData, error: errorOrganismos } = await supabase
.schema('ppto')
.from('clas_organismos')
.select('organismo, organismo_desc_organismo, organismo_grupo, organismo_sub_grupo');
if (errorOrganismos) {
console.warn('Tabla clas_organismos no disponible:', errorOrganismos.message);
} else {
organismos = organismosData || [];
}
// Cargar fuentes (no bloquea si falla)
let fuentes = [];
const { data: fuentesData, error: errorFuentes } = await supabase
.schema('ppto')
.from('clas_fuentes')
.select('fuente, desc_fuente, grupo_fuente, desc_grupo_fuente');
if (errorFuentes) {
console.warn('Tabla clas_fuentes no disponible:', errorFuentes.message);
} else {
fuentes = fuentesData || [];
}
// Transformar entidades para el índice
const entidadesIndex = entidades.map(item => ({
tipo: 'entidad',
codigo: item.entidad,
nombre: item.desc_entidad,
sigla: item.sigla_entidad,
contexto: item.desc_area,
años: item.n_gestiones,
nombre_normalizado: normalizeText(item.desc_entidad),
sigla_normalizada: normalizeText(item.sigla_entidad),
codigo_normalizado: item.entidad?.toString() || ''
}));
// Transformar objetos de gasto para el índice
const nivelLabelsObjetos = { grupo: 'Grupo', subgrupo: 'Subgrupo', partida: 'Partida', subpartida: 'Subpartida' };
const objetosIndex = objetos.map(item => ({
tipo: 'objeto_gasto',
codigo: item.objeto,
nombre: item.desc_objeto,
sigla: null,
contexto: `Objeto del Gasto · ${nivelLabelsObjetos[item.nivel] || item.nivel}`,
nivel: item.nivel,
nombre_normalizado: normalizeText(item.desc_objeto),
sigla_normalizada: '',
codigo_normalizado: item.objeto || ''
}));
// Transformar rubros para el índice
const nivelLabelsRubros = { tipo: 'Tipo', clase: 'Clase', cuenta: 'Cuenta', sub_cuenta: 'Subcuenta' };
const rubrosIndex = rubros.map(item => ({
tipo: 'rubro',
codigo: item.rubro,
nombre: item.desc_rubro,
sigla: null,
contexto: `Rubro · ${nivelLabelsRubros[item.nivel] || item.nivel}`,
nivel: item.nivel,
nombre_normalizado: normalizeText(item.desc_rubro),
sigla_normalizada: '',
codigo_normalizado: item.rubro?.toString() || ''
}));
// Transformar finalidad y función para el índice
const nivelLabelsFinfun = { finalidad: 'Finalidad', grpfuncion: 'Grupo Función', funcion: 'Función' };
const finfunIndex = finfun.map(item => ({
tipo: 'finfun',
codigo: item.finfun,
nombre: item.desc_finfun,
sigla: null,
contexto: `Finalidad y Función · ${nivelLabelsFinfun[item.nivel] || item.nivel}`,
nivel: item.nivel,
nombre_normalizado: normalizeText(item.desc_finfun),
sigla_normalizada: '',
codigo_normalizado: item.finfun?.toString() || ''
}));
// Transformar organismos para el índice
// Todos los registros en la tabla son organismos (nivel hoja)
const organismosIndex = organismos.map(item => ({
tipo: 'organismo',
codigo: item.organismo,
nombre: item.organismo_desc_organismo,
sigla: null,
contexto: `Organismo · Grupo ${item.organismo_grupo}`,
nivel: 'organismo',
nombre_normalizado: normalizeText(item.organismo_desc_organismo),
sigla_normalizada: '',
codigo_normalizado: item.organismo?.toString() || ''
}));
// Transformar fuentes para el índice
const fuentesIndex = fuentes.map(item => ({
tipo: 'fuente',
codigo: item.fuente,
nombre: item.desc_fuente,
sigla: null,
contexto: `Fuente · ${item.desc_grupo_fuente}`,
nivel: 'fuente',
nombre_normalizado: normalizeText(item.desc_fuente),
sigla_normalizada: '',
codigo_normalizado: item.fuente?.toString() || ''
}));
// Combinar todos los conjuntos
searchData = [...entidadesIndex, ...objetosIndex, ...rubrosIndex, ...finfunIndex, ...organismosIndex, ...fuentesIndex];
fuseInstance = new Fuse(searchData, fuseOptions);
return fuseInstance;
}
/**
* Busca en el índice
* @param {string} query - Texto de búsqueda
* @param {number} limit - Máximo de resultados (default 30)
* @returns {Array} Resultados de búsqueda
*/
export function search(query, limit = 30) {
if (!fuseInstance) {
console.warn('Índice no inicializado. Llama initSearchIndex() primero.');
return [];
}
if (!query || query.length < 2) {
return [];
}
const normalizedQuery = normalizeText(query);
const results = fuseInstance.search(normalizedQuery, { limit });
return results.map(result => ({
...result.item,
score: result.score,
matches: result.matches
}));
}
/**
* Obtiene todas las entidades (sin búsqueda)
*/
export function getAllEntidades() {
return searchData.filter(item => item.tipo === 'entidad');
}
/**
* Obtiene todos los objetos de gasto (sin búsqueda)
*/
export function getAllObjetos() {
return searchData.filter(item => item.tipo === 'objeto_gasto');
}
/**
* Verifica si el índice está cargado
*/
export function isIndexReady() {
return fuseInstance !== null;
}
import { writable, derived, get } from 'svelte/store';
import { initSearchIndex, search, isIndexReady } from '$lib/services/search';
import { writable, derived } from 'svelte/store';
// Estado del buscador
export const query = writable('');
export const results = writable([]);
export const isLoading = writable(false);
export const indexLoaded = writable(false);
export const indexLoaded = writable(true);
export const error = writable(null);
export const selectedIndex = writable(-1);
export const filterType = writable('todos'); // todos, entidad, objeto_gasto, rubro, finfun
export const filterType = writable('todos');
// Estado derivado: si está mostrando resultados
export const showResults = derived(
[query, results, indexLoaded],
([$query, $results, $indexLoaded]) => $indexLoaded && $query.length >= 2
);
/**
* Inicializa el índice de búsqueda
*/
export async function initIndex() {
if (isIndexReady()) {
indexLoaded.set(true);
return;
}
isLoading.set(true);
error.set(null);
try {
await initSearchIndex();
indexLoaded.set(true);
} catch (e) {
error.set('Error al cargar el índice de búsqueda');
console.error(e);
} finally {
isLoading.set(false);
}
}
/**
* Realiza la búsqueda
*/
export function performSearch(searchQuery, filter = null) {
query.set(searchQuery);
selectedIndex.set(-1);
if (searchQuery.length < 2) {
results.set([]);
return;
}
let searchResults = search(searchQuery, 30);
// Filtrar por tipo si no es "todos"
const currentFilter = filter !== null ? filter : get(filterType);
if (currentFilter !== 'todos') {
searchResults = searchResults.filter(r => r.tipo === currentFilter);
}
// Limitar resultados
results.set(searchResults.slice(0, 15));
}
/**
* Limpia la búsqueda
*/
export function performSearch() {}
export function clearSearch() {
query.set('');
results.set([]);
selectedIndex.set(-1);
}
/**
* Cambia el filtro de tipo
*/
export function setFilter(type) {
filterType.set(type);
// Re-ejecutar búsqueda con el nuevo filtro
const currentQuery = get(query);
if (currentQuery.length >= 2) {
performSearch(currentQuery, type);
}
}
/**
* Navegación por teclado
*/
export function navigateResults(direction, totalResults) {
selectedIndex.update(current => {
if (direction === 'down') {
return current < totalResults - 1 ? current + 1 : 0;
} else if (direction === 'up') {
return current > 0 ? current - 1 : totalResults - 1;
}
return current;
});
}
export function setFilter() {}
export function navigateResults() {}
......
import { createClient } from '@supabase/supabase-js';
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
import { supabase } from '$lib/supabase';
import { json } from '@sveltejs/kit';
export async function GET() {
const { data, error } = await supabase
.schema('ppto')
.from('clas_institucional')
.select('*')
.limit(5);
if (error) {
return json({ error: error.message }, { status: 500 });
}
return json({
success: true,
count: data.length,
columns: data.length > 0 ? Object.keys(data[0]) : [],
sample: data
});
}
This diff is collapsed. Click to expand it.
......@@ -69,6 +69,7 @@
}
} catch { entityError = true; }
loadingEntities = false;
console.log('[DESCARGAS] entities loaded:', entities.length);
});
</script>
......
import { supabase } from '$lib/supabase';
import { error } from '@sveltejs/kit';
export async function load({ params }) {
console.time('[SERVER] Total load fuente');
const { codigo } = params;
console.time('[SERVER] Query clas_fuentes');
const { data, error: dbError } = await supabase
.schema('ppto')
.from('clas_fuentes')
.select('*')
.eq('fuente', codigo);
console.timeEnd('[SERVER] Query clas_fuentes');
if (dbError || !data || data.length === 0) {
throw error(404, 'Fuente no encontrada');
}
const fuente = data[0];
// Obtener hermanos (otras fuentes del mismo grupo)
let hermanos = [];
console.time('[SERVER] Query hermanos');
const { data: hermanosData } = await supabase
.schema('ppto')
.from('clas_fuentes')
.select('*')
.eq('grupo_fuente', fuente.grupo_fuente)
.neq('fuente', codigo)
.order('fuente');
if (hermanosData) {
hermanos = hermanosData;
}
console.timeEnd('[SERVER] Query hermanos');
console.timeEnd('[SERVER] Total load fuente');
return {
fuente,
hermanos
};
}
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.
import { supabase } from '$lib/supabase';
// Hardcoded a objeto 11700 (Sueldos) para testing de layout
const CODIGO = '11700';
function getGrupoCode(objeto) { return objeto.charAt(0) + '0000'; }
function getSubgrupoCode(objeto) { return objeto.substring(0, 2) + '000'; }
function getPartidaCode(objeto) { return objeto.substring(0, 3) + '00'; }
export async function load() {
const { data } = await supabase.schema('ppto').from('clas_objetos').select('*').eq('objeto', CODIGO);
const objeto = data[0];
const grupoCode = getGrupoCode(CODIGO);
const subgrupoCode = getSubgrupoCode(CODIGO);
const partidaCode = getPartidaCode(CODIGO);
const { data: padresData } = await supabase.schema('ppto').from('clas_objetos').select('*').in('objeto', [grupoCode, subgrupoCode, partidaCode]).order('objeto');
const padres = padresData || [];
// Contar entidades
const nEntidades = objeto.n_entidades || 0;
return { objeto, padres, hijos: [], nEntidades };
}
This diff is collapsed. Click to expand it.
import { supabase } from '$lib/supabase';
import { error } from '@sveltejs/kit';
// Funciones para derivar jerarquía del código objeto
function getGrupoCode(objeto) {
return objeto.charAt(0) + '0000';
}
function getSubgrupoCode(objeto) {
return objeto.substring(0, 2) + '000';
}
function getPartidaCode(objeto) {
return objeto.substring(0, 3) + '00';
}
function getNivel(objeto) {
if (objeto.endsWith('0000')) return 'grupo';
if (objeto.endsWith('000')) return 'subgrupo';
if (objeto.endsWith('00')) return 'partida';
return 'subpartida';
}
export async function load({ params }) {
console.time('[SERVER] Total load objeto');
const { codigo } = params;
console.time('[SERVER] Query clas_objetos');
const { data, error: dbError } = await supabase
.schema('ppto')
.from('clas_objetos')
.select('*')
.eq('objeto', codigo);
console.timeEnd('[SERVER] Query clas_objetos');
if (dbError || !data || data.length === 0) {
throw error(404, 'Objeto no encontrado');
}
// Tomar el primer resultado
const objeto = data[0];
const nivel = objeto.nivel || getNivel(codigo);
// Obtener jerarquía (padres e hijos)
let padres = [];
let hijos = [];
console.time('[SERVER] Query padres');
// Buscar padres según nivel
if (nivel === 'subpartida') {
// Padres: grupo, subgrupo, partida
const grupoCode = getGrupoCode(codigo);
const subgrupoCode = getSubgrupoCode(codigo);
const partidaCode = getPartidaCode(codigo);
const { data: padresData } = await supabase
.schema('ppto')
.from('clas_objetos')
.select('*')
.in('objeto', [grupoCode, subgrupoCode, partidaCode])
.order('objeto');
if (padresData) padres = padresData;
} else if (nivel === 'partida') {
// Padres: grupo, subgrupo
const grupoCode = getGrupoCode(codigo);
const subgrupoCode = getSubgrupoCode(codigo);
const { data: padresData } = await supabase
.schema('ppto')
.from('clas_objetos')
.select('*')
.in('objeto', [grupoCode, subgrupoCode])
.order('objeto');
if (padresData) padres = padresData;
} else if (nivel === 'subgrupo') {
// Padre: grupo
const grupoCode = getGrupoCode(codigo);
const { data: padresData } = await supabase
.schema('ppto')
.from('clas_objetos')
.select('*')
.eq('objeto', grupoCode);
if (padresData) padres = padresData;
}
// grupo no tiene padres
console.timeEnd('[SERVER] Query padres');
console.time('[SERVER] Query hijos');
// Buscar hijos según nivel
if (nivel === 'grupo') {
// Hijos: subgrupos que empiecen con el mismo dígito
const prefix = codigo.charAt(0);
const { data: hijosData } = await supabase
.schema('ppto')
.from('clas_objetos')
.select('*')
.eq('nivel', 'subgrupo')
.like('objeto', `${prefix}%`)
.order('objeto');
hijos = hijosData || [];
} else if (nivel === 'subgrupo') {
// Hijos: partidas que empiecen con los mismos 2 dígitos
const prefix = codigo.substring(0, 2);
const { data: hijosData } = await supabase
.schema('ppto')
.from('clas_objetos')
.select('*')
.eq('nivel', 'partida')
.like('objeto', `${prefix}%`)
.order('objeto');
hijos = hijosData || [];
} else if (nivel === 'partida') {
// Hijos: subpartidas que empiecen con los mismos 3 dígitos
const prefix = codigo.substring(0, 3);
const { data: hijosData } = await supabase
.schema('ppto')
.from('clas_objetos')
.select('*')
.eq('nivel', 'subpartida')
.like('objeto', `${prefix}%`)
.order('objeto');
hijos = hijosData || [];
}
// subpartida no tiene hijos
console.timeEnd('[SERVER] Query hijos');
console.timeEnd('[SERVER] Total load objeto');
return {
objeto,
padres,
hijos
};
}
This diff is collapsed. Click to expand it.
<script>
import { onMount } from 'svelte';
import * as d3 from 'd3';
let mapaData = $state(null);
let boliviaPath = $state('');
let deptoPath = $state('');
let muniPath = $state('');
let error = $state('');
let debug = $state('');
onMount(async () => {
try {
const res = await fetch('/mapa.json');
mapaData = await res.json();
debug = `bolivia: ${mapaData.bolivia.features.length} features, `;
debug += `deptos: ${mapaData.departamentos.features.length}, `;
debug += `munis: ${mapaData.municipios.features.length}`;
// Quitar CRS y corregir winding order
delete mapaData.bolivia.crs;
delete mapaData.departamentos.crs;
delete mapaData.municipios.crs;
// Corregir winding order: invertir todos los anillos
function fixWinding(geojson) {
const fix = (coords, isHole) => {
// Para geo coords: exterior debe ser counterclockwise (area < 0 en D3)
// Simplemente invertimos todos los anillos exteriores
coords.forEach((ring, i) => {
if (i === 0) ring.reverse(); // exterior
// holes quedan como están
});
};
for (const feat of geojson.features || [geojson]) {
const geom = (feat.geometry || feat);
if (geom.type === 'Polygon') {
fix(geom.coordinates);
} else if (geom.type === 'MultiPolygon') {
geom.coordinates.forEach(poly => fix(poly));
}
}
}
fixWinding(mapaData.bolivia);
fixWinding(mapaData.departamentos);
fixWinding(mapaData.municipios);
const bounds = d3.geoBounds(mapaData.bolivia);
debug += ` | bounds: ${JSON.stringify(bounds)}`;
const projection = d3.geoMercator().fitSize([380, 480], mapaData.bolivia);
const pathGen = d3.geoPath(projection);
boliviaPath = pathGen(mapaData.bolivia) || '';
debug += ` | boliviaPath length: ${boliviaPath.length}`;
deptoPath = mapaData.departamentos.features.map(f => pathGen(f)).join(' ');
debug += ` | deptoPath length: ${deptoPath.length}`;
const muni = mapaData.municipios.features[0];
muniPath = pathGen(muni) || '';
debug += ` | muni code: ${muni.properties.codigo}`;
} catch (e) {
error = e.message;
}
});
</script>
<div style="padding: 2rem; background: #1C1C1A; color: #F5F0E8; min-height: 100vh;">
<h1>Test Mapa</h1>
<p style="font-size: 12px; color: #888;">{debug}</p>
{#if error}
<p style="color: red;">{error}</p>
{/if}
<svg width="400" height="500" viewBox="0 0 400 500" style="border: 1px solid #333;">
{#if boliviaPath}
<path d={boliviaPath} fill="none" stroke="#555" stroke-width="1" />
{/if}
{#if deptoPath}
<path d={deptoPath} fill="rgba(255,255,255,0.1)" stroke="#777" stroke-width="0.5" />
{/if}
{#if muniPath}
<path d={muniPath} fill="#D4A574" stroke="#D4A574" stroke-width="1" />
{/if}
</svg>
</div>
import { supabase } from '$lib/supabase';
export const ssr = false;
export async function load({ fetch }) {
// Cargar resumen y población en paralelo
const [resumenRes, pobRes] = await Promise.all([
supabase
.schema('ppto')
.from('entidad_resumen')
.select('codigo, desc, gestion, devengado, ranking')
.eq('tipo', 'gastos')
.eq('tipo_codigo', 'municipio_ubigeo')
.eq('gestion', 2025)
.order('devengado', { ascending: false })
.limit(500),
fetch('/poblacion.csv').then(r => r.text())
]);
const pobRes = await fetch('/poblacion.csv').then(r => r.text());
const poblacionMap = {};
pobRes.split('\n').slice(1).forEach(line => {
......@@ -27,7 +14,7 @@ export async function load({ fetch }) {
const gestiones = [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025];
return {
resumen: resumenRes.data || [],
resumen: [],
poblacionMap,
gestiones,
gestionInicial: 2025
......
......@@ -4,7 +4,6 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import * as d3 from 'd3';
import { supabase } from '$lib/supabase';
import { mapaCache } from '$lib/stores/mapaCache';
let { data } = $props();
......@@ -38,6 +37,22 @@
let clasSearchVal = $state('');
let clasSearchResults = $state([]);
let clasSearchFocused = $state(false);
let clasSelectedIdx = $state(-1);
function handleClasKeydown(e) {
if (e.key === 'ArrowDown') {
e.preventDefault();
clasSelectedIdx = clasSelectedIdx < clasSearchResults.length - 1 ? clasSelectedIdx + 1 : 0;
} else if (e.key === 'ArrowUp') {
e.preventDefault();
clasSelectedIdx = clasSelectedIdx > 0 ? clasSelectedIdx - 1 : clasSearchResults.length - 1;
} else if (e.key === 'Enter' && clasSelectedIdx >= 0) {
e.preventDefault();
seleccionarClasificador(clasSearchResults[clasSelectedIdx]);
} else if (e.key === 'Escape') {
clasSearchFocused = false;
}
}
let clasSearchLoading = $state(false);
let clasSeleccionado = $state(null);
let clasDebounce;
......@@ -89,6 +104,7 @@
function handleClasInput(e) {
clasSearchVal = e.target.value;
clasSelectedIdx = -1;
clearTimeout(clasDebounce);
if (clasSearchVal.length >= 2) {
clasDebounce = setTimeout(() => buscarClasificador(clasSearchVal), 200);
......@@ -362,20 +378,9 @@
return;
}
// Modo total: cargar desde Supabase
if (!resumenCache[gestion]) {
const { data: rows } = await supabase
.schema('ppto')
.from('entidad_resumen')
.select('codigo, desc, gestion, devengado, ranking')
.eq('tipo', 'gastos')
.eq('tipo_codigo', 'municipio_ubigeo')
.eq('gestion', gestion)
.order('devengado', { ascending: false })
.limit(500);
resumenCache[gestion] = rows || [];
}
resumen = resumenCache[gestion];
// Modo total: datos no disponibles sin Supabase
// TODO: implementar endpoint API para resumen por municipio
resumen = [];
poblacionMap = await getPoblacionAno(gestion);
}
......@@ -588,6 +593,7 @@
type="text"
value={clasSearchVal}
oninput={handleClasInput}
onkeydown={handleClasKeydown}
onfocus={() => clasSearchFocused = true}
onblur={() => setTimeout(() => clasSearchFocused = false, 150)}
placeholder="Buscar {clasificadorSeleccionado === 'objeto' ? 'partida' : clasificadorSeleccionado === 'finfun' ? 'finalidad o función' : 'sector'}..."
......@@ -599,8 +605,8 @@
{:else if clasSearchResults.length === 0}
<div class="objeto-search-msg">Sin resultados</div>
{:else}
{#each clasSearchResults as result}
<button class="objeto-search-item" onclick={() => seleccionarClasificador(result)}>
{#each clasSearchResults as result, i}
<button class="objeto-search-item" class:active={clasSelectedIdx === i} onclick={() => seleccionarClasificador(result)}>
<span class="objeto-search-code">{result.codigo}</span>
<span class="objeto-search-name">{@html result.highlight}</span>
</button>
......