From eb5fa191c13f1e735654fecf158f6f2be1b04152 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 5 Dec 2025 12:17:15 -0600 Subject: [PATCH] Agregar filtro Solo Presentes y reorganizar filtros MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reorganizar filtros (ID, Teléfono, Empleados, Presentes) en una fila con wrap - Agregar checkbox 'Presentes' basado en tabla Asistencias de Supabase - Crear endpoint /api/contacts/presentes que consulta última asistencia sin salida - Integrar filtro de presentes en useContacts con carga lazy --- nuxt4/app/components/contacts/Filters.vue | 18 ++++- nuxt4/app/components/contacts/List.vue | 3 +- nuxt4/app/composables/useContacts.ts | 38 +++++++++- nuxt4/server/api/contacts/presentes.get.ts | 86 ++++++++++++++++++++++ 4 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 nuxt4/server/api/contacts/presentes.get.ts diff --git a/nuxt4/app/components/contacts/Filters.vue b/nuxt4/app/components/contacts/Filters.vue index 2660738..f80091f 100644 --- a/nuxt4/app/components/contacts/Filters.vue +++ b/nuxt4/app/components/contacts/Filters.vue @@ -19,7 +19,7 @@ - +
@@ -43,7 +43,13 @@ + + + @@ -94,12 +100,18 @@ const empleadoFilter = computed({ set: (val) => emit('update:modelValue', { ...props.modelValue, empleado: val }) }) +const presenteFilter = computed({ + get: () => props.modelValue.presente, + set: (val) => emit('update:modelValue', { ...props.modelValue, presente: val }) +}) + const hasActiveFilters = computed(() => { return ( props.modelValue.search || props.modelValue.id || props.modelValue.telefono || - !props.modelValue.empleado + !props.modelValue.empleado || + props.modelValue.presente ) }) diff --git a/nuxt4/app/components/contacts/List.vue b/nuxt4/app/components/contacts/List.vue index 94e50ab..501b9b3 100644 --- a/nuxt4/app/components/contacts/List.vue +++ b/nuxt4/app/components/contacts/List.vue @@ -71,6 +71,7 @@ const { error, fetchContacts, fetchAliases, + fetchPresentes, updateAlias, getWhatsAppUrl, clearFilters @@ -78,7 +79,7 @@ const { // Cargar datos al montar onMounted(async () => { - await Promise.all([fetchContacts(), fetchAliases()]) + await Promise.all([fetchContacts(), fetchAliases(), fetchPresentes()]) }) // Manejar actualización de alias diff --git a/nuxt4/app/composables/useContacts.ts b/nuxt4/app/composables/useContacts.ts index 09708c2..f84b8e0 100644 --- a/nuxt4/app/composables/useContacts.ts +++ b/nuxt4/app/composables/useContacts.ts @@ -19,6 +19,7 @@ export interface ContactFilters { id: string telefono: string empleado: boolean + presente: boolean } export const useContacts = () => { @@ -27,6 +28,7 @@ export const useContacts = () => { // Estado const contacts = ref([]) const aliases = ref>({}) + const presentIds = ref>(new Set()) const isLoading = ref(false) const error = ref(null) @@ -37,7 +39,8 @@ export const useContacts = () => { search: '', id: '', telefono: '', - empleado: true + empleado: true, + presente: false }) }) @@ -87,6 +90,19 @@ export const useContacts = () => { } } + /** + * Cargar IDs de empleados presentes + */ + const fetchPresentes = async () => { + try { + const data = await $fetch('/api/contacts/presentes') + presentIds.value = new Set(data) + } catch (err: any) { + console.error('Error al cargar presentes:', err) + // No es crítico, continuar sin filtro de presentes + } + } + /** * Actualizar alias de un contacto */ @@ -119,6 +135,11 @@ export const useContacts = () => { // Primero filtrar solo contactos con teléfono let result = contacts.value.filter(c => c.telefono && c.telefono.trim() !== '') + // Filtro por presentes (solo si está activo) + if (filters.value.presente) { + result = result.filter(c => presentIds.value.has(c.id)) + } + // Filtro por teléfono (parcial) if (filters.value.telefono) { const telFilter = filters.value.telefono.replace(/\D/g, '') @@ -177,7 +198,8 @@ export const useContacts = () => { search: '', id: '', telefono: '', - empleado: true + empleado: true, + presente: false } } @@ -189,15 +211,27 @@ export const useContacts = () => { } ) + // Watch para recargar presentes cuando se activa el filtro + watch( + () => filters.value.presente, + (newVal) => { + if (newVal && presentIds.value.size === 0) { + fetchPresentes() + } + } + ) + return { contacts, aliases, + presentIds, filters, filteredContacts, isLoading, error, fetchContacts, fetchAliases, + fetchPresentes, updateAlias, getDisplayName, hasAlias, diff --git a/nuxt4/server/api/contacts/presentes.get.ts b/nuxt4/server/api/contacts/presentes.get.ts new file mode 100644 index 0000000..bcf785d --- /dev/null +++ b/nuxt4/server/api/contacts/presentes.get.ts @@ -0,0 +1,86 @@ +/** + * API endpoint para obtener IDs de empleados presentes + * Consulta la tabla Asistencias del proyecto facturador en Supabase via Metabase API + * Un empleado está "presente" si su última asistencia tiene entrada pero NO tiene salida + */ + +interface MetabaseResponse { + data: { + cols: Array<{ name: string }> + rows: Array> + } +} + +export default defineEventHandler(async (event): Promise => { + const config = useRuntimeConfig() + const headers = getRequestHeaders(event) + + // Verificar autenticación + const uid = headers['x-authentik-uid'] + if (!uid) { + throw createError({ + statusCode: 401, + message: 'Usuario no autenticado' + }) + } + + // Obtener configuración de Metabase + const metabaseUrl = config.metabaseApiUrl as string + const metabaseApiKey = config.metabaseApiKey as string + const databaseId = config.metabaseDatabaseId as number + + if (!metabaseApiKey) { + throw createError({ + statusCode: 500, + message: 'API Key de Metabase no configurada' + }) + } + + // Query nativa para obtener empleados presentes + // Busca la última asistencia de cada empleado donde salida es NULL + const nativeQuery = ` + WITH ultima_asistencia AS ( + SELECT + empleado_id, + entrada, + salida, + ROW_NUMBER() OVER (PARTITION BY empleado_id ORDER BY entrada DESC) as rn + FROM asistencias + WHERE entrada IS NOT NULL + ) + SELECT empleado_id + FROM ultima_asistencia + WHERE rn = 1 AND salida IS NULL + ` + + const metabaseQuery = { + database: databaseId, + type: 'native', + native: { + query: nativeQuery + } + } + + try { + const response = await $fetch(`${metabaseUrl}/api/dataset`, { + method: 'POST', + headers: { + 'X-API-Key': metabaseApiKey, + 'Content-Type': 'application/json' + }, + body: metabaseQuery + }) + + // Extraer los IDs de empleados presentes + const presentIds: number[] = response.data.rows.map(row => row[0] as number) + + return presentIds + } catch (error: unknown) { + console.error('Error al obtener empleados presentes de Metabase:', error) + const err = error as { statusCode?: number; message?: string } + throw createError({ + statusCode: err.statusCode || 500, + message: err.message || 'Error al obtener empleados presentes' + }) + } +})