Optimize orders import
This commit is contained in:
@@ -634,7 +634,7 @@ router.post('/upload-image', upload.single('image'), async (req, res) => {
|
||||
req.file.size = processingResult.finalSize;
|
||||
|
||||
// Create URL for the uploaded file - using an absolute URL with domain
|
||||
// This will generate a URL like: https://acot.site/uploads/products/filename.jpg
|
||||
// This will generate a URL like: https://tools.acherryontop.com/uploads/products/filename.jpg
|
||||
const baseUrl = 'https://tools.acherryontop.com';
|
||||
const imageUrl = `${baseUrl}/uploads/products/${req.file.filename}`;
|
||||
|
||||
@@ -1046,21 +1046,17 @@ router.get('/list-uploads', (req, res) => {
|
||||
|
||||
// Search products from production database
|
||||
router.get('/search-products', async (req, res) => {
|
||||
const { q, pid, company, dateRange } = req.query;
|
||||
|
||||
if (!q && !pid) {
|
||||
return res.status(400).json({ error: 'Search term or pid is required' });
|
||||
const { q, company, dateRange } = req.query;
|
||||
|
||||
if (!q) {
|
||||
return res.status(400).json({ error: 'Search term is required' });
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const { connection } = await getDbConnection();
|
||||
|
||||
|
||||
// Build WHERE clause with additional filters
|
||||
let whereClause;
|
||||
if (pid) {
|
||||
whereClause = `\n WHERE p.pid = ${connection.escape(Number(pid))}`;
|
||||
} else {
|
||||
whereClause = `
|
||||
let whereClause = `
|
||||
WHERE (
|
||||
p.description LIKE ? OR
|
||||
p.itemnumber LIKE ? OR
|
||||
@@ -1068,7 +1064,6 @@ router.get('/search-products', async (req, res) => {
|
||||
pc1.name LIKE ? OR
|
||||
s.companyname LIKE ?
|
||||
)`;
|
||||
}
|
||||
|
||||
// Add company filter if provided
|
||||
if (company) {
|
||||
@@ -1127,9 +1122,8 @@ router.get('/search-products', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isPidSearch = !!pid;
|
||||
// Special case for wildcard search
|
||||
const isWildcardSearch = !isPidSearch && q === '*';
|
||||
const isWildcardSearch = q === '*';
|
||||
const searchPattern = isWildcardSearch ? '%' : `%${q}%`;
|
||||
const exactPattern = isWildcardSearch ? '%' : q;
|
||||
|
||||
@@ -1189,9 +1183,9 @@ router.get('/search-products', async (req, res) => {
|
||||
LEFT JOIN current_inventory ci ON p.pid = ci.pid
|
||||
${whereClause}
|
||||
GROUP BY p.pid
|
||||
${isPidSearch ? '' : isWildcardSearch ? 'ORDER BY p.datein DESC' : `
|
||||
ORDER BY
|
||||
CASE
|
||||
${isWildcardSearch ? 'ORDER BY p.datein DESC' : `
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN p.description LIKE ? THEN 1
|
||||
WHEN p.itemnumber = ? THEN 2
|
||||
WHEN p.upc = ? THEN 3
|
||||
@@ -1201,12 +1195,10 @@ router.get('/search-products', async (req, res) => {
|
||||
END
|
||||
`}
|
||||
`;
|
||||
|
||||
// Prepare query parameters based on search type
|
||||
|
||||
// Prepare query parameters based on whether it's a wildcard search
|
||||
let queryParams;
|
||||
if (isPidSearch) {
|
||||
queryParams = [];
|
||||
} else if (isWildcardSearch) {
|
||||
if (isWildcardSearch) {
|
||||
queryParams = [
|
||||
searchPattern, // LIKE for description
|
||||
searchPattern, // LIKE for itemnumber
|
||||
@@ -1254,293 +1246,6 @@ 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);
|
||||
if (!pid || pid <= 0) {
|
||||
return res.status(400).json({ error: 'Valid PID is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { connection } = await getDbConnection();
|
||||
|
||||
const [rows] = await connection.query(
|
||||
'SELECT iid, type, width, height, `order`, hidden FROM product_images WHERE pid = ? ORDER BY `order` DESC, type',
|
||||
[pid]
|
||||
);
|
||||
|
||||
// Group by iid and build image URLs using the same logic as the PHP codebase
|
||||
const typeMap = { 1: 'o', 2: 'l', 3: 't', 4: '100x100', 5: '175x175', 6: '300x300', 7: '600x600', 8: '500x500', 9: '150x150' };
|
||||
const padded = String(pid).padStart(10, '0');
|
||||
const pathPrefix = `${padded.substring(0, 4)}/${padded.substring(4, 7)}/`;
|
||||
|
||||
const imagesByIid = {};
|
||||
for (const row of rows) {
|
||||
const typeName = typeMap[row.type];
|
||||
if (!typeName) continue;
|
||||
if (!imagesByIid[row.iid]) {
|
||||
imagesByIid[row.iid] = { iid: row.iid, order: row.order, hidden: !!row.hidden, sizes: {} };
|
||||
}
|
||||
imagesByIid[row.iid].sizes[typeName] = {
|
||||
width: row.width,
|
||||
height: row.height,
|
||||
url: `https://sbing.com/i/products/${pathPrefix}${pid}-${typeName}-${row.iid}.jpg`,
|
||||
};
|
||||
}
|
||||
|
||||
const images = Object.values(imagesByIid).sort((a, b) => b.order - a.order);
|
||||
res.json(images);
|
||||
} catch (error) {
|
||||
console.error('Error fetching product images:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch product images' });
|
||||
}
|
||||
});
|
||||
|
||||
const UPC_SUPPLIER_PREFIX_LEADING_DIGIT = '4';
|
||||
const UPC_MAX_SEQUENCE = 99999;
|
||||
const UPC_RESERVATION_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
Reference in New Issue
Block a user