Agregar filtro Solo Presentes y reorganizar filtros
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 57s
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:
@@ -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
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
86
nuxt4/server/api/contacts/presentes.get.ts
Normal file
86
nuxt4/server/api/contacts/presentes.get.ts
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user