/** * Metabase API Client * * Proporciona funciones para consultar registros externos via Metabase API. * Solo lectura - no modifica datos en la base externa. * * Basado en el patrón de analiticaNucleo. */ import { useRuntimeConfig } from '#imports' // Tipos para las respuestas de Metabase export interface MetabaseQueryResult { data: { rows: T[][] cols: { name: string; display_name: string; base_type: string }[] rows_truncated?: number } database_id: number started_at: string status: string row_count: number running_time: number } /** * Hace una petición autenticada a la API de Metabase */ export async function metabaseFetch( endpoint: string, options: { method?: string; body?: any } = {} ): Promise { const config = useRuntimeConfig() const metabaseUrl = config.metabaseUrl || 'http://metabase:3000' const apiKey = config.metabaseApiKey if (!apiKey) { throw createError({ statusCode: 500, statusMessage: 'NUXT_METABASE_API_KEY no está configurada' }) } const headers: Record = { 'Content-Type': 'application/json', 'X-API-KEY': apiKey } try { const response = await $fetch(`${metabaseUrl}${endpoint}`, { method: options.method || 'GET', headers, body: options.body }) return response } catch (error: any) { console.error(`[Metabase] Request failed for ${endpoint}:`, error?.message || error) throw createError({ statusCode: error.statusCode || 500, statusMessage: error.message || 'Metabase request failed' }) } } /** * Ejecuta una consulta SQL nativa en Metabase */ export async function queryMetabaseSQL( sql: string, databaseId?: number ): Promise> { const config = useRuntimeConfig() const dbId = databaseId || config.metabaseDatabaseId || 2 return metabaseFetch>('/api/dataset', { method: 'POST', body: { database: dbId, type: 'native', native: { query: sql } } }) } /** * Transforma una respuesta de Metabase en un array de objetos */ export function transformMetabaseRows>( result: MetabaseQueryResult ): T[] { if (!result.data?.rows || !result.data?.cols) { return [] } const cols = result.data.cols return result.data.rows.map((row) => { const obj: Record = {} cols.forEach((col, index) => { obj[col.name] = row[index] }) return obj as T }) } // ===================================================== // CONSULTAS ESPECÍFICAS PARA REGISTROS EXTERNOS // ===================================================== /** * Período de cosecha en formato de fechas */ export function getPeriodoFechas(periodo: string = '25-26'): { inicio: string; fin: string } { const [yearStart] = periodo.split('-').map((y) => 2000 + parseInt(y)) return { inicio: `${yearStart}-09-10`, fin: `${yearStart + 1}-09-09` } } /** * Obtiene los ingresos del período usando la vista existente */ export async function getIngresos(periodo: string = '25-26') { const { inicio, fin } = getPeriodoFechas(periodo) const sql = ` SELECT v.id, v.created_at, v.tipo, v.calidad, v.humedad, v.estado, v.observacion, v.sacos_total, v.peso_bruto, v.peso_neto, v.peso_seco, v.cliente_id, v.comercio_id, c.nombre as cliente_nombre, com.nombre as comercio_nombre FROM vista_detalle_ingresos v LEFT JOIN clientes c ON c.id = v.cliente_id LEFT JOIN comercios com ON com.id = v.comercio_id WHERE v.created_at >= '${inicio}' AND v.created_at < '${fin}' AND v.estado != 'anulado' ORDER BY v.created_at DESC ` const result = await queryMetabaseSQL(sql) return transformMetabaseRows(result) } /** * Obtiene las carretas del período */ export async function getCarretas(periodo: string = '25-26') { const { inicio, fin } = getPeriodoFechas(periodo) const sql = ` SELECT id, created_at, titulo, estado, total as libras_mojadas, humedad, ROUND((total * (100 - COALESCE(humedad, 51)) / 100.0) / 100.0, 2) as qq_seco_estimado FROM carretas WHERE created_at >= '${inicio}' AND created_at < '${fin}' ORDER BY created_at DESC ` const result = await queryMetabaseSQL(sql) return transformMetabaseRows(result) } /** * Obtiene las salidas del período */ export async function getSalidas(periodo: string = '25-26') { const { inicio, fin } = getPeriodoFechas(periodo) const sql = ` SELECT s.id, s.created_at, s.comprador, s.observacion, s.pesadas, (SELECT SUM((p->>'peso')::numeric) FROM json_array_elements(s.pesadas) p) as peso_bruto, (SELECT SUM((p->>'sacos')::numeric) FROM json_array_elements(s.pesadas) p) as sacos_total FROM salidas s WHERE s.created_at >= '${inicio}' AND s.created_at < '${fin}' ORDER BY s.created_at DESC ` const result = await queryMetabaseSQL(sql) // Calcular peso_neto y qq_seco en el backend return transformMetabaseRows(result).map((row: any) => ({ ...row, peso_neto: (row.peso_bruto || 0) - ((row.sacos_total || 0) / 2), qq_seco: ((row.peso_bruto || 0) - ((row.sacos_total || 0) / 2)) / 100 })) } /** * Obtiene los rechazos del período */ export async function getRechazos(periodo: string = '25-26') { const { inicio, fin } = getPeriodoFechas(periodo) const sql = ` SELECT r.id, r.created_at, r.tipo, r.estado, r.cantidad, r.unidad, r.precio_unidad, r.observacion, r.comprador_id, c.nombre as comprador_nombre FROM rechazos r LEFT JOIN clientes c ON c.id = r.comprador_id WHERE r.created_at >= '${inicio}' AND r.created_at < '${fin}' AND r.estado != 'anulado' ORDER BY r.created_at DESC ` const result = await queryMetabaseSQL(sql) return transformMetabaseRows(result) } /** * Obtiene el conteo de registros por tipo para un período */ export async function getConteoRegistros(periodo: string = '25-26') { const { inicio, fin } = getPeriodoFechas(periodo) const sqlIngresos = ` SELECT COUNT(*) as total FROM vista_detalle_ingresos WHERE created_at >= '${inicio}' AND created_at < '${fin}' AND estado != 'anulado' ` const sqlCarretas = ` SELECT COUNT(*) as total FROM carretas WHERE created_at >= '${inicio}' AND created_at < '${fin}' ` const sqlSalidas = ` SELECT COUNT(*) as total FROM salidas WHERE created_at >= '${inicio}' AND created_at < '${fin}' ` const sqlRechazos = ` SELECT COUNT(*) as total FROM rechazos WHERE created_at >= '${inicio}' AND created_at < '${fin}' AND estado != 'anulado' ` const [ingresos, carretas, salidas, rechazos] = await Promise.all([ queryMetabaseSQL(sqlIngresos), queryMetabaseSQL(sqlCarretas), queryMetabaseSQL(sqlSalidas), queryMetabaseSQL(sqlRechazos) ]) return { ingresos: parseInt(ingresos.data.rows[0]?.[0] || '0'), carretas: parseInt(carretas.data.rows[0]?.[0] || '0'), salidas: parseInt(salidas.data.rows[0]?.[0] || '0'), rechazos: parseInt(rechazos.data.rows[0]?.[0] || '0') } }