Showing
9 changed files
with
440 additions
and
33 deletions
| ... | @@ -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.
src/routes/rubro/[codigo]/+page.server.js
0 → 100644
| 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 | +} |
src/routes/rubro/[codigo]/+page.svelte
0 → 100644
| 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> |
-
Please register or login to post a comment