Enhance product page filtering
This commit is contained in:
@@ -13,12 +13,6 @@ router.get('/', async (req, res) => {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const offset = (page - 1) * limit;
|
||||
const search = req.query.search || '';
|
||||
const category = req.query.category || 'all';
|
||||
const vendor = req.query.vendor || 'all';
|
||||
const stockStatus = req.query.stockStatus || 'all';
|
||||
const minPrice = parseFloat(req.query.minPrice) || 0;
|
||||
const maxPrice = req.query.maxPrice ? parseFloat(req.query.maxPrice) : null;
|
||||
const sortColumn = req.query.sortColumn || 'title';
|
||||
const sortDirection = req.query.sortDirection === 'desc' ? 'DESC' : 'ASC';
|
||||
|
||||
@@ -26,12 +20,19 @@ router.get('/', async (req, res) => {
|
||||
const conditions = ['p.visible = true'];
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
// Handle text search filters
|
||||
if (req.query.search) {
|
||||
conditions.push('(p.title LIKE ? OR p.SKU LIKE ?)');
|
||||
params.push(`%${search}%`, `%${search}%`);
|
||||
params.push(`%${req.query.search}%`, `%${req.query.search}%`);
|
||||
}
|
||||
|
||||
if (category !== 'all') {
|
||||
if (req.query.sku) {
|
||||
conditions.push('p.SKU LIKE ?');
|
||||
params.push(`%${req.query.sku}%`);
|
||||
}
|
||||
|
||||
// Handle select filters
|
||||
if (req.query.category && req.query.category !== 'all') {
|
||||
conditions.push(`
|
||||
p.product_id IN (
|
||||
SELECT pc.product_id
|
||||
@@ -40,99 +41,144 @@ router.get('/', async (req, res) => {
|
||||
WHERE c.name = ?
|
||||
)
|
||||
`);
|
||||
params.push(category);
|
||||
params.push(req.query.category);
|
||||
}
|
||||
|
||||
if (vendor !== 'all') {
|
||||
if (req.query.vendor && req.query.vendor !== 'all') {
|
||||
conditions.push('p.vendor = ?');
|
||||
params.push(vendor);
|
||||
params.push(req.query.vendor);
|
||||
}
|
||||
|
||||
if (stockStatus !== 'all') {
|
||||
switch (stockStatus) {
|
||||
case 'out_of_stock':
|
||||
conditions.push('p.stock_quantity = 0');
|
||||
break;
|
||||
case 'low_stock':
|
||||
conditions.push('p.stock_quantity > 0 AND p.stock_quantity <= 5');
|
||||
break;
|
||||
case 'in_stock':
|
||||
conditions.push('p.stock_quantity > 5');
|
||||
break;
|
||||
}
|
||||
if (req.query.brand && req.query.brand !== 'all') {
|
||||
conditions.push('p.brand = ?');
|
||||
params.push(req.query.brand);
|
||||
}
|
||||
|
||||
if (minPrice > 0) {
|
||||
if (req.query.abcClass) {
|
||||
conditions.push('pm.abc_class = ?');
|
||||
params.push(req.query.abcClass);
|
||||
}
|
||||
|
||||
// Handle numeric range filters
|
||||
if (req.query.minStock) {
|
||||
conditions.push('p.stock_quantity >= ?');
|
||||
params.push(parseFloat(req.query.minStock));
|
||||
}
|
||||
|
||||
if (req.query.maxStock) {
|
||||
conditions.push('p.stock_quantity <= ?');
|
||||
params.push(parseFloat(req.query.maxStock));
|
||||
}
|
||||
|
||||
if (req.query.minPrice) {
|
||||
conditions.push('p.price >= ?');
|
||||
params.push(minPrice);
|
||||
params.push(parseFloat(req.query.minPrice));
|
||||
}
|
||||
|
||||
if (maxPrice) {
|
||||
if (req.query.maxPrice) {
|
||||
conditions.push('p.price <= ?');
|
||||
params.push(maxPrice);
|
||||
params.push(parseFloat(req.query.maxPrice));
|
||||
}
|
||||
|
||||
if (req.query.minSalesAvg) {
|
||||
conditions.push('pm.daily_sales_avg >= ?');
|
||||
params.push(parseFloat(req.query.minSalesAvg));
|
||||
}
|
||||
|
||||
if (req.query.maxSalesAvg) {
|
||||
conditions.push('pm.daily_sales_avg <= ?');
|
||||
params.push(parseFloat(req.query.maxSalesAvg));
|
||||
}
|
||||
|
||||
if (req.query.minMargin) {
|
||||
conditions.push('pm.avg_margin_percent >= ?');
|
||||
params.push(parseFloat(req.query.minMargin));
|
||||
}
|
||||
|
||||
if (req.query.maxMargin) {
|
||||
conditions.push('pm.avg_margin_percent <= ?');
|
||||
params.push(parseFloat(req.query.maxMargin));
|
||||
}
|
||||
|
||||
if (req.query.minGMROI) {
|
||||
conditions.push('pm.gmroi >= ?');
|
||||
params.push(parseFloat(req.query.minGMROI));
|
||||
}
|
||||
|
||||
if (req.query.maxGMROI) {
|
||||
conditions.push('pm.gmroi <= ?');
|
||||
params.push(parseFloat(req.query.maxGMROI));
|
||||
}
|
||||
|
||||
// Handle status filters
|
||||
if (req.query.stockStatus && req.query.stockStatus !== 'all') {
|
||||
conditions.push('pm.stock_status = ?');
|
||||
params.push(req.query.stockStatus);
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
const [countResult] = await pool.query(
|
||||
`SELECT COUNT(*) as total FROM products p WHERE ${conditions.join(' AND ')}`,
|
||||
`SELECT COUNT(DISTINCT p.product_id) as total
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
WHERE ${conditions.join(' AND ')}`,
|
||||
params
|
||||
);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// Get paginated results with metrics
|
||||
// Get available filters
|
||||
const [categories] = await pool.query(
|
||||
'SELECT name FROM categories ORDER BY name'
|
||||
);
|
||||
const [vendors] = await pool.query(
|
||||
'SELECT DISTINCT vendor FROM products WHERE visible = true AND vendor IS NOT NULL AND vendor != "" ORDER BY vendor'
|
||||
);
|
||||
|
||||
// Main query with all fields
|
||||
const query = `
|
||||
WITH product_thresholds AS (
|
||||
SELECT
|
||||
p.product_id,
|
||||
COALESCE(
|
||||
(SELECT overstock_days FROM stock_thresholds st
|
||||
JOIN product_categories pc ON st.category_id = pc.category_id
|
||||
WHERE pc.product_id = p.product_id
|
||||
AND st.vendor = p.vendor LIMIT 1),
|
||||
(SELECT overstock_days FROM stock_thresholds st
|
||||
JOIN product_categories pc ON st.category_id = pc.category_id
|
||||
WHERE pc.product_id = p.product_id
|
||||
AND st.vendor IS NULL LIMIT 1),
|
||||
(SELECT overstock_days FROM stock_thresholds st
|
||||
WHERE st.category_id IS NULL
|
||||
AND st.vendor = p.vendor LIMIT 1),
|
||||
(SELECT overstock_days FROM stock_thresholds st
|
||||
WHERE st.category_id IS NULL
|
||||
AND st.vendor IS NULL LIMIT 1),
|
||||
90
|
||||
) as target_days
|
||||
FROM products p
|
||||
)
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.title,
|
||||
p.SKU,
|
||||
p.stock_quantity,
|
||||
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,
|
||||
p.*,
|
||||
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.avg_lead_time_days,
|
||||
pm.current_lead_time,
|
||||
pm.target_lead_time,
|
||||
pm.lead_time_status
|
||||
pm.lead_time_status,
|
||||
pm.days_of_inventory as days_of_stock,
|
||||
COALESCE(pm.days_of_inventory / NULLIF(pt.target_days, 0), 0) as stock_coverage_ratio
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
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
|
||||
LEFT JOIN product_thresholds pt ON p.product_id = pt.product_id
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
GROUP BY p.product_id
|
||||
ORDER BY ${sortColumn} ${sortDirection}
|
||||
@@ -141,64 +187,40 @@ router.get('/', async (req, res) => {
|
||||
|
||||
const [rows] = await pool.query(query, [...params, limit, offset]);
|
||||
|
||||
// Transform the categories string into an array and parse numeric values
|
||||
const productsWithCategories = rows.map(product => ({
|
||||
...product,
|
||||
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
|
||||
// Transform the results
|
||||
const products = rows.map(row => ({
|
||||
...row,
|
||||
categories: row.categories ? row.categories.split(',') : [],
|
||||
price: parseFloat(row.price),
|
||||
cost_price: parseFloat(row.cost_price),
|
||||
landing_cost_price: parseFloat(row.landing_cost_price),
|
||||
stock_quantity: parseInt(row.stock_quantity),
|
||||
daily_sales_avg: parseFloat(row.daily_sales_avg) || 0,
|
||||
weekly_sales_avg: parseFloat(row.weekly_sales_avg) || 0,
|
||||
monthly_sales_avg: parseFloat(row.monthly_sales_avg) || 0,
|
||||
avg_margin_percent: parseFloat(row.avg_margin_percent) || 0,
|
||||
gmroi: parseFloat(row.gmroi) || 0,
|
||||
lead_time_days: parseInt(row.lead_time_days) || 0,
|
||||
days_of_stock: parseFloat(row.days_of_stock) || 0,
|
||||
stock_coverage_ratio: parseFloat(row.stock_coverage_ratio) || 0
|
||||
}));
|
||||
|
||||
// Get unique categories and vendors for filters
|
||||
const [categories] = await pool.query(
|
||||
'SELECT name FROM categories ORDER BY name'
|
||||
);
|
||||
const [vendors] = await pool.query(
|
||||
'SELECT DISTINCT vendor FROM products WHERE visible = true AND vendor IS NOT NULL AND vendor != "" ORDER BY vendor'
|
||||
);
|
||||
|
||||
res.json({
|
||||
products: productsWithCategories,
|
||||
products,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
currentPage: page,
|
||||
limit
|
||||
totalPages: Math.ceil(total / limit)
|
||||
},
|
||||
filters: {
|
||||
categories: categories.map(c => c.name),
|
||||
vendors: vendors.map(v => v.vendor)
|
||||
categories: categories.map(category => category.name),
|
||||
vendors: vendors.map(vendor => vendor.vendor)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching products:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch products' });
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user