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:
51
nuxt4/server/api/contacts/aliases.get.ts
Normal file
51
nuxt4/server/api/contacts/aliases.get.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { readFile, mkdir } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
* API endpoint para obtener los aliases de contactos del usuario actual
|
||||
* Los aliases se guardan en archivos JSON por usuario
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const headers = getRequestHeaders(event)
|
||||
|
||||
// Verificar autenticación
|
||||
const uid = headers['x-authentik-uid']
|
||||
if (!uid) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Usuario no autenticado'
|
||||
})
|
||||
}
|
||||
|
||||
// Sanitizar UID para usar como nombre de archivo
|
||||
const safeUid = uid.replace(/[^a-zA-Z0-9-_]/g, '_')
|
||||
|
||||
// Ruta del archivo de aliases
|
||||
const dataDir = config.dataDir || './data'
|
||||
const aliasesDir = join(dataDir, 'contact-aliases')
|
||||
const aliasesFile = join(aliasesDir, `${safeUid}.json`)
|
||||
|
||||
try {
|
||||
// Crear directorio si no existe
|
||||
if (!existsSync(aliasesDir)) {
|
||||
await mkdir(aliasesDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Leer archivo de aliases si existe
|
||||
if (existsSync(aliasesFile)) {
|
||||
const content = await readFile(aliasesFile, 'utf-8')
|
||||
return JSON.parse(content)
|
||||
}
|
||||
|
||||
// Si no existe, retornar objeto vacío
|
||||
return {}
|
||||
} catch (error: any) {
|
||||
console.error('Error al leer aliases:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'Error al obtener los aliases'
|
||||
})
|
||||
}
|
||||
})
|
||||
86
nuxt4/server/api/contacts/aliases/[id].put.ts
Normal file
86
nuxt4/server/api/contacts/aliases/[id].put.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { readFile, writeFile, mkdir } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
* API endpoint para actualizar el alias de un contacto
|
||||
* PUT /api/contacts/aliases/:id
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const headers = getRequestHeaders(event)
|
||||
|
||||
// Verificar autenticación
|
||||
const uid = headers['x-authentik-uid']
|
||||
if (!uid) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Usuario no autenticado'
|
||||
})
|
||||
}
|
||||
|
||||
// Obtener ID del contacto desde la ruta
|
||||
const contactId = getRouterParam(event, 'id')
|
||||
if (!contactId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'ID de contacto requerido'
|
||||
})
|
||||
}
|
||||
|
||||
// Leer body
|
||||
const body = await readBody(event)
|
||||
const alias = body?.alias?.toString().trim() || ''
|
||||
|
||||
// Validar alias (max 50 caracteres)
|
||||
if (alias.length > 50) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'El alias no puede tener más de 50 caracteres'
|
||||
})
|
||||
}
|
||||
|
||||
// Sanitizar UID para usar como nombre de archivo
|
||||
const safeUid = uid.replace(/[^a-zA-Z0-9-_]/g, '_')
|
||||
|
||||
// Ruta del archivo de aliases
|
||||
const dataDir = config.dataDir || './data'
|
||||
const aliasesDir = join(dataDir, 'contact-aliases')
|
||||
const aliasesFile = join(aliasesDir, `${safeUid}.json`)
|
||||
|
||||
try {
|
||||
// Crear directorio si no existe
|
||||
if (!existsSync(aliasesDir)) {
|
||||
await mkdir(aliasesDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Leer aliases existentes
|
||||
let aliases: Record<string, string> = {}
|
||||
if (existsSync(aliasesFile)) {
|
||||
const content = await readFile(aliasesFile, 'utf-8')
|
||||
aliases = JSON.parse(content)
|
||||
}
|
||||
|
||||
// Actualizar o eliminar alias
|
||||
if (alias) {
|
||||
aliases[contactId] = alias
|
||||
} else {
|
||||
delete aliases[contactId]
|
||||
}
|
||||
|
||||
// Guardar archivo
|
||||
await writeFile(aliasesFile, JSON.stringify(aliases, null, 2), 'utf-8')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
contactId,
|
||||
alias: alias || null
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error al actualizar alias:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'Error al actualizar el alias'
|
||||
})
|
||||
}
|
||||
})
|
||||
111
nuxt4/server/api/contacts/index.get.ts
Normal file
111
nuxt4/server/api/contacts/index.get.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* API endpoint para obtener contactos desde Metabase
|
||||
* Consulta la tabla Clientes del proyecto facturador en Supabase via Metabase API
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
interface MetabaseResponse {
|
||||
data: {
|
||||
cols: Array<{ name: string }>
|
||||
rows: Array<Array<unknown>>
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event): Promise<Contact[]> => {
|
||||
const config = useRuntimeConfig()
|
||||
const headers = getRequestHeaders(event)
|
||||
const query = getQuery(event)
|
||||
|
||||
// Verificar autenticación
|
||||
const uid = headers['x-authentik-uid']
|
||||
if (!uid) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Usuario no autenticado'
|
||||
})
|
||||
}
|
||||
|
||||
// Obtener configuración de Metabase
|
||||
const metabaseUrl = config.metabaseApiUrl as string
|
||||
const metabaseApiKey = config.metabaseApiKey as string
|
||||
const databaseId = config.metabaseDatabaseId as number
|
||||
const tableId = config.metabaseTableId as number
|
||||
|
||||
if (!metabaseApiKey) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'API Key de Metabase no configurada'
|
||||
})
|
||||
}
|
||||
|
||||
// Construir filtros para la query
|
||||
const filters: unknown[] = []
|
||||
|
||||
// Filtro de empleados (por defecto true)
|
||||
const empleadoFilter = query.empleado !== 'false'
|
||||
if (empleadoFilter) {
|
||||
filters.push(['=', ['field', 'empleado', { 'base-type': 'type/Boolean' }], true])
|
||||
}
|
||||
|
||||
// Filtro por ID exacto
|
||||
if (query.id) {
|
||||
const idNum = parseInt(query.id as string)
|
||||
if (!isNaN(idNum)) {
|
||||
filters.push(['=', ['field', 'id', { 'base-type': 'type/BigInteger' }], idNum])
|
||||
}
|
||||
}
|
||||
|
||||
// Construir la query para Metabase
|
||||
const metabaseQuery = {
|
||||
database: databaseId,
|
||||
type: 'query',
|
||||
query: {
|
||||
'source-table': tableId,
|
||||
'order-by': [['asc', ['field', 'id', { 'base-type': 'type/BigInteger' }]]],
|
||||
filter: filters.length > 0
|
||||
? (filters.length === 1 ? filters[0] : ['and', ...filters])
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await $fetch<MetabaseResponse>(`${metabaseUrl}/api/dataset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-API-Key': metabaseApiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: metabaseQuery
|
||||
})
|
||||
|
||||
// Mapear columnas a objetos
|
||||
const cols = response.data.cols.map((col) => col.name)
|
||||
const contacts: Contact[] = response.data.rows.map((row) => {
|
||||
const contact: Record<string, unknown> = {}
|
||||
cols.forEach((colName, index) => {
|
||||
contact[colName] = row[index]
|
||||
})
|
||||
return contact as unknown as Contact
|
||||
})
|
||||
|
||||
return contacts
|
||||
} catch (error: unknown) {
|
||||
console.error('Error al obtener contactos de Metabase:', error)
|
||||
const err = error as { statusCode?: number; message?: string }
|
||||
throw createError({
|
||||
statusCode: err.statusCode || 500,
|
||||
message: err.message || 'Error al obtener los contactos'
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user