/** * 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([]) const aliases = ref>({}) const presentIds = ref>(new Set()) const isLoading = ref(false) const error = ref(null) // Filtros con persistencia en cookie const filtersCookie = useCookie('contact-filters', { maxAge: 60 * 60 * 24 * 7, // 1 semana default: () => ({ search: '', id: '', telefono: '', empleado: true, presente: true }) }) const filters = ref({ ...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(`/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>('/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('/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 => { 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: true } } // 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 } }