Get frontend dashboard/analytics mostly loading data again

This commit is contained in:
2025-04-08 00:02:43 -04:00
parent 43f76e4ac0
commit f271f3aae4
8 changed files with 575 additions and 613 deletions

View File

@@ -22,11 +22,11 @@ router.get('/stock/metrics', async (req, res) => {
const { rows: [stockMetrics] } = await executeQuery(`
SELECT
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
COALESCE(COUNT(CASE WHEN current_stock > 0 THEN 1 END), 0)::integer as products_in_stock,
COALESCE(SUM(CASE WHEN current_stock > 0 THEN current_stock END), 0)::integer as total_units,
ROUND(COALESCE(SUM(CASE WHEN current_stock > 0 THEN current_stock_cost END), 0)::numeric, 3) as total_cost,
ROUND(COALESCE(SUM(CASE WHEN current_stock > 0 THEN current_stock_retail END), 0)::numeric, 3) as total_retail
FROM product_metrics
`);
console.log('Raw stockMetrics from database:', stockMetrics);
@@ -42,13 +42,13 @@ router.get('/stock/metrics', async (req, res) => {
SELECT
COALESCE(brand, 'Unbranded') as brand,
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
COALESCE(SUM(current_stock), 0)::integer as stock_units,
ROUND(COALESCE(SUM(current_stock_cost), 0)::numeric, 3) as stock_cost,
ROUND(COALESCE(SUM(current_stock_retail), 0)::numeric, 3) as stock_retail
FROM product_metrics
WHERE current_stock > 0
GROUP BY COALESCE(brand, 'Unbranded')
HAVING ROUND(COALESCE(SUM(stock_quantity * cost_price), 0)::numeric, 3) > 0
HAVING ROUND(COALESCE(SUM(current_stock_cost), 0)::numeric, 3) > 0
),
other_brands AS (
SELECT
@@ -130,11 +130,11 @@ router.get('/purchase/metrics', async (req, res) => {
END), 0)::numeric, 3) as total_cost,
ROUND(COALESCE(SUM(CASE
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
THEN po.ordered * p.price
THEN po.ordered * pm.current_price
ELSE 0
END), 0)::numeric, 3) as total_retail
FROM purchase_orders po
JOIN products p ON po.pid = p.pid
JOIN product_metrics pm ON po.pid = pm.pid
`);
const { rows: vendorOrders } = await executeQuery(`
@@ -143,9 +143,9 @@ router.get('/purchase/metrics', async (req, res) => {
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
ROUND(COALESCE(SUM(po.ordered * pm.current_price), 0)::numeric, 3) as retail
FROM purchase_orders po
JOIN products p ON po.pid = p.pid
JOIN product_metrics pm ON po.pid = pm.pid
WHERE po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
GROUP BY po.vendor
HAVING ROUND(COALESCE(SUM(po.ordered * po.cost_price), 0)::numeric, 3) > 0
@@ -223,54 +223,35 @@ router.get('/replenishment/metrics', async (req, res) => {
// Get summary metrics
const { rows: [metrics] } = await executeQuery(`
SELECT
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)::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)::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)::numeric, 3) as total_retail
FROM products p
JOIN product_metrics pm ON p.pid = pm.pid
WHERE p.replenishable = true
AND (pm.stock_status IN ('Critical', 'Reorder')
OR p.stock_quantity < 0)
AND pm.reorder_qty > 0
COUNT(DISTINCT pm.pid)::integer as products_to_replenish,
COALESCE(SUM(pm.replenishment_units), 0)::integer as total_units_needed,
ROUND(COALESCE(SUM(pm.replenishment_cost), 0)::numeric, 3) as total_cost,
ROUND(COALESCE(SUM(pm.replenishment_retail), 0)::numeric, 3) as total_retail
FROM product_metrics pm
WHERE pm.is_replenishable = true
AND (pm.status IN ('Critical', 'Reorder')
OR pm.current_stock < 0)
AND pm.replenishment_units > 0
`);
// Get top variants to replenish
const { rows: variants } = await executeQuery(`
SELECT
p.pid,
p.title,
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::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::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::numeric, 3) as replenish_retail,
pm.stock_status
FROM products p
JOIN product_metrics pm ON p.pid = pm.pid
WHERE p.replenishable = true
AND (pm.stock_status IN ('Critical', 'Reorder')
OR p.stock_quantity < 0)
AND pm.reorder_qty > 0
pm.pid,
pm.title,
pm.current_stock::integer as current_stock,
pm.replenishment_units::integer as replenish_qty,
ROUND(pm.replenishment_cost::numeric, 3) as replenish_cost,
ROUND(pm.replenishment_retail::numeric, 3) as replenish_retail,
pm.status,
pm.planning_period_days::text as planning_period
FROM product_metrics pm
WHERE pm.is_replenishable = true
AND (pm.status IN ('Critical', 'Reorder')
OR pm.current_stock < 0)
AND pm.replenishment_units > 0
ORDER BY
CASE pm.stock_status
CASE pm.status
WHEN 'Critical' THEN 1
WHEN 'Reorder' THEN 2
END,
@@ -280,7 +261,7 @@ router.get('/replenishment/metrics', async (req, res) => {
// If no data, provide dummy data
if (!metrics || variants.length === 0) {
console.log('No replenishment metrics found, returning dummy data');
console.log('No replenishment metrics found in new schema, returning dummy data');
return res.json({
productsToReplenish: 15,
@@ -288,11 +269,11 @@ router.get('/replenishment/metrics', async (req, res) => {
replenishmentCost: 15000.00,
replenishmentRetail: 30000.00,
topVariants: [
{ id: 1, title: "Test Product 1", currentStock: 5, replenishQty: 20, replenishCost: 500, replenishRetail: 1000, status: "Critical" },
{ id: 2, title: "Test Product 2", currentStock: 10, replenishQty: 15, replenishCost: 450, replenishRetail: 900, status: "Critical" },
{ id: 3, title: "Test Product 3", currentStock: 15, replenishQty: 10, replenishCost: 300, replenishRetail: 600, status: "Reorder" },
{ id: 4, title: "Test Product 4", currentStock: 20, replenishQty: 20, replenishCost: 200, replenishRetail: 400, status: "Reorder" },
{ id: 5, title: "Test Product 5", currentStock: 25, replenishQty: 10, replenishCost: 150, replenishRetail: 300, status: "Reorder" }
{ id: 1, title: "Test Product 1", currentStock: 5, replenishQty: 20, replenishCost: 500, replenishRetail: 1000, status: "Critical", planningPeriod: "30" },
{ id: 2, title: "Test Product 2", currentStock: 10, replenishQty: 15, replenishCost: 450, replenishRetail: 900, status: "Critical", planningPeriod: "30" },
{ id: 3, title: "Test Product 3", currentStock: 15, replenishQty: 10, replenishCost: 300, replenishRetail: 600, status: "Reorder", planningPeriod: "30" },
{ id: 4, title: "Test Product 4", currentStock: 20, replenishQty: 20, replenishCost: 200, replenishRetail: 400, status: "Reorder", planningPeriod: "30" },
{ id: 5, title: "Test Product 5", currentStock: 25, replenishQty: 10, replenishCost: 150, replenishRetail: 300, status: "Reorder", planningPeriod: "30" }
]
});
}
@@ -310,7 +291,8 @@ router.get('/replenishment/metrics', async (req, res) => {
replenishQty: parseInt(v.replenish_qty) || 0,
replenishCost: parseFloat(v.replenish_cost) || 0,
replenishRetail: parseFloat(v.replenish_retail) || 0,
status: v.stock_status
status: v.status,
planningPeriod: v.planning_period
}))
};
@@ -325,11 +307,11 @@ router.get('/replenishment/metrics', async (req, res) => {
replenishmentCost: 15000.00,
replenishmentRetail: 30000.00,
topVariants: [
{ id: 1, title: "Test Product 1", currentStock: 5, replenishQty: 20, replenishCost: 500, replenishRetail: 1000, status: "Critical" },
{ id: 2, title: "Test Product 2", currentStock: 10, replenishQty: 15, replenishCost: 450, replenishRetail: 900, status: "Critical" },
{ id: 3, title: "Test Product 3", currentStock: 15, replenishQty: 10, replenishCost: 300, replenishRetail: 600, status: "Reorder" },
{ id: 4, title: "Test Product 4", currentStock: 20, replenishQty: 20, replenishCost: 200, replenishRetail: 400, status: "Reorder" },
{ id: 5, title: "Test Product 5", currentStock: 25, replenishQty: 10, replenishCost: 150, replenishRetail: 300, status: "Reorder" }
{ id: 1, title: "Test Product 1", currentStock: 5, replenishQty: 20, replenishCost: 500, replenishRetail: 1000, status: "Critical", planningPeriod: "30" },
{ id: 2, title: "Test Product 2", currentStock: 10, replenishQty: 15, replenishCost: 450, replenishRetail: 900, status: "Critical", planningPeriod: "30" },
{ id: 3, title: "Test Product 3", currentStock: 15, replenishQty: 10, replenishCost: 300, replenishRetail: 600, status: "Reorder", planningPeriod: "30" },
{ id: 4, title: "Test Product 4", currentStock: 20, replenishQty: 20, replenishCost: 200, replenishRetail: 400, status: "Reorder", planningPeriod: "30" },
{ id: 5, title: "Test Product 5", currentStock: 25, replenishQty: 10, replenishCost: 150, replenishRetail: 300, status: "Reorder", planningPeriod: "30" }
]
});
}
@@ -499,74 +481,15 @@ router.get('/forecast/metrics', async (req, res) => {
// Returns overstock metrics by category
router.get('/overstock/metrics', async (req, res) => {
try {
const { rows } = await executeQuery(`
WITH category_overstock AS (
SELECT
c.cat_id,
c.name as category_name,
COUNT(DISTINCT CASE
WHEN pm.stock_status = 'Overstocked'
THEN p.pid
END) as overstocked_products,
SUM(CASE
WHEN pm.stock_status = 'Overstocked'
THEN pm.overstocked_amt
ELSE 0
END) as total_excess_units,
SUM(CASE
WHEN pm.stock_status = 'Overstocked'
THEN pm.overstocked_amt * p.cost_price
ELSE 0
END) as total_excess_cost,
SUM(CASE
WHEN pm.stock_status = 'Overstocked'
THEN pm.overstocked_amt * p.price
ELSE 0
END) as total_excess_retail
FROM categories c
JOIN product_categories pc ON c.cat_id = pc.cat_id
JOIN products p ON pc.pid = p.pid
JOIN product_metrics pm ON p.pid = pm.pid
GROUP BY c.cat_id, c.name
),
filtered_categories AS (
SELECT *
FROM category_overstock
WHERE overstocked_products > 0
ORDER BY total_excess_cost DESC
LIMIT 8
),
summary AS (
SELECT
SUM(overstocked_products) as total_overstocked,
SUM(total_excess_units) as total_excess_units,
SUM(total_excess_cost) as total_excess_cost,
SUM(total_excess_retail) as total_excess_retail
FROM filtered_categories
)
SELECT
s.total_overstocked,
s.total_excess_units,
s.total_excess_cost,
s.total_excess_retail,
json_agg(
json_build_object(
'category', fc.category_name,
'products', fc.overstocked_products,
'units', fc.total_excess_units,
'cost', fc.total_excess_cost,
'retail', fc.total_excess_retail
)
) as category_data
FROM summary s, filtered_categories fc
GROUP BY
s.total_overstocked,
s.total_excess_units,
s.total_excess_cost,
s.total_excess_retail
// Check if we have any products with Overstock status
const { rows: [countCheck] } = await executeQuery(`
SELECT COUNT(*) as overstock_count FROM product_metrics WHERE status = 'Overstock'
`);
if (rows.length === 0) {
console.log('Overstock count:', countCheck.overstock_count);
// If no overstock products, return empty metrics
if (parseInt(countCheck.overstock_count) === 0) {
return res.json({
overstockedProducts: 0,
total_excess_units: 0,
@@ -575,31 +498,51 @@ router.get('/overstock/metrics', async (req, res) => {
category_data: []
});
}
// Get summary metrics in a simpler, more direct query
const { rows: [summaryMetrics] } = await executeQuery(`
SELECT
COUNT(DISTINCT pid)::integer as total_overstocked,
SUM(overstocked_units)::integer as total_excess_units,
ROUND(SUM(overstocked_cost)::numeric, 3) as total_excess_cost,
ROUND(SUM(overstocked_retail)::numeric, 3) as total_excess_retail
FROM product_metrics
WHERE status = 'Overstock'
`);
// Get category breakdowns separately
const { rows: categoryData } = await executeQuery(`
SELECT
c.name as category_name,
COUNT(DISTINCT pm.pid)::integer as overstocked_products,
SUM(pm.overstocked_units)::integer as total_excess_units,
ROUND(SUM(pm.overstocked_cost)::numeric, 3) as total_excess_cost,
ROUND(SUM(pm.overstocked_retail)::numeric, 3) as total_excess_retail
FROM categories c
JOIN product_categories pc ON c.cat_id = pc.cat_id
JOIN product_metrics pm ON pc.pid = pm.pid
WHERE pm.status = 'Overstock'
GROUP BY c.name
ORDER BY total_excess_cost DESC
LIMIT 8
`);
// Generate dummy data if the query returned empty results
if (rows[0].total_overstocked === null || rows[0].total_excess_units === null) {
console.log('Empty overstock metrics results, returning dummy data');
return res.json({
overstockedProducts: 10,
total_excess_units: 500,
total_excess_cost: 5000,
total_excess_retail: 10000,
category_data: [
{ category: "Electronics", products: 3, units: 150, cost: 1500, retail: 3000 },
{ category: "Clothing", products: 4, units: 200, cost: 2000, retail: 4000 },
{ category: "Home Goods", products: 2, units: 100, cost: 1000, retail: 2000 },
{ category: "Office Supplies", products: 1, units: 50, cost: 500, retail: 1000 }
]
});
}
console.log('Summary metrics:', summaryMetrics);
console.log('Category data count:', categoryData.length);
// Format response with explicit type conversion
const response = {
overstockedProducts: parseInt(rows[0].total_overstocked) || 0,
total_excess_units: parseInt(rows[0].total_excess_units) || 0,
total_excess_cost: parseFloat(rows[0].total_excess_cost) || 0,
total_excess_retail: parseFloat(rows[0].total_excess_retail) || 0,
category_data: rows[0].category_data || []
overstockedProducts: parseInt(summaryMetrics.total_overstocked) || 0,
total_excess_units: parseInt(summaryMetrics.total_excess_units) || 0,
total_excess_cost: parseFloat(summaryMetrics.total_excess_cost) || 0,
total_excess_retail: parseFloat(summaryMetrics.total_excess_retail) || 0,
category_data: categoryData.map(cat => ({
category: cat.category_name,
products: parseInt(cat.overstocked_products) || 0,
units: parseInt(cat.total_excess_units) || 0,
cost: parseFloat(cat.total_excess_cost) || 0,
retail: parseFloat(cat.total_excess_retail) || 0
}))
};
res.json(response);
@@ -629,27 +572,26 @@ router.get('/overstock/products', async (req, res) => {
try {
const { rows } = await executeQuery(`
SELECT
p.pid,
p.SKU,
p.title,
p.brand,
p.vendor,
p.stock_quantity,
p.cost_price,
p.price,
pm.daily_sales_avg,
pm.days_of_inventory,
pm.overstocked_amt,
(pm.overstocked_amt * p.cost_price) as excess_cost,
(pm.overstocked_amt * p.price) as excess_retail,
pm.pid,
pm.sku AS SKU,
pm.title,
pm.brand,
pm.vendor,
pm.current_stock as stock_quantity,
pm.current_cost_price as cost_price,
pm.current_price as price,
pm.sales_velocity_daily as daily_sales_avg,
pm.stock_cover_in_days as days_of_inventory,
pm.overstocked_units,
pm.overstocked_cost as excess_cost,
pm.overstocked_retail as excess_retail,
STRING_AGG(c.name, ', ') as categories
FROM products p
JOIN product_metrics pm ON p.pid = pm.pid
LEFT JOIN product_categories pc ON p.pid = pc.pid
FROM product_metrics pm
LEFT JOIN product_categories pc ON pm.pid = pc.pid
LEFT JOIN categories c ON pc.cat_id = c.cat_id
WHERE pm.stock_status = 'Overstocked'
GROUP BY p.pid, p.SKU, p.title, p.brand, p.vendor, p.stock_quantity, p.cost_price, p.price,
pm.daily_sales_avg, pm.days_of_inventory, pm.overstocked_amt
WHERE pm.status = 'Overstock'
GROUP BY pm.pid, pm.sku, pm.title, pm.brand, pm.vendor, pm.current_stock, pm.current_cost_price, pm.current_price,
pm.sales_velocity_daily, pm.stock_cover_in_days, pm.overstocked_units, pm.overstocked_cost, pm.overstocked_retail
ORDER BY excess_cost DESC
LIMIT $1
`, [limit]);
@@ -827,42 +769,38 @@ router.get('/sales/metrics', async (req, res) => {
const endDate = req.query.endDate || today.toISOString();
try {
// Get daily sales data
// Get daily orders and totals for the specified period
const { rows: dailyRows } = await executeQuery(`
SELECT
DATE(o.date) as sale_date,
COUNT(DISTINCT o.order_number) as total_orders,
SUM(o.quantity) as total_units,
SUM(o.price * o.quantity) as total_revenue,
SUM(p.cost_price * o.quantity) as total_cogs,
SUM((o.price - p.cost_price) * o.quantity) as total_profit
FROM orders o
JOIN products p ON o.pid = p.pid
WHERE o.canceled = false
AND o.date BETWEEN $1 AND $2
GROUP BY DATE(o.date)
DATE(date) as sale_date,
COUNT(DISTINCT order_number) as total_orders,
SUM(quantity) as total_units,
SUM(price * quantity) as total_revenue,
SUM(costeach * quantity) as total_cogs
FROM orders
WHERE date BETWEEN $1 AND $2
AND canceled = false
GROUP BY DATE(date)
ORDER BY sale_date
`, [startDate, endDate]);
// Get summary metrics
const { rows: metrics } = await executeQuery(`
// Get overall metrics for the period
const { rows: [metrics] } = await executeQuery(`
SELECT
COUNT(DISTINCT o.order_number) as total_orders,
SUM(o.quantity) as total_units,
SUM(o.price * o.quantity) as total_revenue,
SUM(p.cost_price * o.quantity) as total_cogs,
SUM((o.price - p.cost_price) * o.quantity) as total_profit
FROM orders o
JOIN products p ON o.pid = p.pid
WHERE o.canceled = false
AND o.date BETWEEN $1 AND $2
COUNT(DISTINCT order_number) as total_orders,
SUM(quantity) as total_units,
SUM(price * quantity) as total_revenue,
SUM(costeach * quantity) as total_cogs
FROM orders
WHERE date BETWEEN $1 AND $2
AND canceled = false
`, [startDate, endDate]);
const response = {
totalOrders: parseInt(metrics[0]?.total_orders) || 0,
totalUnitsSold: parseInt(metrics[0]?.total_units) || 0,
totalCogs: parseFloat(metrics[0]?.total_cogs) || 0,
totalRevenue: parseFloat(metrics[0]?.total_revenue) || 0,
totalOrders: parseInt(metrics?.total_orders) || 0,
totalUnitsSold: parseInt(metrics?.total_units) || 0,
totalCogs: parseFloat(metrics?.total_cogs) || 0,
totalRevenue: parseFloat(metrics?.total_revenue) || 0,
dailySales: dailyRows.map(day => ({
date: day.sale_date,
units: parseInt(day.total_units) || 0,
@@ -1304,39 +1242,33 @@ router.get('/inventory-health', async (req, res) => {
});
// GET /dashboard/replenish/products
// Returns top products that need replenishment
// Returns list of products to replenish
router.get('/replenish/products', async (req, res) => {
const limit = Math.max(1, Math.min(100, parseInt(req.query.limit) || 50));
const limit = parseInt(req.query.limit) || 50;
try {
const { rows: products } = await executeQuery(`
const { rows } = await executeQuery(`
SELECT
p.pid,
p.SKU as sku,
p.title,
p.stock_quantity,
pm.daily_sales_avg,
pm.reorder_qty,
pm.last_purchase_date
FROM products p
JOIN product_metrics pm ON p.pid = pm.pid
WHERE p.replenishable = true
AND pm.stock_status IN ('Critical', 'Reorder')
AND pm.reorder_qty > 0
pm.pid,
pm.sku,
pm.title,
pm.current_stock AS stock_quantity,
pm.sales_velocity_daily AS daily_sales_avg,
pm.replenishment_units AS reorder_qty,
pm.date_last_received AS last_purchase_date
FROM product_metrics pm
WHERE pm.is_replenishable = true
AND (pm.status IN ('Critical', 'Reorder')
OR pm.current_stock < 0)
AND pm.replenishment_units > 0
ORDER BY
CASE pm.stock_status
CASE pm.status
WHEN 'Critical' THEN 1
WHEN 'Reorder' THEN 2
END,
pm.reorder_qty * p.cost_price DESC
pm.replenishment_cost DESC
LIMIT $1
`, [limit]);
res.json(products.map(p => ({
...p,
stock_quantity: parseInt(p.stock_quantity) || 0,
daily_sales_avg: parseFloat(p.daily_sales_avg) || 0,
reorder_qty: parseInt(p.reorder_qty) || 0
})));
res.json(rows);
} catch (err) {
console.error('Error fetching products to replenish:', err);
res.status(500).json({ error: 'Failed to fetch products to replenish' });