Add new/preorder/recent filters for product editor, improve data fetching

This commit is contained in:
2026-01-31 13:05:05 -05:00
parent 89d518b57f
commit dd0e989669
7 changed files with 74408 additions and 88 deletions

View File

@@ -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);