Showing
4 changed files
with
561 additions
and
401 deletions
| ... | @@ -1194,6 +1194,24 @@ | ... | @@ -1194,6 +1194,24 @@ |
| 1194 | </div> | 1194 | </div> |
| 1195 | </div> | 1195 | </div> |
| 1196 | 1196 | ||
| 1197 | + <!-- Links directos --> | ||
| 1198 | + <div class="quick-links"> | ||
| 1199 | + <a href="/vista-diaria" class="quick-link quick-link-diaria"> | ||
| 1200 | + <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 1201 | + <polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/> | ||
| 1202 | + </svg> | ||
| 1203 | + Ver ejecución diaria | ||
| 1204 | + <span class="quick-link-arrow">→</span> | ||
| 1205 | + </a> | ||
| 1206 | + <a href="/descargas" class="quick-link quick-link-descargas"> | ||
| 1207 | + <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 1208 | + <path d="M12 4v12"/><polyline points="8 12 12 16 16 12"/><path d="M4 18h16"/> | ||
| 1209 | + </svg> | ||
| 1210 | + Descarga bases de datos | ||
| 1211 | + <span class="quick-link-arrow">→</span> | ||
| 1212 | + </a> | ||
| 1213 | + </div> | ||
| 1214 | + | ||
| 1197 | </div> | 1215 | </div> |
| 1198 | 1216 | ||
| 1199 | <!-- Contenedor de resultados (overlay absoluto) --> | 1217 | <!-- Contenedor de resultados (overlay absoluto) --> |
| ... | @@ -1531,6 +1549,7 @@ | ... | @@ -1531,6 +1549,7 @@ |
| 1531 | 1549 | ||
| 1532 | <!-- Scroll hint --> | 1550 | <!-- Scroll hint --> |
| 1533 | 1551 | ||
| 1552 | + | ||
| 1534 | </main> | 1553 | </main> |
| 1535 | 1554 | ||
| 1536 | <!-- Secciones removidas - el landing es solo el buscador --> | 1555 | <!-- Secciones removidas - el landing es solo el buscador --> |
| ... | @@ -2801,6 +2820,23 @@ | ... | @@ -2801,6 +2820,23 @@ |
| 2801 | :global(html:not(.dark)) .footer-org{color:#999} | 2820 | :global(html:not(.dark)) .footer-org{color:#999} |
| 2802 | :global(html:not(.dark)) .footer-right{color:#999} | 2821 | :global(html:not(.dark)) .footer-right{color:#999} |
| 2803 | 2822 | ||
| 2823 | + /* ── Quick links (debajo de mode cards) ── */ | ||
| 2824 | + /* ── Quick links (debajo de mode cards) ── */ | ||
| 2825 | + .quick-links{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px} | ||
| 2826 | + .quick-link{display:flex;align-items:center;gap:8px;font-family:var(--sans);font-size:16px;font-weight:600;letter-spacing:-0.01em;text-decoration:none;padding:14px 16px;border-radius:12px;transition:all 0.25s ease} | ||
| 2827 | + .page a.quick-link-diaria{color:#C2EDD9;background:rgba(74,139,110,0.18);border:none} | ||
| 2828 | + .page a.quick-link-diaria:hover{color:#E0F5EC;background:rgba(74,139,110,0.28)} | ||
| 2829 | + .page a.quick-link-descargas{color:#F5E6A8;background:rgba(201,167,81,0.18);border:none} | ||
| 2830 | + .page a.quick-link-descargas:hover{color:#FBF2D0;background:rgba(201,167,81,0.28)} | ||
| 2831 | + .quick-link svg{opacity:0.8;transition:opacity 0.25s;flex-shrink:0} | ||
| 2832 | + .quick-link:hover svg{opacity:1} | ||
| 2833 | + .quick-link-arrow{margin-left:auto;opacity:0.4;transition:all 0.25s;transform:translateX(-4px)} | ||
| 2834 | + .quick-link:hover .quick-link-arrow{opacity:0.8;transform:translateX(0)} | ||
| 2835 | + :global(html:not(.dark)) .page a.quick-link-diaria{color:#3A7A5E;background:rgba(74,139,110,0.14)} | ||
| 2836 | + :global(html:not(.dark)) .page a.quick-link-diaria:hover{color:#2D6B4F;background:rgba(74,139,110,0.22)} | ||
| 2837 | + :global(html:not(.dark)) .page a.quick-link-descargas{color:#8A7030;background:rgba(201,167,81,0.14)} | ||
| 2838 | + :global(html:not(.dark)) .page a.quick-link-descargas:hover{color:#7A6228;background:rgba(201,167,81,0.22)} | ||
| 2839 | + | ||
| 2804 | /* ══════════════════════════════════════════════════════════════════════════ | 2840 | /* ══════════════════════════════════════════════════════════════════════════ |
| 2805 | RESPONSIVE - TABLET (768px) | 2841 | RESPONSIVE - TABLET (768px) |
| 2806 | ══════════════════════════════════════════════════════════════════════════ */ | 2842 | ══════════════════════════════════════════════════════════════════════════ */ |
| ... | @@ -2808,6 +2844,7 @@ | ... | @@ -2808,6 +2844,7 @@ |
| 2808 | .page{scroll-snap-type:y proximity} | 2844 | .page{scroll-snap-type:y proximity} |
| 2809 | main[data-section="hero"]{height:auto;min-height:100vh} | 2845 | main[data-section="hero"]{height:auto;min-height:100vh} |
| 2810 | main[data-section="hero"],.landing-screen,.directory,.clasificadores,.historias,.visualizaciones,.manifiesto,.descargas{scroll-snap-stop:normal} | 2846 | main[data-section="hero"],.landing-screen,.directory,.clasificadores,.historias,.visualizaciones,.manifiesto,.descargas{scroll-snap-stop:normal} |
| 2847 | + .quick-links{grid-template-columns:1fr} | ||
| 2811 | 2848 | ||
| 2812 | /* Section dots - bottom horizontal bar */ | 2849 | /* Section dots - bottom horizontal bar */ |
| 2813 | .section-dots{position:fixed;right:auto;left:50%;top:auto;bottom:20px;transform:translateX(-50%);flex-direction:row;gap:10px;padding:12px 20px;border-radius:24px;background:rgba(28,28,26,0.7);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);z-index:1000} | 2850 | .section-dots{position:fixed;right:auto;left:50%;top:auto;bottom:20px;transform:translateX(-50%);flex-direction:row;gap:10px;padding:12px 20px;border-radius:24px;background:rgba(28,28,26,0.7);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);z-index:1000} | ... | ... |
| ... | @@ -11,13 +11,21 @@ export async function GET({ url }) { | ... | @@ -11,13 +11,21 @@ export async function GET({ url }) { |
| 11 | timeline: `${API_BASE}/timeline` | 11 | timeline: `${API_BASE}/timeline` |
| 12 | }; | 12 | }; |
| 13 | 13 | ||
| 14 | - const apiUrl = endpoints[tipo]; | 14 | + let apiUrl = endpoints[tipo]; |
| 15 | if (!apiUrl) { | 15 | if (!apiUrl) { |
| 16 | return new Response(JSON.stringify({ error: 'Invalid tipo' }), { | 16 | return new Response(JSON.stringify({ error: 'Invalid tipo' }), { |
| 17 | status: 400, headers: { 'Content-Type': 'application/json' } | 17 | status: 400, headers: { 'Content-Type': 'application/json' } |
| 18 | }); | 18 | }); |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | + // Pasar parámetros adicionales | ||
| 22 | + const params = new URLSearchParams(); | ||
| 23 | + const entidad = url.searchParams.get('entidad'); | ||
| 24 | + const dias = url.searchParams.get('dias'); | ||
| 25 | + if (entidad) params.set('entidad', entidad); | ||
| 26 | + if (dias) params.set('dias', dias); | ||
| 27 | + if (params.toString()) apiUrl += `?${params.toString()}`; | ||
| 28 | + | ||
| 21 | try { | 29 | try { |
| 22 | const response = await fetch(apiUrl); | 30 | const response = await fetch(apiUrl); |
| 23 | if (!response.ok) { | 31 | if (!response.ok) { | ... | ... |
| ... | @@ -4,9 +4,13 @@ | ... | @@ -4,9 +4,13 @@ |
| 4 | const YEARS = Array.from({ length: 10 }, (_, i) => 2016 + i); | 4 | const YEARS = Array.from({ length: 10 }, (_, i) => 2016 + i); |
| 5 | 5 | ||
| 6 | let mounted = $state(false); | 6 | let mounted = $state(false); |
| 7 | - let dataset = $state('gasto'); | 7 | + |
| 8 | - let datasetMasivo = $state('gasto'); | 8 | + // Sección 1: Por gestión |
| 9 | + let datasetGestion = $state('gasto'); | ||
| 9 | let formatoGestion = $state('csv'); | 10 | let formatoGestion = $state('csv'); |
| 11 | + | ||
| 12 | + // Sección 2: Por entidad | ||
| 13 | + let datasetEntidad = $state('gasto'); | ||
| 10 | let formatoEntidad = $state('csv'); | 14 | let formatoEntidad = $state('csv'); |
| 11 | let entities = $state([]); | 15 | let entities = $state([]); |
| 12 | let entitySearch = $state(''); | 16 | let entitySearch = $state(''); |
| ... | @@ -15,30 +19,27 @@ | ... | @@ -15,30 +19,27 @@ |
| 15 | let selectedIdx = $state(-1); | 19 | let selectedIdx = $state(-1); |
| 16 | let loadingEntities = $state(true); | 20 | let loadingEntities = $state(true); |
| 17 | let entityError = $state(false); | 21 | let entityError = $state(false); |
| 22 | + | ||
| 23 | + // Sección 3: Base completa | ||
| 24 | + let datasetMasivo = $state('gasto'); | ||
| 25 | + | ||
| 26 | + // Sección 4: Ejecución semanal (próximamente) | ||
| 27 | + | ||
| 28 | + // Sección 5: Desarrolladores | ||
| 18 | let copiedUrl = $state(null); | 29 | let copiedUrl = $state(null); |
| 19 | 30 | ||
| 20 | function normalizeText(val) { | 31 | function normalizeText(val) { |
| 21 | return val.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim(); | 32 | return val.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim(); |
| 22 | } | 33 | } |
| 23 | 34 | ||
| 24 | - function buildUrl(partition, value, formato = 'csv') { | 35 | + function buildGestionUrl(year) { |
| 25 | - if (partition === 'completo') { | 36 | + const suffix = formatoGestion === 'xlsx' ? `_xlsx.zip` : `.zip`; |
| 26 | - if (formato === 'parquet') { | 37 | + return `${DOWNLOAD_BASE}${datasetGestion}/gestiones/gestion_${year}${suffix}`; |
| 27 | - return `${DOWNLOAD_BASE}${dataset}/ultimo/${dataset}.parquet`; | 38 | + } |
| 28 | - } | 39 | + |
| 29 | - return `${DOWNLOAD_BASE}${dataset}/ultimo/${dataset}.zip`; | 40 | + function buildEntidadUrl(codigo) { |
| 30 | - } else if (partition === 'gestion') { | 41 | + const suffix = formatoEntidad === 'xlsx' ? `_xlsx.zip` : `.zip`; |
| 31 | - if (formato === 'xlsx') { | 42 | + return `${DOWNLOAD_BASE}${datasetEntidad}/entidades/entidad_${codigo}${suffix}`; |
| 32 | - return `${DOWNLOAD_BASE}${dataset}/gestiones/gestion_${value}_xlsx.zip`; | ||
| 33 | - } | ||
| 34 | - return `${DOWNLOAD_BASE}${dataset}/gestiones/gestion_${value}.zip`; | ||
| 35 | - } else if (partition === 'entidad') { | ||
| 36 | - if (formato === 'xlsx') { | ||
| 37 | - return `${DOWNLOAD_BASE}${dataset}/entidades/entidad_${value}_xlsx.zip`; | ||
| 38 | - } | ||
| 39 | - return `${DOWNLOAD_BASE}${dataset}/entidades/entidad_${value}.zip`; | ||
| 40 | - } | ||
| 41 | - return `${DOWNLOAD_BASE}${dataset}/${partition}/${value}`; | ||
| 42 | } | 43 | } |
| 43 | 44 | ||
| 44 | function buildMasivoUrl(formato) { | 45 | function buildMasivoUrl(formato) { |
| ... | @@ -113,112 +114,125 @@ | ... | @@ -113,112 +114,125 @@ |
| 113 | <p class="subtitle">Descarga los datos del presupuesto público boliviano en distintos formatos</p> | 114 | <p class="subtitle">Descarga los datos del presupuesto público boliviano en distintos formatos</p> |
| 114 | 115 | ||
| 115 | <!-- ═══════════════════════════════════════════ --> | 116 | <!-- ═══════════════════════════════════════════ --> |
| 116 | - <!-- SECCIÓN 1: Por gestión o entidad --> | 117 | + <!-- SECCIÓN 1: Por gestión --> |
| 117 | <!-- ═══════════════════════════════════════════ --> | 118 | <!-- ═══════════════════════════════════════════ --> |
| 118 | <section class="section"> | 119 | <section class="section"> |
| 119 | - <h2 class="section-title">Por gestión o entidad</h2> | 120 | + <h2 class="section-title">Por gestión</h2> |
| 120 | - <p class="section-desc">Descarga los datos segmentados por año fiscal o por entidad pública</p> | 121 | + <p class="section-desc">Datos segmentados por año fiscal</p> |
| 121 | - | 122 | + |
| 122 | - <!-- Toggle Gastos/Ingresos --> | 123 | + <div class="controls-row"> |
| 123 | - <div class="control-row"> | 124 | + <div class="control-row"> |
| 124 | - <span class="control-label">Base</span> | 125 | + <span class="control-label">Base</span> |
| 125 | - <div class="toggle-group"> | 126 | + <div class="toggle-group"> |
| 126 | - <button class="toggle-btn" class:active={dataset === 'gasto'} onclick={() => { dataset = 'gasto'; selectedEntity = null; }}>Gastos</button> | 127 | + <button class="toggle-btn" class:active={datasetGestion === 'gasto'} onclick={() => datasetGestion = 'gasto'}>Gastos</button> |
| 127 | - <button class="toggle-btn" class:active={dataset === 'ingreso'} onclick={() => { dataset = 'ingreso'; selectedEntity = null; }}>Ingresos</button> | 128 | + <button class="toggle-btn" class:active={datasetGestion === 'ingreso'} onclick={() => datasetGestion = 'ingreso'}>Ingresos</button> |
| 128 | - </div> | ||
| 129 | - </div> | ||
| 130 | - | ||
| 131 | - <!-- Por gestión --> | ||
| 132 | - <div class="subsection"> | ||
| 133 | - <div class="subsection-header"> | ||
| 134 | - <h3 class="subsection-title">Por gestión</h3> | ||
| 135 | - <div class="formato-toggle"> | ||
| 136 | - <span class="formato-label">Formato:</span> | ||
| 137 | - <button class="fmt-btn" class:active={formatoGestion === 'csv'} onclick={() => formatoGestion = 'csv'}>CSV</button> | ||
| 138 | - <button class="fmt-btn" class:active={formatoGestion === 'xlsx'} onclick={() => formatoGestion = 'xlsx'}>Excel</button> | ||
| 139 | </div> | 129 | </div> |
| 140 | </div> | 130 | </div> |
| 141 | - <div class="year-grid"> | 131 | + <div class="control-row"> |
| 142 | - {#each YEARS as year} | 132 | + <span class="control-label">Formato</span> |
| 143 | - <a href={buildUrl('gestion', year, formatoGestion)} download rel="external" class="year-link"> | 133 | + <div class="toggle-group"> |
| 144 | - <span class="year-num">{year}</span> | 134 | + <button class="toggle-btn" class:active={formatoGestion === 'csv'} onclick={() => formatoGestion = 'csv'}>CSV</button> |
| 145 | - <span class="year-fmt">.{formatoGestion === 'xlsx' ? 'xlsx' : 'csv'}</span> | 135 | + <button class="toggle-btn" class:active={formatoGestion === 'xlsx'} onclick={() => formatoGestion = 'xlsx'}>Excel</button> |
| 146 | - </a> | 136 | + </div> |
| 147 | - {/each} | ||
| 148 | </div> | 137 | </div> |
| 149 | </div> | 138 | </div> |
| 150 | 139 | ||
| 151 | - <!-- Por entidad --> | 140 | + <div class="year-grid"> |
| 152 | - <div class="subsection"> | 141 | + {#each YEARS as year} |
| 153 | - <div class="subsection-header"> | 142 | + <a href={buildGestionUrl(year)} download rel="external" class="year-link"> |
| 154 | - <h3 class="subsection-title">Por entidad</h3> | 143 | + <span class="year-num">{year}</span> |
| 155 | - <div class="formato-toggle"> | 144 | + <span class="year-fmt">.{formatoGestion === 'xlsx' ? 'xlsx' : 'csv'}</span> |
| 156 | - <span class="formato-label">Formato:</span> | 145 | + </a> |
| 157 | - <button class="fmt-btn" class:active={formatoEntidad === 'csv'} onclick={() => formatoEntidad = 'csv'}>CSV</button> | 146 | + {/each} |
| 158 | - <button class="fmt-btn" class:active={formatoEntidad === 'xlsx'} onclick={() => formatoEntidad = 'xlsx'}>Excel</button> | 147 | + </div> |
| 148 | + </section> | ||
| 149 | + | ||
| 150 | + <!-- ═══════════════════════════════════════════ --> | ||
| 151 | + <!-- SECCIÓN 2: Por entidad --> | ||
| 152 | + <!-- ═══════════════════════════════════════════ --> | ||
| 153 | + <section class="section"> | ||
| 154 | + <h2 class="section-title">Por entidad</h2> | ||
| 155 | + <p class="section-desc">Datos históricos de una entidad pública específica</p> | ||
| 156 | + | ||
| 157 | + <div class="controls-row"> | ||
| 158 | + <div class="control-row"> | ||
| 159 | + <span class="control-label">Base</span> | ||
| 160 | + <div class="toggle-group"> | ||
| 161 | + <button class="toggle-btn" class:active={datasetEntidad === 'gasto'} onclick={() => { datasetEntidad = 'gasto'; selectedEntity = null; }}>Gastos</button> | ||
| 162 | + <button class="toggle-btn" class:active={datasetEntidad === 'ingreso'} onclick={() => { datasetEntidad = 'ingreso'; selectedEntity = null; }}>Ingresos</button> | ||
| 159 | </div> | 163 | </div> |
| 160 | </div> | 164 | </div> |
| 161 | - <div class="entity-search"> | 165 | + <div class="control-row"> |
| 162 | - <input | 166 | + <span class="control-label">Formato</span> |
| 163 | - type="text" | 167 | + <div class="toggle-group"> |
| 164 | - bind:value={entitySearch} | 168 | + <button class="toggle-btn" class:active={formatoEntidad === 'csv'} onclick={() => formatoEntidad = 'csv'}>CSV</button> |
| 165 | - onfocus={() => { entityFocused = true; selectedEntity = null; }} | 169 | + <button class="toggle-btn" class:active={formatoEntidad === 'xlsx'} onclick={() => formatoEntidad = 'xlsx'}>Excel</button> |
| 166 | - onblur={() => setTimeout(() => entityFocused = false, 150)} | 170 | + </div> |
| 167 | - onkeydown={handleKeydown} | ||
| 168 | - placeholder="Buscar entidad..." | ||
| 169 | - class="search-input" | ||
| 170 | - /> | ||
| 171 | - {#if loadingEntities} | ||
| 172 | - <span class="search-status">Cargando entidades...</span> | ||
| 173 | - {:else if entityError} | ||
| 174 | - <span class="search-status error">No se pudieron cargar las entidades</span> | ||
| 175 | - {/if} | ||
| 176 | - | ||
| 177 | - {#if entityFocused && filteredEntities.length > 0} | ||
| 178 | - <div class="search-dropdown"> | ||
| 179 | - {#each filteredEntities as entity, i} | ||
| 180 | - <button | ||
| 181 | - class="search-item" | ||
| 182 | - class:active={selectedIdx === i} | ||
| 183 | - onmouseenter={() => selectedIdx = i} | ||
| 184 | - onclick={() => selectEntity(entity)} | ||
| 185 | - > | ||
| 186 | - <span class="item-name">{entity.desc_entidad}</span> | ||
| 187 | - <span class="item-code">Código {entity.entidad}</span> | ||
| 188 | - </button> | ||
| 189 | - {/each} | ||
| 190 | - </div> | ||
| 191 | - {/if} | ||
| 192 | - | ||
| 193 | - {#if entityFocused && entitySearch.length >= 2 && filteredEntities.length === 0 && !loadingEntities} | ||
| 194 | - <span class="search-status error">Sin resultados</span> | ||
| 195 | - {/if} | ||
| 196 | </div> | 171 | </div> |
| 172 | + </div> | ||
| 197 | 173 | ||
| 198 | - {#if selectedEntity} | 174 | + <div class="entity-search"> |
| 199 | - <div class="entity-result"> | 175 | + <input |
| 200 | - <span class="entity-name">{selectedEntity.desc_entidad}</span> | 176 | + type="text" |
| 201 | - <a href={buildUrl('entidad', selectedEntity.entidad, formatoEntidad)} download rel="external" class="download-link"> | 177 | + bind:value={entitySearch} |
| 202 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> | 178 | + onfocus={() => { entityFocused = true; selectedEntity = null; }} |
| 203 | - Descargar .{formatoEntidad === 'xlsx' ? 'xlsx' : 'csv'} | 179 | + onblur={() => setTimeout(() => entityFocused = false, 150)} |
| 204 | - </a> | 180 | + onkeydown={handleKeydown} |
| 181 | + placeholder="Buscar entidad..." | ||
| 182 | + class="search-input" | ||
| 183 | + /> | ||
| 184 | + {#if loadingEntities} | ||
| 185 | + <span class="search-status">Cargando entidades...</span> | ||
| 186 | + {:else if entityError} | ||
| 187 | + <span class="search-status error">No se pudieron cargar las entidades</span> | ||
| 188 | + {/if} | ||
| 189 | + | ||
| 190 | + {#if entityFocused && filteredEntities.length > 0} | ||
| 191 | + <div class="search-dropdown"> | ||
| 192 | + {#each filteredEntities as entity, i} | ||
| 193 | + <button | ||
| 194 | + class="search-item" | ||
| 195 | + class:active={selectedIdx === i} | ||
| 196 | + onmouseenter={() => selectedIdx = i} | ||
| 197 | + onclick={() => selectEntity(entity)} | ||
| 198 | + > | ||
| 199 | + <span class="item-name">{entity.desc_entidad}</span> | ||
| 200 | + <span class="item-code">Código {entity.entidad}</span> | ||
| 201 | + </button> | ||
| 202 | + {/each} | ||
| 205 | </div> | 203 | </div> |
| 206 | {/if} | 204 | {/if} |
| 205 | + | ||
| 206 | + {#if entityFocused && entitySearch.length >= 2 && filteredEntities.length === 0 && !loadingEntities} | ||
| 207 | + <span class="search-status error">Sin resultados</span> | ||
| 208 | + {/if} | ||
| 207 | </div> | 209 | </div> |
| 210 | + | ||
| 211 | + {#if selectedEntity} | ||
| 212 | + <div class="entity-result"> | ||
| 213 | + <span class="entity-name">{selectedEntity.desc_entidad}</span> | ||
| 214 | + <a href={buildEntidadUrl(selectedEntity.entidad)} download rel="external" class="download-link"> | ||
| 215 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> | ||
| 216 | + Descargar .{formatoEntidad === 'xlsx' ? 'xlsx' : 'csv'} | ||
| 217 | + </a> | ||
| 218 | + </div> | ||
| 219 | + {/if} | ||
| 208 | </section> | 220 | </section> |
| 209 | 221 | ||
| 210 | <!-- ═══════════════════════════════════════════ --> | 222 | <!-- ═══════════════════════════════════════════ --> |
| 211 | - <!-- SECCIÓN 2: Base completa --> | 223 | + <!-- SECCIÓN 3: Base completa --> |
| 212 | <!-- ═══════════════════════════════════════════ --> | 224 | <!-- ═══════════════════════════════════════════ --> |
| 213 | <section class="section"> | 225 | <section class="section"> |
| 214 | <h2 class="section-title">Base completa</h2> | 226 | <h2 class="section-title">Base completa</h2> |
| 215 | <p class="section-desc">Toda la base histórica: todas las entidades y todos los años en un solo archivo</p> | 227 | <p class="section-desc">Toda la base histórica: todas las entidades y todos los años en un solo archivo</p> |
| 216 | 228 | ||
| 217 | - <div class="control-row"> | 229 | + <div class="controls-row"> |
| 218 | - <span class="control-label">Base</span> | 230 | + <div class="control-row"> |
| 219 | - <div class="toggle-group"> | 231 | + <span class="control-label">Base</span> |
| 220 | - <button class="toggle-btn" class:active={datasetMasivo === 'gasto'} onclick={() => datasetMasivo = 'gasto'}>Gastos</button> | 232 | + <div class="toggle-group"> |
| 221 | - <button class="toggle-btn" class:active={datasetMasivo === 'ingreso'} onclick={() => datasetMasivo = 'ingreso'}>Ingresos</button> | 233 | + <button class="toggle-btn" class:active={datasetMasivo === 'gasto'} onclick={() => datasetMasivo = 'gasto'}>Gastos</button> |
| 234 | + <button class="toggle-btn" class:active={datasetMasivo === 'ingreso'} onclick={() => datasetMasivo = 'ingreso'}>Ingresos</button> | ||
| 235 | + </div> | ||
| 222 | </div> | 236 | </div> |
| 223 | </div> | 237 | </div> |
| 224 | 238 | ||
| ... | @@ -245,14 +259,13 @@ | ... | @@ -245,14 +259,13 @@ |
| 245 | </section> | 259 | </section> |
| 246 | 260 | ||
| 247 | <!-- ═══════════════════════════════════════════ --> | 261 | <!-- ═══════════════════════════════════════════ --> |
| 248 | - <!-- SECCIÓN 3: Ejecución semanal --> | 262 | + <!-- SECCIÓN 4: Ejecución semanal --> |
| 249 | <!-- ═══════════════════════════════════════════ --> | 263 | <!-- ═══════════════════════════════════════════ --> |
| 250 | <section class="section"> | 264 | <section class="section"> |
| 251 | <h2 class="section-title">Ejecución semanal</h2> | 265 | <h2 class="section-title">Ejecución semanal</h2> |
| 252 | - <p class="section-desc">Reporte semanal de ejecución presupuestaria con datos agregados</p> | 266 | + <p class="section-desc">Reporte semanal de ejecución presupuestaria</p> |
| 253 | 267 | ||
| 254 | <div class="semanal-downloads disabled"> | 268 | <div class="semanal-downloads disabled"> |
| 255 | - <span class="semanal-badge">Próximamente</span> | ||
| 256 | <div class="masivo-downloads"> | 269 | <div class="masivo-downloads"> |
| 257 | <span class="download-card download-card-disabled"> | 270 | <span class="download-card download-card-disabled"> |
| 258 | <div class="download-card-icon"> | 271 | <div class="download-card-icon"> |
| ... | @@ -277,7 +290,7 @@ | ... | @@ -277,7 +290,7 @@ |
| 277 | </section> | 290 | </section> |
| 278 | 291 | ||
| 279 | <!-- ═══════════════════════════════════════════ --> | 292 | <!-- ═══════════════════════════════════════════ --> |
| 280 | - <!-- SECCIÓN 4: Para desarrolladores --> | 293 | + <!-- SECCIÓN 5: Para desarrolladores --> |
| 281 | <!-- ═══════════════════════════════════════════ --> | 294 | <!-- ═══════════════════════════════════════════ --> |
| 282 | <section class="section section-last"> | 295 | <section class="section section-last"> |
| 283 | <h2 class="section-title">Para desarrolladores</h2> | 296 | <h2 class="section-title">Para desarrolladores</h2> |
| ... | @@ -326,9 +339,8 @@ | ... | @@ -326,9 +339,8 @@ |
| 326 | } | 339 | } |
| 327 | 340 | ||
| 328 | .title { | 341 | .title { |
| 329 | - font-family: 'DM Serif Display', serif; | ||
| 330 | font-size: 2rem; | 342 | font-size: 2rem; |
| 331 | - font-weight: 400; | 343 | + font-weight: 700; |
| 332 | margin: 0 0 0.25rem; | 344 | margin: 0 0 0.25rem; |
| 333 | letter-spacing: -0.02em; | 345 | letter-spacing: -0.02em; |
| 334 | } | 346 | } |
| ... | @@ -364,12 +376,18 @@ | ... | @@ -364,12 +376,18 @@ |
| 364 | line-height: 1.5; | 376 | line-height: 1.5; |
| 365 | } | 377 | } |
| 366 | 378 | ||
| 367 | - /* Control row */ | 379 | + /* Controls */ |
| 380 | + .controls-row { | ||
| 381 | + display: flex; | ||
| 382 | + flex-wrap: wrap; | ||
| 383 | + gap: 1rem; | ||
| 384 | + margin-bottom: 1.25rem; | ||
| 385 | + } | ||
| 386 | + | ||
| 368 | .control-row { | 387 | .control-row { |
| 369 | display: flex; | 388 | display: flex; |
| 370 | align-items: center; | 389 | align-items: center; |
| 371 | gap: 0.75rem; | 390 | gap: 0.75rem; |
| 372 | - margin-bottom: 1.25rem; | ||
| 373 | } | 391 | } |
| 374 | 392 | ||
| 375 | .control-label { | 393 | .control-label { |
| ... | @@ -415,65 +433,6 @@ | ... | @@ -415,65 +433,6 @@ |
| 415 | color: var(--theme-titulo); | 433 | color: var(--theme-titulo); |
| 416 | } | 434 | } |
| 417 | 435 | ||
| 418 | - /* Subsections */ | ||
| 419 | - .subsection { | ||
| 420 | - margin-bottom: 1.5rem; | ||
| 421 | - } | ||
| 422 | - .subsection:last-child { margin-bottom: 0; } | ||
| 423 | - | ||
| 424 | - .subsection-header { | ||
| 425 | - display: flex; | ||
| 426 | - align-items: center; | ||
| 427 | - justify-content: space-between; | ||
| 428 | - margin-bottom: 0.75rem; | ||
| 429 | - flex-wrap: wrap; | ||
| 430 | - gap: 0.5rem; | ||
| 431 | - } | ||
| 432 | - | ||
| 433 | - .subsection-title { | ||
| 434 | - font-size: 0.9375rem; | ||
| 435 | - font-weight: 600; | ||
| 436 | - margin: 0; | ||
| 437 | - } | ||
| 438 | - | ||
| 439 | - /* Formato toggle */ | ||
| 440 | - .formato-toggle { | ||
| 441 | - display: flex; | ||
| 442 | - align-items: center; | ||
| 443 | - gap: 0.375rem; | ||
| 444 | - } | ||
| 445 | - | ||
| 446 | - .formato-label { | ||
| 447 | - font-size: 0.6875rem; | ||
| 448 | - color: var(--theme-texto); | ||
| 449 | - opacity: 0.5; | ||
| 450 | - margin-right: 0.25rem; | ||
| 451 | - } | ||
| 452 | - | ||
| 453 | - .fmt-btn { | ||
| 454 | - font-family: 'Qanelas', var(--font-sans); | ||
| 455 | - font-size: 0.6875rem; | ||
| 456 | - font-weight: 600; | ||
| 457 | - padding: 0.3rem 0.75rem; | ||
| 458 | - border: 1px solid var(--theme-borde); | ||
| 459 | - border-radius: 6px; | ||
| 460 | - background: transparent; | ||
| 461 | - color: var(--theme-texto); | ||
| 462 | - cursor: pointer; | ||
| 463 | - transition: all 0.15s; | ||
| 464 | - } | ||
| 465 | - .fmt-btn:hover { border-color: var(--theme-accent); color: var(--theme-titulo); } | ||
| 466 | - .fmt-btn.active { | ||
| 467 | - background: var(--theme-titulo); | ||
| 468 | - border-color: var(--theme-titulo); | ||
| 469 | - color: var(--theme-body); | ||
| 470 | - } | ||
| 471 | - :global(.dark) .fmt-btn.active { | ||
| 472 | - background: var(--theme-accent); | ||
| 473 | - border-color: var(--theme-accent); | ||
| 474 | - color: var(--theme-body); | ||
| 475 | - } | ||
| 476 | - | ||
| 477 | /* Years */ | 436 | /* Years */ |
| 478 | .year-grid { | 437 | .year-grid { |
| 479 | display: grid; | 438 | display: grid; |
| ... | @@ -777,10 +736,10 @@ | ... | @@ -777,10 +736,10 @@ |
| 777 | @media (max-width: 640px) { | 736 | @media (max-width: 640px) { |
| 778 | .layout { padding: 1rem 1rem 2rem; } | 737 | .layout { padding: 1rem 1rem 2rem; } |
| 779 | .title { font-size: 1.75rem; } | 738 | .title { font-size: 1.75rem; } |
| 739 | + .controls-row { flex-direction: column; gap: 0.75rem; } | ||
| 780 | .year-grid { grid-template-columns: repeat(auto-fill, minmax(75px, 1fr)); } | 740 | .year-grid { grid-template-columns: repeat(auto-fill, minmax(75px, 1fr)); } |
| 781 | .masivo-downloads { grid-template-columns: 1fr; } | 741 | .masivo-downloads { grid-template-columns: 1fr; } |
| 782 | .search-input, .search-dropdown, .entity-result { max-width: 100%; } | 742 | .search-input, .search-dropdown, .entity-result { max-width: 100%; } |
| 783 | - .subsection-header { flex-direction: column; align-items: flex-start; } | ||
| 784 | .entity-result { flex-direction: column; align-items: flex-start; } | 743 | .entity-result { flex-direction: column; align-items: flex-start; } |
| 785 | } | 744 | } |
| 786 | </style> | 745 | </style> | ... | ... |
| ... | @@ -10,6 +10,8 @@ | ... | @@ -10,6 +10,8 @@ |
| 10 | let timeline = $state([]); | 10 | let timeline = $state([]); |
| 11 | let loading = $state(true); | 11 | let loading = $state(true); |
| 12 | let error = $state(null); | 12 | let error = $state(null); |
| 13 | + let fechaActualizacion = $state(null); | ||
| 14 | + let fechaActualizacionRaw = $state(''); | ||
| 13 | 15 | ||
| 14 | // Controles interactivos | 16 | // Controles interactivos |
| 15 | let diasSeleccionados = $state('total'); | 17 | let diasSeleccionados = $state('total'); |
| ... | @@ -93,8 +95,99 @@ | ... | @@ -93,8 +95,99 @@ |
| 93 | document.removeEventListener('mouseup', handleRangeMouseUp); | 95 | document.removeEventListener('mouseup', handleRangeMouseUp); |
| 94 | } | 96 | } |
| 95 | 97 | ||
| 98 | + // Fecha narrativa | ||
| 99 | + let fechaNarrativa = $derived.by(() => { | ||
| 100 | + if (!fechaActualizacion) return ''; | ||
| 101 | + const dias = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado']; | ||
| 102 | + const meses = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre']; | ||
| 103 | + const dia = dias[fechaActualizacion.getDay()]; | ||
| 104 | + const num = fechaActualizacion.getDate(); | ||
| 105 | + const mes = meses[fechaActualizacion.getMonth()]; | ||
| 106 | + const anio = fechaActualizacion.getFullYear(); | ||
| 107 | + return `${dia} ${num} de ${mes} de ${anio}`; | ||
| 108 | + }); | ||
| 109 | + | ||
| 110 | + let horaActualizacion = $derived.by(() => { | ||
| 111 | + if (!fechaActualizacionRaw) return ''; | ||
| 112 | + // Extraer hora directamente del string ISO tal cual viene | ||
| 113 | + const match = fechaActualizacionRaw.match(/T(\d{2}):(\d{2}):(\d{2})/); | ||
| 114 | + if (!match) return ''; | ||
| 115 | + return `${match[1]}:${match[2]}:${match[3]}`; | ||
| 116 | + }); | ||
| 117 | + | ||
| 96 | // Búsqueda para timeline cards | 118 | // Búsqueda para timeline cards |
| 97 | let busquedaTimeline = $state(''); | 119 | let busquedaTimeline = $state(''); |
| 120 | + let searchResults = $state([]); | ||
| 121 | + let searchLoading = $state(false); | ||
| 122 | + let searchTimer = null; | ||
| 123 | + | ||
| 124 | + let searchEntities = $state([]); | ||
| 125 | + | ||
| 126 | + async function buscarEnAPI(query) { | ||
| 127 | + if (!query || query.trim().length < 2) { | ||
| 128 | + searchResults = []; | ||
| 129 | + searchEntities = []; | ||
| 130 | + return; | ||
| 131 | + } | ||
| 132 | + searchLoading = true; | ||
| 133 | + try { | ||
| 134 | + const params = new URLSearchParams({ | ||
| 135 | + q: query, | ||
| 136 | + per_page: '10', | ||
| 137 | + is_class: 'true', | ||
| 138 | + class_: 'entidad' | ||
| 139 | + }); | ||
| 140 | + const res = await fetch(`/api/search?${params}`); | ||
| 141 | + if (!res.ok) throw new Error(`Error ${res.status}`); | ||
| 142 | + const data = await res.json(); | ||
| 143 | + searchResults = data.hits || []; | ||
| 144 | + | ||
| 145 | + // Para cada entidad encontrada, cargar sus programas del timeline | ||
| 146 | + const entities = await Promise.all(searchResults.map(async (hit) => { | ||
| 147 | + const doc = hit.document; | ||
| 148 | + const meta = parseSearchMetadatos(doc.metadatos); | ||
| 149 | + const codigo = meta.entidad || ''; | ||
| 150 | + let programas = []; | ||
| 151 | + try { | ||
| 152 | + const tlRes = await fetch(`/api/diario?tipo=timeline&entidad=${codigo}`); | ||
| 153 | + if (tlRes.ok) { | ||
| 154 | + const tlData = await tlRes.json(); | ||
| 155 | + programas = (Array.isArray(tlData) ? tlData : []).map(item => ({ | ||
| 156 | + programa: item.desc_programa || '', | ||
| 157 | + actividad: item.desc_actividad_proyecto || '', | ||
| 158 | + devengado: item.devengado || 0 | ||
| 159 | + })).sort((a, b) => b.devengado - a.devengado); | ||
| 160 | + } | ||
| 161 | + } catch { /* silencioso */ } | ||
| 162 | + const total = programas.reduce((s, p) => s + p.devengado, 0); | ||
| 163 | + return { | ||
| 164 | + entidad: String(codigo), | ||
| 165 | + entidad_desc_area: meta.entidad_desc_area || '', | ||
| 166 | + entidad_desc_entidad: doc.texto || '', | ||
| 167 | + total: total || doc.devengado || 0, | ||
| 168 | + programas | ||
| 169 | + }; | ||
| 170 | + })); | ||
| 171 | + searchEntities = entities; | ||
| 172 | + } catch (e) { | ||
| 173 | + console.error('Error en búsqueda:', e); | ||
| 174 | + searchResults = []; | ||
| 175 | + searchEntities = []; | ||
| 176 | + } finally { | ||
| 177 | + searchLoading = false; | ||
| 178 | + } | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + function handleSearchInput() { | ||
| 182 | + clearTimeout(searchTimer); | ||
| 183 | + searchTimer = setTimeout(() => buscarEnAPI(busquedaTimeline), 300); | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + function parseSearchMetadatos(meta) { | ||
| 187 | + if (!meta) return {}; | ||
| 188 | + if (typeof meta === 'object') return meta; | ||
| 189 | + try { return JSON.parse(meta); } catch { return {}; } | ||
| 190 | + } | ||
| 98 | 191 | ||
| 99 | // Labels dinámicos para periodos de tiempo | 192 | // Labels dinámicos para periodos de tiempo |
| 100 | function formatearFechaCorta(fecha) { | 193 | function formatearFechaCorta(fecha) { |
| ... | @@ -130,11 +223,6 @@ | ... | @@ -130,11 +223,6 @@ |
| 130 | 223 | ||
| 131 | function irASeccion(index) { | 224 | function irASeccion(index) { |
| 132 | seccionActiva = index; | 225 | seccionActiva = index; |
| 133 | - const elemento = document.getElementById(secciones[index].id); | ||
| 134 | - if (elemento && vistaContainer) { | ||
| 135 | - elemento.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||
| 136 | - } | ||
| 137 | - // Actualizar URL hash | ||
| 138 | history.replaceState(null, '', `#${secciones[index].id}`); | 226 | history.replaceState(null, '', `#${secciones[index].id}`); |
| 139 | } | 227 | } |
| 140 | 228 | ||
| ... | @@ -192,7 +280,7 @@ | ... | @@ -192,7 +280,7 @@ |
| 192 | // Colores del tema | 280 | // Colores del tema |
| 193 | const colores = { | 281 | const colores = { |
| 194 | base: '#363534', | 282 | base: '#363534', |
| 195 | - claro: '#F3EAD8', | 283 | + claro: '#4A8B6E', |
| 196 | rojo: '#DB281C', | 284 | rojo: '#DB281C', |
| 197 | amarillo: '#F4E410', | 285 | amarillo: '#F4E410', |
| 198 | verde: '#007C38', | 286 | verde: '#007C38', |
| ... | @@ -208,7 +296,7 @@ | ... | @@ -208,7 +296,7 @@ |
| 208 | function getChartColors() { | 296 | function getChartColors() { |
| 209 | return { | 297 | return { |
| 210 | barDefault: getCSSVar('--chart-bar-default') || '#e0e0e0', | 298 | barDefault: getCSSVar('--chart-bar-default') || '#e0e0e0', |
| 211 | - barHighlight: getCSSVar('--chart-bar-highlight') || '#F3EAD8', | 299 | + barHighlight: getCSSVar('--chart-bar-highlight') || '#4A8B6E', |
| 212 | barStroke: getCSSVar('--chart-bar-stroke') || '#b0b0b0', | 300 | barStroke: getCSSVar('--chart-bar-stroke') || '#b0b0b0', |
| 213 | barStrokeDefault: getCSSVar('--chart-bar-stroke-default') || 'white', | 301 | barStrokeDefault: getCSSVar('--chart-bar-stroke-default') || 'white', |
| 214 | trackBg: getCSSVar('--chart-track-bg') || '#f5f5f5', | 302 | trackBg: getCSSVar('--chart-track-bg') || '#f5f5f5', |
| ... | @@ -250,11 +338,12 @@ | ... | @@ -250,11 +338,12 @@ |
| 250 | 338 | ||
| 251 | try { | 339 | try { |
| 252 | // Cargar todos los datos en paralelo desde API | 340 | // Cargar todos los datos en paralelo desde API |
| 253 | - const [tsData, entData, clasData, tlData] = await Promise.all([ | 341 | + const [tsData, entData, clasData, tlData, statusData] = await Promise.all([ |
| 254 | cargarDiario('timeseries'), | 342 | cargarDiario('timeseries'), |
| 255 | cargarDiario('entidades'), | 343 | cargarDiario('entidades'), |
| 256 | cargarDiario('clasificadores'), | 344 | cargarDiario('clasificadores'), |
| 257 | - cargarDiario('timeline') | 345 | + cargarDiario('timeline'), |
| 346 | + cargarDiario('status') | ||
| 258 | ]); | 347 | ]); |
| 259 | 348 | ||
| 260 | // Los datos ya vienen como objetos | 349 | // Los datos ya vienen como objetos |
| ... | @@ -262,6 +351,10 @@ | ... | @@ -262,6 +351,10 @@ |
| 262 | ejecucionEntidades = entData; | 351 | ejecucionEntidades = entData; |
| 263 | ejecucionClasificadores = clasData; | 352 | ejecucionClasificadores = clasData; |
| 264 | timeline = tlData; | 353 | timeline = tlData; |
| 354 | + if (statusData?.fecha_actualizacion) { | ||
| 355 | + fechaActualizacionRaw = statusData.fecha_actualizacion; | ||
| 356 | + fechaActualizacion = new Date(statusData.fecha_actualizacion); | ||
| 357 | + } | ||
| 265 | 358 | ||
| 266 | loading = false; | 359 | loading = false; |
| 267 | 360 | ||
| ... | @@ -285,13 +378,10 @@ | ... | @@ -285,13 +378,10 @@ |
| 285 | 378 | ||
| 286 | // Datos por clasificador para las barras apiladas (sin agrupar, cada hijo es un segmento) | 379 | // Datos por clasificador para las barras apiladas (sin agrupar, cada hijo es un segmento) |
| 287 | let datosPorClasificador = $derived.by(() => { | 380 | let datosPorClasificador = $derived.by(() => { |
| 288 | - const dias = diasSeleccionados === 'total' ? 0 : parseInt(diasSeleccionados); | ||
| 289 | const result = {}; | 381 | const result = {}; |
| 290 | 382 | ||
| 291 | clasificadores.forEach(({ key }) => { | 383 | clasificadores.forEach(({ key }) => { |
| 292 | const filtered = ejecucionClasificadores.filter(d => | 384 | const filtered = ejecucionClasificadores.filter(d => |
| 293 | - d.transferencias === incluirTransferencias && | ||
| 294 | - String(d.dias) === String(dias) && | ||
| 295 | d.clasificador === key | 385 | d.clasificador === key |
| 296 | ); | 386 | ); |
| 297 | 387 | ||
| ... | @@ -443,12 +533,43 @@ | ... | @@ -443,12 +533,43 @@ |
| 443 | } | 533 | } |
| 444 | }); | 534 | }); |
| 445 | 535 | ||
| 536 | + // Re-renderizar gráficos al cambiar de sección (porque display:none → ancho 0) | ||
| 537 | + $effect(() => { | ||
| 538 | + const seccion = seccionActiva; | ||
| 539 | + if (loading) return; | ||
| 540 | + setTimeout(() => { | ||
| 541 | + if (seccion === 1) { | ||
| 542 | + clasificadores.forEach(({ key }) => { | ||
| 543 | + if (barContainers[key]) renderizarBarras(key); | ||
| 544 | + }); | ||
| 545 | + } else if (seccion === 2 && entidadesBarContainer) { | ||
| 546 | + renderizarEntidades(); | ||
| 547 | + } | ||
| 548 | + }, 50); | ||
| 549 | + }); | ||
| 550 | + | ||
| 551 | + // Recargar datos cuando cambia el período | ||
| 552 | + $effect(() => { | ||
| 553 | + const dias = diasSeleccionados === 'total' ? '0' : diasSeleccionados; | ||
| 554 | + recargarPorDias(dias); | ||
| 555 | + }); | ||
| 556 | + | ||
| 557 | + async function recargarPorDias(dias) { | ||
| 558 | + try { | ||
| 559 | + const [entData, clasData] = await Promise.all([ | ||
| 560 | + fetch(`/api/diario?tipo=entidades&dias=${dias}`).then(r => r.ok ? r.json() : []), | ||
| 561 | + fetch(`/api/diario?tipo=clasificadores&dias=${dias}`).then(r => r.ok ? r.json() : []) | ||
| 562 | + ]); | ||
| 563 | + ejecucionEntidades = entData; | ||
| 564 | + ejecucionClasificadores = clasData; | ||
| 565 | + } catch (e) { | ||
| 566 | + console.error('Error recargando datos:', e); | ||
| 567 | + } | ||
| 568 | + } | ||
| 569 | + | ||
| 446 | // Todas las entidades filtradas (sin límite) | 570 | // Todas las entidades filtradas (sin límite) |
| 447 | let entidadesTodas = $derived.by(() => { | 571 | let entidadesTodas = $derived.by(() => { |
| 448 | - const dias = diasSeleccionados === 'total' ? 0 : parseInt(diasSeleccionados); | ||
| 449 | let filtered = ejecucionEntidades.filter(d => | 572 | let filtered = ejecucionEntidades.filter(d => |
| 450 | - d.transferencias === incluirTransferencias && | ||
| 451 | - String(d.dias) === String(dias) && | ||
| 452 | d.ejecucion >= rangoEjecucionMin && | 573 | d.ejecucion >= rangoEjecucionMin && |
| 453 | d.ejecucion <= rangoEjecucionMax | 574 | d.ejecucion <= rangoEjecucionMax |
| 454 | ); | 575 | ); |
| ... | @@ -473,9 +594,14 @@ | ... | @@ -473,9 +594,14 @@ |
| 473 | 594 | ||
| 474 | // Agrupar timeline por entidad con sus programas | 595 | // Agrupar timeline por entidad con sus programas |
| 475 | let timelineByEntity = $derived.by(() => { | 596 | let timelineByEntity = $derived.by(() => { |
| 597 | + // Si hay búsqueda activa, usar entidades cargadas con sus programas | ||
| 598 | + if (busquedaTimeline.trim().length >= 2 && searchEntities.length > 0) { | ||
| 599 | + return searchEntities; | ||
| 600 | + } | ||
| 601 | + | ||
| 602 | + // Sin búsqueda: usar datos locales de timeline | ||
| 476 | if (timeline.length === 0) return []; | 603 | if (timeline.length === 0) return []; |
| 477 | 604 | ||
| 478 | - // Agrupar por entidad | ||
| 479 | const grouped = {}; | 605 | const grouped = {}; |
| 480 | timeline.forEach(item => { | 606 | timeline.forEach(item => { |
| 481 | const key = item.entidad; | 607 | const key = item.entidad; |
| ... | @@ -496,24 +622,10 @@ | ... | @@ -496,24 +622,10 @@ |
| 496 | }); | 622 | }); |
| 497 | }); | 623 | }); |
| 498 | 624 | ||
| 499 | - // Convertir a array y ordenar por total | 625 | + return Object.values(grouped) |
| 500 | - let result = Object.values(grouped) | 626 | + .map(g => ({ ...g, programas: g.programas.sort((a, b) => b.devengado - a.devengado) })) |
| 501 | - .map(g => ({ | 627 | + .sort((a, b) => b.total - a.total) |
| 502 | - ...g, | 628 | + .slice(0, 10); |
| 503 | - programas: g.programas.sort((a, b) => b.devengado - a.devengado) | ||
| 504 | - })) | ||
| 505 | - .sort((a, b) => b.total - a.total); | ||
| 506 | - | ||
| 507 | - // Filtrar por búsqueda si hay texto | ||
| 508 | - if (busquedaTimeline.trim()) { | ||
| 509 | - const q = busquedaTimeline.toLowerCase(); | ||
| 510 | - result = result.filter(e => | ||
| 511 | - e.entidad_desc_entidad?.toLowerCase().includes(q) || | ||
| 512 | - e.entidad_desc_area?.toLowerCase().includes(q) | ||
| 513 | - ); | ||
| 514 | - } | ||
| 515 | - | ||
| 516 | - return result.slice(0, 10); | ||
| 517 | }); | 629 | }); |
| 518 | 630 | ||
| 519 | // Renderizar barras de entidades con D3 | 631 | // Renderizar barras de entidades con D3 |
| ... | @@ -666,9 +778,10 @@ | ... | @@ -666,9 +778,10 @@ |
| 666 | .attr('y1', headerHeight) | 778 | .attr('y1', headerHeight) |
| 667 | .attr('y2', height) | 779 | .attr('y2', height) |
| 668 | .attr('stroke', chartColors.grid) | 780 | .attr('stroke', chartColors.grid) |
| 669 | - .attr('stroke-width', 0.5) | 781 | + .attr('stroke-width', 0.75) |
| 670 | - .attr('stroke-dasharray', '1,4') | 782 | + .attr('stroke-dasharray', 'none') |
| 671 | .attr('stroke-linecap', 'round') | 783 | .attr('stroke-linecap', 'round') |
| 784 | + .attr('opacity', 0.6) | ||
| 672 | .attr('opacity', 0.7) | 785 | .attr('opacity', 0.7) |
| 673 | .style('pointer-events', 'none'); // para que no interfiera con hover | 786 | .style('pointer-events', 'none'); // para que no interfiera con hover |
| 674 | } | 787 | } |
| ... | @@ -773,7 +886,7 @@ | ... | @@ -773,7 +886,7 @@ |
| 773 | const colorFuera = isDark ? '#555550' : '#d0d0d0'; | 886 | const colorFuera = isDark ? '#555550' : '#d0d0d0'; |
| 774 | const colorFueraFill = isDark ? '#3a3a38' : '#e8e8e8'; | 887 | const colorFueraFill = isDark ? '#3a3a38' : '#e8e8e8'; |
| 775 | // Color de fill activo = mismo que las barras highlight | 888 | // Color de fill activo = mismo que las barras highlight |
| 776 | - const colorActivoFill = isDark ? '#C9A751' : '#F3EAD8'; | 889 | + const colorActivoFill = isDark ? '#C9A751' : '#4A8B6E'; |
| 777 | const colorActivoLinea = isDark ? '#888880' : '#999999'; | 890 | const colorActivoLinea = isDark ? '#888880' : '#999999'; |
| 778 | 891 | ||
| 779 | // Gráfico 1: Ejecución diaria (diferencia) | 892 | // Gráfico 1: Ejecución diaria (diferencia) |
| ... | @@ -949,24 +1062,33 @@ | ... | @@ -949,24 +1062,33 @@ |
| 949 | <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> | 1062 | <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> |
| 950 | </svelte:head> | 1063 | </svelte:head> |
| 951 | 1064 | ||
| 952 | -<div class="vista-diaria" style="font-family: var(--font-sans);" bind:this={vistaContainer} onscroll={detectarSeccion}> | 1065 | +<!-- Botón flotante de descargas --> |
| 953 | - <!-- Navegación interna fija --> | 1066 | +<a href="/descargas" class="fab-descargas" title="Ir a la página de descarga de datos"> |
| 954 | - <nav class="nav-interna"> | 1067 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 955 | - {#each secciones as seccion, i} | 1068 | + <path d="M12 4v12"/><polyline points="8 12 12 16 16 12"/><path d="M4 18h16"/> |
| 956 | - <button | 1069 | + </svg> |
| 957 | - class="nav-item" | 1070 | + <span class="fab-tooltip">Ir a la página de descarga de datos</span> |
| 958 | - class:active={seccionActiva === i} | 1071 | +</a> |
| 959 | - onclick={() => irASeccion(i)} | 1072 | + |
| 960 | - > | 1073 | +<!-- Navegación interna fija - barra inferior (fuera del scroll container) --> |
| 961 | - <span class="nav-dot"></span> | 1074 | +<nav class="nav-bottom-bar"> |
| 962 | - <span class="nav-label">{seccion.titulo}</span> | 1075 | + <button class="nav-arrow" onclick={() => irASeccion(Math.max(0, seccionActiva - 1))} disabled={seccionActiva === 0}> |
| 963 | - </button> | 1076 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 964 | - {/each} | 1077 | + <path d="M15 18l-6-6 6-6"/> |
| 1078 | + </svg> | ||
| 1079 | + </button> | ||
| 1080 | + <span class="nav-section-label">{secciones[seccionActiva]?.titulo || ''}</span> | ||
| 1081 | + <button class="nav-arrow" onclick={() => irASeccion(Math.min(secciones.length - 1, seccionActiva + 1))} disabled={seccionActiva === secciones.length - 1}> | ||
| 1082 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 1083 | + <path d="M9 18l6-6-6-6"/> | ||
| 1084 | + </svg> | ||
| 1085 | + </button> | ||
| 965 | </nav> | 1086 | </nav> |
| 966 | 1087 | ||
| 1088 | +<div class="vista-diaria" style="font-family: var(--font-sans);" bind:this={vistaContainer}> | ||
| 967 | {#if loading} | 1089 | {#if loading} |
| 968 | <div class="loading"> | 1090 | <div class="loading"> |
| 969 | - <p>Cargando datos...</p> | 1091 | + <div class="loader-spinner"></div> |
| 970 | </div> | 1092 | </div> |
| 971 | {:else if error} | 1093 | {:else if error} |
| 972 | <div class="error"> | 1094 | <div class="error"> |
| ... | @@ -974,12 +1096,17 @@ | ... | @@ -974,12 +1096,17 @@ |
| 974 | </div> | 1096 | </div> |
| 975 | {:else} | 1097 | {:else} |
| 976 | <!-- SECCIÓN 1: Programas (timeline) --> | 1098 | <!-- SECCIÓN 1: Programas (timeline) --> |
| 977 | - <section class="vista-seccion" id="programas"> | 1099 | + <section class="vista-seccion" class:seccion-activa={seccionActiva === 0} id="programas"> |
| 978 | <div class="vista-contenido"> | 1100 | <div class="vista-contenido"> |
| 979 | <div class="contenido-centrado"> | 1101 | <div class="contenido-centrado"> |
| 980 | <header class="seccion-header seccion-header-centrado"> | 1102 | <header class="seccion-header seccion-header-centrado"> |
| 981 | - <h1 class="seccion-titulo-grande">¿En qué programas?</h1> | 1103 | + {#if fechaNarrativa} |
| 982 | - <p class="seccion-descripcion">Detalle de programas y actividades ejecutadas en la última semana</p> | 1104 | + <p class="vista-fecha-titulo">Ejecución presupuestaria del {fechaNarrativa}</p> |
| 1105 | + {/if} | ||
| 1106 | + <h1 class="seccion-titulo-sub">Por programas, proyectos y entidad</h1> | ||
| 1107 | + {#if horaActualizacion} | ||
| 1108 | + <p class="vista-fecha-hora">Actualización: {horaActualizacion}</p> | ||
| 1109 | + {/if} | ||
| 983 | </header> | 1110 | </header> |
| 984 | 1111 | ||
| 985 | <div class="timeline-search"> | 1112 | <div class="timeline-search"> |
| ... | @@ -990,11 +1117,18 @@ | ... | @@ -990,11 +1117,18 @@ |
| 990 | type="text" | 1117 | type="text" |
| 991 | placeholder="Buscar entidad..." | 1118 | placeholder="Buscar entidad..." |
| 992 | bind:value={busquedaTimeline} | 1119 | bind:value={busquedaTimeline} |
| 1120 | + oninput={handleSearchInput} | ||
| 993 | class="timeline-search-input" | 1121 | class="timeline-search-input" |
| 994 | /> | 1122 | /> |
| 1123 | + {#if searchLoading} | ||
| 1124 | + <span class="search-spinner"></span> | ||
| 1125 | + {/if} | ||
| 995 | </div> | 1126 | </div> |
| 996 | 1127 | ||
| 997 | <div class="timeline-cards"> | 1128 | <div class="timeline-cards"> |
| 1129 | + {#if busquedaTimeline.trim().length >= 2 && !searchLoading && searchResults.length === 0} | ||
| 1130 | + <p style="text-align:center;color:var(--theme-texto);opacity:0.5;padding:24px 0;">Sin resultados para "{busquedaTimeline}"</p> | ||
| 1131 | + {/if} | ||
| 998 | {#each timelineByEntity as entidad} | 1132 | {#each timelineByEntity as entidad} |
| 999 | <div class="timeline-card"> | 1133 | <div class="timeline-card"> |
| 1000 | <div class="timeline-card-header"> | 1134 | <div class="timeline-card-header"> |
| ... | @@ -1038,7 +1172,7 @@ | ... | @@ -1038,7 +1172,7 @@ |
| 1038 | </section> | 1172 | </section> |
| 1039 | 1173 | ||
| 1040 | <!-- SECCIÓN 2: Clasificadores --> | 1174 | <!-- SECCIÓN 2: Clasificadores --> |
| 1041 | - <section class="vista-seccion" id="clasificadores"> | 1175 | + <section class="vista-seccion" class:seccion-activa={seccionActiva === 1} id="clasificadores"> |
| 1042 | <div class="vista-contenido"> | 1176 | <div class="vista-contenido"> |
| 1043 | <div class="contenido-centrado"> | 1177 | <div class="contenido-centrado"> |
| 1044 | <!-- Título de sección --> | 1178 | <!-- Título de sección --> |
| ... | @@ -1080,18 +1214,6 @@ | ... | @@ -1080,18 +1214,6 @@ |
| 1080 | {label} | 1214 | {label} |
| 1081 | </button> | 1215 | </button> |
| 1082 | {/each} | 1216 | {/each} |
| 1083 | - <label class="toggle-transferencias"> | ||
| 1084 | - <input type="checkbox" bind:checked={incluirTransferencias} /> | ||
| 1085 | - <span class="toggle-label">Incluir transferencias</span> | ||
| 1086 | - <span class="tooltip-trigger"> | ||
| 1087 | - <svg class="tooltip-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
| 1088 | - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> | ||
| 1089 | - </svg> | ||
| 1090 | - <span class="tooltip-content"> | ||
| 1091 | - Las transferencias son recursos que el gobierno central envía a gobiernos territoriales o al sector privado. Al desactivar esta opción se evita la doble contabilización, mostrando el gasto consolidado. | ||
| 1092 | - </span> | ||
| 1093 | - </span> | ||
| 1094 | - </label> | ||
| 1095 | </div> | 1217 | </div> |
| 1096 | </div> | 1218 | </div> |
| 1097 | </div> | 1219 | </div> |
| ... | @@ -1099,7 +1221,7 @@ | ... | @@ -1099,7 +1221,7 @@ |
| 1099 | 1221 | ||
| 1100 | 1222 | ||
| 1101 | <!-- SECCIÓN 2: Entidades --> | 1223 | <!-- SECCIÓN 2: Entidades --> |
| 1102 | - <section class="vista-seccion" id="entidades"> | 1224 | + <section class="vista-seccion" class:seccion-activa={seccionActiva === 2} id="entidades"> |
| 1103 | <div class="vista-contenido"> | 1225 | <div class="vista-contenido"> |
| 1104 | <div class="contenido-centrado"> | 1226 | <div class="contenido-centrado"> |
| 1105 | <!-- Título de sección --> | 1227 | <!-- Título de sección --> |
| ... | @@ -1110,7 +1232,7 @@ | ... | @@ -1110,7 +1232,7 @@ |
| 1110 | <!-- Info de entidad seleccionada --> | 1232 | <!-- Info de entidad seleccionada --> |
| 1111 | {#if displayEntidad} | 1233 | {#if displayEntidad} |
| 1112 | <div class="entidad-info"> | 1234 | <div class="entidad-info"> |
| 1113 | - <span class="entidad-pct">{formatearPorcentaje(displayEntidad.ejecucion)}</span> | 1235 | + <span class="entidad-pct">{formatearPorcentaje(displayEntidad.ejecucion)} <span class="entidad-pct-nota">del presupuesto 2026</span></span> |
| 1114 | <span class="entidad-nombre">{displayEntidad.entidad_desc_entidad}</span> | 1236 | <span class="entidad-nombre">{displayEntidad.entidad_desc_entidad}</span> |
| 1115 | <span class="entidad-area">{displayEntidad.entidad_desc_area}</span> | 1237 | <span class="entidad-area">{displayEntidad.entidad_desc_area}</span> |
| 1116 | </div> | 1238 | </div> |
| ... | @@ -1119,26 +1241,11 @@ | ... | @@ -1119,26 +1241,11 @@ |
| 1119 | <!-- Barras de ejecución --> | 1241 | <!-- Barras de ejecución --> |
| 1120 | <div class="entidades-bar-container" bind:this={entidadesBarContainer}></div> | 1242 | <div class="entidades-bar-container" bind:this={entidadesBarContainer}></div> |
| 1121 | 1243 | ||
| 1122 | - <!-- Paginación --> | 1244 | + <p class="entidades-nota" style="text-align:center;margin-top:0.5rem;font-size:0.75rem;color:var(--theme-texto);opacity:0.6;"> |
| 1123 | - <div class="entidades-paginacion"> | 1245 | + Top 10 entidades por ejecución presupuestaria |
| 1124 | - <p class="entidades-nota"> | 1246 | + </p> |
| 1125 | - Mostrando {entidadesFiltered.length} de {entidadesTodas.length} entidades | ||
| 1126 | - </p> | ||
| 1127 | - <div class="entidades-botones"> | ||
| 1128 | - {#if entidadesVisibles > 10} | ||
| 1129 | - <button class="entidades-ver-mas" onclick={() => { entidadesVisibles = Math.max(10, entidadesVisibles - 10); }}> | ||
| 1130 | - Ver menos | ||
| 1131 | - </button> | ||
| 1132 | - {/if} | ||
| 1133 | - {#if entidadesFiltered.length < entidadesTodas.length} | ||
| 1134 | - <button class="entidades-ver-mas" onclick={() => { entidadesVisibles += 10; }}> | ||
| 1135 | - Ver {Math.min(10, entidadesTodas.length - entidadesFiltered.length)} más | ||
| 1136 | - </button> | ||
| 1137 | - {/if} | ||
| 1138 | - </div> | ||
| 1139 | - </div> | ||
| 1140 | 1247 | ||
| 1141 | - <!-- Controles centrados debajo (igual que sección 1) --> | 1248 | + <!-- Controles centrados debajo --> |
| 1142 | <div class="controles-footer"> | 1249 | <div class="controles-footer"> |
| 1143 | {#each labelsPeriodo as { value, label }} | 1250 | {#each labelsPeriodo as { value, label }} |
| 1144 | <button | 1251 | <button |
| ... | @@ -1149,57 +1256,6 @@ | ... | @@ -1149,57 +1256,6 @@ |
| 1149 | {label} | 1256 | {label} |
| 1150 | </button> | 1257 | </button> |
| 1151 | {/each} | 1258 | {/each} |
| 1152 | - <label class="toggle-transferencias"> | ||
| 1153 | - <input type="checkbox" bind:checked={incluirTransferencias} /> | ||
| 1154 | - <span class="toggle-label">Incluir transferencias</span> | ||
| 1155 | - <span class="tooltip-trigger"> | ||
| 1156 | - <svg class="tooltip-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
| 1157 | - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> | ||
| 1158 | - </svg> | ||
| 1159 | - <span class="tooltip-content"> | ||
| 1160 | - Las transferencias son recursos que el gobierno central envía a gobiernos territoriales o al sector privado. Al desactivar esta opción se evita la doble contabilización, mostrando el gasto consolidado. | ||
| 1161 | - </span> | ||
| 1162 | - </span> | ||
| 1163 | - </label> | ||
| 1164 | - <div class="separador-control"></div> | ||
| 1165 | - <!-- Filtro de rango --> | ||
| 1166 | - <div class="rango-control-inline"> | ||
| 1167 | - <label class="rango-label">Rango: {rangoEjecucionMin}% – {rangoEjecucionMax}%</label> | ||
| 1168 | - <div class="dual-slider dual-slider-inline" bind:this={sliderContainer}> | ||
| 1169 | - <div class="slider-track"></div> | ||
| 1170 | - <div | ||
| 1171 | - class="slider-range" | ||
| 1172 | - style="left: {rangoEjecucionMin}%; right: {100 - rangoEjecucionMax}%;" | ||
| 1173 | - onmousedown={handleRangeMouseDown} | ||
| 1174 | - role="slider" | ||
| 1175 | - tabindex="0" | ||
| 1176 | - ></div> | ||
| 1177 | - <input | ||
| 1178 | - type="range" | ||
| 1179 | - min="0" | ||
| 1180 | - max="100" | ||
| 1181 | - bind:value={rangoEjecucionMin} | ||
| 1182 | - class="slider-thumb slider-thumb-min" | ||
| 1183 | - oninput={() => { | ||
| 1184 | - if (rangoEjecucionMin > rangoEjecucionMax - 5) { | ||
| 1185 | - rangoEjecucionMin = rangoEjecucionMax - 5; | ||
| 1186 | - } | ||
| 1187 | - }} | ||
| 1188 | - /> | ||
| 1189 | - <input | ||
| 1190 | - type="range" | ||
| 1191 | - min="0" | ||
| 1192 | - max="100" | ||
| 1193 | - bind:value={rangoEjecucionMax} | ||
| 1194 | - class="slider-thumb slider-thumb-max" | ||
| 1195 | - oninput={() => { | ||
| 1196 | - if (rangoEjecucionMax < rangoEjecucionMin + 5) { | ||
| 1197 | - rangoEjecucionMax = rangoEjecucionMin + 5; | ||
| 1198 | - } | ||
| 1199 | - }} | ||
| 1200 | - /> | ||
| 1201 | - </div> | ||
| 1202 | - </div> | ||
| 1203 | </div> | 1259 | </div> |
| 1204 | </div> | 1260 | </div> |
| 1205 | </div> | 1261 | </div> |
| ... | @@ -1244,18 +1300,6 @@ | ... | @@ -1244,18 +1300,6 @@ |
| 1244 | {label} | 1300 | {label} |
| 1245 | </button> | 1301 | </button> |
| 1246 | {/each} | 1302 | {/each} |
| 1247 | - <label class="toggle-transferencias"> | ||
| 1248 | - <input type="checkbox" bind:checked={incluirTransferencias} /> | ||
| 1249 | - <span class="toggle-label">Incluir transferencias</span> | ||
| 1250 | - <span class="tooltip-trigger"> | ||
| 1251 | - <svg class="tooltip-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
| 1252 | - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> | ||
| 1253 | - </svg> | ||
| 1254 | - <span class="tooltip-content"> | ||
| 1255 | - Las transferencias son recursos que el gobierno central envía a gobiernos territoriales o al sector privado. Al desactivar esta opción se evita la doble contabilización, mostrando el gasto consolidado. | ||
| 1256 | - </span> | ||
| 1257 | - </span> | ||
| 1258 | - </label> | ||
| 1259 | </div> | 1303 | </div> |
| 1260 | </div> | 1304 | </div> |
| 1261 | </div> | 1305 | </div> |
| ... | @@ -1267,13 +1311,14 @@ | ... | @@ -1267,13 +1311,14 @@ |
| 1267 | /* Variables de color para gráficos - Modo claro */ | 1311 | /* Variables de color para gráficos - Modo claro */ |
| 1268 | :global(:root) { | 1312 | :global(:root) { |
| 1269 | --chart-bar-default: #e0e0e0; | 1313 | --chart-bar-default: #e0e0e0; |
| 1270 | - --chart-bar-highlight: #F3EAD8; | 1314 | + --chart-bar-highlight: #4A8B6E; |
| 1271 | --chart-bar-stroke: #b0b0b0; | 1315 | --chart-bar-stroke: #b0b0b0; |
| 1272 | --chart-bar-stroke-default: white; | 1316 | --chart-bar-stroke-default: white; |
| 1273 | --chart-track-bg: #f5f5f5; | 1317 | --chart-track-bg: #f5f5f5; |
| 1274 | - --chart-grid: #bbb; | 1318 | + --chart-grid: #999; |
| 1275 | --chart-label: #888; | 1319 | --chart-label: #888; |
| 1276 | - --chart-card-header: #f5f0e6; | 1320 | + --chart-card-header: #ffffff; |
| 1321 | + --chart-card-body: #fafafa; | ||
| 1277 | } | 1322 | } |
| 1278 | 1323 | ||
| 1279 | /* Variables de color para gráficos - Modo oscuro */ | 1324 | /* Variables de color para gráficos - Modo oscuro */ |
| ... | @@ -1283,18 +1328,43 @@ | ... | @@ -1283,18 +1328,43 @@ |
| 1283 | --chart-bar-stroke: #5a5a55; | 1328 | --chart-bar-stroke: #5a5a55; |
| 1284 | --chart-bar-stroke-default: #2a2a28; | 1329 | --chart-bar-stroke-default: #2a2a28; |
| 1285 | --chart-track-bg: #2a2a28; | 1330 | --chart-track-bg: #2a2a28; |
| 1286 | - --chart-grid: #4a4a45; | 1331 | + --chart-grid: #6a6a65; |
| 1287 | --chart-label: #8a8a85; | 1332 | --chart-label: #8a8a85; |
| 1288 | - --chart-card-header: #3a3835; | 1333 | + --chart-card-header: #2a2825; |
| 1334 | + --chart-card-body: #222220; | ||
| 1289 | } | 1335 | } |
| 1290 | 1336 | ||
| 1291 | .vista-diaria { | 1337 | .vista-diaria { |
| 1292 | - background: var(--theme-body); | 1338 | + background: #f2f2f4; |
| 1293 | color: var(--theme-titulo); | 1339 | color: var(--theme-titulo); |
| 1294 | - scroll-snap-type: y mandatory; | ||
| 1295 | - overflow-y: scroll; | ||
| 1296 | height: 100vh; | 1340 | height: 100vh; |
| 1297 | height: 100dvh; | 1341 | height: 100dvh; |
| 1342 | + overflow: hidden; | ||
| 1343 | + } | ||
| 1344 | + :global(.dark) .vista-diaria { | ||
| 1345 | + background: #1C1C1A; | ||
| 1346 | + } | ||
| 1347 | + | ||
| 1348 | + /* Header fecha */ | ||
| 1349 | + .vista-fecha-titulo { | ||
| 1350 | + font-family: 'Qanelas', var(--font-sans); | ||
| 1351 | + font-size: 2rem; | ||
| 1352 | + font-weight: 600; | ||
| 1353 | + color: var(--theme-titulo); | ||
| 1354 | + margin: 0; | ||
| 1355 | + } | ||
| 1356 | + .seccion-titulo-sub { | ||
| 1357 | + font-family: 'Qanelas', var(--font-sans); | ||
| 1358 | + font-size: 1.25rem; | ||
| 1359 | + font-weight: 600; | ||
| 1360 | + color: var(--theme-titulo); | ||
| 1361 | + margin: 0.5rem 0 0.25rem; | ||
| 1362 | + } | ||
| 1363 | + .vista-fecha-hora { | ||
| 1364 | + font-size: 0.75rem; | ||
| 1365 | + color: var(--theme-titulo); | ||
| 1366 | + opacity: 0.8; | ||
| 1367 | + margin: 0.2rem 0 0; | ||
| 1298 | } | 1368 | } |
| 1299 | 1369 | ||
| 1300 | /* Loading/Error */ | 1370 | /* Loading/Error */ |
| ... | @@ -1305,75 +1375,152 @@ | ... | @@ -1305,75 +1375,152 @@ |
| 1305 | justify-content: center; | 1375 | justify-content: center; |
| 1306 | color: var(--theme-texto); | 1376 | color: var(--theme-texto); |
| 1307 | } | 1377 | } |
| 1378 | + .loader-spinner { | ||
| 1379 | + width: 32px; | ||
| 1380 | + height: 32px; | ||
| 1381 | + border: 3px solid rgba(128,128,128,0.2); | ||
| 1382 | + border-top-color: var(--theme-titulo); | ||
| 1383 | + border-radius: 50%; | ||
| 1384 | + animation: spin 0.7s linear infinite; | ||
| 1385 | + } | ||
| 1308 | 1386 | ||
| 1309 | .error { | 1387 | .error { |
| 1310 | color: #DB281C; | 1388 | color: #DB281C; |
| 1311 | } | 1389 | } |
| 1312 | 1390 | ||
| 1313 | - /* Navegación interna fija */ | 1391 | + /* Navegación interna fija - barra inferior */ |
| 1314 | - .nav-interna { | 1392 | + /* Botón flotante descargas */ |
| 1393 | + .fab-descargas { | ||
| 1315 | position: fixed; | 1394 | position: fixed; |
| 1316 | - right: 1.5rem; | 1395 | + bottom: 24px; |
| 1396 | + right: 24px; | ||
| 1397 | + width: 48px; | ||
| 1398 | + height: 48px; | ||
| 1399 | + border-radius: 14px; | ||
| 1400 | + background: rgba(255,255,255,0.85); | ||
| 1401 | + backdrop-filter: blur(16px); | ||
| 1402 | + -webkit-backdrop-filter: blur(16px); | ||
| 1403 | + border: 1px solid rgba(0,0,0,0.08); | ||
| 1404 | + box-shadow: 0 4px 16px rgba(0,0,0,0.1); | ||
| 1405 | + display: flex; | ||
| 1406 | + align-items: center; | ||
| 1407 | + justify-content: center; | ||
| 1408 | + color: var(--theme-titulo); | ||
| 1409 | + text-decoration: none; | ||
| 1410 | + z-index: 101; | ||
| 1411 | + transition: all 0.25s ease; | ||
| 1412 | + cursor: pointer; | ||
| 1413 | + } | ||
| 1414 | + .fab-descargas:hover { | ||
| 1415 | + background: rgba(255,255,255,0.95); | ||
| 1416 | + box-shadow: 0 6px 24px rgba(0,0,0,0.15); | ||
| 1417 | + transform: translateY(-2px); | ||
| 1418 | + } | ||
| 1419 | + :global(.dark) .fab-descargas { | ||
| 1420 | + background: rgba(28,28,26,0.85); | ||
| 1421 | + border-color: rgba(255,255,255,0.1); | ||
| 1422 | + box-shadow: 0 4px 16px rgba(0,0,0,0.3); | ||
| 1423 | + } | ||
| 1424 | + :global(.dark) .fab-descargas:hover { | ||
| 1425 | + background: rgba(42,42,40,0.95); | ||
| 1426 | + box-shadow: 0 6px 24px rgba(0,0,0,0.4); | ||
| 1427 | + } | ||
| 1428 | + .fab-tooltip { | ||
| 1429 | + position: absolute; | ||
| 1430 | + right: calc(100% + 10px); | ||
| 1317 | top: 50%; | 1431 | top: 50%; |
| 1318 | transform: translateY(-50%); | 1432 | transform: translateY(-50%); |
| 1433 | + background: rgba(0,0,0,0.85); | ||
| 1434 | + color: #fff; | ||
| 1435 | + font-size: 0.75rem; | ||
| 1436 | + font-weight: 500; | ||
| 1437 | + padding: 6px 12px; | ||
| 1438 | + border-radius: 8px; | ||
| 1439 | + white-space: nowrap; | ||
| 1440 | + opacity: 0; | ||
| 1441 | + pointer-events: none; | ||
| 1442 | + transition: opacity 0.2s; | ||
| 1443 | + } | ||
| 1444 | + .fab-descargas:hover .fab-tooltip { | ||
| 1445 | + opacity: 1; | ||
| 1446 | + } | ||
| 1447 | + :global(.dark) .fab-tooltip { | ||
| 1448 | + background: rgba(255,255,255,0.9); | ||
| 1449 | + color: #1C1C1A; | ||
| 1450 | + } | ||
| 1451 | + | ||
| 1452 | + .nav-bottom-bar { | ||
| 1453 | + position: fixed; | ||
| 1454 | + bottom: 20px; | ||
| 1455 | + left: 50%; | ||
| 1456 | + transform: translateX(-50%); | ||
| 1319 | display: flex; | 1457 | display: flex; |
| 1320 | - flex-direction: column; | 1458 | + align-items: center; |
| 1321 | - gap: 1rem; | 1459 | + gap: 12px; |
| 1460 | + padding: 8px 16px; | ||
| 1461 | + border-radius: 14px; | ||
| 1462 | + background: rgba(255,255,255,0.7); | ||
| 1463 | + backdrop-filter: blur(20px); | ||
| 1464 | + -webkit-backdrop-filter: blur(20px); | ||
| 1465 | + border: 1px solid rgba(0,0,0,0.08); | ||
| 1322 | z-index: 100; | 1466 | z-index: 100; |
| 1467 | + box-shadow: 0 4px 16px rgba(0,0,0,0.08); | ||
| 1468 | + } | ||
| 1469 | + :global(.dark) .nav-bottom-bar { | ||
| 1470 | + background: rgba(28,28,26,0.75); | ||
| 1471 | + border-color: rgba(255,255,255,0.1); | ||
| 1472 | + box-shadow: 0 4px 16px rgba(0,0,0,0.3); | ||
| 1323 | } | 1473 | } |
| 1324 | 1474 | ||
| 1325 | - .nav-item { | 1475 | + .nav-arrow { |
| 1326 | display: flex; | 1476 | display: flex; |
| 1327 | align-items: center; | 1477 | align-items: center; |
| 1328 | - gap: 0.5rem; | 1478 | + justify-content: center; |
| 1329 | - background: none; | 1479 | + width: 32px; |
| 1480 | + height: 32px; | ||
| 1481 | + border-radius: 8px; | ||
| 1330 | border: none; | 1482 | border: none; |
| 1483 | + background: rgba(0,0,0,0.06); | ||
| 1484 | + color: var(--theme-titulo); | ||
| 1331 | cursor: pointer; | 1485 | cursor: pointer; |
| 1332 | - padding: 0.25rem; | ||
| 1333 | - flex-direction: row-reverse; | ||
| 1334 | - } | ||
| 1335 | - | ||
| 1336 | - .nav-dot { | ||
| 1337 | - width: 10px; | ||
| 1338 | - height: 10px; | ||
| 1339 | - border-radius: 50%; | ||
| 1340 | - background: rgba(0, 0, 0, 0.2); | ||
| 1341 | transition: all 0.2s; | 1486 | transition: all 0.2s; |
| 1342 | } | 1487 | } |
| 1343 | - | 1488 | + .nav-arrow:hover:not(:disabled) { |
| 1344 | - :global(.dark) .nav-dot { | 1489 | + background: rgba(0,0,0,0.12); |
| 1345 | - background: rgba(255, 255, 255, 0.2); | ||
| 1346 | } | 1490 | } |
| 1347 | - | 1491 | + .nav-arrow:disabled { |
| 1348 | - .nav-item.active .nav-dot { | 1492 | + opacity: 0.25; |
| 1349 | - background: var(--theme-accent); | 1493 | + cursor: default; |
| 1350 | - transform: scale(1.2); | ||
| 1351 | } | 1494 | } |
| 1352 | - | 1495 | + :global(.dark) .nav-arrow { |
| 1353 | - .nav-label { | 1496 | + background: rgba(255,255,255,0.08); |
| 1354 | - font-size: 0.7rem; | 1497 | + } |
| 1355 | - color: var(--theme-texto); | 1498 | + :global(.dark) .nav-arrow:hover:not(:disabled) { |
| 1356 | - opacity: 0; | 1499 | + background: rgba(255,255,255,0.15); |
| 1357 | - transition: opacity 0.2s; | ||
| 1358 | - white-space: nowrap; | ||
| 1359 | } | 1500 | } |
| 1360 | 1501 | ||
| 1361 | - .nav-item:hover .nav-label, | 1502 | + .nav-section-label { |
| 1362 | - .nav-item.active .nav-label { | 1503 | + font-size: 0.8rem; |
| 1363 | - opacity: 1; | 1504 | + font-weight: 600; |
| 1505 | + color: var(--theme-titulo); | ||
| 1506 | + white-space: nowrap; | ||
| 1507 | + min-width: 120px; | ||
| 1508 | + text-align: center; | ||
| 1364 | } | 1509 | } |
| 1365 | 1510 | ||
| 1366 | - /* Secciones de 100vh - el navbar tiene ~56px de alto */ | 1511 | + /* Secciones - solo visible la activa */ |
| 1367 | .vista-seccion { | 1512 | .vista-seccion { |
| 1368 | - min-height: 100vh; | 1513 | + height: 100vh; |
| 1369 | - min-height: 100dvh; | 1514 | + height: 100dvh; |
| 1370 | padding-top: 56px; | 1515 | padding-top: 56px; |
| 1516 | + padding-bottom: 70px; | ||
| 1371 | box-sizing: border-box; | 1517 | box-sizing: border-box; |
| 1372 | - display: flex; | 1518 | + display: none; |
| 1373 | flex-direction: column; | 1519 | flex-direction: column; |
| 1374 | - border-bottom: 1px solid var(--theme-borde); | 1520 | + overflow-y: auto; |
| 1375 | - scroll-snap-align: start; | 1521 | + } |
| 1376 | - scroll-snap-stop: always; | 1522 | + .vista-seccion.seccion-activa { |
| 1523 | + display: flex; | ||
| 1377 | } | 1524 | } |
| 1378 | 1525 | ||
| 1379 | 1526 | ||
| ... | @@ -1643,9 +1790,9 @@ | ... | @@ -1643,9 +1790,9 @@ |
| 1643 | left: 0.75rem; | 1790 | left: 0.75rem; |
| 1644 | top: 50%; | 1791 | top: 50%; |
| 1645 | transform: translateY(-50%); | 1792 | transform: translateY(-50%); |
| 1646 | - width: 16px; | 1793 | + width: 18px; |
| 1647 | - height: 16px; | 1794 | + height: 18px; |
| 1648 | - color: var(--theme-texto); | 1795 | + color: var(--theme-titulo); |
| 1649 | opacity: 0.5; | 1796 | opacity: 0.5; |
| 1650 | pointer-events: none; | 1797 | pointer-events: none; |
| 1651 | } | 1798 | } |
| ... | @@ -1655,9 +1802,9 @@ | ... | @@ -1655,9 +1802,9 @@ |
| 1655 | padding: 0.6rem 1rem 0.6rem 2.25rem; | 1802 | padding: 0.6rem 1rem 0.6rem 2.25rem; |
| 1656 | font-size: 0.8rem; | 1803 | font-size: 0.8rem; |
| 1657 | font-family: inherit; | 1804 | font-family: inherit; |
| 1658 | - border: 1px solid var(--theme-borde); | 1805 | + border: none; |
| 1659 | - border-radius: 20px; | 1806 | + border-radius: 12px; |
| 1660 | - background: var(--theme-body); | 1807 | + background: #ffffff; |
| 1661 | color: var(--theme-titulo); | 1808 | color: var(--theme-titulo); |
| 1662 | outline: none; | 1809 | outline: none; |
| 1663 | transition: border-color 0.2s; | 1810 | transition: border-color 0.2s; |
| ... | @@ -1673,10 +1820,25 @@ | ... | @@ -1673,10 +1820,25 @@ |
| 1673 | color: var(--theme-accent); | 1820 | color: var(--theme-accent); |
| 1674 | } | 1821 | } |
| 1675 | 1822 | ||
| 1823 | + :global(.dark) .timeline-search-input { | ||
| 1824 | + background: #252523; | ||
| 1825 | + } | ||
| 1676 | .timeline-search-input::placeholder { | 1826 | .timeline-search-input::placeholder { |
| 1677 | - color: var(--theme-texto); | 1827 | + color: var(--theme-titulo); |
| 1678 | opacity: 0.5; | 1828 | opacity: 0.5; |
| 1829 | + font-size: 0.85rem; | ||
| 1830 | + } | ||
| 1831 | + .search-spinner { | ||
| 1832 | + width: 16px; | ||
| 1833 | + height: 16px; | ||
| 1834 | + border: 2px solid rgba(128,128,128,0.3); | ||
| 1835 | + border-top-color: var(--theme-texto); | ||
| 1836 | + border-radius: 50%; | ||
| 1837 | + animation: spin 0.6s linear infinite; | ||
| 1838 | + flex-shrink: 0; | ||
| 1839 | + margin-right: 8px; | ||
| 1679 | } | 1840 | } |
| 1841 | + @keyframes spin { to { transform: rotate(360deg); } } | ||
| 1680 | 1842 | ||
| 1681 | .timeline-cards { | 1843 | .timeline-cards { |
| 1682 | display: flex; | 1844 | display: flex; |
| ... | @@ -1705,6 +1867,10 @@ | ... | @@ -1705,6 +1867,10 @@ |
| 1705 | justify-content: space-between; | 1867 | justify-content: space-between; |
| 1706 | align-items: flex-start; | 1868 | align-items: flex-start; |
| 1707 | gap: 1rem; | 1869 | gap: 1rem; |
| 1870 | + border-bottom: 1px solid rgba(0,0,0,0.06); | ||
| 1871 | + } | ||
| 1872 | + :global(.dark) .timeline-card-header { | ||
| 1873 | + border-bottom-color: rgba(255,255,255,0.08); | ||
| 1708 | } | 1874 | } |
| 1709 | 1875 | ||
| 1710 | .timeline-card-entity { | 1876 | .timeline-card-entity { |
| ... | @@ -1717,8 +1883,8 @@ | ... | @@ -1717,8 +1883,8 @@ |
| 1717 | font-size: 0.65rem; | 1883 | font-size: 0.65rem; |
| 1718 | text-transform: uppercase; | 1884 | text-transform: uppercase; |
| 1719 | letter-spacing: 0.03em; | 1885 | letter-spacing: 0.03em; |
| 1720 | - color: var(--theme-texto); | 1886 | + color: var(--theme-titulo); |
| 1721 | - opacity: 0.6; | 1887 | + opacity: 0.7; |
| 1722 | } | 1888 | } |
| 1723 | 1889 | ||
| 1724 | .timeline-card-nombre { | 1890 | .timeline-card-nombre { |
| ... | @@ -1755,6 +1921,7 @@ | ... | @@ -1755,6 +1921,7 @@ |
| 1755 | .timeline-card-body { | 1921 | .timeline-card-body { |
| 1756 | padding: 0.5rem 1rem; | 1922 | padding: 0.5rem 1rem; |
| 1757 | text-align: center; | 1923 | text-align: center; |
| 1924 | + background: var(--chart-card-body); | ||
| 1758 | } | 1925 | } |
| 1759 | 1926 | ||
| 1760 | .timeline-prog-row { | 1927 | .timeline-prog-row { |
| ... | @@ -1785,8 +1952,8 @@ | ... | @@ -1785,8 +1952,8 @@ |
| 1785 | 1952 | ||
| 1786 | .timeline-prog-programa { | 1953 | .timeline-prog-programa { |
| 1787 | font-size: 0.6rem; | 1954 | font-size: 0.6rem; |
| 1788 | - color: var(--theme-texto); | 1955 | + color: var(--theme-titulo); |
| 1789 | - opacity: 0.5; | 1956 | + opacity: 0.7; |
| 1790 | line-height: 1.2; | 1957 | line-height: 1.2; |
| 1791 | white-space: nowrap; | 1958 | white-space: nowrap; |
| 1792 | overflow: hidden; | 1959 | overflow: hidden; |
| ... | @@ -1848,6 +2015,12 @@ | ... | @@ -1848,6 +2015,12 @@ |
| 1848 | color: var(--theme-titulo); | 2015 | color: var(--theme-titulo); |
| 1849 | line-height: 1; | 2016 | line-height: 1; |
| 1850 | } | 2017 | } |
| 2018 | + .entidad-pct-nota { | ||
| 2019 | + font-size: 0.85rem; | ||
| 2020 | + font-weight: 500; | ||
| 2021 | + color: var(--theme-texto); | ||
| 2022 | + opacity: 0.7; | ||
| 2023 | + } | ||
| 1851 | 2024 | ||
| 1852 | .entidad-nombre { | 2025 | .entidad-nombre { |
| 1853 | font-size: 1rem; | 2026 | font-size: 1rem; |
| ... | @@ -2066,25 +2239,8 @@ | ... | @@ -2066,25 +2239,8 @@ |
| 2066 | } | 2239 | } |
| 2067 | 2240 | ||
| 2068 | /* Responsive */ | 2241 | /* Responsive */ |
| 2069 | - @media (max-width: 900px) { | ||
| 2070 | - .nav-interna { | ||
| 2071 | - right: 0.75rem; | ||
| 2072 | - } | ||
| 2073 | - | ||
| 2074 | - .nav-label { | ||
| 2075 | - display: none; | ||
| 2076 | - } | ||
| 2077 | - } | ||
| 2078 | 2242 | ||
| 2079 | @media (max-width: 768px) { | 2243 | @media (max-width: 768px) { |
| 2080 | - .vista-diaria { | ||
| 2081 | - scroll-snap-type: y proximity; | ||
| 2082 | - } | ||
| 2083 | - | ||
| 2084 | - .vista-seccion { | ||
| 2085 | - scroll-snap-stop: normal; | ||
| 2086 | - } | ||
| 2087 | - | ||
| 2088 | .vista-contenido { | 2244 | .vista-contenido { |
| 2089 | padding: 1rem; | 2245 | padding: 1rem; |
| 2090 | } | 2246 | } | ... | ... |
-
Please register or login to post a comment