Fix: corregir arquitectura - TODO debe pasar por Metabase
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 49s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 49s
BREAKING: Violación de arquitectura corregida - Eliminar endpoint /api/postgres/query (acceso directo a DB prohibido) - Cambiar /api/clientes para usar query de Metabase en lugar de SQL directo - Crear endpoint /api/metabase/opciones-filtros para obtener opciones - Cambiar loadOpcionesFiltros para usar API en lugar de MCP directo - Usar "Informe Ingresos - Lista de Clientes con Totales" para clientes - Usar "Informe Ingresos - Opciones de Filtros" para opciones - Respetar filosofía: Metabase calcula TODO, Vue solo renderiza - La app NUNCA habla directamente con bases de datos
This commit is contained in:
@@ -599,35 +599,9 @@ function onUpdateFechaHasta(value: string | null) {
|
|||||||
// Cargar opciones de filtros desde Metabase
|
// Cargar opciones de filtros desde Metabase
|
||||||
async function loadOpcionesFiltros() {
|
async function loadOpcionesFiltros() {
|
||||||
try {
|
try {
|
||||||
// Ejecutar la query "Informe Ingresos - Opciones de Filtros" (ID: 53)
|
const result = await $fetch('/api/metabase/opciones-filtros')
|
||||||
const result = await $fetch('/__mcp/mcp__nucleodocs-metabase__metabase_execute_card', {
|
opcionesFiltros.value = result
|
||||||
method: 'POST',
|
console.log('[Informe] Opciones de filtros cargadas:', opcionesFiltros.value)
|
||||||
body: {
|
|
||||||
card_id: 53,
|
|
||||||
parameters: []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result && result.data && result.data.rows && result.data.rows.length > 0) {
|
|
||||||
const row = result.data.rows[0]
|
|
||||||
const cols = result.data.cols
|
|
||||||
|
|
||||||
// Transformar respuesta a objeto
|
|
||||||
const data: any = {}
|
|
||||||
cols.forEach((col: any, index: number) => {
|
|
||||||
data[col.name] = row[index]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Parsear arrays JSON de ubicaciones, calidades, tipos, estados
|
|
||||||
opcionesFiltros.value = {
|
|
||||||
ubicaciones: data.ubicaciones ? JSON.parse(data.ubicaciones) : [],
|
|
||||||
calidades: data.calidades ? JSON.parse(data.calidades) : [],
|
|
||||||
tipos: data.tipos ? JSON.parse(data.tipos) : [],
|
|
||||||
estados: data.estados ? JSON.parse(data.estados) : []
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[Informe] Opciones de filtros cargadas:', opcionesFiltros.value)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Informe] Error loading opciones de filtros:', error)
|
console.error('[Informe] Error loading opciones de filtros:', error)
|
||||||
// No fallar si no se pueden cargar las opciones, solo usar arrays vacíos
|
// No fallar si no se pueden cargar las opciones, solo usar arrays vacíos
|
||||||
|
|||||||
@@ -1,33 +1,64 @@
|
|||||||
/**
|
/**
|
||||||
* Get all clients from Supabase facturador database
|
* Get all clients from Metabase
|
||||||
|
* Uses "Informe Ingresos - Lista de Clientes con Totales" query
|
||||||
* Returns: id, name, ubicacion for use in filters
|
* Returns: id, name, ubicacion for use in filters
|
||||||
*/
|
*/
|
||||||
export default defineEventHandler(async () => {
|
export default defineEventHandler(async () => {
|
||||||
try {
|
try {
|
||||||
// Query clientes table ordered by name
|
// Get all cards to find the clientes query
|
||||||
const query = `
|
const allCards = await getMetabaseCards('all')
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
ubicacion,
|
|
||||||
cedula,
|
|
||||||
telefono,
|
|
||||||
email
|
|
||||||
FROM clientes
|
|
||||||
ORDER BY name ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
const result = await $fetch('/api/postgres/query', {
|
// Find "Informe Ingresos - Lista de Clientes con Totales" query
|
||||||
method: 'POST',
|
const card = allCards.find((c: any) => c.name === 'Informe Ingresos - Lista de Clientes con Totales')
|
||||||
body: { query }
|
|
||||||
|
if (!card) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Clientes query not found in Metabase'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the query with empty filters to get all clientes
|
||||||
|
const result = await executeCardQuery(card.id, [
|
||||||
|
{ type: 'text', target: ['variable', ['template-tag', 'fecha_desde']], value: '' },
|
||||||
|
{ type: 'text', target: ['variable', ['template-tag', 'fecha_hasta']], value: '' },
|
||||||
|
{ type: 'boolean', target: ['variable', ['template-tag', 'incluir_anulados']], value: false },
|
||||||
|
{ type: 'number', target: ['variable', ['template-tag', 'cliente_ids']], value: [] },
|
||||||
|
{ type: 'text', target: ['variable', ['template-tag', 'tipos']], value: [] },
|
||||||
|
{ type: 'text', target: ['variable', ['template-tag', 'estados']], value: [] },
|
||||||
|
{ type: 'text', target: ['variable', ['template-tag', 'ubicaciones']], value: [] },
|
||||||
|
{ type: 'text', target: ['variable', ['template-tag', 'calidades']], value: [] }
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!result.data?.rows || !result.data?.cols) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform rows to objects with only the fields we need
|
||||||
|
const cols = result.data.cols
|
||||||
|
const clientes = result.data.rows.map((row: any[]) => {
|
||||||
|
const obj: any = {}
|
||||||
|
cols.forEach((col: any, index: number) => {
|
||||||
|
obj[col.name] = row[index]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return only the fields needed for the selector
|
||||||
|
return {
|
||||||
|
id: obj.cliente_id,
|
||||||
|
name: obj.cliente_nombre,
|
||||||
|
ubicacion: obj.cliente_ubicacion,
|
||||||
|
cedula: obj.cliente_cedula,
|
||||||
|
// Map any other fields you need from the query result
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
// Sort by name
|
||||||
|
return clientes.sort((a: any, b: any) => a.name.localeCompare(b.name))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[API] Failed to fetch clientes:', error)
|
console.error('[API] Failed to fetch clientes from Metabase:', error)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: error.statusCode || 500,
|
statusCode: error.statusCode || 500,
|
||||||
statusMessage: error.statusMessage || 'Failed to fetch clientes'
|
statusMessage: error.statusMessage || 'Failed to fetch clientes from Metabase'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
62
nuxt4-app/server/api/metabase/opciones-filtros.get.ts
Normal file
62
nuxt4-app/server/api/metabase/opciones-filtros.get.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Get filter options from Metabase
|
||||||
|
* Uses "Informe Ingresos - Opciones de Filtros" query (ID: 53)
|
||||||
|
* Returns: ubicaciones, calidades, tipos, estados as arrays
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
try {
|
||||||
|
// Get all cards to find the opciones query
|
||||||
|
const allCards = await getMetabaseCards('all')
|
||||||
|
|
||||||
|
// Find "Informe Ingresos - Opciones de Filtros" query
|
||||||
|
const card = allCards.find((c: any) => c.name === 'Informe Ingresos - Opciones de Filtros')
|
||||||
|
|
||||||
|
if (!card) {
|
||||||
|
console.warn('[API] Opciones de Filtros query not found, returning empty options')
|
||||||
|
return {
|
||||||
|
ubicaciones: [],
|
||||||
|
calidades: [],
|
||||||
|
tipos: [],
|
||||||
|
estados: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the query (no parameters needed)
|
||||||
|
const result = await executeCardQuery(card.id, [])
|
||||||
|
|
||||||
|
if (!result.data?.rows?.[0] || !result.data?.cols) {
|
||||||
|
return {
|
||||||
|
ubicaciones: [],
|
||||||
|
calidades: [],
|
||||||
|
tipos: [],
|
||||||
|
estados: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = result.data.rows[0]
|
||||||
|
const cols = result.data.cols
|
||||||
|
|
||||||
|
// Transform to object
|
||||||
|
const data: any = {}
|
||||||
|
cols.forEach((col: any, index: number) => {
|
||||||
|
data[col.name] = row[index]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Parse JSON arrays
|
||||||
|
return {
|
||||||
|
ubicaciones: data.ubicaciones ? JSON.parse(data.ubicaciones) : [],
|
||||||
|
calidades: data.calidades ? JSON.parse(data.calidades) : [],
|
||||||
|
tipos: data.tipos ? JSON.parse(data.tipos) : [],
|
||||||
|
estados: data.estados ? JSON.parse(data.estados) : []
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[API] Failed to fetch opciones de filtros from Metabase:', error)
|
||||||
|
// Don't fail, just return empty options
|
||||||
|
return {
|
||||||
|
ubicaciones: [],
|
||||||
|
calidades: [],
|
||||||
|
tipos: [],
|
||||||
|
estados: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Execute a raw SQL query against Supabase/PostgreSQL
|
|
||||||
* This is a server-only endpoint for internal API use
|
|
||||||
*/
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const body = await readBody(event)
|
|
||||||
const { query, params = [] } = body
|
|
||||||
|
|
||||||
if (!query) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
statusMessage: 'Query is required'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Execute query using MCP postgres tool
|
|
||||||
const result = await $fetch('/__mcp/mcp__postgres__query', {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
query,
|
|
||||||
params
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[Postgres] Query failed:', error)
|
|
||||||
throw createError({
|
|
||||||
statusCode: error.statusCode || 500,
|
|
||||||
statusMessage: error.statusMessage || 'Database query failed'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user