Rafael Lopez

ubigeo

This diff is collapsed. Click to expand it.
1 // Proxy para evitar CORS 1 // Proxy para evitar CORS
2 -const API_BASE = 'http://34.171.18.2/api/proyecto/search'; 2 +const API_BASE = 'http://136.112.29.74/api/proyecto/search';
3 3
4 export async function GET({ url }) { 4 export async function GET({ url }) {
5 const q = url.searchParams.get('q') || ''; 5 const q = url.searchParams.get('q') || '';
......
1 +const API_BASE = 'http://136.112.29.74/api/ubigeo/clasificador';
2 +
3 +export async function GET() {
4 + try {
5 + const response = await fetch(API_BASE);
6 + const data = await response.json();
7 + return new Response(JSON.stringify(data), {
8 + headers: { 'Content-Type': 'application/json' }
9 + });
10 + } catch (err) {
11 + return new Response(JSON.stringify({ error: 'Failed to fetch' }), {
12 + status: 500,
13 + headers: { 'Content-Type': 'application/json' }
14 + });
15 + }
16 +}
1 +export async function load({ fetch }) {
2 + const res = await fetch('/api/ubigeo-clasificador');
3 + const data = await res.json();
4 + return { clasificador: data };
5 +}
This diff is collapsed. Click to expand it.
1 import { supabase } from '$lib/supabase'; 1 import { supabase } from '$lib/supabase';
2 import { error } from '@sveltejs/kit'; 2 import { error } from '@sveltejs/kit';
3 3
4 -export async function load({ params }) { 4 +export async function load({ params, fetch }) {
5 const codigo = params.codigo; 5 const codigo = params.codigo;
6 const isDA = codigo.includes('.'); 6 const isDA = codigo.includes('.');
7 const entidadCode = isDA ? codigo.split('.')[0] : codigo; 7 const entidadCode = isDA ? codigo.split('.')[0] : codigo;
...@@ -11,8 +11,8 @@ export async function load({ params }) { ...@@ -11,8 +11,8 @@ export async function load({ params }) {
11 throw error(400, 'Código de entidad inválido'); 11 throw error(400, 'Código de entidad inválido');
12 } 12 }
13 13
14 - // Cargar metadata y resumen en paralelo 14 + // Cargar metadata, resumen y población en paralelo
15 - const [entidadRes, resumenRes] = await Promise.all([ 15 + const [entidadRes, resumenRes, pobRes] = await Promise.all([
16 supabase 16 supabase
17 .schema('ppto') 17 .schema('ppto')
18 .from('clas_institucional') 18 .from('clas_institucional')
...@@ -24,9 +24,31 @@ export async function load({ params }) { ...@@ -24,9 +24,31 @@ export async function load({ params }) {
24 .schema('ppto') 24 .schema('ppto')
25 .from('entidad_resumen') 25 .from('entidad_resumen')
26 .select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking') 26 .select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking')
27 - .eq('codigo', codigo) 27 + .eq('codigo', codigo),
28 +
29 + fetch('/poblacion.csv').then(r => r.text())
28 ]); 30 ]);
29 31
32 + // Buscar población: primero por entidad, si no suma nacional
33 + const poblacionEntidad = {};
34 + const poblacionNacional = {};
35 + pobRes.split('\n').slice(1).forEach(line => {
36 + const [, ent, gestion, pob] = line.split(',');
37 + const g = parseInt(gestion);
38 + const p = parseInt(pob);
39 + if (!g || !p) return;
40 + // Suma nacional
41 + poblacionNacional[g] = (poblacionNacional[g] || 0) + p;
42 + // Por entidad (código sin DA)
43 + if (ent === entidadCode) {
44 + poblacionEntidad[g] = (poblacionEntidad[g] || 0) + p;
45 + }
46 + });
47 +
48 + // Usar población de la entidad si existe, si no la nacional
49 + const tienePobEntidad = Object.keys(poblacionEntidad).length > 0;
50 + const poblacionMap = tienePobEntidad ? poblacionEntidad : poblacionNacional;
51 +
30 if (entidadRes.error || !entidadRes.data) { 52 if (entidadRes.error || !entidadRes.data) {
31 throw error(404, 'Entidad no encontrada'); 53 throw error(404, 'Entidad no encontrada');
32 } 54 }
...@@ -60,6 +82,8 @@ export async function load({ params }) { ...@@ -60,6 +82,8 @@ export async function load({ params }) {
60 codigoEntidadPadre: isDA ? entidadCode : null, 82 codigoEntidadPadre: isDA ? entidadCode : null,
61 resumenData: resumenRes.data || [], 83 resumenData: resumenRes.data || [],
62 distribucionesData: distRes.data || [], 84 distribucionesData: distRes.data || [],
63 - gestionInicial: ultimaGestion 85 + gestionInicial: ultimaGestion,
86 + poblacionMap,
87 + tienePobEntidad
64 }; 88 };
65 } 89 }
......
...@@ -10,6 +10,9 @@ ...@@ -10,6 +10,9 @@
10 let nombreDA = $derived(data.nombreDA); 10 let nombreDA = $derived(data.nombreDA);
11 let nombreEntidadMadre = $derived(data.nombreEntidadMadre); 11 let nombreEntidadMadre = $derived(data.nombreEntidadMadre);
12 let codigoEntidadPadre = $derived(data.codigoEntidadPadre); 12 let codigoEntidadPadre = $derived(data.codigoEntidadPadre);
13 + let poblacionMap = $derived(data.poblacionMap);
14 + let tienePobEntidad = $derived(data.tienePobEntidad);
15 + let modoPerCapita = $state(false);
13 16
14 // Datos desde el loader (Supabase) 17 // Datos desde el loader (Supabase)
15 let codigoSeleccionado = $derived($page.params.codigo); 18 let codigoSeleccionado = $derived($page.params.codigo);
...@@ -137,22 +140,34 @@ ...@@ -137,22 +140,34 @@
137 } 140 }
138 141
139 // Datos derivados (ya filtrados por codigo desde el loader) 142 // Datos derivados (ya filtrados por codigo desde el loader)
143 + function aplicarPerCapita(rows) {
144 + if (!modoPerCapita) return rows;
145 + return rows.map(d => {
146 + const pob = poblacionMap[d.gestion];
147 + return { ...d, devengado: pob ? d.devengado / pob : 0 };
148 + });
149 + }
150 +
140 let historiaGastos = $derived( 151 let historiaGastos = $derived(
141 - resumenData 152 + aplicarPerCapita(
142 - .filter(d => d.tipo === 'gastos') 153 + resumenData
143 - .sort((a, b) => a.gestion - b.gestion) 154 + .filter(d => d.tipo === 'gastos' && d.gestion >= 2016)
155 + .sort((a, b) => a.gestion - b.gestion)
156 + )
144 ); 157 );
145 158
146 let historiaIngresos = $derived( 159 let historiaIngresos = $derived(
147 - resumenData 160 + aplicarPerCapita(
148 - .filter(d => d.tipo === 'ingresos') 161 + resumenData
149 - .sort((a, b) => a.gestion - b.gestion) 162 + .filter(d => d.tipo === 'ingresos' && d.gestion >= 2016)
163 + .sort((a, b) => a.gestion - b.gestion)
164 + )
150 ); 165 );
151 166
152 let tieneIngresos = $derived(historiaIngresos.length > 0); 167 let tieneIngresos = $derived(historiaIngresos.length > 0);
153 168
154 let gestiones = $derived(() => { 169 let gestiones = $derived(() => {
155 - const years = [...new Set(resumenData.map(d => d.gestion))].sort(); 170 + const years = [...new Set(resumenData.map(d => d.gestion))].filter(g => g >= 2016).sort((a, b) => b - a);
156 return years; 171 return years;
157 }); 172 });
158 173
...@@ -176,11 +191,12 @@ ...@@ -176,11 +191,12 @@
176 let distFiltradas = $derived(distribucionesData); 191 let distFiltradas = $derived(distribucionesData);
177 192
178 function prepararSegmentos(data) { 193 function prepararSegmentos(data) {
194 + const pob = modoPerCapita ? poblacionMap[gestionSeleccionada] : null;
179 return data 195 return data
180 .map(d => ({ 196 .map(d => ({
181 codigo: d.hijo, 197 codigo: d.hijo,
182 nombre: d.desc_hijo, 198 nombre: d.desc_hijo,
183 - monto: d.devengado, 199 + monto: pob ? d.devengado / pob : d.devengado,
184 padre: d.desc_padre 200 padre: d.desc_padre
185 })) 201 }))
186 .filter(d => d.monto > 0) 202 .filter(d => d.monto > 0)
...@@ -231,6 +247,18 @@ ...@@ -231,6 +247,18 @@
231 return valor.toFixed(0); 247 return valor.toFixed(0);
232 } 248 }
233 249
250 + function formatearPerCapita(valor) {
251 + if (valor >= 1e6) return `${(valor / 1e6).toFixed(1)} millones`;
252 + if (valor >= 1e4) return `${(valor / 1e3).toFixed(1)} mil`;
253 + return Math.round(valor).toLocaleString('es-BO');
254 + }
255 +
256 + function fmt(valor) {
257 + return modoPerCapita ? formatearPerCapita(valor) : formatearMonto(valor);
258 + }
259 +
260 + let unidadMonto = $derived(modoPerCapita ? 'Bs por persona al año' : 'de Bolivianos');
261 +
234 function formatearMontoCorto(valor) { 262 function formatearMontoCorto(valor) {
235 if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)}B`; 263 if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)}B`;
236 if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)}M`; 264 if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)}M`;
...@@ -551,7 +579,13 @@ ...@@ -551,7 +579,13 @@
551 579
552 <!-- Historia temporal (todos los años) --> 580 <!-- Historia temporal (todos los años) -->
553 <section class="seccion"> 581 <section class="seccion">
554 - <h2 class="seccion-titulo">Historia</h2> 582 + <div class="seccion-titulo-row">
583 + <h2 class="seccion-titulo">Historia</h2>
584 + <div class="percapita-toggle">
585 + <button class="percapita-btn" class:active={!modoPerCapita} onclick={() => { modoPerCapita = false; }}>Total</button>
586 + <button class="percapita-btn" class:active={modoPerCapita} onclick={() => { modoPerCapita = true; }}>Per cápita</button>
587 + </div>
588 + </div>
555 <div class="historia-grid" class:single={!tieneIngresos}> 589 <div class="historia-grid" class:single={!tieneIngresos}>
556 {#if tieneIngresos} 590 {#if tieneIngresos}
557 <div class="historia-card" bind:this={chartRefA}> 591 <div class="historia-card" bind:this={chartRefA}>
...@@ -560,9 +594,9 @@ ...@@ -560,9 +594,9 @@
560 {#if historiaIngresos.length > 0} 594 {#if historiaIngresos.length > 0}
561 <span class="historia-monto ingresos"> 595 <span class="historia-monto ingresos">
562 {#if hoveredIngresos} 596 {#if hoveredIngresos}
563 - {hoveredIngresos.gestion} · {formatearMonto(hoveredIngresos.devengado)} de Bolivianos 597 + {hoveredIngresos.gestion} · {fmt(hoveredIngresos.devengado)} {unidadMonto}
564 {:else} 598 {:else}
565 - {formatearMonto(historiaIngresos.reduce((s, d) => s + d.devengado, 0))} de Bolivianos 599 + {fmt(historiaIngresos.reduce((s, d) => s + d.devengado, 0))} {unidadMonto}
566 {/if} 600 {/if}
567 </span> 601 </span>
568 {/if} 602 {/if}
...@@ -610,9 +644,9 @@ ...@@ -610,9 +644,9 @@
610 {#if historiaGastos.length > 0} 644 {#if historiaGastos.length > 0}
611 <span class="historia-monto gastos"> 645 <span class="historia-monto gastos">
612 {#if hoveredGastos} 646 {#if hoveredGastos}
613 - {hoveredGastos.gestion} · {formatearMonto(hoveredGastos.devengado)} de Bolivianos 647 + {hoveredGastos.gestion} · {fmt(hoveredGastos.devengado)} {unidadMonto}
614 {:else} 648 {:else}
615 - {formatearMonto(historiaGastos.reduce((s, d) => s + d.devengado, 0))} de Bolivianos 649 + {fmt(historiaGastos.reduce((s, d) => s + d.devengado, 0))} {unidadMonto}
616 {/if} 650 {/if}
617 </span> 651 </span>
618 {/if} 652 {/if}
...@@ -712,7 +746,7 @@ ...@@ -712,7 +746,7 @@
712 <div class="ranking-card"> 746 <div class="ranking-card">
713 <div class="ranking-headline"> 747 <div class="ranking-headline">
714 <span class="ranking-pos-big">#{r.posicion}</span> 748 <span class="ranking-pos-big">#{r.posicion}</span>
715 - <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> 749 + <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>
716 </div> 750 </div>
717 <div class="ranking-bars"> 751 <div class="ranking-bars">
718 {#each Array(80) as _, i} 752 {#each Array(80) as _, i}
...@@ -745,7 +779,7 @@ ...@@ -745,7 +779,7 @@
745 <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> 779 <span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
746 </div> 780 </div>
747 <div class="clasificador-valores"> 781 <div class="clasificador-valores">
748 - <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span> 782 + <span class="clasificador-monto">{fmt(displayItem?.monto || 0)} {unidadMonto}</span>
749 <span class="clasificador-pct">{pct}%</span> 783 <span class="clasificador-pct">{pct}%</span>
750 </div> 784 </div>
751 </div> 785 </div>
...@@ -775,7 +809,7 @@ ...@@ -775,7 +809,7 @@
775 <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> 809 <span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
776 </div> 810 </div>
777 <div class="clasificador-valores"> 811 <div class="clasificador-valores">
778 - <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span> 812 + <span class="clasificador-monto">{fmt(displayItem?.monto || 0)} {unidadMonto}</span>
779 <span class="clasificador-pct">{pct}%</span> 813 <span class="clasificador-pct">{pct}%</span>
780 </div> 814 </div>
781 </div> 815 </div>
...@@ -802,6 +836,12 @@ ...@@ -802,6 +836,12 @@
802 font-family: var(--font-sans); 836 font-family: var(--font-sans);
803 } 837 }
804 838
839 + .dashboard > :not(.nav-spacer) {
840 + max-width: 1200px;
841 + margin-left: auto;
842 + margin-right: auto;
843 + }
844 +
805 :global(html:not(.dark)) .dashboard { 845 :global(html:not(.dark)) .dashboard {
806 background: #f5f5f7; 846 background: #f5f5f7;
807 } 847 }
...@@ -1119,6 +1159,45 @@ ...@@ -1119,6 +1159,45 @@
1119 margin-bottom: 2.5rem; 1159 margin-bottom: 2.5rem;
1120 } 1160 }
1121 1161
1162 + .seccion-titulo-row {
1163 + display: flex;
1164 + align-items: center;
1165 + justify-content: space-between;
1166 + gap: 1rem;
1167 + margin-bottom: 1rem;
1168 + }
1169 +
1170 + .seccion-titulo-row .seccion-titulo {
1171 + margin-bottom: 0;
1172 + }
1173 +
1174 + .percapita-toggle {
1175 + display: flex;
1176 + gap: 2px;
1177 + background: rgba(255, 255, 255, 0.06);
1178 + border-radius: 8px;
1179 + padding: 3px;
1180 + flex-shrink: 0;
1181 + }
1182 + :global(html:not(.dark)) .percapita-toggle { background: rgba(0, 0, 0, 0.05); }
1183 +
1184 + .percapita-btn {
1185 + padding: 5px 12px;
1186 + font-size: 0.75rem;
1187 + font-weight: 600;
1188 + font-family: var(--font-sans);
1189 + border: none;
1190 + border-radius: 6px;
1191 + cursor: pointer;
1192 + background: transparent;
1193 + color: var(--theme-texto);
1194 + opacity: 0.6;
1195 + transition: all 0.2s;
1196 + }
1197 + .percapita-btn:hover { opacity: 0.8; }
1198 + .percapita-btn.active { opacity: 1; background: rgba(255, 255, 255, 0.12); color: var(--theme-titulo); }
1199 + :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); }
1200 +
1122 .seccion-titulo { 1201 .seccion-titulo {
1123 font-size: 1.1rem; 1202 font-size: 1.1rem;
1124 font-weight: 600; 1203 font-weight: 600;
......
...@@ -1530,7 +1530,7 @@ ...@@ -1530,7 +1530,7 @@
1530 display: grid; 1530 display: grid;
1531 grid-template-columns: 320px minmax(auto, 900px); 1531 grid-template-columns: 320px minmax(auto, 900px);
1532 gap: 16px; 1532 gap: 16px;
1533 - padding: 1rem 2rem 2rem; 1533 + padding: 3.5rem 2rem 2rem;
1534 max-width: 1600px; 1534 max-width: 1600px;
1535 margin: 0 auto; 1535 margin: 0 auto;
1536 justify-content: center; 1536 justify-content: center;
......
This diff is collapsed. Click to expand it.
1 +import { supabase } from '$lib/supabase';
2 +
3 +// Hardcoded a objeto 11700 (Sueldos) para testing de layout
4 +const CODIGO = '11700';
5 +
6 +function getGrupoCode(objeto) { return objeto.charAt(0) + '0000'; }
7 +function getSubgrupoCode(objeto) { return objeto.substring(0, 2) + '000'; }
8 +function getPartidaCode(objeto) { return objeto.substring(0, 3) + '00'; }
9 +
10 +export async function load() {
11 + const { data } = await supabase.schema('ppto').from('clas_objetos').select('*').eq('objeto', CODIGO);
12 + const objeto = data[0];
13 +
14 + const grupoCode = getGrupoCode(CODIGO);
15 + const subgrupoCode = getSubgrupoCode(CODIGO);
16 + const partidaCode = getPartidaCode(CODIGO);
17 +
18 + const { data: padresData } = await supabase.schema('ppto').from('clas_objetos').select('*').in('objeto', [grupoCode, subgrupoCode, partidaCode]).order('objeto');
19 + const padres = padresData || [];
20 +
21 + // Contar entidades
22 + const nEntidades = objeto.n_entidades || 0;
23 +
24 + return { objeto, padres, hijos: [], nEntidades };
25 +}
This diff is collapsed. Click to expand it.
...@@ -18,7 +18,7 @@ export async function load({ fetch }) { ...@@ -18,7 +18,7 @@ export async function load({ fetch }) {
18 18
19 const poblacionMap = {}; 19 const poblacionMap = {};
20 pobRes.split('\n').slice(1).forEach(line => { 20 pobRes.split('\n').slice(1).forEach(line => {
21 - const [cod, gestion, pob] = line.split(','); 21 + const [cod, , gestion, pob] = line.split(',');
22 if (parseInt(gestion) === 2025) { 22 if (parseInt(gestion) === 2025) {
23 poblacionMap[cod] = parseInt(pob); 23 poblacionMap[cod] = parseInt(pob);
24 } 24 }
......
...@@ -223,7 +223,7 @@ ...@@ -223,7 +223,7 @@
223 } 223 }
224 const map = {}; 224 const map = {};
225 pobCSV.split('\n').slice(1).forEach(line => { 225 pobCSV.split('\n').slice(1).forEach(line => {
226 - const [cod, g, pob] = line.split(','); 226 + const [cod, , g, pob] = line.split(',');
227 if (parseInt(g) === gestion) { 227 if (parseInt(g) === gestion) {
228 map[cod] = parseInt(pob); 228 map[cod] = parseInt(pob);
229 } 229 }
......
...@@ -19,10 +19,10 @@ export async function load({ params, fetch }) { ...@@ -19,10 +19,10 @@ export async function load({ params, fetch }) {
19 throw error(404, 'Ubicación no encontrada'); 19 throw error(404, 'Ubicación no encontrada');
20 } 20 }
21 21
22 - // Parsear población para este código 22 + // Parsear población para este código (codigo_ine)
23 const poblacionMap = {}; 23 const poblacionMap = {};
24 pobRes.split('\n').slice(1).forEach(line => { 24 pobRes.split('\n').slice(1).forEach(line => {
25 - const [cod, gestion, pob] = line.split(','); 25 + const [cod, , gestion, pob] = line.split(',');
26 if (cod === codigo) { 26 if (cod === codigo) {
27 poblacionMap[parseInt(gestion)] = parseInt(pob); 27 poblacionMap[parseInt(gestion)] = parseInt(pob);
28 } 28 }
......
...@@ -246,7 +246,7 @@ ...@@ -246,7 +246,7 @@
246 let tieneIngresos = $derived(historiaIngresos.length > 0); 246 let tieneIngresos = $derived(historiaIngresos.length > 0);
247 247
248 let gestiones = $derived(() => { 248 let gestiones = $derived(() => {
249 - const years = [...new Set(resumenData.map(d => d.gestion))].filter(g => poblacionMap[g]).sort(); 249 + const years = [...new Set(resumenData.map(d => d.gestion))].filter(g => poblacionMap[g]).sort((a, b) => b - a);
250 return years; 250 return years;
251 }); 251 });
252 252
......
This diff could not be displayed because it is too large.