Update backend/frontend

This commit is contained in:
2025-02-14 11:26:02 -05:00
parent 0ef1b6100e
commit cc22fd8c35
16 changed files with 552 additions and 480 deletions

View File

@@ -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,