Rafael Lopez

jhola

Showing 53 changed files with 1602 additions and 452 deletions
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 let { 4 let {
5 data = [], 5 data = [],
6 hoveredYear = $bindable(null), 6 hoveredYear = $bindable(null),
7 + lockedYear = $bindable(null),
7 height = 220, 8 height = 220,
8 fill = false, 9 fill = false,
9 marginTop = 20, 10 marginTop = 20,
...@@ -12,6 +13,17 @@ ...@@ -12,6 +13,17 @@
12 marginLeft = 50 13 marginLeft = 50
13 } = $props(); 14 } = $props();
14 15
16 + // El año activo es el fijado o el hover
17 + let activeYear = $derived(lockedYear ?? hoveredYear);
18 +
19 + function handleBarClick(año) {
20 + if (lockedYear === año) {
21 + lockedYear = null; // desfijar
22 + } else {
23 + lockedYear = año; // fijar
24 + }
25 + }
26 +
15 // Medir altura real del contenedor cuando fill=true 27 // Medir altura real del contenedor cuando fill=true
16 let wrapperEl = $state(null); 28 let wrapperEl = $state(null);
17 let measuredHeight = $state(height); 29 let measuredHeight = $state(height);
...@@ -97,24 +109,41 @@ ...@@ -97,24 +109,41 @@
97 {/each} 109 {/each}
98 110
99 <!-- Barras --> 111 <!-- Barras -->
100 - <div class="bars" onmouseleave={() => hoveredYear = null} role="group"> 112 + <div class="bars" class:has-active={!!lockedYear} onmouseleave={() => { if (!lockedYear) hoveredYear = null; }} role="group">
101 {#each data as d} 113 {#each data as d}
102 {@const safeVal = typeof d.perCapita === 'number' && isFinite(d.perCapita) ? d.perCapita : 0} 114 {@const safeVal = typeof d.perCapita === 'number' && isFinite(d.perCapita) ? d.perCapita : 0}
103 {@const barPct = chartHeight > 0 ? (yScale(safeVal) / chartHeight) * 100 : 0} 115 {@const barPct = chartHeight > 0 ? (yScale(safeVal) / chartHeight) * 100 : 0}
104 <div 116 <div
105 class="bar-container" 117 class="bar-container"
106 - onmouseenter={() => hoveredYear = d.año} 118 + onmouseenter={() => { if (!lockedYear) hoveredYear = d.año; }}
119 + onclick={() => handleBarClick(d.año)}
107 role="button" 120 role="button"
108 tabindex="0" 121 tabindex="0"
109 > 122 >
110 <div 123 <div
111 class="bar" 124 class="bar"
112 - class:hovered={hoveredYear === d.año} 125 + class:hovered={activeYear === d.año}
126 + class:locked={lockedYear === d.año}
113 style="height: {barPct}%; {safeVal > 0 ? 'min-height: 3px;' : ''}" 127 style="height: {barPct}%; {safeVal > 0 ? 'min-height: 3px;' : ''}"
114 ></div> 128 ></div>
115 </div> 129 </div>
116 {/each} 130 {/each}
117 </div> 131 </div>
132 +
133 + <!-- Hint -->
134 + <div class="chart-hint">
135 + {#if lockedYear}
136 + <span class="hint-locked">
137 + {lockedYear} fijado
138 + <button class="hint-unlock" onclick={() => lockedYear = null}>soltar</button>
139 + </span>
140 + {:else if activeYear}
141 + <span class="hint-hover hint-desktop-only">Click para fijar {activeYear}</span>
142 + {:else}
143 + <span class="hint-idle hint-desktop-only">Pasa el cursor sobre las barras</span>
144 + <span class="hint-idle hint-mobile-only">Toca una barra para explorar</span>
145 + {/if}
146 + </div>
118 </div> 147 </div>
119 148
120 <!-- Eje X --> 149 <!-- Eje X -->
...@@ -122,7 +151,7 @@ ...@@ -122,7 +151,7 @@
122 {#each data as d, i} 151 {#each data as d, i}
123 <span 152 <span
124 class="x-label visible" 153 class="x-label visible"
125 - class:hovered={hoveredYear === d.año} 154 + class:hovered={activeYear === d.año}
126 > 155 >
127 {String(d.año).slice(-2)} 156 {String(d.año).slice(-2)}
128 </span> 157 </span>
...@@ -183,7 +212,7 @@ ...@@ -183,7 +212,7 @@
183 display: flex; 212 display: flex;
184 align-items: flex-end; 213 align-items: flex-end;
185 justify-content: center; 214 justify-content: center;
186 - cursor: default; 215 + cursor: pointer;
187 } 216 }
188 217
189 .bar { 218 .bar {
...@@ -198,14 +227,61 @@ ...@@ -198,14 +227,61 @@
198 background: rgba(107, 159, 212, 0.85); 227 background: rgba(107, 159, 212, 0.85);
199 } 228 }
200 229
230 + /* Fade en hover */
201 .bars:hover .bar { 231 .bars:hover .bar {
202 opacity: 0.35; 232 opacity: 0.35;
203 } 233 }
204 -
205 .bars:hover .bar.hovered { 234 .bars:hover .bar.hovered {
206 opacity: 1; 235 opacity: 1;
207 } 236 }
208 237
238 + /* Fade permanente cuando hay año fijado */
239 + .bars.has-active .bar {
240 + opacity: 0.25;
241 + }
242 + .bars.has-active .bar.locked {
243 + opacity: 1;
244 + box-shadow: 0 0 0 2px var(--theme-accent, #C9A751);
245 + }
246 +
247 + /* Chart hint */
248 + .chart-hint {
249 + position: absolute;
250 + top: -2px;
251 + right: 0;
252 + font-family: 'Qanelas', var(--font-sans);
253 + font-size: 0.625rem;
254 + color: var(--theme-texto);
255 + opacity: 0.5;
256 + }
257 + .hint-locked {
258 + display: inline-flex;
259 + align-items: center;
260 + gap: 6px;
261 + color: var(--theme-accent, #C9A751);
262 + opacity: 1;
263 + }
264 + .hint-unlock {
265 + background: none;
266 + border: none;
267 + border-bottom: 1px dotted currentColor;
268 + color: inherit;
269 + font: inherit;
270 + cursor: pointer;
271 + padding: 0;
272 + opacity: 0.7;
273 + transition: opacity 0.15s;
274 + }
275 + .hint-unlock:hover {
276 + opacity: 1;
277 + }
278 + .hint-mobile-only { display: none; }
279 + .hint-desktop-only { display: inline; }
280 + @media (max-width: 768px) {
281 + .hint-mobile-only { display: inline; }
282 + .hint-desktop-only { display: none; }
283 + }
284 +
209 .x-axis { 285 .x-axis {
210 position: absolute; 286 position: absolute;
211 bottom: 0; 287 bottom: 0;
......
...@@ -78,7 +78,6 @@ ...@@ -78,7 +78,6 @@
78 class:active={selectedEntity?.entidad === entity.entidad} 78 class:active={selectedEntity?.entidad === entity.entidad}
79 onclick={() => selectEntity(entity)} 79 onclick={() => selectEntity(entity)}
80 > 80 >
81 - <span class="item-code">{entity.entidad}</span>
82 <span class="item-name">{entity.entidad_desc}</span> 81 <span class="item-name">{entity.entidad_desc}</span>
83 </button> 82 </button>
84 {/each} 83 {/each}
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
5 5
6 let isDark = $state(false); 6 let isDark = $state(false);
7 let isMac = $state(false); 7 let isMac = $state(false);
8 + let canGoBack = $state(false);
9 + let canGoForward = $state(false);
8 10
9 function toggleTheme() { 11 function toggleTheme() {
10 isDark = !isDark; 12 isDark = !isDark;
...@@ -12,14 +14,39 @@ ...@@ -12,14 +14,39 @@
12 localStorage.setItem('theme', isDark ? 'dark' : 'light'); 14 localStorage.setItem('theme', isDark ? 'dark' : 'light');
13 } 15 }
14 16
17 + function goBack() { history.back(); }
18 + function goForward() { history.forward(); }
19 +
20 + function updateNavState() {
21 + // history.length > 1 means there's something to go back to
22 + // We track forward availability via sessionStorage
23 + canGoBack = history.length > 1 && sessionStorage.getItem('nav_depth') > 0;
24 + canGoForward = sessionStorage.getItem('nav_forward') === 'true';
25 + }
26 +
15 onMount(() => { 27 onMount(() => {
16 - // Detect Mac for keyboard shortcut display
17 isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); 28 isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
18 -
19 - // Read actual state from document (set by app.html)
20 isDark = document.documentElement.classList.contains('dark'); 29 isDark = document.documentElement.classList.contains('dark');
21 30
22 - // Listen for theme changes (e.g., from drawer on mobile) 31 + // Track navigation depth for back/forward state
32 + let depth = parseInt(sessionStorage.getItem('nav_depth') || '0');
33 + // On first visit, depth is 0
34 + // We increment on each navigation
35 + depth++;
36 + sessionStorage.setItem('nav_depth', depth.toString());
37 + canGoBack = depth > 1;
38 +
39 + // Listen for popstate (back/forward browser actions) to enable forward
40 + const handlePopstate = () => {
41 + sessionStorage.setItem('nav_forward', 'true');
42 + canGoForward = true;
43 + const d = parseInt(sessionStorage.getItem('nav_depth') || '1');
44 + sessionStorage.setItem('nav_depth', Math.max(0, d - 1).toString());
45 + canGoBack = d - 1 > 0;
46 + };
47 + window.addEventListener('popstate', handlePopstate);
48 +
49 + // Listen for theme changes
23 const observer = new MutationObserver(() => { 50 const observer = new MutationObserver(() => {
24 isDark = document.documentElement.classList.contains('dark'); 51 isDark = document.documentElement.classList.contains('dark');
25 }); 52 });
...@@ -28,12 +55,27 @@ ...@@ -28,12 +55,27 @@
28 attributeFilter: ['class'] 55 attributeFilter: ['class']
29 }); 56 });
30 57
31 - return () => observer.disconnect(); 58 + return () => {
59 + observer.disconnect();
60 + window.removeEventListener('popstate', handlePopstate);
61 + };
32 }); 62 });
33 </script> 63 </script>
34 64
35 -<!-- MÓVIL: Home + Búsqueda + Theme + hamburguesa arriba a la derecha --> 65 +<!-- MÓVIL: Nav + Home + Búsqueda + Theme + hamburguesa -->
36 <div class="navbar-mobile"> 66 <div class="navbar-mobile">
67 + <div class="nav-arrows">
68 + <button onclick={goBack} class="nav-arrow-btn" class:nav-arrow-disabled={!canGoBack} aria-label="Atrás" disabled={!canGoBack}>
69 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
70 + <path d="M19 12H5M12 19l-7-7 7-7"/>
71 + </svg>
72 + </button>
73 + <button onclick={goForward} class="nav-arrow-btn" class:nav-arrow-disabled={!canGoForward} aria-label="Adelante" disabled={!canGoForward}>
74 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
75 + <path d="M5 12h14M12 5l7 7-7 7"/>
76 + </svg>
77 + </button>
78 + </div>
37 <a href="/" class="nav-home-link" aria-label="Ir al inicio"> 79 <a href="/" class="nav-home-link" aria-label="Ir al inicio">
38 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 80 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
39 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> 81 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
...@@ -76,10 +118,24 @@ ...@@ -76,10 +118,24 @@
76 </button> 118 </button>
77 </div> 119 </div>
78 120
79 -<!-- DESKTOP: Barra completa con Home + Search + Theme + Menu --> 121 +<!-- DESKTOP: Barra completa con Nav + Home + Search + Theme + Menu -->
80 <div class="navbar-desktop"> 122 <div class="navbar-desktop">
81 <div class="navbar-desktop-inner"> 123 <div class="navbar-desktop-inner">
82 124
125 + <!-- FLECHAS NAVEGACIÓN -->
126 + <div class="nav-arrows">
127 + <button onclick={goBack} class="nav-arrow-btn" class:nav-arrow-disabled={!canGoBack} aria-label="Atrás" disabled={!canGoBack}>
128 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
129 + <path d="M19 12H5M12 19l-7-7 7-7"/>
130 + </svg>
131 + </button>
132 + <button onclick={goForward} class="nav-arrow-btn" class:nav-arrow-disabled={!canGoForward} aria-label="Adelante" disabled={!canGoForward}>
133 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
134 + <path d="M5 12h14M12 5l7 7-7 7"/>
135 + </svg>
136 + </button>
137 + </div>
138 +
83 <!-- BOTÓN HOME --> 139 <!-- BOTÓN HOME -->
84 <div class="relative group/home"> 140 <div class="relative group/home">
85 <a href="/" class="nav-btn-icon" aria-label="Ir al inicio"> 141 <a href="/" class="nav-btn-icon" aria-label="Ir al inicio">
...@@ -390,4 +446,45 @@ ...@@ -390,4 +446,45 @@
390 background: rgba(0, 0, 0, 0.12); 446 background: rgba(0, 0, 0, 0.12);
391 color: #1A1A18; 447 color: #1A1A18;
392 } 448 }
449 +
450 + /* Navigation arrows */
451 + .nav-arrows {
452 + display: flex;
453 + align-items: center;
454 + gap: 2px;
455 + margin-right: 2px;
456 + }
457 +
458 + .nav-arrow-btn {
459 + display: flex;
460 + align-items: center;
461 + justify-content: center;
462 + padding: 6px;
463 + background: transparent;
464 + border: none;
465 + border-radius: 8px;
466 + color: #B8B5AD;
467 + cursor: pointer;
468 + transition: all 0.2s ease;
469 + }
470 +
471 + .nav-arrow-btn:hover {
472 + background: rgba(255, 255, 255, 0.1);
473 + color: #F5F0E8;
474 + }
475 +
476 + .nav-arrow-disabled {
477 + opacity: 0.2;
478 + cursor: default;
479 + pointer-events: none;
480 + }
481 +
482 + :global(html:not(.dark)) .nav-arrow-btn {
483 + color: #888;
484 + }
485 +
486 + :global(html:not(.dark)) .nav-arrow-btn:hover {
487 + background: rgba(0, 0, 0, 0.06);
488 + color: #1A1A18;
489 + }
393 </style> 490 </style>
......
...@@ -25,7 +25,6 @@ ...@@ -25,7 +25,6 @@
25 { id: 'sectores', label: '¿En qué sectores?', tecnico: 'Sectores económicos' }, 25 { id: 'sectores', label: '¿En qué sectores?', tecnico: 'Sectores económicos' },
26 { id: 'rubro', label: '¿Con qué recursos?', tecnico: 'Rubros' }, 26 { id: 'rubro', label: '¿Con qué recursos?', tecnico: 'Rubros' },
27 { id: 'organismo', label: '¿Quién financia?', tecnico: 'Organismos' }, 27 { id: 'organismo', label: '¿Quién financia?', tecnico: 'Organismos' },
28 - { id: 'fuente', label: 'Origen del ingreso', tecnico: 'Fuentes' },
29 ]; 28 ];
30 29
31 function toggleClassifier(id) { 30 function toggleClassifier(id) {
...@@ -40,7 +39,7 @@ ...@@ -40,7 +39,7 @@
40 } 39 }
41 40
42 // Clasificadores seleccionados → class_ param para Typesense 41 // Clasificadores seleccionados → class_ param para Typesense
43 - const CLASS_API_MAP = { geografico: 'ubigeo', sectores: 'acteco' }; 42 + const CLASS_API_MAP = { geografico: 'ubigeo', sectores: 'acteco', finfun: 'finalidad' };
44 43
45 function getActiveClasses() { 44 function getActiveClasses() {
46 if (selectedClassifiers.length === 0) return []; 45 if (selectedClassifiers.length === 0) return [];
...@@ -102,6 +101,16 @@ ...@@ -102,6 +101,16 @@
102 } else if (isClass && cls === 'ubigeo') { 101 } else if (isClass && cls === 'ubigeo') {
103 codigo = String(meta.municipio_ubigeo || ''); 102 codigo = String(meta.municipio_ubigeo || '');
104 tipo = 'ubigeo'; 103 tipo = 'ubigeo';
104 + } else if (isClass && cls === 'acteco') {
105 + const s = meta.acteco_sector != null ? String(Math.round(meta.acteco_sector)) : '';
106 + if (s) {
107 + codigo = s;
108 + if (meta.acteco_subsector != null) {
109 + codigo += '.' + Math.round(meta.acteco_subsector);
110 + if (meta.acteco_actividad != null) codigo += '.' + Math.round(meta.acteco_actividad);
111 + }
112 + }
113 + tipo = 'acteco';
105 } else if (!isClass) { 114 } else if (!isClass) {
106 // Programa/proyecto 115 // Programa/proyecto
107 tipo = 'programa'; 116 tipo = 'programa';
...@@ -200,9 +209,9 @@ ...@@ -200,9 +209,9 @@
200 entidad: '/entidad/', 209 entidad: '/entidad/',
201 objeto_gasto: '/objeto/', 210 objeto_gasto: '/objeto/',
202 finfun: '/finfun/', 211 finfun: '/finfun/',
212 + acteco: '/acteco/',
203 rubro: '/rubro/', 213 rubro: '/rubro/',
204 organismo: '/organismo/', 214 organismo: '/organismo/',
205 - fuente: '/fuente/',
206 ubigeo: '/ubicacion/', 215 ubigeo: '/ubicacion/',
207 programa: '/proyecto/' 216 programa: '/proyecto/'
208 }; 217 };
...@@ -279,7 +288,7 @@ ...@@ -279,7 +288,7 @@
279 bind:this={searchInput} 288 bind:this={searchInput}
280 type="text" 289 type="text"
281 class="search-modal-input" 290 class="search-modal-input"
282 - placeholder={searchMode === 'programas' ? 'Buscar programas y proyectos...' : selectedClassifiers.length > 0 ? 'Buscar en clasificadores seleccionados...' : 'Selecciona al menos un clasificador...'} 291 + placeholder={searchMode === 'todo' ? 'Buscar en todo...' : searchMode === 'programas' ? 'Buscar programas y proyectos...' : selectedClassifiers.length > 0 ? 'Buscar en clasificadores seleccionados...' : 'Selecciona al menos un clasificador...'}
283 value={searchVal} 292 value={searchVal}
284 oninput={handleInput} 293 oninput={handleInput}
285 /> 294 />
...@@ -291,8 +300,11 @@ ...@@ -291,8 +300,11 @@
291 <!-- Mode toggle + classifier pills --> 300 <!-- Mode toggle + classifier pills -->
292 <div class="search-modal-filters"> 301 <div class="search-modal-filters">
293 <div class="mode-toggle"> 302 <div class="mode-toggle">
303 + <button class="mode-btn" class:active={searchMode === 'todo'} onclick={() => { searchMode = 'todo'; selectedClassifiers = []; landingSearchMode.set('todo'); landingSelectedClassifiers.set([]); if (searchVal.length >= 2) doSearch(searchVal); }}>
304 + Todo
305 + </button>
294 <button class="mode-btn" class:active={searchMode === 'programas'} onclick={() => { searchMode = 'programas'; selectedClassifiers = []; landingSearchMode.set('programas'); landingSelectedClassifiers.set([]); if (searchVal.length >= 2) doSearch(searchVal); }}> 306 <button class="mode-btn" class:active={searchMode === 'programas'} onclick={() => { searchMode = 'programas'; selectedClassifiers = []; landingSearchMode.set('programas'); landingSelectedClassifiers.set([]); if (searchVal.length >= 2) doSearch(searchVal); }}>
295 - Programas 307 + Programas y Proyectos
296 </button> 308 </button>
297 <button class="mode-btn" class:active={searchMode === 'clasificadores'} onclick={() => { searchMode = 'clasificadores'; landingSearchMode.set('clasificadores'); if (searchVal.length >= 2) doSearch(searchVal); }}> 309 <button class="mode-btn" class:active={searchMode === 'clasificadores'} onclick={() => { searchMode = 'clasificadores'; landingSearchMode.set('clasificadores'); if (searchVal.length >= 2) doSearch(searchVal); }}>
298 Clasificadores 310 Clasificadores
......
This diff is collapsed. Click to expand it.
1 +import { error } from '@sveltejs/kit';
2 +
3 +const API_BASE = 'http://136.112.29.74/api/acteco';
4 +
5 +// Jerarquía acteco: códigos con punto "1" → "1.1" → "1.1.1"
6 +// Todos tienen nivel "actividad" en la API, derivamos la profundidad del código
7 +
8 +function getDepth(codigo) {
9 + return String(codigo).split('.').length;
10 +}
11 +
12 +function getParentCode(codigo) {
13 + const parts = String(codigo).split('.');
14 + if (parts.length <= 1) return null;
15 + return parts.slice(0, -1).join('.');
16 +}
17 +
18 +export async function load({ params }) {
19 + const { codigo } = params;
20 +
21 + const res = await fetch(`${API_BASE}/${codigo}`);
22 + if (!res.ok) throw error(404, 'Sector económico no encontrado');
23 + const acteco = await res.json();
24 +
25 + let padres = [];
26 + let hijos = [];
27 +
28 + try {
29 + const clasRes = await fetch(`${API_BASE}/clasificador`);
30 + if (clasRes.ok) {
31 + const clasificador = await clasRes.json();
32 + const depth = getDepth(codigo);
33 +
34 + // Padres: todos los ancestros
35 + if (depth > 1) {
36 + const parts = String(codigo).split('.');
37 + const parentCodes = [];
38 + for (let i = 1; i < parts.length; i++) {
39 + parentCodes.push(parts.slice(0, i).join('.'));
40 + }
41 + padres = clasificador.filter(c => parentCodes.includes(c.acteco));
42 + }
43 +
44 + // Hijos directos: un nivel más de profundidad, mismo prefijo
45 + const prefix = codigo + '.';
46 + const childDepth = depth + 1;
47 + hijos = clasificador.filter(c =>
48 + c.acteco.startsWith(prefix) && getDepth(c.acteco) === childDepth
49 + );
50 +
51 + padres.sort((a, b) => a.acteco.localeCompare(b.acteco));
52 + hijos.sort((a, b) => a.acteco.localeCompare(b.acteco));
53 + }
54 + } catch { /* clasificador optional */ }
55 +
56 + return {
57 + acteco,
58 + padres,
59 + hijos
60 + };
61 +}
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
1 +const API_BASE = 'http://136.112.29.74/api/acteco/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/acteco';
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`;
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/acteco';
2 +
3 +let cachedClasificador = null;
4 +let cachedEstados = null;
5 +let cachedTimestamp = 0;
6 +const CACHE_TTL = 5 * 60 * 1000;
7 +
8 +function getParent(acteco) {
9 + const idx = acteco.lastIndexOf('.');
10 + if (idx === -1) return null;
11 + return acteco.substring(0, idx);
12 +}
13 +
14 +async function loadAndCacheAll() {
15 + const now = Date.now();
16 + if (cachedEstados && (now - cachedTimestamp) < CACHE_TTL) return;
17 +
18 + const clasRes = await fetch(`${API_BASE}/clasificador`);
19 + if (!clasRes.ok) throw new Error('Failed to fetch clasificador');
20 + cachedClasificador = await clasRes.json();
21 +
22 + const estados = new Map();
23 + const batchSize = 80;
24 +
25 + for (let i = 0; i < cachedClasificador.length; i += batchSize) {
26 + const batch = cachedClasificador.slice(i, i + batchSize);
27 + const results = await Promise.all(batch.map(async (item) => {
28 + try {
29 + const res = await fetch(`${API_BASE}/${item.acteco}`);
30 + if (!res.ok) return null;
31 + const data = await res.json();
32 + return { acteco: item.acteco, estados: data.estados || [] };
33 + } catch { return null; }
34 + }));
35 + results.filter(Boolean).forEach(r => estados.set(r.acteco, r.estados));
36 + }
37 +
38 + cachedEstados = estados;
39 + cachedTimestamp = now;
40 +}
41 +
42 +export async function GET({ url }) {
43 + const gestion = parseInt(url.searchParams.get('gestion') || '2025');
44 + const entidad = url.searchParams.get('entidad') || '0';
45 +
46 + try {
47 + if (entidad === '0') {
48 + await loadAndCacheAll();
49 +
50 + const results = [];
51 + for (const item of cachedClasificador) {
52 + const estados = cachedEstados.get(item.acteco);
53 + if (!estados) continue;
54 + const yearData = estados.find(d => d.gestion === gestion);
55 + if (!yearData || !yearData.total) continue;
56 + results.push({
57 + gestion,
58 + nivel: item.nivel || 'actividad',
59 + acteco: item.acteco,
60 + desc_acteco: item.desc_acteco,
61 + parent: getParent(item.acteco),
62 + devengado: yearData.total
63 + });
64 + }
65 +
66 + // Agregar nodos padres faltantes (sectores sin estados propios)
67 + const ids = new Set(results.map(r => r.acteco));
68 + const missingParents = new Set();
69 + results.forEach(r => {
70 + if (r.parent && !ids.has(r.parent)) missingParents.add(r.parent);
71 + });
72 + for (const parentCode of missingParents) {
73 + const item = cachedClasificador.find(c => c.acteco === parentCode);
74 + results.push({
75 + gestion,
76 + nivel: item?.nivel || 'actividad',
77 + acteco: parentCode,
78 + desc_acteco: item?.desc_acteco || parentCode,
79 + parent: getParent(parentCode),
80 + devengado: 0
81 + });
82 + }
83 +
84 + return new Response(JSON.stringify(results), {
85 + headers: { 'Content-Type': 'application/json' }
86 + });
87 + } else {
88 + if (!cachedClasificador) {
89 + const clasRes = await fetch(`${API_BASE}/clasificador`);
90 + cachedClasificador = await clasRes.json();
91 + }
92 +
93 + const results = [];
94 + const batchSize = 80;
95 +
96 + for (let i = 0; i < cachedClasificador.length; i += batchSize) {
97 + const batch = cachedClasificador.slice(i, i + batchSize);
98 + const batchResults = await Promise.all(batch.map(async (item) => {
99 + try {
100 + const res = await fetch(`${API_BASE}/${item.acteco}/entidades/${entidad}`);
101 + if (!res.ok) return null;
102 + const data = await res.json();
103 + const yearData = data.find(d => d.gestion === gestion);
104 + if (!yearData || !yearData.monto) return null;
105 + return {
106 + gestion,
107 + nivel: item.nivel || 'actividad',
108 + acteco: item.acteco,
109 + desc_acteco: item.desc_acteco,
110 + parent: getParent(item.acteco),
111 + devengado: yearData.monto
112 + };
113 + } catch { return null; }
114 + }));
115 + results.push(...batchResults.filter(Boolean));
116 + }
117 +
118 + // Agregar nodos padres faltantes
119 + const ids2 = new Set(results.map(r => r.acteco));
120 + const missing2 = new Set();
121 + results.forEach(r => {
122 + if (r.parent && !ids2.has(r.parent)) missing2.add(r.parent);
123 + });
124 + for (const parentCode of missing2) {
125 + const item = cachedClasificador.find(c => c.acteco === parentCode);
126 + results.push({
127 + gestion,
128 + nivel: item?.nivel || 'actividad',
129 + acteco: parentCode,
130 + desc_acteco: item?.desc_acteco || parentCode,
131 + parent: getParent(parentCode),
132 + devengado: 0
133 + });
134 + }
135 +
136 + return new Response(JSON.stringify(results), {
137 + headers: { 'Content-Type': 'application/json' }
138 + });
139 + }
140 + } catch (err) {
141 + return new Response(JSON.stringify({ error: 'Failed to build treemap' }), {
142 + status: 500, headers: { 'Content-Type': 'application/json' }
143 + });
144 + }
145 +}
...@@ -6,8 +6,7 @@ export async function GET({ url }) { ...@@ -6,8 +6,7 @@ export async function GET({ url }) {
6 6
7 if (!codigo) { 7 if (!codigo) {
8 return new Response(JSON.stringify({ error: 'Missing codigo' }), { 8 return new Response(JSON.stringify({ error: 'Missing codigo' }), {
9 - status: 400, 9 + status: 400, headers: { 'Content-Type': 'application/json' }
10 - headers: { 'Content-Type': 'application/json' }
11 }); 10 });
12 } 11 }
13 12
...@@ -18,8 +17,7 @@ export async function GET({ url }) { ...@@ -18,8 +17,7 @@ export async function GET({ url }) {
18 const response = await fetch(apiUrl.toString()); 17 const response = await fetch(apiUrl.toString());
19 if (!response.ok) { 18 if (!response.ok) {
20 return new Response(JSON.stringify({ error: `API error: ${response.status}` }), { 19 return new Response(JSON.stringify({ error: `API error: ${response.status}` }), {
21 - status: response.status, 20 + status: response.status, headers: { 'Content-Type': 'application/json' }
22 - headers: { 'Content-Type': 'application/json' }
23 }); 21 });
24 } 22 }
25 const data = await response.json(); 23 const data = await response.json();
...@@ -28,8 +26,7 @@ export async function GET({ url }) { ...@@ -28,8 +26,7 @@ export async function GET({ url }) {
28 }); 26 });
29 } catch (err) { 27 } catch (err) {
30 return new Response(JSON.stringify({ error: 'Failed to fetch' }), { 28 return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
31 - status: 500, 29 + status: 500, headers: { 'Content-Type': 'application/json' }
32 - headers: { 'Content-Type': 'application/json' }
33 }); 30 });
34 } 31 }
35 } 32 }
......
1 +const API_BASE = 'http://136.112.29.74/api/entidad';
2 +
3 +export async function GET({ url }) {
4 + const codigo = url.searchParams.get('codigo') || '';
5 + const tipo = url.searchParams.get('tipo') || 'detalle';
6 +
7 + if (!codigo) {
8 + return new Response(JSON.stringify({ error: 'Missing codigo' }), {
9 + status: 400, headers: { 'Content-Type': 'application/json' }
10 + });
11 + }
12 +
13 + // For DAs (codigo contains dot like "1201.1"), extract the entidad number
14 + const entidadNum = codigo.includes('.') ? codigo.split('.')[0] : codigo;
15 +
16 + let apiUrl;
17 + if (tipo === 'detalle') {
18 + apiUrl = `${API_BASE}/${entidadNum}`;
19 + } else if (tipo === 'objetos') {
20 + apiUrl = `${API_BASE}/${entidadNum}/objetos`;
21 + } else if (tipo === 'finfuns') {
22 + apiUrl = `${API_BASE}/${entidadNum}/finfuns`;
23 + } else if (tipo === 'actecos') {
24 + apiUrl = `${API_BASE}/${entidadNum}/actecos`;
25 + } else if (tipo === 'rubros') {
26 + apiUrl = `${API_BASE}/${entidadNum}/rubros`;
27 + } else if (tipo === 'organismos') {
28 + apiUrl = `${API_BASE}/${entidadNum}/organismos`;
29 + } else {
30 + return new Response(JSON.stringify({ error: 'Invalid tipo' }), {
31 + status: 400, headers: { 'Content-Type': 'application/json' }
32 + });
33 + }
34 +
35 + try {
36 + const response = await fetch(apiUrl);
37 + if (!response.ok) {
38 + return new Response(JSON.stringify({ error: `API error: ${response.status}` }), {
39 + status: response.status, headers: { 'Content-Type': 'application/json' }
40 + });
41 + }
42 + const data = await response.json();
43 + return new Response(JSON.stringify(data), {
44 + headers: { 'Content-Type': 'application/json' }
45 + });
46 + } catch (err) {
47 + return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
48 + status: 500, headers: { 'Content-Type': 'application/json' }
49 + });
50 + }
51 +}
1 +const API_BASE = 'http://136.112.29.74/api/finfun/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/finfun';
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`;
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/finfun';
2 +
3 +let cachedClasificador = null;
4 +let cachedEstados = null;
5 +let cachedTimestamp = 0;
6 +const CACHE_TTL = 5 * 60 * 1000;
7 +
8 +function getParent(finfun) {
9 + const idx = finfun.lastIndexOf('.');
10 + if (idx === -1) return null;
11 + return finfun.substring(0, idx);
12 +}
13 +
14 +async function loadAndCacheAll() {
15 + const now = Date.now();
16 + if (cachedEstados && (now - cachedTimestamp) < CACHE_TTL) return;
17 +
18 + const clasRes = await fetch(`${API_BASE}/clasificador`);
19 + if (!clasRes.ok) throw new Error('Failed to fetch clasificador');
20 + cachedClasificador = await clasRes.json();
21 +
22 + const estados = new Map();
23 + const batchSize = 80;
24 +
25 + for (let i = 0; i < cachedClasificador.length; i += batchSize) {
26 + const batch = cachedClasificador.slice(i, i + batchSize);
27 + const results = await Promise.all(batch.map(async (item) => {
28 + try {
29 + const res = await fetch(`${API_BASE}/${item.finfun}`);
30 + if (!res.ok) return null;
31 + const data = await res.json();
32 + return { finfun: item.finfun, estados: data.estados || [] };
33 + } catch { return null; }
34 + }));
35 + results.filter(Boolean).forEach(r => estados.set(r.finfun, r.estados));
36 + }
37 +
38 + cachedEstados = estados;
39 + cachedTimestamp = now;
40 +}
41 +
42 +export async function GET({ url }) {
43 + const gestion = parseInt(url.searchParams.get('gestion') || '2025');
44 + const entidad = url.searchParams.get('entidad') || '0';
45 +
46 + try {
47 + if (entidad === '0') {
48 + await loadAndCacheAll();
49 +
50 + const results = [];
51 + for (const item of cachedClasificador) {
52 + const estados = cachedEstados.get(item.finfun);
53 + if (!estados) continue;
54 + const yearData = estados.find(d => d.gestion === gestion);
55 + if (!yearData || !yearData.total) continue;
56 + results.push({
57 + gestion,
58 + nivel: item.nivel,
59 + finfun: item.finfun,
60 + desc_finfun: item.desc_finfun,
61 + parent: getParent(item.finfun),
62 + devengado: yearData.total
63 + });
64 + }
65 +
66 + return new Response(JSON.stringify(results), {
67 + headers: { 'Content-Type': 'application/json' }
68 + });
69 + } else {
70 + if (!cachedClasificador) {
71 + const clasRes = await fetch(`${API_BASE}/clasificador`);
72 + cachedClasificador = await clasRes.json();
73 + }
74 +
75 + const results = [];
76 + const batchSize = 80;
77 +
78 + for (let i = 0; i < cachedClasificador.length; i += batchSize) {
79 + const batch = cachedClasificador.slice(i, i + batchSize);
80 + const batchResults = await Promise.all(batch.map(async (item) => {
81 + try {
82 + const res = await fetch(`${API_BASE}/${item.finfun}/entidades/${entidad}`);
83 + if (!res.ok) return null;
84 + const data = await res.json();
85 + const yearData = data.find(d => d.gestion === gestion);
86 + if (!yearData || !yearData.monto) return null;
87 + return {
88 + gestion,
89 + nivel: item.nivel,
90 + finfun: item.finfun,
91 + desc_finfun: item.desc_finfun,
92 + parent: getParent(item.finfun),
93 + devengado: yearData.monto
94 + };
95 + } catch { return null; }
96 + }));
97 + results.push(...batchResults.filter(Boolean));
98 + }
99 +
100 + return new Response(JSON.stringify(results), {
101 + headers: { 'Content-Type': 'application/json' }
102 + });
103 + }
104 + } catch (err) {
105 + return new Response(JSON.stringify({ error: 'Failed to build treemap' }), {
106 + status: 500, headers: { 'Content-Type': 'application/json' }
107 + });
108 + }
109 +}
...@@ -6,8 +6,7 @@ export async function GET({ url }) { ...@@ -6,8 +6,7 @@ export async function GET({ url }) {
6 6
7 if (!codigo) { 7 if (!codigo) {
8 return new Response(JSON.stringify({ error: 'Missing codigo' }), { 8 return new Response(JSON.stringify({ error: 'Missing codigo' }), {
9 - status: 400, 9 + status: 400, headers: { 'Content-Type': 'application/json' }
10 - headers: { 'Content-Type': 'application/json' }
11 }); 10 });
12 } 11 }
13 12
...@@ -18,8 +17,7 @@ export async function GET({ url }) { ...@@ -18,8 +17,7 @@ export async function GET({ url }) {
18 const response = await fetch(apiUrl.toString()); 17 const response = await fetch(apiUrl.toString());
19 if (!response.ok) { 18 if (!response.ok) {
20 return new Response(JSON.stringify({ error: `API error: ${response.status}` }), { 19 return new Response(JSON.stringify({ error: `API error: ${response.status}` }), {
21 - status: response.status, 20 + status: response.status, headers: { 'Content-Type': 'application/json' }
22 - headers: { 'Content-Type': 'application/json' }
23 }); 21 });
24 } 22 }
25 const data = await response.json(); 23 const data = await response.json();
...@@ -28,8 +26,7 @@ export async function GET({ url }) { ...@@ -28,8 +26,7 @@ export async function GET({ url }) {
28 }); 26 });
29 } catch (err) { 27 } catch (err) {
30 return new Response(JSON.stringify({ error: 'Failed to fetch' }), { 28 return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
31 - status: 500, 29 + status: 500, headers: { 'Content-Type': 'application/json' }
32 - headers: { 'Content-Type': 'application/json' }
33 }); 30 });
34 } 31 }
35 } 32 }
......
...@@ -6,11 +6,14 @@ export async function GET() { ...@@ -6,11 +6,14 @@ export async function GET() {
6 if (!res.ok) throw new Error(); 6 if (!res.ok) throw new Error();
7 const data = await res.json(); 7 const data = await res.json();
8 // Mapear al formato que espera el treemap 8 // Mapear al formato que espera el treemap
9 - const mapped = data.map(d => ({ 9 + const mapped = data
10 - entidad: d.entidad, 10 + .filter(d => d.entidad !== 0)
11 - desc_entidad: d.desc_entidad, 11 + .map(d => ({
12 - sigla_entidad: d.sigla_entidad || '' 12 + entidad: d.entidad,
13 - })); 13 + desc_entidad: d.desc_entidad,
14 + sigla_entidad: d.sigla_entidad || '',
15 + gestiones: d.gestiones || ''
16 + }));
14 return new Response(JSON.stringify(mapped), { 17 return new Response(JSON.stringify(mapped), {
15 headers: { 'Content-Type': 'application/json' } 18 headers: { 'Content-Type': 'application/json' }
16 }); 19 });
......
1 +const API_BASE = 'http://136.112.29.74/api/organismo/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/organismo';
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`;
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/organismo';
2 +
3 +let cachedClasificador = null;
4 +let cachedEstados = null;
5 +let cachedTimestamp = 0;
6 +const CACHE_TTL = 5 * 60 * 1000;
7 +
8 +// Organismo hierarchy: grupo → subgrupo → organismo
9 +// Parent is derived from clasificador fields, not from code pattern
10 +
11 +async function loadAndCacheAll() {
12 + const now = Date.now();
13 + if (cachedEstados && (now - cachedTimestamp) < CACHE_TTL) return;
14 +
15 + const clasRes = await fetch(`${API_BASE}/clasificador`);
16 + if (!clasRes.ok) throw new Error('Failed to fetch clasificador');
17 + cachedClasificador = await clasRes.json();
18 +
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 code = item.organismo;
27 + const res = await fetch(`${API_BASE}/${code}`);
28 + if (!res.ok) return null;
29 + const data = await res.json();
30 + return { code: String(code), estados: data.estados || [] };
31 + } catch { return null; }
32 + }));
33 + results.filter(Boolean).forEach(r => estados.set(r.code, r.estados));
34 + }
35 +
36 + cachedEstados = estados;
37 + cachedTimestamp = now;
38 +}
39 +
40 +export async function GET({ url }) {
41 + const gestion = parseInt(url.searchParams.get('gestion') || '2025');
42 + const entidad = url.searchParams.get('entidad') || '0';
43 +
44 + try {
45 + if (entidad === '0') {
46 + await loadAndCacheAll();
47 +
48 + const results = [];
49 + const grupoSet = new Set();
50 + const subgrupoSet = new Set();
51 +
52 + for (const item of cachedClasificador) {
53 + const code = String(item.organismo);
54 + const estados = cachedEstados.get(code);
55 + if (!estados) continue;
56 + const yearData = estados.find(d => d.gestion === gestion);
57 + if (!yearData || !yearData.total) continue;
58 +
59 + const grupoKey = `g_${item.organismo_grupo}`;
60 + const subgrupoKey = `sg_${item.organismo_grupo}_${item.organismo_subgrupo}`;
61 +
62 + // Add grupo node if not yet added
63 + if (!grupoSet.has(grupoKey)) {
64 + grupoSet.add(grupoKey);
65 + results.push({
66 + gestion,
67 + nivel: 'grupo',
68 + organismo: grupoKey,
69 + desc_organismo: item.desc_organismo_grupo || `Grupo ${item.organismo_grupo}`,
70 + parent: null,
71 + devengado: 0
72 + });
73 + }
74 +
75 + // Add subgrupo node if not yet added
76 + if (!subgrupoSet.has(subgrupoKey)) {
77 + subgrupoSet.add(subgrupoKey);
78 + results.push({
79 + gestion,
80 + nivel: 'subgrupo',
81 + organismo: subgrupoKey,
82 + desc_organismo: item.desc_organismo_subgrupo || `Subgrupo ${item.organismo_subgrupo}`,
83 + parent: grupoKey,
84 + devengado: 0
85 + });
86 + }
87 +
88 + results.push({
89 + gestion,
90 + nivel: 'organismo',
91 + organismo: code,
92 + desc_organismo: item.desc_organismo || '',
93 + parent: subgrupoKey,
94 + devengado: yearData.total
95 + });
96 + }
97 +
98 + return new Response(JSON.stringify(results), {
99 + headers: { 'Content-Type': 'application/json' }
100 + });
101 + } else {
102 + if (!cachedClasificador) {
103 + const clasRes = await fetch(`${API_BASE}/clasificador`);
104 + cachedClasificador = await clasRes.json();
105 + }
106 +
107 + const results = [];
108 + const grupoSet2 = new Set();
109 + const subgrupoSet2 = new Set();
110 + const batchSize = 80;
111 +
112 + for (let i = 0; i < cachedClasificador.length; i += batchSize) {
113 + const batch = cachedClasificador.slice(i, i + batchSize);
114 + const batchResults = await Promise.all(batch.map(async (item) => {
115 + try {
116 + const code = String(item.organismo);
117 + const res = await fetch(`${API_BASE}/${code}/entidades/${entidad}`);
118 + if (!res.ok) return null;
119 + const data = await res.json();
120 + const yearData = data.find(d => d.gestion === gestion);
121 + if (!yearData || !yearData.monto) return null;
122 + return { item, code, monto: yearData.monto };
123 + } catch { return null; }
124 + }));
125 +
126 + batchResults.filter(Boolean).forEach(({ item, code, monto }) => {
127 + const grupoKey = `g_${item.organismo_grupo}`;
128 + const subgrupoKey = `sg_${item.organismo_grupo}_${item.organismo_subgrupo}`;
129 +
130 + if (!grupoSet2.has(grupoKey)) {
131 + grupoSet2.add(grupoKey);
132 + results.push({ gestion, nivel: 'grupo', organismo: grupoKey, desc_organismo: item.desc_organismo_grupo || '', parent: null, devengado: 0 });
133 + }
134 + if (!subgrupoSet2.has(subgrupoKey)) {
135 + subgrupoSet2.add(subgrupoKey);
136 + results.push({ gestion, nivel: 'subgrupo', organismo: subgrupoKey, desc_organismo: item.desc_organismo_subgrupo || '', parent: grupoKey, devengado: 0 });
137 + }
138 + results.push({ gestion, nivel: 'organismo', organismo: code, desc_organismo: item.desc_organismo || '', parent: subgrupoKey, devengado: monto });
139 + });
140 + }
141 +
142 + return new Response(JSON.stringify(results), {
143 + headers: { 'Content-Type': 'application/json' }
144 + });
145 + }
146 + } catch (err) {
147 + return new Response(JSON.stringify({ error: 'Failed to build treemap' }), {
148 + status: 500, headers: { 'Content-Type': 'application/json' }
149 + });
150 + }
151 +}
1 +const API_BASE = 'http://136.112.29.74/api/rubro/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/rubro';
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`;
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/rubro';
2 +
3 +let cachedClasificador = null;
4 +let cachedEstados = null;
5 +let cachedTimestamp = 0;
6 +const CACHE_TTL = 5 * 60 * 1000;
7 +
8 +function getParent(code, nivel) {
9 + const s = String(code).padStart(5, '0');
10 + if (nivel === 'tipo') return null;
11 + // clase (ABC00) → tipo (AB000)
12 + if (nivel === 'clase') return s.substring(0, 2) + '000';
13 + // cuenta (ABCD0) → clase (ABC00)
14 + if (nivel === 'cuenta') return s.substring(0, 3) + '00';
15 + // sub_cuenta (ABCDE) → cuenta (ABCD0)
16 + if (nivel === 'sub_cuenta') return s.substring(0, 4) + '0';
17 + return null;
18 +}
19 +
20 +async function loadAndCacheAll() {
21 + const now = Date.now();
22 + if (cachedEstados && (now - cachedTimestamp) < CACHE_TTL) return;
23 +
24 + const clasRes = await fetch(`${API_BASE}/clasificador`);
25 + if (!clasRes.ok) throw new Error('Failed to fetch clasificador');
26 + cachedClasificador = await clasRes.json();
27 +
28 + const estados = new Map();
29 + const batchSize = 80;
30 +
31 + for (let i = 0; i < cachedClasificador.length; i += batchSize) {
32 + const batch = cachedClasificador.slice(i, i + batchSize);
33 + const results = await Promise.all(batch.map(async (item) => {
34 + try {
35 + const code = item.rubro;
36 + const res = await fetch(`${API_BASE}/${code}`);
37 + if (!res.ok) return null;
38 + const data = await res.json();
39 + return { code: String(code), estados: data.estados || [] };
40 + } catch { return null; }
41 + }));
42 + results.filter(Boolean).forEach(r => estados.set(r.code, r.estados));
43 + }
44 +
45 + cachedEstados = estados;
46 + cachedTimestamp = now;
47 +}
48 +
49 +export async function GET({ url }) {
50 + const gestion = parseInt(url.searchParams.get('gestion') || '2025');
51 + const entidad = url.searchParams.get('entidad') || '0';
52 +
53 + try {
54 + if (entidad === '0') {
55 + await loadAndCacheAll();
56 +
57 + const results = [];
58 + for (const item of cachedClasificador) {
59 + const code = String(item.rubro);
60 + const estados = cachedEstados.get(code);
61 + if (!estados) continue;
62 + const yearData = estados.find(d => d.gestion === gestion);
63 + if (!yearData || !yearData.total) continue;
64 + results.push({
65 + gestion,
66 + nivel: item.nivel || yearData.nivel || '',
67 + rubro: code,
68 + desc_rubro: item.desc_rubro || '',
69 + parent: getParent(code, item.nivel || yearData.nivel),
70 + devengado: yearData.total
71 + });
72 + }
73 +
74 + // Add missing parent nodes (tipos without estados)
75 + const ids = new Set(results.map(r => r.rubro));
76 + const missingParents = new Set();
77 + results.forEach(r => {
78 + if (r.parent && !ids.has(r.parent)) missingParents.add(r.parent);
79 + });
80 + for (const parentCode of missingParents) {
81 + const item = cachedClasificador.find(c => String(c.rubro) === parentCode);
82 + results.push({
83 + gestion,
84 + nivel: item?.nivel || 'tipo',
85 + rubro: parentCode,
86 + desc_rubro: item?.desc_rubro || parentCode,
87 + parent: getParent(parentCode, item?.nivel || 'tipo'),
88 + devengado: 0
89 + });
90 + }
91 +
92 + return new Response(JSON.stringify(results), {
93 + headers: { 'Content-Type': 'application/json' }
94 + });
95 + } else {
96 + if (!cachedClasificador) {
97 + const clasRes = await fetch(`${API_BASE}/clasificador`);
98 + cachedClasificador = await clasRes.json();
99 + }
100 +
101 + const results = [];
102 + const batchSize = 80;
103 +
104 + for (let i = 0; i < cachedClasificador.length; i += batchSize) {
105 + const batch = cachedClasificador.slice(i, i + batchSize);
106 + const batchResults = await Promise.all(batch.map(async (item) => {
107 + try {
108 + const code = String(item.rubro);
109 + const res = await fetch(`${API_BASE}/${code}/entidades/${entidad}`);
110 + if (!res.ok) return null;
111 + const data = await res.json();
112 + const yearData = data.find(d => d.gestion === gestion);
113 + if (!yearData || !yearData.monto) return null;
114 + return {
115 + gestion,
116 + nivel: item.nivel || '',
117 + rubro: code,
118 + desc_rubro: item.desc_rubro || '',
119 + parent: getParent(code, item.nivel),
120 + devengado: yearData.monto
121 + };
122 + } catch { return null; }
123 + }));
124 + results.push(...batchResults.filter(Boolean));
125 + }
126 +
127 + return new Response(JSON.stringify(results), {
128 + headers: { 'Content-Type': 'application/json' }
129 + });
130 + }
131 + } catch (err) {
132 + return new Response(JSON.stringify({ error: 'Failed to build treemap' }), {
133 + status: 500, headers: { 'Content-Type': 'application/json' }
134 + });
135 + }
136 +}
1 +const API_BASE = 'http://136.112.29.74/api/rubro';
2 +
3 +export async function GET({ url }) {
4 + const codigo = url.searchParams.get('codigo') || '';
5 + const gestion = url.searchParams.get('gestion') || '';
6 +
7 + if (!codigo) {
8 + return new Response(JSON.stringify({ error: 'Missing codigo' }), {
9 + status: 400, headers: { 'Content-Type': 'application/json' }
10 + });
11 + }
12 +
13 + const apiUrl = new URL(`${API_BASE}/${codigo}/ubigeos`);
14 + if (gestion) apiUrl.searchParams.set('gestion', gestion);
15 +
16 + try {
17 + const response = await fetch(apiUrl.toString());
18 + if (!response.ok) {
19 + return new Response(JSON.stringify({ error: `API error: ${response.status}` }), {
20 + status: response.status, headers: { 'Content-Type': 'application/json' }
21 + });
22 + }
23 + const data = await response.json();
24 + return new Response(JSON.stringify(data), {
25 + headers: { 'Content-Type': 'application/json' }
26 + });
27 + } catch (err) {
28 + return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
29 + status: 500, headers: { 'Content-Type': 'application/json' }
30 + });
31 + }
32 +}
1 +const API_BASE = 'http://136.112.29.74/api/ubigeo';
2 +
3 +export async function GET({ url }) {
4 + const codigo = url.searchParams.get('codigo') || '';
5 + const tipo = url.searchParams.get('tipo') || 'detalle';
6 +
7 + if (!codigo) {
8 + return new Response(JSON.stringify({ error: 'Missing codigo' }), {
9 + status: 400, headers: { 'Content-Type': 'application/json' }
10 + });
11 + }
12 +
13 + let apiUrl;
14 + if (tipo === 'detalle') {
15 + apiUrl = `${API_BASE}/${codigo}`;
16 + } else if (tipo === 'objetos') {
17 + apiUrl = `${API_BASE}/${codigo}/objetos`;
18 + } else if (tipo === 'finfuns') {
19 + apiUrl = `${API_BASE}/${codigo}/finfuns`;
20 + } else if (tipo === 'actecos') {
21 + apiUrl = `${API_BASE}/${codigo}/actecos`;
22 + } else if (tipo === 'clasificador') {
23 + apiUrl = `${API_BASE}/clasificador`;
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 +}
This diff could not be displayed because it is too large.
...@@ -230,8 +230,13 @@ ...@@ -230,8 +230,13 @@
230 const data = await res.json(); 230 const data = await res.json();
231 231
232 if (Array.isArray(data)) { 232 if (Array.isArray(data)) {
233 - entities = data.sort((a, b) => (a.desc_entidad || '').localeCompare(b.desc_entidad || '')); 233 + entities = data
234 - console.log('Entities loaded:', entities.length); 234 + .filter(e => {
235 + if (!e.gestiones) return false;
236 + const años = e.gestiones.split(',').map(Number);
237 + return años.includes(year);
238 + })
239 + .sort((a, b) => (a.desc_entidad || '').localeCompare(b.desc_entidad || ''));
235 } 240 }
236 241
237 if (selectedEntity) { 242 if (selectedEntity) {
...@@ -787,7 +792,7 @@ ...@@ -787,7 +792,7 @@
787 792
788 // Años desde gestiones del primer grupo 793 // Años desde gestiones del primer grupo
789 if (data[0]?.gestiones) { 794 if (data[0]?.gestiones) {
790 - availableYears = data[0].gestiones.split(',').map(Number).filter(y => y >= 2016).sort((a, b) => b - a); 795 + availableYears = data[0].gestiones.split(',').map(Number).filter(y => y >= 2016 && y <= 2025).sort((a, b) => b - a);
791 selectedYear = availableYears[0]; 796 selectedYear = availableYears[0];
792 } else { 797 } else {
793 availableYears = Array.from({ length: 10 }, (_, i) => 2025 - i); 798 availableYears = Array.from({ length: 10 }, (_, i) => 2025 - i);
...@@ -1468,12 +1473,12 @@ ...@@ -1468,12 +1473,12 @@
1468 1473
1469 <svelte:window onclick={handleClickOutsideEntity} /> 1474 <svelte:window onclick={handleClickOutsideEntity} />
1470 1475
1471 -<div class="min-h-screen" style="font-family: var(--font-sans); background-color: var(--theme-body); color: var(--theme-titulo);"> 1476 +<div class="min-h-screen" style="font-family: var(--font-sans); background-color: var(--theme-body); color: var(--theme-titulo); padding-top: 60px;">
1472 <!-- Header pedagógico --> 1477 <!-- Header pedagógico -->
1473 <header class="border-b" style="border-color: var(--theme-borde); background-color: var(--theme-body);"> 1478 <header class="border-b" style="border-color: var(--theme-borde); background-color: var(--theme-body);">
1474 - <div class="max-w-screen-xl mx-auto px-4 sm:px-6 {viewMode === 'mapa' || viewMode === 'comparar' ? 'py-2' : 'py-6'}"> 1479 + <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-2">
1475 <!-- Breadcrumb: responsive --> 1480 <!-- Breadcrumb: responsive -->
1476 - <nav class="{viewMode === 'comparar' ? 'mb-1' : 'mb-4'}" style="font-family: var(--font-sans); font-variant-numeric: tabular-nums; font-size: 0.75rem;"> 1481 + <nav class="mb-1" style="font-family: var(--font-sans); font-variant-numeric: tabular-nums; font-size: 0.75rem;">
1477 <!-- Móvil: solo padre --> 1482 <!-- Móvil: solo padre -->
1478 <a href="/clasificadores" class="sm:hidden transition-colors" style="color: var(--theme-texto);"> 1483 <a href="/clasificadores" class="sm:hidden transition-colors" style="color: var(--theme-texto);">
1479 ← Clasificadores 1484 ← Clasificadores
...@@ -1487,25 +1492,10 @@ ...@@ -1487,25 +1492,10 @@
1487 </nav> 1492 </nav>
1488 1493
1489 <div class="{viewMode === 'mapa' || viewMode === 'comparar' ? 'flex-1' : 'max-w-5xl'}"> 1494 <div class="{viewMode === 'mapa' || viewMode === 'comparar' ? 'flex-1' : 'max-w-5xl'}">
1490 - <div class="flex items-center gap-3 {viewMode === 'mapa' || viewMode === 'comparar' ? 'mb-1' : 'mb-3'}"> 1495 + <h1 style="font-family: 'DM Serif Display', serif; font-weight: 400; font-size: 2rem; margin-bottom: 0.25rem; color: var(--theme-titulo);">
1491 - <h1 class="{viewMode === 'mapa' || viewMode === 'comparar' ? 'text-xl' : 'text-3xl'}" style="font-family: var(--font-display); color: var(--theme-titulo);">
1492 ¿En qué se gasta? 1496 ¿En qué se gasta?
1493 - </h1> 1497 + </h1>
1494 - {#if viewMode === 'mapa'} 1498 + <div class="flex items-center gap-3">
1495 - <button
1496 - onclick={() => showTreemapHelp = true}
1497 - class="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs transition-colors"
1498 - style="color: var(--theme-texto); background-color: var(--theme-fill);"
1499 - title="Cómo leer el gráfico"
1500 - >
1501 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1502 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
1503 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
1504 - </svg>
1505 - <span class="hidden sm:inline">Cómo leer</span>
1506 - </button>
1507 - {/if}
1508 - <div style="margin-left: auto; display: flex; align-items: center; gap: 0.75rem;">
1509 <button class="share-btn-clas" onclick={() => { 1499 <button class="share-btn-clas" onclick={() => {
1510 navigator.clipboard.writeText(window.location.href); 1500 navigator.clipboard.writeText(window.location.href);
1511 clasLinkCopied = true; 1501 clasLinkCopied = true;
...@@ -1521,13 +1511,6 @@ ...@@ -1521,13 +1511,6 @@
1521 </svg> 1511 </svg>
1522 {clasLinkCopied ? 'Copiado' : 'Compartir'} 1512 {clasLinkCopied ? 'Copiado' : 'Compartir'}
1523 </button> 1513 </button>
1524 - <a href="/" class="share-btn-clas" style="text-decoration: none;">
1525 - <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1526 - <path d="M19 12H5M12 19l-7-7 7-7"/>
1527 - </svg>
1528 - Volver
1529 - </a>
1530 - </div>
1531 </div> 1514 </div>
1532 {#if viewMode === 'mapa'} 1515 {#if viewMode === 'mapa'}
1533 <div class="title-selectors"> 1516 <div class="title-selectors">
...@@ -1565,7 +1548,6 @@ ...@@ -1565,7 +1548,6 @@
1565 class:active={selectedEntity?.entidad === entity.entidad} 1548 class:active={selectedEntity?.entidad === entity.entidad}
1566 onclick={() => { selectEntity(entity); titleEntityDropdownOpen = false; }} 1549 onclick={() => { selectEntity(entity); titleEntityDropdownOpen = false; }}
1567 > 1550 >
1568 - <span class="option-code">{entity.entidad}</span>
1569 <span class="option-name">{entity.desc_entidad}</span> 1551 <span class="option-name">{entity.desc_entidad}</span>
1570 </button> 1552 </button>
1571 {/each} 1553 {/each}
...@@ -1616,7 +1598,7 @@ ...@@ -1616,7 +1598,7 @@
1616 </div> 1598 </div>
1617 1599
1618 <!-- Controles de visualización (fuera del max-w-3xl para usar todo el ancho) --> 1600 <!-- Controles de visualización (fuera del max-w-3xl para usar todo el ancho) -->
1619 - <div class="{viewMode === 'mapa' ? 'mt-3' : 'mt-8'}"> 1601 + <div class="mt-2">
1620 <!-- Fila principal: Toggle + Guía + Filtros --> 1602 <!-- Fila principal: Toggle + Guía + Filtros -->
1621 <div class="flex flex-col md:flex-row gap-4 md:items-center md:justify-between"> 1603 <div class="flex flex-col md:flex-row gap-4 md:items-center md:justify-between">
1622 <!-- Selector de modo + Guía inline (visible en sm+) --> 1604 <!-- Selector de modo + Guía inline (visible en sm+) -->
...@@ -1770,7 +1752,7 @@ ...@@ -1770,7 +1752,7 @@
1770 {layoutMounted ? 'sidebar-mounted' : ''} 1752 {layoutMounted ? 'sidebar-mounted' : ''}
1771 " 1753 "
1772 > 1754 >
1773 - <div class="h-full lg:h-screen lg:sticky lg:top-0 overflow-y-auto py-6 px-4 lg:px-0 lg:pr-6"> 1755 + <div class="h-full lg:sticky overflow-y-auto py-4 px-4 lg:px-0 lg:pr-6" style="top: 70px; max-height: calc(100vh - 70px);">
1774 <!-- Cerrar en móvil --> 1756 <!-- Cerrar en móvil -->
1775 <div class="flex justify-between items-center mb-4 lg:hidden"> 1757 <div class="flex justify-between items-center mb-4 lg:hidden">
1776 <span class="text-sm font-medium" style="color: var(--theme-titulo);">Grupos de gasto</span> 1758 <span class="text-sm font-medium" style="color: var(--theme-titulo);">Grupos de gasto</span>
...@@ -2040,7 +2022,7 @@ ...@@ -2040,7 +2022,7 @@
2040 2022
2041 <!-- Sidebar derecha: Navegación rápida --> 2023 <!-- Sidebar derecha: Navegación rápida -->
2042 <aside class="w-56 flex-shrink-0 hidden xl:block"> 2024 <aside class="w-56 flex-shrink-0 hidden xl:block">
2043 - <div class="sticky top-0 h-screen overflow-y-auto py-8 pl-6 border-l" style="border-color: var(--theme-borde);"> 2025 + <div class="sticky overflow-y-auto py-4 pl-6 border-l" style="top: 70px; max-height: calc(100vh - 70px); border-color: var(--theme-borde);">
2044 <p class="text-sm uppercase tracking-wide mb-4 font-medium" style="color: var(--theme-texto);">Navegación</p> 2026 <p class="text-sm uppercase tracking-wide mb-4 font-medium" style="color: var(--theme-texto);">Navegación</p>
2045 {#if selectedGrupo} 2027 {#if selectedGrupo}
2046 <nav class="space-y-3"> 2028 <nav class="space-y-3">
...@@ -2286,7 +2268,7 @@ ...@@ -2286,7 +2268,7 @@
2286 class="treemap-node" 2268 class="treemap-node"
2287 transform="translate({node.x0}, {node.y0})" 2269 transform="translate({node.x0}, {node.y0})"
2288 onmouseenter={() => { if (!pinnedNode) hoveredNode = node; }} 2270 onmouseenter={() => { if (!pinnedNode) hoveredNode = node; }}
2289 - onmouseleave={() => { if (!pinnedNode && !tooltipHovered) hoveredNode = null; }} 2271 + onmouseleave={() => { if (!pinnedNode) setTimeout(() => { if (!tooltipHovered) hoveredNode = null; }, 30); }}
2290 onclick={(e) => handleFlatNodeClick(node, e)} 2272 onclick={(e) => handleFlatNodeClick(node, e)}
2291 style="cursor: pointer;" 2273 style="cursor: pointer;"
2292 > 2274 >
...@@ -2606,7 +2588,6 @@ ...@@ -2606,7 +2588,6 @@
2606 <button class="inline-dropdown-option" class:active={!compareA.entity} onclick={() => clearCompareEntity('A')}>Todo el Estado</button> 2588 <button class="inline-dropdown-option" class:active={!compareA.entity} onclick={() => clearCompareEntity('A')}>Todo el Estado</button>
2607 {#each filteredEntitiesA() as entity} 2589 {#each filteredEntitiesA() as entity}
2608 <button class="inline-dropdown-option" class:active={compareA.entity?.entidad === entity.entidad} onclick={() => selectCompareEntity('A', entity)}> 2590 <button class="inline-dropdown-option" class:active={compareA.entity?.entidad === entity.entidad} onclick={() => selectCompareEntity('A', entity)}>
2609 - <span class="font-mono text-xs opacity-50">{entity.entidad}</span>
2610 <span class="truncate">{entity.desc_entidad}</span> 2591 <span class="truncate">{entity.desc_entidad}</span>
2611 </button> 2592 </button>
2612 {/each} 2593 {/each}
...@@ -2768,7 +2749,6 @@ ...@@ -2768,7 +2749,6 @@
2768 <button class="inline-dropdown-option" class:active={!compareB.entity} onclick={() => clearCompareEntity('B')}>Todo el Estado</button> 2749 <button class="inline-dropdown-option" class:active={!compareB.entity} onclick={() => clearCompareEntity('B')}>Todo el Estado</button>
2769 {#each filteredEntitiesB() as entity} 2750 {#each filteredEntitiesB() as entity}
2770 <button class="inline-dropdown-option" class:active={compareB.entity?.entidad === entity.entidad} onclick={() => selectCompareEntity('B', entity)}> 2751 <button class="inline-dropdown-option" class:active={compareB.entity?.entidad === entity.entidad} onclick={() => selectCompareEntity('B', entity)}>
2771 - <span class="font-mono text-xs opacity-50">{entity.entidad}</span>
2772 <span class="truncate">{entity.desc_entidad}</span> 2752 <span class="truncate">{entity.desc_entidad}</span>
2773 </button> 2753 </button>
2774 {/each} 2754 {/each}
......
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
1 -import { supabase } from '$lib/supabase';
2 import { error } from '@sveltejs/kit'; 1 import { error } from '@sveltejs/kit';
3 2
4 export async function load({ params, fetch }) { 3 export async function load({ params, fetch }) {
...@@ -11,25 +10,17 @@ export async function load({ params, fetch }) { ...@@ -11,25 +10,17 @@ export async function load({ params, fetch }) {
11 throw error(400, 'Código de entidad inválido'); 10 throw error(400, 'Código de entidad inválido');
12 } 11 }
13 12
14 - // Cargar metadata, resumen y población en paralelo 13 + // Cargar metadata desde proxy local + población en paralelo
15 - const [entidadRes, resumenRes, pobRes] = await Promise.all([ 14 + const [entidadRes, pobRes] = await Promise.all([
16 - supabase 15 + fetch(`/api/entidad-data?codigo=${entidadNum}&tipo=detalle`).then(r => r.ok ? r.json() : null),
17 - .schema('ppto')
18 - .from('clas_institucional')
19 - .select('*')
20 - .eq('entidad', entidadNum)
21 - .single(),
22 -
23 - supabase
24 - .schema('ppto')
25 - .from('entidad_resumen')
26 - .select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking')
27 - .eq('codigo', codigo),
28 -
29 fetch('/poblacion.csv').then(r => r.text()) 16 fetch('/poblacion.csv').then(r => r.text())
30 ]); 17 ]);
31 18
32 - // Buscar población: primero por entidad, si no suma nacional 19 + if (!entidadRes) {
20 + throw error(404, 'Entidad no encontrada');
21 + }
22 +
23 + // Población
33 const poblacionEntidad = {}; 24 const poblacionEntidad = {};
34 const poblacionNacional = {}; 25 const poblacionNacional = {};
35 pobRes.split('\n').slice(1).forEach(line => { 26 pobRes.split('\n').slice(1).forEach(line => {
...@@ -37,51 +28,79 @@ export async function load({ params, fetch }) { ...@@ -37,51 +28,79 @@ export async function load({ params, fetch }) {
37 const g = parseInt(gestion); 28 const g = parseInt(gestion);
38 const p = parseInt(pob); 29 const p = parseInt(pob);
39 if (!g || !p) return; 30 if (!g || !p) return;
40 - // Suma nacional
41 poblacionNacional[g] = (poblacionNacional[g] || 0) + p; 31 poblacionNacional[g] = (poblacionNacional[g] || 0) + p;
42 - // Por entidad (código sin DA)
43 if (ent === entidadCode) { 32 if (ent === entidadCode) {
44 poblacionEntidad[g] = (poblacionEntidad[g] || 0) + p; 33 poblacionEntidad[g] = (poblacionEntidad[g] || 0) + p;
45 } 34 }
46 }); 35 });
47 36
48 - // Usar población de la entidad si existe, si no la nacional
49 const tienePobEntidad = Object.keys(poblacionEntidad).length > 0; 37 const tienePobEntidad = Object.keys(poblacionEntidad).length > 0;
50 const poblacionMap = tienePobEntidad ? poblacionEntidad : poblacionNacional; 38 const poblacionMap = tienePobEntidad ? poblacionEntidad : poblacionNacional;
51 39
52 - if (entidadRes.error || !entidadRes.data) { 40 + // gastos_ingresos → resumenData (compatible con el formato que espera la página)
53 - throw error(404, 'Entidad no encontrada'); 41 + const gastosIngresos = entidadRes.gastos_ingresos || [];
54 - } 42 + const resumenData = gastosIngresos.map(d => ({
43 + tipo: d.tipo,
44 + gestion: d.gestion,
45 + devengado: d.devengado,
46 + ranking: d.ranking,
47 + codigo: codigo,
48 + desc: entidadRes.desc_entidad
49 + }));
55 50
56 - // Determinar última gestión disponible 51 + // Determinar última gestión
57 - const gestiones = [...new Set((resumenRes.data || []).map(d => d.gestion))].sort((a, b) => b - a); 52 + const gestiones = [...new Set(gastosIngresos.map(d => d.gestion))].filter(g => g <= 2025).sort((a, b) => b - a);
58 const ultimaGestion = gestiones[0] || 2025; 53 const ultimaGestion = gestiones[0] || 2025;
59 54
60 - // Cargar distribuciones solo de la última gestión 55 + // Cargar distribuciones de la última gestión (objetos, finfuns, etc.)
61 - const distRes = await supabase 56 + const distEndpoints = [
62 - .schema('ppto') 57 + { api: 'objetos', dim: 'objeto' },
63 - .from('entidad_distribuciones') 58 + { api: 'finfuns', dim: 'finfun' },
64 - .select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado') 59 + { api: 'actecos', dim: 'acteco' },
65 - .eq('codigo', codigo) 60 + { api: 'rubros', dim: 'rubro' },
66 - .eq('gestion', ultimaGestion); 61 + { api: 'organismos', dim: 'organismo' }
62 + ];
63 + const distResults = await Promise.all(
64 + distEndpoints.map(async ({ api, dim }) => {
65 + try {
66 + const res = await fetch(`/api/entidad-data?codigo=${entidadNum}&tipo=${api}`);
67 + if (!res.ok) return [];
68 + const data = await res.json();
69 + if (!Array.isArray(data)) return [];
70 + return data
71 + .filter(d => d.gestion === ultimaGestion)
72 + .map(d => ({
73 + tipo: d.tipo || 'gastos',
74 + dimension: dim,
75 + gestion: d.gestion,
76 + padre: d.padre,
77 + desc_padre: d.desc_padre,
78 + hijo: d.hijo,
79 + desc_hijo: d.desc_hijo,
80 + devengado: d.devengado
81 + }));
82 + } catch { return []; }
83 + })
84 + );
85 +
86 + const distribucionesData = distResults.flat();
67 87
68 - // Resolver nombre de DA desde el resumen 88 + // DA info
69 let nombreDA = null; 89 let nombreDA = null;
70 let nombreEntidadMadre = null; 90 let nombreEntidadMadre = null;
71 - if (isDA && resumenRes.data?.length > 0) { 91 + if (isDA) {
72 - const firstRow = resumenRes.data[0]; 92 + nombreDA = entidadRes.desc_entidad || null;
73 - nombreDA = firstRow.desc || null; 93 + nombreEntidadMadre = entidadRes.desc_entidad || null;
74 - nombreEntidadMadre = firstRow.desc_padre || null;
75 } 94 }
76 95
77 return { 96 return {
78 - entidad: entidadRes.data, 97 + entidad: entidadRes,
79 isDA, 98 isDA,
80 nombreDA, 99 nombreDA,
81 nombreEntidadMadre, 100 nombreEntidadMadre,
82 codigoEntidadPadre: isDA ? entidadCode : null, 101 codigoEntidadPadre: isDA ? entidadCode : null,
83 - resumenData: resumenRes.data || [], 102 + resumenData,
84 - distribucionesData: distRes.data || [], 103 + distribucionesData,
85 gestionInicial: ultimaGestion, 104 gestionInicial: ultimaGestion,
86 poblacionMap, 105 poblacionMap,
87 tienePobEntidad 106 tienePobEntidad
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
2 import { onMount, tick } from 'svelte'; 2 import { onMount, tick } from 'svelte';
3 import { page } from '$app/stores'; 3 import { page } from '$app/stores';
4 import * as d3 from 'd3'; 4 import * as d3 from 'd3';
5 - import { supabase } from '$lib/supabase';
6 5
7 let { data } = $props(); 6 let { data } = $props();
8 let entidad = $derived(data.entidad); 7 let entidad = $derived(data.entidad);
...@@ -37,15 +36,43 @@ ...@@ -37,15 +36,43 @@
37 return; 36 return;
38 } 37 }
39 cargandoDist = true; 38 cargandoDist = true;
40 - const { data: rows } = await supabase 39 + const entidadNum = codigoSeleccionado.includes('.') ? codigoSeleccionado.split('.')[0] : codigoSeleccionado;
41 - .schema('ppto') 40 + const dims = [
42 - .from('entidad_distribuciones') 41 + { api: 'objetos', dim: 'objeto' },
43 - .select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado') 42 + { api: 'finfuns', dim: 'finfun' },
44 - .eq('codigo', codigoSeleccionado) 43 + { api: 'actecos', dim: 'acteco' },
45 - .eq('gestion', gestion); 44 + { api: 'rubros', dim: 'rubro' },
46 - const result = rows || []; 45 + { api: 'organismos', dim: 'organismo' }
47 - distCache[gestion] = result; 46 + ];
48 - distribucionesData = result; 47 + try {
48 + const results = await Promise.all(
49 + dims.map(async ({ api, dim }) => {
50 + try {
51 + const res = await fetch(`/api/entidad-data?codigo=${entidadNum}&tipo=${api}`);
52 + if (!res.ok) return [];
53 + const data = await res.json();
54 + if (!Array.isArray(data)) return [];
55 + return data
56 + .filter(d => d.gestion === gestion)
57 + .map(d => ({
58 + tipo: d.tipo || 'gastos',
59 + dimension: dim,
60 + gestion: d.gestion,
61 + padre: d.padre,
62 + desc_padre: d.desc_padre,
63 + hijo: d.hijo,
64 + desc_hijo: d.desc_hijo,
65 + devengado: d.devengado
66 + }));
67 + } catch { return []; }
68 + })
69 + );
70 + const result = results.flat();
71 + distCache[gestion] = result;
72 + distribucionesData = result;
73 + } catch {
74 + distribucionesData = [];
75 + }
49 cargandoDist = false; 76 cargandoDist = false;
50 } 77 }
51 78
......
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 finfun 3 +const API_BASE = 'http://136.112.29.74/api/finfun';
5 -// Jerarquía: Finalidad (1-9 o 10) → Grupo Función (2-3 dígitos) → Función (3+ dígitos)
6 -// Nota: Finalidad 10 es especial - tiene 2 dígitos, sus hijos empiezan con "10"
7 4
8 -function getFinalidadCode(finfun) { 5 +// Jerarquía finfun: Finalidad → Grupo Función → Función
9 - const code = String(finfun); 6 +// Códigos con punto: "1" (finalidad), "1.1" (grpfuncion), "1.1.1" (función)
10 - // Si empieza con "10", la finalidad es "10" 7 +
11 - if (code.startsWith('10')) { 8 +function getNivel(codigo) {
12 - return '10'; 9 + const parts = String(codigo).split('.');
13 - } 10 + if (parts.length === 1) return 'finalidad';
14 - // Si no, la finalidad es el primer dígito 11 + if (parts.length === 2) return 'grpfuncion';
15 - return code.charAt(0); 12 + return 'funcion';
16 } 13 }
17 14
18 -function getGrpFuncionCode(finfun) { 15 +function getFinalidadCode(codigo) {
19 - const code = String(finfun); 16 + return String(codigo).split('.')[0];
20 - // Si empieza con "10", el grupo función son los primeros 3 dígitos (ej: "101", "102")
21 - if (code.startsWith('10')) {
22 - return code.substring(0, 3);
23 - }
24 - // Si no, son los primeros 2 dígitos (ej: "11", "21", "93")
25 - return code.substring(0, 2);
26 } 17 }
27 18
28 -function getNivel(finfun) { 19 +function getGrpFuncionCode(codigo) {
29 - const code = String(finfun); 20 + const parts = String(codigo).split('.');
30 - // Finalidades 1-9 tienen longitud 1, finalidad 10 tiene longitud 2 21 + return parts.slice(0, 2).join('.');
31 - if (code.length === 1) return 'finalidad';
32 - if (code === '10') return 'finalidad';
33 - // GrpFuncion: 2 dígitos para 1-9, 3 dígitos para 10
34 - if (code.startsWith('10')) {
35 - if (code.length === 3) return 'grpfuncion';
36 - return 'funcion';
37 - }
38 - if (code.length === 2) return 'grpfuncion';
39 - return 'funcion';
40 } 22 }
41 23
42 export async function load({ params }) { 24 export async function load({ params }) {
43 - console.time('[SERVER] Total load finfun');
44 const { codigo } = params; 25 const { codigo } = params;
45 26
46 - console.time('[SERVER] Query clas_finfun'); 27 + // Cargar datos del finfun desde la API
47 - const { data, error: dbError } = await supabase 28 + const res = await fetch(`${API_BASE}/${codigo}`);
48 - .schema('ppto') 29 + if (!res.ok) throw error(404, 'Finalidad/Función no encontrada');
49 - .from('clas_finfun') 30 + const finfun = await res.json();
50 - .select('*')
51 - .eq('finfun', codigo);
52 - console.timeEnd('[SERVER] Query clas_finfun');
53 -
54 - if (dbError || !data || data.length === 0) {
55 - throw error(404, 'Finalidad/Función no encontrada');
56 - }
57 31
58 - // Tomar el primer resultado
59 - const finfun = data[0];
60 const nivel = finfun.nivel || getNivel(codigo); 32 const nivel = finfun.nivel || getNivel(codigo);
61 33
62 - // Obtener jerarquía (padres e hijos) 34 + // Cargar clasificador completo para derivar padres e hijos
63 let padres = []; 35 let padres = [];
64 let hijos = []; 36 let hijos = [];
65 37
66 - console.time('[SERVER] Query padres'); 38 + try {
67 - 39 + const clasRes = await fetch(`${API_BASE}/clasificador`);
68 - // Buscar padres según nivel 40 + if (clasRes.ok) {
69 - if (nivel === 'funcion') { 41 + const clasificador = await clasRes.json();
70 - // Padres: finalidad y grupo función 42 +
71 - const finalidadCode = getFinalidadCode(codigo); 43 + // Padres
72 - const grpFuncionCode = getGrpFuncionCode(codigo); 44 + if (nivel === 'funcion') {
73 - 45 + const finalidadCode = getFinalidadCode(codigo);
74 - const { data: padresData } = await supabase 46 + const grpCode = getGrpFuncionCode(codigo);
75 - .schema('ppto') 47 + padres = clasificador.filter(c => c.finfun === finalidadCode || c.finfun === grpCode);
76 - .from('clas_finfun') 48 + } else if (nivel === 'grpfuncion') {
77 - .select('*') 49 + const finalidadCode = getFinalidadCode(codigo);
78 - .in('finfun', [finalidadCode, grpFuncionCode]) 50 + padres = clasificador.filter(c => c.finfun === finalidadCode);
79 - .order('finfun'); 51 + }
80 - 52 +
81 - if (padresData) padres = padresData; 53 + // Hijos
82 - } else if (nivel === 'grpfuncion') { 54 + if (nivel === 'finalidad') {
83 - // Padre: finalidad 55 + hijos = clasificador.filter(c =>
84 - const finalidadCode = getFinalidadCode(codigo); 56 + c.nivel === 'grpfuncion' && getFinalidadCode(c.finfun) === codigo && c.finfun !== codigo
85 - 57 + );
86 - const { data: padresData } = await supabase 58 + } else if (nivel === 'grpfuncion') {
87 - .schema('ppto') 59 + hijos = clasificador.filter(c =>
88 - .from('clas_finfun') 60 + c.nivel === 'funcion' && getGrpFuncionCode(c.finfun) === codigo
89 - .select('*') 61 + );
90 - .eq('finfun', finalidadCode); 62 + }
91 - 63 +
92 - if (padresData) padres = padresData; 64 + padres.sort((a, b) => a.finfun.localeCompare(b.finfun));
93 - } 65 + hijos.sort((a, b) => a.finfun.localeCompare(b.finfun));
94 - // finalidad no tiene padres
95 -
96 - console.timeEnd('[SERVER] Query padres');
97 -
98 - console.time('[SERVER] Query hijos');
99 -
100 - // Buscar hijos según nivel
101 - if (nivel === 'finalidad') {
102 - // Hijos: grupos de función de esta finalidad
103 - const { data: hijosData } = await supabase
104 - .schema('ppto')
105 - .from('clas_finfun')
106 - .select('*')
107 - .eq('nivel', 'grpfuncion')
108 - .order('finfun');
109 -
110 - // Filtrar solo los hijos directos de esta finalidad
111 - // Para finalidad "1": hijos son "11", "12", ..., "19" (NO "10x")
112 - // Para finalidad "10": hijos son "101", "102", ..., "109"
113 - if (hijosData) {
114 - hijos = hijosData.filter(h => {
115 - const hijoPadre = getFinalidadCode(h.finfun);
116 - return hijoPadre === codigo;
117 - });
118 - }
119 - } else if (nivel === 'grpfuncion') {
120 - // Hijos: funciones de este grupo
121 - const grpCode = getGrpFuncionCode(codigo);
122 - const { data: hijosData } = await supabase
123 - .schema('ppto')
124 - .from('clas_finfun')
125 - .select('*')
126 - .eq('nivel', 'funcion')
127 - .order('finfun');
128 -
129 - // Filtrar solo los hijos directos de este grupo
130 - if (hijosData) {
131 - hijos = hijosData.filter(h => {
132 - const hijoGrp = getGrpFuncionCode(h.finfun);
133 - return hijoGrp === grpCode;
134 - });
135 } 66 }
136 - } 67 + } catch { /* clasificador optional */ }
137 - // funcion no tiene hijos
138 -
139 - console.timeEnd('[SERVER] Query hijos');
140 68
141 - console.timeEnd('[SERVER] Total load finfun');
142 return { 69 return {
143 finfun, 70 finfun,
144 padres, 71 padres,
......
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
...@@ -46,7 +46,10 @@ ...@@ -46,7 +46,10 @@
46 // Años disponibles desde gestiones del objeto 46 // Años disponibles desde gestiones del objeto
47 let availableYears = $derived.by(() => { 47 let availableYears = $derived.by(() => {
48 if (!objetoInfo?.gestiones) return []; 48 if (!objetoInfo?.gestiones) return [];
49 - return objetoInfo.gestiones.split(',').map(Number).filter(y => y >= 2016).sort((a, b) => b - a); 49 + if (objetoInfo.estados?.length > 0) {
50 + return objetoInfo.estados.map(e => e.gestion).filter(y => y >= 2016 && y > 0).sort((a, b) => b - a);
51 + }
52 + return objetoInfo.gestiones.split(',').map(Number).filter(y => y >= 2016 && y <= new Date().getFullYear()).sort((a, b) => b - a);
50 }); 53 });
51 54
52 // Formatters 55 // Formatters
...@@ -285,12 +288,6 @@ ...@@ -285,12 +288,6 @@
285 </svg> 288 </svg>
286 <span>{linkCopied ? 'Copiado' : 'Compartir'}</span> 289 <span>{linkCopied ? 'Copiado' : 'Compartir'}</span>
287 </button> 290 </button>
288 - <a href="/objeto/{codigo}" class="back-link">
289 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
290 - <path d="M19 12H5M12 19l-7-7 7-7"/>
291 - </svg>
292 - Volver
293 - </a>
294 </div> 291 </div>
295 </div> 292 </div>
296 293
......
1 -import { supabase } from '$lib/supabase';
2 import { error } from '@sveltejs/kit'; 1 import { error } from '@sveltejs/kit';
3 2
3 +const API_BASE = 'http://136.112.29.74/api/organismo';
4 +
4 export async function load({ params }) { 5 export async function load({ params }) {
5 - console.time('[SERVER] Total load organismo');
6 const { codigo } = params; 6 const { codigo } = params;
7 7
8 - console.time('[SERVER] Query clas_organismos'); 8 + const res = await fetch(`${API_BASE}/${codigo}`);
9 - const { data, error: dbError } = await supabase 9 + if (!res.ok) throw error(404, 'Organismo no encontrado');
10 - .schema('ppto') 10 + const organismo = await res.json();
11 - .from('clas_organismos') 11 +
12 - .select('*') 12 + let padres = [];
13 - .eq('organismo', codigo); 13 + let hijos = [];
14 - console.timeEnd('[SERVER] Query clas_organismos'); 14 +
15 - 15 + try {
16 - if (dbError || !data || data.length === 0) { 16 + const clasRes = await fetch(`${API_BASE}/clasificador`);
17 - throw error(404, 'Organismo no encontrado'); 17 + if (clasRes.ok) {
18 - } 18 + const clasificador = await clasRes.json();
19 - 19 + const current = clasificador.find(c => c.organismo === parseInt(codigo));
20 - const organismo = data[0]; 20 + if (current) {
21 - 21 + // Padres: grupo y subgrupo (como objetos virtuales para navegación)
22 - // Obtener hermanos (otros organismos del mismo subgrupo) 22 + padres = [
23 - let hermanos = []; 23 + { organismo: `grupo_${current.organismo_grupo}`, desc_organismo: current.desc_organismo_grupo, nivel: 'grupo', sigla: current.sigla_organismo_grupo },
24 - 24 + { organismo: `subgrupo_${current.organismo_grupo}_${current.organismo_subgrupo}`, desc_organismo: current.desc_organismo_subgrupo, nivel: 'subgrupo', sigla: current.sigla_organismo_subgrupo }
25 - console.time('[SERVER] Query hermanos'); 25 + ];
26 - const { data: hermanosData } = await supabase 26 +
27 - .schema('ppto') 27 + // Hijos: otros organismos del mismo subgrupo
28 - .from('clas_organismos') 28 + hijos = clasificador.filter(c =>
29 - .select('*') 29 + c.organismo_grupo === current.organismo_grupo &&
30 - .eq('organismo_grupo', organismo.organismo_grupo) 30 + c.organismo_subgrupo === current.organismo_subgrupo &&
31 - .eq('organismo_sub_grupo', organismo.organismo_sub_grupo) 31 + c.organismo !== current.organismo &&
32 - .neq('organismo', codigo) 32 + c.organismo !== 0
33 - .order('organismo'); 33 + ).sort((a, b) => (a.desc_organismo || '').localeCompare(b.desc_organismo || ''));
34 - 34 + }
35 - if (hermanosData) { 35 + }
36 - hermanos = hermanosData; 36 + } catch { /* optional */ }
37 - } 37 +
38 - console.timeEnd('[SERVER] Query hermanos'); 38 + return { organismo, padres, hijos };
39 -
40 - console.timeEnd('[SERVER] Total load organismo');
41 - return {
42 - organismo,
43 - hermanos
44 - };
45 } 39 }
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
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 rubro 3 +const API_BASE = 'http://136.112.29.74/api/rubro';
5 -// Jerarquía: Tipo (XX000) → Clase (XXX00) → Cuenta (XXXX0) → Subcuenta (XXXXX)
6 -
7 -function getTipoCode(rubro) {
8 - // Primeros 2 dígitos + 000 → ej: 11000, 12000
9 - return rubro.substring(0, 2) + '000';
10 -}
11 -
12 -function getClaseCode(rubro) {
13 - // Primeros 3 dígitos + 00 → ej: 11100, 11200
14 - return rubro.substring(0, 3) + '00';
15 -}
16 -
17 -function getCuentaCode(rubro) {
18 - // Primeros 4 dígitos + 0 → ej: 11110, 11120
19 - return rubro.substring(0, 4) + '0';
20 -}
21 -
22 -function getNivelFromCode(rubro) {
23 - // Determinar nivel basado en el patrón de ceros
24 - if (rubro.endsWith('000')) return 'tipo'; // XX000
25 - if (rubro.endsWith('00')) return 'clase'; // XXX00
26 - if (rubro.endsWith('0')) return 'cuenta'; // XXXX0
27 - return 'sub_cuenta'; // XXXXX
28 -}
29 -
30 -function getNextNivel(nivel) {
31 - const niveles = { tipo: 'clase', clase: 'cuenta', cuenta: 'sub_cuenta' };
32 - return niveles[nivel] || null;
33 -}
34 4
35 export async function load({ params }) { 5 export async function load({ params }) {
36 const { codigo } = params; 6 const { codigo } = params;
37 7
38 - const { data, error: dbError } = await supabase 8 + const res = await fetch(`${API_BASE}/${codigo}`);
39 - .schema('ppto') 9 + if (!res.ok) throw error(404, 'Rubro no encontrado');
40 - .from('clas_rubros') 10 + const rubro = await res.json();
41 - .select('*')
42 - .eq('rubro', codigo);
43 -
44 - if (dbError || !data || data.length === 0) {
45 - throw error(404, 'Rubro no encontrado');
46 - }
47 11
48 - const rubro = data[0]; 12 + const nivel = rubro.nivel;
49 - const nivel = rubro.nivel || getNivelFromCode(codigo);
50 -
51 - // Obtener jerarquía (padres e hijos)
52 let padres = []; 13 let padres = [];
53 let hijos = []; 14 let hijos = [];
54 15
55 - // Buscar padres según nivel 16 + try {
56 - if (nivel === 'sub_cuenta') { 17 + const clasRes = await fetch(`${API_BASE}/clasificador`);
57 - // Padres: tipo, clase, cuenta 18 + if (clasRes.ok) {
58 - const tipoCode = getTipoCode(codigo); 19 + const clasificador = await clasRes.json();
59 - const claseCode = getClaseCode(codigo); 20 + const s = String(codigo).padStart(5, '0');
60 - const cuentaCode = getCuentaCode(codigo); 21 +
61 - 22 + // Rubro codes are 5 digits: ABCDE
62 - const { data: padresData } = await supabase 23 + // tipo = AB000, clase = ABC00, cuenta = ABCD0, sub_cuenta = ABCDE
63 - .schema('ppto') 24 + const tipoCode = s.substring(0, 2) + '000';
64 - .from('clas_rubros') 25 + const claseCode = s.substring(0, 3) + '00';
65 - .select('*') 26 + const cuentaCode = s.substring(0, 4) + '0';
66 - .in('rubro', [tipoCode, claseCode, cuentaCode]) 27 +
67 - .order('rubro'); 28 + // Padres según nivel
68 - 29 + const parentCodes = [];
69 - if (padresData) padres = padresData; 30 + if (nivel === 'sub_cuenta') parentCodes.push(cuentaCode, claseCode, tipoCode);
70 - } else if (nivel === 'cuenta') { 31 + else if (nivel === 'cuenta') parentCodes.push(claseCode, tipoCode);
71 - // Padres: tipo, clase 32 + else if (nivel === 'clase') parentCodes.push(tipoCode);
72 - const tipoCode = getTipoCode(codigo); 33 +
73 - const claseCode = getClaseCode(codigo); 34 + padres = clasificador.filter(c => {
74 - 35 + const rc = String(c.rubro).padStart(5, '0');
75 - const { data: padresData } = await supabase 36 + return parentCodes.includes(rc) && rc !== s;
76 - .schema('ppto')
77 - .from('clas_rubros')
78 - .select('*')
79 - .in('rubro', [tipoCode, claseCode])
80 - .order('rubro');
81 -
82 - if (padresData) padres = padresData;
83 - } else if (nivel === 'clase') {
84 - // Padre: tipo
85 - const tipoCode = getTipoCode(codigo);
86 -
87 - const { data: padresData } = await supabase
88 - .schema('ppto')
89 - .from('clas_rubros')
90 - .select('*')
91 - .eq('rubro', tipoCode);
92 -
93 - if (padresData) padres = padresData;
94 - }
95 - // tipo no tiene padres
96 -
97 - // Buscar hijos según nivel
98 - const nextNivel = getNextNivel(nivel);
99 - if (nextNivel) {
100 - const { data: hijosData } = await supabase
101 - .schema('ppto')
102 - .from('clas_rubros')
103 - .select('*')
104 - .eq('nivel', nextNivel)
105 - .like('rubro', `${codigo.replace(/0+$/, '')}%`)
106 - .order('rubro');
107 -
108 - if (hijosData) {
109 - // Filtrar solo hijos directos (siguiente nivel)
110 - hijos = hijosData.filter(h => {
111 - if (nivel === 'tipo') {
112 - // Hijos de tipo son clase: XXX00 donde XX = tipo
113 - return h.rubro.startsWith(codigo.substring(0, 2)) && h.nivel === 'clase';
114 - } else if (nivel === 'clase') {
115 - // Hijos de clase son cuenta: XXXX0 donde XXX = clase
116 - return h.rubro.startsWith(codigo.substring(0, 3)) && h.nivel === 'cuenta';
117 - } else if (nivel === 'cuenta') {
118 - // Hijos de cuenta son sub_cuenta: XXXXX donde XXXX = cuenta
119 - return h.rubro.startsWith(codigo.substring(0, 4)) && h.nivel === 'sub_cuenta';
120 - }
121 - return false;
122 }); 37 });
38 +
39 + // Hijos directos
40 + if (nivel === 'tipo') {
41 + hijos = clasificador.filter(c => c.nivel === 'clase' && String(c.rubro).padStart(5, '0').substring(0, 2) === s.substring(0, 2));
42 + } else if (nivel === 'clase') {
43 + hijos = clasificador.filter(c => (c.nivel === 'cuenta') && String(c.rubro).padStart(5, '0').substring(0, 3) === s.substring(0, 3));
44 + } else if (nivel === 'cuenta') {
45 + hijos = clasificador.filter(c => (c.nivel === 'sub_cuenta') && String(c.rubro).padStart(5, '0').substring(0, 4) === s.substring(0, 4));
46 + }
47 +
48 + padres.sort((a, b) => String(a.rubro).localeCompare(String(b.rubro)));
49 + hijos.sort((a, b) => String(a.rubro).localeCompare(String(b.rubro)));
123 } 50 }
124 - } 51 + } catch { /* optional */ }
125 52
126 - return { 53 + return { rubro, padres, hijos };
127 - rubro,
128 - padres,
129 - hijos
130 - };
131 } 54 }
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
44 44
45 const CLAS_SEARCH_MAP = { 45 const CLAS_SEARCH_MAP = {
46 objeto: { class_: 'objeto', codigoFn: (meta) => meta.objeto_subpartida || meta.objeto_partida || meta.objeto_subgrupo || meta.objeto_grupo || '' }, 46 objeto: { class_: 'objeto', codigoFn: (meta) => meta.objeto_subpartida || meta.objeto_partida || meta.objeto_subgrupo || meta.objeto_grupo || '' },
47 - finfun: { class_: 'finfun', codigoFn: (meta) => { 47 + finfun: { class_: 'finalidad', codigoFn: (meta) => {
48 const fin = String(meta.finfun_finalidad || ''); 48 const fin = String(meta.finfun_finalidad || '');
49 if (meta.finfun_funcion !== undefined && meta.finfun_grpfuncion !== undefined) return `${fin}.${meta.finfun_grpfuncion}.${meta.finfun_funcion}`; 49 if (meta.finfun_funcion !== undefined && meta.finfun_grpfuncion !== undefined) return `${fin}.${meta.finfun_grpfuncion}.${meta.finfun_funcion}`;
50 if (meta.finfun_grpfuncion !== undefined) return `${fin}.${meta.finfun_grpfuncion}`; 50 if (meta.finfun_grpfuncion !== undefined) return `${fin}.${meta.finfun_grpfuncion}`;
...@@ -123,13 +123,14 @@ ...@@ -123,13 +123,14 @@
123 resumen = clasUbigeoCache[key]; 123 resumen = clasUbigeoCache[key];
124 return; 124 return;
125 } 125 }
126 +
126 const params = new URLSearchParams({ codigo }); 127 const params = new URLSearchParams({ codigo });
127 if (gestion) params.set('gestion', gestion); 128 if (gestion) params.set('gestion', gestion);
128 try { 129 try {
129 const res = await fetch(`${apiEndpoint}?${params}`); 130 const res = await fetch(`${apiEndpoint}?${params}`);
130 const data = await res.json(); 131 const data = await res.json();
131 const mapped = (data || []) 132 const mapped = (data || [])
132 - .filter(d => d.ubigeo !== '0.0.0' && !/multimunicipal/i.test(d.desc_ubigeo)) 133 + .filter(d => d.ubigeo !== '0.0.0' && !d.ubigeo.endsWith('.0') && !/multimunicipal/i.test(d.desc_ubigeo) && !/multiprovincial/i.test(d.desc_ubigeo) && !/desconocido/i.test(d.desc_ubigeo))
133 .map(d => ({ 134 .map(d => ({
134 codigo: d.ubigeo, 135 codigo: d.ubigeo,
135 desc: d.desc_ubigeo, 136 desc: d.desc_ubigeo,
...@@ -403,6 +404,18 @@ ...@@ -403,6 +404,18 @@
403 const data = await res.json(); 404 const data = await res.json();
404 if (data?.desc_objeto) return data.desc_objeto; 405 if (data?.desc_objeto) return data.desc_objeto;
405 } 406 }
407 + } else if (clas === 'finfun') {
408 + const res = await fetch(`/api/finfun-data?codigo=${codigo}&tipo=clasificador`);
409 + if (res.ok) {
410 + const data = await res.json();
411 + if (data?.desc_finfun) return data.desc_finfun;
412 + }
413 + } else if (clas === 'acteco') {
414 + const res = await fetch(`/api/acteco-data?codigo=${codigo}&tipo=clasificador`);
415 + if (res.ok) {
416 + const data = await res.json();
417 + if (data?.desc_acteco) return data.desc_acteco;
418 + }
406 } 419 }
407 // Fallback: buscar en Typesense 420 // Fallback: buscar en Typesense
408 const cfg = CLAS_SEARCH_MAP[clas]; 421 const cfg = CLAS_SEARCH_MAP[clas];
...@@ -482,6 +495,7 @@ ...@@ -482,6 +495,7 @@
482 <title>Gasto por geografía | Presupuesto Público</title> 495 <title>Gasto por geografía | Presupuesto Público</title>
483 </svelte:head> 496 </svelte:head>
484 497
498 +{#if mapaData}
485 <div class="dashboard"> 499 <div class="dashboard">
486 <div class="mapa-fullscreen"> 500 <div class="mapa-fullscreen">
487 <div class="mapa-top-controls"> 501 <div class="mapa-top-controls">
...@@ -496,6 +510,11 @@ ...@@ -496,6 +510,11 @@
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> 510 <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} 511 Volver a {clasSeleccionado.nombre}
498 </a> 512 </a>
513 + {:else if clasSeleccionado && clasificadorSeleccionado === 'acteco'}
514 + <a href="/acteco/{clasSeleccionado.codigo}" class="back-link-mapa">
515 + <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>
516 + Volver a {clasSeleccionado.nombre}
517 + </a>
499 {:else} 518 {:else}
500 <span class="mapa-location-label">Ubicación geográfica</span> 519 <span class="mapa-location-label">Ubicación geográfica</span>
501 {/if} 520 {/if}
...@@ -698,7 +717,7 @@ ...@@ -698,7 +717,7 @@
698 <span class="tooltip-rank">#{pos}/{allBarras.length}</span> 717 <span class="tooltip-rank">#{pos}/{allBarras.length}</span>
699 {/if} 718 {/if}
700 </div> 719 </div>
701 - <span class="mapa-muni-count">{barrasRankeadas.length} municipios</span> 720 + <span class="mapa-muni-count">{barrasRankeadas.length} municipios/TIOCs</span>
702 </div> 721 </div>
703 </div> 722 </div>
704 723
...@@ -721,7 +740,7 @@ ...@@ -721,7 +740,7 @@
721 <!-- Drawer lateral de ranking --> 740 <!-- Drawer lateral de ranking -->
722 <div class="ranking-drawer" class:open={drawerOpen}> 741 <div class="ranking-drawer" class:open={drawerOpen}>
723 <div class="drawer-header"> 742 <div class="drawer-header">
724 - <span class="drawer-titulo">{barrasRankeadas.length} municipios</span> 743 + <span class="drawer-titulo">{barrasRankeadas.length} municipios/TIOCs</span>
725 <button class="drawer-close" onclick={() => { drawerOpen = false; }}> 744 <button class="drawer-close" onclick={() => { drawerOpen = false; }}>
726 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 745 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
727 <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> 746 <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
...@@ -769,8 +788,33 @@ ...@@ -769,8 +788,33 @@
769 </div> 788 </div>
770 </div> 789 </div>
771 </div> 790 </div>
791 +{:else}
792 +<div class="mapa-loading">
793 + <div class="mapa-loading-inner">
794 + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="animation: spin 1.5s linear infinite; opacity: 0.4;">
795 + <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
796 + </svg>
797 + <span style="font-family: var(--font-sans); font-size: 13px; color: var(--theme-texto); opacity: 0.5;">Cargando mapa...</span>
798 + </div>
799 +</div>
800 +{/if}
772 801
773 <style> 802 <style>
803 + .mapa-loading {
804 + height: 100vh;
805 + display: flex;
806 + align-items: center;
807 + justify-content: center;
808 + background: var(--theme-body);
809 + }
810 + .mapa-loading-inner {
811 + display: flex;
812 + flex-direction: column;
813 + align-items: center;
814 + gap: 12px;
815 + }
816 + @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
817 +
774 .dashboard { 818 .dashboard {
775 min-height: 100vh; 819 min-height: 100vh;
776 background: var(--theme-body); 820 background: var(--theme-body);
......
1 -import { supabase } from '$lib/supabase';
2 import { error } from '@sveltejs/kit'; 1 import { error } from '@sveltejs/kit';
3 2
4 export async function load({ params, fetch }) { 3 export async function load({ params, fetch }) {
5 - const codigo = params.ubigeo; 4 + const municipioUbigeo = params.ubigeo;
6 5
7 - // Cargar resumen y población en paralelo 6 + // Primero obtener el clasificador para mapear municipio_ubigeo → ubigeo con puntos
8 - const [resumenRes, pobRes] = await Promise.all([ 7 + const clasRes = await fetch('/api/ubigeo-data?codigo=0&tipo=clasificador');
9 - supabase 8 + if (!clasRes.ok) throw error(500, 'Error cargando clasificador');
10 - .schema('ppto') 9 + const clasificador = await clasRes.json();
11 - .from('entidad_resumen')
12 - .select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking')
13 - .eq('codigo', codigo),
14 10
11 + const match = clasificador.find(c => String(c.municipio_ubigeo) === municipioUbigeo);
12 + if (!match) throw error(404, 'Ubicación no encontrada');
13 +
14 + const ubigeoCode = match.ubigeo; // formato "2.7.7"
15 +
16 + // Cargar detalle del ubigeo + población en paralelo
17 + const [ubigeoRes, pobRes] = await Promise.all([
18 + fetch(`/api/ubigeo-data?codigo=${ubigeoCode}&tipo=detalle`).then(r => r.ok ? r.json() : null),
15 fetch('/poblacion.csv').then(r => r.text()) 19 fetch('/poblacion.csv').then(r => r.text())
16 ]); 20 ]);
17 21
18 - if (resumenRes.error || !resumenRes.data?.length) { 22 + if (!ubigeoRes) throw error(404, 'Ubicación no encontrada');
19 - throw error(404, 'Ubicación no encontrada');
20 - }
21 23
22 - // Parsear población para este código (codigo_ine) 24 + // Población
23 const poblacionMap = {}; 25 const poblacionMap = {};
24 pobRes.split('\n').slice(1).forEach(line => { 26 pobRes.split('\n').slice(1).forEach(line => {
25 const [cod, , gestion, pob] = line.split(','); 27 const [cod, , gestion, pob] = line.split(',');
26 - if (cod === codigo) { 28 + if (cod === municipioUbigeo) {
27 poblacionMap[parseInt(gestion)] = parseInt(pob); 29 poblacionMap[parseInt(gestion)] = parseInt(pob);
28 } 30 }
29 }); 31 });
30 32
31 - // Determinar última gestión 33 + // gastos_ingresos ��� resumenData
32 - const gestiones = [...new Set(resumenRes.data.map(d => d.gestion))].sort((a, b) => b - a); 34 + const gastosIngresos = ubigeoRes.gastos_ingresos || [];
33 - const ultimaGestion = gestiones[0] || 2025; 35 + const resumenData = gastosIngresos.map(d => ({
36 + tipo: d.tipo,
37 + gestion: d.gestion,
38 + devengado: d.devengado,
39 + ranking: d.ranking,
40 + codigo: municipioUbigeo,
41 + desc: ubigeoRes.desc_ubigeo
42 + }));
34 43
35 - // Cargar distribuciones de la última gestión 44 + // Última gestión
36 - const distRes = await supabase 45 + const gestiones = [...new Set(gastosIngresos.map(d => d.gestion))].filter(g => g <= 2025).sort((a, b) => b - a);
37 - .schema('ppto') 46 + const ultimaGestion = gestiones[0] || 2025;
38 - .from('entidad_distribuciones')
39 - .select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado')
40 - .eq('codigo', codigo)
41 - .eq('gestion', ultimaGestion);
42 47
43 - const firstRow = resumenRes.data[0]; 48 + // Distribuciones de la última gestión
49 + const distEndpoints = [
50 + { api: 'objetos', dim: 'objeto' },
51 + { api: 'finfuns', dim: 'finfun' },
52 + { api: 'actecos', dim: 'acteco' }
53 + ];
54 + const distResults = await Promise.all(
55 + distEndpoints.map(async ({ api, dim }) => {
56 + try {
57 + const res = await fetch(`/api/ubigeo-data?codigo=${ubigeoCode}&tipo=${api}`);
58 + if (!res.ok) return [];
59 + const data = await res.json();
60 + if (!Array.isArray(data)) return [];
61 + return data
62 + .filter(d => d.gestion === ultimaGestion)
63 + .map(d => ({
64 + tipo: d.tipo || 'gastos',
65 + dimension: dim,
66 + gestion: d.gestion,
67 + padre: d.padre,
68 + desc_padre: d.desc_padre,
69 + hijo: d.hijo,
70 + desc_hijo: d.desc_hijo,
71 + devengado: d.devengado
72 + }));
73 + } catch { return []; }
74 + })
75 + );
44 76
45 return { 77 return {
46 - nombre: firstRow.desc || `Ubicación ${codigo}`, 78 + nombre: ubigeoRes.desc_ubigeo || `Ubicación ${municipioUbigeo}`,
47 - nombrePadre: firstRow.desc_padre || null, 79 + nombrePadre: ubigeoRes.desc_departamento || null,
48 - codigo, 80 + codigo: municipioUbigeo,
49 - resumenData: resumenRes.data || [], 81 + ubigeoCode,
50 - distribucionesData: distRes.data || [], 82 + resumenData,
83 + distribucionesData: distResults.flat(),
51 gestionInicial: ultimaGestion, 84 gestionInicial: ultimaGestion,
52 poblacionMap 85 poblacionMap
53 }; 86 };
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
3 import { page } from '$app/stores'; 3 import { page } from '$app/stores';
4 import { get } from 'svelte/store'; 4 import { get } from 'svelte/store';
5 import * as d3 from 'd3'; 5 import * as d3 from 'd3';
6 - import { supabase } from '$lib/supabase';
7 import { mapaCache } from '$lib/stores/mapaCache'; 6 import { mapaCache } from '$lib/stores/mapaCache';
8 7
9 let { data } = $props(); 8 let { data } = $props();
...@@ -107,15 +106,41 @@ ...@@ -107,15 +106,41 @@
107 return; 106 return;
108 } 107 }
109 cargandoDist = true; 108 cargandoDist = true;
110 - const { data: rows } = await supabase 109 + const ubigeoCode = data.ubigeoCode;
111 - .schema('ppto') 110 + const dims = [
112 - .from('entidad_distribuciones') 111 + { api: 'objetos', dim: 'objeto' },
113 - .select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado') 112 + { api: 'finfuns', dim: 'finfun' },
114 - .eq('codigo', codigoSeleccionado) 113 + { api: 'actecos', dim: 'acteco' }
115 - .eq('gestion', gestion); 114 + ];
116 - const result = rows || []; 115 + try {
117 - distCache[gestion] = result; 116 + const results = await Promise.all(
118 - distribucionesData = result; 117 + dims.map(async ({ api, dim }) => {
118 + try {
119 + const res = await fetch(`/api/ubigeo-data?codigo=${ubigeoCode}&tipo=${api}`);
120 + if (!res.ok) return [];
121 + const rows = await res.json();
122 + if (!Array.isArray(rows)) return [];
123 + return rows
124 + .filter(d => d.gestion === gestion)
125 + .map(d => ({
126 + tipo: d.tipo || 'gastos',
127 + dimension: dim,
128 + gestion: d.gestion,
129 + padre: d.padre,
130 + desc_padre: d.desc_padre,
131 + hijo: d.hijo,
132 + desc_hijo: d.desc_hijo,
133 + devengado: d.devengado
134 + }));
135 + } catch { return []; }
136 + })
137 + );
138 + const result = results.flat();
139 + distCache[gestion] = result;
140 + distribucionesData = result;
141 + } catch {
142 + distribucionesData = [];
143 + }
119 cargandoDist = false; 144 cargandoDist = false;
120 } 145 }
121 146
......
This diff could not be displayed because it is too large.