Refactor: Reemplazar selector de clientes con InputMenu de Nuxt UI
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 48s
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:
@@ -1,30 +1,33 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<!-- Search input -->
|
||||
<div class="relative">
|
||||
<UInput
|
||||
v-model="searchQuery"
|
||||
icon="i-lucide-search"
|
||||
placeholder="Buscar clientes por nombre o cédula..."
|
||||
:ui="{ icon: { trailing: { pointer: '' } } }"
|
||||
>
|
||||
<template #trailing>
|
||||
<UButton
|
||||
v-if="searchQuery"
|
||||
icon="i-lucide-x"
|
||||
color="gray"
|
||||
variant="link"
|
||||
size="xs"
|
||||
@click="searchQuery = ''"
|
||||
/>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
<!-- 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="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)]">
|
||||
{{ 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>
|
||||
<UButton
|
||||
size="xs"
|
||||
@@ -35,58 +38,6 @@
|
||||
Limpiar todo
|
||||
</UButton>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -94,10 +45,8 @@
|
||||
interface Cliente {
|
||||
id: number
|
||||
name: string
|
||||
cedula?: number
|
||||
cedula?: string
|
||||
ubicacion?: string
|
||||
telefono?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -112,61 +61,55 @@ const emit = defineEmits<{
|
||||
const clientes = ref<Cliente[]>([])
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const isMenuOpen = ref(false)
|
||||
|
||||
// Computed
|
||||
const filteredClientes = computed(() => {
|
||||
if (!searchQuery.value) return clientes.value
|
||||
// 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()
|
||||
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return clientes.value.filter(c =>
|
||||
c.name.toLowerCase().includes(query) ||
|
||||
c.cedula?.toString().includes(query) ||
|
||||
c.ubicacion?.toLowerCase().includes(query)
|
||||
c.name.toLowerCase().includes(lowerQuery) ||
|
||||
c.cedula?.includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
const ubicaciones = computed(() => {
|
||||
const locs = new Set<string>()
|
||||
clientes.value.forEach(c => {
|
||||
if (c.ubicacion) locs.add(c.ubicacion)
|
||||
})
|
||||
return Array.from(locs).sort()
|
||||
// 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 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() {
|
||||
emit('update:selectedIds', [])
|
||||
}
|
||||
|
||||
function selectByUbicacion(ubicacion: 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()
|
||||
function formatCedula(cedula: string): string {
|
||||
// Format as XXXX-XXXX-XXXXX
|
||||
if (str.length === 13) {
|
||||
return `${str.slice(0, 4)}-${str.slice(4, 8)}-${str.slice(8)}`
|
||||
if (cedula.length === 13) {
|
||||
return `${cedula.slice(0, 4)}-${cedula.slice(4, 8)}-${cedula.slice(8)}`
|
||||
}
|
||||
return str
|
||||
return cedula
|
||||
}
|
||||
|
||||
async function loadClientes() {
|
||||
|
||||
Reference in New Issue
Block a user