All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m46s
- Nuevo schema BD para vinculaciones_externas con constraint único por período - Cliente Metabase para consultar Ingresos, Carretas, Salidas y Rechazos - Endpoints API para registros externos (/api/externos/*) y vinculaciones (/api/vinculaciones/*) - Composable useRegistrosExternos con lógica de vinculación individual y masiva - Componentes: TablaRegistros, ModalAsignar, ProgressDashboard - Tab "Externos" en app.vue con sub-tabs y dashboard de progreso - LotesCard.vue ahora muestra registros vinculados al lote
291 lines
7.2 KiB
TypeScript
291 lines
7.2 KiB
TypeScript
/**
|
|
* 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<T = any> {
|
|
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<T = any>(
|
|
endpoint: string,
|
|
options: { method?: string; body?: any } = {}
|
|
): Promise<T> {
|
|
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<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
'X-API-KEY': apiKey
|
|
}
|
|
|
|
try {
|
|
const response = await $fetch<T>(`${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<T = any>(
|
|
sql: string,
|
|
databaseId?: number
|
|
): Promise<MetabaseQueryResult<T>> {
|
|
const config = useRuntimeConfig()
|
|
const dbId = databaseId || config.metabaseDatabaseId || 2
|
|
|
|
return metabaseFetch<MetabaseQueryResult<T>>('/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<T = Record<string, any>>(
|
|
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<string, any> = {}
|
|
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')
|
|
}
|
|
}
|