Files
perfil/nuxt4/app/composables/useContacts.ts
josedario87 eb5fa191c1
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 57s
Agregar filtro Solo Presentes y reorganizar filtros
- 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
2025-12-05 12:17:15 -06:00

242 lines
5.8 KiB
TypeScript

/**
* Composable para gestión de contactos
* Maneja fetching, filtrado, aliases y acciones
*/
export interface Contact {
id: number
name: string
cedula: number | null
ubicacion: string | null
grupo_estudio: string | null
empleado: boolean
avatar_url: string | null
telefono: string | null
idciat: string | null
}
export interface ContactFilters {
search: string
id: string
telefono: string
empleado: boolean
presente: boolean
}
export const useContacts = () => {
const { fuzzyMatch, fuzzyScore } = useFuzzySearch()
// Estado
const contacts = ref<Contact[]>([])
const aliases = ref<Record<string, string>>({})
const presentIds = ref<Set<number>>(new Set())
const isLoading = ref(false)
const error = ref<string | null>(null)
// Filtros con persistencia en cookie
const filtersCookie = useCookie<ContactFilters>('contact-filters', {
maxAge: 60 * 60 * 24 * 7, // 1 semana
default: () => ({
search: '',
id: '',
telefono: '',
empleado: true,
presente: false
})
})
const filters = ref<ContactFilters>({ ...filtersCookie.value })
// Sincronizar filtros con cookie
watch(filters, (newFilters) => {
filtersCookie.value = newFilters
}, { deep: true })
/**
* Cargar contactos desde el servidor
*/
const fetchContacts = async () => {
isLoading.value = true
error.value = null
try {
const params = new URLSearchParams()
params.set('empleado', filters.value.empleado.toString())
// Filtro por ID se aplica en servidor si es exacto
if (filters.value.id && !isNaN(parseInt(filters.value.id))) {
params.set('id', filters.value.id)
}
const data = await $fetch<Contact[]>(`/api/contacts?${params.toString()}`)
contacts.value = data
} catch (err: any) {
console.error('Error al cargar contactos:', err)
error.value = err.message || 'Error al cargar contactos'
} finally {
isLoading.value = false
}
}
/**
* Cargar aliases del usuario
*/
const fetchAliases = async () => {
try {
const data = await $fetch<Record<string, string>>('/api/contacts/aliases')
aliases.value = data
} catch (err: any) {
console.error('Error al cargar aliases:', err)
// No es crítico, continuar sin aliases
}
}
/**
* 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
*/
const updateAlias = async (contactId: number, alias: string): Promise<boolean> => {
try {
await $fetch(`/api/contacts/aliases/${contactId}`, {
method: 'PUT',
body: { alias }
})
// Actualizar estado local
if (alias) {
aliases.value[contactId.toString()] = alias
} else {
delete aliases.value[contactId.toString()]
}
return true
} catch (err: any) {
console.error('Error al actualizar alias:', err)
return false
}
}
/**
* Contactos filtrados (búsqueda fuzzy en cliente)
* Solo muestra contactos con teléfono registrado
*/
const filteredContacts = computed(() => {
// 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, '')
result = result.filter(c =>
c.telefono?.replace(/\D/g, '').includes(telFilter)
)
}
// Filtro por nombre/alias (fuzzy)
if (filters.value.search) {
result = result
.map(contact => ({
contact,
score: Math.max(
fuzzyScore(contact.name, filters.value.search),
fuzzyScore(aliases.value[contact.id.toString()] || '', filters.value.search)
)
}))
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score)
.map(({ contact }) => contact)
}
return result
})
/**
* Obtener nombre visible (alias o nombre real)
*/
const getDisplayName = (contact: Contact): string => {
return aliases.value[contact.id.toString()] || contact.name
}
/**
* Verificar si el contacto tiene alias
*/
const hasAlias = (contact: Contact): boolean => {
return !!aliases.value[contact.id.toString()]
}
/**
* Generar URL de WhatsApp
*/
const getWhatsAppUrl = (telefono: string | null): string | null => {
if (!telefono) return null
const cleanNumber = telefono.replace(/\D/g, '')
if (!cleanNumber) return null
return `https://wa.me/${cleanNumber}`
}
/**
* Limpiar filtros
*/
const clearFilters = () => {
filters.value = {
search: '',
id: '',
telefono: '',
empleado: true,
presente: false
}
}
// Watch para recargar cuando cambia el filtro de empleado o ID
watch(
() => [filters.value.empleado, filters.value.id],
() => {
fetchContacts()
}
)
// 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,
getWhatsAppUrl,
clearFilters
}
}