Updates for new analytics page + add pipeline chart to PO page

This commit is contained in:
2026-02-09 12:32:13 -05:00
parent 38b12c188f
commit 6834a77a80
10 changed files with 1004 additions and 22 deletions

View File

@@ -181,7 +181,7 @@ router.get('/inventory-summary', async (req, res) => {
END AS inventory_turns_annualized,
CASE
WHEN SUM(avg_stock_cost_30d) > 0
THEN SUM(profit_30d) / SUM(avg_stock_cost_30d)
THEN (SUM(profit_30d) / SUM(avg_stock_cost_30d)) * 12
ELSE 0
END AS gmroi,
CASE
@@ -314,7 +314,7 @@ router.get('/efficiency', async (req, res) => {
SUM(revenue_30d) AS revenue_30d,
CASE
WHEN SUM(avg_stock_cost_30d) > 0
THEN SUM(profit_30d) / SUM(avg_stock_cost_30d)
THEN (SUM(profit_30d) / SUM(avg_stock_cost_30d)) * 12
ELSE 0
END AS gmroi
FROM product_metrics
@@ -684,4 +684,126 @@ router.get('/growth', async (req, res) => {
}
});
// Inventory value over time (uses stock_snapshots — full product coverage)
router.get('/inventory-value', async (req, res) => {
try {
const pool = req.app.locals.pool;
const period = parseInt(req.query.period) || 90;
const validPeriods = [30, 90, 365];
const days = validPeriods.includes(period) ? period : 90;
const { rows } = await pool.query(`
SELECT
snapshot_date AS date,
ROUND(SUM(stock_value)::numeric, 0) AS total_value,
COUNT(DISTINCT pid) AS product_count
FROM stock_snapshots
WHERE snapshot_date >= CURRENT_DATE - make_interval(days => $1)
GROUP BY snapshot_date
ORDER BY snapshot_date
`, [days]);
res.json(rows.map(r => ({
date: r.date,
totalValue: Number(r.total_value) || 0,
productCount: Number(r.product_count) || 0,
})));
} catch (error) {
console.error('Error fetching inventory value:', error);
res.status(500).json({ error: 'Failed to fetch inventory value' });
}
});
// Inventory flow: receiving vs selling per day
router.get('/flow', async (req, res) => {
try {
const pool = req.app.locals.pool;
const period = parseInt(req.query.period) || 30;
const validPeriods = [30, 90];
const days = validPeriods.includes(period) ? period : 30;
const { rows } = await pool.query(`
SELECT
snapshot_date AS date,
COALESCE(SUM(units_received), 0) AS units_received,
ROUND(COALESCE(SUM(cost_received), 0)::numeric, 0) AS cost_received,
COALESCE(SUM(units_sold), 0) AS units_sold,
ROUND(COALESCE(SUM(cogs), 0)::numeric, 0) AS cogs_sold
FROM daily_product_snapshots
WHERE snapshot_date >= CURRENT_DATE - make_interval(days => $1)
GROUP BY snapshot_date
ORDER BY snapshot_date
`, [days]);
res.json(rows.map(r => ({
date: r.date,
unitsReceived: Number(r.units_received) || 0,
costReceived: Number(r.cost_received) || 0,
unitsSold: Number(r.units_sold) || 0,
cogsSold: Number(r.cogs_sold) || 0,
})));
} catch (error) {
console.error('Error fetching inventory flow:', error);
res.status(500).json({ error: 'Failed to fetch inventory flow' });
}
});
// Seasonal pattern distribution
router.get('/seasonal', async (req, res) => {
try {
const pool = req.app.locals.pool;
const { rows: patterns } = await pool.query(`
SELECT
COALESCE(seasonal_pattern, 'unknown') AS pattern,
COUNT(*) AS product_count,
SUM(current_stock_cost) AS stock_cost,
SUM(revenue_30d) AS revenue
FROM product_metrics
WHERE is_visible = true
AND current_stock > 0
GROUP BY seasonal_pattern
ORDER BY COUNT(*) DESC
`);
const { rows: peakSeasons } = await pool.query(`
SELECT
peak_season AS month,
COUNT(*) AS product_count,
SUM(current_stock_cost) AS stock_cost
FROM product_metrics
WHERE is_visible = true
AND current_stock > 0
AND seasonal_pattern IN ('moderate', 'strong')
AND peak_season IS NOT NULL
GROUP BY peak_season
ORDER BY
CASE peak_season
WHEN 'January' THEN 1 WHEN 'February' THEN 2 WHEN 'March' THEN 3
WHEN 'April' THEN 4 WHEN 'May' THEN 5 WHEN 'June' THEN 6
WHEN 'July' THEN 7 WHEN 'August' THEN 8 WHEN 'September' THEN 9
WHEN 'October' THEN 10 WHEN 'November' THEN 11 WHEN 'December' THEN 12
ELSE 13
END
`);
res.json({
patterns: patterns.map(r => ({
pattern: r.pattern,
productCount: Number(r.product_count) || 0,
stockCost: Number(r.stock_cost) || 0,
revenue: Number(r.revenue) || 0,
})),
peakSeasons: peakSeasons.map(r => ({
month: r.month,
productCount: Number(r.product_count) || 0,
stockCost: Number(r.stock_cost) || 0,
})),
});
} catch (error) {
console.error('Error fetching seasonal data:', error);
res.status(500).json({ error: 'Failed to fetch seasonal data' });
}
});
module.exports = router;

View File

@@ -1185,4 +1185,67 @@ router.get('/delivery-metrics', async (req, res) => {
}
});
module.exports = router;
// PO Pipeline — expected arrivals timeline + overdue summary
router.get('/pipeline', async (req, res) => {
try {
const pool = req.app.locals.pool;
// Expected arrivals by week (ordered + electronically_sent with expected_date)
const { rows: arrivals } = await pool.query(`
SELECT
DATE_TRUNC('week', expected_date)::date AS week,
COUNT(DISTINCT po_id) AS po_count,
ROUND(SUM(po_cost_price * ordered)::numeric, 0) AS expected_value,
COUNT(DISTINCT vendor) AS vendor_count
FROM purchase_orders
WHERE status IN ('ordered', 'electronically_sent')
AND expected_date IS NOT NULL
GROUP BY 1
ORDER BY 1
`);
// Overdue POs (expected_date in the past)
const { rows: [overdue] } = await pool.query(`
SELECT
COUNT(DISTINCT po_id) AS po_count,
ROUND(COALESCE(SUM(po_cost_price * ordered), 0)::numeric, 0) AS total_value
FROM purchase_orders
WHERE status IN ('ordered', 'electronically_sent')
AND expected_date IS NOT NULL
AND expected_date < CURRENT_DATE
`);
// Summary: all open POs
const { rows: [summary] } = await pool.query(`
SELECT
COUNT(DISTINCT po_id) AS total_open_pos,
ROUND(COALESCE(SUM(po_cost_price * ordered), 0)::numeric, 0) AS total_on_order_value,
COUNT(DISTINCT vendor) AS vendor_count
FROM purchase_orders
WHERE status IN ('ordered', 'electronically_sent')
`);
res.json({
arrivals: arrivals.map(r => ({
week: r.week,
poCount: Number(r.po_count) || 0,
expectedValue: Number(r.expected_value) || 0,
vendorCount: Number(r.vendor_count) || 0,
})),
overdue: {
count: Number(overdue.po_count) || 0,
value: Number(overdue.total_value) || 0,
},
summary: {
totalOpenPOs: Number(summary.total_open_pos) || 0,
totalOnOrderValue: Number(summary.total_on_order_value) || 0,
vendorCount: Number(summary.vendor_count) || 0,
},
});
} catch (error) {
console.error('Error fetching PO pipeline:', error);
res.status(500).json({ error: 'Failed to fetch PO pipeline' });
}
});
module.exports = router;