Rafael Lopez

ubigeo

This diff is collapsed. Click to expand it.
import { writable } from 'svelte/store';
// Persiste el estado de búsqueda del landing entre navegaciones
export const landingSearchMode = writable('programas'); // 'programas' | 'clasificadores'
export const landingSearchMode = writable('todo'); // 'todo' | 'programas' | 'clasificadores'
export const landingSelectedClassifiers = writable([]); // ['entidad', 'objeto', ...]
export const landingSearchQuery = writable('');
......
import { writable } from 'svelte/store';
// Cache global del mapa - se carga una sola vez en toda la sesión
export const mapaCache = writable(null);
This diff is collapsed. Click to expand it.
// Proxy para evitar CORS
const API_BASE = 'http://34.66.240.236/api/proyecto/search';
const API_BASE = 'http://34.171.18.2/api/proyecto/search';
export async function GET({ url }) {
const q = url.searchParams.get('q') || '';
const is_class = url.searchParams.get('is_class') || 'false';
const mode = url.searchParams.get('mode') || 'gastos'; // 'gastos' | 'ingresos'
const class_ = url.searchParams.get('class_') || ''; // comma-separated: entidad, objeto, finfun, acteco
const page = url.searchParams.get('page') || '1'; // Paginación
const per_page = url.searchParams.get('per_page') || ''; // Resultados por página
const is_class = url.searchParams.get('is_class'); // null si no viene
const mode = url.searchParams.get('mode') || '';
const class_ = url.searchParams.get('class_') || '';
const page = url.searchParams.get('page') || '1';
const per_page = url.searchParams.get('per_page') || '';
// Build URL with all parameters
const apiUrl = new URL(API_BASE);
apiUrl.searchParams.set('q', q);
apiUrl.searchParams.set('is_class', is_class);
apiUrl.searchParams.set('page', page);
// Solo enviar is_class si viene explícitamente
if (is_class !== null) {
apiUrl.searchParams.set('is_class', is_class);
}
if (per_page) apiUrl.searchParams.set('per_page', per_page);
if (mode) apiUrl.searchParams.set('mode', mode);
if (class_) apiUrl.searchParams.set('class_', class_);
......
......@@ -5,21 +5,29 @@
import { supabase } from '$lib/supabase';
let { data } = $props();
const entidad = data.entidad;
const isDA = data.isDA;
const nombreDA = data.nombreDA;
const nombreEntidadMadre = data.nombreEntidadMadre;
const codigoEntidadPadre = data.codigoEntidadPadre;
let entidad = $derived(data.entidad);
let isDA = $derived(data.isDA);
let nombreDA = $derived(data.nombreDA);
let nombreEntidadMadre = $derived(data.nombreEntidadMadre);
let codigoEntidadPadre = $derived(data.codigoEntidadPadre);
// Datos desde el loader (Supabase)
let codigoSeleccionado = $derived($page.params.codigo);
let gestionSeleccionada = $state(data.gestionInicial);
let resumenData = $state(data.resumenData);
let resumenData = $derived(data.resumenData);
let distribucionesData = $state(data.distribucionesData);
let cargando = $state(false);
let distCache = $state({ [data.gestionInicial]: data.distribucionesData });
let cargandoDist = $state(false);
// Resetear al cambiar de entidad
$effect(() => {
const g = data.gestionInicial;
gestionSeleccionada = g;
distribucionesData = data.distribucionesData;
distCache = { [g]: data.distribucionesData };
});
async function cargarDistribuciones(gestion) {
if (distCache[gestion]) {
distribucionesData = distCache[gestion];
......@@ -148,14 +156,21 @@
return years;
});
function parseRanking(rankingStr) {
if (!rankingStr) return null;
const parts = String(rankingStr).split('/');
if (parts.length !== 2) return null;
return { posicion: parseInt(parts[0]), total: parseInt(parts[1]) };
}
let rankingGastos = $derived(() => {
const row = historiaGastos.find(d => d.gestion === gestionSeleccionada);
return row ? { posicion: row.ranking, total: '~600' } : null;
return row ? parseRanking(row.ranking) : null;
});
let rankingIngresos = $derived(() => {
const row = historiaIngresos.find(d => d.gestion === gestionSeleccionada);
return row ? { posicion: row.ranking, total: '~600' } : null;
return row ? parseRanking(row.ranking) : null;
});
let distFiltradas = $derived(distribucionesData);
......@@ -672,34 +687,42 @@
{#if rankingGastos()}
{@const r = rankingGastos()}
{@const total = parseInt(r.total) || 600}
{@const barIdx = Math.max(0, Math.min(79, Math.round((r.posicion / total) * 80)))}
{@const barIdx = Math.max(0, Math.min(79, 80 - Math.round((r.posicion / total) * 80)))}
<div class="ranking-card">
<span class="ranking-card-label">Ranking en gasto</span>
<div class="ranking-headline">
<span class="ranking-pos-big">#{r.posicion}</span>
<span class="ranking-pos-context">de <span class="ranking-total-num">{total}</span> instituciones · {r.posicion <= total / 2 ? 'entre las que más gastan' : 'entre las que menos gastan'}</span>
</div>
<div class="ranking-bars">
{#each Array(80) as _, i}
<div class="ranking-bar-tick"></div>
{/each}
<div class="ranking-marker" style="left: {barIdx * 1.25}%"></div>
</div>
<div class="ranking-caption">
<span class="ranking-pos">#{r.posicion}</span> de {total} entidades
<div class="ranking-extremos">
<span class="ranking-extremo">Menos gasto</span>
<span class="ranking-extremo">Más gasto</span>
</div>
</div>
{/if}
{#if tieneIngresos && rankingIngresos()}
{@const r = rankingIngresos()}
{@const total = parseInt(r.total) || 600}
{@const barIdx = Math.max(0, Math.min(79, Math.round((r.posicion / total) * 80)))}
{@const barIdx = Math.max(0, Math.min(79, 80 - Math.round((r.posicion / total) * 80)))}
<div class="ranking-card">
<span class="ranking-card-label">Ranking en ingresos</span>
<div class="ranking-headline">
<span class="ranking-pos-big">#{r.posicion}</span>
<span class="ranking-pos-context">de <span class="ranking-total-num">{total}</span> instituciones · {r.posicion <= total / 2 ? 'entre las que más ingresan' : 'entre las que menos ingresan'}</span>
</div>
<div class="ranking-bars">
{#each Array(80) as _, i}
<div class="ranking-bar-tick"></div>
{/each}
<div class="ranking-marker" style="left: {barIdx * 1.25}%"></div>
</div>
<div class="ranking-caption">
<span class="ranking-pos">#{r.posicion}</span> de {total} entidades
<div class="ranking-extremos">
<span class="ranking-extremo">Menos ingresos</span>
<span class="ranking-extremo">Más ingresos</span>
</div>
</div>
{/if}
......@@ -712,12 +735,19 @@
<div class="clasificadores-grid">
{#each clasificadoresGasto as { key, label, data }}
{@const displayItem = getDisplayItem(key, data)}
{@const totalClasif = getTotal(data)}
{@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0}
<div class="clasificador-card">
<div class="clasificador-header">
<div class="clasificador-info">
<span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
<div class="clasificador-nombres">
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
</div>
<div class="clasificador-valores">
<span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
<span class="clasificador-pct">{pct}%</span>
</div>
</div>
<span class="clasificador-label">{label}</span>
</div>
......@@ -735,12 +765,19 @@
<div class="clasificadores-grid">
{#each clasificadoresIngreso as { key, label, data }}
{@const displayItem = getDisplayItem(key, data)}
{@const totalClasif = getTotal(data)}
{@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0}
<div class="clasificador-card">
<div class="clasificador-header">
<div class="clasificador-info">
<span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
<div class="clasificador-nombres">
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
</div>
<div class="clasificador-valores">
<span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
<span class="clasificador-pct">{pct}%</span>
</div>
</div>
<span class="clasificador-label">{label}</span>
</div>
......@@ -1039,14 +1076,39 @@
background: #D4A574;
}
.ranking-caption {
margin-top: 0.35rem;
font-size: 0.7rem;
.ranking-extremos {
display: flex;
justify-content: space-between;
margin-top: 0.25rem;
}
.ranking-extremo {
font-size: 0.6rem;
color: var(--theme-texto);
opacity: 0.6;
opacity: 0.4;
}
.ranking-headline {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.ranking-pos-big {
font-size: 1.5rem;
font-weight: 700;
color: var(--theme-titulo);
line-height: 1;
}
.ranking-pos-context {
font-size: 0.8rem;
color: var(--theme-texto);
opacity: 0.7;
}
.ranking-pos {
.ranking-total-num {
font-weight: 700;
color: var(--theme-titulo);
opacity: 1;
......@@ -1150,7 +1212,7 @@
:global(html:not(.dark)) .gestion-option.active {
background: rgba(90, 157, 191, 0.1);
color: #5A9DBF;
color: #3D7A9C;
}
.seccion-sub {
......@@ -1230,7 +1292,7 @@
.historia-monto.ingresos,
.historia-monto.gastos {
color: #5A9DBF;
color: #3D7A9C;
}
:global(html.dark) .historia-monto.ingresos,
......@@ -1270,20 +1332,22 @@
}
.area-path {
fill: rgba(90, 157, 191, 0.12);
fill: rgba(61, 122, 156, 0.4);
}
.line-path {
stroke: #5A9DBF;
stroke: #3D7A9C;
opacity: 0.9;
}
.dot {
fill: #5A9DBF;
fill: #3D7A9C;
opacity: 0.9;
transition: r 0.15s;
}
.dot.active {
fill: #5A9DBF;
fill: #3D7A9C;
}
.hover-line {
......@@ -1339,12 +1403,25 @@
.clasificador-info {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
overflow: hidden;
}
.clasificador-nombres {
display: flex;
align-items: baseline;
gap: 0.4rem;
min-width: 0;
overflow: hidden;
}
.clasificador-valores {
display: flex;
align-items: baseline;
gap: 0.4rem;
}
.clasificador-monto {
font-size: 0.95rem;
font-weight: 600;
......@@ -1353,6 +1430,15 @@
flex-shrink: 0;
}
.clasificador-pct {
font-size: 0.8rem;
font-weight: 600;
color: var(--theme-texto);
opacity: 0.5;
white-space: nowrap;
flex-shrink: 0;
}
.clasificador-padre {
font-size: 0.8rem;
font-weight: 400;
......
<script>
import { onMount } from 'svelte';
import * as d3 from 'd3';
let mapaData = $state(null);
let boliviaPath = $state('');
let deptoPath = $state('');
let muniPath = $state('');
let error = $state('');
let debug = $state('');
onMount(async () => {
try {
const res = await fetch('/mapa.json');
mapaData = await res.json();
debug = `bolivia: ${mapaData.bolivia.features.length} features, `;
debug += `deptos: ${mapaData.departamentos.features.length}, `;
debug += `munis: ${mapaData.municipios.features.length}`;
// Quitar CRS y corregir winding order
delete mapaData.bolivia.crs;
delete mapaData.departamentos.crs;
delete mapaData.municipios.crs;
// Corregir winding order: invertir todos los anillos
function fixWinding(geojson) {
const fix = (coords, isHole) => {
// Para geo coords: exterior debe ser counterclockwise (area < 0 en D3)
// Simplemente invertimos todos los anillos exteriores
coords.forEach((ring, i) => {
if (i === 0) ring.reverse(); // exterior
// holes quedan como están
});
};
for (const feat of geojson.features || [geojson]) {
const geom = (feat.geometry || feat);
if (geom.type === 'Polygon') {
fix(geom.coordinates);
} else if (geom.type === 'MultiPolygon') {
geom.coordinates.forEach(poly => fix(poly));
}
}
}
fixWinding(mapaData.bolivia);
fixWinding(mapaData.departamentos);
fixWinding(mapaData.municipios);
const bounds = d3.geoBounds(mapaData.bolivia);
debug += ` | bounds: ${JSON.stringify(bounds)}`;
const projection = d3.geoMercator().fitSize([380, 480], mapaData.bolivia);
const pathGen = d3.geoPath(projection);
boliviaPath = pathGen(mapaData.bolivia) || '';
debug += ` | boliviaPath length: ${boliviaPath.length}`;
deptoPath = mapaData.departamentos.features.map(f => pathGen(f)).join(' ');
debug += ` | deptoPath length: ${deptoPath.length}`;
const muni = mapaData.municipios.features[0];
muniPath = pathGen(muni) || '';
debug += ` | muni code: ${muni.properties.codigo}`;
} catch (e) {
error = e.message;
}
});
</script>
<div style="padding: 2rem; background: #1C1C1A; color: #F5F0E8; min-height: 100vh;">
<h1>Test Mapa</h1>
<p style="font-size: 12px; color: #888;">{debug}</p>
{#if error}
<p style="color: red;">{error}</p>
{/if}
<svg width="400" height="500" viewBox="0 0 400 500" style="border: 1px solid #333;">
{#if boliviaPath}
<path d={boliviaPath} fill="none" stroke="#555" stroke-width="1" />
{/if}
{#if deptoPath}
<path d={deptoPath} fill="rgba(255,255,255,0.1)" stroke="#777" stroke-width="0.5" />
{/if}
{#if muniPath}
<path d={muniPath} fill="#D4A574" stroke="#D4A574" stroke-width="1" />
{/if}
</svg>
</div>
import { supabase } from '$lib/supabase';
export async function load({ fetch }) {
// Cargar resumen y población en paralelo
const [resumenRes, pobRes] = await Promise.all([
supabase
.schema('ppto')
.from('entidad_resumen')
.select('codigo, desc, gestion, devengado, ranking')
.eq('tipo', 'gastos')
.eq('tipo_codigo', 'municipio_ubigeo')
.eq('gestion', 2025)
.order('devengado', { ascending: false })
.limit(500),
fetch('/poblacion.csv').then(r => r.text())
]);
const poblacionMap = {};
pobRes.split('\n').slice(1).forEach(line => {
const [cod, gestion, pob] = line.split(',');
if (parseInt(gestion) === 2025) {
poblacionMap[cod] = parseInt(pob);
}
});
const gestiones = [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025];
return {
resumen: resumenRes.data || [],
poblacionMap,
gestiones,
gestionInicial: 2025
};
}
This diff is collapsed. Click to expand it.
import { supabase } from '$lib/supabase';
import { error } from '@sveltejs/kit';
export async function load({ params, fetch }) {
const codigo = params.ubigeo;
// Cargar resumen y población en paralelo
const [resumenRes, pobRes] = await Promise.all([
supabase
.schema('ppto')
.from('entidad_resumen')
.select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking')
.eq('codigo', codigo),
fetch('/poblacion.csv').then(r => r.text())
]);
if (resumenRes.error || !resumenRes.data?.length) {
throw error(404, 'Ubicación no encontrada');
}
// Parsear población para este código
const poblacionMap = {};
pobRes.split('\n').slice(1).forEach(line => {
const [cod, gestion, pob] = line.split(',');
if (cod === codigo) {
poblacionMap[parseInt(gestion)] = parseInt(pob);
}
});
// Determinar última gestión
const gestiones = [...new Set(resumenRes.data.map(d => d.gestion))].sort((a, b) => b - a);
const ultimaGestion = gestiones[0] || 2025;
// Cargar distribuciones de la última gestión
const distRes = await supabase
.schema('ppto')
.from('entidad_distribuciones')
.select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado')
.eq('codigo', codigo)
.eq('gestion', ultimaGestion);
const firstRow = resumenRes.data[0];
return {
nombre: firstRow.desc || `Ubicación ${codigo}`,
nombrePadre: firstRow.desc_padre || null,
codigo,
resumenData: resumenRes.data || [],
distribucionesData: distRes.data || [],
gestionInicial: ultimaGestion,
poblacionMap
};
}
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
This diff is collapsed. Click to expand it.