Compare commits
7 Commits
add-permis
...
87d4b9e804
| Author | SHA1 | Date | |
|---|---|---|---|
| 87d4b9e804 | |||
| 75da2c6772 | |||
| 00a02aa788 | |||
| 114018080a | |||
| 228ae8b2a9 | |||
| dd4b3f7145 | |||
| 7eb4077224 |
@@ -23,6 +23,32 @@ CREATE TABLE IF NOT EXISTS templates (
|
|||||||
UNIQUE(company, product_type)
|
UNIQUE(company, product_type)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- AI Prompts table for storing validation prompts
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_prompts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
prompt_text TEXT NOT NULL,
|
||||||
|
prompt_type TEXT NOT NULL CHECK (prompt_type IN ('general', 'company_specific', 'system')),
|
||||||
|
company TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT unique_company_prompt UNIQUE (company),
|
||||||
|
CONSTRAINT company_required_for_specific CHECK (
|
||||||
|
(prompt_type = 'general' AND company IS NULL) OR
|
||||||
|
(prompt_type = 'system' AND company IS NULL) OR
|
||||||
|
(prompt_type = 'company_specific' AND company IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create a unique partial index to ensure only one general prompt
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_general_prompt
|
||||||
|
ON ai_prompts (prompt_type)
|
||||||
|
WHERE prompt_type = 'general';
|
||||||
|
|
||||||
|
-- Create a unique partial index to ensure only one system prompt
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_system_prompt
|
||||||
|
ON ai_prompts (prompt_type)
|
||||||
|
WHERE prompt_type = 'system';
|
||||||
|
|
||||||
-- AI Validation Performance Tracking
|
-- AI Validation Performance Tracking
|
||||||
CREATE TABLE IF NOT EXISTS ai_validation_performance (
|
CREATE TABLE IF NOT EXISTS ai_validation_performance (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
@@ -51,3 +77,9 @@ CREATE TRIGGER update_templates_updated_at
|
|||||||
BEFORE UPDATE ON templates
|
BEFORE UPDATE ON templates
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION update_updated_at_column();
|
EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- Trigger to automatically update the updated_at column for ai_prompts
|
||||||
|
CREATE TRIGGER update_ai_prompts_updated_at
|
||||||
|
BEFORE UPDATE ON ai_prompts
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_updated_at_column();
|
||||||
@@ -120,27 +120,38 @@ async function main() {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
// Create import history record for the overall session
|
// Create import history record for the overall session
|
||||||
const [historyResult] = await localConnection.query(`
|
try {
|
||||||
INSERT INTO import_history (
|
const [historyResult] = await localConnection.query(`
|
||||||
table_name,
|
INSERT INTO import_history (
|
||||||
start_time,
|
table_name,
|
||||||
is_incremental,
|
start_time,
|
||||||
status,
|
is_incremental,
|
||||||
additional_info
|
status,
|
||||||
) VALUES (
|
additional_info
|
||||||
'all_tables',
|
) VALUES (
|
||||||
NOW(),
|
'all_tables',
|
||||||
$1::boolean,
|
NOW(),
|
||||||
'running',
|
$1::boolean,
|
||||||
jsonb_build_object(
|
'running',
|
||||||
'categories_enabled', $2::boolean,
|
jsonb_build_object(
|
||||||
'products_enabled', $3::boolean,
|
'categories_enabled', $2::boolean,
|
||||||
'orders_enabled', $4::boolean,
|
'products_enabled', $3::boolean,
|
||||||
'purchase_orders_enabled', $5::boolean
|
'orders_enabled', $4::boolean,
|
||||||
)
|
'purchase_orders_enabled', $5::boolean
|
||||||
) RETURNING id
|
)
|
||||||
`, [INCREMENTAL_UPDATE, IMPORT_CATEGORIES, IMPORT_PRODUCTS, IMPORT_ORDERS, IMPORT_PURCHASE_ORDERS]);
|
) RETURNING id
|
||||||
importHistoryId = historyResult.rows[0].id;
|
`, [INCREMENTAL_UPDATE, IMPORT_CATEGORIES, IMPORT_PRODUCTS, IMPORT_ORDERS, IMPORT_PURCHASE_ORDERS]);
|
||||||
|
importHistoryId = historyResult.rows[0].id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating import history record:", error);
|
||||||
|
outputProgress({
|
||||||
|
status: "error",
|
||||||
|
operation: "Import process",
|
||||||
|
message: "Failed to create import history record",
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
categories: null,
|
categories: null,
|
||||||
@@ -158,8 +169,8 @@ async function main() {
|
|||||||
if (isImportCancelled) throw new Error("Import cancelled");
|
if (isImportCancelled) throw new Error("Import cancelled");
|
||||||
completedSteps++;
|
completedSteps++;
|
||||||
console.log('Categories import result:', results.categories);
|
console.log('Categories import result:', results.categories);
|
||||||
totalRecordsAdded += parseInt(results.categories?.recordsAdded || 0);
|
totalRecordsAdded += parseInt(results.categories?.recordsAdded || 0) || 0;
|
||||||
totalRecordsUpdated += parseInt(results.categories?.recordsUpdated || 0);
|
totalRecordsUpdated += parseInt(results.categories?.recordsUpdated || 0) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IMPORT_PRODUCTS) {
|
if (IMPORT_PRODUCTS) {
|
||||||
@@ -167,8 +178,8 @@ async function main() {
|
|||||||
if (isImportCancelled) throw new Error("Import cancelled");
|
if (isImportCancelled) throw new Error("Import cancelled");
|
||||||
completedSteps++;
|
completedSteps++;
|
||||||
console.log('Products import result:', results.products);
|
console.log('Products import result:', results.products);
|
||||||
totalRecordsAdded += parseInt(results.products?.recordsAdded || 0);
|
totalRecordsAdded += parseInt(results.products?.recordsAdded || 0) || 0;
|
||||||
totalRecordsUpdated += parseInt(results.products?.recordsUpdated || 0);
|
totalRecordsUpdated += parseInt(results.products?.recordsUpdated || 0) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IMPORT_ORDERS) {
|
if (IMPORT_ORDERS) {
|
||||||
@@ -176,17 +187,34 @@ async function main() {
|
|||||||
if (isImportCancelled) throw new Error("Import cancelled");
|
if (isImportCancelled) throw new Error("Import cancelled");
|
||||||
completedSteps++;
|
completedSteps++;
|
||||||
console.log('Orders import result:', results.orders);
|
console.log('Orders import result:', results.orders);
|
||||||
totalRecordsAdded += parseInt(results.orders?.recordsAdded || 0);
|
totalRecordsAdded += parseInt(results.orders?.recordsAdded || 0) || 0;
|
||||||
totalRecordsUpdated += parseInt(results.orders?.recordsUpdated || 0);
|
totalRecordsUpdated += parseInt(results.orders?.recordsUpdated || 0) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IMPORT_PURCHASE_ORDERS) {
|
if (IMPORT_PURCHASE_ORDERS) {
|
||||||
results.purchaseOrders = await importPurchaseOrders(prodConnection, localConnection, INCREMENTAL_UPDATE);
|
try {
|
||||||
if (isImportCancelled) throw new Error("Import cancelled");
|
results.purchaseOrders = await importPurchaseOrders(prodConnection, localConnection, INCREMENTAL_UPDATE);
|
||||||
completedSteps++;
|
if (isImportCancelled) throw new Error("Import cancelled");
|
||||||
console.log('Purchase orders import result:', results.purchaseOrders);
|
completedSteps++;
|
||||||
totalRecordsAdded += parseInt(results.purchaseOrders?.recordsAdded || 0);
|
console.log('Purchase orders import result:', results.purchaseOrders);
|
||||||
totalRecordsUpdated += parseInt(results.purchaseOrders?.recordsUpdated || 0);
|
|
||||||
|
// Handle potential error status
|
||||||
|
if (results.purchaseOrders?.status === 'error') {
|
||||||
|
console.error('Purchase orders import had an error:', results.purchaseOrders.error);
|
||||||
|
} else {
|
||||||
|
totalRecordsAdded += parseInt(results.purchaseOrders?.recordsAdded || 0) || 0;
|
||||||
|
totalRecordsUpdated += parseInt(results.purchaseOrders?.recordsUpdated || 0) || 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during purchase orders import:', error);
|
||||||
|
// Continue with other imports, don't fail the whole process
|
||||||
|
results.purchaseOrders = {
|
||||||
|
status: 'error',
|
||||||
|
error: error.message,
|
||||||
|
recordsAdded: 0,
|
||||||
|
recordsUpdated: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const endTime = Date.now();
|
const endTime = Date.now();
|
||||||
@@ -214,8 +242,8 @@ async function main() {
|
|||||||
WHERE id = $12
|
WHERE id = $12
|
||||||
`, [
|
`, [
|
||||||
totalElapsedSeconds,
|
totalElapsedSeconds,
|
||||||
totalRecordsAdded,
|
parseInt(totalRecordsAdded) || 0,
|
||||||
totalRecordsUpdated,
|
parseInt(totalRecordsUpdated) || 0,
|
||||||
IMPORT_CATEGORIES,
|
IMPORT_CATEGORIES,
|
||||||
IMPORT_PRODUCTS,
|
IMPORT_PRODUCTS,
|
||||||
IMPORT_ORDERS,
|
IMPORT_ORDERS,
|
||||||
|
|||||||
@@ -47,42 +47,18 @@ async function importCategories(prodConnection, localConnection) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\nProcessing ${categories.length} type ${type} categories`);
|
console.log(`Processing ${categories.length} type ${type} categories`);
|
||||||
if (type === 10) {
|
|
||||||
console.log("Type 10 categories:", JSON.stringify(categories, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// For types that can have parents (11, 21, 12, 13), verify parent existence
|
// For types that can have parents (11, 21, 12, 13), we'll proceed directly
|
||||||
|
// No need to check for parent existence since we process in hierarchical order
|
||||||
let categoriesToInsert = categories;
|
let categoriesToInsert = categories;
|
||||||
if (![10, 20].includes(type)) {
|
|
||||||
// Get all parent IDs
|
|
||||||
const parentIds = [
|
|
||||||
...new Set(
|
|
||||||
categories
|
|
||||||
.filter(c => c && c.parent_id !== null)
|
|
||||||
.map(c => c.parent_id)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log(`Processing ${categories.length} type ${type} categories with ${parentIds.length} unique parent IDs`);
|
|
||||||
console.log('Parent IDs:', parentIds);
|
|
||||||
|
|
||||||
// No need to check for parent existence - we trust they exist since they were just inserted
|
|
||||||
categoriesToInsert = categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (categoriesToInsert.length === 0) {
|
if (categoriesToInsert.length === 0) {
|
||||||
console.log(
|
console.log(`No valid categories of type ${type} to insert`);
|
||||||
`No valid categories of type ${type} to insert`
|
|
||||||
);
|
|
||||||
await localConnection.query(`RELEASE SAVEPOINT category_type_${type}`);
|
await localConnection.query(`RELEASE SAVEPOINT category_type_${type}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Inserting ${categoriesToInsert.length} type ${type} categories`
|
|
||||||
);
|
|
||||||
|
|
||||||
// PostgreSQL upsert query with parameterized values
|
// PostgreSQL upsert query with parameterized values
|
||||||
const values = categoriesToInsert.flatMap((cat) => [
|
const values = categoriesToInsert.flatMap((cat) => [
|
||||||
cat.cat_id,
|
cat.cat_id,
|
||||||
@@ -95,14 +71,10 @@ async function importCategories(prodConnection, localConnection) {
|
|||||||
new Date()
|
new Date()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log('Attempting to insert/update with values:', JSON.stringify(values, null, 2));
|
|
||||||
|
|
||||||
const placeholders = categoriesToInsert
|
const placeholders = categoriesToInsert
|
||||||
.map((_, i) => `($${i * 8 + 1}, $${i * 8 + 2}, $${i * 8 + 3}, $${i * 8 + 4}, $${i * 8 + 5}, $${i * 8 + 6}, $${i * 8 + 7}, $${i * 8 + 8})`)
|
.map((_, i) => `($${i * 8 + 1}, $${i * 8 + 2}, $${i * 8 + 3}, $${i * 8 + 4}, $${i * 8 + 5}, $${i * 8 + 6}, $${i * 8 + 7}, $${i * 8 + 8})`)
|
||||||
.join(',');
|
.join(',');
|
||||||
|
|
||||||
console.log('Using placeholders:', placeholders);
|
|
||||||
|
|
||||||
// Insert categories with ON CONFLICT clause for PostgreSQL
|
// Insert categories with ON CONFLICT clause for PostgreSQL
|
||||||
const query = `
|
const query = `
|
||||||
WITH inserted_categories AS (
|
WITH inserted_categories AS (
|
||||||
@@ -130,16 +102,13 @@ async function importCategories(prodConnection, localConnection) {
|
|||||||
COUNT(*) FILTER (WHERE NOT is_insert) as updated
|
COUNT(*) FILTER (WHERE NOT is_insert) as updated
|
||||||
FROM inserted_categories`;
|
FROM inserted_categories`;
|
||||||
|
|
||||||
console.log('Executing query:', query);
|
|
||||||
|
|
||||||
const result = await localConnection.query(query, values);
|
const result = await localConnection.query(query, values);
|
||||||
console.log('Query result:', result);
|
|
||||||
|
|
||||||
// Get the first result since query returns an array
|
// Get the first result since query returns an array
|
||||||
const queryResult = Array.isArray(result) ? result[0] : result;
|
const queryResult = Array.isArray(result) ? result[0] : result;
|
||||||
|
|
||||||
if (!queryResult || !queryResult.rows || !queryResult.rows[0]) {
|
if (!queryResult || !queryResult.rows || !queryResult.rows[0]) {
|
||||||
console.error('Query failed to return results. Result:', queryResult);
|
console.error('Query failed to return results');
|
||||||
throw new Error('Query did not return expected results');
|
throw new Error('Query did not return expected results');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
let cumulativeProcessedOrders = 0;
|
let cumulativeProcessedOrders = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Begin transaction
|
||||||
|
await localConnection.beginTransaction();
|
||||||
|
|
||||||
// Get last sync info
|
// Get last sync info
|
||||||
const [syncInfo] = await localConnection.query(
|
const [syncInfo] = await localConnection.query(
|
||||||
"SELECT last_sync_timestamp FROM sync_status WHERE table_name = 'orders'"
|
"SELECT last_sync_timestamp FROM sync_status WHERE table_name = 'orders'"
|
||||||
@@ -38,7 +41,6 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
const [[{ total }]] = await prodConnection.query(`
|
const [[{ total }]] = await prodConnection.query(`
|
||||||
SELECT COUNT(*) as total
|
SELECT COUNT(*) as total
|
||||||
FROM order_items oi
|
FROM order_items oi
|
||||||
USE INDEX (PRIMARY)
|
|
||||||
JOIN _order o ON oi.order_id = o.order_id
|
JOIN _order o ON oi.order_id = o.order_id
|
||||||
WHERE o.order_status >= 15
|
WHERE o.order_status >= 15
|
||||||
AND o.date_placed_onlydate >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
AND o.date_placed_onlydate >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
||||||
@@ -78,7 +80,6 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
COALESCE(oi.prod_price_reg - oi.prod_price, 0) as base_discount,
|
COALESCE(oi.prod_price_reg - oi.prod_price, 0) as base_discount,
|
||||||
oi.stamp as last_modified
|
oi.stamp as last_modified
|
||||||
FROM order_items oi
|
FROM order_items oi
|
||||||
USE INDEX (PRIMARY)
|
|
||||||
JOIN _order o ON oi.order_id = o.order_id
|
JOIN _order o ON oi.order_id = o.order_id
|
||||||
WHERE o.order_status >= 15
|
WHERE o.order_status >= 15
|
||||||
AND o.date_placed_onlydate >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
AND o.date_placed_onlydate >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
||||||
@@ -105,15 +106,15 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
|
|
||||||
console.log('Orders: Found', orderItems.length, 'order items to process');
|
console.log('Orders: Found', orderItems.length, 'order items to process');
|
||||||
|
|
||||||
// Create tables in PostgreSQL for debugging
|
// Create tables in PostgreSQL for data processing
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
DROP TABLE IF EXISTS debug_order_items;
|
DROP TABLE IF EXISTS temp_order_items;
|
||||||
DROP TABLE IF EXISTS debug_order_meta;
|
DROP TABLE IF EXISTS temp_order_meta;
|
||||||
DROP TABLE IF EXISTS debug_order_discounts;
|
DROP TABLE IF EXISTS temp_order_discounts;
|
||||||
DROP TABLE IF EXISTS debug_order_taxes;
|
DROP TABLE IF EXISTS temp_order_taxes;
|
||||||
DROP TABLE IF EXISTS debug_order_costs;
|
DROP TABLE IF EXISTS temp_order_costs;
|
||||||
|
|
||||||
CREATE TABLE debug_order_items (
|
CREATE TEMP TABLE temp_order_items (
|
||||||
order_id INTEGER NOT NULL,
|
order_id INTEGER NOT NULL,
|
||||||
pid INTEGER NOT NULL,
|
pid INTEGER NOT NULL,
|
||||||
SKU VARCHAR(50) NOT NULL,
|
SKU VARCHAR(50) NOT NULL,
|
||||||
@@ -123,7 +124,7 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
PRIMARY KEY (order_id, pid)
|
PRIMARY KEY (order_id, pid)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE debug_order_meta (
|
CREATE TEMP TABLE temp_order_meta (
|
||||||
order_id INTEGER NOT NULL,
|
order_id INTEGER NOT NULL,
|
||||||
date DATE NOT NULL,
|
date DATE NOT NULL,
|
||||||
customer VARCHAR(100) NOT NULL,
|
customer VARCHAR(100) NOT NULL,
|
||||||
@@ -135,26 +136,29 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
PRIMARY KEY (order_id)
|
PRIMARY KEY (order_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE debug_order_discounts (
|
CREATE TEMP TABLE temp_order_discounts (
|
||||||
order_id INTEGER NOT NULL,
|
order_id INTEGER NOT NULL,
|
||||||
pid INTEGER NOT NULL,
|
pid INTEGER NOT NULL,
|
||||||
discount DECIMAL(10,2) NOT NULL,
|
discount DECIMAL(10,2) NOT NULL,
|
||||||
PRIMARY KEY (order_id, pid)
|
PRIMARY KEY (order_id, pid)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE debug_order_taxes (
|
CREATE TEMP TABLE temp_order_taxes (
|
||||||
order_id INTEGER NOT NULL,
|
order_id INTEGER NOT NULL,
|
||||||
pid INTEGER NOT NULL,
|
pid INTEGER NOT NULL,
|
||||||
tax DECIMAL(10,2) NOT NULL,
|
tax DECIMAL(10,2) NOT NULL,
|
||||||
PRIMARY KEY (order_id, pid)
|
PRIMARY KEY (order_id, pid)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE debug_order_costs (
|
CREATE TEMP TABLE temp_order_costs (
|
||||||
order_id INTEGER NOT NULL,
|
order_id INTEGER NOT NULL,
|
||||||
pid INTEGER NOT NULL,
|
pid INTEGER NOT NULL,
|
||||||
costeach DECIMAL(10,3) DEFAULT 0.000,
|
costeach DECIMAL(10,3) DEFAULT 0.000,
|
||||||
PRIMARY KEY (order_id, pid)
|
PRIMARY KEY (order_id, pid)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_temp_order_items_pid ON temp_order_items(pid);
|
||||||
|
CREATE INDEX idx_temp_order_meta_order_id ON temp_order_meta(order_id);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Insert order items in batches
|
// Insert order items in batches
|
||||||
@@ -168,7 +172,7 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
INSERT INTO debug_order_items (order_id, pid, SKU, price, quantity, base_discount)
|
INSERT INTO temp_order_items (order_id, pid, SKU, price, quantity, base_discount)
|
||||||
VALUES ${placeholders}
|
VALUES ${placeholders}
|
||||||
ON CONFLICT (order_id, pid) DO UPDATE SET
|
ON CONFLICT (order_id, pid) DO UPDATE SET
|
||||||
SKU = EXCLUDED.SKU,
|
SKU = EXCLUDED.SKU,
|
||||||
@@ -239,7 +243,7 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
INSERT INTO debug_order_meta (
|
INSERT INTO temp_order_meta (
|
||||||
order_id, date, customer, customer_name, status, canceled,
|
order_id, date, customer, customer_name, status, canceled,
|
||||||
summary_discount, summary_subtotal
|
summary_discount, summary_subtotal
|
||||||
)
|
)
|
||||||
@@ -281,7 +285,7 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
INSERT INTO debug_order_discounts (order_id, pid, discount)
|
INSERT INTO temp_order_discounts (order_id, pid, discount)
|
||||||
VALUES ${placeholders}
|
VALUES ${placeholders}
|
||||||
ON CONFLICT (order_id, pid) DO UPDATE SET
|
ON CONFLICT (order_id, pid) DO UPDATE SET
|
||||||
discount = EXCLUDED.discount
|
discount = EXCLUDED.discount
|
||||||
@@ -321,7 +325,7 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
INSERT INTO debug_order_taxes (order_id, pid, tax)
|
INSERT INTO temp_order_taxes (order_id, pid, tax)
|
||||||
VALUES ${placeholders}
|
VALUES ${placeholders}
|
||||||
ON CONFLICT (order_id, pid) DO UPDATE SET
|
ON CONFLICT (order_id, pid) DO UPDATE SET
|
||||||
tax = EXCLUDED.tax
|
tax = EXCLUDED.tax
|
||||||
@@ -330,14 +334,23 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const processCostsBatch = async (batchIds) => {
|
const processCostsBatch = async (batchIds) => {
|
||||||
|
// Modified query to ensure one row per order_id/pid by using a subquery
|
||||||
const [costs] = await prodConnection.query(`
|
const [costs] = await prodConnection.query(`
|
||||||
SELECT
|
SELECT
|
||||||
oc.orderid as order_id,
|
oc.orderid as order_id,
|
||||||
oc.pid,
|
oc.pid,
|
||||||
oc.costeach
|
oc.costeach
|
||||||
FROM order_costs oc
|
FROM order_costs oc
|
||||||
WHERE oc.orderid IN (?)
|
INNER JOIN (
|
||||||
AND oc.pending = 0
|
SELECT
|
||||||
|
orderid,
|
||||||
|
pid,
|
||||||
|
MAX(id) as max_id
|
||||||
|
FROM order_costs
|
||||||
|
WHERE orderid IN (?)
|
||||||
|
AND pending = 0
|
||||||
|
GROUP BY orderid, pid
|
||||||
|
) latest ON oc.orderid = latest.orderid AND oc.pid = latest.pid AND oc.id = latest.max_id
|
||||||
`, [batchIds]);
|
`, [batchIds]);
|
||||||
|
|
||||||
if (costs.length === 0) return;
|
if (costs.length === 0) return;
|
||||||
@@ -357,7 +370,7 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
INSERT INTO debug_order_costs (order_id, pid, costeach)
|
INSERT INTO temp_order_costs (order_id, pid, costeach)
|
||||||
VALUES ${placeholders}
|
VALUES ${placeholders}
|
||||||
ON CONFLICT (order_id, pid) DO UPDATE SET
|
ON CONFLICT (order_id, pid) DO UPDATE SET
|
||||||
costeach = EXCLUDED.costeach
|
costeach = EXCLUDED.costeach
|
||||||
@@ -417,9 +430,9 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
SUM(COALESCE(od.discount, 0)) as promo_discount,
|
SUM(COALESCE(od.discount, 0)) as promo_discount,
|
||||||
COALESCE(ot.tax, 0) as total_tax,
|
COALESCE(ot.tax, 0) as total_tax,
|
||||||
COALESCE(oi.price * 0.5, 0) as costeach
|
COALESCE(oi.price * 0.5, 0) as costeach
|
||||||
FROM debug_order_items oi
|
FROM temp_order_items oi
|
||||||
LEFT JOIN debug_order_discounts od ON oi.order_id = od.order_id AND oi.pid = od.pid
|
LEFT JOIN temp_order_discounts od ON oi.order_id = od.order_id AND oi.pid = od.pid
|
||||||
LEFT JOIN debug_order_taxes ot ON oi.order_id = ot.order_id AND oi.pid = ot.pid
|
LEFT JOIN temp_order_taxes ot ON oi.order_id = ot.order_id AND oi.pid = ot.pid
|
||||||
GROUP BY oi.order_id, oi.pid, ot.tax
|
GROUP BY oi.order_id, oi.pid, ot.tax
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
@@ -447,11 +460,11 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
FROM (
|
FROM (
|
||||||
SELECT DISTINCT ON (order_id, pid)
|
SELECT DISTINCT ON (order_id, pid)
|
||||||
order_id, pid, SKU, price, quantity, base_discount
|
order_id, pid, SKU, price, quantity, base_discount
|
||||||
FROM debug_order_items
|
FROM temp_order_items
|
||||||
WHERE order_id = ANY($1)
|
WHERE order_id = ANY($1)
|
||||||
ORDER BY order_id, pid
|
ORDER BY order_id, pid
|
||||||
) oi
|
) oi
|
||||||
JOIN debug_order_meta om ON oi.order_id = om.order_id
|
JOIN temp_order_meta om ON oi.order_id = om.order_id
|
||||||
LEFT JOIN order_totals ot ON oi.order_id = ot.order_id AND oi.pid = ot.pid
|
LEFT JOIN order_totals ot ON oi.order_id = ot.order_id AND oi.pid = ot.pid
|
||||||
ORDER BY oi.order_id, oi.pid
|
ORDER BY oi.order_id, oi.pid
|
||||||
`, [subBatchIds]);
|
`, [subBatchIds]);
|
||||||
@@ -529,8 +542,8 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
`, batchValues);
|
`, batchValues);
|
||||||
|
|
||||||
const { inserted, updated } = result.rows[0];
|
const { inserted, updated } = result.rows[0];
|
||||||
recordsAdded += inserted;
|
recordsAdded += parseInt(inserted) || 0;
|
||||||
recordsUpdated += updated;
|
recordsUpdated += parseInt(updated) || 0;
|
||||||
importedCount += subBatch.length;
|
importedCount += subBatch.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,18 +569,38 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
last_sync_timestamp = NOW()
|
last_sync_timestamp = NOW()
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Cleanup temporary tables
|
||||||
|
await localConnection.query(`
|
||||||
|
DROP TABLE IF EXISTS temp_order_items;
|
||||||
|
DROP TABLE IF EXISTS temp_order_meta;
|
||||||
|
DROP TABLE IF EXISTS temp_order_discounts;
|
||||||
|
DROP TABLE IF EXISTS temp_order_taxes;
|
||||||
|
DROP TABLE IF EXISTS temp_order_costs;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
await localConnection.commit();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "complete",
|
status: "complete",
|
||||||
totalImported: Math.floor(importedCount),
|
totalImported: Math.floor(importedCount) || 0,
|
||||||
recordsAdded: recordsAdded || 0,
|
recordsAdded: parseInt(recordsAdded) || 0,
|
||||||
recordsUpdated: Math.floor(recordsUpdated),
|
recordsUpdated: parseInt(recordsUpdated) || 0,
|
||||||
totalSkipped: skippedOrders.size,
|
totalSkipped: skippedOrders.size || 0,
|
||||||
missingProducts: missingProducts.size,
|
missingProducts: missingProducts.size || 0,
|
||||||
incrementalUpdate,
|
incrementalUpdate,
|
||||||
lastSyncTime
|
lastSyncTime
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during orders import:", error);
|
console.error("Error during orders import:", error);
|
||||||
|
|
||||||
|
// Rollback transaction
|
||||||
|
try {
|
||||||
|
await localConnection.rollback();
|
||||||
|
} catch (rollbackError) {
|
||||||
|
console.error("Error during rollback:", rollbackError);
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ const { outputProgress, formatElapsedTime, estimateRemaining, calculateRate } =
|
|||||||
const BATCH_SIZE = 100; // Smaller batch size for better progress tracking
|
const BATCH_SIZE = 100; // Smaller batch size for better progress tracking
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
const RETRY_DELAY = 5000; // 5 seconds
|
const RETRY_DELAY = 5000; // 5 seconds
|
||||||
|
const dotenv = require("dotenv");
|
||||||
|
const path = require("path");
|
||||||
|
dotenv.config({ path: path.join(__dirname, "../../.env") });
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
const imageUrlBase = 'https://sbing.com/i/products/0000/';
|
const imageUrlBase = process.env.PRODUCT_IMAGE_URL_BASE || 'https://sbing.com/i/products/0000/';
|
||||||
const getImageUrls = (pid, iid = 1) => {
|
const getImageUrls = (pid, iid = 1) => {
|
||||||
const paddedPid = pid.toString().padStart(6, '0');
|
const paddedPid = pid.toString().padStart(6, '0');
|
||||||
// Use padded PID only for the first 3 digits
|
// Use padded PID only for the first 3 digits
|
||||||
@@ -18,7 +21,7 @@ const getImageUrls = (pid, iid = 1) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add helper function for retrying operations
|
// Add helper function for retrying operations with exponential backoff
|
||||||
async function withRetry(operation, errorMessage) {
|
async function withRetry(operation, errorMessage) {
|
||||||
let lastError;
|
let lastError;
|
||||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
@@ -28,7 +31,8 @@ async function withRetry(operation, errorMessage) {
|
|||||||
lastError = error;
|
lastError = error;
|
||||||
console.error(`${errorMessage} (Attempt ${attempt}/${MAX_RETRIES}):`, error);
|
console.error(`${errorMessage} (Attempt ${attempt}/${MAX_RETRIES}):`, error);
|
||||||
if (attempt < MAX_RETRIES) {
|
if (attempt < MAX_RETRIES) {
|
||||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
const backoffTime = RETRY_DELAY * Math.pow(2, attempt - 1);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, backoffTime));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -772,32 +776,44 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
recordsAdded += parseInt(result.rows[0].inserted, 10) || 0;
|
recordsAdded += parseInt(result.rows[0].inserted, 10) || 0;
|
||||||
recordsUpdated += parseInt(result.rows[0].updated, 10) || 0;
|
recordsUpdated += parseInt(result.rows[0].updated, 10) || 0;
|
||||||
|
|
||||||
// Process category relationships for each product in the batch
|
// Process category relationships in batches
|
||||||
|
const allCategories = [];
|
||||||
for (const row of batch) {
|
for (const row of batch) {
|
||||||
if (row.categories) {
|
if (row.categories) {
|
||||||
const categoryIds = row.categories.split(',').filter(id => id && id.trim());
|
const categoryIds = row.categories.split(',').filter(id => id && id.trim());
|
||||||
if (categoryIds.length > 0) {
|
if (categoryIds.length > 0) {
|
||||||
const catPlaceholders = categoryIds.map((_, idx) =>
|
categoryIds.forEach(catId => {
|
||||||
`($${idx * 2 + 1}, $${idx * 2 + 2})`
|
allCategories.push([row.pid, parseInt(catId.trim(), 10)]);
|
||||||
).join(',');
|
});
|
||||||
const catValues = categoryIds.flatMap(catId => [row.pid, parseInt(catId.trim(), 10)]);
|
|
||||||
|
|
||||||
// First delete existing relationships for this product
|
|
||||||
await localConnection.query(
|
|
||||||
'DELETE FROM product_categories WHERE pid = $1',
|
|
||||||
[row.pid]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Then insert the new relationships
|
|
||||||
await localConnection.query(`
|
|
||||||
INSERT INTO product_categories (pid, cat_id)
|
|
||||||
VALUES ${catPlaceholders}
|
|
||||||
ON CONFLICT (pid, cat_id) DO NOTHING
|
|
||||||
`, catValues);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have categories to process
|
||||||
|
if (allCategories.length > 0) {
|
||||||
|
// First get all products in this batch
|
||||||
|
const productIds = batch.map(p => p.pid);
|
||||||
|
|
||||||
|
// Delete all existing relationships for products in this batch
|
||||||
|
await localConnection.query(
|
||||||
|
'DELETE FROM product_categories WHERE pid = ANY($1)',
|
||||||
|
[productIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert all new relationships in one batch
|
||||||
|
const catPlaceholders = allCategories.map((_, idx) =>
|
||||||
|
`($${idx * 2 + 1}, $${idx * 2 + 2})`
|
||||||
|
).join(',');
|
||||||
|
|
||||||
|
const catValues = allCategories.flat();
|
||||||
|
|
||||||
|
await localConnection.query(`
|
||||||
|
INSERT INTO product_categories (pid, cat_id)
|
||||||
|
VALUES ${catPlaceholders}
|
||||||
|
ON CONFLICT (pid, cat_id) DO NOTHING
|
||||||
|
`, catValues);
|
||||||
|
}
|
||||||
|
|
||||||
outputProgress({
|
outputProgress({
|
||||||
status: "running",
|
status: "running",
|
||||||
operation: "Products import",
|
operation: "Products import",
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
|
|||||||
let recordsUpdated = 0;
|
let recordsUpdated = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Begin transaction for the entire import process
|
||||||
|
await localConnection.beginTransaction();
|
||||||
|
|
||||||
// Get last sync info
|
// Get last sync info
|
||||||
const [syncInfo] = await localConnection.query(
|
const [syncInfo] = await localConnection.query(
|
||||||
"SELECT last_sync_timestamp FROM sync_status WHERE table_name = 'purchase_orders'"
|
"SELECT last_sync_timestamp FROM sync_status WHERE table_name = 'purchase_orders'"
|
||||||
@@ -14,12 +17,10 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
|
|||||||
|
|
||||||
console.log('Purchase Orders: Using last sync time:', lastSyncTime);
|
console.log('Purchase Orders: Using last sync time:', lastSyncTime);
|
||||||
|
|
||||||
// Create temporary tables with PostgreSQL syntax
|
// Create temp tables
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
DROP TABLE IF EXISTS temp_purchase_orders;
|
DROP TABLE IF EXISTS temp_purchase_orders;
|
||||||
DROP TABLE IF EXISTS temp_po_receivings;
|
CREATE TABLE temp_purchase_orders (
|
||||||
|
|
||||||
CREATE TEMP TABLE temp_purchase_orders (
|
|
||||||
po_id INTEGER NOT NULL,
|
po_id INTEGER NOT NULL,
|
||||||
pid INTEGER NOT NULL,
|
pid INTEGER NOT NULL,
|
||||||
sku VARCHAR(50),
|
sku VARCHAR(50),
|
||||||
@@ -33,60 +34,14 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
|
|||||||
cost_price DECIMAL(10,3),
|
cost_price DECIMAL(10,3),
|
||||||
PRIMARY KEY (po_id, pid)
|
PRIMARY KEY (po_id, pid)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TEMP TABLE temp_po_receivings (
|
|
||||||
po_id INTEGER,
|
|
||||||
pid INTEGER NOT NULL,
|
|
||||||
receiving_id INTEGER NOT NULL,
|
|
||||||
qty_each INTEGER,
|
|
||||||
cost_each DECIMAL(10,3),
|
|
||||||
received_date TIMESTAMP WITH TIME ZONE,
|
|
||||||
received_by INTEGER,
|
|
||||||
received_by_name VARCHAR(255),
|
|
||||||
is_alt_po INTEGER,
|
|
||||||
PRIMARY KEY (receiving_id, pid)
|
|
||||||
);
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
outputProgress({
|
|
||||||
operation: `Starting ${incrementalUpdate ? 'incremental' : 'full'} purchase orders import`,
|
|
||||||
status: "running",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get column names - Keep MySQL compatible for production
|
|
||||||
const [columns] = await prodConnection.query(`
|
|
||||||
SELECT COLUMN_NAME
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_NAME = 'purchase_orders'
|
|
||||||
AND COLUMN_NAME != 'updated' -- Exclude the updated column
|
|
||||||
ORDER BY ORDINAL_POSITION
|
|
||||||
`);
|
|
||||||
const columnNames = columns.map(col => col.COLUMN_NAME);
|
|
||||||
|
|
||||||
// Build incremental conditions
|
|
||||||
const incrementalWhereClause = incrementalUpdate
|
|
||||||
? `AND (
|
|
||||||
p.date_updated > ?
|
|
||||||
OR p.date_ordered > ?
|
|
||||||
OR p.date_estin > ?
|
|
||||||
OR r.date_updated > ?
|
|
||||||
OR r.date_created > ?
|
|
||||||
OR r.date_checked > ?
|
|
||||||
OR rp.stamp > ?
|
|
||||||
OR rp.received_date > ?
|
|
||||||
)`
|
|
||||||
: "";
|
|
||||||
const incrementalParams = incrementalUpdate
|
|
||||||
? [lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// First get all relevant PO IDs with basic info - Keep MySQL compatible for production
|
// First get all relevant PO IDs with basic info - Keep MySQL compatible for production
|
||||||
const [[{ total }]] = await prodConnection.query(`
|
const [[{ total }]] = await prodConnection.query(`
|
||||||
SELECT COUNT(*) as total
|
SELECT COUNT(*) as total
|
||||||
FROM (
|
FROM (
|
||||||
SELECT DISTINCT pop.po_id, pop.pid
|
SELECT DISTINCT pop.po_id, pop.pid
|
||||||
FROM po p
|
FROM po p
|
||||||
USE INDEX (idx_date_created)
|
|
||||||
JOIN po_products pop ON p.po_id = pop.po_id
|
JOIN po_products pop ON p.po_id = pop.po_id
|
||||||
JOIN suppliers s ON p.supplier_id = s.supplierid
|
JOIN suppliers s ON p.supplier_id = s.supplierid
|
||||||
WHERE p.date_ordered >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
WHERE p.date_ordered >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
||||||
@@ -97,520 +52,165 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
|
|||||||
OR p.date_estin > ?
|
OR p.date_estin > ?
|
||||||
)
|
)
|
||||||
` : ''}
|
` : ''}
|
||||||
UNION
|
|
||||||
SELECT DISTINCT r.receiving_id as po_id, rp.pid
|
|
||||||
FROM receivings_products rp
|
|
||||||
USE INDEX (received_date)
|
|
||||||
LEFT JOIN receivings r ON r.receiving_id = rp.receiving_id
|
|
||||||
WHERE rp.received_date >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
|
||||||
${incrementalUpdate ? `
|
|
||||||
AND (
|
|
||||||
r.date_created > ?
|
|
||||||
OR r.date_checked > ?
|
|
||||||
OR rp.stamp > ?
|
|
||||||
OR rp.received_date > ?
|
|
||||||
)
|
|
||||||
` : ''}
|
|
||||||
) all_items
|
) all_items
|
||||||
`, incrementalUpdate ? [
|
`, incrementalUpdate ? [lastSyncTime, lastSyncTime, lastSyncTime] : []);
|
||||||
lastSyncTime, lastSyncTime, lastSyncTime, // PO conditions
|
|
||||||
lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime // Receiving conditions
|
|
||||||
] : []);
|
|
||||||
|
|
||||||
console.log('Purchase Orders: Found changes:', total);
|
console.log('Purchase Orders: Found changes:', total);
|
||||||
|
|
||||||
// Get PO list - Keep MySQL compatible for production
|
// Get PO list - Keep MySQL compatible for production
|
||||||
const [poList] = await prodConnection.query(`
|
console.log('Fetching purchase orders in batches...');
|
||||||
SELECT DISTINCT
|
|
||||||
COALESCE(p.po_id, r.receiving_id) as po_id,
|
|
||||||
COALESCE(
|
|
||||||
NULLIF(s1.companyname, ''),
|
|
||||||
NULLIF(s2.companyname, ''),
|
|
||||||
'Unknown Vendor'
|
|
||||||
) as vendor,
|
|
||||||
CASE
|
|
||||||
WHEN p.po_id IS NOT NULL THEN
|
|
||||||
COALESCE(
|
|
||||||
NULLIF(p.date_ordered, '0000-00-00 00:00:00'),
|
|
||||||
p.date_created
|
|
||||||
)
|
|
||||||
WHEN r.receiving_id IS NOT NULL THEN
|
|
||||||
r.date_created
|
|
||||||
END as date,
|
|
||||||
CASE
|
|
||||||
WHEN p.date_estin = '0000-00-00' THEN NULL
|
|
||||||
WHEN p.date_estin IS NULL THEN NULL
|
|
||||||
WHEN p.date_estin NOT REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' THEN NULL
|
|
||||||
ELSE p.date_estin
|
|
||||||
END as expected_date,
|
|
||||||
COALESCE(p.status, 50) as status,
|
|
||||||
p.short_note as notes,
|
|
||||||
p.notes as long_note
|
|
||||||
FROM (
|
|
||||||
SELECT po_id FROM po
|
|
||||||
USE INDEX (idx_date_created)
|
|
||||||
WHERE date_ordered >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
|
||||||
${incrementalUpdate ? `
|
|
||||||
AND (
|
|
||||||
date_ordered > ?
|
|
||||||
OR date_updated > ?
|
|
||||||
OR date_estin > ?
|
|
||||||
)
|
|
||||||
` : ''}
|
|
||||||
UNION
|
|
||||||
SELECT DISTINCT r.receiving_id as po_id
|
|
||||||
FROM receivings r
|
|
||||||
JOIN receivings_products rp USE INDEX (received_date) ON r.receiving_id = rp.receiving_id
|
|
||||||
WHERE rp.received_date >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
|
||||||
${incrementalUpdate ? `
|
|
||||||
AND (
|
|
||||||
r.date_created > ?
|
|
||||||
OR r.date_checked > ?
|
|
||||||
OR rp.stamp > ?
|
|
||||||
OR rp.received_date > ?
|
|
||||||
)
|
|
||||||
` : ''}
|
|
||||||
) ids
|
|
||||||
LEFT JOIN po p ON ids.po_id = p.po_id
|
|
||||||
LEFT JOIN suppliers s1 ON p.supplier_id = s1.supplierid
|
|
||||||
LEFT JOIN receivings r ON ids.po_id = r.receiving_id
|
|
||||||
LEFT JOIN suppliers s2 ON r.supplier_id = s2.supplierid
|
|
||||||
ORDER BY po_id
|
|
||||||
`, incrementalUpdate ? [
|
|
||||||
lastSyncTime, lastSyncTime, lastSyncTime, // PO conditions
|
|
||||||
lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime // Receiving conditions
|
|
||||||
] : []);
|
|
||||||
|
|
||||||
console.log('Sample PO dates:', poList.slice(0, 5).map(po => ({
|
const FETCH_BATCH_SIZE = 5000;
|
||||||
po_id: po.po_id,
|
const INSERT_BATCH_SIZE = 200; // Process 200 records at a time for inserts
|
||||||
raw_date_ordered: po.raw_date_ordered,
|
let offset = 0;
|
||||||
raw_date_created: po.raw_date_created,
|
let allProcessed = false;
|
||||||
raw_date_estin: po.raw_date_estin,
|
|
||||||
computed_date: po.date,
|
|
||||||
expected_date: po.expected_date
|
|
||||||
})));
|
|
||||||
|
|
||||||
const totalItems = total;
|
|
||||||
let processed = 0;
|
|
||||||
|
|
||||||
const BATCH_SIZE = 5000;
|
|
||||||
const PROGRESS_INTERVAL = 500;
|
|
||||||
let lastProgressUpdate = Date.now();
|
|
||||||
|
|
||||||
outputProgress({
|
|
||||||
operation: `Starting purchase orders import - Processing ${totalItems} purchase order items`,
|
|
||||||
status: "running",
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < poList.length; i += BATCH_SIZE) {
|
|
||||||
const batch = poList.slice(i, Math.min(i + BATCH_SIZE, poList.length));
|
|
||||||
const poIds = batch.map(po => po.po_id);
|
|
||||||
|
|
||||||
// Get all products for these POs in one query - Keep MySQL compatible for production
|
|
||||||
const [poProducts] = await prodConnection.query(`
|
|
||||||
SELECT
|
|
||||||
pop.po_id,
|
|
||||||
pop.pid,
|
|
||||||
pr.itemnumber as sku,
|
|
||||||
pr.description as name,
|
|
||||||
pop.cost_each as cost_price,
|
|
||||||
pop.qty_each as ordered
|
|
||||||
FROM po_products pop
|
|
||||||
USE INDEX (PRIMARY)
|
|
||||||
JOIN products pr ON pop.pid = pr.pid
|
|
||||||
WHERE pop.po_id IN (?)
|
|
||||||
`, [poIds]);
|
|
||||||
|
|
||||||
// Process PO products in smaller sub-batches to avoid packet size issues
|
|
||||||
const SUB_BATCH_SIZE = 5000;
|
|
||||||
for (let j = 0; j < poProducts.length; j += SUB_BATCH_SIZE) {
|
|
||||||
const productBatch = poProducts.slice(j, j + SUB_BATCH_SIZE);
|
|
||||||
const productPids = [...new Set(productBatch.map(p => p.pid))];
|
|
||||||
const batchPoIds = [...new Set(productBatch.map(p => p.po_id))];
|
|
||||||
|
|
||||||
// Get receivings for this batch with employee names
|
|
||||||
const [receivings] = await prodConnection.query(`
|
|
||||||
SELECT
|
|
||||||
r.po_id,
|
|
||||||
rp.pid,
|
|
||||||
rp.receiving_id,
|
|
||||||
rp.qty_each,
|
|
||||||
rp.cost_each,
|
|
||||||
COALESCE(rp.received_date, r.date_created) as received_date,
|
|
||||||
rp.received_by,
|
|
||||||
CONCAT(e.firstname, ' ', e.lastname) as received_by_name,
|
|
||||||
CASE
|
|
||||||
WHEN r.po_id IS NULL THEN 2 -- No PO
|
|
||||||
WHEN r.po_id IN (?) THEN 0 -- Original PO
|
|
||||||
ELSE 1 -- Different PO
|
|
||||||
END as is_alt_po
|
|
||||||
FROM receivings_products rp
|
|
||||||
USE INDEX (received_date)
|
|
||||||
LEFT JOIN receivings r ON r.receiving_id = rp.receiving_id
|
|
||||||
LEFT JOIN employees e ON rp.received_by = e.employeeid
|
|
||||||
WHERE rp.pid IN (?)
|
|
||||||
AND rp.received_date >= DATE_SUB(CURRENT_DATE, INTERVAL 5 YEAR)
|
|
||||||
ORDER BY r.po_id, rp.pid, rp.received_date
|
|
||||||
`, [batchPoIds, productPids]);
|
|
||||||
|
|
||||||
// Insert receivings into temp table
|
|
||||||
if (receivings.length > 0) {
|
|
||||||
// Process in smaller chunks to avoid parameter limits
|
|
||||||
const CHUNK_SIZE = 100; // Reduce chunk size to avoid parameter limits
|
|
||||||
for (let i = 0; i < receivings.length; i += CHUNK_SIZE) {
|
|
||||||
const chunk = receivings.slice(i, Math.min(i + CHUNK_SIZE, receivings.length));
|
|
||||||
|
|
||||||
const values = [];
|
|
||||||
const placeholders = [];
|
|
||||||
|
|
||||||
chunk.forEach((r, idx) => {
|
|
||||||
values.push(
|
|
||||||
r.po_id,
|
|
||||||
r.pid,
|
|
||||||
r.receiving_id,
|
|
||||||
r.qty_each,
|
|
||||||
r.cost_each,
|
|
||||||
r.received_date,
|
|
||||||
r.received_by,
|
|
||||||
r.received_by_name || null,
|
|
||||||
r.is_alt_po
|
|
||||||
);
|
|
||||||
|
|
||||||
const offset = idx * 9;
|
|
||||||
placeholders.push(`($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7}, $${offset + 8}, $${offset + 9})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
await localConnection.query(`
|
|
||||||
INSERT INTO temp_po_receivings (
|
|
||||||
po_id, pid, receiving_id, qty_each, cost_each, received_date,
|
|
||||||
received_by, received_by_name, is_alt_po
|
|
||||||
)
|
|
||||||
VALUES ${placeholders.join(',')}
|
|
||||||
ON CONFLICT (receiving_id, pid) DO UPDATE SET
|
|
||||||
po_id = EXCLUDED.po_id,
|
|
||||||
qty_each = EXCLUDED.qty_each,
|
|
||||||
cost_each = EXCLUDED.cost_each,
|
|
||||||
received_date = EXCLUDED.received_date,
|
|
||||||
received_by = EXCLUDED.received_by,
|
|
||||||
received_by_name = EXCLUDED.received_by_name,
|
|
||||||
is_alt_po = EXCLUDED.is_alt_po
|
|
||||||
`, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process each PO product in chunks
|
|
||||||
const PRODUCT_CHUNK_SIZE = 100;
|
|
||||||
for (let i = 0; i < productBatch.length; i += PRODUCT_CHUNK_SIZE) {
|
|
||||||
const chunk = productBatch.slice(i, Math.min(i + PRODUCT_CHUNK_SIZE, productBatch.length));
|
|
||||||
const values = [];
|
|
||||||
const placeholders = [];
|
|
||||||
|
|
||||||
chunk.forEach((product, idx) => {
|
|
||||||
const po = batch.find(p => p.po_id === product.po_id);
|
|
||||||
if (!po) return;
|
|
||||||
|
|
||||||
values.push(
|
|
||||||
product.po_id,
|
|
||||||
product.pid,
|
|
||||||
product.sku,
|
|
||||||
product.name,
|
|
||||||
po.vendor,
|
|
||||||
po.date,
|
|
||||||
po.expected_date,
|
|
||||||
po.status,
|
|
||||||
po.notes || po.long_note,
|
|
||||||
product.ordered,
|
|
||||||
product.cost_price
|
|
||||||
);
|
|
||||||
|
|
||||||
const offset = idx * 11; // Updated to match 11 fields
|
|
||||||
placeholders.push(`($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7}, $${offset + 8}, $${offset + 9}, $${offset + 10}, $${offset + 11})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (placeholders.length > 0) {
|
|
||||||
await localConnection.query(`
|
|
||||||
INSERT INTO temp_purchase_orders (
|
|
||||||
po_id, pid, sku, name, vendor, date, expected_date,
|
|
||||||
status, notes, ordered, cost_price
|
|
||||||
)
|
|
||||||
VALUES ${placeholders.join(',')}
|
|
||||||
ON CONFLICT (po_id, pid) DO UPDATE SET
|
|
||||||
sku = EXCLUDED.sku,
|
|
||||||
name = EXCLUDED.name,
|
|
||||||
vendor = EXCLUDED.vendor,
|
|
||||||
date = EXCLUDED.date,
|
|
||||||
expected_date = EXCLUDED.expected_date,
|
|
||||||
status = EXCLUDED.status,
|
|
||||||
notes = EXCLUDED.notes,
|
|
||||||
ordered = EXCLUDED.ordered,
|
|
||||||
cost_price = EXCLUDED.cost_price
|
|
||||||
`, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
processed += chunk.length;
|
|
||||||
|
|
||||||
// Update progress based on time interval
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastProgressUpdate >= PROGRESS_INTERVAL || processed === totalItems) {
|
|
||||||
outputProgress({
|
|
||||||
status: "running",
|
|
||||||
operation: "Purchase orders import",
|
|
||||||
current: processed,
|
|
||||||
total: totalItems,
|
|
||||||
elapsed: formatElapsedTime((Date.now() - startTime) / 1000),
|
|
||||||
remaining: estimateRemaining(startTime, processed, totalItems),
|
|
||||||
rate: calculateRate(startTime, processed)
|
|
||||||
});
|
|
||||||
lastProgressUpdate = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert final data into purchase_orders table in chunks
|
|
||||||
const FINAL_CHUNK_SIZE = 1000;
|
|
||||||
let totalProcessed = 0;
|
let totalProcessed = 0;
|
||||||
const totalPosResult = await localConnection.query('SELECT COUNT(*) as total_pos FROM temp_purchase_orders');
|
|
||||||
const total_pos = parseInt(totalPosResult.rows?.[0]?.total_pos || '0', 10);
|
|
||||||
|
|
||||||
outputProgress({
|
while (!allProcessed) {
|
||||||
status: "running",
|
console.log(`Fetching batch at offset ${offset}...`);
|
||||||
operation: "Purchase orders final import",
|
const [poList] = await prodConnection.query(`
|
||||||
message: `Processing ${total_pos} purchase orders for final import`,
|
SELECT DISTINCT
|
||||||
current: 0,
|
COALESCE(p.po_id, 0) as po_id,
|
||||||
total: total_pos
|
pop.pid,
|
||||||
});
|
COALESCE(NULLIF(pr.itemnumber, ''), 'NO-SKU') as sku,
|
||||||
|
COALESCE(pr.description, 'Unknown Product') as name,
|
||||||
|
COALESCE(NULLIF(s.companyname, ''), 'Unknown Vendor') as vendor,
|
||||||
|
COALESCE(p.date_ordered, p.date_created) as date,
|
||||||
|
p.date_estin as expected_date,
|
||||||
|
COALESCE(p.status, 1) as status,
|
||||||
|
COALESCE(p.short_note, p.notes) as notes,
|
||||||
|
pop.qty_each as ordered,
|
||||||
|
pop.cost_each as cost_price
|
||||||
|
FROM po p
|
||||||
|
JOIN po_products pop ON p.po_id = pop.po_id
|
||||||
|
JOIN products pr ON pop.pid = pr.pid
|
||||||
|
LEFT JOIN suppliers s ON p.supplier_id = s.supplierid
|
||||||
|
WHERE p.date_ordered >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR)
|
||||||
|
${incrementalUpdate ? `
|
||||||
|
AND (
|
||||||
|
p.date_updated > ?
|
||||||
|
OR p.date_ordered > ?
|
||||||
|
OR p.date_estin > ?
|
||||||
|
)
|
||||||
|
` : ''}
|
||||||
|
ORDER BY p.po_id, pop.pid
|
||||||
|
LIMIT ${FETCH_BATCH_SIZE} OFFSET ${offset}
|
||||||
|
`, incrementalUpdate ? [lastSyncTime, lastSyncTime, lastSyncTime] : []);
|
||||||
|
|
||||||
// Process in chunks using cursor-based pagination
|
if (poList.length === 0) {
|
||||||
let lastPoId = 0;
|
allProcessed = true;
|
||||||
let lastPid = 0;
|
|
||||||
let recordsAdded = 0;
|
|
||||||
let recordsUpdated = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
console.log('Fetching next chunk with lastPoId:', lastPoId, 'lastPid:', lastPid);
|
|
||||||
const chunkResult = await localConnection.query(`
|
|
||||||
SELECT po_id, pid FROM temp_purchase_orders
|
|
||||||
WHERE (po_id, pid) > ($1, $2)
|
|
||||||
ORDER BY po_id, pid
|
|
||||||
LIMIT $3
|
|
||||||
`, [lastPoId, lastPid, FINAL_CHUNK_SIZE]);
|
|
||||||
|
|
||||||
if (!chunkResult?.rows) {
|
|
||||||
console.error('No rows returned from chunk query:', chunkResult);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunk = chunkResult.rows;
|
console.log(`Processing batch of ${poList.length} purchase order items (${offset}-${offset + poList.length})`);
|
||||||
console.log('Got chunk of size:', chunk.length);
|
|
||||||
if (chunk.length === 0) break;
|
|
||||||
|
|
||||||
const result = await localConnection.query(`
|
// Process in smaller batches for inserts
|
||||||
WITH inserted_pos AS (
|
for (let i = 0; i < poList.length; i += INSERT_BATCH_SIZE) {
|
||||||
INSERT INTO purchase_orders (
|
const batch = poList.slice(i, Math.min(i + INSERT_BATCH_SIZE, poList.length));
|
||||||
po_id, pid, sku, name, cost_price, po_cost_price,
|
|
||||||
vendor, date, expected_date, status, notes,
|
// Create parameterized query with placeholders
|
||||||
ordered, received, receiving_status,
|
const placeholders = batch.map((_, idx) => {
|
||||||
received_date, last_received_date, received_by,
|
const base = idx * 11; // 11 columns
|
||||||
receiving_history
|
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, $${base + 11})`;
|
||||||
|
}).join(',');
|
||||||
|
|
||||||
|
// Create flattened values array
|
||||||
|
const values = batch.flatMap(po => [
|
||||||
|
po.po_id,
|
||||||
|
po.pid,
|
||||||
|
po.sku,
|
||||||
|
po.name,
|
||||||
|
po.vendor,
|
||||||
|
po.date,
|
||||||
|
po.expected_date,
|
||||||
|
po.status,
|
||||||
|
po.notes,
|
||||||
|
po.ordered,
|
||||||
|
po.cost_price
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Execute batch insert
|
||||||
|
await localConnection.query(`
|
||||||
|
INSERT INTO temp_purchase_orders (
|
||||||
|
po_id, pid, sku, name, vendor, date, expected_date,
|
||||||
|
status, notes, ordered, cost_price
|
||||||
)
|
)
|
||||||
SELECT
|
VALUES ${placeholders}
|
||||||
po.po_id,
|
|
||||||
po.pid,
|
|
||||||
po.sku,
|
|
||||||
po.name,
|
|
||||||
COALESCE(
|
|
||||||
(
|
|
||||||
SELECT cost_each
|
|
||||||
FROM temp_po_receivings r2
|
|
||||||
WHERE r2.pid = po.pid
|
|
||||||
AND r2.po_id = po.po_id
|
|
||||||
AND r2.is_alt_po = 0
|
|
||||||
AND r2.cost_each > 0
|
|
||||||
ORDER BY r2.received_date
|
|
||||||
LIMIT 1
|
|
||||||
),
|
|
||||||
po.cost_price
|
|
||||||
) as cost_price,
|
|
||||||
po.cost_price as po_cost_price,
|
|
||||||
po.vendor,
|
|
||||||
po.date,
|
|
||||||
po.expected_date,
|
|
||||||
po.status,
|
|
||||||
po.notes,
|
|
||||||
po.ordered,
|
|
||||||
COALESCE(SUM(CASE WHEN r.is_alt_po = 0 THEN r.qty_each END), 0) as received,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(r.receiving_id) = 0 THEN 1 -- created
|
|
||||||
WHEN SUM(CASE WHEN r.is_alt_po = 0 THEN r.qty_each END) < po.ordered THEN 30 -- partial
|
|
||||||
ELSE 40 -- full
|
|
||||||
END as receiving_status,
|
|
||||||
MIN(CASE WHEN r.is_alt_po = 0 THEN r.received_date END) as received_date,
|
|
||||||
MAX(CASE WHEN r.is_alt_po = 0 THEN r.received_date END) as last_received_date,
|
|
||||||
(
|
|
||||||
SELECT r2.received_by_name
|
|
||||||
FROM temp_po_receivings r2
|
|
||||||
WHERE r2.pid = po.pid
|
|
||||||
AND r2.is_alt_po = 0
|
|
||||||
ORDER BY r2.received_date
|
|
||||||
LIMIT 1
|
|
||||||
) as received_by,
|
|
||||||
jsonb_build_object(
|
|
||||||
'ordered_qty', po.ordered,
|
|
||||||
'total_received', COALESCE(SUM(CASE WHEN r.is_alt_po = 0 THEN r.qty_each END), 0),
|
|
||||||
'remaining_unfulfilled', GREATEST(0, po.ordered - COALESCE(SUM(CASE WHEN r.is_alt_po = 0 THEN r.qty_each END), 0)),
|
|
||||||
'excess_received', GREATEST(0, COALESCE(SUM(CASE WHEN r.is_alt_po = 0 THEN r.qty_each END), 0) - po.ordered),
|
|
||||||
'po_cost', po.cost_price,
|
|
||||||
'actual_cost', COALESCE(
|
|
||||||
(
|
|
||||||
SELECT cost_each
|
|
||||||
FROM temp_po_receivings r2
|
|
||||||
WHERE r2.pid = po.pid
|
|
||||||
AND r2.is_alt_po = 0
|
|
||||||
AND r2.cost_each > 0
|
|
||||||
ORDER BY r2.received_date
|
|
||||||
LIMIT 1
|
|
||||||
),
|
|
||||||
po.cost_price
|
|
||||||
),
|
|
||||||
'fulfillment', (
|
|
||||||
SELECT jsonb_agg(
|
|
||||||
jsonb_build_object(
|
|
||||||
'receiving_id', r2.receiving_id,
|
|
||||||
'qty_applied', CASE
|
|
||||||
WHEN r2.running_total <= po.ordered THEN r2.qty_each
|
|
||||||
WHEN r2.running_total - r2.qty_each < po.ordered THEN po.ordered - (r2.running_total - r2.qty_each)
|
|
||||||
ELSE 0
|
|
||||||
END,
|
|
||||||
'qty_total', r2.qty_each,
|
|
||||||
'cost', r2.cost_each,
|
|
||||||
'date', r2.received_date,
|
|
||||||
'received_by', r2.received_by,
|
|
||||||
'received_by_name', r2.received_by_name,
|
|
||||||
'type', CASE r2.is_alt_po
|
|
||||||
WHEN 0 THEN 'original'
|
|
||||||
WHEN 1 THEN 'alternate'
|
|
||||||
ELSE 'no_po'
|
|
||||||
END,
|
|
||||||
'remaining_qty', CASE
|
|
||||||
WHEN r2.running_total <= po.ordered THEN 0
|
|
||||||
WHEN r2.running_total - r2.qty_each < po.ordered THEN r2.running_total - po.ordered
|
|
||||||
ELSE r2.qty_each
|
|
||||||
END,
|
|
||||||
'is_excess', r2.running_total > po.ordered
|
|
||||||
)
|
|
||||||
ORDER BY r2.received_date
|
|
||||||
)
|
|
||||||
FROM (
|
|
||||||
SELECT
|
|
||||||
r2.*,
|
|
||||||
SUM(r2.qty_each) OVER (
|
|
||||||
PARTITION BY r2.pid
|
|
||||||
ORDER BY r2.received_date
|
|
||||||
ROWS UNBOUNDED PRECEDING
|
|
||||||
) as running_total
|
|
||||||
FROM temp_po_receivings r2
|
|
||||||
WHERE r2.pid = po.pid
|
|
||||||
) r2
|
|
||||||
),
|
|
||||||
'alternate_po_receivings', (
|
|
||||||
SELECT jsonb_agg(
|
|
||||||
jsonb_build_object(
|
|
||||||
'receiving_id', r2.receiving_id,
|
|
||||||
'qty', r2.qty_each,
|
|
||||||
'cost', r2.cost_each,
|
|
||||||
'date', r2.received_date,
|
|
||||||
'received_by', r2.received_by,
|
|
||||||
'received_by_name', r2.received_by_name
|
|
||||||
)
|
|
||||||
ORDER BY r2.received_date
|
|
||||||
)
|
|
||||||
FROM temp_po_receivings r2
|
|
||||||
WHERE r2.pid = po.pid AND r2.is_alt_po = 1
|
|
||||||
),
|
|
||||||
'no_po_receivings', (
|
|
||||||
SELECT jsonb_agg(
|
|
||||||
jsonb_build_object(
|
|
||||||
'receiving_id', r2.receiving_id,
|
|
||||||
'qty', r2.qty_each,
|
|
||||||
'cost', r2.cost_each,
|
|
||||||
'date', r2.received_date,
|
|
||||||
'received_by', r2.received_by,
|
|
||||||
'received_by_name', r2.received_by_name
|
|
||||||
)
|
|
||||||
ORDER BY r2.received_date
|
|
||||||
)
|
|
||||||
FROM temp_po_receivings r2
|
|
||||||
WHERE r2.pid = po.pid AND r2.is_alt_po = 2
|
|
||||||
)
|
|
||||||
) as receiving_history
|
|
||||||
FROM temp_purchase_orders po
|
|
||||||
LEFT JOIN temp_po_receivings r ON po.pid = r.pid
|
|
||||||
WHERE (po.po_id, po.pid) IN (
|
|
||||||
SELECT po_id, pid FROM UNNEST($1::int[], $2::int[])
|
|
||||||
)
|
|
||||||
GROUP BY po.po_id, po.pid, po.sku, po.name, po.vendor, po.date,
|
|
||||||
po.expected_date, po.status, po.notes, po.ordered, po.cost_price
|
|
||||||
ON CONFLICT (po_id, pid) DO UPDATE SET
|
ON CONFLICT (po_id, pid) DO UPDATE SET
|
||||||
|
sku = EXCLUDED.sku,
|
||||||
|
name = EXCLUDED.name,
|
||||||
vendor = EXCLUDED.vendor,
|
vendor = EXCLUDED.vendor,
|
||||||
date = EXCLUDED.date,
|
date = EXCLUDED.date,
|
||||||
expected_date = EXCLUDED.expected_date,
|
expected_date = EXCLUDED.expected_date,
|
||||||
status = EXCLUDED.status,
|
status = EXCLUDED.status,
|
||||||
notes = EXCLUDED.notes,
|
notes = EXCLUDED.notes,
|
||||||
ordered = EXCLUDED.ordered,
|
ordered = EXCLUDED.ordered,
|
||||||
received = EXCLUDED.received,
|
cost_price = EXCLUDED.cost_price
|
||||||
receiving_status = EXCLUDED.receiving_status,
|
`, values);
|
||||||
received_date = EXCLUDED.received_date,
|
|
||||||
last_received_date = EXCLUDED.last_received_date,
|
|
||||||
received_by = EXCLUDED.received_by,
|
|
||||||
receiving_history = EXCLUDED.receiving_history,
|
|
||||||
cost_price = EXCLUDED.cost_price,
|
|
||||||
po_cost_price = EXCLUDED.po_cost_price
|
|
||||||
RETURNING xmax
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
COUNT(*) FILTER (WHERE xmax = 0) as inserted,
|
|
||||||
COUNT(*) FILTER (WHERE xmax <> 0) as updated
|
|
||||||
FROM inserted_pos
|
|
||||||
`, [
|
|
||||||
chunk.map(r => r.po_id),
|
|
||||||
chunk.map(r => r.pid)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Add debug logging
|
totalProcessed += batch.length;
|
||||||
console.log('Insert result:', result?.rows?.[0]);
|
|
||||||
|
|
||||||
// Handle the result properly for PostgreSQL with more defensive coding
|
outputProgress({
|
||||||
const resultRow = result?.rows?.[0] || {};
|
status: "running",
|
||||||
const insertCount = parseInt(resultRow.inserted || '0', 10);
|
operation: "Purchase orders import",
|
||||||
const updateCount = parseInt(resultRow.updated || '0', 10);
|
message: `Processed ${totalProcessed}/${total} purchase order items`,
|
||||||
|
current: totalProcessed,
|
||||||
|
total: total,
|
||||||
|
elapsed: formatElapsedTime((Date.now() - startTime) / 1000),
|
||||||
|
remaining: estimateRemaining(startTime, totalProcessed, total),
|
||||||
|
rate: calculateRate(startTime, totalProcessed)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
recordsAdded += insertCount;
|
// Update offset for next batch
|
||||||
recordsUpdated += updateCount;
|
offset += poList.length;
|
||||||
totalProcessed += chunk.length;
|
|
||||||
|
|
||||||
// Update progress
|
// Check if we've received fewer records than the batch size, meaning we're done
|
||||||
outputProgress({
|
if (poList.length < FETCH_BATCH_SIZE) {
|
||||||
status: "running",
|
allProcessed = true;
|
||||||
operation: "Purchase orders final import",
|
|
||||||
message: `Processed ${totalProcessed} of ${total_pos} purchase orders`,
|
|
||||||
current: totalProcessed,
|
|
||||||
total: total_pos,
|
|
||||||
elapsed: formatElapsedTime((Date.now() - startTime) / 1000),
|
|
||||||
remaining: estimateRemaining(startTime, totalProcessed, total_pos),
|
|
||||||
rate: calculateRate(startTime, totalProcessed)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update last processed IDs for next chunk with safety check
|
|
||||||
if (chunk.length > 0) {
|
|
||||||
const lastItem = chunk[chunk.length - 1];
|
|
||||||
if (lastItem) {
|
|
||||||
lastPoId = lastItem.po_id;
|
|
||||||
lastPid = lastItem.pid;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count the temp table contents
|
||||||
|
const [tempCount] = await localConnection.query(`SELECT COUNT(*) FROM temp_purchase_orders`);
|
||||||
|
const tempRowCount = parseInt(tempCount.rows[0].count);
|
||||||
|
console.log(`Successfully inserted ${tempRowCount} rows into temp_purchase_orders`);
|
||||||
|
|
||||||
|
// Now insert into the final table
|
||||||
|
const [result] = await localConnection.query(`
|
||||||
|
WITH inserted_pos AS (
|
||||||
|
INSERT INTO purchase_orders (
|
||||||
|
po_id, pid, sku, name, cost_price, po_cost_price,
|
||||||
|
vendor, date, expected_date, status, notes,
|
||||||
|
ordered, received, receiving_status
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
po_id, pid, sku, name, cost_price, cost_price,
|
||||||
|
vendor, date, expected_date, status, notes,
|
||||||
|
ordered, 0 as received, 1 as receiving_status
|
||||||
|
FROM temp_purchase_orders
|
||||||
|
ON CONFLICT (po_id, pid) DO UPDATE SET
|
||||||
|
vendor = EXCLUDED.vendor,
|
||||||
|
date = EXCLUDED.date,
|
||||||
|
expected_date = EXCLUDED.expected_date,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
notes = EXCLUDED.notes,
|
||||||
|
ordered = EXCLUDED.ordered,
|
||||||
|
cost_price = EXCLUDED.cost_price,
|
||||||
|
po_cost_price = EXCLUDED.po_cost_price
|
||||||
|
RETURNING xmax = 0 as inserted
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(*) FILTER (WHERE inserted) as inserted,
|
||||||
|
COUNT(*) FILTER (WHERE NOT inserted) as updated
|
||||||
|
FROM inserted_pos
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Parse the result
|
||||||
|
const { inserted, updated } = result.rows[0];
|
||||||
|
recordsAdded = parseInt(inserted) || 0;
|
||||||
|
recordsUpdated = parseInt(updated) || 0;
|
||||||
|
|
||||||
// Update sync status
|
// Update sync status
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
INSERT INTO sync_status (table_name, last_sync_timestamp)
|
INSERT INTO sync_status (table_name, last_sync_timestamp)
|
||||||
@@ -620,29 +220,34 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
// Clean up temporary tables
|
// Clean up temporary tables
|
||||||
await localConnection.query(`
|
await localConnection.query(`DROP TABLE IF EXISTS temp_purchase_orders;`);
|
||||||
DROP TABLE IF EXISTS temp_purchase_orders;
|
|
||||||
DROP TABLE IF EXISTS temp_po_receivings;
|
// Commit transaction
|
||||||
`);
|
await localConnection.commit();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "complete",
|
status: "complete",
|
||||||
recordsAdded,
|
recordsAdded: recordsAdded || 0,
|
||||||
recordsUpdated,
|
recordsUpdated: recordsUpdated || 0,
|
||||||
totalRecords: processed
|
totalRecords: totalProcessed
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during purchase orders import:", error);
|
console.error("Error during purchase orders import:", error);
|
||||||
// Attempt cleanup on error
|
|
||||||
|
// Rollback transaction
|
||||||
try {
|
try {
|
||||||
await localConnection.query(`
|
await localConnection.rollback();
|
||||||
DROP TABLE IF EXISTS temp_purchase_orders;
|
} catch (rollbackError) {
|
||||||
DROP TABLE IF EXISTS temp_po_receivings;
|
console.error('Error during rollback:', rollbackError.message);
|
||||||
`);
|
|
||||||
} catch (cleanupError) {
|
|
||||||
console.error('Error during cleanup:', cleanupError);
|
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
error: error.message,
|
||||||
|
recordsAdded: 0,
|
||||||
|
recordsUpdated: 0,
|
||||||
|
totalRecords: 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ async function resetDatabase() {
|
|||||||
SELECT string_agg(tablename, ', ') as tables
|
SELECT string_agg(tablename, ', ') as tables
|
||||||
FROM pg_tables
|
FROM pg_tables
|
||||||
WHERE schemaname = 'public'
|
WHERE schemaname = 'public'
|
||||||
AND tablename NOT IN ('users', 'permissions', 'user_permissions', 'calculate_history', 'import_history');
|
AND tablename NOT IN ('users', 'permissions', 'user_permissions', 'calculate_history', 'import_history', 'ai_prompts', 'ai_validation_performance', 'templates');
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (!tablesResult.rows[0].tables) {
|
if (!tablesResult.rows[0].tables) {
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
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.
|
|
||||||
|
|
||||||
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 (see below for details)
|
|
||||||
}
|
|
||||||
|
|
||||||
IMPORTANT: For all fields that use IDs (categories, supplier, company, line, subline, ship_restrictions, tax_cat, artist, themes, etc.), you MUST return the ID values, not the display names. The system will handle converting IDs to display names.
|
|
||||||
|
|
||||||
Using the provided guidelines, focus on:
|
|
||||||
1. Correcting typos and any incorrect spelling or grammar
|
|
||||||
2. Standardizing product names
|
|
||||||
3. Correcting and enhancing descriptions by adding details, keywords, and SEO-friendly language
|
|
||||||
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 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. If the product name contains a match for one of the size categories (such as 12x12, 6x6, 2oz, etc) you MUST return that size category with the results. A value is not required if none of the size categories apply.
|
|
||||||
|
|
||||||
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. 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. 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. 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]
|
|
||||||
Example: "Cosmos Infinity Chipboard - Stamperia"
|
|
||||||
Example: "Serene Petals 6x6 Paper Pad - Prima"
|
|
||||||
|
|
||||||
Multiple similar products in a line: [Differentiator] [Product Type] - [Line Name] - [Company]
|
|
||||||
Example: "Ice & Shells Stencil - Arctic Antarctic - Stamperia"
|
|
||||||
Example: "Astronomy Paper - Cosmos Infinity - Stamperia"
|
|
||||||
|
|
||||||
Standalone products: [Product Name] - [Company]
|
|
||||||
Example: "Hedwig Puffy Stickers - Paper House Productions"
|
|
||||||
Example: "Heart Tree Dies - Lawn Fawn"
|
|
||||||
|
|
||||||
Color-based products: [Color] [Product Name] - [Company]
|
|
||||||
Example: "Green Valley Enamel Dots - Altenew"
|
|
||||||
Example: "Magenta Aqua Pigment - Brutus Monroe"
|
|
||||||
|
|
||||||
Complex products: [Differentiator] [Line] [Product Type] - [Company]
|
|
||||||
Example: "Size 6 Round Black Velvet Watercolor Brush - Silver Brush Limited" (Size 6 Round is the differentiator, Black Velvet is the line, Watercolor Brush is the product type)
|
|
||||||
|
|
||||||
These should not be included in the name, unless there are multiple products that are otherwise identical:
|
|
||||||
- Product size
|
|
||||||
- Product weight
|
|
||||||
- Number of pages
|
|
||||||
- How many are in the package
|
|
||||||
|
|
||||||
Naming Conventions:
|
|
||||||
- Paper sizes: Use "12x12", "8x8", "6x6" (no spaces or units of measure)
|
|
||||||
- Company names must match backend exactly
|
|
||||||
- Always capitalize every word in the name, including short articles like "The" and "An"
|
|
||||||
- Use "Idea-ology" (not "idea-ology" or "Ideaology")
|
|
||||||
- All stamps are "Stamp Set" (not "Clear Stamps" or "Rubber Stamps")
|
|
||||||
- All dies are "Dies" or "Die" (not "Die Set")
|
|
||||||
- Brands with their own naming conventions should be respected, such as "Doodle Cuts" for dies from Doodlebug
|
|
||||||
|
|
||||||
Special Brand Rules - Ranger:
|
|
||||||
Format: [Product Name] - [Designer Line] - Ranger
|
|
||||||
Possible Designers: Dylusions, Dina Wakley MEdia, Simon Hurley create., Wendy Vecchi
|
|
||||||
Example: "Stacked Stencil - Dina Wakley MEdia - Ranger"
|
|
||||||
|
|
||||||
Special Brand Rules - Tim Holtz products from Ranger:
|
|
||||||
Format: [Color] [Product Name/Type] - Tim Holtz Distress - Ranger
|
|
||||||
Example: "Mermaid Lagoon Tim Holtz Distress Oxide Ink Pad - Ranger"
|
|
||||||
|
|
||||||
Special Brand Rules - Tim Holtz products from Sizzix or Stampers Anonymous:
|
|
||||||
Format: [Product Name] [Product Type] by Tim Holtz - [Company]
|
|
||||||
Example: "Leaf Fragments Thinlits Dies by Tim Holtz - Sizzix"
|
|
||||||
|
|
||||||
Special Brand Rules - Tim Holtz products from Advantus/Idea-ology:
|
|
||||||
Format: [Product Name] - Tim Holtz Idea-ology
|
|
||||||
Example: "Tiny Vials - Tim Holtz Idea-ology"
|
|
||||||
|
|
||||||
Special Brand Rules - Dies from Sizzix:
|
|
||||||
Include die type plus "Dies" or "Die"
|
|
||||||
Examples:
|
|
||||||
"Art Nouveau 3-D Textured Impressions Embossing Folder - Sizzix"
|
|
||||||
"Pocket Pals Thinlits Dies - Sizzix"
|
|
||||||
"Butterfly Wishes Framelits Dies & Stamps - Sizzix"
|
|
||||||
|
|
||||||
Important Notes
|
|
||||||
- Ensure that product names are consistent across all products of the same type
|
|
||||||
- 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
|
|
||||||
|
|
||||||
Incorrect Example: A2 Easel Cards - Black - Photoplay
|
|
||||||
Notes: the differentiating factor should come first: “Black A2 Easel Cards - Photoplay”. Size is ok to include here because this is the name printed on the package.
|
|
||||||
|
|
||||||
Incorrect Example: 6” - Scriber Needle Modeling Tool
|
|
||||||
Notes: this product only comes in one size, so 6” isn’t needed. The company name should also be included.
|
|
||||||
|
|
||||||
Incorrect Example: Slick - White - Tulip Dimensional Fabric Paint 4oz
|
|
||||||
Notes: color should be first, then type, then product, then company, so “White Slick Dimensional Fabric Paint - Tulip”. It appears there’s only one size available so no need to differentiate in the name.
|
|
||||||
|
|
||||||
Incorrect Example: Silhouette Adhesive Cork Sheets 5”X7” 8/Pkg
|
|
||||||
Notes: should be “Adhesive Cork Sheets - Silhouette”
|
|
||||||
|
|
||||||
Incorrect Example: Galaxy - Opaque - American Crafts Color Pour Resin Dyes
|
|
||||||
Notes: “Galaxy Opaque Dye Set - Color Pour Resin - American Crafts”
|
|
||||||
|
|
||||||
Incorrect Example: Slate - Lion Brand Truboo Yarn
|
|
||||||
Notes: [Differentiator] [Line] [Product Type] - [Company] : “Slate Truboo Yarn - Lion Brand”
|
|
||||||
|
|
||||||
Incorrect Example: Rose Quartz Dylusions Shimmer Paint
|
|
||||||
Notes: “Rose Quartz Shimmer Paint - Dylusions - Ranger”
|
|
||||||
|
|
||||||
|
|
||||||
----------PRODUCT DESCRIPTION GUIDELINES----------
|
|
||||||
Product descriptions are an extremely important part of the listing and are the most important part of your response. Care should be taken to ensure they are correct, helpful, and SEO-friendly.
|
|
||||||
|
|
||||||
If a description is provided in the data, use it as a starting point. Correct any spelling errors, typos, poor grammar, or awkward phrasing. If necessary and you have the information, add more details, describe how the customer could use it, etc. Use complete sentences and keep SEO in mind.
|
|
||||||
|
|
||||||
If no description is provided, make one up using the product name, the information you have, and the other provided guidelines. At minimum, a description should be one complete sentence that starts with a capital letter and ends with a period. Unless the product is extremely complex, 2-4 sentences is usually sufficient if you have enough information.
|
|
||||||
|
|
||||||
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 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 it’s just a normal sheet of paper or pack of stickers, you don’t have to pretend like it’s 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 it’s 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!!
|
|
||||||
|
|
||||||
Avoid/remove:
|
|
||||||
- The word "Imported"
|
|
||||||
- Any warnings about Prop 65, choking hazards, etc
|
|
||||||
- The manufacturer's name if it's included as the very first thing in the description
|
|
||||||
- Any statement similar to "comes in a variety of colors, each sold separately"
|
|
||||||
335
inventory-server/src/routes/ai-prompts.js
Normal file
335
inventory-server/src/routes/ai-prompts.js
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get all AI prompts
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = req.app.locals.pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
ORDER BY prompt_type ASC, company ASC
|
||||||
|
`);
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching AI prompts:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to fetch AI prompts',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get prompt by ID
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const pool = req.app.locals.pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE id = $1
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'AI prompt not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching AI prompt:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to fetch AI prompt',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get prompt by company
|
||||||
|
router.get('/company/:companyId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { companyId } = req.params;
|
||||||
|
const pool = req.app.locals.pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE company = $1
|
||||||
|
`, [companyId]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'AI prompt not found for this company' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching AI prompt by company:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to fetch AI prompt by company',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get general prompt
|
||||||
|
router.get('/type/general', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = req.app.locals.pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE prompt_type = 'general'
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'General AI prompt not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching general AI prompt:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to fetch general AI prompt',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get system prompt
|
||||||
|
router.get('/type/system', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = req.app.locals.pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE prompt_type = 'system'
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'System AI prompt not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching system AI prompt:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to fetch system AI prompt',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create new AI prompt
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
prompt_text,
|
||||||
|
prompt_type,
|
||||||
|
company
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!prompt_text || !prompt_type) {
|
||||||
|
return res.status(400).json({ error: 'Prompt text and type are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prompt type
|
||||||
|
if (!['general', 'company_specific', 'system'].includes(prompt_type)) {
|
||||||
|
return res.status(400).json({ error: 'Prompt type must be either "general", "company_specific", or "system"' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate company is provided for company-specific prompts
|
||||||
|
if (prompt_type === 'company_specific' && !company) {
|
||||||
|
return res.status(400).json({ error: 'Company is required for company-specific prompts' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate company is not provided for general or system prompts
|
||||||
|
if ((prompt_type === 'general' || prompt_type === 'system') && company) {
|
||||||
|
return res.status(400).json({ error: 'Company should not be provided for general or system prompts' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = req.app.locals.pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
INSERT INTO ai_prompts (
|
||||||
|
prompt_text,
|
||||||
|
prompt_type,
|
||||||
|
company
|
||||||
|
) VALUES ($1, $2, $3)
|
||||||
|
RETURNING *
|
||||||
|
`, [
|
||||||
|
prompt_text,
|
||||||
|
prompt_type,
|
||||||
|
company
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.status(201).json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating AI prompt:', error);
|
||||||
|
|
||||||
|
// Check for unique constraint violations
|
||||||
|
if (error instanceof Error && error.message.includes('unique constraint')) {
|
||||||
|
if (error.message.includes('unique_company_prompt')) {
|
||||||
|
return res.status(409).json({
|
||||||
|
error: 'A prompt already exists for this company',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
} else if (error.message.includes('idx_unique_general_prompt')) {
|
||||||
|
return res.status(409).json({
|
||||||
|
error: 'A general prompt already exists',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
} else if (error.message.includes('idx_unique_system_prompt')) {
|
||||||
|
return res.status(409).json({
|
||||||
|
error: 'A system prompt already exists',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to create AI prompt',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update AI prompt
|
||||||
|
router.put('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const {
|
||||||
|
prompt_text,
|
||||||
|
prompt_type,
|
||||||
|
company
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!prompt_text || !prompt_type) {
|
||||||
|
return res.status(400).json({ error: 'Prompt text and type are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prompt type
|
||||||
|
if (!['general', 'company_specific', 'system'].includes(prompt_type)) {
|
||||||
|
return res.status(400).json({ error: 'Prompt type must be either "general", "company_specific", or "system"' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate company is provided for company-specific prompts
|
||||||
|
if (prompt_type === 'company_specific' && !company) {
|
||||||
|
return res.status(400).json({ error: 'Company is required for company-specific prompts' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate company is not provided for general or system prompts
|
||||||
|
if ((prompt_type === 'general' || prompt_type === 'system') && company) {
|
||||||
|
return res.status(400).json({ error: 'Company should not be provided for general or system prompts' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = req.app.locals.pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the prompt exists
|
||||||
|
const checkResult = await pool.query('SELECT * FROM ai_prompts WHERE id = $1', [id]);
|
||||||
|
if (checkResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'AI prompt not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
UPDATE ai_prompts
|
||||||
|
SET
|
||||||
|
prompt_text = $1,
|
||||||
|
prompt_type = $2,
|
||||||
|
company = $3
|
||||||
|
WHERE id = $4
|
||||||
|
RETURNING *
|
||||||
|
`, [
|
||||||
|
prompt_text,
|
||||||
|
prompt_type,
|
||||||
|
company,
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating AI prompt:', error);
|
||||||
|
|
||||||
|
// Check for unique constraint violations
|
||||||
|
if (error instanceof Error && error.message.includes('unique constraint')) {
|
||||||
|
if (error.message.includes('unique_company_prompt')) {
|
||||||
|
return res.status(409).json({
|
||||||
|
error: 'A prompt already exists for this company',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
} else if (error.message.includes('idx_unique_general_prompt')) {
|
||||||
|
return res.status(409).json({
|
||||||
|
error: 'A general prompt already exists',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
} else if (error.message.includes('idx_unique_system_prompt')) {
|
||||||
|
return res.status(409).json({
|
||||||
|
error: 'A system prompt already exists',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to update AI prompt',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete AI prompt
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const pool = req.app.locals.pool;
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Database pool not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query('DELETE FROM ai_prompts WHERE id = $1 RETURNING *', [id]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'AI prompt not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'AI prompt deleted successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting AI prompt:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to delete AI prompt',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
router.use((err, req, res, next) => {
|
||||||
|
console.error('AI prompts route error:', err);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal server error',
|
||||||
|
details: err.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -289,8 +289,108 @@ async function generateDebugResponse(productsToUse, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prompt = await loadPrompt(promptConnection, productsToUse);
|
// Get the local PostgreSQL pool to fetch prompts
|
||||||
const fullPrompt = prompt + "\n" + JSON.stringify(productsToUse);
|
const pool = res.app.locals.pool;
|
||||||
|
if (!pool) {
|
||||||
|
console.warn("⚠️ Local database pool not available for prompts");
|
||||||
|
throw new Error("Database connection not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, fetch the system prompt
|
||||||
|
const systemPromptResult = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE prompt_type = 'system'
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get system prompt or use default
|
||||||
|
let systemPrompt = null;
|
||||||
|
if (systemPromptResult.rows.length > 0) {
|
||||||
|
systemPrompt = systemPromptResult.rows[0];
|
||||||
|
console.log("📝 Loaded system prompt from database, ID:", systemPrompt.id);
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ No system prompt found in database, will use default");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, fetch the general prompt
|
||||||
|
const generalPromptResult = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE prompt_type = 'general'
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (generalPromptResult.rows.length === 0) {
|
||||||
|
console.warn("⚠️ No general prompt found in database");
|
||||||
|
throw new Error("No general prompt found in database");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the general prompt text and info
|
||||||
|
const generalPrompt = generalPromptResult.rows[0];
|
||||||
|
console.log("📝 Loaded general prompt from database, ID:", generalPrompt.id);
|
||||||
|
|
||||||
|
// Fetch company-specific prompts if we have products to validate
|
||||||
|
let companyPrompts = [];
|
||||||
|
if (productsToUse && Array.isArray(productsToUse)) {
|
||||||
|
// Extract unique company IDs from products
|
||||||
|
const companyIds = new Set();
|
||||||
|
productsToUse.forEach(product => {
|
||||||
|
if (product.company) {
|
||||||
|
companyIds.add(String(product.company));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (companyIds.size > 0) {
|
||||||
|
console.log(`🔍 Found ${companyIds.size} unique companies in products:`, Array.from(companyIds));
|
||||||
|
|
||||||
|
// Fetch company-specific prompts
|
||||||
|
const companyPromptsResult = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE prompt_type = 'company_specific'
|
||||||
|
AND company = ANY($1)
|
||||||
|
`, [Array.from(companyIds)]);
|
||||||
|
|
||||||
|
companyPrompts = companyPromptsResult.rows;
|
||||||
|
console.log(`📝 Loaded ${companyPrompts.length} company-specific prompts`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find company names from taxonomy for the validation endpoint
|
||||||
|
const companyPromptsWithNames = companyPrompts.map(prompt => {
|
||||||
|
let companyName = "Unknown Company";
|
||||||
|
if (taxonomy.companies && Array.isArray(taxonomy.companies)) {
|
||||||
|
const companyData = taxonomy.companies.find(company =>
|
||||||
|
String(company[0]) === String(prompt.company)
|
||||||
|
);
|
||||||
|
if (companyData && companyData[1]) {
|
||||||
|
companyName = companyData[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: prompt.id,
|
||||||
|
company: prompt.company,
|
||||||
|
companyName: companyName,
|
||||||
|
prompt_text: prompt.prompt_text
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now use loadPrompt to get the actual combined prompt
|
||||||
|
const promptData = await loadPrompt(promptConnection, productsToUse, res.app.locals.pool);
|
||||||
|
const fullUserPrompt = promptData.userContent + "\n" + JSON.stringify(productsToUse);
|
||||||
|
const promptLength = promptData.systemInstructions.length + fullUserPrompt.length; // Store prompt length for performance metrics
|
||||||
|
console.log("📝 Generated prompt length:", promptLength);
|
||||||
|
console.log("📝 System instructions length:", promptData.systemInstructions.length);
|
||||||
|
console.log("📝 User content length:", fullUserPrompt.length);
|
||||||
|
|
||||||
|
// Format the messages as they would be sent to the API
|
||||||
|
const apiMessages = [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: promptData.systemInstructions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: fullUserPrompt
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
// Create the response with taxonomy stats
|
// Create the response with taxonomy stats
|
||||||
let categoriesCount = 0;
|
let categoriesCount = 0;
|
||||||
@@ -330,9 +430,28 @@ async function generateDebugResponse(productsToUse, res) {
|
|||||||
: null,
|
: null,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
basePrompt: prompt,
|
basePrompt: systemPrompt ? systemPrompt.prompt_text + "\n\n" + generalPrompt.prompt_text : generalPrompt.prompt_text,
|
||||||
sampleFullPrompt: fullPrompt,
|
sampleFullPrompt: fullUserPrompt,
|
||||||
promptLength: fullPrompt.length,
|
promptLength: promptLength,
|
||||||
|
apiFormat: apiMessages,
|
||||||
|
promptSources: {
|
||||||
|
...(systemPrompt ? {
|
||||||
|
systemPrompt: {
|
||||||
|
id: systemPrompt.id,
|
||||||
|
prompt_text: systemPrompt.prompt_text
|
||||||
|
}
|
||||||
|
} : {
|
||||||
|
systemPrompt: {
|
||||||
|
id: 0,
|
||||||
|
prompt_text: `You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone. You have meticulous attention to detail and are a master at your craft.`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
generalPrompt: {
|
||||||
|
id: generalPrompt.id,
|
||||||
|
prompt_text: generalPrompt.prompt_text
|
||||||
|
},
|
||||||
|
companyPrompts: companyPromptsWithNames
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Sending response with taxonomy stats:", response.taxonomyStats);
|
console.log("Sending response with taxonomy stats:", response.taxonomyStats);
|
||||||
@@ -513,22 +632,101 @@ SELECT t.cat_id,t.name,null as master_cat_id,1 AS level_order FROM product_categ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the prompt from file and inject taxonomy data
|
// Load prompts from database and inject taxonomy data
|
||||||
async function loadPrompt(connection, productsToValidate = null) {
|
async function loadPrompt(connection, productsToValidate = null, appPool = null) {
|
||||||
try {
|
try {
|
||||||
const promptPath = path.join(
|
|
||||||
__dirname,
|
|
||||||
"..",
|
|
||||||
"prompts",
|
|
||||||
"product-validation.txt"
|
|
||||||
);
|
|
||||||
const basePrompt = await fs.readFile(promptPath, "utf8");
|
|
||||||
|
|
||||||
// Get taxonomy data using the provided MySQL connection
|
// Get taxonomy data using the provided MySQL connection
|
||||||
const taxonomy = await getTaxonomyData(connection);
|
const taxonomy = await getTaxonomyData(connection);
|
||||||
|
|
||||||
// Add system instructions to the prompt
|
// Use the provided pool parameter instead of global.app
|
||||||
const systemInstructions = `You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone. You have meticulous attention to detail and are a master at your craft.`;
|
const pool = appPool;
|
||||||
|
if (!pool) {
|
||||||
|
console.warn("⚠️ Local database pool not available for prompts");
|
||||||
|
throw new Error("Database connection not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the system prompt
|
||||||
|
const systemPromptResult = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE prompt_type = 'system'
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Default system instructions in case the system prompt is not found
|
||||||
|
let systemInstructions = `You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone. You have meticulous attention to detail and are a master at your craft.`;
|
||||||
|
|
||||||
|
// If system prompt exists in the database, use it
|
||||||
|
if (systemPromptResult.rows.length > 0) {
|
||||||
|
systemInstructions = systemPromptResult.rows[0].prompt_text;
|
||||||
|
console.log("📝 Loaded system prompt from database");
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ No system prompt found in database, using default");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the general prompt
|
||||||
|
const generalPromptResult = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE prompt_type = 'general'
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (generalPromptResult.rows.length === 0) {
|
||||||
|
console.warn("⚠️ No general prompt found in database");
|
||||||
|
throw new Error("No general prompt found in database");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the general prompt text
|
||||||
|
const basePrompt = generalPromptResult.rows[0].prompt_text;
|
||||||
|
console.log("📝 Loaded general prompt from database");
|
||||||
|
|
||||||
|
// Fetch company-specific prompts if we have products to validate
|
||||||
|
let companyPrompts = [];
|
||||||
|
if (productsToValidate && Array.isArray(productsToValidate)) {
|
||||||
|
// Extract unique company IDs from products
|
||||||
|
const companyIds = new Set();
|
||||||
|
productsToValidate.forEach(product => {
|
||||||
|
if (product.company) {
|
||||||
|
companyIds.add(String(product.company));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (companyIds.size > 0) {
|
||||||
|
console.log(`🔍 Found ${companyIds.size} unique companies in products:`, Array.from(companyIds));
|
||||||
|
|
||||||
|
// Fetch company-specific prompts
|
||||||
|
const companyPromptsResult = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE prompt_type = 'company_specific'
|
||||||
|
AND company = ANY($1)
|
||||||
|
`, [Array.from(companyIds)]);
|
||||||
|
|
||||||
|
companyPrompts = companyPromptsResult.rows;
|
||||||
|
console.log(`📝 Loaded ${companyPrompts.length} company-specific prompts`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine prompts - start with the general prompt
|
||||||
|
let combinedPrompt = basePrompt;
|
||||||
|
|
||||||
|
// Add any company-specific prompts with annotations
|
||||||
|
if (companyPrompts.length > 0) {
|
||||||
|
combinedPrompt += "\n\n--- COMPANY-SPECIFIC INSTRUCTIONS ---\n";
|
||||||
|
|
||||||
|
for (const prompt of companyPrompts) {
|
||||||
|
// Find company name from taxonomy
|
||||||
|
let companyName = "Unknown Company";
|
||||||
|
if (taxonomy.companies && Array.isArray(taxonomy.companies)) {
|
||||||
|
const companyData = taxonomy.companies.find(company =>
|
||||||
|
String(company[0]) === String(prompt.company)
|
||||||
|
);
|
||||||
|
if (companyData && companyData[1]) {
|
||||||
|
companyName = companyData[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedPrompt += `\n[SPECIFIC TO COMPANY: ${companyName} (ID: ${prompt.company})]:\n${prompt.prompt_text}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedPrompt += "\n--- END COMPANY-SPECIFIC INSTRUCTIONS ---\n";
|
||||||
|
}
|
||||||
|
|
||||||
// If we have products to validate, create a filtered prompt
|
// If we have products to validate, create a filtered prompt
|
||||||
if (productsToValidate) {
|
if (productsToValidate) {
|
||||||
@@ -655,11 +853,14 @@ ${JSON.stringify(mixedTaxonomy.sizeCategories)}${
|
|||||||
|
|
||||||
----------Here is the product data to validate----------`;
|
----------Here is the product data to validate----------`;
|
||||||
|
|
||||||
// Return the filtered prompt
|
// Return both system instructions and user content separately
|
||||||
return systemInstructions + basePrompt + "\n" + taxonomySection;
|
return {
|
||||||
|
systemInstructions,
|
||||||
|
userContent: combinedPrompt + "\n" + taxonomySection
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the full unfiltered prompt
|
// Generate the full unfiltered prompt for taxonomy section
|
||||||
const taxonomySection = `
|
const taxonomySection = `
|
||||||
Available Categories:
|
Available Categories:
|
||||||
${JSON.stringify(taxonomy.categories)}
|
${JSON.stringify(taxonomy.categories)}
|
||||||
@@ -687,7 +888,11 @@ ${JSON.stringify(taxonomy.artists)}
|
|||||||
|
|
||||||
Here is the product data to validate:`;
|
Here is the product data to validate:`;
|
||||||
|
|
||||||
return systemInstructions + basePrompt + "\n" + taxonomySection;
|
// Return both system instructions and user content separately
|
||||||
|
return {
|
||||||
|
systemInstructions,
|
||||||
|
userContent: combinedPrompt + "\n" + taxonomySection
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading prompt:", error);
|
console.error("Error loading prompt:", error);
|
||||||
throw error; // Re-throw to be handled by the calling function
|
throw error; // Re-throw to be handled by the calling function
|
||||||
@@ -735,18 +940,24 @@ router.post("/validate", async (req, res) => {
|
|||||||
|
|
||||||
// Load the prompt with the products data to filter taxonomy
|
// Load the prompt with the products data to filter taxonomy
|
||||||
console.log("🔄 Loading prompt with filtered taxonomy...");
|
console.log("🔄 Loading prompt with filtered taxonomy...");
|
||||||
const prompt = await loadPrompt(connection, products);
|
const promptData = await loadPrompt(connection, products, req.app.locals.pool);
|
||||||
const fullPrompt = prompt + "\n" + JSON.stringify(products);
|
const fullUserPrompt = promptData.userContent + "\n" + JSON.stringify(products);
|
||||||
promptLength = fullPrompt.length; // Store prompt length for performance metrics
|
const promptLength = promptData.systemInstructions.length + fullUserPrompt.length; // Store prompt length for performance metrics
|
||||||
console.log("📝 Generated prompt length:", promptLength);
|
console.log("📝 Generated prompt length:", promptLength);
|
||||||
|
console.log("📝 System instructions length:", promptData.systemInstructions.length);
|
||||||
|
console.log("📝 User content length:", fullUserPrompt.length);
|
||||||
|
|
||||||
console.log("🤖 Sending request to OpenAI...");
|
console.log("🤖 Sending request to OpenAI...");
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "o3-mini",
|
model: "gpt-4o",
|
||||||
messages: [
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: promptData.systemInstructions,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: fullPrompt,
|
content: fullUserPrompt,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
@@ -884,7 +1095,94 @@ router.post("/validate", async (req, res) => {
|
|||||||
console.error("⚠️ Failed to record performance metrics:", metricError);
|
console.error("⚠️ Failed to record performance metrics:", metricError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include performance metrics in the response
|
// Get sources of the prompts for tracking
|
||||||
|
let promptSources = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get system prompt
|
||||||
|
const systemPromptResult = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts WHERE prompt_type = 'system'
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get general prompt
|
||||||
|
const generalPromptResult = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts WHERE prompt_type = 'general'
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Extract unique company IDs from products
|
||||||
|
const companyIds = new Set();
|
||||||
|
products.forEach(product => {
|
||||||
|
if (product.company) {
|
||||||
|
companyIds.add(String(product.company));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let companyPrompts = [];
|
||||||
|
if (companyIds.size > 0) {
|
||||||
|
// Fetch company-specific prompts
|
||||||
|
const companyPromptsResult = await pool.query(`
|
||||||
|
SELECT * FROM ai_prompts
|
||||||
|
WHERE prompt_type = 'company_specific'
|
||||||
|
AND company = ANY($1)
|
||||||
|
`, [Array.from(companyIds)]);
|
||||||
|
|
||||||
|
companyPrompts = companyPromptsResult.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find company names from taxonomy for the validation endpoint
|
||||||
|
const companyPromptsWithNames = companyPrompts.map(prompt => {
|
||||||
|
let companyName = "Unknown Company";
|
||||||
|
if (taxonomy.companies && Array.isArray(taxonomy.companies)) {
|
||||||
|
const companyData = taxonomy.companies.find(company =>
|
||||||
|
String(company[0]) === String(prompt.company)
|
||||||
|
);
|
||||||
|
if (companyData && companyData[1]) {
|
||||||
|
companyName = companyData[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: prompt.id,
|
||||||
|
company: prompt.company,
|
||||||
|
companyName: companyName,
|
||||||
|
prompt_text: prompt.prompt_text
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set prompt sources
|
||||||
|
if (generalPromptResult.rows.length > 0) {
|
||||||
|
const generalPrompt = generalPromptResult.rows[0];
|
||||||
|
let systemPrompt = null;
|
||||||
|
|
||||||
|
if (systemPromptResult.rows.length > 0) {
|
||||||
|
systemPrompt = systemPromptResult.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
promptSources = {
|
||||||
|
...(systemPrompt ? {
|
||||||
|
systemPrompt: {
|
||||||
|
id: systemPrompt.id,
|
||||||
|
prompt_text: systemPrompt.prompt_text
|
||||||
|
}
|
||||||
|
} : {
|
||||||
|
systemPrompt: {
|
||||||
|
id: 0,
|
||||||
|
prompt_text: `You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone. You have meticulous attention to detail and are a master at your craft.`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
generalPrompt: {
|
||||||
|
id: generalPrompt.id,
|
||||||
|
prompt_text: generalPrompt.prompt_text
|
||||||
|
},
|
||||||
|
companyPrompts: companyPromptsWithNames
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (promptSourceError) {
|
||||||
|
console.error("⚠️ Error getting prompt sources:", promptSourceError);
|
||||||
|
// Don't fail the entire validation if just prompt sources retrieval fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include prompt sources in the response
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
changeDetails: changeDetails,
|
changeDetails: changeDetails,
|
||||||
@@ -895,6 +1193,7 @@ router.post("/validate", async (req, res) => {
|
|||||||
isEstimate: true,
|
isEstimate: true,
|
||||||
productCount: products.length
|
productCount: products.length
|
||||||
},
|
},
|
||||||
|
promptSources: promptSources,
|
||||||
...aiResponse,
|
...aiResponse,
|
||||||
});
|
});
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
|
|||||||
@@ -757,8 +757,8 @@ router.get('/history/import', async (req, res) => {
|
|||||||
end_time,
|
end_time,
|
||||||
status,
|
status,
|
||||||
error_message,
|
error_message,
|
||||||
rows_processed::integer,
|
records_added::integer,
|
||||||
files_processed::integer
|
records_updated::integer
|
||||||
FROM import_history
|
FROM import_history
|
||||||
ORDER BY start_time DESC
|
ORDER BY start_time DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const categoriesRouter = require('./routes/categories');
|
|||||||
const importRouter = require('./routes/import');
|
const importRouter = require('./routes/import');
|
||||||
const aiValidationRouter = require('./routes/ai-validation');
|
const aiValidationRouter = require('./routes/ai-validation');
|
||||||
const templatesRouter = require('./routes/templates');
|
const templatesRouter = require('./routes/templates');
|
||||||
|
const aiPromptsRouter = require('./routes/ai-prompts');
|
||||||
|
|
||||||
// Get the absolute path to the .env file
|
// Get the absolute path to the .env file
|
||||||
const envPath = '/var/www/html/inventory/.env';
|
const envPath = '/var/www/html/inventory/.env';
|
||||||
@@ -103,6 +104,7 @@ async function startServer() {
|
|||||||
app.use('/api/import', importRouter);
|
app.use('/api/import', importRouter);
|
||||||
app.use('/api/ai-validation', aiValidationRouter);
|
app.use('/api/ai-validation', aiValidationRouter);
|
||||||
app.use('/api/templates', templatesRouter);
|
app.use('/api/templates', templatesRouter);
|
||||||
|
app.use('/api/ai-prompts', aiPromptsRouter);
|
||||||
|
|
||||||
// Basic health check route
|
// Basic health check route
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const mysql = require('mysql2/promise');
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
let pool;
|
let pool;
|
||||||
|
|
||||||
function initPool(config) {
|
function initPool(config) {
|
||||||
pool = mysql.createPool(config);
|
pool = new Pool(config);
|
||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,108 +1,61 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { AiValidationDialogs } from '../../../components/AiValidationDialogs';
|
import { AiValidationDialogs } from './components/AiValidationDialogs';
|
||||||
import { Product } from '../../../types/product';
|
import { Product } from '../../../../types/products';
|
||||||
import { config } from '../../../config';
|
import {
|
||||||
|
AiValidationProgress,
|
||||||
interface CurrentPrompt {
|
AiValidationDetails,
|
||||||
isOpen: boolean;
|
CurrentPrompt as AiValidationCurrentPrompt
|
||||||
prompt: string;
|
} from './hooks/useAiValidation';
|
||||||
isLoading: boolean;
|
|
||||||
debugData?: {
|
|
||||||
taxonomyStats: {
|
|
||||||
categories: number;
|
|
||||||
themes: number;
|
|
||||||
colors: number;
|
|
||||||
taxCodes: number;
|
|
||||||
sizeCategories: number;
|
|
||||||
suppliers: number;
|
|
||||||
companies: number;
|
|
||||||
artists: number;
|
|
||||||
} | null;
|
|
||||||
basePrompt: string;
|
|
||||||
sampleFullPrompt: string;
|
|
||||||
promptLength: number;
|
|
||||||
estimatedProcessingTime?: {
|
|
||||||
seconds: number | null;
|
|
||||||
sampleCount: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ValidationStepNew: React.FC = () => {
|
const ValidationStepNew: React.FC = () => {
|
||||||
const [aiValidationProgress, setAiValidationProgress] = useState(0);
|
const [aiValidationProgress, setAiValidationProgress] = useState<AiValidationProgress>({
|
||||||
const [aiValidationDetails, setAiValidationDetails] = useState('');
|
isOpen: false,
|
||||||
const [currentPrompt, setCurrentPrompt] = useState<CurrentPrompt>({
|
status: 'idle',
|
||||||
|
step: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const [aiValidationDetails, setAiValidationDetails] = useState<AiValidationDetails>({
|
||||||
|
changes: [],
|
||||||
|
warnings: [],
|
||||||
|
changeDetails: [],
|
||||||
|
isOpen: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const [currentPrompt, setCurrentPrompt] = useState<AiValidationCurrentPrompt>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
prompt: '',
|
prompt: '',
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
});
|
});
|
||||||
const [isChangeReverted, setIsChangeReverted] = useState(false);
|
|
||||||
const [fieldData, setFieldData] = useState<Product[]>([]);
|
|
||||||
|
|
||||||
const showCurrentPrompt = async (products: Product[]) => {
|
// Track reversion state (for internal use)
|
||||||
setCurrentPrompt((prev) => ({ ...prev, isOpen: true, isLoading: true }));
|
const [reversionState, setReversionState] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
try {
|
const [fieldData] = useState<Product[]>([]);
|
||||||
// Get the prompt
|
|
||||||
const promptResponse = await fetch(`${config.apiUrl}/ai-validation/prompt`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ products })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!promptResponse.ok) {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
throw new Error('Failed to fetch AI prompt');
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptData = await promptResponse.json();
|
const revertAiChange = (productIndex: number, fieldKey: string) => {
|
||||||
|
const key = `${productIndex}-${fieldKey}`;
|
||||||
// Get the debug data in the same request or as a separate request
|
setReversionState(prev => ({
|
||||||
const debugResponse = await fetch(`${config.apiUrl}/ai-validation/debug-info`, {
|
...prev,
|
||||||
method: 'POST',
|
[key]: true
|
||||||
headers: { 'Content-Type': 'application/json' },
|
}));
|
||||||
body: JSON.stringify({ prompt: promptData.prompt })
|
|
||||||
});
|
|
||||||
|
|
||||||
let debugData;
|
|
||||||
if (debugResponse.ok) {
|
|
||||||
debugData = await debugResponse.json();
|
|
||||||
} else {
|
|
||||||
// If debug-info fails, use a fallback to get taxonomy stats
|
|
||||||
const fallbackResponse = await fetch(`${config.apiUrl}/ai-validation/debug`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ products: [products[0]] }) // Use first product for stats
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fallbackResponse.ok) {
|
|
||||||
debugData = await fallbackResponse.json();
|
|
||||||
// Set promptLength correctly from the actual prompt
|
|
||||||
debugData.promptLength = promptData.prompt.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentPrompt((prev) => ({
|
|
||||||
...prev,
|
|
||||||
prompt: promptData.prompt,
|
|
||||||
isLoading: false,
|
|
||||||
debugData: debugData
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching prompt:', error);
|
|
||||||
setCurrentPrompt((prev) => ({
|
|
||||||
...prev,
|
|
||||||
prompt: 'Error loading prompt',
|
|
||||||
isLoading: false
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const revertAiChange = () => {
|
const isChangeReverted = (productIndex: number, fieldKey: string): boolean => {
|
||||||
setIsChangeReverted(true);
|
const key = `${productIndex}-${fieldKey}`;
|
||||||
|
return !!reversionState[key];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFieldDisplayValueWithHighlight = (value: string, highlight: string) => {
|
const getFieldDisplayValueWithHighlight = (
|
||||||
// Implementation of getFieldDisplayValueWithHighlight
|
_fieldKey: string,
|
||||||
|
originalValue: any,
|
||||||
|
correctedValue: any
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
originalHtml: String(originalValue),
|
||||||
|
correctedHtml: String(correctedValue)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Loader2, CheckIcon } from "lucide-react";
|
import { Loader2, CheckIcon, XIcon } from "lucide-react";
|
||||||
import { Code } from "@/components/ui/code";
|
import { Code } from "@/components/ui/code";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
CurrentPrompt,
|
CurrentPrompt,
|
||||||
} from "../hooks/useAiValidation";
|
} from "../hooks/useAiValidation";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
interface TaxonomyStats {
|
interface TaxonomyStats {
|
||||||
categories: number;
|
categories: number;
|
||||||
@@ -41,6 +42,20 @@ interface DebugData {
|
|||||||
basePrompt: string;
|
basePrompt: string;
|
||||||
sampleFullPrompt: string;
|
sampleFullPrompt: string;
|
||||||
promptLength: number;
|
promptLength: number;
|
||||||
|
apiFormat?: Array<{
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
}>;
|
||||||
|
promptSources?: {
|
||||||
|
systemPrompt?: { id: number; prompt_text: string };
|
||||||
|
generalPrompt?: { id: number; prompt_text: string };
|
||||||
|
companyPrompts?: Array<{
|
||||||
|
id: number;
|
||||||
|
company: string;
|
||||||
|
companyName?: string;
|
||||||
|
prompt_text: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
estimatedProcessingTime?: {
|
estimatedProcessingTime?: {
|
||||||
seconds: number | null;
|
seconds: number | null;
|
||||||
sampleCount: number;
|
sampleCount: number;
|
||||||
@@ -83,6 +98,75 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
debugData,
|
debugData,
|
||||||
}) => {
|
}) => {
|
||||||
const [costPerMillionTokens, setCostPerMillionTokens] = useState(2.5); // Default cost
|
const [costPerMillionTokens, setCostPerMillionTokens] = useState(2.5); // Default cost
|
||||||
|
const hasCompanyPrompts =
|
||||||
|
currentPrompt.debugData?.promptSources?.companyPrompts &&
|
||||||
|
currentPrompt.debugData.promptSources.companyPrompts.length > 0;
|
||||||
|
|
||||||
|
// Create our own state to track changes
|
||||||
|
const [localReversionState, setLocalReversionState] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
// Initialize local state from the isChangeReverted function when component mounts
|
||||||
|
// or when aiValidationDetails changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (
|
||||||
|
aiValidationDetails.changeDetails &&
|
||||||
|
aiValidationDetails.changeDetails.length > 0
|
||||||
|
) {
|
||||||
|
const initialState: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
aiValidationDetails.changeDetails.forEach((product) => {
|
||||||
|
product.changes.forEach((change) => {
|
||||||
|
const key = `${product.productIndex}-${change.field}`;
|
||||||
|
initialState[key] = isChangeReverted(
|
||||||
|
product.productIndex,
|
||||||
|
change.field
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setLocalReversionState(initialState);
|
||||||
|
}
|
||||||
|
}, [aiValidationDetails.changeDetails, isChangeReverted]);
|
||||||
|
|
||||||
|
// This function will toggle the local state for a given change
|
||||||
|
const toggleChangeAcceptance = (productIndex: number, fieldKey: string) => {
|
||||||
|
const key = `${productIndex}-${fieldKey}`;
|
||||||
|
const currentlyRejected = !!localReversionState[key];
|
||||||
|
|
||||||
|
// Toggle the local state
|
||||||
|
setLocalReversionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: !prev[key],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Only call revertAiChange when toggling to rejected state
|
||||||
|
// Since revertAiChange is specifically for rejecting changes
|
||||||
|
if (!currentlyRejected) {
|
||||||
|
revertAiChange(productIndex, fieldKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to check local reversion state
|
||||||
|
const isChangeLocallyReverted = (
|
||||||
|
productIndex: number,
|
||||||
|
fieldKey: string
|
||||||
|
): boolean => {
|
||||||
|
const key = `${productIndex}-${fieldKey}`;
|
||||||
|
return !!localReversionState[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use "full" as the default tab
|
||||||
|
const defaultTab = "full";
|
||||||
|
const [activeTab, setActiveTab] = useState(defaultTab);
|
||||||
|
|
||||||
|
// Update activeTab when the dialog is opened with new data
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (currentPrompt.isOpen) {
|
||||||
|
setActiveTab("full");
|
||||||
|
}
|
||||||
|
}, [currentPrompt.isOpen]);
|
||||||
|
|
||||||
// Format time helper
|
// Format time helper
|
||||||
const formatTime = (seconds: number): string => {
|
const formatTime = (seconds: number): string => {
|
||||||
@@ -123,136 +207,433 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex flex-col h-[calc(90vh-120px)] overflow-hidden">
|
<div className="flex flex-col h-[calc(90vh-120px)] overflow-hidden">
|
||||||
{/* Debug Information Section */}
|
{/* Debug Information Section - Fixed at the top */}
|
||||||
<div className="mb-4 flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{currentPrompt.isLoading ? (
|
{currentPrompt.isLoading ? (
|
||||||
<div className="flex justify-center items-center h-[100px]"></div>
|
<div className="flex justify-center items-center h-[100px]"></div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<>
|
||||||
<Card className="py-2">
|
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||||
<CardHeader className="py-2">
|
<Card className="py-2">
|
||||||
<CardTitle className="text-base">Prompt Length</CardTitle>
|
<CardHeader className="py-2">
|
||||||
</CardHeader>
|
<CardTitle className="text-base">
|
||||||
<CardContent className="py-2">
|
Prompt Length
|
||||||
<div className="flex flex-col space-y-2">
|
</CardTitle>
|
||||||
<div className="text-sm">
|
</CardHeader>
|
||||||
<span className="text-muted-foreground">
|
<CardContent className="py-2">
|
||||||
Characters:
|
<div className="flex flex-col space-y-2">
|
||||||
</span>{" "}
|
<div className="text-sm">
|
||||||
<span className="font-semibold">{promptLength}</span>
|
<span className="text-muted-foreground">
|
||||||
|
Characters:
|
||||||
|
</span>{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{promptLength}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Tokens:
|
||||||
|
</span>{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
~{Math.round(promptLength / 4)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
</CardContent>
|
||||||
<span className="text-muted-foreground">Tokens:</span>{" "}
|
</Card>
|
||||||
<span className="font-semibold">
|
|
||||||
~{Math.round(promptLength / 4)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="py-2">
|
<Card className="py-2">
|
||||||
<CardHeader className="py-2">
|
<CardHeader className="py-2">
|
||||||
<CardTitle className="text-base">Cost Estimate</CardTitle>
|
<CardTitle className="text-base">
|
||||||
</CardHeader>
|
Cost Estimate
|
||||||
<CardContent className="py-2">
|
</CardTitle>
|
||||||
<div className="flex flex-col space-y-2">
|
</CardHeader>
|
||||||
<div className="flex flex-row items-center">
|
<CardContent className="py-2">
|
||||||
<label
|
<div className="flex flex-col space-y-2">
|
||||||
htmlFor="costPerMillion"
|
<div className="flex flex-row items-center">
|
||||||
className="text-sm text-muted-foreground"
|
<label
|
||||||
>
|
htmlFor="costPerMillion"
|
||||||
$
|
className="text-sm text-muted-foreground"
|
||||||
</label>
|
>
|
||||||
<input
|
$
|
||||||
id="costPerMillion"
|
</label>
|
||||||
className="w-[40px] px-1 border rounded-md text-sm"
|
<input
|
||||||
defaultValue={costPerMillionTokens.toFixed(2)}
|
id="costPerMillion"
|
||||||
onChange={(e) => {
|
className="w-[40px] px-1 border rounded-md text-sm"
|
||||||
const value = parseFloat(e.target.value);
|
defaultValue={costPerMillionTokens.toFixed(2)}
|
||||||
if (!isNaN(value)) {
|
onChange={(e) => {
|
||||||
setCostPerMillionTokens(value);
|
const value = parseFloat(e.target.value);
|
||||||
}
|
if (!isNaN(value)) {
|
||||||
}}
|
setCostPerMillionTokens(value);
|
||||||
/>
|
}
|
||||||
<label
|
}}
|
||||||
htmlFor="costPerMillion"
|
/>
|
||||||
className="text-sm text-muted-foreground ml-1"
|
<label
|
||||||
>
|
htmlFor="costPerMillion"
|
||||||
per million input tokens
|
className="text-sm text-muted-foreground ml-1"
|
||||||
</label>
|
>
|
||||||
|
per million input tokens
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Cost:</span>{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{calculateTokenCost(promptLength).toFixed(1)}¢
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
</CardContent>
|
||||||
<span className="text-muted-foreground">Cost:</span>{" "}
|
</Card>
|
||||||
<span className="font-semibold">
|
|
||||||
{calculateTokenCost(promptLength).toFixed(1)}¢
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="py-2">
|
<Card className="py-2">
|
||||||
<CardHeader className="py-2">
|
<CardHeader className="py-2">
|
||||||
<CardTitle className="text-base">
|
<CardTitle className="text-base">
|
||||||
Processing Time
|
Processing Time
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="py-2">
|
<CardContent className="py-2">
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
{debugData?.estimatedProcessingTime ? (
|
{currentPrompt.debugData?.estimatedProcessingTime ? (
|
||||||
debugData.estimatedProcessingTime.seconds ? (
|
currentPrompt.debugData.estimatedProcessingTime
|
||||||
<>
|
.seconds ? (
|
||||||
<div className="text-sm">
|
<>
|
||||||
<span className="text-muted-foreground">
|
<div className="text-sm">
|
||||||
Estimated time:
|
<span className="text-muted-foreground">
|
||||||
</span>{" "}
|
Estimated time:
|
||||||
<span className="font-semibold">
|
</span>{" "}
|
||||||
{formatTime(
|
<span className="font-semibold">
|
||||||
debugData.estimatedProcessingTime.seconds
|
{formatTime(
|
||||||
)}
|
currentPrompt.debugData
|
||||||
</span>
|
.estimatedProcessingTime.seconds
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Based on{" "}
|
||||||
|
{
|
||||||
|
currentPrompt.debugData
|
||||||
|
.estimatedProcessingTime.sampleCount
|
||||||
|
}{" "}
|
||||||
|
similar validation
|
||||||
|
{currentPrompt.debugData
|
||||||
|
.estimatedProcessingTime.sampleCount !== 1
|
||||||
|
? "s"
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
No historical data available for this prompt
|
||||||
|
size
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
)
|
||||||
Based on{" "}
|
|
||||||
{debugData.estimatedProcessingTime.sampleCount}{" "}
|
|
||||||
similar validation
|
|
||||||
{debugData.estimatedProcessingTime
|
|
||||||
.sampleCount !== 1
|
|
||||||
? "s"
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
No historical data available for this prompt size
|
No processing time data available
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
) : (
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
</CardContent>
|
||||||
No processing time data available
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Prompt Section */}
|
{/* Prompt Section - Scrollable content */}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<ScrollArea className="h-full w-full">
|
{currentPrompt.isLoading ? (
|
||||||
{currentPrompt.isLoading ? (
|
<div className="flex items-center justify-center h-full">
|
||||||
<div className="flex items-center justify-center h-full">
|
<Loader2 className="h-8 w-8 animate-spin" />
|
||||||
<Loader2 className="h-8 w-8 animate-spin" />
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full">
|
{currentPrompt.debugData?.apiFormat ? (
|
||||||
{currentPrompt.prompt}
|
<div className="flex flex-col h-full">
|
||||||
</Code>
|
{/* Prompt Sources Card - Fixed at the top of the content area */}
|
||||||
)}
|
<Card className="py-2 mb-4 flex-shrink-0">
|
||||||
</ScrollArea>
|
<CardHeader className="py-2">
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
Prompt Sources
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="py-2">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-purple-100 hover:bg-purple-200 cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById("system-message")
|
||||||
|
?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
System
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-green-100 hover:bg-green-200 cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById("general-section")
|
||||||
|
?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
General
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{currentPrompt.debugData.promptSources?.companyPrompts?.map(
|
||||||
|
(company, idx) => (
|
||||||
|
<Badge
|
||||||
|
key={idx}
|
||||||
|
variant="outline"
|
||||||
|
className="bg-blue-100 hover:bg-blue-200 cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById("company-section")
|
||||||
|
?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{company.companyName ||
|
||||||
|
`Company ${company.company}`}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-amber-100 hover:bg-amber-200 cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById("taxonomy-section")
|
||||||
|
?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Taxonomy
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-pink-100 hover:bg-pink-200 cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById("product-section")
|
||||||
|
?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Products
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<ScrollArea className="flex-1 w-full overflow-y-auto">
|
||||||
|
{currentPrompt.debugData.apiFormat.map(
|
||||||
|
(message, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="border rounded-md p-2 mb-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id={
|
||||||
|
message.role === "system"
|
||||||
|
? "system-message"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
className={`p-2 mb-2 rounded-sm font-medium ${
|
||||||
|
message.role === "system"
|
||||||
|
? "bg-purple-50 text-purple-800"
|
||||||
|
: "bg-green-50 text-green-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Role: {message.role}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Code
|
||||||
|
className={`whitespace-pre-wrap p-4 break-normal max-w-full ${
|
||||||
|
message.role === "system"
|
||||||
|
? "bg-purple-50/30"
|
||||||
|
: "bg-green-50/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{message.role === "user" ? (
|
||||||
|
<div className="text-wrapper">
|
||||||
|
{(() => {
|
||||||
|
const content = message.content;
|
||||||
|
|
||||||
|
// Find section boundaries by looking for specific markers
|
||||||
|
const companySpecificStartIndex =
|
||||||
|
content.indexOf(
|
||||||
|
"--- COMPANY-SPECIFIC INSTRUCTIONS ---"
|
||||||
|
);
|
||||||
|
const companySpecificEndIndex =
|
||||||
|
content.indexOf(
|
||||||
|
"--- END COMPANY-SPECIFIC INSTRUCTIONS ---"
|
||||||
|
);
|
||||||
|
|
||||||
|
const taxonomyStartIndex =
|
||||||
|
content.indexOf(
|
||||||
|
"All Available Categories:"
|
||||||
|
);
|
||||||
|
const taxonomyFallbackStartIndex =
|
||||||
|
content.indexOf(
|
||||||
|
"Available Categories:"
|
||||||
|
);
|
||||||
|
const actualTaxonomyStartIndex =
|
||||||
|
taxonomyStartIndex >= 0
|
||||||
|
? taxonomyStartIndex
|
||||||
|
: taxonomyFallbackStartIndex;
|
||||||
|
|
||||||
|
const productDataStartIndex =
|
||||||
|
content.indexOf(
|
||||||
|
"----------Here is the product data to validate----------"
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we can't find any markers, just return the content as-is
|
||||||
|
if (
|
||||||
|
actualTaxonomyStartIndex < 0 &&
|
||||||
|
productDataStartIndex < 0 &&
|
||||||
|
companySpecificStartIndex < 0
|
||||||
|
) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine section indices
|
||||||
|
let generalEndIndex = content.length;
|
||||||
|
|
||||||
|
if (companySpecificStartIndex >= 0) {
|
||||||
|
generalEndIndex =
|
||||||
|
companySpecificStartIndex;
|
||||||
|
} else if (
|
||||||
|
actualTaxonomyStartIndex >= 0
|
||||||
|
) {
|
||||||
|
generalEndIndex =
|
||||||
|
actualTaxonomyStartIndex;
|
||||||
|
} else if (productDataStartIndex >= 0) {
|
||||||
|
generalEndIndex = productDataStartIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine where taxonomy starts
|
||||||
|
let taxonomyEndIndex = content.length;
|
||||||
|
if (productDataStartIndex >= 0) {
|
||||||
|
taxonomyEndIndex =
|
||||||
|
productDataStartIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Segments to render with appropriate styling
|
||||||
|
const segments = [];
|
||||||
|
|
||||||
|
// General section (beginning to company/taxonomy/product)
|
||||||
|
if (generalEndIndex > 0) {
|
||||||
|
segments.push(
|
||||||
|
<div
|
||||||
|
id="general-section"
|
||||||
|
key="general"
|
||||||
|
className="border-l-4 border-green-500 pl-4 py-0 my-1"
|
||||||
|
>
|
||||||
|
<div className="text-xs font-semibold text-green-700 mb-2">
|
||||||
|
General Prompt
|
||||||
|
</div>
|
||||||
|
<pre className="whitespace-pre-wrap">
|
||||||
|
{content.substring(
|
||||||
|
0,
|
||||||
|
generalEndIndex
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Company-specific section if present
|
||||||
|
if (
|
||||||
|
companySpecificStartIndex >= 0 &&
|
||||||
|
companySpecificEndIndex >= 0
|
||||||
|
) {
|
||||||
|
segments.push(
|
||||||
|
<div
|
||||||
|
id="company-section"
|
||||||
|
key="company"
|
||||||
|
className="border-l-4 border-blue-500 pl-4 py-0 my-1"
|
||||||
|
>
|
||||||
|
<div className="text-xs font-semibold text-blue-700 mb-2">
|
||||||
|
Company-Specific Instructions
|
||||||
|
</div>
|
||||||
|
<pre className="whitespace-pre-wrap">
|
||||||
|
{content.substring(
|
||||||
|
companySpecificStartIndex,
|
||||||
|
companySpecificEndIndex +
|
||||||
|
"--- END COMPANY-SPECIFIC INSTRUCTIONS ---"
|
||||||
|
.length
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taxonomy section
|
||||||
|
if (actualTaxonomyStartIndex >= 0) {
|
||||||
|
const taxEnd = taxonomyEndIndex;
|
||||||
|
segments.push(
|
||||||
|
<div
|
||||||
|
id="taxonomy-section"
|
||||||
|
key="taxonomy"
|
||||||
|
className="border-l-4 border-amber-500 pl-4 py-0 my-1"
|
||||||
|
>
|
||||||
|
<div className="text-xs font-semibold text-amber-700 mb-2">
|
||||||
|
Taxonomy Data
|
||||||
|
</div>
|
||||||
|
<pre className="whitespace-pre-wrap">
|
||||||
|
{content.substring(
|
||||||
|
actualTaxonomyStartIndex,
|
||||||
|
taxEnd
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product data section
|
||||||
|
if (productDataStartIndex >= 0) {
|
||||||
|
segments.push(
|
||||||
|
<div
|
||||||
|
id="product-section"
|
||||||
|
key="product"
|
||||||
|
className="border-l-4 border-pink-500 pl-4 py-0 my-1"
|
||||||
|
>
|
||||||
|
<div className="text-xs font-semibold text-pink-700 mb-2">
|
||||||
|
Product Data
|
||||||
|
</div>
|
||||||
|
<pre className="whitespace-pre-wrap">
|
||||||
|
{content.substring(
|
||||||
|
productDataStartIndex
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{segments}</>;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre className="whitespace-pre-wrap">
|
||||||
|
{message.content}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Code>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="h-full w-full">
|
||||||
|
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full">
|
||||||
|
{currentPrompt.prompt}
|
||||||
|
</Code>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -280,8 +661,9 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
className="h-full bg-primary transition-all duration-500"
|
className="h-full bg-primary transition-all duration-500"
|
||||||
style={{
|
style={{
|
||||||
width: `${
|
width: `${
|
||||||
aiValidationProgress.progressPercent ??
|
aiValidationProgress.progressPercent !== undefined
|
||||||
Math.round((aiValidationProgress.step / 5) * 100)
|
? Math.round(aiValidationProgress.progressPercent)
|
||||||
|
: Math.round((aiValidationProgress.step / 5) * 100)
|
||||||
}%`,
|
}%`,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
aiValidationProgress.step === -1
|
aiValidationProgress.step === -1
|
||||||
@@ -295,8 +677,9 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
{aiValidationProgress.step === -1
|
{aiValidationProgress.step === -1
|
||||||
? "❌"
|
? "❌"
|
||||||
: `${
|
: `${
|
||||||
aiValidationProgress.progressPercent ??
|
aiValidationProgress.progressPercent !== undefined
|
||||||
Math.round((aiValidationProgress.step / 5) * 100)
|
? Math.round(aiValidationProgress.progressPercent)
|
||||||
|
: Math.round((aiValidationProgress.step / 5) * 100)
|
||||||
}%`}
|
}%`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -355,14 +738,14 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
setAiValidationDetails((prev) => ({ ...prev, isOpen: open }))
|
setAiValidationDetails((prev) => ({ ...prev, isOpen: open }))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DialogContent className="max-w-4xl">
|
<DialogContent className="max-w-6xl w-[90vw]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>AI Validation Results</DialogTitle>
|
<DialogTitle>AI Validation Results</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Review the changes and warnings suggested by the AI
|
Review the changes and warnings suggested by the AI
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ScrollArea className="max-h-[60vh]">
|
<ScrollArea className="max-h-[70vh]">
|
||||||
{aiValidationDetails.changeDetails &&
|
{aiValidationDetails.changeDetails &&
|
||||||
aiValidationDetails.changeDetails.length > 0 ? (
|
aiValidationDetails.changeDetails.length > 0 ? (
|
||||||
<div className="mb-6 space-y-6">
|
<div className="mb-6 space-y-6">
|
||||||
@@ -384,10 +767,16 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[180px]">Field</TableHead>
|
<TableHead className="">Field</TableHead>
|
||||||
<TableHead>Original Value</TableHead>
|
<TableHead className="w-[35%]">
|
||||||
<TableHead>Corrected Value</TableHead>
|
Original Value
|
||||||
<TableHead className="text-right">Action</TableHead>
|
</TableHead>
|
||||||
|
<TableHead className="w-[35%]">
|
||||||
|
Corrected Value
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right">
|
||||||
|
Accept Changes?
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -398,7 +787,7 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
const fieldLabel = field
|
const fieldLabel = field
|
||||||
? field.label
|
? field.label
|
||||||
: change.field;
|
: change.field;
|
||||||
const isReverted = isChangeReverted(
|
const isReverted = isChangeLocallyReverted(
|
||||||
product.productIndex,
|
product.productIndex,
|
||||||
change.field
|
change.field
|
||||||
);
|
);
|
||||||
@@ -421,7 +810,6 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: originalHtml,
|
__html: originalHtml,
|
||||||
}}
|
}}
|
||||||
className={isReverted ? "font-medium" : ""}
|
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -429,36 +817,46 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: correctedHtml,
|
__html: correctedHtml,
|
||||||
}}
|
}}
|
||||||
className={!isReverted ? "font-medium" : ""}
|
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right align-top">
|
||||||
<div className="mt-2">
|
<div className="flex justify-end gap-2">
|
||||||
{isReverted ? (
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
onClick={() => {
|
||||||
className="text-green-600 bg-green-50 hover:bg-green-100 hover:text-green-700"
|
// Toggle to Accepted state if currently rejected
|
||||||
disabled
|
toggleChangeAcceptance(
|
||||||
>
|
product.productIndex,
|
||||||
<CheckIcon className="w-4 h-4 mr-1" />
|
change.field
|
||||||
Reverted
|
);
|
||||||
</Button>
|
}}
|
||||||
) : (
|
className={
|
||||||
<Button
|
!isReverted
|
||||||
variant="outline"
|
? "bg-green-100 text-green-600 border-green-300 flex items-center"
|
||||||
size="sm"
|
: "border-gray-200 text-gray-600 hover:bg-green-50 hover:text-green-600 hover:border-green-200 flex items-center"
|
||||||
onClick={() => {
|
}
|
||||||
// Call the revert function directly
|
>
|
||||||
revertAiChange(
|
<CheckIcon className="h-4 w-4" />
|
||||||
product.productIndex,
|
</Button>
|
||||||
change.field
|
<Button
|
||||||
);
|
variant="outline"
|
||||||
}}
|
size="sm"
|
||||||
>
|
onClick={() => {
|
||||||
Revert Change
|
// Toggle to Rejected state if currently accepted
|
||||||
</Button>
|
toggleChangeAcceptance(
|
||||||
)}
|
product.productIndex,
|
||||||
|
change.field
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
isReverted
|
||||||
|
? "bg-red-100 text-red-600 border-red-300 flex items-center"
|
||||||
|
: "border-gray-200 text-gray-600 hover:bg-red-50 hover:text-red-600 hover:border-red-200 flex items-center"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -56,6 +56,20 @@ export interface CurrentPrompt {
|
|||||||
basePrompt: string;
|
basePrompt: string;
|
||||||
sampleFullPrompt: string;
|
sampleFullPrompt: string;
|
||||||
promptLength: number;
|
promptLength: number;
|
||||||
|
apiFormat?: Array<{
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
}>;
|
||||||
|
promptSources?: {
|
||||||
|
systemPrompt?: { id: number; prompt_text: string };
|
||||||
|
generalPrompt?: { id: number; prompt_text: string };
|
||||||
|
companyPrompts?: Array<{
|
||||||
|
id: number;
|
||||||
|
company: string;
|
||||||
|
companyName: string;
|
||||||
|
prompt_text: string
|
||||||
|
}>;
|
||||||
|
};
|
||||||
estimatedProcessingTime?: {
|
estimatedProcessingTime?: {
|
||||||
seconds: number | null;
|
seconds: number | null;
|
||||||
sampleCount: number;
|
sampleCount: number;
|
||||||
@@ -323,7 +337,9 @@ export const useAiValidation = <T extends string>(
|
|||||||
basePrompt: result.basePrompt || '',
|
basePrompt: result.basePrompt || '',
|
||||||
sampleFullPrompt: result.sampleFullPrompt || '',
|
sampleFullPrompt: result.sampleFullPrompt || '',
|
||||||
promptLength: result.promptLength || (promptContent ? promptContent.length : 0),
|
promptLength: result.promptLength || (promptContent ? promptContent.length : 0),
|
||||||
estimatedProcessingTime: result.estimatedProcessingTime
|
promptSources: result.promptSources,
|
||||||
|
estimatedProcessingTime: result.estimatedProcessingTime,
|
||||||
|
apiFormat: result.apiFormat
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
@@ -490,6 +506,27 @@ export const useAiValidation = <T extends string>(
|
|||||||
throw new Error(result.error || 'AI validation failed');
|
throw new Error(result.error || 'AI validation failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the prompt sources if they exist
|
||||||
|
if (result.promptSources) {
|
||||||
|
setCurrentPrompt(prev => {
|
||||||
|
// Create debugData if it doesn't exist
|
||||||
|
const prevDebugData = prev.debugData || {
|
||||||
|
taxonomyStats: null,
|
||||||
|
basePrompt: '',
|
||||||
|
sampleFullPrompt: '',
|
||||||
|
promptLength: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
debugData: {
|
||||||
|
...prevDebugData,
|
||||||
|
promptSources: result.promptSources
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update progress with actual processing time if available
|
// Update progress with actual processing time if available
|
||||||
if (result.performanceMetrics) {
|
if (result.performanceMetrics) {
|
||||||
console.log('Performance metrics:', result.performanceMetrics);
|
console.log('Performance metrics:', result.performanceMetrics);
|
||||||
|
|||||||
584
inventory/src/components/settings/PromptManagement.tsx
Normal file
584
inventory/src/components/settings/PromptManagement.tsx
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
import { useState, useMemo } 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 { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { ArrowUpDown, Pencil, Trash2, PlusCircle } from "lucide-react";
|
||||||
|
import config from "@/config";
|
||||||
|
import {
|
||||||
|
useReactTable,
|
||||||
|
getCoreRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
SortingState,
|
||||||
|
flexRender,
|
||||||
|
type ColumnDef,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface FieldOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromptFormData {
|
||||||
|
id?: number;
|
||||||
|
prompt_text: string;
|
||||||
|
prompt_type: 'general' | 'company_specific' | 'system';
|
||||||
|
company: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AiPrompt {
|
||||||
|
id: number;
|
||||||
|
prompt_text: string;
|
||||||
|
prompt_type: 'general' | 'company_specific' | 'system';
|
||||||
|
company: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldOptions {
|
||||||
|
companies: FieldOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PromptManagement() {
|
||||||
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||||
|
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||||
|
const [promptToDelete, setPromptToDelete] = useState<AiPrompt | null>(null);
|
||||||
|
const [editingPrompt, setEditingPrompt] = useState<AiPrompt | null>(null);
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([
|
||||||
|
{ id: "prompt_type", desc: true },
|
||||||
|
{ id: "company", desc: false }
|
||||||
|
]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [formData, setFormData] = useState<PromptFormData>({
|
||||||
|
prompt_text: "",
|
||||||
|
prompt_type: "general",
|
||||||
|
company: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: prompts, isLoading } = useQuery<AiPrompt[]>({
|
||||||
|
queryKey: ["ai-prompts"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`${config.apiUrl}/ai-prompts`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch AI prompts");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: fieldOptions } = useQuery<FieldOptions>({
|
||||||
|
queryKey: ["fieldOptions"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`${config.apiUrl}/import/field-options`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch field options");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if general and system prompts already exist
|
||||||
|
const generalPromptExists = useMemo(() => {
|
||||||
|
return prompts?.some(prompt => prompt.prompt_type === 'general');
|
||||||
|
}, [prompts]);
|
||||||
|
|
||||||
|
const systemPromptExists = useMemo(() => {
|
||||||
|
return prompts?.some(prompt => prompt.prompt_type === 'system');
|
||||||
|
}, [prompts]);
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: async (data: PromptFormData) => {
|
||||||
|
const response = await fetch(`${config.apiUrl}/ai-prompts`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.message || error.error || "Failed to create prompt");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["ai-prompts"] });
|
||||||
|
toast.success("Prompt created successfully");
|
||||||
|
resetForm();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to create prompt");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: async (data: PromptFormData) => {
|
||||||
|
if (!data.id) throw new Error("Prompt ID is required for update");
|
||||||
|
|
||||||
|
const response = await fetch(`${config.apiUrl}/ai-prompts/${data.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.message || error.error || "Failed to update prompt");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["ai-prompts"] });
|
||||||
|
toast.success("Prompt updated successfully");
|
||||||
|
resetForm();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to update prompt");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
const response = await fetch(`${config.apiUrl}/ai-prompts/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to delete prompt");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["ai-prompts"] });
|
||||||
|
toast.success("Prompt deleted successfully");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to delete prompt");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEdit = (prompt: AiPrompt) => {
|
||||||
|
setEditingPrompt(prompt);
|
||||||
|
setFormData({
|
||||||
|
id: prompt.id,
|
||||||
|
prompt_text: prompt.prompt_text,
|
||||||
|
prompt_type: prompt.prompt_type,
|
||||||
|
company: prompt.company,
|
||||||
|
});
|
||||||
|
setIsFormOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (prompt: AiPrompt) => {
|
||||||
|
setPromptToDelete(prompt);
|
||||||
|
setIsDeleteOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = () => {
|
||||||
|
if (promptToDelete) {
|
||||||
|
deleteMutation.mutate(promptToDelete.id);
|
||||||
|
setIsDeleteOpen(false);
|
||||||
|
setPromptToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// If prompt_type is general or system, ensure company is null
|
||||||
|
const submitData = {
|
||||||
|
...formData,
|
||||||
|
company: formData.prompt_type === 'company_specific' ? formData.company : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editingPrompt) {
|
||||||
|
updateMutation.mutate(submitData);
|
||||||
|
} else {
|
||||||
|
createMutation.mutate(submitData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
prompt_text: "",
|
||||||
|
prompt_type: "general",
|
||||||
|
company: null,
|
||||||
|
});
|
||||||
|
setEditingPrompt(null);
|
||||||
|
setIsFormOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateClick = () => {
|
||||||
|
resetForm();
|
||||||
|
|
||||||
|
// If general prompt and system prompt exist, default to company-specific
|
||||||
|
if (generalPromptExists && systemPromptExists) {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
prompt_type: 'company_specific'
|
||||||
|
}));
|
||||||
|
} else if (generalPromptExists && !systemPromptExists) {
|
||||||
|
// If general exists but system doesn't, suggest system prompt
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
prompt_type: 'system'
|
||||||
|
}));
|
||||||
|
} else if (!generalPromptExists) {
|
||||||
|
// If no general prompt, suggest that first
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
prompt_type: 'general'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFormOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = useMemo<ColumnDef<AiPrompt>[]>(() => [
|
||||||
|
{
|
||||||
|
accessorKey: "prompt_type",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const type = row.getValue("prompt_type") as string;
|
||||||
|
if (type === 'general') return 'General';
|
||||||
|
if (type === 'system') return 'System';
|
||||||
|
return 'Company Specific';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => row.prompt_text.length,
|
||||||
|
id: "length",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Length
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const length = getValue() as number;
|
||||||
|
return <span>{length.toLocaleString()}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "company",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Company
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const companyId = row.getValue("company");
|
||||||
|
if (!companyId) return 'N/A';
|
||||||
|
return fieldOptions?.companies.find(c => c.value === companyId)?.label || companyId;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "updated_at",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Last Updated
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => new Date(row.getValue("updated_at")).toLocaleDateString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex gap-2 justify-end pr-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleEdit(row.original)}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
onClick={() => handleDeleteClick(row.original)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], [fieldOptions]);
|
||||||
|
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (!prompts) return [];
|
||||||
|
return prompts.filter((prompt) => {
|
||||||
|
const searchString = searchQuery.toLowerCase();
|
||||||
|
return (
|
||||||
|
prompt.prompt_type.toLowerCase().includes(searchString) ||
|
||||||
|
(prompt.company && prompt.company.toLowerCase().includes(searchString))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [prompts, searchQuery]);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: filteredData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
},
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-2xl font-bold">AI Validation Prompts</h2>
|
||||||
|
<Button onClick={handleCreateClick}>
|
||||||
|
<PlusCircle className="mr-2 h-4 w-4" />
|
||||||
|
Create New Prompt
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Search prompts..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div>Loading prompts...</div>
|
||||||
|
) : (
|
||||||
|
<div className="border rounded-lg">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="bg-muted">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id} className="hover:bg-gray-100">
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id} className="pl-6">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="text-center">
|
||||||
|
No prompts found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Prompt Form Dialog */}
|
||||||
|
<Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editingPrompt ? "Edit Prompt" : "Create New Prompt"}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{editingPrompt
|
||||||
|
? "Update this AI validation prompt."
|
||||||
|
: "Create a new AI validation prompt that will be used during product validation."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="prompt_type">Prompt Type</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.prompt_type}
|
||||||
|
onValueChange={(value: 'general' | 'company_specific' | 'system') =>
|
||||||
|
setFormData({ ...formData, prompt_type: value })
|
||||||
|
}
|
||||||
|
disabled={(generalPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id) ||
|
||||||
|
(systemPromptExists && formData.prompt_type !== 'system' && !editingPrompt?.id)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select prompt type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
value="general"
|
||||||
|
disabled={generalPromptExists && !editingPrompt?.prompt_type?.includes('general')}
|
||||||
|
>
|
||||||
|
General
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="system"
|
||||||
|
disabled={systemPromptExists && !editingPrompt?.prompt_type?.includes('system')}
|
||||||
|
>
|
||||||
|
System
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="company_specific">Company Specific</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{generalPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id && systemPromptExists && formData.prompt_type !== 'system' && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
General and system prompts already exist. You can only create company-specific prompts.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{generalPromptExists && !systemPromptExists && formData.prompt_type !== 'general' && !editingPrompt?.id && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
A general prompt already exists. You can create a system prompt or company-specific prompts.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{systemPromptExists && !generalPromptExists && formData.prompt_type !== 'system' && !editingPrompt?.id && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
A system prompt already exists. You can create a general prompt or company-specific prompts.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.prompt_type === 'company_specific' && (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="company">Company</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.company || ''}
|
||||||
|
onValueChange={(value) => setFormData({ ...formData, company: value })}
|
||||||
|
required={formData.prompt_type === 'company_specific'}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select company" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{fieldOptions?.companies.map((company) => (
|
||||||
|
<SelectItem key={company.value} value={company.value}>
|
||||||
|
{company.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="prompt_text">Prompt Text</Label>
|
||||||
|
<Textarea
|
||||||
|
id="prompt_text"
|
||||||
|
value={formData.prompt_text}
|
||||||
|
onChange={(e) => setFormData({ ...formData, prompt_text: e.target.value })}
|
||||||
|
placeholder={`Enter your ${formData.prompt_type === 'system' ? 'system instructions' : 'validation prompt'} text...`}
|
||||||
|
className="h-80 font-mono text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{formData.prompt_type === 'system' && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
System prompts provide the initial instructions to the AI. This sets the tone and approach for all validations.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={() => {
|
||||||
|
resetForm();
|
||||||
|
setIsFormOpen(false);
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{editingPrompt ? "Update" : "Create"} Prompt
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete Prompt</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete this prompt? This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => {
|
||||||
|
setIsDeleteOpen(false);
|
||||||
|
setPromptToDelete(null);
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleDeleteConfirm}>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { PerformanceMetrics } from "@/components/settings/PerformanceMetrics";
|
|||||||
import { CalculationSettings } from "@/components/settings/CalculationSettings";
|
import { CalculationSettings } from "@/components/settings/CalculationSettings";
|
||||||
import { TemplateManagement } from "@/components/settings/TemplateManagement";
|
import { TemplateManagement } from "@/components/settings/TemplateManagement";
|
||||||
import { UserManagement } from "@/components/settings/UserManagement";
|
import { UserManagement } from "@/components/settings/UserManagement";
|
||||||
|
import { PromptManagement } from "@/components/settings/PromptManagement";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Protected } from "@/components/auth/Protected";
|
import { Protected } from "@/components/auth/Protected";
|
||||||
@@ -41,6 +42,7 @@ const SETTINGS_GROUPS: SettingsGroup[] = [
|
|||||||
label: "Content Management",
|
label: "Content Management",
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: "templates", permission: "settings:templates", label: "Template Management" },
|
{ id: "templates", permission: "settings:templates", label: "Template Management" },
|
||||||
|
{ id: "ai-prompts", permission: "settings:templates", label: "AI Prompts" },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -216,6 +218,21 @@ export function Settings() {
|
|||||||
</Protected>
|
</Protected>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="ai-prompts" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||||
|
<Protected
|
||||||
|
permission="settings:templates"
|
||||||
|
fallback={
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
You don't have permission to access AI Prompts.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PromptManagement />
|
||||||
|
</Protected>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="user-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
<TabsContent value="user-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||||
<Protected
|
<Protected
|
||||||
permission="settings:user_management"
|
permission="settings:user_management"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -166,6 +166,12 @@ export default defineConfig(function (_a) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/uploads": {
|
||||||
|
target: "https://inventory.kent.pw",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: function (path) { return path; },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user