sper mejoras de UI

This commit is contained in:
2025-10-01 05:39:26 -06:00
parent d4f6333812
commit c4719d95cc
8 changed files with 1151 additions and 209 deletions

View File

@@ -8,18 +8,56 @@
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<UIcon name="i-lucide-calendar-heat" class="size-5 text-[#c08040]" />
<UIcon name="i-lucide-activity" class="size-5 text-[#c08040]" />
<h3 class="text-base font-semibold text-[var(--brand-text)]">Vista de Calor por Día</h3>
</div>
<UButton
@click="toggleFullscreen"
size="xs"
color="neutral"
variant="ghost"
:icon="isFullscreen ? 'i-lucide-minimize' : 'i-lucide-maximize'"
>
{{ isFullscreen ? 'Salir' : 'Pantalla completa' }}
</UButton>
<div class="flex items-center gap-2">
<!-- Controles de Zoom -->
<div
class="flex items-center gap-1 px-2 py-1 rounded-lg border border-[var(--brand-border)] bg-[var(--brand-bg-secondary)]"
title="También puedes usar Ctrl+Rueda del mouse para zoom"
>
<UButton
@click="zoomOut"
size="xs"
color="neutral"
variant="ghost"
icon="i-lucide-zoom-out"
:disabled="nivelZoom <= 0.5"
title="Alejar (Ctrl+Rueda abajo)"
/>
<span class="text-xs text-[var(--brand-text-muted)] px-2 min-w-[3rem] text-center">
{{ (nivelZoom * 100).toFixed(0) }}%
</span>
<UButton
@click="zoomIn"
size="xs"
color="neutral"
variant="ghost"
icon="i-lucide-zoom-in"
:disabled="nivelZoom >= 2"
title="Acercar (Ctrl+Rueda arriba)"
/>
<div class="w-px h-4 bg-[var(--brand-border)] mx-1" />
<UButton
@click="resetZoom"
size="xs"
color="neutral"
variant="ghost"
icon="i-lucide-refresh-cw"
title="Restablecer zoom (100%)"
/>
</div>
<UButton
@click="toggleFullscreen"
size="xs"
color="neutral"
variant="ghost"
:icon="isFullscreen ? 'i-lucide-minimize' : 'i-lucide-maximize'"
>
{{ isFullscreen ? 'Salir' : 'Pantalla completa' }}
</UButton>
</div>
</div>
<div class="flex items-center gap-3 flex-wrap">
<label class="text-xs text-[var(--brand-text-muted)]">Vista:</label>
@@ -230,7 +268,7 @@
class="flex flex-col items-center"
:style="{ width: `${barMaxWidth}px` }"
>
<div class="text-xs font-medium mb-1" :style="{ color: getCosechaColor(index) }">
<div class="text-xs font-medium mb-1" :style="{ color: getCosechaColor(cosecha.id) }">
{{ cosecha.label }}
</div>
<div class="text-xs text-[var(--brand-text-muted)]">
@@ -291,7 +329,7 @@
:style="{
width: getBarWidth(cosecha.valoresPorDia[dia - 1] || 0),
height: `${barHeight - 2}px`,
backgroundColor: getCosechaColor(cosechaIndex)
backgroundColor: getCosechaColor(cosecha.id)
}"
/>
</div>
@@ -353,7 +391,7 @@
>
<div
class="w-4 h-3 rounded"
:style="{ backgroundColor: getCosechaColor(index) }"
:style="{ backgroundColor: getCosechaColor(cosecha.id) }"
/>
<span class="text-xs text-[var(--brand-text-muted)]">{{ cosecha.label }}</span>
</div>
@@ -383,12 +421,12 @@
</span>
<div
class="w-3 h-3 rounded-full"
:style="{ backgroundColor: getCosechaColor(index) }"
:style="{ backgroundColor: getCosechaColor(cosecha.id) }"
/>
</div>
<!-- Valor total -->
<div class="text-2xl font-bold" :style="{ color: getCosechaColor(index) }">
<div class="text-2xl font-bold" :style="{ color: getCosechaColor(cosecha.id) }">
{{ formatTotal(cosecha.total) }}
</div>
@@ -398,7 +436,7 @@
class="h-full transition-all"
:style="{
width: `${(cosecha.total / maxTotalEnRango) * 100}%`,
backgroundColor: getCosechaColor(index)
backgroundColor: getCosechaColor(cosecha.id)
}"
/>
</div>
@@ -439,10 +477,23 @@ const props = defineProps<Props>()
// Obtener definiciones de cosechas
const cosechasDisponibles = inject<any[]>('cosechasDisponibles', [])
// Obtener configuración de estilos
const estilosGraficas = inject<any>('estilosGraficas', ref({
coloresCosechas: ['#c08040', '#d99a56', '#8b6f47', '#a0826e', '#b89968', '#f0c07c'],
anchoCelda: 80,
altoCelda: 6,
anchoMaxBarra: 300,
altoBarra: 8,
opacidadVacias: 0.05
}))
// Referencia al contenedor y estado de fullscreen
const cardContainer = ref<HTMLElement | null>(null)
const isFullscreen = ref(false)
// Estado de zoom (0.5x a 2x)
const nivelZoom = ref(1)
// Selección de vista, métrica y tipo
const vistaMode = ref<'heatmap' | 'barras'>('barras')
const metrica = ref<'peso' | 'cantidad' | 'inversion'>('peso')
@@ -452,13 +503,27 @@ const tipoSeleccionado = ref<string>('todos')
const primerDiaSeleccionado = ref<number | null>(null)
const rangoSeleccionado = ref<{ diaDesde: number, diaHasta: number, diasTotales: number } | null>(null)
// Configuración de celdas (heatmap)
const cellWidth = 80
const cellHeight = 6
// Configuración de barras
const barMaxWidth = 300
const barHeight = 8
// Configuración de celdas y barras (ahora reactivas desde la configuración y zoom)
const cellWidth = computed(() => {
const value = estilosGraficas.value.anchoCelda
const baseValue = typeof value === 'number' ? value : 80
return Math.round(baseValue * nivelZoom.value)
})
const cellHeight = computed(() => {
const value = estilosGraficas.value.altoCelda
const baseValue = typeof value === 'number' ? value : 6
return Math.round(baseValue * nivelZoom.value)
})
const barMaxWidth = computed(() => {
const value = estilosGraficas.value.anchoMaxBarra
const baseValue = typeof value === 'number' ? value : 300
return Math.round(baseValue * nivelZoom.value)
})
const barHeight = computed(() => {
const value = estilosGraficas.value.altoBarra
const baseValue = typeof value === 'number' ? value : 8
return Math.round(baseValue * nivelZoom.value)
})
// Tooltip state
const tooltipVisible = ref(false)
@@ -630,7 +695,7 @@ function getCellColor(valor: number, max: number): string {
// Escala de color desde transparente a naranja intenso
if (intensity === 0) {
return 'rgba(192, 128, 64, 0.05)' // Muy suave para vacío
return `rgba(192, 128, 64, ${estilosGraficas.value.opacidadVacias})` // Opacidad configurable
}
// Gradiente de naranja con opacidad basada en intensidad
@@ -642,26 +707,42 @@ function getCellColor(valor: number, max: number): string {
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
// Colores sólidos por cosecha (vista barras)
const cosechaColors = [
'#c08040', // Naranja primario
'#d99a56', // Naranja claro
'#8b6f47', // Marrón
'#a0826e', // Café claro
'#b89968', // Beige
'#f0c07c' // Amarillo dorado
]
// Mapeo de cosechas a índices fijos
const cosechaColorMap: Record<string, number> = {
'cosecha-20-21': 0,
'cosecha-21-22': 1,
'cosecha-22-23': 2,
'cosecha-23-24': 3,
'cosecha-24-25': 4,
'cosecha-25-26': 5
}
// Función para obtener color fijo de cosecha
function getCosechaColor(index: number): string {
return cosechaColors[index % cosechaColors.length]
// Función para obtener color fijo de cosecha según su ID (no su orden de selección)
function getCosechaColor(cosechaIdOrIndex: string | number): string {
const colores = estilosGraficas.value.coloresCosechas
// Si es un string, es un ID de cosecha
if (typeof cosechaIdOrIndex === 'string') {
const index = cosechaColorMap[cosechaIdOrIndex] ?? 0
return colores[index % colores.length]
}
// Si es un número, buscar el ID de la cosecha en datosCosechas
const cosecha = datosCosechas.value[cosechaIdOrIndex]
if (cosecha?.id) {
const index = cosechaColorMap[cosecha.id] ?? cosechaIdOrIndex
return colores[index % colores.length]
}
// Fallback al índice directo
return colores[cosechaIdOrIndex % colores.length]
}
// Función para calcular ancho de barra según valor
function getBarWidth(valor: number): string {
if (maxValorDia.value === 0) return '0px'
const percentage = (valor / maxValorDia.value) * 100
const width = (percentage / 100) * barMaxWidth
const width = (percentage / 100) * barMaxWidth.value // .value es necesario en el script setup
return `${width}px`
}
@@ -798,6 +879,23 @@ function formatRangoFecha(diaRelativo: number): string {
})
}
// Funciones de zoom
function zoomIn() {
if (nivelZoom.value < 2) {
nivelZoom.value = Math.min(2, nivelZoom.value + 0.25)
}
}
function zoomOut() {
if (nivelZoom.value > 0.5) {
nivelZoom.value = Math.max(0.5, nivelZoom.value - 0.25)
}
}
function resetZoom() {
nivelZoom.value = 1
}
// Funciones de pantalla completa
async function toggleFullscreen() {
if (!cardContainer.value) return
@@ -822,13 +920,40 @@ function handleFullscreenChange() {
isFullscreen.value = !!document.fullscreenElement
}
// Zoom con rueda del mouse + Ctrl
function handleWheel(event: WheelEvent) {
if (event.ctrlKey || event.metaKey) {
event.preventDefault()
if (event.deltaY < 0) {
// Scroll up = zoom in
zoomIn()
} else {
// Scroll down = zoom out
zoomOut()
}
}
}
// Montar y desmontar listeners
onMounted(() => {
document.addEventListener('fullscreenchange', handleFullscreenChange)
// Agregar listener para zoom con rueda del mouse
const container = cardContainer.value?.$el
if (container) {
container.addEventListener('wheel', handleWheel, { passive: false })
}
})
onUnmounted(() => {
document.removeEventListener('fullscreenchange', handleFullscreenChange)
// Remover listener de zoom
const container = cardContainer.value?.$el
if (container) {
container.removeEventListener('wheel', handleWheel)
}
})
</script>