Files
inventory/inventory-server/src/routes/import.js

1152 lines
40 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { Client } = require('ssh2');
const mysql = require('mysql2/promise');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
// Create uploads directory if it doesn't exist
const uploadsDir = path.join('/var/www/html/inventory/uploads/products');
const reusableUploadsDir = path.join('/var/www/html/inventory/uploads/reusable');
fs.mkdirSync(uploadsDir, { recursive: true });
fs.mkdirSync(reusableUploadsDir, { recursive: true });
// Create a Map to track image upload times and their scheduled deletion
const imageUploadMap = new Map();
// Connection pooling and cache configuration
const connectionCache = {
ssh: null,
dbConnection: null,
lastUsed: 0,
isConnecting: false,
connectionPromise: null,
// Cache expiration time in milliseconds (5 minutes)
expirationTime: 5 * 60 * 1000,
// Cache for query results (key: query string, value: {data, timestamp})
queryCache: new Map(),
// Cache duration for different query types in milliseconds
cacheDuration: {
'field-options': 30 * 60 * 1000, // 30 minutes for field options
'product-lines': 10 * 60 * 1000, // 10 minutes for product lines
'sublines': 10 * 60 * 1000, // 10 minutes for sublines
'default': 60 * 1000 // 1 minute default
}
};
// Function to schedule image deletion after 24 hours
const scheduleImageDeletion = (filename, filePath) => {
// Only schedule deletion for images in the products folder
if (!filePath.includes('/uploads/products/')) {
console.log(`Skipping deletion for non-product image: ${filename}`);
return;
}
// Delete any existing timeout for this file
if (imageUploadMap.has(filename)) {
clearTimeout(imageUploadMap.get(filename).timeoutId);
}
// Schedule deletion after 24 hours (24 * 60 * 60 * 1000 ms)
const timeoutId = setTimeout(() => {
console.log(`Auto-deleting image after 24 hours: ${filename}`);
// Check if file exists before trying to delete
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log(`Successfully auto-deleted image: ${filename}`);
} catch (error) {
console.error(`Error auto-deleting image ${filename}:`, error);
}
} else {
console.log(`File already deleted: ${filename}`);
}
// Remove from tracking map
imageUploadMap.delete(filename);
}, 24 * 60 * 60 * 1000); // 24 hours
// Store upload time and timeout ID
imageUploadMap.set(filename, {
uploadTime: new Date(),
timeoutId: timeoutId,
filePath: filePath
});
};
// Function to clean up scheduled deletions on server restart
const cleanupImagesOnStartup = () => {
console.log('Checking for images to clean up...');
// Check if uploads directory exists
if (!fs.existsSync(uploadsDir)) {
console.log('Uploads directory does not exist');
return;
}
// Read all files in the directory
fs.readdir(uploadsDir, (err, files) => {
if (err) {
console.error('Error reading uploads directory:', err);
return;
}
const now = new Date();
let countDeleted = 0;
files.forEach(filename => {
const filePath = path.join(uploadsDir, filename);
// Get file stats
try {
const stats = fs.statSync(filePath);
const fileCreationTime = stats.birthtime || stats.ctime; // birthtime might not be available on all systems
const ageMs = now.getTime() - fileCreationTime.getTime();
// If file is older than 24 hours, delete it
if (ageMs > 24 * 60 * 60 * 1000) {
fs.unlinkSync(filePath);
countDeleted++;
console.log(`Deleted old image on startup: ${filename} (age: ${Math.round(ageMs / (60 * 60 * 1000))} hours)`);
} else {
// Schedule deletion for remaining time
const remainingMs = (24 * 60 * 60 * 1000) - ageMs;
console.log(`Scheduling deletion for ${filename} in ${Math.round(remainingMs / (60 * 60 * 1000))} hours`);
const timeoutId = setTimeout(() => {
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log(`Successfully auto-deleted scheduled image: ${filename}`);
} catch (error) {
console.error(`Error auto-deleting scheduled image ${filename}:`, error);
}
}
imageUploadMap.delete(filename);
}, remainingMs);
imageUploadMap.set(filename, {
uploadTime: fileCreationTime,
timeoutId: timeoutId,
filePath: filePath
});
}
} catch (error) {
console.error(`Error processing file ${filename}:`, error);
}
});
console.log(`Cleanup completed: ${countDeleted} old images deleted, ${imageUploadMap.size} images scheduled for deletion`);
});
};
// Run cleanup on server start
cleanupImagesOnStartup();
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: function (req, file, cb) {
console.log(`Saving to: ${uploadsDir}`);
cb(null, uploadsDir);
},
filename: function (req, file, cb) {
// Create unique filename with original extension
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
// Make sure we preserve the original file extension
let fileExt = path.extname(file.originalname).toLowerCase();
// Ensure there is a proper extension based on mimetype if none exists
if (!fileExt) {
switch (file.mimetype) {
case 'image/jpeg': fileExt = '.jpg'; break;
case 'image/png': fileExt = '.png'; break;
case 'image/gif': fileExt = '.gif'; break;
case 'image/webp': fileExt = '.webp'; break;
default: fileExt = '.jpg'; // Default to jpg
}
}
const fileName = `${req.body.upc || 'product'}-${uniqueSuffix}${fileExt}`;
console.log(`Generated filename: ${fileName} with mimetype: ${file.mimetype}`);
cb(null, fileName);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB max file size
},
fileFilter: function (req, file, cb) {
// Accept only image files
const filetypes = /jpeg|jpg|png|gif|webp/;
const mimetype = filetypes.test(file.mimetype);
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
if (mimetype && extname) {
return cb(null, true);
}
cb(new Error('Only image files are allowed'));
}
});
// Modified function to get a database connection with connection pooling
async function getDbConnection() {
const now = Date.now();
// Check if we need to refresh the connection due to inactivity
const needsRefresh = !connectionCache.ssh ||
!connectionCache.dbConnection ||
(now - connectionCache.lastUsed > connectionCache.expirationTime);
// If connection is still valid, update last used time and return existing connection
if (!needsRefresh) {
connectionCache.lastUsed = now;
return {
ssh: connectionCache.ssh,
connection: connectionCache.dbConnection
};
}
// If another request is already establishing a connection, wait for that promise
if (connectionCache.isConnecting && connectionCache.connectionPromise) {
try {
await connectionCache.connectionPromise;
return {
ssh: connectionCache.ssh,
connection: connectionCache.dbConnection
};
} catch (error) {
// If that connection attempt failed, we'll try again below
console.error('Error waiting for existing connection:', error);
}
}
// Close existing connections if they exist
if (connectionCache.dbConnection) {
try {
await connectionCache.dbConnection.end();
} catch (error) {
console.error('Error closing existing database connection:', error);
}
}
if (connectionCache.ssh) {
try {
connectionCache.ssh.end();
} catch (error) {
console.error('Error closing existing SSH connection:', error);
}
}
// Mark that we're establishing a new connection
connectionCache.isConnecting = true;
// Create a new promise for this connection attempt
connectionCache.connectionPromise = setupSshTunnel().then(tunnel => {
const { ssh, stream, dbConfig } = tunnel;
return mysql.createConnection({
...dbConfig,
stream
}).then(connection => {
// Store the new connections
connectionCache.ssh = ssh;
connectionCache.dbConnection = connection;
connectionCache.lastUsed = Date.now();
connectionCache.isConnecting = false;
return {
ssh,
connection
};
});
}).catch(error => {
connectionCache.isConnecting = false;
throw error;
});
// Wait for the connection to be established
return connectionCache.connectionPromise;
}
// Helper function to get cached query results or execute query if not cached
async function getCachedQuery(cacheKey, queryType, queryFn) {
// Get cache duration based on query type
const cacheDuration = connectionCache.cacheDuration[queryType] || connectionCache.cacheDuration.default;
// Check if we have a valid cached result
const cachedResult = connectionCache.queryCache.get(cacheKey);
const now = Date.now();
if (cachedResult && (now - cachedResult.timestamp < cacheDuration)) {
console.log(`Cache hit for ${queryType} query: ${cacheKey}`);
return cachedResult.data;
}
// No valid cache found, execute the query
console.log(`Cache miss for ${queryType} query: ${cacheKey}`);
const result = await queryFn();
// Cache the result
connectionCache.queryCache.set(cacheKey, {
data: result,
timestamp: now
});
return result;
}
// Helper function to setup SSH tunnel - ONLY USED BY getDbConnection NOW
async function setupSshTunnel() {
const sshConfig = {
host: process.env.PROD_SSH_HOST,
port: process.env.PROD_SSH_PORT || 22,
username: process.env.PROD_SSH_USER,
privateKey: process.env.PROD_SSH_KEY_PATH
? require('fs').readFileSync(process.env.PROD_SSH_KEY_PATH)
: undefined,
compress: true
};
const dbConfig = {
host: process.env.PROD_DB_HOST || 'localhost',
user: process.env.PROD_DB_USER,
password: process.env.PROD_DB_PASSWORD,
database: process.env.PROD_DB_NAME,
port: process.env.PROD_DB_PORT || 3306,
timezone: 'Z'
};
return new Promise((resolve, reject) => {
const ssh = new Client();
ssh.on('error', (err) => {
console.error('SSH connection error:', err);
reject(err);
});
ssh.on('ready', () => {
ssh.forwardOut(
'127.0.0.1',
0,
dbConfig.host,
dbConfig.port,
(err, stream) => {
if (err) reject(err);
resolve({ ssh, stream, dbConfig });
}
);
}).connect(sshConfig);
});
}
// Image upload endpoint
router.post('/upload-image', upload.single('image'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image file provided' });
}
// Log file information
console.log('File uploaded:', {
filename: req.file.filename,
originalname: req.file.originalname,
mimetype: req.file.mimetype,
size: req.file.size,
path: req.file.path
});
// Ensure the file exists
const filePath = path.join(uploadsDir, req.file.filename);
if (!fs.existsSync(filePath)) {
return res.status(500).json({ error: 'File was not saved correctly' });
}
// Log file access permissions
fs.access(filePath, fs.constants.R_OK, (err) => {
if (err) {
console.error('File permission issue:', err);
} else {
console.log('File is readable');
}
});
// Create URL for the uploaded file - using an absolute URL with domain
// This will generate a URL like: https://inventory.acot.site/uploads/products/filename.jpg
const baseUrl = 'https://inventory.acot.site';
const imageUrl = `${baseUrl}/uploads/products/${req.file.filename}`;
// Schedule this image for deletion in 24 hours
scheduleImageDeletion(req.file.filename, filePath);
// Return success response with image URL
res.status(200).json({
success: true,
imageUrl,
fileName: req.file.filename,
mimetype: req.file.mimetype,
fullPath: filePath,
message: 'Image uploaded successfully (will auto-delete after 24 hours)'
});
} catch (error) {
console.error('Error uploading image:', error);
res.status(500).json({ error: error.message || 'Failed to upload image' });
}
});
// Image deletion endpoint
router.delete('/delete-image', (req, res) => {
try {
const { filename } = req.body;
if (!filename) {
return res.status(400).json({ error: 'Filename is required' });
}
const filePath = path.join(uploadsDir, filename);
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File not found' });
}
// Only allow deletion of images in the products folder
if (!filePath.includes('/uploads/products/')) {
return res.status(403).json({
error: 'Cannot delete images outside the products folder',
message: 'This image is in a protected folder and cannot be deleted through this endpoint'
});
}
// Delete the file
fs.unlinkSync(filePath);
// Clear any scheduled deletion for this file
if (imageUploadMap.has(filename)) {
clearTimeout(imageUploadMap.get(filename).timeoutId);
imageUploadMap.delete(filename);
}
// Return success response
res.status(200).json({
success: true,
message: 'Image deleted successfully'
});
} catch (error) {
console.error('Error deleting image:', error);
res.status(500).json({ error: error.message || 'Failed to delete image' });
}
});
// Get all options for import fields
router.get('/field-options', async (req, res) => {
try {
// Use cached connection
const { connection } = await getDbConnection();
const cacheKey = 'field-options';
const result = await getCachedQuery(cacheKey, 'field-options', async () => {
// Fetch companies (type 1)
const [companies] = await connection.query(`
SELECT cat_id, name
FROM product_categories
WHERE type = 1
ORDER BY name
`);
// Fetch artists (type 40)
const [artists] = await connection.query(`
SELECT cat_id, name
FROM product_categories
WHERE type = 40
ORDER BY name
`);
// Fetch sizes (type 50)
const [sizes] = await connection.query(`
SELECT cat_id, name
FROM product_categories
WHERE type = 50
ORDER BY name
`);
// Fetch themes with subthemes
const [themes] = await connection.query(`
SELECT t.cat_id, t.name AS display_name, t.type, t.name AS sort_theme,
'' AS sort_subtheme, 1 AS level_order
FROM product_categories t
WHERE t.type = 20
UNION ALL
SELECT ts.cat_id, CONCAT(t.name,' - ',ts.name) AS display_name, ts.type,
t.name AS sort_theme, ts.name AS sort_subtheme, 2 AS level_order
FROM product_categories ts
JOIN product_categories t ON ts.master_cat_id = t.cat_id
WHERE ts.type = 21 AND t.type = 20
ORDER BY sort_theme, sort_subtheme
`);
// Fetch categories with all levels
const [categories] = await connection.query(`
SELECT s.cat_id, s.name AS display_name, s.type, s.name AS sort_section,
'' AS sort_category, '' AS sort_subcategory, '' AS sort_subsubcategory,
1 AS level_order
FROM product_categories s
WHERE s.type = 10
UNION ALL
SELECT c.cat_id, CONCAT(s.name,' - ',c.name) AS display_name, c.type,
s.name AS sort_section, c.name AS sort_category, '' AS sort_subcategory,
'' AS sort_subsubcategory, 2 AS level_order
FROM product_categories c
JOIN product_categories s ON c.master_cat_id = s.cat_id
WHERE c.type = 11 AND s.type = 10
UNION ALL
SELECT sc.cat_id, CONCAT(s.name,' - ',c.name,' - ',sc.name) AS display_name,
sc.type, s.name AS sort_section, c.name AS sort_category,
sc.name AS sort_subcategory, '' AS sort_subsubcategory, 3 AS level_order
FROM product_categories sc
JOIN product_categories c ON sc.master_cat_id = c.cat_id
JOIN product_categories s ON c.master_cat_id = s.cat_id
WHERE sc.type = 12 AND c.type = 11 AND s.type = 10
UNION ALL
SELECT ssc.cat_id, CONCAT(s.name,' - ',c.name,' - ',sc.name,' - ',ssc.name) AS display_name,
ssc.type, s.name AS sort_section, c.name AS sort_category,
sc.name AS sort_subcategory, ssc.name AS sort_subsubcategory, 4 AS level_order
FROM product_categories ssc
JOIN product_categories sc ON ssc.master_cat_id = sc.cat_id
JOIN product_categories c ON sc.master_cat_id = c.cat_id
JOIN product_categories s ON c.master_cat_id = s.cat_id
WHERE ssc.type = 13 AND sc.type = 12 AND c.type = 11 AND s.type = 10
ORDER BY sort_section, sort_category, sort_subcategory, sort_subsubcategory
`);
// Fetch colors
const [colors] = await connection.query(`
SELECT color, name, hex_color
FROM product_color_list
ORDER BY \`order\`
`);
// Fetch suppliers
const [suppliers] = await connection.query(`
SELECT supplierid as value, companyname as label
FROM suppliers
WHERE companyname <> ''
ORDER BY companyname
`);
// Fetch tax categories
const [taxCategories] = await connection.query(`
SELECT CAST(tax_code_id AS CHAR) as value, name as label
FROM product_tax_codes
ORDER BY tax_code_id = 0 DESC, name
`);
// Format and return all options
return {
companies: companies.map(c => ({ label: c.name, value: c.cat_id.toString() })),
artists: artists.map(a => ({ label: a.name, value: a.cat_id.toString() })),
sizes: sizes.map(s => ({ label: s.name, value: s.cat_id.toString() })),
themes: themes.map(t => ({
label: t.display_name,
value: t.cat_id.toString(),
type: t.type,
level: t.level_order
})),
categories: categories.map(c => ({
label: c.display_name,
value: c.cat_id.toString(),
type: c.type,
level: c.level_order
})),
colors: colors.map(c => ({
label: c.name,
value: c.color,
hexColor: c.hex_color
})),
suppliers: suppliers,
taxCategories: taxCategories,
shippingRestrictions: [
{ label: "None", value: "0" },
{ label: "US Only", value: "1" },
{ label: "Limited Quantity", value: "2" },
{ label: "US/CA Only", value: "3" },
{ label: "No FedEx 2 Day", value: "4" },
{ label: "North America Only", value: "5" }
]
};
});
// Add debugging to verify category types
console.log(`Returning ${result.categories.length} categories with types: ${Array.from(new Set(result.categories.map(c => c.type))).join(', ')}`);
res.json(result);
} catch (error) {
console.error('Error fetching import field options:', error);
res.status(500).json({ error: 'Failed to fetch import field options' });
}
});
// Get product lines for a specific company
router.get('/product-lines/:companyId', async (req, res) => {
try {
// Use cached connection
const { connection } = await getDbConnection();
const companyId = req.params.companyId;
const cacheKey = `product-lines-${companyId}`;
const lines = await getCachedQuery(cacheKey, 'product-lines', async () => {
const [queryResult] = await connection.query(`
SELECT cat_id as value, name as label
FROM product_categories
WHERE type = 2
AND master_cat_id = ?
ORDER BY name
`, [companyId]);
return queryResult.map(l => ({ label: l.label, value: l.value.toString() }));
});
res.json(lines);
} catch (error) {
console.error('Error fetching product lines:', error);
res.status(500).json({ error: 'Failed to fetch product lines' });
}
});
// Get sublines for a specific product line
router.get('/sublines/:lineId', async (req, res) => {
try {
// Use cached connection
const { connection } = await getDbConnection();
const lineId = req.params.lineId;
const cacheKey = `sublines-${lineId}`;
const sublines = await getCachedQuery(cacheKey, 'sublines', async () => {
const [queryResult] = await connection.query(`
SELECT cat_id as value, name as label
FROM product_categories
WHERE type = 3
AND master_cat_id = ?
ORDER BY name
`, [lineId]);
return queryResult.map(s => ({ label: s.label, value: s.value.toString() }));
});
res.json(sublines);
} catch (error) {
console.error('Error fetching sublines:', error);
res.status(500).json({ error: 'Failed to fetch sublines' });
}
});
// Add a simple endpoint to check file existence and permissions
router.get('/check-file/:filename', (req, res) => {
const { filename } = req.params;
// Prevent directory traversal
if (filename.includes('..') || filename.includes('/')) {
return res.status(400).json({ error: 'Invalid filename' });
}
// First check in products directory
let filePath = path.join(uploadsDir, filename);
let exists = fs.existsSync(filePath);
// If not found in products, check in reusable directory
if (!exists) {
filePath = path.join(reusableUploadsDir, filename);
exists = fs.existsSync(filePath);
}
try {
// Check if file exists
if (!exists) {
return res.status(404).json({
error: 'File not found',
path: filePath,
exists: false,
readable: false
});
}
// Check if file is readable
fs.accessSync(filePath, fs.constants.R_OK);
// Get file stats
const stats = fs.statSync(filePath);
return res.json({
filename,
path: filePath,
exists: true,
readable: true,
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
permissions: stats.mode.toString(8)
});
} catch (error) {
return res.status(500).json({
error: error.message,
path: filePath,
exists: fs.existsSync(filePath),
readable: false
});
}
});
// List all files in uploads directory
router.get('/list-uploads', (req, res) => {
try {
const { directory = 'products' } = req.query;
// Determine which directory to list
let targetDir;
if (directory === 'reusable') {
targetDir = reusableUploadsDir;
} else {
targetDir = uploadsDir; // default to products
}
if (!fs.existsSync(targetDir)) {
return res.status(404).json({ error: 'Uploads directory not found', path: targetDir });
}
const files = fs.readdirSync(targetDir);
const fileDetails = files.map(file => {
const filePath = path.join(targetDir, file);
try {
const stats = fs.statSync(filePath);
return {
filename: file,
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
permissions: stats.mode.toString(8)
};
} catch (error) {
return { filename: file, error: error.message };
}
});
return res.json({
directory: targetDir,
type: directory,
count: files.length,
files: fileDetails
});
} catch (error) {
return res.status(500).json({ error: error.message });
}
});
// Search products from production database
router.get('/search-products', async (req, res) => {
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 = `
WHERE (
p.description LIKE ? OR
p.itemnumber LIKE ? OR
p.upc LIKE ? OR
pc1.name LIKE ? OR
s.companyname LIKE ?
)`;
// Add company filter if provided
if (company) {
whereClause += ` AND p.company = ${connection.escape(company)}`;
}
// Add date range filter if provided
if (dateRange) {
let dateCondition;
const now = new Date();
switch(dateRange) {
case '1week':
// Last week: date is after (current date - 7 days)
const weekAgo = new Date(now);
weekAgo.setDate(now.getDate() - 7);
dateCondition = `p.datein >= ${connection.escape(weekAgo.toISOString().slice(0, 10))}`;
break;
case '1month':
// Last month: date is after (current date - 30 days)
const monthAgo = new Date(now);
monthAgo.setDate(now.getDate() - 30);
dateCondition = `p.datein >= ${connection.escape(monthAgo.toISOString().slice(0, 10))}`;
break;
case '2months':
// Last 2 months: date is after (current date - 60 days)
const twoMonthsAgo = new Date(now);
twoMonthsAgo.setDate(now.getDate() - 60);
dateCondition = `p.datein >= ${connection.escape(twoMonthsAgo.toISOString().slice(0, 10))}`;
break;
case '3months':
// Last 3 months: date is after (current date - 90 days)
const threeMonthsAgo = new Date(now);
threeMonthsAgo.setDate(now.getDate() - 90);
dateCondition = `p.datein >= ${connection.escape(threeMonthsAgo.toISOString().slice(0, 10))}`;
break;
case '6months':
// Last 6 months: date is after (current date - 180 days)
const sixMonthsAgo = new Date(now);
sixMonthsAgo.setDate(now.getDate() - 180);
dateCondition = `p.datein >= ${connection.escape(sixMonthsAgo.toISOString().slice(0, 10))}`;
break;
case '1year':
// Last year: date is after (current date - 365 days)
const yearAgo = new Date(now);
yearAgo.setDate(now.getDate() - 365);
dateCondition = `p.datein >= ${connection.escape(yearAgo.toISOString().slice(0, 10))}`;
break;
default:
// If an unrecognized value is provided, don't add a date condition
dateCondition = null;
}
if (dateCondition) {
whereClause += ` AND ${dateCondition}`;
}
}
// Special case for wildcard search
const isWildcardSearch = q === '*';
const searchPattern = isWildcardSearch ? '%' : `%${q}%`;
const exactPattern = isWildcardSearch ? '%' : q;
// Search for products based on various fields
const query = `
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
${whereClause}
GROUP BY p.pid
${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
WHEN pc1.name LIKE ? THEN 4
WHEN s.companyname LIKE ? THEN 5
ELSE 6
END
`}
`;
// Prepare query parameters based on whether it's a wildcard search
let queryParams;
if (isWildcardSearch) {
queryParams = [
searchPattern, // LIKE for description
searchPattern, // LIKE for itemnumber
searchPattern, // LIKE for upc
searchPattern, // LIKE for brand name
searchPattern // LIKE for company name
];
} else {
queryParams = [
searchPattern, // LIKE for description
searchPattern, // LIKE for itemnumber
searchPattern, // LIKE for upc
searchPattern, // LIKE for brand name
searchPattern, // LIKE for company name
// For ORDER BY clause
searchPattern, // LIKE for description
exactPattern, // Exact match for itemnumber
exactPattern, // Exact match for upc
searchPattern, // LIKE for brand name
searchPattern // LIKE for company name
];
}
const [results] = await connection.query(query, queryParams);
// Debug log to check values
if (results.length > 0) {
console.log('Product search result sample fields:', {
pid: results[0].pid,
tax_code: results[0].tax_code,
tax_code_type: typeof results[0].tax_code,
tax_code_value: `Value: '${results[0].tax_code}'`,
size_cat: results[0].size_cat,
shipping_restrictions: results[0].shipping_restrictions,
supplier: results[0].supplier,
case_qty: results[0].case_qty,
moq: results[0].moq
});
}
res.json(results);
} catch (error) {
console.error('Error searching products:', error);
res.status(500).json({ error: 'Failed to search products' });
}
});
// Endpoint to check UPC and generate item number
router.get('/check-upc-and-generate-sku', async (req, res) => {
const { upc, supplierId } = req.query;
if (!upc || !supplierId) {
return res.status(400).json({ error: 'UPC and supplier ID are required' });
}
try {
const { connection } = await getDbConnection();
// Step 1: Check if the UPC already exists
const [upcCheck] = await connection.query(
'SELECT pid, itemnumber FROM products WHERE upc = ? LIMIT 1',
[upc]
);
if (upcCheck.length > 0) {
return res.status(409).json({
error: 'UPC already exists',
existingProductId: upcCheck[0].pid,
existingItemNumber: upcCheck[0].itemnumber
});
}
// Step 2: Generate item number - supplierId-last5DigitsOfUPC minus last digit
let itemNumber = '';
const upcStr = String(upc);
// Extract the last 5 digits of the UPC, removing the last digit (checksum)
// So we get 5 digits from positions: length-6 to length-2
if (upcStr.length >= 6) {
const lastFiveMinusOne = upcStr.substring(upcStr.length - 6, upcStr.length - 1);
itemNumber = `${supplierId}-${lastFiveMinusOne}`;
} else if (upcStr.length >= 5) {
// If UPC is shorter, use as many digits as possible
const digitsToUse = upcStr.substring(0, upcStr.length - 1);
itemNumber = `${supplierId}-${digitsToUse}`;
} else {
// Very short UPC, just use the whole thing
itemNumber = `${supplierId}-${upcStr}`;
}
// Step 3: Check if the generated item number exists
const [itemNumberCheck] = await connection.query(
'SELECT pid FROM products WHERE itemnumber = ? LIMIT 1',
[itemNumber]
);
// Step 4: If the item number exists, modify it to use the last 5 digits of the UPC
if (itemNumberCheck.length > 0) {
console.log(`Item number ${itemNumber} already exists, using alternative format`);
if (upcStr.length >= 5) {
// Use the last 5 digits (including the checksum)
const lastFive = upcStr.substring(upcStr.length - 5);
itemNumber = `${supplierId}-${lastFive}`;
// Check again if this new item number also exists
const [altItemNumberCheck] = await connection.query(
'SELECT pid FROM products WHERE itemnumber = ? LIMIT 1',
[itemNumber]
);
if (altItemNumberCheck.length > 0) {
// If even the alternative format exists, add a timestamp suffix for uniqueness
const timestamp = Date.now().toString().substring(8, 13); // Get last 5 digits of timestamp
itemNumber = `${supplierId}-${timestamp}`;
console.log(`Alternative item number also exists, using timestamp: ${itemNumber}`);
}
} else {
// For very short UPCs, add a timestamp
const timestamp = Date.now().toString().substring(8, 13); // Get last 5 digits of timestamp
itemNumber = `${supplierId}-${timestamp}`;
}
}
// Return the generated item number
res.json({
success: true,
itemNumber,
upc,
supplierId
});
} catch (error) {
console.error('Error checking UPC and generating item number:', error);
res.status(500).json({
error: 'Failed to check UPC and generate item number',
details: error.message
});
}
});
// Get product categories for a specific product
router.get('/product-categories/:pid', async (req, res) => {
try {
const { pid } = req.params;
if (!pid || isNaN(parseInt(pid))) {
return res.status(400).json({ error: 'Valid product ID is required' });
}
// Use the getDbConnection function instead of getPool
const { connection } = await getDbConnection();
// Query to get categories for a specific product
const query = `
SELECT pc.cat_id, pc.name, pc.type, pc.combined_name, pc.master_cat_id
FROM product_category_index pci
JOIN product_categories pc ON pci.cat_id = pc.cat_id
WHERE pci.pid = ?
ORDER BY pc.type, pc.name
`;
const [rows] = await connection.query(query, [pid]);
// Add debugging to log category types
const categoryTypes = rows.map(row => row.type);
const uniqueTypes = [...new Set(categoryTypes)];
console.log(`Product ${pid} has ${rows.length} categories with types: ${uniqueTypes.join(', ')}`);
console.log('Categories:', rows.map(row => ({ id: row.cat_id, name: row.name, type: row.type })));
// Check for parent categories to filter out deals and black friday
const sectionQuery = `
SELECT pc.cat_id, pc.name
FROM product_categories pc
WHERE pc.type = 10 AND (LOWER(pc.name) LIKE '%deal%' OR LOWER(pc.name) LIKE '%black friday%')
`;
const [dealSections] = await connection.query(sectionQuery);
const dealSectionIds = dealSections.map(section => section.cat_id);
console.log('Filtering out categories from deal sections:', dealSectionIds);
// Filter out categories from deals and black friday sections
const filteredCategories = rows.filter(category => {
// Direct check for top-level deal sections
if (category.type === 10) {
return !dealSectionIds.some(id => id === category.cat_id);
}
// For categories (type 11), check if their parent is a deal section
if (category.type === 11) {
return !dealSectionIds.some(id => id === category.master_cat_id);
}
// For subcategories (type 12), get their parent category first
if (category.type === 12) {
const parentId = category.master_cat_id;
// Find the parent category in our rows
const parentCategory = rows.find(c => c.cat_id === parentId);
// If parent not found or parent's parent is not a deal section, keep it
return !parentCategory || !dealSectionIds.some(id => id === parentCategory.master_cat_id);
}
// For subsubcategories (type 13), check their hierarchy manually
if (category.type === 13) {
const parentId = category.master_cat_id;
// Find the parent subcategory
const parentSubcategory = rows.find(c => c.cat_id === parentId);
if (!parentSubcategory) return true;
// Find the grandparent category
const grandparentId = parentSubcategory.master_cat_id;
const grandparentCategory = rows.find(c => c.cat_id === grandparentId);
// If grandparent not found or grandparent's parent is not a deal section, keep it
return !grandparentCategory || !dealSectionIds.some(id => id === grandparentCategory.master_cat_id);
}
// Keep all other category types
return true;
});
console.log(`Filtered out ${rows.length - filteredCategories.length} deal/black friday categories`);
// Format the response to match the expected format in the frontend
const categories = filteredCategories.map(category => ({
value: category.cat_id.toString(),
label: category.name,
type: category.type,
combined_name: category.combined_name
}));
res.json(categories);
} catch (error) {
console.error('Error fetching product categories:', error);
res.status(500).json({
error: 'Failed to fetch product categories',
details: error.message
});
}
});
module.exports = router;