Clean up inventory overview page
This commit is contained in:
@@ -61,16 +61,72 @@ BEGIN
|
||||
p.uom -- Assuming UOM logic is handled elsewhere or simple (e.g., 1=each)
|
||||
FROM public.products p
|
||||
),
|
||||
-- Stale POs: open, >90 days past expected, AND a newer PO exists for the same product.
|
||||
-- These are likely abandoned/superseded and should not consume receivings in FIFO.
|
||||
StalePOLines AS (
|
||||
SELECT po.po_id, po.pid
|
||||
FROM public.purchase_orders po
|
||||
WHERE po.status IN ('created', 'ordered', 'preordered', 'electronically_sent',
|
||||
'electronically_ready_send', 'receiving_started')
|
||||
AND po.expected_date IS NOT NULL
|
||||
AND po.expected_date < _current_date - INTERVAL '90 days'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.purchase_orders newer
|
||||
WHERE newer.pid = po.pid
|
||||
AND newer.status NOT IN ('canceled', 'done')
|
||||
AND COALESCE(newer.date_ordered, newer.date_created)
|
||||
> COALESCE(po.date_ordered, po.date_created)
|
||||
)
|
||||
),
|
||||
-- All non-canceled, non-stale POs in FIFO order per (pid, supplier).
|
||||
-- Includes closed ('done') POs so they consume receivings before open POs.
|
||||
POFifo AS (
|
||||
SELECT
|
||||
po.pid, po.supplier_id, po.po_id, po.ordered, po.status,
|
||||
po.po_cost_price, po.expected_date,
|
||||
SUM(po.ordered) OVER (
|
||||
PARTITION BY po.pid, po.supplier_id
|
||||
ORDER BY COALESCE(po.date_ordered, po.date_created), po.po_id
|
||||
) - po.ordered AS cumulative_before
|
||||
FROM public.purchase_orders po
|
||||
WHERE po.status != 'canceled'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM StalePOLines s
|
||||
WHERE s.po_id = po.po_id AND s.pid = po.pid
|
||||
)
|
||||
),
|
||||
-- Total received per (product, supplier) across all receivings.
|
||||
SupplierReceived AS (
|
||||
SELECT pid, supplier_id, SUM(qty_each) AS total_received
|
||||
FROM public.receivings
|
||||
WHERE status IN ('partial_received', 'full_received', 'paid')
|
||||
GROUP BY pid, supplier_id
|
||||
),
|
||||
-- FIFO allocation: receivings fill oldest POs first per (pid, supplier).
|
||||
-- Only open PO lines are reported; closed POs just absorb receivings.
|
||||
OnOrderInfo AS (
|
||||
SELECT
|
||||
pid,
|
||||
SUM(ordered) AS on_order_qty,
|
||||
SUM(ordered * po_cost_price) AS on_order_cost,
|
||||
MIN(expected_date) AS earliest_expected_date
|
||||
FROM public.purchase_orders
|
||||
WHERE status IN ('created', 'ordered', 'preordered', 'electronically_sent', 'electronically_ready_send', 'receiving_started')
|
||||
AND status NOT IN ('canceled', 'done')
|
||||
GROUP BY pid
|
||||
po.pid,
|
||||
SUM(GREATEST(0,
|
||||
po.ordered - GREATEST(0, LEAST(po.ordered,
|
||||
COALESCE(sr.total_received, 0) - po.cumulative_before
|
||||
))
|
||||
)) AS on_order_qty,
|
||||
SUM(GREATEST(0,
|
||||
po.ordered - GREATEST(0, LEAST(po.ordered,
|
||||
COALESCE(sr.total_received, 0) - po.cumulative_before
|
||||
))
|
||||
) * po.po_cost_price) AS on_order_cost,
|
||||
MIN(po.expected_date) FILTER (WHERE
|
||||
po.ordered > GREATEST(0, LEAST(po.ordered,
|
||||
COALESCE(sr.total_received, 0) - po.cumulative_before
|
||||
))
|
||||
) AS earliest_expected_date
|
||||
FROM POFifo po
|
||||
LEFT JOIN SupplierReceived sr ON sr.pid = po.pid AND sr.supplier_id = po.supplier_id
|
||||
WHERE po.status IN ('created', 'ordered', 'preordered', 'electronically_sent',
|
||||
'electronically_ready_send', 'receiving_started')
|
||||
GROUP BY po.pid
|
||||
),
|
||||
HistoricalDates AS (
|
||||
-- Note: Calculating these MIN/MAX values hourly can be slow on large tables.
|
||||
@@ -142,6 +198,17 @@ BEGIN
|
||||
FROM public.daily_product_snapshots
|
||||
GROUP BY pid
|
||||
),
|
||||
BeginningStock AS (
|
||||
-- Get stock level from 30 days ago for sell-through calculation.
|
||||
-- Uses the closest available snapshot if exact date is missing (activity-only snapshots).
|
||||
SELECT DISTINCT ON (pid)
|
||||
pid,
|
||||
eod_stock_quantity AS beginning_stock_30d
|
||||
FROM public.daily_product_snapshots
|
||||
WHERE snapshot_date <= _current_date - INTERVAL '30 days'
|
||||
AND snapshot_date >= _current_date - INTERVAL '37 days'
|
||||
ORDER BY pid, snapshot_date DESC
|
||||
),
|
||||
FirstPeriodMetrics AS (
|
||||
SELECT
|
||||
pid,
|
||||
@@ -403,10 +470,10 @@ BEGIN
|
||||
(sa.stockout_days_30d / 30.0) * 100 AS stockout_rate_30d,
|
||||
sa.gross_regular_revenue_30d - sa.gross_revenue_30d AS markdown_30d,
|
||||
((sa.gross_regular_revenue_30d - sa.gross_revenue_30d) / NULLIF(sa.gross_regular_revenue_30d, 0)) * 100 AS markdown_rate_30d,
|
||||
-- Fix sell-through rate: Industry standard is Units Sold / (Beginning Inventory + Units Received)
|
||||
-- Approximating beginning inventory as current stock + units sold - units received
|
||||
-- Sell-through rate: Industry standard is Units Sold / (Beginning Inventory + Units Received)
|
||||
-- Uses actual snapshot from 30 days ago as beginning stock, falls back to avg_stock_units_30d
|
||||
(sa.sales_30d / NULLIF(
|
||||
ci.current_stock + sa.sales_30d + sa.returns_units_30d - sa.received_qty_30d,
|
||||
COALESCE(bs.beginning_stock_30d, sa.avg_stock_units_30d::int, 0) + sa.received_qty_30d,
|
||||
0
|
||||
)) * 100 AS sell_through_30d,
|
||||
|
||||
@@ -555,6 +622,7 @@ BEGIN
|
||||
LEFT JOIN PreviousPeriodMetrics ppm ON ci.pid = ppm.pid
|
||||
LEFT JOIN DemandVariability dv ON ci.pid = dv.pid
|
||||
LEFT JOIN ServiceLevels sl ON ci.pid = sl.pid
|
||||
LEFT JOIN BeginningStock bs ON ci.pid = bs.pid
|
||||
LEFT JOIN SeasonalityAnalysis season ON ci.pid = season.pid
|
||||
WHERE s.exclude_forecast IS FALSE OR s.exclude_forecast IS NULL -- Exclude products explicitly marked
|
||||
|
||||
|
||||
@@ -171,30 +171,37 @@ router.get('/inventory-summary', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
|
||||
const { rows: [summary] } = await pool.query(`
|
||||
SELECT
|
||||
SUM(current_stock_cost) AS stock_investment,
|
||||
SUM(on_order_cost) AS on_order_value,
|
||||
CASE
|
||||
WHEN SUM(avg_stock_cost_30d) > 0
|
||||
THEN (SUM(cogs_30d) / SUM(avg_stock_cost_30d)) * 12
|
||||
ELSE 0
|
||||
END AS inventory_turns_annualized,
|
||||
CASE
|
||||
WHEN SUM(avg_stock_cost_30d) > 0
|
||||
THEN (SUM(profit_30d) / SUM(avg_stock_cost_30d)) * 12
|
||||
ELSE 0
|
||||
END AS gmroi,
|
||||
CASE
|
||||
WHEN SUM(CASE WHEN sales_velocity_daily > 0 THEN 1 ELSE 0 END) > 0
|
||||
THEN SUM(CASE WHEN sales_velocity_daily > 0 THEN stock_cover_in_days ELSE 0 END)
|
||||
/ SUM(CASE WHEN sales_velocity_daily > 0 THEN 1 ELSE 0 END)
|
||||
ELSE 0
|
||||
END AS avg_stock_cover_days,
|
||||
COUNT(*) FILTER (WHERE current_stock > 0) AS products_in_stock,
|
||||
COUNT(*) FILTER (WHERE is_old_stock = true) AS dead_stock_products,
|
||||
SUM(CASE WHEN is_old_stock = true THEN current_stock_cost ELSE 0 END) AS dead_stock_value
|
||||
FROM product_metrics
|
||||
WHERE is_visible = true
|
||||
WITH agg AS (
|
||||
SELECT
|
||||
SUM(current_stock_cost) AS stock_investment,
|
||||
SUM(on_order_cost) AS on_order_value,
|
||||
CASE
|
||||
WHEN SUM(avg_stock_cost_30d) > 0
|
||||
THEN (SUM(cogs_30d) / SUM(avg_stock_cost_30d)) * 12
|
||||
ELSE 0
|
||||
END AS inventory_turns_annualized,
|
||||
CASE
|
||||
WHEN SUM(avg_stock_cost_30d) > 0
|
||||
THEN (SUM(profit_30d) / SUM(avg_stock_cost_30d)) * 12
|
||||
ELSE 0
|
||||
END AS gmroi,
|
||||
COUNT(*) FILTER (WHERE current_stock > 0) AS products_in_stock,
|
||||
COUNT(*) FILTER (WHERE is_old_stock = true AND current_stock > 0) AS dead_stock_products,
|
||||
SUM(CASE WHEN is_old_stock = true AND current_stock > 0 THEN current_stock_cost ELSE 0 END) AS dead_stock_value
|
||||
FROM product_metrics
|
||||
WHERE is_visible = true
|
||||
),
|
||||
cover AS (
|
||||
SELECT
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY stock_cover_in_days) AS median_stock_cover_days
|
||||
FROM product_metrics
|
||||
WHERE is_visible = true
|
||||
AND current_stock > 0
|
||||
AND sales_velocity_daily > 0
|
||||
AND stock_cover_in_days IS NOT NULL
|
||||
)
|
||||
SELECT agg.*, cover.median_stock_cover_days
|
||||
FROM agg, cover
|
||||
`);
|
||||
|
||||
res.json({
|
||||
@@ -202,7 +209,7 @@ router.get('/inventory-summary', async (req, res) => {
|
||||
onOrderValue: Number(summary.on_order_value) || 0,
|
||||
inventoryTurns: Number(summary.inventory_turns_annualized) || 0,
|
||||
gmroi: Number(summary.gmroi) || 0,
|
||||
avgStockCoverDays: Number(summary.avg_stock_cover_days) || 0,
|
||||
avgStockCoverDays: Number(summary.median_stock_cover_days) || 0,
|
||||
productsInStock: Number(summary.products_in_stock) || 0,
|
||||
deadStockProducts: Number(summary.dead_stock_products) || 0,
|
||||
deadStockValue: Number(summary.dead_stock_value) || 0,
|
||||
@@ -266,9 +273,9 @@ router.get('/portfolio', async (req, res) => {
|
||||
// Dead stock and overstock summary
|
||||
const { rows: [stockIssues] } = await pool.query(`
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE is_old_stock = true) AS dead_stock_count,
|
||||
SUM(CASE WHEN is_old_stock = true THEN current_stock_cost ELSE 0 END) AS dead_stock_cost,
|
||||
SUM(CASE WHEN is_old_stock = true THEN current_stock_retail ELSE 0 END) AS dead_stock_retail,
|
||||
COUNT(*) FILTER (WHERE is_old_stock = true AND current_stock > 0) AS dead_stock_count,
|
||||
SUM(CASE WHEN is_old_stock = true AND current_stock > 0 THEN current_stock_cost ELSE 0 END) AS dead_stock_cost,
|
||||
SUM(CASE WHEN is_old_stock = true AND current_stock > 0 THEN current_stock_retail ELSE 0 END) AS dead_stock_retail,
|
||||
COUNT(*) FILTER (WHERE overstocked_units > 0) AS overstock_count,
|
||||
SUM(COALESCE(overstocked_cost, 0)) AS overstock_cost,
|
||||
SUM(COALESCE(overstocked_retail, 0)) AS overstock_retail
|
||||
@@ -300,14 +307,14 @@ router.get('/portfolio', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Capital efficiency — GMROI by vendor (single combined query)
|
||||
// Capital efficiency — GMROI by brand (single combined query)
|
||||
router.get('/efficiency', async (req, res) => {
|
||||
try {
|
||||
const pool = req.app.locals.pool;
|
||||
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
vendor AS vendor_name,
|
||||
COALESCE(brand, 'Unbranded') AS brand_name,
|
||||
COUNT(*) AS product_count,
|
||||
SUM(current_stock_cost) AS stock_cost,
|
||||
SUM(profit_30d) AS profit_30d,
|
||||
@@ -319,17 +326,17 @@ router.get('/efficiency', async (req, res) => {
|
||||
END AS gmroi
|
||||
FROM product_metrics
|
||||
WHERE is_visible = true
|
||||
AND vendor IS NOT NULL
|
||||
AND brand IS NOT NULL
|
||||
AND current_stock_cost > 0
|
||||
GROUP BY vendor
|
||||
GROUP BY brand
|
||||
HAVING SUM(current_stock_cost) > 100
|
||||
ORDER BY SUM(current_stock_cost) DESC
|
||||
LIMIT 30
|
||||
`);
|
||||
|
||||
res.json({
|
||||
vendors: rows.map(r => ({
|
||||
vendor: r.vendor_name,
|
||||
brands: rows.map(r => ({
|
||||
brand: r.brand_name,
|
||||
productCount: Number(r.product_count) || 0,
|
||||
stockCost: Number(r.stock_cost) || 0,
|
||||
profit30d: Number(r.profit_30d) || 0,
|
||||
@@ -527,7 +534,7 @@ router.get('/stockout-risk', async (req, res) => {
|
||||
const { rows } = await pool.query(`
|
||||
WITH base AS (
|
||||
SELECT
|
||||
title, sku, vendor,
|
||||
title, sku, brand,
|
||||
${leadTimeSql} AS lead_time_days,
|
||||
sells_out_in_days, current_stock, sales_velocity_daily,
|
||||
revenue_30d, abc_class
|
||||
@@ -554,7 +561,7 @@ router.get('/stockout-risk', async (req, res) => {
|
||||
products: rows.map(r => ({
|
||||
title: r.title,
|
||||
sku: r.sku,
|
||||
vendor: r.vendor,
|
||||
brand: r.brand,
|
||||
leadTimeDays: Number(r.lead_time_days) || 0,
|
||||
sellsOutInDays: Number(r.sells_out_in_days) || 0,
|
||||
currentStock: Number(r.current_stock) || 0,
|
||||
@@ -624,6 +631,7 @@ router.get('/growth', async (req, res) => {
|
||||
try {
|
||||
const pool = req.app.locals.pool;
|
||||
|
||||
// ABC breakdown — only "comparable" products (sold in BOTH periods, i.e. growth != -100%)
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
COALESCE(abc_class, 'N/A') AS abc_class,
|
||||
@@ -645,21 +653,39 @@ router.get('/growth', async (req, res) => {
|
||||
FROM product_metrics
|
||||
WHERE is_visible = true
|
||||
AND sales_growth_yoy IS NOT NULL
|
||||
AND sales_30d > 0
|
||||
GROUP BY 1, 2, 3
|
||||
ORDER BY abc_class, sort_order
|
||||
`);
|
||||
|
||||
// Summary stats
|
||||
// Summary: comparable products (sold in both periods) with revenue-weighted avg
|
||||
const { rows: [summary] } = await pool.query(`
|
||||
SELECT
|
||||
COUNT(*) AS total_with_yoy,
|
||||
COUNT(*) AS comparable_count,
|
||||
COUNT(*) FILTER (WHERE sales_growth_yoy > 0) AS growing_count,
|
||||
COUNT(*) FILTER (WHERE sales_growth_yoy <= 0) AS declining_count,
|
||||
ROUND(AVG(sales_growth_yoy)::numeric, 1) AS avg_growth,
|
||||
ROUND(
|
||||
CASE WHEN SUM(revenue_30d) > 0
|
||||
THEN SUM(sales_growth_yoy * revenue_30d) / SUM(revenue_30d)
|
||||
ELSE 0
|
||||
END::numeric, 1
|
||||
) AS weighted_avg_growth,
|
||||
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY sales_growth_yoy)::numeric, 1) AS median_growth
|
||||
FROM product_metrics
|
||||
WHERE is_visible = true
|
||||
AND sales_growth_yoy IS NOT NULL
|
||||
AND sales_30d > 0
|
||||
`);
|
||||
|
||||
// Catalog turnover: new products (selling now, no sales last year) and discontinued (sold last year, not now)
|
||||
const { rows: [turnover] } = await pool.query(`
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE sales_growth_yoy IS NULL AND sales_30d > 0 AND age_days < 365) AS new_products,
|
||||
SUM(revenue_30d) FILTER (WHERE sales_growth_yoy IS NULL AND sales_30d > 0 AND age_days < 365) AS new_product_revenue,
|
||||
COUNT(*) FILTER (WHERE sales_growth_yoy = -100) AS discontinued,
|
||||
SUM(current_stock_cost) FILTER (WHERE sales_growth_yoy = -100 AND current_stock > 0) AS discontinued_stock_value
|
||||
FROM product_metrics
|
||||
WHERE is_visible = true
|
||||
`);
|
||||
|
||||
res.json({
|
||||
@@ -671,12 +697,18 @@ router.get('/growth', async (req, res) => {
|
||||
stockCost: Number(r.stock_cost) || 0,
|
||||
})),
|
||||
summary: {
|
||||
totalWithYoy: Number(summary.total_with_yoy) || 0,
|
||||
comparableCount: Number(summary.comparable_count) || 0,
|
||||
growingCount: Number(summary.growing_count) || 0,
|
||||
decliningCount: Number(summary.declining_count) || 0,
|
||||
avgGrowth: Number(summary.avg_growth) || 0,
|
||||
weightedAvgGrowth: Number(summary.weighted_avg_growth) || 0,
|
||||
medianGrowth: Number(summary.median_growth) || 0,
|
||||
},
|
||||
turnover: {
|
||||
newProducts: Number(turnover.new_products) || 0,
|
||||
newProductRevenue: Number(turnover.new_product_revenue) || 0,
|
||||
discontinued: Number(turnover.discontinued) || 0,
|
||||
discontinuedStockValue: Number(turnover.discontinued_stock_value) || 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching growth data:', error);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1190,39 +1190,68 @@ router.get('/pipeline', async (req, res) => {
|
||||
try {
|
||||
const pool = req.app.locals.pool;
|
||||
|
||||
// Expected arrivals by week (ordered + electronically_sent with expected_date)
|
||||
// Stale PO filter (reused across queries)
|
||||
const staleFilter = `
|
||||
WITH stale AS (
|
||||
SELECT po_id, pid
|
||||
FROM purchase_orders po
|
||||
WHERE po.status IN ('created', 'ordered', 'preordered', 'electronically_sent',
|
||||
'electronically_ready_send', 'receiving_started')
|
||||
AND po.expected_date IS NOT NULL
|
||||
AND po.expected_date < CURRENT_DATE - INTERVAL '90 days'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM purchase_orders newer
|
||||
WHERE newer.pid = po.pid
|
||||
AND newer.status NOT IN ('canceled', 'done')
|
||||
AND COALESCE(newer.date_ordered, newer.date_created)
|
||||
> COALESCE(po.date_ordered, po.date_created)
|
||||
)
|
||||
)`;
|
||||
|
||||
// Expected arrivals by week (excludes stale POs)
|
||||
const { rows: arrivals } = await pool.query(`
|
||||
${staleFilter}
|
||||
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
|
||||
DATE_TRUNC('week', po.expected_date)::date AS week,
|
||||
COUNT(DISTINCT po.po_id) AS po_count,
|
||||
ROUND(SUM(po.po_cost_price * po.ordered)::numeric, 0) AS expected_value,
|
||||
COUNT(DISTINCT po.vendor) AS vendor_count
|
||||
FROM purchase_orders po
|
||||
WHERE po.status IN ('ordered', 'electronically_sent')
|
||||
AND po.expected_date IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM stale s WHERE s.po_id = po.po_id AND s.pid = po.pid)
|
||||
GROUP BY 1
|
||||
ORDER BY 1
|
||||
`);
|
||||
|
||||
// Overdue POs (expected_date in the past)
|
||||
// Overdue POs (excludes stale)
|
||||
const { rows: [overdue] } = await pool.query(`
|
||||
${staleFilter}
|
||||
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
|
||||
COUNT(DISTINCT po.po_id) AS po_count,
|
||||
ROUND(COALESCE(SUM(po.po_cost_price * po.ordered), 0)::numeric, 0) AS total_value
|
||||
FROM purchase_orders po
|
||||
WHERE po.status IN ('ordered', 'electronically_sent')
|
||||
AND po.expected_date IS NOT NULL
|
||||
AND po.expected_date < CURRENT_DATE
|
||||
AND NOT EXISTS (SELECT 1 FROM stale s WHERE s.po_id = po.po_id AND s.pid = po.pid)
|
||||
`);
|
||||
|
||||
// Summary: all open POs
|
||||
// Summary: on-order value from product_metrics (FIFO-accurate), PO counts from purchase_orders with staleness filter
|
||||
const { rows: [summary] } = await pool.query(`
|
||||
${staleFilter}
|
||||
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')
|
||||
COUNT(DISTINCT po.po_id) AS total_open_pos,
|
||||
COUNT(DISTINCT po.vendor) AS vendor_count
|
||||
FROM purchase_orders po
|
||||
WHERE po.status IN ('ordered', 'electronically_sent')
|
||||
AND NOT EXISTS (SELECT 1 FROM stale s WHERE s.po_id = po.po_id AND s.pid = po.pid)
|
||||
`);
|
||||
|
||||
const { rows: [onOrderTotal] } = await pool.query(`
|
||||
SELECT ROUND(COALESCE(SUM(on_order_cost), 0)::numeric, 0) AS total_on_order_value
|
||||
FROM product_metrics
|
||||
WHERE is_visible = true
|
||||
`);
|
||||
|
||||
res.json({
|
||||
@@ -1238,7 +1267,7 @@ router.get('/pipeline', async (req, res) => {
|
||||
},
|
||||
summary: {
|
||||
totalOpenPOs: Number(summary.total_open_pos) || 0,
|
||||
totalOnOrderValue: Number(summary.total_on_order_value) || 0,
|
||||
totalOnOrderValue: Number(onOrderTotal.total_on_order_value) || 0,
|
||||
vendorCount: Number(summary.vendor_count) || 0,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user