Feat: agregar indicador visual de cambios pendientes en filtros
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 45s

Implementación de sistema de tracking para detectar cuando los filtros
han cambiado pero no se han aplicado a los datos mostrados.

Indicadores visuales implementados:
- Alerta amarilla prominente con animación pulse y ping
- Borde y sombra amarilla en el card de filtros
- Botón 'Actualizar' cambia a amarillo con emoji de advertencia
- Botón rápido 'Actualizar ahora' dentro de la alerta
- Animaciones llamativas para captar la atención

El sistema compara los filtros actuales con los últimos aplicados
y muestra indicadores visuales evidentes cuando hay diferencias.
This commit is contained in:
2025-10-29 17:16:08 -06:00
parent 4cbc944682
commit a4e4fd6a64

View File

@@ -99,7 +99,14 @@
<!-- Main Content --> <!-- Main Content -->
<template v-else-if="data"> <template v-else-if="data">
<!-- Card de Filtros --> <!-- Card de Filtros -->
<UCard class="brand-card border border-transparent"> <UCard
:class="[
'brand-card border transition-all duration-300',
hasPendingChanges
? 'border-yellow-500 shadow-lg shadow-yellow-500/50 ring-2 ring-yellow-400/50'
: 'border-transparent'
]"
>
<template #header> <template #header>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
@@ -114,6 +121,40 @@
</div> </div>
</div> </div>
<!-- Alerta de cambios pendientes -->
<UAlert
v-if="hasPendingChanges"
color="warning"
variant="solid"
icon="i-lucide-alert-circle"
class="animate-pulse"
>
<template #title>
<div class="flex items-center gap-2">
<span class="font-bold">¡Cambios pendientes!</span>
<span class="inline-flex h-2 w-2 rounded-full bg-yellow-400 animate-ping"></span>
</div>
</template>
<template #description>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<span>Los filtros han cambiado pero no se han aplicado a los datos mostrados. Haz clic en "Actualizar" para aplicar los cambios.</span>
<UButton
:loading="loading"
:disabled="loading"
color="yellow"
variant="solid"
size="xs"
@click="loadData"
>
<template #leading>
<UIcon name="i-lucide-refresh-cw" :class="{ 'animate-spin': loading }" />
</template>
Actualizar ahora
</UButton>
</div>
</template>
</UAlert>
<!-- Alerta roja cuando incluye anulados --> <!-- Alerta roja cuando incluye anulados -->
<UAlert <UAlert
v-if="includeAnulados" v-if="includeAnulados"
@@ -143,14 +184,18 @@
<UButton <UButton
:loading="loading" :loading="loading"
:disabled="loading" :disabled="loading"
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c] disabled:opacity-50 disabled:cursor-not-allowed' }" :ui="{
base: hasPendingChanges
? 'bg-yellow-500 text-black border border-yellow-600 hover:bg-yellow-400 hover:border-yellow-500 disabled:opacity-50 disabled:cursor-not-allowed animate-pulse'
: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c] disabled:opacity-50 disabled:cursor-not-allowed'
}"
size="sm" size="sm"
@click="loadData" @click="loadData"
> >
<template #leading> <template #leading>
<UIcon name="i-lucide-refresh-cw" :class="{ 'animate-spin': loading }" /> <UIcon name="i-lucide-refresh-cw" :class="{ 'animate-spin': loading }" />
</template> </template>
Actualizar {{ hasPendingChanges ? 'Actualizar ⚠️' : 'Actualizar' }}
</UButton> </UButton>
</div> </div>
</template> </template>
@@ -230,6 +275,13 @@ const selectedPreset = ref<PresetValue>('cosecha-25-26')
const fechaDesde = ref<string | null>(null) const fechaDesde = ref<string | null>(null)
const fechaHasta = ref<string | null>(null) const fechaHasta = ref<string | null>(null)
// Filtros aplicados (los que se usaron en la última carga de datos)
const appliedFilters = ref<{
fechaDesde: string | null
fechaHasta: string | null
includeAnulados: boolean
} | null>(null)
// Computed // Computed
const rangoLegible = computed(() => { const rangoLegible = computed(() => {
if (!fechaDesde.value && !fechaHasta.value) return 'Sin filtro de fecha' if (!fechaDesde.value && !fechaHasta.value) return 'Sin filtro de fecha'
@@ -238,6 +290,19 @@ const rangoLegible = computed(() => {
return `${f}${t}` return `${f}${t}`
}) })
// Detectar si hay cambios pendientes sin aplicar
const hasPendingChanges = computed(() => {
// Si no hay datos cargados, no hay cambios pendientes
if (!appliedFilters.value) return false
// Comparar filtros actuales con los aplicados
return (
fechaDesde.value !== appliedFilters.value.fechaDesde ||
fechaHasta.value !== appliedFilters.value.fechaHasta ||
includeAnulados.value !== appliedFilters.value.includeAnulados
)
})
// Format currency helper // Format currency helper
const formatCurrency = (value: number) => { const formatCurrency = (value: number) => {
if (!value) return 'L 0.00' if (!value) return 'L 0.00'
@@ -278,6 +343,13 @@ async function loadData() {
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
}) })
// Guardar los filtros aplicados
appliedFilters.value = {
fechaDesde: fechaDesde.value,
fechaHasta: fechaHasta.value,
includeAnulados: includeAnulados.value
}
} catch (err: any) { } catch (err: any) {
error.value = err.message || 'Error al cargar datos' error.value = err.message || 'Error al cargar datos'
console.error('Error loading panorama data:', err) console.error('Error loading panorama data:', err)