From 407731e17ded2ce8e361ded3a2adf24c3cd6c451 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 1 Apr 2026 12:26:39 -0400 Subject: [PATCH] Add product lines page, tweak audit log --- inventory-server/src/routes/linesAggregate.js | 381 ++++++++ inventory-server/src/server.js | 2 + inventory/src/App.tsx | 8 + .../src/components/layout/AppSidebar.tsx | 7 + .../src/components/products/StatusBadge.tsx | 6 +- .../src/components/settings/AuditLog.tsx | 75 +- inventory/src/pages/ProductLines.tsx | 863 ++++++++++++++++++ inventory/src/pages/Settings.tsx | 2 +- inventory/src/utils/lifecyclePhases.ts | 4 +- inventory/tsconfig.tsbuildinfo | 2 +- 10 files changed, 1342 insertions(+), 8 deletions(-) create mode 100644 inventory-server/src/routes/linesAggregate.js create mode 100644 inventory/src/pages/ProductLines.tsx diff --git a/inventory-server/src/routes/linesAggregate.js b/inventory-server/src/routes/linesAggregate.js new file mode 100644 index 0000000..85f9a95 --- /dev/null +++ b/inventory-server/src/routes/linesAggregate.js @@ -0,0 +1,381 @@ +const express = require('express'); +const router = express.Router(); +const { parseValue } = require('../utils/apiHelpers'); + +// --- Configuration & Helpers --- +const DEFAULT_PAGE_LIMIT = 50; +const MAX_PAGE_LIMIT = 200; + +// Base aggregation query that produces line-level metrics from product_metrics +// Filters to lines with stock or sales in the past year +const LINE_AGGREGATE_SQL = ` + SELECT + pm.brand, + pm.line, + COUNT(*) AS product_count, + COUNT(CASE WHEN pm.is_visible THEN 1 END) AS active_product_count, + COUNT(CASE WHEN pm.is_replenishable THEN 1 END) AS replenishable_product_count, + -- Stock + SUM(COALESCE(pm.current_stock, 0)) AS current_stock_units, + SUM(COALESCE(pm.current_stock_cost, 0)) AS current_stock_cost, + SUM(COALESCE(pm.current_stock_retail, 0)) AS current_stock_retail, + SUM(COALESCE(pm.on_order_qty, 0)) AS on_order_qty, + SUM(COALESCE(pm.on_order_cost, 0)) AS on_order_cost, + -- Sales periods + SUM(COALESCE(pm.sales_7d, 0)) AS sales_7d, + SUM(COALESCE(pm.revenue_7d, 0)) AS revenue_7d, + SUM(COALESCE(pm.sales_30d, 0)) AS sales_30d, + SUM(COALESCE(pm.revenue_30d, 0)) AS revenue_30d, + SUM(COALESCE(pm.profit_30d, 0)) AS profit_30d, + SUM(COALESCE(pm.cogs_30d, 0)) AS cogs_30d, + SUM(COALESCE(pm.sales_365d, 0)) AS sales_365d, + SUM(COALESCE(pm.revenue_365d, 0)) AS revenue_365d, + SUM(COALESCE(pm.lifetime_sales, 0)) AS lifetime_sales, + SUM(COALESCE(pm.lifetime_revenue, 0)) AS lifetime_revenue, + -- Weighted average margin (by revenue) + CASE WHEN SUM(pm.revenue_30d) > 0 + THEN SUM(pm.profit_30d) * 100.0 / SUM(pm.revenue_30d) + ELSE NULL + END AS avg_margin_30d, + -- Velocity & coverage + SUM(COALESCE(pm.sales_velocity_daily, 0)) AS total_velocity_daily, + AVG(CASE WHEN pm.current_stock > 0 AND pm.sales_velocity_daily > 0 + THEN pm.stock_cover_in_days ELSE NULL END) AS avg_stock_cover_days, + AVG(CASE WHEN pm.sells_out_in_days IS NOT NULL AND pm.current_stock > 0 + THEN pm.sells_out_in_days ELSE NULL END) AS avg_sells_out_in_days, + -- Stockouts & replenishment + SUM(COALESCE(pm.stockout_days_30d, 0)) AS total_stockout_days_30d, + SUM(COALESCE(pm.replenishment_units, 0)) AS total_replenishment_units, + SUM(COALESCE(pm.replenishment_cost, 0)) AS total_replenishment_cost, + -- Lifecycle distribution (counts per phase) + COUNT(CASE WHEN pm.lifecycle_phase = 'launch' THEN 1 END) AS phase_launch, + COUNT(CASE WHEN pm.lifecycle_phase = 'mature' THEN 1 END) AS phase_mature, + COUNT(CASE WHEN pm.lifecycle_phase = 'slow_mover' THEN 1 END) AS phase_slow_mover, + COUNT(CASE WHEN pm.lifecycle_phase = 'decay' THEN 1 END) AS phase_decay, + COUNT(CASE WHEN pm.lifecycle_phase = 'dormant' THEN 1 END) AS phase_dormant, + COUNT(CASE WHEN pm.lifecycle_phase = 'preorder' THEN 1 END) AS phase_preorder, + -- Status distribution (exclude preorder-phase products — they get their own segment) + COUNT(CASE WHEN pm.status = 'Healthy' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_healthy, + COUNT(CASE WHEN pm.status = 'Reorder Soon' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_reorder, + COUNT(CASE WHEN pm.status = 'Critical' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_critical, + COUNT(CASE WHEN pm.status = 'Overstock' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_overstock, + COUNT(CASE WHEN pm.status = 'At Risk' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_at_risk, + COUNT(CASE WHEN pm.status = 'New' AND COALESCE(pm.lifecycle_phase, '') != 'preorder' THEN 1 END) AS status_new, + -- ABC class distribution + COUNT(CASE WHEN pm.abc_class = 'A' THEN 1 END) AS abc_a_count, + COUNT(CASE WHEN pm.abc_class = 'B' THEN 1 END) AS abc_b_count, + COUNT(CASE WHEN pm.abc_class = 'C' THEN 1 END) AS abc_c_count, + -- Date range + MIN(pm.date_first_received) AS earliest_received, + MAX(pm.date_last_sold) AS latest_sale, + -- Growth (weighted by revenue) + CASE WHEN SUM(CASE WHEN pm.sales_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_30d ELSE 0 END) > 0 + THEN SUM(CASE WHEN pm.sales_growth_30d_vs_prev IS NOT NULL THEN pm.sales_growth_30d_vs_prev * pm.revenue_30d ELSE 0 END) + / SUM(CASE WHEN pm.sales_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_30d ELSE 0 END) + ELSE NULL + END AS sales_growth_30d_vs_prev, + CASE WHEN SUM(CASE WHEN pm.revenue_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_30d ELSE 0 END) > 0 + THEN SUM(CASE WHEN pm.revenue_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_growth_30d_vs_prev * pm.revenue_30d ELSE 0 END) + / SUM(CASE WHEN pm.revenue_growth_30d_vs_prev IS NOT NULL THEN pm.revenue_30d ELSE 0 END) + ELSE NULL + END AS revenue_growth_30d_vs_prev, + -- Derived line status + CASE + WHEN COUNT(CASE WHEN pm.lifecycle_phase = 'preorder' THEN 1 END) > COUNT(*) * 0.5 THEN 'preorder' + WHEN SUM(CASE WHEN pm.current_stock > 0 THEN 1 ELSE 0 END) = 0 THEN 'out_of_stock' + WHEN SUM(pm.sales_30d) > 0 THEN 'active' + WHEN SUM(pm.sales_365d) > 0 THEN 'slow' + ELSE 'dormant' + END AS line_status, + -- Dominant lifecycle phase + (ARRAY['launch','mature','slow_mover','decay','dormant','preorder'])[ + (SELECT i FROM unnest(ARRAY[ + COUNT(CASE WHEN pm.lifecycle_phase = 'launch' THEN 1 END), + COUNT(CASE WHEN pm.lifecycle_phase = 'mature' THEN 1 END), + COUNT(CASE WHEN pm.lifecycle_phase = 'slow_mover' THEN 1 END), + COUNT(CASE WHEN pm.lifecycle_phase = 'decay' THEN 1 END), + COUNT(CASE WHEN pm.lifecycle_phase = 'dormant' THEN 1 END), + COUNT(CASE WHEN pm.lifecycle_phase = 'preorder' THEN 1 END) + ]) WITH ORDINALITY AS t(cnt, i) + ORDER BY cnt DESC LIMIT 1) + ] AS dominant_lifecycle_phase + FROM product_metrics pm + WHERE pm.line IS NOT NULL AND pm.line != '' + AND (pm.current_stock > 0 OR pm.sales_365d > 0) + GROUP BY pm.brand, pm.line +`; + +// Column map for sorting/filtering on the aggregated result +const COLUMN_MAP = { + brand: { dbCol: 'agg.brand', type: 'string' }, + line: { dbCol: 'agg.line', type: 'string' }, + productCount: { dbCol: 'agg.product_count', type: 'number' }, + activeProductCount: { dbCol: 'agg.active_product_count', type: 'number' }, + currentStockUnits: { dbCol: 'agg.current_stock_units', type: 'number' }, + currentStockCost: { dbCol: 'agg.current_stock_cost', type: 'number' }, + currentStockRetail: { dbCol: 'agg.current_stock_retail', type: 'number' }, + onOrderQty: { dbCol: 'agg.on_order_qty', type: 'number' }, + sales7d: { dbCol: 'agg.sales_7d', type: 'number' }, + revenue7d: { dbCol: 'agg.revenue_7d', type: 'number' }, + sales30d: { dbCol: 'agg.sales_30d', type: 'number' }, + revenue30d: { dbCol: 'agg.revenue_30d', type: 'number' }, + profit30d: { dbCol: 'agg.profit_30d', type: 'number' }, + sales365d: { dbCol: 'agg.sales_365d', type: 'number' }, + revenue365d: { dbCol: 'agg.revenue_365d', type: 'number' }, + lifetimeSales: { dbCol: 'agg.lifetime_sales', type: 'number' }, + lifetimeRevenue: { dbCol: 'agg.lifetime_revenue', type: 'number' }, + avgMargin30d: { dbCol: 'agg.avg_margin_30d', type: 'number' }, + totalVelocityDaily: { dbCol: 'agg.total_velocity_daily', type: 'number' }, + avgStockCoverDays: { dbCol: 'agg.avg_stock_cover_days', type: 'number' }, + avgSellsOutInDays: { dbCol: 'agg.avg_sells_out_in_days', type: 'number' }, + salesGrowth30dVsPrev: { dbCol: 'agg.sales_growth_30d_vs_prev', type: 'number' }, + revenueGrowth30dVsPrev: { dbCol: 'agg.revenue_growth_30d_vs_prev', type: 'number' }, + lineStatus: { dbCol: 'agg.line_status', type: 'string' }, + dominantLifecyclePhase: { dbCol: 'agg.dominant_lifecycle_phase', type: 'string' }, + name: { dbCol: 'agg.line', type: 'string' }, +}; + +function getSafeColumnInfo(queryParamKey) { + return COLUMN_MAP[queryParamKey] || null; +} + +// --- Route Handlers --- + +// GET /lines-aggregate/filter-options +router.get('/filter-options', async (req, res) => { + const pool = req.app.locals.pool; + try { + const { rows: brandRows } = await pool.query(` + SELECT DISTINCT pm.brand + FROM product_metrics pm + WHERE pm.line IS NOT NULL AND pm.line != '' + AND (pm.current_stock > 0 OR pm.sales_365d > 0) + AND pm.brand IS NOT NULL AND pm.brand != '' + ORDER BY pm.brand + `); + + const statuses = ['active', 'preorder', 'slow', 'out_of_stock', 'dormant']; + const phases = ['launch', 'mature', 'slow_mover', 'decay', 'dormant', 'preorder']; + + res.json({ + brands: brandRows.map(r => r.brand), + statuses, + phases, + }); + } catch (error) { + console.error('Error fetching line filter options:', error); + res.status(500).json({ error: 'Failed to fetch filter options' }); + } +}); + +// GET /lines-aggregate/stats +router.get('/stats', async (req, res) => { + const pool = req.app.locals.pool; + try { + const { rows: [stats] } = await pool.query(` + SELECT + COUNT(*) AS total_lines, + COUNT(DISTINCT brand) AS brand_count, + COUNT(CASE WHEN line_status = 'active' THEN 1 END) AS active_lines, + COUNT(CASE WHEN line_status = 'out_of_stock' THEN 1 END) AS oos_lines, + COUNT(CASE WHEN line_status = 'active' AND replenishable_product_count > 0 AND status_reorder > replenishable_product_count * 0.2 THEN 1 END) AS lines_needing_restock, + ROUND(AVG(product_count), 1) AS avg_products_per_line, + SUM(revenue_30d) AS total_revenue_30d, + CASE WHEN SUM(revenue_30d) > 0 + THEN SUM(profit_30d) * 100.0 / SUM(revenue_30d) + ELSE 0 + END AS overall_avg_margin + FROM (${LINE_AGGREGATE_SQL}) agg + `); + + res.json({ + totalLines: parseInt(stats?.total_lines || 0), + brandCount: parseInt(stats?.brand_count || 0), + activeLines: parseInt(stats?.active_lines || 0), + oosLines: parseInt(stats?.oos_lines || 0), + linesNeedingRestock: parseInt(stats?.lines_needing_restock || 0), + avgProductsPerLine: parseFloat(stats?.avg_products_per_line || 0), + totalRevenue30d: parseFloat(stats?.total_revenue_30d || 0), + avgMargin: parseFloat(stats?.overall_avg_margin || 0), + }); + } catch (error) { + console.error('Error fetching line stats:', error); + res.status(500).json({ error: 'Failed to fetch line stats.' }); + } +}); + +// GET /lines-aggregate/ (List product lines) +router.get('/', async (req, res) => { + const pool = req.app.locals.pool; + try { + // --- Pagination --- + let page = parseInt(req.query.page, 10) || 1; + let limit = parseInt(req.query.limit, 10) || DEFAULT_PAGE_LIMIT; + limit = Math.min(limit, MAX_PAGE_LIMIT); + const offset = (page - 1) * limit; + + // --- Sorting --- + // Sort presets for curated multi-column sorts + const SORT_PRESETS = { + 'by-brand': 'ORDER BY agg.brand ASC, agg.revenue_30d DESC NULLS LAST', + 'top-revenue': 'ORDER BY agg.revenue_30d DESC NULLS LAST', + 'needs-restock': 'ORDER BY agg.total_replenishment_units DESC NULLS LAST, agg.avg_stock_cover_days ASC NULLS FIRST', + 'fastest-growing': 'ORDER BY agg.sales_growth_30d_vs_prev DESC NULLS LAST', + 'newest': 'ORDER BY agg.earliest_received DESC NULLS LAST', + 'low-stock': 'ORDER BY CASE WHEN agg.sales_30d > 0 THEN 0 ELSE 1 END, agg.avg_stock_cover_days ASC NULLS LAST', + }; + + let sortClause; + const sortQueryKey = req.query.sort || 'by-brand'; + if (SORT_PRESETS[sortQueryKey]) { + sortClause = SORT_PRESETS[sortQueryKey]; + } else { + const sortColumnInfo = getSafeColumnInfo(sortQueryKey); + const sortColumn = sortColumnInfo ? sortColumnInfo.dbCol : 'agg.brand'; + const sortDirection = req.query.order?.toLowerCase() === 'desc' ? 'DESC' : 'ASC'; + const nullsOrder = (sortDirection === 'ASC' ? 'NULLS FIRST' : 'NULLS LAST'); + sortClause = `ORDER BY ${sortColumn} ${sortDirection} ${nullsOrder}`; + } + + // --- Filtering --- + const conditions = []; + const params = []; + let paramCounter = 1; + + for (const key in req.query) { + if (['page', 'limit', 'sort', 'order', 'preset'].includes(key)) continue; + + let filterKey = key; + let operator = '='; + const value = req.query[key]; + + const operatorMatch = key.match(/^(.*)_(eq|ne|gt|gte|lt|lte|like|ilike|between|in)$/); + if (operatorMatch) { + filterKey = operatorMatch[1]; + operator = operatorMatch[2]; + } + + const columnInfo = getSafeColumnInfo(filterKey); + if (columnInfo) { + const dbColumn = columnInfo.dbCol; + const valueType = columnInfo.type; + try { + let conditionFragment = ''; + let needsParam = true; + switch (operator.toLowerCase()) { + case 'eq': operator = '='; break; + case 'ne': operator = '<>'; break; + case 'gt': operator = '>'; break; + case 'gte': operator = '>='; break; + case 'lt': operator = '<'; break; + case 'lte': operator = '<='; break; + case 'like': operator = 'LIKE'; needsParam = false; params.push(`%${parseValue(value, valueType)}%`); break; + case 'ilike': operator = 'ILIKE'; needsParam = false; params.push(`%${parseValue(value, valueType)}%`); break; + case 'between': + const [val1, val2] = String(value).split(','); + if (val1 !== undefined && val2 !== undefined) { + conditionFragment = `${dbColumn} BETWEEN $${paramCounter++} AND $${paramCounter++}`; + params.push(parseValue(val1, valueType), parseValue(val2, valueType)); + needsParam = false; + } else continue; + break; + case 'in': + const inValues = String(value).split(','); + if (inValues.length > 0) { + const placeholders = inValues.map(() => `$${paramCounter++}`).join(', '); + conditionFragment = `${dbColumn} IN (${placeholders})`; + params.push(...inValues.map(v => parseValue(v, valueType))); + needsParam = false; + } else continue; + break; + default: operator = '='; break; + } + + if (needsParam) { + conditionFragment = `${dbColumn} ${operator} $${paramCounter++}`; + params.push(parseValue(value, valueType)); + } else if (!conditionFragment) { + conditionFragment = `${dbColumn} ${operator} $${paramCounter++}`; + } + + if (conditionFragment) { + conditions.push(`(${conditionFragment})`); + } + } catch (parseError) { + console.warn(`Skipping filter for key "${key}" due to parsing error: ${parseError.message}`); + } + } + } + + // --- Execute Queries --- + const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; + + const baseSql = `FROM (${LINE_AGGREGATE_SQL}) agg ${whereClause}`; + + const countSql = `SELECT COUNT(*) AS total ${baseSql}`; + const dataSql = `SELECT agg.* ${baseSql} ${sortClause} LIMIT $${paramCounter} OFFSET $${paramCounter + 1}`; + const dataParams = [...params, limit, offset]; + + const [countResult, dataResult] = await Promise.all([ + pool.query(countSql, params), + pool.query(dataSql, dataParams) + ]); + + const total = parseInt(countResult.rows[0].total, 10); + const lines = dataResult.rows.map(row => { + const transformedRow = { ...row }; + for (const key in row) { + if (row[key] === null || row[key] === undefined) continue; + // snake_case -> camelCase + let camelKey = key.replace(/_(\d+[a-z])/g, '$1'); + camelKey = camelKey.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); + if (camelKey !== key) { + transformedRow[camelKey] = row[key]; + } + } + return transformedRow; + }); + + res.json({ + lines, + pagination: { total, pages: Math.ceil(total / limit), currentPage: page, limit }, + }); + } catch (error) { + console.error('Error fetching line metrics list:', error); + res.status(500).json({ error: 'Failed to fetch line metrics.' }); + } +}); + +// GET /lines-aggregate/:brand/:line/products (All products in a line) +router.get('/:brand/:line/products', async (req, res) => { + const pool = req.app.locals.pool; + const { brand, line } = req.params; + + try { + const { rows } = await pool.query(` + SELECT + pm.pid, pm.title, pm.sku, + pm.current_stock, pm.current_stock_retail, + pm.sales_30d, pm.revenue_30d, pm.profit_30d, pm.margin_30d, + pm.sales_365d, pm.revenue_365d, + pm.sales_velocity_daily, pm.stock_cover_in_days, + pm.lifecycle_phase, pm.status, pm.abc_class, + pm.on_order_qty, pm.replenishment_units, + pm.date_first_received, pm.date_last_sold + FROM product_metrics pm + WHERE pm.brand = $1 AND pm.line = $2 + AND (pm.current_stock > 0 OR pm.sales_365d > 0) + ORDER BY pm.revenue_30d DESC NULLS LAST + `, [brand, line]); + + res.json({ + totalProducts: rows.length, + products: rows, + }); + } catch (error) { + console.error('Error fetching line products:', error); + res.status(500).json({ error: 'Failed to fetch line products.' }); + } +}); + +module.exports = router; diff --git a/inventory-server/src/server.js b/inventory-server/src/server.js index 933a91e..eaff3ea 100644 --- a/inventory-server/src/server.js +++ b/inventory-server/src/server.js @@ -27,6 +27,7 @@ const importSessionsRouter = require('./routes/import-sessions'); const importAuditLogRouter = require('./routes/import-audit-log'); const productEditorAuditLogRouter = require('./routes/product-editor-audit-log'); const newsletterRouter = require('./routes/newsletter'); +const linesAggregateRouter = require('./routes/linesAggregate'); // Get the absolute path to the .env file const envPath = '/var/www/html/inventory/.env'; @@ -138,6 +139,7 @@ async function startServer() { app.use('/api/import-audit-log', importAuditLogRouter); app.use('/api/product-editor-audit-log', productEditorAuditLogRouter); app.use('/api/newsletter', newsletterRouter); + app.use('/api/lines-aggregate', linesAggregateRouter); // Basic health check route app.get('/health', (req, res) => { diff --git a/inventory/src/App.tsx b/inventory/src/App.tsx index 62d82e8..4dc3f7a 100644 --- a/inventory/src/App.tsx +++ b/inventory/src/App.tsx @@ -26,6 +26,7 @@ const HtsLookup = lazy(() => import('./pages/HtsLookup')); const Vendors = lazy(() => import('./pages/Vendors')); const Categories = lazy(() => import('./pages/Categories')); const Brands = lazy(() => import('./pages/Brands')); +const ProductLines = lazy(() => import('./pages/ProductLines')); const PurchaseOrders = lazy(() => import('./pages/PurchaseOrders')); const BlackFridayDashboard = lazy(() => import('./pages/BlackFridayDashboard')); const Newsletter = lazy(() => import('./pages/Newsletter')); @@ -149,6 +150,13 @@ function App() { } /> + + }> + + + + } /> }> diff --git a/inventory/src/components/layout/AppSidebar.tsx b/inventory/src/components/layout/AppSidebar.tsx index 57283d0..a09036a 100644 --- a/inventory/src/components/layout/AppSidebar.tsx +++ b/inventory/src/components/layout/AppSidebar.tsx @@ -16,6 +16,7 @@ import { FilePenLine, PenLine, Mail, + Layers, } from "lucide-react"; import { IconCrystalBall } from "@tabler/icons-react"; import { @@ -78,6 +79,12 @@ const inventoryItems = [ url: "/brands", permission: "access:brands" }, + { + title: "Product Lines", + icon: Layers, + url: "/product-lines", + permission: "access:product_lines" + }, { title: "Vendors", icon: Truck, diff --git a/inventory/src/components/products/StatusBadge.tsx b/inventory/src/components/products/StatusBadge.tsx index c48d2e5..5326cc8 100644 --- a/inventory/src/components/products/StatusBadge.tsx +++ b/inventory/src/components/products/StatusBadge.tsx @@ -2,11 +2,11 @@ import { cn } from "@/lib/utils"; const STATUS_STYLES: Record = { 'Critical': 'bg-red-600 text-white border-transparent', - 'Reorder Soon': 'bg-yellow-500 text-black border-secondary', + 'Reorder Soon': 'bg-yellow-400 text-black border-transparent', 'Healthy': 'bg-green-600 text-white border-transparent', - 'Overstock': 'bg-blue-600 text-white border-secondary', + 'Overstock': 'bg-teal-700 text-white border-transparent', 'At Risk': 'border-orange-500 text-orange-600', - 'New': 'bg-purple-600 text-white border-transparent', + 'New': 'bg-green-600 text-white border-transparent', 'Unknown': 'bg-muted text-muted-foreground border-transparent', }; diff --git a/inventory/src/components/settings/AuditLog.tsx b/inventory/src/components/settings/AuditLog.tsx index f50b24e..a892614 100644 --- a/inventory/src/components/settings/AuditLog.tsx +++ b/inventory/src/components/settings/AuditLog.tsx @@ -484,7 +484,7 @@ function DetailView({ detail, logType }: { detail: AuditDetail; logType: LogType )} - + {detail.response_payload != null && ( @@ -493,6 +493,36 @@ function DetailView({ detail, logType }: { detail: AuditDetail; logType: LogType ); } +/** Keys in request_payload that were NOT sent to the API — stored for audit context only */ +const CONTEXT_ONLY_KEYS = new Set(["previous_values", "previous_ids"]); + +function RequestPayloadSection({ payload }: { payload: unknown }) { + const parsed = typeof payload === "string" ? (() => { try { return JSON.parse(payload); } catch { return payload; } })() : payload; + + // Extract context-only keys from the payload + let apiPayload = parsed; + let contextData: Record | null = null; + + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + const obj = parsed as Record; + const contextEntries = Object.entries(obj).filter(([k]) => CONTEXT_ONLY_KEYS.has(k)); + if (contextEntries.length > 0) { + const remaining = Object.fromEntries(Object.entries(obj).filter(([k]) => !CONTEXT_ONLY_KEYS.has(k))); + apiPayload = remaining; + contextData = Object.fromEntries(contextEntries); + } + } + + return ( + <> + + {contextData && ( + + )} + + ); +} + // --- Formatted JSON viewer --- /** Unwrap double-encoded JSON strings from JSONB columns */ @@ -689,6 +719,18 @@ function JsonArray({ items, depth }: { items: unknown[]; depth: number }) { ); } + // Arrays of objects — render each item as a collapsible block + const hasObjects = items.some((v) => v !== null && typeof v === "object"); + if (hasObjects) { + return ( +
+ {items.map((item, i) => ( + + ))} +
+ ); + } + return (
{items.map((item, i) => ( @@ -700,3 +742,34 @@ function JsonArray({ items, depth }: { items: unknown[]; depth: number }) {
); } + +function JsonArrayItem({ index, value, depth }: { index: number; value: unknown; depth: number }) { + const [open, setOpen] = useState(true); + const isObject = value !== null && typeof value === "object"; + + if (!isObject) { + return ( +
+ [{index}] + +
+ ); + } + + return ( +
+ + {open && ( +
+ +
+ )} +
+ ); +} diff --git a/inventory/src/pages/ProductLines.tsx b/inventory/src/pages/ProductLines.tsx new file mode 100644 index 0000000..9fb4db6 --- /dev/null +++ b/inventory/src/pages/ProductLines.tsx @@ -0,0 +1,863 @@ +import { useState, useMemo, useCallback } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { motion, AnimatePresence } from "framer-motion"; +import { Input } from "@/components/ui/input"; +import config from "../config"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Badge } from "@/components/ui/badge"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import { PHASE_CONFIG } from "@/utils/lifecyclePhases"; + +// --- Types --- + +interface LineMetric { + brand: string; + line: string; + product_count: number; + active_product_count: number; + replenishable_product_count: number; + current_stock_units: number; + current_stock_cost: number; + current_stock_retail: number; + on_order_qty: number; + on_order_cost: number; + sales_7d: number; + revenue_7d: number; + sales_30d: number; + revenue_30d: number; + profit_30d: number; + cogs_30d: number; + sales_365d: number; + revenue_365d: number; + lifetime_sales: number; + lifetime_revenue: number; + avg_margin_30d: number | null; + total_velocity_daily: number; + avg_stock_cover_days: number | null; + avg_sells_out_in_days: number | null; + total_stockout_days_30d: number; + total_replenishment_units: number; + total_replenishment_cost: number; + phase_launch: number; + phase_mature: number; + phase_slow_mover: number; + phase_decay: number; + phase_dormant: number; + phase_preorder: number; + status_healthy: number; + status_reorder: number; + status_critical: number; + status_overstock: number; + status_at_risk: number; + status_new: number; + abc_a_count: number; + abc_b_count: number; + abc_c_count: number; + earliest_received: string | null; + latest_sale: string | null; + sales_growth_30d_vs_prev: number | null; + revenue_growth_30d_vs_prev: number | null; + line_status: string; + dominant_lifecycle_phase: string | null; +} + +interface LineResponse { + lines: LineMetric[]; + pagination: { total: number; pages: number; currentPage: number; limit: number }; +} + +interface LineFilterOptions { + brands: string[]; + statuses: string[]; + phases: string[]; +} + +interface LineStats { + totalLines: number; + brandCount: number; + activeLines: number; + oosLines: number; + linesNeedingRestock: number; + avgProductsPerLine: number; + totalRevenue30d: number; + avgMargin: number; +} + +interface LineProduct { + pid: number; + title: string; + sku: string; + current_stock: number; + sales_30d: number; + revenue_30d: number; + profit_30d: number; + margin_30d: number | null; + sales_365d: number; + lifecycle_phase: string | null; + status: string | null; + abc_class: string | null; + on_order_qty: number; + replenishment_units: number; +} + +interface LineProductsResponse { + totalProducts: number; + products: LineProduct[]; +} + +// Column-level sorts (for clicking table headers) +type SortableColumn = + | 'brand' | 'line' | 'productCount' | 'activeProductCount' + | 'currentStockUnits' | 'currentStockCost' | 'revenue30d' | 'profit30d' + | 'avgMargin30d' | 'sales30d' | 'sales365d' | 'revenue365d' + | 'totalVelocityDaily' | 'avgStockCoverDays' + | 'salesGrowth30dVsPrev' | 'revenueGrowth30dVsPrev' + | 'lineStatus' | 'dominantLifecyclePhase'; + +// Curated multi-column sort presets (backend handles the ORDER BY) +type SortPreset = 'by-brand' | 'top-revenue' | 'needs-restock' | 'fastest-growing' | 'newest' | 'low-stock'; + +const SORT_PRESETS: { value: SortPreset; label: string }[] = [ + { value: 'by-brand', label: 'By Brand' }, + { value: 'top-revenue', label: 'Top Revenue' }, + { value: 'needs-restock', label: 'Needs Restock' }, + { value: 'fastest-growing', label: 'Fastest Growing' }, + { value: 'newest', label: 'Newest Lines' }, + { value: 'low-stock', label: 'Low Stock Cover' }, +]; + +interface Filters { + search: string; + brand: string; + status: string; + phase: string; +} + +// --- Formatting helpers --- + +const formatCurrency = (value: number | string | null | undefined, digits = 0): string => { + if (value == null) return 'N/A'; + const num = typeof value === 'string' ? parseFloat(value) : value; + if (isNaN(num)) return 'N/A'; + return new Intl.NumberFormat('en-US', { + style: 'currency', currency: 'USD', + minimumFractionDigits: digits, maximumFractionDigits: digits + }).format(num); +}; + +const formatNumber = (value: number | string | null | undefined, digits = 0): string => { + if (value == null) return 'N/A'; + const num = typeof value === 'string' ? parseFloat(value) : value; + if (isNaN(num)) return 'N/A'; + return num.toLocaleString(undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits }); +}; + +const formatPercentage = (value: number | string | null | undefined, digits = 1): string => { + if (value == null) return 'N/A'; + const num = typeof value === 'string' ? parseFloat(value) : value; + if (isNaN(num)) return 'N/A'; + return `${num.toFixed(digits)}%`; +}; + +const formatGrowth = (value: number | string | null | undefined, digits = 1) => { + if (value == null) return --; + const num = typeof value === 'string' ? parseFloat(value) : value; + if (isNaN(num)) return --; + const formatted = `${num >= 0 ? '+' : ''}${num.toFixed(digits)}%`; + return = 0 ? 'text-green-600' : 'text-red-600'}>{formatted}; +}; + +const ITEMS_PER_PAGE = 50; + +// --- Status & Phase badges --- + +const statusConfig: Record = { + active: { label: 'Active', variant: 'default' }, + preorder: { label: 'Pre-order', variant: 'secondary' }, + slow: { label: 'Slow', variant: 'secondary' }, + out_of_stock: { label: 'Out of Stock', variant: 'destructive' }, + dormant: { label: 'Dormant', variant: 'outline' }, +}; + +const getPhaseLabel = (phase: string): string => + PHASE_CONFIG[phase]?.label || phase; + +const getPhaseColor = (phase: string): string => + PHASE_CONFIG[phase]?.color || '#94A3B8'; + +// Mini bar showing lifecycle phase distribution using shared PHASE_CONFIG colors +function PhaseBar({ line }: { line: LineMetric }) { + const phases = [ + { key: 'preorder', count: Number(line.phase_preorder) }, + { key: 'launch', count: Number(line.phase_launch) }, + { key: 'decay', count: Number(line.phase_decay) }, + { key: 'mature', count: Number(line.phase_mature) }, + { key: 'slow_mover', count: Number(line.phase_slow_mover) }, + { key: 'dormant', count: Number(line.phase_dormant) }, + ]; + const total = phases.reduce((s, p) => s + p.count, 0); + if (total === 0) return --; + + const activePhases = phases.filter(p => p.count > 0); + + return ( + + + +
+
+ {activePhases.map(p => ( +
+ ))} +
+ + {line.dominant_lifecycle_phase ? getPhaseLabel(line.dominant_lifecycle_phase) : '--'} + +
+ + +
+ {activePhases.map(p => ( +
+
+ {getPhaseLabel(p.key)} + {p.count} +
+ ))} +
+ + + + ); +} + +// Stock health status bar — ordered by severity for visual scanning +const STATUS_BAR_CONFIG: Record = { + critical: { label: 'Critical', color: '#ef4444' }, + reorder: { label: 'Reorder Soon', color: '#fbbf24' }, + healthy: { label: 'Healthy', color: '#22c55e' }, + at_risk: { label: 'At Risk', color: '#f97316' }, + overstock: { label: 'Overstock', color: '#0f766e' }, + preorder: { label: 'Pre-order', color: '#3b82f6' }, +}; + +// Determines display order — array position = render order in bar +const STATUS_ORDER: string[] = ['critical', 'reorder', 'healthy', 'at_risk', 'overstock', 'preorder']; + +// Map DB status strings to STATUS_BAR_CONFIG keys +const STATUS_KEY_MAP: Record = { + 'Critical': 'critical', + 'Reorder Soon': 'reorder', + 'Healthy': 'healthy', + 'Overstock': 'overstock', + 'At Risk': 'at_risk', + 'New': 'healthy', +}; + +function StatusBar({ line }: { line: LineMetric }) { + const statusCounts: Record = { + critical: Number(line.status_critical), + reorder: Number(line.status_reorder), + healthy: Number(line.status_healthy) + Number(line.status_new), + at_risk: Number(line.status_at_risk), + overstock: Number(line.status_overstock), + preorder: Number(line.phase_preorder), + }; + + const statuses = STATUS_ORDER.map(key => ({ key, count: statusCounts[key] })); + const total = statuses.reduce((s, st) => s + st.count, 0); + if (total === 0) return --; + + const active = statuses.filter(s => s.count > 0); + const stockCover = line.avg_stock_cover_days != null ? Number(line.avg_stock_cover_days) : null; + const onOrder = Number(line.on_order_qty); + + // Summary label: pick the most notable non-healthy/non-preorder status, or "Good" + const neutralCount = statusCounts.healthy + statusCounts.preorder; + const healthyPct = neutralCount / total; + let summaryLabel = 'Good'; + if (Number(line.current_stock_units) === 0 && onOrder === 0) { + summaryLabel = 'No Stock'; + } else if (stockCover !== null && stockCover < 14) { + summaryLabel = `${Math.round(stockCover)}d cover`; + } else if (healthyPct < 0.8 && active.length > 1) { + // Find the largest problem segment + const biggest = active + .filter(s => s.key !== 'healthy' && s.key !== 'preorder') + .sort((a, b) => b.count - a.count)[0]; + if (biggest) summaryLabel = `${biggest.count} ${STATUS_BAR_CONFIG[biggest.key].label}`; + } + + return ( + + + +
+
+ {active.map(s => ( +
+ ))} +
+ {summaryLabel} +
+ + +
+ {active.map(s => ( +
+
+ {STATUS_BAR_CONFIG[s.key].label} + {s.count} +
+ ))} + {stockCover !== null && ( +
+ {Math.round(stockCover)}d avg cover +
+ )} + {onOrder > 0 && ( +
+ {formatNumber(onOrder)} on order +
+ )} +
+ + + + ); +} + +// Expandable product detail row +function LineProductDetail({ brand, line }: { brand: string; line: string }) { + const { data, isLoading } = useQuery({ + queryKey: ['lineProducts', brand, line], + queryFn: async () => { + const res = await fetch( + `${config.apiUrl}/lines-aggregate/${encodeURIComponent(brand)}/${encodeURIComponent(line)}/products`, + { credentials: 'include' } + ); + if (!res.ok) throw new Error('Failed to fetch'); + return res.json(); + }, + }); + + if (isLoading) { + return ( + + +
+ + +
+
+
+ ); + } + + if (!data || data.products.length === 0) return null; + + return ( + + +

{data.totalProducts} products in this line — sorted by revenue

+
+ + + + Product + Stock + Sales (30d) + Revenue (30d) + Margin + Phase + Status + + + + {data.products.map((p) => ( + + + + + + {p.title} + + +

{p.title}

+
+
+
+
+ {formatNumber(p.current_stock)} + {formatNumber(p.sales_30d)} + {formatCurrency(p.revenue_30d)} + {formatPercentage(p.margin_30d)} + + {p.lifecycle_phase ? ( + + {getPhaseLabel(p.lifecycle_phase)} + + ) : '--'} + + + {p.status ? ( + + {p.status} + + ) : '--'} + +
+ ))} +
+
+
+
+
+ ); +} + +// --- Main Page Component --- + +export function ProductLines() { + const [page, setPage] = useState(1); + const [limit] = useState(ITEMS_PER_PAGE); + // Sort can be either a preset (compound sort) or a single column + const [sortPreset, setSortPreset] = useState('newest'); + const [sortColumn, setSortColumn] = useState("brand"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const [expandedLine, setExpandedLine] = useState(null); + const [filters, setFilters] = useState({ + search: "", + brand: "all", + status: "all", + phase: "all", + }); + + // The active sort key sent to the backend + const activeSort = sortPreset || sortColumn; + + // --- Data Fetching --- + + const queryParams = useMemo(() => { + const params = new URLSearchParams(); + params.set('page', page.toString()); + params.set('limit', limit.toString()); + params.set('sort', activeSort); + if (!sortPreset) { + params.set('order', sortDirection); + } + + if (filters.search) { + params.set('line_ilike', filters.search); + } + if (filters.brand !== 'all') { + params.set('brand', filters.brand); + } + if (filters.status !== 'all') { + params.set('lineStatus', filters.status); + } + if (filters.phase !== 'all') { + params.set('dominantLifecyclePhase', filters.phase); + } + return params; + }, [page, limit, activeSort, sortPreset, sortDirection, filters]); + + const { data: listData, isLoading: isLoadingList, error: listError } = useQuery({ + queryKey: ['productLines', queryParams.toString()], + queryFn: async () => { + const res = await fetch(`${config.apiUrl}/lines-aggregate?${queryParams.toString()}`, { + credentials: 'include' + }); + if (!res.ok) throw new Error(`Network response was not ok (${res.status})`); + return res.json(); + }, + placeholderData: (prev) => prev, + }); + + const { data: statsData, isLoading: isLoadingStats } = useQuery({ + queryKey: ['productLineStats'], + queryFn: async () => { + const res = await fetch(`${config.apiUrl}/lines-aggregate/stats`, { credentials: 'include' }); + if (!res.ok) throw new Error("Failed to fetch stats"); + return res.json(); + }, + }); + + const { data: filterOptions } = useQuery({ + queryKey: ['productLineFilterOptions'], + queryFn: async () => { + const res = await fetch(`${config.apiUrl}/lines-aggregate/filter-options`, { credentials: 'include' }); + if (!res.ok) throw new Error("Failed to fetch filter options"); + return res.json(); + }, + }); + + // --- Handlers --- + + const handleSort = useCallback((column: SortableColumn) => { + setSortPreset(null); // Clear preset when manually sorting by column + setSortDirection(prev => (sortColumn === column && prev === "desc" ? "asc" : "desc")); + setSortColumn(column); + setPage(1); + }, [sortColumn]); + + const handlePresetChange = useCallback((preset: string) => { + if (preset === 'custom') { + // Switching to the "custom" placeholder does nothing; user clicks column headers + return; + } + setSortPreset(preset as SortPreset); + setPage(1); + }, []); + + const handleFilterChange = useCallback((name: keyof Filters, value: string) => { + setFilters(prev => ({ ...prev, [name]: value })); + setPage(1); + }, []); + + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= (listData?.pagination.pages ?? 1)) { + setPage(newPage); + } + }; + + const toggleExpand = (brand: string, line: string) => { + const key = `${brand}|||${line}`; + setExpandedLine(prev => prev === key ? null : key); + }; + + // --- Derived --- + const lines = listData?.lines ?? []; + const pagination = listData?.pagination; + const totalPages = pagination?.pages ?? 0; + + const sortIndicator = (col: SortableColumn) => { + if (sortPreset || sortColumn !== col) return null; + return {sortDirection === 'asc' ? '\u2191' : '\u2193'}; + }; + + // --- Render --- + return ( + + {/* Header */} + +
+

Product Lines

+
+
+ + {/* Stats Cards */} + + + + Product Lines + + + {isLoadingStats ? : ( +
{formatNumber(statsData?.totalLines)}
+ )} +

+ {isLoadingStats ? : + `${formatNumber(statsData?.activeLines)} active across ${formatNumber(statsData?.brandCount)} brands`} +

+
+
+ + + Avg Products / Line + + + {isLoadingStats ? : ( +
{formatNumber(statsData?.avgProductsPerLine, 1)}
+ )} +

Average line depth

+
+
+ + + Avg Margin (30d) + + + {isLoadingStats ? : ( +
{formatPercentage(statsData?.avgMargin)}
+ )} +

+ {isLoadingStats ? : + `${formatCurrency(statsData?.totalRevenue30d)} total revenue`} +

+
+
+ { setSortPreset('needs-restock'); setPage(1); }} + > + + Lines Needing Restock + + + {isLoadingStats ? : ( +
{formatNumber(statsData?.linesNeedingRestock)}
+ )} +

+ {isLoadingStats ? : + `${formatNumber(statsData?.oosLines)} fully out of stock`} +

+
+
+
+ + {/* Filters & Sort */} +
+ handleFilterChange('search', e.target.value)} + className="w-full sm:w-[250px]" + /> + + + +
+ +
+
+ + {/* Data Table */} +
+ + + + + handleSort("brand")} className="cursor-pointer"> + Brand{sortIndicator("brand")} + + handleSort("line")} className="cursor-pointer"> + Line{sortIndicator("line")} + + handleSort("productCount")} className="cursor-pointer text-right"> + Products{sortIndicator("productCount")} + + handleSort("currentStockUnits")} className="cursor-pointer text-right"> + Stock{sortIndicator("currentStockUnits")} + + handleSort("revenue30d")} className="cursor-pointer text-right"> + Revenue (30d){sortIndicator("revenue30d")} + + handleSort("profit30d")} className="cursor-pointer text-right"> + Profit (30d){sortIndicator("profit30d")} + + handleSort("avgMargin30d")} className="cursor-pointer text-right"> + Margin{sortIndicator("avgMargin30d")} + + handleSort("totalVelocityDaily")} className="cursor-pointer text-right whitespace-nowrap"> + Velocity/d{sortIndicator("totalVelocityDaily")} + + handleSort("salesGrowth30dVsPrev")} className="cursor-pointer text-right"> + Growth{sortIndicator("salesGrowth30dVsPrev")} + + Lifecycle + Product Status + handleSort("lineStatus")} className="cursor-pointer text-right"> + Status{sortIndicator("lineStatus")} + + + + + {isLoadingList && !listData ? ( + Array.from({ length: 8 }).map((_, i) => ( + + + + + + + + + + + + + + + + )) + ) : listError ? ( + + + Error loading product lines: {listError.message} + + + ) : lines.length === 0 ? ( + + + No product lines found matching your criteria. + + + ) : ( + lines.map((line) => { + const key = `${line.brand}|||${line.line}`; + const isExpanded = expandedLine === key; + const sc = statusConfig[line.line_status] || statusConfig.dormant; + + return ( + + toggleExpand(line.brand, line.line)} + > + + {isExpanded ? : } + + {line.brand} + {line.line} + {formatNumber(line.product_count)} + {formatNumber(line.current_stock_units)} + {formatCurrency(line.revenue_30d)} + {formatCurrency(line.profit_30d)} + {formatPercentage(line.avg_margin_30d)} + {formatNumber(line.total_velocity_daily, 1)} + {formatGrowth(line.sales_growth_30d_vs_prev)} + + + + + + + + {sc.label} + + + {isExpanded && ( + + )} + + ); + }) + )} + +
+
+ + {/* Pagination */} + {totalPages > 1 && pagination && ( + + + + + { e.preventDefault(); handlePageChange(pagination.currentPage - 1); }} + aria-disabled={pagination.currentPage === 1} + className={pagination.currentPage === 1 ? "pointer-events-none opacity-50" : ""} + /> + + {[...Array(Math.min(totalPages, 7))].map((_, i) => { + // Show pages around current page + let pageNum: number; + if (totalPages <= 7) { + pageNum = i + 1; + } else if (pagination.currentPage <= 4) { + pageNum = i + 1; + } else if (pagination.currentPage >= totalPages - 3) { + pageNum = totalPages - 6 + i; + } else { + pageNum = pagination.currentPage - 3 + i; + } + return ( + + { e.preventDefault(); handlePageChange(pageNum); }} + isActive={pagination.currentPage === pageNum} + > + {pageNum} + + + ); + })} + + { e.preventDefault(); handlePageChange(pagination.currentPage + 1); }} + aria-disabled={pagination.currentPage >= totalPages} + className={pagination.currentPage >= totalPages ? "pointer-events-none opacity-50" : ""} + /> + + + + + )} +
+ ); +} + +export default ProductLines; diff --git a/inventory/src/pages/Settings.tsx b/inventory/src/pages/Settings.tsx index 102d595..a7666be 100644 --- a/inventory/src/pages/Settings.tsx +++ b/inventory/src/pages/Settings.tsx @@ -269,7 +269,7 @@ export function Settings() { diff --git a/inventory/src/utils/lifecyclePhases.ts b/inventory/src/utils/lifecyclePhases.ts index 3b495f3..f8a7b75 100644 --- a/inventory/src/utils/lifecyclePhases.ts +++ b/inventory/src/utils/lifecyclePhases.ts @@ -1,9 +1,9 @@ export const PHASE_CONFIG: Record = { preorder: { label: "Pre-order", color: "#3B82F6" }, - launch: { label: "Launch", color: "#22C55E" }, + launch: { label: "Launch", color: "#84cc16" }, decay: { label: "Active", color: "#F59E0B" }, mature: { label: "Evergreen", color: "#8B5CF6" }, - slow_mover: { label: "Slow Mover", color: "#14B8A6" }, + slow_mover: { label: "Slow Mover", color: "#06B6D4" }, dormant: { label: "Dormant", color: "#6B7280" }, unknown: { label: "Unclassified", color: "#94A3B8" }, } diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo index c6c46b8..6676046 100644 --- a/inventory/tsconfig.tsbuildinfo +++ b/inventory/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/config.ts","./src/components/ai/aidescriptioncompare.tsx","./src/components/analytics/agingsellthrough.tsx","./src/components/analytics/capitalefficiency.tsx","./src/components/analytics/discountimpact.tsx","./src/components/analytics/growthmomentum.tsx","./src/components/analytics/inventoryflow.tsx","./src/components/analytics/inventorytrends.tsx","./src/components/analytics/inventoryvaluetrend.tsx","./src/components/analytics/portfolioanalysis.tsx","./src/components/analytics/seasonalpatterns.tsx","./src/components/analytics/stockhealth.tsx","./src/components/analytics/stockoutrisk.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.tsx","./src/components/bulk-edit/bulkeditrow.tsx","./src/components/chat/chatroom.tsx","./src/components/chat/chattest.tsx","./src/components/chat/roomlist.tsx","./src/components/chat/searchresults.tsx","./src/components/dashboard/financialoverview.tsx","./src/components/dashboard/operationsmetrics.tsx","./src/components/dashboard/payrollmetrics.tsx","./src/components/dashboard/periodselectionpopover.tsx","./src/components/dashboard/shared/dashboardbadge.tsx","./src/components/dashboard/shared/dashboardcharttooltip.tsx","./src/components/dashboard/shared/dashboardmultistatcardmini.tsx","./src/components/dashboard/shared/dashboardsectionheader.tsx","./src/components/dashboard/shared/dashboardskeleton.tsx","./src/components/dashboard/shared/dashboardstatcard.tsx","./src/components/dashboard/shared/dashboardstatcardmini.tsx","./src/components/dashboard/shared/dashboardstates.tsx","./src/components/dashboard/shared/dashboardtable.tsx","./src/components/dashboard/shared/index.ts","./src/components/discount-simulator/configpanel.tsx","./src/components/discount-simulator/resultschart.tsx","./src/components/discount-simulator/resultstable.tsx","./src/components/discount-simulator/summarycard.tsx","./src/components/forecasting/daterangepickerquick.tsx","./src/components/forecasting/quickorderbuilder.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/layout/navuser.tsx","./src/components/newsletter/campaignhistorydialog.tsx","./src/components/newsletter/newsletterstats.tsx","./src/components/newsletter/recommendationtable.tsx","./src/components/overview/bestsellers.tsx","./src/components/overview/forecastaccuracy.tsx","./src/components/overview/forecastmetrics.tsx","./src/components/overview/overstockmetrics.tsx","./src/components/overview/purchasemetrics.tsx","./src/components/overview/replenishmentmetrics.tsx","./src/components/overview/salesmetrics.tsx","./src/components/overview/stockmetrics.tsx","./src/components/overview/topoverstockedproducts.tsx","./src/components/overview/topreplenishproducts.tsx","./src/components/product-editor/comboboxfield.tsx","./src/components/product-editor/editablecomboboxfield.tsx","./src/components/product-editor/editableinput.tsx","./src/components/product-editor/editablemultiselect.tsx","./src/components/product-editor/imagemanager.tsx","./src/components/product-editor/producteditform.tsx","./src/components/product-editor/productsearch.tsx","./src/components/product-editor/types.ts","./src/components/product-editor/useproductsuggestions.ts","./src/components/product-import/createproductcategorydialog.tsx","./src/components/product-import/reactspreadsheetimport.tsx","./src/components/product-import/config.ts","./src/components/product-import/index.ts","./src/components/product-import/translationsrsiprops.ts","./src/components/product-import/types.ts","./src/components/product-import/components/closeconfirmationdialog.tsx","./src/components/product-import/components/modalwrapper.tsx","./src/components/product-import/components/providers.tsx","./src/components/product-import/components/savesessiondialog.tsx","./src/components/product-import/components/savedsessionslist.tsx","./src/components/product-import/components/table.tsx","./src/components/product-import/hooks/usersi.ts","./src/components/product-import/steps/steps.tsx","./src/components/product-import/steps/uploadflow.tsx","./src/components/product-import/steps/imageuploadstep/imageuploadstep.tsx","./src/components/product-import/steps/imageuploadstep/types.ts","./src/components/product-import/steps/imageuploadstep/components/droppablecontainer.tsx","./src/components/product-import/steps/imageuploadstep/components/genericdropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/copybutton.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/imagedropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/productcard.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/sortableimage.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection/unassignedimageitem.tsx","./src/components/product-import/steps/imageuploadstep/hooks/usebulkimageupload.ts","./src/components/product-import/steps/imageuploadstep/hooks/usedraganddrop.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimageoperations.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimagesinit.ts","./src/components/product-import/steps/imageuploadstep/hooks/useurlimageupload.ts","./src/components/product-import/steps/matchcolumnsstep/matchcolumnsstep.tsx","./src/components/product-import/steps/matchcolumnsstep/types.ts","./src/components/product-import/steps/matchcolumnsstep/components/matchicon.tsx","./src/components/product-import/steps/matchcolumnsstep/components/templatecolumn.tsx","./src/components/product-import/steps/matchcolumnsstep/utils/findmatch.ts","./src/components/product-import/steps/matchcolumnsstep/utils/findunmatchedrequiredfields.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getfieldoptions.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getmatchedcolumns.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizecheckboxvalue.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizetabledata.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setignorecolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setsubcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/uniqueentries.ts","./src/components/product-import/steps/selectheaderstep/selectheaderstep.tsx","./src/components/product-import/steps/selectheaderstep/components/selectheadertable.tsx","./src/components/product-import/steps/selectheaderstep/components/columns.tsx","./src/components/product-import/steps/selectsheetstep/selectsheetstep.tsx","./src/components/product-import/steps/uploadstep/uploadstep.tsx","./src/components/product-import/steps/uploadstep/components/dropzone.tsx","./src/components/product-import/steps/uploadstep/components/columns.tsx","./src/components/product-import/steps/uploadstep/utils/readfilesasync.ts","./src/components/product-import/steps/validationstep/index.tsx","./src/components/product-import/steps/validationstep/components/aisuggestionbadge.tsx","./src/components/product-import/steps/validationstep/components/copydownbanner.tsx","./src/components/product-import/steps/validationstep/components/floatingselectionbar.tsx","./src/components/product-import/steps/validationstep/components/initializingoverlay.tsx","./src/components/product-import/steps/validationstep/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstep/components/suggestionbadges.tsx","./src/components/product-import/steps/validationstep/components/validationcontainer.tsx","./src/components/product-import/steps/validationstep/components/validationfooter.tsx","./src/components/product-import/steps/validationstep/components/validationtable.tsx","./src/components/product-import/steps/validationstep/components/validationtoolbar.tsx","./src/components/product-import/steps/validationstep/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/comboboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstep/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstep/contexts/aisuggestionscontext.tsx","./src/components/product-import/steps/validationstep/dialogs/aidebugdialog.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationprogress.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationresults.tsx","./src/components/product-import/steps/validationstep/dialogs/sanitycheckdialog.tsx","./src/components/product-import/steps/validationstep/hooks/useautoinlineaivalidation.ts","./src/components/product-import/steps/validationstep/hooks/usecopydownvalidation.ts","./src/components/product-import/steps/validationstep/hooks/usefieldoptions.ts","./src/components/product-import/steps/validationstep/hooks/useinlineaivalidation.ts","./src/components/product-import/steps/validationstep/hooks/useproductlines.ts","./src/components/product-import/steps/validationstep/hooks/usesanitycheck.ts","./src/components/product-import/steps/validationstep/hooks/usetemplatemanagement.ts","./src/components/product-import/steps/validationstep/hooks/useupcvalidation.ts","./src/components/product-import/steps/validationstep/hooks/usevalidationactions.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/index.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiapi.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiprogress.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaitransform.ts","./src/components/product-import/steps/validationstep/store/selectors.ts","./src/components/product-import/steps/validationstep/store/types.ts","./src/components/product-import/steps/validationstep/store/validationstore.ts","./src/components/product-import/steps/validationstep/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstep/utils/countryutils.ts","./src/components/product-import/steps/validationstep/utils/datamutations.ts","./src/components/product-import/steps/validationstep/utils/inlineaipayload.ts","./src/components/product-import/steps/validationstep/utils/priceutils.ts","./src/components/product-import/steps/validationstep/utils/upcutils.ts","./src/components/product-import/steps/validationstepold/index.tsx","./src/components/product-import/steps/validationstepold/types.ts","./src/components/product-import/steps/validationstepold/components/aivalidationdialogs.tsx","./src/components/product-import/steps/validationstepold/components/basecellcontent.tsx","./src/components/product-import/steps/validationstepold/components/initializingvalidation.tsx","./src/components/product-import/steps/validationstepold/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstepold/components/upcvalidationtableadapter.tsx","./src/components/product-import/steps/validationstepold/components/validationcell.tsx","./src/components/product-import/steps/validationstepold/components/validationcontainer.tsx","./src/components/product-import/steps/validationstepold/components/validationtable.tsx","./src/components/product-import/steps/validationstepold/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstepold/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstepold/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstepold/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstepold/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstepold/hooks/useaivalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/usefieldvalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/usefiltermanagement.tsx","./src/components/product-import/steps/validationstepold/hooks/useinitialvalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/useproductlinesfetching.tsx","./src/components/product-import/steps/validationstepold/hooks/userowoperations.tsx","./src/components/product-import/steps/validationstepold/hooks/usetemplatemanagement.tsx","./src/components/product-import/steps/validationstepold/hooks/useuniqueitemnumbersvalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/useuniquevalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/useupcvalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/usevalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/usevalidationstate.tsx","./src/components/product-import/steps/validationstepold/hooks/validationtypes.ts","./src/components/product-import/steps/validationstepold/types/index.ts","./src/components/product-import/steps/validationstepold/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstepold/utils/countryutils.ts","./src/components/product-import/steps/validationstepold/utils/datamutations.ts","./src/components/product-import/steps/validationstepold/utils/priceutils.ts","./src/components/product-import/steps/validationstepold/utils/upcutils.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/productsummarycards.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/statusbadge.tsx","./src/components/products/columndefinitions.ts","./src/components/purchase-orders/categorymetricscard.tsx","./src/components/purchase-orders/filtercontrols.tsx","./src/components/purchase-orders/ordermetricscard.tsx","./src/components/purchase-orders/paginationcontrols.tsx","./src/components/purchase-orders/pipelinecard.tsx","./src/components/purchase-orders/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.tsx","./src/components/settings/auditlog.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/globalsettings.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/productsettings.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/settings/vendorsettings.tsx","./src/components/templates/searchproducttemplatedialog.tsx","./src/components/templates/templateform.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/code.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/page-loading.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/config/dashboard.ts","./src/contexts/authcontext.tsx","./src/contexts/dashboardscrollcontext.tsx","./src/contexts/importsessioncontext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/usedebounce.ts","./src/hooks/useimportautosave.ts","./src/lib/utils.ts","./src/lib/dashboard/chartconfig.ts","./src/lib/dashboard/designtokens.ts","./src/pages/analytics.tsx","./src/pages/blackfridaydashboard.tsx","./src/pages/brands.tsx","./src/pages/bulkedit.tsx","./src/pages/categories.tsx","./src/pages/chat.tsx","./src/pages/dashboard.tsx","./src/pages/discountsimulator.tsx","./src/pages/forecasting.tsx","./src/pages/htslookup.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/newsletter.tsx","./src/pages/overview.tsx","./src/pages/producteditor.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx","./src/pages/smalldashboard.tsx","./src/pages/vendors.tsx","./src/services/apiv2.ts","./src/services/importauditlogapi.ts","./src/services/importsessionapi.ts","./src/services/producteditor.ts","./src/services/producteditorauditlog.ts","./src/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/importsession.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/emojiutils.ts","./src/utils/formatcurrency.ts","./src/utils/lifecyclephases.ts","./src/utils/naturallanguageperiod.ts","./src/utils/productutils.ts","./src/utils/transformutils.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/config.ts","./src/components/ai/aidescriptioncompare.tsx","./src/components/analytics/agingsellthrough.tsx","./src/components/analytics/capitalefficiency.tsx","./src/components/analytics/discountimpact.tsx","./src/components/analytics/growthmomentum.tsx","./src/components/analytics/inventoryflow.tsx","./src/components/analytics/inventorytrends.tsx","./src/components/analytics/inventoryvaluetrend.tsx","./src/components/analytics/portfolioanalysis.tsx","./src/components/analytics/seasonalpatterns.tsx","./src/components/analytics/stockhealth.tsx","./src/components/analytics/stockoutrisk.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.tsx","./src/components/bulk-edit/bulkeditrow.tsx","./src/components/chat/chatroom.tsx","./src/components/chat/chattest.tsx","./src/components/chat/roomlist.tsx","./src/components/chat/searchresults.tsx","./src/components/dashboard/financialoverview.tsx","./src/components/dashboard/operationsmetrics.tsx","./src/components/dashboard/payrollmetrics.tsx","./src/components/dashboard/periodselectionpopover.tsx","./src/components/dashboard/shared/dashboardbadge.tsx","./src/components/dashboard/shared/dashboardcharttooltip.tsx","./src/components/dashboard/shared/dashboardmultistatcardmini.tsx","./src/components/dashboard/shared/dashboardsectionheader.tsx","./src/components/dashboard/shared/dashboardskeleton.tsx","./src/components/dashboard/shared/dashboardstatcard.tsx","./src/components/dashboard/shared/dashboardstatcardmini.tsx","./src/components/dashboard/shared/dashboardstates.tsx","./src/components/dashboard/shared/dashboardtable.tsx","./src/components/dashboard/shared/index.ts","./src/components/discount-simulator/configpanel.tsx","./src/components/discount-simulator/resultschart.tsx","./src/components/discount-simulator/resultstable.tsx","./src/components/discount-simulator/summarycard.tsx","./src/components/forecasting/daterangepickerquick.tsx","./src/components/forecasting/quickorderbuilder.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/layout/navuser.tsx","./src/components/newsletter/campaignhistorydialog.tsx","./src/components/newsletter/newsletterstats.tsx","./src/components/newsletter/recommendationtable.tsx","./src/components/overview/bestsellers.tsx","./src/components/overview/forecastaccuracy.tsx","./src/components/overview/forecastmetrics.tsx","./src/components/overview/overstockmetrics.tsx","./src/components/overview/purchasemetrics.tsx","./src/components/overview/replenishmentmetrics.tsx","./src/components/overview/salesmetrics.tsx","./src/components/overview/stockmetrics.tsx","./src/components/overview/topoverstockedproducts.tsx","./src/components/overview/topreplenishproducts.tsx","./src/components/product-editor/comboboxfield.tsx","./src/components/product-editor/editablecomboboxfield.tsx","./src/components/product-editor/editableinput.tsx","./src/components/product-editor/editablemultiselect.tsx","./src/components/product-editor/imagemanager.tsx","./src/components/product-editor/producteditform.tsx","./src/components/product-editor/productsearch.tsx","./src/components/product-editor/types.ts","./src/components/product-editor/useproductsuggestions.ts","./src/components/product-import/createproductcategorydialog.tsx","./src/components/product-import/reactspreadsheetimport.tsx","./src/components/product-import/config.ts","./src/components/product-import/index.ts","./src/components/product-import/translationsrsiprops.ts","./src/components/product-import/types.ts","./src/components/product-import/components/closeconfirmationdialog.tsx","./src/components/product-import/components/modalwrapper.tsx","./src/components/product-import/components/providers.tsx","./src/components/product-import/components/savesessiondialog.tsx","./src/components/product-import/components/savedsessionslist.tsx","./src/components/product-import/components/table.tsx","./src/components/product-import/hooks/usersi.ts","./src/components/product-import/steps/steps.tsx","./src/components/product-import/steps/uploadflow.tsx","./src/components/product-import/steps/imageuploadstep/imageuploadstep.tsx","./src/components/product-import/steps/imageuploadstep/types.ts","./src/components/product-import/steps/imageuploadstep/components/droppablecontainer.tsx","./src/components/product-import/steps/imageuploadstep/components/genericdropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/copybutton.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/imagedropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/productcard.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/sortableimage.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection/unassignedimageitem.tsx","./src/components/product-import/steps/imageuploadstep/hooks/usebulkimageupload.ts","./src/components/product-import/steps/imageuploadstep/hooks/usedraganddrop.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimageoperations.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimagesinit.ts","./src/components/product-import/steps/imageuploadstep/hooks/useurlimageupload.ts","./src/components/product-import/steps/matchcolumnsstep/matchcolumnsstep.tsx","./src/components/product-import/steps/matchcolumnsstep/types.ts","./src/components/product-import/steps/matchcolumnsstep/components/matchicon.tsx","./src/components/product-import/steps/matchcolumnsstep/components/templatecolumn.tsx","./src/components/product-import/steps/matchcolumnsstep/utils/findmatch.ts","./src/components/product-import/steps/matchcolumnsstep/utils/findunmatchedrequiredfields.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getfieldoptions.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getmatchedcolumns.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizecheckboxvalue.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizetabledata.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setignorecolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setsubcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/uniqueentries.ts","./src/components/product-import/steps/selectheaderstep/selectheaderstep.tsx","./src/components/product-import/steps/selectheaderstep/components/selectheadertable.tsx","./src/components/product-import/steps/selectheaderstep/components/columns.tsx","./src/components/product-import/steps/selectsheetstep/selectsheetstep.tsx","./src/components/product-import/steps/uploadstep/uploadstep.tsx","./src/components/product-import/steps/uploadstep/components/dropzone.tsx","./src/components/product-import/steps/uploadstep/components/columns.tsx","./src/components/product-import/steps/uploadstep/utils/readfilesasync.ts","./src/components/product-import/steps/validationstep/index.tsx","./src/components/product-import/steps/validationstep/components/aisuggestionbadge.tsx","./src/components/product-import/steps/validationstep/components/copydownbanner.tsx","./src/components/product-import/steps/validationstep/components/floatingselectionbar.tsx","./src/components/product-import/steps/validationstep/components/initializingoverlay.tsx","./src/components/product-import/steps/validationstep/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstep/components/suggestionbadges.tsx","./src/components/product-import/steps/validationstep/components/validationcontainer.tsx","./src/components/product-import/steps/validationstep/components/validationfooter.tsx","./src/components/product-import/steps/validationstep/components/validationtable.tsx","./src/components/product-import/steps/validationstep/components/validationtoolbar.tsx","./src/components/product-import/steps/validationstep/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/comboboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstep/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstep/contexts/aisuggestionscontext.tsx","./src/components/product-import/steps/validationstep/dialogs/aidebugdialog.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationprogress.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationresults.tsx","./src/components/product-import/steps/validationstep/dialogs/sanitycheckdialog.tsx","./src/components/product-import/steps/validationstep/hooks/useautoinlineaivalidation.ts","./src/components/product-import/steps/validationstep/hooks/usecopydownvalidation.ts","./src/components/product-import/steps/validationstep/hooks/usefieldoptions.ts","./src/components/product-import/steps/validationstep/hooks/useinlineaivalidation.ts","./src/components/product-import/steps/validationstep/hooks/useproductlines.ts","./src/components/product-import/steps/validationstep/hooks/usesanitycheck.ts","./src/components/product-import/steps/validationstep/hooks/usetemplatemanagement.ts","./src/components/product-import/steps/validationstep/hooks/useupcvalidation.ts","./src/components/product-import/steps/validationstep/hooks/usevalidationactions.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/index.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiapi.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiprogress.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaitransform.ts","./src/components/product-import/steps/validationstep/store/selectors.ts","./src/components/product-import/steps/validationstep/store/types.ts","./src/components/product-import/steps/validationstep/store/validationstore.ts","./src/components/product-import/steps/validationstep/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstep/utils/countryutils.ts","./src/components/product-import/steps/validationstep/utils/datamutations.ts","./src/components/product-import/steps/validationstep/utils/inlineaipayload.ts","./src/components/product-import/steps/validationstep/utils/priceutils.ts","./src/components/product-import/steps/validationstep/utils/upcutils.ts","./src/components/product-import/steps/validationstepold/index.tsx","./src/components/product-import/steps/validationstepold/types.ts","./src/components/product-import/steps/validationstepold/components/aivalidationdialogs.tsx","./src/components/product-import/steps/validationstepold/components/basecellcontent.tsx","./src/components/product-import/steps/validationstepold/components/initializingvalidation.tsx","./src/components/product-import/steps/validationstepold/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstepold/components/upcvalidationtableadapter.tsx","./src/components/product-import/steps/validationstepold/components/validationcell.tsx","./src/components/product-import/steps/validationstepold/components/validationcontainer.tsx","./src/components/product-import/steps/validationstepold/components/validationtable.tsx","./src/components/product-import/steps/validationstepold/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstepold/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstepold/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstepold/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstepold/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstepold/hooks/useaivalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/usefieldvalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/usefiltermanagement.tsx","./src/components/product-import/steps/validationstepold/hooks/useinitialvalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/useproductlinesfetching.tsx","./src/components/product-import/steps/validationstepold/hooks/userowoperations.tsx","./src/components/product-import/steps/validationstepold/hooks/usetemplatemanagement.tsx","./src/components/product-import/steps/validationstepold/hooks/useuniqueitemnumbersvalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/useuniquevalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/useupcvalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/usevalidation.tsx","./src/components/product-import/steps/validationstepold/hooks/usevalidationstate.tsx","./src/components/product-import/steps/validationstepold/hooks/validationtypes.ts","./src/components/product-import/steps/validationstepold/types/index.ts","./src/components/product-import/steps/validationstepold/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstepold/utils/countryutils.ts","./src/components/product-import/steps/validationstepold/utils/datamutations.ts","./src/components/product-import/steps/validationstepold/utils/priceutils.ts","./src/components/product-import/steps/validationstepold/utils/upcutils.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/productsummarycards.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/statusbadge.tsx","./src/components/products/columndefinitions.ts","./src/components/purchase-orders/categorymetricscard.tsx","./src/components/purchase-orders/filtercontrols.tsx","./src/components/purchase-orders/ordermetricscard.tsx","./src/components/purchase-orders/paginationcontrols.tsx","./src/components/purchase-orders/pipelinecard.tsx","./src/components/purchase-orders/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.tsx","./src/components/settings/auditlog.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/globalsettings.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/productsettings.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/settings/vendorsettings.tsx","./src/components/templates/searchproducttemplatedialog.tsx","./src/components/templates/templateform.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/code.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/page-loading.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/config/dashboard.ts","./src/contexts/authcontext.tsx","./src/contexts/dashboardscrollcontext.tsx","./src/contexts/importsessioncontext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/usedebounce.ts","./src/hooks/useimportautosave.ts","./src/lib/utils.ts","./src/lib/dashboard/chartconfig.ts","./src/lib/dashboard/designtokens.ts","./src/pages/analytics.tsx","./src/pages/blackfridaydashboard.tsx","./src/pages/brands.tsx","./src/pages/bulkedit.tsx","./src/pages/categories.tsx","./src/pages/chat.tsx","./src/pages/dashboard.tsx","./src/pages/discountsimulator.tsx","./src/pages/forecasting.tsx","./src/pages/htslookup.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/newsletter.tsx","./src/pages/overview.tsx","./src/pages/producteditor.tsx","./src/pages/productlines.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx","./src/pages/smalldashboard.tsx","./src/pages/vendors.tsx","./src/services/apiv2.ts","./src/services/importauditlogapi.ts","./src/services/importsessionapi.ts","./src/services/producteditor.ts","./src/services/producteditorauditlog.ts","./src/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/importsession.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/emojiutils.ts","./src/utils/formatcurrency.ts","./src/utils/lifecyclephases.ts","./src/utils/naturallanguageperiod.ts","./src/utils/productutils.ts","./src/utils/transformutils.ts"],"version":"5.6.3"} \ No newline at end of file