Update backend/frontend
This commit is contained in:
@@ -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' });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,30 +136,28 @@ 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 => ({
|
||||
...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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -85,9 +85,7 @@ export function DataManagement() {
|
||||
const [] = useState<ImportProgress | null>(null);
|
||||
const [eventSource, setEventSource] = useState<EventSource | null>(null);
|
||||
const [importHistory, setImportHistory] = useState<ImportHistoryRecord[]>([]);
|
||||
const [calculateHistory, setCalculateHistory] = useState<
|
||||
CalculateHistoryRecord[]
|
||||
>([]);
|
||||
const [calculateHistory, setCalculateHistory] = useState<CalculateHistoryRecord[]>([]);
|
||||
const [moduleStatus, setModuleStatus] = useState<ModuleStatus[]>([]);
|
||||
const [tableStatus, setTableStatus] = useState<TableStatus[]>([]);
|
||||
const [scriptOutput, setScriptOutput] = useState<string[]>([]);
|
||||
@@ -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([]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<PurchaseOrdersResponse>,
|
||||
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() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Vendors</SelectItem>
|
||||
{filterOptions.vendors.map(vendor => (
|
||||
{filterOptions?.vendors?.map(vendor => (
|
||||
<SelectItem key={vendor} value={vendor}>
|
||||
{vendor}
|
||||
</SelectItem>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user