Showing
53 changed files
with
1577 additions
and
427 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.
src/routes/acteco/[codigo]/+page.server.js
0 → 100644
| 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 | +} |
src/routes/acteco/[codigo]/+page.svelte
0 → 100644
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 | +} |
src/routes/api/acteco-data/+server.js
0 → 100644
| 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 | +} |
src/routes/api/acteco-treemap/+server.js
0 → 100644
| 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 | } | ... | ... |
src/routes/api/entidad-data/+server.js
0 → 100644
| 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 | +} |
src/routes/api/finfun-data/+server.js
0 → 100644
| 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 | +} |
src/routes/api/finfun-treemap/+server.js
0 → 100644
| 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,10 +6,13 @@ export async function GET() { | ... | @@ -6,10 +6,13 @@ 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 | + .filter(d => d.entidad !== 0) | ||
| 11 | + .map(d => ({ | ||
| 10 | entidad: d.entidad, | 12 | entidad: d.entidad, |
| 11 | desc_entidad: d.desc_entidad, | 13 | desc_entidad: d.desc_entidad, |
| 12 | - sigla_entidad: d.sigla_entidad || '' | 14 | + sigla_entidad: d.sigla_entidad || '', |
| 15 | + gestiones: d.gestiones || '' | ||
| 13 | })); | 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' } | ... | ... |
| 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 | +} |
src/routes/api/organismo-data/+server.js
0 → 100644
| 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 | +} |
src/routes/api/organismo-treemap/+server.js
0 → 100644
| 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 | +} |
src/routes/api/rubro-clasificador/+server.js
0 → 100644
| 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 | +} |
src/routes/api/rubro-data/+server.js
0 → 100644
| 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 | +} |
src/routes/api/rubro-treemap/+server.js
0 → 100644
| 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 | +} |
src/routes/api/rubro-ubigeos/+server.js
0 → 100644
| 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 | +} |
src/routes/api/ubigeo-data/+server.js
0 → 100644
| 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 | +} |
src/routes/clasificadores/funcional/+page.js
0 → 100644
| 1 | +export const ssr = false; |
This diff could not be displayed because it is too large.
| 1 | +export const ssr = false; |
| ... | @@ -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} | ... | ... |
| 1 | +export const ssr = false; |
This diff could not be displayed because it is too large.
src/routes/clasificadores/rubros/+page.js
0 → 100644
| 1 | +export const ssr = false; |
This diff could not be displayed because it is too large.
src/routes/clasificadores/sectores/+page.js
0 → 100644
| 1 | +export const ssr = false; |
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' } |
| 46 | + ]; | ||
| 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(); | ||
| 47 | distCache[gestion] = result; | 71 | distCache[gestion] = result; |
| 48 | distribucionesData = 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 { |
| 39 | + const clasRes = await fetch(`${API_BASE}/clasificador`); | ||
| 40 | + if (clasRes.ok) { | ||
| 41 | + const clasificador = await clasRes.json(); | ||
| 67 | 42 | ||
| 68 | - // Buscar padres según nivel | 43 | + // Padres |
| 69 | if (nivel === 'funcion') { | 44 | if (nivel === 'funcion') { |
| 70 | - // Padres: finalidad y grupo función | ||
| 71 | const finalidadCode = getFinalidadCode(codigo); | 45 | const finalidadCode = getFinalidadCode(codigo); |
| 72 | - const grpFuncionCode = getGrpFuncionCode(codigo); | 46 | + const grpCode = getGrpFuncionCode(codigo); |
| 73 | - | 47 | + padres = clasificador.filter(c => c.finfun === finalidadCode || c.finfun === grpCode); |
| 74 | - const { data: padresData } = await supabase | ||
| 75 | - .schema('ppto') | ||
| 76 | - .from('clas_finfun') | ||
| 77 | - .select('*') | ||
| 78 | - .in('finfun', [finalidadCode, grpFuncionCode]) | ||
| 79 | - .order('finfun'); | ||
| 80 | - | ||
| 81 | - if (padresData) padres = padresData; | ||
| 82 | } else if (nivel === 'grpfuncion') { | 48 | } else if (nivel === 'grpfuncion') { |
| 83 | - // Padre: finalidad | ||
| 84 | const finalidadCode = getFinalidadCode(codigo); | 49 | const finalidadCode = getFinalidadCode(codigo); |
| 85 | - | 50 | + padres = clasificador.filter(c => c.finfun === finalidadCode); |
| 86 | - const { data: padresData } = await supabase | ||
| 87 | - .schema('ppto') | ||
| 88 | - .from('clas_finfun') | ||
| 89 | - .select('*') | ||
| 90 | - .eq('finfun', finalidadCode); | ||
| 91 | - | ||
| 92 | - if (padresData) padres = padresData; | ||
| 93 | } | 51 | } |
| 94 | - // finalidad no tiene padres | ||
| 95 | 52 | ||
| 96 | - console.timeEnd('[SERVER] Query padres'); | 53 | + // Hijos |
| 97 | - | ||
| 98 | - console.time('[SERVER] Query hijos'); | ||
| 99 | - | ||
| 100 | - // Buscar hijos según nivel | ||
| 101 | if (nivel === 'finalidad') { | 54 | if (nivel === 'finalidad') { |
| 102 | - // Hijos: grupos de función de esta finalidad | 55 | + hijos = clasificador.filter(c => |
| 103 | - const { data: hijosData } = await supabase | 56 | + c.nivel === 'grpfuncion' && getFinalidadCode(c.finfun) === codigo && c.finfun !== codigo |
| 104 | - .schema('ppto') | 57 | + ); |
| 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') { | 58 | } else if (nivel === 'grpfuncion') { |
| 120 | - // Hijos: funciones de este grupo | 59 | + hijos = clasificador.filter(c => |
| 121 | - const grpCode = getGrpFuncionCode(codigo); | 60 | + c.nivel === 'funcion' && getGrpFuncionCode(c.finfun) === codigo |
| 122 | - const { data: hijosData } = await supabase | 61 | + ); |
| 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 | } | 62 | } |
| 136 | - } | ||
| 137 | - // funcion no tiene hijos | ||
| 138 | 63 | ||
| 139 | - console.timeEnd('[SERVER] Query hijos'); | 64 | + padres.sort((a, b) => a.finfun.localeCompare(b.finfun)); |
| 65 | + hijos.sort((a, b) => a.finfun.localeCompare(b.finfun)); | ||
| 66 | + } | ||
| 67 | + } catch { /* clasificador optional */ } | ||
| 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 | + const clasificador = await clasRes.json(); | ||
| 19 | + const current = clasificador.find(c => c.organismo === parseInt(codigo)); | ||
| 20 | + if (current) { | ||
| 21 | + // Padres: grupo y subgrupo (como objetos virtuales para navegación) | ||
| 22 | + padres = [ | ||
| 23 | + { organismo: `grupo_${current.organismo_grupo}`, desc_organismo: current.desc_organismo_grupo, nivel: 'grupo', sigla: current.sigla_organismo_grupo }, | ||
| 24 | + { organismo: `subgrupo_${current.organismo_grupo}_${current.organismo_subgrupo}`, desc_organismo: current.desc_organismo_subgrupo, nivel: 'subgrupo', sigla: current.sigla_organismo_subgrupo } | ||
| 25 | + ]; | ||
| 26 | + | ||
| 27 | + // Hijos: otros organismos del mismo subgrupo | ||
| 28 | + hijos = clasificador.filter(c => | ||
| 29 | + c.organismo_grupo === current.organismo_grupo && | ||
| 30 | + c.organismo_subgrupo === current.organismo_subgrupo && | ||
| 31 | + c.organismo !== current.organismo && | ||
| 32 | + c.organismo !== 0 | ||
| 33 | + ).sort((a, b) => (a.desc_organismo || '').localeCompare(b.desc_organismo || '')); | ||
| 18 | } | 34 | } |
| 19 | - | ||
| 20 | - const organismo = data[0]; | ||
| 21 | - | ||
| 22 | - // Obtener hermanos (otros organismos del mismo subgrupo) | ||
| 23 | - let hermanos = []; | ||
| 24 | - | ||
| 25 | - console.time('[SERVER] Query hermanos'); | ||
| 26 | - const { data: hermanosData } = await supabase | ||
| 27 | - .schema('ppto') | ||
| 28 | - .from('clas_organismos') | ||
| 29 | - .select('*') | ||
| 30 | - .eq('organismo_grupo', organismo.organismo_grupo) | ||
| 31 | - .eq('organismo_sub_grupo', organismo.organismo_sub_grupo) | ||
| 32 | - .neq('organismo', codigo) | ||
| 33 | - .order('organismo'); | ||
| 34 | - | ||
| 35 | - if (hermanosData) { | ||
| 36 | - hermanos = hermanosData; | ||
| 37 | } | 35 | } |
| 38 | - console.timeEnd('[SERVER] Query hermanos'); | 36 | + } catch { /* optional */ } |
| 39 | 37 | ||
| 40 | - console.timeEnd('[SERVER] Total load organismo'); | 38 | + return { organismo, padres, hijos }; |
| 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 | 11 | ||
| 44 | - if (dbError || !data || data.length === 0) { | 12 | + const nivel = rubro.nivel; |
| 45 | - throw error(404, 'Rubro no encontrado'); | ||
| 46 | - } | ||
| 47 | - | ||
| 48 | - const rubro = data[0]; | ||
| 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') | 37 | + }); |
| 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 | 38 | ||
| 108 | - if (hijosData) { | 39 | + // Hijos directos |
| 109 | - // Filtrar solo hijos directos (siguiente nivel) | ||
| 110 | - hijos = hijosData.filter(h => { | ||
| 111 | if (nivel === 'tipo') { | 40 | if (nivel === 'tipo') { |
| 112 | - // Hijos de tipo son clase: XXX00 donde XX = tipo | 41 | + hijos = clasificador.filter(c => c.nivel === 'clase' && String(c.rubro).padStart(5, '0').substring(0, 2) === s.substring(0, 2)); |
| 113 | - return h.rubro.startsWith(codigo.substring(0, 2)) && h.nivel === 'clase'; | ||
| 114 | } else if (nivel === 'clase') { | 42 | } else if (nivel === 'clase') { |
| 115 | - // Hijos de clase son cuenta: XXXX0 donde XXX = clase | 43 | + hijos = clasificador.filter(c => (c.nivel === 'cuenta') && String(c.rubro).padStart(5, '0').substring(0, 3) === s.substring(0, 3)); |
| 116 | - return h.rubro.startsWith(codigo.substring(0, 3)) && h.nivel === 'cuenta'; | ||
| 117 | } else if (nivel === 'cuenta') { | 44 | } else if (nivel === 'cuenta') { |
| 118 | - // Hijos de cuenta son sub_cuenta: XXXXX donde XXXX = cuenta | 45 | + hijos = clasificador.filter(c => (c.nivel === 'sub_cuenta') && String(c.rubro).padStart(5, '0').substring(0, 4) === s.substring(0, 4)); |
| 119 | - return h.rubro.startsWith(codigo.substring(0, 4)) && h.nivel === 'sub_cuenta'; | ||
| 120 | - } | ||
| 121 | - return false; | ||
| 122 | - }); | ||
| 123 | } | 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))); | ||
| 124 | } | 50 | } |
| 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 { |
| 116 | + const results = await Promise.all( | ||
| 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(); | ||
| 117 | distCache[gestion] = result; | 139 | distCache[gestion] = result; |
| 118 | distribucionesData = 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.
-
Please register or login to post a comment