Add create PO page, remove old quick order builder from forecasting page, reorder sidebar, combine brands/vendors pages
This commit is contained in:
0
inventory-server/chat/export-chat-data.sh
Executable file → Normal file
0
inventory-server/chat/export-chat-data.sh
Executable file → Normal file
0
inventory-server/chat/verify-migration.js
Executable file → Normal file
0
inventory-server/chat/verify-migration.js
Executable file → Normal file
@@ -504,6 +504,152 @@ router.get('/search', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Batch lookup of product display data by pid list (used by Create PO page)
|
||||
// Accepts ?pids=1,2,3 — comma-separated; de-duped server-side; capped at 500.
|
||||
// Returns rows in the same order as the deduped input pids; missing pids are silently dropped.
|
||||
router.get('/batch', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
const raw = req.query.pids;
|
||||
if (!raw || typeof raw !== 'string') {
|
||||
return res.status(400).json({ error: 'pids query parameter is required' });
|
||||
}
|
||||
|
||||
const pids = Array.from(new Set(
|
||||
raw.split(',')
|
||||
.map(s => parseInt(s.trim(), 10))
|
||||
.filter(n => Number.isInteger(n) && n > 0)
|
||||
)).slice(0, 500);
|
||||
|
||||
if (pids.length === 0) {
|
||||
return res.status(400).json({ error: 'No valid pids provided' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
p.pid,
|
||||
p.title,
|
||||
p.image_175 AS image_url,
|
||||
p.barcode,
|
||||
p.vendor_reference,
|
||||
p.notions_reference,
|
||||
p.notions_inv_count,
|
||||
pm.current_stock,
|
||||
p.baskets,
|
||||
pm.on_order_qty,
|
||||
p.total_sold,
|
||||
pm.current_cost_price,
|
||||
pm.date_last_sold,
|
||||
pm.date_first_received,
|
||||
p.moq
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON pm.pid = p.pid
|
||||
WHERE p.pid = ANY($1::bigint[])
|
||||
`, [pids]);
|
||||
|
||||
// products.pid is BIGINT, which the pg driver returns as a STRING by
|
||||
// default (to preserve precision for values > 2^53). Coerce to Number
|
||||
// so the JSON response has numeric pids and Map lookups work.
|
||||
const normalized = rows.map(r => ({ ...r, pid: Number(r.pid) }));
|
||||
const byPid = new Map(normalized.map(r => [r.pid, r]));
|
||||
// Preserve the requested order so the frontend can append rows in input order
|
||||
const ordered = pids.map(pid => byPid.get(pid)).filter(Boolean);
|
||||
res.json(ordered);
|
||||
} catch (error) {
|
||||
console.error('Error fetching batch products:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch products' });
|
||||
}
|
||||
});
|
||||
|
||||
// Bulk resolve a list of identifiers (UPC / SKU / supplier # / notions # / pid)
|
||||
// to candidate products in ONE query. Used by the Create PO paste/upload flow.
|
||||
// Body: { identifiers: string[] }
|
||||
// Response: { results: Array<{ identifier: string, candidates: Candidate[] }> }
|
||||
// Results are returned in the same order as the input identifiers, with
|
||||
// duplicates preserved (so the caller can pair results back to input rows
|
||||
// positionally).
|
||||
router.post('/resolve-identifiers', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
const body = req.body || {};
|
||||
const raw = Array.isArray(body.identifiers) ? body.identifiers : null;
|
||||
|
||||
if (!raw) {
|
||||
return res.status(400).json({ error: 'identifiers array is required' });
|
||||
}
|
||||
|
||||
// Clean and cap. Cleaned keeps ORIGINAL order (duplicates preserved) so the
|
||||
// response aligns with the caller's input rows positionally.
|
||||
const cleaned = raw
|
||||
.map(s => (typeof s === 'string' ? s.trim() : ''))
|
||||
.filter(s => s.length > 0)
|
||||
.slice(0, 1000);
|
||||
|
||||
if (cleaned.length === 0) {
|
||||
return res.json({ results: [] });
|
||||
}
|
||||
|
||||
// Dedupe for the DB lookup, and split numeric-looking values off for a
|
||||
// separate bigint equality check (so the pid index can be used).
|
||||
const uniqueTextIds = Array.from(new Set(cleaned));
|
||||
const numericPids = Array.from(new Set(
|
||||
uniqueTextIds
|
||||
.filter(s => /^\d+$/.test(s) && s.length <= 18) // safe for Number()
|
||||
.map(s => Number(s))
|
||||
.filter(n => Number.isSafeInteger(n) && n > 0)
|
||||
));
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
p.pid,
|
||||
p.title,
|
||||
p.sku,
|
||||
p.barcode,
|
||||
p.vendor_reference,
|
||||
p.notions_reference,
|
||||
p.brand
|
||||
FROM products p
|
||||
WHERE p.sku = ANY($1::text[])
|
||||
OR p.barcode = ANY($1::text[])
|
||||
OR p.vendor_reference = ANY($1::text[])
|
||||
OR p.notions_reference = ANY($1::text[])
|
||||
OR p.pid = ANY($2::bigint[])
|
||||
`, [uniqueTextIds, numericPids]);
|
||||
|
||||
// Normalize pid to Number once (products.pid is BIGINT → pg returns string)
|
||||
const products = rows.map(r => ({
|
||||
pid: Number(r.pid),
|
||||
title: r.title,
|
||||
sku: r.sku,
|
||||
barcode: r.barcode,
|
||||
vendor_reference: r.vendor_reference,
|
||||
notions_reference: r.notions_reference,
|
||||
brand: r.brand,
|
||||
}));
|
||||
|
||||
// Group per-input-identifier. A product counts as a match for an
|
||||
// identifier if any of its indexable fields equals the identifier string
|
||||
// (or the pid matches when the identifier is numeric). The comparison is
|
||||
// done in JS against the fetched products — cheap because the product
|
||||
// count is bounded by the DB result set.
|
||||
const results = cleaned.map(identifier => {
|
||||
const candidates = products.filter(p => (
|
||||
p.sku === identifier ||
|
||||
p.barcode === identifier ||
|
||||
p.vendor_reference === identifier ||
|
||||
p.notions_reference === identifier ||
|
||||
String(p.pid) === identifier
|
||||
));
|
||||
return { identifier, candidates };
|
||||
});
|
||||
|
||||
res.json({ results });
|
||||
} catch (error) {
|
||||
console.error('Error resolving identifiers:', error);
|
||||
res.status(500).json({ error: 'Failed to resolve identifiers' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get a single product
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user