Add product lines page, tweak audit log
This commit is contained in:
381
inventory-server/src/routes/linesAggregate.js
Normal file
381
inventory-server/src/routes/linesAggregate.js
Normal file
@@ -0,0 +1,381 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { parseValue } = require('../utils/apiHelpers');
|
||||
|
||||
// --- Configuration & Helpers ---
|
||||
const DEFAULT_PAGE_LIMIT = 50;
|
||||
const MAX_PAGE_LIMIT = 200;
|
||||
|
||||
// Base aggregation query that produces line-level metrics from product_metrics
|
||||
// Filters to lines with stock or sales in the past year
|
||||
const LINE_AGGREGATE_SQL = `
|
||||
SELECT
|
||||
pm.brand,
|
||||
pm.line,
|
||||
COUNT(*) AS product_count,
|
||||
COUNT(CASE WHEN pm.is_visible THEN 1 END) AS active_product_count,
|
||||
COUNT(CASE WHEN pm.is_replenishable THEN 1 END) AS replenishable_product_count,
|
||||
-- Stock
|
||||
SUM(COALESCE(pm.current_stock, 0)) AS current_stock_units,
|
||||
SUM(COALESCE(pm.current_stock_cost, 0)) AS current_stock_cost,
|
||||
SUM(COALESCE(pm.current_stock_retail, 0)) AS current_stock_retail,
|
||||
SUM(COALESCE(pm.on_order_qty, 0)) AS on_order_qty,
|
||||
SUM(COALESCE(pm.on_order_cost, 0)) AS on_order_cost,
|
||||
-- Sales periods
|
||||
SUM(COALESCE(pm.sales_7d, 0)) AS sales_7d,
|
||||
SUM(COALESCE(pm.revenue_7d, 0)) AS revenue_7d,
|
||||
SUM(COALESCE(pm.sales_30d, 0)) AS sales_30d,
|
||||
SUM(COALESCE(pm.revenue_30d, 0)) AS revenue_30d,
|
||||
SUM(COALESCE(pm.profit_30d, 0)) AS profit_30d,
|
||||
SUM(COALESCE(pm.cogs_30d, 0)) AS cogs_30d,
|
||||
SUM(COALESCE(pm.sales_365d, 0)) AS sales_365d,
|
||||
SUM(COALESCE(pm.revenue_365d, 0)) AS revenue_365d,
|
||||
SUM(COALESCE(pm.lifetime_sales, 0)) AS lifetime_sales,
|
||||
SUM(COALESCE(pm.lifetime_revenue, 0)) AS lifetime_revenue,
|
||||
-- Weighted average margin (by revenue)
|
||||
CASE WHEN SUM(pm.revenue_30d) > 0
|
||||
THEN SUM(pm.profit_30d) * 100.0 / SUM(pm.revenue_30d)
|
||||
ELSE NULL
|
||||
END AS avg_margin_30d,
|
||||
-- Velocity & coverage
|
||||
SUM(COALESCE(pm.sales_velocity_daily, 0)) AS total_velocity_daily,
|
||||
AVG(CASE WHEN pm.current_stock > 0 AND pm.sales_velocity_daily > 0
|
||||
THEN pm.stock_cover_in_days ELSE NULL END) AS avg_stock_cover_days,
|
||||
AVG(CASE WHEN pm.sells_out_in_days IS NOT NULL AND pm.current_stock > 0
|
||||
THEN pm.sells_out_in_days ELSE NULL END) AS avg_sells_out_in_days,
|
||||
-- Stockouts & replenishment
|
||||
SUM(COALESCE(pm.stockout_days_30d, 0)) AS total_stockout_days_30d,
|
||||
SUM(COALESCE(pm.replenishment_units, 0)) AS total_replenishment_units,
|
||||
SUM(COALESCE(pm.replenishment_cost, 0)) AS total_replenishment_cost,
|
||||
-- Lifecycle distribution (counts per phase)
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'launch' THEN 1 END) AS phase_launch,
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'mature' THEN 1 END) AS phase_mature,
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'slow_mover' THEN 1 END) AS phase_slow_mover,
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'decay' THEN 1 END) AS phase_decay,
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'dormant' THEN 1 END) AS phase_dormant,
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'preorder' THEN 1 END) AS phase_preorder,
|
||||
-- Status distribution (exclude preorder-phase products — they get their own segment)
|
||||
COUNT(CASE WHEN pm.status = 'Healthy' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_healthy,
|
||||
COUNT(CASE WHEN pm.status = 'Reorder Soon' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_reorder,
|
||||
COUNT(CASE WHEN pm.status = 'Critical' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_critical,
|
||||
COUNT(CASE WHEN pm.status = 'Overstock' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_overstock,
|
||||
COUNT(CASE WHEN pm.status = 'At Risk' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_at_risk,
|
||||
COUNT(CASE WHEN pm.status = 'New' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_new,
|
||||
-- ABC class distribution
|
||||
COUNT(CASE WHEN pm.abc_class = 'A' THEN 1 END) AS abc_a_count,
|
||||
COUNT(CASE WHEN pm.abc_class = 'B' THEN 1 END) AS abc_b_count,
|
||||
COUNT(CASE WHEN pm.abc_class = 'C' THEN 1 END) AS abc_c_count,
|
||||
-- Date range
|
||||
MIN(pm.date_first_received) AS earliest_received,
|
||||
MAX(pm.date_last_sold) AS latest_sale,
|
||||
-- Growth (weighted by revenue)
|
||||
CASE WHEN SUM(CASE WHEN pm.sales_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_30d ELSE 0 END) > 0
|
||||
THEN SUM(CASE WHEN pm.sales_growth_30d_vs_prev IS NOT NULL THEN pm.sales_growth_30d_vs_prev * pm.revenue_30d ELSE 0 END)
|
||||
/ SUM(CASE WHEN pm.sales_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_30d ELSE 0 END)
|
||||
ELSE NULL
|
||||
END AS sales_growth_30d_vs_prev,
|
||||
CASE WHEN SUM(CASE WHEN pm.revenue_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_30d ELSE 0 END) > 0
|
||||
THEN SUM(CASE WHEN pm.revenue_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_growth_30d_vs_prev * pm.revenue_30d ELSE 0 END)
|
||||
/ SUM(CASE WHEN pm.revenue_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_30d ELSE 0 END)
|
||||
ELSE NULL
|
||||
END AS revenue_growth_30d_vs_prev,
|
||||
-- Derived line status
|
||||
CASE
|
||||
WHEN COUNT(CASE WHEN pm.lifecycle_phase = 'preorder' THEN 1 END) > COUNT(*) * 0.5 THEN 'preorder'
|
||||
WHEN SUM(CASE WHEN pm.current_stock > 0 THEN 1 ELSE 0 END) = 0 THEN 'out_of_stock'
|
||||
WHEN SUM(pm.sales_30d) > 0 THEN 'active'
|
||||
WHEN SUM(pm.sales_365d) > 0 THEN 'slow'
|
||||
ELSE 'dormant'
|
||||
END AS line_status,
|
||||
-- Dominant lifecycle phase
|
||||
(ARRAY['launch','mature','slow_mover','decay','dormant','preorder'])[
|
||||
(SELECT i FROM unnest(ARRAY[
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'launch' THEN 1 END),
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'mature' THEN 1 END),
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'slow_mover' THEN 1 END),
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'decay' THEN 1 END),
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'dormant' THEN 1 END),
|
||||
COUNT(CASE WHEN pm.lifecycle_phase = 'preorder' THEN 1 END)
|
||||
]) WITH ORDINALITY AS t(cnt, i)
|
||||
ORDER BY cnt DESC LIMIT 1)
|
||||
] AS dominant_lifecycle_phase
|
||||
FROM product_metrics pm
|
||||
WHERE pm.line IS NOT NULL AND pm.line != ''
|
||||
AND (pm.current_stock > 0 OR pm.sales_365d > 0)
|
||||
GROUP BY pm.brand, pm.line
|
||||
`;
|
||||
|
||||
// Column map for sorting/filtering on the aggregated result
|
||||
const COLUMN_MAP = {
|
||||
brand: { dbCol: 'agg.brand', type: 'string' },
|
||||
line: { dbCol: 'agg.line', type: 'string' },
|
||||
productCount: { dbCol: 'agg.product_count', type: 'number' },
|
||||
activeProductCount: { dbCol: 'agg.active_product_count', type: 'number' },
|
||||
currentStockUnits: { dbCol: 'agg.current_stock_units', type: 'number' },
|
||||
currentStockCost: { dbCol: 'agg.current_stock_cost', type: 'number' },
|
||||
currentStockRetail: { dbCol: 'agg.current_stock_retail', type: 'number' },
|
||||
onOrderQty: { dbCol: 'agg.on_order_qty', type: 'number' },
|
||||
sales7d: { dbCol: 'agg.sales_7d', type: 'number' },
|
||||
revenue7d: { dbCol: 'agg.revenue_7d', type: 'number' },
|
||||
sales30d: { dbCol: 'agg.sales_30d', type: 'number' },
|
||||
revenue30d: { dbCol: 'agg.revenue_30d', type: 'number' },
|
||||
profit30d: { dbCol: 'agg.profit_30d', type: 'number' },
|
||||
sales365d: { dbCol: 'agg.sales_365d', type: 'number' },
|
||||
revenue365d: { dbCol: 'agg.revenue_365d', type: 'number' },
|
||||
lifetimeSales: { dbCol: 'agg.lifetime_sales', type: 'number' },
|
||||
lifetimeRevenue: { dbCol: 'agg.lifetime_revenue', type: 'number' },
|
||||
avgMargin30d: { dbCol: 'agg.avg_margin_30d', type: 'number' },
|
||||
totalVelocityDaily: { dbCol: 'agg.total_velocity_daily', type: 'number' },
|
||||
avgStockCoverDays: { dbCol: 'agg.avg_stock_cover_days', type: 'number' },
|
||||
avgSellsOutInDays: { dbCol: 'agg.avg_sells_out_in_days', type: 'number' },
|
||||
salesGrowth30dVsPrev: { dbCol: 'agg.sales_growth_30d_vs_prev', type: 'number' },
|
||||
revenueGrowth30dVsPrev: { dbCol: 'agg.revenue_growth_30d_vs_prev', type: 'number' },
|
||||
lineStatus: { dbCol: 'agg.line_status', type: 'string' },
|
||||
dominantLifecyclePhase: { dbCol: 'agg.dominant_lifecycle_phase', type: 'string' },
|
||||
name: { dbCol: 'agg.line', type: 'string' },
|
||||
};
|
||||
|
||||
function getSafeColumnInfo(queryParamKey) {
|
||||
return COLUMN_MAP[queryParamKey] || null;
|
||||
}
|
||||
|
||||
// --- Route Handlers ---
|
||||
|
||||
// GET /lines-aggregate/filter-options
|
||||
router.get('/filter-options', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
try {
|
||||
const { rows: brandRows } = await pool.query(`
|
||||
SELECT DISTINCT pm.brand
|
||||
FROM product_metrics pm
|
||||
WHERE pm.line IS NOT NULL AND pm.line != ''
|
||||
AND (pm.current_stock > 0 OR pm.sales_365d > 0)
|
||||
AND pm.brand IS NOT NULL AND pm.brand != ''
|
||||
ORDER BY pm.brand
|
||||
`);
|
||||
|
||||
const statuses = ['active', 'preorder', 'slow', 'out_of_stock', 'dormant'];
|
||||
const phases = ['launch', 'mature', 'slow_mover', 'decay', 'dormant', 'preorder'];
|
||||
|
||||
res.json({
|
||||
brands: brandRows.map(r => r.brand),
|
||||
statuses,
|
||||
phases,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching line filter options:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch filter options' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /lines-aggregate/stats
|
||||
router.get('/stats', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
try {
|
||||
const { rows: [stats] } = await pool.query(`
|
||||
SELECT
|
||||
COUNT(*) AS total_lines,
|
||||
COUNT(DISTINCT brand) AS brand_count,
|
||||
COUNT(CASE WHEN line_status = 'active' THEN 1 END) AS active_lines,
|
||||
COUNT(CASE WHEN line_status = 'out_of_stock' THEN 1 END) AS oos_lines,
|
||||
COUNT(CASE WHEN line_status = 'active' AND replenishable_product_count > 0 AND status_reorder > replenishable_product_count * 0.2 THEN 1 END) AS lines_needing_restock,
|
||||
ROUND(AVG(product_count), 1) AS avg_products_per_line,
|
||||
SUM(revenue_30d) AS total_revenue_30d,
|
||||
CASE WHEN SUM(revenue_30d) > 0
|
||||
THEN SUM(profit_30d) * 100.0 / SUM(revenue_30d)
|
||||
ELSE 0
|
||||
END AS overall_avg_margin
|
||||
FROM (${LINE_AGGREGATE_SQL}) agg
|
||||
`);
|
||||
|
||||
res.json({
|
||||
totalLines: parseInt(stats?.total_lines || 0),
|
||||
brandCount: parseInt(stats?.brand_count || 0),
|
||||
activeLines: parseInt(stats?.active_lines || 0),
|
||||
oosLines: parseInt(stats?.oos_lines || 0),
|
||||
linesNeedingRestock: parseInt(stats?.lines_needing_restock || 0),
|
||||
avgProductsPerLine: parseFloat(stats?.avg_products_per_line || 0),
|
||||
totalRevenue30d: parseFloat(stats?.total_revenue_30d || 0),
|
||||
avgMargin: parseFloat(stats?.overall_avg_margin || 0),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching line stats:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch line stats.' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /lines-aggregate/ (List product lines)
|
||||
router.get('/', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
try {
|
||||
// --- Pagination ---
|
||||
let page = parseInt(req.query.page, 10) || 1;
|
||||
let limit = parseInt(req.query.limit, 10) || DEFAULT_PAGE_LIMIT;
|
||||
limit = Math.min(limit, MAX_PAGE_LIMIT);
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// --- Sorting ---
|
||||
// Sort presets for curated multi-column sorts
|
||||
const SORT_PRESETS = {
|
||||
'by-brand': 'ORDER BY agg.brand ASC, agg.revenue_30d DESC NULLS LAST',
|
||||
'top-revenue': 'ORDER BY agg.revenue_30d DESC NULLS LAST',
|
||||
'needs-restock': 'ORDER BY agg.total_replenishment_units DESC NULLS LAST, agg.avg_stock_cover_days ASC NULLS FIRST',
|
||||
'fastest-growing': 'ORDER BY agg.sales_growth_30d_vs_prev DESC NULLS LAST',
|
||||
'newest': 'ORDER BY agg.earliest_received DESC NULLS LAST',
|
||||
'low-stock': 'ORDER BY CASE WHEN agg.sales_30d > 0 THEN 0 ELSE 1 END, agg.avg_stock_cover_days ASC NULLS LAST',
|
||||
};
|
||||
|
||||
let sortClause;
|
||||
const sortQueryKey = req.query.sort || 'by-brand';
|
||||
if (SORT_PRESETS[sortQueryKey]) {
|
||||
sortClause = SORT_PRESETS[sortQueryKey];
|
||||
} else {
|
||||
const sortColumnInfo = getSafeColumnInfo(sortQueryKey);
|
||||
const sortColumn = sortColumnInfo ? sortColumnInfo.dbCol : 'agg.brand';
|
||||
const sortDirection = req.query.order?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
||||
const nullsOrder = (sortDirection === 'ASC' ? 'NULLS FIRST' : 'NULLS LAST');
|
||||
sortClause = `ORDER BY ${sortColumn} ${sortDirection} ${nullsOrder}`;
|
||||
}
|
||||
|
||||
// --- Filtering ---
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
let paramCounter = 1;
|
||||
|
||||
for (const key in req.query) {
|
||||
if (['page', 'limit', 'sort', 'order', 'preset'].includes(key)) continue;
|
||||
|
||||
let filterKey = key;
|
||||
let operator = '=';
|
||||
const value = req.query[key];
|
||||
|
||||
const operatorMatch = key.match(/^(.*)_(eq|ne|gt|gte|lt|lte|like|ilike|between|in)$/);
|
||||
if (operatorMatch) {
|
||||
filterKey = operatorMatch[1];
|
||||
operator = operatorMatch[2];
|
||||
}
|
||||
|
||||
const columnInfo = getSafeColumnInfo(filterKey);
|
||||
if (columnInfo) {
|
||||
const dbColumn = columnInfo.dbCol;
|
||||
const valueType = columnInfo.type;
|
||||
try {
|
||||
let conditionFragment = '';
|
||||
let needsParam = true;
|
||||
switch (operator.toLowerCase()) {
|
||||
case 'eq': operator = '='; break;
|
||||
case 'ne': operator = '<>'; break;
|
||||
case 'gt': operator = '>'; break;
|
||||
case 'gte': operator = '>='; break;
|
||||
case 'lt': operator = '<'; break;
|
||||
case 'lte': operator = '<='; break;
|
||||
case 'like': operator = 'LIKE'; needsParam = false; params.push(`%${parseValue(value, valueType)}%`); break;
|
||||
case 'ilike': operator = 'ILIKE'; needsParam = false; params.push(`%${parseValue(value, valueType)}%`); break;
|
||||
case 'between':
|
||||
const [val1, val2] = String(value).split(',');
|
||||
if (val1 !== undefined && val2 !== undefined) {
|
||||
conditionFragment = `${dbColumn} BETWEEN $${paramCounter++} AND $${paramCounter++}`;
|
||||
params.push(parseValue(val1, valueType), parseValue(val2, valueType));
|
||||
needsParam = false;
|
||||
} else continue;
|
||||
break;
|
||||
case 'in':
|
||||
const inValues = String(value).split(',');
|
||||
if (inValues.length > 0) {
|
||||
const placeholders = inValues.map(() => `$${paramCounter++}`).join(', ');
|
||||
conditionFragment = `${dbColumn} IN (${placeholders})`;
|
||||
params.push(...inValues.map(v => parseValue(v, valueType)));
|
||||
needsParam = false;
|
||||
} else continue;
|
||||
break;
|
||||
default: operator = '='; break;
|
||||
}
|
||||
|
||||
if (needsParam) {
|
||||
conditionFragment = `${dbColumn} ${operator} $${paramCounter++}`;
|
||||
params.push(parseValue(value, valueType));
|
||||
} else if (!conditionFragment) {
|
||||
conditionFragment = `${dbColumn} ${operator} $${paramCounter++}`;
|
||||
}
|
||||
|
||||
if (conditionFragment) {
|
||||
conditions.push(`(${conditionFragment})`);
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn(`Skipping filter for key "${key}" due to parsing error: ${parseError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Execute Queries ---
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
const baseSql = `FROM (${LINE_AGGREGATE_SQL}) agg ${whereClause}`;
|
||||
|
||||
const countSql = `SELECT COUNT(*) AS total ${baseSql}`;
|
||||
const dataSql = `SELECT agg.* ${baseSql} ${sortClause} LIMIT $${paramCounter} OFFSET $${paramCounter + 1}`;
|
||||
const dataParams = [...params, limit, offset];
|
||||
|
||||
const [countResult, dataResult] = await Promise.all([
|
||||
pool.query(countSql, params),
|
||||
pool.query(dataSql, dataParams)
|
||||
]);
|
||||
|
||||
const total = parseInt(countResult.rows[0].total, 10);
|
||||
const lines = dataResult.rows.map(row => {
|
||||
const transformedRow = { ...row };
|
||||
for (const key in row) {
|
||||
if (row[key] === null || row[key] === undefined) continue;
|
||||
// snake_case -> camelCase
|
||||
let camelKey = key.replace(/_(\d+[a-z])/g, '$1');
|
||||
camelKey = camelKey.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
if (camelKey !== key) {
|
||||
transformedRow[camelKey] = row[key];
|
||||
}
|
||||
}
|
||||
return transformedRow;
|
||||
});
|
||||
|
||||
res.json({
|
||||
lines,
|
||||
pagination: { total, pages: Math.ceil(total / limit), currentPage: page, limit },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching line metrics list:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch line metrics.' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /lines-aggregate/:brand/:line/products (All products in a line)
|
||||
router.get('/:brand/:line/products', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
const { brand, line } = req.params;
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
pm.pid, pm.title, pm.sku,
|
||||
pm.current_stock, pm.current_stock_retail,
|
||||
pm.sales_30d, pm.revenue_30d, pm.profit_30d, pm.margin_30d,
|
||||
pm.sales_365d, pm.revenue_365d,
|
||||
pm.sales_velocity_daily, pm.stock_cover_in_days,
|
||||
pm.lifecycle_phase, pm.status, pm.abc_class,
|
||||
pm.on_order_qty, pm.replenishment_units,
|
||||
pm.date_first_received, pm.date_last_sold
|
||||
FROM product_metrics pm
|
||||
WHERE pm.brand = $1 AND pm.line = $2
|
||||
AND (pm.current_stock > 0 OR pm.sales_365d > 0)
|
||||
ORDER BY pm.revenue_30d DESC NULLS LAST
|
||||
`, [brand, line]);
|
||||
|
||||
res.json({
|
||||
totalProducts: rows.length,
|
||||
products: rows,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching line products:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch line products.' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -27,6 +27,7 @@ const importSessionsRouter = require('./routes/import-sessions');
|
||||
const importAuditLogRouter = require('./routes/import-audit-log');
|
||||
const productEditorAuditLogRouter = require('./routes/product-editor-audit-log');
|
||||
const newsletterRouter = require('./routes/newsletter');
|
||||
const linesAggregateRouter = require('./routes/linesAggregate');
|
||||
|
||||
// Get the absolute path to the .env file
|
||||
const envPath = '/var/www/html/inventory/.env';
|
||||
@@ -138,6 +139,7 @@ async function startServer() {
|
||||
app.use('/api/import-audit-log', importAuditLogRouter);
|
||||
app.use('/api/product-editor-audit-log', productEditorAuditLogRouter);
|
||||
app.use('/api/newsletter', newsletterRouter);
|
||||
app.use('/api/lines-aggregate', linesAggregateRouter);
|
||||
|
||||
// Basic health check route
|
||||
app.get('/health', (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user