Showing
43 changed files
with
1169 additions
and
1218 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 ''" | ... | ... |
src/lib/api.js
0 → 100644
| 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'; | 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 | 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'; | 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'; | 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/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'; | 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/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'; | 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 | 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'; | 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> |
| 96 | - <!-- Header --> | 25 | + <div class="clas-container"> |
| 97 | - <header class="page-header"> | 26 | + <!-- Header --> |
| 98 | - <nav class="breadcrumb"> | 27 | + <div class="clas-header-section"> |
| 99 | - <a href="/" class="breadcrumb-link sm-hidden">← Inicio</a> | 28 | + <div class="clas-header-row"> |
| 100 | - <div class="breadcrumb-full"> | 29 | + <div> |
| 101 | - <a href="/">Inicio</a> | 30 | + <p class="section-tag">CLASIFICADORES</p> |
| 102 | - <span class="breadcrumb-sep">/</span> | 31 | + <h2 class="section-title">Elige cómo explorar el presupuesto</h2> |
| 103 | - <span class="breadcrumb-current">Clasificadores</span> | 32 | + <p class="clas-subtitle">Navega por cada una de las 2,626 piezas de los 8 clasificadores.</p> |
| 33 | + </div> | ||
| 104 | </div> | 34 | </div> |
| 105 | - </nav> | ||
| 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> | 35 | </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> | 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> |
| 121 | - <div class="group-header"> | 42 | + </div> |
| 122 | - <div class="group-icon" style="--c: var(--theme-titulo)"> | 43 | + <div> |
| 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> | 44 | + <div class="clas-group-label">¿QUIÉN?</div> |
| 124 | - </div> | 45 | + <div class="clas-group-sub">La entidad que gasta o recibe</div> |
| 125 | - <div> | ||
| 126 | - <div class="group-label">¿QUIÉN?</div> | ||
| 127 | - <div class="group-sub">La entidad que gasta o recibe</div> | ||
| 128 | - </div> | ||
| 129 | </div> | 46 | </div> |
| 130 | - | ||
| 131 | - <a | ||
| 132 | - href="/clasificadores/institucional" | ||
| 133 | - class="widget-card widget-h" | ||
| 134 | - class:wh={hovered === 'quien'} | ||
| 135 | - on:mouseenter={() => hovered = 'quien'} | ||
| 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> | ||
| 141 | - </div> | ||
| 142 | - <div class="widget-h-right"> | ||
| 143 | - <span class="widget-tech">INSTITUCIONAL</span> | ||
| 144 | - <button class="help-btn help-sm" on:click={(e) => openModal(e, 'quien')}>?</button> | ||
| 145 | - <span class="widget-arrow" class:arrow-show={hovered === 'quien'}>→</span> | ||
| 146 | - </div> | ||
| 147 | - </a> | ||
| 148 | </div> | 47 | </div> |
| 48 | + <a href="/clasificadores/institucional" class="widget-card widget-h" class:widget-hover={hWidget === 'quien'} onmouseenter={() => hWidget = 'quien'} onmouseleave={() => hWidget = null}> | ||
| 49 | + <div class="widget-left"> | ||
| 50 | + <div class="widget-headline"> | ||
| 51 | + <span class="widget-num-big">756</span> | ||
| 52 | + <span class="widget-label">instituciones públicas</span> | ||
| 53 | + </div> | ||
| 54 | + <div class="widget-desc">Ministerios, gobernaciones, municipios, empresas públicas, universidades</div> | ||
| 55 | + </div> | ||
| 56 | + <div class="widget-right"> | ||
| 57 | + <span class="widget-tech">INSTITUCIONAL</span> | ||
| 58 | + <span class="widget-arrow" class:arrow-show={hWidget === 'quien'}>→</span> | ||
| 59 | + </div> | ||
| 60 | + </a> | ||
| 149 | </div> | 61 | </div> |
| 150 | 62 | ||
| 151 | - <!-- ═══ INGRESOS + GASTOS ═══ --> | 63 | + <!-- Grid: Ingresos + Gastos --> |
| 152 | - <div class="main-grid"> | 64 | + <div class="clas-main-grid"> |
| 153 | - | ||
| 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} | ||
| 175 | - on:mouseleave={() => hovered = null} | ||
| 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> | 82 | </div> |
| 181 | - <p class="widget-desc widget-desc-grow">{w.d}</p> | 83 | + <p class="widget-desc">{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"> | 112 | + </div> |
| 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> | 113 | + <div> |
| 215 | - </div> | 114 | + <div class="clas-subgroup-label">¿EN QUÉ?</div> |
| 216 | - <div> | ||
| 217 | - <div class="sub-group-label">¿En qué se gasta?</div> | ||
| 218 | - <div class="sub-group-tag">DOS CLASIFICADORES</div> | ||
| 219 | - </div> | ||
| 220 | </div> | 115 | </div> |
| 221 | </div> | 116 | </div> |
| 222 | - | 117 | + <div class="widgets-row"> |
| 223 | - <div class="sub-grid"> | 118 | + {#each WIDGETS_GASTOS_QUE as w} |
| 224 | - {#each WIDGETS_EN_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}> |
| 225 | - <a | 120 | + <div class="widget-headline"> |
| 226 | - href={w.href} | 121 | + <span class="widget-num-big widget-num-green">{w.n}</span> |
| 227 | - class="widget-card sub-card" | 122 | + <span class="widget-label">{w.label}</span> |
| 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> | 123 | </div> |
| 236 | - <p class="widget-desc sub-desc">{w.d}</p> | 124 | + <p class="widget-desc widget-desc-sm">{w.d}</p> |
| 237 | - <div class="widget-footer sub-footer"> | 125 | + <div class="widget-footer"> |
| 238 | - <span class="widget-tech sub-tech">{w.t}</span> | 126 | + <span class="widget-tech">{w.t}</span> |
| 239 | - <span class="widget-arrow" class:arrow-show={hovered === w.id}>→</span> | 127 | + <span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span> |
| 240 | </div> | 128 | </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} | 140 | + </div> |
| 254 | - on:mouseenter={() => hovered = w.id} | 141 | + <div> |
| 255 | - on:mouseleave={() => hovered = null} | 142 | + <div class="clas-subgroup-label">¿PARA QUÉ?</div> |
| 256 | - > | 143 | + </div> |
| 257 | - <div class="widget-top"> | 144 | + </div> |
| 258 | - <h4 class="widget-q">{w.q}</h4> | 145 | + <a href="/clasificadores/funcional" class="widget-card widget-full" class:widget-hover={hWidget === 'finalidad'} onmouseenter={() => hWidget = 'finalidad'} onmouseleave={() => hWidget = null}> |
| 259 | - <button class="help-btn" on:click={(e) => openModal(e, w.id)}>?</button> | 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> | ||
| 151 | + <div class="widget-footer"> | ||
| 152 | + <span class="widget-tech">Finalidad</span> | ||
| 153 | + <span class="widget-arrow" class:arrow-show={hWidget === 'finalidad'}>→</span> | ||
| 154 | + </div> | ||
| 155 | + </a> | ||
| 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> | ||
| 162 | + </div> | ||
| 163 | + <div> | ||
| 164 | + <div class="clas-subgroup-label">¿DÓNDE?</div> | ||
| 165 | + </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> | ||
| 260 | </div> | 171 | </div> |
| 261 | - <p class="widget-desc">{w.d}</p> | 172 | + <p class="widget-desc">Departamentos, municipios</p> |
| 262 | <div class="widget-footer"> | 173 | <div class="widget-footer"> |
| 263 | - <span class="widget-tech">{w.t}</span> | 174 | + <span class="widget-tech">Geográfico</span> |
| 264 | - <span class="widget-arrow" class:arrow-show={hovered === w.id}>→</span> | 175 | + <span class="widget-arrow" class:arrow-show={hWidget === 'donde'}>→</span> |
| 265 | </div> | 176 | </div> |
| 266 | - <div class="widget-glow widget-glow-green"></div> | ||
| 267 | </a> | 177 | </a> |
| 268 | - {/each} | 178 | + </div> |
| 269 | </div> | 179 | </div> |
| 270 | </div> | 180 | </div> |
| 271 | </div> | 181 | </div> |
| 272 | </div> | 182 | </div> |
| 273 | </div> | 183 | </div> |
| 274 | - </main> | ||
| 275 | -</div> | ||
| 276 | - | ||
| 277 | -<!-- ═══ MODAL ═══ --> | ||
| 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> | ||
| 306 | - </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> | ||
| 316 | - </div> | ||
| 317 | - | ||
| 318 | - <div class="modal-example"> | ||
| 319 | - <p>{modal.example}</p> | ||
| 320 | - </div> | ||
| 321 | - </div> | ||
| 322 | </div> | 184 | </div> |
| 323 | -{/if} | 185 | +</div> |
| 324 | - | ||
| 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) --> | ... | ... |
src/routes/descargas/+page.svelte
0 → 100644
| 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; | ... | ... |
static/descargas.html
0 → 100644
| 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> |
-
Please register or login to post a comment