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
719 lines
18 KiB
TypeScript
719 lines
18 KiB
TypeScript
import { query, getClient } from './db'
|
|
import type { PoolClient } from 'pg'
|
|
|
|
// =====================================================
|
|
// TIPOS TYPESCRIPT
|
|
// =====================================================
|
|
|
|
export interface Lote {
|
|
id: string
|
|
codigo: string | null
|
|
tipo: string
|
|
fecha_creado: Date
|
|
lugar_id: number | null
|
|
cantidad_kg: number | null
|
|
meta: Record<string, any> | null
|
|
}
|
|
|
|
export interface Operacion {
|
|
id: string
|
|
tipo: string
|
|
fecha: Date
|
|
lugar_id: number | null
|
|
meta: Record<string, any> | null
|
|
}
|
|
|
|
export interface OperacionLote {
|
|
operacion_id: string
|
|
lote_id: string
|
|
rol: 'input' | 'output'
|
|
cantidad_kg: number | null
|
|
}
|
|
|
|
export interface TrazabilidadRow {
|
|
lote_id: string
|
|
codigo: string | null
|
|
tipo: string
|
|
cantidad_kg: number | null
|
|
operacion_id: string | null
|
|
operacion_tipo: string | null
|
|
profundidad: number
|
|
parent_lote_id: string | null
|
|
}
|
|
|
|
export interface LoteConOrigen extends Lote {
|
|
operacion_id: string | null
|
|
operacion_tipo: string | null
|
|
operacion_fecha: Date | null
|
|
}
|
|
|
|
// =====================================================
|
|
// QUERIES PARA LOTES
|
|
// =====================================================
|
|
|
|
/**
|
|
* Obtiene todos los lotes con filtros opcionales
|
|
*/
|
|
export async function getLotes(filtros?: {
|
|
tipo?: string
|
|
limit?: number
|
|
offset?: number
|
|
soloFinales?: boolean
|
|
}): Promise<Lote[]> {
|
|
let sql = 'SELECT * FROM lotes WHERE 1=1'
|
|
const params: any[] = []
|
|
let paramCount = 1
|
|
|
|
if (filtros?.tipo) {
|
|
sql += ` AND tipo = $${paramCount}`
|
|
params.push(filtros.tipo)
|
|
paramCount++
|
|
}
|
|
|
|
// Filtrar solo lotes sin hijos (que no son input de ninguna operación)
|
|
if (filtros?.soloFinales) {
|
|
sql += ` AND id NOT IN (
|
|
SELECT DISTINCT lote_id
|
|
FROM operacion_lotes
|
|
WHERE rol = 'input'
|
|
)`
|
|
}
|
|
|
|
sql += ' ORDER BY fecha_creado DESC'
|
|
|
|
if (filtros?.limit) {
|
|
sql += ` LIMIT $${paramCount}`
|
|
params.push(filtros.limit)
|
|
paramCount++
|
|
}
|
|
|
|
if (filtros?.offset) {
|
|
sql += ` OFFSET $${paramCount}`
|
|
params.push(filtros.offset)
|
|
}
|
|
|
|
const result = await query<Lote>(sql, params)
|
|
return result.rows
|
|
}
|
|
|
|
/**
|
|
* Obtiene un lote por su ID
|
|
*/
|
|
export async function getLoteById(id: string): Promise<Lote | null> {
|
|
const result = await query<Lote>(
|
|
'SELECT * FROM lotes WHERE id = $1',
|
|
[id]
|
|
)
|
|
return result.rows[0] || null
|
|
}
|
|
|
|
/**
|
|
* Obtiene un lote por su código
|
|
*/
|
|
export async function getLoteByCodigo(codigo: string): Promise<Lote | null> {
|
|
const result = await query<Lote>(
|
|
'SELECT * FROM lotes WHERE codigo = $1',
|
|
[codigo]
|
|
)
|
|
return result.rows[0] || null
|
|
}
|
|
|
|
/**
|
|
* Crea un nuevo lote
|
|
*/
|
|
export async function createLote(data: {
|
|
codigo?: string
|
|
tipo: string
|
|
cantidad_kg?: number
|
|
lugar_id?: number
|
|
meta?: Record<string, any>
|
|
}): Promise<Lote> {
|
|
const result = await query<Lote>(
|
|
`INSERT INTO lotes (codigo, tipo, cantidad_kg, lugar_id, meta)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING *`,
|
|
[
|
|
data.codigo || null,
|
|
data.tipo,
|
|
data.cantidad_kg || null,
|
|
data.lugar_id || null,
|
|
data.meta ? JSON.stringify(data.meta) : null,
|
|
]
|
|
)
|
|
return result.rows[0]
|
|
}
|
|
|
|
/**
|
|
* Actualiza un lote existente
|
|
*/
|
|
export async function updateLote(
|
|
id: string,
|
|
data: Partial<{
|
|
codigo: string | null
|
|
tipo: string
|
|
cantidad_kg: number | null
|
|
lugar_id: number | null
|
|
meta: Record<string, any> | null
|
|
}>
|
|
): Promise<Lote | null> {
|
|
const fields: string[] = []
|
|
const params: any[] = []
|
|
let paramCount = 1
|
|
|
|
if (data.codigo !== undefined) {
|
|
fields.push(`codigo = $${paramCount}`)
|
|
params.push(data.codigo)
|
|
paramCount++
|
|
}
|
|
|
|
if (data.tipo !== undefined) {
|
|
fields.push(`tipo = $${paramCount}`)
|
|
params.push(data.tipo)
|
|
paramCount++
|
|
}
|
|
|
|
if (data.cantidad_kg !== undefined) {
|
|
fields.push(`cantidad_kg = $${paramCount}`)
|
|
params.push(data.cantidad_kg)
|
|
paramCount++
|
|
}
|
|
|
|
if (data.lugar_id !== undefined) {
|
|
fields.push(`lugar_id = $${paramCount}`)
|
|
params.push(data.lugar_id)
|
|
paramCount++
|
|
}
|
|
|
|
if (data.meta !== undefined) {
|
|
fields.push(`meta = $${paramCount}`)
|
|
params.push(data.meta ? JSON.stringify(data.meta) : null)
|
|
paramCount++
|
|
}
|
|
|
|
if (fields.length === 0) {
|
|
return getLoteById(id)
|
|
}
|
|
|
|
params.push(id)
|
|
|
|
const sql = `
|
|
UPDATE lotes
|
|
SET ${fields.join(', ')}
|
|
WHERE id = $${paramCount}
|
|
RETURNING *
|
|
`
|
|
|
|
const result = await query<Lote>(sql, params)
|
|
return result.rows[0] || null
|
|
}
|
|
|
|
/**
|
|
* Elimina un lote
|
|
* CUIDADO: Solo debe usarse en casos excepcionales. Preferir marcar como inactivo.
|
|
*/
|
|
export async function deleteLote(id: string): Promise<boolean> {
|
|
const result = await query(
|
|
'DELETE FROM lotes WHERE id = $1',
|
|
[id]
|
|
)
|
|
return (result.rowCount ?? 0) > 0
|
|
}
|
|
|
|
/**
|
|
* Obtiene todos los lotes con información de su operación de origen
|
|
*/
|
|
export async function getLotesConOrigen(): Promise<LoteConOrigen[]> {
|
|
const result = await query<LoteConOrigen>(`
|
|
SELECT * FROM vista_lotes_con_origen
|
|
ORDER BY fecha_creado DESC
|
|
`)
|
|
return result.rows
|
|
}
|
|
|
|
// =====================================================
|
|
// QUERIES PARA OPERACIONES
|
|
// =====================================================
|
|
|
|
/**
|
|
* Obtiene todas las operaciones con filtros opcionales
|
|
*/
|
|
export async function getOperaciones(filtros?: {
|
|
tipo?: string
|
|
limit?: number
|
|
offset?: number
|
|
}): Promise<Operacion[]> {
|
|
let sql = 'SELECT * FROM operaciones WHERE 1=1'
|
|
const params: any[] = []
|
|
let paramCount = 1
|
|
|
|
if (filtros?.tipo) {
|
|
sql += ` AND tipo = $${paramCount}`
|
|
params.push(filtros.tipo)
|
|
paramCount++
|
|
}
|
|
|
|
sql += ' ORDER BY fecha DESC'
|
|
|
|
if (filtros?.limit) {
|
|
sql += ` LIMIT $${paramCount}`
|
|
params.push(filtros.limit)
|
|
paramCount++
|
|
}
|
|
|
|
if (filtros?.offset) {
|
|
sql += ` OFFSET $${paramCount}`
|
|
params.push(filtros.offset)
|
|
}
|
|
|
|
const result = await query<Operacion>(sql, params)
|
|
return result.rows
|
|
}
|
|
|
|
/**
|
|
* Obtiene una operación por su ID
|
|
*/
|
|
export async function getOperacionById(id: string): Promise<Operacion | null> {
|
|
const result = await query<Operacion>(
|
|
'SELECT * FROM operaciones WHERE id = $1',
|
|
[id]
|
|
)
|
|
return result.rows[0] || null
|
|
}
|
|
|
|
/**
|
|
* Obtiene una operación con sus lotes relacionados (inputs y outputs)
|
|
*/
|
|
export async function getOperacionConLotes(id: string): Promise<{
|
|
operacion: Operacion
|
|
inputs: Array<Lote & { cantidad_kg_usada: number }>
|
|
outputs: Array<Lote & { cantidad_kg_producida: number }>
|
|
} | null> {
|
|
const operacion = await getOperacionById(id)
|
|
if (!operacion) return null
|
|
|
|
// Obtener lotes de entrada
|
|
const inputsResult = await query<Lote & { cantidad_kg_usada: number }>(`
|
|
SELECT l.*, ol.cantidad_kg as cantidad_kg_usada
|
|
FROM lotes l
|
|
JOIN operacion_lotes ol ON ol.lote_id = l.id
|
|
WHERE ol.operacion_id = $1 AND ol.rol = 'input'
|
|
ORDER BY l.codigo
|
|
`, [id])
|
|
|
|
// Obtener lotes de salida
|
|
const outputsResult = await query<Lote & { cantidad_kg_producida: number }>(`
|
|
SELECT l.*, ol.cantidad_kg as cantidad_kg_producida
|
|
FROM lotes l
|
|
JOIN operacion_lotes ol ON ol.lote_id = l.id
|
|
WHERE ol.operacion_id = $1 AND ol.rol = 'output'
|
|
ORDER BY l.codigo
|
|
`, [id])
|
|
|
|
return {
|
|
operacion,
|
|
inputs: inputsResult.rows,
|
|
outputs: outputsResult.rows,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Crea una nueva operación con sus lotes relacionados (TRANSACCIÓN)
|
|
* Esta función asegura que la operación y sus relaciones se creen atómicamente.
|
|
*/
|
|
export async function createOperacion(data: {
|
|
tipo: string
|
|
fecha?: Date
|
|
lugar_id?: number
|
|
meta?: Record<string, any>
|
|
inputs: Array<{ lote_id: string; cantidad_kg?: number }>
|
|
outputs: Array<{ codigo?: string; tipo: string; cantidad_kg?: number; meta?: Record<string, any> }>
|
|
}): Promise<{
|
|
operacion: Operacion
|
|
lotes_creados: Lote[]
|
|
}> {
|
|
const client = await getClient()
|
|
|
|
try {
|
|
await client.query('BEGIN')
|
|
|
|
// 1. Crear la operación
|
|
const operacionResult = await client.query<Operacion>(
|
|
`INSERT INTO operaciones (tipo, fecha, lugar_id, meta)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING *`,
|
|
[
|
|
data.tipo,
|
|
data.fecha || new Date(),
|
|
data.lugar_id || null,
|
|
data.meta ? JSON.stringify(data.meta) : null,
|
|
]
|
|
)
|
|
const operacion = operacionResult.rows[0]
|
|
|
|
// 2. Relacionar lotes de entrada
|
|
for (const input of data.inputs) {
|
|
await client.query(
|
|
`INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
|
VALUES ($1, $2, 'input', $3)`,
|
|
[operacion.id, input.lote_id, input.cantidad_kg || null]
|
|
)
|
|
}
|
|
|
|
// 3. Crear y relacionar lotes de salida
|
|
const lotesCreados: Lote[] = []
|
|
for (const output of data.outputs) {
|
|
const loteResult = await client.query<Lote>(
|
|
`INSERT INTO lotes (codigo, tipo, cantidad_kg, meta)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING *`,
|
|
[
|
|
output.codigo || null,
|
|
output.tipo,
|
|
output.cantidad_kg || null,
|
|
output.meta ? JSON.stringify(output.meta) : null,
|
|
]
|
|
)
|
|
const lote = loteResult.rows[0]
|
|
lotesCreados.push(lote)
|
|
|
|
await client.query(
|
|
`INSERT INTO operacion_lotes (operacion_id, lote_id, rol, cantidad_kg)
|
|
VALUES ($1, $2, 'output', $3)`,
|
|
[operacion.id, lote.id, output.cantidad_kg || null]
|
|
)
|
|
}
|
|
|
|
await client.query('COMMIT')
|
|
|
|
return {
|
|
operacion,
|
|
lotes_creados: lotesCreados,
|
|
}
|
|
} catch (error) {
|
|
await client.query('ROLLBACK')
|
|
throw error
|
|
} finally {
|
|
client.release()
|
|
}
|
|
}
|
|
|
|
// =====================================================
|
|
// QUERIES PARA OPERACION_LOTES
|
|
// =====================================================
|
|
|
|
/**
|
|
* Obtiene todas las relaciones lote-operación para una operación específica
|
|
*/
|
|
export async function getOperacionLotes(operacionId: string): Promise<OperacionLote[]> {
|
|
const result = await query<OperacionLote>(
|
|
`SELECT * FROM operacion_lotes WHERE operacion_id = $1 ORDER BY rol`,
|
|
[operacionId]
|
|
)
|
|
return result.rows
|
|
}
|
|
|
|
// =====================================================
|
|
// QUERIES DE TRAZABILIDAD
|
|
// =====================================================
|
|
|
|
/**
|
|
* Obtiene el historial completo de un lote usando la función recursiva de PostgreSQL
|
|
*/
|
|
export async function getTrazabilidad(loteId: string): Promise<TrazabilidadRow[]> {
|
|
const result = await query<TrazabilidadRow>(
|
|
'SELECT * FROM get_trazabilidad($1)',
|
|
[loteId]
|
|
)
|
|
return result.rows
|
|
}
|
|
|
|
/**
|
|
* Obtiene estadísticas de un lote (cuántos ancestros tiene, profundidad máxima, etc.)
|
|
*/
|
|
export async function getEstadisticasLote(loteId: string): Promise<{
|
|
total_ancestros: number
|
|
profundidad_maxima: number
|
|
kg_iniciales: number | null
|
|
}> {
|
|
const trazabilidad = await getTrazabilidad(loteId)
|
|
|
|
const profundidadMaxima = Math.max(...trazabilidad.map(t => t.profundidad))
|
|
const totalAncestros = trazabilidad.length - 1 // -1 para no contar el lote mismo
|
|
|
|
// Buscar lotes de ingreso (profundidad máxima)
|
|
const ingresos = trazabilidad.filter(t => t.profundidad === profundidadMaxima)
|
|
const kgIniciales = ingresos.reduce((sum, t) => sum + (t.cantidad_kg || 0), 0)
|
|
|
|
return {
|
|
total_ancestros: totalAncestros,
|
|
profundidad_maxima: profundidadMaxima,
|
|
kg_iniciales: kgIniciales,
|
|
}
|
|
}
|
|
|
|
// =====================================================
|
|
// TIPOS PARA VINCULACIONES EXTERNAS
|
|
// =====================================================
|
|
|
|
export interface VinculacionExterna {
|
|
id: string
|
|
tipo_registro: 'ingreso' | 'carreta' | 'salida' | 'rechazo'
|
|
registro_id: number
|
|
lote_id: string
|
|
fecha_vinculacion: Date
|
|
usuario_id: string | null
|
|
observaciones: string | null
|
|
datos_cache: Record<string, any> | null
|
|
periodo_cosecha: string
|
|
}
|
|
|
|
export interface LoteConVinculaciones extends Lote {
|
|
ingresos_vinculados: number
|
|
carretas_vinculadas: number
|
|
salidas_vinculadas: number
|
|
rechazos_vinculados: number
|
|
total_vinculaciones: number
|
|
}
|
|
|
|
// =====================================================
|
|
// QUERIES PARA VINCULACIONES EXTERNAS
|
|
// =====================================================
|
|
|
|
/**
|
|
* Obtiene todas las vinculaciones con filtros opcionales
|
|
*/
|
|
export async function getVinculaciones(filtros?: {
|
|
tipo_registro?: string
|
|
lote_id?: string
|
|
periodo_cosecha?: string
|
|
limit?: number
|
|
offset?: number
|
|
}): Promise<VinculacionExterna[]> {
|
|
let sql = 'SELECT * FROM vinculaciones_externas WHERE 1=1'
|
|
const params: any[] = []
|
|
let paramCount = 1
|
|
|
|
if (filtros?.tipo_registro) {
|
|
sql += ` AND tipo_registro = $${paramCount}`
|
|
params.push(filtros.tipo_registro)
|
|
paramCount++
|
|
}
|
|
|
|
if (filtros?.lote_id) {
|
|
sql += ` AND lote_id = $${paramCount}`
|
|
params.push(filtros.lote_id)
|
|
paramCount++
|
|
}
|
|
|
|
if (filtros?.periodo_cosecha) {
|
|
sql += ` AND periodo_cosecha = $${paramCount}`
|
|
params.push(filtros.periodo_cosecha)
|
|
paramCount++
|
|
}
|
|
|
|
sql += ' ORDER BY fecha_vinculacion DESC'
|
|
|
|
if (filtros?.limit) {
|
|
sql += ` LIMIT $${paramCount}`
|
|
params.push(filtros.limit)
|
|
paramCount++
|
|
}
|
|
|
|
if (filtros?.offset) {
|
|
sql += ` OFFSET $${paramCount}`
|
|
params.push(filtros.offset)
|
|
}
|
|
|
|
const result = await query<VinculacionExterna>(sql, params)
|
|
return result.rows
|
|
}
|
|
|
|
/**
|
|
* Obtiene vinculaciones por lote
|
|
*/
|
|
export async function getVinculacionesByLote(loteId: string): Promise<VinculacionExterna[]> {
|
|
const result = await query<VinculacionExterna>(
|
|
'SELECT * FROM vinculaciones_externas WHERE lote_id = $1 ORDER BY tipo_registro, fecha_vinculacion DESC',
|
|
[loteId]
|
|
)
|
|
return result.rows
|
|
}
|
|
|
|
/**
|
|
* Obtiene IDs de registros ya vinculados por tipo
|
|
*/
|
|
export async function getRegistrosVinculados(
|
|
tipo: string,
|
|
periodo: string = '25-26'
|
|
): Promise<number[]> {
|
|
const result = await query<{ registro_id: number }>(
|
|
'SELECT registro_id FROM vinculaciones_externas WHERE tipo_registro = $1 AND periodo_cosecha = $2',
|
|
[tipo, periodo]
|
|
)
|
|
return result.rows.map(r => r.registro_id)
|
|
}
|
|
|
|
/**
|
|
* Crea una nueva vinculación
|
|
*/
|
|
export async function createVinculacion(data: {
|
|
tipo_registro: string
|
|
registro_id: number
|
|
lote_id: string
|
|
usuario_id?: string
|
|
observaciones?: string
|
|
datos_cache?: Record<string, any>
|
|
periodo_cosecha?: string
|
|
}): Promise<VinculacionExterna> {
|
|
const result = await query<VinculacionExterna>(
|
|
`INSERT INTO vinculaciones_externas
|
|
(tipo_registro, registro_id, lote_id, usuario_id, observaciones, datos_cache, periodo_cosecha)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING *`,
|
|
[
|
|
data.tipo_registro,
|
|
data.registro_id,
|
|
data.lote_id,
|
|
data.usuario_id || null,
|
|
data.observaciones || null,
|
|
data.datos_cache ? JSON.stringify(data.datos_cache) : null,
|
|
data.periodo_cosecha || '25-26'
|
|
]
|
|
)
|
|
return result.rows[0]
|
|
}
|
|
|
|
/**
|
|
* Crea múltiples vinculaciones en una transacción
|
|
*/
|
|
export async function createVinculacionesMasivas(
|
|
vinculaciones: Array<{
|
|
tipo_registro: string
|
|
registro_id: number
|
|
lote_id: string
|
|
usuario_id?: string
|
|
observaciones?: string
|
|
datos_cache?: Record<string, any>
|
|
periodo_cosecha?: string
|
|
}>
|
|
): Promise<VinculacionExterna[]> {
|
|
const client = await getClient()
|
|
|
|
try {
|
|
await client.query('BEGIN')
|
|
|
|
const resultados: VinculacionExterna[] = []
|
|
|
|
for (const v of vinculaciones) {
|
|
const result = await client.query<VinculacionExterna>(
|
|
`INSERT INTO vinculaciones_externas
|
|
(tipo_registro, registro_id, lote_id, usuario_id, observaciones, datos_cache, periodo_cosecha)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING *`,
|
|
[
|
|
v.tipo_registro,
|
|
v.registro_id,
|
|
v.lote_id,
|
|
v.usuario_id || null,
|
|
v.observaciones || null,
|
|
v.datos_cache ? JSON.stringify(v.datos_cache) : null,
|
|
v.periodo_cosecha || '25-26'
|
|
]
|
|
)
|
|
resultados.push(result.rows[0])
|
|
}
|
|
|
|
await client.query('COMMIT')
|
|
return resultados
|
|
} catch (error) {
|
|
await client.query('ROLLBACK')
|
|
throw error
|
|
} finally {
|
|
client.release()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Elimina una vinculación
|
|
*/
|
|
export async function deleteVinculacion(id: string): Promise<boolean> {
|
|
const result = await query(
|
|
'DELETE FROM vinculaciones_externas WHERE id = $1',
|
|
[id]
|
|
)
|
|
return (result.rowCount ?? 0) > 0
|
|
}
|
|
|
|
/**
|
|
* Obtiene lotes con conteo de vinculaciones
|
|
*/
|
|
export async function getLotesConVinculaciones(filtros?: {
|
|
tipo?: string
|
|
limit?: number
|
|
offset?: number
|
|
}): Promise<LoteConVinculaciones[]> {
|
|
let sql = 'SELECT * FROM vista_lotes_con_vinculaciones WHERE 1=1'
|
|
const params: any[] = []
|
|
let paramCount = 1
|
|
|
|
if (filtros?.tipo) {
|
|
sql += ` AND tipo = $${paramCount}`
|
|
params.push(filtros.tipo)
|
|
paramCount++
|
|
}
|
|
|
|
sql += ' ORDER BY fecha_creado DESC'
|
|
|
|
if (filtros?.limit) {
|
|
sql += ` LIMIT $${paramCount}`
|
|
params.push(filtros.limit)
|
|
paramCount++
|
|
}
|
|
|
|
if (filtros?.offset) {
|
|
sql += ` OFFSET $${paramCount}`
|
|
params.push(filtros.offset)
|
|
}
|
|
|
|
const result = await query<LoteConVinculaciones>(sql, params)
|
|
return result.rows
|
|
}
|
|
|
|
/**
|
|
* Obtiene estadísticas de vinculación para un período
|
|
*/
|
|
export async function getEstadisticasVinculacion(periodo: string = '25-26'): Promise<{
|
|
ingreso: { vinculados: number }
|
|
carreta: { vinculados: number }
|
|
salida: { vinculados: number }
|
|
rechazo: { vinculados: number }
|
|
total_vinculados: number
|
|
}> {
|
|
const result = await query<{ tipo_registro: string; count: string }>(
|
|
`SELECT tipo_registro, COUNT(*) as count
|
|
FROM vinculaciones_externas
|
|
WHERE periodo_cosecha = $1
|
|
GROUP BY tipo_registro`,
|
|
[periodo]
|
|
)
|
|
|
|
const stats = {
|
|
ingreso: { vinculados: 0 },
|
|
carreta: { vinculados: 0 },
|
|
salida: { vinculados: 0 },
|
|
rechazo: { vinculados: 0 },
|
|
total_vinculados: 0
|
|
}
|
|
|
|
for (const row of result.rows) {
|
|
const count = parseInt(row.count)
|
|
if (row.tipo_registro === 'ingreso') stats.ingreso.vinculados = count
|
|
else if (row.tipo_registro === 'carreta') stats.carreta.vinculados = count
|
|
else if (row.tipo_registro === 'salida') stats.salida.vinculados = count
|
|
else if (row.tipo_registro === 'rechazo') stats.rechazo.vinculados = count
|
|
stats.total_vinculados += count
|
|
}
|
|
|
|
return stats
|
|
}
|