Rafael Lopez

objetos

......@@ -45,7 +45,7 @@
scaleBand()
.domain(data.map(d => d.año))
.range([0, 100]) // porcentaje
.padding(0.2)
.padding(0.6)
);
// Para CSS bottom positioning: 0 → 0%, max → 100%
......@@ -112,15 +112,11 @@
<!-- Eje X -->
<div class="x-axis" style="height: {marginBottom}px; left: {marginLeft}px; right: {marginRight}px">
{#each data as d, i}
{@const isFirst = i === 0}
{@const isLast = i === data.length - 1}
{@const isMid = i === midIndex && data.length > 5}
{@const showLabel = isFirst || isLast || isMid || hoveredYear === d.año}
<span
class="x-label"
class:visible={showLabel}
class="x-label visible"
class:hovered={hoveredYear === d.año}
>
{d.año}
{String(d.año).slice(-2)}
</span>
{/each}
</div>
......@@ -170,7 +166,7 @@
bottom: 0;
display: flex;
align-items: flex-end;
gap: 2px;
gap: 4px;
}
.bar-container {
......@@ -178,11 +174,12 @@
height: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
cursor: default;
}
.bar {
width: 100%;
width: 80%;
background: #c4897d;
border-radius: 4px 4px 0 0;
transition: opacity 0.1s ease;
......@@ -206,20 +203,27 @@
bottom: 0;
display: flex;
align-items: center;
gap: 4px;
}
.x-label {
flex: 1;
text-align: center;
font-family: 'Qanelas', var(--font-sans);
font-size: 0.6875rem;
font-family: 'DM Mono', monospace;
font-size: 0.5625rem;
color: var(--theme-texto);
opacity: 0;
transition: opacity 0.2s ease;
}
.x-label.visible {
opacity: 0.5;
}
.x-label.hovered {
opacity: 1;
color: var(--theme-titulo);
font-weight: 600;
}
.chart-empty {
......
......@@ -101,12 +101,14 @@
padding: 0.125rem 0;
background: transparent;
border: none;
border-bottom: 1px dotted #333333;
border-bottom: 1px dotted #666666;
color: var(--theme-titulo);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: border-color 0.2s;
max-width: 100%;
min-width: 0;
}
.entity-btn:hover {
......@@ -115,6 +117,9 @@
.entity-label {
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chevron {
......@@ -132,7 +137,8 @@
position: absolute;
top: calc(100% + 4px);
left: 0;
min-width: 280px;
right: 0;
min-width: min(280px, calc(100vw - 2rem));
background: var(--theme-surface);
border: 1px solid var(--theme-borde);
border-radius: 10px;
......
<script>
import { goto } from '$app/navigation';
import { indexLoaded } from '$lib/stores/searchStore';
import { search } from '$lib/services/search';
let {
preserveVista = false,
......@@ -16,20 +14,46 @@
let selectedIdx = $state(-1);
let debounceTimer;
function handleSearchInput(e) {
const val = e.target.value;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (val.length >= 2) {
function parseMetadatos(meta) {
if (!meta) return {};
if (typeof meta === 'object') return meta;
try { return JSON.parse(meta); } catch { return {}; }
}
async function doSearch(query) {
isSearching = true;
// Buscar y filtrar solo objetos de gasto
const allResults = search(val, 50);
localResults = allResults.filter(r => r.tipo === 'objeto_gasto').slice(0, 15);
try {
const params = new URLSearchParams({
q: query,
per_page: '20',
is_class: 'true',
class_: 'objeto'
});
const res = await fetch(`/api/search?${params}`);
if (!res.ok) throw new Error();
const data = await res.json();
localResults = (data.hits || []).map(hit => {
const meta = parseMetadatos(hit.document.metadatos);
const codigo = meta.objeto_subpartida || meta.objeto_partida || meta.objeto_subgrupo || meta.objeto_grupo || '';
const nombre = hit.document.texto;
const highlight = hit.highlights?.[0]?.snippet || nombre;
return { codigo, nombre, highlight };
});
selectedIdx = -1;
} catch {
localResults = [];
}
isSearching = false;
}
function handleSearchInput(e) {
searchVal = e.target.value;
clearTimeout(debounceTimer);
if (searchVal.length >= 2) {
debounceTimer = setTimeout(() => doSearch(searchVal), 200);
} else {
localResults = [];
}
}, 200);
}
function handleSearchKeydown(e) {
......@@ -86,7 +110,6 @@
onfocus={() => searchFocused = true}
onblur={handleSearchBlur}
placeholder="Buscar objeto..."
disabled={!$indexLoaded}
/>
{#if searchVal}
<button class="search-clear" onclick={handleClear}>
......@@ -110,7 +133,7 @@
onmouseenter={() => selectedIdx = i}
>
<span class="item-code">{result.codigo}</span>
<span class="item-name">{result.nombre}</span>
<span class="item-name">{@html result.highlight}</span>
</button>
{/each}
{/if}
......@@ -229,4 +252,11 @@
font-size: 0.8125rem;
color: var(--theme-titulo);
}
:global(.item-name mark) {
background: rgba(201, 167, 81, 0.3);
color: inherit;
border-radius: 2px;
padding: 0 1px;
}
</style>
......
......@@ -72,13 +72,13 @@
// Detectar tipos de clasificadores presentes en los resultados
// Mapeo inverso: API class_ → id interno del clasificador
const CLASS_REVERSE_MAP = { ubigeo: 'geografico', acteco: 'sectores' };
const CLASS_REVERSE_MAP = { ubigeo: 'geografico', acteco: 'sectores', finalidad: 'finfun' };
// Filtrar hits según clasificadores seleccionados (la API puede devolver tipos extra)
$: filteredHits = (() => {
if (searchMode !== 'clasificadores' || selectedClassifiers.length === 0) return allHits;
const allowedClasses = new Set(selectedClassifiers.map(c => {
const map = { geografico: 'ubigeo', sectores: 'acteco' };
const map = { geografico: 'ubigeo', sectores: 'acteco', finfun: 'finalidad' };
return map[c] || c;
}));
return allHits.filter(h => allowedClasses.has(h.document.class_));
......@@ -86,7 +86,12 @@
$: activeClassTypes = (() => {
const types = new Set();
filteredHits.forEach(h => { if (h.document.class_) types.add(h.document.class_); });
filteredHits.forEach(h => {
if (h.document.class_) {
const normalized = CLASS_REVERSE_MAP[h.document.class_] || h.document.class_;
types.add(normalized);
}
});
return types;
})();
$: isMixedClassSearch = activeClassTypes.size > 1;
......@@ -154,14 +159,14 @@
if (isMixedClassSearch || searchMode === 'todo') {
// Filtrar duplicados de finfun
const filtered = filteredHits.filter(hit => {
if (hit.document.class_ !== 'finfun') return true;
if (hit.document.class_ !== 'finfun' && hit.document.class_ !== 'finalidad') return true;
const m = parseMetadatos(hit.document.metadatos);
return !(m.finfun_funcion === 0 && m.finfun_grpfuncion !== undefined);
});
const groups = {};
filtered.forEach(hit => {
const type = hit.document.class_ || 'otros';
const type = CLASS_REVERSE_MAP[hit.document.class_] || hit.document.class_ || 'otros';
const groupName = CLASS_LABELS[type] || type;
if (!groups[groupName]) {
groups[groupName] = { name: groupName, hits: [], totalMonto: 0, classType: type };
......@@ -692,7 +697,7 @@
const isClassResult = hit.document.is_class === true;
let url;
const isFinfunClass = hit.document.class_ === 'finfun';
const isFinfunClass = hit.document.class_ === 'finfun' || hit.document.class_ === 'finalidad';
const isUbigeoClass = hit.document.class_ === 'ubigeo';
......@@ -1207,7 +1212,7 @@
{#each group.hits.slice(0, maxVisible) as hit, i (getHitKey(hit, gi * 1000 + i))}
{@const meta = parseMetadatos(hit.document.metadatos)}
{@const globalIdx = flatHitsForNav.indexOf(hit)}
{@const hitClass = hit.document.class_}
{@const hitClass = CLASS_REVERSE_MAP[hit.document.class_] || hit.document.class_}
{#if hitClass === 'entidad'}
{@const isDA = meta.da && meta.entidad_desc_entidad}
{@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null}
......@@ -1323,10 +1328,10 @@
{@const subarea = isEntidadClass ? extractAfterHyphen(meta.entidad_desc_subarea) : null}
{@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null}
{@const isObjetoClass = hit.document.class_ === 'objeto'}
{@const isFinfunClass = hit.document.class_ === 'finfun'}
{@const isFinfunClass = hit.document.class_ === 'finfun' || hit.document.class_ === 'finalidad'}
{@const isUbigeoClass = hit.document.class_ === 'ubigeo'}
{@const isClassResult = hit.document.is_class === true}
{@const hitType = hit.document.class_}
{@const hitType = CLASS_REVERSE_MAP[hit.document.class_] || hit.document.class_}
<a
href={isEntidadClass
? `/entidad/${meta.da ? `${meta.entidad}.${meta.da}` : meta.entidad}`
......
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
......@@ -28,8 +28,8 @@
let topEntidadesPorAño = $state({});
const POBLACION = 12000000;
const objetoCodigo = '11700';
const objetoNivel = 'subpartida';
let objetoCodigo = $derived(objetoData.objeto);
let objetoNivel = $derived(objetoData.nivel);
let gastoPerCapita = $derived(
datosAnuales.map(d => ({
......
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.