Agregar filtro Solo Presentes y reorganizar filtros
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 57s

- 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
This commit is contained in:
2025-12-05 12:17:15 -06:00
parent 39a01d351b
commit eb5fa191c1
4 changed files with 139 additions and 6 deletions

View File

@@ -19,7 +19,7 @@
</button> </button>
</div> </div>
<!-- Filtros adicionales --> <!-- Filtros en una fila con wrap -->
<div class="filter-row"> <div class="filter-row">
<!-- Filtro por ID --> <!-- Filtro por ID -->
<div class="filter-field filter-id"> <div class="filter-field filter-id">
@@ -43,7 +43,13 @@
<!-- Checkbox empleados --> <!-- Checkbox empleados -->
<label class="filter-checkbox"> <label class="filter-checkbox">
<UCheckbox v-model="empleadoFilter" /> <UCheckbox v-model="empleadoFilter" />
<span>Solo empleados</span> <span>Empleados</span>
</label>
<!-- Checkbox presentes -->
<label class="filter-checkbox">
<UCheckbox v-model="presenteFilter" />
<span>Presentes</span>
</label> </label>
<!-- Botón limpiar --> <!-- Botón limpiar -->
@@ -94,12 +100,18 @@ const empleadoFilter = computed({
set: (val) => emit('update:modelValue', { ...props.modelValue, empleado: val }) 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(() => { const hasActiveFilters = computed(() => {
return ( return (
props.modelValue.search || props.modelValue.search ||
props.modelValue.id || props.modelValue.id ||
props.modelValue.telefono || props.modelValue.telefono ||
!props.modelValue.empleado !props.modelValue.empleado ||
props.modelValue.presente
) )
}) })

View File

@@ -71,6 +71,7 @@ const {
error, error,
fetchContacts, fetchContacts,
fetchAliases, fetchAliases,
fetchPresentes,
updateAlias, updateAlias,
getWhatsAppUrl, getWhatsAppUrl,
clearFilters clearFilters
@@ -78,7 +79,7 @@ const {
// Cargar datos al montar // Cargar datos al montar
onMounted(async () => { onMounted(async () => {
await Promise.all([fetchContacts(), fetchAliases()]) await Promise.all([fetchContacts(), fetchAliases(), fetchPresentes()])
}) })
// Manejar actualización de alias // Manejar actualización de alias

View File

@@ -19,6 +19,7 @@ export interface ContactFilters {
id: string id: string
telefono: string telefono: string
empleado: boolean empleado: boolean
presente: boolean
} }
export const useContacts = () => { export const useContacts = () => {
@@ -27,6 +28,7 @@ export const useContacts = () => {
// Estado // Estado
const contacts = ref<Contact[]>([]) const contacts = ref<Contact[]>([])
const aliases = ref<Record<string, string>>({}) const aliases = ref<Record<string, string>>({})
const presentIds = ref<Set<number>>(new Set())
const isLoading = ref(false) const isLoading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
@@ -37,7 +39,8 @@ export const useContacts = () => {
search: '', search: '',
id: '', id: '',
telefono: '', 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<number[]>('/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 * Actualizar alias de un contacto
*/ */
@@ -119,6 +135,11 @@ export const useContacts = () => {
// Primero filtrar solo contactos con teléfono // Primero filtrar solo contactos con teléfono
let result = contacts.value.filter(c => c.telefono && c.telefono.trim() !== '') 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) // Filtro por teléfono (parcial)
if (filters.value.telefono) { if (filters.value.telefono) {
const telFilter = filters.value.telefono.replace(/\D/g, '') const telFilter = filters.value.telefono.replace(/\D/g, '')
@@ -177,7 +198,8 @@ export const useContacts = () => {
search: '', search: '',
id: '', id: '',
telefono: '', 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 { return {
contacts, contacts,
aliases, aliases,
presentIds,
filters, filters,
filteredContacts, filteredContacts,
isLoading, isLoading,
error, error,
fetchContacts, fetchContacts,
fetchAliases, fetchAliases,
fetchPresentes,
updateAlias, updateAlias,
getDisplayName, getDisplayName,
hasAlias, hasAlias,

View File

@@ -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<Array<unknown>>
}
}
export default defineEventHandler(async (event): Promise<number[]> => {
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<MetabaseResponse>(`${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'
})
}
})