From c9b656d34b625eef0b9266bb682922d37db720e1 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Apr 2025 09:52:36 -0400 Subject: [PATCH] Tweaks and fixes for products table --- inventory-server/db/metrics-schema-new.sql | 22 ++ inventory-server/scripts/import/products.js | 66 +++- .../metrics-new/update_product_metrics.sql | 31 +- inventory-server/src/routes/metrics.js | 106 +++--- .../components/products/ProductFilters.tsx | 181 +++++++--- .../src/components/products/ProductTable.tsx | 162 +++++---- .../src/components/products/ProductViews.tsx | 23 +- inventory/src/pages/Products.tsx | 338 ++++++++++++------ inventory/src/types/products.ts | 167 ++++++++- inventory/src/utils/productUtils.ts | 2 + 10 files changed, 788 insertions(+), 310 deletions(-) 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 ( @@ -98,7 +100,7 @@ function SortableHeader({ column, columnDef, onSort, sortColumn, sortDirection } ref={setNodeRef} style={style} className={cn( - "cursor-pointer select-none group", + "cursor-pointer select-none group whitespace-nowrap", columnDef?.width, sortColumn === column && "bg-accent/50" )} @@ -207,84 +209,102 @@ export function ProductTable({ onDragEnd={handleDragEnd} onDragCancel={() => setActiveId(null)} > -
+
{isLoading && (
)} - - - - - {orderedVisibleColumns.map((columnKey) => ( - def.key === columnKey)} - onSort={onSort} - sortColumn={sortColumn} - sortDirection={sortDirection} - /> - ))} - - - - - {products.length === 0 && !isLoading ? ( +
+
+ - - No products found matching your criteria. - - - ) : ( - products.map((product) => ( - onRowClick?.(product)} - className="cursor-pointer hover:bg-muted/50" - data-state={isLoading ? 'loading' : undefined} + {orderedVisibleColumns.map((columnKey) => ( - c.key===columnKey)?.width)}> - {columnKey === 'imageUrl' ? ( -
- {product.imageUrl ? ( - {product.title - ) : ( -
No Image
- )} -
- ) : ( - formatColumnValue(product, columnKey) - )} -
+ def.key === columnKey)} + onSort={onSort} + sortColumn={sortColumn} + sortDirection={sortDirection} + /> ))} -
- )) - )} - {isLoading && products.length === 0 && Array.from({length: 10}).map((_, i) => ( - - {orderedVisibleColumns.map(key => ( - c.key===key)?.width)}> - - - ))} + - ))} - -
+ + + {products.length === 0 && !isLoading ? ( + + + No products found matching your criteria. + + + ) : ( + products.map((product) => ( + onRowClick?.(product)} + className="cursor-pointer hover:bg-muted/50" + data-state={isLoading ? 'loading' : undefined} + > + {orderedVisibleColumns.map((columnKey) => { + const colDef = columnDefs.find(c => c.key === columnKey); + return ( + + {columnKey === 'imageUrl' ? ( +
+ {product.imageUrl ? ( + {product.title + ) : ( +
No Image
+ )} +
+ ) : ( + formatColumnValue(product, columnKey) + )} +
+ ); + })} +
+ )) + )} + {isLoading && products.length === 0 && Array.from({length: 10}).map((_, i) => ( + + {orderedVisibleColumns.map(key => { + const colDef = columnDefs.find(c => c.key === key); + return ( + + + + ); + })} + + ))} +
+ +
); diff --git a/inventory/src/components/products/ProductViews.tsx b/inventory/src/components/products/ProductViews.tsx index efcf77b..63ef05b 100644 --- a/inventory/src/components/products/ProductViews.tsx +++ b/inventory/src/components/products/ProductViews.tsx @@ -1,5 +1,4 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Product } from "@/types/products" import { AlertTriangle, CheckCircle2, @@ -15,7 +14,6 @@ export type ProductView = { label: string icon: any iconClassName: string - columns: (keyof Product)[] } export const PRODUCT_VIEWS: ProductView[] = [ @@ -23,50 +21,43 @@ export const PRODUCT_VIEWS: ProductView[] = [ id: "all", label: "All Products", icon: PackageSearch, - iconClassName: "", - columns: ["image", "title", "SKU", "stock_quantity", "price", "stock_status"] + iconClassName: "" }, { id: "critical", label: "Critical Stock", icon: AlertTriangle, - iconClassName: "", - columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "reorder_qty", "last_purchase_date", "lead_time_status"] + iconClassName: "" }, { id: "reorder", label: "Reorder Soon", icon: PackagePlus, - iconClassName: "", - columns: ["image", "title", "SKU", "stock_quantity", "daily_sales_avg", "reorder_qty", "last_purchase_date", "lead_time_status"] + iconClassName: "" }, { id: "healthy", label: "Healthy Stock", icon: CheckCircle2, - iconClassName: "", - columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"] + iconClassName: "" }, { id: "at-risk", label: "At Risk", icon: Timer, - iconClassName: "", - columns: ["image", "title", "stock_quantity", "daily_sales_avg", "weekly_sales_avg", "days_of_inventory", "last_sale_date"] + iconClassName: "" }, { id: "overstocked", label: "Overstock", icon: PackageX, - iconClassName: "", - columns: ["image", "title", "stock_quantity", "daily_sales_avg", "overstocked_amt", "days_of_inventory", "last_sale_date"] + iconClassName: "" }, { id: "new", label: "New Products", icon: Sparkles, - iconClassName: "", - columns: ["image", "title", "stock_quantity", "daily_sales_avg", "stock_status", "abc_class"] + iconClassName: "" } ] diff --git a/inventory/src/pages/Products.tsx b/inventory/src/pages/Products.tsx index cfd5571..0a57524 100644 --- a/inventory/src/pages/Products.tsx +++ b/inventory/src/pages/Products.tsx @@ -8,8 +8,7 @@ import { ProductTableSkeleton } from '@/components/products/ProductTableSkeleton import { ProductDetail } from '@/components/products/ProductDetail'; import { ProductViews } from '@/components/products/ProductViews'; import { Button } from '@/components/ui/button'; -import { Product, ProductMetric, ProductMetricColumnKey } from '@/types/products'; -import { getProductStatus } from '@/utils/productUtils'; +import { ProductMetric, ProductMetricColumnKey } from '@/types/products'; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -45,73 +44,144 @@ interface ColumnDef { // Define available columns with their groups const AVAILABLE_COLUMNS: ColumnDef[] = [ - { key: 'imageUrl', label: 'Image', group: 'Basic Info', noLabel: true, width: 'w-[60px]' }, - { key: 'title', label: 'Name', group: 'Basic Info' }, - { key: 'sku', label: 'SKU', group: 'Basic Info' }, - { key: 'brand', label: 'Company', group: 'Basic Info' }, - { key: 'vendor', label: 'Supplier', group: 'Basic Info' }, - { key: 'isVisible', label: 'Visible', group: 'Basic Info' }, - { key: 'isReplenishable', label: 'Replenishable', group: 'Basic Info' }, - { key: 'dateCreated', label: 'Created', group: 'Basic Info' }, + // Identity & Basic Info + { key: 'imageUrl', label: 'Image', group: 'Product Identity', noLabel: true, width: 'w-[60px]' }, + { key: 'title', label: 'Name', group: 'Product Identity'}, + { key: 'sku', label: 'Item Number', group: 'Product Identity' }, + { key: 'barcode', label: 'UPC', group: 'Product Identity' }, + { key: 'brand', label: 'Company', group: 'Product Identity' }, + { key: 'line', label: 'Line', group: 'Product Identity' }, + { key: 'subline', label: 'Subline', group: 'Product Identity' }, + { key: 'artist', label: 'Artist', group: 'Product Identity' }, + { key: 'isVisible', label: 'Visible', group: 'Product Identity' }, + { key: 'isReplenishable', label: 'Replenishable', group: 'Product Identity' }, + { key: 'abcClass', label: 'ABC Class', group: 'Product Identity' }, + { key: 'status', label: 'Status', group: 'Product Identity' }, + { key: 'dateCreated', label: 'Created', group: 'Dates' }, - // Current Status + // Supply Chain + { key: 'vendor', label: 'Supplier', group: 'Supply Chain' }, + { key: 'vendorReference', label: 'Supplier #', group: 'Supply Chain' }, + { key: 'notionsReference', label: 'Notions #', group: 'Supply Chain' }, + { key: 'harmonizedTariffCode', label: 'Tariff Code', group: 'Supply Chain' }, + { key: 'countryOfOrigin', label: 'Country', group: 'Supply Chain' }, + { key: 'location', label: 'Location', group: 'Supply Chain' }, + { key: 'moq', label: 'MOQ', group: 'Supply Chain', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + + // Physical Properties + { key: 'weight', label: 'Weight', group: 'Physical', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'dimensions', label: 'Dimensions', group: 'Physical', format: (v) => v ? `${v.length}×${v.width}×${v.height}` : '-' }, + + // Customer Engagement + { key: 'rating', label: 'Rating', group: 'Customer', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'reviews', label: 'Reviews', group: 'Customer', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'baskets', label: 'Basket Adds', group: 'Customer', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'notifies', label: 'Stock Alerts', group: 'Customer', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + + // Inventory & Stock + { key: 'currentStock', label: 'Current Stock', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'preorderCount', label: 'Preorders', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'notionsInvCount', label: 'Notions Inv.', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'configSafetyStock', label: 'Safety Stock', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'replenishmentUnits', label: 'Replenish Units', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'stockCoverInDays', label: 'Stock Cover (Days)', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'sellsOutInDays', label: 'Sells Out In (Days)', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'onOrderQty', label: 'On Order', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'earliestExpectedDate', label: 'Expected Date', group: 'Inventory' }, + { key: 'isOldStock', label: 'Old Stock', group: 'Inventory' }, + { key: 'overstockedUnits', label: 'Overstock Qty', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'stockoutDays30d', label: 'Stockout Days (30d)', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'stockoutRate30d', label: 'Stockout Rate %', group: 'Inventory', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' }, + { key: 'avgStockUnits30d', label: 'Avg Stock Units (30d)', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'receivedQty30d', label: 'Received Qty (30d)', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'poCoverInDays', label: 'PO Cover (Days)', group: 'Inventory', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + + // Pricing & Costs { key: 'currentPrice', label: 'Price', group: 'Pricing', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, { key: 'currentRegularPrice', label: 'Regular Price', group: 'Pricing', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, { key: 'currentCostPrice', label: 'Cost', group: 'Pricing', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, { key: 'currentLandingCostPrice', label: 'Landing Cost', group: 'Pricing', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'currentStock', label: 'Stock', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, - { key: 'currentStockCost', label: 'Stock Cost', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'currentStockRetail', label: 'Stock Retail', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'currentStockGross', label: 'Stock Gross', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'onOrderQty', label: 'On Order', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, - { key: 'onOrderCost', label: 'On Order Cost', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'onOrderRetail', label: 'On Order Retail', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'earliestExpectedDate', label: 'Expected Date', group: 'Stock' }, + { key: 'currentStockCost', label: 'Stock Cost', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'currentStockRetail', label: 'Stock Retail', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'currentStockGross', label: 'Stock Gross', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'onOrderCost', label: 'On Order Cost', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'onOrderRetail', label: 'On Order Retail', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'overstockedCost', label: 'Overstock Cost', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'overstockedRetail', label: 'Overstock Retail', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'avgStockCost30d', label: 'Avg Stock Cost (30d)', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'avgStockRetail30d', label: 'Avg Stock Retail (30d)', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'avgStockGross30d', label: 'Avg Stock Gross (30d)', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'receivedCost30d', label: 'Received Cost (30d)', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'replenishmentCost', label: 'Replenishment Cost', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'replenishmentRetail', label: 'Replenishment Retail', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'replenishmentProfit', label: 'Replenishment Profit', group: 'Valuation', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - // Dates + // Dates & Timing { key: 'dateFirstReceived', label: 'First Received', group: 'Dates' }, { key: 'dateLastReceived', label: 'Last Received', group: 'Dates' }, { key: 'dateFirstSold', label: 'First Sold', group: 'Dates' }, { key: 'dateLastSold', label: 'Last Sold', group: 'Dates' }, { key: 'ageDays', label: 'Age (Days)', group: 'Dates', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'avgLeadTimeDays', label: 'Avg Lead Time', group: 'Dates', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'replenishDate', label: 'Replenish Date', group: 'Dates' }, + { key: 'planningPeriodDays', label: 'Planning Period (Days)', group: 'Dates', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, - // Product Status - { key: 'status', label: 'Status', group: 'Status' }, - - // Rolling Metrics + // Sales & Revenue + { key: 'salesVelocityDaily', label: 'Daily Velocity', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'yesterdaySales', label: 'Yesterday Sales', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, { key: 'sales7d', label: 'Sales (7d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, { key: 'revenue7d', label: 'Revenue (7d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, { key: 'sales14d', label: 'Sales (14d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, { key: 'revenue14d', label: 'Revenue (14d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, { key: 'sales30d', label: 'Sales (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, { key: 'revenue30d', label: 'Revenue (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'cogs30d', label: 'COGS (30d)', group: 'Financial', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'profit30d', label: 'Profit (30d)', group: 'Financial', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, { key: 'sales365d', label: 'Sales (365d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, { key: 'revenue365d', label: 'Revenue (365d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'avgSalesPerDay30d', label: 'Avg Sales/Day (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'avgSalesPerMonth30d', label: 'Avg Sales/Month (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'asp30d', label: 'ASP (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'acp30d', label: 'ACP (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'avgRos30d', label: 'Avg ROS (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'returnsUnits30d', label: 'Returns Units (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'returnsRevenue30d', label: 'Returns Revenue (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'discounts30d', label: 'Discounts (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'grossRevenue30d', label: 'Gross Revenue (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'grossRegularRevenue30d', label: 'Gross Regular Revenue (30d)', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'lifetimeSales', label: 'Lifetime Sales', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'lifetimeRevenue', label: 'Lifetime Revenue', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - // KPIs + // Financial Performance + { key: 'cogs30d', label: 'COGS (30d)', group: 'Financial', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'profit30d', label: 'Profit (30d)', group: 'Financial', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, { key: 'margin30d', label: 'Margin %', group: 'Financial', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' }, { key: 'markup30d', label: 'Markup %', group: 'Financial', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' }, { key: 'gmroi30d', label: 'GMROI', group: 'Financial', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, { key: 'stockturn30d', label: 'Stock Turn', group: 'Financial', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, { key: 'sellThrough30d', label: 'Sell Through %', group: 'Financial', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' }, - { key: 'avgLeadTimeDays', label: 'Avg Lead Time', group: 'Lead Time', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'returnRate30d', label: 'Return Rate %', group: 'Financial', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' }, + { key: 'discountRate30d', label: 'Discount Rate %', group: 'Financial', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' }, + { key: 'markdown30d', label: 'Markdown (30d)', group: 'Financial', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'markdownRate30d', label: 'Markdown Rate %', group: 'Financial', format: (v) => v === 0 ? '0%' : v ? `${v.toFixed(1)}%` : '-' }, - // Replenishment - { key: 'abcClass', label: 'ABC Class', group: 'Stock' }, - { key: 'salesVelocityDaily', label: 'Daily Velocity', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, - { key: 'stockCoverInDays', label: 'Stock Cover (Days)', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, - { key: 'sellsOutInDays', label: 'Sells Out In (Days)', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, - { key: 'overstockedUnits', label: 'Overstock Qty', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, - { key: 'overstockedCost', label: 'Overstock Cost', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'overstockedRetail', label: 'Overstock Retail', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - { key: 'isOldStock', label: 'Old Stock', group: 'Stock' }, - { key: 'yesterdaySales', label: 'Yesterday Sales', group: 'Sales', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + // Forecasting + { key: 'leadTimeForecastUnits', label: 'Lead Time Forecast Units', group: 'Forecasting', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'daysOfStockForecastUnits', label: 'Days of Stock Forecast Units', group: 'Forecasting', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'planningPeriodForecastUnits', label: 'Planning Period Forecast Units', group: 'Forecasting', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'leadTimeClosingStock', label: 'Lead Time Closing Stock', group: 'Forecasting', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'daysOfStockClosingStock', label: 'Days of Stock Closing Stock', group: 'Forecasting', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'replenishmentNeededRaw', label: 'Replenishment Needed Raw', group: 'Forecasting', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'forecastLostSalesUnits', label: 'Forecast Lost Sales Units', group: 'Forecasting', format: (v) => v === 0 ? '0' : v ? v.toFixed(1) : '-' }, + { key: 'forecastLostRevenue', label: 'Forecast Lost Revenue', group: 'Forecasting', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, - // Config & Replenishment columns - { key: 'configSafetyStock', label: 'Safety Stock', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, - { key: 'replenishmentUnits', label: 'Replenish Units', group: 'Stock', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + // First Period Performance + { key: 'first7DaysSales', label: 'First 7 Days Sales', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'first7DaysRevenue', label: 'First 7 Days Revenue', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'first30DaysSales', label: 'First 30 Days Sales', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'first30DaysRevenue', label: 'First 30 Days Revenue', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'first60DaysSales', label: 'First 60 Days Sales', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'first60DaysRevenue', label: 'First 60 Days Revenue', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, + { key: 'first90DaysSales', label: 'First 90 Days Sales', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toString() : '-' }, + { key: 'first90DaysRevenue', label: 'First 90 Days Revenue', group: 'First Period', format: (v) => v === 0 ? '0' : v ? v.toFixed(2) : '-' }, ]; // Define default columns for each view @@ -120,84 +190,105 @@ const VIEW_COLUMNS: Record = { 'imageUrl', 'title', 'brand', - 'vendor', - 'currentStock', 'status', - 'salesVelocityDaily', + 'currentStock', 'currentPrice', - 'currentRegularPrice', - 'sales7d', + 'salesVelocityDaily', 'sales30d', 'revenue30d', - 'currentStockCost', + 'profit30d', + 'stockCoverInDays', + 'currentStockCost' ], critical: [ + 'status', 'imageUrl', 'title', 'currentStock', 'configSafetyStock', - 'sales7d', - 'sales30d', 'replenishmentUnits', 'salesVelocityDaily', + 'sales7d', + 'sales30d', + 'onOrderQty', + 'earliestExpectedDate', 'vendor', 'dateLastReceived', - 'avgLeadTimeDays', + 'avgLeadTimeDays' ], reorder: [ + 'status', + 'imageUrl', + 'title', + 'currentStock', + 'configSafetyStock', + 'replenishmentUnits', + 'salesVelocityDaily', + 'sellsOutInDays', + 'currentCostPrice', + 'sales30d', + 'vendor', + 'avgLeadTimeDays', + 'dateLastReceived' + ], + overstocked: [ + 'status', + 'imageUrl', + 'title', + 'currentStock', + 'overstockedUnits', + 'sales7d', + 'sales30d', + 'salesVelocityDaily', + 'stockCoverInDays', + 'stockturn30d', + 'currentStockCost', + 'overstockedCost', + 'dateLastSold' + ], + 'at-risk': [ + 'status', + 'imageUrl', + 'title', + 'currentStock', + 'configSafetyStock', + 'salesVelocityDaily', + 'sales7d', + 'sales30d', + 'stockCoverInDays', + 'sellsOutInDays', + 'dateLastSold', + 'avgLeadTimeDays', + 'profit30d' + ], + new: [ + 'status', 'imageUrl', 'title', 'currentStock', 'salesVelocityDaily', 'sales7d', - 'sales30d', - 'replenishmentUnits', - 'vendor', - 'dateLastReceived', - 'avgLeadTimeDays', - ], - overstocked: [ - 'imageUrl', - 'title', - 'currentStock', - 'sales7d', - 'sales30d', - 'overstockedUnits', - 'stockCoverInDays', - 'currentStockCost', - 'stockturn30d', - ], - 'at-risk': [ - 'imageUrl', - 'title', - 'currentStock', - 'configSafetyStock', - 'sales7d', - 'sales30d', - 'stockCoverInDays', - 'dateLastSold', - 'avgLeadTimeDays', - ], - new: [ - 'imageUrl', - 'title', - 'currentStock', 'vendor', 'brand', 'currentPrice', - 'currentRegularPrice', + 'currentCostPrice', 'dateFirstReceived', + 'ageDays', + 'abcClass' ], healthy: [ + 'status', 'imageUrl', 'title', 'currentStock', - 'sales7d', + 'stockCoverInDays', + 'salesVelocityDaily', 'sales30d', 'revenue30d', - 'stockCoverInDays', 'profit30d', + 'margin30d', 'gmroi30d', + 'stockturn30d' ], }; @@ -327,7 +418,14 @@ export function Products() { } if (activeView && activeView !== 'all') { - params.append('stock_status', activeView === 'at-risk' ? 'At Risk' : activeView); + const stockStatus = activeView === 'at-risk' ? 'At Risk' : + activeView === 'reorder' ? 'Reorder Soon' : + activeView === 'overstocked' ? 'Overstock' : + activeView === 'new' ? 'New' : + activeView.charAt(0).toUpperCase() + activeView.slice(1); + + console.log(`View: ${activeView} → Stock Status: ${stockStatus}`); + params.append('stock_status', stockStatus); } // Transform filters to match API expectations @@ -534,7 +632,7 @@ export function Products() { e.preventDefault()} onPointerDownOutside={(e) => { // Only close if clicking outside the dropdown @@ -549,45 +647,47 @@ export function Products() { } }} > - Toggle columns +
+ Toggle columns + +
-
+
{Object.entries(columnsByGroup).map(([group, columns]) => ( -
- +
+ {group} - {columns.map((column) => ( - { - handleColumnVisibilityChange(column.key, checked); - }} - onSelect={(e) => { - // Prevent closing by stopping propagation - e.preventDefault(); - }} - > - {column.label} - - ))} +
+ {columns.map((column) => ( + { + handleColumnVisibilityChange(column.key, checked); + }} + onSelect={(e) => { + e.preventDefault(); + }} + > + {column.label} + + ))} +
))}
- - ); diff --git a/inventory/src/types/products.ts b/inventory/src/types/products.ts index 488b16a..9475b3f 100644 --- a/inventory/src/types/products.ts +++ b/inventory/src/types/products.ts @@ -80,7 +80,7 @@ export interface Product { } // Type for product status (used for calculated statuses) -export type ProductStatus = "Critical" | "Reorder Soon" | "Healthy" | "Overstock" | "At Risk" | "Unknown"; +export type ProductStatus = "Critical" | "Reorder Soon" | "Healthy" | "Overstock" | "At Risk" | "New" | "Unknown"; // Represents data returned by the /metrics endpoint (from product_metrics table) export interface ProductMetric { @@ -93,6 +93,30 @@ export interface ProductMetric { isVisible: boolean; isReplenishable: boolean; + // Additional Product Fields + barcode: string | null; + vendorReference: string | null; // Supplier # + notionsReference: string | null; // Notions # + preorderCount: number | null; + notionsInvCount: number | null; + harmonizedTariffCode: string | null; + line: string | null; + subline: string | null; + artist: string | null; + moq: number | null; + rating: number | null; + reviews: number | null; + weight: number | null; + dimensions: { + length: number | null; + width: number | null; + height: number | null; + } | null; + countryOfOrigin: string | null; + location: string | null; + baskets: number | null; // Number of times added to basket + notifies: number | null; // Number of stock notifications + // Current Status currentPrice: number | null; currentRegularPrice: number | null; @@ -201,7 +225,146 @@ export interface ProductFilterOptions { } // Type for keys used in sorting/filtering (matching frontend state/UI) -export type ProductMetricColumnKey = keyof Omit | 'pid' | 'status'; +export type ProductMetricColumnKey = + | 'pid' + | 'title' + | 'sku' + | 'barcode' + | 'brand' + | 'line' + | 'subline' + | 'artist' + | 'vendor' + | 'vendorReference' + | 'notionsReference' + | 'harmonizedTariffCode' + | 'countryOfOrigin' + | 'location' + | 'moq' + | 'weight' + | 'dimensions' + | 'rating' + | 'reviews' + | 'baskets' + | 'notifies' + | 'preorderCount' + | 'notionsInvCount' + | 'isVisible' + | 'isReplenishable' + | 'abcClass' + | 'status' + | 'dateCreated' + | 'currentStock' + | 'currentStockCost' + | 'currentStockRetail' + | 'currentStockGross' + | 'ageDays' + | 'replenishDate' + | 'planningPeriodDays' + | 'currentPrice' + | 'currentRegularPrice' + | 'currentCostPrice' + | 'currentLandingCostPrice' + | 'configSafetyStock' + | 'replenishmentUnits' + | 'stockCoverInDays' + | 'sellsOutInDays' + | 'onOrderQty' + | 'earliestExpectedDate' + | 'isOldStock' + | 'overstockedUnits' + | 'stockoutDays30d' + | 'stockoutRate30d' + | 'avgStockUnits30d' + | 'avgStockCost30d' + | 'avgStockRetail30d' + | 'avgStockGross30d' + | 'receivedQty30d' + | 'receivedCost30d' + | 'configLeadTime' + | 'configDaysOfStock' + | 'poCoverInDays' + | 'toOrderUnits' + | 'costPrice' + | 'valueAtCost' + | 'profit' + | 'margin' + | 'targetPrice' + | 'replenishmentCost' + | 'replenishmentRetail' + | 'replenishmentProfit' + | 'onOrderCost' + | 'onOrderRetail' + | 'overstockedCost' + | 'overstockedRetail' + | 'sales7d' + | 'revenue7d' + | 'sales14d' + | 'revenue14d' + | 'sales30d' + | 'units30d' + | 'revenue30d' + | 'sales365d' + | 'revenue365d' + | 'avgSalePrice30d' + | 'avgDailySales30d' + | 'avgDailyRevenue30d' + | 'stockturnRate30d' + | 'margin30d' + | 'cogs30d' + | 'profit30d' + | 'roas30d' + | 'adSpend30d' + | 'gmroi30d' + | 'first7DaysSales' + | 'first7DaysRevenue' + | 'first30DaysSales' + | 'first30DaysRevenue' + | 'first60DaysSales' + | 'first60DaysRevenue' + | 'first90DaysSales' + | 'first90DaysRevenue' + | 'lifetimeSales' + | 'lifetimeRevenue' + | 'lifetimeAvgPrice' + | 'forecastSalesUnits' + | 'forecastSalesValue' + | 'forecastStockCover' + | 'forecastedOutOfStockDate' + | 'salesVelocity' + | 'salesVelocityDaily' + | 'dateLastSold' + | 'yesterdaySales' + | 'avgSalesPerDay30d' + | 'avgSalesPerMonth30d' + | 'returnsUnits30d' + | 'returnsRevenue30d' + | 'discounts30d' + | 'grossRevenue30d' + | 'grossRegularRevenue30d' + | 'asp30d' + | 'acp30d' + | 'avgRos30d' + | 'markup30d' + | 'stockturn30d' + | 'sellThrough30d' + | 'returnRate30d' + | 'discountRate30d' + | 'markdown30d' + | 'markdownRate30d' + | 'leadTimeForecastUnits' + | 'daysOfStockForecastUnits' + | 'planningPeriodForecastUnits' + | 'leadTimeClosingStock' + | 'daysOfStockClosingStock' + | 'replenishmentNeededRaw' + | 'forecastLostSalesUnits' + | 'forecastLostRevenue' + | 'avgLeadTimeDays' + | 'dateLastReceived' + | 'dateFirstReceived' + | 'dateFirstSold' + | 'imageUrl'; // Mapping frontend keys to backend query param keys export const FRONTEND_TO_BACKEND_KEY_MAP: Record = { diff --git a/inventory/src/utils/productUtils.ts b/inventory/src/utils/productUtils.ts index 9ef6ac7..de872b9 100644 --- a/inventory/src/utils/productUtils.ts +++ b/inventory/src/utils/productUtils.ts @@ -81,6 +81,8 @@ export function getStatusBadge(status: ProductStatus): string { return '
Overstock
'; case 'At Risk': return '
At Risk
'; + case 'New': + return '
New
'; default: return '
Unknown
'; }