Files
analiticaNucleo/nuxt4-app/app/components/SimpleMultiSelector.vue
josedario87 86265b4aa8
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 46s
Feat: Reemplazar checkboxes por InputMenu en filtros restantes
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)
2025-10-30 14:18:52 -06:00

126 lines
4.0 KiB
Vue

<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>