Files
analiticaNucleo/nuxt4-app/app/components/UbicacionMultiSelector.vue
josedario87 dae7c73749
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 48s
Fix: Normalizar array de ubicaciones y agregar soporte para loading
Problema identificado:
- El array de ubicaciones viene como array de strings simples,
  no como objetos con {label, value}
- Ejemplo: ["breñales, la union, lempira", "buenos aires, ..."]

Solución implementada:

1. Normalización de datos:
   - Crear computed normalizedUbicaciones que transforma el array
   - Si el item es string: usa el string como label y value
   - Si el item es objeto: usa sus propiedades label y value
   - Filtrar null/undefined durante la transformación

2. Agregar prop loading:
   - Agregada prop opcional loading?: boolean
   - Pasar loading al UInputMenu para mostrar spinner

3. Simplificar lógica de filtrado:
   - Usar normalizedUbicaciones como base
   - Filtrar por query sin errores de toLowerCase

4. Mejorar selectedUbicacionesObjects:
   - Usar normalizedUbicaciones para encontrar seleccionados
   - Comparar con value normalizado

Ahora el componente:
- Muestra todas las ubicaciones correctamente
- Funciona con array de strings o array de objetos
- Muestra animación de loading cuando carga datos
- Búsqueda funciona sin errores
2025-10-30 14:14:44 -06:00

139 lines
4.3 KiB
Vue

<template>
<div class="space-y-3">
<!-- InputMenu for ubicacion search and selection -->
<UInputMenu
:model-value="selectedUbicacionesObjects"
:items="filteredItems"
:loading="loading"
multiple
label-key="label"
value-key="value"
icon="i-lucide-map-pin"
placeholder="Buscar ubicaciones..."
size="sm"
ignore-filter
: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"
@update:search-term="searchQuery = $event"
>
<template #empty>
<div class="text-center py-2">
<span class="text-[var(--brand-text-muted)] text-sm">
No se encontraron ubicaciones
</span>
</div>
</template>
</UInputMenu>
<!-- Selected count and clear all -->
<div v-if="selectedUbicaciones.length > 0" class="flex items-center justify-between text-sm">
<span class="text-[var(--brand-text-muted)]">
{{ selectedUbicaciones.length }} ubicación{{ selectedUbicaciones.length !== 1 ? 'es' : '' }} seleccionada{{ selectedUbicaciones.length !== 1 ? 's' : '' }}
</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 UbicacionItem {
label: string
value: string
}
const props = defineProps<{
selectedUbicaciones: string[]
ubicaciones: UbicacionItem[] | string[]
loading?: boolean
}>()
const emit = defineEmits<{
'update:selectedUbicaciones': [value: string[]]
}>()
// State
const searchQuery = ref('')
// Computed - Normalize ubicaciones to InputMenuItem format
const normalizedUbicaciones = computed((): InputMenuItem[] => {
if (!Array.isArray(props.ubicaciones)) return []
return props.ubicaciones
.filter(u => u) // Filter out null/undefined
.map(u => {
// If it's already an object with label and value, use it
if (typeof u === 'object' && u.label && u.value) {
return { label: u.label, value: u.value }
}
// If it's a string, use it as both label and value
if (typeof u === 'string') {
return { label: u, value: u }
}
return null
})
.filter((u): u is InputMenuItem => u !== null)
})
// Computed - Get selected ubicaciones as objects for InputMenu
const selectedUbicacionesObjects = computed(() => {
return normalizedUbicaciones.value.filter(u =>
props.selectedUbicaciones.includes(u.value as string)
)
})
// Computed - Filter items based on search
const filteredItems = computed((): InputMenuItem[] => {
const query = searchQuery.value.trim().toLowerCase()
if (!query) {
return normalizedUbicaciones.value
}
return normalizedUbicaciones.value.filter(u =>
u.label && typeof u.label === 'string' && u.label.toLowerCase().includes(query)
)
})
// Methods
function onSelectionChange(value: any[]) {
if (!Array.isArray(value)) {
emit('update:selectedUbicaciones', [])
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:selectedUbicaciones', values)
}
function clearAll() {
emit('update:selectedUbicaciones', [])
}
</script>