Rafael Lopez

objetos

1 node_modules 1 node_modules
2 +API_ENDPOINTS.md
2 3
3 # Output 4 # Output
4 .output 5 .output
......
...@@ -36,9 +36,16 @@ ...@@ -36,9 +36,16 @@
36 let chartHeight = $derived(Math.max(effectiveHeight - marginTop - marginBottom, 50)); 36 let chartHeight = $derived(Math.max(effectiveHeight - marginTop - marginBottom, 50));
37 37
38 // Máximo del eje Y (con guard para data vacía o valores undefined) 38 // Máximo del eje Y (con guard para data vacía o valores undefined)
39 - let maxPerCapita = $derived( 39 + let maxPerCapita = $derived.by(() => {
40 - data.length > 0 ? Math.max(...data.map(d => d.perCapita || 0), 1) : 1 40 + if (data.length === 0) return 1;
41 - ); 41 + const vals = data.map(d => typeof d.perCapita === 'number' && isFinite(d.perCapita) ? d.perCapita : 0);
42 + const max = Math.max(...vals);
43 + const min = Math.min(...vals.filter(v => v > 0));
44 + // Si max es 0, usar 1 como fallback
45 + if (max <= 0) return 1;
46 + // Agregar 10% de margen arriba para que la barra más alta no toque el techo
47 + return max * 1.1;
48 + });
42 49
43 // Escalas D3 50 // Escalas D3
44 let xScale = $derived( 51 let xScale = $derived(
...@@ -92,7 +99,8 @@ ...@@ -92,7 +99,8 @@
92 <!-- Barras --> 99 <!-- Barras -->
93 <div class="bars" onmouseleave={() => hoveredYear = null} role="group"> 100 <div class="bars" onmouseleave={() => hoveredYear = null} role="group">
94 {#each data as d} 101 {#each data as d}
95 - {@const barPct = (yScale(d.perCapita) / chartHeight) * 100} 102 + {@const safeVal = typeof d.perCapita === 'number' && isFinite(d.perCapita) ? d.perCapita : 0}
103 + {@const barPct = chartHeight > 0 ? (yScale(safeVal) / chartHeight) * 100 : 0}
96 <div 104 <div
97 class="bar-container" 105 class="bar-container"
98 onmouseenter={() => hoveredYear = d.año} 106 onmouseenter={() => hoveredYear = d.año}
...@@ -102,7 +110,7 @@ ...@@ -102,7 +110,7 @@
102 <div 110 <div
103 class="bar" 111 class="bar"
104 class:hovered={hoveredYear === d.año} 112 class:hovered={hoveredYear === d.año}
105 - style="height: {barPct}%" 113 + style="height: {barPct}%; {safeVal > 0 ? 'min-height: 3px;' : ''}"
106 ></div> 114 ></div>
107 </div> 115 </div>
108 {/each} 116 {/each}
......
...@@ -109,7 +109,7 @@ ...@@ -109,7 +109,7 @@
109 onkeydown={handleSearchKeydown} 109 onkeydown={handleSearchKeydown}
110 onfocus={() => searchFocused = true} 110 onfocus={() => searchFocused = true}
111 onblur={handleSearchBlur} 111 onblur={handleSearchBlur}
112 - placeholder="Buscar objeto..." 112 + placeholder="Buscar otro gasto..."
113 /> 113 />
114 {#if searchVal} 114 {#if searchVal}
115 <button class="search-clear" onclick={handleClear}> 115 <button class="search-clear" onclick={handleClear}>
......
...@@ -108,7 +108,19 @@ ...@@ -108,7 +108,19 @@
108 codigo = `${meta.entidad || ''}-${meta.programa || ''}-${meta.proyecto || ''}`; 108 codigo = `${meta.entidad || ''}-${meta.programa || ''}-${meta.proyecto || ''}`;
109 } 109 }
110 110
111 - return { tipo, codigo, nombre, highlight, devengado: hit.document.devengado, gestion: hit.document.gestion }; 111 + // Contexto extra para programas
112 + let contexto = '';
113 + if (!isClass) {
114 + const ents = meta.entidades || [];
115 + const entNombre = ents.length === 1 ? ents[0].entidad_desc_entidad : ents.length > 1 ? `${ents.length} entidades` : '';
116 + const prog = meta.programa_desc || '';
117 + if (entNombre && prog) contexto = `${entNombre} · Programa: ${prog}`;
118 + else if (entNombre) contexto = entNombre;
119 + else if (prog) contexto = `Programa: ${prog}`;
120 + console.log('[SEARCH] programa contexto:', { isClass, tipo, nombre, entNombre, prog, contexto, metaKeys: Object.keys(meta) });
121 + }
122 +
123 + return { tipo, codigo, nombre, highlight, devengado: hit.document.devengado, gestion: hit.document.gestion, contexto };
112 }); 124 });
113 selectedIdx = -1; 125 selectedIdx = -1;
114 } catch { 126 } catch {
...@@ -184,7 +196,6 @@ ...@@ -184,7 +196,6 @@
184 } 196 }
185 197
186 function goToResult(item) { 198 function goToResult(item) {
187 - closeModal();
188 const routes = { 199 const routes = {
189 entidad: '/entidad/', 200 entidad: '/entidad/',
190 objeto_gasto: '/objeto/', 201 objeto_gasto: '/objeto/',
...@@ -196,7 +207,12 @@ ...@@ -196,7 +207,12 @@
196 programa: '/proyecto/' 207 programa: '/proyecto/'
197 }; 208 };
198 const base = routes[item.tipo]; 209 const base = routes[item.tipo];
199 - if (base) goto(`${base}${item.codigo}`); 210 + if (base) {
211 + const url = `${base}${item.codigo}`;
212 + console.log('[SEARCH] goToResult:', item.tipo, item.codigo, '→', url);
213 + open = false;
214 + window.location.href = url;
215 + }
200 } 216 }
201 217
202 function handleBackdropClick(e) { 218 function handleBackdropClick(e) {
...@@ -319,6 +335,9 @@ ...@@ -319,6 +335,9 @@
319 {typeConfig[item.tipo]?.label || item.tipo} 335 {typeConfig[item.tipo]?.label || item.tipo}
320 </span> 336 </span>
321 <span class="result-name">{@html item.highlight}</span> 337 <span class="result-name">{@html item.highlight}</span>
338 + {#if item.contexto}
339 + <span class="result-contexto">{item.contexto}</span>
340 + {/if}
322 </div> 341 </div>
323 <span class="result-code">{item.codigo}</span> 342 <span class="result-code">{item.codigo}</span>
324 </button> 343 </button>
...@@ -646,6 +665,15 @@ ...@@ -646,6 +665,15 @@
646 text-overflow: ellipsis; 665 text-overflow: ellipsis;
647 } 666 }
648 667
668 + .result-contexto {
669 + font-size: 0.75rem;
670 + color: var(--theme-texto);
671 + opacity: 0.6;
672 + white-space: nowrap;
673 + overflow: hidden;
674 + text-overflow: ellipsis;
675 + }
676 +
649 .result-desc { 677 .result-desc {
650 font-size: 0.8125rem; 678 font-size: 0.8125rem;
651 color: var(--theme-texto); 679 color: var(--theme-texto);
......
...@@ -1379,9 +1379,16 @@ ...@@ -1379,9 +1379,16 @@
1379 </div> 1379 </div>
1380 <div class="result-meta"> 1380 <div class="result-meta">
1381 <span class="result-monto">{formatMonto(hit.document.devengado)}</span> 1381 <span class="result-monto">{formatMonto(hit.document.devengado)}</span>
1382 - {#if meta.entidad_desc_entidad} 1382 + {#if meta.entidades?.length === 1}
1383 <span class="result-sep">·</span> 1383 <span class="result-sep">·</span>
1384 - <span class="result-entity">{meta.entidad_desc_entidad}</span> 1384 + <span class="result-entity">{meta.entidades[0].entidad_desc_entidad}</span>
1385 + {:else if meta.entidades?.length > 1}
1386 + <span class="result-sep">·</span>
1387 + <span class="result-entity">{meta.entidades.length} entidades</span>
1388 + {/if}
1389 + {#if meta.programa_desc}
1390 + <span class="result-sep">·</span>
1391 + <span class="result-entity">{meta.programa_desc}</span>
1385 {/if} 1392 {/if}
1386 </div> 1393 </div>
1387 </div> 1394 </div>
...@@ -1443,10 +1450,20 @@ ...@@ -1443,10 +1450,20 @@
1443 <span class="result-year">{formatYears(meta.gestion)}</span> 1450 <span class="result-year">{formatYears(meta.gestion)}</span>
1444 <span class="result-sep">·</span> 1451 <span class="result-sep">·</span>
1445 <span class="result-monto">{formatMonto(hit.document.devengado)}</span> 1452 <span class="result-monto">{formatMonto(hit.document.devengado)}</span>
1446 - {#if meta.entidad_desc_entidad} 1453 + {#if meta.entidades?.length === 1}
1454 + <span class="result-sep">·</span>
1455 + <span class="result-entity">{meta.entidades[0].entidad_desc_entidad}</span>
1456 + {:else if meta.entidades?.length > 1}
1457 + <span class="result-sep">·</span>
1458 + <span class="result-entity">{meta.entidades.length} entidades</span>
1459 + {:else if meta.entidad_desc_entidad}
1447 <span class="result-sep">·</span> 1460 <span class="result-sep">·</span>
1448 <span class="result-entity">{meta.entidad_desc_entidad}</span> 1461 <span class="result-entity">{meta.entidad_desc_entidad}</span>
1449 {/if} 1462 {/if}
1463 + {#if meta.programa_desc}
1464 + <span class="result-sep">·</span>
1465 + <span class="result-entity">Programa: {meta.programa_desc}</span>
1466 + {/if}
1450 {/if} 1467 {/if}
1451 {#if isEntidadClass || isClassResult} 1468 {#if isEntidadClass || isClassResult}
1452 <span class="result-monto">{formatMonto(hit.document.devengado)}</span> 1469 <span class="result-monto">{formatMonto(hit.document.devengado)}</span>
......
1 +const API_BASE = 'http://136.112.29.74/api/entidad/clasificador';
2 +
3 +export async function GET() {
4 + try {
5 + const response = await fetch(API_BASE);
6 + if (!response.ok) {
7 + return new Response(JSON.stringify({ error: `API error: ${response.status}` }), {
8 + status: response.status, headers: { 'Content-Type': 'application/json' }
9 + });
10 + }
11 + const data = await response.json();
12 + return new Response(JSON.stringify(data), {
13 + headers: { 'Content-Type': 'application/json' }
14 + });
15 + } catch {
16 + return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
17 + status: 500, headers: { 'Content-Type': 'application/json' }
18 + });
19 + }
20 +}
1 +const API_BASE = 'http://136.112.29.74/api/objeto/clasificador';
2 +
3 +export async function GET() {
4 + try {
5 + const response = await fetch(API_BASE);
6 + if (!response.ok) {
7 + return new Response(JSON.stringify({ error: `API error: ${response.status}` }), {
8 + status: response.status, headers: { 'Content-Type': 'application/json' }
9 + });
10 + }
11 + const data = await response.json();
12 + return new Response(JSON.stringify(data), {
13 + headers: { 'Content-Type': 'application/json' }
14 + });
15 + } catch {
16 + return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
17 + status: 500, headers: { 'Content-Type': 'application/json' }
18 + });
19 + }
20 +}
1 +const API_BASE = 'http://136.112.29.74/api/objeto';
2 +
3 +export async function GET({ url }) {
4 + const codigo = url.searchParams.get('codigo') || '';
5 + const tipo = url.searchParams.get('tipo') || 'clasificador';
6 + const entidad = url.searchParams.get('entidad') || '';
7 + const gestion = url.searchParams.get('gestion') || '';
8 +
9 + if (!codigo) {
10 + return new Response(JSON.stringify({ error: 'Missing codigo' }), {
11 + status: 400, headers: { 'Content-Type': 'application/json' }
12 + });
13 + }
14 +
15 + let apiUrl;
16 + if (tipo === 'clasificador') {
17 + apiUrl = `${API_BASE}/${codigo}`;
18 + } else if (tipo === 'entidades-lista') {
19 + apiUrl = `${API_BASE}/${codigo}/entidades/map`;
20 + } else if (tipo === 'entidad' && entidad) {
21 + apiUrl = `${API_BASE}/${codigo}/entidades/${entidad}`;
22 + } else if (tipo === 'entidades-año' && gestion) {
23 + apiUrl = `${API_BASE}/${codigo}/entidades?gestion=${gestion}`;
24 + } else {
25 + return new Response(JSON.stringify({ error: 'Invalid tipo' }), {
26 + status: 400, headers: { 'Content-Type': 'application/json' }
27 + });
28 + }
29 +
30 + try {
31 + const response = await fetch(apiUrl);
32 + if (!response.ok) {
33 + return new Response(JSON.stringify({ error: `API error: ${response.status}` }), {
34 + status: response.status, headers: { 'Content-Type': 'application/json' }
35 + });
36 + }
37 + const data = await response.json();
38 + return new Response(JSON.stringify(data), {
39 + headers: { 'Content-Type': 'application/json' }
40 + });
41 + } catch (err) {
42 + return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
43 + status: 500, headers: { 'Content-Type': 'application/json' }
44 + });
45 + }
46 +}
1 +const API_BASE = 'http://136.112.29.74/api/entidad/clasificador';
2 +
3 +export async function GET() {
4 + try {
5 + const res = await fetch(API_BASE);
6 + if (!res.ok) throw new Error();
7 + const data = await res.json();
8 + // Mapear al formato que espera el treemap
9 + const mapped = data.map(d => ({
10 + entidad: d.entidad,
11 + desc_entidad: d.desc_entidad,
12 + sigla_entidad: d.sigla_entidad || ''
13 + }));
14 + return new Response(JSON.stringify(mapped), {
15 + headers: { 'Content-Type': 'application/json' }
16 + });
17 + } catch {
18 + return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
19 + status: 500, headers: { 'Content-Type': 'application/json' }
20 + });
21 + }
22 +}
1 +const API_BASE = 'http://136.112.29.74/api/objeto';
2 +
3 +// Cache en memoria del servidor
4 +let cachedClasificador = null;
5 +let cachedEstados = null; // Map: objeto → estados[]
6 +let cachedTimestamp = 0;
7 +const CACHE_TTL = 5 * 60 * 1000; // 5 minutos
8 +
9 +async function loadAndCacheAll() {
10 + const now = Date.now();
11 + if (cachedEstados && (now - cachedTimestamp) < CACHE_TTL) return;
12 +
13 + // 1. Cargar clasificador
14 + const clasRes = await fetch(`${API_BASE}/clasificador`);
15 + if (!clasRes.ok) throw new Error('Failed to fetch clasificador');
16 + cachedClasificador = await clasRes.json();
17 +
18 + // 2. Cargar todos los objetos en paralelo (lotes de 80)
19 + const estados = new Map();
20 + const batchSize = 80;
21 +
22 + for (let i = 0; i < cachedClasificador.length; i += batchSize) {
23 + const batch = cachedClasificador.slice(i, i + batchSize);
24 + const results = await Promise.all(batch.map(async (item) => {
25 + try {
26 + const res = await fetch(`${API_BASE}/${item.objeto}`);
27 + if (!res.ok) return null;
28 + const data = await res.json();
29 + return { objeto: item.objeto, estados: data.estados || [] };
30 + } catch { return null; }
31 + }));
32 + results.filter(Boolean).forEach(r => estados.set(r.objeto, r.estados));
33 + }
34 +
35 + cachedEstados = estados;
36 + cachedTimestamp = now;
37 +}
38 +
39 +export async function GET({ url }) {
40 + const gestion = parseInt(url.searchParams.get('gestion') || '2025');
41 + const entidad = url.searchParams.get('entidad') || '0';
42 +
43 + try {
44 + if (entidad === '0') {
45 + // Todo el estado — usar cache
46 + await loadAndCacheAll();
47 +
48 + const results = [];
49 + for (const item of cachedClasificador) {
50 + const estados = cachedEstados.get(item.objeto);
51 + if (!estados) continue;
52 + const yearData = estados.find(d => d.gestion === gestion);
53 + if (!yearData || !yearData.total) continue;
54 + results.push({
55 + gestion,
56 + nivel: item.nivel,
57 + objeto: item.objeto,
58 + desc_objeto: item.desc_objeto,
59 + parent: getParent(item.objeto, item.nivel),
60 + devengado: yearData.total
61 + });
62 + }
63 +
64 + return new Response(JSON.stringify(results), {
65 + headers: { 'Content-Type': 'application/json' }
66 + });
67 + } else {
68 + // Entidad específica — cargar por entidad (no cacheable eficientemente)
69 + if (!cachedClasificador) {
70 + const clasRes = await fetch(`${API_BASE}/clasificador`);
71 + cachedClasificador = await clasRes.json();
72 + }
73 +
74 + const results = [];
75 + const batchSize = 80;
76 +
77 + for (let i = 0; i < cachedClasificador.length; i += batchSize) {
78 + const batch = cachedClasificador.slice(i, i + batchSize);
79 + const batchResults = await Promise.all(batch.map(async (item) => {
80 + try {
81 + const res = await fetch(`${API_BASE}/${item.objeto}/entidades/${entidad}`);
82 + if (!res.ok) return null;
83 + const data = await res.json();
84 + const yearData = data.find(d => d.gestion === gestion);
85 + if (!yearData || !yearData.monto) return null;
86 + return {
87 + gestion,
88 + nivel: item.nivel,
89 + objeto: item.objeto,
90 + desc_objeto: item.desc_objeto,
91 + parent: getParent(item.objeto, item.nivel),
92 + devengado: yearData.monto
93 + };
94 + } catch { return null; }
95 + }));
96 + results.push(...batchResults.filter(Boolean));
97 + }
98 +
99 + return new Response(JSON.stringify(results), {
100 + headers: { 'Content-Type': 'application/json' }
101 + });
102 + }
103 + } catch (err) {
104 + return new Response(JSON.stringify({ error: 'Failed to build treemap' }), {
105 + status: 500, headers: { 'Content-Type': 'application/json' }
106 + });
107 + }
108 +}
109 +
110 +function getParent(objeto, nivel) {
111 + if (nivel === 'grupo') return null;
112 + if (nivel === 'subgrupo') return objeto.charAt(0) + '0000';
113 + if (nivel === 'partida') return objeto.substring(0, 2) + '000';
114 + if (nivel === 'subpartida') return objeto.substring(0, 3) + '00';
115 + return null;
116 +}
1 -import { supabase } from '$lib/supabase';
2 import { error } from '@sveltejs/kit'; 1 import { error } from '@sveltejs/kit';
3 2
4 -// Funciones para derivar jerarquía del código objeto 3 +const API_BASE = 'http://136.112.29.74/api/objeto';
5 -function getGrupoCode(objeto) {
6 - return objeto.charAt(0) + '0000';
7 -}
8 -
9 -function getSubgrupoCode(objeto) {
10 - return objeto.substring(0, 2) + '000';
11 -}
12 -
13 -function getPartidaCode(objeto) {
14 - return objeto.substring(0, 3) + '00';
15 -}
16 -
17 -function getNivel(objeto) {
18 - if (objeto.endsWith('0000')) return 'grupo';
19 - if (objeto.endsWith('000')) return 'subgrupo';
20 - if (objeto.endsWith('00')) return 'partida';
21 - return 'subpartida';
22 -}
23 4
24 export async function load({ params }) { 5 export async function load({ params }) {
25 - console.time('[SERVER] Total load objeto');
26 const { codigo } = params; 6 const { codigo } = params;
27 7
28 - console.time('[SERVER] Query clas_objetos'); 8 + // Cargar clasificador desde la API
29 - const { data, error: dbError } = await supabase 9 + const res = await fetch(`${API_BASE}/${codigo}`);
30 - .schema('ppto') 10 + if (!res.ok) throw error(404, 'Objeto no encontrado');
31 - .from('clas_objetos') 11 + const objeto = await res.json();
32 - .select('*')
33 - .eq('objeto', codigo);
34 - console.timeEnd('[SERVER] Query clas_objetos');
35 12
36 - if (dbError || !data || data.length === 0) { 13 + // Derivar padres e hijos del código
37 - throw error(404, 'Objeto no encontrado'); 14 + const nivel = objeto.nivel;
38 - } 15 + let padresCodes = [];
39 -
40 - // Tomar el primer resultado
41 - const objeto = data[0];
42 - const nivel = objeto.nivel || getNivel(codigo);
43 -
44 - // Obtener jerarquía (padres e hijos)
45 - let padres = [];
46 - let hijos = [];
47 16
48 - console.time('[SERVER] Query padres');
49 -
50 - // Buscar padres según nivel
51 if (nivel === 'subpartida') { 17 if (nivel === 'subpartida') {
52 - // Padres: grupo, subgrupo, partida 18 + padresCodes = [
53 - const grupoCode = getGrupoCode(codigo); 19 + codigo.charAt(0) + '0000',
54 - const subgrupoCode = getSubgrupoCode(codigo); 20 + codigo.substring(0, 2) + '000',
55 - const partidaCode = getPartidaCode(codigo); 21 + codigo.substring(0, 3) + '00'
56 - 22 + ];
57 - const { data: padresData } = await supabase
58 - .schema('ppto')
59 - .from('clas_objetos')
60 - .select('*')
61 - .in('objeto', [grupoCode, subgrupoCode, partidaCode])
62 - .order('objeto');
63 -
64 - if (padresData) padres = padresData;
65 } else if (nivel === 'partida') { 23 } else if (nivel === 'partida') {
66 - // Padres: grupo, subgrupo 24 + padresCodes = [
67 - const grupoCode = getGrupoCode(codigo); 25 + codigo.charAt(0) + '0000',
68 - const subgrupoCode = getSubgrupoCode(codigo); 26 + codigo.substring(0, 2) + '000'
69 - 27 + ];
70 - const { data: padresData } = await supabase
71 - .schema('ppto')
72 - .from('clas_objetos')
73 - .select('*')
74 - .in('objeto', [grupoCode, subgrupoCode])
75 - .order('objeto');
76 -
77 - if (padresData) padres = padresData;
78 } else if (nivel === 'subgrupo') { 28 } else if (nivel === 'subgrupo') {
79 - // Padre: grupo 29 + padresCodes = [codigo.charAt(0) + '0000'];
80 - const grupoCode = getGrupoCode(codigo);
81 -
82 - const { data: padresData } = await supabase
83 - .schema('ppto')
84 - .from('clas_objetos')
85 - .select('*')
86 - .eq('objeto', grupoCode);
87 -
88 - if (padresData) padres = padresData;
89 - }
90 - // grupo no tiene padres
91 -
92 - console.timeEnd('[SERVER] Query padres');
93 -
94 - console.time('[SERVER] Query hijos');
95 -
96 - // Buscar hijos según nivel
97 - if (nivel === 'grupo') {
98 - // Hijos: subgrupos que empiecen con el mismo dígito
99 - const prefix = codigo.charAt(0);
100 - const { data: hijosData } = await supabase
101 - .schema('ppto')
102 - .from('clas_objetos')
103 - .select('*')
104 - .eq('nivel', 'subgrupo')
105 - .like('objeto', `${prefix}%`)
106 - .order('objeto');
107 -
108 - hijos = hijosData || [];
109 - } else if (nivel === 'subgrupo') {
110 - // Hijos: partidas que empiecen con los mismos 2 dígitos
111 - const prefix = codigo.substring(0, 2);
112 - const { data: hijosData } = await supabase
113 - .schema('ppto')
114 - .from('clas_objetos')
115 - .select('*')
116 - .eq('nivel', 'partida')
117 - .like('objeto', `${prefix}%`)
118 - .order('objeto');
119 -
120 - hijos = hijosData || [];
121 - } else if (nivel === 'partida') {
122 - // Hijos: subpartidas que empiecen con los mismos 3 dígitos
123 - const prefix = codigo.substring(0, 3);
124 - const { data: hijosData } = await supabase
125 - .schema('ppto')
126 - .from('clas_objetos')
127 - .select('*')
128 - .eq('nivel', 'subpartida')
129 - .like('objeto', `${prefix}%`)
130 - .order('objeto');
131 -
132 - hijos = hijosData || [];
133 } 30 }
134 - // subpartida no tiene hijos
135 31
136 - console.timeEnd('[SERVER] Query hijos'); 32 + // Cargar padres e hijos en paralelo desde la API
33 + const [padresResults, hijosResult] = await Promise.all([
34 + Promise.all(padresCodes.map(async (c) => {
35 + try {
36 + const r = await fetch(`${API_BASE}/${c}`);
37 + if (r.ok) return await r.json();
38 + } catch {}
39 + return null;
40 + })),
41 + (async () => {
42 + // Hijos: buscar en el clasificador
43 + try {
44 + const r = await fetch(`${API_BASE}/clasificador`);
45 + if (!r.ok) return [];
46 + const all = await r.json();
47 +
48 + if (nivel === 'grupo') {
49 + return all.filter(o => o.nivel === 'subgrupo' && o.objeto.charAt(0) === codigo.charAt(0) && o.objeto !== codigo);
50 + } else if (nivel === 'subgrupo') {
51 + return all.filter(o => o.nivel === 'partida' && o.objeto.substring(0, 2) === codigo.substring(0, 2));
52 + } else if (nivel === 'partida') {
53 + return all.filter(o => o.nivel === 'subpartida' && o.objeto.substring(0, 3) === codigo.substring(0, 3));
54 + }
55 + return [];
56 + } catch { return []; }
57 + })()
58 + ]);
59 +
60 + const padres = padresResults.filter(Boolean).sort((a, b) => a.objeto.localeCompare(b.objeto));
61 + const hijos = hijosResult.sort((a, b) => a.objeto.localeCompare(b.objeto));
137 62
138 - console.timeEnd('[SERVER] Total load objeto');
139 return { 63 return {
140 objeto, 64 objeto,
141 padres, 65 padres,
142 - hijos 66 + hijos,
67 + nEntidades: 0
143 }; 68 };
144 } 69 }
......
This diff is collapsed. Click to expand it.
1 +import { redirect } from '@sveltejs/kit';
2 +
3 +export function load({ params }) {
4 + throw redirect(301, `/objeto/${params.codigo}?entidad=${params.entidad}`);
5 +}
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
25 let clasificadorSeleccionado = $state('total'); // total, objeto, finfun, acteco 25 let clasificadorSeleccionado = $state('total'); // total, objeto, finfun, acteco
26 let subPartidaSearch = $state(''); 26 let subPartidaSearch = $state('');
27 let subPartidaSeleccionada = $state(null); 27 let subPartidaSeleccionada = $state(null);
28 + let mapLinkCopied = $state(false);
28 29
29 const CLASIFICADORES_MAPA = [ 30 const CLASIFICADORES_MAPA = [
30 { id: 'total', label: 'Gasto total', disponible: true }, 31 { id: 'total', label: 'Gasto total', disponible: true },
...@@ -128,7 +129,7 @@ ...@@ -128,7 +129,7 @@
128 const res = await fetch(`${apiEndpoint}?${params}`); 129 const res = await fetch(`${apiEndpoint}?${params}`);
129 const data = await res.json(); 130 const data = await res.json();
130 const mapped = (data || []) 131 const mapped = (data || [])
131 - .filter(d => d.ubigeo !== '0.0.0') 132 + .filter(d => d.ubigeo !== '0.0.0' && !/multimunicipal/i.test(d.desc_ubigeo))
132 .map(d => ({ 133 .map(d => ({
133 codigo: d.ubigeo, 134 codigo: d.ubigeo,
134 desc: d.desc_ubigeo, 135 desc: d.desc_ubigeo,
...@@ -393,11 +394,19 @@ ...@@ -393,11 +394,19 @@
393 goto(url.toString(), { replaceState: true, noScroll: true }); 394 goto(url.toString(), { replaceState: true, noScroll: true });
394 } 395 }
395 396
396 - // Resolver nombre de partida por código desde Typesense 397 + // Resolver nombre de partida por código desde la API
397 async function resolverNombre(clas, codigo) { 398 async function resolverNombre(clas, codigo) {
398 - const cfg = CLAS_SEARCH_MAP[clas];
399 - if (!cfg) return codigo;
400 try { 399 try {
400 + if (clas === 'objeto') {
401 + const res = await fetch(`/api/objeto-data?codigo=${codigo}&tipo=clasificador`);
402 + if (res.ok) {
403 + const data = await res.json();
404 + if (data?.desc_objeto) return data.desc_objeto;
405 + }
406 + }
407 + // Fallback: buscar en Typesense
408 + const cfg = CLAS_SEARCH_MAP[clas];
409 + if (!cfg) return codigo;
401 const params = new URLSearchParams({ q: codigo, per_page: '5', is_class: 'true', class_: cfg.class_ }); 410 const params = new URLSearchParams({ q: codigo, per_page: '5', is_class: 'true', class_: cfg.class_ });
402 const res = await fetch(`/api/search?${params}`); 411 const res = await fetch(`/api/search?${params}`);
403 if (!res.ok) return codigo; 412 if (!res.ok) return codigo;
...@@ -477,10 +486,34 @@ ...@@ -477,10 +486,34 @@
477 <div class="mapa-fullscreen"> 486 <div class="mapa-fullscreen">
478 <div class="mapa-top-controls"> 487 <div class="mapa-top-controls">
479 <nav class="breadcrumb"> 488 <nav class="breadcrumb">
480 - <button class="back-btn" onclick={() => { history.back(); }}>←</button> 489 + {#if clasSeleccionado && clasificadorSeleccionado === 'objeto'}
481 - <a href="/">Inicio</a> 490 + <a href="/objeto/{clasSeleccionado.codigo}" class="back-link-mapa">
482 - <span class="sep">/</span> 491 + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
483 - <span>Ubicación geográfica</span> 492 + Volver a {clasSeleccionado.nombre}
493 + </a>
494 + {:else if clasSeleccionado && clasificadorSeleccionado === 'finfun'}
495 + <a href="/finfun/{clasSeleccionado.codigo}" class="back-link-mapa">
496 + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
497 + Volver a {clasSeleccionado.nombre}
498 + </a>
499 + {:else}
500 + <span class="mapa-location-label">Ubicación geográfica</span>
501 + {/if}
502 + <button class="copy-link-btn-mapa" onclick={() => {
503 + navigator.clipboard.writeText(window.location.href);
504 + mapLinkCopied = true;
505 + setTimeout(() => mapLinkCopied = false, 2000);
506 + }}>
507 + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
508 + {#if mapLinkCopied}
509 + <path d="M20 6L9 17l-5-5"/>
510 + {:else}
511 + <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
512 + <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
513 + {/if}
514 + </svg>
515 + {mapLinkCopied ? 'Copiado' : 'Compartir'}
516 + </button>
484 </nav> 517 </nav>
485 <!-- Fila 1: Dropdowns inline --> 518 <!-- Fila 1: Dropdowns inline -->
486 <div class="titulo-dropdowns"> 519 <div class="titulo-dropdowns">
...@@ -527,7 +560,6 @@ ...@@ -527,7 +560,6 @@
527 <div class="objeto-search-mapa"> 560 <div class="objeto-search-mapa">
528 {#if clasSeleccionado} 561 {#if clasSeleccionado}
529 <div class="objeto-selected-pill"> 562 <div class="objeto-selected-pill">
530 - <span class="objeto-pill-code">{clasSeleccionado.codigo}</span>
531 <span class="objeto-pill-name">{clasSeleccionado.nombre}</span> 563 <span class="objeto-pill-name">{clasSeleccionado.nombre}</span>
532 <button class="objeto-pill-clear" onclick={() => { limpiarClasificador(); cargarGestion(gestionSeleccionada); }}>✕</button> 564 <button class="objeto-pill-clear" onclick={() => { limpiarClasificador(); cargarGestion(gestionSeleccionada); }}>✕</button>
533 </div> 565 </div>
...@@ -769,12 +801,41 @@ ...@@ -769,12 +801,41 @@
769 flex-shrink: 0; 801 flex-shrink: 0;
770 } 802 }
771 803
772 - .breadcrumb { font-size: 0.7rem; color: var(--theme-texto); opacity: 0.5; display: flex; align-items: center; gap: 0.3rem; } 804 + .breadcrumb { font-size: 0.7rem; color: var(--theme-texto); display: flex; align-items: center; gap: 0.75rem; }
773 - .back-btn { background: none; border: none; cursor: pointer; color: var(--theme-texto); font-size: 0.85rem; padding: 0; opacity: 0.6; transition: opacity 0.15s; } 805 +
774 - .back-btn:hover { opacity: 1; } 806 + .back-link-mapa {
775 - .breadcrumb a { color: var(--theme-texto); text-decoration: none; } 807 + display: inline-flex;
776 - .breadcrumb a:hover { text-decoration: underline; opacity: 1; } 808 + align-items: center;
777 - .breadcrumb .sep { margin: 0 0.3rem; } 809 + gap: 0.25rem;
810 + font-family: 'DM Mono', monospace;
811 + font-size: 0.6875rem;
812 + color: var(--theme-accent);
813 + text-decoration: none;
814 + transition: opacity 0.15s;
815 + }
816 + .back-link-mapa:hover { opacity: 0.7; }
817 +
818 + .mapa-location-label {
819 + font-size: 0.6875rem;
820 + color: var(--theme-texto);
821 + opacity: 0.5;
822 + }
823 +
824 + .copy-link-btn-mapa {
825 + display: flex;
826 + align-items: center;
827 + gap: 0.25rem;
828 + font-size: 0.6875rem;
829 + color: var(--theme-texto);
830 + opacity: 0.5;
831 + background: none;
832 + border: none;
833 + padding: 0;
834 + cursor: pointer;
835 + transition: opacity 0.15s;
836 + margin-left: auto;
837 + }
838 + .copy-link-btn-mapa:hover { opacity: 1; }
778 839
779 .titulo-dropdowns { 840 .titulo-dropdowns {
780 display: flex; 841 display: flex;
......