Showing
24 changed files
with
4906 additions
and
251 deletions
| ... | @@ -8,9 +8,11 @@ | ... | @@ -8,9 +8,11 @@ |
| 8 | "name": "ppto", | 8 | "name": "ppto", |
| 9 | "version": "0.0.1", | 9 | "version": "0.0.1", |
| 10 | "dependencies": { | 10 | "dependencies": { |
| 11 | + "@observablehq/plot": "^0.6.17", | ||
| 11 | "@supabase/supabase-js": "^2.98.0", | 12 | "@supabase/supabase-js": "^2.98.0", |
| 12 | "d3": "^7.9.0", | 13 | "d3": "^7.9.0", |
| 13 | "fuse.js": "^7.1.0", | 14 | "fuse.js": "^7.1.0", |
| 15 | + "hyparquet": "^1.25.1", | ||
| 14 | "marked": "^17.0.4" | 16 | "marked": "^17.0.4" |
| 15 | }, | 17 | }, |
| 16 | "devDependencies": { | 18 | "devDependencies": { |
| ... | @@ -515,6 +517,20 @@ | ... | @@ -515,6 +517,20 @@ |
| 515 | "@jridgewell/sourcemap-codec": "^1.4.14" | 517 | "@jridgewell/sourcemap-codec": "^1.4.14" |
| 516 | } | 518 | } |
| 517 | }, | 519 | }, |
| 520 | + "node_modules/@observablehq/plot": { | ||
| 521 | + "version": "0.6.17", | ||
| 522 | + "resolved": "https://registry.npmjs.org/@observablehq/plot/-/plot-0.6.17.tgz", | ||
| 523 | + "integrity": "sha512-/qaXP/7mc4MUS0s4cPPFASDRjtsWp85/TbfsciqDgU1HwYixbSbbytNuInD8AcTYC3xaxACgVX06agdfQy9W+g==", | ||
| 524 | + "license": "ISC", | ||
| 525 | + "dependencies": { | ||
| 526 | + "d3": "^7.9.0", | ||
| 527 | + "interval-tree-1d": "^1.0.0", | ||
| 528 | + "isoformat": "^0.2.0" | ||
| 529 | + }, | ||
| 530 | + "engines": { | ||
| 531 | + "node": ">=12" | ||
| 532 | + } | ||
| 533 | + }, | ||
| 518 | "node_modules/@polka/url": { | 534 | "node_modules/@polka/url": { |
| 519 | "version": "1.0.0-next.29", | 535 | "version": "1.0.0-next.29", |
| 520 | "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", | 536 | "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", |
| ... | @@ -1410,6 +1426,12 @@ | ... | @@ -1410,6 +1426,12 @@ |
| 1410 | "node": ">= 0.4" | 1426 | "node": ">= 0.4" |
| 1411 | } | 1427 | } |
| 1412 | }, | 1428 | }, |
| 1429 | + "node_modules/binary-search-bounds": { | ||
| 1430 | + "version": "2.0.5", | ||
| 1431 | + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", | ||
| 1432 | + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", | ||
| 1433 | + "license": "MIT" | ||
| 1434 | + }, | ||
| 1413 | "node_modules/clsx": { | 1435 | "node_modules/clsx": { |
| 1414 | "version": "2.1.1", | 1436 | "version": "2.1.1", |
| 1415 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", | 1437 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", |
| ... | @@ -1998,6 +2020,12 @@ | ... | @@ -1998,6 +2020,12 @@ |
| 1998 | "dev": true, | 2020 | "dev": true, |
| 1999 | "license": "ISC" | 2021 | "license": "ISC" |
| 2000 | }, | 2022 | }, |
| 2023 | + "node_modules/hyparquet": { | ||
| 2024 | + "version": "1.25.1", | ||
| 2025 | + "resolved": "https://registry.npmjs.org/hyparquet/-/hyparquet-1.25.1.tgz", | ||
| 2026 | + "integrity": "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w==", | ||
| 2027 | + "license": "MIT" | ||
| 2028 | + }, | ||
| 2001 | "node_modules/iceberg-js": { | 2029 | "node_modules/iceberg-js": { |
| 2002 | "version": "0.8.1", | 2030 | "version": "0.8.1", |
| 2003 | "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", | 2031 | "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", |
| ... | @@ -2028,6 +2056,15 @@ | ... | @@ -2028,6 +2056,15 @@ |
| 2028 | "node": ">=12" | 2056 | "node": ">=12" |
| 2029 | } | 2057 | } |
| 2030 | }, | 2058 | }, |
| 2059 | + "node_modules/interval-tree-1d": { | ||
| 2060 | + "version": "1.0.4", | ||
| 2061 | + "resolved": "https://registry.npmjs.org/interval-tree-1d/-/interval-tree-1d-1.0.4.tgz", | ||
| 2062 | + "integrity": "sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==", | ||
| 2063 | + "license": "MIT", | ||
| 2064 | + "dependencies": { | ||
| 2065 | + "binary-search-bounds": "^2.0.0" | ||
| 2066 | + } | ||
| 2067 | + }, | ||
| 2031 | "node_modules/is-reference": { | 2068 | "node_modules/is-reference": { |
| 2032 | "version": "3.0.3", | 2069 | "version": "3.0.3", |
| 2033 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", | 2070 | "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", |
| ... | @@ -2038,6 +2075,12 @@ | ... | @@ -2038,6 +2075,12 @@ |
| 2038 | "@types/estree": "^1.0.6" | 2075 | "@types/estree": "^1.0.6" |
| 2039 | } | 2076 | } |
| 2040 | }, | 2077 | }, |
| 2078 | + "node_modules/isoformat": { | ||
| 2079 | + "version": "0.2.1", | ||
| 2080 | + "resolved": "https://registry.npmjs.org/isoformat/-/isoformat-0.2.1.tgz", | ||
| 2081 | + "integrity": "sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==", | ||
| 2082 | + "license": "ISC" | ||
| 2083 | + }, | ||
| 2041 | "node_modules/jiti": { | 2084 | "node_modules/jiti": { |
| 2042 | "version": "2.6.1", | 2085 | "version": "2.6.1", |
| 2043 | "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", | 2086 | "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", | ... | ... |
| ... | @@ -19,9 +19,11 @@ | ... | @@ -19,9 +19,11 @@ |
| 19 | "vite": "^7.3.1" | 19 | "vite": "^7.3.1" |
| 20 | }, | 20 | }, |
| 21 | "dependencies": { | 21 | "dependencies": { |
| 22 | + "@observablehq/plot": "^0.6.17", | ||
| 22 | "@supabase/supabase-js": "^2.98.0", | 23 | "@supabase/supabase-js": "^2.98.0", |
| 23 | "d3": "^7.9.0", | 24 | "d3": "^7.9.0", |
| 24 | "fuse.js": "^7.1.0", | 25 | "fuse.js": "^7.1.0", |
| 26 | + "hyparquet": "^1.25.1", | ||
| 25 | "marked": "^17.0.4" | 27 | "marked": "^17.0.4" |
| 26 | } | 28 | } |
| 27 | } | 29 | } | ... | ... |
| ... | @@ -32,8 +32,14 @@ | ... | @@ -32,8 +32,14 @@ |
| 32 | }); | 32 | }); |
| 33 | </script> | 33 | </script> |
| 34 | 34 | ||
| 35 | -<!-- MÓVIL: Búsqueda + Theme + hamburguesa arriba a la derecha --> | 35 | +<!-- MÓVIL: Home + Búsqueda + Theme + hamburguesa arriba a la derecha --> |
| 36 | <div class="navbar-mobile"> | 36 | <div class="navbar-mobile"> |
| 37 | + <a href="/" class="nav-home-link" aria-label="Ir al inicio"> | ||
| 38 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 39 | + <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> | ||
| 40 | + <polyline points="9 22 9 12 15 12 15 22"/> | ||
| 41 | + </svg> | ||
| 42 | + </a> | ||
| 37 | <button | 43 | <button |
| 38 | onclick={onOpenSearch} | 44 | onclick={onOpenSearch} |
| 39 | class="nav-btn-icon-mobile" | 45 | class="nav-btn-icon-mobile" |
| ... | @@ -70,10 +76,25 @@ | ... | @@ -70,10 +76,25 @@ |
| 70 | </button> | 76 | </button> |
| 71 | </div> | 77 | </div> |
| 72 | 78 | ||
| 73 | -<!-- DESKTOP: Barra completa con Search + Theme + Menu --> | 79 | +<!-- DESKTOP: Barra completa con Home + Search + Theme + Menu --> |
| 74 | <div class="navbar-desktop"> | 80 | <div class="navbar-desktop"> |
| 75 | <div class="navbar-desktop-inner"> | 81 | <div class="navbar-desktop-inner"> |
| 76 | 82 | ||
| 83 | + <!-- BOTÓN HOME --> | ||
| 84 | + <div class="relative group/home"> | ||
| 85 | + <a href="/" class="nav-btn-icon" aria-label="Ir al inicio"> | ||
| 86 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 87 | + <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> | ||
| 88 | + <polyline points="9 22 9 12 15 12 15 22"/> | ||
| 89 | + </svg> | ||
| 90 | + </a> | ||
| 91 | + <!-- Tooltip --> | ||
| 92 | + <div class="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover/home:opacity-100 group-hover/home:visible transition-all duration-200 pointer-events-none backdrop-blur-sm"> | ||
| 93 | + Inicio | ||
| 94 | + <div class="absolute right-3 bottom-full mb-[-4px] w-2 h-2 bg-gray-900/90 rotate-45"></div> | ||
| 95 | + </div> | ||
| 96 | + </div> | ||
| 97 | + | ||
| 77 | <!-- BOTÓN BÚSQUEDA GLOBAL --> | 98 | <!-- BOTÓN BÚSQUEDA GLOBAL --> |
| 78 | <div class="relative group/search"> | 99 | <div class="relative group/search"> |
| 79 | <button | 100 | <button |
| ... | @@ -152,6 +173,13 @@ | ... | @@ -152,6 +173,13 @@ |
| 152 | z-index: 110; | 173 | z-index: 110; |
| 153 | align-items: center; | 174 | align-items: center; |
| 154 | gap: 0.25rem; | 175 | gap: 0.25rem; |
| 176 | + padding: 6px; | ||
| 177 | + background: rgba(40, 40, 38, 0.7); | ||
| 178 | + backdrop-filter: blur(20px) saturate(180%); | ||
| 179 | + -webkit-backdrop-filter: blur(20px) saturate(180%); | ||
| 180 | + border: 1px solid rgba(255, 255, 255, 0.1); | ||
| 181 | + border-radius: 14px; | ||
| 182 | + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15); | ||
| 155 | } | 183 | } |
| 156 | 184 | ||
| 157 | .navbar-desktop { | 185 | .navbar-desktop { |
| ... | @@ -160,6 +188,20 @@ | ... | @@ -160,6 +188,20 @@ |
| 160 | top: 1.5rem; | 188 | top: 1.5rem; |
| 161 | right: 1.5rem; | 189 | right: 1.5rem; |
| 162 | z-index: 110; | 190 | z-index: 110; |
| 191 | + padding: 6px; | ||
| 192 | + background: rgba(40, 40, 38, 0.7); | ||
| 193 | + backdrop-filter: blur(20px) saturate(180%); | ||
| 194 | + -webkit-backdrop-filter: blur(20px) saturate(180%); | ||
| 195 | + border: 1px solid rgba(255, 255, 255, 0.1); | ||
| 196 | + border-radius: 14px; | ||
| 197 | + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15); | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + :global(html:not(.dark)) .navbar-mobile, | ||
| 201 | + :global(html:not(.dark)) .navbar-desktop { | ||
| 202 | + background: rgba(255, 255, 255, 0.7); | ||
| 203 | + border-color: rgba(0, 0, 0, 0.08); | ||
| 204 | + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); | ||
| 163 | } | 205 | } |
| 164 | 206 | ||
| 165 | .navbar-desktop-inner { | 207 | .navbar-desktop-inner { |
| ... | @@ -201,16 +243,16 @@ | ... | @@ -201,16 +243,16 @@ |
| 201 | align-items: center; | 243 | align-items: center; |
| 202 | gap: 10px; | 244 | gap: 10px; |
| 203 | padding: 7px 12px 7px 12px; | 245 | padding: 7px 12px 7px 12px; |
| 204 | - background: rgba(255, 255, 255, 0.05); | 246 | + background: rgba(255, 255, 255, 0.08); |
| 205 | - border: 1px solid rgba(255, 255, 255, 0.12); | 247 | + border: 1px solid rgba(255, 255, 255, 0.1); |
| 206 | border-radius: 10px; | 248 | border-radius: 10px; |
| 207 | - color: #9B9890; | 249 | + color: #B8B5AD; |
| 208 | cursor: pointer; | 250 | cursor: pointer; |
| 209 | transition: all 0.2s ease; | 251 | transition: all 0.2s ease; |
| 210 | } | 252 | } |
| 211 | 253 | ||
| 212 | .nav-search-btn:hover { | 254 | .nav-search-btn:hover { |
| 213 | - background: rgba(255, 255, 255, 0.1); | 255 | + background: rgba(255, 255, 255, 0.15); |
| 214 | border-color: rgba(201, 167, 81, 0.4); | 256 | border-color: rgba(201, 167, 81, 0.4); |
| 215 | color: #F5F0E8; | 257 | color: #F5F0E8; |
| 216 | } | 258 | } |
| ... | @@ -252,28 +294,28 @@ | ... | @@ -252,28 +294,28 @@ |
| 252 | align-items: center; | 294 | align-items: center; |
| 253 | justify-content: center; | 295 | justify-content: center; |
| 254 | padding: 8px; | 296 | padding: 8px; |
| 255 | - background: rgba(255, 255, 255, 0.05); | 297 | + background: rgba(255, 255, 255, 0.08); |
| 256 | - border: 1px solid rgba(255, 255, 255, 0.12); | 298 | + border: 1px solid rgba(255, 255, 255, 0.1); |
| 257 | border-radius: 10px; | 299 | border-radius: 10px; |
| 258 | - color: #9B9890; | 300 | + color: #B8B5AD; |
| 259 | cursor: pointer; | 301 | cursor: pointer; |
| 260 | transition: all 0.2s ease; | 302 | transition: all 0.2s ease; |
| 261 | } | 303 | } |
| 262 | 304 | ||
| 263 | .nav-btn-icon:hover { | 305 | .nav-btn-icon:hover { |
| 264 | - background: rgba(255, 255, 255, 0.1); | 306 | + background: rgba(255, 255, 255, 0.15); |
| 265 | border-color: rgba(201, 167, 81, 0.4); | 307 | border-color: rgba(201, 167, 81, 0.4); |
| 266 | color: #F5F0E8; | 308 | color: #F5F0E8; |
| 267 | } | 309 | } |
| 268 | 310 | ||
| 269 | :global(html:not(.dark)) .nav-btn-icon { | 311 | :global(html:not(.dark)) .nav-btn-icon { |
| 270 | - background: rgba(0, 0, 0, 0.04); | 312 | + background: rgba(0, 0, 0, 0.05); |
| 271 | - border-color: rgba(0, 0, 0, 0.12); | 313 | + border-color: rgba(0, 0, 0, 0.08); |
| 272 | - color: #666; | 314 | + color: #555; |
| 273 | } | 315 | } |
| 274 | 316 | ||
| 275 | :global(html:not(.dark)) .nav-btn-icon:hover { | 317 | :global(html:not(.dark)) .nav-btn-icon:hover { |
| 276 | - background: rgba(0, 0, 0, 0.08); | 318 | + background: rgba(0, 0, 0, 0.1); |
| 277 | border-color: rgba(201, 167, 81, 0.5); | 319 | border-color: rgba(201, 167, 81, 0.5); |
| 278 | color: #1A1A18; | 320 | color: #1A1A18; |
| 279 | } | 321 | } |
| ... | @@ -285,23 +327,66 @@ | ... | @@ -285,23 +327,66 @@ |
| 285 | box-shadow: 0 1px 2px rgba(0,0,0,0.1), inset 0 1px 0 rgba(255,255,255,0.8); | 327 | box-shadow: 0 1px 2px rgba(0,0,0,0.1), inset 0 1px 0 rgba(255,255,255,0.8); |
| 286 | } | 328 | } |
| 287 | 329 | ||
| 330 | + /* Home link - hereda estilos de nav-btn-icon */ | ||
| 331 | + .nav-home-link { | ||
| 332 | + display: flex; | ||
| 333 | + align-items: center; | ||
| 334 | + justify-content: center; | ||
| 335 | + padding: 8px; | ||
| 336 | + background: rgba(255, 255, 255, 0.08); | ||
| 337 | + border: 1px solid rgba(255, 255, 255, 0.1); | ||
| 338 | + border-radius: 10px; | ||
| 339 | + color: #B8B5AD; | ||
| 340 | + cursor: pointer; | ||
| 341 | + transition: all 0.2s ease; | ||
| 342 | + text-decoration: none; | ||
| 343 | + } | ||
| 344 | + | ||
| 345 | + .nav-home-link:hover { | ||
| 346 | + background: rgba(255, 255, 255, 0.15); | ||
| 347 | + border-color: rgba(201, 167, 81, 0.4); | ||
| 348 | + color: #F5F0E8; | ||
| 349 | + } | ||
| 350 | + | ||
| 351 | + :global(html:not(.dark)) .nav-home-link { | ||
| 352 | + background: rgba(0, 0, 0, 0.05); | ||
| 353 | + border-color: rgba(0, 0, 0, 0.08); | ||
| 354 | + color: #555; | ||
| 355 | + } | ||
| 356 | + | ||
| 357 | + :global(html:not(.dark)) .nav-home-link:hover { | ||
| 358 | + background: rgba(0, 0, 0, 0.1); | ||
| 359 | + border-color: rgba(201, 167, 81, 0.5); | ||
| 360 | + color: #1A1A18; | ||
| 361 | + } | ||
| 362 | + | ||
| 288 | /* Mobile icon buttons */ | 363 | /* Mobile icon buttons */ |
| 289 | .nav-btn-icon-mobile { | 364 | .nav-btn-icon-mobile { |
| 290 | display: flex; | 365 | display: flex; |
| 291 | align-items: center; | 366 | align-items: center; |
| 292 | justify-content: center; | 367 | justify-content: center; |
| 293 | padding: 8px; | 368 | padding: 8px; |
| 294 | - background: rgba(255, 255, 255, 0.05); | 369 | + background: rgba(255, 255, 255, 0.1); |
| 295 | - border: 1px solid rgba(255, 255, 255, 0.12); | 370 | + border: 1px solid rgba(255, 255, 255, 0.08); |
| 296 | border-radius: 8px; | 371 | border-radius: 8px; |
| 297 | - color: #9B9890; | 372 | + color: #B8B5AD; |
| 298 | cursor: pointer; | 373 | cursor: pointer; |
| 299 | transition: all 0.2s ease; | 374 | transition: all 0.2s ease; |
| 300 | } | 375 | } |
| 301 | 376 | ||
| 377 | + .nav-btn-icon-mobile:hover { | ||
| 378 | + background: rgba(255, 255, 255, 0.18); | ||
| 379 | + color: #F5F0E8; | ||
| 380 | + } | ||
| 381 | + | ||
| 302 | :global(html:not(.dark)) .nav-btn-icon-mobile { | 382 | :global(html:not(.dark)) .nav-btn-icon-mobile { |
| 303 | - background: rgba(0, 0, 0, 0.04); | 383 | + background: rgba(0, 0, 0, 0.06); |
| 304 | - border-color: rgba(0, 0, 0, 0.12); | 384 | + border-color: rgba(0, 0, 0, 0.06); |
| 305 | - color: #666; | 385 | + color: #555; |
| 386 | + } | ||
| 387 | + | ||
| 388 | + :global(html:not(.dark)) .nav-btn-icon-mobile:hover { | ||
| 389 | + background: rgba(0, 0, 0, 0.12); | ||
| 390 | + color: #1A1A18; | ||
| 306 | } | 391 | } |
| 307 | </style> | 392 | </style> | ... | ... |
| ... | @@ -24,7 +24,10 @@ | ... | @@ -24,7 +24,10 @@ |
| 24 | let searchFilters = $state({ | 24 | let searchFilters = $state({ |
| 25 | entidad: true, | 25 | entidad: true, |
| 26 | objeto_gasto: true, | 26 | objeto_gasto: true, |
| 27 | - rubro: true | 27 | + rubro: true, |
| 28 | + finfun: true, | ||
| 29 | + organismo: true, | ||
| 30 | + fuente: true | ||
| 28 | }); | 31 | }); |
| 29 | 32 | ||
| 30 | // Filtered results based on active filters | 33 | // Filtered results based on active filters |
| ... | @@ -90,6 +93,12 @@ | ... | @@ -90,6 +93,12 @@ |
| 90 | goto(`/objeto/${item.codigo}`); | 93 | goto(`/objeto/${item.codigo}`); |
| 91 | } else if (item.tipo === 'rubro') { | 94 | } else if (item.tipo === 'rubro') { |
| 92 | goto(`/rubro/${item.codigo}`); | 95 | goto(`/rubro/${item.codigo}`); |
| 96 | + } else if (item.tipo === 'finfun') { | ||
| 97 | + goto(`/finfun/${item.codigo}`); | ||
| 98 | + } else if (item.tipo === 'organismo') { | ||
| 99 | + goto(`/organismo/${item.codigo}`); | ||
| 100 | + } else if (item.tipo === 'fuente') { | ||
| 101 | + goto(`/fuente/${item.codigo}`); | ||
| 93 | } | 102 | } |
| 94 | } | 103 | } |
| 95 | 104 | ||
| ... | @@ -99,11 +108,38 @@ | ... | @@ -99,11 +108,38 @@ |
| 99 | } | 108 | } |
| 100 | } | 109 | } |
| 101 | 110 | ||
| 102 | - // Type labels and colors | 111 | + // Type labels, colors and descriptions |
| 103 | const typeConfig = { | 112 | const typeConfig = { |
| 104 | - entidad: { label: 'Institución', color: '#C9A751' }, | 113 | + entidad: { |
| 105 | - objeto_gasto: { label: 'Objeto de gasto', color: '#6B9F78' }, | 114 | + label: 'Institución', |
| 106 | - rubro: { label: 'Rubro', color: '#7B8EC9' } | 115 | + color: '#C9A751', |
| 116 | + description: 'Ministerios, municipios, universidades...' | ||
| 117 | + }, | ||
| 118 | + objeto_gasto: { | ||
| 119 | + label: 'Objeto de gasto', | ||
| 120 | + color: '#6B9F78', | ||
| 121 | + description: 'Sueldos, viáticos, servicios básicos...' | ||
| 122 | + }, | ||
| 123 | + rubro: { | ||
| 124 | + label: 'Rubro', | ||
| 125 | + color: '#7B8EC9', | ||
| 126 | + description: 'Impuestos, tasas, transferencias...' | ||
| 127 | + }, | ||
| 128 | + finfun: { | ||
| 129 | + label: 'Finalidad', | ||
| 130 | + color: '#C97B8E', | ||
| 131 | + description: 'Manufacturas, construcción, transporte...' | ||
| 132 | + }, | ||
| 133 | + organismo: { | ||
| 134 | + label: 'Organismo', | ||
| 135 | + color: '#9B7BC9', | ||
| 136 | + description: 'Tipo de entidad: central, descentralizada...' | ||
| 137 | + }, | ||
| 138 | + fuente: { | ||
| 139 | + label: 'Fuente', | ||
| 140 | + color: '#7BC9A1', | ||
| 141 | + description: 'Origen del financiamiento: TGN, créditos...' | ||
| 142 | + } | ||
| 107 | }; | 143 | }; |
| 108 | </script> | 144 | </script> |
| 109 | 145 | ||
| ... | @@ -129,32 +165,85 @@ | ... | @@ -129,32 +165,85 @@ |
| 129 | </button> | 165 | </button> |
| 130 | </div> | 166 | </div> |
| 131 | 167 | ||
| 132 | - <!-- Filters --> | 168 | + <!-- Filters section with explicit descriptions --> |
| 133 | <div class="search-modal-filters"> | 169 | <div class="search-modal-filters"> |
| 134 | - <button | 170 | + <div class="filters-header"> |
| 135 | - class="search-filter-chip" | 171 | + <span class="filters-label">Buscar en:</span> |
| 136 | - class:filter-active={searchFilters.entidad} | 172 | + </div> |
| 137 | - onclick={() => toggleFilter('entidad')} | 173 | + <div class="filters-grid"> |
| 138 | - > | 174 | + <button |
| 139 | - <span class="filter-dot" style="background:{typeConfig.entidad.color}"></span> | 175 | + class="search-filter-card" |
| 140 | - Instituciones | 176 | + class:filter-active={searchFilters.entidad} |
| 141 | - </button> | 177 | + onclick={() => toggleFilter('entidad')} |
| 142 | - <button | 178 | + > |
| 143 | - class="search-filter-chip" | 179 | + <div class="filter-card-header"> |
| 144 | - class:filter-active={searchFilters.objeto_gasto} | 180 | + <span class="filter-dot" style="background:{typeConfig.entidad.color}"></span> |
| 145 | - onclick={() => toggleFilter('objeto_gasto')} | 181 | + <span class="filter-title">Instituciones</span> |
| 146 | - > | 182 | + <span class="filter-check">{searchFilters.entidad ? '✓' : ''}</span> |
| 147 | - <span class="filter-dot" style="background:{typeConfig.objeto_gasto.color}"></span> | 183 | + </div> |
| 148 | - Objetos de gasto | 184 | + <span class="filter-desc">{typeConfig.entidad.description}</span> |
| 149 | - </button> | 185 | + </button> |
| 150 | - <button | 186 | + <button |
| 151 | - class="search-filter-chip" | 187 | + class="search-filter-card" |
| 152 | - class:filter-active={searchFilters.rubro} | 188 | + class:filter-active={searchFilters.objeto_gasto} |
| 153 | - onclick={() => toggleFilter('rubro')} | 189 | + onclick={() => toggleFilter('objeto_gasto')} |
| 154 | - > | 190 | + > |
| 155 | - <span class="filter-dot" style="background:{typeConfig.rubro.color}"></span> | 191 | + <div class="filter-card-header"> |
| 156 | - Rubros | 192 | + <span class="filter-dot" style="background:{typeConfig.objeto_gasto.color}"></span> |
| 157 | - </button> | 193 | + <span class="filter-title">Gastos</span> |
| 194 | + <span class="filter-check">{searchFilters.objeto_gasto ? '✓' : ''}</span> | ||
| 195 | + </div> | ||
| 196 | + <span class="filter-desc">{typeConfig.objeto_gasto.description}</span> | ||
| 197 | + </button> | ||
| 198 | + <button | ||
| 199 | + class="search-filter-card" | ||
| 200 | + class:filter-active={searchFilters.rubro} | ||
| 201 | + onclick={() => toggleFilter('rubro')} | ||
| 202 | + > | ||
| 203 | + <div class="filter-card-header"> | ||
| 204 | + <span class="filter-dot" style="background:{typeConfig.rubro.color}"></span> | ||
| 205 | + <span class="filter-title">Ingresos</span> | ||
| 206 | + <span class="filter-check">{searchFilters.rubro ? '✓' : ''}</span> | ||
| 207 | + </div> | ||
| 208 | + <span class="filter-desc">{typeConfig.rubro.description}</span> | ||
| 209 | + </button> | ||
| 210 | + <button | ||
| 211 | + class="search-filter-card" | ||
| 212 | + class:filter-active={searchFilters.finfun} | ||
| 213 | + onclick={() => toggleFilter('finfun')} | ||
| 214 | + > | ||
| 215 | + <div class="filter-card-header"> | ||
| 216 | + <span class="filter-dot" style="background:{typeConfig.finfun.color}"></span> | ||
| 217 | + <span class="filter-title">Finalidad</span> | ||
| 218 | + <span class="filter-check">{searchFilters.finfun ? '✓' : ''}</span> | ||
| 219 | + </div> | ||
| 220 | + <span class="filter-desc">{typeConfig.finfun.description}</span> | ||
| 221 | + </button> | ||
| 222 | + <button | ||
| 223 | + class="search-filter-card" | ||
| 224 | + class:filter-active={searchFilters.organismo} | ||
| 225 | + onclick={() => toggleFilter('organismo')} | ||
| 226 | + > | ||
| 227 | + <div class="filter-card-header"> | ||
| 228 | + <span class="filter-dot" style="background:{typeConfig.organismo.color}"></span> | ||
| 229 | + <span class="filter-title">Organismos</span> | ||
| 230 | + <span class="filter-check">{searchFilters.organismo ? '✓' : ''}</span> | ||
| 231 | + </div> | ||
| 232 | + <span class="filter-desc">{typeConfig.organismo.description}</span> | ||
| 233 | + </button> | ||
| 234 | + <button | ||
| 235 | + class="search-filter-card" | ||
| 236 | + class:filter-active={searchFilters.fuente} | ||
| 237 | + onclick={() => toggleFilter('fuente')} | ||
| 238 | + > | ||
| 239 | + <div class="filter-card-header"> | ||
| 240 | + <span class="filter-dot" style="background:{typeConfig.fuente.color}"></span> | ||
| 241 | + <span class="filter-title">Fuentes</span> | ||
| 242 | + <span class="filter-check">{searchFilters.fuente ? '✓' : ''}</span> | ||
| 243 | + </div> | ||
| 244 | + <span class="filter-desc">{typeConfig.fuente.description}</span> | ||
| 245 | + </button> | ||
| 246 | + </div> | ||
| 158 | </div> | 247 | </div> |
| 159 | 248 | ||
| 160 | <!-- Results --> | 249 | <!-- Results --> |
| ... | @@ -164,15 +253,7 @@ | ... | @@ -164,15 +253,7 @@ |
| 164 | {:else if $isLoading} | 253 | {:else if $isLoading} |
| 165 | <div class="search-modal-status">Buscando...</div> | 254 | <div class="search-modal-status">Buscando...</div> |
| 166 | {:else if searchVal.length < 2} | 255 | {:else if searchVal.length < 2} |
| 167 | - <div class="search-modal-hint"> | 256 | + <div class="search-modal-empty"></div> |
| 168 | - <span class="hint-icon"> | ||
| 169 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 170 | - <circle cx="12" cy="12" r="10"/> | ||
| 171 | - <path d="M12 16v-4M12 8h.01"/> | ||
| 172 | - </svg> | ||
| 173 | - </span> | ||
| 174 | - <span>Escribe al menos 2 caracteres para buscar</span> | ||
| 175 | - </div> | ||
| 176 | {:else if filteredResults.length === 0} | 257 | {:else if filteredResults.length === 0} |
| 177 | <div class="search-modal-status">Sin resultados para "{searchVal}"</div> | 258 | <div class="search-modal-status">Sin resultados para "{searchVal}"</div> |
| 178 | {:else} | 259 | {:else} |
| ... | @@ -218,9 +299,7 @@ | ... | @@ -218,9 +299,7 @@ |
| 218 | .search-modal-backdrop { | 299 | .search-modal-backdrop { |
| 219 | position: fixed; | 300 | position: fixed; |
| 220 | inset: 0; | 301 | inset: 0; |
| 221 | - background: rgba(0, 0, 0, 0.6); | 302 | + background: rgba(0, 0, 0, 0.5); |
| 222 | - backdrop-filter: blur(4px); | ||
| 223 | - -webkit-backdrop-filter: blur(4px); | ||
| 224 | z-index: 9999; | 303 | z-index: 9999; |
| 225 | display: flex; | 304 | display: flex; |
| 226 | align-items: flex-start; | 305 | align-items: flex-start; |
| ... | @@ -231,13 +310,27 @@ | ... | @@ -231,13 +310,27 @@ |
| 231 | 310 | ||
| 232 | .search-modal { | 311 | .search-modal { |
| 233 | width: 100%; | 312 | width: 100%; |
| 234 | - max-width: 580px; | 313 | + max-width: 620px; |
| 235 | - background: var(--theme-body); | 314 | + background: rgba(40, 40, 38, 0.75); |
| 236 | - border: 1px solid var(--theme-borde); | 315 | + backdrop-filter: blur(40px) saturate(200%); |
| 316 | + -webkit-backdrop-filter: blur(40px) saturate(200%); | ||
| 317 | + border: 1px solid rgba(255, 255, 255, 0.15); | ||
| 237 | border-radius: 16px; | 318 | border-radius: 16px; |
| 238 | - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4); | 319 | + box-shadow: |
| 320 | + 0 25px 50px -12px rgba(0, 0, 0, 0.6), | ||
| 321 | + 0 0 0 1px rgba(255, 255, 255, 0.08) inset, | ||
| 322 | + 0 1px 0 rgba(255, 255, 255, 0.1) inset; | ||
| 239 | overflow: hidden; | 323 | overflow: hidden; |
| 240 | - animation: slideIn 0.2s ease-out; | 324 | + animation: slideIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); |
| 325 | + } | ||
| 326 | + | ||
| 327 | + :global(html:not(.dark)) .search-modal { | ||
| 328 | + background: rgba(255, 255, 255, 0.72); | ||
| 329 | + border-color: rgba(0, 0, 0, 0.08); | ||
| 330 | + box-shadow: | ||
| 331 | + 0 25px 50px -12px rgba(0, 0, 0, 0.25), | ||
| 332 | + 0 0 0 1px rgba(255, 255, 255, 0.5) inset, | ||
| 333 | + 0 1px 0 rgba(255, 255, 255, 0.8) inset; | ||
| 241 | } | 334 | } |
| 242 | 335 | ||
| 243 | @keyframes fadeIn { | 336 | @keyframes fadeIn { |
| ... | @@ -248,7 +341,7 @@ | ... | @@ -248,7 +341,7 @@ |
| 248 | @keyframes slideIn { | 341 | @keyframes slideIn { |
| 249 | from { | 342 | from { |
| 250 | opacity: 0; | 343 | opacity: 0; |
| 251 | - transform: translateY(-20px) scale(0.98); | 344 | + transform: translateY(-8px) scale(0.96); |
| 252 | } | 345 | } |
| 253 | to { | 346 | to { |
| 254 | opacity: 1; | 347 | opacity: 1; |
| ... | @@ -261,8 +354,12 @@ | ... | @@ -261,8 +354,12 @@ |
| 261 | display: flex; | 354 | display: flex; |
| 262 | align-items: center; | 355 | align-items: center; |
| 263 | gap: 12px; | 356 | gap: 12px; |
| 264 | - padding: 16px 20px; | 357 | + padding: 18px 20px; |
| 265 | - border-bottom: 1px solid var(--theme-borde); | 358 | + border-bottom: 1px solid rgba(255, 255, 255, 0.08); |
| 359 | + } | ||
| 360 | + | ||
| 361 | + :global(html:not(.dark)) .search-modal-header { | ||
| 362 | + border-bottom-color: rgba(0, 0, 0, 0.08); | ||
| 266 | } | 363 | } |
| 267 | 364 | ||
| 268 | .search-modal-icon { | 365 | .search-modal-icon { |
| ... | @@ -297,56 +394,125 @@ | ... | @@ -297,56 +394,125 @@ |
| 297 | .search-modal-close kbd { | 394 | .search-modal-close kbd { |
| 298 | font-family: inherit; | 395 | font-family: inherit; |
| 299 | font-size: 0.6875rem; | 396 | font-size: 0.6875rem; |
| 300 | - padding: 4px 8px; | 397 | + padding: 5px 10px; |
| 301 | - background: var(--theme-fill); | 398 | + background: rgba(255, 255, 255, 0.1); |
| 302 | - border: 1px solid var(--theme-borde); | 399 | + border: 1px solid rgba(255, 255, 255, 0.15); |
| 303 | border-radius: 6px; | 400 | border-radius: 6px; |
| 304 | color: var(--theme-texto); | 401 | color: var(--theme-texto); |
| 305 | - opacity: 0.7; | 402 | + opacity: 0.8; |
| 306 | - transition: opacity 0.15s; | 403 | + transition: all 0.15s; |
| 404 | + } | ||
| 405 | + | ||
| 406 | + :global(html:not(.dark)) .search-modal-close kbd { | ||
| 407 | + background: rgba(0, 0, 0, 0.06); | ||
| 408 | + border-color: rgba(0, 0, 0, 0.12); | ||
| 307 | } | 409 | } |
| 308 | 410 | ||
| 309 | .search-modal-close:hover kbd { | 411 | .search-modal-close:hover kbd { |
| 310 | opacity: 1; | 412 | opacity: 1; |
| 413 | + background: rgba(255, 255, 255, 0.15); | ||
| 414 | + } | ||
| 415 | + | ||
| 416 | + :global(html:not(.dark)) .search-modal-close:hover kbd { | ||
| 417 | + background: rgba(0, 0, 0, 0.1); | ||
| 311 | } | 418 | } |
| 312 | 419 | ||
| 313 | /* Filters */ | 420 | /* Filters */ |
| 314 | .search-modal-filters { | 421 | .search-modal-filters { |
| 315 | - display: flex; | 422 | + padding: 12px 16px; |
| 423 | + border-bottom: 1px solid rgba(255, 255, 255, 0.08); | ||
| 424 | + background: rgba(255, 255, 255, 0.03); | ||
| 425 | + } | ||
| 426 | + | ||
| 427 | + :global(html:not(.dark)) .search-modal-filters { | ||
| 428 | + border-bottom-color: rgba(0, 0, 0, 0.08); | ||
| 429 | + background: rgba(0, 0, 0, 0.02); | ||
| 430 | + } | ||
| 431 | + | ||
| 432 | + .filters-header { | ||
| 433 | + margin-bottom: 8px; | ||
| 434 | + } | ||
| 435 | + | ||
| 436 | + .filters-label { | ||
| 437 | + font-size: 0.6875rem; | ||
| 438 | + font-weight: 600; | ||
| 439 | + text-transform: uppercase; | ||
| 440 | + letter-spacing: 0.08em; | ||
| 441 | + color: var(--theme-texto); | ||
| 442 | + opacity: 0.5; | ||
| 443 | + } | ||
| 444 | + | ||
| 445 | + .filters-grid { | ||
| 446 | + display: grid; | ||
| 447 | + grid-template-columns: repeat(2, 1fr); | ||
| 316 | gap: 8px; | 448 | gap: 8px; |
| 317 | - padding: 12px 20px; | ||
| 318 | - border-bottom: 1px solid var(--theme-borde); | ||
| 319 | - background: var(--theme-fill); | ||
| 320 | } | 449 | } |
| 321 | 450 | ||
| 322 | - .search-filter-chip { | 451 | + .search-filter-card { |
| 323 | display: flex; | 452 | display: flex; |
| 324 | - align-items: center; | 453 | + flex-direction: column; |
| 325 | - gap: 6px; | 454 | + gap: 3px; |
| 326 | - padding: 6px 12px; | 455 | + padding: 8px 10px; |
| 327 | - background: var(--theme-body); | 456 | + background: rgba(255, 255, 255, 0.05); |
| 328 | - border: 1px solid var(--theme-borde); | 457 | + border: 1px solid rgba(255, 255, 255, 0.1); |
| 329 | - border-radius: 20px; | 458 | + border-radius: 8px; |
| 330 | - font-size: 0.8125rem; | ||
| 331 | - color: var(--theme-texto); | ||
| 332 | cursor: pointer; | 459 | cursor: pointer; |
| 333 | - transition: all 0.15s; | 460 | + transition: all 0.2s ease; |
| 461 | + text-align: left; | ||
| 334 | opacity: 0.5; | 462 | opacity: 0.5; |
| 335 | } | 463 | } |
| 336 | 464 | ||
| 337 | - .search-filter-chip:hover { | 465 | + :global(html:not(.dark)) .search-filter-card { |
| 466 | + background: rgba(0, 0, 0, 0.03); | ||
| 467 | + border-color: rgba(0, 0, 0, 0.1); | ||
| 468 | + } | ||
| 469 | + | ||
| 470 | + .search-filter-card:hover { | ||
| 338 | opacity: 0.8; | 471 | opacity: 0.8; |
| 472 | + background: rgba(255, 255, 255, 0.08); | ||
| 473 | + } | ||
| 474 | + | ||
| 475 | + :global(html:not(.dark)) .search-filter-card:hover { | ||
| 476 | + background: rgba(0, 0, 0, 0.06); | ||
| 339 | } | 477 | } |
| 340 | 478 | ||
| 341 | - .search-filter-chip.filter-active { | 479 | + .search-filter-card.filter-active { |
| 342 | opacity: 1; | 480 | opacity: 1; |
| 343 | - border-color: var(--theme-texto); | 481 | + border-color: rgba(201, 167, 81, 0.5); |
| 482 | + background: rgba(201, 167, 81, 0.1); | ||
| 483 | + } | ||
| 484 | + | ||
| 485 | + .filter-card-header { | ||
| 486 | + display: flex; | ||
| 487 | + align-items: center; | ||
| 488 | + gap: 6px; | ||
| 344 | } | 489 | } |
| 345 | 490 | ||
| 346 | .filter-dot { | 491 | .filter-dot { |
| 347 | width: 8px; | 492 | width: 8px; |
| 348 | height: 8px; | 493 | height: 8px; |
| 349 | border-radius: 50%; | 494 | border-radius: 50%; |
| 495 | + flex-shrink: 0; | ||
| 496 | + } | ||
| 497 | + | ||
| 498 | + .filter-title { | ||
| 499 | + font-size: 0.8125rem; | ||
| 500 | + font-weight: 600; | ||
| 501 | + color: var(--theme-titulo); | ||
| 502 | + flex: 1; | ||
| 503 | + } | ||
| 504 | + | ||
| 505 | + .filter-check { | ||
| 506 | + font-size: 0.6875rem; | ||
| 507 | + color: #C9A751; | ||
| 508 | + font-weight: 600; | ||
| 509 | + } | ||
| 510 | + | ||
| 511 | + .filter-desc { | ||
| 512 | + font-size: 0.625rem; | ||
| 513 | + color: var(--theme-texto); | ||
| 514 | + opacity: 0.6; | ||
| 515 | + line-height: 1.3; | ||
| 350 | } | 516 | } |
| 351 | 517 | ||
| 352 | /* Results */ | 518 | /* Results */ |
| ... | @@ -355,25 +521,15 @@ | ... | @@ -355,25 +521,15 @@ |
| 355 | overflow-y: auto; | 521 | overflow-y: auto; |
| 356 | } | 522 | } |
| 357 | 523 | ||
| 358 | - .search-modal-status, | 524 | + .search-modal-status { |
| 359 | - .search-modal-hint { | ||
| 360 | padding: 24px 20px; | 525 | padding: 24px 20px; |
| 361 | text-align: center; | 526 | text-align: center; |
| 362 | color: var(--theme-texto); | 527 | color: var(--theme-texto); |
| 363 | font-size: 0.875rem; | 528 | font-size: 0.875rem; |
| 364 | } | 529 | } |
| 365 | 530 | ||
| 366 | - .search-modal-hint { | 531 | + .search-modal-empty { |
| 367 | - display: flex; | 532 | + padding: 16px; |
| 368 | - align-items: center; | ||
| 369 | - justify-content: center; | ||
| 370 | - gap: 8px; | ||
| 371 | - opacity: 0.6; | ||
| 372 | - } | ||
| 373 | - | ||
| 374 | - .hint-icon { | ||
| 375 | - display: flex; | ||
| 376 | - opacity: 0.7; | ||
| 377 | } | 533 | } |
| 378 | 534 | ||
| 379 | .search-result-item { | 535 | .search-result-item { |
| ... | @@ -385,10 +541,14 @@ | ... | @@ -385,10 +541,14 @@ |
| 385 | padding: 12px 20px; | 541 | padding: 12px 20px; |
| 386 | background: none; | 542 | background: none; |
| 387 | border: none; | 543 | border: none; |
| 388 | - border-top: 1px solid var(--theme-borde); | 544 | + border-top: 1px solid rgba(255, 255, 255, 0.06); |
| 389 | cursor: pointer; | 545 | cursor: pointer; |
| 390 | text-align: left; | 546 | text-align: left; |
| 391 | - transition: background 0.1s; | 547 | + transition: background 0.15s ease; |
| 548 | + } | ||
| 549 | + | ||
| 550 | + :global(html:not(.dark)) .search-result-item { | ||
| 551 | + border-top-color: rgba(0, 0, 0, 0.06); | ||
| 392 | } | 552 | } |
| 393 | 553 | ||
| 394 | .search-result-item:first-child { | 554 | .search-result-item:first-child { |
| ... | @@ -397,7 +557,12 @@ | ... | @@ -397,7 +557,12 @@ |
| 397 | 557 | ||
| 398 | .search-result-item:hover, | 558 | .search-result-item:hover, |
| 399 | .search-result-item.result-selected { | 559 | .search-result-item.result-selected { |
| 400 | - background: var(--theme-fill); | 560 | + background: rgba(255, 255, 255, 0.08); |
| 561 | + } | ||
| 562 | + | ||
| 563 | + :global(html:not(.dark)) .search-result-item:hover, | ||
| 564 | + :global(html:not(.dark)) .search-result-item.result-selected { | ||
| 565 | + background: rgba(0, 0, 0, 0.05); | ||
| 401 | } | 566 | } |
| 402 | 567 | ||
| 403 | .result-main { | 568 | .result-main { |
| ... | @@ -437,10 +602,14 @@ | ... | @@ -437,10 +602,14 @@ |
| 437 | font-family: 'DM Mono', monospace; | 602 | font-family: 'DM Mono', monospace; |
| 438 | font-size: 0.75rem; | 603 | font-size: 0.75rem; |
| 439 | color: var(--theme-texto); | 604 | color: var(--theme-texto); |
| 440 | - opacity: 0.5; | 605 | + opacity: 0.6; |
| 441 | - padding: 4px 8px; | 606 | + padding: 4px 10px; |
| 442 | - background: var(--theme-fill); | 607 | + background: rgba(255, 255, 255, 0.08); |
| 443 | - border-radius: 4px; | 608 | + border-radius: 6px; |
| 609 | + } | ||
| 610 | + | ||
| 611 | + :global(html:not(.dark)) .result-code { | ||
| 612 | + background: rgba(0, 0, 0, 0.06); | ||
| 444 | } | 613 | } |
| 445 | 614 | ||
| 446 | /* Footer */ | 615 | /* Footer */ |
| ... | @@ -448,8 +617,13 @@ | ... | @@ -448,8 +617,13 @@ |
| 448 | display: flex; | 617 | display: flex; |
| 449 | gap: 16px; | 618 | gap: 16px; |
| 450 | padding: 12px 20px; | 619 | padding: 12px 20px; |
| 451 | - border-top: 1px solid var(--theme-borde); | 620 | + border-top: 1px solid rgba(255, 255, 255, 0.08); |
| 452 | - background: var(--theme-fill); | 621 | + background: rgba(255, 255, 255, 0.03); |
| 622 | + } | ||
| 623 | + | ||
| 624 | + :global(html:not(.dark)) .search-modal-footer { | ||
| 625 | + border-top-color: rgba(0, 0, 0, 0.08); | ||
| 626 | + background: rgba(0, 0, 0, 0.02); | ||
| 453 | } | 627 | } |
| 454 | 628 | ||
| 455 | .footer-hint { | 629 | .footer-hint { |
| ... | @@ -464,31 +638,53 @@ | ... | @@ -464,31 +638,53 @@ |
| 464 | .footer-hint kbd { | 638 | .footer-hint kbd { |
| 465 | font-family: inherit; | 639 | font-family: inherit; |
| 466 | font-size: 0.625rem; | 640 | font-size: 0.625rem; |
| 467 | - padding: 2px 5px; | 641 | + padding: 3px 6px; |
| 468 | - background: var(--theme-body); | 642 | + background: rgba(255, 255, 255, 0.1); |
| 469 | - border: 1px solid var(--theme-borde); | 643 | + border: 1px solid rgba(255, 255, 255, 0.12); |
| 470 | border-radius: 4px; | 644 | border-radius: 4px; |
| 471 | } | 645 | } |
| 472 | 646 | ||
| 647 | + :global(html:not(.dark)) .footer-hint kbd { | ||
| 648 | + background: rgba(0, 0, 0, 0.06); | ||
| 649 | + border-color: rgba(0, 0, 0, 0.1); | ||
| 650 | + } | ||
| 651 | + | ||
| 473 | /* Responsive */ | 652 | /* Responsive */ |
| 474 | @media (max-width: 640px) { | 653 | @media (max-width: 640px) { |
| 475 | .search-modal-backdrop { | 654 | .search-modal-backdrop { |
| 476 | padding: 0; | 655 | padding: 0; |
| 477 | align-items: flex-end; | 656 | align-items: flex-end; |
| 657 | + background: rgba(0, 0, 0, 0.4); | ||
| 478 | } | 658 | } |
| 479 | 659 | ||
| 480 | .search-modal { | 660 | .search-modal { |
| 481 | max-width: 100%; | 661 | max-width: 100%; |
| 482 | border-radius: 20px 20px 0 0; | 662 | border-radius: 20px 20px 0 0; |
| 483 | - max-height: 85vh; | 663 | + max-height: 90vh; |
| 664 | + border-bottom: none; | ||
| 484 | } | 665 | } |
| 485 | 666 | ||
| 486 | .search-modal-results { | 667 | .search-modal-results { |
| 487 | - max-height: 50vh; | 668 | + max-height: 45vh; |
| 669 | + } | ||
| 670 | + | ||
| 671 | + .filters-grid { | ||
| 672 | + grid-template-columns: 1fr; | ||
| 673 | + gap: 8px; | ||
| 488 | } | 674 | } |
| 489 | 675 | ||
| 490 | - .search-modal-filters { | 676 | + .search-filter-card { |
| 491 | - flex-wrap: wrap; | 677 | + flex-direction: row; |
| 678 | + align-items: center; | ||
| 679 | + padding: 10px 14px; | ||
| 680 | + } | ||
| 681 | + | ||
| 682 | + .filter-card-header { | ||
| 683 | + flex: 1; | ||
| 684 | + } | ||
| 685 | + | ||
| 686 | + .filter-desc { | ||
| 687 | + display: none; | ||
| 492 | } | 688 | } |
| 493 | 689 | ||
| 494 | .search-modal-footer { | 690 | .search-modal-footer { | ... | ... |
| ... | @@ -61,6 +61,43 @@ export async function initSearchIndex() { | ... | @@ -61,6 +61,43 @@ export async function initSearchIndex() { |
| 61 | throw errorRubros; | 61 | throw errorRubros; |
| 62 | } | 62 | } |
| 63 | 63 | ||
| 64 | + // Cargar finalidad y función | ||
| 65 | + const { data: finfun, error: errorFinfun } = await supabase | ||
| 66 | + .schema('ppto') | ||
| 67 | + .from('clas_finfun') | ||
| 68 | + .select('finfun, desc_finfun, nivel'); | ||
| 69 | + | ||
| 70 | + if (errorFinfun) { | ||
| 71 | + console.error('Error cargando finalidad/función:', errorFinfun); | ||
| 72 | + throw errorFinfun; | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + // Cargar organismos (no bloquea si falla) | ||
| 76 | + let organismos = []; | ||
| 77 | + const { data: organismosData, error: errorOrganismos } = await supabase | ||
| 78 | + .schema('ppto') | ||
| 79 | + .from('clas_organismos') | ||
| 80 | + .select('organismo, organismo_desc_organismo, organismo_grupo, organismo_sub_grupo'); | ||
| 81 | + | ||
| 82 | + if (errorOrganismos) { | ||
| 83 | + console.warn('Tabla clas_organismos no disponible:', errorOrganismos.message); | ||
| 84 | + } else { | ||
| 85 | + organismos = organismosData || []; | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + // Cargar fuentes (no bloquea si falla) | ||
| 89 | + let fuentes = []; | ||
| 90 | + const { data: fuentesData, error: errorFuentes } = await supabase | ||
| 91 | + .schema('ppto') | ||
| 92 | + .from('clas_fuentes') | ||
| 93 | + .select('fuente, desc_fuente, grupo_fuente, desc_grupo_fuente'); | ||
| 94 | + | ||
| 95 | + if (errorFuentes) { | ||
| 96 | + console.warn('Tabla clas_fuentes no disponible:', errorFuentes.message); | ||
| 97 | + } else { | ||
| 98 | + fuentes = fuentesData || []; | ||
| 99 | + } | ||
| 100 | + | ||
| 64 | // Transformar entidades para el índice | 101 | // Transformar entidades para el índice |
| 65 | const entidadesIndex = entidades.map(item => ({ | 102 | const entidadesIndex = entidades.map(item => ({ |
| 66 | tipo: 'entidad', | 103 | tipo: 'entidad', |
| ... | @@ -102,8 +139,49 @@ export async function initSearchIndex() { | ... | @@ -102,8 +139,49 @@ export async function initSearchIndex() { |
| 102 | codigo_normalizado: item.rubro?.toString() || '' | 139 | codigo_normalizado: item.rubro?.toString() || '' |
| 103 | })); | 140 | })); |
| 104 | 141 | ||
| 142 | + // Transformar finalidad y función para el índice | ||
| 143 | + const nivelLabelsFinfun = { finalidad: 'Finalidad', grpfuncion: 'Grupo Función', funcion: 'Función' }; | ||
| 144 | + const finfunIndex = finfun.map(item => ({ | ||
| 145 | + tipo: 'finfun', | ||
| 146 | + codigo: item.finfun, | ||
| 147 | + nombre: item.desc_finfun, | ||
| 148 | + sigla: null, | ||
| 149 | + contexto: `Finalidad y Función · ${nivelLabelsFinfun[item.nivel] || item.nivel}`, | ||
| 150 | + nivel: item.nivel, | ||
| 151 | + nombre_normalizado: normalizeText(item.desc_finfun), | ||
| 152 | + sigla_normalizada: '', | ||
| 153 | + codigo_normalizado: item.finfun?.toString() || '' | ||
| 154 | + })); | ||
| 155 | + | ||
| 156 | + // Transformar organismos para el índice | ||
| 157 | + // Todos los registros en la tabla son organismos (nivel hoja) | ||
| 158 | + const organismosIndex = organismos.map(item => ({ | ||
| 159 | + tipo: 'organismo', | ||
| 160 | + codigo: item.organismo, | ||
| 161 | + nombre: item.organismo_desc_organismo, | ||
| 162 | + sigla: null, | ||
| 163 | + contexto: `Organismo · Grupo ${item.organismo_grupo}`, | ||
| 164 | + nivel: 'organismo', | ||
| 165 | + nombre_normalizado: normalizeText(item.organismo_desc_organismo), | ||
| 166 | + sigla_normalizada: '', | ||
| 167 | + codigo_normalizado: item.organismo?.toString() || '' | ||
| 168 | + })); | ||
| 169 | + | ||
| 170 | + // Transformar fuentes para el índice | ||
| 171 | + const fuentesIndex = fuentes.map(item => ({ | ||
| 172 | + tipo: 'fuente', | ||
| 173 | + codigo: item.fuente, | ||
| 174 | + nombre: item.desc_fuente, | ||
| 175 | + sigla: null, | ||
| 176 | + contexto: `Fuente · ${item.desc_grupo_fuente}`, | ||
| 177 | + nivel: 'fuente', | ||
| 178 | + nombre_normalizado: normalizeText(item.desc_fuente), | ||
| 179 | + sigla_normalizada: '', | ||
| 180 | + codigo_normalizado: item.fuente?.toString() || '' | ||
| 181 | + })); | ||
| 182 | + | ||
| 105 | // Combinar todos los conjuntos | 183 | // Combinar todos los conjuntos |
| 106 | - searchData = [...entidadesIndex, ...objetosIndex, ...rubrosIndex]; | 184 | + searchData = [...entidadesIndex, ...objetosIndex, ...rubrosIndex, ...finfunIndex, ...organismosIndex, ...fuentesIndex]; |
| 107 | 185 | ||
| 108 | fuseInstance = new Fuse(searchData, fuseOptions); | 186 | fuseInstance = new Fuse(searchData, fuseOptions); |
| 109 | 187 | ... | ... |
| ... | @@ -8,7 +8,7 @@ export const isLoading = writable(false); | ... | @@ -8,7 +8,7 @@ export const isLoading = writable(false); |
| 8 | export const indexLoaded = writable(false); | 8 | export const indexLoaded = writable(false); |
| 9 | export const error = writable(null); | 9 | export const error = writable(null); |
| 10 | export const selectedIndex = writable(-1); | 10 | export const selectedIndex = writable(-1); |
| 11 | -export const filterType = writable('todos'); // todos, entidad, objeto_gasto, rubro | 11 | +export const filterType = writable('todos'); // todos, entidad, objeto_gasto, rubro, finfun |
| 12 | 12 | ||
| 13 | // Estado derivado: si está mostrando resultados | 13 | // Estado derivado: si está mostrando resultados |
| 14 | export const showResults = derived( | 14 | export const showResults = derived( | ... | ... |
| ... | @@ -294,9 +294,9 @@ | ... | @@ -294,9 +294,9 @@ |
| 294 | <!-- Bloque 1: Identidad --> | 294 | <!-- Bloque 1: Identidad --> |
| 295 | <div class="hero-identity"> | 295 | <div class="hero-identity"> |
| 296 | <h1 class="hero-title anim" style="--d:200ms">Presupuesto <span class="gold">Abierto</span></h1> | 296 | <h1 class="hero-title anim" style="--d:200ms">Presupuesto <span class="gold">Abierto</span></h1> |
| 297 | - <p class="hero-sub anim" style="--d:340ms">Información diaria e histórica de todo el Estado Boliviano.</p> | 297 | + <p class="hero-sub anim" style="--d:340ms"><a href="/vista-diaria" class="hero-sub-link">Información diaria</a> e histórica de todo el Estado Boliviano.</p> |
| 298 | <div class="hero-stats anim" style="--d:420ms"> | 298 | <div class="hero-stats anim" style="--d:420ms"> |
| 299 | - <span class="stat-num">21</span> años<span class="stat-dot">·</span><span class="stat-num">8.3M</span> registros<span class="stat-dot">·</span><span class="stat-num">647</span> instituciones<span class="stat-dot">·</span><span class="stat-num">7</span> clasificadores<span class="stat-dot">·</span><span class="stat-num">10</span> visualizaciones<span class="stat-dot">·</span><span class="stat-num">4</span> historias | 299 | + <span class="stat-num">21</span> años<span class="stat-dot">·</span><span class="stat-num">8.3M</span> registros<span class="stat-dot">·</span><span class="stat-num">647</span> instituciones<span class="stat-dot">·</span><span class="stat-num">7</span> <a href="#clasificadores" class="stat-link">clasificadores</a><span class="stat-dot">·</span><span class="stat-num">10</span> <a href="#visualizaciones" class="stat-link">visualizaciones</a><span class="stat-dot">·</span><span class="stat-num">4</span> <a href="#historias" class="stat-link">historias</a> |
| 300 | </div> | 300 | </div> |
| 301 | </div> | 301 | </div> |
| 302 | 302 | ||
| ... | @@ -305,20 +305,20 @@ | ... | @@ -305,20 +305,20 @@ |
| 305 | <div class="search-wrap"> | 305 | <div class="search-wrap"> |
| 306 | <!-- Search bar unificado --> | 306 | <!-- Search bar unificado --> |
| 307 | <div class="search-bar" class:search-focused={searchFocused} class:search-has-results={searchFocused && $query.length >= 2 && ($results.length > 0 || $results.length === 0 || $isLoading)} style="--accent:{accent}"> | 307 | <div class="search-bar" class:search-focused={searchFocused} class:search-has-results={searchFocused && $query.length >= 2 && ($results.length > 0 || $results.length === 0 || $isLoading)} style="--accent:{accent}"> |
| 308 | - <!-- Toggle integrado (desktop) --> | 308 | + <!-- Link al diario + Badge informativo (desktop) --> |
| 309 | - <div class="toggle-bar toggle-desktop"> | 309 | + <div class="search-meta toggle-desktop"> |
| 310 | - <button class="toggle-btn" class:toggle-active={mode === 'archivo'} on:click={() => switchMode('archivo')}> | 310 | + <a href="/vista-diaria" class="search-meta-item search-diario-link"> |
| 311 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={mode === 'archivo' ? '#C9A751' : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | 311 | + Ver gasto de hoy |
| 312 | - <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> | 312 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 313 | + <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/> | ||
| 313 | </svg> | 314 | </svg> |
| 314 | - 2005 a 2025 | 315 | + </a> |
| 315 | - </button> | 316 | + <span class="search-meta-item search-badge"> |
| 316 | - <button class="toggle-btn" class:toggle-active={mode === 'diario'} on:click={() => switchMode('diario')}> | 317 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> |
| 317 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={mode === 'diario' ? '#4A8B6E' : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | 318 | + <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> |
| 318 | - <polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/> | ||
| 319 | </svg> | 319 | </svg> |
| 320 | - Diario | 320 | + Viendo 2005 – 2025 |
| 321 | - </button> | 321 | + </span> |
| 322 | </div> | 322 | </div> |
| 323 | <div class="search-divider"></div> | 323 | <div class="search-divider"></div> |
| 324 | 324 | ||
| ... | @@ -451,21 +451,19 @@ | ... | @@ -451,21 +451,19 @@ |
| 451 | 451 | ||
| 452 | <!-- Mobile settings panel --> | 452 | <!-- Mobile settings panel --> |
| 453 | <div class="mobile-settings-panel" class:mobile-settings-panel-open={mobileSettingsOpen}> | 453 | <div class="mobile-settings-panel" class:mobile-settings-panel-open={mobileSettingsOpen}> |
| 454 | - <div class="mobile-toggle-bar"> | 454 | + <div class="mobile-meta-bar"> |
| 455 | - <button class="mobile-toggle-btn" class:toggle-active={mode === 'archivo'} on:click={() => { switchMode('archivo'); mobileSettingsOpen = false; }}> | 455 | + <a href="/vista-diaria" class="mobile-meta-item mobile-diario-link"> |
| 456 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={mode === 'archivo' ? '#C9A751' : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | 456 | + Ver hoy |
| 457 | - <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> | 457 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 458 | + <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/> | ||
| 458 | </svg> | 459 | </svg> |
| 459 | - <span>Histórico</span> | 460 | + </a> |
| 460 | - <span class="mobile-toggle-sub">2005 — 2025</span> | 461 | + <span class="mobile-meta-item mobile-search-badge"> |
| 461 | - </button> | 462 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> |
| 462 | - <button class="mobile-toggle-btn" class:toggle-active={mode === 'diario'} on:click={() => { switchMode('diario'); mobileSettingsOpen = false; }}> | 463 | + <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> |
| 463 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={mode === 'diario' ? '#4A8B6E' : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 464 | - <polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/> | ||
| 465 | </svg> | 464 | </svg> |
| 466 | - <span>Diario</span> | 465 | + Viendo 2005 – 2025 |
| 467 | - <span class="mobile-toggle-sub">En vivo</span> | 466 | + </span> |
| 468 | - </button> | ||
| 469 | </div> | 467 | </div> |
| 470 | <div class="mobile-filter-section"> | 468 | <div class="mobile-filter-section"> |
| 471 | <div class="mobile-filter-title">Buscar</div> | 469 | <div class="mobile-filter-title">Buscar</div> |
| ... | @@ -944,20 +942,27 @@ | ... | @@ -944,20 +942,27 @@ |
| 944 | .hero-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.25em;color:#B8B5AD} | 942 | .hero-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.25em;color:#B8B5AD} |
| 945 | .hero-title{font-size:clamp(44px,5.5vw,72px);font-weight:900;line-height:1.02;letter-spacing:-0.035em;max-width:720px;margin:14px auto 0} | 943 | .hero-title{font-size:clamp(44px,5.5vw,72px);font-weight:900;line-height:1.02;letter-spacing:-0.035em;max-width:720px;margin:14px auto 0} |
| 946 | .hero-sub{font-family:var(--sans);font-size:clamp(16px,1.6vw,18px);color:#A5A29A;max-width:100%;margin-top:14px;line-height:1.6} | 944 | .hero-sub{font-family:var(--sans);font-size:clamp(16px,1.6vw,18px);color:#A5A29A;max-width:100%;margin-top:14px;line-height:1.6} |
| 945 | + .hero-sub-link{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(165,162,154,0.5);transition:all 0.2s;padding-bottom:1px} | ||
| 946 | + .hero-sub-link:hover{color:#4A8B6E;border-bottom-color:#4A8B6E} | ||
| 947 | .hero-stats{font-family:var(--mono);font-size:13px;letter-spacing:0.04em;color:#B8B5AD;margin-top:16px;line-height:1} | 947 | .hero-stats{font-family:var(--mono);font-size:13px;letter-spacing:0.04em;color:#B8B5AD;margin-top:16px;line-height:1} |
| 948 | .stat-num{color:#F5F0E8} | 948 | .stat-num{color:#F5F0E8} |
| 949 | .stat-dot{margin:0 10px;opacity:0.3} | 949 | .stat-dot{margin:0 10px;opacity:0.3} |
| 950 | + .stat-link{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(184,181,173,0.4);transition:all 0.2s;padding-bottom:1px} | ||
| 951 | + .stat-link:hover{color:#C9A751;border-bottom-color:#C9A751} | ||
| 950 | 952 | ||
| 951 | /* Search */ | 953 | /* Search */ |
| 952 | - .search-wrap{max-width:860px;width:100%;margin:0 auto;position:relative;z-index:9999} | 954 | + .search-wrap{max-width:960px;width:100%;margin:0 auto;position:relative;z-index:9999} |
| 953 | .search-bar{display:flex;align-items:center;background:rgba(255,255,255,0.04);border-radius:16px;padding:8px 14px 8px 8px;transition:all 0.4s cubic-bezier(0.16,1,0.3,1);box-shadow:0 0 0 1px rgba(201,167,81,0.12),0 8px 32px rgba(0,0,0,0.25);position:relative} | 955 | .search-bar{display:flex;align-items:center;background:rgba(255,255,255,0.04);border-radius:16px;padding:8px 14px 8px 8px;transition:all 0.4s cubic-bezier(0.16,1,0.3,1);box-shadow:0 0 0 1px rgba(201,167,81,0.12),0 8px 32px rgba(0,0,0,0.25);position:relative} |
| 954 | .search-focused{background:rgba(255,255,255,0.07);box-shadow:0 0 0 1.5px rgba(201,167,81,0.35),0 8px 40px rgba(0,0,0,0.3)} | 956 | .search-focused{background:rgba(255,255,255,0.07);box-shadow:0 0 0 1.5px rgba(201,167,81,0.35),0 8px 40px rgba(0,0,0,0.3)} |
| 955 | .search-has-results{border-radius:16px 16px 0 0} | 957 | .search-has-results{border-radius:16px 16px 0 0} |
| 956 | - .toggle-bar{display:flex;border-radius:10px;padding:2px;background:rgba(0,0,0,0.2);flex-shrink:0} | 958 | + .search-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;padding:0 4px} |
| 957 | - .toggle-btn{background:transparent;border:none;border-radius:8px;padding:12px 16px;cursor:pointer;display:flex;align-items:center;gap:7px;transition:all 0.3s;font-family:var(--sans);font-size:14px;font-weight:400;color:#9B9890} | 959 | + .search-meta-item{display:flex;align-items:center;gap:7px;font-family:var(--sans);font-size:13px;font-weight:500;padding:10px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} |
| 958 | - .toggle-btn svg{display:block;flex-shrink:0} | 960 | + .search-meta-item svg{display:block;flex-shrink:0} |
| 959 | - .toggle-btn:hover{color:#B8B5AD} | 961 | + .search-badge{color:#C9A751;background:rgba(201,167,81,0.12)} |
| 960 | - .toggle-active{background:rgba(255,255,255,0.1);font-weight:600;color:#F5F0E8} | 962 | + .search-diario-link{color:#4A8B6E;background:rgba(74,139,110,0.12)} |
| 963 | + .search-diario-link:hover{background:rgba(74,139,110,0.22);color:#5AA080} | ||
| 964 | + .search-diario-link svg{transition:transform 0.2s} | ||
| 965 | + .search-diario-link:hover svg{transform:translateX(3px)} | ||
| 961 | .search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0} | 966 | .search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0} |
| 962 | .search-icon{flex-shrink:0;transition:stroke 0.3s} | 967 | .search-icon{flex-shrink:0;transition:stroke 0.3s} |
| 963 | .search-input-wrap{flex:1;position:relative;display:flex;align-items:center} | 968 | .search-input-wrap{flex:1;position:relative;display:flex;align-items:center} |
| ... | @@ -995,12 +1000,12 @@ | ... | @@ -995,12 +1000,12 @@ |
| 995 | /* Mobile settings panel (hidden on desktop) */ | 1000 | /* Mobile settings panel (hidden on desktop) */ |
| 996 | .mobile-settings-panel{display:none;overflow:hidden;max-height:0;opacity:0;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)} | 1001 | .mobile-settings-panel{display:none;overflow:hidden;max-height:0;opacity:0;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)} |
| 997 | .mobile-settings-panel-open{max-height:300px;opacity:1;margin-top:12px} | 1002 | .mobile-settings-panel-open{max-height:300px;opacity:1;margin-top:12px} |
| 998 | - .mobile-toggle-bar{display:flex;gap:10px} | 1003 | + .mobile-meta-bar{display:flex;gap:10px;padding:4px 0} |
| 999 | - .mobile-toggle-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;padding:16px 12px;border-radius:12px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);cursor:pointer;transition:all 0.3s} | 1004 | + .mobile-meta-item{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;font-family:var(--sans);font-size:13px;font-weight:500;padding:12px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} |
| 1000 | - .mobile-toggle-btn.toggle-active{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.15)} | 1005 | + .mobile-meta-item svg{flex-shrink:0} |
| 1001 | - .mobile-toggle-btn span{font-family:var(--sans);font-size:14px;font-weight:600;color:#F5F0E8} | 1006 | + .mobile-search-badge{color:#C9A751;background:rgba(201,167,81,0.12)} |
| 1002 | - .mobile-toggle-btn svg{margin-bottom:4px} | 1007 | + .mobile-diario-link{color:#4A8B6E;background:rgba(74,139,110,0.12)} |
| 1003 | - .mobile-toggle-sub{font-size:11px!important;font-weight:400!important;color:#9B9890!important;font-family:var(--mono)!important;letter-spacing:0.05em} | 1008 | + .mobile-diario-link:hover,.mobile-diario-link:active{background:rgba(74,139,110,0.22)} |
| 1004 | .mobile-filter-section{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.06)} | 1009 | .mobile-filter-section{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.06)} |
| 1005 | .mobile-filter-title{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:0.1em;color:#6B6860;text-transform:uppercase;margin-bottom:8px} | 1010 | .mobile-filter-title{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:0.1em;color:#6B6860;text-transform:uppercase;margin-bottom:8px} |
| 1006 | .mobile-filter-bar{display:flex;gap:8px;flex-wrap:wrap} | 1011 | .mobile-filter-bar{display:flex;gap:8px;flex-wrap:wrap} |
| ... | @@ -1337,8 +1342,7 @@ | ... | @@ -1337,8 +1342,7 @@ |
| 1337 | /* Search */ | 1342 | /* Search */ |
| 1338 | .search-wrap{padding:0} | 1343 | .search-wrap{padding:0} |
| 1339 | .mobile-settings-btn{width:36px;height:36px} | 1344 | .mobile-settings-btn{width:36px;height:36px} |
| 1340 | - .mobile-toggle-btn{padding:14px 10px} | 1345 | + .mobile-meta-item{padding:10px 12px;font-size:12px} |
| 1341 | - .mobile-toggle-btn span{font-size:13px} | ||
| 1342 | .search-results{max-height:220px} | 1346 | .search-results{max-height:220px} |
| 1343 | .search-result-item{padding:10px 14px} | 1347 | .search-result-item{padding:10px 14px} |
| 1344 | .result-name{font-size:12px} | 1348 | .result-name{font-size:12px} | ... | ... |
| 1 | -<script> | 1 | +me pare<script> |
| 2 | import { supabase } from '$lib/supabase'; | 2 | import { supabase } from '$lib/supabase'; |
| 3 | import { onMount, untrack } from 'svelte'; | 3 | import { onMount, untrack } from 'svelte'; |
| 4 | import { page } from '$app/stores'; | 4 | import { page } from '$app/stores'; |
| ... | @@ -79,17 +79,24 @@ | ... | @@ -79,17 +79,24 @@ |
| 79 | '10': 'Protección Social', | 79 | '10': 'Protección Social', |
| 80 | }; | 80 | }; |
| 81 | 81 | ||
| 82 | - // Función para obtener color según la finalidad (primer dígito del código finfun) | 82 | + // Helper para obtener la finalidad de un código (maneja caso especial de "10") |
| 83 | + function getFinalidadKey(finfun) { | ||
| 84 | + const code = String(finfun); | ||
| 85 | + if (code.startsWith('10')) return '10'; | ||
| 86 | + return code.charAt(0); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + // Función para obtener color según la finalidad | ||
| 83 | function getNodeColor(finfun, opacity = 1) { | 90 | function getNodeColor(finfun, opacity = 1) { |
| 84 | - const finalidadDigit = String(finfun).charAt(0); | 91 | + const finalidadKey = getFinalidadKey(finfun); |
| 85 | - const baseColor = FINALIDAD_COLORS[finalidadDigit] || '#888888'; | 92 | + const baseColor = FINALIDAD_COLORS[finalidadKey] || '#888888'; |
| 86 | if (opacity === 1) return baseColor; | 93 | if (opacity === 1) return baseColor; |
| 87 | return d3.color(baseColor).copy({opacity}).formatRgb(); | 94 | return d3.color(baseColor).copy({opacity}).formatRgb(); |
| 88 | } | 95 | } |
| 89 | 96 | ||
| 90 | function getTextColor(finfun) { | 97 | function getTextColor(finfun) { |
| 91 | - const finalidadDigit = String(finfun).charAt(0); | 98 | + const finalidadKey = getFinalidadKey(finfun); |
| 92 | - const baseColor = FINALIDAD_COLORS[finalidadDigit] || '#888888'; | 99 | + const baseColor = FINALIDAD_COLORS[finalidadKey] || '#888888'; |
| 93 | const color = d3.color(baseColor); | 100 | const color = d3.color(baseColor); |
| 94 | const r = color.r / 255; | 101 | const r = color.r / 255; |
| 95 | const g = color.g / 255; | 102 | const g = color.g / 255; |
| ... | @@ -99,8 +106,8 @@ | ... | @@ -99,8 +106,8 @@ |
| 99 | } | 106 | } |
| 100 | 107 | ||
| 101 | function getTextColorSecondary(finfun) { | 108 | function getTextColorSecondary(finfun) { |
| 102 | - const finalidadDigit = String(finfun).charAt(0); | 109 | + const finalidadKey = getFinalidadKey(finfun); |
| 103 | - const baseColor = FINALIDAD_COLORS[finalidadDigit] || '#888888'; | 110 | + const baseColor = FINALIDAD_COLORS[finalidadKey] || '#888888'; |
| 104 | const color = d3.color(baseColor); | 111 | const color = d3.color(baseColor); |
| 105 | const r = color.r / 255; | 112 | const r = color.r / 255; |
| 106 | const g = color.g / 255; | 113 | const g = color.g / 255; |
| ... | @@ -610,13 +617,26 @@ | ... | @@ -610,13 +617,26 @@ |
| 610 | }); | 617 | }); |
| 611 | 618 | ||
| 612 | // Helpers para derivar jerarquía desde código finfun | 619 | // Helpers para derivar jerarquía desde código finfun |
| 613 | - // Jerarquía: Finalidad (1 dígito) → Grupo Función (2 dígitos) → Función (3+ dígitos) | 620 | + // Jerarquía: Finalidad (1-9 o 10) → Grupo Función (2-3 dígitos) → Función (3+ dígitos) |
| 621 | + // Nota: Finalidad 10 es especial - tiene 2 dígitos, sus hijos empiezan con "10" | ||
| 614 | function getFinalidadFromFinfun(finfun) { | 622 | function getFinalidadFromFinfun(finfun) { |
| 615 | - return finfun.charAt(0); | 623 | + const code = String(finfun); |
| 624 | + // Si empieza con "10", la finalidad es "10" | ||
| 625 | + if (code.startsWith('10')) { | ||
| 626 | + return '10'; | ||
| 627 | + } | ||
| 628 | + // Si no, la finalidad es el primer dígito | ||
| 629 | + return code.charAt(0); | ||
| 616 | } | 630 | } |
| 617 | 631 | ||
| 618 | function getGrpFuncionFromFinfun(finfun) { | 632 | function getGrpFuncionFromFinfun(finfun) { |
| 619 | - return finfun.substring(0, 2); | 633 | + const code = String(finfun); |
| 634 | + // Si empieza con "10", el grupo función son los primeros 3 dígitos (ej: "101", "102") | ||
| 635 | + if (code.startsWith('10')) { | ||
| 636 | + return code.substring(0, 3); | ||
| 637 | + } | ||
| 638 | + // Si no, son los primeros 2 dígitos (ej: "11", "21", "93") | ||
| 639 | + return code.substring(0, 2); | ||
| 620 | } | 640 | } |
| 621 | 641 | ||
| 622 | function getItemsForFinalidad(finalidadCode) { | 642 | function getItemsForFinalidad(finalidadCode) { | ... | ... |
| 1 | +<script> | ||
| 2 | + import { supabase } from '$lib/supabase'; | ||
| 3 | + import { onMount } from 'svelte'; | ||
| 4 | + import { fade, scale } from 'svelte/transition'; | ||
| 5 | + import { cubicOut } from 'svelte/easing'; | ||
| 6 | + | ||
| 7 | + let loading = $state(true); | ||
| 8 | + let searchQuery = $state(''); | ||
| 9 | + let grupos = $state([]); | ||
| 10 | + let allItems = $state([]); | ||
| 11 | + let selectedGrupo = $state(null); | ||
| 12 | + let selectedItem = $state(null); | ||
| 13 | + let highlightedItem = $state(null); | ||
| 14 | + let sidebarOpen = $state(false); | ||
| 15 | + | ||
| 16 | + onMount(async () => { | ||
| 17 | + // Forzar recálculo de layout para navegación cliente | ||
| 18 | + requestAnimationFrame(() => { | ||
| 19 | + document.body.offsetHeight; // Force reflow | ||
| 20 | + }); | ||
| 21 | + | ||
| 22 | + const { data, error } = await supabase | ||
| 23 | + .schema('ppto') | ||
| 24 | + .from('clas_fuentes') | ||
| 25 | + .select('*') | ||
| 26 | + .order('fuente'); | ||
| 27 | + | ||
| 28 | + if (!error && data) { | ||
| 29 | + allItems = data; | ||
| 30 | + | ||
| 31 | + // Crear grupos sintéticos desde valores únicos de grupo_fuente | ||
| 32 | + const gruposUnicos = [...new Set(data.map(item => item.grupo_fuente))].filter(g => g != null); | ||
| 33 | + grupos = gruposUnicos.map(grupoNum => { | ||
| 34 | + const sample = data.find(item => item.grupo_fuente === grupoNum); | ||
| 35 | + return { | ||
| 36 | + grupo_fuente: grupoNum, | ||
| 37 | + desc_grupo_fuente: sample?.desc_grupo_fuente || `Grupo ${grupoNum}` | ||
| 38 | + }; | ||
| 39 | + }).sort((a, b) => Number(a.grupo_fuente) - Number(b.grupo_fuente)); | ||
| 40 | + | ||
| 41 | + if (grupos.length > 0) { | ||
| 42 | + selectedGrupo = grupos[0]; | ||
| 43 | + } | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + loading = false; | ||
| 47 | + }); | ||
| 48 | + | ||
| 49 | + // Obtener fuentes para un grupo específico | ||
| 50 | + function getFuentesForGrupo(grupo) { | ||
| 51 | + if (!grupo) return []; | ||
| 52 | + return allItems | ||
| 53 | + .filter(item => item.grupo_fuente === grupo.grupo_fuente) | ||
| 54 | + .sort((a, b) => String(a.fuente).localeCompare(String(b.fuente))); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + // Búsqueda global | ||
| 58 | + function searchGlobal(query) { | ||
| 59 | + if (!query || query.length < 2) return []; | ||
| 60 | + const q = query.toLowerCase(); | ||
| 61 | + | ||
| 62 | + return allItems | ||
| 63 | + .filter(item => | ||
| 64 | + item.fuente?.toString().includes(q) || | ||
| 65 | + item.desc_fuente?.toLowerCase().includes(q) || | ||
| 66 | + item.desc_grupo_fuente?.toLowerCase().includes(q) | ||
| 67 | + ) | ||
| 68 | + .slice(0, 20); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + function parseDescripciones(descripcionesStr) { | ||
| 72 | + if (!descripcionesStr) return []; | ||
| 73 | + try { | ||
| 74 | + const parsed = typeof descripcionesStr === 'string' | ||
| 75 | + ? JSON.parse(descripcionesStr) | ||
| 76 | + : descripcionesStr; | ||
| 77 | + return parsed.sort((a, b) => { | ||
| 78 | + const maxYearA = getMaxYear(a.rangos); | ||
| 79 | + const maxYearB = getMaxYear(b.rangos); | ||
| 80 | + return maxYearB - maxYearA; | ||
| 81 | + }); | ||
| 82 | + } catch { | ||
| 83 | + return []; | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + function getMaxYear(rangos) { | ||
| 88 | + if (!rangos) return 0; | ||
| 89 | + const years = rangos.match(/\d{4}/g); | ||
| 90 | + if (!years) return 0; | ||
| 91 | + return Math.max(...years.map(y => parseInt(y))); | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + function selectGrupo(grupo) { | ||
| 95 | + selectedGrupo = grupo; | ||
| 96 | + selectedItem = null; | ||
| 97 | + searchQuery = ''; | ||
| 98 | + highlightedItem = null; | ||
| 99 | + sidebarOpen = false; | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + function openDetail(item) { | ||
| 103 | + selectedItem = item; | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + function closeDetail() { | ||
| 107 | + selectedItem = null; | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + function goToSearchResult(item) { | ||
| 111 | + const targetGrupo = grupos.find(g => g.grupo_fuente === item.grupo_fuente); | ||
| 112 | + | ||
| 113 | + if (targetGrupo) { | ||
| 114 | + selectedGrupo = targetGrupo; | ||
| 115 | + highlightedItem = item.fuente; | ||
| 116 | + searchQuery = ''; | ||
| 117 | + sidebarOpen = false; | ||
| 118 | + | ||
| 119 | + setTimeout(() => { | ||
| 120 | + const element = document.getElementById(`f-${item.fuente}`); | ||
| 121 | + if (element) { | ||
| 122 | + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||
| 123 | + } | ||
| 124 | + }, 100); | ||
| 125 | + | ||
| 126 | + setTimeout(() => { | ||
| 127 | + highlightedItem = null; | ||
| 128 | + }, 2000); | ||
| 129 | + } | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + let grupoContent = $derived(selectedGrupo ? getFuentesForGrupo(selectedGrupo) : []); | ||
| 133 | + let searchResults = $derived(searchGlobal(searchQuery)); | ||
| 134 | + let isSearching = $derived(searchQuery.length >= 2); | ||
| 135 | + | ||
| 136 | + // Contadores | ||
| 137 | + let totalItems = $derived(allItems.length); | ||
| 138 | + let totalGrupos = $derived(grupos.length); | ||
| 139 | +</script> | ||
| 140 | + | ||
| 141 | +<svelte:head> | ||
| 142 | + <title>Fuentes de Financiamiento | Presupuesto Público</title> | ||
| 143 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 144 | + <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" /> | ||
| 145 | +</svelte:head> | ||
| 146 | + | ||
| 147 | +<div class="min-h-screen" style="font-family: 'Instrument Sans', sans-serif; background-color: var(--theme-body); color: var(--theme-titulo); transition: background-color 0.2s, color 0.2s;"> | ||
| 148 | + <!-- Header pedagógico --> | ||
| 149 | + <header class="border-b" style="border-color: var(--theme-borde); background-color: var(--theme-body);"> | ||
| 150 | + <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-6"> | ||
| 151 | + <!-- Breadcrumb: responsive --> | ||
| 152 | + <nav class="mb-4" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;"> | ||
| 153 | + <!-- Móvil: solo padre --> | ||
| 154 | + <a href="/clasificadores" class="sm:hidden transition-colors" style="color: var(--theme-texto);"> | ||
| 155 | + ← Clasificadores | ||
| 156 | + </a> | ||
| 157 | + <!-- Desktop: ruta completa --> | ||
| 158 | + <div class="hidden sm:flex items-center gap-2" style="color: var(--theme-texto);"> | ||
| 159 | + <a href="/" class="transition-colors hover:opacity-80">Inicio</a> | ||
| 160 | + <span style="opacity: 0.5;">/</span> | ||
| 161 | + <a href="/clasificadores" class="transition-colors hover:opacity-80">Clasificadores</a> | ||
| 162 | + </div> | ||
| 163 | + </nav> | ||
| 164 | + | ||
| 165 | + <div class="max-w-3xl"> | ||
| 166 | + <p class="text-xs uppercase tracking-widest mb-2" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);"> | ||
| 167 | + Clasificador 02 | ||
| 168 | + </p> | ||
| 169 | + <h1 class="text-3xl mb-3" style="font-family: 'DM Serif Display', serif; color: var(--theme-titulo);"> | ||
| 170 | + ¿Con qué recursos se financia? | ||
| 171 | + </h1> | ||
| 172 | + | ||
| 173 | + <p class="leading-relaxed mb-3" style="color: var(--theme-texto);"> | ||
| 174 | + Organiza los recursos según su <strong style="color: var(--theme-titulo);">origen de financiamiento</strong>: | ||
| 175 | + Tesoro General, recursos específicos, créditos externos e internos, donaciones. | ||
| 176 | + Permite entender de dónde provienen los fondos para el gasto público. | ||
| 177 | + </p> | ||
| 178 | + | ||
| 179 | + {#if totalItems > 0} | ||
| 180 | + <p class="text-sm mb-5" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);"> | ||
| 181 | + <span style="color: var(--theme-titulo); font-weight: 500;">{totalItems}</span> fuentes de financiamiento en | ||
| 182 | + <span style="color: var(--theme-titulo); font-weight: 500;">{totalGrupos}</span> grupos | ||
| 183 | + </p> | ||
| 184 | + {/if} | ||
| 185 | + | ||
| 186 | + <!-- Jerarquía --> | ||
| 187 | + <div class="hidden sm:flex flex-wrap items-center gap-2 sm:gap-3 text-xs mb-6" style="font-family: 'DM Mono', monospace;"> | ||
| 188 | + <span class="px-2.5 py-1 rounded-full font-medium" style="background-color: var(--theme-accent); color: var(--theme-body); opacity: 0.9;"> | ||
| 189 | + Grupo | ||
| 190 | + </span> | ||
| 191 | + <span style="color: var(--theme-texto);">→</span> | ||
| 192 | + <span class="px-2.5 py-1 rounded-full" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);"> | ||
| 193 | + Fuente | ||
| 194 | + </span> | ||
| 195 | + </div> | ||
| 196 | + <!-- Versión móvil simplificada --> | ||
| 197 | + <p class="sm:hidden text-sm mb-4" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);"> | ||
| 198 | + Jerarquía: Grupo → Fuente | ||
| 199 | + </p> | ||
| 200 | + </div> | ||
| 201 | + </div> | ||
| 202 | + </header> | ||
| 203 | + | ||
| 204 | + {#if loading} | ||
| 205 | + <div class="flex items-center justify-center py-20"> | ||
| 206 | + <p style="color: var(--theme-texto);">Cargando clasificador...</p> | ||
| 207 | + </div> | ||
| 208 | + {:else} | ||
| 209 | + <!-- Botón móvil para abrir sidebar --> | ||
| 210 | + <div class="lg:hidden px-4 py-3 border-b" style="border-color: var(--theme-borde); background-color: var(--theme-fill);"> | ||
| 211 | + <button | ||
| 212 | + onclick={() => sidebarOpen = !sidebarOpen} | ||
| 213 | + class="flex items-center gap-2 text-sm" | ||
| 214 | + style="color: var(--theme-texto);" | ||
| 215 | + > | ||
| 216 | + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 217 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> | ||
| 218 | + </svg> | ||
| 219 | + <span class="font-medium">{selectedGrupo ? selectedGrupo.desc_grupo_fuente : 'Seleccionar grupo'}</span> | ||
| 220 | + <svg class="w-4 h-4 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 221 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | ||
| 222 | + </svg> | ||
| 223 | + </button> | ||
| 224 | + </div> | ||
| 225 | + | ||
| 226 | + <div class="clasificador-layout max-w-screen-xl mx-auto flex px-4" style="display: flex !important; flex-direction: row;"> | ||
| 227 | + <!-- Sidebar izquierda: Grupos --> | ||
| 228 | + {#if sidebarOpen} | ||
| 229 | + <div | ||
| 230 | + transition:fade={{ duration: 150 }} | ||
| 231 | + class="fixed inset-0 bg-black/30 z-40 lg:hidden" | ||
| 232 | + onclick={() => sidebarOpen = false} | ||
| 233 | + ></div> | ||
| 234 | + {/if} | ||
| 235 | + <aside class=" | ||
| 236 | + {sidebarOpen ? 'translate-x-0' : '-translate-x-full'} | ||
| 237 | + lg:translate-x-0 | ||
| 238 | + fixed lg:relative | ||
| 239 | + inset-y-0 left-0 | ||
| 240 | + w-72 lg:w-64 | ||
| 241 | + z-50 lg:z-auto | ||
| 242 | + transition-transform duration-200 ease-in-out | ||
| 243 | + lg:flex-shrink-0 | ||
| 244 | + shadow-xl lg:shadow-none | ||
| 245 | + sidebar-left | ||
| 246 | + "> | ||
| 247 | + <div class="h-full lg:h-screen lg:sticky lg:top-0 overflow-y-auto py-6 px-4 lg:px-0 lg:pr-6"> | ||
| 248 | + <!-- Cerrar en móvil --> | ||
| 249 | + <div class="flex justify-between items-center mb-4 lg:hidden"> | ||
| 250 | + <span class="text-sm font-medium" style="color: var(--theme-titulo);">Grupos de fuentes</span> | ||
| 251 | + <button onclick={() => sidebarOpen = false} style="color: var(--theme-texto);"> | ||
| 252 | + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 253 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | ||
| 254 | + </svg> | ||
| 255 | + </button> | ||
| 256 | + </div> | ||
| 257 | + | ||
| 258 | + <!-- Buscador --> | ||
| 259 | + <div class="mb-6"> | ||
| 260 | + <input | ||
| 261 | + type="text" | ||
| 262 | + bind:value={searchQuery} | ||
| 263 | + placeholder="Buscar..." | ||
| 264 | + class="w-full px-3 py-2 text-sm rounded-md focus:outline-none focus:ring-2" | ||
| 265 | + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);" | ||
| 266 | + /> | ||
| 267 | + </div> | ||
| 268 | + | ||
| 269 | + <!-- Resultados de búsqueda --> | ||
| 270 | + {#if isSearching} | ||
| 271 | + <div class="mb-4"> | ||
| 272 | + <p class="text-xs uppercase tracking-wide mb-2" style="color: var(--theme-texto);"> | ||
| 273 | + {searchResults.length} resultados | ||
| 274 | + </p> | ||
| 275 | + <div class="space-y-1"> | ||
| 276 | + {#each searchResults as result} | ||
| 277 | + <button | ||
| 278 | + class="w-full text-left px-2 py-2 text-sm rounded transition-all" | ||
| 279 | + style="color: var(--theme-titulo);" | ||
| 280 | + onclick={() => goToSearchResult(result)} | ||
| 281 | + > | ||
| 282 | + <span class="text-xs block" style="color: var(--theme-texto);">Fuente</span> | ||
| 283 | + <span class="font-mono text-xs font-medium" style="color: var(--theme-accent);">{result.fuente}</span> | ||
| 284 | + <span class="ml-1">{result.desc_fuente}</span> | ||
| 285 | + </button> | ||
| 286 | + {/each} | ||
| 287 | + {#if searchResults.length === 0} | ||
| 288 | + <p class="text-sm px-2" style="color: var(--theme-texto);">Sin resultados</p> | ||
| 289 | + {/if} | ||
| 290 | + </div> | ||
| 291 | + </div> | ||
| 292 | + {:else} | ||
| 293 | + <!-- Lista de grupos --> | ||
| 294 | + <nav> | ||
| 295 | + <p class="text-xs uppercase tracking-wide mb-3 px-2 hidden lg:block" style="color: var(--theme-texto);">Grupos</p> | ||
| 296 | + <ul class="space-y-1"> | ||
| 297 | + {#each grupos as grupo} | ||
| 298 | + <li> | ||
| 299 | + <button | ||
| 300 | + class="w-full text-left px-3 py-2 rounded-md text-sm transition-all" | ||
| 301 | + style="{selectedGrupo?.grupo_fuente === grupo.grupo_fuente | ||
| 302 | + ? `background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent); color: var(--theme-titulo); font-weight: 500; border-left: 2px solid var(--theme-accent);` | ||
| 303 | + : `color: var(--theme-texto);`}" | ||
| 304 | + onclick={() => selectGrupo(grupo)} | ||
| 305 | + > | ||
| 306 | + <span class="font-mono text-xs block" style="color: var(--theme-texto);">Grupo {grupo.grupo_fuente}</span> | ||
| 307 | + {grupo.desc_grupo_fuente} | ||
| 308 | + </button> | ||
| 309 | + </li> | ||
| 310 | + {/each} | ||
| 311 | + </ul> | ||
| 312 | + </nav> | ||
| 313 | + {/if} | ||
| 314 | + </div> | ||
| 315 | + </aside> | ||
| 316 | + | ||
| 317 | + <!-- Contenido principal --> | ||
| 318 | + <main class="flex-1 min-w-0 lg:border-l xl:border-r" style="border-color: var(--theme-borde);"> | ||
| 319 | + <div class="px-4 sm:px-6 lg:px-10 py-6 lg:py-8"> | ||
| 320 | + {#if selectedGrupo} | ||
| 321 | + {#key selectedGrupo.grupo_fuente} | ||
| 322 | + <div in:fade={{ duration: 200, delay: 50 }}> | ||
| 323 | + <!-- Título del grupo --> | ||
| 324 | + <div class="mb-8 lg:mb-10 pb-6 lg:pb-8 border-b" style="border-color: var(--theme-borde);"> | ||
| 325 | + <p class="text-sm font-mono mb-1" style="color: var(--theme-texto);">Grupo {selectedGrupo.grupo_fuente}</p> | ||
| 326 | + <h2 class="text-lg sm:text-xl font-medium mb-2 flex items-center gap-2 sm:gap-3 flex-wrap" style="color: var(--theme-titulo);"> | ||
| 327 | + {selectedGrupo.desc_grupo_fuente} | ||
| 328 | + </h2> | ||
| 329 | + <p class="text-sm" style="color: var(--theme-texto);"> | ||
| 330 | + {grupoContent.length} fuentes de financiamiento en este grupo | ||
| 331 | + </p> | ||
| 332 | + </div> | ||
| 333 | + | ||
| 334 | + <!-- Fuentes --> | ||
| 335 | + <div class="space-y-8"> | ||
| 336 | + {#each grupoContent as fuente} | ||
| 337 | + {@const fuenteDescs = parseDescripciones(fuente.descripciones)} | ||
| 338 | + <section | ||
| 339 | + id="f-{fuente.fuente}" | ||
| 340 | + class="scroll-mt-4 {highlightedItem === fuente.fuente ? 'highlighted' : ''}" | ||
| 341 | + > | ||
| 342 | + <div class="flex items-start gap-2 sm:gap-4 mb-3"> | ||
| 343 | + <span class="font-mono text-xs sm:text-sm pt-1" style="color: var(--theme-texto);">{fuente.fuente}</span> | ||
| 344 | + <div class="flex-1"> | ||
| 345 | + <h3 class="text-base sm:text-lg font-medium flex items-center gap-2 flex-wrap" style="color: var(--theme-titulo);"> | ||
| 346 | + <span class="text-left">{fuente.desc_fuente}</span> | ||
| 347 | + {#if fuente.n_variaciones > 1} | ||
| 348 | + <button | ||
| 349 | + class="text-xs font-normal text-orange-500 hover:text-orange-700 transition-colors underline decoration-dotted decoration-orange-300 hover:decoration-orange-500 cursor-pointer" | ||
| 350 | + onclick={() => openDetail(fuente)} | ||
| 351 | + title="Ver variaciones de descripción" | ||
| 352 | + > | ||
| 353 | + {fuente.n_variaciones} var. | ||
| 354 | + </button> | ||
| 355 | + {/if} | ||
| 356 | + <a | ||
| 357 | + href="/fuente/{fuente.fuente}" | ||
| 358 | + class="transition-colors hover:text-[var(--theme-accent)]" | ||
| 359 | + style="color: var(--theme-texto);" | ||
| 360 | + title="Ver página de detalle" | ||
| 361 | + > | ||
| 362 | + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 363 | + <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" /> | ||
| 364 | + </svg> | ||
| 365 | + </a> | ||
| 366 | + </h3> | ||
| 367 | + {#if fuenteDescs.length > 0} | ||
| 368 | + <p class="text-sm mt-1 leading-relaxed" style="color: var(--theme-texto);">{fuenteDescs[0].descripcion}</p> | ||
| 369 | + {/if} | ||
| 370 | + <!-- Vigencia temporal --> | ||
| 371 | + {#if fuente.gestiones} | ||
| 372 | + <p class="text-xs mt-3 font-mono" style="color: var(--theme-texto); opacity: 0.7;"> | ||
| 373 | + Vigente: {fuente.gestiones} | ||
| 374 | + </p> | ||
| 375 | + {/if} | ||
| 376 | + </div> | ||
| 377 | + </div> | ||
| 378 | + </section> | ||
| 379 | + {/each} | ||
| 380 | + </div> | ||
| 381 | + </div> | ||
| 382 | + {/key} | ||
| 383 | + {/if} | ||
| 384 | + </div> | ||
| 385 | + </main> | ||
| 386 | + | ||
| 387 | + <!-- Sidebar derecha: En esta página --> | ||
| 388 | + <aside class="w-56 flex-shrink-0 hidden xl:block"> | ||
| 389 | + <div class="sticky top-0 h-screen overflow-y-auto p-4 border-l" style="border-color: var(--theme-borde);"> | ||
| 390 | + <p class="text-xs uppercase tracking-wide mb-3" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">En esta página</p> | ||
| 391 | + {#if selectedGrupo && !isSearching} | ||
| 392 | + <nav class="space-y-2"> | ||
| 393 | + {#each grupoContent as fuente} | ||
| 394 | + <a | ||
| 395 | + href="#f-{fuente.fuente}" | ||
| 396 | + class="block text-sm truncate transition-colors hover:text-[var(--theme-accent)]" | ||
| 397 | + style="color: var(--theme-texto);" | ||
| 398 | + title="{fuente.desc_fuente}" | ||
| 399 | + > | ||
| 400 | + <span class="font-mono text-xs" style="opacity: 0.6;">{fuente.fuente}</span> | ||
| 401 | + {fuente.desc_fuente} | ||
| 402 | + </a> | ||
| 403 | + {/each} | ||
| 404 | + </nav> | ||
| 405 | + {/if} | ||
| 406 | + </div> | ||
| 407 | + </aside> | ||
| 408 | + </div> | ||
| 409 | + {/if} | ||
| 410 | +</div> | ||
| 411 | + | ||
| 412 | +<!-- Modal de detalle --> | ||
| 413 | +{#if selectedItem} | ||
| 414 | + <div | ||
| 415 | + transition:fade={{ duration: 150 }} | ||
| 416 | + class="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50" | ||
| 417 | + onclick={closeDetail} | ||
| 418 | + > | ||
| 419 | + <div | ||
| 420 | + transition:scale={{ duration: 200, start: 0.95, easing: cubicOut }} | ||
| 421 | + class="rounded-xl shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col" | ||
| 422 | + style="background-color: var(--theme-surface);" | ||
| 423 | + onclick={(e) => e.stopPropagation()} | ||
| 424 | + > | ||
| 425 | + <div class="px-6 py-4 border-b flex justify-between items-start" style="background-color: var(--theme-fill); border-color: var(--theme-borde);"> | ||
| 426 | + <div> | ||
| 427 | + <p class="text-xs uppercase tracking-wide font-medium" style="color: var(--theme-texto);">Fuente de Financiamiento</p> | ||
| 428 | + <h3 class="text-lg font-medium mt-1" style="color: var(--theme-titulo);"> | ||
| 429 | + <span class="font-mono" style="color: var(--theme-texto);">{selectedItem.fuente}</span> | ||
| 430 | + <span class="mx-2" style="color: var(--theme-texto); opacity: 0.5;">·</span> | ||
| 431 | + {selectedItem.desc_fuente} | ||
| 432 | + </h3> | ||
| 433 | + </div> | ||
| 434 | + <button | ||
| 435 | + onclick={closeDetail} | ||
| 436 | + class="text-2xl leading-none" | ||
| 437 | + style="color: var(--theme-texto);" | ||
| 438 | + > | ||
| 439 | + × | ||
| 440 | + </button> | ||
| 441 | + </div> | ||
| 442 | + | ||
| 443 | + <div class="p-6 overflow-y-auto flex-1"> | ||
| 444 | + <h4 class="text-sm font-medium mb-4" style="color: var(--theme-titulo);"> | ||
| 445 | + {#if selectedItem.n_variaciones > 1} | ||
| 446 | + Descripciones ({selectedItem.n_variaciones} variaciones) | ||
| 447 | + {:else} | ||
| 448 | + Descripción | ||
| 449 | + {/if} | ||
| 450 | + </h4> | ||
| 451 | + | ||
| 452 | + <div class="space-y-4"> | ||
| 453 | + {#each parseDescripciones(selectedItem.descripciones) as desc, i} | ||
| 454 | + <div class="border-l-2 pl-4 py-3 rounded-r" style="{i === 0 ? `border-color: var(--theme-accent); background-color: color-mix(in srgb, var(--theme-accent) 10%, transparent);` : `border-color: var(--theme-borde);`}"> | ||
| 455 | + <p class="text-sm mb-2" style="color: var(--theme-texto);"> | ||
| 456 | + {#if i === 0 && selectedItem.n_variaciones > 1} | ||
| 457 | + <span class="font-medium" style="color: var(--theme-accent);">Vigente</span> | ||
| 458 | + <span class="mx-1" style="opacity: 0.5;">·</span> | ||
| 459 | + {/if} | ||
| 460 | + <span class="font-mono">{desc.rangos}</span> | ||
| 461 | + </p> | ||
| 462 | + <p class="text-base leading-relaxed" style="color: var(--theme-titulo);">{desc.descripcion}</p> | ||
| 463 | + </div> | ||
| 464 | + {/each} | ||
| 465 | + </div> | ||
| 466 | + | ||
| 467 | + <!-- Vigencia temporal --> | ||
| 468 | + {#if selectedItem.gestiones} | ||
| 469 | + <div class="mt-6 pt-4 border-t" style="border-color: var(--theme-borde);"> | ||
| 470 | + <p class="text-xs font-mono" style="color: var(--theme-texto);"> | ||
| 471 | + <span class="font-medium">Años con datos:</span> {selectedItem.gestiones} | ||
| 472 | + </p> | ||
| 473 | + </div> | ||
| 474 | + {/if} | ||
| 475 | + </div> | ||
| 476 | + </div> | ||
| 477 | + </div> | ||
| 478 | +{/if} | ||
| 479 | + | ||
| 480 | +<style> | ||
| 481 | + /* Sidebar izquierdo: con fondo en móvil, transparente en desktop */ | ||
| 482 | + .sidebar-left { | ||
| 483 | + background-color: var(--theme-surface); | ||
| 484 | + } | ||
| 485 | + @media (min-width: 1024px) { | ||
| 486 | + .sidebar-left { | ||
| 487 | + background-color: transparent; | ||
| 488 | + } | ||
| 489 | + } | ||
| 490 | + | ||
| 491 | + /* Highlight pulse animation para resultados de búsqueda */ | ||
| 492 | + @keyframes highlight-pulse { | ||
| 493 | + 0%, 100% { | ||
| 494 | + box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-accent) 30%, transparent); | ||
| 495 | + } | ||
| 496 | + 50% { | ||
| 497 | + box-shadow: 0 0 0 4px color-mix(in srgb, var(--theme-accent) 50%, transparent); | ||
| 498 | + } | ||
| 499 | + } | ||
| 500 | + | ||
| 501 | + .highlighted { | ||
| 502 | + animation: highlight-pulse 0.8s ease-in-out 2; | ||
| 503 | + border-radius: 0.5rem; | ||
| 504 | + padding: 0.5rem; | ||
| 505 | + margin-left: -0.5rem; | ||
| 506 | + } | ||
| 507 | + | ||
| 508 | + /* Layout principal - fallback nativo */ | ||
| 509 | + .clasificador-layout { | ||
| 510 | + display: flex !important; | ||
| 511 | + flex-direction: row !important; | ||
| 512 | + } | ||
| 513 | + | ||
| 514 | + /* Sidebar - fallback para responsive */ | ||
| 515 | + .sidebar-left { | ||
| 516 | + position: fixed; | ||
| 517 | + transform: translateX(-100%); | ||
| 518 | + } | ||
| 519 | + | ||
| 520 | + @media (min-width: 1024px) { | ||
| 521 | + .sidebar-left { | ||
| 522 | + position: relative !important; | ||
| 523 | + transform: translateX(0) !important; | ||
| 524 | + flex-shrink: 0; | ||
| 525 | + width: 16rem; | ||
| 526 | + z-index: auto !important; | ||
| 527 | + box-shadow: none !important; | ||
| 528 | + } | ||
| 529 | + } | ||
| 530 | + | ||
| 531 | + @media (max-width: 1023px) { | ||
| 532 | + .clasificador-layout { | ||
| 533 | + display: block !important; | ||
| 534 | + } | ||
| 535 | + } | ||
| 536 | +</style> |
| 1 | +<script> | ||
| 2 | + import { supabase } from '$lib/supabase'; | ||
| 3 | + import { onMount } from 'svelte'; | ||
| 4 | + import { fade, scale } from 'svelte/transition'; | ||
| 5 | + import { cubicOut } from 'svelte/easing'; | ||
| 6 | + | ||
| 7 | + let loading = $state(true); | ||
| 8 | + let searchQuery = $state(''); | ||
| 9 | + let grupos = $state([]); | ||
| 10 | + let allItems = $state([]); | ||
| 11 | + let selectedGrupo = $state(null); | ||
| 12 | + let selectedItem = $state(null); | ||
| 13 | + let highlightedItem = $state(null); | ||
| 14 | + let sidebarOpen = $state(false); | ||
| 15 | + | ||
| 16 | + onMount(async () => { | ||
| 17 | + requestAnimationFrame(() => { | ||
| 18 | + document.body.offsetHeight; | ||
| 19 | + }); | ||
| 20 | + | ||
| 21 | + const { data, error } = await supabase | ||
| 22 | + .schema('ppto') | ||
| 23 | + .from('clas_organismos') | ||
| 24 | + .select('*') | ||
| 25 | + .order('organismo'); | ||
| 26 | + | ||
| 27 | + if (!error && data) { | ||
| 28 | + allItems = data; | ||
| 29 | + | ||
| 30 | + // Extraer grupos únicos desde organismo_grupo | ||
| 31 | + const gruposUnicos = [...new Set(data.map(d => d.organismo_grupo))].filter(g => g != null); | ||
| 32 | + | ||
| 33 | + grupos = gruposUnicos.map(grupoNum => { | ||
| 34 | + const sample = data.find(d => d.organismo_grupo === grupoNum); | ||
| 35 | + return { | ||
| 36 | + organismo_grupo: grupoNum, | ||
| 37 | + organismo_desc_grupo: sample?.organismo_desc_grupo || `Grupo ${grupoNum}`, | ||
| 38 | + organismo_sigla_grupo: sample?.organismo_sigla_grupo || '' | ||
| 39 | + }; | ||
| 40 | + }).sort((a, b) => Number(a.organismo_grupo) - Number(b.organismo_grupo)); | ||
| 41 | + | ||
| 42 | + if (grupos.length > 0) { | ||
| 43 | + selectedGrupo = grupos[0]; | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + loading = false; | ||
| 48 | + }); | ||
| 49 | + | ||
| 50 | + // Helper para obtener subgrupos de un grupo | ||
| 51 | + function getItemsForGrupo(grupo) { | ||
| 52 | + if (!grupo) return { subgrupos: [] }; | ||
| 53 | + | ||
| 54 | + const grupoNum = grupo.organismo_grupo; | ||
| 55 | + const itemsDelGrupo = allItems.filter(item => item.organismo_grupo === grupoNum); | ||
| 56 | + const subgruposUnicos = [...new Set(itemsDelGrupo.map(i => i.organismo_sub_grupo))].filter(s => s != null); | ||
| 57 | + | ||
| 58 | + const subgrupos = subgruposUnicos.map(subgrupoNum => { | ||
| 59 | + const itemsDelSubgrupo = itemsDelGrupo.filter(i => i.organismo_sub_grupo === subgrupoNum); | ||
| 60 | + const sample = itemsDelSubgrupo[0]; | ||
| 61 | + | ||
| 62 | + return { | ||
| 63 | + organismo_sub_grupo: subgrupoNum, | ||
| 64 | + organismo_desc_sub_grupo: sample?.organismo_desc_sub_grupo || `Subgrupo ${subgrupoNum}`, | ||
| 65 | + organismo_sigla_sub_grupo: sample?.organismo_sigla_sub_grupo || '', | ||
| 66 | + organismo_grupo: grupoNum, | ||
| 67 | + organismos: itemsDelSubgrupo.sort((a, b) => String(a.organismo).localeCompare(String(b.organismo))) | ||
| 68 | + }; | ||
| 69 | + }).sort((a, b) => Number(a.organismo_sub_grupo) - Number(b.organismo_sub_grupo)); | ||
| 70 | + | ||
| 71 | + return { subgrupos }; | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + // Búsqueda global | ||
| 75 | + function searchGlobal(query) { | ||
| 76 | + if (!query || query.length < 2) return []; | ||
| 77 | + const q = query.toLowerCase(); | ||
| 78 | + | ||
| 79 | + return allItems | ||
| 80 | + .filter(item => | ||
| 81 | + item.organismo?.toString().toLowerCase().includes(q) || | ||
| 82 | + item.organismo_desc_organismo?.toLowerCase().includes(q) | ||
| 83 | + ) | ||
| 84 | + .slice(0, 20); | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + function selectGrupo(grupo) { | ||
| 88 | + selectedGrupo = grupo; | ||
| 89 | + searchQuery = ''; | ||
| 90 | + highlightedItem = null; | ||
| 91 | + sidebarOpen = false; | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + function openDetail(item) { | ||
| 95 | + selectedItem = item; | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + function closeDetail() { | ||
| 99 | + selectedItem = null; | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + function goToSearchResult(item) { | ||
| 103 | + const targetGrupo = grupos.find(g => g.organismo_grupo === item.organismo_grupo); | ||
| 104 | + | ||
| 105 | + if (targetGrupo) { | ||
| 106 | + selectedGrupo = targetGrupo; | ||
| 107 | + highlightedItem = item.organismo; | ||
| 108 | + searchQuery = ''; | ||
| 109 | + sidebarOpen = false; | ||
| 110 | + | ||
| 111 | + setTimeout(() => { | ||
| 112 | + const element = document.getElementById(`org-${item.organismo}`); | ||
| 113 | + if (element) { | ||
| 114 | + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||
| 115 | + } | ||
| 116 | + }, 100); | ||
| 117 | + | ||
| 118 | + setTimeout(() => { | ||
| 119 | + highlightedItem = null; | ||
| 120 | + }, 2000); | ||
| 121 | + } | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + function parseDescripciones(descripcionesStr) { | ||
| 125 | + if (!descripcionesStr) return []; | ||
| 126 | + try { | ||
| 127 | + const parsed = typeof descripcionesStr === 'string' | ||
| 128 | + ? JSON.parse(descripcionesStr) | ||
| 129 | + : descripcionesStr; | ||
| 130 | + return parsed.sort((a, b) => { | ||
| 131 | + const maxYearA = getMaxYear(a.rangos); | ||
| 132 | + const maxYearB = getMaxYear(b.rangos); | ||
| 133 | + return maxYearB - maxYearA; | ||
| 134 | + }); | ||
| 135 | + } catch { | ||
| 136 | + return []; | ||
| 137 | + } | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + function getMaxYear(rangos) { | ||
| 141 | + if (!rangos) return 0; | ||
| 142 | + const years = rangos.match(/\d{4}/g); | ||
| 143 | + if (!years) return 0; | ||
| 144 | + return Math.max(...years.map(y => parseInt(y))); | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + let grupoContent = $derived(selectedGrupo ? getItemsForGrupo(selectedGrupo) : { subgrupos: [] }); | ||
| 148 | + let searchResults = $derived(searchGlobal(searchQuery)); | ||
| 149 | + let isSearching = $derived(searchQuery.length >= 2); | ||
| 150 | + | ||
| 151 | + // Contadores | ||
| 152 | + let totalItems = $derived(allItems.length); | ||
| 153 | + let numSubgrupos = $derived(() => { | ||
| 154 | + const pairs = new Set(); | ||
| 155 | + allItems.forEach(i => pairs.add(`${i.organismo_grupo}-${i.organismo_sub_grupo}`)); | ||
| 156 | + return pairs.size; | ||
| 157 | + }); | ||
| 158 | +</script> | ||
| 159 | + | ||
| 160 | +<svelte:head> | ||
| 161 | + <title>Organismos | Presupuesto Público</title> | ||
| 162 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 163 | + <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" /> | ||
| 164 | +</svelte:head> | ||
| 165 | + | ||
| 166 | +<div class="min-h-screen" style="font-family: 'Instrument Sans', sans-serif; background-color: var(--theme-body); color: var(--theme-titulo);"> | ||
| 167 | + <!-- Header --> | ||
| 168 | + <header class="border-b" style="border-color: var(--theme-borde);"> | ||
| 169 | + <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-6"> | ||
| 170 | + <nav class="mb-4" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;"> | ||
| 171 | + <a href="/clasificadores" class="sm:hidden transition-colors" style="color: var(--theme-texto);"> | ||
| 172 | + ← Clasificadores | ||
| 173 | + </a> | ||
| 174 | + <div class="hidden sm:flex items-center gap-2" style="color: var(--theme-texto);"> | ||
| 175 | + <a href="/" class="transition-colors hover:opacity-80">Inicio</a> | ||
| 176 | + <span style="opacity: 0.5;">/</span> | ||
| 177 | + <a href="/clasificadores" class="transition-colors hover:opacity-80">Clasificadores</a> | ||
| 178 | + </div> | ||
| 179 | + </nav> | ||
| 180 | + | ||
| 181 | + <div class="max-w-3xl"> | ||
| 182 | + <p class="text-xs uppercase tracking-widest mb-2" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);"> | ||
| 183 | + Clasificador de Organismos Financiadores | ||
| 184 | + </p> | ||
| 185 | + <h1 class="text-3xl mb-3" style="font-family: 'DM Serif Display', serif; color: var(--theme-titulo);"> | ||
| 186 | + ¿Quién financia el gasto? | ||
| 187 | + </h1> | ||
| 188 | + | ||
| 189 | + <p class="leading-relaxed mb-3" style="color: var(--theme-texto);"> | ||
| 190 | + Clasifica las fuentes de financiamiento según su <strong style="color: var(--theme-titulo);">origen</strong>: | ||
| 191 | + recursos internos, crédito externo, donaciones, etc. | ||
| 192 | + </p> | ||
| 193 | + | ||
| 194 | + {#if totalItems > 0} | ||
| 195 | + <p class="text-sm mb-5" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);"> | ||
| 196 | + <span style="color: var(--theme-titulo); font-weight: 500;">{totalItems}</span> organismos en | ||
| 197 | + {grupos.length} grupos y {numSubgrupos()} subgrupos | ||
| 198 | + </p> | ||
| 199 | + {/if} | ||
| 200 | + | ||
| 201 | + <div class="hidden sm:flex flex-wrap items-center gap-2 sm:gap-3 text-xs mb-6" style="font-family: 'DM Mono', monospace;"> | ||
| 202 | + <span class="px-2.5 py-1 rounded-full font-medium" style="background-color: var(--theme-accent); color: var(--theme-body);"> | ||
| 203 | + Grupo | ||
| 204 | + </span> | ||
| 205 | + <span style="color: var(--theme-texto);">→</span> | ||
| 206 | + <span class="px-2.5 py-1 rounded-full" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);"> | ||
| 207 | + Subgrupo | ||
| 208 | + </span> | ||
| 209 | + <span style="color: var(--theme-texto);">→</span> | ||
| 210 | + <span class="px-2.5 py-1 rounded-full" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);"> | ||
| 211 | + Organismo | ||
| 212 | + </span> | ||
| 213 | + </div> | ||
| 214 | + </div> | ||
| 215 | + </div> | ||
| 216 | + </header> | ||
| 217 | + | ||
| 218 | + {#if loading} | ||
| 219 | + <div class="flex items-center justify-center py-20"> | ||
| 220 | + <p style="color: var(--theme-texto);">Cargando clasificador...</p> | ||
| 221 | + </div> | ||
| 222 | + {:else} | ||
| 223 | + <!-- Botón móvil --> | ||
| 224 | + <div class="lg:hidden px-4 py-3 border-b" style="border-color: var(--theme-borde); background-color: var(--theme-fill);"> | ||
| 225 | + <button | ||
| 226 | + onclick={() => sidebarOpen = !sidebarOpen} | ||
| 227 | + class="flex items-center gap-2 text-sm w-full" | ||
| 228 | + style="color: var(--theme-texto);" | ||
| 229 | + > | ||
| 230 | + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 231 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> | ||
| 232 | + </svg> | ||
| 233 | + <span class="font-medium truncate">{selectedGrupo ? selectedGrupo.organismo_desc_grupo : 'Seleccionar grupo'}</span> | ||
| 234 | + <svg class="w-4 h-4 ml-auto flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 235 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | ||
| 236 | + </svg> | ||
| 237 | + </button> | ||
| 238 | + </div> | ||
| 239 | + | ||
| 240 | + <div class="max-w-screen-xl mx-auto flex"> | ||
| 241 | + <!-- Sidebar izquierda --> | ||
| 242 | + {#if sidebarOpen} | ||
| 243 | + <div | ||
| 244 | + transition:fade={{ duration: 150 }} | ||
| 245 | + class="fixed inset-0 bg-black/30 z-40 lg:hidden" | ||
| 246 | + onclick={() => sidebarOpen = false} | ||
| 247 | + ></div> | ||
| 248 | + {/if} | ||
| 249 | + <aside class=" | ||
| 250 | + {sidebarOpen ? 'translate-x-0' : '-translate-x-full'} | ||
| 251 | + lg:translate-x-0 | ||
| 252 | + fixed lg:relative | ||
| 253 | + inset-y-0 left-0 | ||
| 254 | + w-72 lg:w-56 | ||
| 255 | + z-50 lg:z-auto | ||
| 256 | + transition-transform duration-200 ease-in-out | ||
| 257 | + lg:flex-shrink-0 | ||
| 258 | + shadow-xl lg:shadow-none | ||
| 259 | + " style="background-color: var(--theme-body);"> | ||
| 260 | + <div class="h-full lg:h-screen lg:sticky lg:top-0 overflow-y-auto py-6 px-4 lg:px-0 lg:pr-4"> | ||
| 261 | + <div class="flex justify-between items-center mb-4 lg:hidden"> | ||
| 262 | + <span class="text-sm font-medium" style="color: var(--theme-titulo);">Grupos</span> | ||
| 263 | + <button onclick={() => sidebarOpen = false} style="color: var(--theme-texto);"> | ||
| 264 | + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 265 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | ||
| 266 | + </svg> | ||
| 267 | + </button> | ||
| 268 | + </div> | ||
| 269 | + | ||
| 270 | + <!-- Buscador --> | ||
| 271 | + <div class="mb-6"> | ||
| 272 | + <input | ||
| 273 | + type="text" | ||
| 274 | + bind:value={searchQuery} | ||
| 275 | + placeholder="Buscar organismo..." | ||
| 276 | + class="w-full px-3 py-2 text-sm rounded-md focus:outline-none focus:ring-2" | ||
| 277 | + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);" | ||
| 278 | + /> | ||
| 279 | + </div> | ||
| 280 | + | ||
| 281 | + {#if isSearching} | ||
| 282 | + <div class="space-y-1"> | ||
| 283 | + <p class="text-xs uppercase tracking-wide mb-2 px-2" style="color: var(--theme-texto);"> | ||
| 284 | + {searchResults.length} resultados | ||
| 285 | + </p> | ||
| 286 | + {#each searchResults as result} | ||
| 287 | + <button | ||
| 288 | + class="w-full text-left px-2 py-2 text-sm rounded transition-all" | ||
| 289 | + style="color: var(--theme-titulo);" | ||
| 290 | + onclick={() => goToSearchResult(result)} | ||
| 291 | + > | ||
| 292 | + <span class="font-mono text-xs font-medium" style="color: var(--theme-accent);">{result.organismo}</span> | ||
| 293 | + <span class="ml-1 block truncate">{result.organismo_desc_organismo}</span> | ||
| 294 | + </button> | ||
| 295 | + {/each} | ||
| 296 | + {#if searchResults.length === 0} | ||
| 297 | + <p class="text-sm px-2" style="color: var(--theme-texto);">Sin resultados</p> | ||
| 298 | + {/if} | ||
| 299 | + </div> | ||
| 300 | + {:else} | ||
| 301 | + <nav> | ||
| 302 | + <p class="text-xs uppercase tracking-wide mb-3 px-2 hidden lg:block" style="color: var(--theme-texto);">Grupos</p> | ||
| 303 | + <ul class="space-y-1"> | ||
| 304 | + {#each grupos as grupo} | ||
| 305 | + <li> | ||
| 306 | + <button | ||
| 307 | + class="w-full text-left px-3 py-2 rounded-md text-sm transition-all" | ||
| 308 | + style="{selectedGrupo?.organismo_grupo === grupo.organismo_grupo | ||
| 309 | + ? `background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent); color: var(--theme-titulo); font-weight: 500; border-left: 2px solid var(--theme-accent);` | ||
| 310 | + : `color: var(--theme-texto);`}" | ||
| 311 | + onclick={() => selectGrupo(grupo)} | ||
| 312 | + > | ||
| 313 | + <span class="font-mono text-xs block" style="color: var(--theme-texto);">{grupo.organismo_grupo}</span> | ||
| 314 | + {grupo.organismo_desc_grupo} | ||
| 315 | + </button> | ||
| 316 | + </li> | ||
| 317 | + {/each} | ||
| 318 | + </ul> | ||
| 319 | + </nav> | ||
| 320 | + {/if} | ||
| 321 | + </div> | ||
| 322 | + </aside> | ||
| 323 | + | ||
| 324 | + <!-- Contenido principal --> | ||
| 325 | + <main class="flex-1 min-w-0 lg:border-l xl:border-r" style="border-color: var(--theme-borde);"> | ||
| 326 | + <div class="px-4 sm:px-6 lg:px-10 py-6 lg:py-8"> | ||
| 327 | + {#if selectedGrupo} | ||
| 328 | + {#key selectedGrupo.organismo_grupo} | ||
| 329 | + <div in:fade={{ duration: 200, delay: 50 }}> | ||
| 330 | + <!-- Título del grupo --> | ||
| 331 | + <div class="mb-8 lg:mb-10 pb-6 lg:pb-8 border-b" style="border-color: var(--theme-borde);"> | ||
| 332 | + <p class="text-sm font-mono mb-1" style="color: var(--theme-texto);">Grupo {selectedGrupo.organismo_grupo}</p> | ||
| 333 | + <h2 class="text-lg sm:text-xl font-medium mb-2" style="color: var(--theme-titulo);"> | ||
| 334 | + {selectedGrupo.organismo_desc_grupo} | ||
| 335 | + </h2> | ||
| 336 | + </div> | ||
| 337 | + | ||
| 338 | + <!-- Subgrupos --> | ||
| 339 | + <div class="space-y-12"> | ||
| 340 | + {#each grupoContent.subgrupos as subgrupo} | ||
| 341 | + <section | ||
| 342 | + id="sg-{subgrupo.organismo_sub_grupo}" | ||
| 343 | + class="scroll-mt-4" | ||
| 344 | + > | ||
| 345 | + <div class="flex items-start gap-2 sm:gap-4 mb-3"> | ||
| 346 | + <span class="font-mono text-xs sm:text-sm pt-1" style="color: var(--theme-texto);">{subgrupo.organismo_sub_grupo}</span> | ||
| 347 | + <div class="flex-1"> | ||
| 348 | + <h3 class="text-base sm:text-lg font-medium" style="color: var(--theme-titulo);"> | ||
| 349 | + {subgrupo.organismo_desc_sub_grupo} | ||
| 350 | + </h3> | ||
| 351 | + </div> | ||
| 352 | + </div> | ||
| 353 | + | ||
| 354 | + <!-- Organismos --> | ||
| 355 | + {#if subgrupo.organismos?.length > 0} | ||
| 356 | + <div class="ml-4 sm:ml-8 lg:ml-16 space-y-3 border-l pl-4 sm:pl-6 lg:pl-8" style="border-color: var(--theme-borde);"> | ||
| 357 | + {#each subgrupo.organismos as org} | ||
| 358 | + {@const orgDescs = parseDescripciones(org.descripciones)} | ||
| 359 | + <div | ||
| 360 | + id="org-{org.organismo}" | ||
| 361 | + class="scroll-mt-4 py-2 transition-all duration-300 rounded-md px-2 -mx-2" | ||
| 362 | + style="{highlightedItem === org.organismo ? 'background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent);' : ''}" | ||
| 363 | + > | ||
| 364 | + <div class="flex items-start gap-2 sm:gap-3"> | ||
| 365 | + <span class="font-mono text-xs pt-0.5 flex-shrink-0" style="color: var(--theme-texto);">{org.organismo}</span> | ||
| 366 | + <div class="flex-1 min-w-0"> | ||
| 367 | + <h4 class="text-sm font-medium flex items-center gap-2 flex-wrap" style="color: var(--theme-titulo);"> | ||
| 368 | + <span class="text-left">{org.organismo_desc_organismo}</span> | ||
| 369 | + {#if org.n_variaciones > 1} | ||
| 370 | + <button | ||
| 371 | + class="text-xs font-normal text-orange-500 hover:text-orange-700 transition-colors underline decoration-dotted decoration-orange-300 hover:decoration-orange-500 cursor-pointer" | ||
| 372 | + onclick={() => openDetail(org)} | ||
| 373 | + title="Ver variaciones de descripción" | ||
| 374 | + > | ||
| 375 | + {org.n_variaciones} var. | ||
| 376 | + </button> | ||
| 377 | + {/if} | ||
| 378 | + <a | ||
| 379 | + href="/organismo/{org.organismo}" | ||
| 380 | + class="transition-colors hover:text-[var(--theme-accent)]" | ||
| 381 | + style="color: var(--theme-texto);" | ||
| 382 | + title="Ver página de detalle" | ||
| 383 | + > | ||
| 384 | + <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 385 | + <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" /> | ||
| 386 | + </svg> | ||
| 387 | + </a> | ||
| 388 | + </h4> | ||
| 389 | + {#if org.organismo_sigla_organismo && org.organismo_sigla_organismo !== 'NC'} | ||
| 390 | + <p class="text-xs font-mono mt-0.5" style="color: var(--theme-texto); opacity: 0.7;">{org.organismo_sigla_organismo}</p> | ||
| 391 | + {/if} | ||
| 392 | + {#if orgDescs.length > 0 && orgDescs[0].descripcion} | ||
| 393 | + <p class="text-xs mt-1 leading-relaxed" style="color: var(--theme-texto);">{orgDescs[0].descripcion}</p> | ||
| 394 | + {/if} | ||
| 395 | + {#if org.gestiones} | ||
| 396 | + <p class="text-xs mt-1 font-mono" style="color: var(--theme-texto); opacity: 0.6;"> | ||
| 397 | + Vigente: {org.gestiones} | ||
| 398 | + </p> | ||
| 399 | + {/if} | ||
| 400 | + </div> | ||
| 401 | + </div> | ||
| 402 | + </div> | ||
| 403 | + {/each} | ||
| 404 | + </div> | ||
| 405 | + {/if} | ||
| 406 | + </section> | ||
| 407 | + {/each} | ||
| 408 | + </div> | ||
| 409 | + </div> | ||
| 410 | + {/key} | ||
| 411 | + {/if} | ||
| 412 | + </div> | ||
| 413 | + </main> | ||
| 414 | + | ||
| 415 | + <!-- Sidebar derecha: En esta página --> | ||
| 416 | + <aside class="w-56 flex-shrink-0 hidden xl:block"> | ||
| 417 | + <div class="sticky top-0 h-screen overflow-y-auto p-4 border-l" style="border-color: var(--theme-borde);"> | ||
| 418 | + <p class="text-xs uppercase tracking-wide mb-3" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">En esta página</p> | ||
| 419 | + {#if selectedGrupo && !isSearching} | ||
| 420 | + <nav class="space-y-2"> | ||
| 421 | + {#each grupoContent.subgrupos as subgrupo} | ||
| 422 | + <div> | ||
| 423 | + <a | ||
| 424 | + href="#sg-{subgrupo.organismo_sub_grupo}" | ||
| 425 | + class="block text-sm truncate transition-colors hover:text-[var(--theme-accent)]" | ||
| 426 | + style="color: var(--theme-texto);" | ||
| 427 | + title="{subgrupo.organismo_desc_sub_grupo}" | ||
| 428 | + > | ||
| 429 | + {subgrupo.organismo_desc_sub_grupo} | ||
| 430 | + </a> | ||
| 431 | + {#if subgrupo.organismos?.length > 0} | ||
| 432 | + <div class="ml-3 mt-1 space-y-1 border-l pl-2" style="border-color: var(--theme-borde);"> | ||
| 433 | + {#each subgrupo.organismos.slice(0, 5) as org} | ||
| 434 | + <a | ||
| 435 | + href="#org-{org.organismo}" | ||
| 436 | + class="block text-xs truncate transition-colors hover:text-[var(--theme-accent)]" | ||
| 437 | + style="color: var(--theme-texto); opacity: 0.7;" | ||
| 438 | + title="{org.organismo_desc_organismo}" | ||
| 439 | + > | ||
| 440 | + {org.organismo_desc_organismo} | ||
| 441 | + </a> | ||
| 442 | + {/each} | ||
| 443 | + {#if subgrupo.organismos.length > 5} | ||
| 444 | + <span class="text-xs" style="color: var(--theme-texto); opacity: 0.5;">+{subgrupo.organismos.length - 5} más</span> | ||
| 445 | + {/if} | ||
| 446 | + </div> | ||
| 447 | + {/if} | ||
| 448 | + </div> | ||
| 449 | + {/each} | ||
| 450 | + </nav> | ||
| 451 | + {/if} | ||
| 452 | + </div> | ||
| 453 | + </aside> | ||
| 454 | + </div> | ||
| 455 | + {/if} | ||
| 456 | +</div> | ||
| 457 | + | ||
| 458 | +<!-- Modal de detalle --> | ||
| 459 | +{#if selectedItem} | ||
| 460 | + <div | ||
| 461 | + transition:fade={{ duration: 150 }} | ||
| 462 | + class="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50" | ||
| 463 | + onclick={closeDetail} | ||
| 464 | + > | ||
| 465 | + <div | ||
| 466 | + transition:scale={{ duration: 200, start: 0.95, easing: cubicOut }} | ||
| 467 | + class="rounded-xl shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col" | ||
| 468 | + style="background-color: var(--theme-surface);" | ||
| 469 | + onclick={(e) => e.stopPropagation()} | ||
| 470 | + > | ||
| 471 | + <div class="px-6 py-4 border-b flex justify-between items-start" style="background-color: var(--theme-fill); border-color: var(--theme-borde);"> | ||
| 472 | + <div> | ||
| 473 | + <p class="text-xs uppercase tracking-wide font-medium" style="color: var(--theme-texto);">Organismo</p> | ||
| 474 | + <h3 class="text-lg font-medium mt-1" style="color: var(--theme-titulo);"> | ||
| 475 | + <span class="font-mono" style="color: var(--theme-texto);">{selectedItem.organismo}</span> | ||
| 476 | + <span class="mx-2" style="color: var(--theme-texto); opacity: 0.5;">·</span> | ||
| 477 | + {selectedItem.organismo_desc_organismo} | ||
| 478 | + </h3> | ||
| 479 | + </div> | ||
| 480 | + <button | ||
| 481 | + onclick={closeDetail} | ||
| 482 | + class="text-2xl leading-none" | ||
| 483 | + style="color: var(--theme-texto);" | ||
| 484 | + > | ||
| 485 | + × | ||
| 486 | + </button> | ||
| 487 | + </div> | ||
| 488 | + | ||
| 489 | + <div class="p-6 overflow-y-auto flex-1"> | ||
| 490 | + <h4 class="text-sm font-medium mb-4" style="color: var(--theme-titulo);"> | ||
| 491 | + {#if selectedItem.n_variaciones > 1} | ||
| 492 | + Descripciones ({selectedItem.n_variaciones} variaciones) | ||
| 493 | + {:else} | ||
| 494 | + Descripción | ||
| 495 | + {/if} | ||
| 496 | + </h4> | ||
| 497 | + | ||
| 498 | + {#if selectedItem.descripciones} | ||
| 499 | + {@const descs = parseDescripciones(selectedItem.descripciones)} | ||
| 500 | + {#if descs.length > 0} | ||
| 501 | + <div class="space-y-4"> | ||
| 502 | + {#each descs as desc} | ||
| 503 | + <div class="p-4 rounded-lg" style="background-color: var(--theme-fill);"> | ||
| 504 | + <p class="text-xs font-mono mb-2" style="color: var(--theme-accent);">{desc.rangos}</p> | ||
| 505 | + <p class="text-sm leading-relaxed" style="color: var(--theme-titulo);">{desc.descripcion || 'Sin descripción'}</p> | ||
| 506 | + </div> | ||
| 507 | + {/each} | ||
| 508 | + </div> | ||
| 509 | + {:else} | ||
| 510 | + <p class="text-sm" style="color: var(--theme-texto);">Sin descripciones registradas.</p> | ||
| 511 | + {/if} | ||
| 512 | + {:else} | ||
| 513 | + <p class="text-sm" style="color: var(--theme-texto);">Sin descripciones registradas.</p> | ||
| 514 | + {/if} | ||
| 515 | + </div> | ||
| 516 | + </div> | ||
| 517 | + </div> | ||
| 518 | +{/if} |
| ... | @@ -48,16 +48,20 @@ | ... | @@ -48,16 +48,20 @@ |
| 48 | }); | 48 | }); |
| 49 | 49 | ||
| 50 | // Helpers para derivar jerarquía desde código rubro | 50 | // Helpers para derivar jerarquía desde código rubro |
| 51 | + // Jerarquía: Tipo (XX000) → Clase (XXX00) → Cuenta (XXXX0) → Subcuenta (XXXXX) | ||
| 51 | function getTipoFromRubro(rubro) { | 52 | function getTipoFromRubro(rubro) { |
| 52 | - return rubro.charAt(0); | 53 | + // Tipo = primeros 2 dígitos (XX de XX000) |
| 54 | + return rubro.substring(0, 2); | ||
| 53 | } | 55 | } |
| 54 | 56 | ||
| 55 | function getClaseFromRubro(rubro) { | 57 | function getClaseFromRubro(rubro) { |
| 56 | - return rubro.substring(0, 2); | 58 | + // Clase = primeros 3 dígitos (XXX de XXX00) |
| 59 | + return rubro.substring(0, 3); | ||
| 57 | } | 60 | } |
| 58 | 61 | ||
| 59 | function getCuentaFromRubro(rubro) { | 62 | function getCuentaFromRubro(rubro) { |
| 60 | - return rubro.substring(0, 3); | 63 | + // Cuenta = primeros 4 dígitos (XXXX de XXXX0) |
| 64 | + return rubro.substring(0, 4); | ||
| 61 | } | 65 | } |
| 62 | 66 | ||
| 63 | function getItemsForTipo(tipoCode) { | 67 | function getItemsForTipo(tipoCode) { | ... | ... |
| ... | @@ -2,18 +2,40 @@ import { supabase } from '$lib/supabase'; | ... | @@ -2,18 +2,40 @@ import { supabase } from '$lib/supabase'; |
| 2 | import { error } from '@sveltejs/kit'; | 2 | import { error } from '@sveltejs/kit'; |
| 3 | 3 | ||
| 4 | // Funciones para derivar jerarquía del código finfun | 4 | // Funciones para derivar jerarquía del código finfun |
| 5 | -// Jerarquía: Finalidad (1 dígito) → Grupo Función (2 dígitos) → Función (3+ dígitos) | 5 | +// Jerarquía: Finalidad (1-9 o 10) → Grupo Función (2-3 dígitos) → Función (3+ dígitos) |
| 6 | +// Nota: Finalidad 10 es especial - tiene 2 dígitos, sus hijos empiezan con "10" | ||
| 7 | + | ||
| 6 | function getFinalidadCode(finfun) { | 8 | function getFinalidadCode(finfun) { |
| 7 | - return finfun.charAt(0); | 9 | + const code = String(finfun); |
| 10 | + // Si empieza con "10", la finalidad es "10" | ||
| 11 | + if (code.startsWith('10')) { | ||
| 12 | + return '10'; | ||
| 13 | + } | ||
| 14 | + // Si no, la finalidad es el primer dígito | ||
| 15 | + return code.charAt(0); | ||
| 8 | } | 16 | } |
| 9 | 17 | ||
| 10 | function getGrpFuncionCode(finfun) { | 18 | function getGrpFuncionCode(finfun) { |
| 11 | - return finfun.substring(0, 2); | 19 | + const code = String(finfun); |
| 20 | + // Si empieza con "10", el grupo función son los primeros 3 dígitos (ej: "101", "102") | ||
| 21 | + if (code.startsWith('10')) { | ||
| 22 | + return code.substring(0, 3); | ||
| 23 | + } | ||
| 24 | + // Si no, son los primeros 2 dígitos (ej: "11", "21", "93") | ||
| 25 | + return code.substring(0, 2); | ||
| 12 | } | 26 | } |
| 13 | 27 | ||
| 14 | function getNivel(finfun) { | 28 | function getNivel(finfun) { |
| 15 | - if (finfun.length === 1) return 'finalidad'; | 29 | + const code = String(finfun); |
| 16 | - if (finfun.length === 2) return 'grpfuncion'; | 30 | + // Finalidades 1-9 tienen longitud 1, finalidad 10 tiene longitud 2 |
| 31 | + if (code.length === 1) return 'finalidad'; | ||
| 32 | + if (code === '10') return 'finalidad'; | ||
| 33 | + // GrpFuncion: 2 dígitos para 1-9, 3 dígitos para 10 | ||
| 34 | + if (code.startsWith('10')) { | ||
| 35 | + if (code.length === 3) return 'grpfuncion'; | ||
| 36 | + return 'funcion'; | ||
| 37 | + } | ||
| 38 | + if (code.length === 2) return 'grpfuncion'; | ||
| 17 | return 'funcion'; | 39 | return 'funcion'; |
| 18 | } | 40 | } |
| 19 | 41 | ||
| ... | @@ -77,27 +99,40 @@ export async function load({ params }) { | ... | @@ -77,27 +99,40 @@ export async function load({ params }) { |
| 77 | 99 | ||
| 78 | // Buscar hijos según nivel | 100 | // Buscar hijos según nivel |
| 79 | if (nivel === 'finalidad') { | 101 | if (nivel === 'finalidad') { |
| 80 | - // Hijos: grupos de función que empiecen con el mismo dígito | 102 | + // Hijos: grupos de función de esta finalidad |
| 81 | const { data: hijosData } = await supabase | 103 | const { data: hijosData } = await supabase |
| 82 | .schema('ppto') | 104 | .schema('ppto') |
| 83 | .from('clas_finfun') | 105 | .from('clas_finfun') |
| 84 | .select('*') | 106 | .select('*') |
| 85 | .eq('nivel', 'grpfuncion') | 107 | .eq('nivel', 'grpfuncion') |
| 86 | - .like('finfun', `${codigo}%`) | ||
| 87 | .order('finfun'); | 108 | .order('finfun'); |
| 88 | 109 | ||
| 89 | - hijos = hijosData || []; | 110 | + // Filtrar solo los hijos directos de esta finalidad |
| 111 | + // Para finalidad "1": hijos son "11", "12", ..., "19" (NO "10x") | ||
| 112 | + // Para finalidad "10": hijos son "101", "102", ..., "109" | ||
| 113 | + if (hijosData) { | ||
| 114 | + hijos = hijosData.filter(h => { | ||
| 115 | + const hijoPadre = getFinalidadCode(h.finfun); | ||
| 116 | + return hijoPadre === codigo; | ||
| 117 | + }); | ||
| 118 | + } | ||
| 90 | } else if (nivel === 'grpfuncion') { | 119 | } else if (nivel === 'grpfuncion') { |
| 91 | - // Hijos: funciones que empiecen con los mismos 2 dígitos | 120 | + // Hijos: funciones de este grupo |
| 121 | + const grpCode = getGrpFuncionCode(codigo); | ||
| 92 | const { data: hijosData } = await supabase | 122 | const { data: hijosData } = await supabase |
| 93 | .schema('ppto') | 123 | .schema('ppto') |
| 94 | .from('clas_finfun') | 124 | .from('clas_finfun') |
| 95 | .select('*') | 125 | .select('*') |
| 96 | .eq('nivel', 'funcion') | 126 | .eq('nivel', 'funcion') |
| 97 | - .like('finfun', `${codigo}%`) | ||
| 98 | .order('finfun'); | 127 | .order('finfun'); |
| 99 | 128 | ||
| 100 | - hijos = hijosData || []; | 129 | + // Filtrar solo los hijos directos de este grupo |
| 130 | + if (hijosData) { | ||
| 131 | + hijos = hijosData.filter(h => { | ||
| 132 | + const hijoGrp = getGrpFuncionCode(h.finfun); | ||
| 133 | + return hijoGrp === grpCode; | ||
| 134 | + }); | ||
| 135 | + } | ||
| 101 | } | 136 | } |
| 102 | // funcion no tiene hijos | 137 | // funcion no tiene hijos |
| 103 | 138 | ... | ... |
| ... | @@ -1029,9 +1029,9 @@ | ... | @@ -1029,9 +1029,9 @@ |
| 1029 | <p class="context-hint">Funciones relacionadas. <span class="desktop-hint">Haz click</span><span class="mobile-hint">Toca</span> para explorar.</p> | 1029 | <p class="context-hint">Funciones relacionadas. <span class="desktop-hint">Haz click</span><span class="mobile-hint">Toca</span> para explorar.</p> |
| 1030 | <div class="tree"> | 1030 | <div class="tree"> |
| 1031 | {#each padres as padre, i} | 1031 | {#each padres as padre, i} |
| 1032 | - <a href="/finfun/{padre.objeto}" class="tree-node has-tooltip" style="--indent: {i}" data-tooltip="{padre.desc_finfun}"> | 1032 | + <a href="/finfun/{padre.finfun}" class="tree-node has-tooltip" style="--indent: {i}" data-tooltip="{padre.desc_finfun}"> |
| 1033 | <span class="tree-dot"></span> | 1033 | <span class="tree-dot"></span> |
| 1034 | - <span class="tree-code">{padre.objeto}</span> | 1034 | + <span class="tree-code">{padre.finfun}</span> |
| 1035 | <span class="tree-name">{padre.desc_finfun}</span> | 1035 | <span class="tree-name">{padre.desc_finfun}</span> |
| 1036 | </a> | 1036 | </a> |
| 1037 | {/each} | 1037 | {/each} |
| ... | @@ -1045,9 +1045,9 @@ | ... | @@ -1045,9 +1045,9 @@ |
| 1045 | {#if hijos.length > 0} | 1045 | {#if hijos.length > 0} |
| 1046 | <div class="tree-children" style="--indent: {padres.length + 1}"> | 1046 | <div class="tree-children" style="--indent: {padres.length + 1}"> |
| 1047 | {#each hijos as hijo} | 1047 | {#each hijos as hijo} |
| 1048 | - <a href="/finfun/{hijo.objeto}" class="tree-node tree-node-child has-tooltip" data-tooltip="{hijo.desc_finfun}"> | 1048 | + <a href="/finfun/{hijo.finfun}" class="tree-node tree-node-child has-tooltip" data-tooltip="{hijo.desc_finfun}"> |
| 1049 | <span class="tree-dot"></span> | 1049 | <span class="tree-dot"></span> |
| 1050 | - <span class="tree-code">{hijo.objeto}</span> | 1050 | + <span class="tree-code">{hijo.finfun}</span> |
| 1051 | <span class="tree-name">{hijo.desc_finfun}</span> | 1051 | <span class="tree-name">{hijo.desc_finfun}</span> |
| 1052 | </a> | 1052 | </a> |
| 1053 | {/each} | 1053 | {/each} | ... | ... |
src/routes/fuente/[codigo]/+page.server.js
0 → 100644
| 1 | +import { supabase } from '$lib/supabase'; | ||
| 2 | +import { error } from '@sveltejs/kit'; | ||
| 3 | + | ||
| 4 | +export async function load({ params }) { | ||
| 5 | + console.time('[SERVER] Total load fuente'); | ||
| 6 | + const { codigo } = params; | ||
| 7 | + | ||
| 8 | + console.time('[SERVER] Query clas_fuentes'); | ||
| 9 | + const { data, error: dbError } = await supabase | ||
| 10 | + .schema('ppto') | ||
| 11 | + .from('clas_fuentes') | ||
| 12 | + .select('*') | ||
| 13 | + .eq('fuente', codigo); | ||
| 14 | + console.timeEnd('[SERVER] Query clas_fuentes'); | ||
| 15 | + | ||
| 16 | + if (dbError || !data || data.length === 0) { | ||
| 17 | + throw error(404, 'Fuente no encontrada'); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + const fuente = data[0]; | ||
| 21 | + | ||
| 22 | + // Obtener hermanos (otras fuentes del mismo grupo) | ||
| 23 | + let hermanos = []; | ||
| 24 | + | ||
| 25 | + console.time('[SERVER] Query hermanos'); | ||
| 26 | + const { data: hermanosData } = await supabase | ||
| 27 | + .schema('ppto') | ||
| 28 | + .from('clas_fuentes') | ||
| 29 | + .select('*') | ||
| 30 | + .eq('grupo_fuente', fuente.grupo_fuente) | ||
| 31 | + .neq('fuente', codigo) | ||
| 32 | + .order('fuente'); | ||
| 33 | + | ||
| 34 | + if (hermanosData) { | ||
| 35 | + hermanos = hermanosData; | ||
| 36 | + } | ||
| 37 | + console.timeEnd('[SERVER] Query hermanos'); | ||
| 38 | + | ||
| 39 | + console.timeEnd('[SERVER] Total load fuente'); | ||
| 40 | + return { | ||
| 41 | + fuente, | ||
| 42 | + hermanos | ||
| 43 | + }; | ||
| 44 | +} |
src/routes/fuente/[codigo]/+page.svelte
0 → 100644
| 1 | +<script> | ||
| 2 | + import { onMount } from 'svelte'; | ||
| 3 | + | ||
| 4 | + // Datos del servidor (reactivos) | ||
| 5 | + let { data } = $props(); | ||
| 6 | + let fuenteData = $derived(data.fuente); | ||
| 7 | + let hermanos = $derived(data.hermanos); | ||
| 8 | + | ||
| 9 | + // Estado | ||
| 10 | + let mounted = $state(false); | ||
| 11 | + | ||
| 12 | + onMount(() => { | ||
| 13 | + setTimeout(() => { mounted = true; }, 50); | ||
| 14 | + }); | ||
| 15 | + | ||
| 16 | + function parseDescripciones(descripcionesStr) { | ||
| 17 | + if (!descripcionesStr) return []; | ||
| 18 | + try { | ||
| 19 | + const parsed = typeof descripcionesStr === 'string' | ||
| 20 | + ? JSON.parse(descripcionesStr) | ||
| 21 | + : descripcionesStr; | ||
| 22 | + return parsed.sort((a, b) => { | ||
| 23 | + const maxYearA = getMaxYear(a.rangos); | ||
| 24 | + const maxYearB = getMaxYear(b.rangos); | ||
| 25 | + return maxYearB - maxYearA; | ||
| 26 | + }); | ||
| 27 | + } catch { | ||
| 28 | + return []; | ||
| 29 | + } | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + function getMaxYear(rangos) { | ||
| 33 | + if (!rangos) return 0; | ||
| 34 | + const years = rangos.match(/\d{4}/g); | ||
| 35 | + if (!years) return 0; | ||
| 36 | + return Math.max(...years.map(y => parseInt(y))); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + let descripciones = $derived(parseDescripciones(fuenteData.descripciones)); | ||
| 40 | +</script> | ||
| 41 | + | ||
| 42 | +<svelte:head> | ||
| 43 | + <title>{fuenteData.desc_fuente || fuenteData.fuente} | Clasificador de Fuentes</title> | ||
| 44 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 45 | + <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" /> | ||
| 46 | +</svelte:head> | ||
| 47 | + | ||
| 48 | +<div class="page" class:mounted> | ||
| 49 | + <!-- Link a clasificadores --> | ||
| 50 | + <nav class="page-nav"> | ||
| 51 | + <a href="/clasificadores/fuentes" class="back-link"> | ||
| 52 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 53 | + <path d="M19 12H5M12 19l-7-7 7-7"/> | ||
| 54 | + </svg> | ||
| 55 | + Clasificador | ||
| 56 | + </a> | ||
| 57 | + </nav> | ||
| 58 | + | ||
| 59 | + <div class="dashboard-layout"> | ||
| 60 | + <!-- CONTEXTO IZQUIERDO --> | ||
| 61 | + <aside class="dashboard-context"> | ||
| 62 | + <div class="context-card"> | ||
| 63 | + <h2 class="context-main-title">Contexto</h2> | ||
| 64 | + | ||
| 65 | + <!-- Info del grupo --> | ||
| 66 | + <div class="context-section"> | ||
| 67 | + <h3 class="context-subtitle">Ubicación</h3> | ||
| 68 | + <div class="ubicacion-info"> | ||
| 69 | + <div class="ubicacion-item"> | ||
| 70 | + <span class="ubicacion-label">Grupo {fuenteData.grupo_fuente}</span> | ||
| 71 | + <span class="ubicacion-value">{fuenteData.desc_grupo_fuente}</span> | ||
| 72 | + </div> | ||
| 73 | + </div> | ||
| 74 | + </div> | ||
| 75 | + | ||
| 76 | + <!-- Hermanos --> | ||
| 77 | + {#if hermanos.length > 0} | ||
| 78 | + <div class="context-separator"></div> | ||
| 79 | + <div class="context-section"> | ||
| 80 | + <h3 class="context-subtitle">Mismo grupo ({hermanos.length})</h3> | ||
| 81 | + <p class="context-hint">Otras fuentes del mismo grupo.</p> | ||
| 82 | + <div class="hermanos-list"> | ||
| 83 | + {#each hermanos.slice(0, 10) as hermano} | ||
| 84 | + <a href="/fuente/{hermano.fuente}" class="hermano-link"> | ||
| 85 | + <span class="hermano-code">{hermano.fuente}</span> | ||
| 86 | + <span class="hermano-name">{hermano.desc_fuente}</span> | ||
| 87 | + </a> | ||
| 88 | + {/each} | ||
| 89 | + {#if hermanos.length > 10} | ||
| 90 | + <span class="hermanos-more">+{hermanos.length - 10} más</span> | ||
| 91 | + {/if} | ||
| 92 | + </div> | ||
| 93 | + </div> | ||
| 94 | + {/if} | ||
| 95 | + </div> | ||
| 96 | + </aside> | ||
| 97 | + | ||
| 98 | + <!-- CONTENIDO PRINCIPAL --> | ||
| 99 | + <main class="dashboard-main"> | ||
| 100 | + <div class="main-content"> | ||
| 101 | + <!-- Header --> | ||
| 102 | + <div class="main-header"> | ||
| 103 | + <div class="main-title-pills"> | ||
| 104 | + <span class="pill pill-codigo">{fuenteData.fuente}</span> | ||
| 105 | + </div> | ||
| 106 | + <h1 class="main-title-name">{fuenteData.desc_fuente}</h1> | ||
| 107 | + </div> | ||
| 108 | + | ||
| 109 | + <!-- Info cards --> | ||
| 110 | + <div class="info-grid"> | ||
| 111 | + <div class="info-card"> | ||
| 112 | + <span class="info-label">Grupo</span> | ||
| 113 | + <span class="info-value">{fuenteData.grupo_fuente}</span> | ||
| 114 | + <span class="info-desc">{fuenteData.desc_grupo_fuente}</span> | ||
| 115 | + </div> | ||
| 116 | + | ||
| 117 | + {#if fuenteData.gestiones} | ||
| 118 | + <div class="info-card"> | ||
| 119 | + <span class="info-label">Gestiones</span> | ||
| 120 | + <span class="info-value">{fuenteData.n_gestiones || ''} años</span> | ||
| 121 | + <span class="info-desc">{fuenteData.gestiones}</span> | ||
| 122 | + </div> | ||
| 123 | + {/if} | ||
| 124 | + | ||
| 125 | + {#if fuenteData.n_variaciones > 1} | ||
| 126 | + <div class="info-card"> | ||
| 127 | + <span class="info-label">Variaciones</span> | ||
| 128 | + <span class="info-value">{fuenteData.n_variaciones}</span> | ||
| 129 | + <span class="info-desc">Cambios en la descripción a lo largo del tiempo</span> | ||
| 130 | + </div> | ||
| 131 | + {/if} | ||
| 132 | + </div> | ||
| 133 | + | ||
| 134 | + <!-- Descripciones --> | ||
| 135 | + {#if descripciones.length > 0} | ||
| 136 | + <div class="descripciones-section"> | ||
| 137 | + <h2 class="descripciones-title"> | ||
| 138 | + {#if descripciones.length > 1} | ||
| 139 | + Descripciones ({descripciones.length} variaciones) | ||
| 140 | + {:else} | ||
| 141 | + Descripción | ||
| 142 | + {/if} | ||
| 143 | + </h2> | ||
| 144 | + <div class="descripciones-list"> | ||
| 145 | + {#each descripciones as desc, i} | ||
| 146 | + <div class="descripcion-item" class:vigente={i === 0}> | ||
| 147 | + <p class="descripcion-rangos"> | ||
| 148 | + {#if i === 0 && descripciones.length > 1} | ||
| 149 | + <span class="vigente-badge">Vigente</span> | ||
| 150 | + {/if} | ||
| 151 | + {desc.rangos} | ||
| 152 | + </p> | ||
| 153 | + <p class="descripcion-texto">{desc.descripcion}</p> | ||
| 154 | + </div> | ||
| 155 | + {/each} | ||
| 156 | + </div> | ||
| 157 | + </div> | ||
| 158 | + {/if} | ||
| 159 | + </div> | ||
| 160 | + </main> | ||
| 161 | + </div> | ||
| 162 | +</div> | ||
| 163 | + | ||
| 164 | +<style> | ||
| 165 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 166 | + BASE | ||
| 167 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 168 | + .page { | ||
| 169 | + min-height: 100vh; | ||
| 170 | + padding: 1rem; | ||
| 171 | + opacity: 0; | ||
| 172 | + transform: translateY(10px); | ||
| 173 | + transition: opacity 0.4s ease, transform 0.4s ease; | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + .page.mounted { | ||
| 177 | + opacity: 1; | ||
| 178 | + transform: translateY(0); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 182 | + NAV | ||
| 183 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 184 | + .page-nav { | ||
| 185 | + max-width: 1400px; | ||
| 186 | + margin: 0 auto 1.5rem; | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + .back-link { | ||
| 190 | + display: inline-flex; | ||
| 191 | + align-items: center; | ||
| 192 | + gap: 0.5rem; | ||
| 193 | + color: var(--text-secondary, #888); | ||
| 194 | + text-decoration: none; | ||
| 195 | + font-size: 0.875rem; | ||
| 196 | + transition: color 0.2s; | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + .back-link:hover { | ||
| 200 | + color: var(--text-primary, #fff); | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 204 | + LAYOUT | ||
| 205 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 206 | + .dashboard-layout { | ||
| 207 | + display: grid; | ||
| 208 | + grid-template-columns: 300px 1fr; | ||
| 209 | + gap: 2rem; | ||
| 210 | + max-width: 1400px; | ||
| 211 | + margin: 0 auto; | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + @media (max-width: 900px) { | ||
| 215 | + .dashboard-layout { | ||
| 216 | + grid-template-columns: 1fr; | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + .dashboard-context { | ||
| 220 | + order: 2; | ||
| 221 | + } | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 225 | + SIDEBAR | ||
| 226 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 227 | + .dashboard-context { | ||
| 228 | + position: sticky; | ||
| 229 | + top: 5rem; | ||
| 230 | + height: fit-content; | ||
| 231 | + } | ||
| 232 | + | ||
| 233 | + .context-card { | ||
| 234 | + background: var(--card-bg, rgba(255, 255, 255, 0.03)); | ||
| 235 | + border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 236 | + border-radius: 16px; | ||
| 237 | + padding: 1.5rem; | ||
| 238 | + } | ||
| 239 | + | ||
| 240 | + .context-main-title { | ||
| 241 | + font-family: 'Instrument Sans', sans-serif; | ||
| 242 | + font-size: 0.75rem; | ||
| 243 | + font-weight: 600; | ||
| 244 | + text-transform: uppercase; | ||
| 245 | + letter-spacing: 0.05em; | ||
| 246 | + color: var(--text-tertiary, #666); | ||
| 247 | + margin-bottom: 1.5rem; | ||
| 248 | + } | ||
| 249 | + | ||
| 250 | + .context-section { | ||
| 251 | + margin-bottom: 1.5rem; | ||
| 252 | + } | ||
| 253 | + | ||
| 254 | + .context-subtitle { | ||
| 255 | + font-family: 'Instrument Sans', sans-serif; | ||
| 256 | + font-size: 0.8rem; | ||
| 257 | + font-weight: 500; | ||
| 258 | + color: var(--text-secondary, #aaa); | ||
| 259 | + margin-bottom: 0.75rem; | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + .context-hint { | ||
| 263 | + font-size: 0.75rem; | ||
| 264 | + color: var(--text-tertiary, #666); | ||
| 265 | + margin-bottom: 1rem; | ||
| 266 | + } | ||
| 267 | + | ||
| 268 | + .context-separator { | ||
| 269 | + height: 1px; | ||
| 270 | + background: var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 271 | + margin: 1.5rem 0; | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 275 | + UBICACION INFO | ||
| 276 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 277 | + .ubicacion-info { | ||
| 278 | + display: flex; | ||
| 279 | + flex-direction: column; | ||
| 280 | + gap: 0.75rem; | ||
| 281 | + } | ||
| 282 | + | ||
| 283 | + .ubicacion-item { | ||
| 284 | + display: flex; | ||
| 285 | + flex-direction: column; | ||
| 286 | + gap: 0.25rem; | ||
| 287 | + padding: 0.75rem; | ||
| 288 | + background: var(--hover-bg, rgba(255, 255, 255, 0.03)); | ||
| 289 | + border-radius: 8px; | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + .ubicacion-label { | ||
| 293 | + font-family: 'DM Mono', monospace; | ||
| 294 | + font-size: 0.7rem; | ||
| 295 | + color: var(--text-tertiary, #666); | ||
| 296 | + text-transform: uppercase; | ||
| 297 | + } | ||
| 298 | + | ||
| 299 | + .ubicacion-value { | ||
| 300 | + font-size: 0.85rem; | ||
| 301 | + color: var(--text-primary, #fff); | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 305 | + HERMANOS LIST | ||
| 306 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 307 | + .hermanos-list { | ||
| 308 | + display: flex; | ||
| 309 | + flex-direction: column; | ||
| 310 | + gap: 0.25rem; | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + .hermano-link { | ||
| 314 | + display: flex; | ||
| 315 | + align-items: center; | ||
| 316 | + gap: 0.5rem; | ||
| 317 | + padding: 0.5rem 0.75rem; | ||
| 318 | + border-radius: 6px; | ||
| 319 | + text-decoration: none; | ||
| 320 | + color: var(--text-secondary, #aaa); | ||
| 321 | + font-size: 0.8rem; | ||
| 322 | + transition: all 0.2s ease; | ||
| 323 | + } | ||
| 324 | + | ||
| 325 | + .hermano-link:hover { | ||
| 326 | + background: var(--hover-bg, rgba(255, 255, 255, 0.05)); | ||
| 327 | + color: var(--text-primary, #fff); | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + .hermano-code { | ||
| 331 | + font-family: 'DM Mono', monospace; | ||
| 332 | + font-size: 0.7rem; | ||
| 333 | + opacity: 0.6; | ||
| 334 | + flex-shrink: 0; | ||
| 335 | + min-width: 2.5rem; | ||
| 336 | + } | ||
| 337 | + | ||
| 338 | + .hermano-name { | ||
| 339 | + white-space: nowrap; | ||
| 340 | + overflow: hidden; | ||
| 341 | + text-overflow: ellipsis; | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + .hermanos-more { | ||
| 345 | + font-size: 0.75rem; | ||
| 346 | + color: var(--text-tertiary, #666); | ||
| 347 | + padding: 0.5rem 0.75rem; | ||
| 348 | + } | ||
| 349 | + | ||
| 350 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 351 | + MAIN CONTENT | ||
| 352 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 353 | + .dashboard-main { | ||
| 354 | + min-width: 0; | ||
| 355 | + } | ||
| 356 | + | ||
| 357 | + .main-content { | ||
| 358 | + background: var(--card-bg, rgba(255, 255, 255, 0.03)); | ||
| 359 | + border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 360 | + border-radius: 16px; | ||
| 361 | + padding: 2rem; | ||
| 362 | + } | ||
| 363 | + | ||
| 364 | + .main-header { | ||
| 365 | + margin-bottom: 2rem; | ||
| 366 | + } | ||
| 367 | + | ||
| 368 | + .main-title-pills { | ||
| 369 | + display: flex; | ||
| 370 | + gap: 0.5rem; | ||
| 371 | + margin-bottom: 0.75rem; | ||
| 372 | + } | ||
| 373 | + | ||
| 374 | + .pill { | ||
| 375 | + padding: 0.25rem 0.75rem; | ||
| 376 | + border-radius: 999px; | ||
| 377 | + font-size: 0.75rem; | ||
| 378 | + font-weight: 500; | ||
| 379 | + } | ||
| 380 | + | ||
| 381 | + .pill-codigo { | ||
| 382 | + background: var(--hover-bg, rgba(255, 255, 255, 0.05)); | ||
| 383 | + color: var(--text-secondary, #aaa); | ||
| 384 | + font-family: 'DM Mono', monospace; | ||
| 385 | + } | ||
| 386 | + | ||
| 387 | + .main-title-name { | ||
| 388 | + font-family: 'DM Serif Display', serif; | ||
| 389 | + font-size: 2rem; | ||
| 390 | + font-weight: 400; | ||
| 391 | + color: var(--text-primary, #fff); | ||
| 392 | + line-height: 1.2; | ||
| 393 | + margin: 0; | ||
| 394 | + } | ||
| 395 | + | ||
| 396 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 397 | + INFO GRID | ||
| 398 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 399 | + .info-grid { | ||
| 400 | + display: grid; | ||
| 401 | + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | ||
| 402 | + gap: 1rem; | ||
| 403 | + margin-bottom: 2rem; | ||
| 404 | + } | ||
| 405 | + | ||
| 406 | + .info-card { | ||
| 407 | + display: flex; | ||
| 408 | + flex-direction: column; | ||
| 409 | + gap: 0.25rem; | ||
| 410 | + padding: 1rem; | ||
| 411 | + background: var(--hover-bg, rgba(255, 255, 255, 0.03)); | ||
| 412 | + border-radius: 12px; | ||
| 413 | + border: 1px solid var(--border-color, rgba(255, 255, 255, 0.05)); | ||
| 414 | + } | ||
| 415 | + | ||
| 416 | + .info-label { | ||
| 417 | + font-size: 0.75rem; | ||
| 418 | + text-transform: uppercase; | ||
| 419 | + letter-spacing: 0.05em; | ||
| 420 | + color: var(--text-tertiary, #666); | ||
| 421 | + } | ||
| 422 | + | ||
| 423 | + .info-value { | ||
| 424 | + font-family: 'DM Mono', monospace; | ||
| 425 | + font-size: 1.25rem; | ||
| 426 | + color: var(--text-primary, #fff); | ||
| 427 | + } | ||
| 428 | + | ||
| 429 | + .info-desc { | ||
| 430 | + font-size: 0.8rem; | ||
| 431 | + color: var(--text-secondary, #aaa); | ||
| 432 | + line-height: 1.4; | ||
| 433 | + } | ||
| 434 | + | ||
| 435 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 436 | + DESCRIPCIONES | ||
| 437 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 438 | + .descripciones-section { | ||
| 439 | + border-top: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 440 | + padding-top: 2rem; | ||
| 441 | + } | ||
| 442 | + | ||
| 443 | + .descripciones-title { | ||
| 444 | + font-family: 'Instrument Sans', sans-serif; | ||
| 445 | + font-size: 0.875rem; | ||
| 446 | + font-weight: 600; | ||
| 447 | + color: var(--text-secondary, #aaa); | ||
| 448 | + margin-bottom: 1rem; | ||
| 449 | + } | ||
| 450 | + | ||
| 451 | + .descripciones-list { | ||
| 452 | + display: flex; | ||
| 453 | + flex-direction: column; | ||
| 454 | + gap: 1rem; | ||
| 455 | + } | ||
| 456 | + | ||
| 457 | + .descripcion-item { | ||
| 458 | + padding: 1rem; | ||
| 459 | + border-left: 2px solid var(--border-color, rgba(255, 255, 255, 0.1)); | ||
| 460 | + border-radius: 0 8px 8px 0; | ||
| 461 | + background: var(--hover-bg, rgba(255, 255, 255, 0.02)); | ||
| 462 | + } | ||
| 463 | + | ||
| 464 | + .descripcion-item.vigente { | ||
| 465 | + border-left-color: var(--theme-accent, #3b82f6); | ||
| 466 | + background: color-mix(in srgb, var(--theme-accent, #3b82f6) 10%, transparent); | ||
| 467 | + } | ||
| 468 | + | ||
| 469 | + .descripcion-rangos { | ||
| 470 | + font-family: 'DM Mono', monospace; | ||
| 471 | + font-size: 0.75rem; | ||
| 472 | + color: var(--text-tertiary, #666); | ||
| 473 | + margin-bottom: 0.5rem; | ||
| 474 | + } | ||
| 475 | + | ||
| 476 | + .vigente-badge { | ||
| 477 | + display: inline-block; | ||
| 478 | + padding: 0.125rem 0.5rem; | ||
| 479 | + border-radius: 999px; | ||
| 480 | + background: var(--theme-accent, #3b82f6); | ||
| 481 | + color: white; | ||
| 482 | + font-size: 0.65rem; | ||
| 483 | + font-weight: 600; | ||
| 484 | + text-transform: uppercase; | ||
| 485 | + letter-spacing: 0.05em; | ||
| 486 | + margin-right: 0.5rem; | ||
| 487 | + } | ||
| 488 | + | ||
| 489 | + .descripcion-texto { | ||
| 490 | + font-size: 0.9rem; | ||
| 491 | + color: var(--text-primary, #fff); | ||
| 492 | + line-height: 1.6; | ||
| 493 | + } | ||
| 494 | +</style> |
| 1 | +import { supabase } from '$lib/supabase'; | ||
| 2 | +import { error } from '@sveltejs/kit'; | ||
| 3 | + | ||
| 4 | +export async function load({ params }) { | ||
| 5 | + console.time('[SERVER] Total load organismo'); | ||
| 6 | + const { codigo } = params; | ||
| 7 | + | ||
| 8 | + console.time('[SERVER] Query clas_organismos'); | ||
| 9 | + const { data, error: dbError } = await supabase | ||
| 10 | + .schema('ppto') | ||
| 11 | + .from('clas_organismos') | ||
| 12 | + .select('*') | ||
| 13 | + .eq('organismo', codigo); | ||
| 14 | + console.timeEnd('[SERVER] Query clas_organismos'); | ||
| 15 | + | ||
| 16 | + if (dbError || !data || data.length === 0) { | ||
| 17 | + throw error(404, 'Organismo no encontrado'); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + const organismo = data[0]; | ||
| 21 | + | ||
| 22 | + // Obtener hermanos (otros organismos del mismo subgrupo) | ||
| 23 | + let hermanos = []; | ||
| 24 | + | ||
| 25 | + console.time('[SERVER] Query hermanos'); | ||
| 26 | + const { data: hermanosData } = await supabase | ||
| 27 | + .schema('ppto') | ||
| 28 | + .from('clas_organismos') | ||
| 29 | + .select('*') | ||
| 30 | + .eq('organismo_grupo', organismo.organismo_grupo) | ||
| 31 | + .eq('organismo_sub_grupo', organismo.organismo_sub_grupo) | ||
| 32 | + .neq('organismo', codigo) | ||
| 33 | + .order('organismo'); | ||
| 34 | + | ||
| 35 | + if (hermanosData) { | ||
| 36 | + hermanos = hermanosData; | ||
| 37 | + } | ||
| 38 | + console.timeEnd('[SERVER] Query hermanos'); | ||
| 39 | + | ||
| 40 | + console.timeEnd('[SERVER] Total load organismo'); | ||
| 41 | + return { | ||
| 42 | + organismo, | ||
| 43 | + hermanos | ||
| 44 | + }; | ||
| 45 | +} |
src/routes/organismo/[codigo]/+page.svelte
0 → 100644
| 1 | +<script> | ||
| 2 | + import { onMount } from 'svelte'; | ||
| 3 | + | ||
| 4 | + // Datos del servidor (reactivos) | ||
| 5 | + let { data } = $props(); | ||
| 6 | + let organismoData = $derived(data.organismo); | ||
| 7 | + let hermanos = $derived(data.hermanos); | ||
| 8 | + | ||
| 9 | + // Estado | ||
| 10 | + let mounted = $state(false); | ||
| 11 | + | ||
| 12 | + onMount(() => { | ||
| 13 | + setTimeout(() => { mounted = true; }, 50); | ||
| 14 | + }); | ||
| 15 | +</script> | ||
| 16 | + | ||
| 17 | +<svelte:head> | ||
| 18 | + <title>{organismoData.organismo_desc_organismo || organismoData.organismo} | Clasificador de Organismos</title> | ||
| 19 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 20 | + <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" /> | ||
| 21 | +</svelte:head> | ||
| 22 | + | ||
| 23 | +<div class="page" class:mounted> | ||
| 24 | + <!-- Link a clasificadores --> | ||
| 25 | + <nav class="page-nav"> | ||
| 26 | + <a href="/clasificadores/organismos" class="back-link"> | ||
| 27 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 28 | + <path d="M19 12H5M12 19l-7-7 7-7"/> | ||
| 29 | + </svg> | ||
| 30 | + Clasificador | ||
| 31 | + </a> | ||
| 32 | + </nav> | ||
| 33 | + | ||
| 34 | + <div class="dashboard-layout"> | ||
| 35 | + <!-- CONTEXTO IZQUIERDO --> | ||
| 36 | + <aside class="dashboard-context"> | ||
| 37 | + <div class="context-card"> | ||
| 38 | + <h2 class="context-main-title">Contexto</h2> | ||
| 39 | + | ||
| 40 | + <!-- Info del grupo y subgrupo --> | ||
| 41 | + <div class="context-section"> | ||
| 42 | + <h3 class="context-subtitle">Ubicación</h3> | ||
| 43 | + <div class="ubicacion-info"> | ||
| 44 | + <div class="ubicacion-item"> | ||
| 45 | + <span class="ubicacion-label">Grupo {organismoData.organismo_grupo}</span> | ||
| 46 | + <span class="ubicacion-value">{organismoData.organismo_desc_grupo}</span> | ||
| 47 | + </div> | ||
| 48 | + <div class="ubicacion-item"> | ||
| 49 | + <span class="ubicacion-label">Subgrupo {organismoData.organismo_sub_grupo}</span> | ||
| 50 | + <span class="ubicacion-value">{organismoData.organismo_desc_sub_grupo}</span> | ||
| 51 | + </div> | ||
| 52 | + </div> | ||
| 53 | + </div> | ||
| 54 | + | ||
| 55 | + <!-- Hermanos --> | ||
| 56 | + {#if hermanos.length > 0} | ||
| 57 | + <div class="context-separator"></div> | ||
| 58 | + <div class="context-section"> | ||
| 59 | + <h3 class="context-subtitle">Mismo subgrupo ({hermanos.length})</h3> | ||
| 60 | + <p class="context-hint">Otros organismos del mismo subgrupo.</p> | ||
| 61 | + <div class="hermanos-list"> | ||
| 62 | + {#each hermanos.slice(0, 10) as hermano} | ||
| 63 | + <a href="/organismo/{hermano.organismo}" class="hermano-link"> | ||
| 64 | + <span class="hermano-code">{hermano.organismo}</span> | ||
| 65 | + <span class="hermano-name">{hermano.organismo_desc_organismo}</span> | ||
| 66 | + </a> | ||
| 67 | + {/each} | ||
| 68 | + {#if hermanos.length > 10} | ||
| 69 | + <span class="hermanos-more">+{hermanos.length - 10} más</span> | ||
| 70 | + {/if} | ||
| 71 | + </div> | ||
| 72 | + </div> | ||
| 73 | + {/if} | ||
| 74 | + </div> | ||
| 75 | + </aside> | ||
| 76 | + | ||
| 77 | + <!-- CONTENIDO PRINCIPAL --> | ||
| 78 | + <main class="dashboard-main"> | ||
| 79 | + <div class="main-content"> | ||
| 80 | + <!-- Header --> | ||
| 81 | + <div class="main-header"> | ||
| 82 | + <div class="main-title-pills"> | ||
| 83 | + <span class="pill pill-codigo">{organismoData.organismo}</span> | ||
| 84 | + </div> | ||
| 85 | + <h1 class="main-title-name">{organismoData.organismo_desc_organismo}</h1> | ||
| 86 | + {#if organismoData.organismo_sigla_organismo && organismoData.organismo_sigla_organismo !== 'NC'} | ||
| 87 | + <p class="main-sigla">{organismoData.organismo_sigla_organismo}</p> | ||
| 88 | + {/if} | ||
| 89 | + </div> | ||
| 90 | + | ||
| 91 | + <!-- Info cards --> | ||
| 92 | + <div class="info-grid"> | ||
| 93 | + <div class="info-card"> | ||
| 94 | + <span class="info-label">Grupo</span> | ||
| 95 | + <span class="info-value">{organismoData.organismo_grupo}</span> | ||
| 96 | + <span class="info-desc">{organismoData.organismo_desc_grupo}</span> | ||
| 97 | + </div> | ||
| 98 | + | ||
| 99 | + <div class="info-card"> | ||
| 100 | + <span class="info-label">Subgrupo</span> | ||
| 101 | + <span class="info-value">{organismoData.organismo_sub_grupo}</span> | ||
| 102 | + <span class="info-desc">{organismoData.organismo_desc_sub_grupo}</span> | ||
| 103 | + </div> | ||
| 104 | + | ||
| 105 | + {#if organismoData.gestiones} | ||
| 106 | + <div class="info-card"> | ||
| 107 | + <span class="info-label">Gestiones</span> | ||
| 108 | + <span class="info-value">{organismoData.n_gestiones || ''} años</span> | ||
| 109 | + <span class="info-desc">{organismoData.gestiones}</span> | ||
| 110 | + </div> | ||
| 111 | + {/if} | ||
| 112 | + </div> | ||
| 113 | + </div> | ||
| 114 | + </main> | ||
| 115 | + </div> | ||
| 116 | +</div> | ||
| 117 | + | ||
| 118 | +<style> | ||
| 119 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 120 | + BASE | ||
| 121 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 122 | + .page { | ||
| 123 | + min-height: 100vh; | ||
| 124 | + padding: 1rem; | ||
| 125 | + opacity: 0; | ||
| 126 | + transform: translateY(10px); | ||
| 127 | + transition: opacity 0.4s ease, transform 0.4s ease; | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + .page.mounted { | ||
| 131 | + opacity: 1; | ||
| 132 | + transform: translateY(0); | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 136 | + NAV | ||
| 137 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 138 | + .page-nav { | ||
| 139 | + max-width: 1400px; | ||
| 140 | + margin: 0 auto 1.5rem; | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + .back-link { | ||
| 144 | + display: inline-flex; | ||
| 145 | + align-items: center; | ||
| 146 | + gap: 0.5rem; | ||
| 147 | + color: var(--text-secondary, #888); | ||
| 148 | + text-decoration: none; | ||
| 149 | + font-size: 0.875rem; | ||
| 150 | + transition: color 0.2s; | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + .back-link:hover { | ||
| 154 | + color: var(--text-primary, #fff); | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 158 | + LAYOUT | ||
| 159 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 160 | + .dashboard-layout { | ||
| 161 | + display: grid; | ||
| 162 | + grid-template-columns: 300px 1fr; | ||
| 163 | + gap: 2rem; | ||
| 164 | + max-width: 1400px; | ||
| 165 | + margin: 0 auto; | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + @media (max-width: 900px) { | ||
| 169 | + .dashboard-layout { | ||
| 170 | + grid-template-columns: 1fr; | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + .dashboard-context { | ||
| 174 | + order: 2; | ||
| 175 | + } | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 179 | + SIDEBAR | ||
| 180 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 181 | + .dashboard-context { | ||
| 182 | + position: sticky; | ||
| 183 | + top: 5rem; | ||
| 184 | + height: fit-content; | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + .context-card { | ||
| 188 | + background: var(--card-bg, rgba(255, 255, 255, 0.03)); | ||
| 189 | + border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 190 | + border-radius: 16px; | ||
| 191 | + padding: 1.5rem; | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + .context-main-title { | ||
| 195 | + font-family: 'Instrument Sans', sans-serif; | ||
| 196 | + font-size: 0.75rem; | ||
| 197 | + font-weight: 600; | ||
| 198 | + text-transform: uppercase; | ||
| 199 | + letter-spacing: 0.05em; | ||
| 200 | + color: var(--text-tertiary, #666); | ||
| 201 | + margin-bottom: 1.5rem; | ||
| 202 | + } | ||
| 203 | + | ||
| 204 | + .context-section { | ||
| 205 | + margin-bottom: 1.5rem; | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + .context-subtitle { | ||
| 209 | + font-family: 'Instrument Sans', sans-serif; | ||
| 210 | + font-size: 0.8rem; | ||
| 211 | + font-weight: 500; | ||
| 212 | + color: var(--text-secondary, #aaa); | ||
| 213 | + margin-bottom: 0.75rem; | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + .context-hint { | ||
| 217 | + font-size: 0.75rem; | ||
| 218 | + color: var(--text-tertiary, #666); | ||
| 219 | + margin-bottom: 1rem; | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + .context-separator { | ||
| 223 | + height: 1px; | ||
| 224 | + background: var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 225 | + margin: 1.5rem 0; | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 229 | + UBICACION INFO | ||
| 230 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 231 | + .ubicacion-info { | ||
| 232 | + display: flex; | ||
| 233 | + flex-direction: column; | ||
| 234 | + gap: 0.75rem; | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + .ubicacion-item { | ||
| 238 | + display: flex; | ||
| 239 | + flex-direction: column; | ||
| 240 | + gap: 0.25rem; | ||
| 241 | + padding: 0.75rem; | ||
| 242 | + background: var(--hover-bg, rgba(255, 255, 255, 0.03)); | ||
| 243 | + border-radius: 8px; | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + .ubicacion-label { | ||
| 247 | + font-family: 'DM Mono', monospace; | ||
| 248 | + font-size: 0.7rem; | ||
| 249 | + color: var(--text-tertiary, #666); | ||
| 250 | + text-transform: uppercase; | ||
| 251 | + } | ||
| 252 | + | ||
| 253 | + .ubicacion-value { | ||
| 254 | + font-size: 0.85rem; | ||
| 255 | + color: var(--text-primary, #fff); | ||
| 256 | + } | ||
| 257 | + | ||
| 258 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 259 | + HERMANOS LIST | ||
| 260 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 261 | + .hermanos-list { | ||
| 262 | + display: flex; | ||
| 263 | + flex-direction: column; | ||
| 264 | + gap: 0.25rem; | ||
| 265 | + } | ||
| 266 | + | ||
| 267 | + .hermano-link { | ||
| 268 | + display: flex; | ||
| 269 | + align-items: center; | ||
| 270 | + gap: 0.5rem; | ||
| 271 | + padding: 0.5rem 0.75rem; | ||
| 272 | + border-radius: 6px; | ||
| 273 | + text-decoration: none; | ||
| 274 | + color: var(--text-secondary, #aaa); | ||
| 275 | + font-size: 0.8rem; | ||
| 276 | + transition: all 0.2s ease; | ||
| 277 | + } | ||
| 278 | + | ||
| 279 | + .hermano-link:hover { | ||
| 280 | + background: var(--hover-bg, rgba(255, 255, 255, 0.05)); | ||
| 281 | + color: var(--text-primary, #fff); | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + .hermano-code { | ||
| 285 | + font-family: 'DM Mono', monospace; | ||
| 286 | + font-size: 0.7rem; | ||
| 287 | + opacity: 0.6; | ||
| 288 | + flex-shrink: 0; | ||
| 289 | + min-width: 2.5rem; | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + .hermano-name { | ||
| 293 | + white-space: nowrap; | ||
| 294 | + overflow: hidden; | ||
| 295 | + text-overflow: ellipsis; | ||
| 296 | + } | ||
| 297 | + | ||
| 298 | + .hermanos-more { | ||
| 299 | + font-size: 0.75rem; | ||
| 300 | + color: var(--text-tertiary, #666); | ||
| 301 | + padding: 0.5rem 0.75rem; | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 305 | + MAIN CONTENT | ||
| 306 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 307 | + .dashboard-main { | ||
| 308 | + min-width: 0; | ||
| 309 | + } | ||
| 310 | + | ||
| 311 | + .main-content { | ||
| 312 | + background: var(--card-bg, rgba(255, 255, 255, 0.03)); | ||
| 313 | + border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08)); | ||
| 314 | + border-radius: 16px; | ||
| 315 | + padding: 2rem; | ||
| 316 | + } | ||
| 317 | + | ||
| 318 | + .main-header { | ||
| 319 | + margin-bottom: 2rem; | ||
| 320 | + } | ||
| 321 | + | ||
| 322 | + .main-title-pills { | ||
| 323 | + display: flex; | ||
| 324 | + gap: 0.5rem; | ||
| 325 | + margin-bottom: 0.75rem; | ||
| 326 | + } | ||
| 327 | + | ||
| 328 | + .pill { | ||
| 329 | + padding: 0.25rem 0.75rem; | ||
| 330 | + border-radius: 999px; | ||
| 331 | + font-size: 0.75rem; | ||
| 332 | + font-weight: 500; | ||
| 333 | + } | ||
| 334 | + | ||
| 335 | + .pill-codigo { | ||
| 336 | + background: var(--hover-bg, rgba(255, 255, 255, 0.05)); | ||
| 337 | + color: var(--text-secondary, #aaa); | ||
| 338 | + font-family: 'DM Mono', monospace; | ||
| 339 | + } | ||
| 340 | + | ||
| 341 | + .main-title-name { | ||
| 342 | + font-family: 'DM Serif Display', serif; | ||
| 343 | + font-size: 2rem; | ||
| 344 | + font-weight: 400; | ||
| 345 | + color: var(--text-primary, #fff); | ||
| 346 | + line-height: 1.2; | ||
| 347 | + margin: 0; | ||
| 348 | + } | ||
| 349 | + | ||
| 350 | + .main-sigla { | ||
| 351 | + font-family: 'DM Mono', monospace; | ||
| 352 | + font-size: 0.9rem; | ||
| 353 | + color: var(--text-tertiary, #666); | ||
| 354 | + margin-top: 0.5rem; | ||
| 355 | + } | ||
| 356 | + | ||
| 357 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 358 | + INFO GRID | ||
| 359 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 360 | + .info-grid { | ||
| 361 | + display: grid; | ||
| 362 | + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | ||
| 363 | + gap: 1rem; | ||
| 364 | + } | ||
| 365 | + | ||
| 366 | + .info-card { | ||
| 367 | + display: flex; | ||
| 368 | + flex-direction: column; | ||
| 369 | + gap: 0.25rem; | ||
| 370 | + padding: 1rem; | ||
| 371 | + background: var(--hover-bg, rgba(255, 255, 255, 0.03)); | ||
| 372 | + border-radius: 12px; | ||
| 373 | + border: 1px solid var(--border-color, rgba(255, 255, 255, 0.05)); | ||
| 374 | + } | ||
| 375 | + | ||
| 376 | + .info-label { | ||
| 377 | + font-size: 0.75rem; | ||
| 378 | + text-transform: uppercase; | ||
| 379 | + letter-spacing: 0.05em; | ||
| 380 | + color: var(--text-tertiary, #666); | ||
| 381 | + } | ||
| 382 | + | ||
| 383 | + .info-value { | ||
| 384 | + font-family: 'DM Mono', monospace; | ||
| 385 | + font-size: 1.25rem; | ||
| 386 | + color: var(--text-primary, #fff); | ||
| 387 | + } | ||
| 388 | + | ||
| 389 | + .info-desc { | ||
| 390 | + font-size: 0.8rem; | ||
| 391 | + color: var(--text-secondary, #aaa); | ||
| 392 | + line-height: 1.4; | ||
| 393 | + } | ||
| 394 | +</style> |
| 1 | import { supabase } from '$lib/supabase'; | 1 | import { supabase } from '$lib/supabase'; |
| 2 | import { error } from '@sveltejs/kit'; | 2 | import { error } from '@sveltejs/kit'; |
| 3 | 3 | ||
| 4 | +// Funciones para derivar jerarquía del código rubro | ||
| 5 | +// Jerarquía: Tipo (XX000) → Clase (XXX00) → Cuenta (XXXX0) → Subcuenta (XXXXX) | ||
| 6 | + | ||
| 7 | +function getTipoCode(rubro) { | ||
| 8 | + // Primeros 2 dígitos + 000 → ej: 11000, 12000 | ||
| 9 | + return rubro.substring(0, 2) + '000'; | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +function getClaseCode(rubro) { | ||
| 13 | + // Primeros 3 dígitos + 00 → ej: 11100, 11200 | ||
| 14 | + return rubro.substring(0, 3) + '00'; | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +function getCuentaCode(rubro) { | ||
| 18 | + // Primeros 4 dígitos + 0 → ej: 11110, 11120 | ||
| 19 | + return rubro.substring(0, 4) + '0'; | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +function getNivelFromCode(rubro) { | ||
| 23 | + // Determinar nivel basado en el patrón de ceros | ||
| 24 | + if (rubro.endsWith('000')) return 'tipo'; // XX000 | ||
| 25 | + if (rubro.endsWith('00')) return 'clase'; // XXX00 | ||
| 26 | + if (rubro.endsWith('0')) return 'cuenta'; // XXXX0 | ||
| 27 | + return 'sub_cuenta'; // XXXXX | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +function getNextNivel(nivel) { | ||
| 31 | + const niveles = { tipo: 'clase', clase: 'cuenta', cuenta: 'sub_cuenta' }; | ||
| 32 | + return niveles[nivel] || null; | ||
| 33 | +} | ||
| 34 | + | ||
| 4 | export async function load({ params }) { | 35 | export async function load({ params }) { |
| 5 | const { codigo } = params; | 36 | const { codigo } = params; |
| 6 | 37 | ||
| ... | @@ -15,88 +46,81 @@ export async function load({ params }) { | ... | @@ -15,88 +46,81 @@ export async function load({ params }) { |
| 15 | } | 46 | } |
| 16 | 47 | ||
| 17 | const rubro = data[0]; | 48 | const rubro = data[0]; |
| 49 | + const nivel = rubro.nivel || getNivelFromCode(codigo); | ||
| 18 | 50 | ||
| 19 | // Obtener jerarquía (padres e hijos) | 51 | // Obtener jerarquía (padres e hijos) |
| 20 | let padres = []; | 52 | let padres = []; |
| 21 | let hijos = []; | 53 | let hijos = []; |
| 22 | 54 | ||
| 23 | - // Buscar padre según nivel | 55 | + // Buscar padres según nivel |
| 24 | - if (rubro.nivel === 'sub_cuenta') { | 56 | + if (nivel === 'sub_cuenta') { |
| 25 | - // Padre es cuenta | 57 | + // Padres: tipo, clase, cuenta |
| 26 | - const { data: padreData } = await supabase | 58 | + const tipoCode = getTipoCode(codigo); |
| 59 | + const claseCode = getClaseCode(codigo); | ||
| 60 | + const cuentaCode = getCuentaCode(codigo); | ||
| 61 | + | ||
| 62 | + const { data: padresData } = await supabase | ||
| 27 | .schema('ppto') | 63 | .schema('ppto') |
| 28 | .from('clas_rubros') | 64 | .from('clas_rubros') |
| 29 | .select('*') | 65 | .select('*') |
| 30 | - .eq('nivel', 'cuenta') | 66 | + .in('rubro', [tipoCode, claseCode, cuentaCode]) |
| 31 | - .eq('tipo', rubro.tipo) | 67 | + .order('rubro'); |
| 32 | - .eq('clase', rubro.clase) | 68 | + |
| 33 | - .eq('cuenta', rubro.cuenta); | 69 | + if (padresData) padres = padresData; |
| 34 | - if (padreData?.length) padres.push(padreData[0]); | 70 | + } else if (nivel === 'cuenta') { |
| 35 | - } | 71 | + // Padres: tipo, clase |
| 72 | + const tipoCode = getTipoCode(codigo); | ||
| 73 | + const claseCode = getClaseCode(codigo); | ||
| 36 | 74 | ||
| 37 | - if (rubro.nivel === 'cuenta' || rubro.nivel === 'sub_cuenta') { | 75 | + const { data: padresData } = await supabase |
| 38 | - // Padre es clase | ||
| 39 | - const { data: padreData } = await supabase | ||
| 40 | .schema('ppto') | 76 | .schema('ppto') |
| 41 | .from('clas_rubros') | 77 | .from('clas_rubros') |
| 42 | .select('*') | 78 | .select('*') |
| 43 | - .eq('nivel', 'clase') | 79 | + .in('rubro', [tipoCode, claseCode]) |
| 44 | - .eq('tipo', rubro.tipo) | 80 | + .order('rubro'); |
| 45 | - .eq('clase', rubro.clase); | ||
| 46 | - if (padreData?.length) padres.unshift(padreData[0]); | ||
| 47 | - } | ||
| 48 | 81 | ||
| 49 | - if (rubro.nivel !== 'tipo') { | 82 | + if (padresData) padres = padresData; |
| 50 | - // Padre es tipo | 83 | + } else if (nivel === 'clase') { |
| 51 | - const { data: padreData } = await supabase | 84 | + // Padre: tipo |
| 85 | + const tipoCode = getTipoCode(codigo); | ||
| 86 | + | ||
| 87 | + const { data: padresData } = await supabase | ||
| 52 | .schema('ppto') | 88 | .schema('ppto') |
| 53 | .from('clas_rubros') | 89 | .from('clas_rubros') |
| 54 | .select('*') | 90 | .select('*') |
| 55 | - .eq('nivel', 'tipo') | 91 | + .eq('rubro', tipoCode); |
| 56 | - .eq('tipo', rubro.tipo); | 92 | + |
| 57 | - if (padreData?.length) padres.unshift(padreData[0]); | 93 | + if (padresData) padres = padresData; |
| 58 | } | 94 | } |
| 95 | + // tipo no tiene padres | ||
| 59 | 96 | ||
| 60 | // Buscar hijos según nivel | 97 | // Buscar hijos según nivel |
| 61 | - if (rubro.nivel === 'tipo') { | 98 | + const nextNivel = getNextNivel(nivel); |
| 99 | + if (nextNivel) { | ||
| 62 | const { data: hijosData } = await supabase | 100 | const { data: hijosData } = await supabase |
| 63 | .schema('ppto') | 101 | .schema('ppto') |
| 64 | .from('clas_rubros') | 102 | .from('clas_rubros') |
| 65 | .select('*') | 103 | .select('*') |
| 66 | - .eq('nivel', 'clase') | 104 | + .eq('nivel', nextNivel) |
| 67 | - .eq('tipo', rubro.tipo) | 105 | + .like('rubro', `${codigo.replace(/0+$/, '')}%`) |
| 68 | .order('rubro'); | 106 | .order('rubro'); |
| 69 | - hijos = hijosData?.reduce((acc, item) => { | 107 | + |
| 70 | - if (!acc.find(h => h.rubro === item.rubro)) acc.push(item); | 108 | + if (hijosData) { |
| 71 | - return acc; | 109 | + // Filtrar solo hijos directos (siguiente nivel) |
| 72 | - }, []) || []; | 110 | + hijos = hijosData.filter(h => { |
| 73 | - } else if (rubro.nivel === 'clase') { | 111 | + if (nivel === 'tipo') { |
| 74 | - const { data: hijosData } = await supabase | 112 | + // Hijos de tipo son clase: XXX00 donde XX = tipo |
| 75 | - .schema('ppto') | 113 | + return h.rubro.startsWith(codigo.substring(0, 2)) && h.nivel === 'clase'; |
| 76 | - .from('clas_rubros') | 114 | + } else if (nivel === 'clase') { |
| 77 | - .select('*') | 115 | + // Hijos de clase son cuenta: XXXX0 donde XXX = clase |
| 78 | - .eq('nivel', 'cuenta') | 116 | + return h.rubro.startsWith(codigo.substring(0, 3)) && h.nivel === 'cuenta'; |
| 79 | - .eq('tipo', rubro.tipo) | 117 | + } else if (nivel === 'cuenta') { |
| 80 | - .eq('clase', rubro.clase) | 118 | + // Hijos de cuenta son sub_cuenta: XXXXX donde XXXX = cuenta |
| 81 | - .order('rubro'); | 119 | + return h.rubro.startsWith(codigo.substring(0, 4)) && h.nivel === 'sub_cuenta'; |
| 82 | - hijos = hijosData?.reduce((acc, item) => { | 120 | + } |
| 83 | - if (!acc.find(h => h.rubro === item.rubro)) acc.push(item); | 121 | + return false; |
| 84 | - return acc; | 122 | + }); |
| 85 | - }, []) || []; | 123 | + } |
| 86 | - } else if (rubro.nivel === 'cuenta') { | ||
| 87 | - const { data: hijosData } = await supabase | ||
| 88 | - .schema('ppto') | ||
| 89 | - .from('clas_rubros') | ||
| 90 | - .select('*') | ||
| 91 | - .eq('nivel', 'sub_cuenta') | ||
| 92 | - .eq('tipo', rubro.tipo) | ||
| 93 | - .eq('clase', rubro.clase) | ||
| 94 | - .eq('cuenta', rubro.cuenta) | ||
| 95 | - .order('rubro'); | ||
| 96 | - hijos = hijosData?.reduce((acc, item) => { | ||
| 97 | - if (!acc.find(h => h.rubro === item.rubro)) acc.push(item); | ||
| 98 | - return acc; | ||
| 99 | - }, []) || []; | ||
| 100 | } | 124 | } |
| 101 | 125 | ||
| 102 | return { | 126 | return { | ... | ... |
src/routes/vista-diaria/+page.js
0 → 100644
src/routes/vista-diaria/+page.svelte
0 → 100644
| 1 | +<script> | ||
| 2 | + import { onMount } from 'svelte'; | ||
| 3 | + import { parquetReadObjects, asyncBufferFromUrl } from 'hyparquet'; | ||
| 4 | + import * as Plot from '@observablehq/plot'; | ||
| 5 | + import * as d3 from 'd3'; | ||
| 6 | + | ||
| 7 | + // Estado de datos | ||
| 8 | + let timeserie = $state([]); | ||
| 9 | + let ejecucionEntidades = $state([]); | ||
| 10 | + let ejecucionClasificadores = $state([]); | ||
| 11 | + let timeline = $state([]); | ||
| 12 | + let loading = $state(true); | ||
| 13 | + let error = $state(null); | ||
| 14 | + | ||
| 15 | + // Controles interactivos | ||
| 16 | + let diasSeleccionados = $state('total'); | ||
| 17 | + let incluirTransferencias = $state(false); // Por defecto sin transferencias (como el código R) | ||
| 18 | + let busquedaEntidad = $state(''); | ||
| 19 | + | ||
| 20 | + // Clasificadores disponibles con sus labels (keys deben coincidir con el parquet) | ||
| 21 | + const clasificadores = [ | ||
| 22 | + { key: 'Entidades', label: 'Quién gastó más' }, | ||
| 23 | + { key: 'Objetos', label: 'En qué se gastó más' }, | ||
| 24 | + { key: 'Finalidad y Función', label: 'Para qué función' }, | ||
| 25 | + { key: 'Actividad Económica', label: 'En qué sector económico' }, | ||
| 26 | + { key: 'Ubicación', label: 'En qué municipio se gastó más' } | ||
| 27 | + ]; | ||
| 28 | + | ||
| 29 | + // Estado de hover por clasificador | ||
| 30 | + let hoverData = $state({ | ||
| 31 | + 'Entidades': null, | ||
| 32 | + 'Objetos': null, | ||
| 33 | + 'Finalidad y Función': null, | ||
| 34 | + 'Actividad Económica': null, | ||
| 35 | + 'Ubicación': null | ||
| 36 | + }); | ||
| 37 | + | ||
| 38 | + // Contenedores SVG para cada clasificador | ||
| 39 | + let barContainers = $state({}); | ||
| 40 | + | ||
| 41 | + // Contenedor y estado para entidades | ||
| 42 | + let entidadesBarContainer = $state(null); | ||
| 43 | + let entidadHover = $state(null); | ||
| 44 | + | ||
| 45 | + // Control de visualización de entidades | ||
| 46 | + let entidadesVisibles = $state(10); | ||
| 47 | + let rangoEjecucionMin = $state(0); | ||
| 48 | + let rangoEjecucionMax = $state(100); | ||
| 49 | + | ||
| 50 | + // Drag del rango | ||
| 51 | + let sliderContainer = $state(null); | ||
| 52 | + let isDraggingRange = $state(false); | ||
| 53 | + let dragStartX = $state(0); | ||
| 54 | + let dragStartMin = $state(0); | ||
| 55 | + let dragStartMax = $state(0); | ||
| 56 | + | ||
| 57 | + function handleRangeMouseDown(e) { | ||
| 58 | + isDraggingRange = true; | ||
| 59 | + dragStartX = e.clientX; | ||
| 60 | + dragStartMin = rangoEjecucionMin; | ||
| 61 | + dragStartMax = rangoEjecucionMax; | ||
| 62 | + document.addEventListener('mousemove', handleRangeMouseMove); | ||
| 63 | + document.addEventListener('mouseup', handleRangeMouseUp); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + function handleRangeMouseMove(e) { | ||
| 67 | + if (!isDraggingRange || !sliderContainer) return; | ||
| 68 | + | ||
| 69 | + const rect = sliderContainer.getBoundingClientRect(); | ||
| 70 | + const deltaX = e.clientX - dragStartX; | ||
| 71 | + const deltaPercent = (deltaX / rect.width) * 100; | ||
| 72 | + const rangeSize = dragStartMax - dragStartMin; | ||
| 73 | + | ||
| 74 | + let newMin = dragStartMin + deltaPercent; | ||
| 75 | + let newMax = dragStartMax + deltaPercent; | ||
| 76 | + | ||
| 77 | + // Limitar al rango 0-100 | ||
| 78 | + if (newMin < 0) { | ||
| 79 | + newMin = 0; | ||
| 80 | + newMax = rangeSize; | ||
| 81 | + } | ||
| 82 | + if (newMax > 100) { | ||
| 83 | + newMax = 100; | ||
| 84 | + newMin = 100 - rangeSize; | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + rangoEjecucionMin = Math.round(newMin); | ||
| 88 | + rangoEjecucionMax = Math.round(newMax); | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + function handleRangeMouseUp() { | ||
| 92 | + isDraggingRange = false; | ||
| 93 | + document.removeEventListener('mousemove', handleRangeMouseMove); | ||
| 94 | + document.removeEventListener('mouseup', handleRangeMouseUp); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + // Búsqueda para timeline cards | ||
| 98 | + let busquedaTimeline = $state(''); | ||
| 99 | + | ||
| 100 | + // Labels dinámicos para periodos de tiempo | ||
| 101 | + function formatearFechaCorta(fecha) { | ||
| 102 | + return fecha.toLocaleDateString('es-BO', { day: 'numeric', month: 'short' }).replace('.', ''); | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + let labelsPeriodo = $derived.by(() => { | ||
| 106 | + const hoy = new Date(); | ||
| 107 | + const ayer = new Date(hoy); | ||
| 108 | + ayer.setDate(ayer.getDate() - 1); | ||
| 109 | + | ||
| 110 | + const hace3 = new Date(hoy); | ||
| 111 | + hace3.setDate(hace3.getDate() - 3); | ||
| 112 | + | ||
| 113 | + const hace7 = new Date(hoy); | ||
| 114 | + hace7.setDate(hace7.getDate() - 7); | ||
| 115 | + | ||
| 116 | + return [ | ||
| 117 | + { value: '1', label: `Hoy, ${formatearFechaCorta(hoy)}` }, | ||
| 118 | + { value: '3', label: `Últimos 3 días` }, | ||
| 119 | + { value: '7', label: `Última semana` }, | ||
| 120 | + { value: 'total', label: `En ${hoy.getFullYear()}` } | ||
| 121 | + ]; | ||
| 122 | + }); | ||
| 123 | + | ||
| 124 | + // Navegación interna entre secciones | ||
| 125 | + let seccionActiva = $state(0); | ||
| 126 | + const secciones = [ | ||
| 127 | + { id: 'clasificadores', titulo: '¿En qué se gasta?' }, | ||
| 128 | + { id: 'entidades', titulo: '¿Quién ejecuta más?' }, | ||
| 129 | + { id: 'programas', titulo: '¿En qué programas?' }, | ||
| 130 | + { id: 'evolucion', titulo: '¿Cómo evoluciona?' } | ||
| 131 | + ]; | ||
| 132 | + | ||
| 133 | + function irASeccion(index) { | ||
| 134 | + seccionActiva = index; | ||
| 135 | + const elemento = document.getElementById(secciones[index].id); | ||
| 136 | + if (elemento && vistaContainer) { | ||
| 137 | + elemento.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||
| 138 | + } | ||
| 139 | + // Actualizar URL hash | ||
| 140 | + history.replaceState(null, '', `#${secciones[index].id}`); | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + // Contenedor para scroll-snap | ||
| 144 | + let vistaContainer = $state(null); | ||
| 145 | + | ||
| 146 | + // Detectar sección activa al hacer scroll | ||
| 147 | + function detectarSeccion() { | ||
| 148 | + if (!vistaContainer) return; | ||
| 149 | + const scrollY = vistaContainer.scrollTop; | ||
| 150 | + const vh = window.innerHeight; | ||
| 151 | + | ||
| 152 | + let nuevaSeccion = 0; | ||
| 153 | + if (scrollY < vh * 0.5) { | ||
| 154 | + nuevaSeccion = 0; | ||
| 155 | + } else if (scrollY < vh * 1.5) { | ||
| 156 | + nuevaSeccion = 1; | ||
| 157 | + } else if (scrollY < vh * 2.5) { | ||
| 158 | + nuevaSeccion = 2; | ||
| 159 | + } else { | ||
| 160 | + nuevaSeccion = 3; | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + // Solo actualizar URL si cambió la sección | ||
| 164 | + if (nuevaSeccion !== seccionActiva) { | ||
| 165 | + seccionActiva = nuevaSeccion; | ||
| 166 | + history.replaceState(null, '', `#${secciones[nuevaSeccion].id}`); | ||
| 167 | + } | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | + // Ir a sección según hash en URL al cargar | ||
| 171 | + function irASeccionDesdeHash() { | ||
| 172 | + const hash = window.location.hash.slice(1); | ||
| 173 | + if (hash) { | ||
| 174 | + const index = secciones.findIndex(s => s.id === hash); | ||
| 175 | + if (index !== -1) { | ||
| 176 | + setTimeout(() => { | ||
| 177 | + const elemento = document.getElementById(hash); | ||
| 178 | + if (elemento) { | ||
| 179 | + elemento.scrollIntoView({ behavior: 'auto', block: 'start' }); | ||
| 180 | + seccionActiva = index; | ||
| 181 | + } | ||
| 182 | + }, 100); | ||
| 183 | + } | ||
| 184 | + } | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + // Cards expandidas (por id de entidad) | ||
| 188 | + let cardsExpandidas = $state({}); | ||
| 189 | + | ||
| 190 | + // Elementos DOM para los gráficos | ||
| 191 | + let chartDiariaContainer = $state(null); | ||
| 192 | + let chartAcumuladaContainer = $state(null); | ||
| 193 | + | ||
| 194 | + // Colores del tema | ||
| 195 | + const colores = { | ||
| 196 | + base: '#363534', | ||
| 197 | + claro: '#F3EAD8', | ||
| 198 | + rojo: '#DB281C', | ||
| 199 | + amarillo: '#F4E410', | ||
| 200 | + verde: '#007C38', | ||
| 201 | + acento: '#C9A751' | ||
| 202 | + }; | ||
| 203 | + | ||
| 204 | + // Función para obtener variables CSS | ||
| 205 | + function getCSSVar(name) { | ||
| 206 | + return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + // Colores de gráficos (se actualizan según el tema) | ||
| 210 | + function getChartColors() { | ||
| 211 | + return { | ||
| 212 | + barDefault: getCSSVar('--chart-bar-default') || '#e0e0e0', | ||
| 213 | + barHighlight: getCSSVar('--chart-bar-highlight') || '#F3EAD8', | ||
| 214 | + barStroke: getCSSVar('--chart-bar-stroke') || '#b0b0b0', | ||
| 215 | + barStrokeDefault: getCSSVar('--chart-bar-stroke-default') || 'white', | ||
| 216 | + trackBg: getCSSVar('--chart-track-bg') || '#f5f5f5', | ||
| 217 | + grid: getCSSVar('--chart-grid') || '#bbb', | ||
| 218 | + label: getCSSVar('--chart-label') || '#888' | ||
| 219 | + }; | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + // Cargar archivo Parquet usando asyncBufferFromUrl | ||
| 223 | + async function cargarParquet(url) { | ||
| 224 | + const file = await asyncBufferFromUrl({ url }); | ||
| 225 | + const data = await parquetReadObjects({ file }); | ||
| 226 | + return data; | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + onMount(async () => { | ||
| 230 | + // Observer para detectar cambios de tema y re-renderizar gráficos | ||
| 231 | + const themeObserver = new MutationObserver((mutations) => { | ||
| 232 | + mutations.forEach((mutation) => { | ||
| 233 | + if (mutation.attributeName === 'class') { | ||
| 234 | + // Re-renderizar gráficos cuando cambia el tema | ||
| 235 | + if (!loading) { | ||
| 236 | + setTimeout(() => { | ||
| 237 | + clasificadores.forEach(({ key }) => { | ||
| 238 | + if (barContainers[key]) { | ||
| 239 | + renderizarBarras(key); | ||
| 240 | + } | ||
| 241 | + }); | ||
| 242 | + if (entidadesBarContainer) { | ||
| 243 | + renderizarEntidades(); | ||
| 244 | + } | ||
| 245 | + }, 50); | ||
| 246 | + } | ||
| 247 | + } | ||
| 248 | + }); | ||
| 249 | + }); | ||
| 250 | + | ||
| 251 | + themeObserver.observe(document.documentElement, { attributes: true }); | ||
| 252 | + | ||
| 253 | + try { | ||
| 254 | + // Cargar todos los parquets en paralelo | ||
| 255 | + const [tsData, entData, clasData, tlData] = await Promise.all([ | ||
| 256 | + cargarParquet('/timeserie@4.parquet'), | ||
| 257 | + cargarParquet('/ejecucion_entidades@1.parquet'), | ||
| 258 | + cargarParquet('/ejecucion_clasificadores@3.parquet'), | ||
| 259 | + cargarParquet('/timeline@1.parquet') | ||
| 260 | + ]); | ||
| 261 | + | ||
| 262 | + // Los datos ya vienen como objetos | ||
| 263 | + timeserie = tsData; | ||
| 264 | + ejecucionEntidades = entData; | ||
| 265 | + ejecucionClasificadores = clasData; | ||
| 266 | + timeline = tlData; | ||
| 267 | + | ||
| 268 | + loading = false; | ||
| 269 | + | ||
| 270 | + // Ir a sección según hash en URL | ||
| 271 | + irASeccionDesdeHash(); | ||
| 272 | + } catch (e) { | ||
| 273 | + error = e.message; | ||
| 274 | + loading = false; | ||
| 275 | + console.error('Error cargando datos:', e); | ||
| 276 | + } | ||
| 277 | + | ||
| 278 | + return () => { | ||
| 279 | + themeObserver.disconnect(); | ||
| 280 | + }; | ||
| 281 | + }); | ||
| 282 | + | ||
| 283 | + // Filtrar datos por transferencias y días | ||
| 284 | + let timeserieFiltered = $derived( | ||
| 285 | + timeserie.filter(d => d.transferencias === incluirTransferencias) | ||
| 286 | + ); | ||
| 287 | + | ||
| 288 | + // Datos por clasificador para las barras apiladas (sin agrupar, cada hijo es un segmento) | ||
| 289 | + let datosPorClasificador = $derived.by(() => { | ||
| 290 | + const dias = diasSeleccionados === 'total' ? 'total' : diasSeleccionados; | ||
| 291 | + const result = {}; | ||
| 292 | + | ||
| 293 | + clasificadores.forEach(({ key }) => { | ||
| 294 | + const filtered = ejecucionClasificadores.filter(d => | ||
| 295 | + d.transferencias === incluirTransferencias && | ||
| 296 | + String(d.dias) === String(dias) && | ||
| 297 | + d.clasificador === key | ||
| 298 | + ); | ||
| 299 | + | ||
| 300 | + // Ordenar por devengado descendente (el más grande primero) | ||
| 301 | + result[key] = filtered.sort((a, b) => b.devengado - a.devengado); | ||
| 302 | + }); | ||
| 303 | + | ||
| 304 | + return result; | ||
| 305 | + }); | ||
| 306 | + | ||
| 307 | + // Total por clasificador | ||
| 308 | + let totalesPorClasificador = $derived.by(() => { | ||
| 309 | + const result = {}; | ||
| 310 | + clasificadores.forEach(({ key }) => { | ||
| 311 | + const datos = datosPorClasificador[key] || []; | ||
| 312 | + result[key] = datos.reduce((sum, d) => sum + d.devengado, 0); | ||
| 313 | + }); | ||
| 314 | + return result; | ||
| 315 | + }); | ||
| 316 | + | ||
| 317 | + // Item mostrado (hover o primero) | ||
| 318 | + function getDisplayItem(key) { | ||
| 319 | + const hover = hoverData[key]; | ||
| 320 | + if (hover) return hover; | ||
| 321 | + const datos = datosPorClasificador[key] || []; | ||
| 322 | + return datos[0] || null; | ||
| 323 | + } | ||
| 324 | + | ||
| 325 | + // Renderizar barras D3 para un clasificador | ||
| 326 | + function renderizarBarras(key) { | ||
| 327 | + const container = barContainers[key]; | ||
| 328 | + if (!container) return; | ||
| 329 | + | ||
| 330 | + const datos = datosPorClasificador[key] || []; | ||
| 331 | + if (datos.length === 0) return; | ||
| 332 | + | ||
| 333 | + const total = totalesPorClasificador[key] || 1; | ||
| 334 | + const height = 52; | ||
| 335 | + const radius = 4; | ||
| 336 | + | ||
| 337 | + // Limpiar | ||
| 338 | + d3.select(container).selectAll('*').remove(); | ||
| 339 | + | ||
| 340 | + // Obtener ancho real del contenedor | ||
| 341 | + const containerRect = container.getBoundingClientRect(); | ||
| 342 | + const width = containerRect.width || container.clientWidth || 800; | ||
| 343 | + | ||
| 344 | + const svg = d3.select(container) | ||
| 345 | + .append('svg') | ||
| 346 | + .attr('width', '100%') | ||
| 347 | + .attr('height', height) | ||
| 348 | + .attr('viewBox', `0 0 ${width} ${height}`) | ||
| 349 | + .attr('preserveAspectRatio', 'none') | ||
| 350 | + .style('display', 'block'); | ||
| 351 | + | ||
| 352 | + // Calcular posiciones acumuladas | ||
| 353 | + let cumulative = 0; | ||
| 354 | + const segments = datos.map(d => { | ||
| 355 | + const segWidth = (d.devengado / total) * width; | ||
| 356 | + const segment = { | ||
| 357 | + ...d, | ||
| 358 | + x: cumulative, | ||
| 359 | + width: segWidth | ||
| 360 | + }; | ||
| 361 | + cumulative += segWidth; | ||
| 362 | + return segment; | ||
| 363 | + }); | ||
| 364 | + | ||
| 365 | + // Colores dinámicos según el tema | ||
| 366 | + const chartColors = getChartColors(); | ||
| 367 | + const colorDefault = chartColors.barDefault; | ||
| 368 | + const colorHover = chartColors.barHighlight; | ||
| 369 | + const colorFirst = chartColors.barHighlight; | ||
| 370 | + | ||
| 371 | + // Crear grupo para cada segmento | ||
| 372 | + const bars = svg.selectAll('rect') | ||
| 373 | + .data(segments) | ||
| 374 | + .enter() | ||
| 375 | + .append('rect') | ||
| 376 | + .attr('x', d => d.x) | ||
| 377 | + .attr('y', 2) | ||
| 378 | + .attr('width', d => Math.max(d.width - 1, 1)) | ||
| 379 | + .attr('height', height - 4) | ||
| 380 | + .attr('rx', (d, i) => { | ||
| 381 | + // Bordes redondeados solo en extremos | ||
| 382 | + if (i === 0) return radius; | ||
| 383 | + if (i === segments.length - 1) return radius; | ||
| 384 | + return 0; | ||
| 385 | + }) | ||
| 386 | + .attr('fill', (d, i) => i === 0 ? colorFirst : colorDefault) | ||
| 387 | + .attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : chartColors.barStrokeDefault) | ||
| 388 | + .attr('stroke-width', (d, i) => i === 0 ? 1 : 0.5) | ||
| 389 | + .style('cursor', 'pointer') | ||
| 390 | + .on('mouseenter', function(event, d) { | ||
| 391 | + // Resetear todos los segmentos primero | ||
| 392 | + bars | ||
| 393 | + .attr('fill', colorDefault) | ||
| 394 | + .attr('stroke', chartColors.barStrokeDefault) | ||
| 395 | + .attr('stroke-width', 0.5); | ||
| 396 | + // Highlight el actual | ||
| 397 | + d3.select(this) | ||
| 398 | + .attr('fill', colorHover) | ||
| 399 | + .attr('stroke', chartColors.barStroke) | ||
| 400 | + .attr('stroke-width', 1); | ||
| 401 | + // Actualizar estado | ||
| 402 | + hoverData[key] = d; | ||
| 403 | + hoverData = { ...hoverData }; | ||
| 404 | + }) | ||
| 405 | + .on('mouseleave', function(event, d) { | ||
| 406 | + // Restaurar el primero como destacado | ||
| 407 | + bars | ||
| 408 | + .attr('fill', (d, i) => i === 0 ? colorFirst : colorDefault) | ||
| 409 | + .attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : chartColors.barStrokeDefault) | ||
| 410 | + .attr('stroke-width', (d, i) => i === 0 ? 1 : 0.5); | ||
| 411 | + // Limpiar hover | ||
| 412 | + hoverData[key] = null; | ||
| 413 | + hoverData = { ...hoverData }; | ||
| 414 | + }); | ||
| 415 | + } | ||
| 416 | + | ||
| 417 | + // Renderizar todas las barras cuando cambian los datos o controles | ||
| 418 | + $effect(() => { | ||
| 419 | + // Dependencias explícitas | ||
| 420 | + const datos = datosPorClasificador; | ||
| 421 | + const trans = incluirTransferencias; | ||
| 422 | + const dias = diasSeleccionados; | ||
| 423 | + | ||
| 424 | + if (!loading) { | ||
| 425 | + setTimeout(() => { | ||
| 426 | + clasificadores.forEach(({ key }) => { | ||
| 427 | + if (barContainers[key]) { | ||
| 428 | + renderizarBarras(key); | ||
| 429 | + } | ||
| 430 | + }); | ||
| 431 | + }, 100); | ||
| 432 | + } | ||
| 433 | + }); | ||
| 434 | + | ||
| 435 | + // Renderizar barras de entidades | ||
| 436 | + $effect(() => { | ||
| 437 | + const datos = entidadesFiltered; | ||
| 438 | + const trans = incluirTransferencias; | ||
| 439 | + const dias = diasSeleccionados; | ||
| 440 | + | ||
| 441 | + if (!loading && entidadesBarContainer) { | ||
| 442 | + setTimeout(() => { | ||
| 443 | + renderizarEntidades(); | ||
| 444 | + }, 150); | ||
| 445 | + } | ||
| 446 | + }); | ||
| 447 | + | ||
| 448 | + // Todas las entidades filtradas (sin límite) | ||
| 449 | + let entidadesTodas = $derived.by(() => { | ||
| 450 | + const dias = diasSeleccionados === 'total' ? 'total' : diasSeleccionados; | ||
| 451 | + let filtered = ejecucionEntidades.filter(d => | ||
| 452 | + d.transferencias === incluirTransferencias && | ||
| 453 | + String(d.dias) === String(dias) && | ||
| 454 | + d.ejecucion >= rangoEjecucionMin && | ||
| 455 | + d.ejecucion <= rangoEjecucionMax | ||
| 456 | + ); | ||
| 457 | + | ||
| 458 | + if (busquedaEntidad) { | ||
| 459 | + const q = busquedaEntidad.toLowerCase(); | ||
| 460 | + filtered = filtered.filter(d => | ||
| 461 | + d.entidad_desc_entidad?.toLowerCase().includes(q) || | ||
| 462 | + d.entidad_desc_area?.toLowerCase().includes(q) | ||
| 463 | + ); | ||
| 464 | + } | ||
| 465 | + | ||
| 466 | + // Ordenar por ejecución descendente | ||
| 467 | + return filtered.sort((a, b) => b.ejecucion - a.ejecucion); | ||
| 468 | + }); | ||
| 469 | + | ||
| 470 | + // Entidades visibles (con límite) | ||
| 471 | + let entidadesFiltered = $derived(entidadesTodas.slice(0, entidadesVisibles)); | ||
| 472 | + | ||
| 473 | + // Entidad mostrada (hover o primera) | ||
| 474 | + let displayEntidad = $derived(entidadHover || entidadesFiltered[0] || null); | ||
| 475 | + | ||
| 476 | + // Agrupar timeline por entidad con sus programas | ||
| 477 | + let timelineByEntity = $derived.by(() => { | ||
| 478 | + if (timeline.length === 0) return []; | ||
| 479 | + | ||
| 480 | + // Agrupar por entidad | ||
| 481 | + const grouped = {}; | ||
| 482 | + timeline.forEach(item => { | ||
| 483 | + const key = item.entidad; | ||
| 484 | + if (!grouped[key]) { | ||
| 485 | + grouped[key] = { | ||
| 486 | + entidad: item.entidad, | ||
| 487 | + entidad_desc_area: item.entidad_desc_area, | ||
| 488 | + entidad_desc_entidad: item.entidad_desc_entidad, | ||
| 489 | + total: 0, | ||
| 490 | + programas: [] | ||
| 491 | + }; | ||
| 492 | + } | ||
| 493 | + grouped[key].total += item.devengado; | ||
| 494 | + grouped[key].programas.push({ | ||
| 495 | + programa: item.desc_programa, | ||
| 496 | + actividad: item.desc_actividad_proyecto, | ||
| 497 | + devengado: item.devengado | ||
| 498 | + }); | ||
| 499 | + }); | ||
| 500 | + | ||
| 501 | + // Convertir a array y ordenar por total | ||
| 502 | + let result = Object.values(grouped) | ||
| 503 | + .map(g => ({ | ||
| 504 | + ...g, | ||
| 505 | + programas: g.programas.sort((a, b) => b.devengado - a.devengado) | ||
| 506 | + })) | ||
| 507 | + .sort((a, b) => b.total - a.total); | ||
| 508 | + | ||
| 509 | + // Filtrar por búsqueda si hay texto | ||
| 510 | + if (busquedaTimeline.trim()) { | ||
| 511 | + const q = busquedaTimeline.toLowerCase(); | ||
| 512 | + result = result.filter(e => | ||
| 513 | + e.entidad_desc_entidad?.toLowerCase().includes(q) || | ||
| 514 | + e.entidad_desc_area?.toLowerCase().includes(q) | ||
| 515 | + ); | ||
| 516 | + } | ||
| 517 | + | ||
| 518 | + return result.slice(0, 10); | ||
| 519 | + }); | ||
| 520 | + | ||
| 521 | + // Renderizar barras de entidades con D3 | ||
| 522 | + function renderizarEntidades() { | ||
| 523 | + const container = entidadesBarContainer; | ||
| 524 | + if (!container) return; | ||
| 525 | + | ||
| 526 | + const datos = entidadesFiltered; | ||
| 527 | + if (datos.length === 0) return; | ||
| 528 | + | ||
| 529 | + const barHeight = 32; | ||
| 530 | + const gap = 2; | ||
| 531 | + const headerHeight = 24; | ||
| 532 | + const barsHeight = datos.length * (barHeight + gap); | ||
| 533 | + const height = headerHeight + barsHeight; | ||
| 534 | + | ||
| 535 | + // Limpiar | ||
| 536 | + d3.select(container).selectAll('*').remove(); | ||
| 537 | + | ||
| 538 | + // Obtener ancho real del contenedor | ||
| 539 | + const containerRect = container.getBoundingClientRect(); | ||
| 540 | + const width = containerRect.width || container.clientWidth || 800; | ||
| 541 | + | ||
| 542 | + const svg = d3.select(container) | ||
| 543 | + .append('svg') | ||
| 544 | + .attr('width', '100%') | ||
| 545 | + .attr('height', height) | ||
| 546 | + .attr('viewBox', `0 0 ${width} ${height}`) | ||
| 547 | + .attr('preserveAspectRatio', 'xMinYMin meet') | ||
| 548 | + .style('display', 'block'); | ||
| 549 | + | ||
| 550 | + // Calcular rango dinámico basado en los datos | ||
| 551 | + const ejecuciones = datos.map(d => d.ejecucion); | ||
| 552 | + const minEjec = Math.min(...ejecuciones); | ||
| 553 | + const maxEjec = Math.max(...ejecuciones); | ||
| 554 | + | ||
| 555 | + // Redondear el mínimo hacia abajo y máximo hacia arriba (múltiplos de 5) | ||
| 556 | + const domainMin = Math.max(0, Math.floor(minEjec / 5) * 5 - 5); | ||
| 557 | + const domainMax = Math.min(100, Math.ceil(maxEjec / 5) * 5 + 5); | ||
| 558 | + | ||
| 559 | + // Escala dinámica | ||
| 560 | + const xScale = d3.scaleLinear() | ||
| 561 | + .domain([domainMin, domainMax]) | ||
| 562 | + .range([0, width]); | ||
| 563 | + | ||
| 564 | + // Generar porcentajes para el rango dinámico (cada 10% dentro del rango) | ||
| 565 | + const step = Math.max(5, Math.round((domainMax - domainMin) / 8 / 5) * 5); | ||
| 566 | + const porcentajes = []; | ||
| 567 | + for (let p = domainMin; p <= domainMax; p += step) { | ||
| 568 | + porcentajes.push(p); | ||
| 569 | + } | ||
| 570 | + if (porcentajes[porcentajes.length - 1] !== domainMax) { | ||
| 571 | + porcentajes.push(domainMax); | ||
| 572 | + } | ||
| 573 | + | ||
| 574 | + // Colores dinámicos según el tema | ||
| 575 | + const chartColors = getChartColors(); | ||
| 576 | + const colorDefault = chartColors.barDefault; | ||
| 577 | + const colorHighlight = chartColors.barHighlight; | ||
| 578 | + | ||
| 579 | + // Labels arriba (se dibujan primero) | ||
| 580 | + svg.selectAll('text.pct-label') | ||
| 581 | + .data(porcentajes) | ||
| 582 | + .enter() | ||
| 583 | + .append('text') | ||
| 584 | + .attr('class', 'pct-label') | ||
| 585 | + .attr('x', d => xScale(d)) | ||
| 586 | + .attr('y', 14) | ||
| 587 | + .attr('text-anchor', (d, i) => i === 0 ? 'start' : i === porcentajes.length - 1 ? 'end' : 'middle') | ||
| 588 | + .attr('font-size', '11px') | ||
| 589 | + .attr('font-family', 'Instrument Sans, sans-serif') | ||
| 590 | + .attr('fill', chartColors.label) | ||
| 591 | + .text(d => `${d}%`); | ||
| 592 | + | ||
| 593 | + // Grupo para las barras (offset por el header) | ||
| 594 | + const barsGroup = svg.append('g') | ||
| 595 | + .attr('transform', `translate(0, ${headerHeight})`); | ||
| 596 | + | ||
| 597 | + // Crear barras | ||
| 598 | + const bars = barsGroup.selectAll('g.entidad-bar') | ||
| 599 | + .data(datos) | ||
| 600 | + .enter() | ||
| 601 | + .append('g') | ||
| 602 | + .attr('class', 'entidad-bar') | ||
| 603 | + .attr('transform', (d, i) => `translate(0, ${i * (barHeight + gap)})`); | ||
| 604 | + | ||
| 605 | + // Barra de fondo (100%) | ||
| 606 | + bars.append('rect') | ||
| 607 | + .attr('x', 0) | ||
| 608 | + .attr('y', 0) | ||
| 609 | + .attr('width', width) | ||
| 610 | + .attr('height', barHeight) | ||
| 611 | + .attr('fill', chartColors.trackBg) | ||
| 612 | + .attr('rx', 2); | ||
| 613 | + | ||
| 614 | + // Barra de ejecución (ancho proporcional al rango dinámico) | ||
| 615 | + bars.append('rect') | ||
| 616 | + .attr('class', 'ejecucion-bar') | ||
| 617 | + .attr('x', 0) | ||
| 618 | + .attr('y', 0) | ||
| 619 | + .attr('width', d => { | ||
| 620 | + const clamped = Math.max(domainMin, Math.min(d.ejecucion, domainMax)); | ||
| 621 | + return xScale(clamped); | ||
| 622 | + }) | ||
| 623 | + .attr('height', barHeight) | ||
| 624 | + .attr('fill', (d, i) => i === 0 ? colorHighlight : colorDefault) | ||
| 625 | + .attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : 'none') | ||
| 626 | + .attr('stroke-width', (d, i) => i === 0 ? 1 : 0) | ||
| 627 | + .attr('rx', 2) | ||
| 628 | + .style('cursor', 'pointer'); | ||
| 629 | + | ||
| 630 | + // Área invisible para hover (cubre toda la barra) | ||
| 631 | + bars.append('rect') | ||
| 632 | + .attr('x', 0) | ||
| 633 | + .attr('y', 0) | ||
| 634 | + .attr('width', width) | ||
| 635 | + .attr('height', barHeight) | ||
| 636 | + .attr('fill', 'transparent') | ||
| 637 | + .style('cursor', 'pointer') | ||
| 638 | + .on('mouseenter', function(event, d) { | ||
| 639 | + // Quitar highlight de todas | ||
| 640 | + barsGroup.selectAll('.ejecucion-bar') | ||
| 641 | + .attr('fill', colorDefault) | ||
| 642 | + .attr('stroke', 'none'); | ||
| 643 | + // Highlight la seleccionada con ring | ||
| 644 | + d3.select(this.parentNode).select('.ejecucion-bar') | ||
| 645 | + .attr('fill', colorHighlight) | ||
| 646 | + .attr('stroke', chartColors.barStroke) | ||
| 647 | + .attr('stroke-width', 1); | ||
| 648 | + // Actualizar estado | ||
| 649 | + entidadHover = d; | ||
| 650 | + }) | ||
| 651 | + .on('mouseleave', function() { | ||
| 652 | + // Restaurar primera como destacada con ring | ||
| 653 | + barsGroup.selectAll('.ejecucion-bar') | ||
| 654 | + .attr('fill', (d, i) => i === 0 ? colorHighlight : colorDefault) | ||
| 655 | + .attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : 'none') | ||
| 656 | + .attr('stroke-width', (d, i) => i === 0 ? 1 : 0); | ||
| 657 | + entidadHover = null; | ||
| 658 | + }); | ||
| 659 | + | ||
| 660 | + // Grid lines verticales punteadas finísimas (dibujadas al final para que estén encima) | ||
| 661 | + svg.selectAll('line.grid-line') | ||
| 662 | + .data(porcentajes.slice(1, -1)) // excluir 0 y 100 | ||
| 663 | + .enter() | ||
| 664 | + .append('line') | ||
| 665 | + .attr('class', 'grid-line') | ||
| 666 | + .attr('x1', d => xScale(d)) | ||
| 667 | + .attr('x2', d => xScale(d)) | ||
| 668 | + .attr('y1', headerHeight) | ||
| 669 | + .attr('y2', height) | ||
| 670 | + .attr('stroke', chartColors.grid) | ||
| 671 | + .attr('stroke-width', 0.5) | ||
| 672 | + .attr('stroke-dasharray', '1,4') | ||
| 673 | + .attr('stroke-linecap', 'round') | ||
| 674 | + .attr('opacity', 0.7) | ||
| 675 | + .style('pointer-events', 'none'); // para que no interfiera con hover | ||
| 676 | + } | ||
| 677 | + | ||
| 678 | + // Formatear números en texto literal | ||
| 679 | + function formatearMonto(valor) { | ||
| 680 | + if (valor >= 1e9) { | ||
| 681 | + const num = (valor / 1e9).toFixed(1).replace('.0', ''); | ||
| 682 | + return `Bs ${num} mil millones`; | ||
| 683 | + } | ||
| 684 | + if (valor >= 1e6) { | ||
| 685 | + const num = (valor / 1e6).toFixed(1).replace('.0', ''); | ||
| 686 | + return `Bs ${num} millones`; | ||
| 687 | + } | ||
| 688 | + if (valor >= 1e3) { | ||
| 689 | + const num = (valor / 1e3).toFixed(1).replace('.0', ''); | ||
| 690 | + return `Bs ${num} mil`; | ||
| 691 | + } | ||
| 692 | + return `Bs ${valor.toFixed(0)}`; | ||
| 693 | + } | ||
| 694 | + | ||
| 695 | + // Formatear número completo con separadores de miles | ||
| 696 | + function formatearNumeroCompleto(valor) { | ||
| 697 | + return new Intl.NumberFormat('es-BO', { | ||
| 698 | + maximumFractionDigits: 0 | ||
| 699 | + }).format(valor); | ||
| 700 | + } | ||
| 701 | + | ||
| 702 | + function formatearPorcentaje(valor) { | ||
| 703 | + return `${valor.toFixed(1)}%`; | ||
| 704 | + } | ||
| 705 | + | ||
| 706 | + // Renderizar sparklines minimalistas | ||
| 707 | + function renderizarTimeseries() { | ||
| 708 | + if (timeserieFiltered.length === 0) return; | ||
| 709 | + | ||
| 710 | + const data = timeserieFiltered | ||
| 711 | + .map(d => ({ | ||
| 712 | + ...d, | ||
| 713 | + fecha: new Date(d.fecha), | ||
| 714 | + diferencia: d.diferencia || 0 | ||
| 715 | + })) | ||
| 716 | + .filter(d => !isNaN(d.fecha.getTime())) | ||
| 717 | + .sort((a, b) => a.fecha - b.fecha); | ||
| 718 | + | ||
| 719 | + // Colores dinámicos para el gráfico según tema | ||
| 720 | + const isDarkTheme = document.documentElement.classList.contains('dark'); | ||
| 721 | + const colorTexto = isDarkTheme ? '#B8B5AD' : '#666666'; | ||
| 722 | + const colorGrid = isDarkTheme ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; | ||
| 723 | + const colorRule = isDarkTheme ? 'rgba(255,255,255,0.2)' : '#ccc'; | ||
| 724 | + | ||
| 725 | + const chartConfig = { | ||
| 726 | + height: 200, | ||
| 727 | + marginTop: 20, | ||
| 728 | + marginBottom: 30, | ||
| 729 | + marginLeft: 50, | ||
| 730 | + marginRight: 20, | ||
| 731 | + style: { | ||
| 732 | + background: 'transparent', | ||
| 733 | + color: colorTexto | ||
| 734 | + }, | ||
| 735 | + x: { | ||
| 736 | + label: null, | ||
| 737 | + tickFormat: d => d.toLocaleDateString('es-BO', { day: 'numeric', month: 'short' }), | ||
| 738 | + ticks: 5, | ||
| 739 | + tickSize: 0, | ||
| 740 | + line: false | ||
| 741 | + }, | ||
| 742 | + y: { | ||
| 743 | + label: null, | ||
| 744 | + tickFormat: d => { | ||
| 745 | + if (d >= 1e9) return `${(d / 1e9).toFixed(0)}MM`; | ||
| 746 | + if (d >= 1e6) return `${(d / 1e6).toFixed(0)}M`; | ||
| 747 | + return d; | ||
| 748 | + }, | ||
| 749 | + grid: true, | ||
| 750 | + gridColor: colorGrid, | ||
| 751 | + tickSize: 0, | ||
| 752 | + line: false | ||
| 753 | + } | ||
| 754 | + }; | ||
| 755 | + | ||
| 756 | + // Calcular fecha de corte según el periodo seleccionado | ||
| 757 | + // Usar la última fecha de los datos como referencia, no la fecha de hoy | ||
| 758 | + const ultimaFecha = data.length > 0 ? data[data.length - 1].fecha : new Date(); | ||
| 759 | + let fechaCorte = null; | ||
| 760 | + if (diasSeleccionados !== 'total') { | ||
| 761 | + fechaCorte = new Date(ultimaFecha); | ||
| 762 | + fechaCorte.setDate(fechaCorte.getDate() - parseInt(diasSeleccionados)); | ||
| 763 | + } | ||
| 764 | + | ||
| 765 | + // Separar datos en "activos" (periodo seleccionado) y "fuera de foco" | ||
| 766 | + const dataActiva = fechaCorte | ||
| 767 | + ? data.filter(d => d.fecha >= fechaCorte) | ||
| 768 | + : data; | ||
| 769 | + const dataFuera = fechaCorte | ||
| 770 | + ? data.filter(d => d.fecha < fechaCorte) | ||
| 771 | + : []; | ||
| 772 | + | ||
| 773 | + // Colores para los gráficos (según tema) | ||
| 774 | + const isDark = document.documentElement.classList.contains('dark'); | ||
| 775 | + const colorFuera = isDark ? '#555550' : '#d0d0d0'; | ||
| 776 | + const colorFueraFill = isDark ? '#3a3a38' : '#e8e8e8'; | ||
| 777 | + // Color de fill activo = mismo que las barras highlight | ||
| 778 | + const colorActivoFill = isDark ? '#C9A751' : '#F3EAD8'; | ||
| 779 | + const colorActivoLinea = isDark ? '#888880' : '#999999'; | ||
| 780 | + | ||
| 781 | + // Gráfico 1: Ejecución diaria (diferencia) | ||
| 782 | + if (chartDiariaContainer && data.length > 0) { | ||
| 783 | + chartDiariaContainer.innerHTML = ''; | ||
| 784 | + const widthDiaria = chartDiariaContainer.clientWidth || 400; | ||
| 785 | + | ||
| 786 | + const marks = [ | ||
| 787 | + Plot.ruleY([0], { stroke: colorRule, strokeWidth: 1 }) | ||
| 788 | + ]; | ||
| 789 | + | ||
| 790 | + // Primero la parte fuera de foco (si hay) | ||
| 791 | + if (dataFuera.length > 0) { | ||
| 792 | + marks.push( | ||
| 793 | + Plot.areaY(dataFuera, { | ||
| 794 | + x: 'fecha', | ||
| 795 | + y: 'diferencia', | ||
| 796 | + fill: colorFueraFill, | ||
| 797 | + fillOpacity: 0.5, | ||
| 798 | + curve: 'monotone-x' | ||
| 799 | + }), | ||
| 800 | + Plot.lineY(dataFuera, { | ||
| 801 | + x: 'fecha', | ||
| 802 | + y: 'diferencia', | ||
| 803 | + stroke: colorFuera, | ||
| 804 | + strokeWidth: 1.5, | ||
| 805 | + curve: 'monotone-x' | ||
| 806 | + }) | ||
| 807 | + ); | ||
| 808 | + } | ||
| 809 | + | ||
| 810 | + // Luego la parte activa | ||
| 811 | + if (dataActiva.length > 0) { | ||
| 812 | + marks.push( | ||
| 813 | + Plot.areaY(dataActiva, { | ||
| 814 | + x: 'fecha', | ||
| 815 | + y: 'diferencia', | ||
| 816 | + fill: colorActivoFill, | ||
| 817 | + fillOpacity: 0.6, | ||
| 818 | + curve: 'monotone-x' | ||
| 819 | + }), | ||
| 820 | + Plot.lineY(dataActiva, { | ||
| 821 | + x: 'fecha', | ||
| 822 | + y: 'diferencia', | ||
| 823 | + stroke: colorActivoLinea, | ||
| 824 | + strokeWidth: 1.5, | ||
| 825 | + curve: 'monotone-x' | ||
| 826 | + }) | ||
| 827 | + ); | ||
| 828 | + } | ||
| 829 | + | ||
| 830 | + const plotDiaria = Plot.plot({ | ||
| 831 | + ...chartConfig, | ||
| 832 | + width: widthDiaria, | ||
| 833 | + marks | ||
| 834 | + }); | ||
| 835 | + | ||
| 836 | + chartDiariaContainer.appendChild(plotDiaria); | ||
| 837 | + } | ||
| 838 | + | ||
| 839 | + // Gráfico 2: Ejecución acumulada (devengado) | ||
| 840 | + if (chartAcumuladaContainer && data.length > 0) { | ||
| 841 | + chartAcumuladaContainer.innerHTML = ''; | ||
| 842 | + | ||
| 843 | + // Crear punto de inicio en 1 de enero del año de los datos | ||
| 844 | + const primerDato = data[0]; | ||
| 845 | + const anioData = primerDato.fecha.getFullYear(); | ||
| 846 | + const inicioAnio = new Date(anioData, 0, 1); // 1 de enero | ||
| 847 | + | ||
| 848 | + // Línea desde inicio del año (valor 0) hasta el primer dato | ||
| 849 | + const lineaInicio = [ | ||
| 850 | + { fecha: inicioAnio, devengado: 0 }, | ||
| 851 | + { fecha: primerDato.fecha, devengado: primerDato.devengado } | ||
| 852 | + ]; | ||
| 853 | + | ||
| 854 | + const marksAcum = [ | ||
| 855 | + Plot.ruleY([0], { stroke: colorRule, strokeWidth: 1 }), | ||
| 856 | + // Área y línea recta desde 1 de enero hasta primer dato | ||
| 857 | + Plot.areaY(lineaInicio, { | ||
| 858 | + x: 'fecha', | ||
| 859 | + y: 'devengado', | ||
| 860 | + fill: colorActivoFill, | ||
| 861 | + fillOpacity: 0.6 | ||
| 862 | + }), | ||
| 863 | + Plot.line(lineaInicio, { | ||
| 864 | + x: 'fecha', | ||
| 865 | + y: 'devengado', | ||
| 866 | + stroke: colorActivoLinea, | ||
| 867 | + strokeWidth: 1.5 | ||
| 868 | + }) | ||
| 869 | + ]; | ||
| 870 | + | ||
| 871 | + // Primero la parte fuera de foco (si hay) | ||
| 872 | + if (dataFuera.length > 0) { | ||
| 873 | + marksAcum.push( | ||
| 874 | + Plot.areaY(dataFuera, { | ||
| 875 | + x: 'fecha', | ||
| 876 | + y: 'devengado', | ||
| 877 | + fill: colorFueraFill, | ||
| 878 | + fillOpacity: 0.5, | ||
| 879 | + curve: 'monotone-x' | ||
| 880 | + }), | ||
| 881 | + Plot.lineY(dataFuera, { | ||
| 882 | + x: 'fecha', | ||
| 883 | + y: 'devengado', | ||
| 884 | + stroke: colorFuera, | ||
| 885 | + strokeWidth: 1.5, | ||
| 886 | + curve: 'monotone-x' | ||
| 887 | + }) | ||
| 888 | + ); | ||
| 889 | + } | ||
| 890 | + | ||
| 891 | + // Luego la parte activa | ||
| 892 | + if (dataActiva.length > 0) { | ||
| 893 | + marksAcum.push( | ||
| 894 | + Plot.areaY(dataActiva, { | ||
| 895 | + x: 'fecha', | ||
| 896 | + y: 'devengado', | ||
| 897 | + fill: colorActivoFill, | ||
| 898 | + fillOpacity: 0.6, | ||
| 899 | + curve: 'monotone-x' | ||
| 900 | + }), | ||
| 901 | + Plot.lineY(dataActiva, { | ||
| 902 | + x: 'fecha', | ||
| 903 | + y: 'devengado', | ||
| 904 | + stroke: colorActivoLinea, | ||
| 905 | + strokeWidth: 1.5, | ||
| 906 | + curve: 'monotone-x' | ||
| 907 | + }) | ||
| 908 | + ); | ||
| 909 | + } | ||
| 910 | + | ||
| 911 | + const plotAcumulada = Plot.plot({ | ||
| 912 | + ...chartConfig, | ||
| 913 | + width: chartAcumuladaContainer.clientWidth || 400, | ||
| 914 | + marks: marksAcum | ||
| 915 | + }); | ||
| 916 | + | ||
| 917 | + chartAcumuladaContainer.appendChild(plotAcumulada); | ||
| 918 | + } | ||
| 919 | + } | ||
| 920 | + | ||
| 921 | + | ||
| 922 | + // Reaccionar a cambios en controles y cuando los contenedores estén listos | ||
| 923 | + $effect(() => { | ||
| 924 | + // Dependencias explícitas | ||
| 925 | + const containers = [chartDiariaContainer, chartAcumuladaContainer]; | ||
| 926 | + const filters = [diasSeleccionados, incluirTransferencias]; | ||
| 927 | + const dataReady = timeserieFiltered.length > 0; | ||
| 928 | + | ||
| 929 | + if (!loading && chartDiariaContainer && chartAcumuladaContainer && dataReady) { | ||
| 930 | + setTimeout(() => { | ||
| 931 | + renderizarTimeseries(); | ||
| 932 | + }, 100); | ||
| 933 | + } | ||
| 934 | + }); | ||
| 935 | + | ||
| 936 | + // Calcular totales | ||
| 937 | + let totalDevengado = $derived.by(() => { | ||
| 938 | + if (timeserieFiltered.length === 0) return 0; | ||
| 939 | + return timeserieFiltered[timeserieFiltered.length - 1]?.devengado || 0; | ||
| 940 | + }); | ||
| 941 | + | ||
| 942 | + let diferenciaDiaria = $derived.by(() => { | ||
| 943 | + if (timeserieFiltered.length === 0) return 0; | ||
| 944 | + return timeserieFiltered[timeserieFiltered.length - 1]?.diferencia || 0; | ||
| 945 | + }); | ||
| 946 | +</script> | ||
| 947 | + | ||
| 948 | +<svelte:head> | ||
| 949 | + <title>Vista Diaria | Presupuesto Público</title> | ||
| 950 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 951 | + <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> | ||
| 952 | +</svelte:head> | ||
| 953 | + | ||
| 954 | +<div class="vista-diaria" style="font-family: var(--font-sans);" bind:this={vistaContainer} onscroll={detectarSeccion}> | ||
| 955 | + <!-- Navegación interna fija --> | ||
| 956 | + <nav class="nav-interna"> | ||
| 957 | + {#each secciones as seccion, i} | ||
| 958 | + <button | ||
| 959 | + class="nav-item" | ||
| 960 | + class:active={seccionActiva === i} | ||
| 961 | + onclick={() => irASeccion(i)} | ||
| 962 | + > | ||
| 963 | + <span class="nav-dot"></span> | ||
| 964 | + <span class="nav-label">{seccion.titulo}</span> | ||
| 965 | + </button> | ||
| 966 | + {/each} | ||
| 967 | + </nav> | ||
| 968 | + | ||
| 969 | + {#if loading} | ||
| 970 | + <div class="loading"> | ||
| 971 | + <p>Cargando datos...</p> | ||
| 972 | + </div> | ||
| 973 | + {:else if error} | ||
| 974 | + <div class="error"> | ||
| 975 | + <p>Error: {error}</p> | ||
| 976 | + </div> | ||
| 977 | + {:else} | ||
| 978 | + <!-- SECCIÓN 1: Clasificadores --> | ||
| 979 | + <section class="vista-seccion" id="clasificadores"> | ||
| 980 | + <div class="vista-contenido"> | ||
| 981 | + <div class="contenido-centrado"> | ||
| 982 | + <!-- Título de sección --> | ||
| 983 | + <header class="seccion-header"> | ||
| 984 | + <h1 class="seccion-titulo-grande">Gasto del Estado: quiénes, qué, dónde y cuánto</h1> | ||
| 985 | + </header> | ||
| 986 | + | ||
| 987 | + <!-- Contenido principal: barras clasificadores --> | ||
| 988 | + <div class="clasificadores-grid"> | ||
| 989 | + {#each clasificadores as { key, label }} | ||
| 990 | + {@const datos = datosPorClasificador[key] || []} | ||
| 991 | + {@const displayItem = getDisplayItem(key)} | ||
| 992 | + | ||
| 993 | + {#if datos.length > 0} | ||
| 994 | + <div class="clasificador-row"> | ||
| 995 | + <div class="clasificador-header"> | ||
| 996 | + <div class="clasificador-info"> | ||
| 997 | + <span class="clasificador-monto">{formatearNumeroCompleto(displayItem?.devengado || 0)}</span> | ||
| 998 | + <span class="bs-en">Bs. en</span> | ||
| 999 | + <span class="clasificador-hijo">{displayItem?.hijo || ''}</span> | ||
| 1000 | + <span class="clasificador-padre-annotation">({displayItem?.padre || ''})</span> | ||
| 1001 | + </div> | ||
| 1002 | + <span class="clasificador-tipo">{label}</span> | ||
| 1003 | + </div> | ||
| 1004 | + <div class="barra-d3-container" bind:this={barContainers[key]}></div> | ||
| 1005 | + </div> | ||
| 1006 | + {/if} | ||
| 1007 | + {/each} | ||
| 1008 | + </div> | ||
| 1009 | + | ||
| 1010 | + <!-- Controles centrados debajo --> | ||
| 1011 | + <div class="controles-footer"> | ||
| 1012 | + {#each labelsPeriodo as { value, label }} | ||
| 1013 | + <button | ||
| 1014 | + class="periodo-btn" | ||
| 1015 | + class:active={diasSeleccionados === value} | ||
| 1016 | + onclick={() => diasSeleccionados = value} | ||
| 1017 | + > | ||
| 1018 | + {label} | ||
| 1019 | + </button> | ||
| 1020 | + {/each} | ||
| 1021 | + <label class="toggle-transferencias"> | ||
| 1022 | + <input type="checkbox" bind:checked={incluirTransferencias} /> | ||
| 1023 | + <span class="toggle-label">Incluir transferencias</span> | ||
| 1024 | + <span class="tooltip-trigger"> | ||
| 1025 | + <svg class="tooltip-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
| 1026 | + <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> | ||
| 1027 | + </svg> | ||
| 1028 | + <span class="tooltip-content"> | ||
| 1029 | + Las transferencias son recursos que el gobierno central envía a gobiernos territoriales o al sector privado. Al desactivar esta opción se evita la doble contabilización, mostrando el gasto consolidado. | ||
| 1030 | + </span> | ||
| 1031 | + </span> | ||
| 1032 | + </label> | ||
| 1033 | + </div> | ||
| 1034 | + </div> | ||
| 1035 | + </div> | ||
| 1036 | + </section> | ||
| 1037 | + | ||
| 1038 | + | ||
| 1039 | + <!-- SECCIÓN 2: Entidades --> | ||
| 1040 | + <section class="vista-seccion" id="entidades"> | ||
| 1041 | + <div class="vista-contenido"> | ||
| 1042 | + <div class="contenido-centrado"> | ||
| 1043 | + <!-- Título de sección --> | ||
| 1044 | + <header class="seccion-header"> | ||
| 1045 | + <h1 class="seccion-titulo-grande">¿Quién ejecuta más?</h1> | ||
| 1046 | + </header> | ||
| 1047 | + | ||
| 1048 | + <!-- Info de entidad seleccionada --> | ||
| 1049 | + {#if displayEntidad} | ||
| 1050 | + <div class="entidad-info"> | ||
| 1051 | + <span class="entidad-pct">{formatearPorcentaje(displayEntidad.ejecucion)}</span> | ||
| 1052 | + <span class="entidad-nombre">{displayEntidad.entidad_desc_entidad}</span> | ||
| 1053 | + <span class="entidad-area">{displayEntidad.entidad_desc_area}</span> | ||
| 1054 | + </div> | ||
| 1055 | + {/if} | ||
| 1056 | + | ||
| 1057 | + <!-- Barras de ejecución --> | ||
| 1058 | + <div class="entidades-bar-container" bind:this={entidadesBarContainer}></div> | ||
| 1059 | + | ||
| 1060 | + <!-- Paginación --> | ||
| 1061 | + <div class="entidades-paginacion"> | ||
| 1062 | + <p class="entidades-nota"> | ||
| 1063 | + Mostrando {entidadesFiltered.length} de {entidadesTodas.length} entidades | ||
| 1064 | + </p> | ||
| 1065 | + <div class="entidades-botones"> | ||
| 1066 | + {#if entidadesVisibles > 10} | ||
| 1067 | + <button class="entidades-ver-mas" onclick={() => { entidadesVisibles = Math.max(10, entidadesVisibles - 10); }}> | ||
| 1068 | + Ver menos | ||
| 1069 | + </button> | ||
| 1070 | + {/if} | ||
| 1071 | + {#if entidadesFiltered.length < entidadesTodas.length} | ||
| 1072 | + <button class="entidades-ver-mas" onclick={() => { entidadesVisibles += 10; }}> | ||
| 1073 | + Ver {Math.min(10, entidadesTodas.length - entidadesFiltered.length)} más | ||
| 1074 | + </button> | ||
| 1075 | + {/if} | ||
| 1076 | + </div> | ||
| 1077 | + </div> | ||
| 1078 | + | ||
| 1079 | + <!-- Controles centrados debajo (igual que sección 1) --> | ||
| 1080 | + <div class="controles-footer"> | ||
| 1081 | + {#each labelsPeriodo as { value, label }} | ||
| 1082 | + <button | ||
| 1083 | + class="periodo-btn" | ||
| 1084 | + class:active={diasSeleccionados === value} | ||
| 1085 | + onclick={() => diasSeleccionados = value} | ||
| 1086 | + > | ||
| 1087 | + {label} | ||
| 1088 | + </button> | ||
| 1089 | + {/each} | ||
| 1090 | + <label class="toggle-transferencias"> | ||
| 1091 | + <input type="checkbox" bind:checked={incluirTransferencias} /> | ||
| 1092 | + <span class="toggle-label">Incluir transferencias</span> | ||
| 1093 | + <span class="tooltip-trigger"> | ||
| 1094 | + <svg class="tooltip-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
| 1095 | + <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> | ||
| 1096 | + </svg> | ||
| 1097 | + <span class="tooltip-content"> | ||
| 1098 | + Las transferencias son recursos que el gobierno central envía a gobiernos territoriales o al sector privado. Al desactivar esta opción se evita la doble contabilización, mostrando el gasto consolidado. | ||
| 1099 | + </span> | ||
| 1100 | + </span> | ||
| 1101 | + </label> | ||
| 1102 | + <div class="separador-control"></div> | ||
| 1103 | + <!-- Filtro de rango --> | ||
| 1104 | + <div class="rango-control-inline"> | ||
| 1105 | + <label class="rango-label">Rango: {rangoEjecucionMin}% – {rangoEjecucionMax}%</label> | ||
| 1106 | + <div class="dual-slider dual-slider-inline" bind:this={sliderContainer}> | ||
| 1107 | + <div class="slider-track"></div> | ||
| 1108 | + <div | ||
| 1109 | + class="slider-range" | ||
| 1110 | + style="left: {rangoEjecucionMin}%; right: {100 - rangoEjecucionMax}%;" | ||
| 1111 | + onmousedown={handleRangeMouseDown} | ||
| 1112 | + role="slider" | ||
| 1113 | + tabindex="0" | ||
| 1114 | + ></div> | ||
| 1115 | + <input | ||
| 1116 | + type="range" | ||
| 1117 | + min="0" | ||
| 1118 | + max="100" | ||
| 1119 | + bind:value={rangoEjecucionMin} | ||
| 1120 | + class="slider-thumb slider-thumb-min" | ||
| 1121 | + oninput={() => { | ||
| 1122 | + if (rangoEjecucionMin > rangoEjecucionMax - 5) { | ||
| 1123 | + rangoEjecucionMin = rangoEjecucionMax - 5; | ||
| 1124 | + } | ||
| 1125 | + }} | ||
| 1126 | + /> | ||
| 1127 | + <input | ||
| 1128 | + type="range" | ||
| 1129 | + min="0" | ||
| 1130 | + max="100" | ||
| 1131 | + bind:value={rangoEjecucionMax} | ||
| 1132 | + class="slider-thumb slider-thumb-max" | ||
| 1133 | + oninput={() => { | ||
| 1134 | + if (rangoEjecucionMax < rangoEjecucionMin + 5) { | ||
| 1135 | + rangoEjecucionMax = rangoEjecucionMin + 5; | ||
| 1136 | + } | ||
| 1137 | + }} | ||
| 1138 | + /> | ||
| 1139 | + </div> | ||
| 1140 | + </div> | ||
| 1141 | + </div> | ||
| 1142 | + </div> | ||
| 1143 | + </div> | ||
| 1144 | + </section> | ||
| 1145 | + | ||
| 1146 | + <!-- SECCIÓN 3: Programas --> | ||
| 1147 | + <section class="vista-seccion" id="programas"> | ||
| 1148 | + <div class="vista-contenido"> | ||
| 1149 | + <div class="contenido-centrado"> | ||
| 1150 | + <!-- Título de sección --> | ||
| 1151 | + <header class="seccion-header seccion-header-centrado"> | ||
| 1152 | + <h1 class="seccion-titulo-grande">¿En qué programas?</h1> | ||
| 1153 | + <p class="seccion-descripcion">Detalle de programas y actividades ejecutadas en la última semana</p> | ||
| 1154 | + </header> | ||
| 1155 | + | ||
| 1156 | + <div class="timeline-search"> | ||
| 1157 | + <svg class="search-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
| 1158 | + <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" /> | ||
| 1159 | + </svg> | ||
| 1160 | + <input | ||
| 1161 | + type="text" | ||
| 1162 | + placeholder="Buscar entidad..." | ||
| 1163 | + bind:value={busquedaTimeline} | ||
| 1164 | + class="timeline-search-input" | ||
| 1165 | + /> | ||
| 1166 | + </div> | ||
| 1167 | + | ||
| 1168 | + <div class="timeline-cards"> | ||
| 1169 | + {#each timelineByEntity as entidad} | ||
| 1170 | + <div class="timeline-card"> | ||
| 1171 | + <div class="timeline-card-header"> | ||
| 1172 | + <div class="timeline-card-entity"> | ||
| 1173 | + <span class="timeline-card-area">{entidad.entidad_desc_area}</span> | ||
| 1174 | + <span class="timeline-card-nombre">{entidad.entidad_desc_entidad}</span> | ||
| 1175 | + </div> | ||
| 1176 | + <div class="timeline-card-total"> | ||
| 1177 | + <span class="timeline-card-total-label">Ejecutado</span> | ||
| 1178 | + <span class="timeline-card-total-monto">Bs. {formatearNumeroCompleto(entidad.total)}</span> | ||
| 1179 | + </div> | ||
| 1180 | + </div> | ||
| 1181 | + <div class="timeline-card-body"> | ||
| 1182 | + {#each cardsExpandidas[entidad.entidad] ? entidad.programas : entidad.programas.slice(0, 8) as prog} | ||
| 1183 | + <div class="timeline-prog-row"> | ||
| 1184 | + <div class="timeline-prog-info"> | ||
| 1185 | + <span class="timeline-prog-programa">{prog.programa}</span> | ||
| 1186 | + <span class="timeline-prog-actividad">{prog.actividad}</span> | ||
| 1187 | + </div> | ||
| 1188 | + <span class="timeline-prog-monto">Bs. {formatearNumeroCompleto(prog.devengado)}</span> | ||
| 1189 | + </div> | ||
| 1190 | + {/each} | ||
| 1191 | + | ||
| 1192 | + {#if entidad.programas.length > 8} | ||
| 1193 | + <button | ||
| 1194 | + class="timeline-ver-mas" | ||
| 1195 | + onclick={() => { | ||
| 1196 | + cardsExpandidas[entidad.entidad] = !cardsExpandidas[entidad.entidad]; | ||
| 1197 | + cardsExpandidas = { ...cardsExpandidas }; | ||
| 1198 | + }} | ||
| 1199 | + > | ||
| 1200 | + {cardsExpandidas[entidad.entidad] ? 'Ver menos' : `Ver ${entidad.programas.length - 8} más...`} | ||
| 1201 | + </button> | ||
| 1202 | + {/if} | ||
| 1203 | + </div> | ||
| 1204 | + </div> | ||
| 1205 | + {/each} | ||
| 1206 | + </div> | ||
| 1207 | + </div> | ||
| 1208 | + </div> | ||
| 1209 | + </section> | ||
| 1210 | + | ||
| 1211 | + <!-- SECCIÓN 4: Evolución temporal --> | ||
| 1212 | + <section class="vista-seccion" id="evolucion"> | ||
| 1213 | + <div class="vista-contenido"> | ||
| 1214 | + <div class="contenido-centrado"> | ||
| 1215 | + <!-- Título de sección --> | ||
| 1216 | + <header class="seccion-header"> | ||
| 1217 | + <h1 class="seccion-titulo-grande">¿Cómo evoluciona el gasto?</h1> | ||
| 1218 | + </header> | ||
| 1219 | + | ||
| 1220 | + <!-- Gráficos lado a lado --> | ||
| 1221 | + <div class="charts-grid"> | ||
| 1222 | + <div class="chart-card"> | ||
| 1223 | + <div class="chart-header"> | ||
| 1224 | + <span class="chart-label">Ejecución diaria</span> | ||
| 1225 | + <span class="chart-value">{formatearMonto(diferenciaDiaria)}</span> | ||
| 1226 | + </div> | ||
| 1227 | + <div class="chart-container" bind:this={chartDiariaContainer}></div> | ||
| 1228 | + </div> | ||
| 1229 | + <div class="chart-card"> | ||
| 1230 | + <div class="chart-header"> | ||
| 1231 | + <span class="chart-label">Ejecución acumulada</span> | ||
| 1232 | + <span class="chart-value">{formatearMonto(totalDevengado)}</span> | ||
| 1233 | + </div> | ||
| 1234 | + <div class="chart-container" bind:this={chartAcumuladaContainer}></div> | ||
| 1235 | + </div> | ||
| 1236 | + </div> | ||
| 1237 | + | ||
| 1238 | + <!-- Controles centrados debajo --> | ||
| 1239 | + <div class="controles-footer"> | ||
| 1240 | + {#each labelsPeriodo as { value, label }} | ||
| 1241 | + <button | ||
| 1242 | + class="periodo-btn" | ||
| 1243 | + class:active={diasSeleccionados === value} | ||
| 1244 | + onclick={() => diasSeleccionados = value} | ||
| 1245 | + > | ||
| 1246 | + {label} | ||
| 1247 | + </button> | ||
| 1248 | + {/each} | ||
| 1249 | + <label class="toggle-transferencias"> | ||
| 1250 | + <input type="checkbox" bind:checked={incluirTransferencias} /> | ||
| 1251 | + <span class="toggle-label">Incluir transferencias</span> | ||
| 1252 | + <span class="tooltip-trigger"> | ||
| 1253 | + <svg class="tooltip-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
| 1254 | + <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> | ||
| 1255 | + </svg> | ||
| 1256 | + <span class="tooltip-content"> | ||
| 1257 | + Las transferencias son recursos que el gobierno central envía a gobiernos territoriales o al sector privado. Al desactivar esta opción se evita la doble contabilización, mostrando el gasto consolidado. | ||
| 1258 | + </span> | ||
| 1259 | + </span> | ||
| 1260 | + </label> | ||
| 1261 | + </div> | ||
| 1262 | + </div> | ||
| 1263 | + </div> | ||
| 1264 | + </section> | ||
| 1265 | + {/if} | ||
| 1266 | +</div> | ||
| 1267 | + | ||
| 1268 | +<style> | ||
| 1269 | + /* Variables de color para gráficos - Modo claro */ | ||
| 1270 | + :global(:root) { | ||
| 1271 | + --chart-bar-default: #e0e0e0; | ||
| 1272 | + --chart-bar-highlight: #F3EAD8; | ||
| 1273 | + --chart-bar-stroke: #b0b0b0; | ||
| 1274 | + --chart-bar-stroke-default: white; | ||
| 1275 | + --chart-track-bg: #f5f5f5; | ||
| 1276 | + --chart-grid: #bbb; | ||
| 1277 | + --chart-label: #888; | ||
| 1278 | + --chart-card-header: #f5f0e6; | ||
| 1279 | + } | ||
| 1280 | + | ||
| 1281 | + /* Variables de color para gráficos - Modo oscuro */ | ||
| 1282 | + :global(.dark) { | ||
| 1283 | + --chart-bar-default: #3a3a38; | ||
| 1284 | + --chart-bar-highlight: #C9A751; | ||
| 1285 | + --chart-bar-stroke: #5a5a55; | ||
| 1286 | + --chart-bar-stroke-default: #2a2a28; | ||
| 1287 | + --chart-track-bg: #2a2a28; | ||
| 1288 | + --chart-grid: #4a4a45; | ||
| 1289 | + --chart-label: #8a8a85; | ||
| 1290 | + --chart-card-header: #3a3835; | ||
| 1291 | + } | ||
| 1292 | + | ||
| 1293 | + .vista-diaria { | ||
| 1294 | + background: var(--theme-body); | ||
| 1295 | + color: var(--theme-titulo); | ||
| 1296 | + scroll-snap-type: y mandatory; | ||
| 1297 | + overflow-y: scroll; | ||
| 1298 | + height: 100vh; | ||
| 1299 | + height: 100dvh; | ||
| 1300 | + } | ||
| 1301 | + | ||
| 1302 | + /* Loading/Error */ | ||
| 1303 | + .loading, .error { | ||
| 1304 | + height: 100vh; | ||
| 1305 | + display: flex; | ||
| 1306 | + align-items: center; | ||
| 1307 | + justify-content: center; | ||
| 1308 | + color: var(--theme-texto); | ||
| 1309 | + } | ||
| 1310 | + | ||
| 1311 | + .error { | ||
| 1312 | + color: #DB281C; | ||
| 1313 | + } | ||
| 1314 | + | ||
| 1315 | + /* Navegación interna fija */ | ||
| 1316 | + .nav-interna { | ||
| 1317 | + position: fixed; | ||
| 1318 | + right: 1.5rem; | ||
| 1319 | + top: 50%; | ||
| 1320 | + transform: translateY(-50%); | ||
| 1321 | + display: flex; | ||
| 1322 | + flex-direction: column; | ||
| 1323 | + gap: 1rem; | ||
| 1324 | + z-index: 100; | ||
| 1325 | + } | ||
| 1326 | + | ||
| 1327 | + .nav-item { | ||
| 1328 | + display: flex; | ||
| 1329 | + align-items: center; | ||
| 1330 | + gap: 0.5rem; | ||
| 1331 | + background: none; | ||
| 1332 | + border: none; | ||
| 1333 | + cursor: pointer; | ||
| 1334 | + padding: 0.25rem; | ||
| 1335 | + flex-direction: row-reverse; | ||
| 1336 | + } | ||
| 1337 | + | ||
| 1338 | + .nav-dot { | ||
| 1339 | + width: 10px; | ||
| 1340 | + height: 10px; | ||
| 1341 | + border-radius: 50%; | ||
| 1342 | + background: rgba(0, 0, 0, 0.2); | ||
| 1343 | + transition: all 0.2s; | ||
| 1344 | + } | ||
| 1345 | + | ||
| 1346 | + :global(.dark) .nav-dot { | ||
| 1347 | + background: rgba(255, 255, 255, 0.2); | ||
| 1348 | + } | ||
| 1349 | + | ||
| 1350 | + .nav-item.active .nav-dot { | ||
| 1351 | + background: var(--theme-accent); | ||
| 1352 | + transform: scale(1.2); | ||
| 1353 | + } | ||
| 1354 | + | ||
| 1355 | + .nav-label { | ||
| 1356 | + font-size: 0.7rem; | ||
| 1357 | + color: var(--theme-texto); | ||
| 1358 | + opacity: 0; | ||
| 1359 | + transition: opacity 0.2s; | ||
| 1360 | + white-space: nowrap; | ||
| 1361 | + } | ||
| 1362 | + | ||
| 1363 | + .nav-item:hover .nav-label, | ||
| 1364 | + .nav-item.active .nav-label { | ||
| 1365 | + opacity: 1; | ||
| 1366 | + } | ||
| 1367 | + | ||
| 1368 | + /* Secciones de 100vh - el navbar tiene ~56px de alto */ | ||
| 1369 | + .vista-seccion { | ||
| 1370 | + min-height: 100vh; | ||
| 1371 | + min-height: 100dvh; | ||
| 1372 | + padding-top: 56px; | ||
| 1373 | + box-sizing: border-box; | ||
| 1374 | + display: flex; | ||
| 1375 | + flex-direction: column; | ||
| 1376 | + border-bottom: 1px solid var(--theme-borde); | ||
| 1377 | + scroll-snap-align: start; | ||
| 1378 | + scroll-snap-stop: always; | ||
| 1379 | + } | ||
| 1380 | + | ||
| 1381 | + | ||
| 1382 | + .vista-contenido { | ||
| 1383 | + max-width: 1200px; | ||
| 1384 | + margin: 0 auto; | ||
| 1385 | + padding: 2rem; | ||
| 1386 | + width: 100%; | ||
| 1387 | + flex: 1; | ||
| 1388 | + display: flex; | ||
| 1389 | + flex-direction: column; | ||
| 1390 | + justify-content: center; | ||
| 1391 | + } | ||
| 1392 | + | ||
| 1393 | + .contenido-centrado { | ||
| 1394 | + width: 100%; | ||
| 1395 | + } | ||
| 1396 | + | ||
| 1397 | + /* Controles centrados debajo del gráfico */ | ||
| 1398 | + .controles-footer { | ||
| 1399 | + display: flex; | ||
| 1400 | + flex-wrap: wrap; | ||
| 1401 | + justify-content: center; | ||
| 1402 | + align-items: center; | ||
| 1403 | + gap: 0.5rem; | ||
| 1404 | + margin-top: 2rem; | ||
| 1405 | + padding-top: 1.5rem; | ||
| 1406 | + border-top: 1px solid var(--theme-borde); | ||
| 1407 | + } | ||
| 1408 | + | ||
| 1409 | + .periodo-btn { | ||
| 1410 | + padding: 0.5rem 1rem; | ||
| 1411 | + font-family: 'Qanelas', var(--font-sans); | ||
| 1412 | + font-size: 0.8rem; | ||
| 1413 | + font-weight: 500; | ||
| 1414 | + border: 1px solid var(--theme-borde); | ||
| 1415 | + border-radius: 20px; | ||
| 1416 | + background: transparent; | ||
| 1417 | + color: var(--theme-texto); | ||
| 1418 | + cursor: pointer; | ||
| 1419 | + transition: all 0.2s ease; | ||
| 1420 | + } | ||
| 1421 | + | ||
| 1422 | + .periodo-btn:hover { | ||
| 1423 | + background: var(--theme-fill); | ||
| 1424 | + border-color: var(--theme-accent); | ||
| 1425 | + color: var(--theme-titulo); | ||
| 1426 | + } | ||
| 1427 | + | ||
| 1428 | + .periodo-btn.active { | ||
| 1429 | + background: var(--theme-titulo); | ||
| 1430 | + border-color: var(--theme-titulo); | ||
| 1431 | + color: var(--theme-body); | ||
| 1432 | + } | ||
| 1433 | + | ||
| 1434 | + /* Dark mode: botón activo con acento dorado */ | ||
| 1435 | + :global(.dark) .periodo-btn.active { | ||
| 1436 | + background: var(--theme-accent); | ||
| 1437 | + border-color: var(--theme-accent); | ||
| 1438 | + color: var(--theme-body); | ||
| 1439 | + } | ||
| 1440 | + | ||
| 1441 | + .toggle-transferencias { | ||
| 1442 | + display: flex; | ||
| 1443 | + align-items: center; | ||
| 1444 | + gap: 0.5rem; | ||
| 1445 | + cursor: pointer; | ||
| 1446 | + margin-left: 0.5rem; | ||
| 1447 | + padding-left: 1rem; | ||
| 1448 | + border-left: 1px solid var(--theme-borde); | ||
| 1449 | + } | ||
| 1450 | + | ||
| 1451 | + .toggle-transferencias input[type="checkbox"] { | ||
| 1452 | + width: 16px; | ||
| 1453 | + height: 16px; | ||
| 1454 | + accent-color: var(--theme-accent); | ||
| 1455 | + cursor: pointer; | ||
| 1456 | + } | ||
| 1457 | + | ||
| 1458 | + .toggle-label { | ||
| 1459 | + font-size: 0.75rem; | ||
| 1460 | + color: var(--theme-texto); | ||
| 1461 | + } | ||
| 1462 | + | ||
| 1463 | + .separador-control { | ||
| 1464 | + width: 1px; | ||
| 1465 | + height: 24px; | ||
| 1466 | + background: var(--theme-borde); | ||
| 1467 | + margin: 0 0.5rem; | ||
| 1468 | + } | ||
| 1469 | + | ||
| 1470 | + .rango-control-inline { | ||
| 1471 | + display: flex; | ||
| 1472 | + flex-direction: column; | ||
| 1473 | + align-items: flex-start; | ||
| 1474 | + gap: 0.25rem; | ||
| 1475 | + flex: 1; | ||
| 1476 | + min-width: 0; | ||
| 1477 | + } | ||
| 1478 | + | ||
| 1479 | + .rango-control-inline .rango-label { | ||
| 1480 | + font-size: 0.7rem; | ||
| 1481 | + } | ||
| 1482 | + | ||
| 1483 | + .dual-slider-inline { | ||
| 1484 | + width: 100%; | ||
| 1485 | + } | ||
| 1486 | + | ||
| 1487 | + /* Tooltip */ | ||
| 1488 | + .tooltip-trigger { | ||
| 1489 | + position: relative; | ||
| 1490 | + display: inline-flex; | ||
| 1491 | + align-items: center; | ||
| 1492 | + cursor: help; | ||
| 1493 | + } | ||
| 1494 | + | ||
| 1495 | + .tooltip-icon { | ||
| 1496 | + width: 14px; | ||
| 1497 | + height: 14px; | ||
| 1498 | + color: var(--theme-texto); | ||
| 1499 | + opacity: 0.5; | ||
| 1500 | + transition: opacity 0.2s; | ||
| 1501 | + } | ||
| 1502 | + | ||
| 1503 | + .tooltip-trigger:hover .tooltip-icon { | ||
| 1504 | + opacity: 1; | ||
| 1505 | + color: var(--theme-accent); | ||
| 1506 | + } | ||
| 1507 | + | ||
| 1508 | + .tooltip-content { | ||
| 1509 | + position: absolute; | ||
| 1510 | + bottom: calc(100% + 8px); | ||
| 1511 | + left: 50%; | ||
| 1512 | + transform: translateX(-50%); | ||
| 1513 | + width: 280px; | ||
| 1514 | + padding: 0.75rem 1rem; | ||
| 1515 | + background: var(--theme-titulo); | ||
| 1516 | + color: var(--theme-body); | ||
| 1517 | + font-size: 0.75rem; | ||
| 1518 | + line-height: 1.5; | ||
| 1519 | + border-radius: 8px; | ||
| 1520 | + opacity: 0; | ||
| 1521 | + visibility: hidden; | ||
| 1522 | + transition: all 0.2s ease; | ||
| 1523 | + z-index: 100; | ||
| 1524 | + pointer-events: none; | ||
| 1525 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||
| 1526 | + } | ||
| 1527 | + | ||
| 1528 | + .tooltip-content::after { | ||
| 1529 | + content: ''; | ||
| 1530 | + position: absolute; | ||
| 1531 | + top: 100%; | ||
| 1532 | + left: 50%; | ||
| 1533 | + transform: translateX(-50%); | ||
| 1534 | + border: 6px solid transparent; | ||
| 1535 | + border-top-color: var(--theme-titulo); | ||
| 1536 | + } | ||
| 1537 | + | ||
| 1538 | + .tooltip-trigger:hover .tooltip-content { | ||
| 1539 | + opacity: 1; | ||
| 1540 | + visibility: visible; | ||
| 1541 | + } | ||
| 1542 | + | ||
| 1543 | + /* Header de sección */ | ||
| 1544 | + .seccion-header { | ||
| 1545 | + margin-bottom: 2rem; | ||
| 1546 | + } | ||
| 1547 | + | ||
| 1548 | + .seccion-header-centrado { | ||
| 1549 | + text-align: center; | ||
| 1550 | + } | ||
| 1551 | + | ||
| 1552 | + .seccion-titulo-grande { | ||
| 1553 | + font-family: 'Qanelas', var(--font-sans); | ||
| 1554 | + font-size: 2rem; | ||
| 1555 | + font-weight: 600; | ||
| 1556 | + margin: 0 0 0.25rem; | ||
| 1557 | + color: var(--theme-titulo); | ||
| 1558 | + } | ||
| 1559 | + | ||
| 1560 | + .seccion-descripcion { | ||
| 1561 | + font-size: 0.875rem; | ||
| 1562 | + color: var(--theme-texto); | ||
| 1563 | + margin: 0; | ||
| 1564 | + } | ||
| 1565 | + | ||
| 1566 | + /* Clasificadores Grid */ | ||
| 1567 | + .clasificadores-grid { | ||
| 1568 | + display: flex; | ||
| 1569 | + flex-direction: column; | ||
| 1570 | + gap: 1rem; | ||
| 1571 | + } | ||
| 1572 | + | ||
| 1573 | + .clasificador-row { | ||
| 1574 | + display: flex; | ||
| 1575 | + flex-direction: column; | ||
| 1576 | + gap: 0.35rem; | ||
| 1577 | + } | ||
| 1578 | + | ||
| 1579 | + .clasificador-header { | ||
| 1580 | + display: flex; | ||
| 1581 | + align-items: baseline; | ||
| 1582 | + justify-content: space-between; | ||
| 1583 | + gap: 1rem; | ||
| 1584 | + } | ||
| 1585 | + | ||
| 1586 | + .clasificador-info { | ||
| 1587 | + display: flex; | ||
| 1588 | + align-items: baseline; | ||
| 1589 | + gap: 0.3rem; | ||
| 1590 | + flex-wrap: wrap; | ||
| 1591 | + } | ||
| 1592 | + | ||
| 1593 | + .clasificador-monto { | ||
| 1594 | + font-family: var(--font-sans); | ||
| 1595 | + font-variant-numeric: tabular-nums; | ||
| 1596 | + font-size: 0.95rem; | ||
| 1597 | + font-weight: 600; | ||
| 1598 | + color: var(--theme-titulo); | ||
| 1599 | + } | ||
| 1600 | + | ||
| 1601 | + .bs-en { | ||
| 1602 | + font-size: 0.75rem; | ||
| 1603 | + color: var(--theme-texto); | ||
| 1604 | + opacity: 0.6; | ||
| 1605 | + } | ||
| 1606 | + | ||
| 1607 | + .clasificador-hijo { | ||
| 1608 | + font-size: 0.85rem; | ||
| 1609 | + font-weight: 500; | ||
| 1610 | + color: var(--theme-titulo); | ||
| 1611 | + } | ||
| 1612 | + | ||
| 1613 | + .clasificador-padre-annotation { | ||
| 1614 | + font-size: 0.7rem; | ||
| 1615 | + color: var(--theme-texto); | ||
| 1616 | + opacity: 0.5; | ||
| 1617 | + } | ||
| 1618 | + | ||
| 1619 | + .barra-d3-container { | ||
| 1620 | + width: 100%; | ||
| 1621 | + height: 52px; | ||
| 1622 | + border-radius: 4px; | ||
| 1623 | + overflow: hidden; | ||
| 1624 | + } | ||
| 1625 | + | ||
| 1626 | + .clasificador-tipo { | ||
| 1627 | + font-size: 0.65rem; | ||
| 1628 | + text-transform: uppercase; | ||
| 1629 | + letter-spacing: 0.05em; | ||
| 1630 | + color: var(--theme-texto); | ||
| 1631 | + opacity: 0.7; | ||
| 1632 | + flex-shrink: 0; | ||
| 1633 | + } | ||
| 1634 | + | ||
| 1635 | + /* Timeline Section - Cards */ | ||
| 1636 | + .timeline-search { | ||
| 1637 | + margin: 0 auto 1rem auto; | ||
| 1638 | + position: relative; | ||
| 1639 | + width: 100%; | ||
| 1640 | + max-width: 400px; | ||
| 1641 | + } | ||
| 1642 | + | ||
| 1643 | + .search-icon { | ||
| 1644 | + position: absolute; | ||
| 1645 | + left: 0.75rem; | ||
| 1646 | + top: 50%; | ||
| 1647 | + transform: translateY(-50%); | ||
| 1648 | + width: 16px; | ||
| 1649 | + height: 16px; | ||
| 1650 | + color: var(--theme-texto); | ||
| 1651 | + opacity: 0.5; | ||
| 1652 | + pointer-events: none; | ||
| 1653 | + } | ||
| 1654 | + | ||
| 1655 | + .timeline-search-input { | ||
| 1656 | + width: 100%; | ||
| 1657 | + padding: 0.6rem 1rem 0.6rem 2.25rem; | ||
| 1658 | + font-size: 0.8rem; | ||
| 1659 | + font-family: inherit; | ||
| 1660 | + border: 1px solid var(--theme-borde); | ||
| 1661 | + border-radius: 20px; | ||
| 1662 | + background: var(--theme-body); | ||
| 1663 | + color: var(--theme-titulo); | ||
| 1664 | + outline: none; | ||
| 1665 | + transition: border-color 0.2s; | ||
| 1666 | + } | ||
| 1667 | + | ||
| 1668 | + .timeline-search-input:focus { | ||
| 1669 | + border-color: var(--theme-accent); | ||
| 1670 | + } | ||
| 1671 | + | ||
| 1672 | + .timeline-search-input:focus + .search-icon, | ||
| 1673 | + .timeline-search:focus-within .search-icon { | ||
| 1674 | + opacity: 1; | ||
| 1675 | + color: var(--theme-accent); | ||
| 1676 | + } | ||
| 1677 | + | ||
| 1678 | + .timeline-search-input::placeholder { | ||
| 1679 | + color: var(--theme-texto); | ||
| 1680 | + opacity: 0.5; | ||
| 1681 | + } | ||
| 1682 | + | ||
| 1683 | + .timeline-cards { | ||
| 1684 | + display: flex; | ||
| 1685 | + flex-direction: column; | ||
| 1686 | + gap: 0.75rem; | ||
| 1687 | + max-width: 80%; | ||
| 1688 | + margin: 0 auto; | ||
| 1689 | + } | ||
| 1690 | + | ||
| 1691 | + .timeline-card { | ||
| 1692 | + background: var(--theme-surface); | ||
| 1693 | + border: 1px solid rgba(0, 0, 0, 0.08); | ||
| 1694 | + border-radius: 6px; | ||
| 1695 | + overflow: hidden; | ||
| 1696 | + } | ||
| 1697 | + | ||
| 1698 | + :global(.dark) .timeline-card { | ||
| 1699 | + background: var(--theme-fill); | ||
| 1700 | + border-color: rgba(255, 255, 255, 0.1); | ||
| 1701 | + } | ||
| 1702 | + | ||
| 1703 | + .timeline-card-header { | ||
| 1704 | + background: var(--chart-card-header); | ||
| 1705 | + padding: 0.75rem 1rem; | ||
| 1706 | + display: flex; | ||
| 1707 | + justify-content: space-between; | ||
| 1708 | + align-items: flex-start; | ||
| 1709 | + gap: 1rem; | ||
| 1710 | + } | ||
| 1711 | + | ||
| 1712 | + .timeline-card-entity { | ||
| 1713 | + display: flex; | ||
| 1714 | + flex-direction: column; | ||
| 1715 | + gap: 0.15rem; | ||
| 1716 | + } | ||
| 1717 | + | ||
| 1718 | + .timeline-card-area { | ||
| 1719 | + font-size: 0.65rem; | ||
| 1720 | + text-transform: uppercase; | ||
| 1721 | + letter-spacing: 0.03em; | ||
| 1722 | + color: var(--theme-texto); | ||
| 1723 | + opacity: 0.6; | ||
| 1724 | + } | ||
| 1725 | + | ||
| 1726 | + .timeline-card-nombre { | ||
| 1727 | + font-size: 1rem; | ||
| 1728 | + font-weight: 600; | ||
| 1729 | + color: var(--theme-titulo); | ||
| 1730 | + line-height: 1.2; | ||
| 1731 | + } | ||
| 1732 | + | ||
| 1733 | + .timeline-card-total { | ||
| 1734 | + display: flex; | ||
| 1735 | + flex-direction: column; | ||
| 1736 | + align-items: flex-end; | ||
| 1737 | + gap: 0.1rem; | ||
| 1738 | + flex-shrink: 0; | ||
| 1739 | + } | ||
| 1740 | + | ||
| 1741 | + .timeline-card-total-label { | ||
| 1742 | + font-size: 0.65rem; | ||
| 1743 | + text-transform: uppercase; | ||
| 1744 | + letter-spacing: 0.03em; | ||
| 1745 | + color: var(--theme-texto); | ||
| 1746 | + opacity: 0.6; | ||
| 1747 | + } | ||
| 1748 | + | ||
| 1749 | + .timeline-card-total-monto { | ||
| 1750 | + font-family: var(--font-sans); | ||
| 1751 | + font-variant-numeric: tabular-nums; | ||
| 1752 | + font-size: 1rem; | ||
| 1753 | + font-weight: 500; | ||
| 1754 | + color: var(--theme-titulo); | ||
| 1755 | + } | ||
| 1756 | + | ||
| 1757 | + .timeline-card-body { | ||
| 1758 | + padding: 0.5rem 1rem; | ||
| 1759 | + text-align: center; | ||
| 1760 | + } | ||
| 1761 | + | ||
| 1762 | + .timeline-prog-row { | ||
| 1763 | + display: flex; | ||
| 1764 | + justify-content: space-between; | ||
| 1765 | + align-items: flex-start; | ||
| 1766 | + gap: 1rem; | ||
| 1767 | + padding: 0.4rem 0; | ||
| 1768 | + border-bottom: 1px dotted rgba(0, 0, 0, 0.2); | ||
| 1769 | + text-align: left; | ||
| 1770 | + } | ||
| 1771 | + | ||
| 1772 | + :global(.dark) .timeline-prog-row { | ||
| 1773 | + border-bottom-color: rgba(255, 255, 255, 0.2); | ||
| 1774 | + } | ||
| 1775 | + | ||
| 1776 | + .timeline-prog-row:last-child { | ||
| 1777 | + border-bottom: none; | ||
| 1778 | + } | ||
| 1779 | + | ||
| 1780 | + .timeline-prog-info { | ||
| 1781 | + display: flex; | ||
| 1782 | + flex-direction: column; | ||
| 1783 | + gap: 0.1rem; | ||
| 1784 | + min-width: 0; | ||
| 1785 | + flex: 1; | ||
| 1786 | + } | ||
| 1787 | + | ||
| 1788 | + .timeline-prog-programa { | ||
| 1789 | + font-size: 0.6rem; | ||
| 1790 | + color: var(--theme-texto); | ||
| 1791 | + opacity: 0.5; | ||
| 1792 | + line-height: 1.2; | ||
| 1793 | + white-space: nowrap; | ||
| 1794 | + overflow: hidden; | ||
| 1795 | + text-overflow: ellipsis; | ||
| 1796 | + } | ||
| 1797 | + | ||
| 1798 | + .timeline-prog-actividad { | ||
| 1799 | + font-size: 0.75rem; | ||
| 1800 | + font-weight: 500; | ||
| 1801 | + color: var(--theme-titulo); | ||
| 1802 | + line-height: 1.25; | ||
| 1803 | + } | ||
| 1804 | + | ||
| 1805 | + .timeline-prog-monto { | ||
| 1806 | + font-family: var(--font-sans); | ||
| 1807 | + font-variant-numeric: tabular-nums; | ||
| 1808 | + font-size: 0.75rem; | ||
| 1809 | + color: var(--theme-titulo); | ||
| 1810 | + flex-shrink: 0; | ||
| 1811 | + } | ||
| 1812 | + | ||
| 1813 | + .timeline-ver-mas { | ||
| 1814 | + display: inline; | ||
| 1815 | + padding: 0; | ||
| 1816 | + margin-top: 0.5rem; | ||
| 1817 | + background: transparent; | ||
| 1818 | + border: none; | ||
| 1819 | + border-bottom: 1px dotted var(--theme-texto); | ||
| 1820 | + color: var(--theme-texto); | ||
| 1821 | + font-size: 0.7rem; | ||
| 1822 | + font-family: inherit; | ||
| 1823 | + cursor: pointer; | ||
| 1824 | + transition: all 0.2s; | ||
| 1825 | + } | ||
| 1826 | + | ||
| 1827 | + .timeline-ver-mas:hover { | ||
| 1828 | + color: var(--theme-accent); | ||
| 1829 | + border-bottom-color: var(--theme-accent); | ||
| 1830 | + } | ||
| 1831 | + | ||
| 1832 | + /* Entidades Section */ | ||
| 1833 | + .seccion-subtitulo { | ||
| 1834 | + font-size: 0.875rem; | ||
| 1835 | + color: var(--theme-texto); | ||
| 1836 | + margin: -1rem 0 1.5rem; | ||
| 1837 | + } | ||
| 1838 | + | ||
| 1839 | + .entidad-info { | ||
| 1840 | + margin-bottom: 1.5rem; | ||
| 1841 | + display: flex; | ||
| 1842 | + flex-direction: column; | ||
| 1843 | + gap: 0.15rem; | ||
| 1844 | + } | ||
| 1845 | + | ||
| 1846 | + .entidad-pct { | ||
| 1847 | + font-family: 'Qanelas', var(--font-sans); | ||
| 1848 | + font-size: 2.5rem; | ||
| 1849 | + font-weight: 600; | ||
| 1850 | + color: var(--theme-titulo); | ||
| 1851 | + line-height: 1; | ||
| 1852 | + } | ||
| 1853 | + | ||
| 1854 | + .entidad-nombre { | ||
| 1855 | + font-size: 1rem; | ||
| 1856 | + font-weight: 500; | ||
| 1857 | + color: var(--theme-titulo); | ||
| 1858 | + line-height: 1.2; | ||
| 1859 | + margin-top: 0.25rem; | ||
| 1860 | + } | ||
| 1861 | + | ||
| 1862 | + .entidad-area { | ||
| 1863 | + font-size: 0.7rem; | ||
| 1864 | + color: var(--theme-texto); | ||
| 1865 | + opacity: 0.5; | ||
| 1866 | + text-transform: uppercase; | ||
| 1867 | + letter-spacing: 0.02em; | ||
| 1868 | + line-height: 1.2; | ||
| 1869 | + } | ||
| 1870 | + | ||
| 1871 | + .entidades-bar-container { | ||
| 1872 | + width: 100%; | ||
| 1873 | + overflow: hidden; | ||
| 1874 | + border-radius: 4px; | ||
| 1875 | + } | ||
| 1876 | + | ||
| 1877 | + .entidades-filtros { | ||
| 1878 | + margin-bottom: 1.5rem; | ||
| 1879 | + } | ||
| 1880 | + | ||
| 1881 | + .rango-control { | ||
| 1882 | + display: flex; | ||
| 1883 | + flex-direction: column; | ||
| 1884 | + gap: 0.75rem; | ||
| 1885 | + max-width: 400px; | ||
| 1886 | + } | ||
| 1887 | + | ||
| 1888 | + .rango-label { | ||
| 1889 | + font-size: 0.75rem; | ||
| 1890 | + color: var(--theme-texto); | ||
| 1891 | + font-variant-numeric: tabular-nums; | ||
| 1892 | + } | ||
| 1893 | + | ||
| 1894 | + .dual-slider { | ||
| 1895 | + position: relative; | ||
| 1896 | + height: 18px; | ||
| 1897 | + width: 100%; | ||
| 1898 | + } | ||
| 1899 | + | ||
| 1900 | + .slider-track { | ||
| 1901 | + position: absolute; | ||
| 1902 | + top: 50%; | ||
| 1903 | + transform: translateY(-50%); | ||
| 1904 | + width: 100%; | ||
| 1905 | + height: 3px; | ||
| 1906 | + background: var(--theme-borde); | ||
| 1907 | + border-radius: 2px; | ||
| 1908 | + } | ||
| 1909 | + | ||
| 1910 | + .slider-range { | ||
| 1911 | + position: absolute; | ||
| 1912 | + top: 50%; | ||
| 1913 | + transform: translateY(-50%); | ||
| 1914 | + height: 3px; | ||
| 1915 | + background: var(--theme-titulo); | ||
| 1916 | + border-radius: 2px; | ||
| 1917 | + cursor: grab; | ||
| 1918 | + z-index: 1; | ||
| 1919 | + } | ||
| 1920 | + | ||
| 1921 | + .slider-range:active { | ||
| 1922 | + cursor: grabbing; | ||
| 1923 | + } | ||
| 1924 | + | ||
| 1925 | + .slider-thumb { | ||
| 1926 | + position: absolute; | ||
| 1927 | + top: 0; | ||
| 1928 | + width: 100%; | ||
| 1929 | + height: 100%; | ||
| 1930 | + -webkit-appearance: none; | ||
| 1931 | + appearance: none; | ||
| 1932 | + background: transparent; | ||
| 1933 | + pointer-events: none; | ||
| 1934 | + margin: 0; | ||
| 1935 | + } | ||
| 1936 | + | ||
| 1937 | + .slider-thumb::-webkit-slider-thumb { | ||
| 1938 | + -webkit-appearance: none; | ||
| 1939 | + appearance: none; | ||
| 1940 | + width: 12px; | ||
| 1941 | + height: 12px; | ||
| 1942 | + background: var(--theme-titulo); | ||
| 1943 | + border: none; | ||
| 1944 | + border-radius: 50%; | ||
| 1945 | + cursor: pointer; | ||
| 1946 | + pointer-events: auto; | ||
| 1947 | + box-shadow: 0 1px 3px rgba(0,0,0,0.15); | ||
| 1948 | + transition: transform 0.15s; | ||
| 1949 | + } | ||
| 1950 | + | ||
| 1951 | + .slider-thumb::-webkit-slider-thumb:hover { | ||
| 1952 | + transform: scale(1.15); | ||
| 1953 | + } | ||
| 1954 | + | ||
| 1955 | + .slider-thumb::-moz-range-thumb { | ||
| 1956 | + width: 12px; | ||
| 1957 | + height: 12px; | ||
| 1958 | + background: var(--theme-titulo); | ||
| 1959 | + border: none; | ||
| 1960 | + border-radius: 50%; | ||
| 1961 | + cursor: pointer; | ||
| 1962 | + pointer-events: auto; | ||
| 1963 | + box-shadow: 0 1px 3px rgba(0,0,0,0.15); | ||
| 1964 | + } | ||
| 1965 | + | ||
| 1966 | + .entidades-paginacion { | ||
| 1967 | + display: flex; | ||
| 1968 | + align-items: center; | ||
| 1969 | + justify-content: space-between; | ||
| 1970 | + margin-top: 1rem; | ||
| 1971 | + gap: 1rem; | ||
| 1972 | + flex-wrap: wrap; | ||
| 1973 | + } | ||
| 1974 | + | ||
| 1975 | + .entidades-botones { | ||
| 1976 | + display: flex; | ||
| 1977 | + gap: 0.5rem; | ||
| 1978 | + } | ||
| 1979 | + | ||
| 1980 | + .entidades-nota { | ||
| 1981 | + font-size: 0.75rem; | ||
| 1982 | + color: var(--theme-texto); | ||
| 1983 | + opacity: 0.6; | ||
| 1984 | + } | ||
| 1985 | + | ||
| 1986 | + .entidades-ver-mas { | ||
| 1987 | + padding: 0.5rem 1rem; | ||
| 1988 | + background: transparent; | ||
| 1989 | + border: 1px solid var(--theme-borde); | ||
| 1990 | + border-radius: 6px; | ||
| 1991 | + color: var(--theme-texto); | ||
| 1992 | + font-size: 0.8rem; | ||
| 1993 | + font-family: inherit; | ||
| 1994 | + cursor: pointer; | ||
| 1995 | + transition: all 0.2s; | ||
| 1996 | + } | ||
| 1997 | + | ||
| 1998 | + .entidades-ver-mas:hover { | ||
| 1999 | + background: var(--theme-fill); | ||
| 2000 | + border-color: var(--theme-accent); | ||
| 2001 | + color: var(--theme-titulo); | ||
| 2002 | + } | ||
| 2003 | + | ||
| 2004 | + /* Charts Grid - Sección 4 */ | ||
| 2005 | + .charts-grid { | ||
| 2006 | + display: grid; | ||
| 2007 | + grid-template-columns: 1fr 1fr; | ||
| 2008 | + gap: 2rem; | ||
| 2009 | + margin-bottom: 1rem; | ||
| 2010 | + } | ||
| 2011 | + | ||
| 2012 | + .chart-card { | ||
| 2013 | + background: var(--theme-fill); | ||
| 2014 | + border: 1px solid var(--theme-borde); | ||
| 2015 | + border-radius: 8px; | ||
| 2016 | + padding: 1.25rem; | ||
| 2017 | + } | ||
| 2018 | + | ||
| 2019 | + :global(.dark) .chart-card { | ||
| 2020 | + background: var(--theme-surface); | ||
| 2021 | + } | ||
| 2022 | + | ||
| 2023 | + .chart-header { | ||
| 2024 | + display: flex; | ||
| 2025 | + justify-content: space-between; | ||
| 2026 | + align-items: baseline; | ||
| 2027 | + margin-bottom: 1rem; | ||
| 2028 | + } | ||
| 2029 | + | ||
| 2030 | + .chart-label { | ||
| 2031 | + font-size: 0.8rem; | ||
| 2032 | + color: var(--theme-texto); | ||
| 2033 | + } | ||
| 2034 | + | ||
| 2035 | + .chart-value { | ||
| 2036 | + font-family: 'Qanelas', var(--font-sans); | ||
| 2037 | + font-size: 1.25rem; | ||
| 2038 | + font-weight: 600; | ||
| 2039 | + color: var(--theme-titulo); | ||
| 2040 | + } | ||
| 2041 | + | ||
| 2042 | + .chart-container { | ||
| 2043 | + width: 100%; | ||
| 2044 | + height: 220px; | ||
| 2045 | + } | ||
| 2046 | + | ||
| 2047 | + /* Tipografía y colores para los gráficos Observable Plot */ | ||
| 2048 | + .chart-container :global(svg) { | ||
| 2049 | + font-family: 'Qanelas', var(--font-sans), system-ui, sans-serif; | ||
| 2050 | + } | ||
| 2051 | + | ||
| 2052 | + .chart-container :global(svg text) { | ||
| 2053 | + font-family: 'Qanelas', var(--font-sans), system-ui, sans-serif; | ||
| 2054 | + fill: var(--theme-texto); | ||
| 2055 | + } | ||
| 2056 | + | ||
| 2057 | + .chart-container :global(svg [aria-label="y-grid"] line) { | ||
| 2058 | + stroke: rgba(0, 0, 0, 0.1); | ||
| 2059 | + } | ||
| 2060 | + | ||
| 2061 | + :global(.dark) .chart-container :global(svg [aria-label="y-grid"] line) { | ||
| 2062 | + stroke: rgba(255, 255, 255, 0.1); | ||
| 2063 | + } | ||
| 2064 | + | ||
| 2065 | + .chart-container :global(svg [aria-label="x-axis"] line), | ||
| 2066 | + .chart-container :global(svg [aria-label="y-axis"] line) { | ||
| 2067 | + stroke: var(--theme-borde); | ||
| 2068 | + } | ||
| 2069 | + | ||
| 2070 | + /* Responsive */ | ||
| 2071 | + @media (max-width: 900px) { | ||
| 2072 | + .nav-interna { | ||
| 2073 | + right: 0.75rem; | ||
| 2074 | + } | ||
| 2075 | + | ||
| 2076 | + .nav-label { | ||
| 2077 | + display: none; | ||
| 2078 | + } | ||
| 2079 | + } | ||
| 2080 | + | ||
| 2081 | + @media (max-width: 768px) { | ||
| 2082 | + .vista-diaria { | ||
| 2083 | + scroll-snap-type: y proximity; | ||
| 2084 | + } | ||
| 2085 | + | ||
| 2086 | + .vista-seccion { | ||
| 2087 | + scroll-snap-stop: normal; | ||
| 2088 | + } | ||
| 2089 | + | ||
| 2090 | + .vista-contenido { | ||
| 2091 | + padding: 1rem; | ||
| 2092 | + } | ||
| 2093 | + | ||
| 2094 | + .seccion-titulo-grande { | ||
| 2095 | + font-size: 1.5rem; | ||
| 2096 | + } | ||
| 2097 | + | ||
| 2098 | + .clasificador-header { | ||
| 2099 | + flex-direction: column; | ||
| 2100 | + align-items: flex-start; | ||
| 2101 | + gap: 0.25rem; | ||
| 2102 | + } | ||
| 2103 | + | ||
| 2104 | + .clasificador-tipo { | ||
| 2105 | + order: -1; | ||
| 2106 | + } | ||
| 2107 | + | ||
| 2108 | + .periodo-btn { | ||
| 2109 | + padding: 0.4rem 0.75rem; | ||
| 2110 | + font-size: 0.75rem; | ||
| 2111 | + } | ||
| 2112 | + | ||
| 2113 | + .entidad-pct { | ||
| 2114 | + font-size: 2rem; | ||
| 2115 | + } | ||
| 2116 | + | ||
| 2117 | + .charts-grid { | ||
| 2118 | + grid-template-columns: 1fr; | ||
| 2119 | + gap: 1rem; | ||
| 2120 | + } | ||
| 2121 | + | ||
| 2122 | + .chart-container { | ||
| 2123 | + height: 140px; | ||
| 2124 | + } | ||
| 2125 | + | ||
| 2126 | + .timeline-cards { | ||
| 2127 | + max-width: 100%; | ||
| 2128 | + } | ||
| 2129 | + } | ||
| 2130 | +</style> |
static/ejecucion_clasificadores@3.parquet
0 → 100644
No preview for this file type
static/ejecucion_entidades@1.parquet
0 → 100644
No preview for this file type
static/timeline@1.parquet
0 → 100644
No preview for this file type
static/timeserie@4.parquet
0 → 100644
No preview for this file type
-
Please register or login to post a comment