AI validation tweaks, add templates settings page and schema and routes

This commit is contained in:
2025-02-23 15:14:12 -05:00
parent 959a64aebc
commit 3f16413769
12 changed files with 1167 additions and 201 deletions

View File

@@ -0,0 +1,39 @@
-- Templates table for storing import templates
CREATE TABLE IF NOT EXISTS templates (
id SERIAL PRIMARY KEY,
company TEXT NOT NULL,
product_type TEXT NOT NULL,
supplier TEXT,
msrp DECIMAL(10,2),
cost_each DECIMAL(10,2),
qty_per_unit INTEGER,
case_qty INTEGER,
hts_code TEXT,
description TEXT,
weight DECIMAL(10,2),
length DECIMAL(10,2),
width DECIMAL(10,2),
height DECIMAL(10,2),
tax_cat TEXT,
size_cat TEXT,
categories TEXT[],
ship_restrictions TEXT[],
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(company, product_type)
);
-- Function to update the updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger to automatically update the updated_at column
CREATE TRIGGER update_templates_updated_at
BEFORE UPDATE ON templates
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View File

@@ -16,6 +16,7 @@
"multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0",
"openai": "^4.85.3",
"pg": "^8.13.3",
"pm2": "^5.3.0",
"ssh2": "^1.16.0",
"uuid": "^9.0.1"
@@ -2341,6 +2342,95 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/pg": {
"version": "8.13.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz",
"integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.7.0",
"pg-pool": "^3.7.1",
"pg-protocol": "^1.7.1",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz",
"integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz",
"integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -2584,6 +2674,45 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -3114,6 +3243,15 @@
"source-map": "^0.6.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",

View File

@@ -25,6 +25,7 @@
"multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0",
"openai": "^4.85.3",
"pg": "^8.13.3",
"pm2": "^5.3.0",
"ssh2": "^1.16.0",
"uuid": "^9.0.1"

View File

@@ -1,10 +1,10 @@
I will provide a JSON array with product data. Process the array by combining all products from validData and invalidData arrays into a single array, excluding any fields starting with “__”, such as “__index” or “__errors”. Process each product according to the reference guidelines below. If a field is not included in the data, do not include it in your response unless the specific field guidelines below say otherwise.
I will provide a JSON array with product data. Process the array by combining all products from validData and invalidData arrays into a single array, excluding any fields starting with “__”, such as “__index” or “__errors”. Process each product according to the reference guidelines below. If a field is not included in the data, do not include it in your response (e.g. do not include its key or any value) unless the specific field guidelines below say otherwise. If a product appears to be from an empty or entirely invalid line, do not include it in your response.
Respond in the following JSON structure in minified format (single line, no whitespace):
Your response should be a JSON object with the following structure:
{
"correctedData": [], // Array of corrected products
"changes": [], // Array of strings describing each change made
"warnings": [] // Array of strings with warnings or suggestions for manual review
"warnings": [] // Array of strings with warnings or suggestions for manual review (see below for details)
}
Using the provided guidelines, focus on:
@@ -14,94 +14,101 @@ Using the provided guidelines, focus on:
4. Fixing any obvious errors or inconsistencies between similar products in measurements, prices, or quantities
5. Adding correct categories, themes, and colors
Use only the provided data and your own knowledge to make changes. Do not make assumptions or make up information that you're not sure about. If you're unable to make a change you're confident about, leave the field as is. All data passed in should be validated, corrected, and returned. All values should be strings. Do not leave out any fields that were present in the original data.
Use only the provided data and your own knowledge to make changes. Do not make assumptions or make up information that you're not sure about. If you're unable to make a change you're confident about, leave the field as is. All data passed in should be validated, corrected, and returned. All values returned should be strings, not numbers. Do not leave out any fields that were present in the original data.
Possible reasons for including a warning in the warnings array:
- If you're unable to make a change you're confident about but you believe one needs to be made
- If there are inconsistencies in the data that could be valid but need to be reviewed
- If not enough information is provided to make a change that you believe is needed
- If you infer a value for a required field based on context
----------PRODUCT FIELD GUIDELINES----------
Fields: supplier, private_notes, company, line, subline, artist
Changes: Not allowed
Required: Return if present in the original data
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, return these fields exactly as provided with no changes
Fields: upc, supplier_no, notions_no, item_number
Changes: Formatting only
Required: Return if present in the original data
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, trim outside white space and return these fields exactly as provided with no other changes
Fields: hts_code
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data
Instructions: If present, trim white space and any characters that are not a number or decimal point and return as a string
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, trim white space and any non-numeric characters, then return as a string. Do not validate in any other way.
Fields: image_url
Changes: Formatting only
Required: Return if present in the original data
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, convert all comma-separated values to valid https:// URLs and return
Fields: msrp, cost_each
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, strip any currency symbols and return as a string with exactly two decimal places, even if the last place is a 0.
Fields: qty_per_unit, case_qty
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, strip non-numeric characters and return
Fields: ship_restrictions
Changes: Only add a value if it's not already present
Required: You must always return a value for this field, even if it's not provided in the original data
Instructions: Always return a value exactly as provided, or return 0 if no value is provided. Do not leave this field out even if it's not provided.
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return 0.
Instructions: Always return a value exactly as provided, or return 0 if no value is provided.
Fields: eta
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, return a full month name, day is optional, no year ever (e.g. “January” or “March 3”). This value is not required if not provided.
Fields: name
Changes: Allowed to conform to guidelines, to fix typos or formatting
Required: You must always return a value for this field, even if it's not provided in the original data
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return the most reasonable value possible based on the naming guidelines and the other information you have.
Instructions: Always return a value that is corrected and enhanced per additional guidelines below
Fields: description
Changes: Full creative control allowed within guidelines
Required: You must always return a value for this field, even if it's not provided in the original data
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return the most accurate description possible based on the description guidelines and the other information you have.
Instructions: Always return a value that is corrected and enhanced per additional guidelines below
Fields: weight, length, width, height
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: You must always return a value for this field, even if it's not provided in the original data
Instructions: Always return a reasonable value (weights in ounces and dimensions in inches) that is validated against similar provided products and your knowledge of general object measurements (e.g. a sheet of paper is not going to be 3 inches thick, a pack of stickers is not going to be 250 ounces, this sheet of paper is very likely going to be the same size as that other sheet of paper from the same line). If a value is unusual or unreasonable, change it to match similar products or to be more reasonable. Do not return 0 or null for any of these fields.
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return your best guess based on the other information you have or the dimensions for similar products.
Instructions: Always return a reasonable value (weights in ounces and dimensions in inches) that is validated against similar provided products and your knowledge of general object measurements (e.g. a sheet of paper is not going to be 3 inches thick, a pack of stickers is not going to be 250 ounces, this sheet of paper is very likely going to be the same size as that other sheet of paper from the same line). If a value is unusual or unreasonable, even wildly so, change it to match similar products or to be more reasonable. When correcting unreasonable weights or dimensions, prioritize comparisons to products from the same company and product line first, then broader category matches or common knowledge if necessary.Do not return 0 or null for any of these fields.
Fields: coo
Changes: Formatting only
Required: Return if present in the original data
Instructions: If present, return a valid two character country code, using capital letters
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, convert all country names and abbreviations to the official ISO 3166-1 alpha-2 two-character country code. Convert any value with more than two characters to two characters only (e.g. "United States" or "USA" should both return "US").
Fields: tax_cat
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: You must always return a value for this field, even if it's not provided in the original data
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return 0.
Instructions: Always return a valid numerical tax code ID from the Available Tax Codes array below. Give preference to the value provided, but correct it if another value is more accurate. You must return a value for this field. 0 should be the default value in most cases.
Fields: size_cat
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: Return if present in the original data or if not present and applicable
Instructions: If present or if applicable, return a valid numerical size category ID from the Available Size Categories array below. Give preference to the value provided, but correct it if another value is more accurate. A value is not required if none of the size categories apply.
Required: Return if present in the original data or if not present and applicable. Do not return if not applicable (e.g. if no size categories apply based on what you know about the product).
Instructions: If present or if applicable, return one valid numerical size category ID from the Available Size Categories array below. Give preference to the value provided, but correct it if another value is more accurate. A value is not required if none of the size categories apply, but it's important to include if one clearly applies, such as if the name contains 12x12, 6x8, 2oz, etc.
Fields: themes
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: Return if present in the original data or if not present and applicable
Required: Return if present in the original data or if not present and applicable. Do not return any value if not applicable (e.g. if no themes apply based on what you know about the product).
Instructions: If present, confirm that each provided theme matches what you understand to be a theme of the product. Remove any themes that do not match and add any themes that are missing. Most products will have zero or one theme. Return a comma-separated list of numerical theme IDs from the Available Themes array below. If you choose a sub-theme, you do not need to include its parent theme in the list.
Fields: colors
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: Return if present in the original data or if not present and applicable
Instructions: If present or if applicable, return a comma-separated list of numerical color IDs from the Available Colors array below, using the product name as the primary guide. A value is not required if none of the colors apply. Most products will have zero colors.
Required: Return if present in the original data or if not present and applicable. Do not return any value if not applicable (e.g. if no colors apply based on what you know about the product).
Instructions: If present or if applicable, return a comma-separated list of numerical color IDs from the Available Colors array below, using the product name as the primary guide (e.g. if the name contains Blue or a blue variant, you should return the blue color ID). A value is not required if none of the colors apply. Most products will have zero colors.
Fields: categories
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: You must always return at least one value for this field, even if it's not provided in the original data
Instructions: Always return a comma-separated list of one or more valid numerical category IDs from the Available Categories array below. Give preference to the values provided, particularly if the other information isn't enough to determine a category, but correct them or add new categories if another value is more accurate. Do not return categories in the Deals or Black Friday categories, and strip these from the list if present. If you choose a subcategory at any level, you do not need to include its parent categories in the list. You must return at least one category.
Required: You must always return at least one value for this field, even if it's not provided in the original data. If no value is provided, return the most appropriate category or categories based on the other information you have.
Instructions: Always return a comma-separated list of one or more valid numerical category IDs from the Available Categories array below. Give preference to the values provided, particularly if the other information isn't enough to determine a category, but correct them or add new categories if another value is more accurate. Do not return categories in the Deals or Black Friday categories, and strip these from the list if present. If you choose a subcategory at any level, you do not need to include its parent categories in the list. You must return at least one category and you can return multiple categories if applicable. All categories have equal value so their order is not important. Always try to return the most specific categories possible (e.g. one in the third level of the category hierarchy is better than one in the second level).
----------PRODUCT NAMING GUIDELINES----------
If there's only one of this type of product in a line: [Line Name] [Product Name] - [Company]
@@ -167,6 +174,10 @@ Important Notes
- Use the minimum amount of information needed to uniquely identify the product
- Put detailed specifications in the product description, not its name
Edge Cases
- If the product is missing a company name, infer one from the other products included in the data
- If the product is missing a clear differentiator and needs one to be unique, infer and add one from the other data provided (e.g. the description, existing size categories, etc.)
Incorrect example: MVP Rugby - Collection Pack - Photoplay
Notes: there should be no dash between the line and the product
@@ -202,7 +213,7 @@ If no description is provided, make one up using the product name, the informati
Important Notes:
- Every description should state exactly what's included in the product (e.g. "Includes one 12x12 sheet of patterned cardstock." or "Includes one 6x12 sheet with 27 unique stickers." or "Includes 55 pieces." or "Package includes machine, power cord, 12 sheets of cardstock, 3 dies, and project instructions.")
- Do not use the word "our" in the description (this usually shows up when we copy a description from the manufacturer). Instead use "these" or "[Company name] [product]" or similar. (e.g. don't use "Our journals are hand-made in the USA", instead use "These journals are hand made..." or "Archer & Olive journals are handmade...")
- Don't include fluff like “this is perfect for all your paper crafts” most of the time. If the product helps to solve a unique problem or has a unique feature, by all means describe it, but if its just a normal sheet of paper or pack of stickers, you dont have to pretend like its the best thing ever.
- Don't include statements that add no value like “this is perfect for all your paper crafts”. If the product helps to solve a unique problem or has a unique feature, by all means describe it, but if its just a normal sheet of paper or pack of stickers, you dont have to pretend like its the best thing ever. At the same time, ensure that you add enough copy to ensure good SEO.
- State as many facts as you can about the product, considering the viewpoint of the customer and what they would want to know when looking at it. They probably want to know dimensions, what products its compatible with, how thick the paper is, how many sheets are included, whether the sheets are double-sided or not, which items are in the kit, etc. Say as much as you possibly can with the information that you have.
- !!DO NOT make up information if you aren't sure about it. A minimal correct description is better than a long incorrect one!!

View File

@@ -32,32 +32,7 @@ function clearCache() {
};
}
// Debug endpoint to view prompt and cache status
router.get('/debug', async (req, res) => {
try {
console.log('Debug endpoint called');
const pool = req.app.locals.pool;
// Get a real supplier, company, and artist ID from the database
const [suppliers] = await pool.query('SELECT supplierid FROM suppliers LIMIT 1');
const [companies] = await pool.query('SELECT cat_id FROM product_categories WHERE type = 1 LIMIT 1');
const [artists] = await pool.query('SELECT cat_id FROM product_categories WHERE type = 40 LIMIT 1');
// Create a sample product with real IDs
const productsToUse = [{
supplierid: suppliers[0]?.supplierid || 1234,
company: companies[0]?.cat_id || 567,
artist: artists[0]?.cat_id || 890
}];
return await generateDebugResponse(pool, productsToUse, res);
} catch (error) {
console.error('Debug endpoint error:', error);
res.status(500).json({ error: error.message });
}
});
// New POST endpoint for debug with products
// Debug endpoint for viewing prompt and cache status
router.post('/debug', async (req, res) => {
try {
console.log('Debug POST endpoint called');
@@ -490,8 +465,7 @@ router.post('/validate', async (req, res) => {
content: fullPrompt
}
],
temperature: 0.3,
max_tokens: 4000,
temperature: 0.2,
response_format: { type: "json_object" }
});

View File

@@ -0,0 +1,272 @@
import { Router } from 'express';
import { Pool } from 'pg';
import dotenv from 'dotenv';
import path from 'path';
dotenv.config({ path: path.join(__dirname, "../../.env") });
const router = Router();
// Initialize PostgreSQL connection pool
const pool = new Pool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// Add SSL if needed (based on your environment)
...(process.env.NODE_ENV === 'production' && {
ssl: {
rejectUnauthorized: false
}
})
});
// Get all templates
router.get('/', async (req, res) => {
try {
const result = await pool.query(`
SELECT * FROM templates
ORDER BY company ASC, product_type ASC
`);
res.json(result.rows);
} catch (error) {
console.error('Error fetching templates:', error);
res.status(500).json({
error: 'Failed to fetch templates',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Get template by company and product type
router.get('/:company/:productType', async (req, res) => {
try {
const { company, productType } = req.params;
const result = await pool.query(`
SELECT * FROM templates
WHERE company = $1 AND product_type = $2
`, [company, productType]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Template not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Error fetching template:', error);
res.status(500).json({
error: 'Failed to fetch template',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Create new template
router.post('/', async (req, res) => {
try {
const {
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions
} = req.body;
// Validate required fields
if (!company || !product_type) {
return res.status(400).json({ error: 'Company and Product Type are required' });
}
const result = await pool.query(`
INSERT INTO templates (
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
RETURNING *
`, [
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions
]);
res.status(201).json(result.rows[0]);
} catch (error) {
console.error('Error creating template:', error);
// Check for unique constraint violation
if (error instanceof Error && error.message.includes('unique constraint')) {
return res.status(409).json({
error: 'Template already exists for this company and product type',
details: error.message
});
}
res.status(500).json({
error: 'Failed to create template',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Update template
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const {
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions
} = req.body;
// Validate required fields
if (!company || !product_type) {
return res.status(400).json({ error: 'Company and Product Type are required' });
}
const result = await pool.query(`
UPDATE templates
SET
company = $1,
product_type = $2,
supplier = $3,
msrp = $4,
cost_each = $5,
qty_per_unit = $6,
case_qty = $7,
hts_code = $8,
description = $9,
weight = $10,
length = $11,
width = $12,
height = $13,
tax_cat = $14,
size_cat = $15,
categories = $16,
ship_restrictions = $17
WHERE id = $18
RETURNING *
`, [
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions,
id
]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Template not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Error updating template:', error);
// Check for unique constraint violation
if (error instanceof Error && error.message.includes('unique constraint')) {
return res.status(409).json({
error: 'Template already exists for this company and product type',
details: error.message
});
}
res.status(500).json({
error: 'Failed to update template',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Delete template
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query('DELETE FROM templates WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Template not found' });
}
res.json({ message: 'Template deleted successfully' });
} catch (error) {
console.error('Error deleting template:', error);
res.status(500).json({
error: 'Failed to delete template',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Error handling middleware
router.use((err: Error, req: any, res: any, next: any) => {
console.error('Template route error:', err);
res.status(500).json({
error: 'Internal server error',
details: err.message
});
});
export default router;

View File

@@ -19,6 +19,7 @@ const categoriesRouter = require('./routes/categories');
const testConnectionRouter = require('./routes/test-connection');
const importRouter = require('./routes/import');
const aiValidationRouter = require('./routes/ai-validation');
const templatesRouter = require('./routes/templates');
// Get the absolute path to the .env file
const envPath = path.resolve(process.cwd(), '.env');
@@ -64,8 +65,8 @@ app.use((req, res, next) => {
app.use(corsMiddleware);
// Body parser middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Initialize database pool and start server
async function startServer() {
@@ -95,6 +96,7 @@ async function startServer() {
app.use('/api/categories', categoriesRouter);
app.use('/api/import', importRouter);
app.use('/api/ai-validation', aiValidationRouter);
app.use('/api/templates', templatesRouter);
app.use('/api', testConnectionRouter);
// Basic health check route

View File

@@ -660,75 +660,83 @@ export function DataManagement() {
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
<Table>
<TableBody>
{importHistory.slice(0, 20).map((record) => (
<TableRow key={record.id} className="hover:bg-transparent">
<TableCell className="w-full p-0">
<Accordion type="single" collapsible>
<AccordionItem
value={`import-${record.id}`}
className="border-0"
>
<AccordionTrigger className="px-4 py-2">
<div className="flex justify-between items-start w-full pr-4">
<span className="font-medium min-w-[60px]">
#{record.id}
</span>
<span className="text-sm text-gray-600 min-w-[120px]">
{formatDate(record.start_time)}
</span>
<span className="text-sm min-w-[100px]">
{formatDurationWithSeconds(
record.duration_minutes,
record.status === "running",
record.start_time
)}
</span>
<span
className={`min-w-[80px] ${
record.status === "completed"
? "text-green-600"
: record.status === "failed"
? "text-red-600"
: record.status === "cancelled"
? "text-yellow-600"
: "text-blue-600"
}`}
>
{record.status}
</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-4 pb-2">
<div className="space-y-2 pt-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">End Time:</span>
<span>
{record.end_time
? formatDate(record.end_time)
: "N/A"}
{importHistory.length > 0 ? (
importHistory.slice(0, 20).map((record) => (
<TableRow key={record.id} className="hover:bg-transparent">
<TableCell className="w-full p-0">
<Accordion type="single" collapsible>
<AccordionItem
value={`import-${record.id}`}
className="border-0"
>
<AccordionTrigger className="px-4 py-2">
<div className="flex justify-between items-start w-full pr-4">
<span className="font-medium min-w-[60px]">
#{record.id}
</span>
<span className="text-sm text-gray-600 min-w-[120px]">
{formatDate(record.start_time)}
</span>
<span className="text-sm min-w-[100px]">
{formatDurationWithSeconds(
record.duration_minutes,
record.status === "running",
record.start_time
)}
</span>
<span
className={`min-w-[80px] ${
record.status === "completed"
? "text-green-600"
: record.status === "failed"
? "text-red-600"
: record.status === "cancelled"
? "text-yellow-600"
: "text-blue-600"
}`}
>
{record.status}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Records:</span>
<span>
{record.records_added} added,{" "}
{record.records_updated} updated
</span>
</div>
{record.error_message && (
<div className="text-sm text-red-600 mt-2">
{record.error_message}
</AccordionTrigger>
<AccordionContent className="px-4 pb-2">
<div className="space-y-2 pt-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">End Time:</span>
<span>
{record.end_time
? formatDate(record.end_time)
: "N/A"}
</span>
</div>
)}
{record.additional_info &&
formatJsonData(record.additional_info)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Records:</span>
<span>
{record.records_added} added,{" "}
{record.records_updated} updated
</span>
</div>
{record.error_message && (
<div className="text-sm text-red-600 mt-2">
{record.error_message}
</div>
)}
{record.additional_info &&
formatJsonData(record.additional_info)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell className="text-center text-sm text-muted-foreground py-4">
No import history available
</TableCell>
</TableRow>
))}
)}
</TableBody>
</Table>
</CardContent>
@@ -742,90 +750,98 @@ export function DataManagement() {
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
<Table>
<TableBody>
{calculateHistory.slice(0, 20).map((record) => (
<TableRow key={record.id} className="hover:bg-transparent">
<TableCell className="w-full p-0">
<Accordion type="single" collapsible>
<AccordionItem
value={`calc-${record.id}`}
className="border-0"
>
<AccordionTrigger className="px-4 py-2">
<div className="flex justify-between items-start w-full pr-4">
<span className="font-medium min-w-[60px]">
#{record.id}
</span>
<span className="text-sm text-gray-600 min-w-[120px]">
{formatDate(record.start_time)}
</span>
<span className="text-sm min-w-[100px]">
{formatDurationWithSeconds(
record.duration_minutes,
record.status === "running",
record.start_time
)}
</span>
{calculateHistory.length > 0 ? (
calculateHistory.slice(0, 20).map((record) => (
<TableRow key={record.id} className="hover:bg-transparent">
<TableCell className="w-full p-0">
<Accordion type="single" collapsible>
<AccordionItem
value={`calc-${record.id}`}
className="border-0"
>
<AccordionTrigger className="px-4 py-2">
<div className="flex justify-between items-start w-full pr-4">
<span className="font-medium min-w-[60px]">
#{record.id}
</span>
<span className="text-sm text-gray-600 min-w-[120px]">
{formatDate(record.start_time)}
</span>
<span className="text-sm min-w-[100px]">
{formatDurationWithSeconds(
record.duration_minutes,
record.status === "running",
record.start_time
)}
</span>
<span
className={`min-w-[80px] ${
record.status === "completed"
? "text-green-600"
: record.status === "failed"
? "text-red-600"
: record.status === "cancelled"
? "text-yellow-600"
: "text-blue-600"
}`}
>
{record.status}
</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-4 pb-2">
<div className="space-y-2 pt-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">End Time:</span>
<span>
{record.end_time
? formatDate(record.end_time)
: "N/A"}
<span
className={`min-w-[80px] ${
record.status === "completed"
? "text-green-600"
: record.status === "failed"
? "text-red-600"
: record.status === "cancelled"
? "text-yellow-600"
: "text-blue-600"
}`}
>
{record.status}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">
Processed Products:
</span>
<span>{record.processed_products}</span>
</AccordionTrigger>
<AccordionContent className="px-4 pb-2">
<div className="space-y-2 pt-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">End Time:</span>
<span>
{record.end_time
? formatDate(record.end_time)
: "N/A"}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">
Processed Products:
</span>
<span>{record.processed_products}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">
Processed Orders:
</span>
<span>{record.processed_orders}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">
Processed Purchase Orders:
</span>
<span>{record.processed_purchase_orders}</span>
</div>
{record.error_message && (
<div className="text-sm text-red-600 mt-2">
{record.error_message}
</div>
)}
{record.additional_info &&
formatJsonData(record.additional_info)}
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">
Processed Orders:
</span>
<span>{record.processed_orders}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">
Processed Purchase Orders:
</span>
<span>{record.processed_purchase_orders}</span>
</div>
{record.error_message && (
<div className="text-sm text-red-600 mt-2">
{record.error_message}
</div>
)}
{record.additional_info &&
formatJsonData(record.additional_info)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</AccordionContent>
</AccordionItem>
</Accordion>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell className="text-center text-sm text-muted-foreground py-4">
No calculation history available
</TableCell>
</TableRow>
))}
)}
</TableBody>
</Table>
</CardContent>
</CardContent>
</Card>
</div>
</div>

View File

@@ -0,0 +1,422 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import { toast } from "sonner";
import config from "@/config";
interface Template {
id: number;
company: string;
product_type: string;
supplier?: string;
msrp?: number;
cost_each?: number;
qty_per_unit?: number;
case_qty?: number;
hts_code?: string;
description?: string;
weight?: number;
length?: number;
width?: number;
height?: number;
tax_cat?: string;
size_cat?: string;
categories?: string[];
ship_restrictions?: string[];
created_at: string;
updated_at: string;
}
interface TemplateFormData extends Omit<Template, 'id' | 'created_at' | 'updated_at'> {}
export function TemplateManagement() {
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [formData, setFormData] = useState<TemplateFormData>({
company: "",
product_type: "",
});
const queryClient = useQueryClient();
const { data: templates, isLoading } = useQuery<Template[]>({
queryKey: ["templates"],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/templates`);
if (!response.ok) {
throw new Error("Failed to fetch templates");
}
return response.json();
},
});
const createMutation = useMutation({
mutationFn: async (data: TemplateFormData) => {
const response = await fetch(`${config.apiUrl}/templates`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error("Failed to create template");
}
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["templates"] });
setIsCreateOpen(false);
setFormData({ company: "", product_type: "" });
toast.success("Template created successfully");
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : "Failed to create template");
},
});
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`${config.apiUrl}/templates/${id}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Failed to delete template");
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["templates"] });
toast.success("Template deleted successfully");
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : "Failed to delete template");
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
createMutation.mutate(formData);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const handleArrayInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value.split(",").map((item) => item.trim()),
}));
};
const handleNumberInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value === "" ? undefined : Number(value),
}));
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">Import Templates</h2>
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogTrigger asChild>
<Button>Create Template</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create Import Template</DialogTitle>
<DialogDescription>
Create a new template for importing products. Company and Product Type combination must be unique.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<ScrollArea className="h-[60vh]">
<div className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="company">Company *</Label>
<Input
id="company"
name="company"
value={formData.company}
onChange={handleInputChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="product_type">Product Type *</Label>
<Input
id="product_type"
name="product_type"
value={formData.product_type}
onChange={handleInputChange}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="supplier">Supplier</Label>
<Input
id="supplier"
name="supplier"
value={formData.supplier || ""}
onChange={handleInputChange}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="msrp">MSRP</Label>
<Input
id="msrp"
name="msrp"
type="number"
step="0.01"
value={formData.msrp || ""}
onChange={handleNumberInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cost_each">Cost Each</Label>
<Input
id="cost_each"
name="cost_each"
type="number"
step="0.01"
value={formData.cost_each || ""}
onChange={handleNumberInputChange}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="qty_per_unit">Quantity per Unit</Label>
<Input
id="qty_per_unit"
name="qty_per_unit"
type="number"
value={formData.qty_per_unit || ""}
onChange={handleNumberInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="case_qty">Case Quantity</Label>
<Input
id="case_qty"
name="case_qty"
type="number"
value={formData.case_qty || ""}
onChange={handleNumberInputChange}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="hts_code">HTS Code</Label>
<Input
id="hts_code"
name="hts_code"
value={formData.hts_code || ""}
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
name="description"
value={formData.description || ""}
onChange={handleInputChange}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="weight">Weight</Label>
<Input
id="weight"
name="weight"
type="number"
step="0.01"
value={formData.weight || ""}
onChange={handleNumberInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="length">Length</Label>
<Input
id="length"
name="length"
type="number"
step="0.01"
value={formData.length || ""}
onChange={handleNumberInputChange}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="width">Width</Label>
<Input
id="width"
name="width"
type="number"
step="0.01"
value={formData.width || ""}
onChange={handleNumberInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="height">Height</Label>
<Input
id="height"
name="height"
type="number"
step="0.01"
value={formData.height || ""}
onChange={handleNumberInputChange}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="tax_cat">Tax Category</Label>
<Input
id="tax_cat"
name="tax_cat"
value={formData.tax_cat || ""}
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="size_cat">Size Category</Label>
<Input
id="size_cat"
name="size_cat"
value={formData.size_cat || ""}
onChange={handleInputChange}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="categories">Categories (comma-separated)</Label>
<Input
id="categories"
name="categories"
value={formData.categories?.join(", ") || ""}
onChange={handleArrayInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="ship_restrictions">
Shipping Restrictions (comma-separated)
</Label>
<Input
id="ship_restrictions"
name="ship_restrictions"
value={formData.ship_restrictions?.join(", ") || ""}
onChange={handleArrayInputChange}
/>
</div>
</div>
</ScrollArea>
<DialogFooter>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "Creating..." : "Create Template"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
{isLoading ? (
<div>Loading templates...</div>
) : (
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>Company</TableHead>
<TableHead>Product Type</TableHead>
<TableHead>Supplier</TableHead>
<TableHead>Last Updated</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{templates?.map((template) => (
<TableRow key={template.id}>
<TableCell>{template.company}</TableCell>
<TableCell>{template.product_type}</TableCell>
<TableCell>{template.supplier || "-"}</TableCell>
<TableCell>
{new Date(template.updated_at).toLocaleDateString()}
</TableCell>
<TableCell>
<Button
variant="destructive"
size="sm"
onClick={() => {
if (
window.confirm(
"Are you sure you want to delete this template?"
)
) {
deleteMutation.mutate(template.id);
}
}}
>
Delete
</Button>
</TableCell>
</TableRow>
))}
{templates?.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-center">
No templates found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)}
</div>
);
}

View File

@@ -744,6 +744,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
prompt: null,
isLoading: false,
});
// Memoize filtered data to prevent recalculation on every render
const filteredData = useMemo(() => {
@@ -893,6 +894,59 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
}
}, [rowSelection, data, updateData]);
const discardEmptyAndDuplicateRows = useCallback(() => {
// Helper function to count non-empty values in a row
const countNonEmptyValues = (values: Record<string, any>): number => {
return Object.values(values).filter(val =>
val !== undefined &&
val !== null &&
(typeof val === 'string' ? val.trim() !== '' : true)
).length;
};
// First, analyze all rows to determine if we have rows with multiple values
const rowsWithValues = data.map(row => {
const { __index, __errors, ...values } = row;
return countNonEmptyValues(values);
});
// Check if we have any rows with more than one value
const hasMultiValueRows = rowsWithValues.some(count => count > 1);
// Filter out empty rows and rows with single values (if we have multi-value rows)
const nonEmptyRows = data.filter((row, index) => {
const nonEmptyCount = rowsWithValues[index];
// Keep the row if:
// 1. It has more than one value, OR
// 2. It has exactly one value AND we don't have any rows with multiple values
return nonEmptyCount > 0 && (!hasMultiValueRows || nonEmptyCount > 1);
});
// Then, remove duplicates by creating a unique string representation of each row
const seen = new Set<string>();
const uniqueRows = nonEmptyRows.filter(row => {
const { __index, __errors, ...values } = row;
const rowStr = JSON.stringify(Object.entries(values).sort());
if (seen.has(rowStr)) {
return false;
}
seen.add(rowStr);
return true;
});
// Only update if we actually removed any rows
if (uniqueRows.length < data.length) {
updateData(uniqueRows);
setRowSelection({});
toast({
title: "Rows removed",
description: `Removed ${data.length - uniqueRows.length} empty, single-value, or duplicate rows`,
variant: "default"
});
}
}, [data, updateData, toast]);
const normalizeValue = useCallback((value: any, field: DeepReadonlyField<T>) => {
if (field.fieldType.type === "checkbox") {
if (typeof value === "boolean") return value
@@ -1218,7 +1272,12 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialog>
<Dialog open={aiValidationProgress.isOpen} onOpenChange={() => {}}>
<Dialog open={aiValidationProgress.isOpen} onOpenChange={(open) => {
// Only allow closing if validation failed
if (!open && aiValidationProgress.step === -1) {
setAiValidationProgress(prev => ({ ...prev, isOpen: false }));
}
}}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>AI Validation Progress</DialogTitle>
@@ -1302,6 +1361,13 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
>
{translations.validationStep.discardButtonTitle}
</Button>
<Button
variant="outline"
size="sm"
onClick={discardEmptyAndDuplicateRows}
>
Remove Empty/Duplicates
</Button>
<Button
variant="secondary"
size="sm"

View File

@@ -37,7 +37,24 @@ export function AiValidationDebug() {
const fetchDebugData = async () => {
setIsLoading(true)
try {
const response = await fetch(`${config.apiUrl}/ai-validation/debug`)
// Use a sample product to avoid loading full taxonomy
const sampleProduct = {
title: "Sample Product",
description: "A sample product for testing",
SKU: "SAMPLE-001",
price: "9.99",
cost_each: "5.00",
qty_per_unit: "1",
case_qty: "12"
}
const response = await fetch(`${config.apiUrl}/ai-validation/debug`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ products: [sampleProduct] })
})
if (!response.ok) {
throw new Error('Failed to fetch debug data')
}

View File

@@ -3,6 +3,7 @@ import { DataManagement } from "@/components/settings/DataManagement";
import { StockManagement } from "@/components/settings/StockManagement";
import { PerformanceMetrics } from "@/components/settings/PerformanceMetrics";
import { CalculationSettings } from "@/components/settings/CalculationSettings";
import { TemplateManagement } from "@/components/settings/TemplateManagement";
import { motion } from 'motion/react';
export function Settings() {
@@ -22,6 +23,9 @@ export function Settings() {
<TabsTrigger value="calculation-settings">
Calculation Settings
</TabsTrigger>
<TabsTrigger value="templates">
Import Templates
</TabsTrigger>
</TabsList>
<TabsContent value="data-management">
@@ -39,6 +43,10 @@ export function Settings() {
<TabsContent value="calculation-settings">
<CalculationSettings />
</TabsContent>
<TabsContent value="templates">
<TemplateManagement />
</TabsContent>
</Tabs>
</motion.div>
);