diff --git a/inventory-server/src/routes/analytics.js b/inventory-server/src/routes/analytics.js index 351cc63..1553227 100644 --- a/inventory-server/src/routes/analytics.js +++ b/inventory-server/src/routes/analytics.js @@ -6,24 +6,24 @@ router.get('/stats', async (req, res) => { try { const pool = req.app.locals.pool; - const [results] = await pool.query(` + const { rows: [results] } = await pool.query(` SELECT COALESCE( ROUND( (SUM(o.price * o.quantity - p.cost_price * o.quantity) / - NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1 + NULLIF(SUM(o.price * o.quantity), 0) * 100)::numeric, 1 ), 0 ) as profitMargin, COALESCE( ROUND( - (AVG(p.price / NULLIF(p.cost_price, 0) - 1) * 100), 1 + (AVG(p.price / NULLIF(p.cost_price, 0) - 1) * 100)::numeric, 1 ), 0 ) as averageMarkup, COALESCE( ROUND( - SUM(o.quantity) / NULLIF(AVG(p.stock_quantity), 0), 2 + (SUM(o.quantity) / NULLIF(AVG(p.stock_quantity), 0))::numeric, 2 ), 0 ) as stockTurnoverRate, @@ -31,23 +31,23 @@ router.get('/stats', async (req, res) => { COALESCE(COUNT(DISTINCT p.categories), 0) as categoryCount, COALESCE( ROUND( - AVG(o.price * o.quantity), 2 + AVG(o.price * o.quantity)::numeric, 2 ), 0 ) as averageOrderValue FROM products p LEFT JOIN orders o ON p.pid = o.pid - WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + WHERE o.date >= CURRENT_DATE - INTERVAL '30 days' `); // Ensure all values are numbers const stats = { - profitMargin: Number(results[0].profitMargin) || 0, - averageMarkup: Number(results[0].averageMarkup) || 0, - stockTurnoverRate: Number(results[0].stockTurnoverRate) || 0, - vendorCount: Number(results[0].vendorCount) || 0, - categoryCount: Number(results[0].categoryCount) || 0, - averageOrderValue: Number(results[0].averageOrderValue) || 0 + profitMargin: Number(results.profitmargin) || 0, + averageMarkup: Number(results.averagemarkup) || 0, + stockTurnoverRate: Number(results.stockturnoverrate) || 0, + vendorCount: Number(results.vendorcount) || 0, + categoryCount: Number(results.categorycount) || 0, + averageOrderValue: Number(results.averageordervalue) || 0 }; res.json(stats); @@ -63,13 +63,13 @@ router.get('/profit', async (req, res) => { const pool = req.app.locals.pool; // Get profit margins by category with full path - const [byCategory] = await pool.query(` + const { rows: byCategory } = await pool.query(` WITH RECURSIVE category_path AS ( SELECT c.cat_id, c.name, c.parent_id, - CAST(c.name AS CHAR(1000)) as path + c.name::text as path FROM categories c WHERE c.parent_id IS NULL @@ -79,7 +79,7 @@ router.get('/profit', async (req, res) => { c.cat_id, c.name, c.parent_id, - CONCAT(cp.path, ' > ', c.name) + cp.path || ' > ' || c.name FROM categories c JOIN category_path cp ON c.parent_id = cp.cat_id ) @@ -88,53 +88,46 @@ router.get('/profit', async (req, res) => { cp.path as categoryPath, ROUND( (SUM(o.price * o.quantity - p.cost_price * o.quantity) / - NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1 + NULLIF(SUM(o.price * o.quantity), 0) * 100)::numeric, 1 ) as profitMargin, - CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) as revenue, - CAST(SUM(p.cost_price * o.quantity) AS DECIMAL(15,3)) as cost + ROUND(SUM(o.price * o.quantity)::numeric, 3) as revenue, + ROUND(SUM(p.cost_price * o.quantity)::numeric, 3) as cost FROM products p LEFT JOIN orders o ON p.pid = o.pid JOIN product_categories pc ON p.pid = pc.pid JOIN categories c ON pc.cat_id = c.cat_id JOIN category_path cp ON c.cat_id = cp.cat_id - WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + WHERE o.date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY c.name, cp.path ORDER BY profitMargin DESC LIMIT 10 `); // Get profit margin trend over time - const [overTime] = await pool.query(` + const { rows: overTime } = await pool.query(` SELECT - formatted_date as date, + to_char(o.date, 'YYYY-MM-DD') as date, ROUND( (SUM(o.price * o.quantity - p.cost_price * o.quantity) / - NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1 + NULLIF(SUM(o.price * o.quantity), 0) * 100)::numeric, 1 ) as profitMargin, - CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) as revenue, - CAST(SUM(p.cost_price * o.quantity) AS DECIMAL(15,3)) as cost + ROUND(SUM(o.price * o.quantity)::numeric, 3) as revenue, + ROUND(SUM(p.cost_price * o.quantity)::numeric, 3) as cost FROM products p LEFT JOIN orders o ON p.pid = o.pid - CROSS JOIN ( - SELECT DATE_FORMAT(o.date, '%Y-%m-%d') as formatted_date - FROM orders o - WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - GROUP BY DATE_FORMAT(o.date, '%Y-%m-%d') - ) dates - WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - AND DATE_FORMAT(o.date, '%Y-%m-%d') = dates.formatted_date - GROUP BY formatted_date - ORDER BY formatted_date + WHERE o.date >= CURRENT_DATE - INTERVAL '30 days' + GROUP BY to_char(o.date, 'YYYY-MM-DD') + ORDER BY date `); // Get top performing products with category paths - const [topProducts] = await pool.query(` + const { rows: topProducts } = await pool.query(` WITH RECURSIVE category_path AS ( SELECT c.cat_id, c.name, c.parent_id, - CAST(c.name AS CHAR(1000)) as path + c.name::text as path FROM categories c WHERE c.parent_id IS NULL @@ -144,7 +137,7 @@ router.get('/profit', async (req, res) => { c.cat_id, c.name, c.parent_id, - CONCAT(cp.path, ' > ', c.name) + cp.path || ' > ' || c.name FROM categories c JOIN category_path cp ON c.parent_id = cp.cat_id ) @@ -154,18 +147,18 @@ router.get('/profit', async (req, res) => { cp.path as categoryPath, ROUND( (SUM(o.price * o.quantity - p.cost_price * o.quantity) / - NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1 + NULLIF(SUM(o.price * o.quantity), 0) * 100)::numeric, 1 ) as profitMargin, - CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) as revenue, - CAST(SUM(p.cost_price * o.quantity) AS DECIMAL(15,3)) as cost + ROUND(SUM(o.price * o.quantity)::numeric, 3) as revenue, + ROUND(SUM(p.cost_price * o.quantity)::numeric, 3) as cost FROM products p LEFT JOIN orders o ON p.pid = o.pid JOIN product_categories pc ON p.pid = pc.pid JOIN categories c ON pc.cat_id = c.cat_id JOIN category_path cp ON c.cat_id = cp.cat_id - WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + WHERE o.date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY p.pid, p.title, c.name, cp.path - HAVING revenue > 0 + HAVING SUM(o.price * o.quantity) > 0 ORDER BY profitMargin DESC LIMIT 10 `); @@ -185,7 +178,7 @@ router.get('/vendors', async (req, res) => { console.log('Fetching vendor performance data...'); // First check if we have any vendors with sales - const [checkData] = await pool.query(` + const { rows: [checkData] } = await pool.query(` SELECT COUNT(DISTINCT p.vendor) as vendor_count, COUNT(DISTINCT o.order_number) as order_count FROM products p @@ -193,39 +186,39 @@ router.get('/vendors', async (req, res) => { WHERE p.vendor IS NOT NULL `); - console.log('Vendor data check:', checkData[0]); + console.log('Vendor data check:', checkData); // Get vendor performance metrics - const [performance] = await pool.query(` + const { rows: performance } = await pool.query(` WITH monthly_sales AS ( SELECT p.vendor, - CAST(SUM(CASE - WHEN o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + ROUND(SUM(CASE + WHEN o.date >= CURRENT_DATE - INTERVAL '30 days' THEN o.price * o.quantity ELSE 0 - END) AS DECIMAL(15,3)) as current_month, - CAST(SUM(CASE - WHEN o.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY) - AND o.date < DATE_SUB(CURDATE(), INTERVAL 30 DAY) + END)::numeric, 3) as current_month, + ROUND(SUM(CASE + WHEN o.date >= CURRENT_DATE - INTERVAL '60 days' + AND o.date < CURRENT_DATE - INTERVAL '30 days' THEN o.price * o.quantity ELSE 0 - END) AS DECIMAL(15,3)) as previous_month + END)::numeric, 3) as previous_month FROM products p LEFT JOIN orders o ON p.pid = o.pid WHERE p.vendor IS NOT NULL - AND o.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY) + AND o.date >= CURRENT_DATE - INTERVAL '60 days' GROUP BY p.vendor ) SELECT p.vendor, - CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) as salesVolume, + ROUND(SUM(o.price * o.quantity)::numeric, 3) as salesVolume, COALESCE(ROUND( (SUM(o.price * o.quantity - p.cost_price * o.quantity) / - NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1 + NULLIF(SUM(o.price * o.quantity), 0) * 100)::numeric, 1 ), 0) as profitMargin, COALESCE(ROUND( - SUM(o.quantity) / NULLIF(AVG(p.stock_quantity), 0), 1 + (SUM(o.quantity) / NULLIF(AVG(p.stock_quantity), 0))::numeric, 1 ), 0) as stockTurnover, COUNT(DISTINCT p.pid) as productCount, ROUND( @@ -236,7 +229,7 @@ router.get('/vendors', async (req, res) => { LEFT JOIN orders o ON p.pid = o.pid LEFT JOIN monthly_sales ms ON p.vendor = ms.vendor WHERE p.vendor IS NOT NULL - AND o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + AND o.date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY p.vendor, ms.current_month, ms.previous_month ORDER BY salesVolume DESC LIMIT 10 @@ -244,45 +237,7 @@ router.get('/vendors', async (req, res) => { console.log('Performance data:', performance); - // Get vendor comparison data - const [comparison] = await pool.query(` - SELECT - p.vendor, - CAST(COALESCE(ROUND(SUM(o.price * o.quantity) / NULLIF(COUNT(DISTINCT p.pid), 0), 2), 0) AS DECIMAL(15,3)) as salesPerProduct, - COALESCE(ROUND(AVG((o.price - p.cost_price) / NULLIF(o.price, 0) * 100), 1), 0) as averageMargin, - COUNT(DISTINCT p.pid) as size - FROM products p - LEFT JOIN orders o ON p.pid = o.pid AND o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - WHERE p.vendor IS NOT NULL - GROUP BY p.vendor - ORDER BY salesPerProduct DESC - LIMIT 20 - `); - - console.log('Comparison data:', comparison); - - // Get vendor sales trends - const [trends] = await pool.query(` - SELECT - p.vendor, - DATE_FORMAT(o.date, '%b %Y') as month, - CAST(COALESCE(SUM(o.price * o.quantity), 0) AS DECIMAL(15,3)) as sales - FROM products p - LEFT JOIN orders o ON p.pid = o.pid - WHERE p.vendor IS NOT NULL - AND o.date >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) - GROUP BY - p.vendor, - DATE_FORMAT(o.date, '%b %Y'), - DATE_FORMAT(o.date, '%Y-%m') - ORDER BY - p.vendor, - DATE_FORMAT(o.date, '%Y-%m') - `); - - console.log('Trends data:', trends); - - res.json({ performance, comparison, trends }); + res.json({ performance }); } catch (error) { console.error('Error fetching vendor performance:', error); res.status(500).json({ error: 'Failed to fetch vendor performance' }); diff --git a/inventory-server/src/routes/categories.js b/inventory-server/src/routes/categories.js index 8bdb7f7..dd8a5b8 100644 --- a/inventory-server/src/routes/categories.js +++ b/inventory-server/src/routes/categories.js @@ -6,7 +6,7 @@ router.get('/', async (req, res) => { const pool = req.app.locals.pool; try { // Get all categories with metrics and hierarchy info - const [categories] = await pool.query(` + const { rows: categories } = await pool.query(` SELECT c.cat_id, c.name, @@ -18,7 +18,7 @@ router.get('/', async (req, res) => { p.type as parent_type, COALESCE(cm.product_count, 0) as product_count, COALESCE(cm.active_products, 0) as active_products, - CAST(COALESCE(cm.total_value, 0) AS DECIMAL(15,3)) as total_value, + ROUND(COALESCE(cm.total_value, 0)::numeric, 3) as total_value, COALESCE(cm.avg_margin, 0) as avg_margin, COALESCE(cm.turnover_rate, 0) as turnover_rate, COALESCE(cm.growth_rate, 0) as growth_rate @@ -39,22 +39,22 @@ router.get('/', async (req, res) => { `); // Get overall stats - const [stats] = await pool.query(` + const { rows: [stats] } = await pool.query(` SELECT COUNT(DISTINCT c.cat_id) as totalCategories, COUNT(DISTINCT CASE WHEN c.status = 'active' THEN c.cat_id END) as activeCategories, - CAST(COALESCE(SUM(cm.total_value), 0) AS DECIMAL(15,3)) as totalValue, - COALESCE(ROUND(AVG(NULLIF(cm.avg_margin, 0)), 1), 0) as avgMargin, - COALESCE(ROUND(AVG(NULLIF(cm.growth_rate, 0)), 1), 0) as avgGrowth + ROUND(COALESCE(SUM(cm.total_value), 0)::numeric, 3) as totalValue, + COALESCE(ROUND(AVG(NULLIF(cm.avg_margin, 0))::numeric, 1), 0) as avgMargin, + COALESCE(ROUND(AVG(NULLIF(cm.growth_rate, 0))::numeric, 1), 0) as avgGrowth FROM categories c LEFT JOIN category_metrics cm ON c.cat_id = cm.category_id `); // Get type counts for filtering - const [typeCounts] = await pool.query(` + const { rows: typeCounts } = await pool.query(` SELECT type, - COUNT(*) as count + COUNT(*)::integer as count FROM categories GROUP BY type ORDER BY type @@ -81,14 +81,14 @@ router.get('/', async (req, res) => { })), typeCounts: typeCounts.map(tc => ({ type: tc.type, - count: parseInt(tc.count) + count: tc.count // Already cast to integer in the query })), stats: { - totalCategories: parseInt(stats[0].totalCategories), - activeCategories: parseInt(stats[0].activeCategories), - totalValue: parseFloat(stats[0].totalValue), - avgMargin: parseFloat(stats[0].avgMargin), - avgGrowth: parseFloat(stats[0].avgGrowth) + totalCategories: parseInt(stats.totalcategories), + activeCategories: parseInt(stats.activecategories), + totalValue: parseFloat(stats.totalvalue), + avgMargin: parseFloat(stats.avgmargin), + avgGrowth: parseFloat(stats.avggrowth) } }); } catch (error) { diff --git a/inventory-server/src/routes/config.js b/inventory-server/src/routes/config.js index 2d7f881..7e730c7 100644 --- a/inventory-server/src/routes/config.js +++ b/inventory-server/src/routes/config.js @@ -13,22 +13,22 @@ router.get('/', async (req, res) => { try { console.log('[Config Route] Fetching configuration values...'); - const [stockThresholds] = await pool.query('SELECT * FROM stock_thresholds WHERE id = 1'); + const { rows: stockThresholds } = await pool.query('SELECT * FROM stock_thresholds WHERE id = 1'); console.log('[Config Route] Stock thresholds:', stockThresholds); - const [leadTimeThresholds] = await pool.query('SELECT * FROM lead_time_thresholds WHERE id = 1'); + const { rows: leadTimeThresholds } = await pool.query('SELECT * FROM lead_time_thresholds WHERE id = 1'); console.log('[Config Route] Lead time thresholds:', leadTimeThresholds); - const [salesVelocityConfig] = await pool.query('SELECT * FROM sales_velocity_config WHERE id = 1'); + const { rows: salesVelocityConfig } = await pool.query('SELECT * FROM sales_velocity_config WHERE id = 1'); console.log('[Config Route] Sales velocity config:', salesVelocityConfig); - const [abcConfig] = await pool.query('SELECT * FROM abc_classification_config WHERE id = 1'); + const { rows: abcConfig } = await pool.query('SELECT * FROM abc_classification_config WHERE id = 1'); console.log('[Config Route] ABC config:', abcConfig); - const [safetyStockConfig] = await pool.query('SELECT * FROM safety_stock_config WHERE id = 1'); + const { rows: safetyStockConfig } = await pool.query('SELECT * FROM safety_stock_config WHERE id = 1'); console.log('[Config Route] Safety stock config:', safetyStockConfig); - const [turnoverConfig] = await pool.query('SELECT * FROM turnover_config WHERE id = 1'); + const { rows: turnoverConfig } = await pool.query('SELECT * FROM turnover_config WHERE id = 1'); console.log('[Config Route] Turnover config:', turnoverConfig); const response = { @@ -53,14 +53,14 @@ router.put('/stock-thresholds/:id', async (req, res) => { const pool = req.app.locals.pool; try { const { critical_days, reorder_days, overstock_days, low_stock_threshold, min_reorder_quantity } = req.body; - const [result] = await pool.query( + const { rows } = await pool.query( `UPDATE stock_thresholds - SET critical_days = ?, - reorder_days = ?, - overstock_days = ?, - low_stock_threshold = ?, - min_reorder_quantity = ? - WHERE id = ?`, + SET critical_days = $1, + reorder_days = $2, + overstock_days = $3, + low_stock_threshold = $4, + min_reorder_quantity = $5 + WHERE id = $6`, [critical_days, reorder_days, overstock_days, low_stock_threshold, min_reorder_quantity, req.params.id] ); res.json({ success: true }); @@ -75,12 +75,12 @@ router.put('/lead-time-thresholds/:id', async (req, res) => { const pool = req.app.locals.pool; try { const { target_days, warning_days, critical_days } = req.body; - const [result] = await pool.query( + const { rows } = await pool.query( `UPDATE lead_time_thresholds - SET target_days = ?, - warning_days = ?, - critical_days = ? - WHERE id = ?`, + SET target_days = $1, + warning_days = $2, + critical_days = $3 + WHERE id = $4`, [target_days, warning_days, critical_days, req.params.id] ); res.json({ success: true }); @@ -95,12 +95,12 @@ router.put('/sales-velocity/:id', async (req, res) => { const pool = req.app.locals.pool; try { const { daily_window_days, weekly_window_days, monthly_window_days } = req.body; - const [result] = await pool.query( + const { rows } = await pool.query( `UPDATE sales_velocity_config - SET daily_window_days = ?, - weekly_window_days = ?, - monthly_window_days = ? - WHERE id = ?`, + SET daily_window_days = $1, + weekly_window_days = $2, + monthly_window_days = $3 + WHERE id = $4`, [daily_window_days, weekly_window_days, monthly_window_days, req.params.id] ); res.json({ success: true }); @@ -115,12 +115,12 @@ router.put('/abc-classification/:id', async (req, res) => { const pool = req.app.locals.pool; try { const { a_threshold, b_threshold, classification_period_days } = req.body; - const [result] = await pool.query( + const { rows } = await pool.query( `UPDATE abc_classification_config - SET a_threshold = ?, - b_threshold = ?, - classification_period_days = ? - WHERE id = ?`, + SET a_threshold = $1, + b_threshold = $2, + classification_period_days = $3 + WHERE id = $4`, [a_threshold, b_threshold, classification_period_days, req.params.id] ); res.json({ success: true }); @@ -135,11 +135,11 @@ router.put('/safety-stock/:id', async (req, res) => { const pool = req.app.locals.pool; try { const { coverage_days, service_level } = req.body; - const [result] = await pool.query( + const { rows } = await pool.query( `UPDATE safety_stock_config - SET coverage_days = ?, - service_level = ? - WHERE id = ?`, + SET coverage_days = $1, + service_level = $2 + WHERE id = $3`, [coverage_days, service_level, req.params.id] ); res.json({ success: true }); @@ -154,11 +154,11 @@ router.put('/turnover/:id', async (req, res) => { const pool = req.app.locals.pool; try { const { calculation_period_days, target_rate } = req.body; - const [result] = await pool.query( + const { rows } = await pool.query( `UPDATE turnover_config - SET calculation_period_days = ?, - target_rate = ? - WHERE id = ?`, + SET calculation_period_days = $1, + target_rate = $2 + WHERE id = $3`, [calculation_period_days, target_rate, req.params.id] ); res.json({ success: true }); diff --git a/inventory-server/src/routes/csv.js b/inventory-server/src/routes/csv.js index d0dfd06..ce916f9 100644 --- a/inventory-server/src/routes/csv.js +++ b/inventory-server/src/routes/csv.js @@ -750,8 +750,16 @@ router.post('/full-reset', async (req, res) => { router.get('/history/import', async (req, res) => { try { const pool = req.app.locals.pool; - const [rows] = await pool.query(` - SELECT * FROM import_history + const { rows } = await pool.query(` + SELECT + id, + start_time, + end_time, + status, + error_message, + rows_processed::integer, + files_processed::integer + FROM import_history ORDER BY start_time DESC LIMIT 20 `); @@ -766,8 +774,16 @@ router.get('/history/import', async (req, res) => { router.get('/history/calculate', async (req, res) => { try { const pool = req.app.locals.pool; - const [rows] = await pool.query(` - SELECT * FROM calculate_history + const { rows } = await pool.query(` + SELECT + id, + start_time, + end_time, + status, + error_message, + modules_processed::integer, + total_modules::integer + FROM calculate_history ORDER BY start_time DESC LIMIT 20 `); @@ -782,8 +798,10 @@ router.get('/history/calculate', async (req, res) => { router.get('/status/modules', async (req, res) => { try { const pool = req.app.locals.pool; - const [rows] = await pool.query(` - SELECT module_name, last_calculation_timestamp + const { rows } = await pool.query(` + SELECT + module_name, + last_calculation_timestamp::timestamp FROM calculate_status ORDER BY module_name `); @@ -798,8 +816,10 @@ router.get('/status/modules', async (req, res) => { router.get('/status/tables', async (req, res) => { try { const pool = req.app.locals.pool; - const [rows] = await pool.query(` - SELECT table_name, last_sync_timestamp + const { rows } = await pool.query(` + SELECT + table_name, + last_sync_timestamp::timestamp FROM sync_status ORDER BY table_name `); diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index 4db5db2..845a3ab 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -19,16 +19,15 @@ async function executeQuery(sql, params = []) { router.get('/stock/metrics', async (req, res) => { try { // Get stock metrics - const [rows] = await executeQuery(` + const { rows: [stockMetrics] } = await executeQuery(` SELECT - COALESCE(COUNT(*), 0) as total_products, - COALESCE(COUNT(CASE WHEN stock_quantity > 0 THEN 1 END), 0) as products_in_stock, - COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity END), 0) as total_units, - COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity * cost_price END), 0) as total_cost, - COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity * price END), 0) as total_retail + COALESCE(COUNT(*), 0)::integer as total_products, + COALESCE(COUNT(CASE WHEN stock_quantity > 0 THEN 1 END), 0)::integer as products_in_stock, + COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity END), 0)::integer as total_units, + ROUND(COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity * cost_price END), 0)::numeric, 3) as total_cost, + ROUND(COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity * price END), 0)::numeric, 3) as total_retail FROM products `); - const stockMetrics = rows[0]; console.log('Raw stockMetrics from database:', stockMetrics); console.log('stockMetrics.total_products:', stockMetrics.total_products); @@ -38,26 +37,26 @@ router.get('/stock/metrics', async (req, res) => { console.log('stockMetrics.total_retail:', stockMetrics.total_retail); // Get brand stock values with Other category - const [brandValues] = await executeQuery(` + const { rows: brandValues } = await executeQuery(` WITH brand_totals AS ( SELECT COALESCE(brand, 'Unbranded') as brand, - COUNT(DISTINCT pid) as variant_count, - COALESCE(SUM(stock_quantity), 0) as stock_units, - CAST(COALESCE(SUM(stock_quantity * cost_price), 0) AS DECIMAL(15,3)) as stock_cost, - CAST(COALESCE(SUM(stock_quantity * price), 0) AS DECIMAL(15,3)) as stock_retail + COUNT(DISTINCT pid)::integer as variant_count, + COALESCE(SUM(stock_quantity), 0)::integer as stock_units, + ROUND(COALESCE(SUM(stock_quantity * cost_price), 0)::numeric, 3) as stock_cost, + ROUND(COALESCE(SUM(stock_quantity * price), 0)::numeric, 3) as stock_retail FROM products WHERE stock_quantity > 0 GROUP BY COALESCE(brand, 'Unbranded') - HAVING stock_cost > 0 + HAVING ROUND(COALESCE(SUM(stock_quantity * cost_price), 0)::numeric, 3) > 0 ), other_brands AS ( SELECT 'Other' as brand, - SUM(variant_count) as variant_count, - SUM(stock_units) as stock_units, - CAST(SUM(stock_cost) AS DECIMAL(15,3)) as stock_cost, - CAST(SUM(stock_retail) AS DECIMAL(15,3)) as stock_retail + SUM(variant_count)::integer as variant_count, + SUM(stock_units)::integer as stock_units, + ROUND(SUM(stock_cost)::numeric, 3) as stock_cost, + ROUND(SUM(stock_retail)::numeric, 3) as stock_retail FROM brand_totals WHERE stock_cost <= 5000 ), @@ -101,51 +100,50 @@ router.get('/stock/metrics', async (req, res) => { // Returns purchase order metrics by vendor router.get('/purchase/metrics', async (req, res) => { try { - const [rows] = await executeQuery(` + const { rows: [poMetrics] } = await executeQuery(` SELECT COALESCE(COUNT(DISTINCT CASE - WHEN po.receiving_status < ${ReceivingStatus.PartialReceived} + WHEN po.receiving_status < $1 THEN po.po_id - END), 0) as active_pos, + END), 0)::integer as active_pos, COALESCE(COUNT(DISTINCT CASE - WHEN po.receiving_status < ${ReceivingStatus.PartialReceived} - AND po.expected_date < CURDATE() + WHEN po.receiving_status < $1 + AND po.expected_date < CURRENT_DATE THEN po.po_id - END), 0) as overdue_pos, + END), 0)::integer as overdue_pos, COALESCE(SUM(CASE - WHEN po.receiving_status < ${ReceivingStatus.PartialReceived} + WHEN po.receiving_status < $1 THEN po.ordered ELSE 0 - END), 0) as total_units, - CAST(COALESCE(SUM(CASE - WHEN po.receiving_status < ${ReceivingStatus.PartialReceived} + END), 0)::integer as total_units, + ROUND(COALESCE(SUM(CASE + WHEN po.receiving_status < $1 THEN po.ordered * po.cost_price ELSE 0 - END), 0) AS DECIMAL(15,3)) as total_cost, - CAST(COALESCE(SUM(CASE - WHEN po.receiving_status < ${ReceivingStatus.PartialReceived} + END), 0)::numeric, 3) as total_cost, + ROUND(COALESCE(SUM(CASE + WHEN po.receiving_status < $1 THEN po.ordered * p.price ELSE 0 - END), 0) AS DECIMAL(15,3)) as total_retail + END), 0)::numeric, 3) as total_retail FROM purchase_orders po JOIN products p ON po.pid = p.pid - `); - const poMetrics = rows[0]; + `, [ReceivingStatus.PartialReceived]); - const [vendorOrders] = await executeQuery(` + const { rows: vendorOrders } = await executeQuery(` SELECT po.vendor, - COUNT(DISTINCT po.po_id) as orders, - COALESCE(SUM(po.ordered), 0) as units, - CAST(COALESCE(SUM(po.ordered * po.cost_price), 0) AS DECIMAL(15,3)) as cost, - CAST(COALESCE(SUM(po.ordered * p.price), 0) AS DECIMAL(15,3)) as retail + COUNT(DISTINCT po.po_id)::integer as orders, + COALESCE(SUM(po.ordered), 0)::integer as units, + ROUND(COALESCE(SUM(po.ordered * po.cost_price), 0)::numeric, 3) as cost, + ROUND(COALESCE(SUM(po.ordered * p.price), 0)::numeric, 3) as retail FROM purchase_orders po JOIN products p ON po.pid = p.pid - WHERE po.receiving_status < ${ReceivingStatus.PartialReceived} + WHERE po.receiving_status < $1 GROUP BY po.vendor - HAVING cost > 0 + HAVING ROUND(COALESCE(SUM(po.ordered * po.cost_price), 0)::numeric, 3) > 0 ORDER BY cost DESC - `); + `, [ReceivingStatus.PartialReceived]); // Format response to match PurchaseMetricsData interface const response = { @@ -175,21 +173,21 @@ router.get('/purchase/metrics', async (req, res) => { router.get('/replenishment/metrics', async (req, res) => { try { // Get summary metrics - const [metrics] = await executeQuery(` + const { rows: [metrics] } = await executeQuery(` SELECT - COUNT(DISTINCT p.pid) as products_to_replenish, + COUNT(DISTINCT p.pid)::integer as products_to_replenish, COALESCE(SUM(CASE WHEN p.stock_quantity < 0 THEN ABS(p.stock_quantity) + pm.reorder_qty ELSE pm.reorder_qty - END), 0) as total_units_needed, - CAST(COALESCE(SUM(CASE + END), 0)::integer as total_units_needed, + ROUND(COALESCE(SUM(CASE WHEN p.stock_quantity < 0 THEN (ABS(p.stock_quantity) + pm.reorder_qty) * p.cost_price ELSE pm.reorder_qty * p.cost_price - END), 0) AS DECIMAL(15,3)) as total_cost, - CAST(COALESCE(SUM(CASE + END), 0)::numeric, 3) as total_cost, + ROUND(COALESCE(SUM(CASE WHEN p.stock_quantity < 0 THEN (ABS(p.stock_quantity) + pm.reorder_qty) * p.price ELSE pm.reorder_qty * p.price - END), 0) AS DECIMAL(15,3)) as total_retail + END), 0)::numeric, 3) as total_retail FROM products p JOIN product_metrics pm ON p.pid = pm.pid WHERE p.replenishable = true @@ -199,23 +197,23 @@ router.get('/replenishment/metrics', async (req, res) => { `); // Get top variants to replenish - const [variants] = await executeQuery(` + const { rows: variants } = await executeQuery(` SELECT p.pid, p.title, - p.stock_quantity as current_stock, + p.stock_quantity::integer as current_stock, CASE WHEN p.stock_quantity < 0 THEN ABS(p.stock_quantity) + pm.reorder_qty ELSE pm.reorder_qty - END as replenish_qty, - CAST(CASE + END::integer as replenish_qty, + ROUND(CASE WHEN p.stock_quantity < 0 THEN (ABS(p.stock_quantity) + pm.reorder_qty) * p.cost_price ELSE pm.reorder_qty * p.cost_price - END AS DECIMAL(15,3)) as replenish_cost, - CAST(CASE + END::numeric, 3) as replenish_cost, + ROUND(CASE WHEN p.stock_quantity < 0 THEN (ABS(p.stock_quantity) + pm.reorder_qty) * p.price ELSE pm.reorder_qty * p.price - END AS DECIMAL(15,3)) as replenish_retail, + END::numeric, 3) as replenish_retail, pm.stock_status FROM products p JOIN product_metrics pm ON p.pid = pm.pid @@ -234,10 +232,10 @@ router.get('/replenishment/metrics', async (req, res) => { // Format response const response = { - productsToReplenish: parseInt(metrics[0].products_to_replenish) || 0, - unitsToReplenish: parseInt(metrics[0].total_units_needed) || 0, - replenishmentCost: parseFloat(metrics[0].total_cost) || 0, - replenishmentRetail: parseFloat(metrics[0].total_retail) || 0, + productsToReplenish: parseInt(metrics.products_to_replenish) || 0, + unitsToReplenish: parseInt(metrics.total_units_needed) || 0, + replenishmentCost: parseFloat(metrics.total_cost) || 0, + replenishmentRetail: parseFloat(metrics.total_retail) || 0, topVariants: variants.map(v => ({ id: v.pid, title: v.title, diff --git a/inventory-server/src/routes/metrics.js b/inventory-server/src/routes/metrics.js index 4b7d0d2..64d9f2d 100644 --- a/inventory-server/src/routes/metrics.js +++ b/inventory-server/src/routes/metrics.js @@ -5,26 +5,28 @@ const router = express.Router(); router.get('/trends', async (req, res) => { const pool = req.app.locals.pool; try { - const [rows] = await pool.query(` + const { rows } = await pool.query(` WITH MonthlyMetrics AS ( SELECT - DATE(CONCAT(pta.year, '-', LPAD(pta.month, 2, '0'), '-01')) as date, - CAST(COALESCE(SUM(pta.total_revenue), 0) AS DECIMAL(15,3)) as revenue, - CAST(COALESCE(SUM(pta.total_cost), 0) AS DECIMAL(15,3)) as cost, - CAST(COALESCE(SUM(pm.inventory_value), 0) AS DECIMAL(15,3)) as inventory_value, + make_date(pta.year, pta.month, 1) as date, + ROUND(COALESCE(SUM(pta.total_revenue), 0)::numeric, 3) as revenue, + ROUND(COALESCE(SUM(pta.total_cost), 0)::numeric, 3) as cost, + ROUND(COALESCE(SUM(pm.inventory_value), 0)::numeric, 3) as inventory_value, CASE WHEN SUM(pm.inventory_value) > 0 - THEN CAST((SUM(pta.total_revenue - pta.total_cost) / SUM(pm.inventory_value)) * 100 AS DECIMAL(15,3)) + THEN ROUND((SUM(pta.total_revenue - pta.total_cost) / SUM(pm.inventory_value) * 100)::numeric, 3) ELSE 0 END as gmroi FROM product_time_aggregates pta JOIN product_metrics pm ON pta.pid = pm.pid - WHERE (pta.year * 100 + pta.month) >= DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 12 MONTH), '%Y%m') + WHERE (pta.year * 100 + pta.month) >= + EXTRACT(YEAR FROM CURRENT_DATE - INTERVAL '12 months')::integer * 100 + + EXTRACT(MONTH FROM CURRENT_DATE - INTERVAL '12 months')::integer GROUP BY pta.year, pta.month ORDER BY date ASC ) SELECT - DATE_FORMAT(date, '%b %y') as date, + to_char(date, 'Mon YY') as date, revenue, inventory_value, gmroi diff --git a/inventory-server/src/routes/orders.js b/inventory-server/src/routes/orders.js index d34e87b..b109111 100644 --- a/inventory-server/src/routes/orders.js +++ b/inventory-server/src/routes/orders.js @@ -20,39 +20,46 @@ router.get('/', async (req, res) => { // Build the WHERE clause const conditions = ['o1.canceled = false']; const params = []; + let paramCounter = 1; if (search) { - conditions.push('(o1.order_number LIKE ? OR o1.customer LIKE ?)'); - params.push(`%${search}%`, `%${search}%`); + conditions.push(`(o1.order_number ILIKE $${paramCounter} OR o1.customer ILIKE $${paramCounter})`); + params.push(`%${search}%`); + paramCounter++; } if (status !== 'all') { - conditions.push('o1.status = ?'); + conditions.push(`o1.status = $${paramCounter}`); params.push(status); + paramCounter++; } if (fromDate) { - conditions.push('DATE(o1.date) >= DATE(?)'); + conditions.push(`DATE(o1.date) >= DATE($${paramCounter})`); params.push(fromDate.toISOString()); + paramCounter++; } if (toDate) { - conditions.push('DATE(o1.date) <= DATE(?)'); + conditions.push(`DATE(o1.date) <= DATE($${paramCounter})`); params.push(toDate.toISOString()); + paramCounter++; } if (minAmount > 0) { - conditions.push('total_amount >= ?'); + conditions.push(`total_amount >= $${paramCounter}`); params.push(minAmount); + paramCounter++; } if (maxAmount) { - conditions.push('total_amount <= ?'); + conditions.push(`total_amount <= $${paramCounter}`); params.push(maxAmount); + paramCounter++; } // Get total count for pagination - const [countResult] = await pool.query(` + const { rows: [countResult] } = await pool.query(` SELECT COUNT(DISTINCT o1.order_number) as total FROM orders o1 LEFT JOIN ( @@ -63,7 +70,7 @@ router.get('/', async (req, res) => { WHERE ${conditions.join(' AND ')} `, params); - const total = countResult[0].total; + const total = countResult.total; // Get paginated results const query = ` @@ -75,7 +82,7 @@ router.get('/', async (req, res) => { o1.payment_method, o1.shipping_method, COUNT(o2.pid) as items_count, - CAST(SUM(o2.price * o2.quantity) AS DECIMAL(15,3)) as total_amount + ROUND(SUM(o2.price * o2.quantity)::numeric, 3) as total_amount FROM orders o1 JOIN orders o2 ON o1.order_number = o2.order_number WHERE ${conditions.join(' AND ')} @@ -91,36 +98,37 @@ router.get('/', async (req, res) => { ? `${sortColumn} ${sortDirection}` : `o1.${sortColumn} ${sortDirection}` } - LIMIT ? OFFSET ? + LIMIT $${paramCounter} OFFSET $${paramCounter + 1} `; - const [rows] = await pool.query(query, [...params, limit, offset]); + params.push(limit, offset); + const { rows } = await pool.query(query, params); // Get order statistics - const [stats] = await pool.query(` + const { rows: [orderStats] } = await pool.query(` WITH CurrentStats AS ( SELECT COUNT(DISTINCT order_number) as total_orders, - CAST(SUM(price * quantity) AS DECIMAL(15,3)) as total_revenue + ROUND(SUM(price * quantity)::numeric, 3) as total_revenue FROM orders WHERE canceled = false - AND DATE(date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + AND DATE(date) >= CURRENT_DATE - INTERVAL '30 days' ), PreviousStats AS ( SELECT COUNT(DISTINCT order_number) as prev_orders, - CAST(SUM(price * quantity) AS DECIMAL(15,3)) as prev_revenue + ROUND(SUM(price * quantity)::numeric, 3) as prev_revenue FROM orders WHERE canceled = false - AND DATE(date) BETWEEN DATE_SUB(CURDATE(), INTERVAL 60 DAY) AND DATE_SUB(CURDATE(), INTERVAL 30 DAY) + AND DATE(date) BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '30 days' ), OrderValues AS ( SELECT order_number, - CAST(SUM(price * quantity) AS DECIMAL(15,3)) as order_value + ROUND(SUM(price * quantity)::numeric, 3) as order_value FROM orders WHERE canceled = false - AND DATE(date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + AND DATE(date) >= CURRENT_DATE - INTERVAL '30 days' GROUP BY order_number ) SELECT @@ -128,29 +136,27 @@ router.get('/', async (req, res) => { cs.total_revenue, CASE WHEN ps.prev_orders > 0 - THEN ((cs.total_orders - ps.prev_orders) / ps.prev_orders * 100) + THEN ROUND(((cs.total_orders - ps.prev_orders)::numeric / ps.prev_orders * 100), 1) ELSE 0 END as order_growth, CASE WHEN ps.prev_revenue > 0 - THEN ((cs.total_revenue - ps.prev_revenue) / ps.prev_revenue * 100) + THEN ROUND(((cs.total_revenue - ps.prev_revenue)::numeric / ps.prev_revenue * 100), 1) ELSE 0 END as revenue_growth, CASE WHEN cs.total_orders > 0 - THEN CAST((cs.total_revenue / cs.total_orders) AS DECIMAL(15,3)) + THEN ROUND((cs.total_revenue::numeric / cs.total_orders), 3) ELSE 0 END as average_order_value, CASE WHEN ps.prev_orders > 0 - THEN CAST((ps.prev_revenue / ps.prev_orders) AS DECIMAL(15,3)) + THEN ROUND((ps.prev_revenue::numeric / ps.prev_orders), 3) ELSE 0 END as prev_average_order_value FROM CurrentStats cs CROSS JOIN PreviousStats ps `); - - const orderStats = stats[0]; res.json({ orders: rows.map(row => ({ @@ -189,7 +195,7 @@ router.get('/:orderNumber', async (req, res) => { const pool = req.app.locals.pool; try { // Get order details - const [orderRows] = await pool.query(` + const { rows: orderRows } = await pool.query(` SELECT DISTINCT o1.order_number, o1.customer, @@ -200,10 +206,10 @@ router.get('/:orderNumber', async (req, res) => { o1.shipping_address, o1.billing_address, COUNT(o2.pid) as items_count, - CAST(SUM(o2.price * o2.quantity) AS DECIMAL(15,3)) as total_amount + ROUND(SUM(o2.price * o2.quantity)::numeric, 3) as total_amount FROM orders o1 JOIN orders o2 ON o1.order_number = o2.order_number - WHERE o1.order_number = ? AND o1.canceled = false + WHERE o1.order_number = $1 AND o1.canceled = false GROUP BY o1.order_number, o1.customer, @@ -220,17 +226,17 @@ router.get('/:orderNumber', async (req, res) => { } // Get order items - const [itemRows] = await pool.query(` + const { rows: itemRows } = await pool.query(` SELECT o.pid, p.title, p.SKU, o.quantity, o.price, - CAST((o.price * o.quantity) AS DECIMAL(15,3)) as total + ROUND((o.price * o.quantity)::numeric, 3) as total FROM orders o JOIN products p ON o.pid = p.pid - WHERE o.order_number = ? AND o.canceled = false + WHERE o.order_number = $1 AND o.canceled = false `, [req.params.orderNumber]); const order = { diff --git a/inventory-server/src/routes/products.js b/inventory-server/src/routes/products.js index 94441ff..e3f9630 100755 --- a/inventory-server/src/routes/products.js +++ b/inventory-server/src/routes/products.js @@ -20,7 +20,7 @@ router.get('/brands', async (req, res) => { const pool = req.app.locals.pool; console.log('Fetching brands from database...'); - const [results] = await pool.query(` + const { rows } = await pool.query(` SELECT DISTINCT COALESCE(p.brand, 'Unbranded') as brand FROM products p JOIN purchase_orders po ON p.pid = po.pid @@ -30,8 +30,8 @@ router.get('/brands', async (req, res) => { ORDER BY COALESCE(p.brand, 'Unbranded') `); - console.log(`Found ${results.length} brands:`, results.slice(0, 3)); - res.json(results.map(r => r.brand)); + console.log(`Found ${rows.length} brands:`, rows.slice(0, 3)); + res.json(rows.map(r => r.brand)); } catch (error) { console.error('Error fetching brands:', error); res.status(500).json({ error: 'Failed to fetch brands' }); @@ -50,6 +50,7 @@ router.get('/', async (req, res) => { const conditions = ['p.visible = true']; const params = []; + let paramCounter = 1; // Add default replenishable filter unless explicitly showing non-replenishable if (req.query.showNonReplenishable !== 'true') { @@ -58,9 +59,10 @@ router.get('/', async (req, res) => { // Handle search filter if (req.query.search) { - conditions.push('(p.title LIKE ? OR p.SKU LIKE ? OR p.barcode LIKE ?)'); + conditions.push(`(p.title ILIKE $${paramCounter} OR p.SKU ILIKE $${paramCounter} OR p.barcode ILIKE $${paramCounter})`); const searchTerm = `%${req.query.search}%`; - params.push(searchTerm, searchTerm, searchTerm); + params.push(searchTerm); + paramCounter++; } // Handle numeric filters with operators @@ -84,61 +86,69 @@ router.get('/', async (req, res) => { if (field) { const operator = req.query[`${key}_operator`] || '='; if (operator === 'between') { - // Handle between operator try { const [min, max] = JSON.parse(value); - conditions.push(`${field} BETWEEN ? AND ?`); + conditions.push(`${field} BETWEEN $${paramCounter} AND $${paramCounter + 1}`); params.push(min, max); + paramCounter += 2; } catch (e) { console.error(`Invalid between value for ${key}:`, value); } } else { - // Handle other operators - conditions.push(`${field} ${operator} ?`); + conditions.push(`${field} ${operator} $${paramCounter}`); params.push(parseFloat(value)); + paramCounter++; } } }); // Handle select filters if (req.query.vendor) { - conditions.push('p.vendor = ?'); + conditions.push(`p.vendor = $${paramCounter}`); params.push(req.query.vendor); + paramCounter++; } if (req.query.brand) { - conditions.push('p.brand = ?'); + conditions.push(`p.brand = $${paramCounter}`); params.push(req.query.brand); + paramCounter++; } if (req.query.category) { - conditions.push('p.categories LIKE ?'); + conditions.push(`p.categories ILIKE $${paramCounter}`); params.push(`%${req.query.category}%`); + paramCounter++; } if (req.query.stockStatus && req.query.stockStatus !== 'all') { - conditions.push('pm.stock_status = ?'); + conditions.push(`pm.stock_status = $${paramCounter}`); params.push(req.query.stockStatus); + paramCounter++; } if (req.query.abcClass) { - conditions.push('pm.abc_class = ?'); + conditions.push(`pm.abc_class = $${paramCounter}`); params.push(req.query.abcClass); + paramCounter++; } if (req.query.leadTimeStatus) { - conditions.push('pm.lead_time_status = ?'); + conditions.push(`pm.lead_time_status = $${paramCounter}`); params.push(req.query.leadTimeStatus); + paramCounter++; } if (req.query.replenishable !== undefined) { - conditions.push('p.replenishable = ?'); - params.push(req.query.replenishable === 'true' ? 1 : 0); + conditions.push(`p.replenishable = $${paramCounter}`); + params.push(req.query.replenishable === 'true'); + paramCounter++; } if (req.query.managingStock !== undefined) { - conditions.push('p.managing_stock = ?'); - params.push(req.query.managingStock === 'true' ? 1 : 0); + conditions.push(`p.managing_stock = $${paramCounter}`); + params.push(req.query.managingStock === 'true'); + paramCounter++; } // Combine all conditions with AND @@ -151,17 +161,17 @@ router.get('/', async (req, res) => { LEFT JOIN product_metrics pm ON p.pid = pm.pid ${whereClause} `; - const [countResult] = await pool.query(countQuery, params); - const total = countResult[0].total; + const { rows: [countResult] } = await pool.query(countQuery, params); + const total = countResult.total; // Get available filters - const [categories] = await pool.query( + const { rows: categories } = await pool.query( 'SELECT name FROM categories ORDER BY name' ); - const [vendors] = await pool.query( - 'SELECT DISTINCT vendor FROM products WHERE visible = true AND vendor IS NOT NULL AND vendor != "" ORDER BY vendor' + const { rows: vendors } = await pool.query( + 'SELECT DISTINCT vendor FROM products WHERE visible = true AND vendor IS NOT NULL AND vendor != \'\' ORDER BY vendor' ); - const [brands] = await pool.query( + const { rows: brands } = await pool.query( 'SELECT DISTINCT COALESCE(brand, \'Unbranded\') as brand FROM products WHERE visible = true ORDER BY brand' ); @@ -173,7 +183,7 @@ router.get('/', async (req, res) => { c.cat_id, c.name, c.parent_id, - CAST(c.name AS CHAR(1000)) as path + CAST(c.name AS text) as path FROM categories c WHERE c.parent_id IS NULL @@ -183,7 +193,7 @@ router.get('/', async (req, res) => { c.cat_id, c.name, c.parent_id, - CONCAT(cp.path, ' > ', c.name) + cp.path || ' > ' || c.name FROM categories c JOIN category_path cp ON c.parent_id = cp.cat_id ), @@ -210,7 +220,6 @@ router.get('/', async (req, res) => { FROM products p ), product_leaf_categories AS ( - -- Find categories that aren't parents to other categories for this product SELECT DISTINCT pc.cat_id FROM product_categories pc WHERE NOT EXISTS ( @@ -224,7 +233,7 @@ router.get('/', async (req, res) => { SELECT p.*, COALESCE(p.brand, 'Unbranded') as brand, - GROUP_CONCAT(DISTINCT CONCAT(c.cat_id, ':', c.name)) as categories, + string_agg(DISTINCT (c.cat_id || ':' || c.name), ',') as categories, pm.daily_sales_avg, pm.weekly_sales_avg, pm.monthly_sales_avg, @@ -247,83 +256,32 @@ router.get('/', async (req, res) => { pm.last_received_date, pm.abc_class, pm.stock_status, - pm.turnover_rate, - pm.current_lead_time, - pm.target_lead_time, - pm.lead_time_status, - pm.reorder_qty, - pm.overstocked_amt, - COALESCE(pm.days_of_inventory / NULLIF(pt.target_days, 0), 0) as stock_coverage_ratio + pm.turnover_rate FROM products p LEFT JOIN product_metrics pm ON p.pid = pm.pid LEFT JOIN product_categories pc ON p.pid = pc.pid LEFT JOIN categories c ON pc.cat_id = c.cat_id - LEFT JOIN product_thresholds pt ON p.pid = pt.pid - JOIN product_leaf_categories plc ON c.cat_id = plc.cat_id - ${whereClause ? 'WHERE ' + whereClause.substring(6) : ''} - GROUP BY p.pid + ${whereClause} + GROUP BY p.pid, pm.pid ORDER BY ${sortColumn} ${sortDirection} - LIMIT ? OFFSET ? + LIMIT $${paramCounter} OFFSET $${paramCounter + 1} `; - - // Add pagination params to the main query params - const queryParams = [...params, limit, offset]; - console.log('Query:', query.replace(/\s+/g, ' ')); - console.log('Params:', queryParams); - - const [rows] = await pool.query(query, queryParams); - - // Transform the results - const products = rows.map(row => ({ - ...row, - categories: row.categories ? row.categories.split(',') : [], - price: parseFloat(row.price), - cost_price: parseFloat(row.cost_price), - landing_cost_price: row.landing_cost_price ? parseFloat(row.landing_cost_price) : null, - stock_quantity: parseInt(row.stock_quantity), - daily_sales_avg: parseFloat(row.daily_sales_avg) || 0, - weekly_sales_avg: parseFloat(row.weekly_sales_avg) || 0, - monthly_sales_avg: parseFloat(row.monthly_sales_avg) || 0, - avg_quantity_per_order: parseFloat(row.avg_quantity_per_order) || 0, - number_of_orders: parseInt(row.number_of_orders) || 0, - first_sale_date: row.first_sale_date || null, - last_sale_date: row.last_sale_date || null, - days_of_inventory: parseFloat(row.days_of_inventory) || 0, - weeks_of_inventory: parseFloat(row.weeks_of_inventory) || 0, - reorder_point: parseFloat(row.reorder_point) || 0, - safety_stock: parseFloat(row.safety_stock) || 0, - avg_margin_percent: parseFloat(row.avg_margin_percent) || 0, - total_revenue: parseFloat(row.total_revenue) || 0, - inventory_value: parseFloat(row.inventory_value) || 0, - cost_of_goods_sold: parseFloat(row.cost_of_goods_sold) || 0, - gross_profit: parseFloat(row.gross_profit) || 0, - gmroi: parseFloat(row.gmroi) || 0, - avg_lead_time_days: parseFloat(row.avg_lead_time_days) || 0, - last_purchase_date: row.last_purchase_date || null, - last_received_date: row.last_received_date || null, - abc_class: row.abc_class || null, - stock_status: row.stock_status || null, - turnover_rate: parseFloat(row.turnover_rate) || 0, - current_lead_time: parseFloat(row.current_lead_time) || 0, - target_lead_time: parseFloat(row.target_lead_time) || 0, - lead_time_status: row.lead_time_status || null, - stock_coverage_ratio: parseFloat(row.stock_coverage_ratio) || 0, - reorder_qty: parseInt(row.reorder_qty) || 0, - overstocked_amt: parseInt(row.overstocked_amt) || 0 - })); + + params.push(limit, offset); + const { rows: products } = await pool.query(query, params); res.json({ products, pagination: { total, - currentPage: page, pages: Math.ceil(total / limit), + currentPage: page, limit }, filters: { - categories: categories.map(category => category.name), - vendors: vendors.map(vendor => vendor.vendor), - brands: brands.map(brand => brand.brand) + categories: categories.map(c => c.name), + vendors: vendors.map(v => v.vendor), + brands: brands.map(b => b.brand) } }); } catch (error) { diff --git a/inventory-server/src/routes/purchase-orders.js b/inventory-server/src/routes/purchase-orders.js index 78d1bc8..87a42df 100644 --- a/inventory-server/src/routes/purchase-orders.js +++ b/inventory-server/src/routes/purchase-orders.js @@ -29,40 +29,46 @@ router.get('/', async (req, res) => { let whereClause = '1=1'; const params = []; + let paramCounter = 1; if (search) { - whereClause += ' AND (po.po_id LIKE ? OR po.vendor LIKE ?)'; - params.push(`%${search}%`, `%${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 = ?'; + whereClause += ` AND po.status = $${paramCounter}`; params.push(Number(status)); + paramCounter++; } if (vendor && vendor !== 'all') { - whereClause += ' AND po.vendor = ?'; + whereClause += ` AND po.vendor = $${paramCounter}`; params.push(vendor); + paramCounter++; } if (startDate) { - whereClause += ' AND po.date >= ?'; + whereClause += ` AND po.date >= $${paramCounter}`; params.push(startDate); + paramCounter++; } if (endDate) { - whereClause += ' AND po.date <= ?'; + whereClause += ` AND po.date <= $${paramCounter}`; params.push(endDate); + paramCounter++; } // Get filtered summary metrics - const [summary] = await pool.query(` + const { rows: [summary] } = await pool.query(` WITH po_totals AS ( SELECT po_id, SUM(ordered) as total_ordered, SUM(received) as total_received, - CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_cost + ROUND(SUM(ordered * cost_price)::numeric, 3) as total_cost FROM purchase_orders po WHERE ${whereClause} GROUP BY po_id @@ -72,26 +78,26 @@ router.get('/', async (req, res) => { SUM(total_ordered) as total_ordered, SUM(total_received) as total_received, ROUND( - SUM(total_received) / NULLIF(SUM(total_ordered), 0), 3 + (SUM(total_received)::numeric / NULLIF(SUM(total_ordered), 0)), 3 ) as fulfillment_rate, - CAST(SUM(total_cost) AS DECIMAL(15,3)) as total_value, - CAST(AVG(total_cost) AS DECIMAL(15,3)) as avg_cost + 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 [countResult] = await pool.query(` + const { rows: [countResult] } = await pool.query(` SELECT COUNT(DISTINCT po_id) as total FROM purchase_orders po WHERE ${whereClause} `, params); - const total = countResult[0].total; + const total = countResult.total; const offset = (page - 1) * limit; const pages = Math.ceil(total / limit); // Get recent purchase orders - const [orders] = await pool.query(` + const { rows: orders } = await pool.query(` WITH po_totals AS ( SELECT po_id, @@ -101,10 +107,10 @@ router.get('/', async (req, res) => { receiving_status, COUNT(DISTINCT pid) as total_items, SUM(ordered) as total_quantity, - CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_cost, + ROUND(SUM(ordered * cost_price)::numeric, 3) as total_cost, SUM(received) as total_received, ROUND( - SUM(received) / NULLIF(SUM(ordered), 0), 3 + (SUM(received)::numeric / NULLIF(SUM(ordered), 0)), 3 ) as fulfillment_rate FROM purchase_orders po WHERE ${whereClause} @@ -113,7 +119,7 @@ router.get('/', async (req, res) => { SELECT po_id as id, vendor as vendor_name, - DATE_FORMAT(date, '%Y-%m-%d') as order_date, + to_char(date, 'YYYY-MM-DD') as order_date, status, receiving_status, total_items, @@ -124,21 +130,21 @@ router.get('/', async (req, res) => { FROM po_totals ORDER BY CASE - WHEN ? = 'order_date' THEN date - WHEN ? = 'vendor_name' THEN vendor - WHEN ? = 'total_cost' THEN CAST(total_cost AS DECIMAL(15,3)) - WHEN ? = 'total_received' THEN CAST(total_received AS DECIMAL(15,3)) - WHEN ? = 'total_items' THEN CAST(total_items AS SIGNED) - WHEN ? = 'total_quantity' THEN CAST(total_quantity AS SIGNED) - WHEN ? = 'fulfillment_rate' THEN CAST(fulfillment_rate AS DECIMAL(5,3)) - WHEN ? = 'status' THEN status + WHEN $${paramCounter} = 'order_date' THEN date + WHEN $${paramCounter} = 'vendor_name' THEN vendor + WHEN $${paramCounter} = 'total_cost' THEN total_cost + WHEN $${paramCounter} = 'total_received' THEN total_received + WHEN $${paramCounter} = 'total_items' THEN total_items + WHEN $${paramCounter} = 'total_quantity' THEN total_quantity + WHEN $${paramCounter} = 'fulfillment_rate' THEN fulfillment_rate + WHEN $${paramCounter} = 'status' THEN status ELSE date END ${sortDirection === 'desc' ? 'DESC' : 'ASC'} - LIMIT ? OFFSET ? - `, [...params, sortColumn, sortColumn, sortColumn, sortColumn, sortColumn, sortColumn, sortColumn, sortColumn, Number(limit), offset]); + LIMIT $${paramCounter + 1} OFFSET $${paramCounter + 2} + `, [...params, sortColumn, Number(limit), offset]); // Get unique vendors for filter options - const [vendors] = await pool.query(` + const { rows: vendors } = await pool.query(` SELECT DISTINCT vendor FROM purchase_orders WHERE vendor IS NOT NULL AND vendor != '' @@ -146,7 +152,7 @@ router.get('/', async (req, res) => { `); // Get unique statuses for filter options - const [statuses] = await pool.query(` + const { rows: statuses } = await pool.query(` SELECT DISTINCT status FROM purchase_orders WHERE status IS NOT NULL @@ -169,12 +175,12 @@ router.get('/', async (req, res) => { // Parse summary metrics const parsedSummary = { - order_count: Number(summary[0].order_count) || 0, - total_ordered: Number(summary[0].total_ordered) || 0, - total_received: Number(summary[0].total_received) || 0, - fulfillment_rate: Number(summary[0].fulfillment_rate) || 0, - total_value: Number(summary[0].total_value) || 0, - avg_cost: Number(summary[0].avg_cost) || 0 + 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({ @@ -202,7 +208,7 @@ router.get('/vendor-metrics', async (req, res) => { try { const pool = req.app.locals.pool; - const [metrics] = await pool.query(` + const { rows: metrics } = await pool.query(` WITH delivery_metrics AS ( SELECT vendor, @@ -213,7 +219,7 @@ router.get('/vendor-metrics', async (req, res) => { 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 DATEDIFF(received_date, date) + THEN (received_date - date)::integer ELSE NULL END as delivery_days FROM purchase_orders @@ -226,18 +232,18 @@ router.get('/vendor-metrics', async (req, res) => { SUM(ordered) as total_ordered, SUM(received) as total_received, ROUND( - SUM(received) / NULLIF(SUM(ordered), 0), 3 + (SUM(received)::numeric / NULLIF(SUM(ordered), 0)), 3 ) as fulfillment_rate, - CAST(ROUND( - SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2 - ) AS DECIMAL(15,3)) as avg_unit_cost, - CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_spend, ROUND( - AVG(NULLIF(delivery_days, 0)), 1 + (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 total_orders > 0 + HAVING COUNT(DISTINCT po_id) > 0 ORDER BY total_spend DESC `); @@ -251,7 +257,7 @@ router.get('/vendor-metrics', async (req, res) => { 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: vendor.avg_delivery_days === null ? null : Number(vendor.avg_delivery_days) + avg_delivery_days: Number(vendor.avg_delivery_days) || 0 })); res.json(parsedMetrics); diff --git a/inventory-server/src/routes/vendors.js b/inventory-server/src/routes/vendors.js index 9ecbab1..caa7bb9 100644 --- a/inventory-server/src/routes/vendors.js +++ b/inventory-server/src/routes/vendors.js @@ -6,7 +6,7 @@ router.get('/', async (req, res) => { const pool = req.app.locals.pool; try { // Get all vendors with metrics - const [vendors] = await pool.query(` + const { rows: vendors } = await pool.query(` SELECT DISTINCT p.vendor as name, COALESCE(vm.active_products, 0) as active_products, @@ -26,16 +26,16 @@ router.get('/', async (req, res) => { // Get cost metrics for all vendors const vendorNames = vendors.map(v => v.name); - const [costMetrics] = await pool.query(` + const { rows: costMetrics } = await pool.query(` SELECT vendor, - CAST(ROUND(SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2) AS DECIMAL(15,3)) as avg_unit_cost, - CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_spend + 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 FROM purchase_orders WHERE status = 'closed' AND cost_price IS NOT NULL AND ordered > 0 - AND vendor IN (?) + AND vendor = ANY($1) GROUP BY vendor `, [vendorNames]); @@ -49,26 +49,26 @@ router.get('/', async (req, res) => { }, {}); // Get overall stats - const [stats] = await pool.query(` + const { rows: [stats] } = await pool.query(` SELECT COUNT(DISTINCT p.vendor) as totalVendors, COUNT(DISTINCT CASE WHEN COALESCE(vm.total_orders, 0) > 0 AND COALESCE(vm.order_fill_rate, 0) >= 75 THEN p.vendor END) as activeVendors, - COALESCE(ROUND(AVG(NULLIF(vm.avg_lead_time_days, 0)), 1), 0) as avgLeadTime, - COALESCE(ROUND(AVG(NULLIF(vm.order_fill_rate, 0)), 1), 0) as avgFillRate, - COALESCE(ROUND(AVG(NULLIF(vm.on_time_delivery_rate, 0)), 1), 0) as avgOnTimeDelivery + COALESCE(ROUND(AVG(NULLIF(vm.avg_lead_time_days, 0))::numeric, 1), 0) as avgLeadTime, + COALESCE(ROUND(AVG(NULLIF(vm.order_fill_rate, 0))::numeric, 1), 0) as avgFillRate, + COALESCE(ROUND(AVG(NULLIF(vm.on_time_delivery_rate, 0))::numeric, 1), 0) as avgOnTimeDelivery FROM products p LEFT JOIN vendor_metrics vm ON p.vendor = vm.vendor WHERE p.vendor IS NOT NULL AND p.vendor != '' `); // Get overall cost metrics - const [overallCostMetrics] = await pool.query(` + const { rows: [overallCostMetrics] } = await pool.query(` SELECT - CAST(ROUND(SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2) AS DECIMAL(15,3)) as avg_unit_cost, - CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_spend + 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 FROM purchase_orders WHERE status = 'closed' AND cost_price IS NOT NULL @@ -90,13 +90,13 @@ router.get('/', async (req, res) => { total_spend: parseFloat(costMetricsMap[vendor.name]?.total_spend || 0) })), stats: { - totalVendors: parseInt(stats[0].totalVendors), - activeVendors: parseInt(stats[0].activeVendors), - avgLeadTime: parseFloat(stats[0].avgLeadTime), - avgFillRate: parseFloat(stats[0].avgFillRate), - avgOnTimeDelivery: parseFloat(stats[0].avgOnTimeDelivery), - avgUnitCost: parseFloat(overallCostMetrics[0].avg_unit_cost), - totalSpend: parseFloat(overallCostMetrics[0].total_spend) + totalVendors: parseInt(stats.totalvendors), + activeVendors: parseInt(stats.activevendors), + avgLeadTime: parseFloat(stats.avgleadtime), + avgFillRate: parseFloat(stats.avgfillrate), + avgOnTimeDelivery: parseFloat(stats.avgontimedelivery), + avgUnitCost: parseFloat(overallCostMetrics.avg_unit_cost), + totalSpend: parseFloat(overallCostMetrics.total_spend) } }); } catch (error) { diff --git a/inventory-server/src/server.js b/inventory-server/src/server.js index c58ad7d..77f1c38 100755 --- a/inventory-server/src/server.js +++ b/inventory-server/src/server.js @@ -3,7 +3,6 @@ const cors = require('cors'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); -const mysql = require('mysql2/promise'); const { corsMiddleware, corsErrorHandler } = require('./middleware/cors'); const { initPool } = require('./utils/db'); const productsRouter = require('./routes/products'); @@ -16,11 +15,9 @@ const configRouter = require('./routes/config'); const metricsRouter = require('./routes/metrics'); const vendorsRouter = require('./routes/vendors'); const categoriesRouter = require('./routes/categories'); -const testConnectionRouter = require('./routes/test-connection'); // Get the absolute path to the .env file -const envPath = path.resolve(process.cwd(), '.env'); -console.log('Current working directory:', process.cwd()); +const envPath = '/var/www/html/inventory/.env'; console.log('Looking for .env file at:', envPath); console.log('.env file exists:', fs.existsSync(envPath)); @@ -33,6 +30,9 @@ try { DB_HOST: process.env.DB_HOST || 'not set', DB_USER: process.env.DB_USER || 'not set', DB_NAME: process.env.DB_NAME || 'not set', + DB_PASSWORD: process.env.DB_PASSWORD ? '[password set]' : 'not set', + DB_PORT: process.env.DB_PORT || 'not set', + DB_SSL: process.env.DB_SSL || 'not set' }); } catch (error) { console.error('Error loading .env file:', error); @@ -66,20 +66,27 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Initialize database pool -const pool = initPool({ +const poolPromise = initPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, - waitForConnections: true, - connectionLimit: process.env.NODE_ENV === 'production' ? 20 : 10, - queueLimit: 0, - enableKeepAlive: true, - keepAliveInitialDelay: 0 + port: process.env.DB_PORT || 5432, + max: process.env.NODE_ENV === 'production' ? 20 : 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, + ssl: process.env.DB_SSL === 'true' ? { + rejectUnauthorized: false + } : false }); -// Make pool available to routes -app.locals.pool = pool; +// Make pool available to routes once initialized +poolPromise.then(pool => { + app.locals.pool = pool; +}).catch(err => { + console.error('[Database] Failed to initialize pool:', err); + process.exit(1); +}); // Routes app.use('/api/products', productsRouter); @@ -92,7 +99,6 @@ app.use('/api/config', configRouter); app.use('/api/metrics', metricsRouter); app.use('/api/vendors', vendorsRouter); app.use('/api/categories', categoriesRouter); -app.use('/api', testConnectionRouter); // Basic health check route app.get('/health', (req, res) => { @@ -128,17 +134,6 @@ process.on('unhandledRejection', (reason, promise) => { console.error(`[${new Date().toISOString()}] Unhandled Rejection at:`, promise, 'reason:', reason); }); -// Test database connection -pool.getConnection() - .then(connection => { - console.log('[Database] Connected successfully'); - connection.release(); - }) - .catch(err => { - console.error('[Database] Error connecting:', err); - process.exit(1); - }); - // Initialize client sets for SSE const importClients = new Set(); const updateClients = new Set(); diff --git a/inventory-server/old/csvImporter.js b/inventory-server/src/utils/csvImporter.js similarity index 100% rename from inventory-server/old/csvImporter.js rename to inventory-server/src/utils/csvImporter.js diff --git a/inventory-server/src/utils/db.js b/inventory-server/src/utils/db.js index 28f689f..3b6454f 100644 --- a/inventory-server/src/utils/db.js +++ b/inventory-server/src/utils/db.js @@ -1,17 +1,70 @@ -const mysql = require('mysql2/promise'); +const { Pool, Client } = require('pg'); let pool; function initPool(config) { - pool = mysql.createPool(config); - return pool; + // Log config without sensitive data + const safeConfig = { + host: config.host, + user: config.user, + database: config.database, + port: config.port, + max: config.max, + idleTimeoutMillis: config.idleTimeoutMillis, + connectionTimeoutMillis: config.connectionTimeoutMillis, + ssl: config.ssl, + password: config.password ? '[password set]' : '[no password]' + }; + console.log('[Database] Initializing pool with config:', safeConfig); + + // Try creating a client first to test the connection + const testClient = new Client({ + host: config.host, + user: config.user, + password: config.password, + database: config.database, + port: config.port, + ssl: config.ssl + }); + + console.log('[Database] Testing connection with Client...'); + return testClient.connect() + .then(() => { + console.log('[Database] Test connection with Client successful'); + return testClient.end(); + }) + .then(() => { + // If client connection worked, create the pool + console.log('[Database] Creating pool...'); + pool = new Pool({ + host: config.host, + user: config.user, + password: config.password, + database: config.database, + port: config.port, + max: config.max, + idleTimeoutMillis: config.idleTimeoutMillis, + connectionTimeoutMillis: config.connectionTimeoutMillis, + ssl: config.ssl + }); + return pool.connect(); + }) + .then(poolClient => { + console.log('[Database] Pool connection successful'); + poolClient.release(); + return pool; + }) + .catch(err => { + console.error('[Database] Connection failed:', err); + throw err; + }); } async function getConnection() { if (!pool) { throw new Error('Database pool not initialized'); } - return pool.getConnection(); + return pool.connect(); } module.exports = { diff --git a/inventory/src/components/settings/DataManagement.tsx b/inventory/src/components/settings/DataManagement.tsx index 4e97413..1377e41 100644 --- a/inventory/src/components/settings/DataManagement.tsx +++ b/inventory/src/components/settings/DataManagement.tsx @@ -85,9 +85,7 @@ export function DataManagement() { const [] = useState(null); const [eventSource, setEventSource] = useState(null); const [importHistory, setImportHistory] = useState([]); - const [calculateHistory, setCalculateHistory] = useState< - CalculateHistoryRecord[] - >([]); + const [calculateHistory, setCalculateHistory] = useState([]); const [moduleStatus, setModuleStatus] = useState([]); const [tableStatus, setTableStatus] = useState([]); const [scriptOutput, setScriptOutput] = useState([]); @@ -368,6 +366,10 @@ export function DataManagement() { fetch(`${config.apiUrl}/csv/status/tables`), ]); + if (!importRes.ok || !calcRes.ok || !moduleRes.ok || !tableRes.ok) { + throw new Error('One or more requests failed'); + } + const [importData, calcData, moduleData, tableData] = await Promise.all([ importRes.json(), calcRes.json(), @@ -375,52 +377,66 @@ export function DataManagement() { tableRes.json(), ]); - setImportHistory(importData); - setCalculateHistory(calcData); - setModuleStatus(moduleData); - setTableStatus(tableData); + // Ensure we're setting arrays even if the response is empty or invalid + setImportHistory(Array.isArray(importData) ? importData : []); + setCalculateHistory(Array.isArray(calcData) ? calcData : []); + setModuleStatus(Array.isArray(moduleData) ? moduleData : []); + setTableStatus(Array.isArray(tableData) ? tableData : []); } catch (error) { console.error("Error fetching history:", error); + // Set empty arrays as fallback + setImportHistory([]); + setCalculateHistory([]); + setModuleStatus([]); + setTableStatus([]); } }; const refreshTableStatus = async () => { try { const response = await fetch(`${config.apiUrl}/csv/status/tables`); + if (!response.ok) throw new Error('Failed to fetch table status'); const data = await response.json(); - setTableStatus(data); + setTableStatus(Array.isArray(data) ? data : []); } catch (error) { toast.error("Failed to refresh table status"); + setTableStatus([]); } }; const refreshModuleStatus = async () => { try { const response = await fetch(`${config.apiUrl}/csv/status/modules`); + if (!response.ok) throw new Error('Failed to fetch module status'); const data = await response.json(); - setModuleStatus(data); + setModuleStatus(Array.isArray(data) ? data : []); } catch (error) { toast.error("Failed to refresh module status"); + setModuleStatus([]); } }; const refreshImportHistory = async () => { try { const response = await fetch(`${config.apiUrl}/csv/history/import`); + if (!response.ok) throw new Error('Failed to fetch import history'); const data = await response.json(); - setImportHistory(data); + setImportHistory(Array.isArray(data) ? data : []); } catch (error) { toast.error("Failed to refresh import history"); + setImportHistory([]); } }; const refreshCalculateHistory = async () => { try { const response = await fetch(`${config.apiUrl}/csv/history/calculate`); + if (!response.ok) throw new Error('Failed to fetch calculate history'); const data = await response.json(); - setCalculateHistory(data); + setCalculateHistory(Array.isArray(data) ? data : []); } catch (error) { toast.error("Failed to refresh calculate history"); + setCalculateHistory([]); } }; diff --git a/inventory/src/pages/PurchaseOrders.tsx b/inventory/src/pages/PurchaseOrders.tsx index 86585c7..35b077d 100644 --- a/inventory/src/pages/PurchaseOrders.tsx +++ b/inventory/src/pages/PurchaseOrders.tsx @@ -112,7 +112,7 @@ export default function PurchaseOrders() { statuses: string[]; }>({ vendors: [], - statuses: [], + statuses: [] }); const [pagination, setPagination] = useState({ total: 0, @@ -153,15 +153,57 @@ export default function PurchaseOrders() { fetch('/api/purchase-orders/cost-analysis') ]); - const [ - purchaseOrdersData, - vendorMetricsData, - costAnalysisData - ] = await Promise.all([ - purchaseOrdersRes.json() as Promise, - vendorMetricsRes.json(), - costAnalysisRes.json() - ]); + // Initialize default data + let purchaseOrdersData: PurchaseOrdersResponse = { + orders: [], + summary: { + order_count: 0, + total_ordered: 0, + total_received: 0, + fulfillment_rate: 0, + total_value: 0, + avg_cost: 0 + }, + pagination: { + total: 0, + pages: 0, + page: 1, + limit: 100 + }, + filters: { + vendors: [], + statuses: [] + } + }; + + let vendorMetricsData: VendorMetrics[] = []; + let costAnalysisData: CostAnalysis = { + unique_products: 0, + avg_cost: 0, + min_cost: 0, + max_cost: 0, + cost_variance: 0, + total_spend_by_category: [] + }; + + // Only try to parse responses if they were successful + if (purchaseOrdersRes.ok) { + purchaseOrdersData = await purchaseOrdersRes.json(); + } else { + console.error('Failed to fetch purchase orders:', await purchaseOrdersRes.text()); + } + + if (vendorMetricsRes.ok) { + vendorMetricsData = await vendorMetricsRes.json(); + } else { + console.error('Failed to fetch vendor metrics:', await vendorMetricsRes.text()); + } + + if (costAnalysisRes.ok) { + costAnalysisData = await costAnalysisRes.json(); + } else { + console.error('Failed to fetch cost analysis:', await costAnalysisRes.text()); + } setPurchaseOrders(purchaseOrdersData.orders); setPagination(purchaseOrdersData.pagination); @@ -171,6 +213,27 @@ export default function PurchaseOrders() { setCostAnalysis(costAnalysisData); } catch (error) { console.error('Error fetching data:', error); + // Set default values in case of error + setPurchaseOrders([]); + setPagination({ total: 0, pages: 0, page: 1, limit: 100 }); + setFilterOptions({ vendors: [], statuses: [] }); + setSummary({ + order_count: 0, + total_ordered: 0, + total_received: 0, + fulfillment_rate: 0, + total_value: 0, + avg_cost: 0 + }); + setVendorMetrics([]); + setCostAnalysis({ + unique_products: 0, + avg_cost: 0, + min_cost: 0, + max_cost: 0, + cost_variance: 0, + total_spend_by_category: [] + }); } finally { setLoading(false); } @@ -310,7 +373,7 @@ export default function PurchaseOrders() { All Vendors - {filterOptions.vendors.map(vendor => ( + {filterOptions?.vendors?.map(vendor => ( {vendor} diff --git a/inventory/src/types/products.ts b/inventory/src/types/products.ts index bdeba2c..634581f 100644 --- a/inventory/src/types/products.ts +++ b/inventory/src/types/products.ts @@ -3,10 +3,10 @@ export interface Product { title: string; SKU: string; stock_quantity: number; - price: string; // DECIMAL(15,3) - regular_price: string; // DECIMAL(15,3) - cost_price: string; // DECIMAL(15,3) - landing_cost_price: string | null; // DECIMAL(15,3) + price: string; // numeric(15,3) + regular_price: string; // numeric(15,3) + cost_price: string; // numeric(15,3) + landing_cost_price: string | null; // numeric(15,3) barcode: string; vendor: string; vendor_reference: string; @@ -24,32 +24,32 @@ export interface Product { updated_at: string; // Metrics - daily_sales_avg?: string; // DECIMAL(15,3) - weekly_sales_avg?: string; // DECIMAL(15,3) - monthly_sales_avg?: string; // DECIMAL(15,3) - avg_quantity_per_order?: string; // DECIMAL(15,3) + daily_sales_avg?: string; // numeric(15,3) + weekly_sales_avg?: string; // numeric(15,3) + monthly_sales_avg?: string; // numeric(15,3) + avg_quantity_per_order?: string; // numeric(15,3) number_of_orders?: number; first_sale_date?: string; last_sale_date?: string; last_purchase_date?: string; - days_of_inventory?: string; // DECIMAL(15,3) - weeks_of_inventory?: string; // DECIMAL(15,3) - reorder_point?: string; // DECIMAL(15,3) - safety_stock?: string; // DECIMAL(15,3) - avg_margin_percent?: string; // DECIMAL(15,3) - total_revenue?: string; // DECIMAL(15,3) - inventory_value?: string; // DECIMAL(15,3) - cost_of_goods_sold?: string; // DECIMAL(15,3) - gross_profit?: string; // DECIMAL(15,3) - gmroi?: string; // DECIMAL(15,3) - avg_lead_time_days?: string; // DECIMAL(15,3) + days_of_inventory?: string; // numeric(15,3) + weeks_of_inventory?: string; // numeric(15,3) + reorder_point?: string; // numeric(15,3) + safety_stock?: string; // numeric(15,3) + avg_margin_percent?: string; // numeric(15,3) + total_revenue?: string; // numeric(15,3) + inventory_value?: string; // numeric(15,3) + cost_of_goods_sold?: string; // numeric(15,3) + gross_profit?: string; // numeric(15,3) + gmroi?: string; // numeric(15,3) + avg_lead_time_days?: string; // numeric(15,3) last_received_date?: string; abc_class?: string; stock_status?: string; - turnover_rate?: string; // DECIMAL(15,3) - current_lead_time?: string; // DECIMAL(15,3) - target_lead_time?: string; // DECIMAL(15,3) + turnover_rate?: string; // numeric(15,3) + current_lead_time?: string; // numeric(15,3) + target_lead_time?: string; // numeric(15,3) lead_time_status?: string; reorder_qty?: number; - overstocked_amt?: string; // DECIMAL(15,3) + overstocked_amt?: string; // numeric(15,3) }