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:
317
nuxt4/app/components/contacts/ContactItem.vue
Normal file
317
nuxt4/app/components/contacts/ContactItem.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<div
|
||||
class="contact-item"
|
||||
:class="{ 'contact-item-clickable': contact.telefono }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div class="contact-content">
|
||||
<!-- Avatar -->
|
||||
<div class="contact-avatar">
|
||||
<img
|
||||
v-if="contact.avatar_url && !avatarError"
|
||||
:src="contact.avatar_url"
|
||||
:alt="displayName"
|
||||
class="avatar-img"
|
||||
@error="avatarError = true"
|
||||
/>
|
||||
<UIcon v-else name="i-heroicons-user-circle" class="avatar-icon" />
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="contact-info">
|
||||
<div class="contact-name-row">
|
||||
<span class="contact-name">{{ displayName }}</span>
|
||||
<UBadge v-if="hasAlias" size="xs" color="primary" variant="soft">
|
||||
Alias
|
||||
</UBadge>
|
||||
</div>
|
||||
<span v-if="contact.telefono" class="contact-phone">
|
||||
<UIcon name="i-heroicons-phone" class="phone-icon" />
|
||||
{{ contact.telefono }}
|
||||
</span>
|
||||
<span v-if="contact.ubicacion" class="contact-location">
|
||||
<UIcon name="i-heroicons-map-pin" class="location-icon" />
|
||||
{{ contact.ubicacion }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Acciones -->
|
||||
<div class="contact-actions" @click.stop>
|
||||
<UButton
|
||||
icon="i-heroicons-pencil-square"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
color="neutral"
|
||||
@click="openAliasModal"
|
||||
title="Editar alias"
|
||||
/>
|
||||
<UIcon
|
||||
v-if="contact.telefono"
|
||||
name="i-heroicons-chat-bubble-left-ellipsis"
|
||||
class="whatsapp-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para editar alias -->
|
||||
<UModal v-model:open="showAliasModal">
|
||||
<template #content>
|
||||
<div class="alias-modal">
|
||||
<h3 class="alias-modal-title">Editar alias</h3>
|
||||
<p class="alias-modal-original">
|
||||
<span class="label">Nombre original:</span>
|
||||
{{ contact.name }}
|
||||
</p>
|
||||
<UFormField label="Alias personalizado">
|
||||
<UInput
|
||||
v-model="newAlias"
|
||||
placeholder="Escribe un alias..."
|
||||
maxlength="50"
|
||||
class="alias-input"
|
||||
/>
|
||||
</UFormField>
|
||||
<p class="alias-modal-hint">
|
||||
Máximo 50 caracteres. Deja vacío para eliminar el alias.
|
||||
</p>
|
||||
<div class="alias-modal-actions">
|
||||
<UButton variant="ghost" color="neutral" @click="showAliasModal = false">
|
||||
Cancelar
|
||||
</UButton>
|
||||
<UButton :loading="saving" @click="saveAlias">
|
||||
Guardar
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Contact } from '~/composables/useContacts'
|
||||
|
||||
const props = defineProps<{
|
||||
contact: Contact
|
||||
alias?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update-alias': [{ contactId: number; alias: string }]
|
||||
'click': []
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
// Estado local
|
||||
const avatarError = ref(false)
|
||||
const showAliasModal = ref(false)
|
||||
const newAlias = ref('')
|
||||
const saving = ref(false)
|
||||
|
||||
// Computed
|
||||
const displayName = computed(() => props.alias || props.contact.name)
|
||||
const hasAlias = computed(() => !!props.alias)
|
||||
|
||||
// Métodos
|
||||
const handleClick = () => {
|
||||
if (props.contact.telefono) {
|
||||
emit('click')
|
||||
}
|
||||
}
|
||||
|
||||
const openAliasModal = () => {
|
||||
newAlias.value = props.alias || ''
|
||||
showAliasModal.value = true
|
||||
}
|
||||
|
||||
const saveAlias = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
emit('update-alias', {
|
||||
contactId: props.contact.id,
|
||||
alias: newAlias.value.trim()
|
||||
})
|
||||
showAliasModal.value = false
|
||||
toast.add({
|
||||
title: newAlias.value.trim() ? 'Alias guardado' : 'Alias eliminado',
|
||||
color: 'success'
|
||||
})
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contact-item {
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(15px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.contact-item-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact-item-clickable:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px 0 rgba(var(--color-primary-500), 0.2);
|
||||
border-color: rgba(var(--color-primary-500), 0.4);
|
||||
}
|
||||
|
||||
.contact-content {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--color-primary-500), 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: rgb(var(--color-primary-500));
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.contact-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.contact-name {
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-900);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.contact-phone,
|
||||
.contact-location {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.phone-icon,
|
||||
.location-icon {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.whatsapp-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: #25D366;
|
||||
}
|
||||
|
||||
/* Modal de alias */
|
||||
.alias-modal {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.alias-modal-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-900);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.alias-modal-original {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-600);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.alias-modal-original .label {
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
|
||||
.alias-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.alias-modal-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-gray-500);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.alias-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Modo oscuro */
|
||||
.dark .contact-item {
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.dark .contact-item-clickable:hover {
|
||||
box-shadow: 0 8px 24px 0 rgba(var(--color-primary-500), 0.3) !important;
|
||||
border-color: rgba(var(--color-primary-500), 0.5) !important;
|
||||
}
|
||||
|
||||
.dark .contact-name {
|
||||
color: var(--color-gray-100) !important;
|
||||
}
|
||||
|
||||
.dark .contact-phone,
|
||||
.dark .contact-location {
|
||||
color: var(--color-gray-400) !important;
|
||||
}
|
||||
|
||||
.dark .alias-modal-title {
|
||||
color: var(--color-gray-100) !important;
|
||||
}
|
||||
|
||||
.dark .alias-modal-original .label {
|
||||
color: var(--color-gray-300) !important;
|
||||
}
|
||||
</style>
|
||||
278
nuxt4/app/components/contacts/ContactsFilters.vue
Normal file
278
nuxt4/app/components/contacts/ContactsFilters.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="filters-section">
|
||||
<!-- Búsqueda por nombre (fuzzy) -->
|
||||
<div class="search-input-wrapper">
|
||||
<UIcon name="i-heroicons-magnifying-glass" class="search-icon" />
|
||||
<input
|
||||
v-model="localFilters.search"
|
||||
type="text"
|
||||
placeholder="Buscar por nombre..."
|
||||
class="search-input"
|
||||
/>
|
||||
<button
|
||||
v-if="localFilters.search"
|
||||
class="search-clear"
|
||||
title="Limpiar búsqueda"
|
||||
@click="localFilters.search = ''"
|
||||
>
|
||||
<UIcon name="i-heroicons-x-mark" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filtros adicionales -->
|
||||
<div class="filter-row">
|
||||
<!-- Filtro por ID -->
|
||||
<div class="filter-field filter-id">
|
||||
<UInput
|
||||
v-model="localFilters.id"
|
||||
type="number"
|
||||
placeholder="ID"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Filtro por teléfono -->
|
||||
<div class="filter-field filter-phone">
|
||||
<UInput
|
||||
v-model="localFilters.telefono"
|
||||
placeholder="Teléfono"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox empleados -->
|
||||
<label class="filter-checkbox">
|
||||
<UCheckbox v-model="localFilters.empleado" />
|
||||
<span>Solo empleados</span>
|
||||
</label>
|
||||
|
||||
<!-- Botón limpiar -->
|
||||
<UButton
|
||||
v-if="hasActiveFilters"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size="xs"
|
||||
icon="i-heroicons-x-mark"
|
||||
@click="clearFilters"
|
||||
>
|
||||
Limpiar
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ContactFilters } from '~/composables/useContacts'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: ContactFilters
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [ContactFilters]
|
||||
'clear': []
|
||||
}>()
|
||||
|
||||
const localFilters = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const hasActiveFilters = computed(() => {
|
||||
return (
|
||||
localFilters.value.search ||
|
||||
localFilters.value.id ||
|
||||
localFilters.value.telefono ||
|
||||
!localFilters.value.empleado
|
||||
)
|
||||
})
|
||||
|
||||
const clearFilters = () => {
|
||||
emit('clear')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filters-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: var(--color-gray-400);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 3rem 0.75rem 3rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
border-radius: 1rem;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px) saturate(150%);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
color: var(--color-gray-800);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow:
|
||||
0 2px 6px 0 rgba(31, 38, 135, 0.08),
|
||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-color: rgba(var(--color-primary-500), 0.3);
|
||||
box-shadow:
|
||||
0 4px 12px 0 rgba(var(--color-primary-500), 0.15),
|
||||
0 0 0 3px rgba(var(--color-primary-500), 0.1),
|
||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
color: var(--color-gray-500);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-clear:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: var(--color-gray-700);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.filter-field {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-id {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.filter-phone {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.filter-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-700);
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-checkbox:hover {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-id,
|
||||
.filter-phone {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Modo oscuro */
|
||||
.dark .search-input {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--color-gray-200) !important;
|
||||
box-shadow:
|
||||
0 2px 6px 0 rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.05) !important;
|
||||
}
|
||||
|
||||
.dark .search-input::placeholder {
|
||||
color: var(--color-gray-500) !important;
|
||||
}
|
||||
|
||||
.dark .search-input:focus {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
border-color: rgba(var(--color-primary-500), 0.5) !important;
|
||||
box-shadow:
|
||||
0 4px 12px 0 rgba(var(--color-primary-500), 0.3),
|
||||
0 0 0 3px rgba(var(--color-primary-500), 0.15),
|
||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
.dark .search-icon {
|
||||
color: var(--color-gray-500) !important;
|
||||
}
|
||||
|
||||
.dark .search-clear {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
color: var(--color-gray-400) !important;
|
||||
}
|
||||
|
||||
.dark .search-clear:hover {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--color-gray-200) !important;
|
||||
}
|
||||
|
||||
.dark .filter-checkbox {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--color-gray-300) !important;
|
||||
}
|
||||
|
||||
.dark .filter-checkbox:hover {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
</style>
|
||||
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