Showing
5 changed files
with
2907 additions
and
188 deletions
src/lib/components/ui/Spinner.svelte
0 → 100644
| 1 | +<script> | ||
| 2 | + let { size = 40, color = 'currentColor' } = $props(); | ||
| 3 | +</script> | ||
| 4 | + | ||
| 5 | +<svg | ||
| 6 | + fill={color} | ||
| 7 | + viewBox="0 -10 40 28" | ||
| 8 | + xmlns="http://www.w3.org/2000/svg" | ||
| 9 | + width={size} | ||
| 10 | + height={size * 0.7} | ||
| 11 | + class="spinner" | ||
| 12 | +> | ||
| 13 | + <circle cx="8" cy="12" r="3" class="first"/> | ||
| 14 | + <circle cx="16" cy="12" r="3"/> | ||
| 15 | + <circle cx="24" cy="12" r="3"/> | ||
| 16 | + <circle cx="32" cy="12" r="3" class="second"/> | ||
| 17 | +</svg> | ||
| 18 | + | ||
| 19 | +<style> | ||
| 20 | + @keyframes swing { | ||
| 21 | + 0% { | ||
| 22 | + transform: rotate(0deg); | ||
| 23 | + animation-timing-function: ease-out; | ||
| 24 | + } | ||
| 25 | + 25% { | ||
| 26 | + transform: rotate(50deg); | ||
| 27 | + animation-timing-function: ease-in; | ||
| 28 | + } | ||
| 29 | + 50% { | ||
| 30 | + transform: rotate(0deg); | ||
| 31 | + animation-timing-function: linear; | ||
| 32 | + } | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + @keyframes swing2 { | ||
| 36 | + 0% { | ||
| 37 | + transform: rotate(0deg); | ||
| 38 | + animation-timing-function: linear; | ||
| 39 | + } | ||
| 40 | + 50% { | ||
| 41 | + transform: rotate(0deg); | ||
| 42 | + animation-timing-function: ease-out; | ||
| 43 | + } | ||
| 44 | + 75% { | ||
| 45 | + transform: rotate(-50deg); | ||
| 46 | + animation-timing-function: ease-in; | ||
| 47 | + } | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + .spinner .first { | ||
| 51 | + animation: swing 1.2s linear infinite; | ||
| 52 | + transform-origin: center top; | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + .spinner .second { | ||
| 56 | + animation: swing2 1.2s linear infinite; | ||
| 57 | + transform-origin: center top; | ||
| 58 | + } | ||
| 59 | +</style> |
src/lib/stores/clasificadorCache.js
0 → 100644
| 1 | +/** | ||
| 2 | + * Cache store for clasificador data | ||
| 3 | + * Persists data in memory to avoid reloading on navigation | ||
| 4 | + */ | ||
| 5 | +import { writable } from 'svelte/store'; | ||
| 6 | + | ||
| 7 | +// Cache structure | ||
| 8 | +const createClasificadorCache = () => { | ||
| 9 | + const { subscribe, set, update } = writable({ | ||
| 10 | + objetoGasto: { | ||
| 11 | + allItems: null, | ||
| 12 | + grupos: null, | ||
| 13 | + entities: null, | ||
| 14 | + availableYears: null, | ||
| 15 | + lastFetched: null | ||
| 16 | + } | ||
| 17 | + }); | ||
| 18 | + | ||
| 19 | + return { | ||
| 20 | + subscribe, | ||
| 21 | + | ||
| 22 | + // Set all clasificador objeto data | ||
| 23 | + setObjetoGasto: (data) => { | ||
| 24 | + update(cache => ({ | ||
| 25 | + ...cache, | ||
| 26 | + objetoGasto: { | ||
| 27 | + ...data, | ||
| 28 | + lastFetched: Date.now() | ||
| 29 | + } | ||
| 30 | + })); | ||
| 31 | + }, | ||
| 32 | + | ||
| 33 | + // Get cached data if valid (less than 5 minutes old) | ||
| 34 | + getObjetoGasto: (currentCache) => { | ||
| 35 | + const cache = currentCache.objetoGasto; | ||
| 36 | + if (!cache.lastFetched) return null; | ||
| 37 | + | ||
| 38 | + // Cache valid for 5 minutes | ||
| 39 | + const isValid = Date.now() - cache.lastFetched < 5 * 60 * 1000; | ||
| 40 | + if (!isValid) return null; | ||
| 41 | + | ||
| 42 | + return cache; | ||
| 43 | + }, | ||
| 44 | + | ||
| 45 | + // Clear cache | ||
| 46 | + clear: () => { | ||
| 47 | + set({ | ||
| 48 | + objetoGasto: { | ||
| 49 | + allItems: null, | ||
| 50 | + grupos: null, | ||
| 51 | + entities: null, | ||
| 52 | + availableYears: null, | ||
| 53 | + lastFetched: null | ||
| 54 | + } | ||
| 55 | + }); | ||
| 56 | + } | ||
| 57 | + }; | ||
| 58 | +}; | ||
| 59 | + | ||
| 60 | +export const clasificadorCache = createClasificadorCache(); |
| ... | @@ -4,6 +4,8 @@ | ... | @@ -4,6 +4,8 @@ |
| 4 | import { page } from '$app/stores'; | 4 | import { page } from '$app/stores'; |
| 5 | import { goto } from '$app/navigation'; | 5 | import { goto } from '$app/navigation'; |
| 6 | import * as d3 from 'd3'; | 6 | import * as d3 from 'd3'; |
| 7 | + import { clasificadorCache } from '$lib/stores/clasificadorCache'; | ||
| 8 | + import Spinner from '$lib/components/ui/Spinner.svelte'; | ||
| 7 | 9 | ||
| 8 | let loading = $state(true); | 10 | let loading = $state(true); |
| 9 | let searchQuery = $state(''); | 11 | let searchQuery = $state(''); |
| ... | @@ -37,6 +39,11 @@ | ... | @@ -37,6 +39,11 @@ |
| 37 | let levelDropdownOpen = $state(false); | 39 | let levelDropdownOpen = $state(false); |
| 38 | let yearDropdownOpen = $state(false); | 40 | let yearDropdownOpen = $state(false); |
| 39 | 41 | ||
| 42 | + // Límites dinámicos para scroll infinito en dropdowns | ||
| 43 | + let entityLimit = $state(30); | ||
| 44 | + let compareALimit = $state(30); | ||
| 45 | + let compareBLimit = $state(30); | ||
| 46 | + | ||
| 40 | // Estado para modo comparar | 47 | // Estado para modo comparar |
| 41 | let compareA = $state({ year: null, entity: null }); | 48 | let compareA = $state({ year: null, entity: null }); |
| 42 | let compareB = $state({ year: null, entity: null }); | 49 | let compareB = $state({ year: null, entity: null }); |
| ... | @@ -57,6 +64,7 @@ | ... | @@ -57,6 +64,7 @@ |
| 57 | let treemapHeight = $state(500); | 64 | let treemapHeight = $state(500); |
| 58 | let treemapContainer = $state(null); | 65 | let treemapContainer = $state(null); |
| 59 | let hoveredNode = $state(null); | 66 | let hoveredNode = $state(null); |
| 67 | + let showTreemapHelp = $state(false); | ||
| 60 | 68 | ||
| 61 | // Selector de nivel para vista aplanada | 69 | // Selector de nivel para vista aplanada |
| 62 | let treemapViewLevel = $state('partida'); // 'jerarquico' | 'subgrupo' | 'partida' | 'subpartida' | 70 | let treemapViewLevel = $state('partida'); // 'jerarquico' | 'subgrupo' | 'partida' | 'subpartida' |
| ... | @@ -513,6 +521,30 @@ | ... | @@ -513,6 +521,30 @@ |
| 513 | }); | 521 | }); |
| 514 | 522 | ||
| 515 | onMount(async () => { | 523 | onMount(async () => { |
| 524 | + // Intentar usar cache primero | ||
| 525 | + let cacheValue; | ||
| 526 | + const unsubscribe = clasificadorCache.subscribe(value => { cacheValue = value; }); | ||
| 527 | + unsubscribe(); | ||
| 528 | + | ||
| 529 | + const cachedData = clasificadorCache.getObjetoGasto(cacheValue); | ||
| 530 | + | ||
| 531 | + if (cachedData && cachedData.allItems && cachedData.grupos && cachedData.entities && cachedData.availableYears) { | ||
| 532 | + // Usar datos del cache | ||
| 533 | + allItems = cachedData.allItems; | ||
| 534 | + grupos = cachedData.grupos; | ||
| 535 | + entities = cachedData.entities; | ||
| 536 | + availableYears = cachedData.availableYears; | ||
| 537 | + selectedYear = availableYears[0]; | ||
| 538 | + | ||
| 539 | + if (grupos.length > 0) { | ||
| 540 | + selectedGrupo = grupos[0]; | ||
| 541 | + } | ||
| 542 | + | ||
| 543 | + loading = false; | ||
| 544 | + return; | ||
| 545 | + } | ||
| 546 | + | ||
| 547 | + // Si no hay cache, cargar datos | ||
| 516 | // Cargar clasificador de objetos | 548 | // Cargar clasificador de objetos |
| 517 | const { data, error } = await supabase | 549 | const { data, error } = await supabase |
| 518 | .schema('ppto') | 550 | .schema('ppto') |
| ... | @@ -565,13 +597,21 @@ | ... | @@ -565,13 +597,21 @@ |
| 565 | entities = entidadesData; | 597 | entities = entidadesData; |
| 566 | } | 598 | } |
| 567 | 599 | ||
| 600 | + // Guardar en cache | ||
| 601 | + clasificadorCache.setObjetoGasto({ | ||
| 602 | + allItems, | ||
| 603 | + grupos, | ||
| 604 | + entities, | ||
| 605 | + availableYears | ||
| 606 | + }); | ||
| 607 | + | ||
| 568 | loading = false; | 608 | loading = false; |
| 569 | }); | 609 | }); |
| 570 | 610 | ||
| 571 | // Filtrar entidades por búsqueda | 611 | // Filtrar entidades por búsqueda |
| 572 | let filteredEntities = $derived(() => { | 612 | let filteredEntities = $derived(() => { |
| 573 | if (!entitySearchQuery || entitySearchQuery.length < 2) { | 613 | if (!entitySearchQuery || entitySearchQuery.length < 2) { |
| 574 | - return entities.slice(0, 20); // Mostrar primeras 20 si no hay búsqueda | 614 | + return entities.slice(0, entityLimit); |
| 575 | } | 615 | } |
| 576 | const q = entitySearchQuery.toLowerCase(); | 616 | const q = entitySearchQuery.toLowerCase(); |
| 577 | return entities | 617 | return entities |
| ... | @@ -580,25 +620,42 @@ | ... | @@ -580,25 +620,42 @@ |
| 580 | e.sigla_entidad?.toLowerCase().includes(q) || | 620 | e.sigla_entidad?.toLowerCase().includes(q) || |
| 581 | e.entidad?.toString().includes(q) | 621 | e.entidad?.toString().includes(q) |
| 582 | ) | 622 | ) |
| 583 | - .slice(0, 20); | 623 | + .slice(0, entityLimit); |
| 584 | }); | 624 | }); |
| 585 | 625 | ||
| 586 | function selectEntity(entity) { | 626 | function selectEntity(entity) { |
| 587 | selectedEntity = entity; | 627 | selectedEntity = entity; |
| 588 | entityDropdownOpen = false; | 628 | entityDropdownOpen = false; |
| 589 | entitySearchQuery = ''; | 629 | entitySearchQuery = ''; |
| 630 | + entityLimit = 30; // Reset limit | ||
| 590 | } | 631 | } |
| 591 | 632 | ||
| 592 | function clearEntity() { | 633 | function clearEntity() { |
| 593 | selectedEntity = null; | 634 | selectedEntity = null; |
| 594 | entityDropdownOpen = false; | 635 | entityDropdownOpen = false; |
| 595 | entitySearchQuery = ''; | 636 | entitySearchQuery = ''; |
| 637 | + entityLimit = 30; | ||
| 638 | + } | ||
| 639 | + | ||
| 640 | + // Handler para cargar más entidades al hacer scroll | ||
| 641 | + function handleEntityScroll(event, type) { | ||
| 642 | + const el = event.target; | ||
| 643 | + const threshold = 50; // pixels from bottom | ||
| 644 | + if (el.scrollHeight - el.scrollTop - el.clientHeight < threshold) { | ||
| 645 | + if (type === 'main' && entityLimit < entities.length) { | ||
| 646 | + entityLimit = Math.min(entityLimit + 30, entities.length); | ||
| 647 | + } else if (type === 'A' && compareALimit < entities.length) { | ||
| 648 | + compareALimit = Math.min(compareALimit + 30, entities.length); | ||
| 649 | + } else if (type === 'B' && compareBLimit < entities.length) { | ||
| 650 | + compareBLimit = Math.min(compareBLimit + 30, entities.length); | ||
| 651 | + } | ||
| 652 | + } | ||
| 596 | } | 653 | } |
| 597 | 654 | ||
| 598 | // Filtrar entidades para comparación A | 655 | // Filtrar entidades para comparación A |
| 599 | let filteredEntitiesA = $derived(() => { | 656 | let filteredEntitiesA = $derived(() => { |
| 600 | if (!compareASearchQuery || compareASearchQuery.length < 2) { | 657 | if (!compareASearchQuery || compareASearchQuery.length < 2) { |
| 601 | - return entities.slice(0, 20); | 658 | + return entities.slice(0, compareALimit); |
| 602 | } | 659 | } |
| 603 | const q = compareASearchQuery.toLowerCase(); | 660 | const q = compareASearchQuery.toLowerCase(); |
| 604 | return entities | 661 | return entities |
| ... | @@ -607,13 +664,13 @@ | ... | @@ -607,13 +664,13 @@ |
| 607 | e.sigla_entidad?.toLowerCase().includes(q) || | 664 | e.sigla_entidad?.toLowerCase().includes(q) || |
| 608 | e.entidad?.toString().includes(q) | 665 | e.entidad?.toString().includes(q) |
| 609 | ) | 666 | ) |
| 610 | - .slice(0, 20); | 667 | + .slice(0, compareALimit); |
| 611 | }); | 668 | }); |
| 612 | 669 | ||
| 613 | // Filtrar entidades para comparación B | 670 | // Filtrar entidades para comparación B |
| 614 | let filteredEntitiesB = $derived(() => { | 671 | let filteredEntitiesB = $derived(() => { |
| 615 | if (!compareBSearchQuery || compareBSearchQuery.length < 2) { | 672 | if (!compareBSearchQuery || compareBSearchQuery.length < 2) { |
| 616 | - return entities.slice(0, 20); | 673 | + return entities.slice(0, compareBLimit); |
| 617 | } | 674 | } |
| 618 | const q = compareBSearchQuery.toLowerCase(); | 675 | const q = compareBSearchQuery.toLowerCase(); |
| 619 | return entities | 676 | return entities |
| ... | @@ -622,7 +679,7 @@ | ... | @@ -622,7 +679,7 @@ |
| 622 | e.sigla_entidad?.toLowerCase().includes(q) || | 679 | e.sigla_entidad?.toLowerCase().includes(q) || |
| 623 | e.entidad?.toString().includes(q) | 680 | e.entidad?.toString().includes(q) |
| 624 | ) | 681 | ) |
| 625 | - .slice(0, 20); | 682 | + .slice(0, compareBLimit); |
| 626 | }); | 683 | }); |
| 627 | 684 | ||
| 628 | // Inicializar modo comparar con valores inteligentes | 685 | // Inicializar modo comparar con valores inteligentes |
| ... | @@ -704,6 +761,7 @@ | ... | @@ -704,6 +761,7 @@ |
| 704 | .sort((a, b) => a.objeto.localeCompare(b.objeto)); | 761 | .sort((a, b) => a.objeto.localeCompare(b.objeto)); |
| 705 | 762 | ||
| 706 | const subgrupos = subgruposUnicos.map(sg => { | 763 | const subgrupos = subgruposUnicos.map(sg => { |
| 764 | + // Obtener partidas existentes | ||
| 707 | const partidasUnicas = allItems | 765 | const partidasUnicas = allItems |
| 708 | .filter(item => item.nivel === 'partida' && item.grupo == grupoNum && item.subgrupo == sg.subgrupo) | 766 | .filter(item => item.nivel === 'partida' && item.grupo == grupoNum && item.subgrupo == sg.subgrupo) |
| 709 | .reduce((acc, item) => { | 767 | .reduce((acc, item) => { |
| ... | @@ -714,21 +772,62 @@ | ... | @@ -714,21 +772,62 @@ |
| 714 | }, []) | 772 | }, []) |
| 715 | .sort((a, b) => a.objeto.localeCompare(b.objeto)); | 773 | .sort((a, b) => a.objeto.localeCompare(b.objeto)); |
| 716 | 774 | ||
| 775 | + // Obtener todas las subpartidas del subgrupo | ||
| 776 | + const todasSubpartidas = allItems | ||
| 777 | + .filter(item => item.nivel === 'subpartida' && item.grupo == grupoNum && item.subgrupo == sg.subgrupo) | ||
| 778 | + .reduce((acc, item) => { | ||
| 779 | + if (!acc.find(sp => sp.objeto === item.objeto)) { | ||
| 780 | + acc.push(item); | ||
| 781 | + } | ||
| 782 | + return acc; | ||
| 783 | + }, []) | ||
| 784 | + .sort((a, b) => a.objeto.localeCompare(b.objeto)); | ||
| 785 | + | ||
| 786 | + // Set de números de partida que existen | ||
| 787 | + const partidasExistentes = new Set(partidasUnicas.map(p => p.partida)); | ||
| 788 | + | ||
| 789 | + // Encontrar subpartidas huérfanas (cuya partida no existe) | ||
| 790 | + const subpartidasHuerfanas = todasSubpartidas.filter(sp => !partidasExistentes.has(sp.partida)); | ||
| 791 | + | ||
| 792 | + // Agrupar huérfanas por número de partida para crear partidas sintéticas | ||
| 793 | + const huerfanasPorPartida = {}; | ||
| 794 | + subpartidasHuerfanas.forEach(sp => { | ||
| 795 | + if (!huerfanasPorPartida[sp.partida]) { | ||
| 796 | + huerfanasPorPartida[sp.partida] = []; | ||
| 797 | + } | ||
| 798 | + huerfanasPorPartida[sp.partida].push(sp); | ||
| 799 | + }); | ||
| 800 | + | ||
| 801 | + // Crear partidas sintéticas para las huérfanas | ||
| 802 | + const partidasSinteticas = Object.entries(huerfanasPorPartida).map(([partidaNum, subpartidas]) => { | ||
| 803 | + // Generar código de partida (ej: grupo=3, subgrupo=4, partida=1 -> 34100) | ||
| 804 | + const codigoPartida = `${grupoNum}${sg.subgrupo}${partidaNum}00`; | ||
| 805 | + return { | ||
| 806 | + objeto: codigoPartida, | ||
| 807 | + desc_objeto: subpartidas[0]?.desc_objeto?.split(',')[0] || `Partida ${codigoPartida}`, | ||
| 808 | + nivel: 'partida', | ||
| 809 | + grupo: parseInt(grupoNum), | ||
| 810 | + subgrupo: sg.subgrupo, | ||
| 811 | + partida: parseInt(partidaNum), | ||
| 812 | + _sintetica: true, // Marcar como sintética | ||
| 813 | + subpartidas | ||
| 814 | + }; | ||
| 815 | + }); | ||
| 816 | + | ||
| 817 | + // Asignar subpartidas a partidas existentes | ||
| 717 | const partidas = partidasUnicas.map(p => { | 818 | const partidas = partidasUnicas.map(p => { |
| 718 | - const subpartidas = allItems | 819 | + const subpartidas = todasSubpartidas |
| 719 | - .filter(item => item.nivel === 'subpartida' && item.grupo == grupoNum && item.subgrupo == sg.subgrupo && item.partida == p.partida) | 820 | + .filter(item => item.partida == p.partida) |
| 720 | - .reduce((acc, item) => { | ||
| 721 | - if (!acc.find(sp => sp.objeto === item.objeto)) { | ||
| 722 | - acc.push(item); | ||
| 723 | - } | ||
| 724 | - return acc; | ||
| 725 | - }, []) | ||
| 726 | .sort((a, b) => a.objeto.localeCompare(b.objeto)); | 821 | .sort((a, b) => a.objeto.localeCompare(b.objeto)); |
| 727 | 822 | ||
| 728 | return { ...p, subpartidas }; | 823 | return { ...p, subpartidas }; |
| 729 | }); | 824 | }); |
| 730 | 825 | ||
| 731 | - return { ...sg, partidas }; | 826 | + // Combinar partidas existentes con sintéticas y ordenar |
| 827 | + const todasPartidas = [...partidas, ...partidasSinteticas] | ||
| 828 | + .sort((a, b) => a.objeto.localeCompare(b.objeto)); | ||
| 829 | + | ||
| 830 | + return { ...sg, partidas: todasPartidas }; | ||
| 732 | }); | 831 | }); |
| 733 | 832 | ||
| 734 | return { subgrupos }; | 833 | return { subgrupos }; |
| ... | @@ -794,23 +893,36 @@ | ... | @@ -794,23 +893,36 @@ |
| 794 | } | 893 | } |
| 795 | 894 | ||
| 796 | function goToSearchResult(item) { | 895 | function goToSearchResult(item) { |
| 797 | - // Encontrar el grupo correspondiente | 896 | + // Si es un grupo, simplemente seleccionarlo |
| 897 | + if (item.nivel === 'grupo') { | ||
| 898 | + selectGrupo(item); | ||
| 899 | + return; | ||
| 900 | + } | ||
| 901 | + | ||
| 902 | + // Para otros niveles, encontrar y seleccionar el grupo padre | ||
| 798 | const grupoNum = item.grupo; | 903 | const grupoNum = item.grupo; |
| 799 | - const targetGrupo = grupos.find(g => g.objeto.startsWith(grupoNum)); | 904 | + const targetGrupo = grupos.find(g => g.objeto === grupoNum + '0000'); |
| 800 | 905 | ||
| 801 | if (targetGrupo) { | 906 | if (targetGrupo) { |
| 907 | + // Seleccionar el grupo primero | ||
| 802 | selectedGrupo = targetGrupo; | 908 | selectedGrupo = targetGrupo; |
| 803 | highlightedItem = item.objeto; | 909 | highlightedItem = item.objeto; |
| 804 | searchQuery = ''; | 910 | searchQuery = ''; |
| 911 | + sidebarOpen = false; | ||
| 805 | 912 | ||
| 806 | - // Scroll al elemento después de un breve delay | 913 | + // Esperar a que el contenido se renderice y luego hacer scroll |
| 807 | setTimeout(() => { | 914 | setTimeout(() => { |
| 808 | const prefix = item.nivel === 'subgrupo' ? 'sg' : item.nivel === 'partida' ? 'p' : 'sp'; | 915 | const prefix = item.nivel === 'subgrupo' ? 'sg' : item.nivel === 'partida' ? 'p' : 'sp'; |
| 809 | const element = document.getElementById(`${prefix}-${item.objeto}`); | 916 | const element = document.getElementById(`${prefix}-${item.objeto}`); |
| 810 | if (element) { | 917 | if (element) { |
| 811 | element.scrollIntoView({ behavior: 'smooth', block: 'center' }); | 918 | element.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| 812 | } | 919 | } |
| 813 | - }, 100); | 920 | + |
| 921 | + // Quitar el highlight después de unos segundos | ||
| 922 | + setTimeout(() => { | ||
| 923 | + highlightedItem = null; | ||
| 924 | + }, 3000); | ||
| 925 | + }, 150); | ||
| 814 | } | 926 | } |
| 815 | } | 927 | } |
| 816 | 928 | ||
| ... | @@ -1072,7 +1184,7 @@ | ... | @@ -1072,7 +1184,7 @@ |
| 1072 | <span class="font-medium">Todo el Estado</span> | 1184 | <span class="font-medium">Todo el Estado</span> |
| 1073 | <span class="text-xs ml-2" style="color: var(--theme-texto);">(agregado nacional)</span> | 1185 | <span class="text-xs ml-2" style="color: var(--theme-texto);">(agregado nacional)</span> |
| 1074 | </button> | 1186 | </button> |
| 1075 | - <div class="overflow-y-auto max-h-48"> | 1187 | + <div class="overflow-y-auto max-h-64" onscroll={(e) => handleEntityScroll(e, 'main')}> |
| 1076 | {#each filteredEntities() as entity} | 1188 | {#each filteredEntities() as entity} |
| 1077 | <button | 1189 | <button |
| 1078 | class="w-full text-left px-3 py-2 text-sm" | 1190 | class="w-full text-left px-3 py-2 text-sm" |
| ... | @@ -1088,6 +1200,11 @@ | ... | @@ -1088,6 +1200,11 @@ |
| 1088 | </div> | 1200 | </div> |
| 1089 | </button> | 1201 | </button> |
| 1090 | {/each} | 1202 | {/each} |
| 1203 | + {#if entityLimit < entities.length} | ||
| 1204 | + <div class="text-center py-2 text-xs" style="color: var(--theme-texto);"> | ||
| 1205 | + Scroll para ver más ({entities.length - entityLimit} restantes) | ||
| 1206 | + </div> | ||
| 1207 | + {/if} | ||
| 1091 | </div> | 1208 | </div> |
| 1092 | </div> | 1209 | </div> |
| 1093 | {/if} | 1210 | {/if} |
| ... | @@ -1133,7 +1250,7 @@ | ... | @@ -1133,7 +1250,7 @@ |
| 1133 | 1250 | ||
| 1134 | {#if loading} | 1251 | {#if loading} |
| 1135 | <div class="flex items-center justify-center py-20"> | 1252 | <div class="flex items-center justify-center py-20"> |
| 1136 | - <p style="color: var(--theme-texto);">Cargando clasificador...</p> | 1253 | + <Spinner size={48} color="var(--theme-texto)" /> |
| 1137 | </div> | 1254 | </div> |
| 1138 | {:else} | 1255 | {:else} |
| 1139 | {#if viewMode === 'lista'} | 1256 | {#if viewMode === 'lista'} |
| ... | @@ -1207,13 +1324,17 @@ | ... | @@ -1207,13 +1324,17 @@ |
| 1207 | <div class="space-y-1"> | 1324 | <div class="space-y-1"> |
| 1208 | {#each searchResults as result} | 1325 | {#each searchResults as result} |
| 1209 | <button | 1326 | <button |
| 1210 | - class="w-full text-left px-2 py-2 text-sm rounded transition-all" | 1327 | + class="search-result-item w-full text-left px-3 py-3 text-sm rounded-lg transition-all cursor-pointer" |
| 1211 | - style="color: var(--theme-titulo);" | ||
| 1212 | onclick={() => goToSearchResult(result)} | 1328 | onclick={() => goToSearchResult(result)} |
| 1213 | > | 1329 | > |
| 1214 | - <span class="text-xs block" style="color: var(--theme-texto);">{getNivelLabel(result.nivel)}</span> | 1330 | + <span class="text-xs block mb-0.5" style="color: var(--theme-texto);">{getNivelLabel(result.nivel)}</span> |
| 1215 | - <span class="font-mono text-xs font-medium" style="color: var(--theme-accent);">{result.objeto}</span> | 1331 | + <div class="flex items-baseline gap-2"> |
| 1216 | - <span class="ml-1">{result.desc_objeto}</span> | 1332 | + <span class="font-mono text-xs font-medium" style="color: var(--theme-accent);">{result.objeto}</span> |
| 1333 | + <span class="flex-1" style="color: var(--theme-titulo);">{result.desc_objeto}</span> | ||
| 1334 | + <svg class="w-4 h-4 flex-shrink-0 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 1335 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> | ||
| 1336 | + </svg> | ||
| 1337 | + </div> | ||
| 1217 | </button> | 1338 | </button> |
| 1218 | {/each} | 1339 | {/each} |
| 1219 | {#if searchResults.length === 0} | 1340 | {#if searchResults.length === 0} |
| ... | @@ -1333,10 +1454,14 @@ | ... | @@ -1333,10 +1454,14 @@ |
| 1333 | style="{highlightedItem === partida.objeto ? `box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-accent) 30%, transparent); border-radius: 0.5rem; padding: 0.5rem; margin-left: -0.5rem;` : ''}" | 1454 | style="{highlightedItem === partida.objeto ? `box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-accent) 30%, transparent); border-radius: 0.5rem; padding: 0.5rem; margin-left: -0.5rem;` : ''}" |
| 1334 | > | 1455 | > |
| 1335 | <div class="flex items-start gap-2 sm:gap-3"> | 1456 | <div class="flex items-start gap-2 sm:gap-3"> |
| 1336 | - <span class="font-mono text-xs pt-0.5 shrink-0" style="color: var(--theme-texto);">{partida.objeto}</span> | 1457 | + <span class="font-mono text-xs pt-0.5 shrink-0" style="color: var(--theme-texto); {partida._sintetica ? 'opacity: 0.5;' : ''}">{partida.objeto}</span> |
| 1337 | <div class="flex-1 min-w-0"> | 1458 | <div class="flex-1 min-w-0"> |
| 1338 | <h4 class="text-sm font-medium flex items-center gap-2 flex-wrap" style="color: var(--theme-titulo);"> | 1459 | <h4 class="text-sm font-medium flex items-center gap-2 flex-wrap" style="color: var(--theme-titulo);"> |
| 1339 | - <span class="text-left">{partida.desc_objeto}</span> | 1460 | + {#if partida._sintetica} |
| 1461 | + <span class="text-left opacity-60 italic">(Partida inferida)</span> | ||
| 1462 | + {:else} | ||
| 1463 | + <span class="text-left">{partida.desc_objeto}</span> | ||
| 1464 | + {/if} | ||
| 1340 | {#if partida.n_variaciones > 1} | 1465 | {#if partida.n_variaciones > 1} |
| 1341 | <button | 1466 | <button |
| 1342 | 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" | 1467 | 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" |
| ... | @@ -1346,18 +1471,20 @@ | ... | @@ -1346,18 +1471,20 @@ |
| 1346 | {partida.n_variaciones} var. | 1471 | {partida.n_variaciones} var. |
| 1347 | </button> | 1472 | </button> |
| 1348 | {/if} | 1473 | {/if} |
| 1349 | - <a | 1474 | + {#if !partida._sintetica} |
| 1350 | - href="/objeto/{partida.objeto}" | 1475 | + <a |
| 1351 | - class="transition-colors hover:text-[var(--theme-accent)]" | 1476 | + href="/objeto/{partida.objeto}" |
| 1352 | - style="color: var(--theme-texto);" | 1477 | + class="transition-colors hover:text-[var(--theme-accent)]" |
| 1353 | - title="Ver página de detalle" | 1478 | + style="color: var(--theme-texto);" |
| 1354 | - > | 1479 | + title="Ver página de detalle" |
| 1355 | - <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | 1480 | + > |
| 1356 | - <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" /> | 1481 | + <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 1357 | - </svg> | 1482 | + <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" /> |
| 1358 | - </a> | 1483 | + </svg> |
| 1484 | + </a> | ||
| 1485 | + {/if} | ||
| 1359 | </h4> | 1486 | </h4> |
| 1360 | - {#if partidaDescs.length > 0} | 1487 | + {#if partidaDescs.length > 0 && !partida._sintetica} |
| 1361 | <p class="text-sm mt-1 leading-relaxed" style="color: var(--theme-texto);">{partidaDescs[0].descripcion}</p> | 1488 | <p class="text-sm mt-1 leading-relaxed" style="color: var(--theme-texto);">{partidaDescs[0].descripcion}</p> |
| 1362 | {/if} | 1489 | {/if} |
| 1363 | </div> | 1490 | </div> |
| ... | @@ -1466,35 +1593,53 @@ | ... | @@ -1466,35 +1593,53 @@ |
| 1466 | 1593 | ||
| 1467 | <div class="max-w-screen-xl mx-auto px-4 sm:px-6 pt-3 pb-4"> | 1594 | <div class="max-w-screen-xl mx-auto px-4 sm:px-6 pt-3 pb-4"> |
| 1468 | 1595 | ||
| 1469 | - <!-- Breadcrumb de navegación (solo en modo jerárquico) --> | 1596 | + <!-- Header con breadcrumb y botón de ayuda --> |
| 1470 | - {#if treemapViewLevel === 'jerarquico' && treemapBreadcrumb.length > 0} | 1597 | + <div class="flex items-center justify-between mb-2"> |
| 1471 | - <nav class="flex items-center gap-2 mb-2 flex-wrap" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;"> | 1598 | + <!-- Breadcrumb de navegación (solo en modo jerárquico) --> |
| 1472 | - {#each treemapBreadcrumb as crumb, i} | 1599 | + {#if treemapViewLevel === 'jerarquico' && treemapBreadcrumb.length > 0} |
| 1473 | - {#if i > 0} | 1600 | + <nav class="flex items-center gap-2 flex-wrap" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;"> |
| 1474 | - <span style="color: var(--theme-texto); opacity: 0.5;">›</span> | 1601 | + {#each treemapBreadcrumb as crumb, i} |
| 1475 | - {/if} | 1602 | + {#if i > 0} |
| 1476 | - <button | 1603 | + <span style="color: var(--theme-texto); opacity: 0.5;">›</span> |
| 1477 | - onclick={() => drillUp(i)} | 1604 | + {/if} |
| 1478 | - class="px-2 py-1 rounded transition-colors" | 1605 | + <button |
| 1479 | - style="color: {i === treemapBreadcrumb.length - 1 ? 'var(--theme-titulo)' : 'var(--theme-texto)'}; background-color: {i === treemapBreadcrumb.length - 1 ? 'var(--theme-fill)' : 'transparent'}; cursor: pointer;" | 1606 | + onclick={() => drillUp(i)} |
| 1480 | - onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--theme-fill)'} | 1607 | + class="px-2 py-1 rounded transition-colors" |
| 1481 | - onmouseleave={(e) => e.currentTarget.style.backgroundColor = i === treemapBreadcrumb.length - 1 ? 'var(--theme-fill)' : 'transparent'} | 1608 | + style="color: {i === treemapBreadcrumb.length - 1 ? 'var(--theme-titulo)' : 'var(--theme-texto)'}; background-color: {i === treemapBreadcrumb.length - 1 ? 'var(--theme-fill)' : 'transparent'}; cursor: pointer;" |
| 1482 | - > | 1609 | + onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--theme-fill)'} |
| 1483 | - {crumb.name} | 1610 | + onmouseleave={(e) => e.currentTarget.style.backgroundColor = i === treemapBreadcrumb.length - 1 ? 'var(--theme-fill)' : 'transparent'} |
| 1484 | - </button> | 1611 | + > |
| 1485 | - {/each} | 1612 | + {crumb.name} |
| 1486 | - </nav> | 1613 | + </button> |
| 1487 | - {:else if treemapViewLevel !== 'jerarquico'} | 1614 | + {/each} |
| 1488 | - <!-- Info de nivel aplanado --> | 1615 | + </nav> |
| 1489 | - <div class="mb-2 flex items-center gap-3" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;"> | 1616 | + {:else if treemapViewLevel !== 'jerarquico'} |
| 1490 | - <span class="px-2 py-1 rounded" style="background-color: var(--theme-fill); color: var(--theme-titulo);"> | 1617 | + <!-- Info de nivel aplanado --> |
| 1491 | - {NIVEL_OPTIONS.find(o => o.value === treemapViewLevel)?.label || treemapViewLevel} | 1618 | + <div class="flex items-center gap-3" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;"> |
| 1492 | - </span> | 1619 | + <span class="px-2 py-1 rounded" style="background-color: var(--theme-fill); color: var(--theme-titulo);"> |
| 1493 | - <span style="color: var(--theme-texto);"> | 1620 | + {NIVEL_OPTIONS.find(o => o.value === treemapViewLevel)?.label || treemapViewLevel} |
| 1494 | - {treemapNodes.filter(n => n.type === 'item').length} categorías en {treemapNodes.filter(n => n.type === 'grupo').length} grupos | 1621 | + </span> |
| 1495 | - </span> | 1622 | + <span style="color: var(--theme-texto);"> |
| 1496 | - </div> | 1623 | + {treemapNodes.filter(n => n.type === 'item').length} categorías en {treemapNodes.filter(n => n.type === 'grupo').length} grupos |
| 1497 | - {/if} | 1624 | + </span> |
| 1625 | + </div> | ||
| 1626 | + {:else} | ||
| 1627 | + <div></div> | ||
| 1628 | + {/if} | ||
| 1629 | + | ||
| 1630 | + <!-- Botón de ayuda --> | ||
| 1631 | + <button | ||
| 1632 | + onclick={() => showTreemapHelp = true} | ||
| 1633 | + class="flex items-center gap-1.5 px-2 py-1 rounded-lg text-xs transition-colors" | ||
| 1634 | + style="color: var(--theme-texto); background-color: var(--theme-fill);" | ||
| 1635 | + title="Cómo leer este gráfico" | ||
| 1636 | + > | ||
| 1637 | + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 1638 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | ||
| 1639 | + </svg> | ||
| 1640 | + <span class="hidden sm:inline">Cómo leer</span> | ||
| 1641 | + </button> | ||
| 1642 | + </div> | ||
| 1498 | 1643 | ||
| 1499 | <!-- Contenedor del Treemap --> | 1644 | <!-- Contenedor del Treemap --> |
| 1500 | <div | 1645 | <div |
| ... | @@ -1782,7 +1927,7 @@ | ... | @@ -1782,7 +1927,7 @@ |
| 1782 | > | 1927 | > |
| 1783 | <span class="font-medium">Todo el Estado</span> | 1928 | <span class="font-medium">Todo el Estado</span> |
| 1784 | </button> | 1929 | </button> |
| 1785 | - <div class="overflow-y-auto max-h-48"> | 1930 | + <div class="overflow-y-auto max-h-64" onscroll={(e) => handleEntityScroll(e, 'A')}> |
| 1786 | {#each filteredEntitiesA() as entity} | 1931 | {#each filteredEntitiesA() as entity} |
| 1787 | <button | 1932 | <button |
| 1788 | class="w-full text-left px-3 py-2 text-sm" | 1933 | class="w-full text-left px-3 py-2 text-sm" |
| ... | @@ -1795,6 +1940,11 @@ | ... | @@ -1795,6 +1940,11 @@ |
| 1795 | </div> | 1940 | </div> |
| 1796 | </button> | 1941 | </button> |
| 1797 | {/each} | 1942 | {/each} |
| 1943 | + {#if compareALimit < entities.length} | ||
| 1944 | + <div class="text-center py-2 text-xs" style="color: var(--theme-texto);"> | ||
| 1945 | + Scroll para ver más | ||
| 1946 | + </div> | ||
| 1947 | + {/if} | ||
| 1798 | </div> | 1948 | </div> |
| 1799 | </div> | 1949 | </div> |
| 1800 | {/if} | 1950 | {/if} |
| ... | @@ -1880,7 +2030,7 @@ | ... | @@ -1880,7 +2030,7 @@ |
| 1880 | > | 2030 | > |
| 1881 | <span class="font-medium">Todo el Estado</span> | 2031 | <span class="font-medium">Todo el Estado</span> |
| 1882 | </button> | 2032 | </button> |
| 1883 | - <div class="overflow-y-auto max-h-48"> | 2033 | + <div class="overflow-y-auto max-h-64" onscroll={(e) => handleEntityScroll(e, 'B')}> |
| 1884 | {#each filteredEntitiesB() as entity} | 2034 | {#each filteredEntitiesB() as entity} |
| 1885 | <button | 2035 | <button |
| 1886 | class="w-full text-left px-3 py-2 text-sm" | 2036 | class="w-full text-left px-3 py-2 text-sm" |
| ... | @@ -1893,6 +2043,11 @@ | ... | @@ -1893,6 +2043,11 @@ |
| 1893 | </div> | 2043 | </div> |
| 1894 | </button> | 2044 | </button> |
| 1895 | {/each} | 2045 | {/each} |
| 2046 | + {#if compareBLimit < entities.length} | ||
| 2047 | + <div class="text-center py-2 text-xs" style="color: var(--theme-texto);"> | ||
| 2048 | + Scroll para ver más | ||
| 2049 | + </div> | ||
| 2050 | + {/if} | ||
| 1896 | </div> | 2051 | </div> |
| 1897 | </div> | 2052 | </div> |
| 1898 | {/if} | 2053 | {/if} |
| ... | @@ -1992,6 +2147,109 @@ | ... | @@ -1992,6 +2147,109 @@ |
| 1992 | </div> | 2147 | </div> |
| 1993 | {/if} | 2148 | {/if} |
| 1994 | 2149 | ||
| 2150 | +<!-- Modal de ayuda para el Treemap --> | ||
| 2151 | +{#if showTreemapHelp} | ||
| 2152 | + <div | ||
| 2153 | + class="fixed inset-0 z-50 flex items-center justify-center p-4" | ||
| 2154 | + onclick={() => showTreemapHelp = false} | ||
| 2155 | + role="dialog" | ||
| 2156 | + aria-modal="true" | ||
| 2157 | + > | ||
| 2158 | + <!-- Overlay --> | ||
| 2159 | + <div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div> | ||
| 2160 | + | ||
| 2161 | + <!-- Modal content --> | ||
| 2162 | + <div | ||
| 2163 | + class="relative w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden" | ||
| 2164 | + style="background-color: var(--theme-body);" | ||
| 2165 | + onclick={(e) => e.stopPropagation()} | ||
| 2166 | + > | ||
| 2167 | + <!-- Header --> | ||
| 2168 | + <div class="flex items-center justify-between px-6 py-4 border-b" style="border-color: var(--theme-borde);"> | ||
| 2169 | + <div class="flex items-center gap-3"> | ||
| 2170 | + <div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background-color: var(--theme-fill);"> | ||
| 2171 | + <svg class="w-5 h-5" style="color: var(--theme-accent);" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 2172 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> | ||
| 2173 | + </svg> | ||
| 2174 | + </div> | ||
| 2175 | + <h3 class="text-lg font-semibold" style="color: var(--theme-titulo);">Cómo leer este gráfico</h3> | ||
| 2176 | + </div> | ||
| 2177 | + <button | ||
| 2178 | + onclick={() => showTreemapHelp = false} | ||
| 2179 | + class="p-2 rounded-lg transition-colors" | ||
| 2180 | + style="color: var(--theme-texto);" | ||
| 2181 | + > | ||
| 2182 | + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 2183 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | ||
| 2184 | + </svg> | ||
| 2185 | + </button> | ||
| 2186 | + </div> | ||
| 2187 | + | ||
| 2188 | + <!-- Body --> | ||
| 2189 | + <div class="px-6 py-5 space-y-4" style="color: var(--theme-texto);"> | ||
| 2190 | + <div class="flex gap-4"> | ||
| 2191 | + <div class="w-12 h-12 rounded-lg flex-shrink-0 flex items-center justify-center" style="background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent);"> | ||
| 2192 | + <svg class="w-6 h-6" style="color: var(--theme-accent);" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 2193 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" /> | ||
| 2194 | + </svg> | ||
| 2195 | + </div> | ||
| 2196 | + <div> | ||
| 2197 | + <h4 class="font-medium mb-1" style="color: var(--theme-titulo);">Tamaño = Proporción del gasto</h4> | ||
| 2198 | + <p class="text-sm leading-relaxed">Cada rectángulo representa una categoría de gasto. Cuanto más grande es el rectángulo, mayor es el monto gastado en esa categoría.</p> | ||
| 2199 | + </div> | ||
| 2200 | + </div> | ||
| 2201 | + | ||
| 2202 | + <div class="flex gap-4"> | ||
| 2203 | + <div class="w-12 h-12 rounded-lg flex-shrink-0 flex items-center justify-center" style="background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent);"> | ||
| 2204 | + <svg class="w-6 h-6" style="color: var(--theme-accent);" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 2205 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /> | ||
| 2206 | + </svg> | ||
| 2207 | + </div> | ||
| 2208 | + <div> | ||
| 2209 | + <h4 class="font-medium mb-1" style="color: var(--theme-titulo);">Colores por grupo</h4> | ||
| 2210 | + <p class="text-sm leading-relaxed">Cada color representa un grupo principal del clasificador. Las categorías del mismo grupo comparten el mismo color.</p> | ||
| 2211 | + </div> | ||
| 2212 | + </div> | ||
| 2213 | + | ||
| 2214 | + <div class="flex gap-4"> | ||
| 2215 | + <div class="w-12 h-12 rounded-lg flex-shrink-0 flex items-center justify-center" style="background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent);"> | ||
| 2216 | + <svg class="w-6 h-6" style="color: var(--theme-accent);" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 2217 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" /> | ||
| 2218 | + </svg> | ||
| 2219 | + </div> | ||
| 2220 | + <div> | ||
| 2221 | + <h4 class="font-medium mb-1" style="color: var(--theme-titulo);">Hover para ver detalle</h4> | ||
| 2222 | + <p class="text-sm leading-relaxed">Pasa el cursor sobre cualquier rectángulo para ver el nombre de la categoría, el monto devengado y su porcentaje respecto al total.</p> | ||
| 2223 | + </div> | ||
| 2224 | + </div> | ||
| 2225 | + | ||
| 2226 | + <div class="flex gap-4"> | ||
| 2227 | + <div class="w-12 h-12 rounded-lg flex-shrink-0 flex items-center justify-center" style="background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent);"> | ||
| 2228 | + <svg class="w-6 h-6" style="color: var(--theme-accent);" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| 2229 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" /> | ||
| 2230 | + </svg> | ||
| 2231 | + </div> | ||
| 2232 | + <div> | ||
| 2233 | + <h4 class="font-medium mb-1" style="color: var(--theme-titulo);">Explorar (modo jerárquico)</h4> | ||
| 2234 | + <p class="text-sm leading-relaxed">Haz clic en <span class="inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold mx-1" style="background-color: var(--theme-fill); border: 1px solid var(--theme-borde);">+</span> para entrar en un grupo y ver sus subcategorías en detalle.</p> | ||
| 2235 | + </div> | ||
| 2236 | + </div> | ||
| 2237 | + </div> | ||
| 2238 | + | ||
| 2239 | + <!-- Footer --> | ||
| 2240 | + <div class="px-6 py-4 border-t" style="border-color: var(--theme-borde); background-color: var(--theme-fill);"> | ||
| 2241 | + <button | ||
| 2242 | + onclick={() => showTreemapHelp = false} | ||
| 2243 | + class="w-full py-2.5 rounded-lg font-medium transition-colors" | ||
| 2244 | + style="background-color: var(--theme-accent); color: white;" | ||
| 2245 | + > | ||
| 2246 | + Entendido | ||
| 2247 | + </button> | ||
| 2248 | + </div> | ||
| 2249 | + </div> | ||
| 2250 | + </div> | ||
| 2251 | +{/if} | ||
| 2252 | + | ||
| 1995 | <style> | 2253 | <style> |
| 1996 | /* Dropdown controls - tema oscuro por defecto */ | 2254 | /* Dropdown controls - tema oscuro por defecto */ |
| 1997 | .dropdown-control { | 2255 | .dropdown-control { |
| ... | @@ -2051,4 +2309,33 @@ | ... | @@ -2051,4 +2309,33 @@ |
| 2051 | opacity: 0; | 2309 | opacity: 0; |
| 2052 | transform: scale(0.95); | 2310 | transform: scale(0.95); |
| 2053 | } | 2311 | } |
| 2312 | + | ||
| 2313 | + /* Search result items - más clickeables */ | ||
| 2314 | + .search-result-item { | ||
| 2315 | + background: var(--theme-surface); | ||
| 2316 | + border: 1px solid var(--theme-borde); | ||
| 2317 | + color: var(--theme-titulo); | ||
| 2318 | + } | ||
| 2319 | + .search-result-item:hover { | ||
| 2320 | + background: var(--theme-fill); | ||
| 2321 | + border-color: var(--theme-accent); | ||
| 2322 | + transform: translateX(4px); | ||
| 2323 | + } | ||
| 2324 | + .search-result-item:active { | ||
| 2325 | + transform: translateX(2px); | ||
| 2326 | + opacity: 0.9; | ||
| 2327 | + } | ||
| 2328 | + | ||
| 2329 | + /* Tema claro */ | ||
| 2330 | + :global(.light) .search-result-item, | ||
| 2331 | + :global(html:not(.dark)) .search-result-item { | ||
| 2332 | + background: transparent; | ||
| 2333 | + border: none; | ||
| 2334 | + box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06); | ||
| 2335 | + } | ||
| 2336 | + :global(.light) .search-result-item:hover, | ||
| 2337 | + :global(html:not(.dark)) .search-result-item:hover { | ||
| 2338 | + background: rgba(0,0,0,0.03); | ||
| 2339 | + box-shadow: inset 0 0 0 1px var(--theme-accent); | ||
| 2340 | + } | ||
| 2054 | </style> | 2341 | </style> | ... | ... |
| 1 | <script> | 1 | <script> |
| 2 | + import { onMount } from 'svelte'; | ||
| 3 | + import { tweened } from 'svelte/motion'; | ||
| 4 | + import { cubicOut } from 'svelte/easing'; | ||
| 5 | + import { supabase } from '$lib/supabase'; | ||
| 6 | + import Spinner from '$lib/components/ui/Spinner.svelte'; | ||
| 7 | + | ||
| 8 | + // Datos del servidor (reactivos) | ||
| 2 | let { data } = $props(); | 9 | let { data } = $props(); |
| 10 | + let objetoData = $derived(data.objeto); | ||
| 11 | + let padres = $derived(data.padres); | ||
| 12 | + let hijos = $derived(data.hijos); | ||
| 13 | + | ||
| 14 | + // Código y nivel del objeto desde datos del servidor (reactivos) | ||
| 15 | + let objetoCodigo = $derived(objetoData.objeto); | ||
| 16 | + let objetoNivel = $derived(objetoData.nivel); | ||
| 3 | 17 | ||
| 4 | - const objeto = data.objeto; | 18 | + // ══════════════════════════════════════════════════════════════ |
| 5 | - const padres = data.padres; | 19 | + // ESTADO GENERAL |
| 6 | - const hijos = data.hijos; | 20 | + // ══════════════════════════════════════════════════════════════ |
| 21 | + let mounted = $state(false); | ||
| 22 | + let loading = $state(true); | ||
| 23 | + let hoveredYear = $state(null); | ||
| 24 | + let showInfo = $state(false); | ||
| 25 | + let vista = $state('agregado'); // 'agregado' | 'comparar' | ||
| 26 | + let isDark = $state(true); | ||
| 7 | 27 | ||
| 28 | + // Parsear descripciones del objeto | ||
| 8 | function parseDescripciones(descripcionesStr) { | 29 | function parseDescripciones(descripcionesStr) { |
| 9 | if (!descripcionesStr) return []; | 30 | if (!descripcionesStr) return []; |
| 10 | try { | 31 | try { |
| 11 | const parsed = JSON.parse(descripcionesStr); | 32 | const parsed = JSON.parse(descripcionesStr); |
| 12 | - // Ordenar por año más reciente (extraer el máximo año de cada rango) | ||
| 13 | return parsed.sort((a, b) => { | 33 | return parsed.sort((a, b) => { |
| 14 | const maxYearA = getMaxYear(a.rangos); | 34 | const maxYearA = getMaxYear(a.rangos); |
| 15 | const maxYearB = getMaxYear(b.rangos); | 35 | const maxYearB = getMaxYear(b.rangos); |
| 16 | - return maxYearB - maxYearA; // Descendente, más reciente primero | 36 | + return maxYearB - maxYearA; |
| 17 | }); | 37 | }); |
| 18 | } catch { | 38 | } catch { |
| 19 | return []; | 39 | return []; |
| ... | @@ -32,138 +52,2430 @@ | ... | @@ -32,138 +52,2430 @@ |
| 32 | return labels[nivel] || nivel; | 52 | return labels[nivel] || nivel; |
| 33 | } | 53 | } |
| 34 | 54 | ||
| 35 | - const descripciones = parseDescripciones(objeto.descripciones); | 55 | + let descripciones = $derived(parseDescripciones(objetoData.descripciones)); |
| 56 | + | ||
| 57 | + // Info del objeto (reactivo) | ||
| 58 | + let objeto = $derived({ | ||
| 59 | + codigo: objetoData.objeto, | ||
| 60 | + nombre: objetoData.desc_objeto, | ||
| 61 | + nivel: getNivelLabel(objetoData.nivel), | ||
| 62 | + años: objetoData.gestiones || '', | ||
| 63 | + descripcion: descripciones.length > 0 ? descripciones[0].descripcion : '', | ||
| 64 | + variaciones: descripciones.map(d => ({ rango: d.rangos, texto: d.descripcion })) | ||
| 65 | + }); | ||
| 66 | + | ||
| 67 | + // ══════════════════════════════════════════════════════════════ | ||
| 68 | + // ESTADO SELECTOR DE ENTIDAD | ||
| 69 | + // ══════════════════════════════════════════════════════════════ | ||
| 70 | + let entidades = $state([]); // Lista de entidades disponibles | ||
| 71 | + let selectedEntity = $state(null); // null = Todo el Estado | ||
| 72 | + let entitySearchQuery = $state(''); | ||
| 73 | + let entityDropdownOpen = $state(false); | ||
| 74 | + | ||
| 75 | + // Límites dinámicos para scroll infinito | ||
| 76 | + let entityLimit = $state(30); | ||
| 77 | + let entityLimitA = $state(30); | ||
| 78 | + let entityLimitB = $state(30); | ||
| 79 | + | ||
| 80 | + // Filtrar entidades por búsqueda | ||
| 81 | + let filteredEntities = $derived.by(() => { | ||
| 82 | + if (!entitySearchQuery || entitySearchQuery.length < 2) { | ||
| 83 | + return entidades.slice(0, entityLimit); | ||
| 84 | + } | ||
| 85 | + const q = entitySearchQuery.toLowerCase(); | ||
| 86 | + return entidades | ||
| 87 | + .filter(e => | ||
| 88 | + e.entidad_desc?.toLowerCase().includes(q) || | ||
| 89 | + e.entidad?.toString().includes(q) | ||
| 90 | + ) | ||
| 91 | + .slice(0, entityLimit); | ||
| 92 | + }); | ||
| 93 | + | ||
| 94 | + // Handler para cargar más entidades al hacer scroll | ||
| 95 | + function handleEntityScroll(event, type) { | ||
| 96 | + const el = event.target; | ||
| 97 | + const threshold = 50; | ||
| 98 | + if (el.scrollHeight - el.scrollTop - el.clientHeight < threshold) { | ||
| 99 | + if (type === 'main' && entityLimit < entidades.length) { | ||
| 100 | + entityLimit = Math.min(entityLimit + 30, entidades.length); | ||
| 101 | + } else if (type === 'A' && entityLimitA < entidades.length) { | ||
| 102 | + entityLimitA = Math.min(entityLimitA + 30, entidades.length); | ||
| 103 | + } else if (type === 'B' && entityLimitB < entidades.length) { | ||
| 104 | + entityLimitB = Math.min(entityLimitB + 30, entidades.length); | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + function selectEntity(entity) { | ||
| 110 | + selectedEntity = entity; | ||
| 111 | + entityDropdownOpen = false; | ||
| 112 | + entitySearchQuery = ''; | ||
| 113 | + entityLimit = 30; | ||
| 114 | + loadData(); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + function clearEntity() { | ||
| 118 | + selectedEntity = null; | ||
| 119 | + entityDropdownOpen = false; | ||
| 120 | + entitySearchQuery = ''; | ||
| 121 | + entityLimit = 30; | ||
| 122 | + loadData(); | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + // ══════════════════════════════════════════════════════════════ | ||
| 126 | + // DATOS DE SUPABASE | ||
| 127 | + // ══════════════════════════════════════════════════════════════ | ||
| 128 | + let datosAnuales = $state([]); // Datos por año | ||
| 129 | + let datosHistorico = $state(null); // Fila con gestion=0 | ||
| 130 | + let topEntidadesPorAño = $state({}); // Top 3 por año (solo para Todo el Estado) | ||
| 131 | + | ||
| 132 | + // Cargar datos según selección | ||
| 133 | + async function loadData() { | ||
| 134 | + loading = true; | ||
| 135 | + | ||
| 136 | + if (selectedEntity) { | ||
| 137 | + // Cargar desde vista_objeto_entidad | ||
| 138 | + const { data, error } = await supabase | ||
| 139 | + .schema('ppto') | ||
| 140 | + .from('vista_objeto_entidad') | ||
| 141 | + .select('*') | ||
| 142 | + .eq('objeto', objetoCodigo) | ||
| 143 | + .eq('nivel', objetoNivel) | ||
| 144 | + .eq('entidad', selectedEntity.entidad) | ||
| 145 | + .order('gestion'); | ||
| 146 | + | ||
| 147 | + if (data) { | ||
| 148 | + datosHistorico = data.find(d => d.gestion === 0) || null; | ||
| 149 | + datosAnuales = data.filter(d => d.gestion > 0); | ||
| 150 | + topEntidadesPorAño = {}; // No hay top entidades cuando se filtra por entidad | ||
| 151 | + } | ||
| 152 | + } else { | ||
| 153 | + // Cargar desde vista_objeto_estado (Todo el Estado) | ||
| 154 | + const { data, error } = await supabase | ||
| 155 | + .schema('ppto') | ||
| 156 | + .from('vista_objeto_estado') | ||
| 157 | + .select('*') | ||
| 158 | + .eq('objeto', objetoCodigo) | ||
| 159 | + .order('gestion'); | ||
| 160 | + | ||
| 161 | + if (data) { | ||
| 162 | + datosHistorico = data.find(d => d.gestion === 0) || null; | ||
| 163 | + datosAnuales = data.filter(d => d.gestion > 0); | ||
| 164 | + | ||
| 165 | + // Construir top entidades por año | ||
| 166 | + topEntidadesPorAño = {}; | ||
| 167 | + datosAnuales.forEach(d => { | ||
| 168 | + topEntidadesPorAño[d.gestion] = [ | ||
| 169 | + { nombre: d.top1_entidad, monto: d.top1_monto, porcentaje: d.top1_pct }, | ||
| 170 | + { nombre: d.top2_entidad, monto: d.top2_monto, porcentaje: d.top2_pct }, | ||
| 171 | + { nombre: d.top3_entidad, monto: d.top3_monto, porcentaje: d.top3_pct }, | ||
| 172 | + ].filter(e => e.nombre); | ||
| 173 | + }); | ||
| 174 | + } | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + loading = false; | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + // Cargar lista de entidades disponibles para este objeto | ||
| 181 | + async function loadEntidades() { | ||
| 182 | + const { data, error } = await supabase | ||
| 183 | + .schema('ppto') | ||
| 184 | + .from('entidades_por_objeto') | ||
| 185 | + .select('entidad, entidad_desc') | ||
| 186 | + .eq('objeto', objetoCodigo) | ||
| 187 | + .eq('nivel', objetoNivel) | ||
| 188 | + .order('entidad_desc'); | ||
| 189 | + | ||
| 190 | + if (data) { | ||
| 191 | + entidades = data; | ||
| 192 | + } | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + // ══════════════════════════════════════════════════════════════ | ||
| 196 | + // VALORES DERIVADOS | ||
| 197 | + // ══════════════════════════════════════════════════════════════ | ||
| 198 | + | ||
| 199 | + // Datos para el gráfico (monto o per_capita según preferencia) | ||
| 200 | + let gastoPerCapita = $derived( | ||
| 201 | + datosAnuales.map(d => ({ | ||
| 202 | + año: d.gestion, | ||
| 203 | + monto: selectedEntity ? d.monto : d.total, | ||
| 204 | + perCapita: d.per_capita | ||
| 205 | + })) | ||
| 206 | + ); | ||
| 207 | + | ||
| 208 | + let maxPerCapita = $derived( | ||
| 209 | + gastoPerCapita.length > 0 ? Math.max(...gastoPerCapita.map(g => g.perCapita)) : 1 | ||
| 210 | + ); | ||
| 211 | + | ||
| 212 | + // Histórico | ||
| 213 | + let totalHistorico = $derived(datosHistorico ? (selectedEntity ? datosHistorico.monto : datosHistorico.total) : 0); | ||
| 214 | + let promedioPerCapita = $derived(datosHistorico?.per_capita || 0); | ||
| 215 | + let totalPorcentaje = $derived(datosHistorico?.prop || 0); | ||
| 216 | + let ranking = $derived(datosHistorico?.ranking || 0); | ||
| 217 | + let nPares = $derived(datosHistorico?.n_pares || 0); | ||
| 218 | + let nEntidades = $derived(datosHistorico?.n_entidades || 0); | ||
| 219 | + // Número de entidades - cambia según el año seleccionado y la entidad seleccionada | ||
| 220 | + let totalEntidadesHistorico = $derived.by(() => { | ||
| 221 | + if (selectedEntity && datosAnuales.length > 0) { | ||
| 222 | + // Para entidad seleccionada: promedio de n_entidades de los años que tiene esta entidad | ||
| 223 | + // Esto refleja cuántas entidades participaron durante el período de vida de esta entidad | ||
| 224 | + const suma = datosAnuales.reduce((acc, d) => acc + (d.n_entidades || 0), 0); | ||
| 225 | + return Math.round(suma / datosAnuales.length); | ||
| 226 | + } | ||
| 227 | + // Para "Todo el Estado": usar la lista del dropdown o el histórico | ||
| 228 | + return entidades.length || nEntidades; | ||
| 229 | + }); | ||
| 230 | + | ||
| 231 | + let totalEntidades = $derived.by(() => { | ||
| 232 | + if (hoveredYear) { | ||
| 233 | + // Buscar n_entidades del año específico | ||
| 234 | + const añoData = datosAnuales.find(d => d.gestion === hoveredYear); | ||
| 235 | + return añoData?.n_entidades || totalEntidadesHistorico; | ||
| 236 | + } | ||
| 237 | + return totalEntidadesHistorico; | ||
| 238 | + }); | ||
| 239 | + | ||
| 240 | + // Para el ranking: valores reactivos según hover | ||
| 241 | + let displayRankingValue = $derived.by(() => { | ||
| 242 | + if (hoveredYear) { | ||
| 243 | + const añoData = datosAnuales.find(d => d.gestion === hoveredYear); | ||
| 244 | + return añoData?.ranking || ranking; | ||
| 245 | + } | ||
| 246 | + return ranking; | ||
| 247 | + }); | ||
| 248 | + | ||
| 249 | + let rankingDenominador = $derived.by(() => { | ||
| 250 | + if (selectedEntity) { | ||
| 251 | + // Para entidad: usar n_entidades del año específico o del histórico | ||
| 252 | + if (hoveredYear) { | ||
| 253 | + const añoData = datosAnuales.find(d => d.gestion === hoveredYear); | ||
| 254 | + return añoData?.n_entidades || nEntidades; | ||
| 255 | + } | ||
| 256 | + // Sin hover: usar el n_entidades del histórico (total de entidades únicas del período) | ||
| 257 | + return nEntidades; | ||
| 258 | + } | ||
| 259 | + // Para "Todo el Estado": comparación entre objetos del mismo nivel | ||
| 260 | + return nPares; | ||
| 261 | + }); | ||
| 262 | + | ||
| 263 | + // Top entidades histórico (solo para Todo el Estado) | ||
| 264 | + let topEntidadesTotal = $derived( | ||
| 265 | + datosHistorico && !selectedEntity ? [ | ||
| 266 | + { nombre: datosHistorico.top1_entidad, monto: datosHistorico.top1_monto, porcentaje: datosHistorico.top1_pct }, | ||
| 267 | + { nombre: datosHistorico.top2_entidad, monto: datosHistorico.top2_monto, porcentaje: datosHistorico.top2_pct }, | ||
| 268 | + { nombre: datosHistorico.top3_entidad, monto: datosHistorico.top3_monto, porcentaje: datosHistorico.top3_pct }, | ||
| 269 | + ].filter(e => e.nombre) : [] | ||
| 270 | + ); | ||
| 271 | + | ||
| 272 | + // Años disponibles | ||
| 273 | + let primerAño = $derived(gastoPerCapita.length > 0 ? gastoPerCapita[0].año : 2005); | ||
| 274 | + let ultimoAño = $derived(gastoPerCapita.length > 0 ? gastoPerCapita[gastoPerCapita.length - 1].año : 2025); | ||
| 275 | + | ||
| 276 | + // Current view data (changes on hover) | ||
| 277 | + let currentData = $derived( | ||
| 278 | + hoveredYear ? gastoPerCapita.find(g => g.año === hoveredYear) : null | ||
| 279 | + ); | ||
| 280 | + | ||
| 281 | + let displayMonto = $derived(currentData ? currentData.monto : totalHistorico); | ||
| 282 | + let displayMontoLabel = $derived(currentData ? `gasto en ${currentData.año}` : `gasto ${primerAño}-${ultimoAño}`); | ||
| 283 | + let displayPerCapita = $derived(currentData ? currentData.perCapita : promedioPerCapita); | ||
| 284 | + let displayPerCapitaLabel = $derived(currentData ? `por persona en ${currentData.año}` : 'por persona (promedio)'); | ||
| 285 | + let displayPorcentaje = $derived( | ||
| 286 | + currentData && totalHistorico > 0 | ||
| 287 | + ? (totalPorcentaje * (currentData.monto / (totalHistorico / gastoPerCapita.length))).toFixed(1) | ||
| 288 | + : totalPorcentaje | ||
| 289 | + ); | ||
| 290 | + let displayPorcentajeLabel = $derived(currentData ? `del gasto ${currentData.año}` : 'del gasto total'); | ||
| 291 | + let displayPeriodo = $derived(currentData ? currentData.año : `${primerAño}-${ultimoAño}`); | ||
| 292 | + let displayRanking = $derived(`${displayRankingValue} de ${rankingDenominador}`); | ||
| 293 | + let displayRankingLabel = $derived( | ||
| 294 | + selectedEntity | ||
| 295 | + ? (currentData ? `entre las que más gastan en ${currentData.año}` : 'entre las que más gastan') | ||
| 296 | + : (currentData ? `ranking en ${currentData.año}` : 'ranking entre grupos') | ||
| 297 | + ); | ||
| 298 | + | ||
| 299 | + // Top entidades reactivo según hover | ||
| 300 | + let displayTopEntidades = $derived( | ||
| 301 | + hoveredYear && topEntidadesPorAño[hoveredYear] | ||
| 302 | + ? topEntidadesPorAño[hoveredYear] | ||
| 303 | + : topEntidadesTotal | ||
| 304 | + ); | ||
| 305 | + let displayTopLabel = $derived(hoveredYear ? `en ${hoveredYear}` : `${primerAño}-${ultimoAño}`); | ||
| 306 | + | ||
| 307 | + // ══════════════════════════════════════════════════════════════ | ||
| 308 | + // TWEENED VALUES | ||
| 309 | + // ══════════════════════════════════════════════════════════════ | ||
| 310 | + const tweenOptions = { duration: 300, easing: cubicOut }; | ||
| 311 | + | ||
| 312 | + const twMonto = tweened(0, tweenOptions); | ||
| 313 | + const twPerCapita = tweened(0, tweenOptions); | ||
| 314 | + const twPorcentaje = tweened(0, tweenOptions); | ||
| 315 | + const twTop1 = tweened(0, tweenOptions); | ||
| 316 | + const twTop2 = tweened(0, tweenOptions); | ||
| 317 | + const twTop3 = tweened(0, tweenOptions); | ||
| 318 | + | ||
| 319 | + // ══════════════════════════════════════════════════════════════ | ||
| 320 | + // COMPARACIÓN - Datos reales de Supabase | ||
| 321 | + // ══════════════════════════════════════════════════════════════ | ||
| 322 | + const twMontoA = tweened(0, tweenOptions); | ||
| 323 | + const twMontoB = tweened(0, tweenOptions); | ||
| 324 | + const twPerCapitaA = tweened(0, tweenOptions); | ||
| 325 | + const twPerCapitaB = tweened(0, tweenOptions); | ||
| 326 | + | ||
| 327 | + let hoveredYearComparar = $state(null); | ||
| 328 | + let loadingComparacion = $state(false); | ||
| 329 | + | ||
| 330 | + // Entidades seleccionadas para comparar | ||
| 331 | + let entidadCompararA = $state(null); | ||
| 332 | + let entidadCompararB = $state(null); | ||
| 333 | + | ||
| 334 | + // Datos cargados de cada entidad | ||
| 335 | + let datosCompararA = $state({ anual: [], historico: null }); | ||
| 336 | + let datosCompararB = $state({ anual: [], historico: null }); | ||
| 337 | + | ||
| 338 | + // Dropdown states para comparación | ||
| 339 | + let dropdownAOpen = $state(false); | ||
| 340 | + let dropdownBOpen = $state(false); | ||
| 341 | + let searchA = $state(''); | ||
| 342 | + let searchB = $state(''); | ||
| 343 | + | ||
| 344 | + // Filtrar entidades para cada dropdown | ||
| 345 | + let filteredEntidadesA = $derived.by(() => { | ||
| 346 | + if (!searchA || searchA.length < 2) return entidades.slice(0, entityLimitA); | ||
| 347 | + const q = searchA.toLowerCase(); | ||
| 348 | + return entidades.filter(e => | ||
| 349 | + e.entidad_desc?.toLowerCase().includes(q) || | ||
| 350 | + e.entidad?.toString().includes(q) | ||
| 351 | + ).slice(0, entityLimitA); | ||
| 352 | + }); | ||
| 353 | + | ||
| 354 | + let filteredEntidadesB = $derived.by(() => { | ||
| 355 | + if (!searchB || searchB.length < 2) return entidades.slice(0, entityLimitB); | ||
| 356 | + const q = searchB.toLowerCase(); | ||
| 357 | + return entidades.filter(e => | ||
| 358 | + e.entidad_desc?.toLowerCase().includes(q) || | ||
| 359 | + e.entidad?.toString().includes(q) | ||
| 360 | + ).slice(0, entityLimitB); | ||
| 361 | + }); | ||
| 362 | + | ||
| 363 | + // Cargar datos de una entidad para comparación | ||
| 364 | + async function loadDatosEntidad(entidad) { | ||
| 365 | + const { data, error } = await supabase | ||
| 366 | + .schema('ppto') | ||
| 367 | + .from('vista_objeto_entidad') | ||
| 368 | + .select('*') | ||
| 369 | + .eq('objeto', objetoCodigo) | ||
| 370 | + .eq('nivel', objetoNivel) | ||
| 371 | + .eq('entidad', entidad.entidad) | ||
| 372 | + .order('gestion'); | ||
| 373 | + | ||
| 374 | + if (data) { | ||
| 375 | + return { | ||
| 376 | + anual: data.filter(d => d.gestion > 0), | ||
| 377 | + historico: data.find(d => d.gestion === 0) || null | ||
| 378 | + }; | ||
| 379 | + } | ||
| 380 | + return { anual: [], historico: null }; | ||
| 381 | + } | ||
| 382 | + | ||
| 383 | + // Seleccionar entidad A | ||
| 384 | + async function selectEntidadA(entidad) { | ||
| 385 | + entidadCompararA = entidad; | ||
| 386 | + dropdownAOpen = false; | ||
| 387 | + searchA = ''; | ||
| 388 | + entityLimitA = 30; | ||
| 389 | + loadingComparacion = true; | ||
| 390 | + datosCompararA = await loadDatosEntidad(entidad); | ||
| 391 | + loadingComparacion = false; | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + // Seleccionar entidad B | ||
| 395 | + async function selectEntidadB(entidad) { | ||
| 396 | + entidadCompararB = entidad; | ||
| 397 | + dropdownBOpen = false; | ||
| 398 | + searchB = ''; | ||
| 399 | + entityLimitB = 30; | ||
| 400 | + loadingComparacion = true; | ||
| 401 | + datosCompararB = await loadDatosEntidad(entidad); | ||
| 402 | + loadingComparacion = false; | ||
| 403 | + } | ||
| 404 | + | ||
| 405 | + // Selector de rango de años | ||
| 406 | + let rangoAños = $state('10'); // '5', '10', '15', 'todo' | ||
| 407 | + | ||
| 408 | + // Años comunes entre ambas entidades | ||
| 409 | + let añosComunesTotales = $derived.by(() => { | ||
| 410 | + if (!datosCompararA.anual.length || !datosCompararB.anual.length) return []; | ||
| 411 | + const añosA = new Set(datosCompararA.anual.map(d => d.gestion)); | ||
| 412 | + const añosB = new Set(datosCompararB.anual.map(d => d.gestion)); | ||
| 413 | + return [...añosA].filter(a => añosB.has(a)).sort((a, b) => a - b); | ||
| 414 | + }); | ||
| 415 | + | ||
| 416 | + // Años filtrados según el rango seleccionado | ||
| 417 | + let añosComparacion = $derived.by(() => { | ||
| 418 | + if (rangoAños === 'todo') return añosComunesTotales; | ||
| 419 | + const n = parseInt(rangoAños); | ||
| 420 | + return añosComunesTotales.slice(-n); | ||
| 421 | + }); | ||
| 422 | + | ||
| 423 | + // Datos formateados para el gráfico | ||
| 424 | + let datosGraficoA = $derived.by(() => { | ||
| 425 | + const map = new Map(datosCompararA.anual.map(d => [d.gestion, d])); | ||
| 426 | + return añosComparacion.map(año => map.get(año) || { per_capita: 0, monto: 0 }); | ||
| 427 | + }); | ||
| 428 | + | ||
| 429 | + let datosGraficoB = $derived.by(() => { | ||
| 430 | + const map = new Map(datosCompararB.anual.map(d => [d.gestion, d])); | ||
| 431 | + return añosComparacion.map(año => map.get(año) || { per_capita: 0, monto: 0 }); | ||
| 432 | + }); | ||
| 433 | + | ||
| 434 | + // Máximo para escala del gráfico | ||
| 435 | + let maxComparacion = $derived.by(() => { | ||
| 436 | + const allPerCapita = [ | ||
| 437 | + ...datosGraficoA.map(d => d.per_capita || 0), | ||
| 438 | + ...datosGraficoB.map(d => d.per_capita || 0) | ||
| 439 | + ]; | ||
| 440 | + return Math.max(...allPerCapita, 1) * 1.1; | ||
| 441 | + }); | ||
| 442 | + | ||
| 443 | + // Período para mostrar | ||
| 444 | + let displayPeriodoComparar = $derived.by(() => { | ||
| 445 | + if (hoveredYearComparar) return hoveredYearComparar; | ||
| 446 | + if (añosComparacion.length > 0) { | ||
| 447 | + return `${añosComparacion[0]}-${añosComparacion[añosComparacion.length - 1]}`; | ||
| 448 | + } | ||
| 449 | + return 'Selecciona entidades'; | ||
| 450 | + }); | ||
| 451 | + | ||
| 452 | + // Valores reactivos para comparación (peso y ranking) | ||
| 453 | + let displayPropA = $derived.by(() => { | ||
| 454 | + if (hoveredYearComparar) { | ||
| 455 | + const idx = añosComparacion.indexOf(hoveredYearComparar); | ||
| 456 | + if (idx >= 0) return datosGraficoA[idx]?.prop || 0; | ||
| 457 | + } | ||
| 458 | + return datosCompararA.historico?.prop || 0; | ||
| 459 | + }); | ||
| 460 | + | ||
| 461 | + let displayPropB = $derived.by(() => { | ||
| 462 | + if (hoveredYearComparar) { | ||
| 463 | + const idx = añosComparacion.indexOf(hoveredYearComparar); | ||
| 464 | + if (idx >= 0) return datosGraficoB[idx]?.prop || 0; | ||
| 465 | + } | ||
| 466 | + return datosCompararB.historico?.prop || 0; | ||
| 467 | + }); | ||
| 468 | + | ||
| 469 | + let displayRankingA = $derived.by(() => { | ||
| 470 | + if (hoveredYearComparar) { | ||
| 471 | + const idx = añosComparacion.indexOf(hoveredYearComparar); | ||
| 472 | + if (idx >= 0) return datosGraficoA[idx]?.ranking || '-'; | ||
| 473 | + } | ||
| 474 | + return datosCompararA.historico?.ranking || '-'; | ||
| 475 | + }); | ||
| 476 | + | ||
| 477 | + let displayRankingB = $derived.by(() => { | ||
| 478 | + if (hoveredYearComparar) { | ||
| 479 | + const idx = añosComparacion.indexOf(hoveredYearComparar); | ||
| 480 | + if (idx >= 0) return datosGraficoB[idx]?.ranking || '-'; | ||
| 481 | + } | ||
| 482 | + return datosCompararB.historico?.ranking || '-'; | ||
| 483 | + }); | ||
| 484 | + | ||
| 485 | + let displayNEntidadesA = $derived.by(() => { | ||
| 486 | + if (hoveredYearComparar) { | ||
| 487 | + const idx = añosComparacion.indexOf(hoveredYearComparar); | ||
| 488 | + if (idx >= 0) return datosGraficoA[idx]?.n_entidades || '-'; | ||
| 489 | + } | ||
| 490 | + return datosCompararA.historico?.n_entidades || '-'; | ||
| 491 | + }); | ||
| 492 | + | ||
| 493 | + let displayNEntidadesB = $derived.by(() => { | ||
| 494 | + if (hoveredYearComparar) { | ||
| 495 | + const idx = añosComparacion.indexOf(hoveredYearComparar); | ||
| 496 | + if (idx >= 0) return datosGraficoB[idx]?.n_entidades || '-'; | ||
| 497 | + } | ||
| 498 | + return datosCompararB.historico?.n_entidades || '-'; | ||
| 499 | + }); | ||
| 500 | + | ||
| 501 | + // Colores | ||
| 502 | + let colorA = '#2a9d8f'; | ||
| 503 | + let colorB = '#f4a261'; | ||
| 504 | + | ||
| 505 | + // Actualizar tweened de comparación | ||
| 506 | + $effect(() => { | ||
| 507 | + const year = hoveredYearComparar; | ||
| 508 | + const idx = year ? añosComparacion.indexOf(year) : -1; | ||
| 509 | + | ||
| 510 | + let montoA, montoB, perCapitaA, perCapitaB; | ||
| 511 | + | ||
| 512 | + if (idx >= 0) { | ||
| 513 | + montoA = toNumber(datosGraficoA[idx]?.monto); | ||
| 514 | + montoB = toNumber(datosGraficoB[idx]?.monto); | ||
| 515 | + perCapitaA = toNumber(datosGraficoA[idx]?.per_capita); | ||
| 516 | + perCapitaB = toNumber(datosGraficoB[idx]?.per_capita); | ||
| 517 | + } else { | ||
| 518 | + // Histórico o suma | ||
| 519 | + montoA = toNumber(datosCompararA.historico?.monto); | ||
| 520 | + montoB = toNumber(datosCompararB.historico?.monto); | ||
| 521 | + perCapitaA = toNumber(datosCompararA.historico?.per_capita); | ||
| 522 | + perCapitaB = toNumber(datosCompararB.historico?.per_capita); | ||
| 523 | + } | ||
| 524 | + | ||
| 525 | + twMontoA.set(montoA); | ||
| 526 | + twMontoB.set(montoB); | ||
| 527 | + twPerCapitaA.set(perCapitaA); | ||
| 528 | + twPerCapitaB.set(perCapitaB); | ||
| 529 | + }); | ||
| 530 | + | ||
| 531 | + // Helper para asegurar que siempre tengamos un número | ||
| 532 | + function toNumber(val) { | ||
| 533 | + const n = parseFloat(val); | ||
| 534 | + return isNaN(n) ? 0 : n; | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + // Actualizar tweened cuando cambian los valores | ||
| 538 | + // Leemos TODAS las dependencias explícitamente para que el effect se re-ejecute | ||
| 539 | + $effect(() => { | ||
| 540 | + // Leer todas las dependencias primero (para que Svelte las trackee) | ||
| 541 | + const year = hoveredYear; | ||
| 542 | + const datos = gastoPerCapita; // Leer siempre | ||
| 543 | + const histTotal = toNumber(totalHistorico); | ||
| 544 | + const histPerCapita = toNumber(promedioPerCapita); | ||
| 545 | + const histPorcentaje = toNumber(totalPorcentaje); | ||
| 546 | + | ||
| 547 | + // Calcular valores | ||
| 548 | + const data = year ? datos.find(g => g.año === year) : null; | ||
| 549 | + const monto = toNumber(data ? data.monto : histTotal); | ||
| 550 | + const perCapita = toNumber(data ? data.perCapita : histPerCapita); | ||
| 551 | + const porcentaje = data && histTotal > 0 | ||
| 552 | + ? toNumber((histPorcentaje * (data.monto / (histTotal / datos.length))).toFixed(1)) | ||
| 553 | + : histPorcentaje; | ||
| 554 | + | ||
| 555 | + twMonto.set(monto); | ||
| 556 | + twPerCapita.set(perCapita); | ||
| 557 | + twPorcentaje.set(porcentaje); | ||
| 558 | + }); | ||
| 559 | + | ||
| 560 | + $effect(() => { | ||
| 561 | + // Leer todas las dependencias primero | ||
| 562 | + const year = hoveredYear; | ||
| 563 | + const topsPorAño = topEntidadesPorAño; | ||
| 564 | + const topsTotal = topEntidadesTotal; | ||
| 565 | + | ||
| 566 | + const tops = year && topsPorAño[year] ? topsPorAño[year] : topsTotal; | ||
| 567 | + | ||
| 568 | + twTop1.set(toNumber(tops?.[0]?.porcentaje)); | ||
| 569 | + twTop2.set(toNumber(tops?.[1]?.porcentaje)); | ||
| 570 | + twTop3.set(toNumber(tops?.[2]?.porcentaje)); | ||
| 571 | + }); | ||
| 572 | + | ||
| 573 | + // ══════════════════════════════════════════════════════════════ | ||
| 574 | + // MOUNT | ||
| 575 | + // ══════════════════════════════════════════════════════════════ | ||
| 576 | + onMount(async () => { | ||
| 577 | + setTimeout(() => { mounted = true; }, 50); | ||
| 578 | + | ||
| 579 | + // Leer estado del tema | ||
| 580 | + isDark = document.documentElement.classList.contains('dark'); | ||
| 581 | + | ||
| 582 | + // Observar cambios de tema | ||
| 583 | + const observer = new MutationObserver(() => { | ||
| 584 | + isDark = document.documentElement.classList.contains('dark'); | ||
| 585 | + }); | ||
| 586 | + observer.observe(document.documentElement, { | ||
| 587 | + attributes: true, | ||
| 588 | + attributeFilter: ['class'] | ||
| 589 | + }); | ||
| 590 | + | ||
| 591 | + // Cargar datos | ||
| 592 | + await Promise.all([loadData(), loadEntidades()]); | ||
| 593 | + | ||
| 594 | + // Cerrar dropdown al hacer click fuera | ||
| 595 | + const handleClickOutside = (e) => { | ||
| 596 | + if (entityDropdownOpen && !e.target.closest('.entity-dropdown-container')) { | ||
| 597 | + entityDropdownOpen = false; | ||
| 598 | + } | ||
| 599 | + // Dropdowns de comparación | ||
| 600 | + if (dropdownAOpen && !e.target.closest('.comparador-dropdown-container')) { | ||
| 601 | + dropdownAOpen = false; | ||
| 602 | + } | ||
| 603 | + if (dropdownBOpen && !e.target.closest('.comparador-dropdown-container')) { | ||
| 604 | + dropdownBOpen = false; | ||
| 605 | + } | ||
| 606 | + }; | ||
| 607 | + document.addEventListener('click', handleClickOutside); | ||
| 608 | + | ||
| 609 | + return () => { | ||
| 610 | + observer.disconnect(); | ||
| 611 | + document.removeEventListener('click', handleClickOutside); | ||
| 612 | + }; | ||
| 613 | + }); | ||
| 614 | + | ||
| 615 | + // Recargar datos cuando cambia el objeto (navegación interna) | ||
| 616 | + let previousCodigo = objetoCodigo; | ||
| 617 | + $effect(() => { | ||
| 618 | + if (mounted && objetoCodigo !== previousCodigo) { | ||
| 619 | + previousCodigo = objetoCodigo; | ||
| 620 | + // Resetear estado | ||
| 621 | + loading = true; | ||
| 622 | + selectedEntity = null; | ||
| 623 | + entidadCompararA = null; | ||
| 624 | + entidadCompararB = null; | ||
| 625 | + datosCompararA = { anual: [], historico: null }; | ||
| 626 | + datosCompararB = { anual: [], historico: null }; | ||
| 627 | + vista = 'agregado'; | ||
| 628 | + // Recargar datos | ||
| 629 | + loadData(); | ||
| 630 | + } | ||
| 631 | + }); | ||
| 632 | + | ||
| 633 | + // ══════════════════════════════════════════════════════════════ | ||
| 634 | + // HELPERS | ||
| 635 | + // ══════════════════════════════════════════════════════════════ | ||
| 636 | + function formatMonto(m) { | ||
| 637 | + if (m >= 1e9) return (m / 1e9).toFixed(1) + 'MM'; | ||
| 638 | + if (m >= 1e6) return (m / 1e6).toFixed(1) + 'M'; | ||
| 639 | + if (m >= 1e3) return Math.round(m / 1e3) + 'K'; | ||
| 640 | + return m?.toLocaleString() || '0'; | ||
| 641 | + } | ||
| 642 | + | ||
| 643 | + function formatMontoLargo(m) { | ||
| 644 | + if (m >= 1e9) return Math.round(m / 1e9).toLocaleString('es-BO') + ' mil Mill.'; | ||
| 645 | + if (m >= 1e6) return (m / 1e6).toLocaleString('es-BO', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' Mill.'; | ||
| 646 | + return m?.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0'; | ||
| 647 | + } | ||
| 648 | + | ||
| 649 | + function formatPerCapita(n) { | ||
| 650 | + if (n >= 1000) return n.toLocaleString('es-BO', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); | ||
| 651 | + if (n >= 10) return n.toLocaleString('es-BO', { minimumFractionDigits: 1, maximumFractionDigits: 1 }); | ||
| 652 | + if (n < 1) return n.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); | ||
| 653 | + return n.toLocaleString('es-BO', { minimumFractionDigits: 1, maximumFractionDigits: 1 }); | ||
| 654 | + } | ||
| 36 | </script> | 655 | </script> |
| 37 | 656 | ||
| 38 | <svelte:head> | 657 | <svelte:head> |
| 39 | - <title>{objeto.desc_objeto} | Presupuesto Público</title> | 658 | + <title>{objeto.nombre} | Presupuesto Público</title> |
| 659 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 660 | + <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" /> | ||
| 40 | </svelte:head> | 661 | </svelte:head> |
| 41 | 662 | ||
| 42 | -<div class="min-h-screen bg-white"> | 663 | +<div class="page" class:mounted class:tema-claro={!isDark}> |
| 43 | - <header class="border-b bg-slate-50"> | ||
| 44 | - <div class="max-w-4xl mx-auto px-6 py-4"> | ||
| 45 | - <nav class="text-sm text-slate-500 mb-2 flex flex-wrap items-center gap-1"> | ||
| 46 | - <a href="/" class="hover:text-slate-700">Inicio</a> | ||
| 47 | - <span>/</span> | ||
| 48 | - <a href="/clasificadores/objeto-gasto" class="hover:text-slate-700">Objeto del Gasto</a> | ||
| 49 | - {#each padres as padre} | ||
| 50 | - <span>/</span> | ||
| 51 | - <a href="/objeto/{padre.objeto}" class="hover:text-slate-700">{padre.desc_objeto}</a> | ||
| 52 | - {/each} | ||
| 53 | - <span>/</span> | ||
| 54 | - <span class="text-slate-900">{objeto.desc_objeto}</span> | ||
| 55 | - </nav> | ||
| 56 | - </div> | ||
| 57 | - </header> | ||
| 58 | 664 | ||
| 59 | - <main class="max-w-4xl mx-auto px-6 py-8"> | 665 | + <!-- Header compacto --> |
| 60 | - <!-- Encabezado --> | 666 | + <header> |
| 61 | - <div class="mb-8"> | 667 | + <nav class="breadcrumb"> |
| 62 | - <p class="text-xs text-slate-500 uppercase tracking-wide mb-1">{getNivelLabel(objeto.nivel)}</p> | 668 | + <a href="/clasificadores/objeto-gasto">← Objeto del Gasto</a> |
| 63 | - <div class="flex items-baseline gap-4"> | 669 | + {#each padres as padre} |
| 64 | - <span class="font-mono text-2xl text-slate-400">{objeto.objeto}</span> | 670 | + <span class="breadcrumb-sep">/</span> |
| 65 | - <h1 class="text-2xl font-medium text-slate-900">{objeto.desc_objeto}</h1> | 671 | + <a href="/objeto/{padre.objeto}" class="breadcrumb-link">{padre.desc_objeto}</a> |
| 672 | + {/each} | ||
| 673 | + </nav> | ||
| 674 | + | ||
| 675 | + <div class="title-row"> | ||
| 676 | + <div class="title-main"> | ||
| 677 | + <span class="codigo">{objeto.codigo}</span> | ||
| 678 | + <h1> | ||
| 679 | + {objeto.nombre} | ||
| 680 | + <button class="help-icon" onclick={() => showInfo = !showInfo} class:active={showInfo}> | ||
| 681 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 682 | + <circle cx="12" cy="12" r="10"/> | ||
| 683 | + <path d="M9.5 9a3 3 0 1 1 3.5 2.95c-.5.17-1 .62-1 1.3V15"/> | ||
| 684 | + <circle cx="12" cy="18" r="0.5" fill="currentColor"/> | ||
| 685 | + </svg> | ||
| 686 | + </button> | ||
| 687 | + </h1> | ||
| 66 | </div> | 688 | </div> |
| 67 | </div> | 689 | </div> |
| 68 | 690 | ||
| 69 | - <!-- Descripciones --> | 691 | + <div class="meta"> |
| 70 | - <section class="mb-10"> | 692 | + <span class="badge">{objeto.nivel}</span> |
| 71 | - <h2 class="text-sm font-medium text-slate-700 mb-4"> | 693 | + <span class="years">{objeto.años}</span> |
| 72 | - {#if descripciones.length > 1} | 694 | + </div> |
| 73 | - Descripciones ({descripciones.length} variaciones) | 695 | + </header> |
| 74 | - {:else} | 696 | + |
| 75 | - Descripción | 697 | + <!-- Panel de info (colapsable) --> |
| 698 | + {#if showInfo} | ||
| 699 | + <div class="info-panel"> | ||
| 700 | + <div class="info-content"> | ||
| 701 | + <h4>Definición vigente</h4> | ||
| 702 | + <p>{objeto.descripcion}</p> | ||
| 703 | + | ||
| 704 | + {#if objeto.variaciones.length > 1} | ||
| 705 | + <h4>Variaciones históricas</h4> | ||
| 706 | + {#each objeto.variaciones as v} | ||
| 707 | + <div class="variacion"> | ||
| 708 | + <span class="variacion-rango">{v.rango}</span> | ||
| 709 | + <p>{v.texto}</p> | ||
| 710 | + </div> | ||
| 711 | + {/each} | ||
| 76 | {/if} | 712 | {/if} |
| 77 | - </h2> | 713 | + |
| 78 | - | 714 | + {#if hijos.length > 0} |
| 79 | - <div class="space-y-4"> | 715 | + <h4> |
| 80 | - {#each descripciones as desc, i} | 716 | + {#if objetoNivel === 'grupo'} |
| 81 | - <div class="border-l-2 {i === 0 ? 'border-blue-500 bg-blue-50/50' : 'border-slate-200 bg-slate-50/50'} pl-4 py-3 rounded-r"> | 717 | + Subgrupos ({hijos.length}) |
| 82 | - <p class="text-xs text-slate-500 mb-2"> | 718 | + {:else if objetoNivel === 'subgrupo'} |
| 83 | - {#if i === 0 && descripciones.length > 1} | 719 | + Partidas ({hijos.length}) |
| 84 | - <span class="text-blue-600 font-medium">Vigente</span> | 720 | + {:else if objetoNivel === 'partida'} |
| 85 | - <span class="mx-1">·</span> | 721 | + Subpartidas ({hijos.length}) |
| 86 | - {/if} | 722 | + {/if} |
| 87 | - {desc.rangos} | 723 | + </h4> |
| 88 | - </p> | 724 | + <div class="hijos-list"> |
| 89 | - <p class="text-slate-700 leading-relaxed">{desc.descripcion}</p> | 725 | + {#each hijos as hijo} |
| 726 | + <a href="/objeto/{hijo.objeto}" class="hijo-link"> | ||
| 727 | + <span class="hijo-codigo">{hijo.objeto}</span> | ||
| 728 | + <span class="hijo-nombre">{hijo.desc_objeto}</span> | ||
| 729 | + </a> | ||
| 730 | + {/each} | ||
| 90 | </div> | 731 | </div> |
| 91 | - {/each} | 732 | + {/if} |
| 92 | </div> | 733 | </div> |
| 93 | - </section> | 734 | + </div> |
| 94 | - | 735 | + {/if} |
| 95 | - <!-- Jerarquía --> | 736 | + |
| 96 | - {#if padres.length > 0} | 737 | + <!-- Dashboard principal --> |
| 97 | - <section class="mb-10"> | 738 | + <main> |
| 98 | - <h2 class="text-sm font-medium text-slate-700 mb-4">Jerarquía</h2> | 739 | + |
| 99 | - <div class="space-y-2"> | 740 | + <!-- Toggle de vista --> |
| 100 | - {#each padres as padre, i} | 741 | + <div class="vista-toggle"> |
| 101 | - <div class="flex items-center gap-2" style="margin-left: {i * 1.5}rem"> | 742 | + <button |
| 102 | - <span class="text-slate-300">└</span> | 743 | + class="toggle-btn" |
| 103 | - <a | 744 | + class:active={vista === 'agregado'} |
| 104 | - href="/objeto/{padre.objeto}" | 745 | + onclick={() => vista = 'agregado'} |
| 105 | - class="text-sm text-slate-600 hover:text-blue-600" | 746 | + > |
| 747 | + Agregado | ||
| 748 | + </button> | ||
| 749 | + <button | ||
| 750 | + class="toggle-btn" | ||
| 751 | + class:active={vista === 'comparar'} | ||
| 752 | + onclick={() => vista = 'comparar'} | ||
| 753 | + > | ||
| 754 | + Comparar | ||
| 755 | + </button> | ||
| 756 | + </div> | ||
| 757 | + | ||
| 758 | + <!-- Selector de entidad (solo en vista agregado) --> | ||
| 759 | + {#if vista === 'agregado'} | ||
| 760 | + <div class="entity-selector entity-dropdown-container"> | ||
| 761 | + <span class="selector-label">Viendo</span> | ||
| 762 | + <div class="relative"> | ||
| 763 | + <button | ||
| 764 | + class="selector-btn" | ||
| 765 | + onclick={() => entityDropdownOpen = !entityDropdownOpen} | ||
| 766 | + > | ||
| 767 | + <span class="truncate"> | ||
| 768 | + {#if selectedEntity} | ||
| 769 | + {selectedEntity.entidad_desc?.slice(0, 30)}{selectedEntity.entidad_desc?.length > 30 ? '...' : ''} | ||
| 770 | + {:else} | ||
| 771 | + Todo el sector público | ||
| 772 | + {/if} | ||
| 773 | + </span> | ||
| 774 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 775 | + <path d="M6 9l6 6 6-6"/> | ||
| 776 | + </svg> | ||
| 777 | + </button> | ||
| 778 | + | ||
| 779 | + {#if entityDropdownOpen} | ||
| 780 | + <div class="dropdown-panel"> | ||
| 781 | + <div class="dropdown-search"> | ||
| 782 | + <input | ||
| 783 | + type="text" | ||
| 784 | + bind:value={entitySearchQuery} | ||
| 785 | + placeholder="Buscar entidad..." | ||
| 786 | + class="search-input" | ||
| 787 | + /> | ||
| 788 | + </div> | ||
| 789 | + <button | ||
| 790 | + class="dropdown-item" | ||
| 791 | + class:active={!selectedEntity} | ||
| 792 | + onclick={() => clearEntity()} | ||
| 106 | > | 793 | > |
| 107 | - <span class="font-mono text-xs text-slate-400">{padre.objeto}</span> | 794 | + <span class="font-medium">Todo el sector público</span> |
| 108 | - {padre.desc_objeto} | 795 | + <span class="text-xs opacity-60 ml-2">({nEntidades} entidades)</span> |
| 109 | - </a> | 796 | + </button> |
| 797 | + <div class="dropdown-list" onscroll={(e) => handleEntityScroll(e, 'main')}> | ||
| 798 | + {#each filteredEntities as entity} | ||
| 799 | + <button | ||
| 800 | + class="dropdown-item" | ||
| 801 | + class:active={selectedEntity?.entidad === entity.entidad} | ||
| 802 | + onclick={() => selectEntity(entity)} | ||
| 803 | + > | ||
| 804 | + <span class="entity-code">{entity.entidad}</span> | ||
| 805 | + <span class="truncate">{entity.entidad_desc}</span> | ||
| 806 | + </button> | ||
| 807 | + {/each} | ||
| 808 | + {#if entityLimit < entidades.length} | ||
| 809 | + <div class="text-center py-2 text-xs opacity-60"> | ||
| 810 | + Scroll para ver más ({entidades.length - entityLimit} restantes) | ||
| 811 | + </div> | ||
| 812 | + {/if} | ||
| 813 | + </div> | ||
| 110 | </div> | 814 | </div> |
| 111 | - {/each} | 815 | + {/if} |
| 112 | - <div class="flex items-center gap-2" style="margin-left: {padres.length * 1.5}rem"> | ||
| 113 | - <span class="text-blue-500">└</span> | ||
| 114 | - <span class="text-sm font-medium text-blue-600"> | ||
| 115 | - <span class="font-mono text-xs">{objeto.objeto}</span> | ||
| 116 | - {objeto.desc_objeto} | ||
| 117 | - </span> | ||
| 118 | - </div> | ||
| 119 | </div> | 816 | </div> |
| 120 | - </section> | 817 | + <span class="interaction-hint desktop-hint">Pasa el cursor sobre las barras para ver valores por año</span> |
| 818 | + <span class="interaction-hint mobile-hint">Toca las barras para ver valores por año</span> | ||
| 819 | + </div> | ||
| 121 | {/if} | 820 | {/if} |
| 122 | 821 | ||
| 123 | - <!-- Hijos --> | 822 | + {#if vista === 'agregado'} |
| 124 | - {#if hijos.length > 0} | 823 | + <!-- VISTA AGREGADO --> |
| 125 | - <section class="mb-10"> | 824 | + |
| 126 | - <h2 class="text-sm font-medium text-slate-700 mb-4"> | 825 | + <!-- KPIs arriba --> |
| 127 | - {#if objeto.nivel === 'grupo'} | 826 | + <div class="kpis"> |
| 128 | - Subgrupos ({hijos.length}) | 827 | + <div class="kpi"> |
| 129 | - {:else if objeto.nivel === 'subgrupo'} | 828 | + <span class="kpi-value">Bs {formatMontoLargo(Math.round($twMonto))}</span> |
| 130 | - Partidas ({hijos.length}) | 829 | + <span class="kpi-label">{displayMontoLabel}</span> |
| 131 | - {:else if objeto.nivel === 'partida'} | 830 | + </div> |
| 132 | - Subpartidas ({hijos.length}) | 831 | + |
| 133 | - {/if} | 832 | + <div class="kpi"> |
| 134 | - </h2> | 833 | + <span class="kpi-value">Bs {formatPerCapita($twPerCapita)}</span> |
| 135 | - | 834 | + <span class="kpi-label">{displayPerCapitaLabel}</span> |
| 136 | - <div class="border rounded-lg divide-y"> | 835 | + </div> |
| 137 | - {#each hijos as hijo} | 836 | + |
| 138 | - {@const hijoDescs = parseDescripciones(hijo.descripciones)} | 837 | + <div class="kpi"> |
| 139 | - <a | 838 | + <span class="kpi-value">{$twPorcentaje.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span> |
| 140 | - href="/objeto/{hijo.objeto}" | 839 | + <span class="kpi-label">{displayPorcentajeLabel}</span> |
| 141 | - class="block px-4 py-3 hover:bg-slate-50 transition-colors" | 840 | + </div> |
| 142 | - > | 841 | + |
| 143 | - <div class="flex items-baseline gap-3"> | 842 | + <div class="kpi"> |
| 144 | - <span class="font-mono text-sm text-slate-400">{hijo.objeto}</span> | 843 | + <span class="kpi-value">{displayRanking}</span> |
| 145 | - <span class="text-slate-900">{hijo.desc_objeto}</span> | 844 | + <span class="kpi-label">{displayRankingLabel}</span> |
| 146 | - {#if hijo.n_variaciones > 1} | 845 | + </div> |
| 147 | - <span class="text-xs text-orange-500">({hijo.n_variaciones} var.)</span> | 846 | + </div> |
| 148 | - {/if} | 847 | + |
| 848 | + <!-- Gráfico de barras (per cápita) - protagonista al medio --> | ||
| 849 | + <div class="chart-section chart-protagonista"> | ||
| 850 | + <div class="chart-header"> | ||
| 851 | + <span class="chart-title">Gasto por persona</span> | ||
| 852 | + <span class="chart-period">{displayPeriodo}</span> | ||
| 853 | + </div> | ||
| 854 | + | ||
| 855 | + <div class="chart-wrapper"> | ||
| 856 | + <!-- Y-axis labels --> | ||
| 857 | + <div class="y-axis"> | ||
| 858 | + <span class="y-label">Bs {formatMonto(maxPerCapita)}</span> | ||
| 859 | + <span class="y-label">Bs {formatMonto(Math.round(maxPerCapita / 2))}</span> | ||
| 860 | + <span class="y-label">Bs 0</span> | ||
| 861 | + </div> | ||
| 862 | + | ||
| 863 | + <div | ||
| 864 | + class="chart" | ||
| 865 | + onmouseleave={() => hoveredYear = null} | ||
| 866 | + role="group" | ||
| 867 | + > | ||
| 868 | + {#each gastoPerCapita as g} | ||
| 869 | + <div | ||
| 870 | + class="bar-container" | ||
| 871 | + onmouseenter={() => hoveredYear = g.año} | ||
| 872 | + role="button" | ||
| 873 | + tabindex="0" | ||
| 874 | + > | ||
| 875 | + <div | ||
| 876 | + class="bar" | ||
| 877 | + class:hovered={hoveredYear === g.año} | ||
| 878 | + style="height: {(g.perCapita / maxPerCapita) * 100}%" | ||
| 879 | + ></div> | ||
| 880 | + <span class="bar-label" class:visible={hoveredYear === g.año || g.año % 5 === 0}> | ||
| 881 | + {g.año.toString().slice(-2)} | ||
| 882 | + </span> | ||
| 149 | </div> | 883 | </div> |
| 150 | - {#if hijoDescs.length > 0} | 884 | + {/each} |
| 151 | - <p class="text-sm text-slate-500 mt-1 ml-16">{hijoDescs[0].descripcion}</p> | 885 | + </div> |
| 152 | - {/if} | 886 | + </div> |
| 153 | - </a> | 887 | + </div> |
| 154 | - {/each} | 888 | + |
| 889 | + <!-- Top entidades (solo visible cuando es "Todo el Estado") --> | ||
| 890 | + {#if !selectedEntity} | ||
| 891 | + <div class="top-section"> | ||
| 892 | + <div class="top-header"> | ||
| 893 | + <h3>Mayores ejecutores</h3> | ||
| 894 | + <span class="top-context">{totalEntidades} entidades · {displayTopLabel}</span> | ||
| 895 | + </div> | ||
| 896 | + | ||
| 897 | + <div class="top-list"> | ||
| 898 | + {#each displayTopEntidades as ent, i (ent.nombre)} | ||
| 899 | + <div class="top-item"> | ||
| 900 | + <span class="top-rank">#{i + 1}</span> | ||
| 901 | + <div class="top-info"> | ||
| 902 | + <span class="top-name">{ent.nombre}</span> | ||
| 903 | + <div class="top-bar-bg"> | ||
| 904 | + <div class="top-bar" style="width: {((i === 0 ? $twTop1 : i === 1 ? $twTop2 : $twTop3) / $twTop1) * 100}%"></div> | ||
| 905 | + </div> | ||
| 906 | + </div> | ||
| 907 | + <span class="top-pct">{(i === 0 ? $twTop1 : i === 1 ? $twTop2 : $twTop3).toFixed(1)}%</span> | ||
| 908 | + </div> | ||
| 909 | + {/each} | ||
| 910 | + </div> | ||
| 911 | + </div> | ||
| 912 | + {/if} | ||
| 913 | + | ||
| 914 | + {:else} | ||
| 915 | + <!-- VISTA COMPARAR --> | ||
| 916 | + | ||
| 917 | + <!-- Selectores de entidades con búsqueda --> | ||
| 918 | + <div class="comparador-selectors"> | ||
| 919 | + <div class="selector-group comparador-dropdown-container"> | ||
| 920 | + <span class="selector-dot" style="background: {colorA}"></span> | ||
| 921 | + <div class="relative" style="flex: 1;"> | ||
| 922 | + <button class="selector-btn comparador-btn" onclick={() => { dropdownAOpen = !dropdownAOpen; dropdownBOpen = false; }}> | ||
| 923 | + <span class="truncate"> | ||
| 924 | + {entidadCompararA ? entidadCompararA.entidad_desc : 'Seleccionar entidad'} | ||
| 925 | + </span> | ||
| 926 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 927 | + <path d="M6 9l6 6 6-6"/> | ||
| 928 | + </svg> | ||
| 929 | + </button> | ||
| 930 | + {#if dropdownAOpen} | ||
| 931 | + <div class="dropdown-panel comparador-panel"> | ||
| 932 | + <div class="dropdown-search"> | ||
| 933 | + <input type="text" bind:value={searchA} placeholder="Buscar entidad..." class="search-input" /> | ||
| 934 | + </div> | ||
| 935 | + <div class="dropdown-list" onscroll={(e) => handleEntityScroll(e, 'A')}> | ||
| 936 | + {#each filteredEntidadesA as entity} | ||
| 937 | + <button | ||
| 938 | + class="dropdown-item" | ||
| 939 | + class:active={entidadCompararA?.entidad === entity.entidad} | ||
| 940 | + disabled={entidadCompararB?.entidad === entity.entidad} | ||
| 941 | + onclick={() => selectEntidadA(entity)} | ||
| 942 | + > | ||
| 943 | + <span class="entity-code">{entity.entidad}</span> | ||
| 944 | + <span class="truncate">{entity.entidad_desc}</span> | ||
| 945 | + </button> | ||
| 946 | + {/each} | ||
| 947 | + {#if entityLimitA < entidades.length} | ||
| 948 | + <div class="text-center py-2 text-xs opacity-60"> | ||
| 949 | + Scroll para ver más | ||
| 950 | + </div> | ||
| 951 | + {/if} | ||
| 952 | + </div> | ||
| 953 | + </div> | ||
| 954 | + {/if} | ||
| 955 | + </div> | ||
| 956 | + </div> | ||
| 957 | + | ||
| 958 | + <span class="vs">vs</span> | ||
| 959 | + | ||
| 960 | + <div class="selector-group comparador-dropdown-container"> | ||
| 961 | + <span class="selector-dot" style="background: {colorB}"></span> | ||
| 962 | + <div class="relative" style="flex: 1;"> | ||
| 963 | + <button class="selector-btn comparador-btn" onclick={() => { dropdownBOpen = !dropdownBOpen; dropdownAOpen = false; }}> | ||
| 964 | + <span class="truncate"> | ||
| 965 | + {entidadCompararB ? entidadCompararB.entidad_desc : 'Seleccionar entidad'} | ||
| 966 | + </span> | ||
| 967 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 968 | + <path d="M6 9l6 6 6-6"/> | ||
| 969 | + </svg> | ||
| 970 | + </button> | ||
| 971 | + {#if dropdownBOpen} | ||
| 972 | + <div class="dropdown-panel comparador-panel"> | ||
| 973 | + <div class="dropdown-search"> | ||
| 974 | + <input type="text" bind:value={searchB} placeholder="Buscar entidad..." class="search-input" /> | ||
| 975 | + </div> | ||
| 976 | + <div class="dropdown-list" onscroll={(e) => handleEntityScroll(e, 'B')}> | ||
| 977 | + {#each filteredEntidadesB as entity} | ||
| 978 | + <button | ||
| 979 | + class="dropdown-item" | ||
| 980 | + class:active={entidadCompararB?.entidad === entity.entidad} | ||
| 981 | + disabled={entidadCompararA?.entidad === entity.entidad} | ||
| 982 | + onclick={() => selectEntidadB(entity)} | ||
| 983 | + > | ||
| 984 | + <span class="entity-code">{entity.entidad}</span> | ||
| 985 | + <span class="truncate">{entity.entidad_desc}</span> | ||
| 986 | + </button> | ||
| 987 | + {/each} | ||
| 988 | + {#if entityLimitB < entidades.length} | ||
| 989 | + <div class="text-center py-2 text-xs opacity-60"> | ||
| 990 | + Scroll para ver más | ||
| 991 | + </div> | ||
| 992 | + {/if} | ||
| 993 | + </div> | ||
| 994 | + </div> | ||
| 995 | + {/if} | ||
| 996 | + </div> | ||
| 997 | + </div> | ||
| 998 | + </div> | ||
| 999 | + | ||
| 1000 | + <!-- Mensaje si no hay entidades seleccionadas --> | ||
| 1001 | + {#if !entidadCompararA || !entidadCompararB} | ||
| 1002 | + <div class="comparador-placeholder"> | ||
| 1003 | + <p>Selecciona dos entidades para comparar su gasto en <strong>{objeto.nombre}</strong></p> | ||
| 1004 | + </div> | ||
| 1005 | + {:else if loadingComparacion} | ||
| 1006 | + <div class="comparador-placeholder"> | ||
| 1007 | + <Spinner size={48} color="var(--theme-texto, #5A5650)" /> | ||
| 1008 | + </div> | ||
| 1009 | + {:else if añosComparacion.length === 0} | ||
| 1010 | + <div class="comparador-placeholder"> | ||
| 1011 | + <p>Las entidades seleccionadas no tienen años en común para este objeto.</p> | ||
| 1012 | + </div> | ||
| 1013 | + {:else} | ||
| 1014 | + <!-- Selector de rango de años --> | ||
| 1015 | + {#if añosComunesTotales.length > 10} | ||
| 1016 | + <div class="rango-selector"> | ||
| 1017 | + <span class="rango-label">Mostrar:</span> | ||
| 1018 | + <div class="rango-options"> | ||
| 1019 | + <button class="rango-btn" class:active={rangoAños === '5'} onclick={() => rangoAños = '5'}>5 años</button> | ||
| 1020 | + <button class="rango-btn" class:active={rangoAños === '10'} onclick={() => rangoAños = '10'}>10 años</button> | ||
| 1021 | + <button class="rango-btn" class:active={rangoAños === '15'} onclick={() => rangoAños = '15'}>15 años</button> | ||
| 1022 | + <button class="rango-btn" class:active={rangoAños === 'todo'} onclick={() => rangoAños = 'todo'}>Todo ({añosComunesTotales.length})</button> | ||
| 1023 | + </div> | ||
| 1024 | + </div> | ||
| 1025 | + {/if} | ||
| 1026 | + | ||
| 1027 | + <!-- Gráfico comparativo --> | ||
| 1028 | + <div class="chart-section"> | ||
| 1029 | + <div class="chart-header"> | ||
| 1030 | + <span class="chart-title">Gasto por persona</span> | ||
| 1031 | + <span class="chart-period">{displayPeriodoComparar}</span> | ||
| 1032 | + </div> | ||
| 1033 | + | ||
| 1034 | + <div class="chart-wrapper chart-wrapper-scroll"> | ||
| 1035 | + <div class="y-axis y-axis-sticky"> | ||
| 1036 | + <span class="y-label">Bs {formatMonto(Math.round(maxComparacion))}</span> | ||
| 1037 | + <span class="y-label">Bs {formatMonto(Math.round(maxComparacion / 2))}</span> | ||
| 1038 | + <span class="y-label">Bs 0</span> | ||
| 1039 | + </div> | ||
| 1040 | + | ||
| 1041 | + <div | ||
| 1042 | + class="chart chart-comparar" | ||
| 1043 | + class:chart-muchos-años={añosComparacion.length > 15} | ||
| 1044 | + onmouseleave={() => hoveredYearComparar = null} | ||
| 1045 | + role="group" | ||
| 1046 | + > | ||
| 1047 | + {#each añosComparacion as año, i} | ||
| 1048 | + <div | ||
| 1049 | + class="bar-group" | ||
| 1050 | + class:active={hoveredYearComparar === año} | ||
| 1051 | + onmouseenter={() => hoveredYearComparar = año} | ||
| 1052 | + role="button" | ||
| 1053 | + tabindex="0" | ||
| 1054 | + > | ||
| 1055 | + <div class="bars-pair"> | ||
| 1056 | + <div | ||
| 1057 | + class="bar bar-a" | ||
| 1058 | + style="height: {((datosGraficoA[i]?.per_capita || 0) / maxComparacion) * 100}%; --bar-color: {colorA}" | ||
| 1059 | + ></div> | ||
| 1060 | + <div | ||
| 1061 | + class="bar bar-b" | ||
| 1062 | + style="height: {((datosGraficoB[i]?.per_capita || 0) / maxComparacion) * 100}%; --bar-color: {colorB}" | ||
| 1063 | + ></div> | ||
| 1064 | + </div> | ||
| 1065 | + <span class="bar-label visible" class:highlight={hoveredYearComparar === año}>{año}</span> | ||
| 1066 | + </div> | ||
| 1067 | + {/each} | ||
| 1068 | + </div> | ||
| 1069 | + </div> | ||
| 1070 | + | ||
| 1071 | + <div class="chart-legend"> | ||
| 1072 | + <span class="legend-item"><span class="legend-dot" style="background: {colorA}"></span>{entidadCompararA.entidad_desc}</span> | ||
| 1073 | + <span class="legend-item"><span class="legend-dot" style="background: {colorB}"></span>{entidadCompararB.entidad_desc}</span> | ||
| 1074 | + </div> | ||
| 1075 | + </div> | ||
| 1076 | + | ||
| 1077 | + <!-- KPIs comparativos - tabla --> | ||
| 1078 | + <div class="kpis-tabla"> | ||
| 1079 | + <!-- Header --> | ||
| 1080 | + <div class="tabla-header"> | ||
| 1081 | + <span class="tabla-label"></span> | ||
| 1082 | + <span class="tabla-val" style="color: {colorA}">{entidadCompararA.entidad_desc?.slice(0, 20)}{entidadCompararA.entidad_desc?.length > 20 ? '...' : ''}</span> | ||
| 1083 | + <span class="tabla-val" style="color: {colorB}">{entidadCompararB.entidad_desc?.slice(0, 20)}{entidadCompararB.entidad_desc?.length > 20 ? '...' : ''}</span> | ||
| 1084 | + </div> | ||
| 1085 | + <!-- Gasto --> | ||
| 1086 | + <div class="tabla-row"> | ||
| 1087 | + <span class="tabla-label">{hoveredYearComparar ? `Gasto ${hoveredYearComparar}` : `Gasto ${añosComparacion[0]}-${añosComparacion[añosComparacion.length-1]}`}</span> | ||
| 1088 | + <span class="tabla-val">Bs {formatMontoLargo(Math.round($twMontoA))}</span> | ||
| 1089 | + <span class="tabla-val">Bs {formatMontoLargo(Math.round($twMontoB))}</span> | ||
| 1090 | + </div> | ||
| 1091 | + <!-- Per cápita --> | ||
| 1092 | + <div class="tabla-row"> | ||
| 1093 | + <span class="tabla-label">{hoveredYearComparar ? `Por persona ${hoveredYearComparar}` : 'Por persona (prom.)'}</span> | ||
| 1094 | + <span class="tabla-val">Bs {formatPerCapita($twPerCapitaA)}</span> | ||
| 1095 | + <span class="tabla-val">Bs {formatPerCapita($twPerCapitaB)}</span> | ||
| 1096 | + </div> | ||
| 1097 | + <!-- Porcentaje --> | ||
| 1098 | + <div class="tabla-row"> | ||
| 1099 | + <span class="tabla-label">{hoveredYearComparar ? `Peso en ${hoveredYearComparar}` : 'Peso sobre total'}</span> | ||
| 1100 | + <span class="tabla-val">{displayPropA.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span> | ||
| 1101 | + <span class="tabla-val">{displayPropB.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span> | ||
| 1102 | + </div> | ||
| 1103 | + <!-- Ranking --> | ||
| 1104 | + <div class="tabla-row"> | ||
| 1105 | + <span class="tabla-label">{hoveredYearComparar ? `Ranking ${hoveredYearComparar}` : 'Ranking'}</span> | ||
| 1106 | + <span class="tabla-val">#{displayRankingA} de {displayNEntidadesA}</span> | ||
| 1107 | + <span class="tabla-val">#{displayRankingB} de {displayNEntidadesB}</span> | ||
| 1108 | + </div> | ||
| 155 | </div> | 1109 | </div> |
| 156 | - </section> | 1110 | + {/if} |
| 1111 | + | ||
| 157 | {/if} | 1112 | {/if} |
| 158 | 1113 | ||
| 159 | - <!-- Volver --> | ||
| 160 | - <div class="pt-6 border-t"> | ||
| 161 | - <a | ||
| 162 | - href="/clasificadores/objeto-gasto" | ||
| 163 | - class="text-sm text-blue-600 hover:underline" | ||
| 164 | - > | ||
| 165 | - Volver al clasificador | ||
| 166 | - </a> | ||
| 167 | - </div> | ||
| 168 | </main> | 1114 | </main> |
| 169 | </div> | 1115 | </div> |
| 1116 | + | ||
| 1117 | + | ||
| 1118 | + | ||
| 1119 | +<style> | ||
| 1120 | + .page { | ||
| 1121 | + min-height: 100vh; | ||
| 1122 | + background: #0A0A0A; | ||
| 1123 | + color: #F5F0E8; | ||
| 1124 | + font-family: 'Instrument Sans', -apple-system, sans-serif; | ||
| 1125 | + opacity: 0; | ||
| 1126 | + transition: opacity 0.4s ease; | ||
| 1127 | + } | ||
| 1128 | + .mounted { opacity: 1; } | ||
| 1129 | + | ||
| 1130 | + /* Header */ | ||
| 1131 | + header { | ||
| 1132 | + padding: 1rem 2rem; | ||
| 1133 | + border-bottom: 1px solid #1E1E1C; | ||
| 1134 | + } | ||
| 1135 | + | ||
| 1136 | + .breadcrumb { | ||
| 1137 | + display: flex; | ||
| 1138 | + align-items: center; | ||
| 1139 | + gap: 0.5rem; | ||
| 1140 | + flex-wrap: wrap; | ||
| 1141 | + } | ||
| 1142 | + | ||
| 1143 | + .breadcrumb a { | ||
| 1144 | + font-family: 'DM Mono', monospace; | ||
| 1145 | + font-size: 0.75rem; | ||
| 1146 | + color: #5A5650; | ||
| 1147 | + text-decoration: none; | ||
| 1148 | + transition: color 0.2s; | ||
| 1149 | + } | ||
| 1150 | + .breadcrumb a:hover { color: #F5F0E8; } | ||
| 1151 | + | ||
| 1152 | + .breadcrumb-sep { | ||
| 1153 | + color: #3A3A38; | ||
| 1154 | + font-size: 0.75rem; | ||
| 1155 | + } | ||
| 1156 | + | ||
| 1157 | + .breadcrumb-link { | ||
| 1158 | + max-width: 150px; | ||
| 1159 | + overflow: hidden; | ||
| 1160 | + text-overflow: ellipsis; | ||
| 1161 | + white-space: nowrap; | ||
| 1162 | + } | ||
| 1163 | + | ||
| 1164 | + .title-row { | ||
| 1165 | + display: flex; | ||
| 1166 | + align-items: flex-start; | ||
| 1167 | + justify-content: space-between; | ||
| 1168 | + margin-top: 0.5rem; | ||
| 1169 | + gap: 1rem; | ||
| 1170 | + } | ||
| 1171 | + | ||
| 1172 | + .title-main { | ||
| 1173 | + display: flex; | ||
| 1174 | + align-items: baseline; | ||
| 1175 | + gap: 1rem; | ||
| 1176 | + flex-wrap: wrap; | ||
| 1177 | + } | ||
| 1178 | + | ||
| 1179 | + .codigo { | ||
| 1180 | + font-family: 'DM Mono', monospace; | ||
| 1181 | + font-size: 1.5rem; | ||
| 1182 | + color: #5A5650; | ||
| 1183 | + font-weight: 400; | ||
| 1184 | + } | ||
| 1185 | + | ||
| 1186 | + h1 { | ||
| 1187 | + font-family: 'DM Serif Display', Georgia, serif; | ||
| 1188 | + font-size: 1.5rem; | ||
| 1189 | + font-weight: 400; | ||
| 1190 | + color: #F5F0E8; | ||
| 1191 | + margin: 0; | ||
| 1192 | + display: inline-flex; | ||
| 1193 | + align-items: center; | ||
| 1194 | + gap: 0.5rem; | ||
| 1195 | + } | ||
| 1196 | + | ||
| 1197 | + .help-icon { | ||
| 1198 | + background: transparent; | ||
| 1199 | + border: none; | ||
| 1200 | + padding: 0; | ||
| 1201 | + color: #5A5650; | ||
| 1202 | + cursor: pointer; | ||
| 1203 | + transition: all 0.2s; | ||
| 1204 | + display: inline-flex; | ||
| 1205 | + align-items: center; | ||
| 1206 | + justify-content: center; | ||
| 1207 | + vertical-align: middle; | ||
| 1208 | + } | ||
| 1209 | + .help-icon:hover, .help-icon.active { | ||
| 1210 | + color: #E8C547; | ||
| 1211 | + } | ||
| 1212 | + | ||
| 1213 | + .meta { | ||
| 1214 | + display: flex; | ||
| 1215 | + align-items: center; | ||
| 1216 | + gap: 0.75rem; | ||
| 1217 | + margin-top: 0.5rem; | ||
| 1218 | + } | ||
| 1219 | + | ||
| 1220 | + .badge { | ||
| 1221 | + font-family: 'DM Mono', monospace; | ||
| 1222 | + font-size: 0.625rem; | ||
| 1223 | + letter-spacing: 0.1em; | ||
| 1224 | + text-transform: uppercase; | ||
| 1225 | + padding: 0.25rem 0.5rem; | ||
| 1226 | + background: #1A1A18; | ||
| 1227 | + border: 1px solid #2E2E2C; | ||
| 1228 | + border-radius: 4px; | ||
| 1229 | + color: #8A8578; | ||
| 1230 | + } | ||
| 1231 | + | ||
| 1232 | + .years { | ||
| 1233 | + font-family: 'DM Mono', monospace; | ||
| 1234 | + font-size: 0.75rem; | ||
| 1235 | + color: #5A5650; | ||
| 1236 | + } | ||
| 1237 | + | ||
| 1238 | + /* Info panel */ | ||
| 1239 | + .info-panel { | ||
| 1240 | + background: #111110; | ||
| 1241 | + border-bottom: 1px solid #1E1E1C; | ||
| 1242 | + padding: 1.5rem 2rem; | ||
| 1243 | + } | ||
| 1244 | + | ||
| 1245 | + .info-content h4 { | ||
| 1246 | + font-family: 'DM Mono', monospace; | ||
| 1247 | + font-size: 0.625rem; | ||
| 1248 | + letter-spacing: 0.15em; | ||
| 1249 | + text-transform: uppercase; | ||
| 1250 | + color: #5A5650; | ||
| 1251 | + margin: 0 0 0.5rem 0; | ||
| 1252 | + } | ||
| 1253 | + | ||
| 1254 | + .info-content p { | ||
| 1255 | + font-size: 0.875rem; | ||
| 1256 | + line-height: 1.6; | ||
| 1257 | + color: #8A8578; | ||
| 1258 | + margin: 0 0 1.25rem 0; | ||
| 1259 | + } | ||
| 1260 | + | ||
| 1261 | + .variacion { | ||
| 1262 | + padding-left: 1rem; | ||
| 1263 | + border-left: 2px solid #2E2E2C; | ||
| 1264 | + margin-bottom: 1rem; | ||
| 1265 | + } | ||
| 1266 | + | ||
| 1267 | + .variacion-rango { | ||
| 1268 | + font-family: 'DM Mono', monospace; | ||
| 1269 | + font-size: 0.688rem; | ||
| 1270 | + color: #5A5650; | ||
| 1271 | + } | ||
| 1272 | + | ||
| 1273 | + .variacion p { | ||
| 1274 | + margin-top: 0.25rem; | ||
| 1275 | + font-size: 0.813rem; | ||
| 1276 | + } | ||
| 1277 | + | ||
| 1278 | + /* Hijos list in info panel */ | ||
| 1279 | + .hijos-list { | ||
| 1280 | + display: flex; | ||
| 1281 | + flex-direction: column; | ||
| 1282 | + gap: 0.25rem; | ||
| 1283 | + margin-bottom: 1rem; | ||
| 1284 | + } | ||
| 1285 | + | ||
| 1286 | + .hijo-link { | ||
| 1287 | + display: flex; | ||
| 1288 | + align-items: baseline; | ||
| 1289 | + gap: 0.75rem; | ||
| 1290 | + padding: 0.375rem 0.5rem; | ||
| 1291 | + border-radius: 4px; | ||
| 1292 | + text-decoration: none; | ||
| 1293 | + transition: background 0.2s; | ||
| 1294 | + } | ||
| 1295 | + | ||
| 1296 | + .hijo-link:hover { | ||
| 1297 | + background: rgba(255, 255, 255, 0.05); | ||
| 1298 | + } | ||
| 1299 | + | ||
| 1300 | + .hijo-codigo { | ||
| 1301 | + font-family: 'DM Mono', monospace; | ||
| 1302 | + font-size: 0.688rem; | ||
| 1303 | + color: #5A5650; | ||
| 1304 | + min-width: 3rem; | ||
| 1305 | + } | ||
| 1306 | + | ||
| 1307 | + .hijo-nombre { | ||
| 1308 | + font-size: 0.813rem; | ||
| 1309 | + color: #B0A89C; | ||
| 1310 | + } | ||
| 1311 | + | ||
| 1312 | + .hijo-link:hover .hijo-nombre { | ||
| 1313 | + color: #F5F0E8; | ||
| 1314 | + } | ||
| 1315 | + | ||
| 1316 | + /* Main */ | ||
| 1317 | + main { | ||
| 1318 | + padding: 1rem 2rem; | ||
| 1319 | + max-width: 900px; | ||
| 1320 | + margin: 0 auto; | ||
| 1321 | + } | ||
| 1322 | + | ||
| 1323 | + /* Entity selector */ | ||
| 1324 | + .entity-selector { | ||
| 1325 | + display: flex; | ||
| 1326 | + align-items: center; | ||
| 1327 | + gap: 0.75rem; | ||
| 1328 | + margin-bottom: 0.75rem; | ||
| 1329 | + flex-wrap: wrap; | ||
| 1330 | + } | ||
| 1331 | + | ||
| 1332 | + .interaction-hint { | ||
| 1333 | + font-size: 0.688rem; | ||
| 1334 | + color: #5A5650; | ||
| 1335 | + font-style: italic; | ||
| 1336 | + margin-left: auto; | ||
| 1337 | + } | ||
| 1338 | + | ||
| 1339 | + .desktop-hint { | ||
| 1340 | + display: inline; | ||
| 1341 | + } | ||
| 1342 | + | ||
| 1343 | + .mobile-hint { | ||
| 1344 | + display: none; | ||
| 1345 | + } | ||
| 1346 | + | ||
| 1347 | + @media (max-width: 640px) { | ||
| 1348 | + .desktop-hint { | ||
| 1349 | + display: none; | ||
| 1350 | + } | ||
| 1351 | + .mobile-hint { | ||
| 1352 | + display: inline; | ||
| 1353 | + } | ||
| 1354 | + } | ||
| 1355 | + | ||
| 1356 | + .selector-label { | ||
| 1357 | + font-size: 0.813rem; | ||
| 1358 | + color: #5A5650; | ||
| 1359 | + } | ||
| 1360 | + | ||
| 1361 | + .selector-btn { | ||
| 1362 | + display: flex; | ||
| 1363 | + align-items: center; | ||
| 1364 | + gap: 0.5rem; | ||
| 1365 | + padding: 0.5rem 1rem; | ||
| 1366 | + background: transparent; | ||
| 1367 | + border: none; | ||
| 1368 | + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); | ||
| 1369 | + border-radius: 8px; | ||
| 1370 | + color: #F5F0E8; | ||
| 1371 | + font-size: 0.875rem; | ||
| 1372 | + cursor: pointer; | ||
| 1373 | + transition: all 0.2s; | ||
| 1374 | + } | ||
| 1375 | + .selector-btn:hover { | ||
| 1376 | + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.15); | ||
| 1377 | + } | ||
| 1378 | + .selector-btn svg { | ||
| 1379 | + color: #5A5650; | ||
| 1380 | + } | ||
| 1381 | + | ||
| 1382 | + .relative { | ||
| 1383 | + position: relative; | ||
| 1384 | + } | ||
| 1385 | + | ||
| 1386 | + .dropdown-panel { | ||
| 1387 | + position: absolute; | ||
| 1388 | + top: 100%; | ||
| 1389 | + left: 0; | ||
| 1390 | + margin-top: 0.25rem; | ||
| 1391 | + width: min(500px, 90vw); | ||
| 1392 | + max-height: 360px; | ||
| 1393 | + overflow: hidden; | ||
| 1394 | + background: #1A1A18; | ||
| 1395 | + border: 1px solid #2E2E2C; | ||
| 1396 | + border-radius: 8px; | ||
| 1397 | + box-shadow: 0 10px 40px rgba(0,0,0,0.4); | ||
| 1398 | + z-index: 50; | ||
| 1399 | + } | ||
| 1400 | + | ||
| 1401 | + .dropdown-search { | ||
| 1402 | + padding: 0.5rem; | ||
| 1403 | + border-bottom: 1px solid #2E2E2C; | ||
| 1404 | + } | ||
| 1405 | + | ||
| 1406 | + .search-input { | ||
| 1407 | + width: 100%; | ||
| 1408 | + padding: 0.5rem 0.75rem; | ||
| 1409 | + background: #111110; | ||
| 1410 | + border: 1px solid #2E2E2C; | ||
| 1411 | + border-radius: 6px; | ||
| 1412 | + color: #F5F0E8; | ||
| 1413 | + font-size: 0.875rem; | ||
| 1414 | + } | ||
| 1415 | + .search-input:focus { | ||
| 1416 | + outline: none; | ||
| 1417 | + border-color: #E8C547; | ||
| 1418 | + } | ||
| 1419 | + .search-input::placeholder { | ||
| 1420 | + color: #5A5650; | ||
| 1421 | + } | ||
| 1422 | + | ||
| 1423 | + .dropdown-item { | ||
| 1424 | + display: flex; | ||
| 1425 | + align-items: center; | ||
| 1426 | + gap: 0.5rem; | ||
| 1427 | + width: 100%; | ||
| 1428 | + padding: 0.625rem 0.75rem; | ||
| 1429 | + background: transparent; | ||
| 1430 | + border: none; | ||
| 1431 | + color: #F5F0E8; | ||
| 1432 | + font-size: 0.813rem; | ||
| 1433 | + text-align: left; | ||
| 1434 | + cursor: pointer; | ||
| 1435 | + transition: background 0.15s; | ||
| 1436 | + } | ||
| 1437 | + .dropdown-item:hover { | ||
| 1438 | + background: rgba(255,255,255,0.05); | ||
| 1439 | + } | ||
| 1440 | + .dropdown-item.active { | ||
| 1441 | + background: rgba(232,197,71,0.15); | ||
| 1442 | + } | ||
| 1443 | + | ||
| 1444 | + .dropdown-list { | ||
| 1445 | + max-height: 240px; | ||
| 1446 | + overflow-y: auto; | ||
| 1447 | + } | ||
| 1448 | + | ||
| 1449 | + .entity-code { | ||
| 1450 | + font-family: 'DM Mono', monospace; | ||
| 1451 | + font-size: 0.688rem; | ||
| 1452 | + color: #5A5650; | ||
| 1453 | + min-width: 2.5rem; | ||
| 1454 | + } | ||
| 1455 | + | ||
| 1456 | + .truncate { | ||
| 1457 | + overflow: hidden; | ||
| 1458 | + text-overflow: ellipsis; | ||
| 1459 | + white-space: nowrap; | ||
| 1460 | + } | ||
| 1461 | + | ||
| 1462 | + /* Chart */ | ||
| 1463 | + .chart-section { | ||
| 1464 | + margin-bottom: 1rem; | ||
| 1465 | + } | ||
| 1466 | + | ||
| 1467 | + .chart-protagonista { | ||
| 1468 | + margin-bottom: 1.25rem; | ||
| 1469 | + } | ||
| 1470 | + | ||
| 1471 | + .chart-protagonista .chart { | ||
| 1472 | + height: calc(100vh - 480px); | ||
| 1473 | + min-height: 120px; | ||
| 1474 | + max-height: 200px; | ||
| 1475 | + } | ||
| 1476 | + | ||
| 1477 | + .chart-protagonista .y-axis { | ||
| 1478 | + height: calc(100vh - 480px); | ||
| 1479 | + min-height: 120px; | ||
| 1480 | + max-height: 200px; | ||
| 1481 | + } | ||
| 1482 | + | ||
| 1483 | + .chart-section:has(.chart-comparar) { | ||
| 1484 | + margin-bottom: 0; | ||
| 1485 | + } | ||
| 1486 | + | ||
| 1487 | + .chart-section:has(.chart-comparar) .y-axis { | ||
| 1488 | + height: calc(100vh - 380px); | ||
| 1489 | + min-height: 140px; | ||
| 1490 | + max-height: 280px; | ||
| 1491 | + } | ||
| 1492 | + | ||
| 1493 | + .chart-header { | ||
| 1494 | + display: flex; | ||
| 1495 | + justify-content: space-between; | ||
| 1496 | + align-items: baseline; | ||
| 1497 | + margin-bottom: 0.5rem; | ||
| 1498 | + } | ||
| 1499 | + | ||
| 1500 | + .chart-title { | ||
| 1501 | + font-size: 0.75rem; | ||
| 1502 | + color: #5A5650; | ||
| 1503 | + text-transform: uppercase; | ||
| 1504 | + letter-spacing: 0.1em; | ||
| 1505 | + } | ||
| 1506 | + | ||
| 1507 | + .chart-period { | ||
| 1508 | + font-family: 'DM Mono', monospace; | ||
| 1509 | + font-size: 0.875rem; | ||
| 1510 | + color: #B0A89C; | ||
| 1511 | + font-variant-numeric: tabular-nums; | ||
| 1512 | + min-width: 5rem; | ||
| 1513 | + text-align: right; | ||
| 1514 | + } | ||
| 1515 | + | ||
| 1516 | + .chart-wrapper { | ||
| 1517 | + display: flex; | ||
| 1518 | + gap: 0.75rem; | ||
| 1519 | + } | ||
| 1520 | + | ||
| 1521 | + .chart-wrapper-scroll { | ||
| 1522 | + overflow-x: auto; | ||
| 1523 | + overflow-y: visible; | ||
| 1524 | + scrollbar-width: thin; | ||
| 1525 | + scrollbar-color: #2E2E2C transparent; | ||
| 1526 | + } | ||
| 1527 | + | ||
| 1528 | + .chart-wrapper-scroll::-webkit-scrollbar { | ||
| 1529 | + height: 6px; | ||
| 1530 | + } | ||
| 1531 | + | ||
| 1532 | + .chart-wrapper-scroll::-webkit-scrollbar-track { | ||
| 1533 | + background: transparent; | ||
| 1534 | + } | ||
| 1535 | + | ||
| 1536 | + .chart-wrapper-scroll::-webkit-scrollbar-thumb { | ||
| 1537 | + background: #2E2E2C; | ||
| 1538 | + border-radius: 3px; | ||
| 1539 | + } | ||
| 1540 | + | ||
| 1541 | + .y-axis-sticky { | ||
| 1542 | + position: sticky; | ||
| 1543 | + left: 0; | ||
| 1544 | + z-index: 10; | ||
| 1545 | + background: #0A0A0A; | ||
| 1546 | + padding-right: 0.5rem; | ||
| 1547 | + margin-right: -0.5rem; | ||
| 1548 | + } | ||
| 1549 | + | ||
| 1550 | + .chart-muchos-años { | ||
| 1551 | + min-width: max-content; | ||
| 1552 | + } | ||
| 1553 | + | ||
| 1554 | + .chart-muchos-años .bar-group { | ||
| 1555 | + min-width: 40px; | ||
| 1556 | + } | ||
| 1557 | + | ||
| 1558 | + .y-axis { | ||
| 1559 | + display: flex; | ||
| 1560 | + flex-direction: column; | ||
| 1561 | + justify-content: space-between; | ||
| 1562 | + align-items: flex-end; | ||
| 1563 | + height: 160px; | ||
| 1564 | + padding-bottom: 1.5rem; | ||
| 1565 | + } | ||
| 1566 | + | ||
| 1567 | + .y-label { | ||
| 1568 | + font-family: 'DM Mono', monospace; | ||
| 1569 | + font-size: 0.625rem; | ||
| 1570 | + color: #5A5650; | ||
| 1571 | + } | ||
| 1572 | + | ||
| 1573 | + .chart { | ||
| 1574 | + flex: 1; | ||
| 1575 | + display: flex; | ||
| 1576 | + align-items: flex-end; | ||
| 1577 | + gap: 4px; | ||
| 1578 | + height: 160px; | ||
| 1579 | + padding: 0 0.5rem; | ||
| 1580 | + border-left: 1px solid #2E2E2C; | ||
| 1581 | + } | ||
| 1582 | + | ||
| 1583 | + .bar-container { | ||
| 1584 | + flex: 1; | ||
| 1585 | + display: flex; | ||
| 1586 | + flex-direction: column; | ||
| 1587 | + align-items: center; | ||
| 1588 | + justify-content: flex-end; | ||
| 1589 | + height: 100%; | ||
| 1590 | + cursor: pointer; | ||
| 1591 | + } | ||
| 1592 | + | ||
| 1593 | + .bar { | ||
| 1594 | + width: 100%; | ||
| 1595 | + background: #2a9d8f; | ||
| 1596 | + border-radius: 4px 4px 0 0; | ||
| 1597 | + transition: background 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); | ||
| 1598 | + min-height: 4px; | ||
| 1599 | + } | ||
| 1600 | + | ||
| 1601 | + .bar.hovered { | ||
| 1602 | + background: #E8C547; | ||
| 1603 | + transform: scaleX(1.1); | ||
| 1604 | + } | ||
| 1605 | + | ||
| 1606 | + .bar-label { | ||
| 1607 | + font-family: 'DM Mono', monospace; | ||
| 1608 | + font-size: 0.625rem; | ||
| 1609 | + color: #B0A89C; | ||
| 1610 | + margin-top: 0.5rem; | ||
| 1611 | + opacity: 0; | ||
| 1612 | + transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1), color 0.25s ease; | ||
| 1613 | + } | ||
| 1614 | + .bar-label.visible { opacity: 1; } | ||
| 1615 | + | ||
| 1616 | + /* KPIs */ | ||
| 1617 | + .kpis { | ||
| 1618 | + display: grid; | ||
| 1619 | + grid-template-columns: repeat(4, 1fr); | ||
| 1620 | + gap: 0.5rem; | ||
| 1621 | + margin-bottom: 1rem; | ||
| 1622 | + } | ||
| 1623 | + | ||
| 1624 | + .kpi { | ||
| 1625 | + background: transparent; | ||
| 1626 | + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); | ||
| 1627 | + padding: 0.75rem 0.75rem; | ||
| 1628 | + text-align: center; | ||
| 1629 | + border-radius: 8px; | ||
| 1630 | + transition: box-shadow 0.3s ease; | ||
| 1631 | + } | ||
| 1632 | + | ||
| 1633 | + .kpi-value { | ||
| 1634 | + display: block; | ||
| 1635 | + font-family: 'DM Mono', monospace; | ||
| 1636 | + font-size: 1.125rem; | ||
| 1637 | + font-weight: 500; | ||
| 1638 | + color: #F5F0E8; | ||
| 1639 | + margin-bottom: 0; | ||
| 1640 | + transition: opacity 0.2s ease; | ||
| 1641 | + font-variant-numeric: tabular-nums; | ||
| 1642 | + min-height: 1.5rem; | ||
| 1643 | + } | ||
| 1644 | + | ||
| 1645 | + .kpi-label { | ||
| 1646 | + font-size: 0.5625rem; | ||
| 1647 | + color: #5A5650; | ||
| 1648 | + text-transform: uppercase; | ||
| 1649 | + letter-spacing: 0.03em; | ||
| 1650 | + line-height: 1.3; | ||
| 1651 | + transition: opacity 0.2s ease; | ||
| 1652 | + min-height: 1.25rem; | ||
| 1653 | + display: flex; | ||
| 1654 | + align-items: center; | ||
| 1655 | + justify-content: center; | ||
| 1656 | + } | ||
| 1657 | + | ||
| 1658 | + /* Top entidades */ | ||
| 1659 | + .top-header { | ||
| 1660 | + display: flex; | ||
| 1661 | + justify-content: space-between; | ||
| 1662 | + align-items: baseline; | ||
| 1663 | + margin-bottom: 0.625rem; | ||
| 1664 | + } | ||
| 1665 | + | ||
| 1666 | + .top-section h3 { | ||
| 1667 | + font-family: 'DM Mono', monospace; | ||
| 1668 | + font-size: 0.75rem; | ||
| 1669 | + color: #5A5650; | ||
| 1670 | + text-transform: uppercase; | ||
| 1671 | + letter-spacing: 0.1em; | ||
| 1672 | + margin: 0; | ||
| 1673 | + } | ||
| 1674 | + | ||
| 1675 | + .top-context { | ||
| 1676 | + font-family: 'DM Mono', monospace; | ||
| 1677 | + font-size: 0.688rem; | ||
| 1678 | + color: #5A5650; | ||
| 1679 | + font-variant-numeric: tabular-nums; | ||
| 1680 | + } | ||
| 1681 | + | ||
| 1682 | + .top-list { | ||
| 1683 | + display: flex; | ||
| 1684 | + flex-direction: column; | ||
| 1685 | + gap: 0.5rem; | ||
| 1686 | + } | ||
| 1687 | + | ||
| 1688 | + .top-item { | ||
| 1689 | + display: flex; | ||
| 1690 | + align-items: center; | ||
| 1691 | + gap: 0.75rem; | ||
| 1692 | + padding: 0.625rem 0.875rem; | ||
| 1693 | + background: transparent; | ||
| 1694 | + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); | ||
| 1695 | + border-radius: 8px; | ||
| 1696 | + transition: box-shadow 0.25s ease; | ||
| 1697 | + } | ||
| 1698 | + | ||
| 1699 | + .top-item:hover { | ||
| 1700 | + box-shadow: inset 0 0 0 1px rgba(201, 167, 81, 0.4); | ||
| 1701 | + } | ||
| 1702 | + | ||
| 1703 | + .top-rank { | ||
| 1704 | + font-family: 'DM Mono', monospace; | ||
| 1705 | + font-size: 0.875rem; | ||
| 1706 | + color: #5A5650; | ||
| 1707 | + width: 24px; | ||
| 1708 | + } | ||
| 1709 | + | ||
| 1710 | + .top-info { | ||
| 1711 | + flex: 1; | ||
| 1712 | + } | ||
| 1713 | + | ||
| 1714 | + .top-name { | ||
| 1715 | + display: block; | ||
| 1716 | + font-size: 0.8125rem; | ||
| 1717 | + color: #F5F0E8; | ||
| 1718 | + margin-bottom: 0.375rem; | ||
| 1719 | + } | ||
| 1720 | + | ||
| 1721 | + .top-bar-bg { | ||
| 1722 | + height: 4px; | ||
| 1723 | + background: #1E1E1C; | ||
| 1724 | + border-radius: 2px; | ||
| 1725 | + overflow: hidden; | ||
| 1726 | + } | ||
| 1727 | + | ||
| 1728 | + .top-bar { | ||
| 1729 | + height: 100%; | ||
| 1730 | + background: #C9A751; | ||
| 1731 | + border-radius: 2px; | ||
| 1732 | + transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), background 0.25s ease; | ||
| 1733 | + } | ||
| 1734 | + | ||
| 1735 | + .top-item:hover .top-bar { | ||
| 1736 | + background: #E8C547; | ||
| 1737 | + } | ||
| 1738 | + | ||
| 1739 | + .top-pct { | ||
| 1740 | + font-family: 'DM Mono', monospace; | ||
| 1741 | + font-size: 0.875rem; | ||
| 1742 | + color: #B0A89C; | ||
| 1743 | + min-width: 52px; | ||
| 1744 | + width: 52px; | ||
| 1745 | + text-align: right; | ||
| 1746 | + transition: opacity 0.2s ease; | ||
| 1747 | + font-variant-numeric: tabular-nums; | ||
| 1748 | + } | ||
| 1749 | + | ||
| 1750 | + .see-all { | ||
| 1751 | + background: transparent; | ||
| 1752 | + border: 1px solid #2E2E2C; | ||
| 1753 | + border-radius: 8px; | ||
| 1754 | + padding: 0.75rem 1rem; | ||
| 1755 | + color: #8A8578; | ||
| 1756 | + font-size: 0.813rem; | ||
| 1757 | + cursor: pointer; | ||
| 1758 | + transition: all 0.2s; | ||
| 1759 | + width: 100%; | ||
| 1760 | + margin-top: 1rem; | ||
| 1761 | + } | ||
| 1762 | + .see-all:hover { | ||
| 1763 | + border-color: #E8C547; | ||
| 1764 | + color: #E8C547; | ||
| 1765 | + } | ||
| 1766 | + | ||
| 1767 | + /* Vista Toggle */ | ||
| 1768 | + .vista-toggle { | ||
| 1769 | + display: inline-flex; | ||
| 1770 | + background: transparent; | ||
| 1771 | + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); | ||
| 1772 | + border-radius: 10px; | ||
| 1773 | + padding: 4px; | ||
| 1774 | + margin-bottom: 0.75rem; | ||
| 1775 | + } | ||
| 1776 | + | ||
| 1777 | + .toggle-btn { | ||
| 1778 | + padding: 0.5rem 1.25rem; | ||
| 1779 | + font-size: 0.813rem; | ||
| 1780 | + font-weight: 500; | ||
| 1781 | + color: #5A5650; | ||
| 1782 | + background: transparent; | ||
| 1783 | + border: none; | ||
| 1784 | + border-radius: 7px; | ||
| 1785 | + cursor: pointer; | ||
| 1786 | + transition: all 0.2s; | ||
| 1787 | + } | ||
| 1788 | + | ||
| 1789 | + .toggle-btn:hover { | ||
| 1790 | + color: #8A8578; | ||
| 1791 | + } | ||
| 1792 | + | ||
| 1793 | + .toggle-btn.active { | ||
| 1794 | + background: #F5F0E8; | ||
| 1795 | + color: #0A0A0A; | ||
| 1796 | + } | ||
| 1797 | + | ||
| 1798 | + /* Comparador inline - ajustado para 100vh */ | ||
| 1799 | + .comparador-selectors { | ||
| 1800 | + display: flex; | ||
| 1801 | + align-items: flex-start; | ||
| 1802 | + gap: 1rem; | ||
| 1803 | + margin-bottom: 0.75rem; | ||
| 1804 | + } | ||
| 1805 | + | ||
| 1806 | + .comparador-dropdown-container { | ||
| 1807 | + position: relative; | ||
| 1808 | + } | ||
| 1809 | + | ||
| 1810 | + .comparador-btn { | ||
| 1811 | + width: 100%; | ||
| 1812 | + justify-content: space-between; | ||
| 1813 | + } | ||
| 1814 | + | ||
| 1815 | + .comparador-panel { | ||
| 1816 | + width: 100%; | ||
| 1817 | + min-width: 280px; | ||
| 1818 | + } | ||
| 1819 | + | ||
| 1820 | + .comparador-placeholder { | ||
| 1821 | + display: flex; | ||
| 1822 | + align-items: center; | ||
| 1823 | + justify-content: center; | ||
| 1824 | + min-height: 200px; | ||
| 1825 | + background: rgba(255,255,255,0.02); | ||
| 1826 | + border: 1px dashed #2E2E2C; | ||
| 1827 | + border-radius: 12px; | ||
| 1828 | + padding: 2rem; | ||
| 1829 | + text-align: center; | ||
| 1830 | + } | ||
| 1831 | + | ||
| 1832 | + .comparador-placeholder p { | ||
| 1833 | + color: #5A5650; | ||
| 1834 | + font-size: 0.875rem; | ||
| 1835 | + margin: 0; | ||
| 1836 | + } | ||
| 1837 | + | ||
| 1838 | + .comparador-placeholder strong { | ||
| 1839 | + color: #8A8578; | ||
| 1840 | + } | ||
| 1841 | + | ||
| 1842 | + .rango-selector { | ||
| 1843 | + display: flex; | ||
| 1844 | + align-items: center; | ||
| 1845 | + gap: 0.75rem; | ||
| 1846 | + margin-bottom: 0.75rem; | ||
| 1847 | + } | ||
| 1848 | + | ||
| 1849 | + .rango-label { | ||
| 1850 | + font-size: 0.75rem; | ||
| 1851 | + color: #5A5650; | ||
| 1852 | + } | ||
| 1853 | + | ||
| 1854 | + .rango-options { | ||
| 1855 | + display: flex; | ||
| 1856 | + gap: 0.25rem; | ||
| 1857 | + } | ||
| 1858 | + | ||
| 1859 | + .rango-btn { | ||
| 1860 | + padding: 0.375rem 0.75rem; | ||
| 1861 | + font-size: 0.6875rem; | ||
| 1862 | + font-family: 'DM Mono', monospace; | ||
| 1863 | + color: #5A5650; | ||
| 1864 | + background: transparent; | ||
| 1865 | + border: none; | ||
| 1866 | + border-radius: 6px; | ||
| 1867 | + cursor: pointer; | ||
| 1868 | + transition: all 0.2s; | ||
| 1869 | + } | ||
| 1870 | + | ||
| 1871 | + .rango-btn:hover { | ||
| 1872 | + color: #8A8578; | ||
| 1873 | + background: rgba(255,255,255,0.05); | ||
| 1874 | + } | ||
| 1875 | + | ||
| 1876 | + .rango-btn.active { | ||
| 1877 | + color: #F5F0E8; | ||
| 1878 | + background: rgba(255,255,255,0.1); | ||
| 1879 | + } | ||
| 1880 | + | ||
| 1881 | + .dropdown-item:disabled { | ||
| 1882 | + opacity: 0.4; | ||
| 1883 | + cursor: not-allowed; | ||
| 1884 | + } | ||
| 1885 | + | ||
| 1886 | + .selector-group { | ||
| 1887 | + flex: 1; | ||
| 1888 | + display: flex; | ||
| 1889 | + align-items: center; | ||
| 1890 | + gap: 0.5rem; | ||
| 1891 | + } | ||
| 1892 | + | ||
| 1893 | + .selector-dot { | ||
| 1894 | + width: 10px; | ||
| 1895 | + height: 10px; | ||
| 1896 | + border-radius: 50%; | ||
| 1897 | + flex-shrink: 0; | ||
| 1898 | + } | ||
| 1899 | + | ||
| 1900 | + .selector-group select { | ||
| 1901 | + flex: 1; | ||
| 1902 | + background: transparent; | ||
| 1903 | + border: none; | ||
| 1904 | + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); | ||
| 1905 | + border-radius: 8px; | ||
| 1906 | + padding: 0.5rem 0.75rem; | ||
| 1907 | + color: #F5F0E8; | ||
| 1908 | + font-size: 0.813rem; | ||
| 1909 | + font-family: 'Instrument Sans', sans-serif; | ||
| 1910 | + cursor: pointer; | ||
| 1911 | + } | ||
| 1912 | + .selector-group select:focus { | ||
| 1913 | + outline: none; | ||
| 1914 | + border-color: #E8C547; | ||
| 1915 | + } | ||
| 1916 | + | ||
| 1917 | + .vs { | ||
| 1918 | + font-family: 'DM Mono', monospace; | ||
| 1919 | + font-size: 0.75rem; | ||
| 1920 | + color: #5A5650; | ||
| 1921 | + flex-shrink: 0; | ||
| 1922 | + } | ||
| 1923 | + | ||
| 1924 | + .chart-comparar { | ||
| 1925 | + gap: 1.5rem; | ||
| 1926 | + justify-content: center; | ||
| 1927 | + height: calc(100vh - 380px); | ||
| 1928 | + min-height: 140px; | ||
| 1929 | + max-height: 280px; | ||
| 1930 | + } | ||
| 1931 | + | ||
| 1932 | + .chart-comparar + .chart-legend + .kpis-comparar { | ||
| 1933 | + margin-top: 0; | ||
| 1934 | + } | ||
| 1935 | + | ||
| 1936 | + .bar-group { | ||
| 1937 | + display: flex; | ||
| 1938 | + flex-direction: column; | ||
| 1939 | + align-items: center; | ||
| 1940 | + height: 100%; | ||
| 1941 | + } | ||
| 1942 | + | ||
| 1943 | + .bars-pair { | ||
| 1944 | + display: flex; | ||
| 1945 | + align-items: flex-end; | ||
| 1946 | + gap: 4px; | ||
| 1947 | + flex: 1; | ||
| 1948 | + } | ||
| 1949 | + | ||
| 1950 | + .bar-a, .bar-b { | ||
| 1951 | + width: 20px; | ||
| 1952 | + border-radius: 4px 4px 0 0; | ||
| 1953 | + min-height: 4px; | ||
| 1954 | + background: var(--bar-color); | ||
| 1955 | + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), filter 0.25s ease; | ||
| 1956 | + } | ||
| 1957 | + | ||
| 1958 | + .bar-group.active .bar-a, | ||
| 1959 | + .bar-group.active .bar-b { | ||
| 1960 | + filter: brightness(1.15); | ||
| 1961 | + } | ||
| 1962 | + | ||
| 1963 | + .bar-group { | ||
| 1964 | + cursor: pointer; | ||
| 1965 | + padding: 0.5rem 0.35rem; | ||
| 1966 | + margin: -0.5rem -0.35rem; | ||
| 1967 | + border-radius: 6px; | ||
| 1968 | + transition: background 0.3s cubic-bezier(0.4, 0, 0.2, 1); | ||
| 1969 | + } | ||
| 1970 | + | ||
| 1971 | + .bar-group.active { | ||
| 1972 | + background: rgba(42, 157, 143, 0.15); | ||
| 1973 | + } | ||
| 1974 | + | ||
| 1975 | + .bar-label.highlight { | ||
| 1976 | + color: #F5F0E8; | ||
| 1977 | + font-weight: 500; | ||
| 1978 | + transition: color 0.25s ease; | ||
| 1979 | + } | ||
| 1980 | + | ||
| 1981 | + .chart-legend { | ||
| 1982 | + display: flex; | ||
| 1983 | + justify-content: center; | ||
| 1984 | + gap: 2rem; | ||
| 1985 | + margin-top: 0.5rem; | ||
| 1986 | + margin-bottom: 0.75rem; | ||
| 1987 | + } | ||
| 1988 | + | ||
| 1989 | + .legend-item { | ||
| 1990 | + display: flex; | ||
| 1991 | + align-items: center; | ||
| 1992 | + gap: 0.5rem; | ||
| 1993 | + font-size: 0.75rem; | ||
| 1994 | + color: #8A8578; | ||
| 1995 | + } | ||
| 1996 | + | ||
| 1997 | + .legend-dot { | ||
| 1998 | + width: 10px; | ||
| 1999 | + height: 10px; | ||
| 2000 | + border-radius: 3px; | ||
| 2001 | + } | ||
| 2002 | + | ||
| 2003 | + /* KPIs comparativos - tabla */ | ||
| 2004 | + .kpis-tabla { | ||
| 2005 | + background: transparent; | ||
| 2006 | + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); | ||
| 2007 | + border-radius: 10px; | ||
| 2008 | + padding: 0.5rem 1rem; | ||
| 2009 | + } | ||
| 2010 | + | ||
| 2011 | + .tabla-header { | ||
| 2012 | + display: grid; | ||
| 2013 | + grid-template-columns: 1fr 1fr 1fr; | ||
| 2014 | + gap: 1rem; | ||
| 2015 | + padding-bottom: 0.375rem; | ||
| 2016 | + border-bottom: 1px solid #2E2E2C; | ||
| 2017 | + margin-bottom: 0.375rem; | ||
| 2018 | + } | ||
| 2019 | + | ||
| 2020 | + .tabla-header .tabla-val { | ||
| 2021 | + font-family: 'DM Mono', monospace; | ||
| 2022 | + font-size: 0.625rem; | ||
| 2023 | + letter-spacing: 0.05em; | ||
| 2024 | + text-transform: uppercase; | ||
| 2025 | + text-align: center; | ||
| 2026 | + } | ||
| 2027 | + | ||
| 2028 | + .tabla-row { | ||
| 2029 | + display: grid; | ||
| 2030 | + grid-template-columns: 1fr 1fr 1fr; | ||
| 2031 | + gap: 1rem; | ||
| 2032 | + padding: 0.25rem 0; | ||
| 2033 | + } | ||
| 2034 | + | ||
| 2035 | + .tabla-label { | ||
| 2036 | + font-family: 'DM Mono', monospace; | ||
| 2037 | + font-size: 0.75rem; | ||
| 2038 | + font-weight: 500; | ||
| 2039 | + color: #8A8578; | ||
| 2040 | + display: flex; | ||
| 2041 | + align-items: center; | ||
| 2042 | + text-transform: uppercase; | ||
| 2043 | + letter-spacing: 0.03em; | ||
| 2044 | + } | ||
| 2045 | + | ||
| 2046 | + .tabla-val { | ||
| 2047 | + font-family: 'DM Mono', monospace; | ||
| 2048 | + font-size: 0.9375rem; | ||
| 2049 | + font-weight: 500; | ||
| 2050 | + color: #F5F0E8; | ||
| 2051 | + text-align: center; | ||
| 2052 | + font-variant-numeric: tabular-nums; | ||
| 2053 | + transition: opacity 0.2s ease; | ||
| 2054 | + } | ||
| 2055 | + | ||
| 2056 | + /* Insight comparador */ | ||
| 2057 | + .comparador-insight { | ||
| 2058 | + text-align: center; | ||
| 2059 | + padding: 1rem 1.5rem; | ||
| 2060 | + background: linear-gradient(90deg, rgba(232,197,71,0.05), rgba(74,139,110,0.05)); | ||
| 2061 | + border-radius: 10px; | ||
| 2062 | + border: 1px solid #2E2E2C; | ||
| 2063 | + font-size: 0.875rem; | ||
| 2064 | + color: #8A8578; | ||
| 2065 | + line-height: 1.6; | ||
| 2066 | + } | ||
| 2067 | + | ||
| 2068 | + .comparador-insight strong { | ||
| 2069 | + color: #F5F0E8; | ||
| 2070 | + } | ||
| 2071 | + | ||
| 2072 | + .insight-year { | ||
| 2073 | + font-family: 'DM Mono', monospace; | ||
| 2074 | + font-size: 0.813rem; | ||
| 2075 | + color: #E8C547; | ||
| 2076 | + } | ||
| 2077 | + | ||
| 2078 | + /* Responsive */ | ||
| 2079 | + @media (max-width: 640px) { | ||
| 2080 | + header, main { | ||
| 2081 | + padding: 1.25rem 1rem; | ||
| 2082 | + } | ||
| 2083 | + | ||
| 2084 | + .title-main { | ||
| 2085 | + flex-direction: column; | ||
| 2086 | + gap: 0.25rem; | ||
| 2087 | + } | ||
| 2088 | + | ||
| 2089 | + .codigo { | ||
| 2090 | + font-size: 1.25rem; | ||
| 2091 | + } | ||
| 2092 | + | ||
| 2093 | + h1 { | ||
| 2094 | + font-size: 1.375rem; | ||
| 2095 | + } | ||
| 2096 | + | ||
| 2097 | + .y-axis { | ||
| 2098 | + display: none; | ||
| 2099 | + } | ||
| 2100 | + | ||
| 2101 | + .chart { | ||
| 2102 | + border-left: none; | ||
| 2103 | + height: 120px; | ||
| 2104 | + } | ||
| 2105 | + | ||
| 2106 | + .kpis { | ||
| 2107 | + grid-template-columns: repeat(2, 1fr); | ||
| 2108 | + } | ||
| 2109 | + | ||
| 2110 | + .kpi-value { | ||
| 2111 | + font-size: 1.125rem; | ||
| 2112 | + } | ||
| 2113 | + | ||
| 2114 | + .top-summary { | ||
| 2115 | + flex-direction: column; | ||
| 2116 | + gap: 0.25rem; | ||
| 2117 | + } | ||
| 2118 | + | ||
| 2119 | + .comparador-modal { | ||
| 2120 | + padding: 1.5rem; | ||
| 2121 | + } | ||
| 2122 | + | ||
| 2123 | + .comparador-selectors { | ||
| 2124 | + flex-direction: column; | ||
| 2125 | + gap: 0.75rem; | ||
| 2126 | + } | ||
| 2127 | + | ||
| 2128 | + .vs { | ||
| 2129 | + display: none; | ||
| 2130 | + } | ||
| 2131 | + | ||
| 2132 | + .kpi-values { | ||
| 2133 | + gap: 1rem; | ||
| 2134 | + } | ||
| 2135 | + | ||
| 2136 | + .kpi-val { | ||
| 2137 | + font-size: 0.75rem; | ||
| 2138 | + min-width: 60px; | ||
| 2139 | + } | ||
| 2140 | + | ||
| 2141 | + .comp-bar { | ||
| 2142 | + width: 12px; | ||
| 2143 | + } | ||
| 2144 | + | ||
| 2145 | + .grouped-chart { | ||
| 2146 | + gap: 0.5rem; | ||
| 2147 | + } | ||
| 2148 | + } | ||
| 2149 | + | ||
| 2150 | + /* ========== TEMA CLARO ========== */ | ||
| 2151 | + .tema-claro { | ||
| 2152 | + background: #FAFAF8; | ||
| 2153 | + color: #1A1A18; | ||
| 2154 | + } | ||
| 2155 | + | ||
| 2156 | + .tema-claro header { | ||
| 2157 | + border-bottom-color: #E5E5E0; | ||
| 2158 | + } | ||
| 2159 | + | ||
| 2160 | + .tema-claro .breadcrumb a { | ||
| 2161 | + color: #8A8578; | ||
| 2162 | + } | ||
| 2163 | + .tema-claro .breadcrumb a:hover { | ||
| 2164 | + color: #1A1A18; | ||
| 2165 | + } | ||
| 2166 | + | ||
| 2167 | + .tema-claro .breadcrumb-sep { | ||
| 2168 | + color: #C5C5C0; | ||
| 2169 | + } | ||
| 2170 | + | ||
| 2171 | + .tema-claro .codigo { | ||
| 2172 | + color: #8A8578; | ||
| 2173 | + } | ||
| 2174 | + | ||
| 2175 | + .tema-claro h1 { | ||
| 2176 | + color: #1A1A18; | ||
| 2177 | + } | ||
| 2178 | + | ||
| 2179 | + .tema-claro .help-icon { | ||
| 2180 | + color: #8A8578; | ||
| 2181 | + } | ||
| 2182 | + .tema-claro .help-icon:hover, | ||
| 2183 | + .tema-claro .help-icon.active { | ||
| 2184 | + color: #C9A227; | ||
| 2185 | + } | ||
| 2186 | + | ||
| 2187 | + .tema-claro .badge { | ||
| 2188 | + background: #F0F0EC; | ||
| 2189 | + border-color: #E5E5E0; | ||
| 2190 | + color: #5A5650; | ||
| 2191 | + } | ||
| 2192 | + | ||
| 2193 | + .tema-claro .years { | ||
| 2194 | + color: #8A8578; | ||
| 2195 | + } | ||
| 2196 | + | ||
| 2197 | + .tema-claro .info-panel { | ||
| 2198 | + background: #F5F5F3; | ||
| 2199 | + border-bottom-color: #E5E5E0; | ||
| 2200 | + } | ||
| 2201 | + | ||
| 2202 | + .tema-claro .info-content h4 { | ||
| 2203 | + color: #8A8578; | ||
| 2204 | + } | ||
| 2205 | + | ||
| 2206 | + .tema-claro .info-content p { | ||
| 2207 | + color: #5A5650; | ||
| 2208 | + } | ||
| 2209 | + | ||
| 2210 | + .tema-claro .variacion { | ||
| 2211 | + border-left-color: #D5D5D0; | ||
| 2212 | + } | ||
| 2213 | + | ||
| 2214 | + .tema-claro .variacion-rango { | ||
| 2215 | + color: #8A8578; | ||
| 2216 | + } | ||
| 2217 | + | ||
| 2218 | + .tema-claro .hijo-link:hover { | ||
| 2219 | + background: rgba(0, 0, 0, 0.03); | ||
| 2220 | + } | ||
| 2221 | + | ||
| 2222 | + .tema-claro .hijo-codigo { | ||
| 2223 | + color: #8A8578; | ||
| 2224 | + } | ||
| 2225 | + | ||
| 2226 | + .tema-claro .hijo-nombre { | ||
| 2227 | + color: #5A5650; | ||
| 2228 | + } | ||
| 2229 | + | ||
| 2230 | + .tema-claro .hijo-link:hover .hijo-nombre { | ||
| 2231 | + color: #1A1A18; | ||
| 2232 | + } | ||
| 2233 | + | ||
| 2234 | + .tema-claro .vista-toggle { | ||
| 2235 | + background: transparent; | ||
| 2236 | + box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08); | ||
| 2237 | + } | ||
| 2238 | + | ||
| 2239 | + .tema-claro .toggle-btn { | ||
| 2240 | + color: #8A8578; | ||
| 2241 | + } | ||
| 2242 | + .tema-claro .toggle-btn:hover { | ||
| 2243 | + color: #5A5650; | ||
| 2244 | + } | ||
| 2245 | + .tema-claro .toggle-btn.active { | ||
| 2246 | + background: #1A1A18; | ||
| 2247 | + color: #FAFAF8; | ||
| 2248 | + } | ||
| 2249 | + | ||
| 2250 | + .tema-claro .entity-selector .selector-label { | ||
| 2251 | + color: #8A8578; | ||
| 2252 | + } | ||
| 2253 | + | ||
| 2254 | + .tema-claro .interaction-hint { | ||
| 2255 | + color: #8A8578; | ||
| 2256 | + } | ||
| 2257 | + | ||
| 2258 | + .tema-claro .selector-btn { | ||
| 2259 | + background: transparent; | ||
| 2260 | + border: none; | ||
| 2261 | + box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08); | ||
| 2262 | + color: #1A1A18; | ||
| 2263 | + } | ||
| 2264 | + .tema-claro .selector-btn:hover { | ||
| 2265 | + box-shadow: inset 0 0 0 1px rgba(0,0,0,0.15); | ||
| 2266 | + } | ||
| 2267 | + .tema-claro .selector-btn svg { | ||
| 2268 | + color: #8A8578; | ||
| 2269 | + } | ||
| 2270 | + | ||
| 2271 | + .tema-claro .dropdown-panel { | ||
| 2272 | + background: #FAFAF8; | ||
| 2273 | + border-color: #E5E5E0; | ||
| 2274 | + } | ||
| 2275 | + | ||
| 2276 | + .tema-claro .dropdown-search { | ||
| 2277 | + border-bottom-color: #E5E5E0; | ||
| 2278 | + } | ||
| 2279 | + | ||
| 2280 | + .tema-claro .search-input { | ||
| 2281 | + background: #F5F5F3; | ||
| 2282 | + border-color: #E5E5E0; | ||
| 2283 | + color: #1A1A18; | ||
| 2284 | + } | ||
| 2285 | + .tema-claro .search-input:focus { | ||
| 2286 | + border-color: #C9A227; | ||
| 2287 | + } | ||
| 2288 | + .tema-claro .search-input::placeholder { | ||
| 2289 | + color: #8A8578; | ||
| 2290 | + } | ||
| 2291 | + | ||
| 2292 | + .tema-claro .dropdown-item { | ||
| 2293 | + color: #1A1A18; | ||
| 2294 | + } | ||
| 2295 | + .tema-claro .dropdown-item:hover { | ||
| 2296 | + background: rgba(0,0,0,0.03); | ||
| 2297 | + } | ||
| 2298 | + .tema-claro .dropdown-item.active { | ||
| 2299 | + background: rgba(201,162,39,0.12); | ||
| 2300 | + } | ||
| 2301 | + | ||
| 2302 | + .tema-claro .entity-code { | ||
| 2303 | + color: #8A8578; | ||
| 2304 | + } | ||
| 2305 | + | ||
| 2306 | + .tema-claro .chart-title { | ||
| 2307 | + color: #8A8578; | ||
| 2308 | + } | ||
| 2309 | + | ||
| 2310 | + .tema-claro .chart-period { | ||
| 2311 | + color: #333333; | ||
| 2312 | + } | ||
| 2313 | + | ||
| 2314 | + .tema-claro .y-label { | ||
| 2315 | + color: #8A8578; | ||
| 2316 | + } | ||
| 2317 | + | ||
| 2318 | + .tema-claro .y-axis-sticky { | ||
| 2319 | + background: #FAFAF8; | ||
| 2320 | + } | ||
| 2321 | + | ||
| 2322 | + .tema-claro .chart-wrapper-scroll::-webkit-scrollbar-thumb { | ||
| 2323 | + background: #D5D5D0; | ||
| 2324 | + } | ||
| 2325 | + | ||
| 2326 | + .tema-claro .chart { | ||
| 2327 | + border-left-color: #E5E5E0; | ||
| 2328 | + } | ||
| 2329 | + | ||
| 2330 | + .tema-claro .bar-container .bar { | ||
| 2331 | + background: #2a9d8f; | ||
| 2332 | + } | ||
| 2333 | + | ||
| 2334 | + .tema-claro .bar-container .bar.hovered { | ||
| 2335 | + background: #C9A751; | ||
| 2336 | + } | ||
| 2337 | + | ||
| 2338 | + /* Las barras de comparación mantienen su --bar-color */ | ||
| 2339 | + .tema-claro .bar-a, | ||
| 2340 | + .tema-claro .bar-b { | ||
| 2341 | + background: var(--bar-color); | ||
| 2342 | + } | ||
| 2343 | + | ||
| 2344 | + .tema-claro .bar-label { | ||
| 2345 | + color: #333333; | ||
| 2346 | + } | ||
| 2347 | + | ||
| 2348 | + .tema-claro .kpis { | ||
| 2349 | + background: transparent; | ||
| 2350 | + } | ||
| 2351 | + | ||
| 2352 | + .tema-claro .kpi { | ||
| 2353 | + background: transparent; | ||
| 2354 | + box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08); | ||
| 2355 | + } | ||
| 2356 | + | ||
| 2357 | + .tema-claro .kpi-value { | ||
| 2358 | + color: #1A1A18; | ||
| 2359 | + } | ||
| 2360 | + | ||
| 2361 | + .tema-claro .kpi-label { | ||
| 2362 | + color: #8A8578; | ||
| 2363 | + } | ||
| 2364 | + | ||
| 2365 | + .tema-claro .top-section h3 { | ||
| 2366 | + color: #8A8578; | ||
| 2367 | + } | ||
| 2368 | + | ||
| 2369 | + .tema-claro .top-context { | ||
| 2370 | + color: #8A8578; | ||
| 2371 | + } | ||
| 2372 | + | ||
| 2373 | + .tema-claro .top-item { | ||
| 2374 | + background: transparent; | ||
| 2375 | + box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08); | ||
| 2376 | + } | ||
| 2377 | + | ||
| 2378 | + .tema-claro .top-rank { | ||
| 2379 | + color: #8A8578; | ||
| 2380 | + } | ||
| 2381 | + | ||
| 2382 | + .tema-claro .top-name { | ||
| 2383 | + color: #1A1A18; | ||
| 2384 | + } | ||
| 2385 | + | ||
| 2386 | + .tema-claro .top-bar-bg { | ||
| 2387 | + background: #F0F0EC; | ||
| 2388 | + } | ||
| 2389 | + | ||
| 2390 | + .tema-claro .top-bar { | ||
| 2391 | + background: #C9A751; | ||
| 2392 | + } | ||
| 2393 | + | ||
| 2394 | + .tema-claro .top-item:hover { | ||
| 2395 | + box-shadow: inset 0 0 0 1px rgba(201, 167, 81, 0.5); | ||
| 2396 | + } | ||
| 2397 | + | ||
| 2398 | + .tema-claro .top-item:hover .top-bar { | ||
| 2399 | + background: #B8963F; | ||
| 2400 | + } | ||
| 2401 | + | ||
| 2402 | + .tema-claro .top-pct { | ||
| 2403 | + color: #333333; | ||
| 2404 | + } | ||
| 2405 | + | ||
| 2406 | + /* Comparador - tema claro */ | ||
| 2407 | + .tema-claro .comparador-selectors .selector-group select { | ||
| 2408 | + background: transparent; | ||
| 2409 | + border: none; | ||
| 2410 | + box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08); | ||
| 2411 | + color: #1A1A18; | ||
| 2412 | + } | ||
| 2413 | + .tema-claro .comparador-selectors .selector-group select:focus { | ||
| 2414 | + box-shadow: inset 0 0 0 1px rgba(0,0,0,0.15); | ||
| 2415 | + outline: none; | ||
| 2416 | + } | ||
| 2417 | + | ||
| 2418 | + .tema-claro .comparador-placeholder { | ||
| 2419 | + background: rgba(0,0,0,0.02); | ||
| 2420 | + border-color: #E5E5E0; | ||
| 2421 | + } | ||
| 2422 | + | ||
| 2423 | + .tema-claro .comparador-placeholder p { | ||
| 2424 | + color: #8A8578; | ||
| 2425 | + } | ||
| 2426 | + | ||
| 2427 | + .tema-claro .comparador-placeholder strong { | ||
| 2428 | + color: #5A5650; | ||
| 2429 | + } | ||
| 2430 | + | ||
| 2431 | + .tema-claro .rango-label { | ||
| 2432 | + color: #8A8578; | ||
| 2433 | + } | ||
| 2434 | + | ||
| 2435 | + .tema-claro .rango-btn { | ||
| 2436 | + color: #8A8578; | ||
| 2437 | + } | ||
| 2438 | + | ||
| 2439 | + .tema-claro .rango-btn:hover { | ||
| 2440 | + color: #5A5650; | ||
| 2441 | + background: rgba(0,0,0,0.05); | ||
| 2442 | + } | ||
| 2443 | + | ||
| 2444 | + .tema-claro .rango-btn.active { | ||
| 2445 | + color: #1A1A18; | ||
| 2446 | + background: rgba(0,0,0,0.08); | ||
| 2447 | + } | ||
| 2448 | + | ||
| 2449 | + .tema-claro .vs { | ||
| 2450 | + color: #8A8578; | ||
| 2451 | + } | ||
| 2452 | + | ||
| 2453 | + .tema-claro .chart-legend .legend-item { | ||
| 2454 | + color: #5A5650; | ||
| 2455 | + } | ||
| 2456 | + | ||
| 2457 | + .tema-claro .kpis-tabla { | ||
| 2458 | + background: transparent; | ||
| 2459 | + box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08); | ||
| 2460 | + } | ||
| 2461 | + | ||
| 2462 | + .tema-claro .tabla-header { | ||
| 2463 | + border-bottom-color: #E5E5E0; | ||
| 2464 | + } | ||
| 2465 | + | ||
| 2466 | + .tema-claro .tabla-label { | ||
| 2467 | + color: #5A5650; | ||
| 2468 | + } | ||
| 2469 | + | ||
| 2470 | + .tema-claro .tabla-val { | ||
| 2471 | + color: #1A1A18; | ||
| 2472 | + } | ||
| 2473 | + | ||
| 2474 | + .tema-claro .bar-group.active { | ||
| 2475 | + background: rgba(42, 157, 143, 0.12); | ||
| 2476 | + } | ||
| 2477 | + | ||
| 2478 | + .tema-claro .bar-label.highlight { | ||
| 2479 | + color: #1A1A18; | ||
| 2480 | + } | ||
| 2481 | +</style> | ... | ... |
| ... | @@ -3,6 +3,7 @@ | ... | @@ -3,6 +3,7 @@ |
| 3 | import { tweened } from 'svelte/motion'; | 3 | import { tweened } from 'svelte/motion'; |
| 4 | import { cubicOut } from 'svelte/easing'; | 4 | import { cubicOut } from 'svelte/easing'; |
| 5 | import { supabase } from '$lib/supabase'; | 5 | import { supabase } from '$lib/supabase'; |
| 6 | + import Spinner from '$lib/components/ui/Spinner.svelte'; | ||
| 6 | 7 | ||
| 7 | // ══════════════════════════════════════════════════════════════ | 8 | // ══════════════════════════════════════════════════════════════ |
| 8 | // ESTADO GENERAL | 9 | // ESTADO GENERAL |
| ... | @@ -886,7 +887,7 @@ | ... | @@ -886,7 +887,7 @@ |
| 886 | </div> | 887 | </div> |
| 887 | {:else if loadingComparacion} | 888 | {:else if loadingComparacion} |
| 888 | <div class="comparador-placeholder"> | 889 | <div class="comparador-placeholder"> |
| 889 | - <p>Cargando datos...</p> | 890 | + <Spinner size={48} color="var(--theme-texto, #5A5650)" /> |
| 890 | </div> | 891 | </div> |
| 891 | {:else if añosComparacion.length === 0} | 892 | {:else if añosComparacion.length === 0} |
| 892 | <div class="comparador-placeholder"> | 893 | <div class="comparador-placeholder"> | ... | ... |
-
Please register or login to post a comment