Add UPC validation and automatic item number generation/validation

This commit is contained in:
2025-03-01 19:37:51 -05:00
parent 8271c9f95a
commit 98e3b89d46
5 changed files with 1225 additions and 143 deletions

View File

@@ -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 {

View File

@@ -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); let supplierOption = fieldOptions.suppliers.find(
console.log('Looking for supplier match for vendor:', product.vendor);
// First try exact match
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

View File

@@ -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 =

View File

@@ -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",