Add more columns and column selector to product table

This commit is contained in:
2025-01-13 11:55:30 -05:00
parent ab9b5cb48c
commit dd882490c8
6 changed files with 620 additions and 150 deletions

View File

@@ -23,17 +23,17 @@ router.get('/', async (req, res) => {
const sortDirection = req.query.sortDirection === 'desc' ? 'DESC' : 'ASC';
// Build the WHERE clause
const conditions = ['visible = true'];
const conditions = ['p.visible = true'];
const params = [];
if (search) {
conditions.push('(title LIKE ? OR SKU LIKE ?)');
conditions.push('(p.title LIKE ? OR p.SKU LIKE ?)');
params.push(`%${search}%`, `%${search}%`);
}
if (category !== 'all') {
conditions.push(`
product_id IN (
p.product_id IN (
SELECT pc.product_id
FROM product_categories pc
JOIN categories c ON pc.category_id = c.id
@@ -44,42 +44,42 @@ router.get('/', async (req, res) => {
}
if (vendor !== 'all') {
conditions.push('vendor = ?');
conditions.push('p.vendor = ?');
params.push(vendor);
}
if (stockStatus !== 'all') {
switch (stockStatus) {
case 'out_of_stock':
conditions.push('stock_quantity = 0');
conditions.push('p.stock_quantity = 0');
break;
case 'low_stock':
conditions.push('stock_quantity > 0 AND stock_quantity <= 5');
conditions.push('p.stock_quantity > 0 AND p.stock_quantity <= 5');
break;
case 'in_stock':
conditions.push('stock_quantity > 5');
conditions.push('p.stock_quantity > 5');
break;
}
}
if (minPrice > 0) {
conditions.push('price >= ?');
conditions.push('p.price >= ?');
params.push(minPrice);
}
if (maxPrice) {
conditions.push('price <= ?');
conditions.push('p.price <= ?');
params.push(maxPrice);
}
// Get total count for pagination
const [countResult] = await pool.query(
`SELECT COUNT(*) as total FROM products WHERE ${conditions.join(' AND ')}`,
`SELECT COUNT(*) as total FROM products p WHERE ${conditions.join(' AND ')}`,
params
);
const total = countResult[0].total;
// Get paginated results
// Get paginated results with metrics
const query = `
SELECT
p.product_id,
@@ -89,15 +89,50 @@ router.get('/', async (req, res) => {
p.price,
p.regular_price,
p.cost_price,
p.landing_cost_price,
p.barcode,
p.vendor,
p.vendor_reference,
p.brand,
p.visible,
p.managing_stock,
p.replenishable,
p.moq,
p.uom,
p.image,
GROUP_CONCAT(c.name) as categories
GROUP_CONCAT(DISTINCT c.name) as categories,
-- Metrics from product_metrics
pm.daily_sales_avg,
pm.weekly_sales_avg,
pm.monthly_sales_avg,
pm.avg_quantity_per_order,
pm.number_of_orders,
pm.first_sale_date,
pm.last_sale_date,
pm.days_of_inventory,
pm.weeks_of_inventory,
pm.reorder_point,
pm.safety_stock,
pm.avg_margin_percent,
pm.total_revenue,
pm.inventory_value,
pm.cost_of_goods_sold,
pm.gross_profit,
pm.gmroi,
pm.avg_lead_time_days,
pm.last_purchase_date,
pm.last_received_date,
pm.abc_class,
pm.stock_status,
pm.turnover_rate,
pm.current_lead_time,
pm.target_lead_time,
pm.lead_time_status
FROM products p
LEFT JOIN product_categories pc ON p.product_id = pc.product_id
LEFT JOIN categories c ON pc.category_id = c.id
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
WHERE ${conditions.join(' AND ')}
GROUP BY p.product_id
ORDER BY ${sortColumn} ${sortDirection}
@@ -106,10 +141,38 @@ router.get('/', async (req, res) => {
const [rows] = await pool.query(query, [...params, limit, offset]);
// Transform the categories string into an array
// Transform the categories string into an array and parse numeric values
const productsWithCategories = rows.map(product => ({
...product,
categories: product.categories ? product.categories.split(',') : []
categories: product.categories ? [...new Set(product.categories.split(','))] : [],
// Parse numeric values
price: parseFloat(product.price) || 0,
regular_price: parseFloat(product.regular_price) || 0,
cost_price: parseFloat(product.cost_price) || 0,
landing_cost_price: parseFloat(product.landing_cost_price) || 0,
stock_quantity: parseInt(product.stock_quantity) || 0,
moq: parseInt(product.moq) || 1,
uom: parseInt(product.uom) || 1,
// Parse metrics
daily_sales_avg: parseFloat(product.daily_sales_avg) || null,
weekly_sales_avg: parseFloat(product.weekly_sales_avg) || null,
monthly_sales_avg: parseFloat(product.monthly_sales_avg) || null,
avg_quantity_per_order: parseFloat(product.avg_quantity_per_order) || null,
number_of_orders: parseInt(product.number_of_orders) || null,
days_of_inventory: parseInt(product.days_of_inventory) || null,
weeks_of_inventory: parseInt(product.weeks_of_inventory) || null,
reorder_point: parseInt(product.reorder_point) || null,
safety_stock: parseInt(product.safety_stock) || null,
avg_margin_percent: parseFloat(product.avg_margin_percent) || null,
total_revenue: parseFloat(product.total_revenue) || null,
inventory_value: parseFloat(product.inventory_value) || null,
cost_of_goods_sold: parseFloat(product.cost_of_goods_sold) || null,
gross_profit: parseFloat(product.gross_profit) || null,
gmroi: parseFloat(product.gmroi) || null,
turnover_rate: parseFloat(product.turnover_rate) || null,
avg_lead_time_days: parseInt(product.avg_lead_time_days) || null,
current_lead_time: parseInt(product.current_lead_time) || null,
target_lead_time: parseInt(product.target_lead_time) || null
}));
// Get unique categories and vendors for filters

View File

@@ -79,7 +79,7 @@ const pool = initPool({
app.locals.pool = pool;
// Routes
app.use('/api/dashboard/products', productsRouter);
app.use('/api/products', productsRouter);
app.use('/api/dashboard', dashboardRouter);
app.use('/api/orders', ordersRouter);
app.use('/api/csv', csvRouter);