Rafael Lopez

ubigeo

This diff is collapsed. Click to expand it.
// Proxy para evitar CORS
const API_BASE = 'http://34.171.18.2/api/proyecto/search';
const API_BASE = 'http://136.112.29.74/api/proyecto/search';
export async function GET({ url }) {
const q = url.searchParams.get('q') || '';
......
const API_BASE = 'http://136.112.29.74/api/ubigeo/clasificador';
export async function GET() {
try {
const response = await fetch(API_BASE);
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
});
} catch (err) {
return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
export async function load({ fetch }) {
const res = await fetch('/api/ubigeo-clasificador');
const data = await res.json();
return { clasificador: data };
}
This diff is collapsed. Click to expand it.
import { supabase } from '$lib/supabase';
import { error } from '@sveltejs/kit';
export async function load({ params }) {
export async function load({ params, fetch }) {
const codigo = params.codigo;
const isDA = codigo.includes('.');
const entidadCode = isDA ? codigo.split('.')[0] : codigo;
......@@ -11,8 +11,8 @@ export async function load({ params }) {
throw error(400, 'Código de entidad inválido');
}
// Cargar metadata y resumen en paralelo
const [entidadRes, resumenRes] = await Promise.all([
// Cargar metadata, resumen y población en paralelo
const [entidadRes, resumenRes, pobRes] = await Promise.all([
supabase
.schema('ppto')
.from('clas_institucional')
......@@ -24,9 +24,31 @@ export async function load({ params }) {
.schema('ppto')
.from('entidad_resumen')
.select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking')
.eq('codigo', codigo)
.eq('codigo', codigo),
fetch('/poblacion.csv').then(r => r.text())
]);
// Buscar población: primero por entidad, si no suma nacional
const poblacionEntidad = {};
const poblacionNacional = {};
pobRes.split('\n').slice(1).forEach(line => {
const [, ent, gestion, pob] = line.split(',');
const g = parseInt(gestion);
const p = parseInt(pob);
if (!g || !p) return;
// Suma nacional
poblacionNacional[g] = (poblacionNacional[g] || 0) + p;
// Por entidad (código sin DA)
if (ent === entidadCode) {
poblacionEntidad[g] = (poblacionEntidad[g] || 0) + p;
}
});
// Usar población de la entidad si existe, si no la nacional
const tienePobEntidad = Object.keys(poblacionEntidad).length > 0;
const poblacionMap = tienePobEntidad ? poblacionEntidad : poblacionNacional;
if (entidadRes.error || !entidadRes.data) {
throw error(404, 'Entidad no encontrada');
}
......@@ -60,6 +82,8 @@ export async function load({ params }) {
codigoEntidadPadre: isDA ? entidadCode : null,
resumenData: resumenRes.data || [],
distribucionesData: distRes.data || [],
gestionInicial: ultimaGestion
gestionInicial: ultimaGestion,
poblacionMap,
tienePobEntidad
};
}
......
......@@ -10,6 +10,9 @@
let nombreDA = $derived(data.nombreDA);
let nombreEntidadMadre = $derived(data.nombreEntidadMadre);
let codigoEntidadPadre = $derived(data.codigoEntidadPadre);
let poblacionMap = $derived(data.poblacionMap);
let tienePobEntidad = $derived(data.tienePobEntidad);
let modoPerCapita = $state(false);
// Datos desde el loader (Supabase)
let codigoSeleccionado = $derived($page.params.codigo);
......@@ -137,22 +140,34 @@
}
// Datos derivados (ya filtrados por codigo desde el loader)
function aplicarPerCapita(rows) {
if (!modoPerCapita) return rows;
return rows.map(d => {
const pob = poblacionMap[d.gestion];
return { ...d, devengado: pob ? d.devengado / pob : 0 };
});
}
let historiaGastos = $derived(
aplicarPerCapita(
resumenData
.filter(d => d.tipo === 'gastos')
.filter(d => d.tipo === 'gastos' && d.gestion >= 2016)
.sort((a, b) => a.gestion - b.gestion)
)
);
let historiaIngresos = $derived(
aplicarPerCapita(
resumenData
.filter(d => d.tipo === 'ingresos')
.filter(d => d.tipo === 'ingresos' && d.gestion >= 2016)
.sort((a, b) => a.gestion - b.gestion)
)
);
let tieneIngresos = $derived(historiaIngresos.length > 0);
let gestiones = $derived(() => {
const years = [...new Set(resumenData.map(d => d.gestion))].sort();
const years = [...new Set(resumenData.map(d => d.gestion))].filter(g => g >= 2016).sort((a, b) => b - a);
return years;
});
......@@ -176,11 +191,12 @@
let distFiltradas = $derived(distribucionesData);
function prepararSegmentos(data) {
const pob = modoPerCapita ? poblacionMap[gestionSeleccionada] : null;
return data
.map(d => ({
codigo: d.hijo,
nombre: d.desc_hijo,
monto: d.devengado,
monto: pob ? d.devengado / pob : d.devengado,
padre: d.desc_padre
}))
.filter(d => d.monto > 0)
......@@ -231,6 +247,18 @@
return valor.toFixed(0);
}
function formatearPerCapita(valor) {
if (valor >= 1e6) return `${(valor / 1e6).toFixed(1)} millones`;
if (valor >= 1e4) return `${(valor / 1e3).toFixed(1)} mil`;
return Math.round(valor).toLocaleString('es-BO');
}
function fmt(valor) {
return modoPerCapita ? formatearPerCapita(valor) : formatearMonto(valor);
}
let unidadMonto = $derived(modoPerCapita ? 'Bs por persona al año' : 'de Bolivianos');
function formatearMontoCorto(valor) {
if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)}B`;
if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)}M`;
......@@ -551,7 +579,13 @@
<!-- Historia temporal (todos los años) -->
<section class="seccion">
<div class="seccion-titulo-row">
<h2 class="seccion-titulo">Historia</h2>
<div class="percapita-toggle">
<button class="percapita-btn" class:active={!modoPerCapita} onclick={() => { modoPerCapita = false; }}>Total</button>
<button class="percapita-btn" class:active={modoPerCapita} onclick={() => { modoPerCapita = true; }}>Per cápita</button>
</div>
</div>
<div class="historia-grid" class:single={!tieneIngresos}>
{#if tieneIngresos}
<div class="historia-card" bind:this={chartRefA}>
......@@ -560,9 +594,9 @@
{#if historiaIngresos.length > 0}
<span class="historia-monto ingresos">
{#if hoveredIngresos}
{hoveredIngresos.gestion} · {formatearMonto(hoveredIngresos.devengado)} de Bolivianos
{hoveredIngresos.gestion} · {fmt(hoveredIngresos.devengado)} {unidadMonto}
{:else}
{formatearMonto(historiaIngresos.reduce((s, d) => s + d.devengado, 0))} de Bolivianos
{fmt(historiaIngresos.reduce((s, d) => s + d.devengado, 0))} {unidadMonto}
{/if}
</span>
{/if}
......@@ -610,9 +644,9 @@
{#if historiaGastos.length > 0}
<span class="historia-monto gastos">
{#if hoveredGastos}
{hoveredGastos.gestion} · {formatearMonto(hoveredGastos.devengado)} de Bolivianos
{hoveredGastos.gestion} · {fmt(hoveredGastos.devengado)} {unidadMonto}
{:else}
{formatearMonto(historiaGastos.reduce((s, d) => s + d.devengado, 0))} de Bolivianos
{fmt(historiaGastos.reduce((s, d) => s + d.devengado, 0))} {unidadMonto}
{/if}
</span>
{/if}
......@@ -712,7 +746,7 @@
<div class="ranking-card">
<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>
<span class="ranking-pos-context">de <span class="ranking-total-num">{total}</span> instituciones · {r.posicion <= total / 2 ? 'entre las que más ingreso reciben/generan' : 'entre las que menos ingreso reciben/generan'}</span>
</div>
<div class="ranking-bars">
{#each Array(80) as _, i}
......@@ -745,7 +779,7 @@
<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-monto">{fmt(displayItem?.monto || 0)} {unidadMonto}</span>
<span class="clasificador-pct">{pct}%</span>
</div>
</div>
......@@ -775,7 +809,7 @@
<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-monto">{fmt(displayItem?.monto || 0)} {unidadMonto}</span>
<span class="clasificador-pct">{pct}%</span>
</div>
</div>
......@@ -802,6 +836,12 @@
font-family: var(--font-sans);
}
.dashboard > :not(.nav-spacer) {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
:global(html:not(.dark)) .dashboard {
background: #f5f5f7;
}
......@@ -1119,6 +1159,45 @@
margin-bottom: 2.5rem;
}
.seccion-titulo-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.seccion-titulo-row .seccion-titulo {
margin-bottom: 0;
}
.percapita-toggle {
display: flex;
gap: 2px;
background: rgba(255, 255, 255, 0.06);
border-radius: 8px;
padding: 3px;
flex-shrink: 0;
}
:global(html:not(.dark)) .percapita-toggle { background: rgba(0, 0, 0, 0.05); }
.percapita-btn {
padding: 5px 12px;
font-size: 0.75rem;
font-weight: 600;
font-family: var(--font-sans);
border: none;
border-radius: 6px;
cursor: pointer;
background: transparent;
color: var(--theme-texto);
opacity: 0.6;
transition: all 0.2s;
}
.percapita-btn:hover { opacity: 0.8; }
.percapita-btn.active { opacity: 1; background: rgba(255, 255, 255, 0.12); color: var(--theme-titulo); }
:global(html:not(.dark)) .percapita-btn.active { background: rgba(255, 255, 255, 0.8); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); }
.seccion-titulo {
font-size: 1.1rem;
font-weight: 600;
......
......@@ -1530,7 +1530,7 @@
display: grid;
grid-template-columns: 320px minmax(auto, 900px);
gap: 16px;
padding: 1rem 2rem 2rem;
padding: 3.5rem 2rem 2rem;
max-width: 1600px;
margin: 0 auto;
justify-content: center;
......
This diff is collapsed. Click to expand it.
import { supabase } from '$lib/supabase';
// Hardcoded a objeto 11700 (Sueldos) para testing de layout
const CODIGO = '11700';
function getGrupoCode(objeto) { return objeto.charAt(0) + '0000'; }
function getSubgrupoCode(objeto) { return objeto.substring(0, 2) + '000'; }
function getPartidaCode(objeto) { return objeto.substring(0, 3) + '00'; }
export async function load() {
const { data } = await supabase.schema('ppto').from('clas_objetos').select('*').eq('objeto', CODIGO);
const objeto = data[0];
const grupoCode = getGrupoCode(CODIGO);
const subgrupoCode = getSubgrupoCode(CODIGO);
const partidaCode = getPartidaCode(CODIGO);
const { data: padresData } = await supabase.schema('ppto').from('clas_objetos').select('*').in('objeto', [grupoCode, subgrupoCode, partidaCode]).order('objeto');
const padres = padresData || [];
// Contar entidades
const nEntidades = objeto.n_entidades || 0;
return { objeto, padres, hijos: [], nEntidades };
}
This diff is collapsed. Click to expand it.
......@@ -18,7 +18,7 @@ export async function load({ fetch }) {
const poblacionMap = {};
pobRes.split('\n').slice(1).forEach(line => {
const [cod, gestion, pob] = line.split(',');
const [cod, , gestion, pob] = line.split(',');
if (parseInt(gestion) === 2025) {
poblacionMap[cod] = parseInt(pob);
}
......
......@@ -223,7 +223,7 @@
}
const map = {};
pobCSV.split('\n').slice(1).forEach(line => {
const [cod, g, pob] = line.split(',');
const [cod, , g, pob] = line.split(',');
if (parseInt(g) === gestion) {
map[cod] = parseInt(pob);
}
......
......@@ -19,10 +19,10 @@ export async function load({ params, fetch }) {
throw error(404, 'Ubicación no encontrada');
}
// Parsear población para este código
// Parsear población para este código (codigo_ine)
const poblacionMap = {};
pobRes.split('\n').slice(1).forEach(line => {
const [cod, gestion, pob] = line.split(',');
const [cod, , gestion, pob] = line.split(',');
if (cod === codigo) {
poblacionMap[parseInt(gestion)] = parseInt(pob);
}
......
......@@ -246,7 +246,7 @@
let tieneIngresos = $derived(historiaIngresos.length > 0);
let gestiones = $derived(() => {
const years = [...new Set(resumenData.map(d => d.gestion))].filter(g => poblacionMap[g]).sort();
const years = [...new Set(resumenData.map(d => d.gestion))].filter(g => poblacionMap[g]).sort((a, b) => b - a);
return years;
});
......
This diff could not be displayed because it is too large.