Showing
9 changed files
with
669 additions
and
100 deletions
| ... | @@ -114,7 +114,7 @@ | ... | @@ -114,7 +114,7 @@ |
| 114 | } else if (!isClass) { | 114 | } else if (!isClass) { |
| 115 | // Programa/proyecto | 115 | // Programa/proyecto |
| 116 | tipo = 'programa'; | 116 | tipo = 'programa'; |
| 117 | - codigo = `${meta.entidad || ''}-${meta.programa || ''}-${meta.proyecto || ''}`; | 117 | + codigo = String(meta.programa || ''); |
| 118 | } | 118 | } |
| 119 | 119 | ||
| 120 | // Contexto extra para programas | 120 | // Contexto extra para programas |
| ... | @@ -213,7 +213,7 @@ | ... | @@ -213,7 +213,7 @@ |
| 213 | rubro: '/rubro/', | 213 | rubro: '/rubro/', |
| 214 | organismo: '/organismo/', | 214 | organismo: '/organismo/', |
| 215 | ubigeo: '/ubicacion/', | 215 | ubigeo: '/ubicacion/', |
| 216 | - programa: '/proyecto/' | 216 | + programa: '/programa/' |
| 217 | }; | 217 | }; |
| 218 | const base = routes[item.tipo]; | 218 | const base = routes[item.tipo]; |
| 219 | if (base) { | 219 | if (base) { | ... | ... |
| ... | @@ -807,7 +807,7 @@ | ... | @@ -807,7 +807,7 @@ |
| 807 | } else if (isClassResult) { | 807 | } else if (isClassResult) { |
| 808 | url = `/clasificador/${hit.document.class_}/${hit.document.id}`; | 808 | url = `/clasificador/${hit.document.class_}/${hit.document.id}`; |
| 809 | } else { | 809 | } else { |
| 810 | - url = `/proyecto/${generateCodigo(hit)}`; | 810 | + url = `/programa/${generateCodigo(hit)}`; |
| 811 | } | 811 | } |
| 812 | 812 | ||
| 813 | window.location.href = url; | 813 | window.location.href = url; |
| ... | @@ -921,7 +921,7 @@ | ... | @@ -921,7 +921,7 @@ |
| 921 | 921 | ||
| 922 | function generateCodigo(hit) { | 922 | function generateCodigo(hit) { |
| 923 | const m = parseMetadatos(hit.document.metadatos); | 923 | const m = parseMetadatos(hit.document.metadatos); |
| 924 | - return `${m.entidad}-${m.programa}-${m.proyecto}-${m.actividad}-${hit.document.gestion}`; | 924 | + return String(m.programa || ''); |
| 925 | } | 925 | } |
| 926 | 926 | ||
| 927 | $: stats = filteredHits.length > 0 ? getStats( | 927 | $: stats = filteredHits.length > 0 ? getStats( |
| ... | @@ -1485,7 +1485,7 @@ | ... | @@ -1485,7 +1485,7 @@ |
| 1485 | ? `/rubro/${rubroCode}` | 1485 | ? `/rubro/${rubroCode}` |
| 1486 | : isOrganismoClass | 1486 | : isOrganismoClass |
| 1487 | ? `/organismo/${meta.organismo || ''}` | 1487 | ? `/organismo/${meta.organismo || ''}` |
| 1488 | - : (isClassResult ? `/clasificador/${hit.document.class_}/${hit.document.id}` : `/proyecto/${generateCodigo(hit)}`)} | 1488 | + : (isClassResult ? `/clasificador/${hit.document.class_}/${hit.document.id}` : `/programa/${generateCodigo(hit)}`)} |
| 1489 | class="result-card" | 1489 | class="result-card" |
| 1490 | class:result-card-selected={selectedResultIndex === i} | 1490 | class:result-card-selected={selectedResultIndex === i} |
| 1491 | data-result-index={i} | 1491 | data-result-index={i} | ... | ... |
src/routes/api/diario/+server.js
0 → 100644
| 1 | +const API_BASE = 'http://136.112.29.74/api/diario'; | ||
| 2 | + | ||
| 3 | +export async function GET({ url }) { | ||
| 4 | + const tipo = url.searchParams.get('tipo') || 'status'; | ||
| 5 | + | ||
| 6 | + const endpoints = { | ||
| 7 | + status: `${API_BASE}/status`, | ||
| 8 | + timeseries: `${API_BASE}/timeseries`, | ||
| 9 | + entidades: `${API_BASE}/entidades`, | ||
| 10 | + clasificadores: `${API_BASE}/clasificadores`, | ||
| 11 | + timeline: `${API_BASE}/timeline` | ||
| 12 | + }; | ||
| 13 | + | ||
| 14 | + const apiUrl = endpoints[tipo]; | ||
| 15 | + if (!apiUrl) { | ||
| 16 | + return new Response(JSON.stringify({ error: 'Invalid tipo' }), { | ||
| 17 | + status: 400, headers: { 'Content-Type': 'application/json' } | ||
| 18 | + }); | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + try { | ||
| 22 | + const response = await fetch(apiUrl); | ||
| 23 | + if (!response.ok) { | ||
| 24 | + return new Response(JSON.stringify({ error: `API error: ${response.status}` }), { | ||
| 25 | + status: response.status, headers: { 'Content-Type': 'application/json' } | ||
| 26 | + }); | ||
| 27 | + } | ||
| 28 | + const data = await response.json(); | ||
| 29 | + return new Response(JSON.stringify(data), { | ||
| 30 | + headers: { 'Content-Type': 'application/json' } | ||
| 31 | + }); | ||
| 32 | + } catch { | ||
| 33 | + return new Response(JSON.stringify({ error: 'Failed to fetch' }), { | ||
| 34 | + status: 500, headers: { 'Content-Type': 'application/json' } | ||
| 35 | + }); | ||
| 36 | + } | ||
| 37 | +} |
src/routes/api/programa/+server.js
0 → 100644
| 1 | +const API_BASE = 'http://136.112.29.74/api/programa_proyecto'; | ||
| 2 | + | ||
| 3 | +export async function GET({ url }) { | ||
| 4 | + const codigo = url.searchParams.get('codigo') || ''; | ||
| 5 | + const tipo = url.searchParams.get('tipo') || 'detalle'; // detalle, proyectos | ||
| 6 | + | ||
| 7 | + if (!codigo) { | ||
| 8 | + return new Response(JSON.stringify({ error: 'Missing codigo' }), { | ||
| 9 | + status: 400, headers: { 'Content-Type': 'application/json' } | ||
| 10 | + }); | ||
| 11 | + } | ||
| 12 | + | ||
| 13 | + const apiUrl = tipo === 'proyectos' | ||
| 14 | + ? `${API_BASE}/${codigo}/proyectos` | ||
| 15 | + : `${API_BASE}/${codigo}`; | ||
| 16 | + | ||
| 17 | + try { | ||
| 18 | + const response = await fetch(apiUrl); | ||
| 19 | + if (!response.ok) { | ||
| 20 | + return new Response(JSON.stringify({ error: `API error: ${response.status}` }), { | ||
| 21 | + status: response.status, headers: { 'Content-Type': 'application/json' } | ||
| 22 | + }); | ||
| 23 | + } | ||
| 24 | + const data = await response.json(); | ||
| 25 | + return new Response(JSON.stringify(data), { | ||
| 26 | + headers: { 'Content-Type': 'application/json' } | ||
| 27 | + }); | ||
| 28 | + } catch { | ||
| 29 | + return new Response(JSON.stringify({ error: 'Failed to fetch' }), { | ||
| 30 | + status: 500, headers: { 'Content-Type': 'application/json' } | ||
| 31 | + }); | ||
| 32 | + } | ||
| 33 | +} |
| ... | @@ -11,8 +11,11 @@ | ... | @@ -11,8 +11,11 @@ |
| 11 | let searchableItems = $state([]); | 11 | let searchableItems = $state([]); |
| 12 | let isMac = $state(false); | 12 | let isMac = $state(false); |
| 13 | 13 | ||
| 14 | - // Markdown content embedded directly | 14 | + // Markdown content loaded from static file |
| 15 | - const MARKDOWN_CONTENT = `# Introducción | 15 | + let MARKDOWN_CONTENT = $state(''); |
| 16 | + | ||
| 17 | + // Placeholder for initial content | ||
| 18 | + const INITIAL_CONTENT = `# Introducción | ||
| 16 | 19 | ||
| 17 | El presupuesto público se presenta en dos distribuciones de datos: una histórica y otra en tiempo real. | 20 | El presupuesto público se presenta en dos distribuciones de datos: una histórica y otra en tiempo real. |
| 18 | 21 | ||
| ... | @@ -1043,6 +1046,12 @@ Ejemplo: \`5637823.65\`. | ... | @@ -1043,6 +1046,12 @@ Ejemplo: \`5637823.65\`. |
| 1043 | isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform); | 1046 | isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform); |
| 1044 | 1047 | ||
| 1045 | try { | 1048 | try { |
| 1049 | + // Cargar markdown desde archivo estático | ||
| 1050 | + const res = await fetch('/docs.md'); | ||
| 1051 | + if (res.ok) { | ||
| 1052 | + MARKDOWN_CONTENT = await res.text(); | ||
| 1053 | + } | ||
| 1054 | + | ||
| 1046 | const result = processMarkdown(MARKDOWN_CONTENT); | 1055 | const result = processMarkdown(MARKDOWN_CONTENT); |
| 1047 | content = result.html; | 1056 | content = result.html; |
| 1048 | headings = buildHierarchy(result.headings); | 1057 | headings = buildHierarchy(result.headings); |
| ... | @@ -1327,12 +1336,6 @@ Ejemplo: \`5637823.65\`. | ... | @@ -1327,12 +1336,6 @@ Ejemplo: \`5637823.65\`. |
| 1327 | <header class="docs-header"> | 1336 | <header class="docs-header"> |
| 1328 | <div class="docs-header-content"> | 1337 | <div class="docs-header-content"> |
| 1329 | <div class="docs-header-top"> | 1338 | <div class="docs-header-top"> |
| 1330 | - <a href="/" class="docs-back"> | ||
| 1331 | - <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 1332 | - <path d="M19 12H5M12 19l-7-7 7-7"/> | ||
| 1333 | - </svg> | ||
| 1334 | - <span>Inicio</span> | ||
| 1335 | - </a> | ||
| 1336 | 1339 | ||
| 1337 | <!-- Mobile menu button --> | 1340 | <!-- Mobile menu button --> |
| 1338 | <button class="docs-menu-btn" onclick={toggleSidebar}> | 1341 | <button class="docs-menu-btn" onclick={toggleSidebar}> |
| ... | @@ -1501,7 +1504,7 @@ Ejemplo: \`5637823.65\`. | ... | @@ -1501,7 +1504,7 @@ Ejemplo: \`5637823.65\`. |
| 1501 | .docs-header-content { | 1504 | .docs-header-content { |
| 1502 | max-width: 1000px; | 1505 | max-width: 1000px; |
| 1503 | margin: 0 auto; | 1506 | margin: 0 auto; |
| 1504 | - padding: 1rem 1.5rem; | 1507 | + padding: 80px 1.5rem 1rem; |
| 1505 | } | 1508 | } |
| 1506 | 1509 | ||
| 1507 | .docs-header-top { | 1510 | .docs-header-top { |
| ... | @@ -1568,7 +1571,7 @@ Ejemplo: \`5637823.65\`. | ... | @@ -1568,7 +1571,7 @@ Ejemplo: \`5637823.65\`. |
| 1568 | .docs-layout-wrapper { | 1571 | .docs-layout-wrapper { |
| 1569 | max-width: 700px; | 1572 | max-width: 700px; |
| 1570 | margin: 0 auto; | 1573 | margin: 0 auto; |
| 1571 | - padding-top: 140px; | 1574 | + padding-top: 340px; |
| 1572 | padding-left: 1.5rem; | 1575 | padding-left: 1.5rem; |
| 1573 | padding-right: 1.5rem; | 1576 | padding-right: 1.5rem; |
| 1574 | } | 1577 | } |
| ... | @@ -1603,7 +1606,7 @@ Ejemplo: \`5637823.65\`. | ... | @@ -1603,7 +1606,7 @@ Ejemplo: \`5637823.65\`. |
| 1603 | 1606 | ||
| 1604 | .docs-sidebar-content { | 1607 | .docs-sidebar-content { |
| 1605 | padding-right: 1rem; | 1608 | padding-right: 1rem; |
| 1606 | - padding-top: 2rem; | 1609 | + padding-top: 10rem; |
| 1607 | } | 1610 | } |
| 1608 | } | 1611 | } |
| 1609 | 1612 | ||
| ... | @@ -1824,7 +1827,7 @@ Ejemplo: \`5637823.65\`. | ... | @@ -1824,7 +1827,7 @@ Ejemplo: \`5637823.65\`. |
| 1824 | 1827 | ||
| 1825 | @media (max-width: 1099px) { | 1828 | @media (max-width: 1099px) { |
| 1826 | .docs-layout-wrapper { | 1829 | .docs-layout-wrapper { |
| 1827 | - padding-top: 130px; | 1830 | + padding-top: 330px; |
| 1828 | padding-left: 1rem; | 1831 | padding-left: 1rem; |
| 1829 | padding-right: 1rem; | 1832 | padding-right: 1rem; |
| 1830 | } | 1833 | } |
| ... | @@ -2049,6 +2052,7 @@ Ejemplo: \`5637823.65\`. | ... | @@ -2049,6 +2052,7 @@ Ejemplo: \`5637823.65\`. |
| 2049 | .docs-search-container { | 2052 | .docs-search-container { |
| 2050 | position: relative; | 2053 | position: relative; |
| 2051 | margin-top: 1rem; | 2054 | margin-top: 1rem; |
| 2055 | + max-width: 320px; | ||
| 2052 | } | 2056 | } |
| 2053 | 2057 | ||
| 2054 | .docs-search-input-wrapper { | 2058 | .docs-search-input-wrapper { | ... | ... |
src/routes/programa/[codigo]/+page.svelte
0 → 100644
| 1 | +<script> | ||
| 2 | + import { page } from '$app/stores'; | ||
| 3 | + import { goto } from '$app/navigation'; | ||
| 4 | + import { onMount } from 'svelte'; | ||
| 5 | + import { tweened } from 'svelte/motion'; | ||
| 6 | + import { cubicOut } from 'svelte/easing'; | ||
| 7 | + import BarChart from '$lib/components/objeto/BarChart.svelte'; | ||
| 8 | + | ||
| 9 | + let codigo = $derived($page.params.codigo); | ||
| 10 | + | ||
| 11 | + let mounted = $state(false); | ||
| 12 | + let loading = $state(true); | ||
| 13 | + let linkCopied = $state(false); | ||
| 14 | + let programaData = $state([]); | ||
| 15 | + let proyectos = $state([]); | ||
| 16 | + let hoveredYear = $state(null); | ||
| 17 | + let selectedTab = $state('objetos'); // objetos, finfun, acteco, entidades | ||
| 18 | + | ||
| 19 | + // Serie temporal | ||
| 20 | + let datosAnuales = $derived(programaData.filter(d => d.gestion >= 2016).sort((a, b) => a.gestion - b.gestion)); | ||
| 21 | + let nombre = $derived(datosAnuales[0]?.desc || `Programa ${codigo}`); | ||
| 22 | + let nombrePadre = $derived(datosAnuales[0]?.desc_padre || ''); | ||
| 23 | + | ||
| 24 | + // Datos para el gráfico | ||
| 25 | + let gastoPerCapita = $derived( | ||
| 26 | + datosAnuales.map(d => ({ | ||
| 27 | + año: d.gestion, | ||
| 28 | + monto: d.devengado, | ||
| 29 | + perCapita: d.devengado | ||
| 30 | + })) | ||
| 31 | + ); | ||
| 32 | + | ||
| 33 | + let primerAño = $derived(datosAnuales.length > 0 ? datosAnuales[0].gestion : 2016); | ||
| 34 | + let ultimoAño = $derived(datosAnuales.length > 0 ? datosAnuales[datosAnuales.length - 1].gestion : 2025); | ||
| 35 | + | ||
| 36 | + // Año activo (hover o último) | ||
| 37 | + let currentAnual = $derived(hoveredYear ? datosAnuales.find(d => d.gestion === hoveredYear) : datosAnuales[datosAnuales.length - 1]); | ||
| 38 | + let displayMonto = $derived(currentAnual?.devengado ?? 0); | ||
| 39 | + let displayEntidades = $derived(currentAnual?.n_entidad ?? 0); | ||
| 40 | + let displayObjetos = $derived(currentAnual?.n_objeto ?? 0); | ||
| 41 | + let displayPeriodo = $derived(hoveredYear ? hoveredYear : `${primerAño}-${ultimoAño}`); | ||
| 42 | + | ||
| 43 | + // Distribución según tab seleccionada | ||
| 44 | + let distribucion = $derived.by(() => { | ||
| 45 | + if (!currentAnual) return []; | ||
| 46 | + const items = currentAnual[selectedTab === 'entidades' ? 'entidad' : selectedTab === 'finfun' ? 'finfun' : selectedTab === 'acteco' ? 'acteco' : 'objeto'] || []; | ||
| 47 | + return [...items].filter(d => d.devengado > 0).sort((a, b) => b.devengado - a.devengado); | ||
| 48 | + }); | ||
| 49 | + | ||
| 50 | + let totalDistribucion = $derived(distribucion.reduce((s, d) => s + d.devengado, 0)); | ||
| 51 | + | ||
| 52 | + // Tweens | ||
| 53 | + const tw = { duration: 300, easing: cubicOut }; | ||
| 54 | + const twMonto = tweened(0, tw); | ||
| 55 | + $effect(() => { twMonto.set(typeof displayMonto === 'number' ? displayMonto : 0); }); | ||
| 56 | + | ||
| 57 | + function formatMonto(v) { | ||
| 58 | + if (v >= 1e9) return `${(v / 1e9).toFixed(1)} mil mill.`; | ||
| 59 | + if (v >= 1e6) return `${(v / 1e6).toFixed(1)} mill.`; | ||
| 60 | + if (v >= 1e3) return `${(v / 1e3).toFixed(0)} mil`; | ||
| 61 | + return v?.toLocaleString('es-BO') || '0'; | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + async function loadData() { | ||
| 65 | + loading = true; | ||
| 66 | + try { | ||
| 67 | + const [detRes, proRes] = await Promise.all([ | ||
| 68 | + fetch(`/api/programa?codigo=${codigo}&tipo=detalle`), | ||
| 69 | + fetch(`/api/programa?codigo=${codigo}&tipo=proyectos`) | ||
| 70 | + ]); | ||
| 71 | + const det = await detRes.json(); | ||
| 72 | + const pro = await proRes.json(); | ||
| 73 | + if (Array.isArray(det)) programaData = det; | ||
| 74 | + if (Array.isArray(pro)) proyectos = pro.sort((a, b) => b.devengado - a.devengado); | ||
| 75 | + } catch (err) { | ||
| 76 | + console.error('[Programa] Error:', err); | ||
| 77 | + } | ||
| 78 | + loading = false; | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + onMount(async () => { | ||
| 82 | + mounted = true; | ||
| 83 | + await loadData(); | ||
| 84 | + }); | ||
| 85 | + | ||
| 86 | + const TABS = [ | ||
| 87 | + { id: 'objetos', label: 'En qué se gasta' }, | ||
| 88 | + { id: 'finfun', label: 'Para qué función' }, | ||
| 89 | + { id: 'acteco', label: 'Sector económico' }, | ||
| 90 | + { id: 'entidades', label: 'Quién ejecuta' } | ||
| 91 | + ]; | ||
| 92 | +</script> | ||
| 93 | + | ||
| 94 | +<svelte:head> | ||
| 95 | + <title>{nombre} | Programa</title> | ||
| 96 | + <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| 97 | + <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" /> | ||
| 98 | +</svelte:head> | ||
| 99 | + | ||
| 100 | +<div class="page" class:mounted> | ||
| 101 | + <div class="layout"> | ||
| 102 | + <!-- Header --> | ||
| 103 | + <header class="header"> | ||
| 104 | + <div class="header-top"> | ||
| 105 | + <div class="pills"> | ||
| 106 | + <span class="pill pill-tipo">Programa</span> | ||
| 107 | + <span class="pill pill-codigo">{codigo}</span> | ||
| 108 | + </div> | ||
| 109 | + <div class="header-actions"> | ||
| 110 | + <button class="share-btn" onclick={() => { | ||
| 111 | + navigator.clipboard.writeText(window.location.href); | ||
| 112 | + linkCopied = true; | ||
| 113 | + setTimeout(() => linkCopied = false, 2000); | ||
| 114 | + }}> | ||
| 115 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 116 | + {#if linkCopied} | ||
| 117 | + <path d="M20 6L9 17l-5-5"/> | ||
| 118 | + {:else} | ||
| 119 | + <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/> | ||
| 120 | + <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/> | ||
| 121 | + {/if} | ||
| 122 | + </svg> | ||
| 123 | + {linkCopied ? 'Copiado' : 'Compartir'} | ||
| 124 | + </button> | ||
| 125 | + <a href="/" class="back-link"> | ||
| 126 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 127 | + <path d="M19 12H5M12 19l-7-7 7-7"/> | ||
| 128 | + </svg> | ||
| 129 | + Volver | ||
| 130 | + </a> | ||
| 131 | + </div> | ||
| 132 | + </div> | ||
| 133 | + | ||
| 134 | + <h1 class="title">{nombre}</h1> | ||
| 135 | + {#if nombrePadre} | ||
| 136 | + <p class="subtitle">{nombrePadre}</p> | ||
| 137 | + {/if} | ||
| 138 | + </header> | ||
| 139 | + | ||
| 140 | + {#if loading} | ||
| 141 | + <div class="loading">Cargando...</div> | ||
| 142 | + {:else} | ||
| 143 | + <!-- Main content --> | ||
| 144 | + <div class="content"> | ||
| 145 | + <!-- Gráfico + KPIs --> | ||
| 146 | + <div class="chart-card"> | ||
| 147 | + <div class="chart-header"> | ||
| 148 | + <span class="chart-label">Bs ejecutados <span class="chart-period">· {displayPeriodo}</span></span> | ||
| 149 | + </div> | ||
| 150 | + <div class="chart-wrapper"> | ||
| 151 | + <BarChart data={gastoPerCapita} bind:hoveredYear height={200} fill={true} /> | ||
| 152 | + </div> | ||
| 153 | + <div class="kpis"> | ||
| 154 | + <div class="kpi"> | ||
| 155 | + <span class="kpi-value">{formatMonto(Math.round($twMonto))}</span> | ||
| 156 | + <span class="kpi-label">{hoveredYear ? `ejecutados en ${hoveredYear}` : `ejecutados ${primerAño}-${ultimoAño}`}</span> | ||
| 157 | + </div> | ||
| 158 | + <div class="kpi"> | ||
| 159 | + <span class="kpi-value">{displayEntidades}</span> | ||
| 160 | + <span class="kpi-label">entidades</span> | ||
| 161 | + </div> | ||
| 162 | + <div class="kpi"> | ||
| 163 | + <span class="kpi-value">{displayObjetos}</span> | ||
| 164 | + <span class="kpi-label">partidas de gasto</span> | ||
| 165 | + </div> | ||
| 166 | + </div> | ||
| 167 | + </div> | ||
| 168 | + | ||
| 169 | + <!-- Distribución --> | ||
| 170 | + <div class="dist-card"> | ||
| 171 | + <div class="tabs"> | ||
| 172 | + {#each TABS as tab} | ||
| 173 | + <button class="tab" class:active={selectedTab === tab.id} onclick={() => selectedTab = tab.id}> | ||
| 174 | + {tab.label} | ||
| 175 | + </button> | ||
| 176 | + {/each} | ||
| 177 | + </div> | ||
| 178 | + | ||
| 179 | + <div class="dist-list"> | ||
| 180 | + {#each distribucion as item, i} | ||
| 181 | + {@const pct = totalDistribucion > 0 ? (item.devengado / totalDistribucion * 100) : 0} | ||
| 182 | + <div class="dist-item"> | ||
| 183 | + <div class="dist-bar" style="width: {pct}%"></div> | ||
| 184 | + <span class="dist-rank">#{i + 1}</span> | ||
| 185 | + <span class="dist-name">{item.desc || `Código ${item.codigo}`}</span> | ||
| 186 | + <span class="dist-pct">{pct.toFixed(1)}%</span> | ||
| 187 | + </div> | ||
| 188 | + {/each} | ||
| 189 | + {#if distribucion.length === 0} | ||
| 190 | + <p class="dist-empty">Sin datos para este clasificador</p> | ||
| 191 | + {/if} | ||
| 192 | + </div> | ||
| 193 | + </div> | ||
| 194 | + | ||
| 195 | + <!-- Proyectos/Actividades --> | ||
| 196 | + {#if proyectos.length > 0} | ||
| 197 | + <div class="proyectos-card"> | ||
| 198 | + <h3 class="card-title">Actividades y proyectos ({proyectos.length})</h3> | ||
| 199 | + <div class="proyectos-list"> | ||
| 200 | + {#each proyectos as proy, i} | ||
| 201 | + {@const pct = proyectos[0]?.devengado > 0 ? (proy.devengado / proyectos[0].devengado * 100) : 0} | ||
| 202 | + <div class="proy-item"> | ||
| 203 | + <span class="proy-rank">#{i + 1}</span> | ||
| 204 | + <div class="proy-info"> | ||
| 205 | + <span class="proy-name">{proy.desc}</span> | ||
| 206 | + <span class="proy-code">{proy.codigo}</span> | ||
| 207 | + </div> | ||
| 208 | + <span class="proy-monto">{formatMonto(proy.devengado)} Bs</span> | ||
| 209 | + </div> | ||
| 210 | + {/each} | ||
| 211 | + </div> | ||
| 212 | + </div> | ||
| 213 | + {/if} | ||
| 214 | + </div> | ||
| 215 | + {/if} | ||
| 216 | + </div> | ||
| 217 | +</div> | ||
| 218 | + | ||
| 219 | +<style> | ||
| 220 | + .page { | ||
| 221 | + min-height: 100vh; | ||
| 222 | + width: 100%; | ||
| 223 | + overflow-x: hidden; | ||
| 224 | + background: var(--theme-body); | ||
| 225 | + color: var(--theme-titulo); | ||
| 226 | + font-family: var(--font-sans), -apple-system, sans-serif; | ||
| 227 | + opacity: 0; | ||
| 228 | + transition: opacity 0.4s; | ||
| 229 | + } | ||
| 230 | + .page.mounted { opacity: 1; } | ||
| 231 | + :global(html:not(.dark)) .page { background: #f5f5f7; } | ||
| 232 | + | ||
| 233 | + .layout { | ||
| 234 | + max-width: 1200px; | ||
| 235 | + margin: 0 auto; | ||
| 236 | + padding: 80px 2rem 2rem; | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + /* Header */ | ||
| 240 | + .header { margin-bottom: 1.5rem; } | ||
| 241 | + | ||
| 242 | + .header-top { | ||
| 243 | + display: flex; | ||
| 244 | + justify-content: space-between; | ||
| 245 | + align-items: center; | ||
| 246 | + margin-bottom: 0.375rem; | ||
| 247 | + } | ||
| 248 | + | ||
| 249 | + .pills { display: flex; gap: 0.375rem; } | ||
| 250 | + .pill { | ||
| 251 | + font-family: 'DM Mono', monospace; | ||
| 252 | + font-size: 0.625rem; | ||
| 253 | + padding: 0.125rem 0.5rem; | ||
| 254 | + border-radius: 4px; | ||
| 255 | + letter-spacing: 0.03em; | ||
| 256 | + } | ||
| 257 | + .pill-tipo { background: rgba(201, 167, 81, 0.15); color: var(--theme-accent); } | ||
| 258 | + .pill-codigo { background: var(--theme-borde); color: var(--theme-texto); } | ||
| 259 | + | ||
| 260 | + .header-actions { display: flex; align-items: center; gap: 0.75rem; } | ||
| 261 | + | ||
| 262 | + .share-btn, .back-link { | ||
| 263 | + display: flex; | ||
| 264 | + align-items: center; | ||
| 265 | + gap: 0.25rem; | ||
| 266 | + font-size: 0.6875rem; | ||
| 267 | + color: var(--theme-texto); | ||
| 268 | + opacity: 0.6; | ||
| 269 | + background: none; | ||
| 270 | + border: none; | ||
| 271 | + padding: 0; | ||
| 272 | + cursor: pointer; | ||
| 273 | + text-decoration: none; | ||
| 274 | + transition: opacity 0.15s; | ||
| 275 | + } | ||
| 276 | + .share-btn:hover, .back-link:hover { opacity: 1; } | ||
| 277 | + .back-link { color: var(--theme-accent); opacity: 1; } | ||
| 278 | + | ||
| 279 | + .title { | ||
| 280 | + font-family: 'DM Serif Display', serif; | ||
| 281 | + font-size: 2rem; | ||
| 282 | + font-weight: 400; | ||
| 283 | + margin: 0; | ||
| 284 | + line-height: 1.15; | ||
| 285 | + } | ||
| 286 | + | ||
| 287 | + .subtitle { | ||
| 288 | + font-size: 0.8125rem; | ||
| 289 | + color: var(--theme-texto); | ||
| 290 | + opacity: 0.6; | ||
| 291 | + margin: 0.25rem 0 0; | ||
| 292 | + } | ||
| 293 | + | ||
| 294 | + .loading { | ||
| 295 | + display: flex; | ||
| 296 | + align-items: center; | ||
| 297 | + justify-content: center; | ||
| 298 | + height: 50vh; | ||
| 299 | + color: var(--theme-texto); | ||
| 300 | + opacity: 0.5; | ||
| 301 | + } | ||
| 302 | + | ||
| 303 | + /* Content */ | ||
| 304 | + .content { | ||
| 305 | + display: flex; | ||
| 306 | + flex-direction: column; | ||
| 307 | + gap: 1rem; | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + /* Chart card */ | ||
| 311 | + .chart-card { | ||
| 312 | + background: var(--theme-surface); | ||
| 313 | + border-radius: 12px; | ||
| 314 | + padding: 1rem; | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + .chart-header { margin-bottom: 0.5rem; } | ||
| 318 | + .chart-label { | ||
| 319 | + font-family: 'DM Mono', monospace; | ||
| 320 | + font-size: 0.6875rem; | ||
| 321 | + color: var(--theme-texto); | ||
| 322 | + opacity: 0.7; | ||
| 323 | + } | ||
| 324 | + .chart-period { opacity: 0.5; } | ||
| 325 | + | ||
| 326 | + .chart-wrapper { height: 200px; } | ||
| 327 | + | ||
| 328 | + .kpis { | ||
| 329 | + display: grid; | ||
| 330 | + grid-template-columns: repeat(3, 1fr); | ||
| 331 | + gap: 0.5rem; | ||
| 332 | + margin-top: 0.75rem; | ||
| 333 | + padding-top: 0.75rem; | ||
| 334 | + border-top: 1px solid var(--theme-borde); | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + .kpi { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.125rem; } | ||
| 338 | + .kpi-value { font-family: 'Qanelas', var(--font-sans); font-size: 1rem; font-weight: 600; color: var(--theme-titulo); } | ||
| 339 | + .kpi-label { font-size: 0.625rem; color: var(--theme-texto); opacity: 0.5; } | ||
| 340 | + | ||
| 341 | + /* Distribution */ | ||
| 342 | + .dist-card { | ||
| 343 | + background: var(--theme-surface); | ||
| 344 | + border-radius: 12px; | ||
| 345 | + padding: 1rem; | ||
| 346 | + } | ||
| 347 | + | ||
| 348 | + .tabs { | ||
| 349 | + display: flex; | ||
| 350 | + gap: 0.25rem; | ||
| 351 | + margin-bottom: 0.75rem; | ||
| 352 | + overflow-x: auto; | ||
| 353 | + scrollbar-width: none; | ||
| 354 | + } | ||
| 355 | + .tabs::-webkit-scrollbar { display: none; } | ||
| 356 | + | ||
| 357 | + .tab { | ||
| 358 | + font-size: 0.6875rem; | ||
| 359 | + font-weight: 500; | ||
| 360 | + padding: 0.375rem 0.75rem; | ||
| 361 | + border: 1px solid var(--theme-borde); | ||
| 362 | + border-radius: 6px; | ||
| 363 | + background: transparent; | ||
| 364 | + color: var(--theme-texto); | ||
| 365 | + cursor: pointer; | ||
| 366 | + white-space: nowrap; | ||
| 367 | + transition: all 0.15s; | ||
| 368 | + } | ||
| 369 | + .tab:hover { border-color: var(--theme-accent); } | ||
| 370 | + .tab.active { | ||
| 371 | + background: var(--theme-accent); | ||
| 372 | + border-color: var(--theme-accent); | ||
| 373 | + color: white; | ||
| 374 | + font-weight: 600; | ||
| 375 | + } | ||
| 376 | + | ||
| 377 | + .dist-list { display: flex; flex-direction: column; gap: 0.25rem; } | ||
| 378 | + | ||
| 379 | + .dist-item { | ||
| 380 | + position: relative; | ||
| 381 | + display: flex; | ||
| 382 | + align-items: center; | ||
| 383 | + gap: 0.5rem; | ||
| 384 | + padding: 0.375rem 0.5rem; | ||
| 385 | + border-radius: 6px; | ||
| 386 | + overflow: hidden; | ||
| 387 | + } | ||
| 388 | + | ||
| 389 | + .dist-bar { | ||
| 390 | + position: absolute; | ||
| 391 | + left: 0; | ||
| 392 | + top: 0; | ||
| 393 | + bottom: 0; | ||
| 394 | + background: rgba(201, 167, 81, 0.1); | ||
| 395 | + border-radius: 6px; | ||
| 396 | + } | ||
| 397 | + | ||
| 398 | + .dist-rank { | ||
| 399 | + position: relative; | ||
| 400 | + font-family: 'DM Mono', monospace; | ||
| 401 | + font-size: 0.625rem; | ||
| 402 | + color: var(--theme-accent); | ||
| 403 | + font-weight: 600; | ||
| 404 | + min-width: 1.5rem; | ||
| 405 | + } | ||
| 406 | + | ||
| 407 | + .dist-name { | ||
| 408 | + position: relative; | ||
| 409 | + flex: 1; | ||
| 410 | + font-size: 0.75rem; | ||
| 411 | + color: var(--theme-titulo); | ||
| 412 | + overflow: hidden; | ||
| 413 | + text-overflow: ellipsis; | ||
| 414 | + white-space: nowrap; | ||
| 415 | + } | ||
| 416 | + | ||
| 417 | + .dist-pct { | ||
| 418 | + position: relative; | ||
| 419 | + font-family: 'DM Mono', monospace; | ||
| 420 | + font-size: 0.6875rem; | ||
| 421 | + color: var(--theme-texto); | ||
| 422 | + opacity: 0.7; | ||
| 423 | + } | ||
| 424 | + | ||
| 425 | + .dist-empty { | ||
| 426 | + font-size: 0.8125rem; | ||
| 427 | + color: var(--theme-texto); | ||
| 428 | + opacity: 0.5; | ||
| 429 | + text-align: center; | ||
| 430 | + padding: 1rem; | ||
| 431 | + } | ||
| 432 | + | ||
| 433 | + /* Proyectos */ | ||
| 434 | + .proyectos-card { | ||
| 435 | + background: var(--theme-surface); | ||
| 436 | + border-radius: 12px; | ||
| 437 | + padding: 1rem; | ||
| 438 | + } | ||
| 439 | + | ||
| 440 | + .card-title { | ||
| 441 | + font-size: 0.875rem; | ||
| 442 | + font-weight: 700; | ||
| 443 | + margin: 0 0 0.75rem; | ||
| 444 | + } | ||
| 445 | + | ||
| 446 | + .proyectos-list { display: flex; flex-direction: column; gap: 0.375rem; } | ||
| 447 | + | ||
| 448 | + .proy-item { | ||
| 449 | + display: flex; | ||
| 450 | + align-items: center; | ||
| 451 | + gap: 0.5rem; | ||
| 452 | + padding: 0.375rem 0; | ||
| 453 | + border-bottom: 1px dotted rgba(128, 128, 128, 0.2); | ||
| 454 | + } | ||
| 455 | + .proy-item:last-child { border-bottom: none; } | ||
| 456 | + | ||
| 457 | + .proy-rank { | ||
| 458 | + font-family: 'DM Mono', monospace; | ||
| 459 | + font-size: 0.625rem; | ||
| 460 | + color: var(--theme-accent); | ||
| 461 | + font-weight: 600; | ||
| 462 | + min-width: 1.5rem; | ||
| 463 | + } | ||
| 464 | + | ||
| 465 | + .proy-info { flex: 1; min-width: 0; } | ||
| 466 | + .proy-name { font-size: 0.75rem; color: var(--theme-titulo); display: block; } | ||
| 467 | + .proy-code { | ||
| 468 | + font-family: 'DM Mono', monospace; | ||
| 469 | + font-size: 0.5625rem; | ||
| 470 | + color: var(--theme-texto); | ||
| 471 | + opacity: 0.5; | ||
| 472 | + } | ||
| 473 | + | ||
| 474 | + .proy-monto { | ||
| 475 | + font-family: 'DM Mono', monospace; | ||
| 476 | + font-size: 0.6875rem; | ||
| 477 | + color: var(--theme-titulo); | ||
| 478 | + white-space: nowrap; | ||
| 479 | + } | ||
| 480 | + | ||
| 481 | + /* Responsive */ | ||
| 482 | + @media (max-width: 640px) { | ||
| 483 | + .layout { padding: 70px 1rem 1rem; } | ||
| 484 | + .title { font-size: 1.5rem; } | ||
| 485 | + .kpis { grid-template-columns: repeat(3, 1fr); } | ||
| 486 | + .header-top { flex-direction: column; align-items: flex-start; gap: 0.5rem; } | ||
| 487 | + } | ||
| 488 | +</style> |
| ... | @@ -35,18 +35,24 @@ | ... | @@ -35,18 +35,24 @@ |
| 35 | } | 35 | } |
| 36 | 36 | ||
| 37 | async function cargarMapa(codigo) { | 37 | async function cargarMapa(codigo) { |
| 38 | + console.time('[MAPA] total'); | ||
| 38 | const cached = get(mapaCache); | 39 | const cached = get(mapaCache); |
| 39 | if (cached) { | 40 | if (cached) { |
| 41 | + console.log('[MAPA] usando cache'); | ||
| 40 | mapaData = cached; | 42 | mapaData = cached; |
| 41 | } else if (!mapaData) { | 43 | } else if (!mapaData) { |
| 44 | + console.time('[MAPA] fetch'); | ||
| 42 | const res = await fetch('/mapa.json'); | 45 | const res = await fetch('/mapa.json'); |
| 43 | mapaData = await res.json(); | 46 | mapaData = await res.json(); |
| 47 | + console.timeEnd('[MAPA] fetch'); | ||
| 44 | delete mapaData.bolivia.crs; | 48 | delete mapaData.bolivia.crs; |
| 45 | delete mapaData.departamentos.crs; | 49 | delete mapaData.departamentos.crs; |
| 46 | delete mapaData.municipios.crs; | 50 | delete mapaData.municipios.crs; |
| 51 | + console.time('[MAPA] fixWinding'); | ||
| 47 | fixWinding(mapaData.bolivia); | 52 | fixWinding(mapaData.bolivia); |
| 48 | fixWinding(mapaData.departamentos); | 53 | fixWinding(mapaData.departamentos); |
| 49 | fixWinding(mapaData.municipios); | 54 | fixWinding(mapaData.municipios); |
| 55 | + console.timeEnd('[MAPA] fixWinding'); | ||
| 50 | mapaCache.set(mapaData); | 56 | mapaCache.set(mapaData); |
| 51 | } | 57 | } |
| 52 | 58 | ||
| ... | @@ -73,11 +79,14 @@ | ... | @@ -73,11 +79,14 @@ |
| 73 | mapaMunicipioPath = pathGen(municipio) || ''; | 79 | mapaMunicipioPath = pathGen(municipio) || ''; |
| 74 | 80 | ||
| 75 | // Pre-calcular paths de todos los municipios para hover | 81 | // Pre-calcular paths de todos los municipios para hover |
| 82 | + console.time('[MAPA] allMunisPaths'); | ||
| 76 | mapaAllMunisPaths = mapaData.municipios.features.map(f => ({ | 83 | mapaAllMunisPaths = mapaData.municipios.features.map(f => ({ |
| 77 | path: pathGen(f) || '', | 84 | path: pathGen(f) || '', |
| 78 | codigo: f.properties.codigo, | 85 | codigo: f.properties.codigo, |
| 79 | isCurrent: f.properties.codigo === codigoNum | 86 | isCurrent: f.properties.codigo === codigoNum |
| 80 | })); | 87 | })); |
| 88 | + console.timeEnd('[MAPA] allMunisPaths'); | ||
| 89 | + console.timeEnd('[MAPA] total'); | ||
| 81 | } | 90 | } |
| 82 | 91 | ||
| 83 | // Datos desde el loader (Supabase) | 92 | // Datos desde el loader (Supabase) | ... | ... |
| 1 | <script> | 1 | <script> |
| 2 | import { onMount } from 'svelte'; | 2 | import { onMount } from 'svelte'; |
| 3 | - import { parquetReadObjects, asyncBufferFromUrl } from 'hyparquet'; | ||
| 4 | import * as Plot from '@observablehq/plot'; | 3 | import * as Plot from '@observablehq/plot'; |
| 5 | import * as d3 from 'd3'; | 4 | import * as d3 from 'd3'; |
| 6 | 5 | ||
| ... | @@ -124,10 +123,9 @@ | ... | @@ -124,10 +123,9 @@ |
| 124 | // Navegación interna entre secciones | 123 | // Navegación interna entre secciones |
| 125 | let seccionActiva = $state(0); | 124 | let seccionActiva = $state(0); |
| 126 | const secciones = [ | 125 | const secciones = [ |
| 127 | - { id: 'clasificadores', titulo: '¿En qué se gasta?' }, | ||
| 128 | - { id: 'entidades', titulo: '¿Quién ejecuta más?' }, | ||
| 129 | { id: 'programas', titulo: '¿En qué programas?' }, | 126 | { id: 'programas', titulo: '¿En qué programas?' }, |
| 130 | - { id: 'evolucion', titulo: '¿Cómo evoluciona?' } | 127 | + { id: 'clasificadores', titulo: '¿En qué se gasta?' }, |
| 128 | + { id: 'entidades', titulo: '¿Quién ejecuta más?' } | ||
| 131 | ]; | 129 | ]; |
| 132 | 130 | ||
| 133 | function irASeccion(index) { | 131 | function irASeccion(index) { |
| ... | @@ -219,11 +217,11 @@ | ... | @@ -219,11 +217,11 @@ |
| 219 | }; | 217 | }; |
| 220 | } | 218 | } |
| 221 | 219 | ||
| 222 | - // Cargar archivo Parquet usando asyncBufferFromUrl | 220 | + // Cargar datos desde API |
| 223 | - async function cargarParquet(url) { | 221 | + async function cargarDiario(tipo) { |
| 224 | - const file = await asyncBufferFromUrl({ url }); | 222 | + const res = await fetch(`/api/diario?tipo=${tipo}`); |
| 225 | - const data = await parquetReadObjects({ file }); | 223 | + if (!res.ok) throw new Error(`Error ${res.status}`); |
| 226 | - return data; | 224 | + return await res.json(); |
| 227 | } | 225 | } |
| 228 | 226 | ||
| 229 | onMount(async () => { | 227 | onMount(async () => { |
| ... | @@ -251,12 +249,12 @@ | ... | @@ -251,12 +249,12 @@ |
| 251 | themeObserver.observe(document.documentElement, { attributes: true }); | 249 | themeObserver.observe(document.documentElement, { attributes: true }); |
| 252 | 250 | ||
| 253 | try { | 251 | try { |
| 254 | - // Cargar todos los parquets en paralelo | 252 | + // Cargar todos los datos en paralelo desde API |
| 255 | const [tsData, entData, clasData, tlData] = await Promise.all([ | 253 | const [tsData, entData, clasData, tlData] = await Promise.all([ |
| 256 | - cargarParquet('/timeserie@4.parquet'), | 254 | + cargarDiario('timeseries'), |
| 257 | - cargarParquet('/ejecucion_entidades@1.parquet'), | 255 | + cargarDiario('entidades'), |
| 258 | - cargarParquet('/ejecucion_clasificadores@3.parquet'), | 256 | + cargarDiario('clasificadores'), |
| 259 | - cargarParquet('/timeline@1.parquet') | 257 | + cargarDiario('timeline') |
| 260 | ]); | 258 | ]); |
| 261 | 259 | ||
| 262 | // Los datos ya vienen como objetos | 260 | // Los datos ya vienen como objetos |
| ... | @@ -282,12 +280,12 @@ | ... | @@ -282,12 +280,12 @@ |
| 282 | 280 | ||
| 283 | // Filtrar datos por transferencias y días | 281 | // Filtrar datos por transferencias y días |
| 284 | let timeserieFiltered = $derived( | 282 | let timeserieFiltered = $derived( |
| 285 | - timeserie.filter(d => d.transferencias === incluirTransferencias) | 283 | + timeserie.filter(d => d.transferencias === undefined || d.transferencias === incluirTransferencias) |
| 286 | ); | 284 | ); |
| 287 | 285 | ||
| 288 | // Datos por clasificador para las barras apiladas (sin agrupar, cada hijo es un segmento) | 286 | // Datos por clasificador para las barras apiladas (sin agrupar, cada hijo es un segmento) |
| 289 | let datosPorClasificador = $derived.by(() => { | 287 | let datosPorClasificador = $derived.by(() => { |
| 290 | - const dias = diasSeleccionados === 'total' ? 'total' : diasSeleccionados; | 288 | + const dias = diasSeleccionados === 'total' ? 0 : parseInt(diasSeleccionados); |
| 291 | const result = {}; | 289 | const result = {}; |
| 292 | 290 | ||
| 293 | clasificadores.forEach(({ key }) => { | 291 | clasificadores.forEach(({ key }) => { |
| ... | @@ -447,7 +445,7 @@ | ... | @@ -447,7 +445,7 @@ |
| 447 | 445 | ||
| 448 | // Todas las entidades filtradas (sin límite) | 446 | // Todas las entidades filtradas (sin límite) |
| 449 | let entidadesTodas = $derived.by(() => { | 447 | let entidadesTodas = $derived.by(() => { |
| 450 | - const dias = diasSeleccionados === 'total' ? 'total' : diasSeleccionados; | 448 | + const dias = diasSeleccionados === 'total' ? 0 : parseInt(diasSeleccionados); |
| 451 | let filtered = ejecucionEntidades.filter(d => | 449 | let filtered = ejecucionEntidades.filter(d => |
| 452 | d.transferencias === incluirTransferencias && | 450 | d.transferencias === incluirTransferencias && |
| 453 | String(d.dias) === String(dias) && | 451 | String(d.dias) === String(dias) && |
| ... | @@ -975,7 +973,71 @@ | ... | @@ -975,7 +973,71 @@ |
| 975 | <p>Error: {error}</p> | 973 | <p>Error: {error}</p> |
| 976 | </div> | 974 | </div> |
| 977 | {:else} | 975 | {:else} |
| 978 | - <!-- SECCIÓN 1: Clasificadores --> | 976 | + <!-- SECCIÓN 1: Programas (timeline) --> |
| 977 | + <section class="vista-seccion" id="programas"> | ||
| 978 | + <div class="vista-contenido"> | ||
| 979 | + <div class="contenido-centrado"> | ||
| 980 | + <header class="seccion-header seccion-header-centrado"> | ||
| 981 | + <h1 class="seccion-titulo-grande">¿En qué programas?</h1> | ||
| 982 | + <p class="seccion-descripcion">Detalle de programas y actividades ejecutadas en la última semana</p> | ||
| 983 | + </header> | ||
| 984 | + | ||
| 985 | + <div class="timeline-search"> | ||
| 986 | + <svg class="search-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
| 987 | + <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" /> | ||
| 988 | + </svg> | ||
| 989 | + <input | ||
| 990 | + type="text" | ||
| 991 | + placeholder="Buscar entidad..." | ||
| 992 | + bind:value={busquedaTimeline} | ||
| 993 | + class="timeline-search-input" | ||
| 994 | + /> | ||
| 995 | + </div> | ||
| 996 | + | ||
| 997 | + <div class="timeline-cards"> | ||
| 998 | + {#each timelineByEntity as entidad} | ||
| 999 | + <div class="timeline-card"> | ||
| 1000 | + <div class="timeline-card-header"> | ||
| 1001 | + <div class="timeline-card-entity"> | ||
| 1002 | + <span class="timeline-card-area">{entidad.entidad_desc_area}</span> | ||
| 1003 | + <span class="timeline-card-nombre">{entidad.entidad_desc_entidad}</span> | ||
| 1004 | + </div> | ||
| 1005 | + <div class="timeline-card-total"> | ||
| 1006 | + <span class="timeline-card-total-label">Ejecutado</span> | ||
| 1007 | + <span class="timeline-card-total-monto">Bs. {formatearNumeroCompleto(entidad.total)}</span> | ||
| 1008 | + </div> | ||
| 1009 | + </div> | ||
| 1010 | + <div class="timeline-card-body"> | ||
| 1011 | + {#each cardsExpandidas[entidad.entidad] ? entidad.programas : entidad.programas.slice(0, 8) as prog} | ||
| 1012 | + <div class="timeline-prog-row"> | ||
| 1013 | + <div class="timeline-prog-info"> | ||
| 1014 | + <span class="timeline-prog-programa">{prog.programa}</span> | ||
| 1015 | + <span class="timeline-prog-actividad">{prog.actividad}</span> | ||
| 1016 | + </div> | ||
| 1017 | + <span class="timeline-prog-monto">Bs. {formatearNumeroCompleto(prog.devengado)}</span> | ||
| 1018 | + </div> | ||
| 1019 | + {/each} | ||
| 1020 | + | ||
| 1021 | + {#if entidad.programas.length > 8} | ||
| 1022 | + <button | ||
| 1023 | + class="timeline-ver-mas" | ||
| 1024 | + onclick={() => { | ||
| 1025 | + cardsExpandidas[entidad.entidad] = !cardsExpandidas[entidad.entidad]; | ||
| 1026 | + cardsExpandidas = { ...cardsExpandidas }; | ||
| 1027 | + }} | ||
| 1028 | + > | ||
| 1029 | + {cardsExpandidas[entidad.entidad] ? 'Ver menos' : `Ver ${entidad.programas.length - 8} más...`} | ||
| 1030 | + </button> | ||
| 1031 | + {/if} | ||
| 1032 | + </div> | ||
| 1033 | + </div> | ||
| 1034 | + {/each} | ||
| 1035 | + </div> | ||
| 1036 | + </div> | ||
| 1037 | + </div> | ||
| 1038 | + </section> | ||
| 1039 | + | ||
| 1040 | + <!-- SECCIÓN 2: Clasificadores --> | ||
| 979 | <section class="vista-seccion" id="clasificadores"> | 1041 | <section class="vista-seccion" id="clasificadores"> |
| 980 | <div class="vista-contenido"> | 1042 | <div class="vista-contenido"> |
| 981 | <div class="contenido-centrado"> | 1043 | <div class="contenido-centrado"> |
| ... | @@ -1143,73 +1205,9 @@ | ... | @@ -1143,73 +1205,9 @@ |
| 1143 | </div> | 1205 | </div> |
| 1144 | </section> | 1206 | </section> |
| 1145 | 1207 | ||
| 1146 | - <!-- SECCIÓN 3: Programas --> | ||
| 1147 | - <section class="vista-seccion" id="programas"> | ||
| 1148 | - <div class="vista-contenido"> | ||
| 1149 | - <div class="contenido-centrado"> | ||
| 1150 | - <!-- Título de sección --> | ||
| 1151 | - <header class="seccion-header seccion-header-centrado"> | ||
| 1152 | - <h1 class="seccion-titulo-grande">¿En qué programas?</h1> | ||
| 1153 | - <p class="seccion-descripcion">Detalle de programas y actividades ejecutadas en la última semana</p> | ||
| 1154 | - </header> | ||
| 1155 | - | ||
| 1156 | - <div class="timeline-search"> | ||
| 1157 | - <svg class="search-icon" viewBox="0 0 20 20" fill="currentColor"> | ||
| 1158 | - <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" /> | ||
| 1159 | - </svg> | ||
| 1160 | - <input | ||
| 1161 | - type="text" | ||
| 1162 | - placeholder="Buscar entidad..." | ||
| 1163 | - bind:value={busquedaTimeline} | ||
| 1164 | - class="timeline-search-input" | ||
| 1165 | - /> | ||
| 1166 | - </div> | ||
| 1167 | - | ||
| 1168 | - <div class="timeline-cards"> | ||
| 1169 | - {#each timelineByEntity as entidad} | ||
| 1170 | - <div class="timeline-card"> | ||
| 1171 | - <div class="timeline-card-header"> | ||
| 1172 | - <div class="timeline-card-entity"> | ||
| 1173 | - <span class="timeline-card-area">{entidad.entidad_desc_area}</span> | ||
| 1174 | - <span class="timeline-card-nombre">{entidad.entidad_desc_entidad}</span> | ||
| 1175 | - </div> | ||
| 1176 | - <div class="timeline-card-total"> | ||
| 1177 | - <span class="timeline-card-total-label">Ejecutado</span> | ||
| 1178 | - <span class="timeline-card-total-monto">Bs. {formatearNumeroCompleto(entidad.total)}</span> | ||
| 1179 | - </div> | ||
| 1180 | - </div> | ||
| 1181 | - <div class="timeline-card-body"> | ||
| 1182 | - {#each cardsExpandidas[entidad.entidad] ? entidad.programas : entidad.programas.slice(0, 8) as prog} | ||
| 1183 | - <div class="timeline-prog-row"> | ||
| 1184 | - <div class="timeline-prog-info"> | ||
| 1185 | - <span class="timeline-prog-programa">{prog.programa}</span> | ||
| 1186 | - <span class="timeline-prog-actividad">{prog.actividad}</span> | ||
| 1187 | - </div> | ||
| 1188 | - <span class="timeline-prog-monto">Bs. {formatearNumeroCompleto(prog.devengado)}</span> | ||
| 1189 | - </div> | ||
| 1190 | - {/each} | ||
| 1191 | - | ||
| 1192 | - {#if entidad.programas.length > 8} | ||
| 1193 | - <button | ||
| 1194 | - class="timeline-ver-mas" | ||
| 1195 | - onclick={() => { | ||
| 1196 | - cardsExpandidas[entidad.entidad] = !cardsExpandidas[entidad.entidad]; | ||
| 1197 | - cardsExpandidas = { ...cardsExpandidas }; | ||
| 1198 | - }} | ||
| 1199 | - > | ||
| 1200 | - {cardsExpandidas[entidad.entidad] ? 'Ver menos' : `Ver ${entidad.programas.length - 8} más...`} | ||
| 1201 | - </button> | ||
| 1202 | - {/if} | ||
| 1203 | - </div> | ||
| 1204 | - </div> | ||
| 1205 | - {/each} | ||
| 1206 | - </div> | ||
| 1207 | - </div> | ||
| 1208 | - </div> | ||
| 1209 | - </section> | ||
| 1210 | 1208 | ||
| 1211 | <!-- SECCIÓN 4: Evolución temporal --> | 1209 | <!-- SECCIÓN 4: Evolución temporal --> |
| 1212 | - <section class="vista-seccion" id="evolucion"> | 1210 | + <section class="vista-seccion" id="evolucion" style="display: none;"> |
| 1213 | <div class="vista-contenido"> | 1211 | <div class="vista-contenido"> |
| 1214 | <div class="contenido-centrado"> | 1212 | <div class="contenido-centrado"> |
| 1215 | <!-- Título de sección --> | 1213 | <!-- Título de sección --> | ... | ... |
This diff could not be displayed because it is too large.
-
Please register or login to post a comment