diff --git a/inventory-server/src/routes/products.js b/inventory-server/src/routes/products.js index 6275dba..7e59d1c 100755 --- a/inventory-server/src/routes/products.js +++ b/inventory-server/src/routes/products.js @@ -78,6 +78,55 @@ router.get('/', async (req, res) => { paramCounter++; } + // Add new text filters for the additional fields + if (req.query.description) { + conditions.push(`p.description ILIKE $${paramCounter}`); + params.push(`%${req.query.description}%`); + paramCounter++; + } + + if (req.query.harmonized_tariff_code) { + conditions.push(`p.harmonized_tariff_code ILIKE $${paramCounter}`); + params.push(`%${req.query.harmonized_tariff_code}%`); + paramCounter++; + } + + if (req.query.notions_reference) { + conditions.push(`p.notions_reference ILIKE $${paramCounter}`); + params.push(`%${req.query.notions_reference}%`); + paramCounter++; + } + + if (req.query.line) { + conditions.push(`p.line ILIKE $${paramCounter}`); + params.push(`%${req.query.line}%`); + paramCounter++; + } + + if (req.query.subline) { + conditions.push(`p.subline ILIKE $${paramCounter}`); + params.push(`%${req.query.subline}%`); + paramCounter++; + } + + if (req.query.artist) { + conditions.push(`p.artist ILIKE $${paramCounter}`); + params.push(`%${req.query.artist}%`); + paramCounter++; + } + + if (req.query.country_of_origin) { + conditions.push(`p.country_of_origin ILIKE $${paramCounter}`); + params.push(`%${req.query.country_of_origin}%`); + paramCounter++; + } + + if (req.query.location) { + conditions.push(`p.location ILIKE $${paramCounter}`); + params.push(`%${req.query.location}%`); + paramCounter++; + } + // Handle numeric filters with operators const numericFields = { stock: 'p.stock_quantity', @@ -102,7 +151,16 @@ router.get('/', async (req, res) => { daysOfStock: 'pm.days_of_inventory', weeksOfStock: 'pm.weeks_of_inventory', reorderPoint: 'pm.reorder_point', - safetyStock: 'pm.safety_stock' + safetyStock: 'pm.safety_stock', + // Add new numeric fields + preorderCount: 'p.preorder_count', + notionsInvCount: 'p.notions_inv_count', + rating: 'p.rating', + reviews: 'p.reviews', + weight: 'p.weight', + totalSold: 'p.total_sold', + baskets: 'p.baskets', + notifies: 'p.notifies' }; Object.entries(req.query).forEach(([key, value]) => { @@ -298,7 +356,8 @@ router.get('/', async (req, res) => { pm.last_received_date, pm.abc_class, pm.stock_status, - pm.turnover_rate + pm.turnover_rate, + p.date_last_sold FROM products p LEFT JOIN product_metrics pm ON p.pid = pm.pid LEFT JOIN product_categories pc ON p.pid = pc.pid @@ -515,6 +574,29 @@ router.get('/:id', async (req, res) => { uom: parseInt(productRows[0].uom), managing_stock: Boolean(productRows[0].managing_stock), replenishable: Boolean(productRows[0].replenishable), + // Format new fields + preorder_count: parseInt(productRows[0].preorder_count || 0), + notions_inv_count: parseInt(productRows[0].notions_inv_count || 0), + harmonized_tariff_code: productRows[0].harmonized_tariff_code || '', + notions_reference: productRows[0].notions_reference || '', + line: productRows[0].line || '', + subline: productRows[0].subline || '', + artist: productRows[0].artist || '', + rating: parseFloat(productRows[0].rating || 0), + reviews: parseInt(productRows[0].reviews || 0), + weight: parseFloat(productRows[0].weight || 0), + dimensions: { + length: parseFloat(productRows[0].length || 0), + width: parseFloat(productRows[0].width || 0), + height: parseFloat(productRows[0].height || 0), + }, + country_of_origin: productRows[0].country_of_origin || '', + location: productRows[0].location || '', + total_sold: parseInt(productRows[0].total_sold || 0), + baskets: parseInt(productRows[0].baskets || 0), + notifies: parseInt(productRows[0].notifies || 0), + date_last_sold: productRows[0].date_last_sold || null, + // Format existing analytics fields daily_sales_avg: parseFloat(productRows[0].daily_sales_avg) || 0, weekly_sales_avg: parseFloat(productRows[0].weekly_sales_avg) || 0, monthly_sales_avg: parseFloat(productRows[0].monthly_sales_avg) || 0, diff --git a/inventory/src/components/products/ProductDetail.tsx b/inventory/src/components/products/ProductDetail.tsx index 02affa7..08663c8 100644 --- a/inventory/src/components/products/ProductDetail.tsx +++ b/inventory/src/components/products/ProductDetail.tsx @@ -125,6 +125,11 @@ interface Product { }>; category_paths?: Record; + + description?: string; + + preorder_count: number; + notions_inv_count: number; } interface ProductDetailProps { @@ -225,6 +230,7 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) { Purchase History Financial Vendor + Additional Info @@ -255,6 +261,12 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
UPC
{product?.barcode || "N/A"}
+ {product?.description && ( +
+
Description
+
{product.description}
+
+ )}
Categories
@@ -359,6 +371,51 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
+ +

Customer Engagement

+
+ {product?.total_sold > 0 && ( +
+
Total Sold
+
{product.total_sold}
+
+ )} + {product?.rating > 0 && ( +
+
Rating
+
+ {product.rating.toFixed(1)} + +
+
+ )} + {product?.reviews > 0 && ( +
+
Reviews
+
{product.reviews}
+
+ )} + {product?.baskets > 0 && ( +
+
In Baskets
+
{product.baskets}
+
+ )} + {product?.notifies > 0 && ( +
+
Notify Requests
+
{product.notifies}
+
+ )} + {product?.date_last_sold && ( +
+
Last Sold
+
{formatDate(product.date_last_sold)}
+
+ )} +
+
+

Financial Metrics

@@ -426,6 +483,18 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
Days of Inventory
{product?.metrics?.days_of_inventory || 0}
+ {product?.preorder_count > 0 && ( +
+
Preorders
+
{product?.preorder_count}
+
+ )} + {product?.notions_inv_count > 0 && ( +
+
Notions Inventory
+
{product?.notions_inv_count}
+
+ )}
@@ -506,6 +575,51 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) { + + +

Customer Engagement

+
+ {product?.total_sold > 0 && ( +
+
Total Sold
+
{product.total_sold}
+
+ )} + {product?.rating > 0 && ( +
+
Rating
+
+ {product.rating.toFixed(1)} + +
+
+ )} + {product?.reviews > 0 && ( +
+
Reviews
+
{product.reviews}
+
+ )} + {product?.baskets > 0 && ( +
+
In Baskets
+
{product.baskets}
+
+ )} + {product?.notifies > 0 && ( +
+
Notify Requests
+
{product.notifies}
+
+ )} + {product?.date_last_sold && ( +
+
Last Sold
+
{formatDate(product.date_last_sold)}
+
+ )} +
+
)} @@ -661,6 +775,123 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
No vendor performance data available
)} + + + {isLoading ? ( + + ) : ( +
+ +

Product Details

+
+ {product?.description && ( +
+
Description
+
{product.description}
+
+ )} +
+
Created Date
+
{formatDate(product?.created_at)}
+
+
+
Last Updated
+
{formatDate(product?.updated_at)}
+
+
+
Product ID
+
{product?.pid}
+
+
+
Line
+
{product?.line || 'N/A'}
+
+
+
Subline
+
{product?.subline || 'N/A'}
+
+
+
Artist
+
{product?.artist || 'N/A'}
+
+
+
Country of Origin
+
{product?.country_of_origin || 'N/A'}
+
+
+
Location
+
{product?.location || 'N/A'}
+
+
+
HTS Code
+
{product?.harmonized_tariff_code || 'N/A'}
+
+
+
Notions Reference
+
{product?.notions_reference || 'N/A'}
+
+
+
+ + +

Physical Attributes

+
+
+
Weight
+
{product?.weight ? `${product.weight} kg` : 'N/A'}
+
+
+
Dimensions
+
+ {product?.dimensions + ? `${product.dimensions.length} × ${product.dimensions.width} × ${product.dimensions.height} cm` + : 'N/A' + } +
+
+
+
+ + +

Customer Metrics

+
+
+
Rating
+
+ {product?.rating + ? <> + {product.rating.toFixed(1)} + + + : 'N/A' + } +
+
+
+
Review Count
+
{product?.reviews || 'N/A'}
+
+
+
Total Sold
+
{product?.total_sold || 'N/A'}
+
+
+
Currently in Baskets
+
{product?.baskets || 'N/A'}
+
+
+
Notify Requests
+
{product?.notifies || 'N/A'}
+
+
+
Date Last Sold
+
{formatDate(product?.date_last_sold) || 'N/A'}
+
+
+
+
+ )} +
diff --git a/inventory/src/components/products/ProductFilters.tsx b/inventory/src/components/products/ProductFilters.tsx index 9632ffe..e677f8a 100644 --- a/inventory/src/components/products/ProductFilters.tsx +++ b/inventory/src/components/products/ProductFilters.tsx @@ -56,6 +56,23 @@ const FILTER_OPTIONS: FilterOption[] = [ { id: "vendor_reference", label: "Supplier #", type: "text", group: "Basic Info" }, { id: "brand", label: "Brand", type: "select", group: "Basic Info" }, { id: "category", label: "Category", type: "select", group: "Basic Info" }, + { id: "description", label: "Description", type: "text", group: "Basic Info" }, + { id: "harmonized_tariff_code", label: "HTS Code", type: "text", group: "Basic Info" }, + { id: "notions_reference", label: "Notions Ref", type: "text", group: "Basic Info" }, + { id: "line", label: "Line", type: "text", group: "Basic Info" }, + { id: "subline", label: "Subline", type: "text", group: "Basic Info" }, + { id: "artist", label: "Artist", type: "text", group: "Basic Info" }, + { id: "country_of_origin", label: "Origin", type: "text", group: "Basic Info" }, + { id: "location", label: "Location", type: "text", group: "Basic Info" }, + + // Physical Properties + { + id: "weight", + label: "Weight", + type: "number", + group: "Physical Properties", + operators: ["=", ">", ">=", "<", "<=", "between"], + }, // Inventory Group { @@ -79,6 +96,20 @@ const FILTER_OPTIONS: FilterOption[] = [ group: "Inventory", operators: ["=", ">", ">=", "<", "<=", "between"], }, + { + id: "preorderCount", + label: "Preorder Count", + type: "number", + group: "Inventory", + operators: ["=", ">", ">=", "<", "<=", "between"], + }, + { + id: "notionsInvCount", + label: "Notions Inventory", + type: "number", + group: "Inventory", + operators: ["=", ">", ">=", "<", "<=", "between"], + }, { id: "daysOfStock", label: "Days of Stock", @@ -200,6 +231,47 @@ const FILTER_OPTIONS: FilterOption[] = [ type: "text", group: "Sales Metrics", }, + { + id: "date_last_sold", + label: "Date Last Sold", + type: "text", + group: "Sales Metrics", + }, + { + id: "total_sold", + label: "Total Sold", + type: "number", + group: "Sales Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], + }, + { + id: "baskets", + label: "In Baskets", + type: "number", + group: "Sales Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], + }, + { + id: "notifies", + label: "Notifies", + type: "number", + group: "Sales Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], + }, + { + id: "rating", + label: "Rating", + type: "number", + group: "Sales Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], + }, + { + id: "reviews", + label: "Reviews", + type: "number", + group: "Sales Metrics", + operators: ["=", ">", ">=", "<", "<=", "between"], + }, // Financial Metrics Group { diff --git a/inventory/src/components/products/ProductTable.tsx b/inventory/src/components/products/ProductTable.tsx index dcea38e..45f6197 100644 --- a/inventory/src/components/products/ProductTable.tsx +++ b/inventory/src/components/products/ProductTable.tsx @@ -234,6 +234,11 @@ export function ProductTable({ )) || '-'} ); + case 'dimensions': + if (value) { + return `${value.length}×${value.width}×${value.height}`; + } + return '-'; case 'stock_status': return getStockStatus(product.stock_status); case 'abc_class': @@ -252,6 +257,14 @@ export function ProductTable({ ) : ( Non-Replenishable ); + case 'rating': + if (value === undefined || value === null) return '-'; + return ( +
+ {value.toFixed(1)} + +
+ ); default: if (columnDef?.format && value !== undefined && value !== null) { // For numeric formats (those using toFixed), ensure the value is a number diff --git a/inventory/src/pages/Products.tsx b/inventory/src/pages/Products.tsx index 824cba3..ac20170 100644 --- a/inventory/src/pages/Products.tsx +++ b/inventory/src/pages/Products.tsx @@ -52,8 +52,25 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [ { key: 'vendor', label: 'Supplier', group: 'Basic Info' }, { key: 'vendor_reference', label: 'Supplier #', group: 'Basic Info' }, { key: 'barcode', label: 'UPC', group: 'Basic Info' }, + { key: 'description', label: 'Description', group: 'Basic Info' }, + { key: 'created_at', label: 'Created', group: 'Basic Info' }, + { key: 'harmonized_tariff_code', label: 'HTS Code', group: 'Basic Info' }, + { key: 'notions_reference', label: 'Notions Ref', group: 'Basic Info' }, + { key: 'line', label: 'Line', group: 'Basic Info' }, + { key: 'subline', label: 'Subline', group: 'Basic Info' }, + { key: 'artist', label: 'Artist', group: 'Basic Info' }, + { key: 'country_of_origin', label: 'Origin', group: 'Basic Info' }, + { key: 'location', label: 'Location', group: 'Basic Info' }, + + // Physical properties + { key: 'weight', label: 'Weight', group: 'Physical', format: (v) => v?.toString() ?? '-' }, + { key: 'dimensions', label: 'Dimensions', group: 'Physical', format: (v) => v ? `${v.length}x${v.width}x${v.height}` : '-' }, + + // Stock columns { key: 'stock_quantity', label: 'Shelf Count', group: 'Stock', format: (v) => v?.toString() ?? '-' }, { key: 'stock_status', label: 'Stock Status', group: 'Stock' }, + { key: 'preorder_count', label: 'Preorders', group: 'Stock', format: (v) => v?.toString() ?? '-' }, + { key: 'notions_inv_count', label: 'Notions Inv', group: 'Stock', format: (v) => v?.toString() ?? '-' }, { key: 'days_of_inventory', label: 'Days of Stock', group: 'Stock', format: (v) => v?.toFixed(1) ?? '-' }, { key: 'weeks_of_inventory', label: 'Weeks of Stock', group: 'Stock', format: (v) => v?.toFixed(1) ?? '-' }, { key: 'abc_class', label: 'ABC Class', group: 'Stock' }, @@ -63,10 +80,14 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [ { key: 'reorder_point', label: 'Reorder Point', group: 'Stock', format: (v) => v?.toString() ?? '-' }, { key: 'safety_stock', label: 'Safety Stock', group: 'Stock', format: (v) => v?.toString() ?? '-' }, { key: 'overstocked_amt', label: 'Overstock Amt', group: 'Stock', format: (v) => v?.toString() ?? '-' }, + + // Pricing columns { key: 'price', label: 'Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' }, { key: 'regular_price', label: 'Default Price', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' }, { key: 'cost_price', label: 'Cost', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' }, { key: 'landing_cost_price', label: 'Landing Cost', group: 'Pricing', format: (v) => v?.toFixed(2) ?? '-' }, + + // Sales columns { key: 'daily_sales_avg', label: 'Daily Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' }, { key: 'weekly_sales_avg', label: 'Weekly Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' }, { key: 'monthly_sales_avg', label: 'Monthly Sales', group: 'Sales', format: (v) => v?.toFixed(1) ?? '-' }, @@ -74,12 +95,22 @@ const AVAILABLE_COLUMNS: ColumnDef[] = [ { key: 'number_of_orders', label: 'Order Count', group: 'Sales', format: (v) => v?.toString() ?? '-' }, { key: 'first_sale_date', label: 'First Sale', group: 'Sales' }, { key: 'last_sale_date', label: 'Last Sale', group: 'Sales' }, + { key: 'date_last_sold', label: 'Date Last Sold', group: 'Sales' }, + { key: 'total_sold', label: 'Total Sold', group: 'Sales', format: (v) => v?.toString() ?? '-' }, + { key: 'baskets', label: 'In Baskets', group: 'Sales', format: (v) => v?.toString() ?? '-' }, + { key: 'notifies', label: 'Notifies', group: 'Sales', format: (v) => v?.toString() ?? '-' }, + { key: 'rating', label: 'Rating', group: 'Sales', format: (v) => v ? v.toFixed(1) : '-' }, + { key: 'reviews', label: 'Reviews', group: 'Sales', format: (v) => v?.toString() ?? '-' }, + + // Financial columns { key: 'gmroi', label: 'GMROI', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' }, { key: 'turnover_rate', label: 'Turnover Rate', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' }, { key: 'avg_margin_percent', label: 'Margin %', group: 'Financial', format: (v) => v ? `${v.toFixed(1)}%` : '-' }, { key: 'inventory_value', label: 'Inventory Value', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' }, { key: 'cost_of_goods_sold', label: 'COGS', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' }, { key: 'gross_profit', label: 'Gross Profit', group: 'Financial', format: (v) => v?.toFixed(2) ?? '-' }, + + // Lead Time columns { key: 'current_lead_time', label: 'Current Lead Time', group: 'Lead Time', format: (v) => v?.toFixed(1) ?? '-' }, { key: 'target_lead_time', label: 'Target Lead Time', group: 'Lead Time', format: (v) => v?.toFixed(1) ?? '-' }, { key: 'lead_time_status', label: 'Lead Time Status', group: 'Lead Time' }, diff --git a/inventory/src/types/products.ts b/inventory/src/types/products.ts index cefb8e4..f94271c 100644 --- a/inventory/src/types/products.ts +++ b/inventory/src/types/products.ts @@ -23,6 +23,30 @@ export interface Product { created_at: string; updated_at: string; + // New fields + description?: string; + preorder_count?: number; + notions_inv_count?: number; + harmonized_tariff_code?: string; + notions_reference?: string; + line?: string; + subline?: string; + artist?: string; + rating?: number; + reviews?: number; + weight?: number; + dimensions?: { + length: number; + width: number; + height: number; + }; + country_of_origin?: string; + location?: string; + total_sold?: number; + baskets?: number; + notifies?: number; + date_last_sold?: string; + // Metrics daily_sales_avg?: string; // numeric(15,3) weekly_sales_avg?: string; // numeric(15,3)