Rafael Lopez

final

Showing 43 changed files with 1156 additions and 1205 deletions
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 "version": "0.0.1", 4 "version": "0.0.1",
5 "type": "module", 5 "type": "module",
6 "scripts": { 6 "scripts": {
7 - "dev": "vite dev", 7 + "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 vite dev",
8 "build": "vite build", 8 "build": "vite build",
9 "preview": "vite preview", 9 "preview": "vite preview",
10 "prepare": "svelte-kit sync || echo ''" 10 "prepare": "svelte-kit sync || echo ''"
......
1 +// ═══════════════════════════════════════════════════════════════
2 +// CONFIGURACIÓN CENTRAL DE API
3 +// Cambiar la IP aquí actualiza todas las llamadas del proyecto
4 +// ═══════════════════════════════════════════════════════════════
5 +
6 +export const API_HOST = 'https://abierto.economiayfinanzas.gob.bo/presupuesto';
7 +export const DOWNLOAD_BASE = 'https://abierto.economiayfinanzas.gob.bo/presupuesto/descargas/';
1 <script> 1 <script>
2 import { onMount } from 'svelte'; 2 import { onMount } from 'svelte';
3 import { landingSearchMode, landingSelectedClassifiers, landingSearchQuery } from '$lib/stores/landingSearchState'; 3 import { landingSearchMode, landingSelectedClassifiers, landingSearchQuery } from '$lib/stores/landingSearchState';
4 + import { openDrawer } from '$lib/stores/drawer.js';
4 5
5 // Theme 6 // Theme
6 let isDark = true; 7 let isDark = true;
...@@ -971,7 +972,7 @@ ...@@ -971,7 +972,7 @@
971 $: isSearching = searchVal.length >= 3; 972 $: isSearching = searchVal.length >= 3;
972 </script> 973 </script>
973 974
974 -<svelte:window on:keydown={handleGlobalKeydown} /> 975 +<svelte:window onkeydown={handleGlobalKeydown} />
975 976
976 <svelte:head> 977 <svelte:head>
977 <title>Buscador | Presupuesto Abierto</title> 978 <title>Buscador | Presupuesto Abierto</title>
...@@ -980,18 +981,6 @@ ...@@ -980,18 +981,6 @@
980 <div class="page" class:mounted class:page-locked={allHits.length > 0}> 981 <div class="page" class:mounted class:page-locked={allHits.length > 0}>
981 982
982 <!-- Dots de navegación --> 983 <!-- Dots de navegación -->
983 - <nav class="section-dots" class:section-dots-hidden={currentSection === 'hero'}>
984 - {#each SECTIONS_NAV as s}
985 - <button
986 - class="section-dot"
987 - class:section-dot-active={currentSection === s.id}
988 - on:click={() => scrollToSection(s.id)}
989 - aria-label={s.label}
990 - >
991 - <span class="section-dot-label">{s.label}</span>
992 - </button>
993 - {/each}
994 - </nav>
995 984
996 <!-- Hero --> 985 <!-- Hero -->
997 <main data-section="hero"> 986 <main data-section="hero">
...@@ -1006,7 +995,7 @@ ...@@ -1006,7 +995,7 @@
1006 </div> 995 </div>
1007 996
1008 <div class="hero-topbar-right" class:nav-hidden={!heroInView}> 997 <div class="hero-topbar-right" class:nav-hidden={!heroInView}>
1009 - <button class="nav-theme-btn" on:click={toggleTheme} aria-label={isDark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}> 998 + <button class="nav-theme-btn" onclick={toggleTheme} aria-label={isDark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}>
1010 {#if isDark} 999 {#if isDark}
1011 <svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20"> 1000 <svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20">
1012 <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/> 1001 <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
...@@ -1017,7 +1006,7 @@ ...@@ -1017,7 +1006,7 @@
1017 </svg> 1006 </svg>
1018 {/if} 1007 {/if}
1019 </button> 1008 </button>
1020 - <button class="nav-menu-btn" aria-label="Menú"> 1009 + <button class="nav-menu-btn" aria-label="Menú" onclick={openDrawer}>
1021 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"> 1010 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
1022 <line x1="3" y1="6" x2="21" y2="6"/> 1011 <line x1="3" y1="6" x2="21" y2="6"/>
1023 <line x1="3" y1="12" x2="21" y2="12"/> 1012 <line x1="3" y1="12" x2="21" y2="12"/>
...@@ -1037,7 +1026,7 @@ ...@@ -1037,7 +1026,7 @@
1037 Presupuesto <span class="gold">Abierto</span> 1026 Presupuesto <span class="gold">Abierto</span>
1038 </h1> 1027 </h1>
1039 <p class="hero-sub"> 1028 <p class="hero-sub">
1040 - Información histórica y diaria de todo el Estado Boliviano. 1029 + Información histórica de todo el Estado Boliviano.
1041 </p> 1030 </p>
1042 1031
1043 <!-- Buscador --> 1032 <!-- Buscador -->
...@@ -1053,10 +1042,10 @@ ...@@ -1053,10 +1042,10 @@
1053 type="text" 1042 type="text"
1054 bind:this={searchInput} 1043 bind:this={searchInput}
1055 bind:value={searchVal} 1044 bind:value={searchVal}
1056 - on:input={handleInput} 1045 + oninput={handleInput}
1057 - on:keydown={handleKeydown} 1046 + onkeydown={handleKeydown}
1058 - on:focus={() => searchFocused = true} 1047 + onfocus={() => searchFocused = true}
1059 - on:blur={() => searchFocused = false} 1048 + onblur={() => searchFocused = false}
1060 placeholder="" 1049 placeholder=""
1061 class:has-text={searchVal.length > 0} 1050 class:has-text={searchVal.length > 0}
1062 /> 1051 />
...@@ -1067,12 +1056,12 @@ ...@@ -1067,12 +1056,12 @@
1067 </div> 1056 </div>
1068 {/if} 1057 {/if}
1069 {#if searchVal && !isLoading} 1058 {#if searchVal && !isLoading}
1070 - <button class="search-clear" on:click={clearSearch}> 1059 + <button class="search-clear" onclick={clearSearch}>
1071 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> 1060 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1072 <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> 1061 <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
1073 </svg> 1062 </svg>
1074 </button> 1063 </button>
1075 - <kbd class="search-esc" on:click={clearSearch}>Esc</kbd> 1064 + <kbd class="search-esc" onclick={clearSearch}>Esc</kbd>
1076 {/if} 1065 {/if}
1077 {#if isLoading} 1066 {#if isLoading}
1078 <div class="search-loading"><span class="loading-dot"></span></div> 1067 <div class="search-loading"><span class="loading-dot"></span></div>
...@@ -1087,7 +1076,7 @@ ...@@ -1087,7 +1076,7 @@
1087 <button 1076 <button
1088 class="cat-filter-chip" 1077 class="cat-filter-chip"
1089 class:cat-filter-hidden={hiddenCategories.has(cat.id)} 1078 class:cat-filter-hidden={hiddenCategories.has(cat.id)}
1090 - on:click={() => toggleCategory(cat.id)} 1079 + onclick={() => toggleCategory(cat.id)}
1091 style="--cat-color:{cat.color}" 1080 style="--cat-color:{cat.color}"
1092 > 1081 >
1093 <span class="cat-filter-dot"></span> 1082 <span class="cat-filter-dot"></span>
...@@ -1110,8 +1099,8 @@ ...@@ -1110,8 +1099,8 @@
1110 <div 1099 <div
1111 class="mode-card" 1100 class="mode-card"
1112 class:mode-card-active={searchMode === 'programas'} 1101 class:mode-card-active={searchMode === 'programas'}
1113 - on:click={() => setMode('programas')} 1102 + onclick={() => setMode('programas')}
1114 - on:keydown={(e) => (e.key === 'Enter' || e.key === ' ') && setMode('programas')} 1103 + onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && setMode('programas')}
1115 role="button" 1104 role="button"
1116 tabindex="0" 1105 tabindex="0"
1117 > 1106 >
...@@ -1134,8 +1123,8 @@ ...@@ -1134,8 +1123,8 @@
1134 {#each EXAMPLES_PROGRAMAS as ex} 1123 {#each EXAMPLES_PROGRAMAS as ex}
1135 <span 1124 <span
1136 class="mode-pill" 1125 class="mode-pill"
1137 - on:click|stopPropagation={() => searchFromPill(ex.label)} 1126 + onclick={() => searchFromPill(ex.label)}
1138 - on:keydown={(e) => e.key === 'Enter' && searchFromPill(ex.label)} 1127 + onkeydown={(e) => e.key === 'Enter' && searchFromPill(ex.label)}
1139 role="button" 1128 role="button"
1140 tabindex="0" 1129 tabindex="0"
1141 > 1130 >
...@@ -1152,8 +1141,8 @@ ...@@ -1152,8 +1141,8 @@
1152 <div 1141 <div
1153 class="mode-card" 1142 class="mode-card"
1154 class:mode-card-active={searchMode === 'clasificadores'} 1143 class:mode-card-active={searchMode === 'clasificadores'}
1155 - on:click={() => setMode('clasificadores')} 1144 + onclick={() => setMode('clasificadores')}
1156 - on:keydown={(e) => (e.key === 'Enter' || e.key === ' ') && setMode('clasificadores')} 1145 + onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && setMode('clasificadores')}
1157 role="button" 1146 role="button"
1158 tabindex="0" 1147 tabindex="0"
1159 > 1148 >
...@@ -1176,7 +1165,7 @@ ...@@ -1176,7 +1165,7 @@
1176 class="clf-chip" 1165 class="clf-chip"
1177 class:clf-chip-active={selectedClassifiers.includes(clf.id)} 1166 class:clf-chip-active={selectedClassifiers.includes(clf.id)}
1178 class:clf-chip-disabled={clf.disabled} 1167 class:clf-chip-disabled={clf.disabled}
1179 - on:click|stopPropagation={() => { if (!clf.disabled) toggleClassifier(clf.id); }} 1168 + onclick={() => { if (!clf.disabled) toggleClassifier(clf.id); }}
1180 disabled={clf.disabled} 1169 disabled={clf.disabled}
1181 > 1170 >
1182 <span class="clf-dot"></span> 1171 <span class="clf-dot"></span>
...@@ -1191,8 +1180,8 @@ ...@@ -1191,8 +1180,8 @@
1191 {:else} 1180 {:else}
1192 <span 1181 <span
1193 class="mode-pill" 1182 class="mode-pill"
1194 - on:click|stopPropagation={() => searchFromPill(ex.label, ex.classifier)} 1183 + onclick={() => searchFromPill(ex.label, ex.classifier)}
1195 - on:keydown={(e) => e.key === 'Enter' && searchFromPill(ex.label, ex.classifier)} 1184 + onkeydown={(e) => e.key === 'Enter' && searchFromPill(ex.label, ex.classifier)}
1196 role="button" 1185 role="button"
1197 tabindex="0" 1186 tabindex="0"
1198 > 1187 >
...@@ -1207,26 +1196,6 @@ ...@@ -1207,26 +1196,6 @@
1207 </div> 1196 </div>
1208 </div> 1197 </div>
1209 1198
1210 - <!-- Sección: Ir a -->
1211 - <span class="section-label section-label-goto">Ir a</span>
1212 - <a href="/vista-diaria" class="daily-card">
1213 - <div class="daily-card-left">
1214 - <span class="daily-dot"></span>
1215 - <span class="daily-card-title">Vista diaria</span>
1216 - </div>
1217 - <div class="daily-card-content">
1218 - {#if !dailyLoading && dailyData}
1219 - <span class="daily-card-text">Ayer el Estado ejecutó <strong>{formatMonto(dailyData.monto)}</strong></span>
1220 - {:else if !dailyLoading}
1221 - <span class="daily-card-text">Ver el resumen de ejecución de ayer</span>
1222 - {:else}
1223 - <span class="daily-card-text">Cargando datos...</span>
1224 - {/if}
1225 - </div>
1226 - <svg class="daily-card-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1227 - <path d="M5 12h14M12 5l7 7-7 7"/>
1228 - </svg>
1229 - </a>
1230 </div> 1199 </div>
1231 1200
1232 <!-- Contenedor de resultados (overlay absoluto) --> 1201 <!-- Contenedor de resultados (overlay absoluto) -->
...@@ -1234,7 +1203,7 @@ ...@@ -1234,7 +1203,7 @@
1234 <div 1203 <div
1235 class="results-content results-content-filled" 1204 class="results-content results-content-filled"
1236 class:results-content-visible={allHits.length > 0 || (results && results.hits?.length === 0)} 1205 class:results-content-visible={allHits.length > 0 || (results && results.hits?.length === 0)}
1237 - on:mousedown|preventDefault 1206 + onmousedown={(e) => e.preventDefault()}
1238 > 1207 >
1239 {#if results && results.hits?.length === 0 && !isLoading} 1208 {#if results && results.hits?.length === 0 && !isLoading}
1240 <div class="results-message-inline"> 1209 <div class="results-message-inline">
...@@ -1247,7 +1216,7 @@ ...@@ -1247,7 +1216,7 @@
1247 <div class="search-context-row"> 1216 <div class="search-context-row">
1248 <span class="search-context-title">{searchContextText}</span> 1217 <span class="search-context-title">{searchContextText}</span>
1249 {#if searchMode === 'clasificadores' && selectedClassifiers.length > 0} 1218 {#if searchMode === 'clasificadores' && selectedClassifiers.length > 0}
1250 - <button class="search-explain-link" on:click|stopPropagation={openExplain}> 1219 + <button class="search-explain-link" onclick={openExplain}>
1251 ¿Qué busco? 1220 ¿Qué busco?
1252 </button> 1221 </button>
1253 {/if} 1222 {/if}
...@@ -1277,7 +1246,7 @@ ...@@ -1277,7 +1246,7 @@
1277 </div> 1246 </div>
1278 <div class="sort-buttons"> 1247 <div class="sort-buttons">
1279 {#if !stats.isClassifier} 1248 {#if !stats.isClassifier}
1280 - <button class="sort-btn" class:sort-btn-active={sortBy === 'alpha'} on:click={() => toggleSort('alpha')}> 1249 + <button class="sort-btn" class:sort-btn-active={sortBy === 'alpha'} onclick={() => toggleSort('alpha')}>
1281 A-Z 1250 A-Z
1282 {#if sortBy === 'alpha'} 1251 {#if sortBy === 'alpha'}
1283 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1252 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
...@@ -1287,7 +1256,7 @@ ...@@ -1287,7 +1256,7 @@
1287 {/if} 1256 {/if}
1288 </button> 1257 </button>
1289 {/if} 1258 {/if}
1290 - <button class="sort-btn" class:sort-btn-active={sortBy === 'amount'} on:click={() => toggleSort('amount')}> 1259 + <button class="sort-btn" class:sort-btn-active={sortBy === 'amount'} onclick={() => toggleSort('amount')}>
1291 Monto 1260 Monto
1292 {#if sortBy === 'amount'} 1261 {#if sortBy === 'amount'}
1293 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1262 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
...@@ -1297,7 +1266,7 @@ ...@@ -1297,7 +1266,7 @@
1297 {/if} 1266 {/if}
1298 </button> 1267 </button>
1299 {#if groupedHits} 1268 {#if groupedHits}
1300 - <button class="sort-btn" class:sort-btn-active={sortBy === 'alpha'} on:click={() => toggleSort('alpha')}> 1269 + <button class="sort-btn" class:sort-btn-active={sortBy === 'alpha'} onclick={() => toggleSort('alpha')}>
1301 A-Z 1270 A-Z
1302 {#if sortBy === 'alpha'} 1271 {#if sortBy === 'alpha'}
1303 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1272 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
...@@ -1320,7 +1289,7 @@ ...@@ -1320,7 +1289,7 @@
1320 {@const isCollapsed = collapsedGroups.includes(gi)} 1289 {@const isCollapsed = collapsedGroups.includes(gi)}
1321 {@const maxVisible = isCollapsed ? 0 : group.hits.length} 1290 {@const maxVisible = isCollapsed ? 0 : group.hits.length}
1322 <div class="result-group"> 1291 <div class="result-group">
1323 - <button class="result-group-header" on:click={() => { 1292 + <button class="result-group-header" onclick={() => {
1324 if (isCollapsed) { collapsedGroups = collapsedGroups.filter(x => x !== gi); } 1293 if (isCollapsed) { collapsedGroups = collapsedGroups.filter(x => x !== gi); }
1325 else { collapsedGroups = [...collapsedGroups, gi]; } 1294 else { collapsedGroups = [...collapsedGroups, gi]; }
1326 }}> 1295 }}>
...@@ -1546,7 +1515,7 @@ ...@@ -1546,7 +1515,7 @@
1546 1515
1547 {#if hasMoreResults} 1516 {#if hasMoreResults}
1548 <div class="load-more-container"> 1517 <div class="load-more-container">
1549 - <button class="load-more-btn" on:click={loadMore} disabled={isLoadingMore}> 1518 + <button class="load-more-btn" onclick={loadMore} disabled={isLoadingMore}>
1550 {#if isLoadingMore} 1519 {#if isLoadingMore}
1551 <span class="loading-dot"></span> Cargando... 1520 <span class="loading-dot"></span> Cargando...
1552 {:else} 1521 {:else}
...@@ -1563,19 +1532,11 @@ ...@@ -1563,19 +1532,11 @@
1563 </div> 1532 </div>
1564 1533
1565 <!-- Scroll hint --> 1534 <!-- Scroll hint -->
1566 - <div class="scroll-hint anim" style="--d:800ms" class:scroll-hint-hidden={isSearching || allHits.length > 0}>
1567 - <svg class="scroll-icon-mouse" width="18" height="28" viewBox="0 0 18 28" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
1568 - <rect x="1" y="1" width="16" height="26" rx="8"/><line x1="9" y1="7" x2="9" y2="12" opacity="0.7"/>
1569 - </svg>
1570 - <svg class="scroll-icon-touch" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1571 - <path d="M6 9l6 6 6-6"/>
1572 - </svg>
1573 - <span>Explora la página</span>
1574 - </div>
1575 1535
1576 </main> 1536 </main>
1577 1537
1578 - <!-- ═══ PORTAL DIRECTORY ═══ --> 1538 + <!-- Secciones removidas - el landing es solo el buscador -->
1539 + <div style="display:none">
1579 <section class="directory" data-section="datos"> 1540 <section class="directory" data-section="datos">
1580 <div class="directory-inner"> 1541 <div class="directory-inner">
1581 <!-- Left: Title --> 1542 <!-- Left: Title -->
...@@ -1590,7 +1551,7 @@ ...@@ -1590,7 +1551,7 @@
1590 <div class="directory-right"> 1551 <div class="directory-right">
1591 <!-- Diario --> 1552 <!-- Diario -->
1592 <div use:reveal={{ id: 'door-d', delay: 60 }} class="reveal" class:revealed={revealStates['door-d']}> 1553 <div use:reveal={{ id: 'door-d', delay: 60 }} class="reveal" class:revealed={revealStates['door-d']}>
1593 - <div class="door door-warm" class:door-hovered={hDiario} on:mouseenter={() => hDiario = true} on:mouseleave={() => hDiario = false} role="button" tabindex="0"> 1554 + <div class="door door-warm" class:door-hovered={hDiario} onmouseenter={() => hDiario = true} onmouseleave={() => hDiario = false} role="button" tabindex="0">
1594 <div class="door-content"> 1555 <div class="door-content">
1595 <div class="door-header"> 1556 <div class="door-header">
1596 <div class="door-icon" style="opacity:{hDiario ? 1 : 0.4}"> 1557 <div class="door-icon" style="opacity:{hDiario ? 1 : 0.4}">
...@@ -1622,7 +1583,7 @@ ...@@ -1622,7 +1583,7 @@
1622 1583
1623 <!-- Archivo --> 1584 <!-- Archivo -->
1624 <div use:reveal={{ id: 'door-a', delay: 160 }} class="reveal" class:revealed={revealStates['door-a']}> 1585 <div use:reveal={{ id: 'door-a', delay: 160 }} class="reveal" class:revealed={revealStates['door-a']}>
1625 - <div class="door door-warm" class:door-hovered={hArchivo} on:mouseenter={() => hArchivo = true} on:mouseleave={() => hArchivo = false} role="button" tabindex="0"> 1586 + <div class="door door-warm" class:door-hovered={hArchivo} onmouseenter={() => hArchivo = true} onmouseleave={() => hArchivo = false} role="button" tabindex="0">
1626 <div class="door-content"> 1587 <div class="door-content">
1627 <div class="door-header"> 1588 <div class="door-header">
1628 <div class="door-icon" style="opacity:{hArchivo ? 1 : 0.4}"> 1589 <div class="door-icon" style="opacity:{hArchivo ? 1 : 0.4}">
...@@ -1678,7 +1639,7 @@ ...@@ -1678,7 +1639,7 @@
1678 <div class="clas-group-sub">La entidad que gasta o recibe</div> 1639 <div class="clas-group-sub">La entidad que gasta o recibe</div>
1679 </div> 1640 </div>
1680 </div> 1641 </div>
1681 - <a href="/clasificadores/institucional" class="widget-card widget-h" class:widget-hover={hWidget === 'quien'} on:mouseenter={() => hWidget = 'quien'} on:mouseleave={() => hWidget = null}> 1642 + <a href="/clasificadores/institucional" class="widget-card widget-h" class:widget-hover={hWidget === 'quien'} onmouseenter={() => hWidget = 'quien'} onmouseleave={() => hWidget = null}>
1682 <div class="widget-left"> 1643 <div class="widget-left">
1683 <div class="widget-headline"> 1644 <div class="widget-headline">
1684 <span class="widget-num-big">756</span> 1645 <span class="widget-num-big">756</span>
...@@ -1709,7 +1670,7 @@ ...@@ -1709,7 +1670,7 @@
1709 </div> 1670 </div>
1710 <div class="widgets-col"> 1671 <div class="widgets-col">
1711 {#each WIDGETS_INGRESOS as w} 1672 {#each WIDGETS_INGRESOS as w}
1712 - <a href={w.href} class="widget-card widget-grow" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}> 1673 + <a href={w.href} class="widget-card widget-grow" class:widget-hover={hWidget === w.id} onmouseenter={() => hWidget = w.id} onmouseleave={() => hWidget = null}>
1713 <div class="widget-headline"> 1674 <div class="widget-headline">
1714 <span class="widget-num-big widget-num-gold">{w.n}</span> 1675 <span class="widget-num-big widget-num-gold">{w.n}</span>
1715 <span class="widget-label">{w.label}</span> 1676 <span class="widget-label">{w.label}</span>
...@@ -1751,7 +1712,7 @@ ...@@ -1751,7 +1712,7 @@
1751 </div> 1712 </div>
1752 <div class="widgets-row"> 1713 <div class="widgets-row">
1753 {#each WIDGETS_GASTOS_QUE as w} 1714 {#each WIDGETS_GASTOS_QUE as w}
1754 - <a href={w.href} class="widget-card widget-sm" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}> 1715 + <a href={w.href} class="widget-card widget-sm" class:widget-hover={hWidget === w.id} onmouseenter={() => hWidget = w.id} onmouseleave={() => hWidget = null}>
1755 <div class="widget-headline"> 1716 <div class="widget-headline">
1756 <span class="widget-num-big widget-num-green">{w.n}</span> 1717 <span class="widget-num-big widget-num-green">{w.n}</span>
1757 <span class="widget-label">{w.label}</span> 1718 <span class="widget-label">{w.label}</span>
...@@ -1778,7 +1739,7 @@ ...@@ -1778,7 +1739,7 @@
1778 <div class="clas-subgroup-label">¿PARA QUÉ?</div> 1739 <div class="clas-subgroup-label">¿PARA QUÉ?</div>
1779 </div> 1740 </div>
1780 </div> 1741 </div>
1781 - <a href="/clasificadores/funcional" class="widget-card widget-full" class:widget-hover={hWidget === 'finalidad'} on:mouseenter={() => hWidget = 'finalidad'} on:mouseleave={() => hWidget = null}> 1742 + <a href="/clasificadores/funcional" class="widget-card widget-full" class:widget-hover={hWidget === 'finalidad'} onmouseenter={() => hWidget = 'finalidad'} onmouseleave={() => hWidget = null}>
1782 <div class="widget-headline"> 1743 <div class="widget-headline">
1783 <span class="widget-num-big widget-num-green">132</span> 1744 <span class="widget-num-big widget-num-green">132</span>
1784 <span class="widget-label">finalidades y funciones</span> 1745 <span class="widget-label">finalidades y funciones</span>
...@@ -1801,7 +1762,7 @@ ...@@ -1801,7 +1762,7 @@
1801 <div class="clas-subgroup-label">¿DÓNDE?</div> 1762 <div class="clas-subgroup-label">¿DÓNDE?</div>
1802 </div> 1763 </div>
1803 </div> 1764 </div>
1804 - <a href="/clasificadores/geografico" class="widget-card widget-full" class:widget-hover={hWidget === 'donde'} on:mouseenter={() => hWidget = 'donde'} on:mouseleave={() => hWidget = null}> 1765 + <a href="/clasificadores/geografico" class="widget-card widget-full" class:widget-hover={hWidget === 'donde'} onmouseenter={() => hWidget = 'donde'} onmouseleave={() => hWidget = null}>
1805 <div class="widget-headline"> 1766 <div class="widget-headline">
1806 <span class="widget-num-big widget-num-green">352</span> 1767 <span class="widget-num-big widget-num-green">352</span>
1807 <span class="widget-label">ubicaciones geográficas</span> 1768 <span class="widget-label">ubicaciones geográficas</span>
...@@ -1840,7 +1801,7 @@ ...@@ -1840,7 +1801,7 @@
1840 <div class="hist-grid"> 1801 <div class="hist-grid">
1841 {#each HISTORIAS as h, i} 1802 {#each HISTORIAS as h, i}
1842 <div use:reveal={{ id: `hist-${i}`, delay: 60 + i * 80 }} class="reveal hist-cell" class:revealed={revealStates[`hist-${i}`]} class:hist-tall={h.tall}> 1803 <div use:reveal={{ id: `hist-${i}`, delay: 60 + i * 80 }} class="reveal hist-cell" class:revealed={revealStates[`hist-${i}`]} class:hist-tall={h.tall}>
1843 - <div class="hist-card" class:hist-card-hovered={hHist === i} on:mouseenter={() => hHist = i} on:mouseleave={() => hHist = null} role="button" tabindex="0" style="--card-color:{h.color}"> 1804 + <div class="hist-card" class:hist-card-hovered={hHist === i} onmouseenter={() => hHist = i} onmouseleave={() => hHist = null} role="button" tabindex="0" style="--card-color:{h.color}">
1844 <div class="hist-accent" style="background:{h.color};transform:scaleX({hHist === i ? 1 : 0})"></div> 1805 <div class="hist-accent" style="background:{h.color};transform:scaleX({hHist === i ? 1 : 0})"></div>
1845 <div class="hist-content"> 1806 <div class="hist-content">
1846 <span class="hist-tag" style="color:{h.color}">{h.tag}</span> 1807 <span class="hist-tag" style="color:{h.color}">{h.tag}</span>
...@@ -1877,7 +1838,7 @@ ...@@ -1877,7 +1838,7 @@
1877 {#each VISUALIZACIONES as v, i} 1838 {#each VISUALIZACIONES as v, i}
1878 <div use:reveal={{ id: `vis-${i}`, delay: 60 + i * 80 }} class="reveal" class:revealed={revealStates[`vis-${i}`]}> 1839 <div use:reveal={{ id: `vis-${i}`, delay: 60 + i * 80 }} class="reveal" class:revealed={revealStates[`vis-${i}`]}>
1879 {#if v.href} 1840 {#if v.href}
1880 - <a href={v.href} class="vis-card vis-card-link" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null}> 1841 + <a href={v.href} class="vis-card vis-card-link" class:vis-card-hovered={hVis === i} onmouseenter={() => hVis = i} onmouseleave={() => hVis = null}>
1881 <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div> 1842 <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div>
1882 <div> 1843 <div>
1883 <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span> 1844 <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span>
...@@ -1889,7 +1850,7 @@ ...@@ -1889,7 +1850,7 @@
1889 </div> 1850 </div>
1890 </a> 1851 </a>
1891 {:else} 1852 {:else}
1892 - <div class="vis-card vis-card-disabled" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null} role="button" tabindex="0"> 1853 + <div class="vis-card vis-card-disabled" class:vis-card-hovered={hVis === i} onmouseenter={() => hVis = i} onmouseleave={() => hVis = null} role="button" tabindex="0">
1893 <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div> 1854 <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div>
1894 <div> 1855 <div>
1895 <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span> 1856 <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span>
...@@ -1936,11 +1897,14 @@ ...@@ -1936,11 +1897,14 @@
1936 </div> 1897 </div>
1937 </section> 1898 </section>
1938 1899
1900 + </div>
1901 + <!-- fin secciones ocultas -->
1902 +
1939 <!-- ═══ FOOTER ═══ --> 1903 <!-- ═══ FOOTER ═══ -->
1940 <footer> 1904 <footer>
1941 <div class="footer-inner"> 1905 <div class="footer-inner">
1942 <div class="footer-org">MINISTERIO DE ECONOMÍA Y FINANZAS PÚBLICAS</div> 1906 <div class="footer-org">MINISTERIO DE ECONOMÍA Y FINANZAS PÚBLICAS</div>
1943 - <div class="footer-right">DATOS ABIERTOS · 2005 — 2026</div> 1907 + <div class="footer-right">DATOS ABIERTOS · 2016 — 2025</div>
1944 </div> 1908 </div>
1945 </footer> 1909 </footer>
1946 </div> 1910 </div>
...@@ -1949,11 +1913,11 @@ ...@@ -1949,11 +1913,11 @@
1949 {#if showExplainModal} 1913 {#if showExplainModal}
1950 {@const activeContents = selectedClassifiers.map(id => EXPLAIN_CONTENT[id]).filter(Boolean)} 1914 {@const activeContents = selectedClassifiers.map(id => EXPLAIN_CONTENT[id]).filter(Boolean)}
1951 {@const isSingle = activeContents.length === 1} 1915 {@const isSingle = activeContents.length === 1}
1952 - <div class="explain-backdrop" on:click={() => { showExplainModal = false; }} role="dialog" aria-modal="true"> 1916 + <div class="explain-backdrop" onclick={() => { showExplainModal = false; }} role="dialog" aria-modal="true">
1953 - <div class="explain-modal" on:click|stopPropagation> 1917 + <div class="explain-modal" onclick={(e) => e.stopPropagation()}>
1954 <div class="explain-header"> 1918 <div class="explain-header">
1955 <h2 class="explain-title">{isSingle ? activeContents[0].titulo : '¿Qué busco?'}</h2> 1919 <h2 class="explain-title">{isSingle ? activeContents[0].titulo : '¿Qué busco?'}</h2>
1956 - <button class="explain-close" on:click={() => { showExplainModal = false; }}> 1920 + <button class="explain-close" onclick={() => { showExplainModal = false; }}>
1957 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1921 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1958 <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> 1922 <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
1959 </svg> 1923 </svg>
...@@ -1973,7 +1937,7 @@ ...@@ -1973,7 +1937,7 @@
1973 {/each} 1937 {/each}
1974 </div> 1938 </div>
1975 {#if content.link} 1939 {#if content.link}
1976 - <a href={content.link.href} class="explain-cta" on:click={() => { showExplainModal = false; }}> 1940 + <a href={content.link.href} class="explain-cta" onclick={() => { showExplainModal = false; }}>
1977 {content.link.texto} 1941 {content.link.texto}
1978 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1942 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1979 <path d="M5 12h14M12 5l7 7-7 7"/> 1943 <path d="M5 12h14M12 5l7 7-7 7"/>
...@@ -1997,7 +1961,7 @@ ...@@ -1997,7 +1961,7 @@
1997 {/each} 1961 {/each}
1998 </div> 1962 </div>
1999 {#if content.link} 1963 {#if content.link}
2000 - <a href={content.link.href} class="explain-cta" on:click={() => { showExplainModal = false; }}> 1964 + <a href={content.link.href} class="explain-cta" onclick={() => { showExplainModal = false; }}>
2001 {content.link.texto} 1965 {content.link.texto}
2002 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1966 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2003 <path d="M5 12h14M12 5l7 7-7 7"/> 1967 <path d="M5 12h14M12 5l7 7-7 7"/>
...@@ -2028,7 +1992,7 @@ ...@@ -2028,7 +1992,7 @@
2028 1992
2029 <style> 1993 <style>
2030 @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap'); 1994 @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap');
2031 - :global(*){margin:0;padding:0;box-sizing:border-box} 1995 + .page *{margin:0;padding:0;box-sizing:border-box}
2032 :global(html){width:100%;height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A;scroll-behavior:smooth;transition:background 0.3s} 1996 :global(html){width:100%;height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A;scroll-behavior:smooth;transition:background 0.3s}
2033 :global(html:not(.dark)){background:#ffffff} 1997 :global(html:not(.dark)){background:#ffffff}
2034 :global(body){width:100%;min-height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A;transition:background 0.3s} 1998 :global(body){width:100%;min-height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A;transition:background 0.3s}
...@@ -2037,7 +2001,7 @@ ...@@ -2037,7 +2001,7 @@
2037 :global(::-webkit-scrollbar){width:5px} 2001 :global(::-webkit-scrollbar){width:5px}
2038 :global(::-webkit-scrollbar-track){background:#1C1C1A} 2002 :global(::-webkit-scrollbar-track){background:#1C1C1A}
2039 :global(::-webkit-scrollbar-thumb){background:#444;border-radius:3px} 2003 :global(::-webkit-scrollbar-thumb){background:#444;border-radius:3px}
2040 - :global(a){color:inherit;text-decoration:none} 2004 + .page a{color:inherit;text-decoration:none}
2041 2005
2042 @keyframes pulse{0%,100%{opacity:0.35}50%{opacity:1}} 2006 @keyframes pulse{0%,100%{opacity:0.35}50%{opacity:1}}
2043 @keyframes blink{0%,100%{opacity:1}50%{opacity:0}} 2007 @keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
...@@ -2276,7 +2240,7 @@ ...@@ -2276,7 +2240,7 @@
2276 .pills-hint{font-family:var(--sans);font-size:10px;color:#7B7870;font-weight:500} 2240 .pills-hint{font-family:var(--sans);font-size:10px;color:#7B7870;font-weight:500}
2277 .hint-desktop{display:inline} 2241 .hint-desktop{display:inline}
2278 .hint-mobile{display:none} 2242 .hint-mobile{display:none}
2279 - .mode-pill{font-family:var(--sans);font-size:11px;font-weight:500;padding:5px 10px;border-radius:8px;cursor:pointer;transition:all 0.2s;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.06);color:#D0CCC4} 2243 + .mode-pill{font-family:var(--sans);font-size:11px;font-weight:500;padding:5px 10px;border-radius:8px;cursor:pointer;transition:all 0.2s;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.06);color:#F5F0E8}
2280 .mode-pill:hover{background:rgba(255,255,255,0.12);border-color:rgba(255,255,255,0.25);color:#F5F0E8} 2244 .mode-pill:hover{background:rgba(255,255,255,0.12);border-color:rgba(255,255,255,0.25);color:#F5F0E8}
2281 2245
2282 .mode-card-radio{position:absolute;top:16px;right:16px} 2246 .mode-card-radio{position:absolute;top:16px;right:16px}
......
1 import { error } from '@sveltejs/kit'; 1 import { error } from '@sveltejs/kit';
2 2
3 -const API_BASE = 'http://136.112.29.74/api/acteco'; 3 +import { API_HOST } from '$lib/api.js';
4 +const API_BASE = API_HOST + '/api/acteco';
4 5
5 // Jerarquía acteco: códigos con punto "1" → "1.1" → "1.1.1" 6 // 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 // Todos tienen nivel "actividad" en la API, derivamos la profundidad del código
......
1 -const API_BASE = 'http://136.112.29.74/api/acteco/clasificador'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/acteco/clasificador';
2 3
3 export async function GET() { 4 export async function GET() {
4 try { 5 try {
......
1 -const API_BASE = 'http://136.112.29.74/api/acteco'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/acteco';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/acteco'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/acteco';
2 3
3 let cachedClasificador = null; 4 let cachedClasificador = null;
4 let cachedEstados = null; 5 let cachedEstados = null;
......
1 -const API_BASE = 'http://136.112.29.74/api/acteco'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/acteco';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/diario'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/diario';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const tipo = url.searchParams.get('tipo') || 'status'; 5 const tipo = url.searchParams.get('tipo') || 'status';
......
1 -const API_BASE = 'http://136.112.29.74/api/entidad/clasificador'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/entidad/clasificador';
2 3
3 export async function GET() { 4 export async function GET() {
4 try { 5 try {
......
1 -const API_BASE = 'http://136.112.29.74/api/entidad'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/entidad';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/finfun/clasificador'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/finfun/clasificador';
2 3
3 export async function GET() { 4 export async function GET() {
4 try { 5 try {
5 const response = await fetch(API_BASE); 6 const response = await fetch(API_BASE);
6 - if (!response.ok) { 7 + if (!response.ok) throw new Error();
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(); 8 const data = await response.json();
12 return new Response(JSON.stringify(data), { 9 return new Response(JSON.stringify(data), {
13 headers: { 'Content-Type': 'application/json' } 10 headers: { 'Content-Type': 'application/json' }
......
1 -const API_BASE = 'http://136.112.29.74/api/finfun'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/finfun';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/finfun'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/finfun';
2 3
4 +// Cache en memoria
3 let cachedClasificador = null; 5 let cachedClasificador = null;
4 let cachedEstados = null; 6 let cachedEstados = null;
5 let cachedTimestamp = 0; 7 let cachedTimestamp = 0;
6 const CACHE_TTL = 5 * 60 * 1000; 8 const CACHE_TTL = 5 * 60 * 1000;
7 9
8 -function getParent(finfun) { 10 +function getParent(finfun, nivel) {
9 - const idx = finfun.lastIndexOf('.'); 11 + const code = String(finfun);
10 - if (idx === -1) return null; 12 + if (nivel === 'finalidad') return null;
11 - return finfun.substring(0, idx); 13 + if (nivel === 'grp_funcion') {
14 + return code.startsWith('10') ? '10' : code.charAt(0);
15 + }
16 + if (nivel === 'funcion') {
17 + if (code.startsWith('10')) return code.substring(0, 3);
18 + return code.substring(0, 2);
19 + }
20 + return null;
12 } 21 }
13 22
14 async function loadAndCacheAll() { 23 async function loadAndCacheAll() {
...@@ -58,7 +67,7 @@ export async function GET({ url }) { ...@@ -58,7 +67,7 @@ export async function GET({ url }) {
58 nivel: item.nivel, 67 nivel: item.nivel,
59 finfun: item.finfun, 68 finfun: item.finfun,
60 desc_finfun: item.desc_finfun, 69 desc_finfun: item.desc_finfun,
61 - parent: getParent(item.finfun), 70 + parent: getParent(item.finfun, item.nivel),
62 devengado: yearData.total 71 devengado: yearData.total
63 }); 72 });
64 } 73 }
...@@ -89,7 +98,7 @@ export async function GET({ url }) { ...@@ -89,7 +98,7 @@ export async function GET({ url }) {
89 nivel: item.nivel, 98 nivel: item.nivel,
90 finfun: item.finfun, 99 finfun: item.finfun,
91 desc_finfun: item.desc_finfun, 100 desc_finfun: item.desc_finfun,
92 - parent: getParent(item.finfun), 101 + parent: getParent(item.finfun, item.nivel),
93 devengado: yearData.monto 102 devengado: yearData.monto
94 }; 103 };
95 } catch { return null; } 104 } catch { return null; }
......
1 -const API_BASE = 'http://136.112.29.74/api/finfun'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/finfun';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/objeto/clasificador'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/objeto/clasificador';
2 3
3 export async function GET() { 4 export async function GET() {
4 try { 5 try {
......
1 -const API_BASE = 'http://136.112.29.74/api/objeto'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/objeto';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/entidad/clasificador'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/entidad/clasificador';
2 3
3 export async function GET() { 4 export async function GET() {
4 try { 5 try {
......
1 -const API_BASE = 'http://136.112.29.74/api/objeto'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/objeto';
2 3
3 // Cache en memoria del servidor 4 // Cache en memoria del servidor
4 let cachedClasificador = null; 5 let cachedClasificador = null;
......
1 -const API_BASE = 'http://136.112.29.74/api/objeto'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/objeto';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/organismo/clasificador'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/organismo/clasificador';
2 3
3 export async function GET() { 4 export async function GET() {
4 try { 5 try {
......
1 -const API_BASE = 'http://136.112.29.74/api/organismo'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/organismo';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/organismo'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/organismo';
2 3
3 let cachedClasificador = null; 4 let cachedClasificador = null;
4 let cachedEstados = null; 5 let cachedEstados = null;
......
1 -const API_BASE = 'http://136.112.29.74/api/programa_proyecto'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/programa_proyecto';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/rubro/clasificador'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/rubro/clasificador';
2 3
3 export async function GET() { 4 export async function GET() {
4 try { 5 try {
......
1 -const API_BASE = 'http://136.112.29.74/api/rubro'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/rubro';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 -const API_BASE = 'http://136.112.29.74/api/rubro'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/rubro';
2 3
3 let cachedClasificador = null; 4 let cachedClasificador = null;
4 let cachedEstados = null; 5 let cachedEstados = null;
......
1 -const API_BASE = 'http://136.112.29.74/api/rubro'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/rubro';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 // Proxy para evitar CORS 1 // Proxy para evitar CORS
2 -const API_BASE = 'http://136.112.29.74/api/search'; 2 +import { API_HOST } from '$lib/api.js';
3 +const API_BASE = API_HOST + '/api/search';
3 4
4 export async function GET({ url }) { 5 export async function GET({ url }) {
5 const q = url.searchParams.get('q') || ''; 6 const q = url.searchParams.get('q') || '';
...@@ -37,8 +38,8 @@ export async function GET({ url }) { ...@@ -37,8 +38,8 @@ export async function GET({ url }) {
37 headers: { 'Content-Type': 'application/json' } 38 headers: { 'Content-Type': 'application/json' }
38 }); 39 });
39 } catch (err) { 40 } catch (err) {
40 - console.error('Proxy error:', err); 41 + console.error('Proxy error:', err.message, 'URL:', apiUrl.toString());
41 - return new Response(JSON.stringify({ error: 'Failed to fetch from API' }), { 42 + return new Response(JSON.stringify({ error: 'Failed to fetch from API', detail: err.message, url: apiUrl.toString() }), {
42 status: 500, 43 status: 500,
43 headers: { 'Content-Type': 'application/json' } 44 headers: { 'Content-Type': 'application/json' }
44 }); 45 });
......
1 -const API_BASE = 'http://136.112.29.74/api/ubigeo/clasificador'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/ubigeo/clasificador';
2 3
3 export async function GET() { 4 export async function GET() {
4 try { 5 try {
......
1 -const API_BASE = 'http://136.112.29.74/api/ubigeo'; 1 +import { API_HOST } from '$lib/api.js';
2 +const API_BASE = API_HOST + '/api/ubigeo';
2 3
3 export async function GET({ url }) { 4 export async function GET({ url }) {
4 const codigo = url.searchParams.get('codigo') || ''; 5 const codigo = url.searchParams.get('codigo') || '';
......
1 <script> 1 <script>
2 import { onMount } from 'svelte'; 2 import { onMount } from 'svelte';
3 - import { fade, scale } from 'svelte/transition';
4 - import { cubicOut } from 'svelte/easing';
5 3
6 - let hovered = null; 4 + let mounted = $state(false);
7 - let activeModal = null; 5 + let hWidget = $state(null);
8 - let mounted = false;
9 6
10 - onMount(() => { setTimeout(() => { mounted = true; }, 80); }); 7 + onMount(() => { mounted = true; });
11 -
12 - const MODALS = {
13 - quien: {
14 - title: '¿Quién gasta o recibe?', tech: 'CLASIFICADOR INSTITUCIONAL',
15 - everyday: 'Es como preguntar "¿quién en la familia maneja plata?". Papá tiene su sueldo, mamá maneja el negocio, el abuelo cobra su renta. Cada uno es una "entidad" distinta con su propio presupuesto.',
16 - budget: 'El clasificador institucional cataloga a todas las entidades del sector público: ministerios, gobernaciones, municipios, empresas públicas, universidades. Cada una tiene un código único y un presupuesto asignado.',
17 - example: 'Ejemplo: El Ministerio de Educación (código 040) y el Gobierno Autónomo de La Paz (código 126) son entidades distintas, cada una con sus propios ingresos y gastos.',
18 - color: 'var(--theme-titulo)',
19 - },
20 - rubros: {
21 - title: '¿Qué tipo de ingreso es?', tech: 'CLASIFICADOR DE RECURSOS POR RUBROS',
22 - everyday: 'María gana un sueldo de su trabajo, cobra alquiler de un cuartito, y un amigo le prestó plata. Son tres tipos distintos de ingreso: laboral, patrimonial, y crédito.',
23 - budget: 'En el presupuesto, el Estado recibe ingresos tributarios (impuestos = el "sueldo"), regalías por explotación de recursos (= el "alquiler"), y créditos internacionales (= el "préstamo"). El rubro te dice qué naturaleza tiene cada ingreso.',
24 - example: 'Ejemplo: Los Bs 2.400M que Bolivia recibe por IDH son un rubro distinto a los Bs 800M de crédito del BID, aunque ambos financien al mismo ministerio.',
25 - color: 'var(--theme-accent)',
26 - },
27 - fuentes: {
28 - title: '¿Quién pone la plata?', tech: 'CLASIFICADOR DE FUENTES DE FINANCIAMIENTO',
29 - everyday: 'María sabe que su sueldo se lo paga la empresa TechCorp, el alquiler se lo paga su inquilino Juan, y el préstamo se lo hizo el Banco Nacional. Son las fuentes: quién firma el cheque.',
30 - budget: 'Las fuentes son los bolsillos específicos: el TGN (Tesoro General de la Nación), recursos propios de cada entidad, crédito externo del BID o Banco Mundial, o donaciones de cooperación.',
31 - example: 'Ejemplo: Un crédito del BID y uno del Banco Mundial son el mismo rubro (crédito externo) pero distinta fuente. La fuente dice quién exactamente pone la plata.',
32 - color: 'var(--theme-accent)',
33 - },
34 - objeto: {
35 - title: '¿En qué se gasta concretamente?', tech: 'CLASIFICADOR POR OBJETO DEL GASTO',
36 - everyday: 'Cuando María gasta, compra cosas concretas: arroz, paga la luz, le paga al albañil. Cada compra es un "objeto" distinto — algo tangible que se puede listar.',
37 - budget: 'El Objeto del Gasto es exactamente eso: sueldos, consultorías, materiales de escritorio, combustible, medicamentos. Es la lista del supermercado del Estado.',
38 - example: 'Ejemplo: El grupo 10000 "Servicios Personales" incluye sueldos y aguinaldos. El 20000 "Servicios No Personales" incluye consultorías y alquileres.',
39 - color: '#4A8B6E',
40 - },
41 - sector: {
42 - title: '¿A qué sector de la economía va?', tech: 'CLASIFICADOR POR SECTORES ECONÓMICOS',
43 - everyday: 'Si María contrata un albañil, esa plata va al sector construcción. Si compra medicina, va al sector salud. No importa qué compró — importa qué parte de la economía mueve.',
44 - budget: 'Este clasificador mide el impacto económico: cuánto va a infraestructura, cuánto a servicios, cuánto a industria. Permite evaluar cómo las decisiones fiscales afectan a cada sector.',
45 - example: 'Ejemplo: Un hospital nuevo es "construcción" en Objeto del Gasto pero "salud" en Sector Económico. Misma plata, dos lentes distintos.',
46 - color: '#4A8B6E',
47 - },
48 - finalidad: {
49 - title: '¿Para qué se gasta?', tech: 'CLASIFICADOR POR FINALIDAD Y FUNCIÓN',
50 - everyday: 'María compró arroz (objeto), pero ¿para qué? Para alimentar a su familia. Compró cemento: para tener casa. El "para qué" es el objetivo final, no el insumo.',
51 - budget: 'Este clasificador agrupa por propósito: educación, defensa, salud, protección social. No importa si se gastó en sueldos o en cemento — lo que importa es el fin.',
52 - example: 'Ejemplo: El sueldo de un profesor y la construcción de una escuela son objetos de gasto distintos, pero ambos tienen la misma finalidad: Educación.',
53 - color: '#4A8B6E',
54 - },
55 - donde: {
56 - title: '¿Dónde se gasta?', tech: 'CLASIFICADOR GEOGRÁFICO',
57 - everyday: 'María manda plata a su mamá en Cochabamba y paga el alquiler en La Paz. No es lo mismo gastar en la ciudad que en el área rural, en Santa Cruz que en Potosí.',
58 - budget: 'La dimensión geográfica permite ver cómo se distribuyen los recursos en el territorio: por departamento, municipio o región. Revela las prioridades territoriales del Estado.',
59 - example: 'Ejemplo: Del presupuesto de Educación, ¿cuánto va a La Paz vs. Beni? Este clasificador responde esa pregunta.',
60 - color: '#4A8B6E',
61 - },
62 - };
63 8
64 const WIDGETS_INGRESOS = [ 9 const WIDGETS_INGRESOS = [
65 - { id: 'rubros', q: '¿Qué tipo de ingreso?', d: 'Impuestos, regalías, ventas, créditos — la naturaleza de cada ingreso', t: 'Recursos por rubros', href: '/clasificadores/rubros' }, 10 + { id: 'rubros', label: 'tipos de ingreso', d: 'Impuestos, regalías, ventas, créditos', t: 'Recursos por rubros', n: '324', href: '/clasificadores/rubros' },
66 - { id: 'fuentes', q: '¿Quién pone la plata?', d: 'TGN, crédito externo, donaciones — de qué bolsillo sale', t: 'Fuentes de financiamiento', href: '/clasificadores/fuente' }, 11 + { id: 'organismos', label: 'organismos financiadores', d: 'BID, Banco Mundial, cooperación', t: 'Organismos', n: '228', href: '/clasificadores/organismos' },
67 - ];
68 -
69 - const WIDGETS_EN_QUE = [
70 - { id: 'objeto', q: 'Objeto del gasto', d: 'Sueldos, consultorías, materiales — la lista concreta', t: 'Objeto del gasto', href: '/clasificadores/objeto-gasto' },
71 - { id: 'sector', q: 'Sector económico', d: 'Industria, construcción, servicios — a qué sector va', t: 'Sectores económicos', href: '/clasificadores/sectores' },
72 ]; 12 ];
73 13
74 - const WIDGETS_BOTTOM = [ 14 + const WIDGETS_GASTOS_QUE = [
75 - { id: 'finalidad', q: '¿Para qué se gasta?', d: 'Educación, salud, defensa — el objetivo final', t: 'Finalidad y función', href: '/clasificadores/funcional' }, 15 + { id: 'objeto', label: 'objetos de gasto', d: 'Sueldos, consultorías, materiales', t: 'Objeto del gasto', n: '324', href: '/clasificadores/objeto-gasto' },
76 - { id: 'donde', q: '¿Dónde se gasta?', d: 'Departamentos, municipios — distribución territorial', t: 'Geográfico', href: '/clasificadores/geografico' }, 16 + { id: 'sector', label: 'sectores económicos', d: 'Industria, construcción, servicios', t: 'Sectores', n: '488', href: '/clasificadores/sectores' },
77 ]; 17 ];
78 -
79 - $: modal = activeModal ? MODALS[activeModal] : null;
80 -
81 - function openModal(e, id) {
82 - e.preventDefault();
83 - e.stopPropagation();
84 - activeModal = id;
85 - }
86 - function closeModal() { activeModal = null; }
87 </script> 18 </script>
88 19
89 <svelte:head> 20 <svelte:head>
90 - <title>Clasificadores — Presupuesto Bolivia</title> 21 + <title>Clasificadores | Presupuesto Público</title>
91 - <link rel="preconnect" href="https://fonts.googleapis.com" />
92 - <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
93 </svelte:head> 22 </svelte:head>
94 23
95 <div class="page" class:mounted> 24 <div class="page" class:mounted>
25 + <div class="clas-container">
96 <!-- Header --> 26 <!-- Header -->
97 - <header class="page-header"> 27 + <div class="clas-header-section">
98 - <nav class="breadcrumb"> 28 + <div class="clas-header-row">
99 - <a href="/" class="breadcrumb-link sm-hidden">← Inicio</a> 29 + <div>
100 - <div class="breadcrumb-full"> 30 + <p class="section-tag">CLASIFICADORES</p>
101 - <a href="/">Inicio</a> 31 + <h2 class="section-title">Elige cómo explorar el presupuesto</h2>
102 - <span class="breadcrumb-sep">/</span> 32 + <p class="clas-subtitle">Navega por cada una de las 2,626 piezas de los 8 clasificadores.</p>
103 - <span class="breadcrumb-current">Clasificadores</span> 33 + </div>
104 - </div> 34 + </div>
105 - </nav> 35 + </div>
106 -
107 - <div class="header-content anim" style="--d:0ms">
108 - <h1>Elige cómo explorar el presupuesto</h1>
109 - <p class="subtitulo">
110 - Cada clasificador es una forma distinta de ver la misma información.
111 - Haz clic en <span class="help-inline">?</span> para entender qué es cada uno.
112 - </p>
113 - </div>
114 - </header>
115 36
116 - <main> 37 + <!-- ¿Quién? -->
117 - <!-- ═══ ¿QUIÉN? ═══ --> 38 + <div class="clas-group-card">
118 - <div class="anim" style="--d:100ms"> 39 + <div class="clas-group-header">
119 - <div class="group"> 40 + <div class="clas-group-icon" style="--c: var(--theme-titulo)">
120 - <div class="group-accent" style="--c: var(--theme-titulo)"></div>
121 - <div class="group-header">
122 - <div class="group-icon" style="--c: var(--theme-titulo)">
123 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> 41 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
124 </div> 42 </div>
125 <div> 43 <div>
126 - <div class="group-label">¿QUIÉN?</div> 44 + <div class="clas-group-label">¿QUIÉN?</div>
127 - <div class="group-sub">La entidad que gasta o recibe</div> 45 + <div class="clas-group-sub">La entidad que gasta o recibe</div>
128 </div> 46 </div>
129 </div> 47 </div>
130 - 48 + <a href="/clasificadores/institucional" class="widget-card widget-h" class:widget-hover={hWidget === 'quien'} onmouseenter={() => hWidget = 'quien'} onmouseleave={() => hWidget = null}>
131 - <a 49 + <div class="widget-left">
132 - href="/clasificadores/institucional" 50 + <div class="widget-headline">
133 - class="widget-card widget-h" 51 + <span class="widget-num-big">756</span>
134 - class:wh={hovered === 'quien'} 52 + <span class="widget-label">instituciones públicas</span>
135 - on:mouseenter={() => hovered = 'quien'} 53 + </div>
136 - on:mouseleave={() => hovered = null}
137 - >
138 - <div class="widget-h-left">
139 - <div class="widget-q">Instituciones públicas</div>
140 <div class="widget-desc">Ministerios, gobernaciones, municipios, empresas públicas, universidades</div> 54 <div class="widget-desc">Ministerios, gobernaciones, municipios, empresas públicas, universidades</div>
141 </div> 55 </div>
142 - <div class="widget-h-right"> 56 + <div class="widget-right">
143 <span class="widget-tech">INSTITUCIONAL</span> 57 <span class="widget-tech">INSTITUCIONAL</span>
144 - <button class="help-btn help-sm" on:click={(e) => openModal(e, 'quien')}>?</button> 58 + <span class="widget-arrow" class:arrow-show={hWidget === 'quien'}>→</span>
145 - <span class="widget-arrow" class:arrow-show={hovered === 'quien'}>→</span>
146 </div> 59 </div>
147 </a> 60 </a>
148 </div> 61 </div>
149 - </div>
150 -
151 - <!-- ═══ INGRESOS + GASTOS ═══ -->
152 - <div class="main-grid">
153 62
63 + <!-- Grid: Ingresos + Gastos -->
64 + <div class="clas-main-grid">
154 <!-- INGRESOS --> 65 <!-- INGRESOS -->
155 - <div class="anim col-stretch" style="--d:200ms"> 66 + <div class="clas-col">
156 - <div class="group group-stretch"> 67 + <div class="clas-group-card">
157 - <div class="group-accent" style="--c: var(--theme-accent)"></div> 68 + <div class="clas-group-header">
158 - <div class="group-header"> 69 + <div class="clas-group-icon" style="--c:#C9A751">
159 - <div class="group-icon" style="--c: var(--theme-accent)">
160 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> 70 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
161 </div> 71 </div>
162 <div> 72 <div>
163 - <div class="group-label" style="color: var(--theme-accent)">INGRESOS</div> 73 + <div class="clas-group-label" style="color:#C9A751">INGRESOS</div>
164 - <div class="group-sub">¿De dónde viene la plata?</div>
165 </div> 74 </div>
166 </div> 75 </div>
167 - 76 + <div class="widgets-col">
168 - <div class="widgets-stretch">
169 {#each WIDGETS_INGRESOS as w} 77 {#each WIDGETS_INGRESOS as w}
170 - <a 78 + <a href={w.href} class="widget-card widget-grow" class:widget-hover={hWidget === w.id} onmouseenter={() => hWidget = w.id} onmouseleave={() => hWidget = null}>
171 - href={w.href} 79 + <div class="widget-headline">
172 - class="widget-card widget-grow" 80 + <span class="widget-num-big widget-num-gold">{w.n}</span>
173 - class:wh={hovered === w.id} 81 + <span class="widget-label">{w.label}</span>
174 - on:mouseenter={() => hovered = w.id} 82 + </div>
175 - on:mouseleave={() => hovered = null} 83 + <p class="widget-desc">{w.d}</p>
176 - >
177 - <div class="widget-top">
178 - <h4 class="widget-q">{w.q}</h4>
179 - <button class="help-btn" on:click={(e) => openModal(e, w.id)}>?</button>
180 - </div>
181 - <p class="widget-desc widget-desc-grow">{w.d}</p>
182 <div class="widget-footer"> 84 <div class="widget-footer">
183 <span class="widget-tech">{w.t}</span> 85 <span class="widget-tech">{w.t}</span>
184 - <span class="widget-arrow" class:arrow-show={hovered === w.id}>→</span> 86 + <span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span>
185 </div> 87 </div>
186 - <div class="widget-glow widget-glow-accent"></div>
187 </a> 88 </a>
188 {/each} 89 {/each}
189 </div> 90 </div>
...@@ -191,703 +92,166 @@ ...@@ -191,703 +92,166 @@
191 </div> 92 </div>
192 93
193 <!-- GASTOS --> 94 <!-- GASTOS -->
194 - <div class="anim col-stretch" style="--d:320ms"> 95 + <div class="clas-col">
195 - <div class="group"> 96 + <div class="clas-group-card">
196 - <div class="group-accent" style="--c: #4A8B6E"></div> 97 + <div class="clas-group-header">
197 - <div class="group-header"> 98 + <div class="clas-group-icon" style="--c:#4A8B6E">
198 - <div class="group-icon" style="--c: #4A8B6E">
199 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg> 99 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg>
200 </div> 100 </div>
201 <div> 101 <div>
202 - <div class="group-label" style="color: #4A8B6E">GASTOS</div> 102 + <div class="clas-group-label" style="color:#4A8B6E">GASTOS</div>
203 - <div class="group-sub">¿Cómo se gasta la plata?</div>
204 </div> 103 </div>
205 </div> 104 </div>
206 105
207 - <div class="group-widgets"> 106 + <div class="gastos-content">
208 - <!-- ¿En qué? sub-group --> 107 + <!-- ¿En qué? -->
209 - <div class="sub-group"> 108 + <div class="clas-subgroup">
210 - <div class="sub-group-accent" style="--c: #4A8B6E"></div> 109 + <div class="clas-subgroup-header">
211 - <div class="sub-group-header"> 110 + <div class="clas-subgroup-icon" style="--c:#4A8B6E">
212 - <div class="sub-group-left"> 111 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
213 - <div class="sub-group-icon" style="--c: #4A8B6E">
214 - <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
215 </div> 112 </div>
216 <div> 113 <div>
217 - <div class="sub-group-label">¿En qué se gasta?</div> 114 + <div class="clas-subgroup-label">¿EN QUÉ?</div>
218 - <div class="sub-group-tag">DOS CLASIFICADORES</div>
219 </div> 115 </div>
220 </div> 116 </div>
117 + <div class="widgets-row">
118 + {#each WIDGETS_GASTOS_QUE as w}
119 + <a href={w.href} class="widget-card widget-sm" class:widget-hover={hWidget === w.id} onmouseenter={() => hWidget = w.id} onmouseleave={() => hWidget = null}>
120 + <div class="widget-headline">
121 + <span class="widget-num-big widget-num-green">{w.n}</span>
122 + <span class="widget-label">{w.label}</span>
123 + </div>
124 + <p class="widget-desc widget-desc-sm">{w.d}</p>
125 + <div class="widget-footer">
126 + <span class="widget-tech">{w.t}</span>
127 + <span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span>
221 </div> 128 </div>
222 -
223 - <div class="sub-grid">
224 - {#each WIDGETS_EN_QUE as w}
225 - <a
226 - href={w.href}
227 - class="widget-card sub-card"
228 - class:wh={hovered === w.id}
229 - on:mouseenter={() => hovered = w.id}
230 - on:mouseleave={() => hovered = null}
231 - >
232 - <div class="widget-top">
233 - <h4 class="widget-q sub-q">{w.q}</h4>
234 - <button class="help-btn help-xs" on:click={(e) => openModal(e, w.id)}>?</button>
235 - </div>
236 - <p class="widget-desc sub-desc">{w.d}</p>
237 - <div class="widget-footer sub-footer">
238 - <span class="widget-tech sub-tech">{w.t}</span>
239 - <span class="widget-arrow" class:arrow-show={hovered === w.id}>→</span>
240 - </div>
241 - <div class="widget-glow widget-glow-green"></div>
242 </a> 129 </a>
243 {/each} 130 {/each}
244 </div> 131 </div>
245 </div> 132 </div>
246 133
247 - <!-- Bottom row --> 134 + <!-- Para qué + Dónde -->
248 - <div class="bottom-grid"> 135 + <div class="widgets-row">
249 - {#each WIDGETS_BOTTOM as w} 136 + <div class="clas-subgroup">
250 - <a 137 + <div class="clas-subgroup-header">
251 - href={w.href} 138 + <div class="clas-subgroup-icon" style="--c:#4A8B6E">
252 - class="widget-card" 139 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>
253 - class:wh={hovered === w.id}
254 - on:mouseenter={() => hovered = w.id}
255 - on:mouseleave={() => hovered = null}
256 - >
257 - <div class="widget-top">
258 - <h4 class="widget-q">{w.q}</h4>
259 - <button class="help-btn" on:click={(e) => openModal(e, w.id)}>?</button>
260 </div> 140 </div>
261 - <p class="widget-desc">{w.d}</p> 141 + <div>
142 + <div class="clas-subgroup-label">¿PARA QUÉ?</div>
143 + </div>
144 + </div>
145 + <a href="/clasificadores/funcional" class="widget-card widget-full" class:widget-hover={hWidget === 'finalidad'} onmouseenter={() => hWidget = 'finalidad'} onmouseleave={() => hWidget = null}>
146 + <div class="widget-headline">
147 + <span class="widget-num-big widget-num-green">132</span>
148 + <span class="widget-label">finalidades y funciones</span>
149 + </div>
150 + <p class="widget-desc">Educación, salud, defensa</p>
262 <div class="widget-footer"> 151 <div class="widget-footer">
263 - <span class="widget-tech">{w.t}</span> 152 + <span class="widget-tech">Finalidad</span>
264 - <span class="widget-arrow" class:arrow-show={hovered === w.id}>→</span> 153 + <span class="widget-arrow" class:arrow-show={hWidget === 'finalidad'}>→</span>
265 </div> 154 </div>
266 - <div class="widget-glow widget-glow-green"></div>
267 </a> 155 </a>
268 - {/each}
269 </div> 156 </div>
157 +
158 + <div class="clas-subgroup">
159 + <div class="clas-subgroup-header">
160 + <div class="clas-subgroup-icon" style="--c:#4A8B6E">
161 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg>
270 </div> 162 </div>
163 + <div>
164 + <div class="clas-subgroup-label">¿DÓNDE?</div>
271 </div> 165 </div>
272 </div> 166 </div>
167 + <a href="/ubicacion" class="widget-card widget-full" class:widget-hover={hWidget === 'donde'} onmouseenter={() => hWidget = 'donde'} onmouseleave={() => hWidget = null}>
168 + <div class="widget-headline">
169 + <span class="widget-num-big widget-num-green">352</span>
170 + <span class="widget-label">ubicaciones geográficas</span>
273 </div> 171 </div>
274 - </main> 172 + <p class="widget-desc">Departamentos, municipios</p>
275 -</div> 173 + <div class="widget-footer">
276 - 174 + <span class="widget-tech">Geográfico</span>
277 -<!-- ═══ MODAL ═══ --> 175 + <span class="widget-arrow" class:arrow-show={hWidget === 'donde'}>→</span>
278 -{#if modal}
279 - <div
280 - class="modal-overlay"
281 - on:click={closeModal}
282 - on:keydown={(e) => e.key === 'Escape' && closeModal()}
283 - role="dialog"
284 - aria-modal="true"
285 - transition:fade={{ duration: 150 }}
286 - >
287 - <div
288 - class="modal-box"
289 - on:click|stopPropagation
290 - role="document"
291 - transition:scale={{ duration: 200, start: 0.95, easing: cubicOut }}
292 - >
293 - <button class="modal-close" on:click={closeModal}>
294 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
295 - </button>
296 -
297 - <div class="modal-badge">{modal.tech}</div>
298 - <h2 class="modal-title">{modal.title}</h2>
299 -
300 - <div class="modal-section">
301 - <div class="modal-icon">👤</div>
302 - <div>
303 - <div class="modal-label">EN LA VIDA COTIDIANA</div>
304 - <p class="modal-text">{modal.everyday}</p>
305 </div> 176 </div>
177 + </a>
306 </div> 178 </div>
307 -
308 - <div class="modal-divider"></div>
309 -
310 - <div class="modal-section">
311 - <div class="modal-icon">🏛️</div>
312 - <div>
313 - <div class="modal-label">EN EL PRESUPUESTO</div>
314 - <p class="modal-text">{modal.budget}</p>
315 </div> 179 </div>
316 </div> 180 </div>
317 -
318 - <div class="modal-example">
319 - <p>{modal.example}</p>
320 </div> 181 </div>
321 </div> 182 </div>
322 </div> 183 </div>
323 -{/if} 184 + </div>
324 - 185 +</div>
325 186
326 <style> 187 <style>
327 - /* ── Base ── */
328 .page { 188 .page {
329 - background: var(--theme-body);
330 - color: var(--theme-titulo);
331 min-height: 100vh; 189 min-height: 100vh;
332 - font-family: 'Instrument Sans', sans-serif; 190 + background: var(--theme-body);
333 - transition: background-color 0.2s, color 0.2s;
334 - }
335 -
336 - /* ── Entrance animations ── */
337 - .anim {
338 - opacity: 0;
339 - transform: translateY(24px);
340 - transition: all 0.7s cubic-bezier(0.16,1,0.3,1);
341 - transition-delay: var(--d, 0ms);
342 - }
343 - .mounted .anim {
344 - opacity: 1;
345 - transform: translateY(0);
346 - }
347 -
348 - /* ── Header ── */
349 - .page-header {
350 - border-bottom: 1px solid var(--theme-borde);
351 - padding: 1.5rem 2rem 2rem;
352 - max-width: 1120px;
353 - margin: 0 auto;
354 - }
355 -
356 - .breadcrumb {
357 - margin-bottom: 1rem;
358 - font-family: 'DM Mono', monospace;
359 - font-size: 0.75rem;
360 - }
361 -
362 - .breadcrumb-link {
363 - color: var(--theme-texto);
364 - text-decoration: none;
365 - transition: color 0.2s;
366 - }
367 - .breadcrumb-link:hover {
368 - color: var(--theme-titulo);
369 - }
370 -
371 - .sm-hidden { display: block; }
372 - .breadcrumb-full { display: none; }
373 -
374 - @media (min-width: 640px) {
375 - .sm-hidden { display: none; }
376 - .breadcrumb-full {
377 - display: flex;
378 - align-items: center;
379 - gap: 0.5rem;
380 - color: var(--theme-texto);
381 - }
382 - .breadcrumb-full a {
383 - color: var(--theme-texto);
384 - text-decoration: none;
385 - transition: color 0.2s;
386 - }
387 - .breadcrumb-full a:hover {
388 - color: var(--theme-titulo);
389 - }
390 - .breadcrumb-sep {
391 - opacity: 0.5;
392 - }
393 - .breadcrumb-current {
394 - color: var(--theme-titulo);
395 - }
396 - }
397 -
398 - .header-content h1 {
399 - font-family: 'DM Serif Display', serif;
400 - font-size: clamp(1.75rem, 4vw, 2rem);
401 - line-height: 1.1;
402 - letter-spacing: -0.02em;
403 - margin-bottom: 0.5rem;
404 - }
405 -
406 - .subtitulo {
407 - font-size: 0.95rem;
408 - line-height: 1.5;
409 - color: var(--theme-texto);
410 - max-width: 600px;
411 - }
412 -
413 - .help-inline {
414 - font-family: 'DM Serif Display', serif;
415 - font-weight: 700;
416 - color: var(--theme-texto);
417 - border: 1px solid var(--theme-borde);
418 - border-radius: 50%;
419 - padding: 0 5px;
420 - font-size: 0.7rem;
421 - }
422 -
423 - main {
424 - max-width: 1120px;
425 - margin: 0 auto;
426 - padding: 2rem 2rem 4rem;
427 - }
428 -
429 - /* ── Group (level 1) ── */
430 - .group {
431 - background: var(--theme-surface);
432 - border: 1px solid var(--theme-borde);
433 - border-radius: 1.5rem;
434 - padding: 1.25rem 1.125rem 1.125rem;
435 - position: relative;
436 - overflow: hidden;
437 - }
438 - .group-stretch {
439 - display: flex;
440 - flex-direction: column;
441 - height: 100%;
442 - }
443 - .group-accent {
444 - position: absolute;
445 - top: 0;
446 - left: 0;
447 - right: 0;
448 - height: 2px;
449 - background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--c) 30%, transparent), transparent);
450 - border-radius: 1.5rem 1.5rem 0 0;
451 - }
452 - .group-header {
453 - display: flex;
454 - align-items: center;
455 - gap: 0.625rem;
456 - margin-bottom: 0.875rem;
457 - padding: 0 0.25rem;
458 - }
459 - .group-icon {
460 - width: 2rem;
461 - height: 2rem;
462 - border-radius: 0.625rem;
463 - background: color-mix(in srgb, var(--c) 10%, transparent);
464 - border: 1px solid color-mix(in srgb, var(--c) 20%, transparent);
465 - display: flex;
466 - align-items: center;
467 - justify-content: center;
468 - flex-shrink: 0;
469 - color: var(--c);
470 - }
471 - .group-label {
472 - font-family: 'DM Mono', monospace;
473 - font-size: 0.75rem;
474 - letter-spacing: 0.15em;
475 - font-weight: 600;
476 - line-height: 1;
477 - color: var(--theme-titulo);
478 - }
479 - .group-sub {
480 - font-family: 'Instrument Sans', sans-serif;
481 - font-size: 0.688rem;
482 - color: var(--theme-texto);
483 - margin-top: 0.25rem;
484 - opacity: 0.8;
485 - }
486 -
487 - .group-widgets {
488 - display: flex;
489 - flex-direction: column;
490 - gap: 0.625rem;
491 - }
492 - .widgets-stretch {
493 - display: flex;
494 - flex-direction: column;
495 - gap: 0.625rem;
496 - flex: 1;
497 - }
498 -
499 - /* ── Widget card (level 2) ── */
500 - .widget-card {
501 - display: block;
502 - background: var(--theme-fill);
503 - border: 1px solid var(--theme-borde);
504 - border-radius: 1rem;
505 - padding: 1.375rem;
506 - position: relative;
507 - overflow: hidden;
508 - cursor: pointer;
509 - text-decoration: none;
510 - color: var(--theme-titulo);
511 - transition: all 0.4s cubic-bezier(0.16,1,0.3,1);
512 - }
513 - .wh {
514 - background: color-mix(in srgb, var(--theme-fill) 80%, var(--theme-titulo) 5%);
515 - border-color: color-mix(in srgb, var(--theme-borde) 50%, var(--theme-texto) 20%);
516 - transform: translateY(-1px);
517 - }
518 - .widget-grow {
519 - flex: 1;
520 - display: flex;
521 - flex-direction: column;
522 - }
523 -
524 - /* Horizontal variant */
525 - .widget-h {
526 - display: flex;
527 - justify-content: space-between;
528 - align-items: center;
529 - }
530 - .widget-h-left { flex: 1; }
531 - .widget-h-right {
532 - display: flex;
533 - align-items: center;
534 - gap: 0.625rem;
535 - flex-shrink: 0;
536 - }
537 -
538 - .widget-top {
539 - display: flex;
540 - justify-content: space-between;
541 - align-items: flex-start;
542 - gap: 0.625rem;
543 - margin-bottom: 0.5rem;
544 - position: relative;
545 - z-index: 1;
546 - }
547 - .widget-q {
548 - font-size: 1rem;
549 - font-weight: 600;
550 - letter-spacing: -0.01em;
551 - line-height: 1.2;
552 - font-family: 'Instrument Sans', sans-serif;
553 - color: var(--theme-titulo);
554 - }
555 - .widget-desc {
556 - font-family: 'Instrument Sans', sans-serif;
557 - font-size: 0.75rem;
558 - color: var(--theme-texto);
559 - line-height: 1.5;
560 - position: relative;
561 - z-index: 1;
562 - }
563 - .widget-desc-grow { flex: 1; }
564 - .widget-footer {
565 - display: flex;
566 - justify-content: space-between;
567 - align-items: center;
568 - margin-top: 1rem;
569 - position: relative;
570 - z-index: 1;
571 - }
572 - .widget-tech {
573 - font-family: 'DM Mono', monospace;
574 - font-size: 0.5rem;
575 - letter-spacing: 0.12em;
576 - color: var(--theme-texto);
577 - text-transform: uppercase;
578 - }
579 - .widget-arrow {
580 - font-family: 'Instrument Sans', sans-serif;
581 - font-size: 0.813rem;
582 - color: transparent;
583 - transform: translateX(-6px);
584 - transition: all 0.35s cubic-bezier(0.16,1,0.3,1);
585 - }
586 - .arrow-show {
587 color: var(--theme-titulo); 191 color: var(--theme-titulo);
588 - transform: translateX(0); 192 + font-family: var(--font-sans), -apple-system, sans-serif;
589 - }
590 - .widget-glow {
591 - position: absolute;
592 - inset: 0;
593 - pointer-events: none;
594 opacity: 0; 193 opacity: 0;
595 transition: opacity 0.4s; 194 transition: opacity 0.4s;
596 } 195 }
597 - .widget-glow-accent { 196 + .page.mounted { opacity: 1; }
598 - background: radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--theme-accent) 6%, transparent), transparent 70%); 197 + :global(html:not(.dark)) .page { background: #f5f5f7; }
599 - } 198 +
600 - .widget-glow-green { 199 + .clas-container { max-width: 1100px; margin: 0 auto; padding: 80px 2rem 2rem; }
601 - background: radial-gradient(circle at 50% 50%, rgba(74,139,110,0.06), transparent 70%); 200 +
602 - } 201 + .clas-header-section { margin-bottom: 1.5rem; }
603 - .wh > .widget-glow, 202 + .clas-header-row { display: flex; justify-content: space-between; align-items: flex-end; }
604 - .wh .widget-glow { opacity: 1; } 203 +
605 - 204 + .section-tag { font-family: 'DM Mono', monospace; font-size: 0.6875rem; letter-spacing: 0.15em; color: var(--theme-texto); opacity: 0.5; margin: 0 0 0.25rem; }
606 - /* ── Sub-group (¿En qué?) ── */ 205 + .section-title { font-family: 'DM Serif Display', serif; font-size: 2rem; font-weight: 400; margin: 0 0 0.25rem; line-height: 1.15; }
607 - .sub-group { 206 + .clas-subtitle { font-size: 0.875rem; color: var(--theme-texto); opacity: 0.6; margin: 0; }
608 - background: color-mix(in srgb, var(--theme-surface) 60%, var(--theme-body) 40%); 207 +
609 - border: 1px solid var(--theme-borde); 208 + .clas-group-card { background: var(--theme-surface); border: 1px solid var(--theme-borde); border-radius: 20px; padding: 16px; margin-bottom: 12px; display: flex; flex-direction: column; transition: all 0.3s; }
610 - border-radius: 1.125rem; 209 + .clas-group-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
611 - padding: 1rem 0.875rem 0.875rem; 210 + .clas-group-icon { width: 36px; height: 36px; border-radius: 12px; background: color-mix(in srgb, var(--c) 10%, transparent); border: 1px solid color-mix(in srgb, var(--c) 15%, transparent); display: flex; align-items: center; justify-content: center; color: var(--c); flex-shrink: 0; }
612 - position: relative; 211 + .clas-group-label { font-family: 'DM Mono', monospace; font-size: 0.75rem; letter-spacing: 0.15em; font-weight: 600; color: var(--theme-titulo); transition: transform 0.3s; }
613 - overflow: hidden; 212 + .clas-group-sub { font-size: 0.8125rem; color: var(--theme-texto); opacity: 0.6; }
614 - } 213 +
615 - .sub-group-accent { 214 + .clas-main-grid { display: grid; grid-template-columns: 1fr 2fr; gap: 12px; align-items: stretch; }
616 - position: absolute; 215 + .clas-col .clas-group-card { flex: 1; height: 100%; }
617 - top: 0; 216 +
618 - left: 15%; 217 + .gastos-content { display: flex; flex-direction: column; gap: 10px; flex: 1; }
619 - right: 15%; 218 + .gastos-content .clas-subgroup { flex: 1; margin-bottom: 0; }
620 - height: 1px; 219 +
621 - background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--c) 25%, transparent), transparent); 220 + .clas-subgroup { background: rgba(128, 128, 128, 0.05); border: 1px solid var(--theme-borde); border-radius: 14px; padding: 12px; display: flex; flex-direction: column; transition: all 0.3s; }
622 - } 221 + .clas-subgroup .widgets-row { flex: 1; }
623 - .sub-group-header { 222 + .clas-subgroup-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
624 - display: flex; 223 + .clas-subgroup-icon { width: 32px; height: 32px; border-radius: 10px; background: color-mix(in srgb, var(--c) 10%, transparent); border: 1px solid color-mix(in srgb, var(--c) 15%, transparent); display: flex; align-items: center; justify-content: center; color: var(--c); flex-shrink: 0; }
625 - align-items: center; 224 + .clas-subgroup-label { font-family: 'DM Mono', monospace; font-size: 0.75rem; letter-spacing: 0.15em; font-weight: 600; color: var(--theme-titulo); }
626 - justify-content: space-between; 225 +
627 - margin-bottom: 0.75rem; 226 + .widget-card { display: block; background: var(--theme-surface); border: 1px solid var(--theme-borde); border-radius: 14px; padding: 14px; cursor: pointer; text-decoration: none; color: var(--theme-titulo); transition: all 0.35s cubic-bezier(0.16, 1, 0.3, 1); }
628 - padding: 0 0.25rem; 227 + .widget-card:hover { border-color: var(--theme-accent); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
629 - } 228 +
630 - .sub-group-left { 229 + .widget-h { display: flex; justify-content: space-between; align-items: center; gap: 1rem; }
631 - display: flex; 230 + .widget-left { flex: 1; }
632 - align-items: center; 231 + .widget-right { display: flex; flex-direction: column; align-items: flex-end; gap: 0.5rem; }
633 - gap: 0.5rem; 232 +
634 - } 233 + .widgets-col { display: flex; flex-direction: column; gap: 8px; flex: 1; }
635 - .sub-group-icon { 234 + .widgets-col .widget-card { flex: 1; }
636 - width: 1.5rem; 235 + .widgets-row { display: flex; gap: 8px; }
637 - height: 1.5rem; 236 + .widgets-row .widget-card { flex: 1; display: flex; flex-direction: column; justify-content: space-between; }
638 - border-radius: 0.375rem; 237 +
639 - background: color-mix(in srgb, var(--c) 10%, transparent); 238 + .widget-headline { display: flex; align-items: baseline; gap: 6px; margin-bottom: 4px; }
640 - border: 1px solid color-mix(in srgb, var(--c) 18%, transparent); 239 + .widget-num-big { font-family: 'DM Mono', monospace; font-size: 1.5rem; font-weight: 600; color: var(--theme-titulo); line-height: 1; }
641 - display: flex; 240 + .widget-num-gold { color: #C9A751; }
642 - align-items: center; 241 + .widget-num-green { color: #4A8B6E; }
643 - justify-content: center; 242 + .widget-label { font-size: 0.8125rem; color: var(--theme-texto); }
644 - flex-shrink: 0; 243 + .widget-desc { font-size: 0.75rem; color: var(--theme-texto); opacity: 0.6; margin: 0; line-height: 1.4; }
645 - color: var(--c); 244 + .widget-desc-sm { font-size: 0.6875rem; }
646 - } 245 + .widget-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; }
647 - .sub-group-label { 246 + .widget-tech { font-family: 'DM Mono', monospace; font-size: 0.5625rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--theme-texto); opacity: 0.4; }
648 - font-family: 'Instrument Sans', sans-serif; 247 + .widget-arrow { font-size: 1.25rem; color: var(--theme-accent); opacity: 0; transition: all 0.3s; }
649 - font-size: 0.75rem; 248 + .arrow-show { opacity: 1; transform: translateX(3px); }
650 - font-weight: 600; 249 +
651 - color: var(--theme-texto); 250 + @media (max-width: 768px) {
652 - line-height: 1.1; 251 + .clas-container { padding: 70px 1rem 1rem; }
653 - } 252 + .section-title { font-size: 1.5rem; }
654 - .sub-group-tag { 253 + .clas-main-grid { grid-template-columns: 1fr; }
655 - font-family: 'DM Mono', monospace; 254 + .widgets-row { flex-direction: column; }
656 - font-size: 0.438rem; 255 + .widget-h { flex-direction: column; align-items: flex-start; }
657 - letter-spacing: 0.1em;
658 - margin-top: 0.125rem;
659 - color: #4A8B6E;
660 - opacity: 0.7;
661 - }
662 -
663 - .sub-grid {
664 - display: grid;
665 - grid-template-columns: 1fr 1fr;
666 - gap: 0.5rem;
667 - }
668 -
669 - /* ── Sub-widget (level 3) ── */
670 - .sub-card {
671 - background: var(--theme-fill);
672 - border-color: var(--theme-borde);
673 - padding: 1rem;
674 - border-radius: 0.75rem;
675 - }
676 - .sub-card.wh {
677 - background: color-mix(in srgb, var(--theme-fill) 80%, var(--theme-titulo) 5%);
678 - border-color: color-mix(in srgb, var(--theme-borde) 50%, var(--theme-texto) 20%);
679 - }
680 - .sub-q {
681 - font-size: 0.813rem;
682 - font-weight: 500;
683 - color: var(--theme-titulo);
684 - }
685 - .sub-desc {
686 - font-size: 0.688rem;
687 - opacity: 0.9;
688 - }
689 - .sub-footer { margin-top: 0.75rem; }
690 - .sub-tech { font-size: 0.375rem; }
691 -
692 - .bottom-grid {
693 - display: grid;
694 - grid-template-columns: 1fr 1fr;
695 - gap: 0.625rem;
696 - }
697 -
698 - /* ── Main grid ── */
699 - .main-grid {
700 - display: grid;
701 - grid-template-columns: 1fr 2fr;
702 - gap: 1rem;
703 - margin-top: 1rem;
704 - align-items: stretch;
705 - }
706 - .col-stretch {
707 - display: flex;
708 - flex-direction: column;
709 - }
710 -
711 - /* ── Help button ── */
712 - .help-btn {
713 - width: 1.625rem;
714 - height: 1.625rem;
715 - border-radius: 50%;
716 - border: 1px solid var(--theme-borde);
717 - background: transparent;
718 - color: var(--theme-texto);
719 - font-family: 'DM Serif Display', serif;
720 - font-size: 0.75rem;
721 - font-weight: 600;
722 - cursor: pointer;
723 - transition: all 0.3s;
724 - display: flex;
725 - align-items: center;
726 - justify-content: center;
727 - flex-shrink: 0;
728 - }
729 - .help-btn:hover {
730 - border-color: var(--theme-accent);
731 - color: var(--theme-accent);
732 - background: color-mix(in srgb, var(--theme-accent) 8%, transparent);
733 - }
734 - .help-sm { width: 1.375rem; height: 1.375rem; font-size: 0.625rem; }
735 - .help-xs { width: 1.25rem; height: 1.25rem; font-size: 0.563rem; }
736 -
737 - /* ── Modal ── */
738 - .modal-overlay {
739 - position: fixed;
740 - inset: 0;
741 - background: rgba(0,0,0,0.6);
742 - backdrop-filter: blur(12px);
743 - display: flex;
744 - align-items: center;
745 - justify-content: center;
746 - z-index: 1000;
747 - padding: 2.5rem;
748 - }
749 - .modal-box {
750 - background: var(--theme-surface);
751 - border: 1px solid var(--theme-borde);
752 - border-radius: 1.5rem;
753 - max-width: 520px;
754 - width: 100%;
755 - padding: 2rem 2.25rem;
756 - position: relative;
757 - max-height: 85vh;
758 - overflow-y: auto;
759 - }
760 - .modal-close {
761 - position: absolute;
762 - top: 1rem;
763 - right: 1rem;
764 - background: transparent;
765 - border: 1px solid var(--theme-borde);
766 - border-radius: 50%;
767 - width: 2rem;
768 - height: 2rem;
769 - display: flex;
770 - align-items: center;
771 - justify-content: center;
772 - color: var(--theme-texto);
773 - cursor: pointer;
774 - transition: all 0.2s;
775 - }
776 - .modal-close:hover {
777 - border-color: var(--theme-texto);
778 - color: var(--theme-titulo);
779 - }
780 - .modal-badge {
781 - font-family: 'DM Mono', monospace;
782 - font-size: 0.563rem;
783 - letter-spacing: 0.15em;
784 - border-radius: 0.375rem;
785 - padding: 0.25rem 0.563rem;
786 - display: inline-block;
787 - margin-bottom: 0.875rem;
788 - background: color-mix(in srgb, var(--theme-accent) 12%, transparent);
789 - color: var(--theme-accent);
790 - }
791 - .modal-title {
792 - font-family: 'DM Serif Display', serif;
793 - font-size: 1.5rem;
794 - font-weight: 400;
795 - letter-spacing: -0.02em;
796 - line-height: 1.15;
797 - margin-bottom: 1.5rem;
798 - color: var(--theme-titulo);
799 - }
800 - .modal-section {
801 - display: flex;
802 - gap: 0.875rem;
803 - }
804 - .modal-icon {
805 - font-size: 1.125rem;
806 - width: 2.25rem;
807 - height: 2.25rem;
808 - background: var(--theme-fill);
809 - border-radius: 0.625rem;
810 - display: flex;
811 - align-items: center;
812 - justify-content: center;
813 - flex-shrink: 0;
814 - border: 1px solid var(--theme-borde);
815 - }
816 - .modal-label {
817 - font-family: 'DM Mono', monospace;
818 - font-size: 0.563rem;
819 - letter-spacing: 0.15em;
820 - color: var(--theme-texto);
821 - margin-bottom: 0.313rem;
822 - }
823 - .modal-text {
824 - font-family: 'Instrument Sans', sans-serif;
825 - font-size: 0.813rem;
826 - line-height: 1.65;
827 - color: var(--theme-texto);
828 - }
829 - .modal-divider {
830 - height: 1px;
831 - background: var(--theme-borde);
832 - margin: 1.125rem 0;
833 - margin-left: 3.125rem;
834 - }
835 - .modal-example {
836 - margin-top: 1.25rem;
837 - margin-left: 3.125rem;
838 - padding: 0.875rem 1.125rem;
839 - border-left: 3px solid var(--theme-accent);
840 - background: color-mix(in srgb, var(--theme-accent) 5%, transparent);
841 - border-radius: 0 0.625rem 0.625rem 0;
842 - }
843 - .modal-example p {
844 - font-family: 'Instrument Sans', sans-serif;
845 - font-size: 0.75rem;
846 - line-height: 1.6;
847 - color: var(--theme-texto);
848 - font-style: italic;
849 - }
850 -
851 - /* ── Responsive ── */
852 - @media (max-width: 900px) {
853 - .main-grid {
854 - grid-template-columns: 1fr;
855 - }
856 - .page-header,
857 - main {
858 - padding-left: 1.5rem;
859 - padding-right: 1.5rem;
860 - }
861 - }
862 -
863 - @media (max-width: 640px) {
864 - .page-header,
865 - main {
866 - padding-left: 1rem;
867 - padding-right: 1rem;
868 - }
869 - .sub-grid,
870 - .bottom-grid {
871 - grid-template-columns: 1fr;
872 - }
873 - .widget-h {
874 - flex-direction: column;
875 - align-items: flex-start;
876 - gap: 0.75rem;
877 - }
878 - .widget-h-right {
879 - width: 100%;
880 - justify-content: space-between;
881 - }
882 - .modal-overlay {
883 - padding: 1rem;
884 - }
885 - .modal-box {
886 - padding: 1.5rem;
887 - }
888 - .modal-example,
889 - .modal-divider {
890 - margin-left: 0;
891 - }
892 } 256 }
893 </style> 257 </style>
......
1 <script> 1 <script>
2 - import { supabase } from '$lib/supabase';
3 import { onMount, untrack } from 'svelte'; 2 import { onMount, untrack } from 'svelte';
4 import { page } from '$app/stores'; 3 import { page } from '$app/stores';
5 import { goto } from '$app/navigation'; 4 import { goto } from '$app/navigation';
...@@ -154,16 +153,11 @@ ...@@ -154,16 +153,11 @@
154 treemapNodes = []; 153 treemapNodes = [];
155 try { 154 try {
156 const entidadFiltro = selectedEntity?.entidad ?? 0; 155 const entidadFiltro = selectedEntity?.entidad ?? 0;
157 - const { data, error: dbError } = await supabase 156 + const res = await fetch(`/api/finfun-treemap?gestion=${selectedYear}&entidad=${entidadFiltro}`);
158 - .schema('ppto') 157 + const data = await res.json();
159 - .from('treemap_finfun') 158 +
160 - .select('gestion, nivel, finfun, desc_funcion, parent, devengado') 159 + if (data.error) {
161 - .eq('gestion', selectedYear) 160 + console.error('Error fetching treemap data:', data.error);
162 - .eq('entidad', entidadFiltro)
163 - .gt('devengado', 0);
164 -
165 - if (dbError) {
166 - console.error('Error fetching treemap data:', dbError.message);
167 return; 161 return;
168 } 162 }
169 163
...@@ -171,7 +165,7 @@ ...@@ -171,7 +165,7 @@
171 gestion: d.gestion, 165 gestion: d.gestion,
172 nivel: d.nivel, 166 nivel: d.nivel,
173 finfun: d.finfun, 167 finfun: d.finfun,
174 - desc_finfun: d.desc_funcion, 168 + desc_finfun: d.desc_finfun,
175 parent: d.parent, 169 parent: d.parent,
176 devengado: d.devengado || 0 170 devengado: d.devengado || 0
177 })); 171 }));
...@@ -186,15 +180,11 @@ ...@@ -186,15 +180,11 @@
186 async function loadEntitiesForYear(year) { 180 async function loadEntitiesForYear(year) {
187 if (!year) return; 181 if (!year) return;
188 try { 182 try {
189 - const { data, error: dbError } = await supabase 183 + const res = await fetch('/api/objeto-treemap-entidades');
190 - .schema('ppto') 184 + const data = await res.json();
191 - .from('entidades_treemap') 185 +
192 - .select('entidad, desc_entidad, sigla_entidad') 186 + if (data.error) {
193 - .eq('gestion', year) 187 + console.error('Error loading entities:', data.error);
194 - .order('desc_entidad');
195 -
196 - if (dbError) {
197 - console.error('Error loading entities:', dbError.message);
198 return; 188 return;
199 } 189 }
200 entities = data || []; 190 entities = data || [];
...@@ -580,13 +570,10 @@ ...@@ -580,13 +570,10 @@
580 document.body.offsetHeight; // Force reflow 570 document.body.offsetHeight; // Force reflow
581 }); 571 });
582 572
583 - const { data, error } = await supabase 573 + const clasRes = await fetch('/api/finfun-clasificador');
584 - .schema('ppto') 574 + const data = await clasRes.json();
585 - .from('clas_finfun')
586 - .select('*')
587 - .order('finfun');
588 575
589 - if (!error && data) { 576 + if (Array.isArray(data)) {
590 allItems = data; 577 allItems = data;
591 578
592 // Extraer finalidades únicas (nivel más agregado) 579 // Extraer finalidades únicas (nivel más agregado)
...@@ -842,55 +829,6 @@ ...@@ -842,55 +829,6 @@
842 Jerarquía: Finalidad → Grupo Función → Función 829 Jerarquía: Finalidad → Grupo Función → Función
843 </p> 830 </p>
844 831
845 - <!-- Toggle de modos: Lista, Mapa, Comparar + Botón filtros móvil -->
846 - <div class="flex items-center gap-3">
847 - <div class="header-modes">
848 - <button
849 - class="header-mode-btn"
850 - class:active={viewMode === 'lista'}
851 - onclick={() => setMode('lista')}
852 - >
853 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
854 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
855 - </svg>
856 - Lista
857 - </button>
858 - <button
859 - class="header-mode-btn"
860 - class:active={viewMode === 'mapa'}
861 - onclick={() => setMode('mapa')}
862 - >
863 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
864 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
865 - </svg>
866 - Mapa
867 - </button>
868 - <button
869 - class="header-mode-btn"
870 - class:active={viewMode === 'comparar'}
871 - onclick={() => setMode('comparar')}
872 - >
873 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
874 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
875 - </svg>
876 - Comparar
877 - </button>
878 - </div>
879 -
880 - <!-- Botón filtros para móvil en modo mapa -->
881 - {#if viewMode === 'mapa'}
882 - <button
883 - class="lg:hidden flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm"
884 - style="background-color: var(--theme-fill); color: var(--theme-titulo);"
885 - onclick={() => mapaSidebarOpen = !mapaSidebarOpen}
886 - >
887 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
888 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
889 - </svg>
890 - Filtros
891 - </button>
892 - {/if}
893 - </div>
894 </div> 832 </div>
895 </div> 833 </div>
896 </header> 834 </header>
......
...@@ -1441,52 +1441,6 @@ ...@@ -1441,52 +1441,6 @@
1441 <div class="mt-2"> 1441 <div class="mt-2">
1442 <div class="flex flex-col md:flex-row gap-4 md:items-center md:justify-between"> 1442 <div class="flex flex-col md:flex-row gap-4 md:items-center md:justify-between">
1443 <div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4"> 1443 <div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
1444 - {#if viewMode === 'lista'}
1445 - <div class="header-modes">
1446 - <button
1447 - class="header-mode-btn active"
1448 - onclick={() => setMode('lista')}
1449 - >
1450 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1451 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
1452 - </svg>
1453 - Lista
1454 - </button>
1455 - <button
1456 - class="header-mode-btn"
1457 - onclick={() => setMode('mapa')}
1458 - >
1459 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1460 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
1461 - </svg>
1462 - Mapa
1463 - </button>
1464 - <button
1465 - class="header-mode-btn"
1466 - onclick={() => { setMode('comparar'); initCompareMode(); }}
1467 - >
1468 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1469 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
1470 - </svg>
1471 - Comparar
1472 - </button>
1473 - </div>
1474 -
1475 - <!-- Guía inline (solo para lista) -->
1476 - <div class="hidden sm:block text-sm border-l pl-4" style="color: var(--theme-texto); border-color: var(--theme-borde);">
1477 - <span class="flex items-center gap-1.5">
1478 - Clic en
1479 - <span class="inline-flex items-center justify-center w-5 h-5 rounded" style="background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent); color: var(--theme-accent);">
1480 - <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1481 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
1482 - </svg>
1483 - </span>
1484 - para ver el presupuesto histórico ·
1485 - <span class="underline decoration-dotted font-medium" style="color: var(--theme-accent);">N var.</span>
1486 - = número de cambios en la descripción oficial
1487 - </span>
1488 - </div>
1489 - {/if}
1490 </div> 1444 </div>
1491 1445
1492 <div class="flex flex-wrap items-center gap-3 flex-shrink-0"> 1446 <div class="flex flex-wrap items-center gap-3 flex-shrink-0">
......
1 <script> 1 <script>
2 - import { supabase } from '$lib/supabase';
3 import { onMount } from 'svelte'; 2 import { onMount } from 'svelte';
4 import { page } from '$app/stores'; 3 import { page } from '$app/stores';
5 import { goto } from '$app/navigation'; 4 import { goto } from '$app/navigation';
...@@ -33,15 +32,12 @@ ...@@ -33,15 +32,12 @@
33 document.body.offsetHeight; // Force reflow 32 document.body.offsetHeight; // Force reflow
34 }); 33 });
35 34
36 - const { data, error } = await supabase 35 + try {
37 - .schema('ppto') 36 + const res = await fetch('/api/entidad-clasificador');
38 - .from('clas_institucional') 37 + const data = await res.json();
39 - .select('*')
40 - .order('desc_area')
41 - .order('desc_entidad');
42 38
43 - if (!error && data) { 39 + if (Array.isArray(data)) {
44 - allEntidades = data; 40 + allEntidades = data.sort((a, b) => (a.desc_area || '').localeCompare(b.desc_area || '') || (a.desc_entidad || '').localeCompare(b.desc_entidad || ''));
45 41
46 // Agrupar por área (segundo nivel) 42 // Agrupar por área (segundo nivel)
47 const areasMap = {}; 43 const areasMap = {};
...@@ -74,6 +70,9 @@ ...@@ -74,6 +70,9 @@
74 selectedArea = areas[0]; 70 selectedArea = areas[0];
75 } 71 }
76 } 72 }
73 + } catch (err) {
74 + console.error('Error loading clasificador:', err);
75 + }
77 76
78 loading = false; 77 loading = false;
79 }); 78 });
...@@ -238,49 +237,6 @@ ...@@ -238,49 +237,6 @@
238 237
239 <!-- Controles de visualización --> 238 <!-- Controles de visualización -->
240 <div class="mt-8"> 239 <div class="mt-8">
241 - <div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
242 - <!-- Toggle Vista -->
243 - <div class="flex items-center gap-3">
244 - <span class="text-sm font-medium" style="color: var(--theme-titulo);">Explorar como</span>
245 - <nav class="inline-flex rounded-xl border-2 p-1 shadow-sm" style="border-color: var(--theme-titulo); background-color: var(--theme-surface);">
246 - <a
247 - href="?modo=lista"
248 - class="px-4 py-2 text-sm font-semibold rounded-lg transition-all"
249 - style="{viewMode === 'lista' ? `background-color: var(--theme-titulo); color: var(--theme-body);` : `color: var(--theme-texto);`}"
250 - onclick={(e) => { e.preventDefault(); setMode('lista'); }}
251 - >
252 - Lista
253 - </a>
254 - <a
255 - href="?modo=mapa"
256 - class="px-4 py-2 text-sm font-semibold rounded-lg transition-all"
257 - style="{viewMode === 'mapa' ? `background-color: var(--theme-titulo); color: var(--theme-body);` : `color: var(--theme-texto);`}"
258 - onclick={(e) => { e.preventDefault(); setMode('mapa'); }}
259 - >
260 - Mapa
261 - </a>
262 - <a
263 - href="?modo=comparar"
264 - class="px-4 py-2 text-sm font-semibold rounded-lg transition-all"
265 - style="{viewMode === 'comparar' ? `background-color: var(--theme-titulo); color: var(--theme-body);` : `color: var(--theme-texto);`}"
266 - onclick={(e) => { e.preventDefault(); setMode('comparar'); }}
267 - >
268 - Comparar
269 - </a>
270 - </nav>
271 - </div>
272 -
273 - <!-- Guía inline -->
274 - <div class="hidden sm:block text-sm border-l pl-4" style="color: var(--theme-texto); border-color: var(--theme-borde);">
275 - {#if viewMode === 'lista'}
276 - <span>Navega por áreas y subáreas</span>
277 - {:else if viewMode === 'mapa'}
278 - <span>Visualización geográfica del presupuesto</span>
279 - {:else}
280 - <span>Compara dos entidades o periodos</span>
281 - {/if}
282 - </div>
283 - </div>
284 </div> 240 </div>
285 </div> 241 </div>
286 </header> 242 </header>
......
...@@ -1604,52 +1604,6 @@ ...@@ -1604,52 +1604,6 @@
1604 <!-- Selector de modo + Guía inline (visible en sm+) --> 1604 <!-- Selector de modo + Guía inline (visible en sm+) -->
1605 <div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4"> 1605 <div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
1606 <!-- Selector de modo (solo para lista, en mapa y comparar está en el sidebar) --> 1606 <!-- Selector de modo (solo para lista, en mapa y comparar está en el sidebar) -->
1607 - {#if viewMode === 'lista'}
1608 - <div class="header-modes">
1609 - <button
1610 - class="header-mode-btn active"
1611 - onclick={() => setMode('lista')}
1612 - >
1613 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1614 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
1615 - </svg>
1616 - Lista
1617 - </button>
1618 - <button
1619 - class="header-mode-btn"
1620 - onclick={() => setMode('mapa')}
1621 - >
1622 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1623 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
1624 - </svg>
1625 - Mapa
1626 - </button>
1627 - <button
1628 - class="header-mode-btn"
1629 - onclick={() => { setMode('comparar'); initCompareMode(); }}
1630 - >
1631 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1632 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
1633 - </svg>
1634 - Comparar
1635 - </button>
1636 - </div>
1637 -
1638 - <!-- Guía inline (solo para lista) -->
1639 - <div class="hidden sm:block text-sm border-l pl-4" style="color: var(--theme-texto); border-color: var(--theme-borde);">
1640 - <span class="flex items-center gap-1.5">
1641 - Clic en
1642 - <span class="inline-flex items-center justify-center w-5 h-5 rounded" style="background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent); color: var(--theme-accent);">
1643 - <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1644 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
1645 - </svg>
1646 - </span>
1647 - para ver el presupuesto histórico ·
1648 - <span class="underline decoration-dotted font-medium" style="color: var(--theme-accent);">N var.</span>
1649 - = número de cambios en la descripción oficial
1650 - </span>
1651 - </div>
1652 - {/if}
1653 </div> 1607 </div>
1654 1608
1655 <!-- Filtros (botón móvil para abrir sidebar en mapa y comparar) --> 1609 <!-- Filtros (botón móvil para abrir sidebar en mapa y comparar) -->
......
...@@ -1453,52 +1453,6 @@ ...@@ -1453,52 +1453,6 @@
1453 <div class="mt-2"> 1453 <div class="mt-2">
1454 <div class="flex flex-col md:flex-row gap-4 md:items-center md:justify-between"> 1454 <div class="flex flex-col md:flex-row gap-4 md:items-center md:justify-between">
1455 <div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4"> 1455 <div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
1456 - {#if viewMode === 'lista'}
1457 - <div class="header-modes">
1458 - <button
1459 - class="header-mode-btn active"
1460 - onclick={() => setMode('lista')}
1461 - >
1462 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1463 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
1464 - </svg>
1465 - Lista
1466 - </button>
1467 - <button
1468 - class="header-mode-btn"
1469 - onclick={() => setMode('mapa')}
1470 - >
1471 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1472 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
1473 - </svg>
1474 - Mapa
1475 - </button>
1476 - <button
1477 - class="header-mode-btn"
1478 - onclick={() => { setMode('comparar'); initCompareMode(); }}
1479 - >
1480 - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1481 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
1482 - </svg>
1483 - Comparar
1484 - </button>
1485 - </div>
1486 -
1487 - <!-- Guía inline (solo para lista) -->
1488 - <div class="hidden sm:block text-sm border-l pl-4" style="color: var(--theme-texto); border-color: var(--theme-borde);">
1489 - <span class="flex items-center gap-1.5">
1490 - Clic en
1491 - <span class="inline-flex items-center justify-center w-5 h-5 rounded" style="background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent); color: var(--theme-accent);">
1492 - <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1493 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
1494 - </svg>
1495 - </span>
1496 - para ver el presupuesto histórico ·
1497 - <span class="underline decoration-dotted font-medium" style="color: var(--theme-accent);">N var.</span>
1498 - = número de cambios en la descripción oficial
1499 - </span>
1500 - </div>
1501 - {/if}
1502 </div> 1456 </div>
1503 1457
1504 <!-- Filtros (botón móvil para abrir sidebar en mapa y comparar) --> 1458 <!-- Filtros (botón móvil para abrir sidebar en mapa y comparar) -->
......
1 +<script>
2 + import { onMount } from 'svelte';
3 + import { DOWNLOAD_BASE } from '$lib/api.js';
4 + const YEARS = Array.from({ length: 10 }, (_, i) => 2016 + i);
5 +
6 + let mounted = $state(false);
7 + let dataset = $state('gastos');
8 + let entities = $state([]);
9 + let entitySearch = $state('');
10 + let entityFocused = $state(false);
11 + let selectedEntity = $state(null);
12 + let selectedIdx = $state(-1);
13 + let loadingEntities = $state(true);
14 + let entityError = $state(false);
15 +
16 + function normalizeText(val) {
17 + return val.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim();
18 + }
19 +
20 + function buildUrl(partition, value) {
21 + return `${DOWNLOAD_BASE}${dataset}/${partition}/${value}`;
22 + }
23 +
24 + let filteredEntities = $derived.by(() => {
25 + if (!entitySearch || entitySearch.length < 2) return [];
26 + const q = normalizeText(entitySearch);
27 + return entities.filter(e => e.normalized.includes(q)).slice(0, 12);
28 + });
29 +
30 + function selectEntity(entity) {
31 + selectedEntity = entity;
32 + entitySearch = entity.desc_entidad;
33 + entityFocused = false;
34 + selectedIdx = -1;
35 + }
36 +
37 + function handleKeydown(e) {
38 + if (e.key === 'ArrowDown') {
39 + e.preventDefault();
40 + selectedIdx = selectedIdx < filteredEntities.length - 1 ? selectedIdx + 1 : 0;
41 + } else if (e.key === 'ArrowUp') {
42 + e.preventDefault();
43 + selectedIdx = selectedIdx > 0 ? selectedIdx - 1 : filteredEntities.length - 1;
44 + } else if (e.key === 'Enter' && selectedIdx >= 0) {
45 + e.preventDefault();
46 + selectEntity(filteredEntities[selectedIdx]);
47 + } else if (e.key === 'Escape') {
48 + entityFocused = false;
49 + }
50 + }
51 +
52 + onMount(async () => {
53 + mounted = true;
54 + try {
55 + const res = await fetch('/api/entidad-clasificador');
56 + const data = await res.json();
57 + if (Array.isArray(data)) {
58 + entities = data
59 + .filter(d => d.entidad && d.desc_entidad)
60 + .map(d => ({ entidad: d.entidad, desc_entidad: d.desc_entidad, normalized: normalizeText(d.desc_entidad) }))
61 + .sort((a, b) => a.desc_entidad.localeCompare(b.desc_entidad));
62 + }
63 + } catch { entityError = true; }
64 + loadingEntities = false;
65 + });
66 +</script>
67 +
68 +<svelte:head>
69 + <title>Descargas | Presupuesto Público</title>
70 + <link rel="preconnect" href="https://fonts.googleapis.com" />
71 + <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
72 +</svelte:head>
73 +
74 +<div class="page" class:mounted>
75 + <div class="layout">
76 + <h1 class="title">Descargas</h1>
77 + <p class="subtitle">Descarga los datos del presupuesto público en formato CSV</p>
78 +
79 + <!-- Dataset toggle -->
80 + <div class="section">
81 + <span class="section-label">Base</span>
82 + <div class="toggle-group">
83 + <button class="toggle-btn" class:active={dataset === 'gastos'} onclick={() => { dataset = 'gastos'; selectedEntity = null; }}>Gastos</button>
84 + <button class="toggle-btn" class:active={dataset === 'ingresos'} onclick={() => { dataset = 'ingresos'; selectedEntity = null; }}>Ingresos</button>
85 + </div>
86 + </div>
87 +
88 + <!-- Completo -->
89 + <div class="section">
90 + <h2 class="section-title">Completo</h2>
91 + <a href={buildUrl('completo', 'ultimo')} class="download-link">
92 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
93 + Descargar todo ({dataset})
94 + </a>
95 + </div>
96 +
97 + <!-- Por gestión -->
98 + <div class="section">
99 + <h2 class="section-title">Por gestión</h2>
100 + <div class="year-grid">
101 + {#each YEARS as year}
102 + <a href={buildUrl('gestion', year)} class="year-link">{year}</a>
103 + {/each}
104 + </div>
105 + </div>
106 +
107 + <!-- Por entidad -->
108 + <div class="section">
109 + <h2 class="section-title">Por entidad</h2>
110 + <div class="entity-search">
111 + <input
112 + type="text"
113 + bind:value={entitySearch}
114 + onfocus={() => { entityFocused = true; selectedEntity = null; }}
115 + onblur={() => setTimeout(() => entityFocused = false, 150)}
116 + onkeydown={handleKeydown}
117 + placeholder="Buscar entidad..."
118 + class="search-input"
119 + />
120 + {#if loadingEntities}
121 + <span class="search-status">Cargando entidades...</span>
122 + {:else if entityError}
123 + <span class="search-status error">No se pudieron cargar las entidades</span>
124 + {/if}
125 +
126 + {#if entityFocused && filteredEntities.length > 0}
127 + <div class="search-dropdown">
128 + {#each filteredEntities as entity, i}
129 + <button
130 + class="search-item"
131 + class:active={selectedIdx === i}
132 + onmouseenter={() => selectedIdx = i}
133 + onclick={() => selectEntity(entity)}
134 + >
135 + <span class="item-name">{entity.desc_entidad}</span>
136 + <span class="item-code">Código {entity.entidad}</span>
137 + </button>
138 + {/each}
139 + </div>
140 + {/if}
141 +
142 + {#if entityFocused && entitySearch.length >= 2 && filteredEntities.length === 0 && !loadingEntities}
143 + <span class="search-status error">Sin resultados</span>
144 + {/if}
145 + </div>
146 +
147 + {#if selectedEntity}
148 + <div class="entity-result">
149 + <span class="entity-name">{selectedEntity.desc_entidad}</span>
150 + <a href={buildUrl('entidad', selectedEntity.entidad)} class="download-link">
151 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
152 + Descargar
153 + </a>
154 + </div>
155 + {/if}
156 + </div>
157 + </div>
158 +</div>
159 +
160 +<style>
161 + .page {
162 + min-height: 100vh;
163 + background: var(--theme-body);
164 + color: var(--theme-titulo);
165 + font-family: var(--font-sans), -apple-system, sans-serif;
166 + opacity: 0;
167 + transition: opacity 0.4s;
168 + }
169 + .page.mounted { opacity: 1; }
170 + :global(html:not(.dark)) .page { background: #f5f5f7; }
171 +
172 + .layout {
173 + max-width: 700px;
174 + margin: 0 auto;
175 + padding: 100px 2rem 3rem;
176 + }
177 +
178 + .title {
179 + font-family: 'DM Serif Display', serif;
180 + font-size: 2rem;
181 + font-weight: 400;
182 + margin: 0 0 0.25rem;
183 + }
184 +
185 + .subtitle {
186 + font-size: 0.875rem;
187 + color: var(--theme-texto);
188 + opacity: 0.6;
189 + margin: 0 0 2rem;
190 + }
191 +
192 + .section { margin-bottom: 2rem; }
193 +
194 + .section-label {
195 + font-family: 'DM Mono', monospace;
196 + font-size: 0.6875rem;
197 + letter-spacing: 0.1em;
198 + text-transform: uppercase;
199 + color: var(--theme-texto);
200 + opacity: 0.5;
201 + display: block;
202 + margin-bottom: 0.5rem;
203 + }
204 +
205 + .section-title {
206 + font-size: 1rem;
207 + font-weight: 700;
208 + margin: 0 0 0.75rem;
209 + }
210 +
211 + /* Toggle */
212 + .toggle-group {
213 + display: flex;
214 + gap: 0.25rem;
215 + background: var(--theme-surface);
216 + border-radius: 8px;
217 + padding: 3px;
218 + width: fit-content;
219 + }
220 +
221 + .toggle-btn {
222 + font-size: 0.8125rem;
223 + font-weight: 500;
224 + padding: 0.375rem 1rem;
225 + border: none;
226 + border-radius: 6px;
227 + background: transparent;
228 + color: var(--theme-texto);
229 + cursor: pointer;
230 + transition: all 0.15s;
231 + }
232 + .toggle-btn:hover { color: var(--theme-titulo); }
233 + .toggle-btn.active {
234 + background: var(--theme-accent);
235 + color: white;
236 + font-weight: 600;
237 + }
238 +
239 + /* Download link */
240 + .download-link {
241 + display: inline-flex;
242 + align-items: center;
243 + gap: 0.375rem;
244 + font-size: 0.8125rem;
245 + color: var(--theme-accent);
246 + text-decoration: none;
247 + border-bottom: 1px dotted rgba(201, 167, 81, 0.4);
248 + padding-bottom: 1px;
249 + transition: opacity 0.15s;
250 + }
251 + .download-link:hover { opacity: 0.7; }
252 +
253 + /* Years */
254 + .year-grid {
255 + display: flex;
256 + flex-wrap: wrap;
257 + gap: 0.5rem;
258 + }
259 +
260 + .year-link {
261 + font-family: 'DM Mono', monospace;
262 + font-size: 0.75rem;
263 + padding: 0.375rem 0.75rem;
264 + border: 1px solid var(--theme-borde);
265 + border-radius: 6px;
266 + color: var(--theme-titulo);
267 + text-decoration: none;
268 + transition: all 0.15s;
269 + }
270 + .year-link:hover {
271 + border-color: var(--theme-accent);
272 + color: var(--theme-accent);
273 + }
274 +
275 + /* Entity search */
276 + .entity-search {
277 + position: relative;
278 + }
279 +
280 + .search-input {
281 + width: 100%;
282 + max-width: 400px;
283 + padding: 0.625rem 1rem;
284 + border: 1px solid var(--theme-borde);
285 + border-radius: 8px;
286 + background: var(--theme-surface);
287 + color: var(--theme-titulo);
288 + font-size: 0.8125rem;
289 + outline: none;
290 + transition: border-color 0.15s;
291 + }
292 + .search-input:focus { border-color: var(--theme-accent); }
293 + .search-input::placeholder { color: var(--theme-texto); opacity: 0.5; }
294 +
295 + .search-status {
296 + display: block;
297 + font-size: 0.75rem;
298 + color: var(--theme-texto);
299 + opacity: 0.6;
300 + margin-top: 0.375rem;
301 + }
302 + .search-status.error { color: #b42318; opacity: 1; }
303 +
304 + .search-dropdown {
305 + position: absolute;
306 + top: calc(100% + 4px);
307 + left: 0;
308 + max-width: 400px;
309 + width: 100%;
310 + background: var(--theme-surface);
311 + border: 1px solid var(--theme-borde);
312 + border-radius: 8px;
313 + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
314 + max-height: 240px;
315 + overflow-y: auto;
316 + z-index: 10;
317 + }
318 +
319 + .search-item {
320 + display: flex;
321 + flex-direction: column;
322 + width: 100%;
323 + padding: 0.625rem 1rem;
324 + border: none;
325 + border-top: 1px solid var(--theme-borde);
326 + background: transparent;
327 + text-align: left;
328 + cursor: pointer;
329 + transition: background 0.15s;
330 + }
331 + .search-item:first-child { border-top: none; }
332 + .search-item:hover, .search-item.active { background: var(--theme-surface-hover, rgba(128,128,128,0.08)); }
333 +
334 + .item-name { font-size: 0.8125rem; color: var(--theme-titulo); }
335 + .item-code { font-family: 'DM Mono', monospace; font-size: 0.6875rem; color: var(--theme-texto); opacity: 0.5; }
336 +
337 + .entity-result {
338 + display: flex;
339 + align-items: center;
340 + justify-content: space-between;
341 + gap: 1rem;
342 + margin-top: 1rem;
343 + padding: 0.75rem 1rem;
344 + background: var(--theme-surface);
345 + border-radius: 8px;
346 + max-width: 400px;
347 + }
348 +
349 + .entity-name {
350 + font-size: 0.8125rem;
351 + font-weight: 500;
352 + color: var(--theme-titulo);
353 + }
354 +
355 + @media (max-width: 640px) {
356 + .layout { padding: 80px 1rem 2rem; }
357 + .title { font-size: 1.5rem; }
358 + .search-input, .search-dropdown, .entity-result { max-width: 100%; }
359 + }
360 +</style>
1 import { error } from '@sveltejs/kit'; 1 import { error } from '@sveltejs/kit';
2 2
3 -const API_BASE = 'http://136.112.29.74/api/finfun'; 3 +import { API_HOST } from '$lib/api.js';
4 +const API_BASE = API_HOST + '/api/finfun';
4 5
5 // Jerarquía finfun: Finalidad → Grupo Función → Función 6 // Jerarquía finfun: Finalidad → Grupo Función → Función
6 // Códigos con punto: "1" (finalidad), "1.1" (grpfuncion), "1.1.1" (función) 7 // Códigos con punto: "1" (finalidad), "1.1" (grpfuncion), "1.1.1" (función)
......
1 import { error } from '@sveltejs/kit'; 1 import { error } from '@sveltejs/kit';
2 2
3 -const API_BASE = 'http://136.112.29.74/api/objeto'; 3 +import { API_HOST } from '$lib/api.js';
4 +const API_BASE = API_HOST + '/api/objeto';
4 5
5 export async function load({ params }) { 6 export async function load({ params }) {
6 const { codigo } = params; 7 const { codigo } = params;
......
1 import { error } from '@sveltejs/kit'; 1 import { error } from '@sveltejs/kit';
2 2
3 -const API_BASE = 'http://136.112.29.74/api/organismo'; 3 +import { API_HOST } from '$lib/api.js';
4 +const API_BASE = API_HOST + '/api/organismo';
4 5
5 export async function load({ params }) { 6 export async function load({ params }) {
6 const { codigo } = params; 7 const { codigo } = params;
......
1 import { error } from '@sveltejs/kit'; 1 import { error } from '@sveltejs/kit';
2 2
3 -const API_BASE = 'http://136.112.29.74/api/rubro'; 3 +import { API_HOST } from '$lib/api.js';
4 +const API_BASE = API_HOST + '/api/rubro';
4 5
5 export async function load({ params }) { 6 export async function load({ params }) {
6 const { codigo } = params; 7 const { codigo } = params;
......
1 +<!DOCTYPE html>
2 +<html lang="es">
3 +<head>
4 + <meta charset="utf-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1">
6 + <title>Descargas presupuestarias</title>
7 + <style>
8 + :root {
9 + --text: #1f1f1f;
10 + --muted: #666;
11 + --link: #0b57d0;
12 + }
13 +
14 + * {
15 + box-sizing: border-box;
16 + }
17 +
18 + body {
19 + margin: 0;
20 + font-family: sans-serif;
21 + color: var(--text);
22 + line-height: 1.5;
23 + }
24 +
25 + main {
26 + max-width: 800px;
27 + margin: 0 auto;
28 + padding: 24px 16px 40px;
29 + }
30 +
31 + h1,
32 + h2,
33 + p {
34 + margin-top: 0;
35 + }
36 +
37 + h1 {
38 + margin-bottom: 16px;
39 + }
40 +
41 + h2 {
42 + margin-bottom: 8px;
43 + font-size: 1rem;
44 + }
45 +
46 + section {
47 + margin-bottom: 20px;
48 + }
49 +
50 + label,
51 + input,
52 + button {
53 + font: inherit;
54 + }
55 +
56 + input {
57 + width: 100%;
58 + padding: 8px 10px;
59 + border: 1px solid #ccc;
60 + background: #fff;
61 + }
62 +
63 + .field {
64 + display: grid;
65 + gap: 6px;
66 + }
67 +
68 + .radio-group {
69 + display: flex;
70 + flex-wrap: wrap;
71 + gap: 12px;
72 + }
73 +
74 + .radio-option {
75 + display: inline-flex;
76 + align-items: center;
77 + gap: 8px;
78 + }
79 +
80 + .radio-option input {
81 + width: auto;
82 + padding: 0;
83 + border: 0;
84 + }
85 +
86 + .links,
87 + .results {
88 + padding: 0;
89 + margin: 0;
90 + list-style: none;
91 + }
92 +
93 + .links {
94 + display: flex;
95 + flex-wrap: wrap;
96 + gap: 8px 12px;
97 + }
98 +
99 + .search-area {
100 + position: relative;
101 + }
102 +
103 + .results {
104 + position: absolute;
105 + top: calc(100% + 4px);
106 + left: 0;
107 + right: 0;
108 + z-index: 10;
109 + max-height: 240px;
110 + overflow-y: auto;
111 + border-top: 1px solid #ddd;
112 + border-bottom: 1px solid #ddd;
113 + background: #fff;
114 + }
115 +
116 + .results[hidden],
117 + .entity-downloads[hidden] {
118 + display: none;
119 + }
120 +
121 + .results li + li {
122 + border-top: 1px solid #ddd;
123 + }
124 +
125 + .result-button {
126 + display: block;
127 + width: 100%;
128 + padding: 10px 0;
129 + border: 0;
130 + background: transparent;
131 + text-align: left;
132 + cursor: pointer;
133 + }
134 +
135 + .result-button:hover,
136 + .result-button.active,
137 + .result-button:focus {
138 + background: #f3f3f3;
139 + }
140 +
141 + .meta,
142 + .status {
143 + color: var(--muted);
144 + }
145 +
146 + .status {
147 + min-height: 1.25rem;
148 + margin: 6px 0 0;
149 + font-size: 0.9rem;
150 + }
151 +
152 + .status.error {
153 + color: #b42318;
154 + }
155 +
156 + .entity-downloads p {
157 + margin-bottom: 8px;
158 + }
159 +
160 + a {
161 + color: var(--link);
162 + }
163 + </style>
164 +</head>
165 +<body>
166 + <main>
167 + <h1>Descargas</h1>
168 +
169 + <section>
170 + <div class="field">
171 + <span>Base</span>
172 + <div class="radio-group">
173 + <label class="radio-option">
174 + <input type="radio" name="dataset" value="gastos" checked>
175 + <span>Gastos</span>
176 + </label>
177 + <label class="radio-option">
178 + <input type="radio" name="dataset" value="ingresos">
179 + <span>Ingresos</span>
180 + </label>
181 + </div>
182 + </div>
183 + </section>
184 +
185 + <section>
186 + <h2>Completo</h2>
187 + <ul class="links" id="full-download"></ul>
188 + </section>
189 +
190 + <section>
191 + <h2>Por gestión</h2>
192 + <ul class="links" id="year-downloads"></ul>
193 + </section>
194 +
195 + <section>
196 + <h2>Por entidad</h2>
197 + <div class="field search-area">
198 + <label for="entity-search">Entidad</label>
199 + <input
200 + id="entity-search"
201 + name="entity-search"
202 + type="search"
203 + placeholder="Busca entidades"
204 + autocomplete="off"
205 + >
206 + <ul class="results" id="entity-results" hidden></ul>
207 + </div>
208 + <p class="status" id="entity-status">Cargando entidades...</p>
209 + <div class="entity-downloads" id="entity-downloads" hidden>
210 + <p id="entity-name"></p>
211 + <ul class="links" id="entity-links"></ul>
212 + </div>
213 + </section>
214 + </main>
215 +
216 + <script>
217 + const API_BASE = "http://136.112.29.74/api/";
218 + const DOWNLOAD_BASE = "https://abierto.economiayfinanzas.gob.bo/presupuesto/descargas/";
219 + const ENTITY_API = `${API_BASE}entidad/clasificador`;
220 + const YEARS = Array.from({ length: 10 }, (_, index) => 2016 + index);
221 + const MAX_ENTITY_RESULTS = 12;
222 + let entities = [];
223 + let activeResultIndex = -1;
224 +
225 + function setStatus(message, type = "info") {
226 + const status = document.getElementById("entity-status");
227 + status.textContent = message;
228 + status.classList.toggle("error", type === "error");
229 + }
230 +
231 + function currentDataset() {
232 + return document.querySelector('input[name="dataset"]:checked').value;
233 + }
234 +
235 + function buildUrl(partition, value) {
236 + return `${DOWNLOAD_BASE}${currentDataset()}/${partition}/${value}`;
237 + }
238 +
239 + function normalizeText(value) {
240 + return value
241 + .normalize("NFD")
242 + .replace(/[\u0300-\u036f]/g, "")
243 + .toLowerCase()
244 + .trim();
245 + }
246 +
247 + function createLinkItem(label, href) {
248 + const item = document.createElement("li");
249 + const link = document.createElement("a");
250 + link.href = href;
251 + link.textContent = label;
252 + item.appendChild(link);
253 + return item;
254 + }
255 +
256 + function renderFullDownload() {
257 + const container = document.getElementById("full-download");
258 + container.innerHTML = "";
259 + container.appendChild(createLinkItem("Descargar", buildUrl("completo", "ultimo")));
260 + }
261 +
262 + function renderYearDownloads() {
263 + const container = document.getElementById("year-downloads");
264 + container.innerHTML = "";
265 +
266 + YEARS.forEach((year) => {
267 + container.appendChild(createLinkItem(String(year), buildUrl("gestion", year)));
268 + });
269 + }
270 +
271 + function renderEntityDownloads(entity) {
272 + const wrapper = document.getElementById("entity-downloads");
273 + const name = document.getElementById("entity-name");
274 + const links = document.getElementById("entity-links");
275 +
276 + name.textContent = entity.desc_entidad;
277 + links.innerHTML = "";
278 + links.appendChild(createLinkItem("Descargar", buildUrl("entidad", entity.entidad)));
279 + wrapper.hidden = false;
280 + }
281 +
282 + function selectEntity(entity) {
283 + document.getElementById("entity-search").value = entity.desc_entidad;
284 + document.getElementById("entity-results").hidden = true;
285 + activeResultIndex = -1;
286 + renderEntityDownloads(entity);
287 + }
288 +
289 + function resultButtons() {
290 + return Array.from(document.querySelectorAll("#entity-results .result-button"));
291 + }
292 +
293 + function setActiveResult(index) {
294 + const buttons = resultButtons();
295 +
296 + buttons.forEach((button, buttonIndex) => {
297 + button.classList.toggle("active", buttonIndex === index);
298 + });
299 +
300 + activeResultIndex = index;
301 +
302 + if (index >= 0 && buttons[index]) {
303 + buttons[index].scrollIntoView({ block: "nearest" });
304 + }
305 + }
306 +
307 + function renderEntityResults(matches) {
308 + const results = document.getElementById("entity-results");
309 + results.innerHTML = "";
310 + activeResultIndex = -1;
311 +
312 + if (!matches.length) {
313 + results.hidden = true;
314 + return;
315 + }
316 +
317 + matches.forEach((entity) => {
318 + const item = document.createElement("li");
319 + const button = document.createElement("button");
320 + const meta = document.createElement("span");
321 +
322 + button.type = "button";
323 + button.className = "result-button";
324 + button.textContent = entity.desc_entidad;
325 + button.addEventListener("click", () => selectEntity(entity));
326 +
327 + meta.className = "meta";
328 + meta.textContent = `Código ${entity.entidad}`;
329 + button.appendChild(document.createElement("br"));
330 + button.appendChild(meta);
331 +
332 + item.appendChild(button);
333 + results.appendChild(item);
334 + });
335 +
336 + results.hidden = false;
337 + }
338 +
339 + function moveActiveResult(direction) {
340 + const buttons = resultButtons();
341 +
342 + if (!buttons.length) {
343 + return;
344 + }
345 +
346 + if (activeResultIndex === -1) {
347 + setActiveResult(direction > 0 ? 0 : buttons.length - 1);
348 + return;
349 + }
350 +
351 + const nextIndex = (activeResultIndex + direction + buttons.length) % buttons.length;
352 + setActiveResult(nextIndex);
353 + }
354 +
355 + function updateEntitySearch() {
356 + const query = normalizeText(document.getElementById("entity-search").value);
357 +
358 + document.getElementById("entity-downloads").hidden = true;
359 +
360 + if (!entities.length) {
361 + return;
362 + }
363 +
364 + if (!query) {
365 + setStatus("");
366 + document.getElementById("entity-results").hidden = true;
367 + activeResultIndex = -1;
368 + return;
369 + }
370 +
371 + const matches = entities
372 + .filter((entity) => entity.normalizedName.includes(query))
373 + .slice(0, MAX_ENTITY_RESULTS);
374 +
375 + if (!matches.length) {
376 + setStatus("Sin resultados.", "error");
377 + document.getElementById("entity-results").hidden = true;
378 + activeResultIndex = -1;
379 + return;
380 + }
381 +
382 + setStatus(`${matches.length} resultado(s).`);
383 + renderEntityResults(matches);
384 + }
385 +
386 + async function loadEntities() {
387 + try {
388 + const response = await fetch(ENTITY_API);
389 +
390 + if (!response.ok) {
391 + throw new Error(`HTTP ${response.status}`);
392 + }
393 +
394 + const data = await response.json();
395 +
396 + entities = data
397 + .filter((entity) => entity && entity.entidad && entity.desc_entidad)
398 + .map((entity) => ({
399 + entidad: entity.entidad,
400 + desc_entidad: entity.desc_entidad,
401 + normalizedName: normalizeText(entity.desc_entidad)
402 + }));
403 +
404 + setStatus("");
405 + } catch (error) {
406 + setStatus("No se pudieron cargar las entidades.", "error");
407 + }
408 + }
409 +
410 + function rerenderDownloads() {
411 + renderFullDownload();
412 + renderYearDownloads();
413 +
414 + const selectedName = document.getElementById("entity-search").value;
415 + const selectedEntity = entities.find(
416 + (entity) => entity.desc_entidad === selectedName
417 + );
418 +
419 + if (selectedEntity) {
420 + renderEntityDownloads(selectedEntity);
421 + }
422 + }
423 +
424 + document.querySelectorAll('input[name="dataset"]').forEach((input) => {
425 + input.addEventListener("change", rerenderDownloads);
426 + });
427 + document.getElementById("entity-search").addEventListener("input", updateEntitySearch);
428 + document.getElementById("entity-search").addEventListener("keydown", (event) => {
429 + const results = document.getElementById("entity-results");
430 +
431 + if (event.key === "ArrowDown") {
432 + event.preventDefault();
433 + if (!results.hidden) {
434 + moveActiveResult(1);
435 + }
436 + }
437 +
438 + if (event.key === "ArrowUp") {
439 + event.preventDefault();
440 + if (!results.hidden) {
441 + moveActiveResult(-1);
442 + }
443 + }
444 +
445 + if (event.key === "Enter") {
446 + const buttons = resultButtons();
447 + if (!results.hidden && activeResultIndex >= 0 && buttons[activeResultIndex]) {
448 + event.preventDefault();
449 + buttons[activeResultIndex].click();
450 + }
451 + }
452 +
453 + if (event.key === "Escape") {
454 + results.hidden = true;
455 + activeResultIndex = -1;
456 + }
457 + });
458 +
459 + renderFullDownload();
460 + renderYearDownloads();
461 + loadEntities();
462 + </script>
463 +</body>
464 +</html>