Files
analiticaNucleo/nuxt4-app/app/components/informe-ingresos/FiltrosPanel.vue
josedario87 87ec3b1eb3
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 47s
Style: Aplicar tema personalizado a todos los filtros InputMenu
Se aplica el mismo tema café/dorado a todos los InputMenu de filtros:
- Tipo de café
- Estado
- Ubicación
- Calidad

Estilos aplicados (igual que ClienteMultiSelector):
- Fondo: --brand-surface (#1f180f)
- Borde: --brand-border (#3a2a16)
- Focus ring: ring-1 con --brand-primary (#e0c080)
- Item highlighted: rgba(224,192,128,0.12)
- Tags: color --brand-primary con bordes dorados
- Transición suave en el borde de focus

Todos los filtros ahora mantienen la coherencia visual
con el resto de la aplicación.
2025-10-30 14:02:15 -06:00

336 lines
13 KiB
Vue

<template>
<div class="flex flex-col gap-6">
<!-- Fila 1: Selector de Clientes -->
<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)"
/>
</div>
<!-- Fila 3: Filtros Avanzados (grid de 4 columnas) -->
<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é -->
<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"
:ui="{
root: 'focus-within:ring-1 focus-within:ring-[var(--brand-primary)] transition-shadow',
base: 'bg-[var(--brand-surface)] text-[var(--brand-text)] border border-[var(--brand-border)] focus:ring-1 focus:ring-[var(--brand-primary)] focus:border-[var(--brand-primary)]',
placeholder: 'placeholder-[var(--brand-text-muted)]',
leadingIcon: 'text-[var(--brand-text-muted)]',
content: 'bg-[var(--brand-surface)] border border-[var(--brand-border)]',
item: 'text-[var(--brand-text)] data-highlighted:bg-[rgba(224,192,128,0.12)] data-highlighted:text-[var(--brand-text)]',
tagsItem: 'bg-[rgba(224,192,128,0.14)] border border-[rgba(224,192,128,0.28)] text-[var(--brand-primary)]',
tagsItemText: 'text-[var(--brand-primary)]',
tagsItemDelete: 'text-[var(--brand-text-muted)] hover:text-[var(--brand-primary)] hover:bg-[rgba(224,192,128,0.2)]'
}"
/>
</div>
</Transition>
<!-- Estado -->
<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"
:ui="{
root: 'focus-within:ring-1 focus-within:ring-[var(--brand-primary)] transition-shadow',
base: 'bg-[var(--brand-surface)] text-[var(--brand-text)] border border-[var(--brand-border)] focus:ring-1 focus:ring-[var(--brand-primary)] focus:border-[var(--brand-primary)]',
placeholder: 'placeholder-[var(--brand-text-muted)]',
leadingIcon: 'text-[var(--brand-text-muted)]',
content: 'bg-[var(--brand-surface)] border border-[var(--brand-border)]',
item: 'text-[var(--brand-text)] data-highlighted:bg-[rgba(224,192,128,0.12)] data-highlighted:text-[var(--brand-text)]',
tagsItem: 'bg-[rgba(224,192,128,0.14)] border border-[rgba(224,192,128,0.28)] text-[var(--brand-primary)]',
tagsItemText: 'text-[var(--brand-primary)]',
tagsItemDelete: 'text-[var(--brand-text-muted)] hover:text-[var(--brand-primary)] hover:bg-[rgba(224,192,128,0.2)]'
}"
/>
</div>
</Transition>
<!-- Ubicación -->
<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"
:ui="{
root: 'focus-within:ring-1 focus-within:ring-[var(--brand-primary)] transition-shadow',
base: 'bg-[var(--brand-surface)] text-[var(--brand-text)] border border-[var(--brand-border)] focus:ring-1 focus:ring-[var(--brand-primary)] focus:border-[var(--brand-primary)]',
placeholder: 'placeholder-[var(--brand-text-muted)]',
leadingIcon: 'text-[var(--brand-text-muted)]',
content: 'bg-[var(--brand-surface)] border border-[var(--brand-border)]',
item: 'text-[var(--brand-text)] data-highlighted:bg-[rgba(224,192,128,0.12)] data-highlighted:text-[var(--brand-text)]',
tagsItem: 'bg-[rgba(224,192,128,0.14)] border border-[rgba(224,192,128,0.28)] text-[var(--brand-primary)]',
tagsItemText: 'text-[var(--brand-primary)]',
tagsItemDelete: 'text-[var(--brand-text-muted)] hover:text-[var(--brand-primary)] hover:bg-[rgba(224,192,128,0.2)]'
}"
/>
</div>
</Transition>
<!-- Calidad -->
<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"
:ui="{
root: 'focus-within:ring-1 focus-within:ring-[var(--brand-primary)] transition-shadow',
base: 'bg-[var(--brand-surface)] text-[var(--brand-text)] border border-[var(--brand-border)] focus:ring-1 focus:ring-[var(--brand-primary)] focus:border-[var(--brand-primary)]',
placeholder: 'placeholder-[var(--brand-text-muted)]',
leadingIcon: 'text-[var(--brand-text-muted)]',
content: 'bg-[var(--brand-surface)] border border-[var(--brand-border)]',
item: 'text-[var(--brand-text)] data-highlighted:bg-[rgba(224,192,128,0.12)] data-highlighted:text-[var(--brand-text)]',
tagsItem: 'bg-[rgba(224,192,128,0.14)] border border-[rgba(224,192,128,0.28)] text-[var(--brand-primary)]',
tagsItemText: 'text-[var(--brand-primary)]',
tagsItemDelete: 'text-[var(--brand-text-muted)] hover:text-[var(--brand-primary)] hover:bg-[rgba(224,192,128,0.2)]'
}"
/>
</div>
</Transition>
</div>
</div>
<!-- Checkbox de incluir anulados -->
<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>
<script setup lang="ts">
interface Cliente {
id: number
name: string
cedula?: number
ubicacion?: string
telefono?: string
}
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'
interface Props {
clientes: Cliente[]
selectedClienteIds: number[]
selectedPreset: PresetValue
fechaDesde: string | null
fechaHasta: string | null
selectedTipos: string[]
selectedEstados: string[]
selectedUbicaciones: string[]
selectedCalidades: string[]
tiposOptions: any[]
estadosOptions: any[]
ubicacionesOptions: any[]
calidadesOptions: any[]
includeAnulados: boolean
noFilter: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:selectedClienteIds': [value: number[]]
'update:selectedPreset': [value: PresetValue]
'update:fechaDesde': [value: string | null]
'update:fechaHasta': [value: string | null]
'update:selectedTipos': [value: string[]]
'update:selectedEstados': [value: string[]]
'update:selectedUbicaciones': [value: string[]]
'update:selectedCalidades': [value: string[]]
'update:includeAnulados': [value: boolean]
'update:noFilter': [value: boolean]
'hideFiltros': []
}>()
const selectedTipos = computed({
get: () => props.selectedTipos,
set: (value) => emit('update:selectedTipos', value)
})
const selectedEstados = computed({
get: () => props.selectedEstados,
set: (value) => emit('update:selectedEstados', value)
})
const selectedUbicaciones = computed({
get: () => props.selectedUbicaciones,
set: (value) => emit('update:selectedUbicaciones', value)
})
const selectedCalidades = computed({
get: () => props.selectedCalidades,
set: (value) => emit('update:selectedCalidades', value)
})
const includeAnulados = computed({
get: () => props.includeAnulados,
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>