const { pool } = require('../db'); const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim(); const normalizeNumber = (value) => { if (value === '' || value === null || value === undefined) return null; const number = Number(String(value).replace(',', '.')); return Number.isFinite(number) && number > 0 ? number : null; }; const normalizeNonNegativeNumber = (value) => { if (value === '' || value === null || value === undefined) return null; const number = Number(String(value).replace(',', '.')); return Number.isFinite(number) && number >= 0 ? number : null; }; const normalizeKey = (value) => normalizeText(value) .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase(); const mapReceipt = (row) => ({ id: row.id, category: row.category, product: row.product, quantity: Number(row.quantity), unit: row.unit, supplier: row.supplier || '', invoice: row.invoice || '', notes: row.notes || '', status: row.status, createdAt: row.created_at, updatedAt: row.updated_at, approvedAt: row.approved_at }); const mapLot = (row) => ({ id: row.id, receiptId: row.receipt_id, category: row.category, product: row.product, quantity: Number(row.quantity), unit: row.unit, supplier: row.supplier || '', invoice: row.invoice || '', status: row.status, createdAt: row.created_at, updatedAt: row.updated_at }); const mapMovement = (row) => ({ id: row.id, receiptId: row.receipt_id, lotId: row.lot_id, type: row.type, category: row.category, product: row.product, quantity: Number(row.quantity), unit: row.unit, reason: row.reason || '', createdAt: row.created_at }); const mapFabricPlan = (row) => ({ id: row.id, material: row.material, color: row.color || '', quantityKg: Number(row.quantity_kg), supplier: row.supplier || '', priority: row.priority, status: row.status, createdAt: row.created_at, updatedAt: row.updated_at }); const createValidationError = (message) => { const error = new Error(message); error.statusCode = 400; return error; }; const listReceipts = async () => { const result = await pool.query(` SELECT id, category, product, quantity, unit, supplier, invoice, notes, status, created_at, updated_at, approved_at FROM supply_receipts ORDER BY created_at DESC, id DESC `); return result.rows.map(mapReceipt); }; const listLots = async () => { const result = await pool.query(` SELECT id, receipt_id, category, product, quantity, unit, supplier, invoice, status, created_at, updated_at FROM supply_stock_lots WHERE status = 'active' ORDER BY created_at DESC, id DESC `); return result.rows.map(mapLot); }; const listMovements = async () => { const result = await pool.query(` SELECT id, receipt_id, lot_id, type, category, product, quantity, unit, reason, created_at FROM supply_movements ORDER BY created_at DESC, id DESC `); return result.rows.map(mapMovement); }; const listFabricPlans = async () => { const result = await pool.query(` SELECT id, material, color, quantity_kg, supplier, priority, status, created_at, updated_at FROM supply_fabric_plans WHERE status = 'active' ORDER BY CASE priority WHEN 'Crítico' THEN 1 WHEN 'Atenção' THEN 2 ELSE 3 END, created_at DESC, id DESC `); return result.rows.map(mapFabricPlan); }; const buildPurchaseNeeds = (plans, lots, receipts) => { const needsByMaterial = new Map(); plans.forEach(plan => { const key = normalizeKey(plan.material); if (!key) return; const current = needsByMaterial.get(key) || { material: plan.material, plannedKg: 0, stockKg: 0, pendingKg: 0, purchaseKg: 0, priority: 'Normal', suppliers: new Set(), colors: new Set() }; current.plannedKg += plan.quantityKg; if (plan.supplier) current.suppliers.add(plan.supplier); if (plan.color) current.colors.add(plan.color); if (plan.priority === 'Crítico') current.priority = 'Crítico'; if (plan.priority === 'Atenção' && current.priority !== 'Crítico') current.priority = 'Atenção'; needsByMaterial.set(key, current); }); lots.forEach(lot => { if (lot.unit !== 'kg') return; const need = needsByMaterial.get(normalizeKey(lot.product)); if (need) need.stockKg += lot.quantity; }); receipts.forEach(receipt => { if (receipt.status !== 'pending' || receipt.unit !== 'kg') return; const need = needsByMaterial.get(normalizeKey(receipt.product)); if (need) need.pendingKg += receipt.quantity; }); return Array.from(needsByMaterial.values()) .map(need => { const purchaseKg = Math.max(need.plannedKg - need.stockKg - need.pendingKg, 0); let status = 'ok'; if (purchaseKg > 0 && (need.priority === 'Crítico' || need.stockKg === 0)) status = 'critical'; else if (purchaseKg > 0) status = 'attention'; return { material: need.material, plannedKg: need.plannedKg, stockKg: need.stockKg, pendingKg: need.pendingKg, purchaseKg, priority: need.priority, status, suppliers: Array.from(need.suppliers), colors: Array.from(need.colors) }; }) .sort((a, b) => { const statusOrder = { critical: 1, attention: 2, ok: 3 }; return statusOrder[a.status] - statusOrder[b.status] || b.purchaseKg - a.purchaseKg || a.material.localeCompare(b.material); }); }; const buildStats = (receipts, lots, purchaseNeeds) => { const totalQuantityKg = lots.reduce((total, lot) => ( lot.unit === 'kg' ? total + lot.quantity : total ), 0); return { totalQuantityKg, activeLots: lots.length, rolls: lots.filter(lot => lot.unit === 'rolos').reduce((total, lot) => total + lot.quantity, 0), alerts: purchaseNeeds.filter(need => need.status !== 'ok').length, pendingReceipts: receipts.filter(receipt => receipt.status === 'pending').length, approvedReceipts: receipts.filter(receipt => receipt.status === 'approved').length }; }; const getSupplySummary = async () => { const [receipts, lots, movements, fabricPlans] = await Promise.all([ listReceipts(), listLots(), listMovements(), listFabricPlans() ]); const purchaseNeeds = buildPurchaseNeeds(fabricPlans, lots, receipts); return { receipts, lots, movements, fabricPlans, purchaseNeeds, stats: buildStats(receipts, lots, purchaseNeeds) }; }; const listPurchaseNeeds = async () => { const [plans, lots, receipts] = await Promise.all([ listFabricPlans(), listLots(), listReceipts() ]); return buildPurchaseNeeds(plans, lots, receipts); }; const createReceipt = async (payload) => { const category = normalizeText(payload.category); const product = normalizeText(payload.product); const quantity = normalizeNumber(payload.quantity); const unit = normalizeText(payload.unit) || 'kg'; if (!category) throw createValidationError('Categoria é obrigatória.'); if (!product) throw createValidationError('Produto ou material é obrigatório.'); if (!quantity) throw createValidationError('Quantidade deve ser maior que zero.'); const result = await pool.query(` INSERT INTO supply_receipts ( category, product, quantity, unit, supplier, invoice, notes, status, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', CURRENT_TIMESTAMP) RETURNING id, category, product, quantity, unit, supplier, invoice, notes, status, created_at, updated_at, approved_at `, [ category, product, quantity, unit, normalizeText(payload.supplier) || null, normalizeText(payload.invoice) || null, normalizeText(payload.notes) || null ]); return mapReceipt(result.rows[0]); }; const approveReceipt = async (id) => { const client = await pool.connect(); try { await client.query('BEGIN'); const receiptResult = await client.query(` SELECT id, category, product, quantity, unit, supplier, invoice, notes, status, created_at, updated_at, approved_at FROM supply_receipts WHERE id = $1 FOR UPDATE `, [id]); if (!receiptResult.rowCount) { throw createValidationError('Recebimento não encontrado.'); } const receipt = receiptResult.rows[0]; if (receipt.status === 'approved') { await client.query('COMMIT'); return mapReceipt(receipt); } const updatedReceiptResult = await client.query(` UPDATE supply_receipts SET status = 'approved', approved_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING id, category, product, quantity, unit, supplier, invoice, notes, status, created_at, updated_at, approved_at `, [id]); const lotResult = await client.query(` INSERT INTO supply_stock_lots ( receipt_id, category, product, quantity, unit, supplier, invoice, status, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'active', CURRENT_TIMESTAMP) RETURNING id `, [ receipt.id, receipt.category, receipt.product, receipt.quantity, receipt.unit, receipt.supplier, receipt.invoice ]); await client.query(` INSERT INTO supply_movements ( receipt_id, lot_id, type, category, product, quantity, unit, reason ) VALUES ($1, $2, 'receipt', $3, $4, $5, $6, $7) `, [ receipt.id, lotResult.rows[0].id, receipt.category, receipt.product, receipt.quantity, receipt.unit, `Recebimento aprovado${receipt.invoice ? ` · NF ${receipt.invoice}` : ''}` ]); await client.query('COMMIT'); return mapReceipt(updatedReceiptResult.rows[0]); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } }; const deleteReceipt = async (id) => { const client = await pool.connect(); try { await client.query('BEGIN'); await client.query('DELETE FROM supply_movements WHERE receipt_id = $1', [id]); await client.query('DELETE FROM supply_stock_lots WHERE receipt_id = $1', [id]); await client.query('DELETE FROM supply_receipts WHERE id = $1', [id]); await client.query('COMMIT'); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } }; const createFabricPlan = async (payload) => { const material = normalizeText(payload.material); const quantityKg = normalizeNumber(payload.quantityKg); if (!material) throw createValidationError('Malha ou tecido é obrigatório.'); if (!quantityKg) throw createValidationError('Quantidade deve ser maior que zero.'); const result = await pool.query(` INSERT INTO supply_fabric_plans ( material, color, quantity_kg, supplier, priority, status, updated_at ) VALUES ($1, $2, $3, $4, $5, 'active', CURRENT_TIMESTAMP) RETURNING id, material, color, quantity_kg, supplier, priority, status, created_at, updated_at `, [ material, normalizeText(payload.color) || 'Todas as cores', quantityKg, normalizeText(payload.supplier) || null, normalizeText(payload.priority) || 'Normal' ]); return mapFabricPlan(result.rows[0]); }; const deleteFabricPlan = async (id) => { await pool.query(` UPDATE supply_fabric_plans SET status = 'removed', updated_at = CURRENT_TIMESTAMP WHERE id = $1 `, [id]); }; const updateLotQuantity = async (client, lotId, quantity) => { const status = quantity > 0 ? 'active' : 'depleted'; const result = await client.query(` UPDATE supply_stock_lots SET quantity = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING id, receipt_id, category, product, quantity, unit, supplier, invoice, status, created_at, updated_at `, [lotId, quantity, status]); return mapLot(result.rows[0]); }; const adjustInventoryLot = async (id, payload) => { const countedQuantity = normalizeNonNegativeNumber(payload.countedQuantity); const reason = normalizeText(payload.reason); if (countedQuantity === null) throw createValidationError('Quantidade contada deve ser zero ou maior.'); if (!reason) throw createValidationError('Justificativa do ajuste é obrigatória.'); const client = await pool.connect(); try { await client.query('BEGIN'); const lotResult = await client.query(` SELECT id, receipt_id, category, product, quantity, unit, supplier, invoice, status, created_at, updated_at FROM supply_stock_lots WHERE id = $1 FOR UPDATE `, [id]); if (!lotResult.rowCount) throw createValidationError('Lote não encontrado.'); const lot = lotResult.rows[0]; const currentQuantity = Number(lot.quantity); const difference = countedQuantity - currentQuantity; const updatedLot = await updateLotQuantity(client, lot.id, countedQuantity); await client.query(` INSERT INTO supply_movements ( lot_id, type, category, product, quantity, unit, reason ) VALUES ($1, 'inventory_adjustment', $2, $3, $4, $5, $6) `, [ lot.id, lot.category, lot.product, difference, lot.unit, `${reason} · sistema ${currentQuantity} ${lot.unit} · contado ${countedQuantity} ${lot.unit}` ]); await client.query('COMMIT'); return updatedLot; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } }; const consumeLotForProduction = async (id, payload) => { const quantity = normalizeNumber(payload.quantity); const reason = normalizeText(payload.reason); const productionOrderNumber = normalizeText(payload.productionOrderNumber); if (!quantity) throw createValidationError('Quantidade de saída deve ser maior que zero.'); if (!productionOrderNumber && !reason) throw createValidationError('Informe a OP ou uma justificativa para a saída.'); const client = await pool.connect(); try { await client.query('BEGIN'); const lotResult = await client.query(` SELECT id, receipt_id, category, product, quantity, unit, supplier, invoice, status, created_at, updated_at FROM supply_stock_lots WHERE id = $1 FOR UPDATE `, [id]); if (!lotResult.rowCount) throw createValidationError('Lote não encontrado.'); const lot = lotResult.rows[0]; const currentQuantity = Number(lot.quantity); if (lot.status !== 'active' || currentQuantity <= 0) throw createValidationError('Lote sem saldo disponível.'); if (quantity > currentQuantity) throw createValidationError('Quantidade de saída maior que o saldo do lote.'); const updatedLot = await updateLotQuantity(client, lot.id, currentQuantity - quantity); const movementReason = [ productionOrderNumber ? `OP ${productionOrderNumber}` : '', reason ].filter(Boolean).join(' · '); await client.query(` INSERT INTO supply_movements ( lot_id, type, category, product, quantity, unit, reason ) VALUES ($1, 'production_exit', $2, $3, $4, $5, $6) `, [ lot.id, lot.category, lot.product, -quantity, lot.unit, movementReason || 'Saída para produção' ]); await client.query('COMMIT'); return updatedLot; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } }; module.exports = { adjustInventoryLot, approveReceipt, consumeLotForProduction, createFabricPlan, createReceipt, deleteFabricPlan, deleteReceipt, getSupplySummary, listFabricPlans, listLots, listMovements, listPurchaseNeeds, listReceipts };