Add UPC validation and automatic item number generation/validation
This commit is contained in:
@@ -899,6 +899,100 @@ router.get('/search-products', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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-last6DigitsOfUPC minus last digit
|
||||||
|
let itemNumber = '';
|
||||||
|
const upcStr = String(upc);
|
||||||
|
|
||||||
|
// Extract the last 6 digits of the UPC, removing the last digit (checksum)
|
||||||
|
// So we get 5 digits from positions: length-7 to length-2
|
||||||
|
if (upcStr.length >= 7) {
|
||||||
|
const lastSixMinusOne = upcStr.substring(upcStr.length - 7, upcStr.length - 1);
|
||||||
|
itemNumber = `${supplierId}-${lastSixMinusOne}`;
|
||||||
|
} else if (upcStr.length >= 6) {
|
||||||
|
// 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
|
// Get product categories for a specific product
|
||||||
router.get('/product-categories/:pid', async (req, res) => {
|
router.get('/product-categories/:pid', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -269,10 +269,6 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleProductSelect = (product: Product) => {
|
const handleProductSelect = (product: Product) => {
|
||||||
console.log('Selected product supplier data:', {
|
|
||||||
vendor: product.vendor,
|
|
||||||
vendor_reference: product.vendor_reference
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure all values are of the correct type
|
// Ensure all values are of the correct type
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -303,17 +299,13 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
|
|
||||||
// Try to find the supplier ID from the vendor name
|
// Try to find the supplier ID from the vendor name
|
||||||
if (product.vendor && fieldOptions) {
|
if (product.vendor && fieldOptions) {
|
||||||
console.log('Available suppliers:', fieldOptions.suppliers);
|
|
||||||
console.log('Looking for supplier match for vendor:', product.vendor);
|
|
||||||
|
|
||||||
// First try exact match
|
|
||||||
let supplierOption = fieldOptions.suppliers.find(
|
let supplierOption = fieldOptions.suppliers.find(
|
||||||
supplier => supplier.label.toLowerCase() === product.vendor.toLowerCase()
|
supplier => supplier.label.toLowerCase() === product.vendor.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
// If no exact match, try partial match
|
// If no exact match, try partial match
|
||||||
if (!supplierOption) {
|
if (!supplierOption) {
|
||||||
console.log('No exact match found, trying partial match');
|
|
||||||
supplierOption = fieldOptions.suppliers.find(
|
supplierOption = fieldOptions.suppliers.find(
|
||||||
supplier => supplier.label.toLowerCase().includes(product.vendor.toLowerCase()) ||
|
supplier => supplier.label.toLowerCase().includes(product.vendor.toLowerCase()) ||
|
||||||
product.vendor.toLowerCase().includes(supplier.label.toLowerCase())
|
product.vendor.toLowerCase().includes(supplier.label.toLowerCase())
|
||||||
@@ -325,11 +317,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
...prev,
|
...prev,
|
||||||
supplier: supplierOption.value
|
supplier: supplierOption.value
|
||||||
}));
|
}));
|
||||||
console.log('Found supplier match:', {
|
|
||||||
vendorName: product.vendor,
|
|
||||||
matchedSupplier: supplierOption.label,
|
|
||||||
supplierId: supplierOption.value
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.log('No supplier match found for vendor:', product.vendor);
|
console.log('No supplier match found for vendor:', product.vendor);
|
||||||
}
|
}
|
||||||
@@ -337,7 +325,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
|
|
||||||
// Fetch product categories
|
// Fetch product categories
|
||||||
if (product.pid) {
|
if (product.pid) {
|
||||||
console.log('Fetching categories for product ID:', product.pid);
|
|
||||||
fetchProductCategories(product.pid);
|
fetchProductCategories(product.pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +336,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
const fetchProductCategories = async (productId: number) => {
|
const fetchProductCategories = async (productId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/import/product-categories/${productId}`);
|
const response = await axios.get(`/api/import/product-categories/${productId}`);
|
||||||
console.log('Product categories:', response.data);
|
|
||||||
|
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (response.data && Array.isArray(response.data)) {
|
||||||
// Filter out categories with type 20 (themes) and type 21 (subthemes)
|
// Filter out categories with type 20 (themes) and type 21 (subthemes)
|
||||||
@@ -356,7 +344,6 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
category.type !== 20 && category.type !== 21
|
category.type !== 20 && category.type !== 21
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Filtered categories (excluding themes):', filteredCategories);
|
|
||||||
|
|
||||||
// Extract category IDs and update form data
|
// Extract category IDs and update form data
|
||||||
const categoryIds = filteredCategories.map((category: any) => category.value);
|
const categoryIds = filteredCategories.map((category: any) => category.value);
|
||||||
@@ -422,28 +409,14 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
|
|
||||||
// Log supplier information for debugging
|
// Log supplier information for debugging
|
||||||
if (formData.supplier) {
|
if (formData.supplier) {
|
||||||
const supplierOption = fieldOptions?.suppliers.find(
|
|
||||||
supplier => supplier.value === formData.supplier
|
|
||||||
);
|
|
||||||
console.log('Submitting supplier:', {
|
|
||||||
id: formData.supplier,
|
|
||||||
name: supplierOption?.label || 'Unknown',
|
|
||||||
allSuppliers: fieldOptions?.suppliers.map(s => ({ id: s.value, name: s.label }))
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.log('No supplier selected for submission');
|
console.log('No supplier selected for submission');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log categories information for debugging
|
// Log categories information for debugging
|
||||||
if (formData.categories && formData.categories.length > 0) {
|
if (formData.categories && formData.categories.length > 0) {
|
||||||
const categoryOptions = formData.categories.map(catId => {
|
|
||||||
const category = fieldOptions?.categories.find(c => c.value === catId);
|
|
||||||
return {
|
|
||||||
id: catId,
|
|
||||||
name: category?.label || 'Unknown Category'
|
|
||||||
};
|
|
||||||
});
|
|
||||||
console.log('Submitting categories:', categoryOptions);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('No categories selected for submission');
|
console.log('No categories selected for submission');
|
||||||
}
|
}
|
||||||
@@ -471,11 +444,9 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
ship_restrictions: formData.ship_restrictions || null
|
ship_restrictions: formData.ship_restrictions || null
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Sending template data:', dataToSend);
|
|
||||||
|
|
||||||
const response = await axios.post('/api/templates', dataToSend);
|
const response = await axios.post('/api/templates', dataToSend);
|
||||||
|
|
||||||
console.log('Template creation response:', response);
|
|
||||||
|
|
||||||
if (response.status >= 200 && response.status < 300) {
|
if (response.status >= 200 && response.status < 300) {
|
||||||
toast.success('Template created successfully');
|
toast.success('Template created successfully');
|
||||||
@@ -1041,8 +1012,6 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
category => category.type && validCategoryTypes.includes(category.type)
|
category => category.type && validCategoryTypes.includes(category.type)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Filtered categories for dropdown:', filteredCategories.length);
|
|
||||||
|
|
||||||
return [...filteredCategories].sort((a, b) => {
|
return [...filteredCategories].sort((a, b) => {
|
||||||
const aSelected = selected.has(a.value);
|
const aSelected = selected.has(a.value);
|
||||||
const bSelected = selected.has(b.value);
|
const bSelected = selected.has(b.value);
|
||||||
@@ -1171,10 +1140,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
|||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
// Make sure we're setting the ID (value), not the label
|
// Make sure we're setting the ID (value), not the label
|
||||||
handleSelectChange('supplier', supplier.value);
|
handleSelectChange('supplier', supplier.value);
|
||||||
console.log('Selected supplier from dropdown:', {
|
|
||||||
label: supplier.label,
|
|
||||||
value: supplier.value
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -65,7 +65,7 @@ export type Data<T extends string> = {
|
|||||||
// Data model RSI uses for spreadsheet imports
|
// Data model RSI uses for spreadsheet imports
|
||||||
export type Fields<T extends string> = DeepReadonly<Field<T>[]>
|
export type Fields<T extends string> = DeepReadonly<Field<T>[]>
|
||||||
|
|
||||||
export type Field<T extends string = string> = {
|
export type Field<T extends string> = {
|
||||||
// UI-facing field label
|
// UI-facing field label
|
||||||
label: string
|
label: string
|
||||||
// Field's unique identifier
|
// Field's unique identifier
|
||||||
@@ -75,14 +75,14 @@ export type Field<T extends string = string> = {
|
|||||||
// Alternate labels used for fields' auto-matching, e.g. "fname" -> "firstName"
|
// Alternate labels used for fields' auto-matching, e.g. "fname" -> "firstName"
|
||||||
alternateMatches?: string[]
|
alternateMatches?: string[]
|
||||||
// Validations used for field entries
|
// Validations used for field entries
|
||||||
validations?: Validation[]
|
validations?: ValidationConfig[]
|
||||||
// Field entry component
|
// Field entry component
|
||||||
fieldType: FieldType
|
fieldType: FieldType
|
||||||
// UI-facing values shown to user as field examples pre-upload phase
|
// UI-facing values shown to user as field examples pre-upload phase
|
||||||
example?: string
|
example?: string
|
||||||
width?: number
|
width?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: any, additionalData?: any) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldType =
|
export type FieldType =
|
||||||
|
|||||||
@@ -23,6 +23,39 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
width: 220,
|
width: 220,
|
||||||
validations: [{ rule: "required" as const, errorMessage: "Required", level: "error" as ErrorLevel }],
|
validations: [{ rule: "required" as const, errorMessage: "Required", level: "error" as ErrorLevel }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Company",
|
||||||
|
key: "company",
|
||||||
|
description: "Company/Brand name",
|
||||||
|
fieldType: {
|
||||||
|
type: "select",
|
||||||
|
options: [], // Will be populated from API
|
||||||
|
},
|
||||||
|
width: 200,
|
||||||
|
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Line",
|
||||||
|
key: "line",
|
||||||
|
description: "Product line",
|
||||||
|
alternateMatches: ["collection"],
|
||||||
|
fieldType: {
|
||||||
|
type: "select",
|
||||||
|
options: [], // Will be populated dynamically based on company selection
|
||||||
|
},
|
||||||
|
width: 180,
|
||||||
|
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Sub Line",
|
||||||
|
key: "subline",
|
||||||
|
description: "Product sub-line",
|
||||||
|
fieldType: {
|
||||||
|
type: "select",
|
||||||
|
options: [], // Will be populated dynamically based on line selection
|
||||||
|
},
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "UPC",
|
label: "UPC",
|
||||||
key: "upc",
|
key: "upc",
|
||||||
@@ -78,7 +111,7 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
key: "item_number",
|
key: "item_number",
|
||||||
description: "Internal item reference number",
|
description: "Internal item reference number",
|
||||||
fieldType: { type: "input" },
|
fieldType: { type: "input" },
|
||||||
width: 120,
|
width: 130,
|
||||||
validations: [
|
validations: [
|
||||||
{ rule: "required", errorMessage: "Required", level: "error" },
|
{ rule: "required", errorMessage: "Required", level: "error" },
|
||||||
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
|
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
|
||||||
@@ -148,39 +181,6 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
width: 180,
|
width: 180,
|
||||||
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Company",
|
|
||||||
key: "company",
|
|
||||||
description: "Company/Brand name",
|
|
||||||
fieldType: {
|
|
||||||
type: "select",
|
|
||||||
options: [], // Will be populated from API
|
|
||||||
},
|
|
||||||
width: 200,
|
|
||||||
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Line",
|
|
||||||
key: "line",
|
|
||||||
description: "Product line",
|
|
||||||
alternateMatches: ["collection"],
|
|
||||||
fieldType: {
|
|
||||||
type: "select",
|
|
||||||
options: [], // Will be populated dynamically based on company selection
|
|
||||||
},
|
|
||||||
width: 180,
|
|
||||||
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Sub Line",
|
|
||||||
key: "subline",
|
|
||||||
description: "Product sub-line",
|
|
||||||
fieldType: {
|
|
||||||
type: "select",
|
|
||||||
options: [], // Will be populated dynamically based on line selection
|
|
||||||
},
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "Artist",
|
label: "Artist",
|
||||||
key: "artist",
|
key: "artist",
|
||||||
|
|||||||
Reference in New Issue
Block a user