Files
analiticaNucleo/nuxt4-app/app/components/ClienteMultiSelector.vue
josedario87 c2bf441db7
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 48s
Refactor: Reemplazar selector de clientes con InputMenu de Nuxt UI
Cambios implementados:
- Reemplazar input con checkboxes por UInputMenu con selección múltiple
- Filtrado por nombre y cédula únicamente (sin ubicación)
- El menú de sugerencias solo se abre después de 4 caracteres
- Mantener contador de seleccionados y botón "Limpiar todo"
- Actualizar tipo de cedula de number a string para manejar formato correcto
- Simplificar lógica eliminando filtros por ubicación

Mejoras de UX:
- Interfaz más limpia y moderna con InputMenu
- Búsqueda más eficiente con mínimo de caracteres
- Tags visuales para items seleccionados
- Formato de cédula mantenido (XXXX-XXXX-XXXXX)
2025-10-30 13:12:59 -06:00

132 lines
3.1 KiB
Vue

<template>
<div class="space-y-3">
<!-- InputMenu for cliente search and selection -->
<UInputMenu
v-model="selectedClientes"
v-model:search-term="searchQuery"
v-model:open="isMenuOpen"
:items="filteredItems"
:loading="loading"
multiple
icon="i-lucide-search"
placeholder="Buscar clientes por nombre o cédula (mínimo 4 caracteres)..."
value-key="id"
>
<template #item="{ item }">
<div class="flex-1 min-w-0">
<div class="font-medium truncate">
{{ item.name }}
</div>
<div class="text-xs text-[var(--brand-text-muted)]">
<span v-if="item.cedula">{{ formatCedula(item.cedula) }}</span>
</div>
</div>
</template>
</UInputMenu>
<!-- Selected count and clear all -->
<div v-if="selectedClientes.length > 0" class="flex items-center justify-between text-sm">
<span class="text-[var(--brand-text-muted)]">
{{ selectedClientes.length }} cliente{{ selectedClientes.length !== 1 ? 's' : '' }} seleccionado{{ selectedClientes.length !== 1 ? 's' : '' }}
</span>
<UButton
size="xs"
color="gray"
variant="link"
@click="clearAll"
>
Limpiar todo
</UButton>
</div>
</div>
</template>
<script setup lang="ts">
interface Cliente {
id: number
name: string
cedula?: string
ubicacion?: string
}
const props = defineProps<{
selectedIds: number[]
}>()
const emit = defineEmits<{
'update:selectedIds': [value: number[]]
}>()
// State
const clientes = ref<Cliente[]>([])
const loading = ref(false)
const searchQuery = ref('')
const isMenuOpen = ref(false)
// Computed - Sync with props
const selectedClientes = computed({
get: () => {
return clientes.value.filter(c => props.selectedIds.includes(c.id))
},
set: (value: Cliente[]) => {
emit('update:selectedIds', value.map(c => c.id))
}
})
// Computed - Filter items based on search (min 4 characters)
const filteredItems = computed(() => {
const query = searchQuery.value.trim()
// Only show items if search has at least 4 characters
if (query.length < 4) {
return []
}
const lowerQuery = query.toLowerCase()
return clientes.value.filter(c =>
c.name.toLowerCase().includes(lowerQuery) ||
c.cedula?.includes(query)
)
})
// Watch searchQuery to control menu opening
watch(searchQuery, (newValue) => {
if (newValue.trim().length >= 4 && filteredItems.value.length > 0) {
isMenuOpen.value = true
} else {
isMenuOpen.value = false
}
})
// Methods
function clearAll() {
emit('update:selectedIds', [])
}
function formatCedula(cedula: string): string {
// Format as XXXX-XXXX-XXXXX
if (cedula.length === 13) {
return `${cedula.slice(0, 4)}-${cedula.slice(4, 8)}-${cedula.slice(8)}`
}
return cedula
}
async function loadClientes() {
loading.value = true
try {
const result = await $fetch('/api/clientes')
clientes.value = result as Cliente[]
} catch (error) {
console.error('[ClienteSelector] Error loading clientes:', error)
} finally {
loading.value = false
}
}
// Load on mount
onMounted(() => {
loadClientes()
})
</script>