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 | null } export interface Operacion { id: string tipo: string fecha: Date lugar_id: number | null meta: Record | 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 { 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(sql, params) return result.rows } /** * Obtiene un lote por su ID */ export async function getLoteById(id: string): Promise { const result = await query( '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 { const result = await query( '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 }): Promise { const result = await query( `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 | null }> ): Promise { 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(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 { 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 { const result = await query(` 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 { 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(sql, params) return result.rows } /** * Obtiene una operación por su ID */ export async function getOperacionById(id: string): Promise { const result = await query( '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 outputs: Array } | null> { const operacion = await getOperacionById(id) if (!operacion) return null // Obtener lotes de entrada const inputsResult = await query(` 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(` 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 inputs: Array<{ lote_id: string; cantidad_kg?: number }> outputs: Array<{ codigo?: string; tipo: string; cantidad_kg?: number; meta?: Record }> }): 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( `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( `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 { const result = await query( `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 { const result = await query( '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, } }