All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 46s
- Crear endpoint /api/clientes para obtener clientes desde Supabase - Crear endpoint /api/postgres/query para ejecutar queries SQL - Crear componente ClienteMultiSelector con búsqueda y filtro por ubicación - Agregar filtros de clientes, ubicaciones y calidades en informe-ingresos.vue - Cargar opciones de filtros desde Metabase (query ID 53) - Actualizar detección de cambios pendientes con nuevos filtros - Enviar cliente_ids, ubicaciones y calidades al endpoint de Metabase - Componente con formato de cédula y ordenamiento por nombre - Búsqueda por nombre, cédula o ubicación - Contador de selección y botón limpiar todo - Botones rápidos para seleccionar por ubicación
189 lines
5.0 KiB
Vue
189 lines
5.0 KiB
Vue
<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>
|
|
|
|
<!-- Selected count and clear all -->
|
|
<div v-if="selectedIds.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' : '' }}
|
|
</span>
|
|
<UButton
|
|
size="xs"
|
|
color="gray"
|
|
variant="link"
|
|
@click="clearAll"
|
|
>
|
|
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>
|
|
|
|
<script setup lang="ts">
|
|
interface Cliente {
|
|
id: number
|
|
name: string
|
|
cedula?: number
|
|
ubicacion?: string
|
|
telefono?: string
|
|
email?: 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('')
|
|
|
|
// Computed
|
|
const filteredClientes = computed(() => {
|
|
if (!searchQuery.value) return clientes.value
|
|
|
|
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)
|
|
)
|
|
})
|
|
|
|
const ubicaciones = computed(() => {
|
|
const locs = new Set<string>()
|
|
clientes.value.forEach(c => {
|
|
if (c.ubicacion) locs.add(c.ubicacion)
|
|
})
|
|
return Array.from(locs).sort()
|
|
})
|
|
|
|
// 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()
|
|
// Format as XXXX-XXXX-XXXXX
|
|
if (str.length === 13) {
|
|
return `${str.slice(0, 4)}-${str.slice(4, 8)}-${str.slice(8)}`
|
|
}
|
|
return str
|
|
}
|
|
|
|
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>
|