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
|
||||
router.get('/product-categories/:pid', async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -269,10 +269,6 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
};
|
||||
|
||||
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
|
||||
setFormData({
|
||||
@@ -303,17 +299,13 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
|
||||
// Try to find the supplier ID from the vendor name
|
||||
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()
|
||||
);
|
||||
|
||||
// If no exact match, try partial match
|
||||
if (!supplierOption) {
|
||||
console.log('No exact match found, trying partial match');
|
||||
|
||||
supplierOption = fieldOptions.suppliers.find(
|
||||
supplier => supplier.label.toLowerCase().includes(product.vendor.toLowerCase()) ||
|
||||
product.vendor.toLowerCase().includes(supplier.label.toLowerCase())
|
||||
@@ -325,11 +317,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
...prev,
|
||||
supplier: supplierOption.value
|
||||
}));
|
||||
console.log('Found supplier match:', {
|
||||
vendorName: product.vendor,
|
||||
matchedSupplier: supplierOption.label,
|
||||
supplierId: supplierOption.value
|
||||
});
|
||||
|
||||
} else {
|
||||
console.log('No supplier match found for vendor:', product.vendor);
|
||||
}
|
||||
@@ -337,7 +325,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
|
||||
// Fetch product categories
|
||||
if (product.pid) {
|
||||
console.log('Fetching categories for product ID:', product.pid);
|
||||
|
||||
fetchProductCategories(product.pid);
|
||||
}
|
||||
|
||||
@@ -348,7 +336,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
const fetchProductCategories = async (productId: number) => {
|
||||
try {
|
||||
const response = await axios.get(`/api/import/product-categories/${productId}`);
|
||||
console.log('Product categories:', response.data);
|
||||
|
||||
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
// 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
|
||||
);
|
||||
|
||||
console.log('Filtered categories (excluding themes):', filteredCategories);
|
||||
|
||||
// Extract category IDs and update form data
|
||||
const categoryIds = filteredCategories.map((category: any) => category.value);
|
||||
@@ -422,28 +409,14 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
|
||||
// Log supplier information for debugging
|
||||
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 {
|
||||
console.log('No supplier selected for submission');
|
||||
}
|
||||
|
||||
// Log categories information for debugging
|
||||
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 {
|
||||
console.log('No categories selected for submission');
|
||||
}
|
||||
@@ -471,11 +444,9 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
ship_restrictions: formData.ship_restrictions || null
|
||||
};
|
||||
|
||||
console.log('Sending template data:', dataToSend);
|
||||
|
||||
const response = await axios.post('/api/templates', dataToSend);
|
||||
|
||||
console.log('Template creation response:', response);
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
toast.success('Template created successfully');
|
||||
@@ -1040,9 +1011,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
const filteredCategories = fieldOptions.categories.filter(
|
||||
category => category.type && validCategoryTypes.includes(category.type)
|
||||
);
|
||||
|
||||
console.log('Filtered categories for dropdown:', filteredCategories.length);
|
||||
|
||||
|
||||
return [...filteredCategories].sort((a, b) => {
|
||||
const aSelected = selected.has(a.value);
|
||||
const bSelected = selected.has(b.value);
|
||||
@@ -1171,10 +1140,7 @@ export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: Prod
|
||||
onSelect={() => {
|
||||
// Make sure we're setting the ID (value), not the label
|
||||
handleSelectChange('supplier', supplier.value);
|
||||
console.log('Selected supplier from dropdown:', {
|
||||
label: supplier.label,
|
||||
value: supplier.value
|
||||
});
|
||||
|
||||
}}
|
||||
>
|
||||
<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
|
||||
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
|
||||
label: string
|
||||
// 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"
|
||||
alternateMatches?: string[]
|
||||
// Validations used for field entries
|
||||
validations?: Validation[]
|
||||
validations?: ValidationConfig[]
|
||||
// Field entry component
|
||||
fieldType: FieldType
|
||||
// UI-facing values shown to user as field examples pre-upload phase
|
||||
example?: string
|
||||
width?: number
|
||||
disabled?: boolean
|
||||
onChange?: (value: string) => void
|
||||
onChange?: (value: any, additionalData?: any) => void
|
||||
}
|
||||
|
||||
export type FieldType =
|
||||
|
||||
@@ -23,6 +23,39 @@ const BASE_IMPORT_FIELDS = [
|
||||
width: 220,
|
||||
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",
|
||||
key: "upc",
|
||||
@@ -78,7 +111,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
key: "item_number",
|
||||
description: "Internal item reference number",
|
||||
fieldType: { type: "input" },
|
||||
width: 120,
|
||||
width: 130,
|
||||
validations: [
|
||||
{ rule: "required", errorMessage: "Required", level: "error" },
|
||||
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
|
||||
@@ -148,39 +181,6 @@ const BASE_IMPORT_FIELDS = [
|
||||
width: 180,
|
||||
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",
|
||||
key: "artist",
|
||||
|
||||
Reference in New Issue
Block a user