diff --git a/inventory-server/db/metrics-schema-new.sql b/inventory-server/db/metrics-schema-new.sql
index 7c5aa33..239a77d 100644
--- a/inventory-server/db/metrics-schema-new.sql
+++ b/inventory-server/db/metrics-schema-new.sql
@@ -53,6 +53,28 @@ CREATE TABLE public.product_metrics (
image_url VARCHAR, -- (e.g., products.image_175)
is_visible BOOLEAN,
is_replenishable BOOLEAN,
+
+ -- Additional product fields
+ barcode VARCHAR,
+ harmonized_tariff_code VARCHAR,
+ vendor_reference VARCHAR,
+ notions_reference VARCHAR,
+ line VARCHAR,
+ subline VARCHAR,
+ artist VARCHAR,
+ moq INT,
+ rating NUMERIC(10, 2),
+ reviews INT,
+ weight NUMERIC(14, 4),
+ length NUMERIC(14, 4),
+ width NUMERIC(14, 4),
+ height NUMERIC(14, 4),
+ country_of_origin VARCHAR,
+ location VARCHAR,
+ baskets INT,
+ notifies INT,
+ preorder_count INT,
+ notions_inv_count INT,
-- Current Status (Refreshed Hourly)
current_price NUMERIC(10, 2),
diff --git a/inventory-server/scripts/import/products.js b/inventory-server/scripts/import/products.js
index 2cf113e..ec156d1 100644
--- a/inventory-server/scripts/import/products.js
+++ b/inventory-server/scripts/import/products.js
@@ -8,7 +8,29 @@ dotenv.config({ path: path.join(__dirname, "../../.env") });
// Utility functions
const imageUrlBase = process.env.PRODUCT_IMAGE_URL_BASE || 'https://sbing.com/i/products/0000/';
-const getImageUrls = (pid, iid = 1) => {
+
+// Modified to accept a db connection for querying product_images
+const getImageUrls = async (pid, prodConnection, iid = null) => {
+ // If iid isn't provided, try to get it from product_images
+ if (iid === null && prodConnection) {
+ try {
+ // Query for images with order=255 (default/primary images)
+ const [primaryImages] = await prodConnection.query(
+ 'SELECT iid FROM product_images WHERE pid = ? AND `order` = 255 LIMIT 1',
+ [pid]
+ );
+
+ // Use the found iid or default to 1
+ iid = primaryImages.length > 0 ? primaryImages[0].iid : 1;
+ } catch (error) {
+ console.error(`Error fetching primary image for pid ${pid}:`, error);
+ iid = 1; // Fallback to default
+ }
+ } else {
+ // Use default if connection not provided
+ iid = iid || 1;
+ }
+
const paddedPid = pid.toString().padStart(6, '0');
// Use padded PID only for the first 3 digits
const prefix = paddedPid.slice(0, 3);
@@ -237,9 +259,11 @@ async function importMissingProducts(prodConnection, localConnection, missingPid
return `(${Array.from({ length: 47 }, (_, i) => `$${base + i + 1}`).join(', ')})`;
}).join(',');
- const values = batch.flatMap(row => {
- const imageUrls = getImageUrls(row.pid);
- return [
+ // Process image URLs for the batch
+ const processedValues = [];
+ for (const row of batch) {
+ const imageUrls = await getImageUrls(row.pid, prodConnection);
+ processedValues.push([
row.pid,
row.title,
row.description,
@@ -287,8 +311,10 @@ async function importMissingProducts(prodConnection, localConnection, missingPid
imageUrls.image_full,
null,
null
- ];
- });
+ ]);
+ }
+
+ const values = processedValues.flat();
const [result] = await localConnection.query(`
WITH inserted_products AS (
@@ -442,9 +468,11 @@ async function materializeCalculations(prodConnection, localConnection, incremen
return `(${Array.from({ length: 47 }, (_, i) => `$${base + i + 1}`).join(', ')})`;
}).join(',');
- const values = batch.flatMap(row => {
- const imageUrls = getImageUrls(row.pid);
- return [
+ // Process image URLs for the batch
+ const processedValues = [];
+ for (const row of batch) {
+ const imageUrls = await getImageUrls(row.pid, prodConnection);
+ processedValues.push([
row.pid,
row.title,
row.description,
@@ -492,8 +520,10 @@ async function materializeCalculations(prodConnection, localConnection, incremen
imageUrls.image_full,
null,
null
- ];
- });
+ ]);
+ }
+
+ const values = processedValues.flat();
await localConnection.query(`
INSERT INTO temp_products (
@@ -665,9 +695,11 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
return `(${Array.from({ length: 47 }, (_, i) => `$${base + i + 1}`).join(', ')})`;
}).join(',');
- const values = batch.flatMap(row => {
- const imageUrls = getImageUrls(row.pid);
- return [
+ // Process image URLs for the batch
+ const processedValues = [];
+ for (const row of batch) {
+ const imageUrls = await getImageUrls(row.pid, prodConnection);
+ processedValues.push([
row.pid,
row.title,
row.description,
@@ -715,8 +747,10 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
imageUrls.image_full,
row.options,
row.tags
- ];
- });
+ ]);
+ }
+
+ const values = processedValues.flat();
const [result] = await localConnection.query(`
WITH upserted AS (
diff --git a/inventory-server/scripts/metrics-new/update_product_metrics.sql b/inventory-server/scripts/metrics-new/update_product_metrics.sql
index 4dff4d4..3306bff 100644
--- a/inventory-server/scripts/metrics-new/update_product_metrics.sql
+++ b/inventory-server/scripts/metrics-new/update_product_metrics.sql
@@ -28,6 +28,27 @@ BEGIN
COALESCE(p.image_175, p.image) as image_url,
p.visible as is_visible,
p.replenishable as is_replenishable,
+ -- Add new product fields
+ p.barcode,
+ p.harmonized_tariff_code,
+ p.vendor_reference,
+ p.notions_reference,
+ p.line,
+ p.subline,
+ p.artist,
+ p.moq,
+ p.rating,
+ p.reviews,
+ p.weight,
+ p.length,
+ p.width,
+ p.height,
+ p.country_of_origin,
+ p.location,
+ p.baskets,
+ p.notifies,
+ p.preorder_count,
+ p.notions_inv_count,
COALESCE(p.price, 0.00) as current_price,
COALESCE(p.regular_price, 0.00) as current_regular_price,
COALESCE(p.cost_price, 0.00) as current_cost_price,
@@ -36,7 +57,6 @@ BEGIN
p.created_at,
p.first_received,
p.date_last_sold,
- p.moq,
p.uom -- Assuming UOM logic is handled elsewhere or simple (e.g., 1=each)
FROM public.products p
),
@@ -185,6 +205,9 @@ BEGIN
-- Final UPSERT into product_metrics
INSERT INTO public.product_metrics (
pid, last_calculated, sku, title, brand, vendor, image_url, is_visible, is_replenishable,
+ barcode, harmonized_tariff_code, vendor_reference, notions_reference, line, subline, artist,
+ moq, rating, reviews, weight, length, width, height, country_of_origin, location,
+ baskets, notifies, preorder_count, notions_inv_count,
current_price, current_regular_price, current_cost_price, current_landing_cost_price,
current_stock, current_stock_cost, current_stock_retail, current_stock_gross,
on_order_qty, on_order_cost, on_order_retail, earliest_expected_date,
@@ -214,6 +237,9 @@ BEGIN
)
SELECT
ci.pid, _start_time, ci.sku, ci.title, ci.brand, ci.vendor, ci.image_url, ci.is_visible, ci.is_replenishable,
+ ci.barcode, ci.harmonized_tariff_code, ci.vendor_reference, ci.notions_reference, ci.line, ci.subline, ci.artist,
+ ci.moq, ci.rating, ci.reviews, ci.weight, ci.length, ci.width, ci.height, ci.country_of_origin, ci.location,
+ ci.baskets, ci.notifies, ci.preorder_count, ci.notions_inv_count,
ci.current_price, ci.current_regular_price, ci.current_cost_price, ci.current_effective_cost,
ci.current_stock, ci.current_stock * ci.current_effective_cost, ci.current_stock * ci.current_price, ci.current_stock * ci.current_regular_price,
COALESCE(ooi.on_order_qty, 0), COALESCE(ooi.on_order_cost, 0.00), COALESCE(ooi.on_order_qty, 0) * ci.current_price, ooi.earliest_expected_date,
@@ -694,6 +720,9 @@ BEGIN
ON CONFLICT (pid) DO UPDATE SET
last_calculated = EXCLUDED.last_calculated,
sku = EXCLUDED.sku, title = EXCLUDED.title, brand = EXCLUDED.brand, vendor = EXCLUDED.vendor, image_url = EXCLUDED.image_url, is_visible = EXCLUDED.is_visible, is_replenishable = EXCLUDED.is_replenishable,
+ barcode = EXCLUDED.barcode, harmonized_tariff_code = EXCLUDED.harmonized_tariff_code, vendor_reference = EXCLUDED.vendor_reference, notions_reference = EXCLUDED.notions_reference, line = EXCLUDED.line, subline = EXCLUDED.subline, artist = EXCLUDED.artist,
+ moq = EXCLUDED.moq, rating = EXCLUDED.rating, reviews = EXCLUDED.reviews, weight = EXCLUDED.weight, length = EXCLUDED.length, width = EXCLUDED.width, height = EXCLUDED.height, country_of_origin = EXCLUDED.country_of_origin, location = EXCLUDED.location,
+ baskets = EXCLUDED.baskets, notifies = EXCLUDED.notifies, preorder_count = EXCLUDED.preorder_count, notions_inv_count = EXCLUDED.notions_inv_count,
current_price = EXCLUDED.current_price, current_regular_price = EXCLUDED.current_regular_price, current_cost_price = EXCLUDED.current_cost_price, current_landing_cost_price = EXCLUDED.current_landing_cost_price,
current_stock = EXCLUDED.current_stock, current_stock_cost = EXCLUDED.current_stock_cost, current_stock_retail = EXCLUDED.current_stock_retail, current_stock_gross = EXCLUDED.current_stock_gross,
on_order_qty = EXCLUDED.on_order_qty, on_order_cost = EXCLUDED.on_order_cost, on_order_retail = EXCLUDED.on_order_retail, earliest_expected_date = EXCLUDED.earliest_expected_date,
diff --git a/inventory-server/src/routes/metrics.js b/inventory-server/src/routes/metrics.js
index b02aac7..27c2b71 100644
--- a/inventory-server/src/routes/metrics.js
+++ b/inventory-server/src/routes/metrics.js
@@ -19,6 +19,27 @@ const COLUMN_MAP = {
imageUrl: 'pm.image_url',
isVisible: 'pm.is_visible',
isReplenishable: 'pm.is_replenishable',
+ // Additional Product Fields
+ barcode: 'pm.barcode',
+ harmonizedTariffCode: 'pm.harmonized_tariff_code',
+ vendorReference: 'pm.vendor_reference',
+ notionsReference: 'pm.notions_reference',
+ line: 'pm.line',
+ subline: 'pm.subline',
+ artist: 'pm.artist',
+ moq: 'pm.moq',
+ rating: 'pm.rating',
+ reviews: 'pm.reviews',
+ weight: 'pm.weight',
+ length: 'pm.length',
+ width: 'pm.width',
+ height: 'pm.height',
+ countryOfOrigin: 'pm.country_of_origin',
+ location: 'pm.location',
+ baskets: 'pm.baskets',
+ notifies: 'pm.notifies',
+ preorderCount: 'pm.preorder_count',
+ notionsInvCount: 'pm.notions_inv_count',
// Current Status
currentPrice: 'pm.current_price',
currentRegularPrice: 'pm.current_regular_price',
@@ -125,50 +146,49 @@ const COLUMN_MAP = {
status: 'pm.status'
};
-// Map of column types for proper sorting
+// Define column types for use in sorting/filtering
+// This helps apply correct comparison operators and sorting logic
const COLUMN_TYPES = {
- // Numeric columns
- pid: 'number',
- currentPrice: 'number',
- currentRegularPrice: 'number',
- currentCostPrice: 'number',
- currentLandingCostPrice: 'number',
- currentStock: 'number',
- currentStockCost: 'number',
- currentStockRetail: 'number',
- currentStockGross: 'number',
- onOrderQty: 'number',
- onOrderCost: 'number',
- onOrderRetail: 'number',
- ageDays: 'number',
- sales7d: 'number',
- revenue7d: 'number',
- sales14d: 'number',
- revenue14d: 'number',
- sales30d: 'number',
- revenue30d: 'number',
- cogs30d: 'number',
- profit30d: 'number',
- // ... other numeric columns
-
- // Date columns
- dateCreated: 'date',
- dateFirstReceived: 'date',
- dateLastReceived: 'date',
- dateFirstSold: 'date',
- dateLastSold: 'date',
- earliestExpectedDate: 'date',
- replenishDate: 'date',
-
- // Status column - special handling
- status: 'status',
-
- // String columns default to 'string' type
-
- // Boolean columns
- isVisible: 'boolean',
- isReplenishable: 'boolean',
- isOldStock: 'boolean'
+ // Numeric columns (use numeric operators and sorting)
+ numeric: [
+ 'pid', 'currentPrice', 'currentRegularPrice', 'currentCostPrice', 'currentLandingCostPrice',
+ 'currentStock', 'currentStockCost', 'currentStockRetail', 'currentStockGross',
+ 'onOrderQty', 'onOrderCost', 'onOrderRetail', 'ageDays',
+ 'sales7d', 'revenue7d', 'sales14d', 'revenue14d', 'sales30d', 'revenue30d',
+ 'cogs30d', 'profit30d', 'returnsUnits30d', 'returnsRevenue30d', 'discounts30d',
+ 'grossRevenue30d', 'grossRegularRevenue30d', 'stockoutDays30d', 'sales365d', 'revenue365d',
+ 'avgStockUnits30d', 'avgStockCost30d', 'avgStockRetail30d', 'avgStockGross30d',
+ 'receivedQty30d', 'receivedCost30d', 'lifetimeSales', 'lifetimeRevenue',
+ 'first7DaysSales', 'first7DaysRevenue', 'first30DaysSales', 'first30DaysRevenue',
+ 'first60DaysSales', 'first60DaysRevenue', 'first90DaysSales', 'first90DaysRevenue',
+ 'asp30d', 'acp30d', 'avgRos30d', 'avgSalesPerDay30d', 'avgSalesPerMonth30d',
+ 'margin30d', 'markup30d', 'gmroi30d', 'stockturn30d', 'returnRate30d', 'discountRate30d',
+ 'stockoutRate30d', 'markdown30d', 'markdownRate30d', 'sellThrough30d', 'avgLeadTimeDays',
+ 'salesVelocityDaily', 'configLeadTime', 'configDaysOfStock', 'configSafetyStock',
+ 'planningPeriodDays', 'leadTimeForecastUnits', 'daysOfStockForecastUnits',
+ 'planningPeriodForecastUnits', 'leadTimeClosingStock', 'daysOfStockClosingStock',
+ 'replenishmentNeededRaw', 'replenishmentUnits', 'replenishmentCost', 'replenishmentRetail',
+ 'replenishmentProfit', 'toOrderUnits', 'forecastLostSalesUnits', 'forecastLostRevenue',
+ 'stockCoverInDays', 'poCoverInDays', 'sellsOutInDays', 'overstockedUnits',
+ 'overstockedCost', 'overstockedRetail', 'yesterdaySales',
+ // New numeric columns
+ 'moq', 'rating', 'reviews', 'weight', 'length', 'width', 'height',
+ 'baskets', 'notifies', 'preorderCount', 'notionsInvCount'
+ ],
+ // Date columns (use date operators and sorting)
+ date: [
+ 'dateCreated', 'dateFirstReceived', 'dateLastReceived', 'dateFirstSold', 'dateLastSold',
+ 'earliestExpectedDate', 'replenishDate', 'forecastedOutOfStockDate'
+ ],
+ // String columns (use string operators and sorting)
+ string: [
+ 'sku', 'title', 'brand', 'vendor', 'imageUrl', 'abcClass', 'status',
+ // New string columns
+ 'barcode', 'harmonizedTariffCode', 'vendorReference', 'notionsReference',
+ 'line', 'subline', 'artist', 'countryOfOrigin', 'location'
+ ],
+ // Boolean columns (use boolean operators and sorting)
+ boolean: ['isVisible', 'isReplenishable', 'isOldStock']
};
// Special sort handling for certain columns
diff --git a/inventory/src/components/products/ProductFilters.tsx b/inventory/src/components/products/ProductFilters.tsx
index a47b717..e76a0ac 100644
--- a/inventory/src/components/products/ProductFilters.tsx
+++ b/inventory/src/components/products/ProductFilters.tsx
@@ -21,17 +21,37 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Skeleton } from "@/components/ui/skeleton";
import { ProductFilterOptions, ProductMetricColumnKey } from "@/types/products";
+// Define operators for different filter types
+const STRING_OPERATORS: ComparisonOperator[] = ["contains", "equals", "starts_with", "ends_with", "not_contains", "is_empty", "is_not_empty"];
+const NUMBER_OPERATORS: ComparisonOperator[] = ["=", ">", ">=", "<", "<=", "between", "is_empty", "is_not_empty"];
+const BOOLEAN_OPERATORS: ComparisonOperator[] = ["is_true", "is_false"];
+const DATE_OPERATORS: ComparisonOperator[] = ["=", ">", ">=", "<", "<=", "between", "is_empty", "is_not_empty"];
+const SELECT_OPERATORS: ComparisonOperator[] = ["=", "!=", "in", "not_in", "is_empty", "is_not_empty"];
+
+interface FilterOption {
+ id: ProductMetricColumnKey | 'search';
+ label: string;
+ type: "select" | "number" | "boolean" | "text" | "date" | "string";
+ options?: { label: string; value: string }[];
+ group: string;
+ operators?: ComparisonOperator[];
+}
+
type FilterValue = string | number | boolean;
-type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "between";
+
+export type ComparisonOperator =
+ | "=" | "!=" | ">" | ">=" | "<" | "<=" | "between"
+ | "contains" | "equals" | "starts_with" | "ends_with" | "not_contains"
+ | "in" | "not_in" | "is_empty" | "is_not_empty" | "is_true" | "is_false";
+
+// Support both simple values and complex ones with operators
+export type ActiveFilterValue = FilterValue | FilterValueWithOperator;
interface FilterValueWithOperator {
value: FilterValue | string[] | number[];
operator: ComparisonOperator;
}
-// Support both simple values and complex ones with operators
-export type ActiveFilterValue = FilterValue | FilterValueWithOperator;
-
interface ActiveFilterDisplay {
id: string;
label: string;
@@ -39,71 +59,149 @@ interface ActiveFilterDisplay {
displayValue: string;
}
-export interface FilterOption {
- id: ProductMetricColumnKey | 'search';
- label: string;
- type: "select" | "number" | "boolean" | "text" | "date";
- options?: { label: string; value: string }[];
- group: string;
- operators?: ComparisonOperator[];
-}
-
-// Base filter options - static part of the filters, which will be merged with dynamic options
+// Base filter options available to users
const BASE_FILTER_OPTIONS: FilterOption[] = [
- // Search Group
- { id: "search", label: "Search (Title, SKU...)", type: "text", group: "Search" },
+ // Basic Info group
+ { id: 'sku', label: 'SKU', type: 'text', group: 'Basic Info', operators: STRING_OPERATORS },
+ { id: 'title', label: 'Name', type: 'text', group: 'Basic Info', operators: STRING_OPERATORS },
+ { id: 'barcode', label: 'UPC', type: 'text', group: 'Basic Info', operators: STRING_OPERATORS },
+ { id: 'vendor', label: 'Supplier', type: 'select', group: 'Basic Info', operators: SELECT_OPERATORS, options: [] },
+ { id: 'brand', label: 'Company', type: 'select', group: 'Basic Info', operators: SELECT_OPERATORS, options: [] },
+ { id: 'line', label: 'Line', type: 'text', group: 'Basic Info', operators: STRING_OPERATORS },
+ { id: 'subline', label: 'Subline', type: 'text', group: 'Basic Info', operators: STRING_OPERATORS },
+ { id: 'artist', label: 'Artist', type: 'text', group: 'Basic Info', operators: STRING_OPERATORS },
+ { id: 'isVisible', label: 'Visible', type: 'boolean', group: 'Basic Info', operators: BOOLEAN_OPERATORS },
+ { id: 'isReplenishable', label: 'Replenishable', type: 'boolean', group: 'Basic Info', operators: BOOLEAN_OPERATORS },
+ { id: 'abcClass', label: 'ABC Class', type: 'select', group: 'Basic Info', operators: SELECT_OPERATORS, options: [] },
+ { id: 'status', label: 'Status', type: 'select', group: 'Basic Info', operators: SELECT_OPERATORS, options: [
+ { value: 'in_stock', label: 'In Stock' },
+ { value: 'low_stock', label: 'Low Stock' },
+ { value: 'out_of_stock', label: 'Out of Stock' },
+ { value: 'discontinued', label: 'Discontinued' },
+ ]},
+ { id: 'dateCreated', label: 'Created Date', type: 'date', group: 'Basic Info', operators: DATE_OPERATORS },
- // Basic Info Group
- { id: "sku", label: "SKU", type: "text", group: "Basic Info" },
- { id: "vendor", label: "Vendor", type: "select", group: "Basic Info" },
- { id: "brand", label: "Brand", type: "select", group: "Basic Info" },
- { id: "isVisible", label: "Is Visible", type: "select", options: [{label: 'Yes', value: 'true'}, {label: 'No', value: 'false'}], group: "Basic Info" },
- { id: "isReplenishable", label: "Is Replenishable", type: "select", options: [{label: 'Yes', value: 'true'}, {label: 'No', value: 'false'}], group: "Basic Info" },
- { id: "dateCreated", label: "Date Created", type: "date", group: "Basic Info", operators: ["=", ">", ">=", "<", "<=", "between"] },
- { id: "ageDays", label: "Age (Days)", type: "number", group: "Basic Info", operators: ["=", ">", ">=", "<", "<=", "between"] },
-
- // Status Group
- // { id: "status", label: "Status", type: "select", options: [...] } - Will be populated dynamically or handled via views
+ // Supply Chain group
+ { id: 'vendorReference', label: 'Supplier #', type: 'text', group: 'Supply Chain', operators: STRING_OPERATORS },
+ { id: 'notionsReference', label: 'Notions #', type: 'text', group: 'Supply Chain', operators: STRING_OPERATORS },
+ { id: 'harmonizedTariffCode', label: 'Tariff Code', type: 'text', group: 'Supply Chain', operators: STRING_OPERATORS },
+ { id: 'countryOfOrigin', label: 'Country of Origin', type: 'text', group: 'Supply Chain', operators: STRING_OPERATORS },
+ { id: 'location', label: 'Location', type: 'text', group: 'Supply Chain', operators: STRING_OPERATORS },
+ { id: 'moq', label: 'MOQ', type: 'number', group: 'Supply Chain', operators: NUMBER_OPERATORS },
- // Stock Group
- { id: "abcClass", label: "ABC Class", type: "select", group: "Stock" },
- { id: "currentStock", label: "Current Stock", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
- { id: "currentStockCost", label: "Stock Cost", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
- { id: "currentStockRetail", label: "Stock Retail", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
- { id: "onOrderQty", label: "On Order Qty", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
- { id: "stockCoverInDays", label: "Stock Cover (Days)", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
- { id: "sellsOutInDays", label: "Sells Out In (Days)", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
- { id: "overstockedUnits", label: "Overstock Units", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
- { id: "isOldStock", label: "Is Old Stock", type: "select", options: [{label: 'Yes', value: 'true'}, {label: 'No', value: 'false'}], group: "Stock" },
+ // Physical Properties group
+ { id: 'weight', label: 'Weight', type: 'number', group: 'Physical', operators: NUMBER_OPERATORS },
+ { id: 'dimensions', label: 'Dimensions', type: 'text', group: 'Physical', operators: STRING_OPERATORS },
+
+ // Customer Engagement group
+ { id: 'rating', label: 'Rating', type: 'number', group: 'Customer', operators: NUMBER_OPERATORS },
+ { id: 'reviews', label: 'Reviews Count', type: 'number', group: 'Customer', operators: NUMBER_OPERATORS },
+ { id: 'baskets', label: 'Basket Adds', type: 'number', group: 'Customer', operators: NUMBER_OPERATORS },
+ { id: 'notifies', label: 'Stock Alerts', type: 'number', group: 'Customer', operators: NUMBER_OPERATORS },
+
+ // Stock group
+ { id: 'currentStock', label: 'Current Stock', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
+ { id: 'preorderCount', label: 'Preorders', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
+ { id: 'notionsInvCount', label: 'Notions Inventory', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
+ { id: 'onOrderQty', label: 'On Order', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
+ { id: 'configSafetyStock', label: 'Safety Stock', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
+ { id: 'replenishmentUnits', label: 'Replenish Qty', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
+ { id: 'toOrderUnits', label: 'To Order', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
+ { id: 'stockCoverInDays', label: 'Stock Cover (Days)', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
+ { id: 'sellsOutInDays', label: 'Sells Out In (Days)', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
+ { id: 'isOldStock', label: 'Old Stock', type: 'boolean', group: 'Stock', operators: BOOLEAN_OPERATORS },
+ { id: 'overstockedUnits', label: 'Overstock Qty', type: 'number', group: 'Stock', operators: NUMBER_OPERATORS },
// Pricing Group
{ id: "currentPrice", label: "Current Price", type: "number", group: "Pricing", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "currentRegularPrice", label: "Regular Price", type: "number", group: "Pricing", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "currentCostPrice", label: "Cost Price", type: "number", group: "Pricing", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "currentLandingCostPrice", label: "Landing Cost", type: "number", group: "Pricing", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ // Valuation Group
+ { id: "avgStockCost30d", label: "Avg Stock Cost (30d)", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "avgStockRetail30d", label: "Avg Stock Retail (30d)", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "avgStockGross30d", label: "Avg Stock Gross (30d)", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "receivedCost30d", label: "Received Cost (30d)", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "replenishmentCost", label: "Replenishment Cost", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "replenishmentRetail", label: "Replenishment Retail", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "replenishmentProfit", label: "Replenishment Profit", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "onOrderCost", label: "On Order Cost", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "onOrderRetail", label: "On Order Retail", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "overstockedCost", label: "Overstock Cost", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "overstockedRetail", label: "Overstock Retail", type: "number", group: "Valuation", operators: ["=", ">", ">=", "<", "<=", "between"] },
+
// Sales Metrics Group
{ id: "sales7d", label: "Sales (7d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "revenue7d", label: "Revenue (7d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "sales14d", label: "Sales (14d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "revenue14d", label: "Revenue (14d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "sales30d", label: "Sales (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "revenue30d", label: "Revenue (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
- { id: "profit30d", label: "Profit (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "sales365d", label: "Sales (365d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "revenue365d", label: "Revenue (365d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "salesVelocityDaily", label: "Daily Velocity", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "dateLastSold", label: "Date Last Sold", type: "date", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "yesterdaySales", label: "Sales (Yesterday)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "avgSalesPerDay30d", label: "Avg Sales/Day (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "avgSalesPerMonth30d", label: "Avg Sales/Month (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "returnsUnits30d", label: "Returns Units (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "returnsRevenue30d", label: "Returns Revenue (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "discounts30d", label: "Discounts (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "grossRevenue30d", label: "Gross Revenue (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "grossRegularRevenue30d", label: "Gross Regular Revenue (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "asp30d", label: "ASP (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "acp30d", label: "ACP (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "avgRos30d", label: "Avg ROS (30d)", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "lifetimeSales", label: "Lifetime Sales", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "lifetimeRevenue", label: "Lifetime Revenue", type: "number", group: "Sales Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+
+ // First Period Group
+ { id: "first7DaysSales", label: "First 7 Days Sales", type: "number", group: "First Period", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "first7DaysRevenue", label: "First 7 Days Revenue", type: "number", group: "First Period", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "first30DaysSales", label: "First 30 Days Sales", type: "number", group: "First Period", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "first30DaysRevenue", label: "First 30 Days Revenue", type: "number", group: "First Period", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "first60DaysSales", label: "First 60 Days Sales", type: "number", group: "First Period", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "first60DaysRevenue", label: "First 60 Days Revenue", type: "number", group: "First Period", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "first90DaysSales", label: "First 90 Days Sales", type: "number", group: "First Period", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "first90DaysRevenue", label: "First 90 Days Revenue", type: "number", group: "First Period", operators: ["=", ">", ">=", "<", "<=", "between"] },
// Financial KPIs
+ { id: "cogs30d", label: "COGS (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "profit30d", label: "Profit (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "margin30d", label: "Margin % (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "markup30d", label: "Markup % (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "gmroi30d", label: "GMROI (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "stockturn30d", label: "Stock Turn (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "sellThrough30d", label: "Sell Thru % (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "returnRate30d", label: "Return Rate % (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "discountRate30d", label: "Discount Rate % (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "markdown30d", label: "Markdown (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "markdownRate30d", label: "Markdown Rate % (30d)", type: "number", group: "Financial Metrics", operators: ["=", ">", ">=", "<", "<=", "between"] },
+
+ // Forecasting Group
+ { id: "leadTimeForecastUnits", label: "Lead Time Forecast Units", type: "number", group: "Forecasting", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "daysOfStockForecastUnits", label: "Days of Stock Forecast Units", type: "number", group: "Forecasting", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "planningPeriodForecastUnits", label: "Planning Period Forecast Units", type: "number", group: "Forecasting", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "leadTimeClosingStock", label: "Lead Time Closing Stock", type: "number", group: "Forecasting", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "daysOfStockClosingStock", label: "Days of Stock Closing Stock", type: "number", group: "Forecasting", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "replenishmentNeededRaw", label: "Replenishment Needed Raw", type: "number", group: "Forecasting", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "forecastLostSalesUnits", label: "Forecast Lost Sales Units", type: "number", group: "Forecasting", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "forecastLostRevenue", label: "Forecast Lost Revenue", type: "number", group: "Forecasting", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "stockoutDays30d", label: "Stockout Days (30d)", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "stockoutRate30d", label: "Stockout Rate %", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "avgStockUnits30d", label: "Avg Stock Units (30d)", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "receivedQty30d", label: "Received Qty (30d)", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "poCoverInDays", label: "PO Cover (Days)", type: "number", group: "Stock", operators: ["=", ">", ">=", "<", "<=", "between"] },
// Lead Time & Replenishment
- { id: "avgLeadTimeDays", label: "Avg Lead Time", type: "number", group: "Lead Time", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "configLeadTime", label: "Config Lead Time", type: "number", group: "Lead Time", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "configDaysOfStock", label: "Config Days of Stock", type: "number", group: "Lead Time", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "avgLeadTimeDays", label: "Avg Lead Time", type: "number", group: "Lead Time", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "earliestExpectedDate", label: "Next Arrival Date", type: "date", group: "Lead Time", operators: ["=", ">", ">=", "<", "<=", "between"] },
{ id: "dateLastReceived", label: "Date Last Received", type: "date", group: "Lead Time", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "dateFirstReceived", label: "First Received", type: "date", group: "Dates", operators: ["=", ">", ">=", "<", "<=", "between"] },
+ { id: "dateFirstSold", label: "First Sold", type: "date", group: "Dates", operators: ["=", ">", ">=", "<", "<=", "between"] },
];
interface ProductFiltersProps {
@@ -285,13 +383,12 @@ export function ProductFilters({
const activeFiltersList = React.useMemo((): ActiveFilterDisplay[] => {
return Object.entries(activeFilters)
- .map(([id, value]): ActiveFilterDisplay | null => {
+ .map(([id, value]): ActiveFilterDisplay => {
const option = processedFilterOptions.find(opt => opt.id === id);
- if (!option) return null; // Should not happen if state is clean
return {
id,
- label: option.label,
+ label: option?.label || id,
value,
displayValue: getFilterDisplayValue(id, value),
};
diff --git a/inventory/src/components/products/ProductTable.tsx b/inventory/src/components/products/ProductTable.tsx
index c711ba2..2906504 100644
--- a/inventory/src/components/products/ProductTable.tsx
+++ b/inventory/src/components/products/ProductTable.tsx
@@ -78,6 +78,8 @@ function SortableHeader({ column, columnDef, onSort, sortColumn, sortDirection }
zIndex: isDragging ? 10 : 1,
position: 'relative' as const,
touchAction: 'none' as const,
+ width: columnDef?.width ? undefined : 'auto',
+ minWidth: columnDef?.key === 'imageUrl' ? '60px' : '100px',
};
// Skip rendering content for 'noLabel' columns (like image)
@@ -85,8 +87,8 @@ function SortableHeader({ column, columnDef, onSort, sortColumn, sortDirection }
return (