Add new/preorder/recent filters for product editor, improve data fetching
This commit is contained in:
@@ -1254,6 +1254,251 @@ router.get('/search-products', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Shared SELECT for product queries (matches search-products fields)
|
||||
const PRODUCT_SELECT = `
|
||||
SELECT
|
||||
p.pid,
|
||||
p.description AS title,
|
||||
p.notes AS description,
|
||||
p.itemnumber AS sku,
|
||||
p.upc AS barcode,
|
||||
p.harmonized_tariff_code,
|
||||
pcp.price_each AS price,
|
||||
p.sellingprice AS regular_price,
|
||||
CASE
|
||||
WHEN EXISTS (SELECT 1 FROM product_inventory WHERE pid = p.pid AND count > 0)
|
||||
THEN (SELECT ROUND(AVG(costeach), 5) FROM product_inventory WHERE pid = p.pid AND count > 0)
|
||||
ELSE (SELECT costeach FROM product_inventory WHERE pid = p.pid ORDER BY daterec DESC LIMIT 1)
|
||||
END AS cost_price,
|
||||
s.companyname AS vendor,
|
||||
sid.supplier_itemnumber AS vendor_reference,
|
||||
sid.notions_itemnumber AS notions_reference,
|
||||
sid.supplier_id AS supplier,
|
||||
sid.notions_case_pack AS case_qty,
|
||||
pc1.name AS brand,
|
||||
p.company AS brand_id,
|
||||
pc2.name AS line,
|
||||
p.line AS line_id,
|
||||
pc3.name AS subline,
|
||||
p.subline AS subline_id,
|
||||
pc4.name AS artist,
|
||||
p.artist AS artist_id,
|
||||
COALESCE(CASE
|
||||
WHEN sid.supplier_id = 92 THEN sid.notions_qty_per_unit
|
||||
ELSE sid.supplier_qty_per_unit
|
||||
END, sid.notions_qty_per_unit) AS moq,
|
||||
p.weight,
|
||||
p.length,
|
||||
p.width,
|
||||
p.height,
|
||||
p.country_of_origin,
|
||||
ci.totalsold AS total_sold,
|
||||
p.datein AS first_received,
|
||||
pls.date_sold AS date_last_sold,
|
||||
IF(p.tax_code IS NULL, '', CAST(p.tax_code AS CHAR)) AS tax_code,
|
||||
CAST(p.size_cat AS CHAR) AS size_cat,
|
||||
CAST(p.shipping_restrictions AS CHAR) AS shipping_restrictions
|
||||
FROM products p
|
||||
LEFT JOIN product_current_prices pcp ON p.pid = pcp.pid AND pcp.active = 1
|
||||
LEFT JOIN supplier_item_data sid ON p.pid = sid.pid
|
||||
LEFT JOIN suppliers s ON sid.supplier_id = s.supplierid
|
||||
LEFT JOIN product_categories pc1 ON p.company = pc1.cat_id
|
||||
LEFT JOIN product_categories pc2 ON p.line = pc2.cat_id
|
||||
LEFT JOIN product_categories pc3 ON p.subline = pc3.cat_id
|
||||
LEFT JOIN product_categories pc4 ON p.artist = pc4.cat_id
|
||||
LEFT JOIN product_last_sold pls ON p.pid = pls.pid
|
||||
LEFT JOIN current_inventory ci ON p.pid = ci.pid`;
|
||||
|
||||
// Load products for a specific line (company + line + optional subline)
|
||||
router.get('/line-products', async (req, res) => {
|
||||
const { company, line, subline } = req.query;
|
||||
if (!company || !line) {
|
||||
return res.status(400).json({ error: 'company and line are required' });
|
||||
}
|
||||
try {
|
||||
const { connection } = await getDbConnection();
|
||||
let where = 'WHERE p.company = ? AND p.line = ?';
|
||||
const params = [Number(company), Number(line)];
|
||||
if (subline) {
|
||||
where += ' AND p.subline = ?';
|
||||
params.push(Number(subline));
|
||||
}
|
||||
const query = `${PRODUCT_SELECT} ${where} GROUP BY p.pid ORDER BY p.description`;
|
||||
const [results] = await connection.query(query, params);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
console.error('Error loading line products:', error);
|
||||
res.status(500).json({ error: 'Failed to load line products' });
|
||||
}
|
||||
});
|
||||
|
||||
// Load new products (last 45 days by release date, excluding preorders)
|
||||
router.get('/new-products', async (req, res) => {
|
||||
try {
|
||||
const { connection } = await getDbConnection();
|
||||
const query = `${PRODUCT_SELECT}
|
||||
LEFT JOIN shop_inventory si2 ON p.pid = si2.pid AND si2.store = 0
|
||||
WHERE DATEDIFF(NOW(), p.date_ol) <= 45
|
||||
AND p.notnew = 0
|
||||
AND (si2.all IS NULL OR si2.all != 2)
|
||||
GROUP BY p.pid
|
||||
ORDER BY IF(p.date_ol != '0000-00-00', p.date_ol, p.date_created) DESC`;
|
||||
const [results] = await connection.query(query);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
console.error('Error loading new products:', error);
|
||||
res.status(500).json({ error: 'Failed to load new products' });
|
||||
}
|
||||
});
|
||||
|
||||
// Load preorder products
|
||||
router.get('/preorder-products', async (req, res) => {
|
||||
try {
|
||||
const { connection } = await getDbConnection();
|
||||
const query = `${PRODUCT_SELECT}
|
||||
LEFT JOIN shop_inventory si2 ON p.pid = si2.pid AND si2.store = 0
|
||||
WHERE si2.all = 2
|
||||
GROUP BY p.pid
|
||||
ORDER BY IF(p.date_ol != '0000-00-00', p.date_ol, p.date_created) DESC`;
|
||||
const [results] = await connection.query(query);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
console.error('Error loading preorder products:', error);
|
||||
res.status(500).json({ error: 'Failed to load preorder products' });
|
||||
}
|
||||
});
|
||||
|
||||
// Load hidden recently-created products from local PG, enriched from MySQL
|
||||
router.get('/hidden-new-products', async (req, res) => {
|
||||
try {
|
||||
const pool = req.app.locals.pool;
|
||||
const pgResult = await pool.query(
|
||||
`SELECT pid FROM products WHERE visible = false AND created_at > NOW() - INTERVAL '90 days' ORDER BY created_at DESC LIMIT 500`
|
||||
);
|
||||
const pids = pgResult.rows.map(r => r.pid);
|
||||
if (pids.length === 0) return res.json([]);
|
||||
|
||||
const { connection } = await getDbConnection();
|
||||
const placeholders = pids.map(() => '?').join(',');
|
||||
const query = `${PRODUCT_SELECT} WHERE p.pid IN (${placeholders}) GROUP BY p.pid ORDER BY FIELD(p.pid, ${placeholders})`;
|
||||
const [results] = await connection.query(query, [...pids, ...pids]);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
console.error('Error loading hidden new products:', error);
|
||||
res.status(500).json({ error: 'Failed to load hidden new products' });
|
||||
}
|
||||
});
|
||||
|
||||
// Load landing page extras (featured lines) for new/preorder pages
|
||||
router.get('/landing-extras', async (req, res) => {
|
||||
const { catId, sid } = req.query;
|
||||
if (!catId) {
|
||||
return res.status(400).json({ error: 'catId is required' });
|
||||
}
|
||||
try {
|
||||
const { connection } = await getDbConnection();
|
||||
const [results] = await connection.query(
|
||||
`SELECT extra_id, image, extra_cat_id, path, name, top_text, is_new
|
||||
FROM product_category_landing_extras
|
||||
WHERE cat_id = ? AND sid = ? AND section_cat_id = 0 AND hidden = 0
|
||||
ORDER BY \`order\` DESC, name ASC`,
|
||||
[Number(catId), Number(sid) || 0]
|
||||
);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
console.error('Error loading landing extras:', error);
|
||||
res.status(500).json({ error: 'Failed to load landing extras' });
|
||||
}
|
||||
});
|
||||
|
||||
// Load products by shop path (resolves category names to IDs)
|
||||
router.get('/path-products', async (req, res) => {
|
||||
res.set('Cache-Control', 'no-store');
|
||||
const { path: shopPath } = req.query;
|
||||
if (!shopPath) {
|
||||
return res.status(400).json({ error: 'path is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { connection } = await getDbConnection();
|
||||
|
||||
// Strip common URL prefixes (full URLs, /shop/, leading slash)
|
||||
const cleanPath = String(shopPath)
|
||||
.replace(/^https?:\/\/[^/]+/, '')
|
||||
.replace(/^\/shop\//, '/')
|
||||
.replace(/^\//, '');
|
||||
const parts = cleanPath.split('/');
|
||||
const filters = {};
|
||||
for (let i = 0; i < parts.length - 1; i += 2) {
|
||||
filters[parts[i]] = decodeURIComponent(parts[i + 1]).replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
if (Object.keys(filters).length === 0) {
|
||||
return res.status(400).json({ error: 'No valid filters found in path' });
|
||||
}
|
||||
|
||||
// Resolve category names to IDs (order matters: company -> line -> subline)
|
||||
const typeMap = { company: 1, line: 2, subline: 3, section: 10, cat: 11, subcat: 12, subsubcat: 13 };
|
||||
const resolvedIds = {};
|
||||
const resolveOrder = ['company', 'line', 'subline', 'section', 'cat', 'subcat', 'subsubcat'];
|
||||
|
||||
for (const key of resolveOrder) {
|
||||
const value = filters[key];
|
||||
if (!value) continue;
|
||||
const type = typeMap[key];
|
||||
if (!type) continue;
|
||||
const types = key === 'cat' ? [11, 20] : key === 'subcat' ? [12, 21] : [type];
|
||||
|
||||
// For line/subline, filter by parent (master_cat_id) to disambiguate
|
||||
let parentFilter = '';
|
||||
const qParams = [value];
|
||||
if (key === 'line' && resolvedIds.company != null) {
|
||||
parentFilter = ' AND master_cat_id = ?';
|
||||
qParams.push(resolvedIds.company);
|
||||
} else if (key === 'subline' && resolvedIds.line != null) {
|
||||
parentFilter = ' AND master_cat_id = ?';
|
||||
qParams.push(resolvedIds.line);
|
||||
}
|
||||
|
||||
const [rows] = await connection.query(
|
||||
`SELECT cat_id FROM product_categories WHERE LOWER(name) = LOWER(?) AND type IN (${types.join(',')})${parentFilter} LIMIT 1`,
|
||||
qParams
|
||||
);
|
||||
if (rows.length > 0) {
|
||||
resolvedIds[key] = rows[0].cat_id;
|
||||
} else {
|
||||
return res.json([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Build WHERE using resolved IDs
|
||||
const whereParts = [];
|
||||
const params = [];
|
||||
const directFields = { company: 'p.company', line: 'p.line', subline: 'p.subline' };
|
||||
|
||||
for (const [key, catId] of Object.entries(resolvedIds)) {
|
||||
if (directFields[key]) {
|
||||
whereParts.push(`${directFields[key]} = ?`);
|
||||
params.push(catId);
|
||||
} else {
|
||||
whereParts.push('EXISTS (SELECT 1 FROM product_category_index pci2 WHERE pci2.pid = p.pid AND pci2.cat_id = ?)');
|
||||
params.push(catId);
|
||||
}
|
||||
}
|
||||
|
||||
if (whereParts.length === 0) {
|
||||
return res.status(400).json({ error: 'No valid filters found in path' });
|
||||
}
|
||||
|
||||
const query = `${PRODUCT_SELECT} WHERE ${whereParts.join(' AND ')} GROUP BY p.pid ORDER BY p.description`;
|
||||
const [results] = await connection.query(query, params);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
console.error('Error loading path products:', error);
|
||||
res.status(500).json({ error: 'Failed to load products by path' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get product images for a given PID from production DB
|
||||
router.get('/product-images/:pid', async (req, res) => {
|
||||
const pid = parseInt(req.params.pid, 10);
|
||||
|
||||
Reference in New Issue
Block a user