292 lines
9.4 KiB
Vue
292 lines
9.4 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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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>
|