sper mejoras de UI
This commit is contained in:
@@ -180,6 +180,11 @@ 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']
|
||||
}))
|
||||
|
||||
// Dimensiones del SVG
|
||||
const svgWidth = 900
|
||||
const svgHeight = 450
|
||||
@@ -303,11 +308,35 @@ function getLinePath(puntos: Array<{ diaRelativo: number, valor: number }>): str
|
||||
return path
|
||||
}
|
||||
|
||||
// Colores para las cosechas
|
||||
const cosechaColors = ['#c08040', '#d99a56', '#f0c07c', '#8b6f47', '#a0826e', '#b89968']
|
||||
// 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
|
||||
}
|
||||
|
||||
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 evolucionCosechas
|
||||
const cosecha = evolucionCosechas.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]
|
||||
}
|
||||
|
||||
function formatValue(value: number): string {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -188,6 +188,11 @@ 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']
|
||||
}))
|
||||
|
||||
// Dimensiones del SVG
|
||||
const svgWidth = 500
|
||||
const svgHeight = 350
|
||||
@@ -287,10 +292,34 @@ function getBarHeight(value: number, max: number): number {
|
||||
return (value / max) * chartHeight
|
||||
}
|
||||
|
||||
// Colores para las cosechas
|
||||
const cosechaColors = ['#c08040', '#d99a56', '#f0c07c', '#8b6f47', '#a0826e', '#b89968']
|
||||
// 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
|
||||
}
|
||||
|
||||
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 datosPorTipo
|
||||
const cosecha = datosPorTipo.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]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -171,6 +171,11 @@ 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']
|
||||
}))
|
||||
|
||||
// Dimensiones del SVG
|
||||
const svgWidth = 400
|
||||
const svgHeight = 300
|
||||
@@ -245,10 +250,35 @@ function getBarHeight(value: number, max: number): number {
|
||||
}
|
||||
|
||||
// Colores para las barras
|
||||
const colors = ['#c08040', '#d99a56', '#f0c07c', '#8b6f47', '#a0826e', '#b89968']
|
||||
// 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
|
||||
}
|
||||
|
||||
function getColor(index: number): string {
|
||||
return colors[index % colors.length]
|
||||
// Función para obtener color fijo de cosecha según su ID (no su orden de selección)
|
||||
function getColor(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]
|
||||
}
|
||||
|
||||
function formatMoney(value: number): string {
|
||||
|
||||
@@ -1,138 +1,193 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Fila 1: Selector de Clientes -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text-muted)] uppercase tracking-wide">
|
||||
Clientes
|
||||
</h3>
|
||||
<ClienteSelector
|
||||
:clientes="clientes"
|
||||
:selected-ids="selectedClienteIds"
|
||||
@update:selected-ids="emit('update:selectedClienteIds', $event)"
|
||||
/>
|
||||
<UButton
|
||||
v-if="selectedClienteIds.length > 0"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
@click="emit('update:selectedClienteIds', [])"
|
||||
block
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-lucide-x" />
|
||||
</template>
|
||||
Limpiar selección
|
||||
</UButton>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<div v-if="selectedClienteIds.length === 0" class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text-muted)] uppercase tracking-wide">
|
||||
Clientes
|
||||
</h3>
|
||||
<ClienteSelector
|
||||
:clientes="clientes"
|
||||
:selected-ids="selectedClienteIds"
|
||||
@update:selected-ids="emit('update:selectedClienteIds', $event)"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Fila 2: Selector de Rango de Fechas -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text-muted)] uppercase tracking-wide">
|
||||
Rango de Fechas
|
||||
</h3>
|
||||
<DateRangeSelector
|
||||
:selected-preset="selectedPreset"
|
||||
:fecha-desde="fechaDesde"
|
||||
:fecha-hasta="fechaHasta"
|
||||
@update:selected-preset="emit('update:selectedPreset', $event)"
|
||||
@update:fecha-desde="emit('update:fechaDesde', $event)"
|
||||
@update:fecha-hasta="emit('update:fechaHasta', $event)"
|
||||
/>
|
||||
<UButton
|
||||
v-if="fechaDesde || fechaHasta"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
@click="clearDates"
|
||||
block
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-lucide-x" />
|
||||
</template>
|
||||
Limpiar fechas
|
||||
</UButton>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<div v-if="!fechaDesde && !fechaHasta" class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text-muted)] uppercase tracking-wide">
|
||||
Rango de Fechas
|
||||
</h3>
|
||||
<DateRangeSelector
|
||||
:selected-preset="selectedPreset"
|
||||
:fecha-desde="fechaDesde"
|
||||
:fecha-hasta="fechaHasta"
|
||||
@update:selected-preset="emit('update:selectedPreset', $event)"
|
||||
@update:fecha-desde="emit('update:fechaDesde', $event)"
|
||||
@update:fecha-hasta="emit('update:fechaHasta', $event)"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Fila 3: Filtros Avanzados (grid de 4 columnas) -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-if="hasAvailableAdvancedFilters" class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text-muted)] uppercase tracking-wide">
|
||||
Filtros Avanzados
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<!-- Tipo de Café -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">Tipo</label>
|
||||
<UInputMenu
|
||||
v-model="selectedTipos"
|
||||
:items="tiposOptions"
|
||||
value-key="value"
|
||||
multiple
|
||||
placeholder="Todos"
|
||||
size="xs"
|
||||
icon="i-lucide-coffee"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div v-if="selectedTipos.length === 0" class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">Tipo</label>
|
||||
<UInputMenu
|
||||
v-model="selectedTipos"
|
||||
:items="tiposOptions"
|
||||
value-key="value"
|
||||
multiple
|
||||
placeholder="Todos"
|
||||
size="xs"
|
||||
icon="i-lucide-coffee"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Estado -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">Estado</label>
|
||||
<UInputMenu
|
||||
v-model="selectedEstados"
|
||||
:items="estadosOptions"
|
||||
value-key="value"
|
||||
multiple
|
||||
placeholder="Todos"
|
||||
size="xs"
|
||||
icon="i-lucide-check-circle"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div v-if="selectedEstados.length === 0" class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">Estado</label>
|
||||
<UInputMenu
|
||||
v-model="selectedEstados"
|
||||
:items="estadosOptions"
|
||||
value-key="value"
|
||||
multiple
|
||||
placeholder="Todos"
|
||||
size="xs"
|
||||
icon="i-lucide-check-circle"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Ubicación -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">Ubicación</label>
|
||||
<UInputMenu
|
||||
v-model="selectedUbicaciones"
|
||||
:items="ubicacionesOptions"
|
||||
value-key="value"
|
||||
multiple
|
||||
placeholder="Todas"
|
||||
size="xs"
|
||||
icon="i-lucide-map-pin"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div v-if="selectedUbicaciones.length === 0" class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">Ubicación</label>
|
||||
<UInputMenu
|
||||
v-model="selectedUbicaciones"
|
||||
:items="ubicacionesOptions"
|
||||
value-key="value"
|
||||
multiple
|
||||
placeholder="Todas"
|
||||
size="xs"
|
||||
icon="i-lucide-map-pin"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Calidad -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">Calidad</label>
|
||||
<UInputMenu
|
||||
v-model="selectedCalidades"
|
||||
:items="calidadesOptions"
|
||||
value-key="value"
|
||||
multiple
|
||||
placeholder="Todas"
|
||||
size="xs"
|
||||
icon="i-lucide-star"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div v-if="selectedCalidades.length === 0" class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">Calidad</label>
|
||||
<UInputMenu
|
||||
v-model="selectedCalidades"
|
||||
:items="calidadesOptions"
|
||||
value-key="value"
|
||||
multiple
|
||||
placeholder="Todas"
|
||||
size="xs"
|
||||
icon="i-lucide-star"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox de incluir anulados -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<UCheckbox v-model="includeAnulados" label="Incluir anulados" size="sm" />
|
||||
<UAlert
|
||||
v-if="includeAnulados"
|
||||
color="error"
|
||||
variant="soft"
|
||||
icon="i-lucide-alert-triangle"
|
||||
title="Incluir anulados activado"
|
||||
description="Los cálculos incluyen registros anulados."
|
||||
/>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<div v-if="!includeAnulados" class="flex flex-col gap-3">
|
||||
<UCheckbox v-model="includeAnulados" label="Incluir anulados" size="sm" />
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Botón "Sin filtros" -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<div v-if="!noFilter && !hasAnyActiveFilter" class="flex flex-col gap-3">
|
||||
<UButton
|
||||
@click="handleNoFilter"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
color="neutral"
|
||||
icon="i-lucide-ban"
|
||||
block
|
||||
>
|
||||
Sin filtros
|
||||
</UButton>
|
||||
<p class="text-xs text-[var(--brand-text-muted)] text-center">
|
||||
Confirma que no quieres aplicar ningún filtro
|
||||
</p>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -154,6 +209,7 @@ interface Props {
|
||||
ubicacionesOptions: any[]
|
||||
calidadesOptions: any[]
|
||||
includeAnulados: boolean
|
||||
noFilter: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -168,6 +224,8 @@ const emit = defineEmits<{
|
||||
'update:selectedUbicaciones': [value: string[]]
|
||||
'update:selectedCalidades': [value: string[]]
|
||||
'update:includeAnulados': [value: boolean]
|
||||
'update:noFilter': [value: boolean]
|
||||
'hideFiltros': []
|
||||
}>()
|
||||
|
||||
const selectedTipos = computed({
|
||||
@@ -195,9 +253,37 @@ const includeAnulados = computed({
|
||||
set: (value) => emit('update:includeAnulados', value)
|
||||
})
|
||||
|
||||
const noFilter = computed({
|
||||
get: () => props.noFilter,
|
||||
set: (value) => emit('update:noFilter', value)
|
||||
})
|
||||
|
||||
const hasAvailableAdvancedFilters = computed(() => {
|
||||
return selectedTipos.value.length === 0 ||
|
||||
selectedEstados.value.length === 0 ||
|
||||
selectedUbicaciones.value.length === 0 ||
|
||||
selectedCalidades.value.length === 0
|
||||
})
|
||||
|
||||
const hasAnyActiveFilter = computed(() => {
|
||||
return props.selectedClienteIds.length > 0 ||
|
||||
props.fechaDesde !== null ||
|
||||
props.fechaHasta !== null ||
|
||||
props.selectedTipos.length > 0 ||
|
||||
props.selectedEstados.length > 0 ||
|
||||
props.selectedUbicaciones.length > 0 ||
|
||||
props.selectedCalidades.length > 0 ||
|
||||
props.includeAnulados
|
||||
})
|
||||
|
||||
function clearDates() {
|
||||
emit('update:fechaDesde', null)
|
||||
emit('update:fechaHasta', null)
|
||||
emit('update:selectedPreset', '')
|
||||
}
|
||||
|
||||
function handleNoFilter() {
|
||||
emit('update:noFilter', true)
|
||||
emit('hideFiltros')
|
||||
}
|
||||
</script>
|
||||
|
||||
293
nuxt4-app/app/components/ingresos/FiltrosActivos.vue
Normal file
293
nuxt4-app/app/components/ingresos/FiltrosActivos.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<UCard class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-lucide-filter" class="size-4 text-[#c08040]" />
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text)]">Filtros Activos</h3>
|
||||
<span class="text-xs text-[var(--brand-text-muted)]">({{ totalFiltros }})</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
@click="showFiltros"
|
||||
icon="i-lucide-plus"
|
||||
>
|
||||
Agregar filtros
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="totalFiltros > 0"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
@click="limpiarTodos"
|
||||
icon="i-lucide-x"
|
||||
>
|
||||
Limpiar todos
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="totalFiltros === 0" class="text-center py-4 text-sm text-[var(--brand-text-muted)]">
|
||||
No hay filtros activos
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-wrap gap-2">
|
||||
<!-- Clientes -->
|
||||
<div
|
||||
v-for="clienteId in selectedClienteIds"
|
||||
:key="`cliente-${clienteId}`"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-blue-500/10 text-blue-400 border border-blue-500/30 transition-all hover:bg-blue-500/20"
|
||||
>
|
||||
<UIcon name="i-lucide-user" class="size-3" />
|
||||
<span>{{ getNombreCliente(clienteId) }}</span>
|
||||
<button
|
||||
@click="removeCliente(clienteId)"
|
||||
class="ml-1 hover:text-blue-300 transition-colors"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Botón para agregar más clientes -->
|
||||
<button
|
||||
v-if="selectedClienteIds.length > 0"
|
||||
@click="showClienteSelector"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-blue-500/5 text-blue-400 border border-blue-500/20 border-dashed transition-all hover:bg-blue-500/10 hover:border-blue-500/40"
|
||||
title="Agregar más clientes"
|
||||
>
|
||||
<UIcon name="i-lucide-user-plus" class="size-3" />
|
||||
<span>Agregar cliente</span>
|
||||
</button>
|
||||
|
||||
<!-- Fechas -->
|
||||
<div
|
||||
v-if="fechaDesde || fechaHasta"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-purple-500/10 text-purple-400 border border-purple-500/30 transition-all hover:bg-purple-500/20"
|
||||
>
|
||||
<UIcon name="i-lucide-calendar" class="size-3" />
|
||||
<span>{{ fechaDesde || '—' }} → {{ fechaHasta || '—' }}</span>
|
||||
<button
|
||||
@click="removeFechas"
|
||||
class="ml-1 hover:text-purple-300 transition-colors"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tipos -->
|
||||
<div
|
||||
v-for="tipo in selectedTipos"
|
||||
:key="`tipo-${tipo}`"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-orange-500/10 text-orange-400 border border-orange-500/30 transition-all hover:bg-orange-500/20"
|
||||
>
|
||||
<UIcon name="i-lucide-coffee" class="size-3" />
|
||||
<span>{{ getLabelTipo(tipo) }}</span>
|
||||
<button
|
||||
@click="removeTipo(tipo)"
|
||||
class="ml-1 hover:text-orange-300 transition-colors"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Estados -->
|
||||
<div
|
||||
v-for="estado in selectedEstados"
|
||||
:key="`estado-${estado}`"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/30 transition-all hover:bg-green-500/20"
|
||||
>
|
||||
<UIcon name="i-lucide-check-circle" class="size-3" />
|
||||
<span>{{ getLabelEstado(estado) }}</span>
|
||||
<button
|
||||
@click="removeEstado(estado)"
|
||||
class="ml-1 hover:text-green-300 transition-colors"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Ubicaciones -->
|
||||
<div
|
||||
v-for="ubicacion in selectedUbicaciones"
|
||||
:key="`ubicacion-${ubicacion}`"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-cyan-500/10 text-cyan-400 border border-cyan-500/30 transition-all hover:bg-cyan-500/20"
|
||||
>
|
||||
<UIcon name="i-lucide-map-pin" class="size-3" />
|
||||
<span>{{ ubicacion }}</span>
|
||||
<button
|
||||
@click="removeUbicacion(ubicacion)"
|
||||
class="ml-1 hover:text-cyan-300 transition-colors"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Calidades -->
|
||||
<div
|
||||
v-for="calidad in selectedCalidades"
|
||||
:key="`calidad-${calidad}`"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-yellow-500/10 text-yellow-400 border border-yellow-500/30 transition-all hover:bg-yellow-500/20"
|
||||
>
|
||||
<UIcon name="i-lucide-star" class="size-3" />
|
||||
<span>{{ calidad }}</span>
|
||||
<button
|
||||
@click="removeCalidad(calidad)"
|
||||
class="ml-1 hover:text-yellow-300 transition-colors"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Incluir Anulados -->
|
||||
<div
|
||||
v-if="includeAnulados"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-red-500/10 text-red-400 border border-red-500/30 transition-all hover:bg-red-500/20"
|
||||
>
|
||||
<UIcon name="i-lucide-alert-triangle" class="size-3" />
|
||||
<span>Con anulados</span>
|
||||
<button
|
||||
@click="removeAnulados"
|
||||
class="ml-1 hover:text-red-300 transition-colors"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sin Filtros -->
|
||||
<div
|
||||
v-if="noFilter"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-gray-500/10 text-gray-400 border border-gray-500/30 transition-all hover:bg-gray-500/20"
|
||||
>
|
||||
<UIcon name="i-lucide-ban" class="size-3" />
|
||||
<span>Sin filtros</span>
|
||||
<button
|
||||
@click="removeNoFilter"
|
||||
class="ml-1 hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
clientes: any[]
|
||||
selectedClienteIds: number[]
|
||||
fechaDesde: string | null
|
||||
fechaHasta: string | null
|
||||
selectedTipos: string[]
|
||||
selectedEstados: string[]
|
||||
selectedUbicaciones: string[]
|
||||
selectedCalidades: string[]
|
||||
includeAnulados: boolean
|
||||
noFilter: boolean
|
||||
tiposOptions: any[]
|
||||
estadosOptions: any[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:selectedClienteIds': [value: number[]]
|
||||
'update:fechaDesde': [value: string | null]
|
||||
'update:fechaHasta': [value: string | null]
|
||||
'update:selectedPreset': [value: string]
|
||||
'update:selectedTipos': [value: string[]]
|
||||
'update:selectedEstados': [value: string[]]
|
||||
'update:selectedUbicaciones': [value: string[]]
|
||||
'update:selectedCalidades': [value: string[]]
|
||||
'update:includeAnulados': [value: boolean]
|
||||
'update:noFilter': [value: boolean]
|
||||
'showClienteSelector': []
|
||||
'showFiltros': []
|
||||
}>()
|
||||
|
||||
const totalFiltros = computed(() => {
|
||||
let count = 0
|
||||
count += props.selectedClienteIds.length
|
||||
count += (props.fechaDesde || props.fechaHasta) ? 1 : 0
|
||||
count += props.selectedTipos.length
|
||||
count += props.selectedEstados.length
|
||||
count += props.selectedUbicaciones.length
|
||||
count += props.selectedCalidades.length
|
||||
count += props.includeAnulados ? 1 : 0
|
||||
count += props.noFilter ? 1 : 0
|
||||
return count
|
||||
})
|
||||
|
||||
function showClienteSelector() {
|
||||
emit('showClienteSelector')
|
||||
}
|
||||
|
||||
function showFiltros() {
|
||||
emit('showFiltros')
|
||||
}
|
||||
|
||||
function getNombreCliente(id: number): string {
|
||||
const cliente = props.clientes.find(c => c.id === id)
|
||||
return cliente?.name || `Cliente ${id}`
|
||||
}
|
||||
|
||||
function getLabelTipo(value: string): string {
|
||||
const tipo = props.tiposOptions.find(t => t.value === value)
|
||||
return tipo?.label || value
|
||||
}
|
||||
|
||||
function getLabelEstado(value: string): string {
|
||||
const estado = props.estadosOptions.find(e => e.value === value)
|
||||
return estado?.label || value
|
||||
}
|
||||
|
||||
function removeCliente(id: number) {
|
||||
emit('update:selectedClienteIds', props.selectedClienteIds.filter(cid => cid !== id))
|
||||
}
|
||||
|
||||
function removeFechas() {
|
||||
emit('update:fechaDesde', null)
|
||||
emit('update:fechaHasta', null)
|
||||
emit('update:selectedPreset', '')
|
||||
}
|
||||
|
||||
function removeTipo(tipo: string) {
|
||||
emit('update:selectedTipos', props.selectedTipos.filter(t => t !== tipo))
|
||||
}
|
||||
|
||||
function removeEstado(estado: string) {
|
||||
emit('update:selectedEstados', props.selectedEstados.filter(e => e !== estado))
|
||||
}
|
||||
|
||||
function removeUbicacion(ubicacion: string) {
|
||||
emit('update:selectedUbicaciones', props.selectedUbicaciones.filter(u => u !== ubicacion))
|
||||
}
|
||||
|
||||
function removeCalidad(calidad: string) {
|
||||
emit('update:selectedCalidades', props.selectedCalidades.filter(c => c !== calidad))
|
||||
}
|
||||
|
||||
function removeAnulados() {
|
||||
emit('update:includeAnulados', false)
|
||||
}
|
||||
|
||||
function removeNoFilter() {
|
||||
emit('update:noFilter', false)
|
||||
}
|
||||
|
||||
function limpiarTodos() {
|
||||
emit('update:selectedClienteIds', [])
|
||||
emit('update:fechaDesde', null)
|
||||
emit('update:fechaHasta', null)
|
||||
emit('update:selectedPreset', '')
|
||||
emit('update:selectedTipos', [])
|
||||
emit('update:selectedEstados', [])
|
||||
emit('update:selectedUbicaciones', [])
|
||||
emit('update:selectedCalidades', [])
|
||||
emit('update:includeAnulados', false)
|
||||
emit('update:noFilter', false)
|
||||
}
|
||||
</script>
|
||||
@@ -10,6 +10,17 @@
|
||||
|
||||
<template #right>
|
||||
<div class="flex items-center gap-3">
|
||||
<UButton
|
||||
@click="mostrarConfiguracion = !mostrarConfiguracion"
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-lucide-palette"
|
||||
:class="{ 'bg-[#c08040]/20 text-[#c08040]': mostrarConfiguracion }"
|
||||
>
|
||||
Configurar estilos
|
||||
</UButton>
|
||||
<div class="w-px h-4 bg-[var(--brand-border)]" />
|
||||
<USwitch
|
||||
v-model="pageSections.heatmap"
|
||||
size="xs"
|
||||
@@ -113,6 +124,188 @@
|
||||
</UCard>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Configuración de Estilos -->
|
||||
<UModal v-model:open="mostrarConfiguracion">
|
||||
<UCard class="brand-card">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-lucide-palette" class="size-5 text-[#c08040]" />
|
||||
<h3 id="modal-title" class="text-base font-semibold text-[var(--brand-text)]">Configuración de Estilos</h3>
|
||||
</div>
|
||||
<UButton
|
||||
@click="resetearConfiguracion"
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-lucide-rotate-ccw"
|
||||
>
|
||||
Resetear
|
||||
</UButton>
|
||||
</div>
|
||||
<p id="modal-description" class="sr-only">
|
||||
Personaliza los colores, dimensiones y opacidad de las gráficas
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Sección: Colores de Cosechas -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-[var(--brand-text)] mb-3 flex items-center gap-2">
|
||||
<UIcon name="i-lucide-droplets" class="size-4" />
|
||||
Colores de Cosechas (por año)
|
||||
</h4>
|
||||
<p class="text-xs text-[var(--brand-text-muted)] mb-3">
|
||||
Los colores son fijos para cada cosecha, sin importar el orden en que se seleccionen
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="(cosecha, index) in cosechasDisponibles"
|
||||
:key="`color-${cosecha.id}`"
|
||||
class="flex items-center gap-3 p-3 rounded-lg border border-[var(--brand-border)] bg-[var(--brand-bg-secondary)]"
|
||||
>
|
||||
<label class="text-xs text-[var(--brand-text-muted)] flex-1">
|
||||
{{ cosecha.label }}
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
v-model="estilosGraficas.coloresCosechas[index]"
|
||||
class="w-10 h-8 rounded cursor-pointer border border-[var(--brand-border)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección: Dimensiones -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-[var(--brand-text)] mb-3 flex items-center gap-2">
|
||||
<UIcon name="i-lucide-ruler" class="size-4" />
|
||||
Dimensiones
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Ancho de celda (Heatmap) -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">
|
||||
Ancho de celda (Heatmap)
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="estilosGraficas.anchoCelda"
|
||||
min="40"
|
||||
max="150"
|
||||
step="10"
|
||||
class="flex-1"
|
||||
/>
|
||||
<span class="text-xs text-[var(--brand-text)] w-12 text-right">
|
||||
{{ estilosGraficas.anchoCelda }}px
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alto de celda (Heatmap) -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">
|
||||
Alto de celda (Heatmap)
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="estilosGraficas.altoCelda"
|
||||
min="4"
|
||||
max="20"
|
||||
step="1"
|
||||
class="flex-1"
|
||||
/>
|
||||
<span class="text-xs text-[var(--brand-text)] w-12 text-right">
|
||||
{{ estilosGraficas.altoCelda }}px
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ancho máximo de barra -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">
|
||||
Ancho máximo de barra
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="estilosGraficas.anchoMaxBarra"
|
||||
min="150"
|
||||
max="500"
|
||||
step="25"
|
||||
class="flex-1"
|
||||
/>
|
||||
<span class="text-xs text-[var(--brand-text)] w-12 text-right">
|
||||
{{ estilosGraficas.anchoMaxBarra }}px
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alto de barra -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">
|
||||
Alto de barra
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="estilosGraficas.altoBarra"
|
||||
min="6"
|
||||
max="24"
|
||||
step="2"
|
||||
class="flex-1"
|
||||
/>
|
||||
<span class="text-xs text-[var(--brand-text)] w-12 text-right">
|
||||
{{ estilosGraficas.altoBarra }}px
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección: Opacidad -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-[var(--brand-text)] mb-3 flex items-center gap-2">
|
||||
<UIcon name="i-lucide-sun-dim" class="size-4" />
|
||||
Opacidad
|
||||
</h4>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-[var(--brand-text-muted)]">
|
||||
Opacidad de celdas vacías (Heatmap)
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="estilosGraficas.opacidadVacias"
|
||||
min="0"
|
||||
max="0.3"
|
||||
step="0.05"
|
||||
class="flex-1"
|
||||
/>
|
||||
<span class="text-xs text-[var(--brand-text)] w-12 text-right">
|
||||
{{ (estilosGraficas.opacidadVacias * 100).toFixed(0) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton
|
||||
@click="mostrarConfiguracion = false"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
>
|
||||
Cerrar
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -133,6 +326,62 @@ const pageSections = ref({
|
||||
porTipo: false
|
||||
})
|
||||
|
||||
// Estado del modal de configuración
|
||||
const mostrarConfiguracion = ref(false)
|
||||
|
||||
// Configuración de estilos por defecto
|
||||
const estilosGraficasDefault = {
|
||||
coloresCosechas: [
|
||||
'#c08040', // Cosecha 20-21
|
||||
'#d99a56', // Cosecha 21-22
|
||||
'#8b6f47', // Cosecha 22-23
|
||||
'#a0826e', // Cosecha 23-24
|
||||
'#b89968', // Cosecha 24-25
|
||||
'#f0c07c' // Cosecha 25-26
|
||||
],
|
||||
anchoCelda: 80,
|
||||
altoCelda: 6,
|
||||
anchoMaxBarra: 300,
|
||||
altoBarra: 8,
|
||||
opacidadVacias: 0.05
|
||||
}
|
||||
|
||||
// Cargar configuración desde cookies
|
||||
function cargarConfiguracionDesdeCookies() {
|
||||
const cookie = useCookie('estilos-graficas', {
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 año
|
||||
sameSite: 'lax'
|
||||
})
|
||||
|
||||
if (cookie.value) {
|
||||
try {
|
||||
return { ...estilosGraficasDefault, ...cookie.value }
|
||||
} catch (e) {
|
||||
console.error('Error al cargar configuración desde cookies:', e)
|
||||
return { ...estilosGraficasDefault }
|
||||
}
|
||||
}
|
||||
|
||||
return { ...estilosGraficasDefault }
|
||||
}
|
||||
|
||||
// Configuración reactiva de estilos
|
||||
const estilosGraficas = ref(cargarConfiguracionDesdeCookies())
|
||||
|
||||
// Guardar en cookies cuando cambie la configuración
|
||||
watch(estilosGraficas, (newValue) => {
|
||||
const cookie = useCookie('estilos-graficas', {
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 año
|
||||
sameSite: 'lax'
|
||||
})
|
||||
cookie.value = newValue
|
||||
}, { deep: true })
|
||||
|
||||
// Función para resetear la configuración
|
||||
function resetearConfiguracion() {
|
||||
estilosGraficas.value = { ...estilosGraficasDefault }
|
||||
}
|
||||
|
||||
// Definición de cosechas disponibles
|
||||
const cosechasDisponibles = [
|
||||
{ id: 'cosecha-20-21', label: 'Cosecha 20-21', periodo: 'Sep 2020 - Sep 2021', fechaInicio: '2020-09-25', fechaFin: '2021-09-24' },
|
||||
@@ -156,6 +405,7 @@ const ingresos = computed(() => ingresosStore.allRecords as IngresoRecord[])
|
||||
const loading = computed(() => ingresosStore.isLoading)
|
||||
const error = computed(() => ingresosStore.error)
|
||||
|
||||
// Exportar cosechas para los componentes
|
||||
// Exportar cosechas y configuración para los componentes
|
||||
provide('cosechasDisponibles', cosechasDisponibles)
|
||||
provide('estilosGraficas', estilosGraficas)
|
||||
</script>
|
||||
|
||||
@@ -1,26 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<!-- Toolbar con switches -->
|
||||
<!-- Toolbar con switches de secciones -->
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<div class="flex items-center gap-4">
|
||||
<USwitch
|
||||
v-model="showFiltros"
|
||||
checked-icon="i-lucide-eye"
|
||||
unchecked-icon="i-lucide-eye-off"
|
||||
label="Filtros"
|
||||
size="sm"
|
||||
/>
|
||||
<USwitch
|
||||
v-model="showMetadatos"
|
||||
checked-icon="i-lucide-eye"
|
||||
unchecked-icon="i-lucide-eye-off"
|
||||
label="Metadatos"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<div class="flex items-center gap-3">
|
||||
<USwitch
|
||||
@@ -80,7 +61,13 @@
|
||||
|
||||
<!-- Main Content -->
|
||||
<template v-else>
|
||||
<!-- Metadatos Card Colapsable -->
|
||||
<!-- Metadatos Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<MetadatosCard v-if="ingresosMetadata" :metadata="ingresosMetadata" :compact="true" />
|
||||
<MetadatosCard v-if="clientesMetadata" :metadata="clientesMetadata" :compact="true" />
|
||||
</div>
|
||||
|
||||
<!-- Filtros Activos Badge Card -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
@@ -89,10 +76,63 @@
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<div v-show="!metadatosCollapsed" class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<MetadatosCard v-if="ingresosMetadata" :metadata="ingresosMetadata" :compact="true" />
|
||||
<MetadatosCard v-if="clientesMetadata" :metadata="clientesMetadata" :compact="true" />
|
||||
</div>
|
||||
<IngresosFiltrosActivos
|
||||
v-if="hasFiltrosActivos"
|
||||
:clientes="clientes"
|
||||
:selected-cliente-ids="selectedClienteIds"
|
||||
:fecha-desde="fechaDesde"
|
||||
:fecha-hasta="fechaHasta"
|
||||
:selected-tipos="selectedTipos"
|
||||
:selected-estados="selectedEstados"
|
||||
:selected-ubicaciones="selectedUbicaciones"
|
||||
:selected-calidades="selectedCalidades"
|
||||
:include-anulados="includeAnulados"
|
||||
:no-filter="noFilter"
|
||||
:tipos-options="tiposCafeOptions"
|
||||
:estados-options="estadosOptions"
|
||||
@update:selected-cliente-ids="selectedClienteIds = $event"
|
||||
@update:fecha-desde="fechaDesde = $event"
|
||||
@update:fecha-hasta="fechaHasta = $event"
|
||||
@update:selected-preset="selectedPreset = $event"
|
||||
@update:selected-tipos="selectedTipos = $event"
|
||||
@update:selected-estados="selectedEstados = $event"
|
||||
@update:selected-ubicaciones="selectedUbicaciones = $event"
|
||||
@update:selected-calidades="selectedCalidades = $event"
|
||||
@update:include-anulados="includeAnulados = $event"
|
||||
@update:no-filter="noFilter = $event"
|
||||
@show-cliente-selector="showClienteSelectorTemp = true"
|
||||
@show-filtros="filtrosCollapsed = false"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<!-- Selector de clientes temporal (cuando se hace clic en "Agregar cliente") -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<UCard v-if="showClienteSelectorTemp" class="brand-card border border-blue-500/30">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text)]">Agregar más clientes</h3>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
icon="i-lucide-x"
|
||||
@click="showClienteSelectorTemp = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<ClienteSelector
|
||||
:clientes="clientes"
|
||||
:selected-ids="selectedClienteIds"
|
||||
@update:selected-ids="handleClienteSelection"
|
||||
/>
|
||||
</UCard>
|
||||
</Transition>
|
||||
|
||||
<!-- Filtros Card Colapsable -->
|
||||
@@ -105,6 +145,24 @@
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<UCard v-show="!filtrosCollapsed" class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-lucide-filter" class="size-4 text-[#c08040]" />
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text)]">Filtros Disponibles</h3>
|
||||
</div>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
icon="i-lucide-chevron-up"
|
||||
@click="filtrosCollapsed = true"
|
||||
>
|
||||
Ocultar
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<InformeIngresosFiltrosPanel
|
||||
:clientes="clientes"
|
||||
:selected-cliente-ids="selectedClienteIds"
|
||||
@@ -120,6 +178,7 @@
|
||||
:ubicaciones-options="ubicacionesOptions"
|
||||
:calidades-options="calidadesOptions"
|
||||
:include-anulados="includeAnulados"
|
||||
:no-filter="noFilter"
|
||||
@update:selected-cliente-ids="selectedClienteIds = $event"
|
||||
@update:selected-preset="selectedPreset = $event"
|
||||
@update:fecha-desde="fechaDesde = $event"
|
||||
@@ -129,22 +188,12 @@
|
||||
@update:selected-ubicaciones="selectedUbicaciones = $event"
|
||||
@update:selected-calidades="selectedCalidades = $event"
|
||||
@update:include-anulados="includeAnulados = $event"
|
||||
@update:no-filter="noFilter = $event"
|
||||
@hide-filtros="filtrosCollapsed = true"
|
||||
/>
|
||||
</UCard>
|
||||
</Transition>
|
||||
|
||||
<!-- Clientes Seleccionados Cards -->
|
||||
<div v-if="clientesSeleccionados.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<TransitionGroup name="cliente-card">
|
||||
<ClientesClienteCard
|
||||
v-for="cliente in clientesSeleccionados"
|
||||
:key="cliente.id"
|
||||
:cliente="cliente"
|
||||
@remove="removeCliente(cliente.id)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<!-- Totales por Café -->
|
||||
<UCard v-show="pageSections.totalesCafe" class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
@@ -539,25 +588,42 @@ const ingresos = computed(() => ingresosStore.allRecords as IngresoRecord[])
|
||||
const clientes = computed(() => clientesStore.allRecords as ClienteRecord[])
|
||||
|
||||
// -------------------------------
|
||||
// Filtros
|
||||
// Filtros con persistencia en cookies
|
||||
// -------------------------------
|
||||
const includeAnulados = ref(false)
|
||||
const includeAnulados = useCookie<boolean>('informe-ingresos-anulados', { default: () => false })
|
||||
const noFilter = useCookie<boolean>('informe-ingresos-no-filter', { default: () => false })
|
||||
|
||||
type PresetValue =
|
||||
| '' | 'custom' | 'hoy' | 'semana' | 'mes' | 'ytd'
|
||||
| 'cosecha-20-21' | 'cosecha-21-22' | 'cosecha-22-23'
|
||||
| 'cosecha-23-24' | 'cosecha-24-25' | 'cosecha-25-26'
|
||||
|
||||
const selectedPreset = ref<PresetValue>('cosecha-25-26')
|
||||
const fechaDesde = ref<string | null>(null)
|
||||
const fechaHasta = ref<string | null>(null)
|
||||
const selectedClienteIds = ref<number[]>([])
|
||||
const selectedPreset = useCookie<PresetValue>('informe-ingresos-preset', { default: () => 'cosecha-25-26' })
|
||||
const fechaDesde = useCookie<string | null>('informe-ingresos-fecha-desde', { default: () => null })
|
||||
const fechaHasta = useCookie<string | null>('informe-ingresos-fecha-hasta', { default: () => null })
|
||||
const selectedClienteIds = useCookie<number[]>('informe-ingresos-clientes', { default: () => [] })
|
||||
|
||||
// Filtros avanzados
|
||||
const selectedTipos = ref<string[]>([])
|
||||
const selectedEstados = ref<string[]>([])
|
||||
const selectedUbicaciones = ref<string[]>([])
|
||||
const selectedCalidades = ref<string[]>([])
|
||||
const selectedTipos = useCookie<string[]>('informe-ingresos-tipos', { default: () => [] })
|
||||
const selectedEstados = useCookie<string[]>('informe-ingresos-estados', { default: () => [] })
|
||||
const selectedUbicaciones = useCookie<string[]>('informe-ingresos-ubicaciones', { default: () => [] })
|
||||
const selectedCalidades = useCookie<string[]>('informe-ingresos-calidades', { default: () => [] })
|
||||
|
||||
// Watch para sobrescribir noFilter cuando se agregue cualquier filtro
|
||||
watch([selectedClienteIds, fechaDesde, fechaHasta, selectedTipos, selectedEstados, selectedUbicaciones, selectedCalidades, includeAnulados], () => {
|
||||
const hasAnyFilter = selectedClienteIds.value.length > 0 ||
|
||||
fechaDesde.value !== null ||
|
||||
fechaHasta.value !== null ||
|
||||
selectedTipos.value.length > 0 ||
|
||||
selectedEstados.value.length > 0 ||
|
||||
selectedUbicaciones.value.length > 0 ||
|
||||
selectedCalidades.value.length > 0 ||
|
||||
includeAnulados.value
|
||||
|
||||
if (hasAnyFilter && noFilter.value) {
|
||||
noFilter.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Opciones para filtros avanzados
|
||||
const tiposCafeOptions = [
|
||||
@@ -744,6 +810,27 @@ const clientesFiltrados = computed((): ClienteRecord[] => {
|
||||
// Métricos basados en filtrados
|
||||
const ingresosMetrics = useIngresosMetrics(ingresosFiltrados)
|
||||
|
||||
// Detectar si hay filtros activos (incluyendo "Sin filtros")
|
||||
const hasFiltrosActivos = computed(() => {
|
||||
return selectedClienteIds.value.length > 0 ||
|
||||
fechaDesde.value !== null ||
|
||||
fechaHasta.value !== null ||
|
||||
selectedTipos.value.length > 0 ||
|
||||
selectedEstados.value.length > 0 ||
|
||||
selectedUbicaciones.value.length > 0 ||
|
||||
selectedCalidades.value.length > 0 ||
|
||||
includeAnulados.value ||
|
||||
noFilter.value
|
||||
})
|
||||
|
||||
// Watch para mostrar automáticamente el panel de filtros si no hay barras visibles
|
||||
watch([hasFiltrosActivos, filtrosCollapsed], () => {
|
||||
// Si no hay filtros activos Y el panel de filtros está colapsado, expandirlo
|
||||
if (!hasFiltrosActivos.value && filtrosCollapsed.value) {
|
||||
filtrosCollapsed.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Toggle para vista de totales
|
||||
const showMonetaryView = ref(false)
|
||||
|
||||
@@ -761,6 +848,19 @@ function toggleTableFullscreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// Estado temporal para mostrar selector de clientes desde el botón "+"
|
||||
const showClienteSelectorTemp = ref(false)
|
||||
|
||||
function handleClienteSelection(newIds: number[]) {
|
||||
selectedClienteIds.value = newIds
|
||||
// Cerrar el selector temporal cuando se hace una selección
|
||||
if (newIds.length > selectedClienteIds.value.length) {
|
||||
nextTick(() => {
|
||||
showClienteSelectorTemp.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Atajo de teclado para salir de fullscreen con Escape
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isTableFullscreen.value) {
|
||||
|
||||
Reference in New Issue
Block a user