Rafael Lopez

comparador

......@@ -257,31 +257,216 @@
const twTop2 = tweened(0, tweenOptions);
const twTop3 = tweened(0, tweenOptions);
// Comparación (placeholder por ahora - TODO: implementar)
// ══════════════════════════════════════════════════════════════
// COMPARACIÓN - Datos reales de Supabase
// ══════════════════════════════════════════════════════════════
const twMontoA = tweened(0, tweenOptions);
const twMontoB = tweened(0, tweenOptions);
const twPerCapitaA = tweened(0, tweenOptions);
const twPerCapitaB = tweened(0, tweenOptions);
let hoveredYearComparar = $state(null);
let entidadA = $state(0);
let entidadB = $state(1);
let displayPeriodoComparar = $derived(
hoveredYearComparar ? hoveredYearComparar : `${años[0]}-${años[años.length - 1]}`
);
let loadingComparacion = $state(false);
// Entidades seleccionadas para comparar
let entidadCompararA = $state(null);
let entidadCompararB = $state(null);
// Datos cargados de cada entidad
let datosCompararA = $state({ anual: [], historico: null });
let datosCompararB = $state({ anual: [], historico: null });
// Dropdown states para comparación
let dropdownAOpen = $state(false);
let dropdownBOpen = $state(false);
let searchA = $state('');
let searchB = $state('');
// Filtrar entidades para cada dropdown
let filteredEntidadesA = $derived.by(() => {
if (!searchA || searchA.length < 2) return entidades.slice(0, 20);
const q = searchA.toLowerCase();
return entidades.filter(e =>
e.entidad_desc?.toLowerCase().includes(q) ||
e.entidad?.toString().includes(q)
).slice(0, 30);
});
let filteredEntidadesB = $derived.by(() => {
if (!searchB || searchB.length < 2) return entidades.slice(0, 20);
const q = searchB.toLowerCase();
return entidades.filter(e =>
e.entidad_desc?.toLowerCase().includes(q) ||
e.entidad?.toString().includes(q)
).slice(0, 30);
});
// Cargar datos de una entidad para comparación
async function loadDatosEntidad(entidad) {
const { data, error } = await supabase
.schema('ppto')
.from('vista_objeto_entidad')
.select('*')
.eq('objeto', objetoCodigo)
.eq('nivel', objetoNivel)
.eq('entidad', entidad.entidad)
.order('gestion');
if (data) {
return {
anual: data.filter(d => d.gestion > 0),
historico: data.find(d => d.gestion === 0) || null
};
}
return { anual: [], historico: null };
}
// Seleccionar entidad A
async function selectEntidadA(entidad) {
entidadCompararA = entidad;
dropdownAOpen = false;
searchA = '';
loadingComparacion = true;
datosCompararA = await loadDatosEntidad(entidad);
loadingComparacion = false;
}
// Seleccionar entidad B
async function selectEntidadB(entidad) {
entidadCompararB = entidad;
dropdownBOpen = false;
searchB = '';
loadingComparacion = true;
datosCompararB = await loadDatosEntidad(entidad);
loadingComparacion = false;
}
// Selector de rango de años
let rangoAños = $state('10'); // '5', '10', '15', 'todo'
// Datos dummy para comparación (TODO: conectar a Supabase)
const entidadesComparables = [
{ id: 0, nombre: 'Entidad A', datos: [], porcentajeObjeto: 0, ranking: 1 },
{ id: 1, nombre: 'Entidad B', datos: [], porcentajeObjeto: 0, ranking: 2 }
// Años comunes entre ambas entidades
let añosComunesTotales = $derived.by(() => {
if (!datosCompararA.anual.length || !datosCompararB.anual.length) return [];
const añosA = new Set(datosCompararA.anual.map(d => d.gestion));
const añosB = new Set(datosCompararB.anual.map(d => d.gestion));
return [...añosA].filter(a => añosB.has(a)).sort((a, b) => a - b);
});
// Años filtrados según el rango seleccionado
let añosComparacion = $derived.by(() => {
if (rangoAños === 'todo') return añosComunesTotales;
const n = parseInt(rangoAños);
return añosComunesTotales.slice(-n);
});
// Datos formateados para el gráfico
let datosGraficoA = $derived.by(() => {
const map = new Map(datosCompararA.anual.map(d => [d.gestion, d]));
return añosComparacion.map(año => map.get(año) || { per_capita: 0, monto: 0 });
});
let datosGraficoB = $derived.by(() => {
const map = new Map(datosCompararB.anual.map(d => [d.gestion, d]));
return añosComparacion.map(año => map.get(año) || { per_capita: 0, monto: 0 });
});
// Máximo para escala del gráfico
let maxComparacion = $derived.by(() => {
const allPerCapita = [
...datosGraficoA.map(d => d.per_capita || 0),
...datosGraficoB.map(d => d.per_capita || 0)
];
let entA = $derived(entidadesComparables[entidadA] || entidadesComparables[0]);
let entB = $derived(entidadesComparables[entidadB] || entidadesComparables[1]);
let maxComparacion = $state(100);
let años = $state([2020, 2021, 2022, 2023, 2024]);
return Math.max(...allPerCapita, 1) * 1.1;
});
// Período para mostrar
let displayPeriodoComparar = $derived.by(() => {
if (hoveredYearComparar) return hoveredYearComparar;
if (añosComparacion.length > 0) {
return `${añosComparacion[0]}-${añosComparacion[añosComparacion.length - 1]}`;
}
return 'Selecciona entidades';
});
// Valores reactivos para comparación (peso y ranking)
let displayPropA = $derived.by(() => {
if (hoveredYearComparar) {
const idx = añosComparacion.indexOf(hoveredYearComparar);
if (idx >= 0) return datosGraficoA[idx]?.prop || 0;
}
return datosCompararA.historico?.prop || 0;
});
let displayPropB = $derived.by(() => {
if (hoveredYearComparar) {
const idx = añosComparacion.indexOf(hoveredYearComparar);
if (idx >= 0) return datosGraficoB[idx]?.prop || 0;
}
return datosCompararB.historico?.prop || 0;
});
let displayRankingA = $derived.by(() => {
if (hoveredYearComparar) {
const idx = añosComparacion.indexOf(hoveredYearComparar);
if (idx >= 0) return datosGraficoA[idx]?.ranking || '-';
}
return datosCompararA.historico?.ranking || '-';
});
let displayRankingB = $derived.by(() => {
if (hoveredYearComparar) {
const idx = añosComparacion.indexOf(hoveredYearComparar);
if (idx >= 0) return datosGraficoB[idx]?.ranking || '-';
}
return datosCompararB.historico?.ranking || '-';
});
let displayNEntidadesA = $derived.by(() => {
if (hoveredYearComparar) {
const idx = añosComparacion.indexOf(hoveredYearComparar);
if (idx >= 0) return datosGraficoA[idx]?.n_entidades || '-';
}
return datosCompararA.historico?.n_entidades || '-';
});
let displayNEntidadesB = $derived.by(() => {
if (hoveredYearComparar) {
const idx = añosComparacion.indexOf(hoveredYearComparar);
if (idx >= 0) return datosGraficoB[idx]?.n_entidades || '-';
}
return datosCompararB.historico?.n_entidades || '-';
});
// Colores
let colorA = '#2a9d8f';
let colorB = '#f4a261';
// Actualizar tweened de comparación
$effect(() => {
const year = hoveredYearComparar;
const idx = year ? añosComparacion.indexOf(year) : -1;
let montoA, montoB, perCapitaA, perCapitaB;
if (idx >= 0) {
montoA = toNumber(datosGraficoA[idx]?.monto);
montoB = toNumber(datosGraficoB[idx]?.monto);
perCapitaA = toNumber(datosGraficoA[idx]?.per_capita);
perCapitaB = toNumber(datosGraficoB[idx]?.per_capita);
} else {
// Histórico o suma
montoA = toNumber(datosCompararA.historico?.monto);
montoB = toNumber(datosCompararB.historico?.monto);
perCapitaA = toNumber(datosCompararA.historico?.per_capita);
perCapitaB = toNumber(datosCompararB.historico?.per_capita);
}
twMontoA.set(montoA);
twMontoB.set(montoB);
twPerCapitaA.set(perCapitaA);
twPerCapitaB.set(perCapitaB);
});
// Helper para asegurar que siempre tengamos un número
function toNumber(val) {
const n = parseFloat(val);
......@@ -350,6 +535,13 @@
if (entityDropdownOpen && !e.target.closest('.entity-dropdown-container')) {
entityDropdownOpen = false;
}
// Dropdowns de comparación
if (dropdownAOpen && !e.target.closest('.comparador-dropdown-container')) {
dropdownAOpen = false;
}
if (dropdownBOpen && !e.target.closest('.comparador-dropdown-container')) {
dropdownBOpen = false;
}
};
document.addEventListener('click', handleClickOutside);
......@@ -614,26 +806,105 @@
{:else}
<!-- VISTA COMPARAR -->
<!-- Selectores de entidades -->
<!-- Selectores de entidades con búsqueda -->
<div class="comparador-selectors">
<div class="selector-group">
<div class="selector-group comparador-dropdown-container">
<span class="selector-dot" style="background: {colorA}"></span>
<select bind:value={entidadA}>
{#each entidadesComparables as ent}
<option value={ent.id} disabled={ent.id === entidadB}>{ent.nombre}</option>
<div class="relative" style="flex: 1;">
<button class="selector-btn comparador-btn" onclick={() => { dropdownAOpen = !dropdownAOpen; dropdownBOpen = false; }}>
<span class="truncate">
{entidadCompararA ? entidadCompararA.entidad_desc : 'Seleccionar entidad'}
</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
{#if dropdownAOpen}
<div class="dropdown-panel comparador-panel">
<div class="dropdown-search">
<input type="text" bind:value={searchA} placeholder="Buscar entidad..." class="search-input" />
</div>
<div class="dropdown-list">
{#each filteredEntidadesA as entity}
<button
class="dropdown-item"
class:active={entidadCompararA?.entidad === entity.entidad}
disabled={entidadCompararB?.entidad === entity.entidad}
onclick={() => selectEntidadA(entity)}
>
<span class="entity-code">{entity.entidad}</span>
<span class="truncate">{entity.entidad_desc}</span>
</button>
{/each}
</select>
</div>
</div>
{/if}
</div>
</div>
<span class="vs">vs</span>
<div class="selector-group">
<div class="selector-group comparador-dropdown-container">
<span class="selector-dot" style="background: {colorB}"></span>
<select bind:value={entidadB}>
{#each entidadesComparables as ent}
<option value={ent.id} disabled={ent.id === entidadA}>{ent.nombre}</option>
<div class="relative" style="flex: 1;">
<button class="selector-btn comparador-btn" onclick={() => { dropdownBOpen = !dropdownBOpen; dropdownAOpen = false; }}>
<span class="truncate">
{entidadCompararB ? entidadCompararB.entidad_desc : 'Seleccionar entidad'}
</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
{#if dropdownBOpen}
<div class="dropdown-panel comparador-panel">
<div class="dropdown-search">
<input type="text" bind:value={searchB} placeholder="Buscar entidad..." class="search-input" />
</div>
<div class="dropdown-list">
{#each filteredEntidadesB as entity}
<button
class="dropdown-item"
class:active={entidadCompararB?.entidad === entity.entidad}
disabled={entidadCompararA?.entidad === entity.entidad}
onclick={() => selectEntidadB(entity)}
>
<span class="entity-code">{entity.entidad}</span>
<span class="truncate">{entity.entidad_desc}</span>
</button>
{/each}
</select>
</div>
</div>
{/if}
</div>
</div>
</div>
<!-- Mensaje si no hay entidades seleccionadas -->
{#if !entidadCompararA || !entidadCompararB}
<div class="comparador-placeholder">
<p>Selecciona dos entidades para comparar su gasto en <strong>{objeto.nombre}</strong></p>
</div>
{:else if loadingComparacion}
<div class="comparador-placeholder">
<p>Cargando datos...</p>
</div>
{:else if añosComparacion.length === 0}
<div class="comparador-placeholder">
<p>Las entidades seleccionadas no tienen años en común para este objeto.</p>
</div>
{:else}
<!-- Selector de rango de años -->
{#if añosComunesTotales.length > 10}
<div class="rango-selector">
<span class="rango-label">Mostrar:</span>
<div class="rango-options">
<button class="rango-btn" class:active={rangoAños === '5'} onclick={() => rangoAños = '5'}>5 años</button>
<button class="rango-btn" class:active={rangoAños === '10'} onclick={() => rangoAños = '10'}>10 años</button>
<button class="rango-btn" class:active={rangoAños === '15'} onclick={() => rangoAños = '15'}>15 años</button>
<button class="rango-btn" class:active={rangoAños === 'todo'} onclick={() => rangoAños = 'todo'}>Todo ({añosComunesTotales.length})</button>
</div>
</div>
{/if}
<!-- Gráfico comparativo -->
<div class="chart-section">
......@@ -642,19 +913,20 @@
<span class="chart-period">{displayPeriodoComparar}</span>
</div>
<div class="chart-wrapper">
<div class="y-axis">
<span class="y-label">Bs {formatMonto(maxComparacion)}</span>
<div class="chart-wrapper chart-wrapper-scroll">
<div class="y-axis y-axis-sticky">
<span class="y-label">Bs {formatMonto(Math.round(maxComparacion))}</span>
<span class="y-label">Bs {formatMonto(Math.round(maxComparacion / 2))}</span>
<span class="y-label">Bs 0</span>
</div>
<div
class="chart chart-comparar"
class:chart-muchos-años={añosComparacion.length > 15}
onmouseleave={() => hoveredYearComparar = null}
role="group"
>
{#each años as año, i}
{#each añosComparacion as año, i}
<div
class="bar-group"
class:active={hoveredYearComparar === año}
......@@ -665,11 +937,11 @@
<div class="bars-pair">
<div
class="bar bar-a"
style="height: {(entA.datos[i].perCapita / maxComparacion) * 100}%; --bar-color: {colorA}"
style="height: {((datosGraficoA[i]?.per_capita || 0) / maxComparacion) * 100}%; --bar-color: {colorA}"
></div>
<div
class="bar bar-b"
style="height: {(entB.datos[i].perCapita / maxComparacion) * 100}%; --bar-color: {colorB}"
style="height: {((datosGraficoB[i]?.per_capita || 0) / maxComparacion) * 100}%; --bar-color: {colorB}"
></div>
</div>
<span class="bar-label visible" class:highlight={hoveredYearComparar === año}>{año}</span>
......@@ -679,8 +951,8 @@
</div>
<div class="chart-legend">
<span class="legend-item"><span class="legend-dot" style="background: {colorA}"></span>{entA.nombre}</span>
<span class="legend-item"><span class="legend-dot" style="background: {colorB}"></span>{entB.nombre}</span>
<span class="legend-item"><span class="legend-dot" style="background: {colorA}"></span>{entidadCompararA.entidad_desc}</span>
<span class="legend-item"><span class="legend-dot" style="background: {colorB}"></span>{entidadCompararB.entidad_desc}</span>
</div>
</div>
......@@ -689,34 +961,35 @@
<!-- Header -->
<div class="tabla-header">
<span class="tabla-label"></span>
<span class="tabla-val" style="color: {colorA}">{entA.nombre}</span>
<span class="tabla-val" style="color: {colorB}">{entB.nombre}</span>
<span class="tabla-val" style="color: {colorA}">{entidadCompararA.entidad_desc?.slice(0, 20)}{entidadCompararA.entidad_desc?.length > 20 ? '...' : ''}</span>
<span class="tabla-val" style="color: {colorB}">{entidadCompararB.entidad_desc?.slice(0, 20)}{entidadCompararB.entidad_desc?.length > 20 ? '...' : ''}</span>
</div>
<!-- Gasto -->
<div class="tabla-row">
<span class="tabla-label">{hoveredYearComparar ? `Gasto ${hoveredYearComparar}` : 'Gasto 2020-2024'}</span>
<span class="tabla-label">{hoveredYearComparar ? `Gasto ${hoveredYearComparar}` : `Gasto ${añosComparacion[0]}-${añosComparacion[añosComparacion.length-1]}`}</span>
<span class="tabla-val">Bs {formatMontoLargo(Math.round($twMontoA))}</span>
<span class="tabla-val">Bs {formatMontoLargo(Math.round($twMontoB))}</span>
</div>
<!-- Per cápita -->
<div class="tabla-row">
<span class="tabla-label">{hoveredYearComparar ? `Por persona ${hoveredYearComparar}` : 'Por persona (prom.)'}</span>
<span class="tabla-val">Bs {Math.round($twPerCapitaA).toLocaleString()}</span>
<span class="tabla-val">Bs {Math.round($twPerCapitaB).toLocaleString()}</span>
<span class="tabla-val">Bs {formatPerCapita($twPerCapitaA)}</span>
<span class="tabla-val">Bs {formatPerCapita($twPerCapitaB)}</span>
</div>
<!-- Porcentaje -->
<div class="tabla-row">
<span class="tabla-label">Peso sobre total</span>
<span class="tabla-val">{entA.porcentajeObjeto}%</span>
<span class="tabla-val">{entB.porcentajeObjeto}%</span>
<span class="tabla-label">{hoveredYearComparar ? `Peso en ${hoveredYearComparar}` : 'Peso sobre total'}</span>
<span class="tabla-val">{displayPropA.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span>
<span class="tabla-val">{displayPropB.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span>
</div>
<!-- Ranking -->
<div class="tabla-row">
<span class="tabla-label">Ranking</span>
<span class="tabla-val">#{entA.ranking}</span>
<span class="tabla-val">#{entB.ranking}</span>
<span class="tabla-label">{hoveredYearComparar ? `Ranking ${hoveredYearComparar}` : 'Ranking'}</span>
<span class="tabla-val">#{displayRankingA} de {displayNEntidadesA}</span>
<span class="tabla-val">#{displayRankingB} de {displayNEntidadesB}</span>
</div>
</div>
{/if}
{/if}
......@@ -1070,6 +1343,43 @@
gap: 0.75rem;
}
.chart-wrapper-scroll {
overflow-x: auto;
overflow-y: visible;
scrollbar-width: thin;
scrollbar-color: #2E2E2C transparent;
}
.chart-wrapper-scroll::-webkit-scrollbar {
height: 6px;
}
.chart-wrapper-scroll::-webkit-scrollbar-track {
background: transparent;
}
.chart-wrapper-scroll::-webkit-scrollbar-thumb {
background: #2E2E2C;
border-radius: 3px;
}
.y-axis-sticky {
position: sticky;
left: 0;
z-index: 10;
background: #0A0A0A;
padding-right: 0.5rem;
margin-right: -0.5rem;
}
.chart-muchos-años {
min-width: max-content;
}
.chart-muchos-años .bar-group {
min-width: 40px;
}
.y-axis {
display: flex;
flex-direction: column;
......@@ -1313,11 +1623,91 @@
/* Comparador inline - ajustado para 100vh */
.comparador-selectors {
display: flex;
align-items: center;
align-items: flex-start;
gap: 1rem;
margin-bottom: 0.75rem;
}
.comparador-dropdown-container {
position: relative;
}
.comparador-btn {
width: 100%;
justify-content: space-between;
}
.comparador-panel {
width: 100%;
min-width: 280px;
}
.comparador-placeholder {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
background: rgba(255,255,255,0.02);
border: 1px dashed #2E2E2C;
border-radius: 12px;
padding: 2rem;
text-align: center;
}
.comparador-placeholder p {
color: #5A5650;
font-size: 0.875rem;
margin: 0;
}
.comparador-placeholder strong {
color: #8A8578;
}
.rango-selector {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.rango-label {
font-size: 0.75rem;
color: #5A5650;
}
.rango-options {
display: flex;
gap: 0.25rem;
}
.rango-btn {
padding: 0.375rem 0.75rem;
font-size: 0.6875rem;
font-family: 'DM Mono', monospace;
color: #5A5650;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.rango-btn:hover {
color: #8A8578;
background: rgba(255,255,255,0.05);
}
.rango-btn.active {
color: #F5F0E8;
background: rgba(255,255,255,0.1);
}
.dropdown-item:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.selector-group {
flex: 1;
display: flex;
......@@ -1730,6 +2120,14 @@
color: #8A8578;
}
.tema-claro .y-axis-sticky {
background: #FAFAF8;
}
.tema-claro .chart-wrapper-scroll::-webkit-scrollbar-thumb {
background: #D5D5D0;
}
.tema-claro .chart {
border-left-color: #E5E5E0;
}
......@@ -1822,6 +2220,37 @@
outline: none;
}
.tema-claro .comparador-placeholder {
background: rgba(0,0,0,0.02);
border-color: #E5E5E0;
}
.tema-claro .comparador-placeholder p {
color: #8A8578;
}
.tema-claro .comparador-placeholder strong {
color: #5A5650;
}
.tema-claro .rango-label {
color: #8A8578;
}
.tema-claro .rango-btn {
color: #8A8578;
}
.tema-claro .rango-btn:hover {
color: #5A5650;
background: rgba(0,0,0,0.05);
}
.tema-claro .rango-btn.active {
color: #1A1A18;
background: rgba(0,0,0,0.08);
}
.tema-claro .vs {
color: #8A8578;
}
......