Rafael Lopez

docs

......@@ -114,7 +114,7 @@
} else if (!isClass) {
// Programa/proyecto
tipo = 'programa';
codigo = `${meta.entidad || ''}-${meta.programa || ''}-${meta.proyecto || ''}`;
codigo = String(meta.programa || '');
}
// Contexto extra para programas
......@@ -213,7 +213,7 @@
rubro: '/rubro/',
organismo: '/organismo/',
ubigeo: '/ubicacion/',
programa: '/proyecto/'
programa: '/programa/'
};
const base = routes[item.tipo];
if (base) {
......
......@@ -807,7 +807,7 @@
} else if (isClassResult) {
url = `/clasificador/${hit.document.class_}/${hit.document.id}`;
} else {
url = `/proyecto/${generateCodigo(hit)}`;
url = `/programa/${generateCodigo(hit)}`;
}
window.location.href = url;
......@@ -921,7 +921,7 @@
function generateCodigo(hit) {
const m = parseMetadatos(hit.document.metadatos);
return `${m.entidad}-${m.programa}-${m.proyecto}-${m.actividad}-${hit.document.gestion}`;
return String(m.programa || '');
}
$: stats = filteredHits.length > 0 ? getStats(
......@@ -1485,7 +1485,7 @@
? `/rubro/${rubroCode}`
: isOrganismoClass
? `/organismo/${meta.organismo || ''}`
: (isClassResult ? `/clasificador/${hit.document.class_}/${hit.document.id}` : `/proyecto/${generateCodigo(hit)}`)}
: (isClassResult ? `/clasificador/${hit.document.class_}/${hit.document.id}` : `/programa/${generateCodigo(hit)}`)}
class="result-card"
class:result-card-selected={selectedResultIndex === i}
data-result-index={i}
......
const API_BASE = 'http://136.112.29.74/api/diario';
export async function GET({ url }) {
const tipo = url.searchParams.get('tipo') || 'status';
const endpoints = {
status: `${API_BASE}/status`,
timeseries: `${API_BASE}/timeseries`,
entidades: `${API_BASE}/entidades`,
clasificadores: `${API_BASE}/clasificadores`,
timeline: `${API_BASE}/timeline`
};
const apiUrl = endpoints[tipo];
if (!apiUrl) {
return new Response(JSON.stringify({ error: 'Invalid tipo' }), {
status: 400, headers: { 'Content-Type': 'application/json' }
});
}
try {
const response = await fetch(apiUrl);
if (!response.ok) {
return new Response(JSON.stringify({ error: `API error: ${response.status}` }), {
status: response.status, headers: { 'Content-Type': 'application/json' }
});
}
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
});
} catch {
return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
status: 500, headers: { 'Content-Type': 'application/json' }
});
}
}
const API_BASE = 'http://136.112.29.74/api/programa_proyecto';
export async function GET({ url }) {
const codigo = url.searchParams.get('codigo') || '';
const tipo = url.searchParams.get('tipo') || 'detalle'; // detalle, proyectos
if (!codigo) {
return new Response(JSON.stringify({ error: 'Missing codigo' }), {
status: 400, headers: { 'Content-Type': 'application/json' }
});
}
const apiUrl = tipo === 'proyectos'
? `${API_BASE}/${codigo}/proyectos`
: `${API_BASE}/${codigo}`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
return new Response(JSON.stringify({ error: `API error: ${response.status}` }), {
status: response.status, headers: { 'Content-Type': 'application/json' }
});
}
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
});
} catch {
return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
status: 500, headers: { 'Content-Type': 'application/json' }
});
}
}
......@@ -11,8 +11,11 @@
let searchableItems = $state([]);
let isMac = $state(false);
// Markdown content embedded directly
const MARKDOWN_CONTENT = `# Introducción
// Markdown content loaded from static file
let MARKDOWN_CONTENT = $state('');
// Placeholder for initial content
const INITIAL_CONTENT = `# Introducción
El presupuesto público se presenta en dos distribuciones de datos: una histórica y otra en tiempo real.
......@@ -1043,6 +1046,12 @@ Ejemplo: \`5637823.65\`.
isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
try {
// Cargar markdown desde archivo estático
const res = await fetch('/docs.md');
if (res.ok) {
MARKDOWN_CONTENT = await res.text();
}
const result = processMarkdown(MARKDOWN_CONTENT);
content = result.html;
headings = buildHierarchy(result.headings);
......@@ -1327,12 +1336,6 @@ Ejemplo: \`5637823.65\`.
<header class="docs-header">
<div class="docs-header-content">
<div class="docs-header-top">
<a href="/" class="docs-back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
<span>Inicio</span>
</a>
<!-- Mobile menu button -->
<button class="docs-menu-btn" onclick={toggleSidebar}>
......@@ -1501,7 +1504,7 @@ Ejemplo: \`5637823.65\`.
.docs-header-content {
max-width: 1000px;
margin: 0 auto;
padding: 1rem 1.5rem;
padding: 80px 1.5rem 1rem;
}
.docs-header-top {
......@@ -1568,7 +1571,7 @@ Ejemplo: \`5637823.65\`.
.docs-layout-wrapper {
max-width: 700px;
margin: 0 auto;
padding-top: 140px;
padding-top: 340px;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
......@@ -1603,7 +1606,7 @@ Ejemplo: \`5637823.65\`.
.docs-sidebar-content {
padding-right: 1rem;
padding-top: 2rem;
padding-top: 10rem;
}
}
......@@ -1824,7 +1827,7 @@ Ejemplo: \`5637823.65\`.
@media (max-width: 1099px) {
.docs-layout-wrapper {
padding-top: 130px;
padding-top: 330px;
padding-left: 1rem;
padding-right: 1rem;
}
......@@ -2049,6 +2052,7 @@ Ejemplo: \`5637823.65\`.
.docs-search-container {
position: relative;
margin-top: 1rem;
max-width: 320px;
}
.docs-search-input-wrapper {
......
This diff is collapsed. Click to expand it.
......@@ -35,18 +35,24 @@
}
async function cargarMapa(codigo) {
console.time('[MAPA] total');
const cached = get(mapaCache);
if (cached) {
console.log('[MAPA] usando cache');
mapaData = cached;
} else if (!mapaData) {
console.time('[MAPA] fetch');
const res = await fetch('/mapa.json');
mapaData = await res.json();
console.timeEnd('[MAPA] fetch');
delete mapaData.bolivia.crs;
delete mapaData.departamentos.crs;
delete mapaData.municipios.crs;
console.time('[MAPA] fixWinding');
fixWinding(mapaData.bolivia);
fixWinding(mapaData.departamentos);
fixWinding(mapaData.municipios);
console.timeEnd('[MAPA] fixWinding');
mapaCache.set(mapaData);
}
......@@ -73,11 +79,14 @@
mapaMunicipioPath = pathGen(municipio) || '';
// Pre-calcular paths de todos los municipios para hover
console.time('[MAPA] allMunisPaths');
mapaAllMunisPaths = mapaData.municipios.features.map(f => ({
path: pathGen(f) || '',
codigo: f.properties.codigo,
isCurrent: f.properties.codigo === codigoNum
}));
console.timeEnd('[MAPA] allMunisPaths');
console.timeEnd('[MAPA] total');
}
// Datos desde el loader (Supabase)
......
<script>
import { onMount } from 'svelte';
import { parquetReadObjects, asyncBufferFromUrl } from 'hyparquet';
import * as Plot from '@observablehq/plot';
import * as d3 from 'd3';
......@@ -124,10 +123,9 @@
// Navegación interna entre secciones
let seccionActiva = $state(0);
const secciones = [
{ id: 'clasificadores', titulo: '¿En qué se gasta?' },
{ id: 'entidades', titulo: '¿Quién ejecuta más?' },
{ id: 'programas', titulo: '¿En qué programas?' },
{ id: 'evolucion', titulo: '¿Cómo evoluciona?' }
{ id: 'clasificadores', titulo: '¿En qué se gasta?' },
{ id: 'entidades', titulo: '¿Quién ejecuta más?' }
];
function irASeccion(index) {
......@@ -219,11 +217,11 @@
};
}
// Cargar archivo Parquet usando asyncBufferFromUrl
async function cargarParquet(url) {
const file = await asyncBufferFromUrl({ url });
const data = await parquetReadObjects({ file });
return data;
// Cargar datos desde API
async function cargarDiario(tipo) {
const res = await fetch(`/api/diario?tipo=${tipo}`);
if (!res.ok) throw new Error(`Error ${res.status}`);
return await res.json();
}
onMount(async () => {
......@@ -251,12 +249,12 @@
themeObserver.observe(document.documentElement, { attributes: true });
try {
// Cargar todos los parquets en paralelo
// Cargar todos los datos en paralelo desde API
const [tsData, entData, clasData, tlData] = await Promise.all([
cargarParquet('/timeserie@4.parquet'),
cargarParquet('/ejecucion_entidades@1.parquet'),
cargarParquet('/ejecucion_clasificadores@3.parquet'),
cargarParquet('/timeline@1.parquet')
cargarDiario('timeseries'),
cargarDiario('entidades'),
cargarDiario('clasificadores'),
cargarDiario('timeline')
]);
// Los datos ya vienen como objetos
......@@ -282,12 +280,12 @@
// Filtrar datos por transferencias y días
let timeserieFiltered = $derived(
timeserie.filter(d => d.transferencias === incluirTransferencias)
timeserie.filter(d => d.transferencias === undefined || d.transferencias === incluirTransferencias)
);
// Datos por clasificador para las barras apiladas (sin agrupar, cada hijo es un segmento)
let datosPorClasificador = $derived.by(() => {
const dias = diasSeleccionados === 'total' ? 'total' : diasSeleccionados;
const dias = diasSeleccionados === 'total' ? 0 : parseInt(diasSeleccionados);
const result = {};
clasificadores.forEach(({ key }) => {
......@@ -447,7 +445,7 @@
// Todas las entidades filtradas (sin límite)
let entidadesTodas = $derived.by(() => {
const dias = diasSeleccionados === 'total' ? 'total' : diasSeleccionados;
const dias = diasSeleccionados === 'total' ? 0 : parseInt(diasSeleccionados);
let filtered = ejecucionEntidades.filter(d =>
d.transferencias === incluirTransferencias &&
String(d.dias) === String(dias) &&
......@@ -975,7 +973,71 @@
<p>Error: {error}</p>
</div>
{:else}
<!-- SECCIÓN 1: Clasificadores -->
<!-- SECCIÓN 1: Programas (timeline) -->
<section class="vista-seccion" id="programas">
<div class="vista-contenido">
<div class="contenido-centrado">
<header class="seccion-header seccion-header-centrado">
<h1 class="seccion-titulo-grande">¿En qué programas?</h1>
<p class="seccion-descripcion">Detalle de programas y actividades ejecutadas en la última semana</p>
</header>
<div class="timeline-search">
<svg class="search-icon" viewBox="0 0 20 20" fill="currentColor">
<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" />
</svg>
<input
type="text"
placeholder="Buscar entidad..."
bind:value={busquedaTimeline}
class="timeline-search-input"
/>
</div>
<div class="timeline-cards">
{#each timelineByEntity as entidad}
<div class="timeline-card">
<div class="timeline-card-header">
<div class="timeline-card-entity">
<span class="timeline-card-area">{entidad.entidad_desc_area}</span>
<span class="timeline-card-nombre">{entidad.entidad_desc_entidad}</span>
</div>
<div class="timeline-card-total">
<span class="timeline-card-total-label">Ejecutado</span>
<span class="timeline-card-total-monto">Bs. {formatearNumeroCompleto(entidad.total)}</span>
</div>
</div>
<div class="timeline-card-body">
{#each cardsExpandidas[entidad.entidad] ? entidad.programas : entidad.programas.slice(0, 8) as prog}
<div class="timeline-prog-row">
<div class="timeline-prog-info">
<span class="timeline-prog-programa">{prog.programa}</span>
<span class="timeline-prog-actividad">{prog.actividad}</span>
</div>
<span class="timeline-prog-monto">Bs. {formatearNumeroCompleto(prog.devengado)}</span>
</div>
{/each}
{#if entidad.programas.length > 8}
<button
class="timeline-ver-mas"
onclick={() => {
cardsExpandidas[entidad.entidad] = !cardsExpandidas[entidad.entidad];
cardsExpandidas = { ...cardsExpandidas };
}}
>
{cardsExpandidas[entidad.entidad] ? 'Ver menos' : `Ver ${entidad.programas.length - 8} más...`}
</button>
{/if}
</div>
</div>
{/each}
</div>
</div>
</div>
</section>
<!-- SECCIÓN 2: Clasificadores -->
<section class="vista-seccion" id="clasificadores">
<div class="vista-contenido">
<div class="contenido-centrado">
......@@ -1143,73 +1205,9 @@
</div>
</section>
<!-- SECCIÓN 3: Programas -->
<section class="vista-seccion" id="programas">
<div class="vista-contenido">
<div class="contenido-centrado">
<!-- Título de sección -->
<header class="seccion-header seccion-header-centrado">
<h1 class="seccion-titulo-grande">¿En qué programas?</h1>
<p class="seccion-descripcion">Detalle de programas y actividades ejecutadas en la última semana</p>
</header>
<div class="timeline-search">
<svg class="search-icon" viewBox="0 0 20 20" fill="currentColor">
<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" />
</svg>
<input
type="text"
placeholder="Buscar entidad..."
bind:value={busquedaTimeline}
class="timeline-search-input"
/>
</div>
<div class="timeline-cards">
{#each timelineByEntity as entidad}
<div class="timeline-card">
<div class="timeline-card-header">
<div class="timeline-card-entity">
<span class="timeline-card-area">{entidad.entidad_desc_area}</span>
<span class="timeline-card-nombre">{entidad.entidad_desc_entidad}</span>
</div>
<div class="timeline-card-total">
<span class="timeline-card-total-label">Ejecutado</span>
<span class="timeline-card-total-monto">Bs. {formatearNumeroCompleto(entidad.total)}</span>
</div>
</div>
<div class="timeline-card-body">
{#each cardsExpandidas[entidad.entidad] ? entidad.programas : entidad.programas.slice(0, 8) as prog}
<div class="timeline-prog-row">
<div class="timeline-prog-info">
<span class="timeline-prog-programa">{prog.programa}</span>
<span class="timeline-prog-actividad">{prog.actividad}</span>
</div>
<span class="timeline-prog-monto">Bs. {formatearNumeroCompleto(prog.devengado)}</span>
</div>
{/each}
{#if entidad.programas.length > 8}
<button
class="timeline-ver-mas"
onclick={() => {
cardsExpandidas[entidad.entidad] = !cardsExpandidas[entidad.entidad];
cardsExpandidas = { ...cardsExpandidas };
}}
>
{cardsExpandidas[entidad.entidad] ? 'Ver menos' : `Ver ${entidad.programas.length - 8} más...`}
</button>
{/if}
</div>
</div>
{/each}
</div>
</div>
</div>
</section>
<!-- SECCIÓN 4: Evolución temporal -->
<section class="vista-seccion" id="evolucion">
<section class="vista-seccion" id="evolucion" style="display: none;">
<div class="vista-contenido">
<div class="contenido-centrado">
<!-- Título de sección -->
......
This diff could not be displayed because it is too large.