Rafael Lopez

ubigeo

This diff is collapsed. Click to expand it.
1 import { writable } from 'svelte/store'; 1 import { writable } from 'svelte/store';
2 2
3 // Persiste el estado de búsqueda del landing entre navegaciones 3 // Persiste el estado de búsqueda del landing entre navegaciones
4 -export const landingSearchMode = writable('programas'); // 'programas' | 'clasificadores' 4 +export const landingSearchMode = writable('todo'); // 'todo' | 'programas' | 'clasificadores'
5 export const landingSelectedClassifiers = writable([]); // ['entidad', 'objeto', ...] 5 export const landingSelectedClassifiers = writable([]); // ['entidad', 'objeto', ...]
6 export const landingSearchQuery = writable(''); 6 export const landingSearchQuery = writable('');
......
1 +import { writable } from 'svelte/store';
2 +
3 +// Cache global del mapa - se carga una sola vez en toda la sesión
4 +export const mapaCache = writable(null);
This diff is collapsed. Click to expand it.
1 // Proxy para evitar CORS 1 // Proxy para evitar CORS
2 -const API_BASE = 'http://34.66.240.236/api/proyecto/search'; 2 +const API_BASE = 'http://34.171.18.2/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') || '';
6 - const is_class = url.searchParams.get('is_class') || 'false'; 6 + const is_class = url.searchParams.get('is_class'); // null si no viene
7 - const mode = url.searchParams.get('mode') || 'gastos'; // 'gastos' | 'ingresos' 7 + const mode = url.searchParams.get('mode') || '';
8 - const class_ = url.searchParams.get('class_') || ''; // comma-separated: entidad, objeto, finfun, acteco 8 + const class_ = url.searchParams.get('class_') || '';
9 - const page = url.searchParams.get('page') || '1'; // Paginación 9 + const page = url.searchParams.get('page') || '1';
10 - const per_page = url.searchParams.get('per_page') || ''; // Resultados por página 10 + const per_page = url.searchParams.get('per_page') || '';
11 11
12 - // Build URL with all parameters
13 const apiUrl = new URL(API_BASE); 12 const apiUrl = new URL(API_BASE);
14 apiUrl.searchParams.set('q', q); 13 apiUrl.searchParams.set('q', q);
15 - apiUrl.searchParams.set('is_class', is_class);
16 apiUrl.searchParams.set('page', page); 14 apiUrl.searchParams.set('page', page);
15 +
16 + // Solo enviar is_class si viene explícitamente
17 + if (is_class !== null) {
18 + apiUrl.searchParams.set('is_class', is_class);
19 + }
17 if (per_page) apiUrl.searchParams.set('per_page', per_page); 20 if (per_page) apiUrl.searchParams.set('per_page', per_page);
18 if (mode) apiUrl.searchParams.set('mode', mode); 21 if (mode) apiUrl.searchParams.set('mode', mode);
19 if (class_) apiUrl.searchParams.set('class_', class_); 22 if (class_) apiUrl.searchParams.set('class_', class_);
......
...@@ -5,21 +5,29 @@ ...@@ -5,21 +5,29 @@
5 import { supabase } from '$lib/supabase'; 5 import { supabase } from '$lib/supabase';
6 6
7 let { data } = $props(); 7 let { data } = $props();
8 - const entidad = data.entidad; 8 + let entidad = $derived(data.entidad);
9 - const isDA = data.isDA; 9 + let isDA = $derived(data.isDA);
10 - const nombreDA = data.nombreDA; 10 + let nombreDA = $derived(data.nombreDA);
11 - const nombreEntidadMadre = data.nombreEntidadMadre; 11 + let nombreEntidadMadre = $derived(data.nombreEntidadMadre);
12 - const codigoEntidadPadre = data.codigoEntidadPadre; 12 + let codigoEntidadPadre = $derived(data.codigoEntidadPadre);
13 13
14 // Datos desde el loader (Supabase) 14 // Datos desde el loader (Supabase)
15 let codigoSeleccionado = $derived($page.params.codigo); 15 let codigoSeleccionado = $derived($page.params.codigo);
16 let gestionSeleccionada = $state(data.gestionInicial); 16 let gestionSeleccionada = $state(data.gestionInicial);
17 - let resumenData = $state(data.resumenData); 17 + let resumenData = $derived(data.resumenData);
18 let distribucionesData = $state(data.distribucionesData); 18 let distribucionesData = $state(data.distribucionesData);
19 let cargando = $state(false); 19 let cargando = $state(false);
20 let distCache = $state({ [data.gestionInicial]: data.distribucionesData }); 20 let distCache = $state({ [data.gestionInicial]: data.distribucionesData });
21 let cargandoDist = $state(false); 21 let cargandoDist = $state(false);
22 22
23 + // Resetear al cambiar de entidad
24 + $effect(() => {
25 + const g = data.gestionInicial;
26 + gestionSeleccionada = g;
27 + distribucionesData = data.distribucionesData;
28 + distCache = { [g]: data.distribucionesData };
29 + });
30 +
23 async function cargarDistribuciones(gestion) { 31 async function cargarDistribuciones(gestion) {
24 if (distCache[gestion]) { 32 if (distCache[gestion]) {
25 distribucionesData = distCache[gestion]; 33 distribucionesData = distCache[gestion];
...@@ -148,14 +156,21 @@ ...@@ -148,14 +156,21 @@
148 return years; 156 return years;
149 }); 157 });
150 158
159 + function parseRanking(rankingStr) {
160 + if (!rankingStr) return null;
161 + const parts = String(rankingStr).split('/');
162 + if (parts.length !== 2) return null;
163 + return { posicion: parseInt(parts[0]), total: parseInt(parts[1]) };
164 + }
165 +
151 let rankingGastos = $derived(() => { 166 let rankingGastos = $derived(() => {
152 const row = historiaGastos.find(d => d.gestion === gestionSeleccionada); 167 const row = historiaGastos.find(d => d.gestion === gestionSeleccionada);
153 - return row ? { posicion: row.ranking, total: '~600' } : null; 168 + return row ? parseRanking(row.ranking) : null;
154 }); 169 });
155 170
156 let rankingIngresos = $derived(() => { 171 let rankingIngresos = $derived(() => {
157 const row = historiaIngresos.find(d => d.gestion === gestionSeleccionada); 172 const row = historiaIngresos.find(d => d.gestion === gestionSeleccionada);
158 - return row ? { posicion: row.ranking, total: '~600' } : null; 173 + return row ? parseRanking(row.ranking) : null;
159 }); 174 });
160 175
161 let distFiltradas = $derived(distribucionesData); 176 let distFiltradas = $derived(distribucionesData);
...@@ -672,34 +687,42 @@ ...@@ -672,34 +687,42 @@
672 {#if rankingGastos()} 687 {#if rankingGastos()}
673 {@const r = rankingGastos()} 688 {@const r = rankingGastos()}
674 {@const total = parseInt(r.total) || 600} 689 {@const total = parseInt(r.total) || 600}
675 - {@const barIdx = Math.max(0, Math.min(79, Math.round((r.posicion / total) * 80)))} 690 + {@const barIdx = Math.max(0, Math.min(79, 80 - Math.round((r.posicion / total) * 80)))}
676 <div class="ranking-card"> 691 <div class="ranking-card">
677 - <span class="ranking-card-label">Ranking en gasto</span> 692 + <div class="ranking-headline">
693 + <span class="ranking-pos-big">#{r.posicion}</span>
694 + <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>
695 + </div>
678 <div class="ranking-bars"> 696 <div class="ranking-bars">
679 {#each Array(80) as _, i} 697 {#each Array(80) as _, i}
680 <div class="ranking-bar-tick"></div> 698 <div class="ranking-bar-tick"></div>
681 {/each} 699 {/each}
682 <div class="ranking-marker" style="left: {barIdx * 1.25}%"></div> 700 <div class="ranking-marker" style="left: {barIdx * 1.25}%"></div>
683 </div> 701 </div>
684 - <div class="ranking-caption"> 702 + <div class="ranking-extremos">
685 - <span class="ranking-pos">#{r.posicion}</span> de {total} entidades 703 + <span class="ranking-extremo">Menos gasto</span>
704 + <span class="ranking-extremo">Más gasto</span>
686 </div> 705 </div>
687 </div> 706 </div>
688 {/if} 707 {/if}
689 {#if tieneIngresos && rankingIngresos()} 708 {#if tieneIngresos && rankingIngresos()}
690 {@const r = rankingIngresos()} 709 {@const r = rankingIngresos()}
691 {@const total = parseInt(r.total) || 600} 710 {@const total = parseInt(r.total) || 600}
692 - {@const barIdx = Math.max(0, Math.min(79, Math.round((r.posicion / total) * 80)))} 711 + {@const barIdx = Math.max(0, Math.min(79, 80 - Math.round((r.posicion / total) * 80)))}
693 <div class="ranking-card"> 712 <div class="ranking-card">
694 - <span class="ranking-card-label">Ranking en ingresos</span> 713 + <div class="ranking-headline">
714 + <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>
716 + </div>
695 <div class="ranking-bars"> 717 <div class="ranking-bars">
696 {#each Array(80) as _, i} 718 {#each Array(80) as _, i}
697 <div class="ranking-bar-tick"></div> 719 <div class="ranking-bar-tick"></div>
698 {/each} 720 {/each}
699 <div class="ranking-marker" style="left: {barIdx * 1.25}%"></div> 721 <div class="ranking-marker" style="left: {barIdx * 1.25}%"></div>
700 </div> 722 </div>
701 - <div class="ranking-caption"> 723 + <div class="ranking-extremos">
702 - <span class="ranking-pos">#{r.posicion}</span> de {total} entidades 724 + <span class="ranking-extremo">Menos ingresos</span>
725 + <span class="ranking-extremo">Más ingresos</span>
703 </div> 726 </div>
704 </div> 727 </div>
705 {/if} 728 {/if}
...@@ -712,13 +735,20 @@ ...@@ -712,13 +735,20 @@
712 <div class="clasificadores-grid"> 735 <div class="clasificadores-grid">
713 {#each clasificadoresGasto as { key, label, data }} 736 {#each clasificadoresGasto as { key, label, data }}
714 {@const displayItem = getDisplayItem(key, data)} 737 {@const displayItem = getDisplayItem(key, data)}
738 + {@const totalClasif = getTotal(data)}
739 + {@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0}
715 <div class="clasificador-card"> 740 <div class="clasificador-card">
716 <div class="clasificador-header"> 741 <div class="clasificador-header">
717 <div class="clasificador-info"> 742 <div class="clasificador-info">
718 - <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span> 743 + <div class="clasificador-nombres">
719 <span class="clasificador-padre">{displayItem?.padre || ''}</span> 744 <span class="clasificador-padre">{displayItem?.padre || ''}</span>
720 <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> 745 <span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
721 </div> 746 </div>
747 + <div class="clasificador-valores">
748 + <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
749 + <span class="clasificador-pct">{pct}%</span>
750 + </div>
751 + </div>
722 <span class="clasificador-label">{label}</span> 752 <span class="clasificador-label">{label}</span>
723 </div> 753 </div>
724 <div class="barra-container" bind:this={barContainers[key]}></div> 754 <div class="barra-container" bind:this={barContainers[key]}></div>
...@@ -735,13 +765,20 @@ ...@@ -735,13 +765,20 @@
735 <div class="clasificadores-grid"> 765 <div class="clasificadores-grid">
736 {#each clasificadoresIngreso as { key, label, data }} 766 {#each clasificadoresIngreso as { key, label, data }}
737 {@const displayItem = getDisplayItem(key, data)} 767 {@const displayItem = getDisplayItem(key, data)}
768 + {@const totalClasif = getTotal(data)}
769 + {@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0}
738 <div class="clasificador-card"> 770 <div class="clasificador-card">
739 <div class="clasificador-header"> 771 <div class="clasificador-header">
740 <div class="clasificador-info"> 772 <div class="clasificador-info">
741 - <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span> 773 + <div class="clasificador-nombres">
742 <span class="clasificador-padre">{displayItem?.padre || ''}</span> 774 <span class="clasificador-padre">{displayItem?.padre || ''}</span>
743 <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> 775 <span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
744 </div> 776 </div>
777 + <div class="clasificador-valores">
778 + <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
779 + <span class="clasificador-pct">{pct}%</span>
780 + </div>
781 + </div>
745 <span class="clasificador-label">{label}</span> 782 <span class="clasificador-label">{label}</span>
746 </div> 783 </div>
747 <div class="barra-container" bind:this={barContainers[key]}></div> 784 <div class="barra-container" bind:this={barContainers[key]}></div>
...@@ -1039,14 +1076,39 @@ ...@@ -1039,14 +1076,39 @@
1039 background: #D4A574; 1076 background: #D4A574;
1040 } 1077 }
1041 1078
1042 - .ranking-caption { 1079 + .ranking-extremos {
1043 - margin-top: 0.35rem; 1080 + display: flex;
1044 - font-size: 0.7rem; 1081 + justify-content: space-between;
1082 + margin-top: 0.25rem;
1083 + }
1084 +
1085 + .ranking-extremo {
1086 + font-size: 0.6rem;
1045 color: var(--theme-texto); 1087 color: var(--theme-texto);
1046 - opacity: 0.6; 1088 + opacity: 0.4;
1089 + }
1090 +
1091 + .ranking-headline {
1092 + display: flex;
1093 + align-items: baseline;
1094 + gap: 0.5rem;
1095 + margin-bottom: 0.75rem;
1047 } 1096 }
1048 1097
1049 - .ranking-pos { 1098 + .ranking-pos-big {
1099 + font-size: 1.5rem;
1100 + font-weight: 700;
1101 + color: var(--theme-titulo);
1102 + line-height: 1;
1103 + }
1104 +
1105 + .ranking-pos-context {
1106 + font-size: 0.8rem;
1107 + color: var(--theme-texto);
1108 + opacity: 0.7;
1109 + }
1110 +
1111 + .ranking-total-num {
1050 font-weight: 700; 1112 font-weight: 700;
1051 color: var(--theme-titulo); 1113 color: var(--theme-titulo);
1052 opacity: 1; 1114 opacity: 1;
...@@ -1150,7 +1212,7 @@ ...@@ -1150,7 +1212,7 @@
1150 1212
1151 :global(html:not(.dark)) .gestion-option.active { 1213 :global(html:not(.dark)) .gestion-option.active {
1152 background: rgba(90, 157, 191, 0.1); 1214 background: rgba(90, 157, 191, 0.1);
1153 - color: #5A9DBF; 1215 + color: #3D7A9C;
1154 } 1216 }
1155 1217
1156 .seccion-sub { 1218 .seccion-sub {
...@@ -1230,7 +1292,7 @@ ...@@ -1230,7 +1292,7 @@
1230 1292
1231 .historia-monto.ingresos, 1293 .historia-monto.ingresos,
1232 .historia-monto.gastos { 1294 .historia-monto.gastos {
1233 - color: #5A9DBF; 1295 + color: #3D7A9C;
1234 } 1296 }
1235 1297
1236 :global(html.dark) .historia-monto.ingresos, 1298 :global(html.dark) .historia-monto.ingresos,
...@@ -1270,20 +1332,22 @@ ...@@ -1270,20 +1332,22 @@
1270 } 1332 }
1271 1333
1272 .area-path { 1334 .area-path {
1273 - fill: rgba(90, 157, 191, 0.12); 1335 + fill: rgba(61, 122, 156, 0.4);
1274 } 1336 }
1275 1337
1276 .line-path { 1338 .line-path {
1277 - stroke: #5A9DBF; 1339 + stroke: #3D7A9C;
1340 + opacity: 0.9;
1278 } 1341 }
1279 1342
1280 .dot { 1343 .dot {
1281 - fill: #5A9DBF; 1344 + fill: #3D7A9C;
1345 + opacity: 0.9;
1282 transition: r 0.15s; 1346 transition: r 0.15s;
1283 } 1347 }
1284 1348
1285 .dot.active { 1349 .dot.active {
1286 - fill: #5A9DBF; 1350 + fill: #3D7A9C;
1287 } 1351 }
1288 1352
1289 .hover-line { 1353 .hover-line {
...@@ -1339,12 +1403,25 @@ ...@@ -1339,12 +1403,25 @@
1339 1403
1340 .clasificador-info { 1404 .clasificador-info {
1341 display: flex; 1405 display: flex;
1406 + flex-direction: column;
1407 + gap: 0.15rem;
1408 + min-width: 0;
1409 + overflow: hidden;
1410 + }
1411 +
1412 + .clasificador-nombres {
1413 + display: flex;
1342 align-items: baseline; 1414 align-items: baseline;
1343 gap: 0.4rem; 1415 gap: 0.4rem;
1344 - min-width: 0;
1345 overflow: hidden; 1416 overflow: hidden;
1346 } 1417 }
1347 1418
1419 + .clasificador-valores {
1420 + display: flex;
1421 + align-items: baseline;
1422 + gap: 0.4rem;
1423 + }
1424 +
1348 .clasificador-monto { 1425 .clasificador-monto {
1349 font-size: 0.95rem; 1426 font-size: 0.95rem;
1350 font-weight: 600; 1427 font-weight: 600;
...@@ -1353,6 +1430,15 @@ ...@@ -1353,6 +1430,15 @@
1353 flex-shrink: 0; 1430 flex-shrink: 0;
1354 } 1431 }
1355 1432
1433 + .clasificador-pct {
1434 + font-size: 0.8rem;
1435 + font-weight: 600;
1436 + color: var(--theme-texto);
1437 + opacity: 0.5;
1438 + white-space: nowrap;
1439 + flex-shrink: 0;
1440 + }
1441 +
1356 .clasificador-padre { 1442 .clasificador-padre {
1357 font-size: 0.8rem; 1443 font-size: 0.8rem;
1358 font-weight: 400; 1444 font-weight: 400;
......
1 +<script>
2 + import { onMount } from 'svelte';
3 + import * as d3 from 'd3';
4 +
5 + let mapaData = $state(null);
6 + let boliviaPath = $state('');
7 + let deptoPath = $state('');
8 + let muniPath = $state('');
9 + let error = $state('');
10 + let debug = $state('');
11 +
12 + onMount(async () => {
13 + try {
14 + const res = await fetch('/mapa.json');
15 + mapaData = await res.json();
16 +
17 + debug = `bolivia: ${mapaData.bolivia.features.length} features, `;
18 + debug += `deptos: ${mapaData.departamentos.features.length}, `;
19 + debug += `munis: ${mapaData.municipios.features.length}`;
20 +
21 + // Quitar CRS y corregir winding order
22 + delete mapaData.bolivia.crs;
23 + delete mapaData.departamentos.crs;
24 + delete mapaData.municipios.crs;
25 +
26 + // Corregir winding order: invertir todos los anillos
27 + function fixWinding(geojson) {
28 + const fix = (coords, isHole) => {
29 + // Para geo coords: exterior debe ser counterclockwise (area < 0 en D3)
30 + // Simplemente invertimos todos los anillos exteriores
31 + coords.forEach((ring, i) => {
32 + if (i === 0) ring.reverse(); // exterior
33 + // holes quedan como están
34 + });
35 + };
36 + for (const feat of geojson.features || [geojson]) {
37 + const geom = (feat.geometry || feat);
38 + if (geom.type === 'Polygon') {
39 + fix(geom.coordinates);
40 + } else if (geom.type === 'MultiPolygon') {
41 + geom.coordinates.forEach(poly => fix(poly));
42 + }
43 + }
44 + }
45 +
46 + fixWinding(mapaData.bolivia);
47 + fixWinding(mapaData.departamentos);
48 + fixWinding(mapaData.municipios);
49 +
50 + const bounds = d3.geoBounds(mapaData.bolivia);
51 + debug += ` | bounds: ${JSON.stringify(bounds)}`;
52 +
53 + const projection = d3.geoMercator().fitSize([380, 480], mapaData.bolivia);
54 + const pathGen = d3.geoPath(projection);
55 +
56 + boliviaPath = pathGen(mapaData.bolivia) || '';
57 + debug += ` | boliviaPath length: ${boliviaPath.length}`;
58 +
59 + deptoPath = mapaData.departamentos.features.map(f => pathGen(f)).join(' ');
60 + debug += ` | deptoPath length: ${deptoPath.length}`;
61 +
62 + const muni = mapaData.municipios.features[0];
63 + muniPath = pathGen(muni) || '';
64 + debug += ` | muni code: ${muni.properties.codigo}`;
65 +
66 + } catch (e) {
67 + error = e.message;
68 + }
69 + });
70 +</script>
71 +
72 +<div style="padding: 2rem; background: #1C1C1A; color: #F5F0E8; min-height: 100vh;">
73 + <h1>Test Mapa</h1>
74 + <p style="font-size: 12px; color: #888;">{debug}</p>
75 + {#if error}
76 + <p style="color: red;">{error}</p>
77 + {/if}
78 +
79 + <svg width="400" height="500" viewBox="0 0 400 500" style="border: 1px solid #333;">
80 + {#if boliviaPath}
81 + <path d={boliviaPath} fill="none" stroke="#555" stroke-width="1" />
82 + {/if}
83 + {#if deptoPath}
84 + <path d={deptoPath} fill="rgba(255,255,255,0.1)" stroke="#777" stroke-width="0.5" />
85 + {/if}
86 + {#if muniPath}
87 + <path d={muniPath} fill="#D4A574" stroke="#D4A574" stroke-width="1" />
88 + {/if}
89 + </svg>
90 +</div>
1 +import { supabase } from '$lib/supabase';
2 +
3 +export async function load({ fetch }) {
4 + // Cargar resumen y población en paralelo
5 + const [resumenRes, pobRes] = await Promise.all([
6 + supabase
7 + .schema('ppto')
8 + .from('entidad_resumen')
9 + .select('codigo, desc, gestion, devengado, ranking')
10 + .eq('tipo', 'gastos')
11 + .eq('tipo_codigo', 'municipio_ubigeo')
12 + .eq('gestion', 2025)
13 + .order('devengado', { ascending: false })
14 + .limit(500),
15 +
16 + fetch('/poblacion.csv').then(r => r.text())
17 + ]);
18 +
19 + const poblacionMap = {};
20 + pobRes.split('\n').slice(1).forEach(line => {
21 + const [cod, gestion, pob] = line.split(',');
22 + if (parseInt(gestion) === 2025) {
23 + poblacionMap[cod] = parseInt(pob);
24 + }
25 + });
26 +
27 + const gestiones = [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025];
28 +
29 + return {
30 + resumen: resumenRes.data || [],
31 + poblacionMap,
32 + gestiones,
33 + gestionInicial: 2025
34 + };
35 +}
This diff is collapsed. Click to expand it.
1 +import { supabase } from '$lib/supabase';
2 +import { error } from '@sveltejs/kit';
3 +
4 +export async function load({ params, fetch }) {
5 + const codigo = params.ubigeo;
6 +
7 + // Cargar resumen y población en paralelo
8 + const [resumenRes, pobRes] = await Promise.all([
9 + supabase
10 + .schema('ppto')
11 + .from('entidad_resumen')
12 + .select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking')
13 + .eq('codigo', codigo),
14 +
15 + fetch('/poblacion.csv').then(r => r.text())
16 + ]);
17 +
18 + if (resumenRes.error || !resumenRes.data?.length) {
19 + throw error(404, 'Ubicación no encontrada');
20 + }
21 +
22 + // Parsear población para este código
23 + const poblacionMap = {};
24 + pobRes.split('\n').slice(1).forEach(line => {
25 + const [cod, gestion, pob] = line.split(',');
26 + if (cod === codigo) {
27 + poblacionMap[parseInt(gestion)] = parseInt(pob);
28 + }
29 + });
30 +
31 + // Determinar última gestión
32 + const gestiones = [...new Set(resumenRes.data.map(d => d.gestion))].sort((a, b) => b - a);
33 + const ultimaGestion = gestiones[0] || 2025;
34 +
35 + // Cargar distribuciones de la última gestión
36 + const distRes = await supabase
37 + .schema('ppto')
38 + .from('entidad_distribuciones')
39 + .select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado')
40 + .eq('codigo', codigo)
41 + .eq('gestion', ultimaGestion);
42 +
43 + const firstRow = resumenRes.data[0];
44 +
45 + return {
46 + nombre: firstRow.desc || `Ubicación ${codigo}`,
47 + nombrePadre: firstRow.desc_padre || null,
48 + codigo,
49 + resumenData: resumenRes.data || [],
50 + distribucionesData: distRes.data || [],
51 + gestionInicial: ultimaGestion,
52 + poblacionMap
53 + };
54 +}
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.