Showing
10 changed files
with
1477 additions
and
38 deletions
| ... | @@ -45,7 +45,7 @@ | ... | @@ -45,7 +45,7 @@ |
| 45 | scaleBand() | 45 | scaleBand() |
| 46 | .domain(data.map(d => d.año)) | 46 | .domain(data.map(d => d.año)) |
| 47 | .range([0, 100]) // porcentaje | 47 | .range([0, 100]) // porcentaje |
| 48 | - .padding(0.2) | 48 | + .padding(0.6) |
| 49 | ); | 49 | ); |
| 50 | 50 | ||
| 51 | // Para CSS bottom positioning: 0 → 0%, max → 100% | 51 | // Para CSS bottom positioning: 0 → 0%, max → 100% |
| ... | @@ -112,15 +112,11 @@ | ... | @@ -112,15 +112,11 @@ |
| 112 | <!-- Eje X --> | 112 | <!-- Eje X --> |
| 113 | <div class="x-axis" style="height: {marginBottom}px; left: {marginLeft}px; right: {marginRight}px"> | 113 | <div class="x-axis" style="height: {marginBottom}px; left: {marginLeft}px; right: {marginRight}px"> |
| 114 | {#each data as d, i} | 114 | {#each data as d, i} |
| 115 | - {@const isFirst = i === 0} | ||
| 116 | - {@const isLast = i === data.length - 1} | ||
| 117 | - {@const isMid = i === midIndex && data.length > 5} | ||
| 118 | - {@const showLabel = isFirst || isLast || isMid || hoveredYear === d.año} | ||
| 119 | <span | 115 | <span |
| 120 | - class="x-label" | 116 | + class="x-label visible" |
| 121 | - class:visible={showLabel} | 117 | + class:hovered={hoveredYear === d.año} |
| 122 | > | 118 | > |
| 123 | - {d.año} | 119 | + {String(d.año).slice(-2)} |
| 124 | </span> | 120 | </span> |
| 125 | {/each} | 121 | {/each} |
| 126 | </div> | 122 | </div> |
| ... | @@ -170,7 +166,7 @@ | ... | @@ -170,7 +166,7 @@ |
| 170 | bottom: 0; | 166 | bottom: 0; |
| 171 | display: flex; | 167 | display: flex; |
| 172 | align-items: flex-end; | 168 | align-items: flex-end; |
| 173 | - gap: 2px; | 169 | + gap: 4px; |
| 174 | } | 170 | } |
| 175 | 171 | ||
| 176 | .bar-container { | 172 | .bar-container { |
| ... | @@ -178,11 +174,12 @@ | ... | @@ -178,11 +174,12 @@ |
| 178 | height: 100%; | 174 | height: 100%; |
| 179 | display: flex; | 175 | display: flex; |
| 180 | align-items: flex-end; | 176 | align-items: flex-end; |
| 177 | + justify-content: center; | ||
| 181 | cursor: default; | 178 | cursor: default; |
| 182 | } | 179 | } |
| 183 | 180 | ||
| 184 | .bar { | 181 | .bar { |
| 185 | - width: 100%; | 182 | + width: 80%; |
| 186 | background: #c4897d; | 183 | background: #c4897d; |
| 187 | border-radius: 4px 4px 0 0; | 184 | border-radius: 4px 4px 0 0; |
| 188 | transition: opacity 0.1s ease; | 185 | transition: opacity 0.1s ease; |
| ... | @@ -206,20 +203,27 @@ | ... | @@ -206,20 +203,27 @@ |
| 206 | bottom: 0; | 203 | bottom: 0; |
| 207 | display: flex; | 204 | display: flex; |
| 208 | align-items: center; | 205 | align-items: center; |
| 206 | + gap: 4px; | ||
| 209 | } | 207 | } |
| 210 | 208 | ||
| 211 | .x-label { | 209 | .x-label { |
| 212 | flex: 1; | 210 | flex: 1; |
| 213 | text-align: center; | 211 | text-align: center; |
| 214 | - font-family: 'Qanelas', var(--font-sans); | 212 | + font-family: 'DM Mono', monospace; |
| 215 | - font-size: 0.6875rem; | 213 | + font-size: 0.5625rem; |
| 216 | color: var(--theme-texto); | 214 | color: var(--theme-texto); |
| 217 | opacity: 0; | 215 | opacity: 0; |
| 218 | transition: opacity 0.2s ease; | 216 | transition: opacity 0.2s ease; |
| 219 | } | 217 | } |
| 220 | 218 | ||
| 221 | .x-label.visible { | 219 | .x-label.visible { |
| 220 | + opacity: 0.5; | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + .x-label.hovered { | ||
| 222 | opacity: 1; | 224 | opacity: 1; |
| 225 | + color: var(--theme-titulo); | ||
| 226 | + font-weight: 600; | ||
| 223 | } | 227 | } |
| 224 | 228 | ||
| 225 | .chart-empty { | 229 | .chart-empty { | ... | ... |
| ... | @@ -101,12 +101,14 @@ | ... | @@ -101,12 +101,14 @@ |
| 101 | padding: 0.125rem 0; | 101 | padding: 0.125rem 0; |
| 102 | background: transparent; | 102 | background: transparent; |
| 103 | border: none; | 103 | border: none; |
| 104 | - border-bottom: 1px dotted #333333; | 104 | + border-bottom: 1px dotted #666666; |
| 105 | color: var(--theme-titulo); | 105 | color: var(--theme-titulo); |
| 106 | font-size: 1rem; | 106 | font-size: 1rem; |
| 107 | font-weight: 500; | 107 | font-weight: 500; |
| 108 | cursor: pointer; | 108 | cursor: pointer; |
| 109 | transition: border-color 0.2s; | 109 | transition: border-color 0.2s; |
| 110 | + max-width: 100%; | ||
| 111 | + min-width: 0; | ||
| 110 | } | 112 | } |
| 111 | 113 | ||
| 112 | .entity-btn:hover { | 114 | .entity-btn:hover { |
| ... | @@ -115,6 +117,9 @@ | ... | @@ -115,6 +117,9 @@ |
| 115 | 117 | ||
| 116 | .entity-label { | 118 | .entity-label { |
| 117 | text-align: left; | 119 | text-align: left; |
| 120 | + overflow: hidden; | ||
| 121 | + text-overflow: ellipsis; | ||
| 122 | + white-space: nowrap; | ||
| 118 | } | 123 | } |
| 119 | 124 | ||
| 120 | .chevron { | 125 | .chevron { |
| ... | @@ -132,7 +137,8 @@ | ... | @@ -132,7 +137,8 @@ |
| 132 | position: absolute; | 137 | position: absolute; |
| 133 | top: calc(100% + 4px); | 138 | top: calc(100% + 4px); |
| 134 | left: 0; | 139 | left: 0; |
| 135 | - min-width: 280px; | 140 | + right: 0; |
| 141 | + min-width: min(280px, calc(100vw - 2rem)); | ||
| 136 | background: var(--theme-surface); | 142 | background: var(--theme-surface); |
| 137 | border: 1px solid var(--theme-borde); | 143 | border: 1px solid var(--theme-borde); |
| 138 | border-radius: 10px; | 144 | border-radius: 10px; | ... | ... |
| 1 | <script> | 1 | <script> |
| 2 | import { goto } from '$app/navigation'; | 2 | import { goto } from '$app/navigation'; |
| 3 | - import { indexLoaded } from '$lib/stores/searchStore'; | ||
| 4 | - import { search } from '$lib/services/search'; | ||
| 5 | 3 | ||
| 6 | let { | 4 | let { |
| 7 | preserveVista = false, | 5 | preserveVista = false, |
| ... | @@ -16,20 +14,46 @@ | ... | @@ -16,20 +14,46 @@ |
| 16 | let selectedIdx = $state(-1); | 14 | let selectedIdx = $state(-1); |
| 17 | let debounceTimer; | 15 | let debounceTimer; |
| 18 | 16 | ||
| 19 | - function handleSearchInput(e) { | 17 | + function parseMetadatos(meta) { |
| 20 | - const val = e.target.value; | 18 | + if (!meta) return {}; |
| 21 | - clearTimeout(debounceTimer); | 19 | + if (typeof meta === 'object') return meta; |
| 22 | - debounceTimer = setTimeout(() => { | 20 | + try { return JSON.parse(meta); } catch { return {}; } |
| 23 | - if (val.length >= 2) { | 21 | + } |
| 22 | + | ||
| 23 | + async function doSearch(query) { | ||
| 24 | isSearching = true; | 24 | isSearching = true; |
| 25 | - // Buscar y filtrar solo objetos de gasto | 25 | + try { |
| 26 | - const allResults = search(val, 50); | 26 | + const params = new URLSearchParams({ |
| 27 | - localResults = allResults.filter(r => r.tipo === 'objeto_gasto').slice(0, 15); | 27 | + q: query, |
| 28 | + per_page: '20', | ||
| 29 | + is_class: 'true', | ||
| 30 | + class_: 'objeto' | ||
| 31 | + }); | ||
| 32 | + const res = await fetch(`/api/search?${params}`); | ||
| 33 | + if (!res.ok) throw new Error(); | ||
| 34 | + const data = await res.json(); | ||
| 35 | + localResults = (data.hits || []).map(hit => { | ||
| 36 | + const meta = parseMetadatos(hit.document.metadatos); | ||
| 37 | + const codigo = meta.objeto_subpartida || meta.objeto_partida || meta.objeto_subgrupo || meta.objeto_grupo || ''; | ||
| 38 | + const nombre = hit.document.texto; | ||
| 39 | + const highlight = hit.highlights?.[0]?.snippet || nombre; | ||
| 40 | + return { codigo, nombre, highlight }; | ||
| 41 | + }); | ||
| 42 | + selectedIdx = -1; | ||
| 43 | + } catch { | ||
| 44 | + localResults = []; | ||
| 45 | + } | ||
| 28 | isSearching = false; | 46 | isSearching = false; |
| 47 | + } | ||
| 48 | + | ||
| 49 | + function handleSearchInput(e) { | ||
| 50 | + searchVal = e.target.value; | ||
| 51 | + clearTimeout(debounceTimer); | ||
| 52 | + if (searchVal.length >= 2) { | ||
| 53 | + debounceTimer = setTimeout(() => doSearch(searchVal), 200); | ||
| 29 | } else { | 54 | } else { |
| 30 | localResults = []; | 55 | localResults = []; |
| 31 | } | 56 | } |
| 32 | - }, 200); | ||
| 33 | } | 57 | } |
| 34 | 58 | ||
| 35 | function handleSearchKeydown(e) { | 59 | function handleSearchKeydown(e) { |
| ... | @@ -86,7 +110,6 @@ | ... | @@ -86,7 +110,6 @@ |
| 86 | onfocus={() => searchFocused = true} | 110 | onfocus={() => searchFocused = true} |
| 87 | onblur={handleSearchBlur} | 111 | onblur={handleSearchBlur} |
| 88 | placeholder="Buscar objeto..." | 112 | placeholder="Buscar objeto..." |
| 89 | - disabled={!$indexLoaded} | ||
| 90 | /> | 113 | /> |
| 91 | {#if searchVal} | 114 | {#if searchVal} |
| 92 | <button class="search-clear" onclick={handleClear}> | 115 | <button class="search-clear" onclick={handleClear}> |
| ... | @@ -110,7 +133,7 @@ | ... | @@ -110,7 +133,7 @@ |
| 110 | onmouseenter={() => selectedIdx = i} | 133 | onmouseenter={() => selectedIdx = i} |
| 111 | > | 134 | > |
| 112 | <span class="item-code">{result.codigo}</span> | 135 | <span class="item-code">{result.codigo}</span> |
| 113 | - <span class="item-name">{result.nombre}</span> | 136 | + <span class="item-name">{@html result.highlight}</span> |
| 114 | </button> | 137 | </button> |
| 115 | {/each} | 138 | {/each} |
| 116 | {/if} | 139 | {/if} |
| ... | @@ -229,4 +252,11 @@ | ... | @@ -229,4 +252,11 @@ |
| 229 | font-size: 0.8125rem; | 252 | font-size: 0.8125rem; |
| 230 | color: var(--theme-titulo); | 253 | color: var(--theme-titulo); |
| 231 | } | 254 | } |
| 255 | + | ||
| 256 | + :global(.item-name mark) { | ||
| 257 | + background: rgba(201, 167, 81, 0.3); | ||
| 258 | + color: inherit; | ||
| 259 | + border-radius: 2px; | ||
| 260 | + padding: 0 1px; | ||
| 261 | + } | ||
| 232 | </style> | 262 | </style> | ... | ... |
| ... | @@ -72,13 +72,13 @@ | ... | @@ -72,13 +72,13 @@ |
| 72 | 72 | ||
| 73 | // Detectar tipos de clasificadores presentes en los resultados | 73 | // Detectar tipos de clasificadores presentes en los resultados |
| 74 | // Mapeo inverso: API class_ → id interno del clasificador | 74 | // Mapeo inverso: API class_ → id interno del clasificador |
| 75 | - const CLASS_REVERSE_MAP = { ubigeo: 'geografico', acteco: 'sectores' }; | 75 | + const CLASS_REVERSE_MAP = { ubigeo: 'geografico', acteco: 'sectores', finalidad: 'finfun' }; |
| 76 | 76 | ||
| 77 | // Filtrar hits según clasificadores seleccionados (la API puede devolver tipos extra) | 77 | // Filtrar hits según clasificadores seleccionados (la API puede devolver tipos extra) |
| 78 | $: filteredHits = (() => { | 78 | $: filteredHits = (() => { |
| 79 | if (searchMode !== 'clasificadores' || selectedClassifiers.length === 0) return allHits; | 79 | if (searchMode !== 'clasificadores' || selectedClassifiers.length === 0) return allHits; |
| 80 | const allowedClasses = new Set(selectedClassifiers.map(c => { | 80 | const allowedClasses = new Set(selectedClassifiers.map(c => { |
| 81 | - const map = { geografico: 'ubigeo', sectores: 'acteco' }; | 81 | + const map = { geografico: 'ubigeo', sectores: 'acteco', finfun: 'finalidad' }; |
| 82 | return map[c] || c; | 82 | return map[c] || c; |
| 83 | })); | 83 | })); |
| 84 | return allHits.filter(h => allowedClasses.has(h.document.class_)); | 84 | return allHits.filter(h => allowedClasses.has(h.document.class_)); |
| ... | @@ -86,7 +86,12 @@ | ... | @@ -86,7 +86,12 @@ |
| 86 | 86 | ||
| 87 | $: activeClassTypes = (() => { | 87 | $: activeClassTypes = (() => { |
| 88 | const types = new Set(); | 88 | const types = new Set(); |
| 89 | - filteredHits.forEach(h => { if (h.document.class_) types.add(h.document.class_); }); | 89 | + filteredHits.forEach(h => { |
| 90 | + if (h.document.class_) { | ||
| 91 | + const normalized = CLASS_REVERSE_MAP[h.document.class_] || h.document.class_; | ||
| 92 | + types.add(normalized); | ||
| 93 | + } | ||
| 94 | + }); | ||
| 90 | return types; | 95 | return types; |
| 91 | })(); | 96 | })(); |
| 92 | $: isMixedClassSearch = activeClassTypes.size > 1; | 97 | $: isMixedClassSearch = activeClassTypes.size > 1; |
| ... | @@ -154,14 +159,14 @@ | ... | @@ -154,14 +159,14 @@ |
| 154 | if (isMixedClassSearch || searchMode === 'todo') { | 159 | if (isMixedClassSearch || searchMode === 'todo') { |
| 155 | // Filtrar duplicados de finfun | 160 | // Filtrar duplicados de finfun |
| 156 | const filtered = filteredHits.filter(hit => { | 161 | const filtered = filteredHits.filter(hit => { |
| 157 | - if (hit.document.class_ !== 'finfun') return true; | 162 | + if (hit.document.class_ !== 'finfun' && hit.document.class_ !== 'finalidad') return true; |
| 158 | const m = parseMetadatos(hit.document.metadatos); | 163 | const m = parseMetadatos(hit.document.metadatos); |
| 159 | return !(m.finfun_funcion === 0 && m.finfun_grpfuncion !== undefined); | 164 | return !(m.finfun_funcion === 0 && m.finfun_grpfuncion !== undefined); |
| 160 | }); | 165 | }); |
| 161 | 166 | ||
| 162 | const groups = {}; | 167 | const groups = {}; |
| 163 | filtered.forEach(hit => { | 168 | filtered.forEach(hit => { |
| 164 | - const type = hit.document.class_ || 'otros'; | 169 | + const type = CLASS_REVERSE_MAP[hit.document.class_] || hit.document.class_ || 'otros'; |
| 165 | const groupName = CLASS_LABELS[type] || type; | 170 | const groupName = CLASS_LABELS[type] || type; |
| 166 | if (!groups[groupName]) { | 171 | if (!groups[groupName]) { |
| 167 | groups[groupName] = { name: groupName, hits: [], totalMonto: 0, classType: type }; | 172 | groups[groupName] = { name: groupName, hits: [], totalMonto: 0, classType: type }; |
| ... | @@ -692,7 +697,7 @@ | ... | @@ -692,7 +697,7 @@ |
| 692 | const isClassResult = hit.document.is_class === true; | 697 | const isClassResult = hit.document.is_class === true; |
| 693 | 698 | ||
| 694 | let url; | 699 | let url; |
| 695 | - const isFinfunClass = hit.document.class_ === 'finfun'; | 700 | + const isFinfunClass = hit.document.class_ === 'finfun' || hit.document.class_ === 'finalidad'; |
| 696 | 701 | ||
| 697 | const isUbigeoClass = hit.document.class_ === 'ubigeo'; | 702 | const isUbigeoClass = hit.document.class_ === 'ubigeo'; |
| 698 | 703 | ||
| ... | @@ -1207,7 +1212,7 @@ | ... | @@ -1207,7 +1212,7 @@ |
| 1207 | {#each group.hits.slice(0, maxVisible) as hit, i (getHitKey(hit, gi * 1000 + i))} | 1212 | {#each group.hits.slice(0, maxVisible) as hit, i (getHitKey(hit, gi * 1000 + i))} |
| 1208 | {@const meta = parseMetadatos(hit.document.metadatos)} | 1213 | {@const meta = parseMetadatos(hit.document.metadatos)} |
| 1209 | {@const globalIdx = flatHitsForNav.indexOf(hit)} | 1214 | {@const globalIdx = flatHitsForNav.indexOf(hit)} |
| 1210 | - {@const hitClass = hit.document.class_} | 1215 | + {@const hitClass = CLASS_REVERSE_MAP[hit.document.class_] || hit.document.class_} |
| 1211 | {#if hitClass === 'entidad'} | 1216 | {#if hitClass === 'entidad'} |
| 1212 | {@const isDA = meta.da && meta.entidad_desc_entidad} | 1217 | {@const isDA = meta.da && meta.entidad_desc_entidad} |
| 1213 | {@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null} | 1218 | {@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null} |
| ... | @@ -1323,10 +1328,10 @@ | ... | @@ -1323,10 +1328,10 @@ |
| 1323 | {@const subarea = isEntidadClass ? extractAfterHyphen(meta.entidad_desc_subarea) : null} | 1328 | {@const subarea = isEntidadClass ? extractAfterHyphen(meta.entidad_desc_subarea) : null} |
| 1324 | {@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null} | 1329 | {@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null} |
| 1325 | {@const isObjetoClass = hit.document.class_ === 'objeto'} | 1330 | {@const isObjetoClass = hit.document.class_ === 'objeto'} |
| 1326 | - {@const isFinfunClass = hit.document.class_ === 'finfun'} | 1331 | + {@const isFinfunClass = hit.document.class_ === 'finfun' || hit.document.class_ === 'finalidad'} |
| 1327 | {@const isUbigeoClass = hit.document.class_ === 'ubigeo'} | 1332 | {@const isUbigeoClass = hit.document.class_ === 'ubigeo'} |
| 1328 | {@const isClassResult = hit.document.is_class === true} | 1333 | {@const isClassResult = hit.document.is_class === true} |
| 1329 | - {@const hitType = hit.document.class_} | 1334 | + {@const hitType = CLASS_REVERSE_MAP[hit.document.class_] || hit.document.class_} |
| 1330 | <a | 1335 | <a |
| 1331 | href={isEntidadClass | 1336 | href={isEntidadClass |
| 1332 | ? `/entidad/${meta.da ? `${meta.entidad}.${meta.da}` : meta.entidad}` | 1337 | ? `/entidad/${meta.da ? `${meta.entidad}.${meta.da}` : meta.entidad}` | ... | ... |
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
| ... | @@ -28,8 +28,8 @@ | ... | @@ -28,8 +28,8 @@ |
| 28 | let topEntidadesPorAño = $state({}); | 28 | let topEntidadesPorAño = $state({}); |
| 29 | 29 | ||
| 30 | const POBLACION = 12000000; | 30 | const POBLACION = 12000000; |
| 31 | - const objetoCodigo = '11700'; | 31 | + let objetoCodigo = $derived(objetoData.objeto); |
| 32 | - const objetoNivel = 'subpartida'; | 32 | + let objetoNivel = $derived(objetoData.nivel); |
| 33 | 33 | ||
| 34 | let gastoPerCapita = $derived( | 34 | let gastoPerCapita = $derived( |
| 35 | datosAnuales.map(d => ({ | 35 | datosAnuales.map(d => ({ | ... | ... |
| 1 | +import { supabase } from '$lib/supabase'; | ||
| 2 | +import { error } from '@sveltejs/kit'; | ||
| 3 | + | ||
| 4 | +// Funciones para derivar jerarquía del código objeto | ||
| 5 | +function getGrupoCode(objeto) { | ||
| 6 | + return objeto.charAt(0) + '0000'; | ||
| 7 | +} | ||
| 8 | + | ||
| 9 | +function getSubgrupoCode(objeto) { | ||
| 10 | + return objeto.substring(0, 2) + '000'; | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +function getPartidaCode(objeto) { | ||
| 14 | + return objeto.substring(0, 3) + '00'; | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +function getNivel(objeto) { | ||
| 18 | + if (objeto.endsWith('0000')) return 'grupo'; | ||
| 19 | + if (objeto.endsWith('000')) return 'subgrupo'; | ||
| 20 | + if (objeto.endsWith('00')) return 'partida'; | ||
| 21 | + return 'subpartida'; | ||
| 22 | +} | ||
| 23 | + | ||
| 24 | +export async function load({ params }) { | ||
| 25 | + console.time('[SERVER] Total load objeto'); | ||
| 26 | + const { codigo } = params; | ||
| 27 | + | ||
| 28 | + console.time('[SERVER] Query clas_objetos'); | ||
| 29 | + const { data, error: dbError } = await supabase | ||
| 30 | + .schema('ppto') | ||
| 31 | + .from('clas_objetos') | ||
| 32 | + .select('*') | ||
| 33 | + .eq('objeto', codigo); | ||
| 34 | + console.timeEnd('[SERVER] Query clas_objetos'); | ||
| 35 | + | ||
| 36 | + if (dbError || !data || data.length === 0) { | ||
| 37 | + throw error(404, 'Objeto no encontrado'); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + // Tomar el primer resultado | ||
| 41 | + const objeto = data[0]; | ||
| 42 | + const nivel = objeto.nivel || getNivel(codigo); | ||
| 43 | + | ||
| 44 | + // Obtener jerarquía (padres e hijos) | ||
| 45 | + let padres = []; | ||
| 46 | + let hijos = []; | ||
| 47 | + | ||
| 48 | + console.time('[SERVER] Query padres'); | ||
| 49 | + | ||
| 50 | + // Buscar padres según nivel | ||
| 51 | + if (nivel === 'subpartida') { | ||
| 52 | + // Padres: grupo, subgrupo, partida | ||
| 53 | + const grupoCode = getGrupoCode(codigo); | ||
| 54 | + const subgrupoCode = getSubgrupoCode(codigo); | ||
| 55 | + const partidaCode = getPartidaCode(codigo); | ||
| 56 | + | ||
| 57 | + const { data: padresData } = await supabase | ||
| 58 | + .schema('ppto') | ||
| 59 | + .from('clas_objetos') | ||
| 60 | + .select('*') | ||
| 61 | + .in('objeto', [grupoCode, subgrupoCode, partidaCode]) | ||
| 62 | + .order('objeto'); | ||
| 63 | + | ||
| 64 | + if (padresData) padres = padresData; | ||
| 65 | + } else if (nivel === 'partida') { | ||
| 66 | + // Padres: grupo, subgrupo | ||
| 67 | + const grupoCode = getGrupoCode(codigo); | ||
| 68 | + const subgrupoCode = getSubgrupoCode(codigo); | ||
| 69 | + | ||
| 70 | + const { data: padresData } = await supabase | ||
| 71 | + .schema('ppto') | ||
| 72 | + .from('clas_objetos') | ||
| 73 | + .select('*') | ||
| 74 | + .in('objeto', [grupoCode, subgrupoCode]) | ||
| 75 | + .order('objeto'); | ||
| 76 | + | ||
| 77 | + if (padresData) padres = padresData; | ||
| 78 | + } else if (nivel === 'subgrupo') { | ||
| 79 | + // Padre: grupo | ||
| 80 | + const grupoCode = getGrupoCode(codigo); | ||
| 81 | + | ||
| 82 | + const { data: padresData } = await supabase | ||
| 83 | + .schema('ppto') | ||
| 84 | + .from('clas_objetos') | ||
| 85 | + .select('*') | ||
| 86 | + .eq('objeto', grupoCode); | ||
| 87 | + | ||
| 88 | + if (padresData) padres = padresData; | ||
| 89 | + } | ||
| 90 | + // grupo no tiene padres | ||
| 91 | + | ||
| 92 | + console.timeEnd('[SERVER] Query padres'); | ||
| 93 | + | ||
| 94 | + console.time('[SERVER] Query hijos'); | ||
| 95 | + | ||
| 96 | + // Buscar hijos según nivel | ||
| 97 | + if (nivel === 'grupo') { | ||
| 98 | + // Hijos: subgrupos que empiecen con el mismo dígito | ||
| 99 | + const prefix = codigo.charAt(0); | ||
| 100 | + const { data: hijosData } = await supabase | ||
| 101 | + .schema('ppto') | ||
| 102 | + .from('clas_objetos') | ||
| 103 | + .select('*') | ||
| 104 | + .eq('nivel', 'subgrupo') | ||
| 105 | + .like('objeto', `${prefix}%`) | ||
| 106 | + .order('objeto'); | ||
| 107 | + | ||
| 108 | + hijos = hijosData || []; | ||
| 109 | + } else if (nivel === 'subgrupo') { | ||
| 110 | + // Hijos: partidas que empiecen con los mismos 2 dígitos | ||
| 111 | + const prefix = codigo.substring(0, 2); | ||
| 112 | + const { data: hijosData } = await supabase | ||
| 113 | + .schema('ppto') | ||
| 114 | + .from('clas_objetos') | ||
| 115 | + .select('*') | ||
| 116 | + .eq('nivel', 'partida') | ||
| 117 | + .like('objeto', `${prefix}%`) | ||
| 118 | + .order('objeto'); | ||
| 119 | + | ||
| 120 | + hijos = hijosData || []; | ||
| 121 | + } else if (nivel === 'partida') { | ||
| 122 | + // Hijos: subpartidas que empiecen con los mismos 3 dígitos | ||
| 123 | + const prefix = codigo.substring(0, 3); | ||
| 124 | + const { data: hijosData } = await supabase | ||
| 125 | + .schema('ppto') | ||
| 126 | + .from('clas_objetos') | ||
| 127 | + .select('*') | ||
| 128 | + .eq('nivel', 'subpartida') | ||
| 129 | + .like('objeto', `${prefix}%`) | ||
| 130 | + .order('objeto'); | ||
| 131 | + | ||
| 132 | + hijos = hijosData || []; | ||
| 133 | + } | ||
| 134 | + // subpartida no tiene hijos | ||
| 135 | + | ||
| 136 | + console.timeEnd('[SERVER] Query hijos'); | ||
| 137 | + | ||
| 138 | + console.timeEnd('[SERVER] Total load objeto'); | ||
| 139 | + return { | ||
| 140 | + objeto, | ||
| 141 | + padres, | ||
| 142 | + hijos | ||
| 143 | + }; | ||
| 144 | +} |
src/routes/test/layout/[codigo]/+page.svelte
0 → 100644
| 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 ObjetoSearch from '$lib/components/objeto/ObjetoSearch.svelte'; | ||
| 7 | + import BarChart from '$lib/components/objeto/BarChart.svelte'; | ||
| 8 | + import EntitySelector from '$lib/components/objeto/EntitySelector.svelte'; | ||
| 9 | + import { initIndex } from '$lib/stores/searchStore'; | ||
| 10 | + | ||
| 11 | + let { data } = $props(); | ||
| 12 | + let objetoData = $derived(data.objeto); | ||
| 13 | + let padres = $derived(data.padres); | ||
| 14 | + let hijos = $derived(data.hijos); | ||
| 15 | + | ||
| 16 | + let mounted = $state(false); | ||
| 17 | + let loading = $state(true); | ||
| 18 | + let hoveredYear = $state(null); | ||
| 19 | + let showDefinicionesModal = $state(false); | ||
| 20 | + let showMobileSidebar = $state(false); | ||
| 21 | + let metrica = $state('percapita'); // 'percapita' | 'monto' | ||
| 22 | + | ||
| 23 | + // Parsear descripciones | ||
| 24 | + function parseDescripciones(str) { | ||
| 25 | + if (!str) return []; | ||
| 26 | + try { | ||
| 27 | + const parsed = JSON.parse(str); | ||
| 28 | + return parsed.sort((a, b) => { | ||
| 29 | + const yA = a.rangos?.match(/\d{4}/g) || []; | ||
| 30 | + const yB = b.rangos?.match(/\d{4}/g) || []; | ||
| 31 | + return Math.max(...yB.map(Number), 0) - Math.max(...yA.map(Number), 0); | ||
| 32 | + }); | ||
| 33 | + } catch { return []; } | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + let descripciones = $derived(parseDescripciones(objetoData.descripciones)); | ||
| 37 | + let variaciones = $derived(descripciones.map(d => ({ rango: d.rangos, texto: d.descripcion }))); | ||
| 38 | + | ||
| 39 | + // Estado selector entidad | ||
| 40 | + let entidades = $state([]); | ||
| 41 | + let selectedEntity = $state(null); | ||
| 42 | + let nEntidades = $derived(data.nEntidades); | ||
| 43 | + | ||
| 44 | + // Datos | ||
| 45 | + let datosAnuales = $state([]); | ||
| 46 | + let datosHistorico = $state(null); | ||
| 47 | + let topEntidadesPorAño = $state({}); | ||
| 48 | + | ||
| 49 | + const POBLACION = 12000000; | ||
| 50 | + let objetoCodigo = $derived(objetoData.objeto); | ||
| 51 | + let objetoNivel = $derived(objetoData.nivel); | ||
| 52 | + | ||
| 53 | + let gastoPerCapita = $derived( | ||
| 54 | + datosAnuales.map(d => { | ||
| 55 | + const monto = selectedEntity ? (d.monto ?? d.devengado) : d.total; | ||
| 56 | + const perCapita = d.per_capita ?? (d.devengado ? d.devengado / POBLACION : 0); | ||
| 57 | + return { | ||
| 58 | + año: d.gestion, | ||
| 59 | + monto, | ||
| 60 | + perCapita: metrica === 'percapita' ? perCapita : monto | ||
| 61 | + }; | ||
| 62 | + }) | ||
| 63 | + ); | ||
| 64 | + | ||
| 65 | + let primerAño = $derived(gastoPerCapita.length > 0 ? gastoPerCapita[0].año : 2005); | ||
| 66 | + let ultimoAño = $derived(gastoPerCapita.length > 0 ? gastoPerCapita[gastoPerCapita.length - 1].año : 2025); | ||
| 67 | + | ||
| 68 | + let totalHistorico = $derived(datosHistorico ? (selectedEntity ? datosHistorico.monto : datosHistorico.total) : 0); | ||
| 69 | + let promedioPerCapita = $derived(datosHistorico?.per_capita || 0); | ||
| 70 | + let totalPorcentaje = $derived(datosHistorico?.prop || 0); | ||
| 71 | + let ranking = $derived(datosHistorico?.ranking || 0); | ||
| 72 | + let nPares = $derived(datosHistorico?.n_pares || 0); | ||
| 73 | + | ||
| 74 | + let currentData = $derived( | ||
| 75 | + hoveredYear ? gastoPerCapita.find(g => g.año === hoveredYear) : null | ||
| 76 | + ); | ||
| 77 | + | ||
| 78 | + let displayMonto = $derived(currentData ? currentData.monto : totalHistorico); | ||
| 79 | + let displayMontoLabel = $derived(currentData ? `gastados en ${currentData.año}` : `gastados ${primerAño}-${ultimoAño}`); | ||
| 80 | + let displayPerCapita = $derived(currentData ? currentData.perCapita : promedioPerCapita); | ||
| 81 | + let displayPerCapitaLabel = $derived(currentData ? `por cada boliviano en ${currentData.año}` : 'por cada boliviano'); | ||
| 82 | + let displayPorcentaje = $derived(currentData ? (totalHistorico > 0 ? totalPorcentaje * (currentData.monto / (totalHistorico / gastoPerCapita.length)) : 0) : totalPorcentaje); | ||
| 83 | + let displayPeriodo = $derived(currentData ? currentData.año : `${primerAño}-${ultimoAño}`); | ||
| 84 | + let displayRanking = $derived(`${currentData ? (datosAnuales.find(d => d.gestion === hoveredYear)?.ranking || ranking) : ranking} de ${selectedEntity ? (datosHistorico?.n_entidades || nEntidades) : nPares}`); | ||
| 85 | + let displayPorcentajeLabel = $derived(currentData ? `del gasto en ${currentData.año}` : 'del gasto total'); | ||
| 86 | + let nivelKey = $derived(objetoData.nivel?.toLowerCase()); | ||
| 87 | + let nivelPlural = $derived({ grupo: 'grupos', subgrupo: 'subgrupos', partida: 'partidas', subpartida: 'subpartidas' }[nivelKey] || 'objetos'); | ||
| 88 | + let nivelLabel = $derived({ grupo: 'Grupo', subgrupo: 'Subgrupo', partida: 'Partida', subpartida: 'Subpartida' }[nivelKey] || objetoData.nivel); | ||
| 89 | + let displayRankingLabel = $derived(currentData ? `${nivelPlural} más gastadas en ${currentData.año}` : `${nivelPlural} más gastadas`); | ||
| 90 | + | ||
| 91 | + // Top entidades | ||
| 92 | + let topEntidadesTotal = $derived( | ||
| 93 | + datosHistorico && !selectedEntity ? [ | ||
| 94 | + { nombre: datosHistorico.top1_entidad, porcentaje: datosHistorico.top1_pct }, | ||
| 95 | + { nombre: datosHistorico.top2_entidad, porcentaje: datosHistorico.top2_pct }, | ||
| 96 | + { nombre: datosHistorico.top3_entidad, porcentaje: datosHistorico.top3_pct }, | ||
| 97 | + ].filter(e => e.nombre) : [] | ||
| 98 | + ); | ||
| 99 | + let displayTopEntidades = $derived( | ||
| 100 | + hoveredYear && topEntidadesPorAño[hoveredYear] ? topEntidadesPorAño[hoveredYear] : topEntidadesTotal | ||
| 101 | + ); | ||
| 102 | + | ||
| 103 | + // Tweens | ||
| 104 | + const tw = { duration: 300, easing: cubicOut }; | ||
| 105 | + const twMonto = tweened(0, tw); | ||
| 106 | + const twPerCapita = tweened(0, tw); | ||
| 107 | + const twPorcentaje = tweened(0, tw); | ||
| 108 | + const twTop1 = tweened(0, tw); | ||
| 109 | + const twTop2 = tweened(0, tw); | ||
| 110 | + const twTop3 = tweened(0, tw); | ||
| 111 | + | ||
| 112 | + $effect(() => { twMonto.set(displayMonto); }); | ||
| 113 | + $effect(() => { twPerCapita.set(displayPerCapita); }); | ||
| 114 | + $effect(() => { twPorcentaje.set(displayPorcentaje); }); | ||
| 115 | + $effect(() => { if (displayTopEntidades[0]) twTop1.set(displayTopEntidades[0].porcentaje); }); | ||
| 116 | + $effect(() => { if (displayTopEntidades[1]) twTop2.set(displayTopEntidades[1].porcentaje); }); | ||
| 117 | + $effect(() => { if (displayTopEntidades[2]) twTop3.set(displayTopEntidades[2].porcentaje); }); | ||
| 118 | + | ||
| 119 | + function formatMontoLargo(n) { | ||
| 120 | + if (n >= 1e9) return `${(n / 1e9).toFixed(1)} mil mill.`; | ||
| 121 | + if (n >= 1e6) return `${(n / 1e6).toFixed(1)} mill.`; | ||
| 122 | + if (n >= 1e3) return `${(n / 1e3).toFixed(0)} mil`; | ||
| 123 | + return n.toLocaleString('es-BO'); | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + function formatPerCapita(n) { | ||
| 127 | + return n.toLocaleString('es-BO', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + async function loadData() { | ||
| 131 | + loading = true; | ||
| 132 | + if (selectedEntity) { | ||
| 133 | + const { data } = await supabase.schema('ppto').from('vista_objeto_entidad').select('*') | ||
| 134 | + .eq('objeto', objetoCodigo).eq('nivel', objetoNivel).eq('entidad', selectedEntity.entidad).order('gestion'); | ||
| 135 | + if (data) { | ||
| 136 | + datosHistorico = data.find(d => d.gestion === 0) || null; | ||
| 137 | + datosAnuales = data.filter(d => d.gestion > 0); | ||
| 138 | + topEntidadesPorAño = {}; | ||
| 139 | + } | ||
| 140 | + } else { | ||
| 141 | + const { data } = await supabase.schema('ppto').from('vista_objeto_estado').select('*') | ||
| 142 | + .eq('objeto', objetoCodigo).order('gestion'); | ||
| 143 | + if (data) { | ||
| 144 | + datosHistorico = data.find(d => d.gestion === 0) || null; | ||
| 145 | + datosAnuales = data.filter(d => d.gestion > 0); | ||
| 146 | + topEntidadesPorAño = {}; | ||
| 147 | + datosAnuales.forEach(d => { | ||
| 148 | + topEntidadesPorAño[d.gestion] = [ | ||
| 149 | + { nombre: d.top1_entidad, porcentaje: d.top1_pct }, | ||
| 150 | + { nombre: d.top2_entidad, porcentaje: d.top2_pct }, | ||
| 151 | + { nombre: d.top3_entidad, porcentaje: d.top3_pct }, | ||
| 152 | + ].filter(e => e.nombre); | ||
| 153 | + }); | ||
| 154 | + } | ||
| 155 | + } | ||
| 156 | + loading = false; | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + async function loadEntidades() { | ||
| 160 | + const { data } = await supabase.schema('ppto').from('entidades_por_objeto').select('entidad, entidad_desc') | ||
| 161 | + .eq('objeto', objetoCodigo).eq('nivel', objetoNivel).order('entidad_desc'); | ||
| 162 | + if (data) entidades = data; | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + onMount(async () => { | ||
| 166 | + mounted = true; | ||
| 167 | + initIndex(); | ||
| 168 | + await Promise.all([loadData(), loadEntidades()]); | ||
| 169 | + }); | ||
| 170 | +</script> | ||
| 171 | + | ||
| 172 | +<svelte:head> | ||
| 173 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 174 | + <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" /> | ||
| 175 | +</svelte:head> | ||
| 176 | + | ||
| 177 | +<div class="page" class:mounted> | ||
| 178 | + <nav class="page-nav"> | ||
| 179 | + <a href="/clasificadores/objeto-gasto" class="back-link"> | ||
| 180 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 181 | + <path d="M19 12H5M12 19l-7-7 7-7"/> | ||
| 182 | + </svg> | ||
| 183 | + Clasificador | ||
| 184 | + </a> | ||
| 185 | + </nav> | ||
| 186 | + | ||
| 187 | + <div class="dashboard-layout"> | ||
| 188 | + <!-- Sidebar --> | ||
| 189 | + <aside class="dashboard-context" class:mobile-open={showMobileSidebar}> | ||
| 190 | + <!-- Mobile toggle --> | ||
| 191 | + <button class="mobile-sidebar-toggle" onclick={() => showMobileSidebar = !showMobileSidebar}> | ||
| 192 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 193 | + <circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | ||
| 194 | + </svg> | ||
| 195 | + <span>{showMobileSidebar ? 'Ocultar herramientas' : 'Herramientas'}</span> | ||
| 196 | + </button> | ||
| 197 | + | ||
| 198 | + <div class="context-card"> | ||
| 199 | + <h2 class="context-main-title">Herramientas</h2> | ||
| 200 | + | ||
| 201 | + <!-- Buscar --> | ||
| 202 | + <div class="context-section"> | ||
| 203 | + <h3 class="context-subtitle">Buscar</h3> | ||
| 204 | + <p class="context-hint">Buscar otro objeto de gasto</p> | ||
| 205 | + <ObjetoSearch /> | ||
| 206 | + </div> | ||
| 207 | + | ||
| 208 | + <div class="context-separator"></div> | ||
| 209 | + | ||
| 210 | + <!-- Navegación --> | ||
| 211 | + {#if padres.length > 0 || hijos.length > 0} | ||
| 212 | + <div class="context-section"> | ||
| 213 | + <h3 class="context-subtitle">Navegación</h3> | ||
| 214 | + <p class="context-hint">Partidas relacionadas. Haz click para explorar.</p> | ||
| 215 | + <div class="tree"> | ||
| 216 | + {#each padres as padre, i} | ||
| 217 | + <a href="/objeto/{padre.objeto}" class="tree-node has-tooltip" style="--indent: {i}" data-tooltip="{padre.desc_objeto}"> | ||
| 218 | + <span class="tree-dot"></span> | ||
| 219 | + <span class="tree-code">{padre.objeto}</span> | ||
| 220 | + <span class="tree-name">{padre.desc_objeto}</span> | ||
| 221 | + </a> | ||
| 222 | + {/each} | ||
| 223 | + | ||
| 224 | + <div class="tree-node tree-node-current has-tooltip" style="--indent: {padres.length}" data-tooltip="{objetoData.desc_objeto}"> | ||
| 225 | + <span class="tree-dot"></span> | ||
| 226 | + <span class="tree-code">{objetoData.objeto}</span> | ||
| 227 | + <span class="tree-name">{objetoData.desc_objeto}</span> | ||
| 228 | + </div> | ||
| 229 | + | ||
| 230 | + {#if hijos.length > 0} | ||
| 231 | + <div class="tree-children" style="--indent: {padres.length + 1}"> | ||
| 232 | + {#each hijos as hijo} | ||
| 233 | + <a href="/objeto/{hijo.objeto}" class="tree-node tree-node-child has-tooltip" data-tooltip="{hijo.desc_objeto}"> | ||
| 234 | + <span class="tree-dot"></span> | ||
| 235 | + <span class="tree-code">{hijo.objeto}</span> | ||
| 236 | + <span class="tree-name">{hijo.desc_objeto}</span> | ||
| 237 | + </a> | ||
| 238 | + {/each} | ||
| 239 | + </div> | ||
| 240 | + {/if} | ||
| 241 | + </div> | ||
| 242 | + </div> | ||
| 243 | + | ||
| 244 | + <div class="context-separator"></div> | ||
| 245 | + {/if} | ||
| 246 | + | ||
| 247 | + <!-- Explorar --> | ||
| 248 | + <div class="context-section"> | ||
| 249 | + <h3 class="context-subtitle">Explorar visualizaciones</h3> | ||
| 250 | + <div class="explore-scroll"> | ||
| 251 | + <a href="/objeto/{objetoData.objeto}?vista=comparar" class="explore-card"> | ||
| 252 | + <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 253 | + <rect x="4" y="14" width="4" height="6"/><rect x="10" y="8" width="4" height="12"/><rect x="16" y="4" width="4" height="16"/> | ||
| 254 | + </svg> | ||
| 255 | + <span class="explore-title">Comparar dos entidades</span> | ||
| 256 | + <span class="explore-desc">¿Quién gasta más en {objetoData.desc_objeto?.toLowerCase()}?</span> | ||
| 257 | + </a> | ||
| 258 | + <a href="/clasificadores/objeto-gasto" class="explore-card"> | ||
| 259 | + <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 260 | + <path d="M3 20h18M5 17h2v3H5zM9 14h2v6H9zM13 11h2v9h-2zM17 8h2v12h-2z"/> | ||
| 261 | + </svg> | ||
| 262 | + <span class="explore-title">Ranking</span> | ||
| 263 | + <span class="explore-desc">{nEntidades} entidades que gastan en {objetoData.desc_objeto?.toLowerCase()}</span> | ||
| 264 | + </a> | ||
| 265 | + <a href="/ubicacion" class="explore-card explore-card-disabled"> | ||
| 266 | + <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 267 | + <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/> | ||
| 268 | + </svg> | ||
| 269 | + <span class="explore-title">Mapa</span> | ||
| 270 | + <span class="explore-desc">Dónde se concentra el gasto en {objetoData.desc_objeto?.toLowerCase()}</span> | ||
| 271 | + </a> | ||
| 272 | + <a href="/clasificadores/objeto-gasto?modo=mapa" class="explore-card"> | ||
| 273 | + <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 274 | + <circle cx="12" cy="12" r="10"/><path d="M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10 15 15 0 0 1-4-10A15 15 0 0 1 12 2z"/> | ||
| 275 | + </svg> | ||
| 276 | + <span class="explore-title">Peso relativo</span> | ||
| 277 | + <span class="explore-desc">Cuánto pesa {objetoData.desc_objeto?.toLowerCase()} respecto a otros gastos</span> | ||
| 278 | + </a> | ||
| 279 | + <a href="/clasificadores/objeto-gasto" class="explore-card"> | ||
| 280 | + <svg class="explore-watermark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="0.75"> | ||
| 281 | + <path d="M4 6h16M8 10h12M12 14h8M8 18h12"/> | ||
| 282 | + </svg> | ||
| 283 | + <span class="explore-title">Clasificador</span> | ||
| 284 | + <span class="explore-desc">Ver todos los objetos de gasto y navegar entre ellos</span> | ||
| 285 | + </a> | ||
| 286 | + </div> | ||
| 287 | + </div> | ||
| 288 | + | ||
| 289 | + <div class="context-separator"></div> | ||
| 290 | + | ||
| 291 | + <!-- Comparabilidad --> | ||
| 292 | + <div class="context-section"> | ||
| 293 | + <h3 class="context-subtitle">Comparabilidad</h3> | ||
| 294 | + <div class="vigencia-compact"> | ||
| 295 | + <div class="vigencia-status-icon vigencia-full"> | ||
| 296 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 297 | + <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> | ||
| 298 | + <polyline points="22 4 12 14.01 9 11.01"/> | ||
| 299 | + </svg> | ||
| 300 | + </div> | ||
| 301 | + <div class="vigencia-text"> | ||
| 302 | + <span class="vigencia-main">Comparable en todo el período</span> | ||
| 303 | + <span class="vigencia-sub">2005 – 2025</span> | ||
| 304 | + </div> | ||
| 305 | + </div> | ||
| 306 | + </div> | ||
| 307 | + | ||
| 308 | + <div class="context-separator"></div> | ||
| 309 | + | ||
| 310 | + <!-- Definición --> | ||
| 311 | + <div class="context-section"> | ||
| 312 | + <h3 class="context-subtitle">Definición</h3> | ||
| 313 | + <button class="def-link" onclick={() => showDefinicionesModal = true}> | ||
| 314 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 315 | + <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/> | ||
| 316 | + </svg> | ||
| 317 | + {#if variaciones.length === 1} | ||
| 318 | + Ver definición oficial | ||
| 319 | + {:else} | ||
| 320 | + Ver {variaciones.length} variaciones en la definición | ||
| 321 | + {/if} | ||
| 322 | + </button> | ||
| 323 | + </div> | ||
| 324 | + </div> | ||
| 325 | + </aside> | ||
| 326 | + | ||
| 327 | + <!-- Main --> | ||
| 328 | + <main class="dashboard-main"> | ||
| 329 | + <div class="chart-protagonista"> | ||
| 330 | + <!-- Header --> | ||
| 331 | + <div class="chart-top-row"> | ||
| 332 | + <div class="main-title-pills"> | ||
| 333 | + <span class="pill pill-nivel">{nivelLabel}</span> | ||
| 334 | + <span class="pill pill-codigo">{objetoData.objeto}</span> | ||
| 335 | + </div> | ||
| 336 | + </div> | ||
| 337 | + | ||
| 338 | + <h2 class="main-title-name">{objetoData.desc_objeto}</h2> | ||
| 339 | + | ||
| 340 | + <div class="chart-controls"> | ||
| 341 | + <EntitySelector {entidades} bind:selectedEntity {nEntidades} /> | ||
| 342 | + </div> | ||
| 343 | + | ||
| 344 | + <div class="chart-header"> | ||
| 345 | + <span class="chart-title"> | ||
| 346 | + {metrica === 'percapita' ? 'Bs/habitante' : 'Bs (monto total)'} | ||
| 347 | + <span class="chart-period">· {displayPeriodo}</span> | ||
| 348 | + </span> | ||
| 349 | + <button class="metrica-toggle" onclick={() => metrica = metrica === 'percapita' ? 'monto' : 'percapita'}> | ||
| 350 | + {metrica === 'percapita' ? 'Ver monto real' : 'Ver per cápita'} | ||
| 351 | + </button> | ||
| 352 | + </div> | ||
| 353 | + | ||
| 354 | + {#if loading} | ||
| 355 | + <div class="chart-loading" style="flex: 1; min-height: 100px;"> | ||
| 356 | + <span>Cargando...</span> | ||
| 357 | + </div> | ||
| 358 | + {:else} | ||
| 359 | + <div class="chart-flex-wrapper"> | ||
| 360 | + <BarChart data={gastoPerCapita} bind:hoveredYear height={200} fill={true} /> | ||
| 361 | + </div> | ||
| 362 | + {/if} | ||
| 363 | + | ||
| 364 | + <!-- KPIs --> | ||
| 365 | + <div class="kpis"> | ||
| 366 | + <div class="kpi"> | ||
| 367 | + <span class="kpi-value">{formatMontoLargo(Math.round($twMonto))}</span> | ||
| 368 | + <span class="kpi-label">{displayMontoLabel}</span> | ||
| 369 | + </div> | ||
| 370 | + <div class="kpi"> | ||
| 371 | + <span class="kpi-value">{formatPerCapita($twPerCapita)} Bs.</span> | ||
| 372 | + <span class="kpi-label">{displayPerCapitaLabel}</span> | ||
| 373 | + </div> | ||
| 374 | + <div class="kpi"> | ||
| 375 | + <span class="kpi-value">{$twPorcentaje.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span> | ||
| 376 | + <span class="kpi-label">{displayPorcentajeLabel}</span> | ||
| 377 | + </div> | ||
| 378 | + <div class="kpi"> | ||
| 379 | + <span class="kpi-value">{displayRanking}</span> | ||
| 380 | + <span class="kpi-label">{displayRankingLabel}</span> | ||
| 381 | + </div> | ||
| 382 | + </div> | ||
| 383 | + </div> | ||
| 384 | + | ||
| 385 | + {#if !selectedEntity && displayTopEntidades.length > 0} | ||
| 386 | + <div class="ranking-section"> | ||
| 387 | + <h3 class="ranking-title">Concentración del gasto</h3> | ||
| 388 | + <p class="ranking-desc">Porcentaje del total gastado en {objetoData.desc_objeto?.toLowerCase()} por todo el Estado {hoveredYear ? `en ${hoveredYear}` : `(${primerAño}-${ultimoAño})`}</p> | ||
| 389 | + <div class="ranking-cards"> | ||
| 390 | + {#each displayTopEntidades as ent, i} | ||
| 391 | + {@const pct = i === 0 ? $twTop1 : i === 1 ? $twTop2 : $twTop3} | ||
| 392 | + <div class="ranking-card"> | ||
| 393 | + <span class="ranking-position">#{i + 1}</span> | ||
| 394 | + <span class="ranking-name">{ent.nombre}</span> | ||
| 395 | + <span class="ranking-pct">{pct.toFixed(1)}%</span> | ||
| 396 | + </div> | ||
| 397 | + {/each} | ||
| 398 | + </div> | ||
| 399 | + </div> | ||
| 400 | + {/if} | ||
| 401 | + </main> | ||
| 402 | + </div> | ||
| 403 | + | ||
| 404 | + <!-- Modal de definiciones --> | ||
| 405 | + {#if showDefinicionesModal} | ||
| 406 | + <div class="modal-overlay" onclick={() => showDefinicionesModal = false}> | ||
| 407 | + <div class="modal-content" onclick={(e) => e.stopPropagation()}> | ||
| 408 | + <div class="modal-header"> | ||
| 409 | + <h3>Definiciones de "{objetoData.desc_objeto}"</h3> | ||
| 410 | + <button class="modal-close" onclick={() => showDefinicionesModal = false}> | ||
| 411 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 412 | + <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> | ||
| 413 | + </svg> | ||
| 414 | + </button> | ||
| 415 | + </div> | ||
| 416 | + <div class="modal-body"> | ||
| 417 | + {#each variaciones as v, i} | ||
| 418 | + <div class="def-item" class:def-current={i === 0}> | ||
| 419 | + <div class="def-header"> | ||
| 420 | + <span class="def-period">{v.rango}</span> | ||
| 421 | + {#if i === 0} | ||
| 422 | + <span class="def-badge">Vigente</span> | ||
| 423 | + {/if} | ||
| 424 | + </div> | ||
| 425 | + <p class="def-text">{v.texto}</p> | ||
| 426 | + </div> | ||
| 427 | + {/each} | ||
| 428 | + </div> | ||
| 429 | + </div> | ||
| 430 | + </div> | ||
| 431 | + {/if} | ||
| 432 | +</div> | ||
| 433 | + | ||
| 434 | +<style> | ||
| 435 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 436 | + PAGE | ||
| 437 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 438 | + .page { | ||
| 439 | + min-height: 100vh; | ||
| 440 | + width: 100%; | ||
| 441 | + max-width: 100%; | ||
| 442 | + overflow-x: hidden; | ||
| 443 | + background: var(--theme-body); | ||
| 444 | + color: var(--theme-titulo); | ||
| 445 | + font-family: var(--font-sans), -apple-system, sans-serif; | ||
| 446 | + opacity: 0; | ||
| 447 | + transition: opacity 0.4s ease; | ||
| 448 | + } | ||
| 449 | + .page.mounted { opacity: 1; } | ||
| 450 | + | ||
| 451 | + :global(html:not(.dark)) .page { | ||
| 452 | + background: #f5f5f7; | ||
| 453 | + } | ||
| 454 | + | ||
| 455 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 456 | + NAV | ||
| 457 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 458 | + .page-nav { | ||
| 459 | + padding: 2rem 2rem 0.5rem; | ||
| 460 | + max-width: 1400px; | ||
| 461 | + margin: 0 auto; | ||
| 462 | + } | ||
| 463 | + | ||
| 464 | + .back-link { | ||
| 465 | + display: inline-flex; | ||
| 466 | + align-items: center; | ||
| 467 | + gap: 0.375rem; | ||
| 468 | + font-family: 'DM Mono', monospace; | ||
| 469 | + font-size: 0.6875rem; | ||
| 470 | + color: var(--theme-texto); | ||
| 471 | + text-decoration: none; | ||
| 472 | + transition: color 0.2s; | ||
| 473 | + } | ||
| 474 | + .back-link:hover { color: var(--theme-accent); } | ||
| 475 | + | ||
| 476 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 477 | + LAYOUT | ||
| 478 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 479 | + .dashboard-layout { | ||
| 480 | + display: grid; | ||
| 481 | + grid-template-columns: minmax(200px, 30%) 1fr; | ||
| 482 | + gap: 1.5rem; | ||
| 483 | + max-width: 1400px; | ||
| 484 | + margin: 0 auto; | ||
| 485 | + padding: 30px 2rem; | ||
| 486 | + height: calc(100vh - 4rem); | ||
| 487 | + overflow: hidden; | ||
| 488 | + } | ||
| 489 | + | ||
| 490 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 491 | + SIDEBAR | ||
| 492 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 493 | + .dashboard-context { | ||
| 494 | + display: flex; | ||
| 495 | + flex-direction: column; | ||
| 496 | + min-height: 0; | ||
| 497 | + } | ||
| 498 | + | ||
| 499 | + .context-card { | ||
| 500 | + background: var(--theme-surface); | ||
| 501 | + border-radius: 12px; | ||
| 502 | + padding: 0.75rem; | ||
| 503 | + display: flex; | ||
| 504 | + flex-direction: column; | ||
| 505 | + gap: 8px; | ||
| 506 | + flex: 1; | ||
| 507 | + min-height: 0; | ||
| 508 | + overflow-y: auto; | ||
| 509 | + scrollbar-width: thin; | ||
| 510 | + } | ||
| 511 | + | ||
| 512 | + .context-card::-webkit-scrollbar { width: 4px; } | ||
| 513 | + .context-card::-webkit-scrollbar-thumb { | ||
| 514 | + background: rgba(128, 128, 128, 0.3); | ||
| 515 | + border-radius: 2px; | ||
| 516 | + } | ||
| 517 | + | ||
| 518 | + .context-main-title { | ||
| 519 | + font-size: 1rem; | ||
| 520 | + font-weight: 600; | ||
| 521 | + color: var(--theme-titulo); | ||
| 522 | + margin: 0; | ||
| 523 | + line-height: 1.2; | ||
| 524 | + text-transform: uppercase; | ||
| 525 | + letter-spacing: 0.05em; | ||
| 526 | + opacity: 0.5; | ||
| 527 | + } | ||
| 528 | + | ||
| 529 | + .context-subtitle { | ||
| 530 | + font-family: 'Qanelas', var(--font-sans); | ||
| 531 | + font-size: 0.875rem; | ||
| 532 | + font-weight: 700; | ||
| 533 | + color: var(--theme-titulo); | ||
| 534 | + margin: 0; | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + .context-hint { | ||
| 538 | + font-size: 0.75rem; | ||
| 539 | + color: var(--theme-texto); | ||
| 540 | + opacity: 0.7; | ||
| 541 | + margin: 0; | ||
| 542 | + line-height: 1.4; | ||
| 543 | + } | ||
| 544 | + | ||
| 545 | + .context-section { | ||
| 546 | + display: flex; | ||
| 547 | + flex-direction: column; | ||
| 548 | + gap: 0.5rem; | ||
| 549 | + } | ||
| 550 | + | ||
| 551 | + .context-separator { | ||
| 552 | + border-top: 1px solid var(--theme-borde); | ||
| 553 | + margin: 0.25rem 0; | ||
| 554 | + } | ||
| 555 | + | ||
| 556 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 557 | + TREE | ||
| 558 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 559 | + .tree { | ||
| 560 | + font-size: 0.75rem; | ||
| 561 | + line-height: 1.6; | ||
| 562 | + } | ||
| 563 | + | ||
| 564 | + .tree-node { | ||
| 565 | + display: flex; | ||
| 566 | + align-items: center; | ||
| 567 | + gap: 0.375rem; | ||
| 568 | + padding: 0.375rem 0.5rem; | ||
| 569 | + margin-left: calc(var(--indent, 0) * 1rem); | ||
| 570 | + border-left: 2px solid transparent; | ||
| 571 | + border-radius: 0 6px 6px 0; | ||
| 572 | + text-decoration: none; | ||
| 573 | + color: var(--theme-texto); | ||
| 574 | + transition: all 0.15s; | ||
| 575 | + } | ||
| 576 | + .tree-node:hover { color: var(--theme-accent); } | ||
| 577 | + | ||
| 578 | + .tree-node-current { | ||
| 579 | + color: var(--theme-titulo); | ||
| 580 | + font-weight: 600; | ||
| 581 | + border-left-color: var(--theme-accent); | ||
| 582 | + background: rgba(201, 167, 81, 0.1); | ||
| 583 | + } | ||
| 584 | + | ||
| 585 | + .tree-children { | ||
| 586 | + margin-left: calc(var(--indent, 0) * 1rem); | ||
| 587 | + } | ||
| 588 | + | ||
| 589 | + .tree-node-child { | ||
| 590 | + padding-left: 0.75rem; | ||
| 591 | + } | ||
| 592 | + | ||
| 593 | + .tree-dot { | ||
| 594 | + width: 4px; | ||
| 595 | + height: 4px; | ||
| 596 | + border-radius: 50%; | ||
| 597 | + background: currentColor; | ||
| 598 | + flex-shrink: 0; | ||
| 599 | + opacity: 0.4; | ||
| 600 | + } | ||
| 601 | + | ||
| 602 | + .tree-code { | ||
| 603 | + font-family: 'DM Mono', monospace; | ||
| 604 | + font-size: 0.6875rem; | ||
| 605 | + min-width: 2.5rem; | ||
| 606 | + opacity: 0.6; | ||
| 607 | + } | ||
| 608 | + | ||
| 609 | + .tree-name { | ||
| 610 | + font-size: 0.6875rem; | ||
| 611 | + white-space: nowrap; | ||
| 612 | + overflow: hidden; | ||
| 613 | + text-overflow: ellipsis; | ||
| 614 | + } | ||
| 615 | + | ||
| 616 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 617 | + EXPLORE | ||
| 618 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 619 | + .explore-scroll { | ||
| 620 | + display: flex; | ||
| 621 | + gap: 8px; | ||
| 622 | + overflow-x: auto; | ||
| 623 | + scrollbar-width: none; | ||
| 624 | + padding-bottom: 2px; | ||
| 625 | + } | ||
| 626 | + .explore-scroll::-webkit-scrollbar { display: none; } | ||
| 627 | + | ||
| 628 | + .explore-card { | ||
| 629 | + position: relative; | ||
| 630 | + display: flex; | ||
| 631 | + flex-direction: column; | ||
| 632 | + justify-content: flex-start; | ||
| 633 | + padding: 0.375rem 0.5rem; | ||
| 634 | + width: 120px; | ||
| 635 | + height: 95px; | ||
| 636 | + border-radius: 8px; | ||
| 637 | + background: var(--theme-borde); | ||
| 638 | + text-decoration: none; | ||
| 639 | + color: var(--theme-texto); | ||
| 640 | + transition: all 0.15s; | ||
| 641 | + flex-shrink: 0; | ||
| 642 | + overflow: hidden; | ||
| 643 | + } | ||
| 644 | + .explore-card:hover { | ||
| 645 | + background: rgba(201, 167, 81, 0.15); | ||
| 646 | + color: var(--theme-accent); | ||
| 647 | + } | ||
| 648 | + .explore-card:hover .explore-watermark { | ||
| 649 | + opacity: 0.18; | ||
| 650 | + color: var(--theme-accent); | ||
| 651 | + } | ||
| 652 | + .explore-card:hover .explore-title { | ||
| 653 | + color: var(--theme-accent); | ||
| 654 | + } | ||
| 655 | + | ||
| 656 | + .explore-watermark { | ||
| 657 | + position: absolute; | ||
| 658 | + bottom: -4px; | ||
| 659 | + right: -4px; | ||
| 660 | + width: 44px; | ||
| 661 | + height: 44px; | ||
| 662 | + opacity: 0.15; | ||
| 663 | + pointer-events: none; | ||
| 664 | + color: var(--theme-accent, #C9A751); | ||
| 665 | + } | ||
| 666 | + | ||
| 667 | + .explore-title { | ||
| 668 | + position: relative; | ||
| 669 | + font-size: 0.75rem; | ||
| 670 | + font-weight: 600; | ||
| 671 | + line-height: 1.3; | ||
| 672 | + color: var(--theme-titulo); | ||
| 673 | + z-index: 1; | ||
| 674 | + } | ||
| 675 | + | ||
| 676 | + .explore-desc { | ||
| 677 | + position: relative; | ||
| 678 | + font-size: 0.625rem; | ||
| 679 | + line-height: 1.35; | ||
| 680 | + opacity: 0.6; | ||
| 681 | + margin-top: 3px; | ||
| 682 | + z-index: 1; | ||
| 683 | + } | ||
| 684 | + | ||
| 685 | + .explore-card-disabled { | ||
| 686 | + opacity: 0.3; | ||
| 687 | + pointer-events: none; | ||
| 688 | + } | ||
| 689 | + | ||
| 690 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 691 | + VIGENCIA | ||
| 692 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 693 | + .vigencia-compact { | ||
| 694 | + display: flex; | ||
| 695 | + align-items: center; | ||
| 696 | + gap: 0.75rem; | ||
| 697 | + } | ||
| 698 | + | ||
| 699 | + .vigencia-status-icon { | ||
| 700 | + width: 32px; | ||
| 701 | + height: 32px; | ||
| 702 | + border-radius: 8px; | ||
| 703 | + display: flex; | ||
| 704 | + align-items: center; | ||
| 705 | + justify-content: center; | ||
| 706 | + flex-shrink: 0; | ||
| 707 | + } | ||
| 708 | + | ||
| 709 | + .vigencia-status-icon.vigencia-full { | ||
| 710 | + background: rgba(74, 158, 107, 0.15); | ||
| 711 | + color: #4A9E6B; | ||
| 712 | + } | ||
| 713 | + | ||
| 714 | + .vigencia-text { | ||
| 715 | + display: flex; | ||
| 716 | + flex-direction: column; | ||
| 717 | + gap: 0.125rem; | ||
| 718 | + } | ||
| 719 | + | ||
| 720 | + .vigencia-main { | ||
| 721 | + font-size: 0.75rem; | ||
| 722 | + font-weight: 500; | ||
| 723 | + color: var(--theme-titulo); | ||
| 724 | + } | ||
| 725 | + | ||
| 726 | + .vigencia-sub { | ||
| 727 | + font-size: 0.625rem; | ||
| 728 | + color: var(--theme-texto); | ||
| 729 | + opacity: 0.5; | ||
| 730 | + } | ||
| 731 | + | ||
| 732 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 733 | + DEFINICIÓN | ||
| 734 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 735 | + .def-link { | ||
| 736 | + display: flex; | ||
| 737 | + align-items: center; | ||
| 738 | + gap: 0.375rem; | ||
| 739 | + font-size: 0.75rem; | ||
| 740 | + color: var(--theme-accent, #C9A751); | ||
| 741 | + background: none; | ||
| 742 | + border: none; | ||
| 743 | + padding: 0; | ||
| 744 | + cursor: pointer; | ||
| 745 | + transition: opacity 0.15s; | ||
| 746 | + } | ||
| 747 | + .def-link:hover { opacity: 0.7; } | ||
| 748 | + .def-link svg { flex-shrink: 0; opacity: 0.8; } | ||
| 749 | + | ||
| 750 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 751 | + MAIN | ||
| 752 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 753 | + .dashboard-main { | ||
| 754 | + display: flex; | ||
| 755 | + flex-direction: column; | ||
| 756 | + gap: 1.5rem; | ||
| 757 | + min-height: 0; | ||
| 758 | + overflow: hidden; | ||
| 759 | + } | ||
| 760 | + | ||
| 761 | + .chart-protagonista { | ||
| 762 | + background: var(--theme-surface); | ||
| 763 | + border-radius: 12px; | ||
| 764 | + padding: 0.75rem; | ||
| 765 | + display: flex; | ||
| 766 | + flex-direction: column; | ||
| 767 | + min-height: 0; | ||
| 768 | + flex: 2; | ||
| 769 | + overflow: hidden; | ||
| 770 | + } | ||
| 771 | + | ||
| 772 | + .ranking-section { | ||
| 773 | + background: var(--theme-surface); | ||
| 774 | + border-radius: 12px; | ||
| 775 | + padding: 0.75rem; | ||
| 776 | + flex: 1; | ||
| 777 | + display: flex; | ||
| 778 | + flex-direction: column; | ||
| 779 | + min-height: 0; | ||
| 780 | + } | ||
| 781 | + | ||
| 782 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 783 | + CHART & KPIs | ||
| 784 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 785 | + .chart-top-row { | ||
| 786 | + display: flex; | ||
| 787 | + justify-content: space-between; | ||
| 788 | + align-items: center; | ||
| 789 | + margin-bottom: 0.375rem; | ||
| 790 | + } | ||
| 791 | + | ||
| 792 | + .main-title-pills { | ||
| 793 | + display: flex; | ||
| 794 | + gap: 0.375rem; | ||
| 795 | + } | ||
| 796 | + | ||
| 797 | + .pill { | ||
| 798 | + font-family: 'DM Mono', monospace; | ||
| 799 | + font-size: 0.625rem; | ||
| 800 | + padding: 0.125rem 0.5rem; | ||
| 801 | + border-radius: 4px; | ||
| 802 | + letter-spacing: 0.03em; | ||
| 803 | + } | ||
| 804 | + | ||
| 805 | + .pill-nivel { | ||
| 806 | + background: rgba(201, 167, 81, 0.15); | ||
| 807 | + color: var(--theme-accent); | ||
| 808 | + } | ||
| 809 | + | ||
| 810 | + .pill-codigo { | ||
| 811 | + background: var(--theme-borde); | ||
| 812 | + color: var(--theme-texto); | ||
| 813 | + } | ||
| 814 | + | ||
| 815 | + .main-title-name { | ||
| 816 | + font-family: 'DM Serif Display', serif; | ||
| 817 | + font-size: 2rem; | ||
| 818 | + font-weight: 400; | ||
| 819 | + margin: 0 0 0.5rem; | ||
| 820 | + line-height: 1.15; | ||
| 821 | + color: var(--theme-titulo); | ||
| 822 | + } | ||
| 823 | + | ||
| 824 | + .chart-controls { | ||
| 825 | + margin-bottom: 0.5rem; | ||
| 826 | + } | ||
| 827 | + | ||
| 828 | + .chart-header { | ||
| 829 | + display: flex; | ||
| 830 | + align-items: center; | ||
| 831 | + justify-content: space-between; | ||
| 832 | + margin-bottom: 0.25rem; | ||
| 833 | + } | ||
| 834 | + | ||
| 835 | + .chart-title { | ||
| 836 | + font-family: 'DM Mono', monospace; | ||
| 837 | + font-size: 0.6875rem; | ||
| 838 | + color: var(--theme-texto); | ||
| 839 | + opacity: 0.7; | ||
| 840 | + } | ||
| 841 | + | ||
| 842 | + .chart-period { | ||
| 843 | + opacity: 0.5; | ||
| 844 | + } | ||
| 845 | + | ||
| 846 | + .metrica-toggle { | ||
| 847 | + font-family: 'DM Mono', monospace; | ||
| 848 | + font-size: 0.625rem; | ||
| 849 | + color: var(--theme-accent); | ||
| 850 | + background: rgba(201, 167, 81, 0.1); | ||
| 851 | + border: 1px solid rgba(201, 167, 81, 0.2); | ||
| 852 | + padding: 0.2rem 0.5rem; | ||
| 853 | + border-radius: 4px; | ||
| 854 | + cursor: pointer; | ||
| 855 | + transition: all 0.15s; | ||
| 856 | + } | ||
| 857 | + | ||
| 858 | + .metrica-toggle:hover { | ||
| 859 | + background: rgba(201, 167, 81, 0.2); | ||
| 860 | + } | ||
| 861 | + | ||
| 862 | + .chart-loading { | ||
| 863 | + display: flex; | ||
| 864 | + align-items: center; | ||
| 865 | + justify-content: center; | ||
| 866 | + color: var(--theme-texto); | ||
| 867 | + opacity: 0.5; | ||
| 868 | + } | ||
| 869 | + | ||
| 870 | + .chart-flex-wrapper { | ||
| 871 | + flex: 1; | ||
| 872 | + min-height: 0; | ||
| 873 | + } | ||
| 874 | + | ||
| 875 | + .kpis { | ||
| 876 | + display: grid; | ||
| 877 | + grid-template-columns: repeat(4, 1fr); | ||
| 878 | + gap: 0.5rem; | ||
| 879 | + margin-top: 0.5rem; | ||
| 880 | + padding-top: 0.75rem; | ||
| 881 | + border-top: 1px solid var(--theme-borde); | ||
| 882 | + } | ||
| 883 | + | ||
| 884 | + .kpi { | ||
| 885 | + display: flex; | ||
| 886 | + flex-direction: column; | ||
| 887 | + align-items: center; | ||
| 888 | + gap: 0.125rem; | ||
| 889 | + text-align: center; | ||
| 890 | + } | ||
| 891 | + | ||
| 892 | + .kpi-value { | ||
| 893 | + font-family: 'Qanelas', var(--font-sans); | ||
| 894 | + font-size: 1rem; | ||
| 895 | + font-weight: 600; | ||
| 896 | + color: var(--theme-titulo); | ||
| 897 | + } | ||
| 898 | + | ||
| 899 | + .kpi-label { | ||
| 900 | + font-size: 0.625rem; | ||
| 901 | + color: var(--theme-texto); | ||
| 902 | + opacity: 0.5; | ||
| 903 | + line-height: 1.35; | ||
| 904 | + } | ||
| 905 | + | ||
| 906 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 907 | + RANKING | ||
| 908 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 909 | + .ranking-title { | ||
| 910 | + font-size: 1rem; | ||
| 911 | + font-weight: 600; | ||
| 912 | + color: var(--theme-titulo); | ||
| 913 | + margin: 0 0 0.25rem; | ||
| 914 | + } | ||
| 915 | + | ||
| 916 | + .ranking-desc { | ||
| 917 | + font-size: 0.6875rem; | ||
| 918 | + color: var(--theme-texto); | ||
| 919 | + opacity: 0.5; | ||
| 920 | + margin: 0 0 0.5rem; | ||
| 921 | + } | ||
| 922 | + | ||
| 923 | + .ranking-cards { | ||
| 924 | + display: flex; | ||
| 925 | + flex-direction: column; | ||
| 926 | + gap: 0.25rem; | ||
| 927 | + } | ||
| 928 | + | ||
| 929 | + .ranking-card { | ||
| 930 | + display: flex; | ||
| 931 | + align-items: center; | ||
| 932 | + gap: 0.75rem; | ||
| 933 | + padding: 0.5rem 0.625rem; | ||
| 934 | + border-radius: 8px; | ||
| 935 | + background: rgba(128, 128, 128, 0.06); | ||
| 936 | + } | ||
| 937 | + | ||
| 938 | + .ranking-position { | ||
| 939 | + font-family: 'DM Mono', monospace; | ||
| 940 | + font-size: 0.6875rem; | ||
| 941 | + color: var(--theme-accent); | ||
| 942 | + font-weight: 600; | ||
| 943 | + min-width: 1.5rem; | ||
| 944 | + } | ||
| 945 | + | ||
| 946 | + .ranking-name { | ||
| 947 | + flex: 1; | ||
| 948 | + font-size: 0.8125rem; | ||
| 949 | + color: var(--theme-titulo); | ||
| 950 | + white-space: nowrap; | ||
| 951 | + overflow: hidden; | ||
| 952 | + text-overflow: ellipsis; | ||
| 953 | + } | ||
| 954 | + | ||
| 955 | + .ranking-pct { | ||
| 956 | + font-family: 'DM Mono', monospace; | ||
| 957 | + font-size: 0.875rem; | ||
| 958 | + font-weight: 600; | ||
| 959 | + color: var(--theme-titulo); | ||
| 960 | + } | ||
| 961 | + | ||
| 962 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 963 | + MODAL | ||
| 964 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 965 | + .modal-overlay { | ||
| 966 | + position: fixed; | ||
| 967 | + inset: 0; | ||
| 968 | + background: rgba(0, 0, 0, 0.6); | ||
| 969 | + backdrop-filter: blur(4px); | ||
| 970 | + display: flex; | ||
| 971 | + align-items: center; | ||
| 972 | + justify-content: center; | ||
| 973 | + z-index: 1000; | ||
| 974 | + padding: 1rem; | ||
| 975 | + } | ||
| 976 | + | ||
| 977 | + .modal-content { | ||
| 978 | + background: var(--theme-surface); | ||
| 979 | + border-radius: 16px; | ||
| 980 | + max-width: 560px; | ||
| 981 | + width: 100%; | ||
| 982 | + max-height: 80vh; | ||
| 983 | + overflow: hidden; | ||
| 984 | + display: flex; | ||
| 985 | + flex-direction: column; | ||
| 986 | + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2); | ||
| 987 | + } | ||
| 988 | + | ||
| 989 | + .modal-header { | ||
| 990 | + display: flex; | ||
| 991 | + align-items: center; | ||
| 992 | + justify-content: space-between; | ||
| 993 | + padding: 1.25rem 1.5rem; | ||
| 994 | + border-bottom: 1px solid var(--theme-borde); | ||
| 995 | + } | ||
| 996 | + | ||
| 997 | + .modal-header h3 { | ||
| 998 | + font-size: 1rem; | ||
| 999 | + font-weight: 600; | ||
| 1000 | + color: var(--theme-titulo); | ||
| 1001 | + margin: 0; | ||
| 1002 | + } | ||
| 1003 | + | ||
| 1004 | + .modal-close { | ||
| 1005 | + color: var(--theme-texto); | ||
| 1006 | + background: transparent; | ||
| 1007 | + border: none; | ||
| 1008 | + padding: 0.25rem; | ||
| 1009 | + cursor: pointer; | ||
| 1010 | + border-radius: 6px; | ||
| 1011 | + transition: background 0.2s; | ||
| 1012 | + } | ||
| 1013 | + | ||
| 1014 | + .modal-close:hover { | ||
| 1015 | + background: var(--theme-borde); | ||
| 1016 | + } | ||
| 1017 | + | ||
| 1018 | + .modal-body { | ||
| 1019 | + padding: 1.5rem; | ||
| 1020 | + overflow-y: auto; | ||
| 1021 | + display: flex; | ||
| 1022 | + flex-direction: column; | ||
| 1023 | + gap: 1rem; | ||
| 1024 | + } | ||
| 1025 | + | ||
| 1026 | + .def-item { | ||
| 1027 | + padding: 1rem; | ||
| 1028 | + background: var(--theme-body); | ||
| 1029 | + border-radius: 10px; | ||
| 1030 | + border-left: 3px solid var(--theme-borde); | ||
| 1031 | + } | ||
| 1032 | + | ||
| 1033 | + .def-current { | ||
| 1034 | + border-left-color: #6B9FD4; | ||
| 1035 | + } | ||
| 1036 | + | ||
| 1037 | + .def-header { | ||
| 1038 | + display: flex; | ||
| 1039 | + align-items: center; | ||
| 1040 | + gap: 0.5rem; | ||
| 1041 | + margin-bottom: 0.5rem; | ||
| 1042 | + } | ||
| 1043 | + | ||
| 1044 | + .def-period { | ||
| 1045 | + font-family: 'DM Mono', monospace; | ||
| 1046 | + font-size: 0.75rem; | ||
| 1047 | + font-weight: 500; | ||
| 1048 | + color: #6B9FD4; | ||
| 1049 | + } | ||
| 1050 | + | ||
| 1051 | + .def-badge { | ||
| 1052 | + font-size: 0.625rem; | ||
| 1053 | + font-weight: 600; | ||
| 1054 | + text-transform: uppercase; | ||
| 1055 | + letter-spacing: 0.05em; | ||
| 1056 | + color: white; | ||
| 1057 | + background: #4A9E6B; | ||
| 1058 | + padding: 0.125rem 0.5rem; | ||
| 1059 | + border-radius: 4px; | ||
| 1060 | + } | ||
| 1061 | + | ||
| 1062 | + .def-text { | ||
| 1063 | + font-size: 0.875rem; | ||
| 1064 | + color: var(--theme-texto); | ||
| 1065 | + line-height: 1.6; | ||
| 1066 | + margin: 0; | ||
| 1067 | + } | ||
| 1068 | + | ||
| 1069 | + :global(html.dark) .def-current { | ||
| 1070 | + border-left-color: var(--theme-accent); | ||
| 1071 | + } | ||
| 1072 | + | ||
| 1073 | + :global(html.dark) .def-period { | ||
| 1074 | + color: var(--theme-accent); | ||
| 1075 | + } | ||
| 1076 | + | ||
| 1077 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 1078 | + MOBILE SIDEBAR | ||
| 1079 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 1080 | + .mobile-sidebar-toggle { | ||
| 1081 | + display: none; | ||
| 1082 | + } | ||
| 1083 | + | ||
| 1084 | + @media (max-width: 900px) { | ||
| 1085 | + .dashboard-layout { | ||
| 1086 | + grid-template-columns: 1fr; | ||
| 1087 | + gap: 0.5rem; | ||
| 1088 | + height: auto; | ||
| 1089 | + overflow: visible; | ||
| 1090 | + padding: 1.5rem 1rem 2rem; | ||
| 1091 | + align-content: center; | ||
| 1092 | + min-height: calc(100vh - 4rem); | ||
| 1093 | + } | ||
| 1094 | + | ||
| 1095 | + .dashboard-main { | ||
| 1096 | + overflow: visible; | ||
| 1097 | + } | ||
| 1098 | + | ||
| 1099 | + .chart-protagonista { | ||
| 1100 | + flex: none; | ||
| 1101 | + } | ||
| 1102 | + | ||
| 1103 | + .ranking-section { | ||
| 1104 | + flex: none; | ||
| 1105 | + } | ||
| 1106 | + | ||
| 1107 | + .mobile-sidebar-toggle { | ||
| 1108 | + display: flex; | ||
| 1109 | + align-items: center; | ||
| 1110 | + gap: 0.5rem; | ||
| 1111 | + padding: 0.625rem 1rem; | ||
| 1112 | + background: var(--theme-surface); | ||
| 1113 | + border: 1px solid var(--theme-borde); | ||
| 1114 | + border-radius: 10px; | ||
| 1115 | + color: var(--theme-titulo); | ||
| 1116 | + font-family: 'Qanelas', var(--font-sans); | ||
| 1117 | + font-size: 0.8125rem; | ||
| 1118 | + font-weight: 500; | ||
| 1119 | + cursor: pointer; | ||
| 1120 | + transition: all 0.2s; | ||
| 1121 | + margin-bottom: 0.25rem; | ||
| 1122 | + width: 100%; | ||
| 1123 | + } | ||
| 1124 | + | ||
| 1125 | + .mobile-sidebar-toggle:hover { | ||
| 1126 | + background: var(--theme-surface-hover); | ||
| 1127 | + } | ||
| 1128 | + | ||
| 1129 | + .mobile-sidebar-toggle svg { | ||
| 1130 | + color: var(--theme-texto); | ||
| 1131 | + opacity: 0.6; | ||
| 1132 | + } | ||
| 1133 | + | ||
| 1134 | + .dashboard-context .context-card { | ||
| 1135 | + display: none; | ||
| 1136 | + } | ||
| 1137 | + | ||
| 1138 | + .dashboard-context.mobile-open .context-card { | ||
| 1139 | + display: flex; | ||
| 1140 | + animation: slideDown 0.25s ease; | ||
| 1141 | + } | ||
| 1142 | + | ||
| 1143 | + @keyframes slideDown { | ||
| 1144 | + from { opacity: 0; transform: translateY(-8px); } | ||
| 1145 | + to { opacity: 1; transform: translateY(0); } | ||
| 1146 | + } | ||
| 1147 | + | ||
| 1148 | + .context-card { | ||
| 1149 | + height: auto; | ||
| 1150 | + overflow-y: visible; | ||
| 1151 | + } | ||
| 1152 | + | ||
| 1153 | + .page-nav { | ||
| 1154 | + padding: 0.75rem 1rem; | ||
| 1155 | + } | ||
| 1156 | + | ||
| 1157 | + .kpis { | ||
| 1158 | + grid-template-columns: repeat(2, 1fr); | ||
| 1159 | + } | ||
| 1160 | + } | ||
| 1161 | + | ||
| 1162 | + @media (max-width: 480px) { | ||
| 1163 | + .dashboard-layout { | ||
| 1164 | + padding: 0.5rem; | ||
| 1165 | + } | ||
| 1166 | + | ||
| 1167 | + .page-nav { | ||
| 1168 | + padding: 0.5rem; | ||
| 1169 | + } | ||
| 1170 | + | ||
| 1171 | + .chart-protagonista, | ||
| 1172 | + .ranking-section, | ||
| 1173 | + .context-card { | ||
| 1174 | + padding: 0.5rem; | ||
| 1175 | + border-radius: 8px; | ||
| 1176 | + } | ||
| 1177 | + | ||
| 1178 | + .main-title-name { | ||
| 1179 | + font-size: 1.375rem; | ||
| 1180 | + } | ||
| 1181 | + | ||
| 1182 | + .chart-header { | ||
| 1183 | + flex-direction: column; | ||
| 1184 | + align-items: flex-start; | ||
| 1185 | + gap: 0.25rem; | ||
| 1186 | + } | ||
| 1187 | + | ||
| 1188 | + .kpis { | ||
| 1189 | + grid-template-columns: repeat(2, 1fr); | ||
| 1190 | + gap: 0.375rem; | ||
| 1191 | + } | ||
| 1192 | + | ||
| 1193 | + .kpi-value { | ||
| 1194 | + font-size: 0.8125rem; | ||
| 1195 | + } | ||
| 1196 | + | ||
| 1197 | + .kpi-label { | ||
| 1198 | + font-size: 0.5625rem; | ||
| 1199 | + } | ||
| 1200 | + | ||
| 1201 | + .explore-scroll { | ||
| 1202 | + gap: 6px; | ||
| 1203 | + } | ||
| 1204 | + | ||
| 1205 | + .explore-card { | ||
| 1206 | + width: 100px; | ||
| 1207 | + height: 80px; | ||
| 1208 | + } | ||
| 1209 | + | ||
| 1210 | + .explore-title { | ||
| 1211 | + font-size: 0.625rem; | ||
| 1212 | + } | ||
| 1213 | + | ||
| 1214 | + .explore-desc { | ||
| 1215 | + font-size: 0.5rem; | ||
| 1216 | + } | ||
| 1217 | + | ||
| 1218 | + .ranking-title { | ||
| 1219 | + font-size: 0.875rem; | ||
| 1220 | + } | ||
| 1221 | + | ||
| 1222 | + .ranking-name { | ||
| 1223 | + font-size: 0.6875rem; | ||
| 1224 | + } | ||
| 1225 | + | ||
| 1226 | + .ranking-pct { | ||
| 1227 | + font-size: 0.75rem; | ||
| 1228 | + } | ||
| 1229 | + | ||
| 1230 | + .modal-content { | ||
| 1231 | + margin: 0.5rem; | ||
| 1232 | + max-height: 90vh; | ||
| 1233 | + } | ||
| 1234 | + | ||
| 1235 | + .tree-name { | ||
| 1236 | + max-width: 150px; | ||
| 1237 | + } | ||
| 1238 | + } | ||
| 1239 | + | ||
| 1240 | + /* ═══════════════════════════════════════════════════════════════ | ||
| 1241 | + DARK MODE | ||
| 1242 | + ═══════════════════════════════════════════════════════════════ */ | ||
| 1243 | + :global(html.dark) .context-hint { | ||
| 1244 | + color: rgba(255, 255, 255, 0.5); | ||
| 1245 | + } | ||
| 1246 | + | ||
| 1247 | + :global(html.dark) .def-link { | ||
| 1248 | + color: var(--theme-accent, #C9A751); | ||
| 1249 | + } | ||
| 1250 | +</style> |
static/gap.png
0 → 100644
380 KB
-
Please register or login to post a comment