Agregar sección Contactos con UTabs y conexión a Metabase
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m37s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m37s
- Implementar UTabs (Contactos, Aplicaciones, Perfil) en app.vue - Crear componentes ContactsList, ContactsFilters, ContactItem - Agregar server routes para obtener contactos via Metabase API - Sistema de aliases por usuario guardados en archivos JSON - Filtros: nombre (fuzzy search), ID, teléfono, empleado - Click en contacto abre WhatsApp - Estilo glassmorphism consistente con la app
This commit is contained in:
119
nuxt4/app/composables/useFuzzySearch.ts
Normal file
119
nuxt4/app/composables/useFuzzySearch.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Composable para búsqueda fuzzy de texto
|
||||
* Normaliza acentos y es tolerante a typos básicos
|
||||
*/
|
||||
export const useFuzzySearch = () => {
|
||||
/**
|
||||
* Normaliza un texto para comparación:
|
||||
* - Convierte a minúsculas
|
||||
* - Elimina acentos/diacríticos
|
||||
* - Elimina espacios extra
|
||||
*/
|
||||
const normalize = (text: string): string => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si el texto contiene la query (fuzzy)
|
||||
* Retorna true si hay match
|
||||
*/
|
||||
const fuzzyMatch = (text: string, query: string): boolean => {
|
||||
if (!query) return true
|
||||
if (!text) return false
|
||||
|
||||
const normalizedText = normalize(text)
|
||||
const normalizedQuery = normalize(query)
|
||||
|
||||
// Match exacto (substring)
|
||||
if (normalizedText.includes(normalizedQuery)) return true
|
||||
|
||||
// Match por palabras - cada palabra de la query debe coincidir con alguna palabra del texto
|
||||
const textWords = normalizedText.split(/\s+/)
|
||||
const queryWords = normalizedQuery.split(/\s+/)
|
||||
|
||||
return queryWords.every(queryWord =>
|
||||
textWords.some(textWord =>
|
||||
textWord.startsWith(queryWord) || textWord.includes(queryWord)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula un score de relevancia (0-100)
|
||||
* Mayor score = mejor match
|
||||
*/
|
||||
const fuzzyScore = (text: string, query: string): number => {
|
||||
if (!query) return 100
|
||||
if (!text) return 0
|
||||
|
||||
const normalizedText = normalize(text)
|
||||
const normalizedQuery = normalize(query)
|
||||
|
||||
// Match exacto completo
|
||||
if (normalizedText === normalizedQuery) return 100
|
||||
|
||||
// Match al inicio
|
||||
if (normalizedText.startsWith(normalizedQuery)) return 90
|
||||
|
||||
// Match exacto (substring)
|
||||
if (normalizedText.includes(normalizedQuery)) {
|
||||
const position = normalizedText.indexOf(normalizedQuery)
|
||||
return 80 - (position / normalizedText.length) * 20
|
||||
}
|
||||
|
||||
// Match por palabras
|
||||
const textWords = normalizedText.split(/\s+/)
|
||||
const queryWords = normalizedQuery.split(/\s+/)
|
||||
|
||||
let matchedWords = 0
|
||||
let startMatches = 0
|
||||
|
||||
queryWords.forEach(queryWord => {
|
||||
const match = textWords.find(textWord =>
|
||||
textWord.startsWith(queryWord) || textWord.includes(queryWord)
|
||||
)
|
||||
if (match) {
|
||||
matchedWords++
|
||||
if (match.startsWith(queryWord)) startMatches++
|
||||
}
|
||||
})
|
||||
|
||||
if (matchedWords === 0) return 0
|
||||
|
||||
const matchRatio = matchedWords / queryWords.length
|
||||
const startBonus = startMatches / queryWords.length * 10
|
||||
|
||||
return Math.round(matchRatio * 60 + startBonus)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtra y ordena una lista por relevancia de búsqueda
|
||||
*/
|
||||
const filterAndSort = <T>(
|
||||
items: T[],
|
||||
query: string,
|
||||
getText: (item: T) => string
|
||||
): T[] => {
|
||||
if (!query) return items
|
||||
|
||||
return items
|
||||
.map(item => ({
|
||||
item,
|
||||
score: fuzzyScore(getText(item), query)
|
||||
}))
|
||||
.filter(({ score }) => score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map(({ item }) => item)
|
||||
}
|
||||
|
||||
return {
|
||||
normalize,
|
||||
fuzzyMatch,
|
||||
fuzzyScore,
|
||||
filterAndSort
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user