const express = require('express'); const router = express.Router(); // Status code constants const STATUS = { CANCELED: 0, CREATED: 1, ELECTRONICALLY_READY_SEND: 10, ORDERED: 11, PREORDERED: 12, ELECTRONICALLY_SENT: 13, RECEIVING_STARTED: 15, DONE: 50 }; const RECEIVING_STATUS = { CANCELED: 0, CREATED: 1, PARTIAL_RECEIVED: 30, FULL_RECEIVED: 40, PAID: 50 }; // Get all purchase orders with summary metrics router.get('/', async (req, res) => { try { const pool = req.app.locals.pool; const { search, status, vendor, startDate, endDate, page = 1, limit = 100, sortColumn = 'date', sortDirection = 'desc' } = req.query; let whereClause = '1=1'; const params = []; let paramCounter = 1; if (search) { whereClause += ` AND (po.po_id ILIKE $${paramCounter} OR po.vendor ILIKE $${paramCounter})`; params.push(`%${search}%`); paramCounter++; } if (status && status !== 'all') { whereClause += ` AND po.status = $${paramCounter}`; params.push(Number(status)); paramCounter++; } if (vendor && vendor !== 'all') { whereClause += ` AND po.vendor = $${paramCounter}`; params.push(vendor); paramCounter++; } if (startDate) { whereClause += ` AND po.date >= $${paramCounter}`; params.push(startDate); paramCounter++; } if (endDate) { whereClause += ` AND po.date <= $${paramCounter}`; params.push(endDate); paramCounter++; } // Get filtered summary metrics const { rows: [summary] } = await pool.query(` WITH po_totals AS ( SELECT po_id, SUM(ordered) as total_ordered, SUM(received) as total_received, ROUND(SUM(ordered * cost_price)::numeric, 3) as total_cost FROM purchase_orders po WHERE ${whereClause} GROUP BY po_id ) SELECT COUNT(DISTINCT po_id) as order_count, SUM(total_ordered) as total_ordered, SUM(total_received) as total_received, ROUND( (SUM(total_received)::numeric / NULLIF(SUM(total_ordered), 0)), 3 ) as fulfillment_rate, ROUND(SUM(total_cost)::numeric, 3) as total_value, ROUND(AVG(total_cost)::numeric, 3) as avg_cost FROM po_totals `, params); // Get total count for pagination const { rows: [countResult] } = await pool.query(` SELECT COUNT(DISTINCT po_id) as total FROM purchase_orders po WHERE ${whereClause} `, params); const total = countResult.total; const offset = (page - 1) * limit; const pages = Math.ceil(total / limit); // Get recent purchase orders let orderByClause; if (sortColumn === 'order_date') { orderByClause = `date ${sortDirection === 'desc' ? 'DESC' : 'ASC'}`; } else if (sortColumn === 'vendor_name') { orderByClause = `vendor ${sortDirection === 'desc' ? 'DESC' : 'ASC'}`; } else if (sortColumn === 'total_cost') { orderByClause = `total_cost ${sortDirection === 'desc' ? 'DESC' : 'ASC'}`; } else if (sortColumn === 'total_received') { orderByClause = `total_received ${sortDirection === 'desc' ? 'DESC' : 'ASC'}`; } else if (sortColumn === 'total_items') { orderByClause = `total_items ${sortDirection === 'desc' ? 'DESC' : 'ASC'}`; } else if (sortColumn === 'total_quantity') { orderByClause = `total_quantity ${sortDirection === 'desc' ? 'DESC' : 'ASC'}`; } else if (sortColumn === 'fulfillment_rate') { orderByClause = `fulfillment_rate ${sortDirection === 'desc' ? 'DESC' : 'ASC'}`; } else if (sortColumn === 'status') { orderByClause = `status ${sortDirection === 'desc' ? 'DESC' : 'ASC'}`; } else { orderByClause = `date ${sortDirection === 'desc' ? 'DESC' : 'ASC'}`; } const { rows: orders } = await pool.query(` WITH po_totals AS ( SELECT po_id, vendor, date, status, receiving_status, COUNT(DISTINCT pid) as total_items, SUM(ordered) as total_quantity, ROUND(SUM(ordered * cost_price)::numeric, 3) as total_cost, SUM(received) as total_received, ROUND( (SUM(received)::numeric / NULLIF(SUM(ordered), 0)), 3 ) as fulfillment_rate FROM purchase_orders po WHERE ${whereClause} GROUP BY po_id, vendor, date, status, receiving_status ) SELECT po_id as id, vendor as vendor_name, to_char(date, 'YYYY-MM-DD') as order_date, status, receiving_status, total_items, total_quantity, total_cost, total_received, fulfillment_rate FROM po_totals ORDER BY ${orderByClause} LIMIT $${paramCounter} OFFSET $${paramCounter + 1} `, [...params, Number(limit), offset]); // Get unique vendors for filter options const { rows: vendors } = await pool.query(` SELECT DISTINCT vendor FROM purchase_orders WHERE vendor IS NOT NULL AND vendor != '' ORDER BY vendor `); // Get unique statuses for filter options const { rows: statuses } = await pool.query(` SELECT DISTINCT status FROM purchase_orders WHERE status IS NOT NULL ORDER BY status `); // Parse numeric values const parsedOrders = orders.map(order => ({ id: order.id, vendor_name: order.vendor_name, order_date: order.order_date, status: Number(order.status), receiving_status: Number(order.receiving_status), total_items: Number(order.total_items) || 0, total_quantity: Number(order.total_quantity) || 0, total_cost: Number(order.total_cost) || 0, total_received: Number(order.total_received) || 0, fulfillment_rate: Number(order.fulfillment_rate) || 0 })); // Parse summary metrics const parsedSummary = { order_count: Number(summary.order_count) || 0, total_ordered: Number(summary.total_ordered) || 0, total_received: Number(summary.total_received) || 0, fulfillment_rate: Number(summary.fulfillment_rate) || 0, total_value: Number(summary.total_value) || 0, avg_cost: Number(summary.avg_cost) || 0 }; res.json({ orders: parsedOrders, summary: parsedSummary, pagination: { total, pages, page: Number(page), limit: Number(limit) }, filters: { vendors: vendors.map(v => v.vendor), statuses: statuses.map(s => Number(s.status)) } }); } catch (error) { console.error('Error fetching purchase orders:', error); res.status(500).json({ error: 'Failed to fetch purchase orders' }); } }); // Get vendor performance metrics router.get('/vendor-metrics', async (req, res) => { try { const pool = req.app.locals.pool; const { rows: metrics } = await pool.query(` WITH delivery_metrics AS ( SELECT vendor, po_id, ordered, received, cost_price, CASE WHEN status >= ${STATUS.RECEIVING_STARTED} AND receiving_status >= ${RECEIVING_STATUS.PARTIAL_RECEIVED} AND received_date IS NOT NULL AND date IS NOT NULL THEN (received_date - date)::integer ELSE NULL END as delivery_days FROM purchase_orders WHERE vendor IS NOT NULL AND vendor != '' AND status != ${STATUS.CANCELED} -- Exclude canceled orders ) SELECT vendor as vendor_name, COUNT(DISTINCT po_id) as total_orders, SUM(ordered) as total_ordered, SUM(received) as total_received, ROUND( (SUM(received)::numeric / NULLIF(SUM(ordered), 0)), 3 ) as fulfillment_rate, ROUND( (SUM(ordered * cost_price)::numeric / NULLIF(SUM(ordered), 0)), 2 ) as avg_unit_cost, ROUND(SUM(ordered * cost_price)::numeric, 3) as total_spend, ROUND( AVG(NULLIF(delivery_days, 0))::numeric, 1 ) as avg_delivery_days FROM delivery_metrics GROUP BY vendor HAVING COUNT(DISTINCT po_id) > 0 ORDER BY total_spend DESC `); // Parse numeric values const parsedMetrics = metrics.map(vendor => ({ id: vendor.vendor_name, vendor_name: vendor.vendor_name, total_orders: Number(vendor.total_orders) || 0, total_ordered: Number(vendor.total_ordered) || 0, total_received: Number(vendor.total_received) || 0, fulfillment_rate: Number(vendor.fulfillment_rate) || 0, avg_unit_cost: Number(vendor.avg_unit_cost) || 0, total_spend: Number(vendor.total_spend) || 0, avg_delivery_days: Number(vendor.avg_delivery_days) || 0 })); res.json(parsedMetrics); } catch (error) { console.error('Error fetching vendor metrics:', error); res.status(500).json({ error: 'Failed to fetch vendor metrics' }); } }); // Get cost analysis router.get('/cost-analysis', async (req, res) => { try { const pool = req.app.locals.pool; const { rows: analysis } = await pool.query(` WITH category_costs AS ( SELECT c.name as category, po.pid, po.cost_price, po.ordered, po.received, po.status, po.receiving_status FROM purchase_orders po JOIN product_categories pc ON po.pid = pc.pid JOIN categories c ON pc.cat_id = c.cat_id WHERE po.status != ${STATUS.CANCELED} -- Exclude canceled orders ) SELECT category, COUNT(DISTINCT pid) as unique_products, ROUND(AVG(cost_price)::numeric, 3) as avg_cost, ROUND(MIN(cost_price)::numeric, 3) as min_cost, ROUND(MAX(cost_price)::numeric, 3) as max_cost, ROUND(STDDEV(cost_price)::numeric, 3) as cost_variance, ROUND(SUM(ordered * cost_price)::numeric, 3) as total_spend FROM category_costs GROUP BY category ORDER BY total_spend DESC `); // Parse numeric values const parsedAnalysis = { unique_products: 0, avg_cost: 0, min_cost: 0, max_cost: 0, cost_variance: 0, total_spend_by_category: analysis.map(cat => ({ category: cat.category, total_spend: Number(cat.total_spend) || 0 })) }; // Calculate aggregated stats if data exists if (analysis.length > 0) { parsedAnalysis.unique_products = analysis.reduce((sum, cat) => sum + Number(cat.unique_products || 0), 0); // Calculate weighted average cost const totalProducts = parsedAnalysis.unique_products; if (totalProducts > 0) { parsedAnalysis.avg_cost = analysis.reduce((sum, cat) => sum + (Number(cat.avg_cost || 0) * Number(cat.unique_products || 0)), 0) / totalProducts; } // Find min and max across all categories parsedAnalysis.min_cost = Math.min(...analysis.map(cat => Number(cat.min_cost || 0))); parsedAnalysis.max_cost = Math.max(...analysis.map(cat => Number(cat.max_cost || 0))); // Average variance parsedAnalysis.cost_variance = analysis.reduce((sum, cat) => sum + Number(cat.cost_variance || 0), 0) / analysis.length; } res.json(parsedAnalysis); } catch (error) { console.error('Error fetching cost analysis:', error); res.status(500).json({ error: 'Failed to fetch cost analysis' }); } }); // Get receiving status metrics router.get('/receiving-status', async (req, res) => { try { const pool = req.app.locals.pool; const { rows: status } = await pool.query(` WITH po_totals AS ( SELECT po_id, status, receiving_status, SUM(ordered) as total_ordered, SUM(received) as total_received, ROUND(SUM(ordered * cost_price)::numeric, 3) as total_cost FROM purchase_orders WHERE status != ${STATUS.CANCELED} GROUP BY po_id, status, receiving_status ) SELECT COUNT(DISTINCT po_id) as order_count, SUM(total_ordered) as total_ordered, SUM(total_received) as total_received, ROUND( SUM(total_received) / NULLIF(SUM(total_ordered), 0), 3 ) as fulfillment_rate, ROUND(SUM(total_cost)::numeric, 3) as total_value, ROUND(AVG(total_cost)::numeric, 3) as avg_cost, COUNT(DISTINCT CASE WHEN receiving_status = ${RECEIVING_STATUS.CREATED} THEN po_id END) as pending_count, COUNT(DISTINCT CASE WHEN receiving_status = ${RECEIVING_STATUS.PARTIAL_RECEIVED} THEN po_id END) as partial_count, COUNT(DISTINCT CASE WHEN receiving_status >= ${RECEIVING_STATUS.FULL_RECEIVED} THEN po_id END) as completed_count, COUNT(DISTINCT CASE WHEN receiving_status = ${RECEIVING_STATUS.CANCELED} THEN po_id END) as canceled_count FROM po_totals `); // Parse numeric values const parsedStatus = { order_count: Number(status[0]?.order_count) || 0, total_ordered: Number(status[0]?.total_ordered) || 0, total_received: Number(status[0]?.total_received) || 0, fulfillment_rate: Number(status[0]?.fulfillment_rate) || 0, total_value: Number(status[0]?.total_value) || 0, avg_cost: Number(status[0]?.avg_cost) || 0, status_breakdown: { pending: Number(status[0]?.pending_count) || 0, partial: Number(status[0]?.partial_count) || 0, completed: Number(status[0]?.completed_count) || 0, canceled: Number(status[0]?.canceled_count) || 0 } }; res.json(parsedStatus); } catch (error) { console.error('Error fetching receiving status:', error); res.status(500).json({ error: 'Failed to fetch receiving status' }); } }); // Get order vs received quantities by product router.get('/order-vs-received', async (req, res) => { try { const pool = req.app.locals.pool; const { rows: quantities } = await pool.query(` SELECT p.product_id, p.title as product, p.SKU as sku, SUM(po.ordered) as ordered_quantity, SUM(po.received) as received_quantity, ROUND( SUM(po.received) / NULLIF(SUM(po.ordered), 0) * 100, 1 ) as fulfillment_rate, COUNT(DISTINCT po.po_id) as order_count FROM products p JOIN purchase_orders po ON p.product_id = po.product_id WHERE po.date >= (CURRENT_DATE - INTERVAL '90 days') GROUP BY p.product_id, p.title, p.SKU HAVING COUNT(DISTINCT po.po_id) > 0 ORDER BY SUM(po.ordered) DESC LIMIT 20 `); // Parse numeric values and add id for React keys const parsedQuantities = quantities.map(q => ({ id: q.product_id, ...q, ordered_quantity: Number(q.ordered_quantity), received_quantity: Number(q.received_quantity), fulfillment_rate: Number(q.fulfillment_rate), order_count: Number(q.order_count) })); res.json(parsedQuantities); } catch (error) { console.error('Error fetching order vs received quantities:', error); res.status(500).json({ error: 'Failed to fetch order vs received quantities' }); } }); module.exports = router;