Rafael Lopez

rubros

...@@ -7,7 +7,7 @@ Buscador instantáneo con Fuse.js en cliente. Busca en clasificadores presupuest ...@@ -7,7 +7,7 @@ Buscador instantáneo con Fuse.js en cliente. Busca en clasificadores presupuest
7 7
8 ### Motor de búsqueda 8 ### Motor de búsqueda
9 - **Tecnología**: Fuse.js (cliente) 9 - **Tecnología**: Fuse.js (cliente)
10 -- **Razón**: ~1,000 registros totales (~600 entidades + ~450 objetos), cabe en memoria, búsqueda instantánea sin latencia 10 +- **Razón**: ~1,400 registros totales (~600 entidades + ~450 objetos + ~380 rubros), cabe en memoria, búsqueda instantánea sin latencia
11 11
12 ### Configuración Fuse.js 12 ### Configuración Fuse.js
13 ```javascript 13 ```javascript
...@@ -34,10 +34,10 @@ const fuseOptions = { ...@@ -34,10 +34,10 @@ const fuseOptions = {
34 ### Comportamiento 34 ### Comportamiento
35 | Parámetro | Valor | 35 | Parámetro | Valor |
36 |-----------|-------| 36 |-----------|-------|
37 -| Debounce | 200-300ms | 37 +| Debounce | 250ms |
38 | Mínimo caracteres | 2 | 38 | Mínimo caracteres | 2 |
39 -| Máximo resultados | 10-30 | 39 +| Máximo resultados | 15 |
40 -| Navegación | Flechas ↑↓ + Enter + scroll de mouse y loq ue corresponda para mobiles| 40 +| Navegación | Flechas ↑↓ + Enter + scroll de mouse |
41 41
42 ## Estructura del Índice 42 ## Estructura del Índice
43 43
...@@ -70,6 +70,18 @@ const searchData = [ ...@@ -70,6 +70,18 @@ const searchData = [
70 nombre_normalizado: 'pasajes', 70 nombre_normalizado: 'pasajes',
71 sigla_normalizada: '', 71 sigla_normalizada: '',
72 codigo_normalizado: '25100' 72 codigo_normalizado: '25100'
73 + },
74 + // Rubros
75 + {
76 + tipo: 'rubro',
77 + codigo: 13000,
78 + nombre: 'INGRESOS POR IMPUESTOS',
79 + sigla: null,
80 + contexto: 'Rubro · Tipo',
81 + nivel: 'tipo',
82 + nombre_normalizado: 'ingresos por impuestos',
83 + sigla_normalizada: '',
84 + codigo_normalizado: '13000'
73 } 85 }
74 ] 86 ]
75 ``` 87 ```
...@@ -77,6 +89,7 @@ const searchData = [ ...@@ -77,6 +89,7 @@ const searchData = [
77 ### Fuentes de datos 89 ### Fuentes de datos
78 - **Entidades**: tabla `ppto.clas_institucional` 90 - **Entidades**: tabla `ppto.clas_institucional`
79 - **Objetos de gasto**: tabla `ppto.clas_objetos` 91 - **Objetos de gasto**: tabla `ppto.clas_objetos`
92 +- **Rubros**: tabla `ppto.clas_rubros`
80 93
81 ## Columnas de Búsqueda 94 ## Columnas de Búsqueda
82 95
...@@ -96,6 +109,13 @@ const searchData = [ ...@@ -96,6 +109,13 @@ const searchData = [
96 | `objeto` | ✓ | ✓ código (monospace) | 109 | `objeto` | ✓ | ✓ código (monospace) |
97 | `nivel` | ✗ | ✓ contexto ("Objeto del Gasto · Partida") | 110 | `nivel` | ✗ | ✓ contexto ("Objeto del Gasto · Partida") |
98 111
112 +### Rubros (clas_rubros)
113 +| Columna | Buscar | Mostrar |
114 +|---------|--------|---------|
115 +| `desc_rubros` | ✓ | ✓ nombre principal |
116 +| `rubro` | ✓ | ✓ código (monospace) |
117 +| `nivel` | ✗ | ✓ contexto ("Rubro · Tipo") |
118 +
99 ## Flujo de Búsqueda 119 ## Flujo de Búsqueda
100 120
101 ``` 121 ```
...@@ -109,7 +129,7 @@ const searchData = [ ...@@ -109,7 +129,7 @@ const searchData = [
109 Fuse devuelve matches → renderizar lista con highlighting 129 Fuse devuelve matches → renderizar lista con highlighting
110 130
111 4. SELECCIÓN 131 4. SELECCIÓN
112 - Click o Enter → navegar a /entidad/[codigo] o /gasto/[codigo] 132 + Click o Enter → navegar a URL según tipo
113 ``` 133 ```
114 134
115 ## Estructura de URLs 135 ## Estructura de URLs
...@@ -117,28 +137,38 @@ const searchData = [ ...@@ -117,28 +137,38 @@ const searchData = [
117 ``` 137 ```
118 /entidad/[codigo] → /entidad/139 138 /entidad/[codigo] → /entidad/139
119 /objeto/[codigo] → /objeto/25100 139 /objeto/[codigo] → /objeto/25100
120 -/area/[codigo] → /area/1.1.4 (futuro, opcional) 140 +/rubro/[codigo] → /rubro/13000
121 ``` 141 ```
122 142
123 ## Formato de Resultados 143 ## Formato de Resultados
124 144
145 +Resultados agrupados por tipo de clasificador:
146 +
125 ``` 147 ```
126 ┌─────────────────────────────────────────────────────┐ 148 ┌─────────────────────────────────────────────────────┐
127 -│ 🔍 [Buscar entidad u objeto de gasto...] │ 149 +│ 🔍 [Buscar entidad, objeto de gasto o rubro...] │
150 +├─────────────────────────────────────────────────────┤
151 +│ ENTIDADES │
128 ├─────────────────────────────────────────────────────┤ 152 ├─────────────────────────────────────────────────────┤
129 -│ Universidad Mayor De San Andrés (UMSA) │ ← Entidad 153 +│ Universidad Mayor De San Andrés (UMSA) │
130 │ Universidades Públicas │ 154 │ Universidades Públicas │
131 ├─────────────────────────────────────────────────────┤ 155 ├─────────────────────────────────────────────────────┤
132 -│ 25100 Pasajes │ ← Objeto de gasto 156 +│ OBJETOS DEL GASTO │
157 +├─────────────────────────────────────────────────────┤
158 +│ 25100 Pasajes │
133 │ Objeto del Gasto · Partida │ 159 │ Objeto del Gasto · Partida │
134 ├─────────────────────────────────────────────────────┤ 160 ├─────────────────────────────────────────────────────┤
135 -│ ... │ 161 +│ RUBROS │
162 +├─────────────────────────────────────────────────────┤
163 +│ 13000 INGRESOS POR IMPUESTOS │
164 +│ Rubro · Tipo │
136 └─────────────────────────────────────────────────────┘ 165 └─────────────────────────────────────────────────────┘
137 ``` 166 ```
138 167
139 ### Diferencias visuales 168 ### Diferencias visuales
140 - **Entidades**: Nombre + (sigla) en primera línea 169 - **Entidades**: Nombre + (sigla) en primera línea
141 - **Objetos de gasto**: Código (monospace) + Nombre en primera línea 170 - **Objetos de gasto**: Código (monospace) + Nombre en primera línea
171 +- **Rubros**: Código (monospace) + Nombre en primera línea
142 172
143 ## Estados del Buscador 173 ## Estados del Buscador
144 174
...@@ -177,6 +207,7 @@ src/lib/ ...@@ -177,6 +207,7 @@ src/lib/
177 |--------------|--------|-------| 207 |--------------|--------|-------|
178 | Entidades | ✅ Implementado | `ppto.clas_institucional` | 208 | Entidades | ✅ Implementado | `ppto.clas_institucional` |
179 | Objetos de gasto | ✅ Implementado | `ppto.clas_objetos` | 209 | Objetos de gasto | ✅ Implementado | `ppto.clas_objetos` |
210 +| Rubros | ✅ Implementado | `ppto.clas_rubros` |
180 | Fuente de financiamiento | ⏳ Pendiente | - | 211 | Fuente de financiamiento | ⏳ Pendiente | - |
181 | Geográfico | ⏳ Pendiente | - | 212 | Geográfico | ⏳ Pendiente | - |
182 213
......
...@@ -52,6 +52,8 @@ ...@@ -52,6 +52,8 @@
52 goto(`/entidad/${item.codigo}`); 52 goto(`/entidad/${item.codigo}`);
53 } else if (item.tipo === 'objeto_gasto') { 53 } else if (item.tipo === 'objeto_gasto') {
54 goto(`/objeto/${item.codigo}`); 54 goto(`/objeto/${item.codigo}`);
55 + } else if (item.tipo === 'rubro') {
56 + goto(`/rubro/${item.codigo}`);
55 } 57 }
56 clearSearch(); 58 clearSearch();
57 inputRef.value = ''; 59 inputRef.value = '';
...@@ -78,7 +80,7 @@ ...@@ -78,7 +80,7 @@
78 <input 80 <input
79 bind:this={inputRef} 81 bind:this={inputRef}
80 type="text" 82 type="text"
81 - placeholder={$indexLoaded ? "Buscar entidad u objeto de gasto..." : "Cargando..."} 83 + placeholder={$indexLoaded ? "Buscar entidad, objeto de gasto o rubro..." : "Cargando..."}
82 disabled={!$indexLoaded} 84 disabled={!$indexLoaded}
83 class="w-full p-2 border rounded" 85 class="w-full p-2 border rounded"
84 oninput={handleInput} 86 oninput={handleInput}
......
...@@ -9,31 +9,96 @@ ...@@ -9,31 +9,96 @@
9 goto(`/entidad/${item.codigo}`); 9 goto(`/entidad/${item.codigo}`);
10 } else if (item.tipo === 'objeto_gasto') { 10 } else if (item.tipo === 'objeto_gasto') {
11 goto(`/objeto/${item.codigo}`); 11 goto(`/objeto/${item.codigo}`);
12 + } else if (item.tipo === 'rubro') {
13 + goto(`/rubro/${item.codigo}`);
12 } 14 }
13 clearSearch(); 15 clearSearch();
14 } 16 }
17 +
18 + // Agrupar resultados por tipo
19 + let groupedResults = $derived(() => {
20 + const entidades = results.filter(r => r.tipo === 'entidad');
21 + const objetos = results.filter(r => r.tipo === 'objeto_gasto');
22 + const rubros = results.filter(r => r.tipo === 'rubro');
23 + return { entidades, objetos, rubros };
24 + });
25 +
26 + // Calcular índice global para navegación con teclado
27 + function getGlobalIndex(tipo, localIndex) {
28 + const g = groupedResults();
29 + if (tipo === 'entidad') {
30 + return localIndex;
31 + } else if (tipo === 'objeto_gasto') {
32 + return g.entidades.length + localIndex;
33 + } else {
34 + return g.entidades.length + g.objetos.length + localIndex;
35 + }
36 + }
15 </script> 37 </script>
16 38
17 {#if results.length > 0} 39 {#if results.length > 0}
18 <div class="absolute top-full left-0 right-0 mt-1 bg-white border rounded shadow-lg max-h-80 overflow-y-auto z-50"> 40 <div class="absolute top-full left-0 right-0 mt-1 bg-white border rounded shadow-lg max-h-80 overflow-y-auto z-50">
19 - {#each results as item, index} 41 + {#if groupedResults().entidades.length > 0}
20 - <button 42 + <div class="px-3 py-1.5 bg-slate-50 border-b">
21 - type="button" 43 + <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Entidades</span>
22 - class="w-full text-left px-3 py-2 text-sm hover:bg-gray-100 {index === selectedIndex ? 'bg-gray-100' : ''}" 44 + </div>
23 - onclick={() => handleSelect(item)} 45 + {#each groupedResults().entidades as item, index}
24 - > 46 + {@const globalIdx = getGlobalIndex('entidad', index)}
25 - <div class="flex items-center gap-2"> 47 + <button
26 - {#if item.tipo === 'objeto_gasto'} 48 + type="button"
49 + class="w-full text-left px-3 py-2 text-sm hover:bg-gray-100 {globalIdx === selectedIndex ? 'bg-blue-50' : ''}"
50 + onclick={() => handleSelect(item)}
51 + >
52 + <div class="flex items-center gap-2">
53 + <span>{item.nombre}</span>
54 + {#if item.sigla}
55 + <span class="text-slate-400">({item.sigla})</span>
56 + {/if}
57 + </div>
58 + <div class="text-xs text-gray-500">{item.contexto}</div>
59 + </button>
60 + {/each}
61 + {/if}
62 +
63 + {#if groupedResults().objetos.length > 0}
64 + <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 ? 'border-t' : ''}">
65 + <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Objetos del Gasto</span>
66 + </div>
67 + {#each groupedResults().objetos as item, index}
68 + {@const globalIdx = getGlobalIndex('objeto_gasto', index)}
69 + <button
70 + type="button"
71 + class="w-full text-left px-3 py-2 text-sm hover:bg-gray-100 {globalIdx === selectedIndex ? 'bg-blue-50' : ''}"
72 + onclick={() => handleSelect(item)}
73 + >
74 + <div class="flex items-center gap-2">
75 + <span class="font-mono text-xs text-slate-400">{item.codigo}</span>
76 + <span>{item.nombre}</span>
77 + </div>
78 + <div class="text-xs text-gray-500">{item.contexto}</div>
79 + </button>
80 + {/each}
81 + {/if}
82 +
83 + {#if groupedResults().rubros.length > 0}
84 + <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 || groupedResults().objetos.length > 0 ? 'border-t' : ''}">
85 + <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Rubros</span>
86 + </div>
87 + {#each groupedResults().rubros as item, index}
88 + {@const globalIdx = getGlobalIndex('rubro', index)}
89 + <button
90 + type="button"
91 + class="w-full text-left px-3 py-2 text-sm hover:bg-gray-100 {globalIdx === selectedIndex ? 'bg-blue-50' : ''}"
92 + onclick={() => handleSelect(item)}
93 + >
94 + <div class="flex items-center gap-2">
27 <span class="font-mono text-xs text-slate-400">{item.codigo}</span> 95 <span class="font-mono text-xs text-slate-400">{item.codigo}</span>
28 - {/if} 96 + <span>{item.nombre}</span>
29 - <span>{item.nombre}</span> 97 + </div>
30 - {#if item.sigla} 98 + <div class="text-xs text-gray-500">{item.contexto}</div>
31 - <span class="text-slate-400">({item.sigla})</span> 99 + </button>
32 - {/if} 100 + {/each}
33 - </div> 101 + {/if}
34 - <div class="text-xs text-gray-500">{item.contexto}</div>
35 - </button>
36 - {/each}
37 </div> 102 </div>
38 {:else if query.length >= 2} 103 {:else if query.length >= 2}
39 <div class="absolute top-full left-0 right-0 mt-1 bg-white border rounded p-3 text-sm text-gray-500"> 104 <div class="absolute top-full left-0 right-0 mt-1 bg-white border rounded p-3 text-sm text-gray-500">
......
...@@ -50,6 +50,17 @@ export async function initSearchIndex() { ...@@ -50,6 +50,17 @@ export async function initSearchIndex() {
50 throw errorObjetos; 50 throw errorObjetos;
51 } 51 }
52 52
53 + // Cargar rubros
54 + const { data: rubros, error: errorRubros } = await supabase
55 + .schema('ppto')
56 + .from('clas_rubros')
57 + .select('rubro, desc_rubros, nivel');
58 +
59 + if (errorRubros) {
60 + console.error('Error cargando rubros:', errorRubros);
61 + throw errorRubros;
62 + }
63 +
53 // Transformar entidades para el índice 64 // Transformar entidades para el índice
54 const entidadesIndex = entidades.map(item => ({ 65 const entidadesIndex = entidades.map(item => ({
55 tipo: 'entidad', 66 tipo: 'entidad',
...@@ -64,21 +75,35 @@ export async function initSearchIndex() { ...@@ -64,21 +75,35 @@ export async function initSearchIndex() {
64 })); 75 }));
65 76
66 // Transformar objetos de gasto para el índice 77 // Transformar objetos de gasto para el índice
67 - const nivelLabels = { grupo: 'Grupo', subgrupo: 'Subgrupo', partida: 'Partida', subpartida: 'Subpartida' }; 78 + const nivelLabelsObjetos = { grupo: 'Grupo', subgrupo: 'Subgrupo', partida: 'Partida', subpartida: 'Subpartida' };
68 const objetosIndex = objetos.map(item => ({ 79 const objetosIndex = objetos.map(item => ({
69 tipo: 'objeto_gasto', 80 tipo: 'objeto_gasto',
70 codigo: item.objeto, 81 codigo: item.objeto,
71 nombre: item.desc_objeto, 82 nombre: item.desc_objeto,
72 sigla: null, 83 sigla: null,
73 - contexto: `Objeto del Gasto · ${nivelLabels[item.nivel] || item.nivel}`, 84 + contexto: `Objeto del Gasto · ${nivelLabelsObjetos[item.nivel] || item.nivel}`,
74 nivel: item.nivel, 85 nivel: item.nivel,
75 nombre_normalizado: normalizeText(item.desc_objeto), 86 nombre_normalizado: normalizeText(item.desc_objeto),
76 sigla_normalizada: '', 87 sigla_normalizada: '',
77 codigo_normalizado: item.objeto || '' 88 codigo_normalizado: item.objeto || ''
78 })); 89 }));
79 90
80 - // Combinar ambos conjuntos 91 + // Transformar rubros para el índice
81 - searchData = [...entidadesIndex, ...objetosIndex]; 92 + const nivelLabelsRubros = { tipo: 'Tipo', clase: 'Clase', cuenta: 'Cuenta', sub_cuenta: 'Subcuenta' };
93 + const rubrosIndex = rubros.map(item => ({
94 + tipo: 'rubro',
95 + codigo: item.rubro,
96 + nombre: item.desc_rubros,
97 + sigla: null,
98 + contexto: `Rubro · ${nivelLabelsRubros[item.nivel] || item.nivel}`,
99 + nivel: item.nivel,
100 + nombre_normalizado: normalizeText(item.desc_rubros),
101 + sigla_normalizada: '',
102 + codigo_normalizado: item.rubro?.toString() || ''
103 + }));
104 +
105 + // Combinar todos los conjuntos
106 + searchData = [...entidadesIndex, ...objetosIndex, ...rubrosIndex];
82 107
83 fuseInstance = new Fuse(searchData, fuseOptions); 108 fuseInstance = new Fuse(searchData, fuseOptions);
84 109
......
...@@ -50,7 +50,7 @@ export function performSearch(searchQuery) { ...@@ -50,7 +50,7 @@ export function performSearch(searchQuery) {
50 return; 50 return;
51 } 51 }
52 52
53 - const searchResults = search(searchQuery, 30); 53 + const searchResults = search(searchQuery, 15);
54 results.set(searchResults); 54 results.set(searchResults);
55 } 55 }
56 56
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 const clasificadores = [ 2 const clasificadores = [
3 { id: 'institucional', nombre: 'Institucional', descripcion: 'Ministerios, universidades, municipios, empresas públicas', disponible: true }, 3 { id: 'institucional', nombre: 'Institucional', descripcion: 'Ministerios, universidades, municipios, empresas públicas', disponible: true },
4 { id: 'objeto-gasto', nombre: 'Objeto del Gasto', descripcion: 'Servicios personales, bienes, inversiones', disponible: true }, 4 { id: 'objeto-gasto', nombre: 'Objeto del Gasto', descripcion: 'Servicios personales, bienes, inversiones', disponible: true },
5 + { id: 'rubros', nombre: 'Rubros de Ingresos', descripcion: 'Ingresos tributarios, no tributarios, transferencias', disponible: true },
5 { id: 'fuente', nombre: 'Fuente de Financiamiento', descripcion: 'Tesoro, créditos externos, recursos propios', disponible: false }, 6 { id: 'fuente', nombre: 'Fuente de Financiamiento', descripcion: 'Tesoro, créditos externos, recursos propios', disponible: false },
6 { id: 'organismo', nombre: 'Organismo Financiador', descripcion: 'BID, Banco Mundial, CAF', disponible: false }, 7 { id: 'organismo', nombre: 'Organismo Financiador', descripcion: 'BID, Banco Mundial, CAF', disponible: false },
7 { id: 'geografico', nombre: 'Geográfico', descripcion: 'Departamentos, provincias, municipios', disponible: false }, 8 { id: 'geografico', nombre: 'Geográfico', descripcion: 'Departamentos, provincias, municipios', disponible: false },
......
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 }) {
5 + const { codigo } = params;
6 +
7 + const { data, error: dbError } = await supabase
8 + .schema('ppto')
9 + .from('clas_rubros')
10 + .select('*')
11 + .eq('rubro', codigo);
12 +
13 + if (dbError || !data || data.length === 0) {
14 + throw error(404, 'Rubro no encontrado');
15 + }
16 +
17 + const rubro = data[0];
18 +
19 + // Obtener jerarquía (padres e hijos)
20 + let padres = [];
21 + let hijos = [];
22 +
23 + // Buscar padre según nivel
24 + if (rubro.nivel === 'sub_cuenta') {
25 + // Padre es cuenta
26 + const { data: padreData } = await supabase
27 + .schema('ppto')
28 + .from('clas_rubros')
29 + .select('*')
30 + .eq('nivel', 'cuenta')
31 + .eq('tipo', rubro.tipo)
32 + .eq('clase', rubro.clase)
33 + .eq('cuenta', rubro.cuenta);
34 + if (padreData?.length) padres.push(padreData[0]);
35 + }
36 +
37 + if (rubro.nivel === 'cuenta' || rubro.nivel === 'sub_cuenta') {
38 + // Padre es clase
39 + const { data: padreData } = await supabase
40 + .schema('ppto')
41 + .from('clas_rubros')
42 + .select('*')
43 + .eq('nivel', 'clase')
44 + .eq('tipo', rubro.tipo)
45 + .eq('clase', rubro.clase);
46 + if (padreData?.length) padres.unshift(padreData[0]);
47 + }
48 +
49 + if (rubro.nivel !== 'tipo') {
50 + // Padre es tipo
51 + const { data: padreData } = await supabase
52 + .schema('ppto')
53 + .from('clas_rubros')
54 + .select('*')
55 + .eq('nivel', 'tipo')
56 + .eq('tipo', rubro.tipo);
57 + if (padreData?.length) padres.unshift(padreData[0]);
58 + }
59 +
60 + // Buscar hijos según nivel
61 + if (rubro.nivel === 'tipo') {
62 + const { data: hijosData } = await supabase
63 + .schema('ppto')
64 + .from('clas_rubros')
65 + .select('*')
66 + .eq('nivel', 'clase')
67 + .eq('tipo', rubro.tipo)
68 + .order('rubro');
69 + hijos = hijosData?.reduce((acc, item) => {
70 + if (!acc.find(h => h.rubro === item.rubro)) acc.push(item);
71 + return acc;
72 + }, []) || [];
73 + } else if (rubro.nivel === 'clase') {
74 + const { data: hijosData } = await supabase
75 + .schema('ppto')
76 + .from('clas_rubros')
77 + .select('*')
78 + .eq('nivel', 'cuenta')
79 + .eq('tipo', rubro.tipo)
80 + .eq('clase', rubro.clase)
81 + .order('rubro');
82 + hijos = hijosData?.reduce((acc, item) => {
83 + if (!acc.find(h => h.rubro === item.rubro)) acc.push(item);
84 + return acc;
85 + }, []) || [];
86 + } else if (rubro.nivel === 'cuenta') {
87 + const { data: hijosData } = await supabase
88 + .schema('ppto')
89 + .from('clas_rubros')
90 + .select('*')
91 + .eq('nivel', 'sub_cuenta')
92 + .eq('tipo', rubro.tipo)
93 + .eq('clase', rubro.clase)
94 + .eq('cuenta', rubro.cuenta)
95 + .order('rubro');
96 + hijos = hijosData?.reduce((acc, item) => {
97 + if (!acc.find(h => h.rubro === item.rubro)) acc.push(item);
98 + return acc;
99 + }, []) || [];
100 + }
101 +
102 + return {
103 + rubro,
104 + padres,
105 + hijos
106 + };
107 +}
1 +<script>
2 + let { data } = $props();
3 +
4 + const rubro = data.rubro;
5 + const padres = data.padres;
6 + const hijos = data.hijos;
7 +
8 + function parseDescripciones(descripcionesStr) {
9 + if (!descripcionesStr) return [];
10 + try {
11 + const parsed = JSON.parse(descripcionesStr);
12 + return parsed.sort((a, b) => {
13 + const maxYearA = getMaxYear(a.rangos);
14 + const maxYearB = getMaxYear(b.rangos);
15 + return maxYearB - maxYearA;
16 + });
17 + } catch {
18 + return [];
19 + }
20 + }
21 +
22 + function getMaxYear(rangos) {
23 + if (!rangos) return 0;
24 + const years = rangos.match(/\d{4}/g);
25 + if (!years) return 0;
26 + return Math.max(...years.map(y => parseInt(y)));
27 + }
28 +
29 + function getNivelLabel(nivel) {
30 + const labels = { tipo: 'Tipo', clase: 'Clase', cuenta: 'Cuenta', sub_cuenta: 'Subcuenta' };
31 + return labels[nivel] || nivel;
32 + }
33 +
34 + // Obtener la descripción correcta según el nivel
35 + function getDescripcion(item) {
36 + if (item.nivel === 'tipo') return item.desc_tipo;
37 + if (item.nivel === 'clase') return item.desc_clase;
38 + if (item.nivel === 'cuenta') return item.desc_cuenta;
39 + if (item.nivel === 'sub_cuenta') return item.desc_sub_cuenta;
40 + return item.desc_rubros;
41 + }
42 +
43 + function getHijosLabel(nivel) {
44 + const labels = { tipo: 'Clases', clase: 'Cuentas', cuenta: 'Subcuentas' };
45 + return labels[nivel] || 'Hijos';
46 + }
47 +
48 + const descripciones = parseDescripciones(rubro.descripciones);
49 +</script>
50 +
51 +<svelte:head>
52 + <title>{getDescripcion(rubro)} | Presupuesto Público</title>
53 +</svelte:head>
54 +
55 +<div class="min-h-screen bg-white">
56 + <header class="border-b bg-slate-50">
57 + <div class="max-w-4xl mx-auto px-6 py-4">
58 + <nav class="text-sm text-slate-500 mb-2 flex flex-wrap items-center gap-1">
59 + <a href="/" class="hover:text-slate-700">Inicio</a>
60 + <span>/</span>
61 + <a href="/clasificadores/rubros" class="hover:text-slate-700">Rubros</a>
62 + {#each padres as padre}
63 + <span>/</span>
64 + <a href="/rubro/{padre.rubro}" class="hover:text-slate-700">{getDescripcion(padre)}</a>
65 + {/each}
66 + <span>/</span>
67 + <span class="text-slate-900">{getDescripcion(rubro)}</span>
68 + </nav>
69 + </div>
70 + </header>
71 +
72 + <main class="max-w-4xl mx-auto px-6 py-8">
73 + <!-- Encabezado -->
74 + <div class="mb-8">
75 + <p class="text-xs text-slate-500 uppercase tracking-wide mb-1">{getNivelLabel(rubro.nivel)}</p>
76 + <div class="flex items-baseline gap-4">
77 + <span class="font-mono text-2xl text-slate-400">{rubro.rubro}</span>
78 + <h1 class="text-2xl font-medium text-slate-900">{getDescripcion(rubro)}</h1>
79 + </div>
80 + </div>
81 +
82 + <!-- Descripciones -->
83 + <section class="mb-10">
84 + <h2 class="text-sm font-medium text-slate-700 mb-4">
85 + {#if descripciones.length > 1}
86 + Descripciones ({descripciones.length} variaciones)
87 + {:else}
88 + Descripción
89 + {/if}
90 + </h2>
91 +
92 + <div class="space-y-4">
93 + {#each descripciones as desc, i}
94 + <div class="border-l-2 {i === 0 ? 'border-blue-500 bg-blue-50/50' : 'border-slate-200 bg-slate-50/50'} pl-4 py-3 rounded-r">
95 + <p class="text-xs text-slate-500 mb-2">
96 + {#if i === 0 && descripciones.length > 1}
97 + <span class="text-blue-600 font-medium">Vigente</span>
98 + <span class="mx-1">·</span>
99 + {/if}
100 + {desc.rangos}
101 + </p>
102 + <p class="text-slate-700 leading-relaxed">{desc.descripcion}</p>
103 + </div>
104 + {/each}
105 + </div>
106 + </section>
107 +
108 + <!-- Jerarquía -->
109 + {#if padres.length > 0}
110 + <section class="mb-10">
111 + <h2 class="text-sm font-medium text-slate-700 mb-4">Jerarquía</h2>
112 + <div class="space-y-2">
113 + {#each padres as padre, i}
114 + <div class="flex items-center gap-2" style="margin-left: {i * 1.5}rem">
115 + <span class="text-slate-300">└</span>
116 + <a
117 + href="/rubro/{padre.rubro}"
118 + class="text-sm text-slate-600 hover:text-blue-600"
119 + >
120 + <span class="font-mono text-xs text-slate-400">{padre.rubro}</span>
121 + {getDescripcion(padre)}
122 + </a>
123 + </div>
124 + {/each}
125 + <div class="flex items-center gap-2" style="margin-left: {padres.length * 1.5}rem">
126 + <span class="text-blue-500">└</span>
127 + <span class="text-sm font-medium text-blue-600">
128 + <span class="font-mono text-xs">{rubro.rubro}</span>
129 + {getDescripcion(rubro)}
130 + </span>
131 + </div>
132 + </div>
133 + </section>
134 + {/if}
135 +
136 + <!-- Hijos -->
137 + {#if hijos.length > 0}
138 + <section class="mb-10">
139 + <h2 class="text-sm font-medium text-slate-700 mb-4">
140 + {getHijosLabel(rubro.nivel)} ({hijos.length})
141 + </h2>
142 +
143 + <div class="border rounded-lg divide-y">
144 + {#each hijos as hijo}
145 + {@const hijoDescs = parseDescripciones(hijo.descripciones)}
146 + <a
147 + href="/rubro/{hijo.rubro}"
148 + class="block px-4 py-3 hover:bg-slate-50 transition-colors"
149 + >
150 + <div class="flex items-baseline gap-3">
151 + <span class="font-mono text-sm text-slate-400">{hijo.rubro}</span>
152 + <span class="text-slate-900">{getDescripcion(hijo)}</span>
153 + {#if hijo.n_variaciones > 1}
154 + <span class="text-xs text-orange-500">({hijo.n_variaciones} var.)</span>
155 + {/if}
156 + </div>
157 + {#if hijoDescs.length > 0}
158 + <p class="text-sm text-slate-500 mt-1 ml-16">{hijoDescs[0].descripcion}</p>
159 + {/if}
160 + </a>
161 + {/each}
162 + </div>
163 + </section>
164 + {/if}
165 +
166 + <!-- Volver -->
167 + <div class="pt-6 border-t">
168 + <a
169 + href="/clasificadores/rubros"
170 + class="text-sm text-blue-600 hover:underline"
171 + >
172 + Volver al clasificador
173 + </a>
174 + </div>
175 + </main>
176 +</div>