Add product search and template creation functionality to validation step

This commit is contained in:
2025-03-01 12:24:04 -05:00
parent e0a7787139
commit f7bdefb0a3
7 changed files with 2855 additions and 560 deletions

View File

@@ -566,6 +566,9 @@ router.get('/field-options', async (req, res) => {
};
});
// 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);
@@ -715,4 +718,232 @@ router.get('/list-uploads', (req, res) => {
}
});
// 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,
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,
p.totalsold AS total_sold,
p.datein AS first_received,
pls.date_sold AS date_last_sold
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
${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);
res.json(results);
} catch (error) {
console.error('Error searching products:', error);
res.status(500).json({ error: 'Failed to search products' });
}
});
// 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
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 })));
// Format the response to match the expected format in the frontend
const categories = rows.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;

View File

@@ -42,7 +42,7 @@
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.6",
@@ -57,6 +57,7 @@
"@tanstack/virtual-core": "^3.11.2",
"@types/js-levenshtein": "^1.1.3",
"@types/uuid": "^10.0.0",
"axios": "^1.8.1",
"chakra-react-select": "^4.7.5",
"chakra-ui-steps": "^2.0.4",
"chart.js": "^4.4.7",
@@ -5316,6 +5317,12 @@
"node": ">=10"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
@@ -5363,6 +5370,17 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@@ -5507,6 +5525,19 @@
"ieee754": "^1.2.1"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -6185,6 +6216,18 @@
"integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
@@ -6491,6 +6534,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
@@ -6528,6 +6580,20 @@
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -6556,6 +6622,51 @@
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
@@ -7041,6 +7152,26 @@
"node": ">=10"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
@@ -7069,6 +7200,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -7193,6 +7339,30 @@
"node": ">=6.9.0"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -7202,6 +7372,19 @@
"node": ">=6"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -7283,6 +7466,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -7306,6 +7501,33 @@
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -7770,6 +7992,15 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
@@ -7804,6 +8035,27 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@@ -8474,6 +8726,12 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@@ -44,7 +44,7 @@
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.6",
@@ -59,6 +59,7 @@
"@tanstack/virtual-core": "^3.11.2",
"@types/js-levenshtein": "^1.1.3",
"@types/uuid": "^10.0.0",
"axios": "^1.8.1",
"chakra-react-select": "^4.7.5",
"chakra-ui-steps": "^2.0.4",
"chart.js": "^4.4.7",

File diff suppressed because it is too large Load Diff

View File

@@ -296,24 +296,54 @@ export const ImageUploadStep = <T extends string>({
// Get the current product
const product = newData[productIndex];
// We need to update product_images array directly instead of the image_url field
// Initialize product_images array if it doesn't exist
if (!product.product_images) {
product.product_images = [];
} else if (typeof product.product_images === 'string') {
// Handle case where it might be a comma-separated string
product.product_images = product.product_images.split(',').filter(Boolean);
return newData;
}
// Handle different formats of product_images
let images: any[] = [];
if (typeof product.product_images === 'string') {
try {
// Try to parse as JSON
images = JSON.parse(product.product_images);
} catch (e) {
// If not JSON, split by comma if it's a string
images = product.product_images.split(',').filter(Boolean).map(url => ({
imageUrl: url.trim(),
pid: product.id || 0,
iid: 0,
type: 0,
order: 255,
width: 0,
height: 0,
hidden: 0
}));
}
} else if (Array.isArray(product.product_images)) {
// Use the array directly
images = product.product_images;
} else if (product.product_images) {
// Handle case where it might be a single value
images = [product.product_images];
}
// Filter out the image URL we're removing
if (Array.isArray(product.product_images)) {
product.product_images = product.product_images.filter((url: string) => url && url !== imageUrl);
}
const filteredImages = images.filter(img => {
const imgUrl = typeof img === 'string' ? img : img.imageUrl;
return imgUrl !== imageUrl;
});
// Update the product_images field
product.product_images = filteredImages;
return newData;
};
// Function to add an image URL to a product
const addImageToProduct = (productIndex: number, imageUrl: string) => {
const addImageToProduct = (productIndex: number, imageUrl: string, imageData?: any) => {
// Create a copy of the data
const newData = [...data];
@@ -323,21 +353,76 @@ export const ImageUploadStep = <T extends string>({
// Initialize product_images array if it doesn't exist
if (!product.product_images) {
product.product_images = [];
} else if (typeof product.product_images === 'string') {
// Handle case where it might be a comma-separated string
product.product_images = product.product_images.split(',').filter(Boolean);
}
// Ensure it's an array
if (!Array.isArray(product.product_images)) {
product.product_images = [product.product_images].filter(Boolean);
// Handle different formats of product_images
let images: any[] = [];
if (typeof product.product_images === 'string') {
try {
// Try to parse as JSON
images = JSON.parse(product.product_images);
} catch (e) {
// If not JSON, split by comma if it's a string
images = product.product_images.split(',').filter(Boolean).map(url => ({
imageUrl: url.trim(),
pid: product.id || 0,
iid: 0,
type: 0,
order: 255,
width: 0,
height: 0,
hidden: 0
}));
}
} else if (Array.isArray(product.product_images)) {
// Use the array directly
images = product.product_images;
} else if (product.product_images) {
// Handle case where it might be a single value
images = [product.product_images];
}
// Check if the image URL already exists
const exists = images.some(img => {
const imgUrl = typeof img === 'string' ? img : img.imageUrl;
return imgUrl === imageUrl;
});
// Only add if the URL doesn't already exist
if (!product.product_images.includes(imageUrl)) {
product.product_images.push(imageUrl);
if (!exists) {
// Create a new image object with schema fields
const newImage = imageData || {
imageUrl,
pid: product.id || 0,
iid: Math.floor(Math.random() * 10000), // Generate a temporary iid
type: 0,
order: images.length,
width: 0,
height: 0,
hidden: 0
};
// If imageData is a string, convert it to an object
if (typeof newImage === 'string') {
newImage = {
imageUrl: newImage,
pid: product.id || 0,
iid: Math.floor(Math.random() * 10000),
type: 0,
order: images.length,
width: 0,
height: 0,
hidden: 0
};
}
images.push(newImage);
}
// Update the product_images field
product.product_images = images;
return newData;
};
@@ -530,7 +615,15 @@ export const ImageUploadStep = <T extends string>({
productIndex,
imageUrl: '',
loading: true,
fileName: file.name
fileName: file.name,
// Add schema fields
pid: data[productIndex].id || 0,
iid: 0, // Will be assigned by server
type: 0,
order: productImages.filter(img => img.productIndex === productIndex).length + i,
width: 0,
height: 0,
hidden: 0
};
setProductImages(prev => [...prev, newImage]);
@@ -559,13 +652,21 @@ export const ImageUploadStep = <T extends string>({
setProductImages(prev =>
prev.map(img =>
(img.loading && img.productIndex === productIndex && img.fileName === file.name)
? { ...img, imageUrl: result.imageUrl, loading: false }
? {
...img,
imageUrl: result.imageUrl,
loading: false,
// Update schema fields if returned from server
iid: result.iid || img.iid,
width: result.width || img.width,
height: result.height || img.height
}
: img
)
);
// Update the product data with the new image URL
addImageToProduct(productIndex, result.imageUrl);
addImageToProduct(productIndex, result.imageUrl, result);
toast.success(`Image uploaded for ${data[productIndex].name || `Product #${productIndex + 1}`}`);
} catch (error) {
@@ -1305,20 +1406,33 @@ export const ImageUploadStep = <T extends string>({
// Create a unique ID for this image
const imageId = `image-${productIndex}-url-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
// Get the next order value for this product
const nextOrder = productImages
.filter(img => img.productIndex === productIndex)
.length;
// Create the new image object with the URL
const newImage: ProductImageSortable = {
id: imageId,
productIndex,
imageUrl: validatedUrl,
loading: false, // We're not loading from server, so it's ready immediately
fileName: "From URL"
fileName: "From URL",
// Add schema fields
pid: data[productIndex].id || 0,
iid: Math.floor(Math.random() * 10000), // Generate a temporary iid
type: 0,
order: nextOrder,
width: 0,
height: 0,
hidden: 0
};
// Add the image directly to the product images list
setProductImages(prev => [...prev, newImage]);
// Update the product data with the new image URL
addImageToProduct(productIndex, validatedUrl);
addImageToProduct(productIndex, validatedUrl, newImage);
// Clear the URL input field on success
setUrlInputs(prev => ({ ...prev, [productIndex]: '' }));

View File

@@ -3,7 +3,7 @@ import { useRsi } from "../../hooks/useRsi"
import type { Meta, Error } from "./types"
import { addErrorsAndRunHooks } from "./utils/dataMutations"
import type { Data, SelectOption, Result, Fields, Field } from "../../types"
import { Check, ChevronsUpDown, ArrowDown, AlertCircle, Loader2, X, Plus } from "lucide-react"
import { Check, ChevronsUpDown, ArrowDown, AlertCircle, Loader2, X, Plus, Edit3 } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Command,
@@ -79,6 +79,7 @@ import { useQuery } from "@tanstack/react-query"
import { Badge } from "@/components/ui/badge"
import { CheckIcon } from "lucide-react"
import * as Diff from 'diff'
import { ProductSearchDialog } from '../../../../../components/products/ProductSearchDialog';
// Template interface
interface Template {
@@ -845,9 +846,8 @@ function useTemplates<T extends string>(
newTemplateType: "",
})
// Fetch templates
useEffect(() => {
const fetchTemplates = async () => {
// Function to fetch templates
const fetchTemplates = useCallback(async () => {
try {
const response = await fetch(`${config.apiUrl}/templates`)
if (!response.ok) throw new Error('Failed to fetch templates')
@@ -861,10 +861,25 @@ function useTemplates<T extends string>(
variant: "destructive",
})
}
}
fetchTemplates()
}, [toast])
// Fetch templates on mount
useEffect(() => {
fetchTemplates()
// Add event listener for template refresh
const handleRefreshTemplates = () => {
fetchTemplates()
}
window.addEventListener('refresh-templates', handleRefreshTemplates)
// Clean up event listener
return () => {
window.removeEventListener('refresh-templates', handleRefreshTemplates)
}
}, [fetchTemplates])
const applyTemplate = useCallback(async (templateId: string, rowIndices?: number[]) => {
if (!templateId) return
@@ -891,7 +906,64 @@ function useTemplates<T extends string>(
row[key as keyof typeof row] = value as any;
}
}
// Handle array values
// Special handling for categories field
else if (key === 'categories') {
console.log('Applying categories from template:', {
key,
value,
type: typeof value,
isArray: Array.isArray(value)
});
// If categories is an array, use it directly
if (Array.isArray(value)) {
row[key as keyof typeof row] = value as any;
console.log('Categories is array, using directly:', value);
}
// If categories is a string (possibly a PostgreSQL array representation), parse it
else if (typeof value === 'string') {
try {
// Try to parse as JSON if it's a JSON string
if (value.startsWith('[') && value.endsWith(']')) {
const parsed = JSON.parse(value);
row[key as keyof typeof row] = parsed as any;
console.log('Categories parsed from JSON string:', parsed);
}
// Handle PostgreSQL array format like {value1,value2}
else if (value.startsWith('{') && value.endsWith('}')) {
const parsed = value.substring(1, value.length - 1).split(',');
row[key as keyof typeof row] = parsed as any;
console.log('Categories parsed from PostgreSQL array:', parsed);
}
// If it's a comma-separated string
else if (value.includes(',')) {
const parsed = value.split(',');
row[key as keyof typeof row] = parsed as any;
console.log('Categories parsed from comma-separated string:', parsed);
}
// If it's a single value
else if (value.trim()) {
const parsed = [value.trim()];
row[key as keyof typeof row] = parsed as any;
console.log('Categories parsed from single value:', parsed);
}
// Empty value
else {
row[key as keyof typeof row] = [] as any;
console.log('Categories is empty string, using empty array');
}
} catch (e) {
console.error('Error parsing categories:', e);
row[key as keyof typeof row] = [] as any;
}
}
// Default to empty array for any other case
else {
row[key as keyof typeof row] = [] as any;
console.log('Categories is not array or string, using empty array');
}
}
// Handle other array values
else if (Array.isArray(value)) {
row[key as keyof typeof row] = [...value] as any;
}
@@ -913,7 +985,7 @@ function useTemplates<T extends string>(
title: "Template Applied",
description: `Applied template to ${rowIndices?.length || data.length} row(s)`,
})
}, [templates, data.length, setData, toast])
}, [templates, setData, data.length, toast])
const saveAsTemplate = useCallback(async () => {
const { newTemplateName, newTemplateType } = state
@@ -1131,6 +1203,7 @@ export const ValidationStep = <T extends string>({
// Track which changes have been reverted
const [revertedChanges, setRevertedChanges] = useState<Set<string>>(new Set());
const [isProductSearchDialogOpen, setIsProductSearchDialogOpen] = useState(false);
// Fetch product lines when company is selected
const { data: productLines } = useQuery({
@@ -1342,6 +1415,7 @@ export const ValidationStep = <T extends string>({
isLoading: false,
});
// Get template state and functions from the useTemplates hook
const {
templates,
selectedTemplateId,
@@ -2268,7 +2342,37 @@ export const ValidationStep = <T extends string>({
return revertedChanges.has(revertKey);
};
// Function to refresh templates after a new one is created
const refreshTemplates = useCallback(async () => {
try {
const response = await fetch(`${config.apiUrl}/templates`);
if (!response.ok) throw new Error('Failed to fetch templates');
// We don't need to manually update templates state as it's managed by the useTemplates hook
// Just trigger a toast notification to inform the user
toast({
title: 'Templates refreshed',
description: 'Template list has been updated',
});
// Force a refetch of templates by re-running the useEffect in useTemplates
// This is a workaround since we don't have direct access to setTemplates
const fetchEvent = new CustomEvent('refresh-templates');
window.dispatchEvent(fetchEvent);
} catch (error) {
console.error('Error refreshing templates:', error);
toast({
title: 'Error',
description: 'Failed to refresh templates',
variant: 'destructive'
});
}
}, [toast]);
return (
<div className="flex flex-col h-[calc(100vh-10rem)] overflow-hidden">
<div className="flex-1 overflow-hidden">
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
<CopyDownDialog
isOpen={!!copyDownField}
@@ -2609,7 +2713,7 @@ export const ValidationStep = <T extends string>({
<div className="px-8 pt-6">
<div className="mb-6 flex flex-col gap-4">
<div className="flex flex-wrap items-center justify-between">
<h2 className="text-3xl font-semibold text-foreground">
<h2 className="text-2xl font-semibold text-foreground">
{translations.validationStep.title}
</h2>
<div className="flex items-center gap-4">
@@ -2631,6 +2735,13 @@ export const ValidationStep = <T extends string>({
Add Row
</Button>
)}
<Button
variant="outline"
onClick={() => setIsProductSearchDialogOpen(true)}
>
<Edit3 className="h-4 w-4" />
Create New Template
</Button>
<div className="flex items-center gap-2">
<Switch
checked={filterByErrors}
@@ -2713,10 +2824,21 @@ export const ValidationStep = <T extends string>({
{Object.keys(rowSelection).length > 0 && (
<div className="fixed bottom-24 left-1/2 transform -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-5 duration-300">
<div className="bg-card shadow-xl rounded-lg border border-muted px-4 py-3 flex items-center gap-3">
<div className="mr-2 bg-muted text-primary px-2 py-1 rounded-md text-xs font-medium border border-primary">
<div className="flex items-center gap-2">
<div className="mr-2 bg-muted items-center flex text-primary pl-2 pr-7 h-[32px] flex-shrink-0 rounded-md text-xs font-medium border border-primary">
{Object.keys(rowSelection).length} selected
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setRowSelection({})}
className="h-8 w-8 -ml-[48px] hover:bg-transparent hover:text-muted-foreground"
title="Clear selection"
>
<X className="h-4 w-4" />
</Button>
</div>
<Select
value={selectedTemplateId || ""}
onValueChange={(value) => {
@@ -2755,15 +2877,6 @@ export const ValidationStep = <T extends string>({
{isFromScratch ? "Delete Row" : translations.validationStep.discardButtonTitle}
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setRowSelection({})}
className="h-9 w-9"
title="Clear selection"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
)}
@@ -2803,6 +2916,15 @@ export const ValidationStep = <T extends string>({
</div>
</div>
</div>
</div>
</div>
<ProductSearchDialog
isOpen={isProductSearchDialogOpen}
onClose={() => setIsProductSearchDialogOpen(false)}
onTemplateCreated={refreshTemplates}
/>
</div>
)
}

View File

@@ -314,7 +314,7 @@ const BASE_IMPORT_FIELDS = [
key: "categories",
description: "Product categories",
fieldType: {
type: "select",
type: "multi-select",
options: [], // Will be populated from API
},
width: 350,
@@ -484,7 +484,7 @@ export function Import() {
return {
...field,
fieldType: {
type: "select" as const,
type: "multi-select" as const,
options: fieldOptions.categories || [],
},
};