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:
201
nuxt4/app/components/contacts/ContactsList.vue
Normal file
201
nuxt4/app/components/contacts/ContactsList.vue
Normal 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>
|
||||
Reference in New Issue
Block a user