Feat: Reemplazar checkboxes por InputMenu en filtros restantes
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 46s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 46s
Nuevo componente creado:
- SimpleMultiSelector.vue: Componente genérico reutilizable
- Normaliza strings a objetos {label, value}
- Capitaliza primera letra automáticamente
- Props configurables: icon, placeholder, labels singular/plural
- Mismo estilo espectacular que otros selectores
Cambios en informe-ingresos.vue:
1. Layout mejorado:
- Grid de 3 columnas (antes 2 columnas + 1 separado)
- Tipos, Estados y Calidades en mismo nivel
- Distribución más equilibrada y consistente
2. Reemplazar UCheckboxGroup por SimpleMultiSelector:
- Tipos de Café: icon coffee, "tipo/tipos"
- Estados: icon check-circle, "estado/estados"
- Calidades: icon star, "calidad/calidades"
3. Labels consistentes:
- Todas las secciones usan mismo formato
- text-sm font-medium mb-2
Beneficios:
- Interfaz más limpia y moderna
- Búsqueda integrada en cada filtro
- Tags visuales de selección
- Consistencia total en todos los filtros
- Mismo tema café/dorado en toda la app
- Mejor uso del espacio (3 columnas iguales)
This commit is contained in:
125
nuxt4-app/app/components/SimpleMultiSelector.vue
Normal file
125
nuxt4-app/app/components/SimpleMultiSelector.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<!-- InputMenu for simple multiple selection -->
|
||||
<UInputMenu
|
||||
:model-value="selectedItemsObjects"
|
||||
:items="normalizedItems"
|
||||
:loading="loading"
|
||||
multiple
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
:icon="icon"
|
||||
:placeholder="placeholder"
|
||||
size="sm"
|
||||
: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)]',
|
||||
itemLeadingIcon: 'text-[var(--brand-text-muted)]',
|
||||
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)]'
|
||||
}"
|
||||
@update:model-value="onSelectionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="text-center py-2">
|
||||
<span class="text-[var(--brand-text-muted)] text-sm">
|
||||
No hay opciones disponibles
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</UInputMenu>
|
||||
|
||||
<!-- Selected count and clear all -->
|
||||
<div v-if="selectedItems.length > 0" class="flex items-center justify-between text-sm">
|
||||
<span class="text-[var(--brand-text-muted)]">
|
||||
{{ selectedItems.length }} {{ selectedItems.length !== 1 ? itemsLabel : itemLabel }} seleccionad{{ selectedItems.length !== 1 ? 'os' : 'o' }}
|
||||
</span>
|
||||
<UButton
|
||||
size="xs"
|
||||
color="gray"
|
||||
variant="link"
|
||||
@click="clearAll"
|
||||
>
|
||||
Limpiar todo
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { InputMenuItem } from '@nuxt/ui'
|
||||
|
||||
interface SimpleItem {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
selectedItems: string[]
|
||||
items: SimpleItem[] | string[]
|
||||
loading?: boolean
|
||||
icon?: string
|
||||
placeholder?: string
|
||||
itemLabel?: string // singular: "tipo", "estado", "calidad"
|
||||
itemsLabel?: string // plural: "tipos", "estados", "calidades"
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:selectedItems': [value: string[]]
|
||||
}>()
|
||||
|
||||
// Computed - Normalize items to InputMenuItem format
|
||||
const normalizedItems = computed((): InputMenuItem[] => {
|
||||
if (!Array.isArray(props.items)) return []
|
||||
|
||||
return props.items
|
||||
.filter(item => item) // Filter out null/undefined
|
||||
.map(item => {
|
||||
// If it's already an object with label and value, use it
|
||||
if (typeof item === 'object' && item.label && item.value) {
|
||||
return { label: item.label, value: item.value }
|
||||
}
|
||||
// If it's a string, use it as both label and value (capitalize first letter)
|
||||
if (typeof item === 'string') {
|
||||
const capitalizedLabel = item.charAt(0).toUpperCase() + item.slice(1)
|
||||
return { label: capitalizedLabel, value: item }
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((item): item is InputMenuItem => item !== null)
|
||||
})
|
||||
|
||||
// Computed - Get selected items as objects for InputMenu
|
||||
const selectedItemsObjects = computed(() => {
|
||||
return normalizedItems.value.filter(item =>
|
||||
props.selectedItems.includes(item.value as string)
|
||||
)
|
||||
})
|
||||
|
||||
// Methods
|
||||
function onSelectionChange(value: any[]) {
|
||||
if (!Array.isArray(value)) {
|
||||
emit('update:selectedItems', [])
|
||||
return
|
||||
}
|
||||
|
||||
// Extract values from selected items
|
||||
const values = value
|
||||
.filter(item => item && (item.value || typeof item === 'string'))
|
||||
.map(item => {
|
||||
if (typeof item === 'string') return item
|
||||
return item.value || item
|
||||
})
|
||||
|
||||
emit('update:selectedItems', values)
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
emit('update:selectedItems', [])
|
||||
}
|
||||
</script>
|
||||
@@ -80,55 +80,49 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<!-- Grid de 3 filtros simples -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Tipos de Café -->
|
||||
<div>
|
||||
<UCheckboxGroup
|
||||
v-model="selectedTipos"
|
||||
legend="Tipos de Café"
|
||||
:items="tiposItems"
|
||||
color="primary"
|
||||
variant="list"
|
||||
orientation="vertical"
|
||||
size="sm"
|
||||
:ui="{
|
||||
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
|
||||
}"
|
||||
<label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Tipos de Café</label>
|
||||
<SimpleMultiSelector
|
||||
:selected-items="selectedTipos"
|
||||
:items="opcionesFiltros.tipos"
|
||||
icon="i-lucide-coffee"
|
||||
placeholder="Todos los tipos"
|
||||
item-label="tipo"
|
||||
items-label="tipos"
|
||||
@update:selected-items="selectedTipos = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Estados -->
|
||||
<div>
|
||||
<UCheckboxGroup
|
||||
v-model="selectedEstados"
|
||||
legend="Estados"
|
||||
:items="estadosItems"
|
||||
color="primary"
|
||||
variant="list"
|
||||
orientation="vertical"
|
||||
size="sm"
|
||||
:ui="{
|
||||
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
|
||||
}"
|
||||
<label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Estados</label>
|
||||
<SimpleMultiSelector
|
||||
:selected-items="selectedEstados"
|
||||
:items="opcionesFiltros.estados"
|
||||
icon="i-lucide-check-circle"
|
||||
placeholder="Todos los estados"
|
||||
item-label="estado"
|
||||
items-label="estados"
|
||||
@update:selected-items="selectedEstados = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calidades -->
|
||||
<div v-if="opcionesFiltros.calidades.length > 0">
|
||||
<UCheckboxGroup
|
||||
v-model="selectedCalidades"
|
||||
legend="Calidades"
|
||||
:items="opcionesFiltros.calidades"
|
||||
color="primary"
|
||||
variant="list"
|
||||
orientation="horizontal"
|
||||
size="sm"
|
||||
:ui="{
|
||||
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
|
||||
}"
|
||||
/>
|
||||
<!-- Calidades -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Calidades</label>
|
||||
<SimpleMultiSelector
|
||||
:selected-items="selectedCalidades"
|
||||
:items="opcionesFiltros.calidades"
|
||||
icon="i-lucide-star"
|
||||
placeholder="Todas las calidades"
|
||||
item-label="calidad"
|
||||
items-label="calidades"
|
||||
@update:selected-items="selectedCalidades = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -257,55 +251,49 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<!-- Grid de 3 filtros simples -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Tipos de Café -->
|
||||
<div>
|
||||
<UCheckboxGroup
|
||||
v-model="selectedTipos"
|
||||
legend="Tipos de Café"
|
||||
:items="tiposItems"
|
||||
color="primary"
|
||||
variant="list"
|
||||
orientation="vertical"
|
||||
size="sm"
|
||||
:ui="{
|
||||
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
|
||||
}"
|
||||
<label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Tipos de Café</label>
|
||||
<SimpleMultiSelector
|
||||
:selected-items="selectedTipos"
|
||||
:items="opcionesFiltros.tipos"
|
||||
icon="i-lucide-coffee"
|
||||
placeholder="Todos los tipos"
|
||||
item-label="tipo"
|
||||
items-label="tipos"
|
||||
@update:selected-items="selectedTipos = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Estados -->
|
||||
<div>
|
||||
<UCheckboxGroup
|
||||
v-model="selectedEstados"
|
||||
legend="Estados"
|
||||
:items="estadosItems"
|
||||
color="primary"
|
||||
variant="list"
|
||||
orientation="vertical"
|
||||
size="sm"
|
||||
:ui="{
|
||||
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
|
||||
}"
|
||||
<label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Estados</label>
|
||||
<SimpleMultiSelector
|
||||
:selected-items="selectedEstados"
|
||||
:items="opcionesFiltros.estados"
|
||||
icon="i-lucide-check-circle"
|
||||
placeholder="Todos los estados"
|
||||
item-label="estado"
|
||||
items-label="estados"
|
||||
@update:selected-items="selectedEstados = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calidades -->
|
||||
<div v-if="opcionesFiltros.calidades.length > 0">
|
||||
<UCheckboxGroup
|
||||
v-model="selectedCalidades"
|
||||
legend="Calidades"
|
||||
:items="opcionesFiltros.calidades"
|
||||
color="primary"
|
||||
variant="list"
|
||||
orientation="horizontal"
|
||||
size="sm"
|
||||
:ui="{
|
||||
container: 'hover:bg-[#c08040]/5 transition-colors duration-200 rounded-md px-2 -mx-2'
|
||||
}"
|
||||
/>
|
||||
<!-- Calidades -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2 text-[var(--brand-text)]">Calidades</label>
|
||||
<SimpleMultiSelector
|
||||
:selected-items="selectedCalidades"
|
||||
:items="opcionesFiltros.calidades"
|
||||
icon="i-lucide-star"
|
||||
placeholder="Todas las calidades"
|
||||
item-label="calidad"
|
||||
items-label="calidades"
|
||||
@update:selected-items="selectedCalidades = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user