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

- 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:
2025-12-05 11:41:26 -06:00
parent 00596bd6df
commit 59f25adabe
13 changed files with 1512 additions and 17 deletions

View File

@@ -0,0 +1,201 @@
<template>
<div class="contacts-container">
<!-- Header -->
<div class="contacts-header">
<div class="flex items-center justify-between mb-4">
<h2 class="contacts-title">
<UIcon name="i-heroicons-users" class="w-6 h-6" />
Contactos
</h2>
<span v-if="filteredContacts.length > 0" class="contact-counter">
{{ filteredContacts.length }}
</span>
</div>
<!-- Filtros -->
<ContactsContactsFilters
v-model="filters"
@clear="clearFilters"
/>
</div>
<!-- Estados -->
<div v-if="isLoading" class="empty-state">
<UIcon name="i-heroicons-arrow-path" class="w-12 h-12 animate-spin text-primary" />
<p class="mt-4 text-gray-600 dark:text-gray-400">Cargando contactos...</p>
</div>
<div v-else-if="error" class="empty-state">
<UIcon name="i-heroicons-exclamation-triangle" class="w-16 h-16 text-error" />
<p class="mt-4 text-error font-semibold text-lg">Error al cargar contactos</p>
<p class="text-sm text-gray-500 mt-2">{{ error }}</p>
<UButton class="mt-4" variant="soft" @click="fetchContacts">
Reintentar
</UButton>
</div>
<div v-else-if="contacts.length === 0" class="empty-state">
<UIcon name="i-heroicons-user-group" class="w-16 h-16 text-gray-400" />
<p class="mt-4 text-gray-600 dark:text-gray-400 text-lg">No hay contactos disponibles</p>
</div>
<div v-else-if="filteredContacts.length === 0" class="empty-state">
<UIcon name="i-heroicons-funnel" class="w-16 h-16 text-gray-400" />
<p class="mt-4 text-gray-600 dark:text-gray-400 text-lg">No se encontraron contactos</p>
<p class="text-sm text-gray-500 mt-2">Prueba ajustando los filtros</p>
</div>
<!-- Lista de contactos -->
<div v-else class="contacts-list">
<ContactsContactItem
v-for="contact in filteredContacts"
:key="contact.id"
:contact="contact"
:alias="aliases[contact.id.toString()]"
@update-alias="handleUpdateAlias"
@click="handleContactClick(contact)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import type { Contact } from '~/composables/useContacts'
const {
contacts,
aliases,
filters,
filteredContacts,
isLoading,
error,
fetchContacts,
fetchAliases,
updateAlias,
getWhatsAppUrl,
clearFilters
} = useContacts()
// Cargar datos al montar
onMounted(async () => {
await Promise.all([fetchContacts(), fetchAliases()])
})
// Manejar actualización de alias
const handleUpdateAlias = async ({ contactId, alias }: { contactId: number; alias: string }) => {
await updateAlias(contactId, alias)
}
// Click en contacto abre WhatsApp
const handleContactClick = (contact: Contact) => {
const url = getWhatsAppUrl(contact.telefono)
if (url) {
window.open(url, '_blank')
}
}
</script>
<style scoped>
.contacts-container {
background: rgba(255, 255, 255, 0.35);
backdrop-filter: blur(20px) saturate(180%);
border-radius: 1.5rem;
padding: 2rem;
box-shadow:
0 8px 32px 0 rgba(31, 38, 135, 0.15),
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.18);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.contacts-header {
margin-bottom: 1.5rem;
}
.contacts-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.75rem;
font-weight: 700;
color: var(--color-gray-900);
margin: 0;
}
.contact-counter {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.5rem;
height: 2.5rem;
padding: 0 0.75rem;
font-size: 1.125rem;
font-weight: 700;
border-radius: 0.875rem;
background: rgba(var(--color-primary-500), 0.15);
backdrop-filter: blur(10px) saturate(150%);
border: 1px solid rgba(var(--color-primary-500), 0.3);
color: rgb(var(--color-primary-500));
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 2px 8px 0 rgba(var(--color-primary-500), 0.2),
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -1px 2px 0 rgba(var(--color-primary-500), 0.1);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
text-align: center;
}
.contacts-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* Responsive */
@media (max-width: 640px) {
.contacts-container {
padding: 1.5rem;
}
.contacts-title {
font-size: 1.5rem;
}
.contacts-list {
gap: 0.5rem;
}
}
</style>
<style>
/* Modo oscuro */
.dark .contacts-container {
background: rgba(0, 0, 0, 0.15) !important;
box-shadow:
0 8px 32px 0 rgba(0, 0, 0, 0.5),
inset 0 1px 1px 0 rgba(255, 255, 255, 0.05) !important;
border: 1px solid rgba(255, 255, 255, 0.08) !important;
}
.dark .contacts-title {
color: var(--color-gray-100) !important;
}
.dark .contact-counter {
background: rgba(var(--color-primary-500), 0.25) !important;
border-color: rgba(var(--color-primary-500), 0.5) !important;
color: rgb(var(--color-primary-400)) !important;
box-shadow:
0 2px 8px 0 rgba(var(--color-primary-500), 0.4),
0 0 0 1px rgba(var(--color-primary-500), 0.3),
inset 0 1px 1px 0 rgba(255, 255, 255, 0.08),
inset 0 -1px 2px 0 rgba(var(--color-primary-500), 0.2) !important;
}
</style>