Get frontend dashboard/analytics mostly loading data again
This commit is contained in:
@@ -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' });
|
||||
|
||||
Reference in New Issue
Block a user