Refactor: Reemplazar selector de clientes con InputMenu de Nuxt UI
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 48s

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)
This commit is contained in:
2025-10-30 13:12:59 -06:00
parent ff62e595b2
commit c2bf441db7

View File

@@ -1,30 +1,33 @@
<template> <template>
<div class="space-y-3"> <div class="space-y-3">
<!-- Search input --> <!-- InputMenu for cliente search and selection -->
<div class="relative"> <UInputMenu
<UInput v-model="selectedClientes"
v-model="searchQuery" v-model:search-term="searchQuery"
icon="i-lucide-search" v-model:open="isMenuOpen"
placeholder="Buscar clientes por nombre o cédula..." :items="filteredItems"
:ui="{ icon: { trailing: { pointer: '' } } }" :loading="loading"
> multiple
<template #trailing> icon="i-lucide-search"
<UButton placeholder="Buscar clientes por nombre o cédula (mínimo 4 caracteres)..."
v-if="searchQuery" value-key="id"
icon="i-lucide-x" >
color="gray" <template #item="{ item }">
variant="link" <div class="flex-1 min-w-0">
size="xs" <div class="font-medium truncate">
@click="searchQuery = ''" {{ item.name }}
/> </div>
</template> <div class="text-xs text-[var(--brand-text-muted)]">
</UInput> <span v-if="item.cedula">{{ formatCedula(item.cedula) }}</span>
</div> </div>
</div>
</template>
</UInputMenu>
<!-- Selected count and clear all --> <!-- Selected count and clear all -->
<div v-if="selectedIds.length > 0" class="flex items-center justify-between text-sm"> <div v-if="selectedClientes.length > 0" class="flex items-center justify-between text-sm">
<span class="text-[var(--brand-text-muted)]"> <span class="text-[var(--brand-text-muted)]">
{{ selectedIds.length }} cliente{{ selectedIds.length !== 1 ? 's' : '' }} seleccionado{{ selectedIds.length !== 1 ? 's' : '' }} {{ selectedClientes.length }} cliente{{ selectedClientes.length !== 1 ? 's' : '' }} seleccionado{{ selectedClientes.length !== 1 ? 's' : '' }}
</span> </span>
<UButton <UButton
size="xs" size="xs"
@@ -35,58 +38,6 @@
Limpiar todo Limpiar todo
</UButton> </UButton>
</div> </div>
<!-- List of clientes with checkboxes -->
<div class="max-h-64 overflow-y-auto space-y-2 border border-[var(--brand-border)] rounded-lg p-3">
<div v-if="loading" class="py-4 text-center text-sm text-[var(--brand-text-muted)]">
Cargando clientes...
</div>
<div v-else-if="filteredClientes.length === 0" class="py-4 text-center text-sm text-[var(--brand-text-muted)]">
{{ searchQuery ? 'No se encontraron clientes' : 'No hay clientes disponibles' }}
</div>
<div
v-else
v-for="cliente in filteredClientes"
:key="cliente.id"
class="flex items-center gap-2 p-2 rounded hover:bg-[#2a1f10] cursor-pointer transition-colors"
@click="toggleCliente(cliente.id)"
>
<UCheckbox
:model-value="isSelected(cliente.id)"
@click.stop="toggleCliente(cliente.id)"
/>
<div class="flex-1 min-w-0">
<div class="font-medium text-[var(--brand-text)] truncate">
{{ cliente.name }}
</div>
<div class="text-xs text-[var(--brand-text-muted)] flex items-center gap-2">
<span v-if="cliente.cedula">{{ formatCedula(cliente.cedula) }}</span>
<span v-if="cliente.ubicacion" class="truncate"> {{ cliente.ubicacion }}</span>
</div>
</div>
</div>
</div>
<!-- Quick filters by ubicacion -->
<div v-if="ubicaciones.length > 0" class="space-y-2">
<div class="text-xs font-medium text-[var(--brand-text-muted)] uppercase tracking-wide">
Filtrar por ubicación
</div>
<div class="flex flex-wrap gap-2">
<UButton
v-for="ubicacion in ubicaciones"
:key="ubicacion"
size="xs"
color="gray"
variant="soft"
@click="selectByUbicacion(ubicacion)"
>
{{ ubicacion }}
</UButton>
</div>
</div>
</div> </div>
</template> </template>
@@ -94,10 +45,8 @@
interface Cliente { interface Cliente {
id: number id: number
name: string name: string
cedula?: number cedula?: string
ubicacion?: string ubicacion?: string
telefono?: string
email?: string
} }
const props = defineProps<{ const props = defineProps<{
@@ -112,61 +61,55 @@ const emit = defineEmits<{
const clientes = ref<Cliente[]>([]) const clientes = ref<Cliente[]>([])
const loading = ref(false) const loading = ref(false)
const searchQuery = ref('') const searchQuery = ref('')
const isMenuOpen = ref(false)
// Computed // Computed - Sync with props
const filteredClientes = computed(() => { const selectedClientes = computed({
if (!searchQuery.value) return clientes.value 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()
const query = searchQuery.value.toLowerCase()
return clientes.value.filter(c => return clientes.value.filter(c =>
c.name.toLowerCase().includes(query) || c.name.toLowerCase().includes(lowerQuery) ||
c.cedula?.toString().includes(query) || c.cedula?.includes(query)
c.ubicacion?.toLowerCase().includes(query)
) )
}) })
const ubicaciones = computed(() => { // Watch searchQuery to control menu opening
const locs = new Set<string>() watch(searchQuery, (newValue) => {
clientes.value.forEach(c => { if (newValue.trim().length >= 4 && filteredItems.value.length > 0) {
if (c.ubicacion) locs.add(c.ubicacion) isMenuOpen.value = true
}) } else {
return Array.from(locs).sort() isMenuOpen.value = false
}
}) })
// Methods // Methods
function isSelected(id: number): boolean {
return props.selectedIds.includes(id)
}
function toggleCliente(id: number) {
const newIds = isSelected(id)
? props.selectedIds.filter(i => i !== id)
: [...props.selectedIds, id]
emit('update:selectedIds', newIds)
}
function clearAll() { function clearAll() {
emit('update:selectedIds', []) emit('update:selectedIds', [])
} }
function selectByUbicacion(ubicacion: string) { function formatCedula(cedula: string): string {
const clientesInUbicacion = clientes.value
.filter(c => c.ubicacion === ubicacion)
.map(c => c.id)
// Add to selection (not replace)
const newIds = [...new Set([...props.selectedIds, ...clientesInUbicacion])]
emit('update:selectedIds', newIds)
}
function formatCedula(cedula: number): string {
const str = cedula.toString()
// Format as XXXX-XXXX-XXXXX // Format as XXXX-XXXX-XXXXX
if (str.length === 13) { if (cedula.length === 13) {
return `${str.slice(0, 4)}-${str.slice(4, 8)}-${str.slice(8)}` return `${cedula.slice(0, 4)}-${cedula.slice(4, 8)}-${cedula.slice(8)}`
} }
return str return cedula
} }
async function loadClientes() { async function loadClientes() {