Fix: corregir arquitectura - TODO debe pasar por Metabase
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:
2025-10-29 18:43:04 -06:00
parent b9c2e43c88
commit 52e6f5cdce
4 changed files with 115 additions and 82 deletions

View File

@@ -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

View File

@@ -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'
}) })
} }
}) })

View 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: []
}
}
})

View File

@@ -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'
})
}
})