diff --git a/inventory/package-lock.json b/inventory/package-lock.json index 12504d2..599ff16 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -84,6 +84,7 @@ "@eslint/js": "^9.17.0", "@types/fs-extra": "^11.0.4", "@types/lodash": "^4.17.15", + "@types/luxon": "^3.7.1", "@types/node": "^22.10.5", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -3153,6 +3154,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.13.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", diff --git a/inventory/package.json b/inventory/package.json index a657515..35db14b 100644 --- a/inventory/package.json +++ b/inventory/package.json @@ -87,6 +87,7 @@ "@eslint/js": "^9.17.0", "@types/fs-extra": "^11.0.4", "@types/lodash": "^4.17.15", + "@types/luxon": "^3.7.1", "@types/node": "^22.10.5", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/inventory/src/App.tsx b/inventory/src/App.tsx index c7cb5aa..c67fd01 100644 --- a/inventory/src/App.tsx +++ b/inventory/src/App.tsx @@ -27,6 +27,7 @@ const Vendors = lazy(() => import('./pages/Vendors')); const Categories = lazy(() => import('./pages/Categories')); const Brands = lazy(() => import('./pages/Brands')); const PurchaseOrders = lazy(() => import('./pages/PurchaseOrders')); +const BlackFridayDashboard = lazy(() => import('./pages/BlackFridayDashboard')); // 2. Dashboard app - separate chunk const Dashboard = lazy(() => import('./pages/Dashboard')); @@ -210,6 +211,13 @@ function App() { } /> + + }> + + + + } /> } /> diff --git a/inventory/src/components/auth/FirstAccessiblePage.tsx b/inventory/src/components/auth/FirstAccessiblePage.tsx index dfa91c3..540e495 100644 --- a/inventory/src/components/auth/FirstAccessiblePage.tsx +++ b/inventory/src/components/auth/FirstAccessiblePage.tsx @@ -6,6 +6,7 @@ import { AuthContext } from "@/contexts/AuthContext"; // Dashboard is first so users with dashboard access default to it const PAGES = [ { path: "/dashboard", permission: "access:dashboard" }, + { path: "/dashboard/black-friday", permission: "access:black_friday_dashboard" }, { path: "/overview", permission: "access:overview" }, { path: "/products", permission: "access:products" }, { path: "/categories", permission: "access:categories" }, diff --git a/inventory/src/components/layout/AppSidebar.tsx b/inventory/src/components/layout/AppSidebar.tsx index 5ce772c..6b55a9d 100644 --- a/inventory/src/components/layout/AppSidebar.tsx +++ b/inventory/src/components/layout/AppSidebar.tsx @@ -12,6 +12,7 @@ import { LayoutDashboard, Percent, FileSearch, + ShoppingCart, } from "lucide-react"; import { IconCrystalBall } from "@tabler/icons-react"; import { @@ -40,6 +41,12 @@ const dashboardItems = [ icon: LayoutDashboard, url: "/dashboard", permission: "access:dashboard" + }, + { + title: "Black Friday", + icon: ShoppingCart, + url: "/dashboard/black-friday", + permission: "access:black_friday_dashboard" } ]; diff --git a/inventory/src/pages/BlackFridayDashboard.tsx b/inventory/src/pages/BlackFridayDashboard.tsx new file mode 100644 index 0000000..23aabaa --- /dev/null +++ b/inventory/src/pages/BlackFridayDashboard.tsx @@ -0,0 +1,1385 @@ +import { useEffect, useMemo, useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + Tooltip as RechartsTooltip, + XAxis, + YAxis, + Legend, + TooltipProps, +} from "recharts"; +import { + RefreshCw, + TrendingUp, + Sparkles, + DollarSign, + ShoppingBag, + Percent, + ArrowUpRight, + ArrowDownRight, + Trophy, + Activity, + Clock3, + Zap, + Users, +} from "lucide-react"; +import { acotService } from "@/services/dashboard/acotService"; +import { cn } from "@/lib/utils"; +import { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"; +import { DateTime } from "luxon"; + +type BlackFridayRange = { + start: DateTime; + end: DateTime; + label: string; +}; + +type DayMetrics = { + label: string; + date: Date; + revenue: number; + orders: number; + avgOrderValue: number; + profit: number; + cogs: number; + margin: number; +}; + +type YearMetrics = { + year: number; + range: BlackFridayRange; + days: DayMetrics[]; + totals: { + revenue: number; + orders: number; + avgOrderValue: number; + profit: number; + margin: number; + cogs: number; + }; +}; + +type RealtimeSnapshot = { + last5MinUsers: number; + last30MinUsers: number; + lastUpdated: string | null; +}; + +type DebugBucketNote = { + year: number; + source: "stats" | "financial"; + raw: string; + bucket: string; + index: number; +}; + +type StatsDetailsResponse = { + stats?: Array>; +} | null; + +type FinancialsResponse = { + trend?: Array>; + totals?: Record; +} | null; + +const DAY_LABELS = ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue"]; +const BUSINESS_TIMEZONE = "America/New_York"; +const BUSINESS_DAY_START_HOUR = 1; +const COLOR_PALETTE = [ + "#10b981", // emerald-500 - current year gets the best color + "#6366f1", // indigo-500 + "#f59e0b", // amber-500 + "#8b5cf6", // violet-500 + "#64748b", // slate-500 + "#94a3b8", // slate-400 +]; + +const formatCurrency = (value: number, digits = 0) => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }).format(Number.isFinite(value) ? value : 0); + +const formatNumber = (value: number) => + Number.isFinite(value) ? value.toLocaleString("en-US") : "0"; + +const mapRealtimeBasic = (payload: Record): RealtimeSnapshot => { + const last30Raw = + (payload as { userResponse?: { rows?: Array<{ metricValues?: Array<{ value?: string }> }> } }) + ?.userResponse?.rows?.[0]?.metricValues?.[0]?.value ?? "0"; + const last5Raw = + (payload as { fiveMinResponse?: { rows?: Array<{ metricValues?: Array<{ value?: string }> }> } }) + ?.fiveMinResponse?.rows?.[0]?.metricValues?.[0]?.value ?? "0"; + + const last30MinUsers = Number.parseInt(String(last30Raw), 10); + const last5MinUsers = Number.parseInt(String(last5Raw), 10); + + return { + last5MinUsers: Number.isFinite(last5MinUsers) ? last5MinUsers : 0, + last30MinUsers: Number.isFinite(last30MinUsers) ? last30MinUsers : 0, + lastUpdated: new Date().toISOString(), + }; +}; + +const formatPercent = (value: number | null | undefined, digits = 1) => { + if (value === null || value === undefined || Number.isNaN(value)) { + return "—"; + } + return `${value.toFixed(digits)}%`; +}; + +const percentChange = (current: number, previous: number | null | undefined) => { + if (previous === null || previous === undefined || previous === 0) { + return null; + } + + return ((current - previous) / Math.abs(previous)) * 100; +}; + +const formatLastUpdated = (value: string | null | undefined) => { + if (!value) return "—"; + const dt = DateTime.fromISO(value); + if (!dt.isValid) return "—"; + return dt.toLocal().toLocaleString({timeStyle: "short"}); +}; + +const toBusinessDateTime = (value: unknown) => { + if (!value) return null; + + if (value instanceof Date) { + return DateTime.fromJSDate(value, { zone: BUSINESS_TIMEZONE }); + } + if (value instanceof DateTime) { + return value.setZone(BUSINESS_TIMEZONE); + } + if (typeof value === "string") { + const trimmed = value.trim(); + const datePart = trimmed.split("T")[0]; + const isoDateOnly = /^\d{4}-\d{2}-\d{2}$/; + // Date-only strings (e.g., "2024-11-28") + if (isoDateOnly.test(trimmed)) { + const dateOnly = DateTime.fromISO(trimmed, { + zone: BUSINESS_TIMEZONE, + }).set({ hour: BUSINESS_DAY_START_HOUR, minute: 0, second: 0, millisecond: 0 }); + return dateOnly.isValid ? dateOnly : null; + } + + // Stats responses often send midnight-Z timestamps (e.g., 2024-11-29T00:00:00.000Z) that + // represent a whole business day. Treat those as date-only to avoid shifting a day earlier. + if (isoDateOnly.test(datePart)) { + const dateOnly = DateTime.fromISO(datePart, { + zone: BUSINESS_TIMEZONE, + }).set({ hour: BUSINESS_DAY_START_HOUR, minute: 0, second: 0, millisecond: 0 }); + return dateOnly.isValid ? dateOnly : null; + } + + const hasZulu = trimmed.endsWith("Z"); + const isoLike = hasZulu ? trimmed.slice(0, -1) : trimmed; + // ISO strings with time: + // - If Z, parse as UTC then convert to business zone + // - Otherwise, treat as wall time in business zone + let dt = hasZulu + ? DateTime.fromISO(trimmed, { zone: "utc", setZone: true }).setZone(BUSINESS_TIMEZONE) + : DateTime.fromISO(isoLike, { zone: BUSINESS_TIMEZONE, setZone: false }); + if (!dt.isValid) { + dt = hasZulu + ? DateTime.fromSQL(trimmed, { zone: "utc", setZone: true }).setZone(BUSINESS_TIMEZONE) + : DateTime.fromSQL(isoLike, { zone: BUSINESS_TIMEZONE, setZone: false }); + } + return dt.isValid ? dt : null; + } + if (typeof value === "number") { + return DateTime.fromMillis(value, { zone: BUSINESS_TIMEZONE }); + } + return null; +}; + +const getBusinessDayStart = (dt: DateTime) => { + const start = dt.set({ + hour: BUSINESS_DAY_START_HOUR, + minute: 0, + second: 0, + millisecond: 0, + }); + return dt.hour < BUSINESS_DAY_START_HOUR ? start.minus({ days: 1 }) : start; +}; + +const formatRangeLabel = (start: DateTime, end: DateTime) => { + const startZoned = start.setZone(BUSINESS_TIMEZONE); + const endZoned = end.setZone(BUSINESS_TIMEZONE); + return `${startZoned.toFormat("LLL d")} - ${endZoned.toFormat("LLL d")}`; +}; + +const computeDayIndex = (timestamp: unknown, rangeStart: DateTime) => { + try { + const dt = toBusinessDateTime(timestamp); + if (!dt || !dt.isValid || !rangeStart?.isValid) return -1; + + const start = getBusinessDayStart(rangeStart); + const target = getBusinessDayStart(dt); + + return Math.floor(target.diff(start, "days").days); + } catch (err) { + console.error("[BlackFridayDashboard] Failed to compute day index", err); + return -1; + } +}; + +const getBlackFridayRange = (year: number): BlackFridayRange => { + const novemberFirst = DateTime.fromObject( + { year, month: 11, day: 1 }, + { zone: BUSINESS_TIMEZONE } + ); + if (!novemberFirst.isValid) { + throw new Error(`Invalid start date for year ${year}: ${novemberFirst.invalidReason ?? "unknown"}`); + } + + const offsetToThursday = (4 - novemberFirst.weekday + 7) % 7; // 4 = Thursday + const thanksgiving = novemberFirst.plus({ days: offsetToThursday + 21 }); // 4th Thursday + const start = thanksgiving.set({ + hour: BUSINESS_DAY_START_HOUR, + minute: 0, + second: 0, + millisecond: 0, + }); + const end = start.plus({ days: DAY_LABELS.length }).minus({ milliseconds: 1 }); + + return { + start, + end, + label: formatRangeLabel(start, end), + }; +}; + +const buildDayBuckets = (start: DateTime) => + DAY_LABELS.map((label, index) => { + const dayStart = getBusinessDayStart(start.plus({ days: index })); + return { + label, + date: dayStart.toJSDate(), + revenue: 0, + orders: 0, + avgOrderValue: 0, + profit: 0, + cogs: 0, + margin: 0, + }; + }); + +const mapFinancialTrendToDays = ( + days: DayMetrics[], + trend: Array>, + start: DateTime, + debug?: DebugBucketNote[] +) => { + if (!Array.isArray(trend)) return; + + trend.forEach((point) => { + const timestamp = point.date || point.timestamp; + if (!timestamp) return; + + const dayIndex = computeDayIndex(timestamp as string, start); + + if (dayIndex < 0 || dayIndex >= days.length) return; + + if (debug) { + debug.push({ + year: start.year, + source: "financial", + raw: String(timestamp), + bucket: days[dayIndex]?.label ?? "n/a", + index: dayIndex, + }); + } + + const target = days[dayIndex]; + const income = Number(point.income ?? 0); + const profit = Number(point.profit ?? 0); + const cogs = Number(point.cogs ?? 0); + const orders = Number(point.orders ?? 0); + + if (!target.revenue && Number.isFinite(income)) target.revenue = income; + if (!target.orders && Number.isFinite(orders)) target.orders = orders; + target.profit = profit; + target.cogs = cogs; + // Margin is calculated in finalizeMetrics to ensure consistency with final revenue + }); +}; + +const mapStatsToDays = ( + days: DayMetrics[], + stats: Array>, + start: DateTime, + debug?: DebugBucketNote[] +) => { + if (!Array.isArray(stats)) return; + + stats.forEach((entry) => { + const timestamp = entry.date || entry.timestamp; + if (!timestamp) return; + + const dayIndex = computeDayIndex(timestamp as string, start); + + if (dayIndex < 0 || dayIndex >= days.length) return; + + if (debug) { + debug.push({ + year: start.year, + source: "stats", + raw: String(timestamp), + bucket: days[dayIndex]?.label ?? "n/a", + index: dayIndex, + }); + } + + const target = days[dayIndex]; + const revenue = Number(entry.revenue ?? entry.total ?? entry.income ?? 0); + const orders = Number( + entry.orders ?? entry.count ?? entry.orderCount ?? entry.quantity ?? 0 + ); + + target.revenue = revenue; + target.orders = orders; + }); +}; + +const finalizeMetrics = (days: DayMetrics[]) => { + days.forEach((day) => { + day.avgOrderValue = day.orders > 0 ? day.revenue / day.orders : 0; + + if (day.profit === 0 && day.revenue > 0 && day.cogs > 0) { + day.profit = day.revenue - day.cogs; + } + + // Calculate margin based on the final revenue and profit figures + // This fixes discrepancies where financial income source != stats revenue source + if (day.revenue > 0) { + const profit = day.profit || (day.cogs > 0 ? day.revenue - day.cogs : 0); + day.margin = profit ? (profit / day.revenue) * 100 : 0; + } else { + day.margin = 0; + } + }); +}; + +const computeTotals = ( + days: DayMetrics[], + financialTotals?: Record +) => { + const revenue = days.reduce((sum, day) => sum + day.revenue, 0); + const orders = days.reduce((sum, day) => sum + day.orders, 0); + const cogs = days.reduce((sum, day) => sum + day.cogs, 0); + const profitFromDays = days.reduce((sum, day) => sum + day.profit, 0); + + const incomeFromTotals = Number(financialTotals?.income); + const profitFromTotals = Number(financialTotals?.profit); + const cogsFromTotals = Number(financialTotals?.cogs); + const marginFromTotals = Number(financialTotals?.margin); + + const income = Number.isFinite(incomeFromTotals) ? incomeFromTotals : revenue; + const profit = Number.isFinite(profitFromTotals) + ? profitFromTotals + : profitFromDays || income - cogs; + const safeCogs = Number.isFinite(cogsFromTotals) ? cogsFromTotals : cogs; + const margin = + Number.isFinite(marginFromTotals) && marginFromTotals !== 0 + ? marginFromTotals + : income !== 0 + ? (profit / income) * 100 + : 0; + + return { + revenue, + orders, + avgOrderValue: orders > 0 ? revenue / orders : 0, + profit, + margin, + cogs: safeCogs, + }; +}; + +function InsightBadge({ + value, + positiveLabel, + negativeLabel, + className, + size = "default", +}: { + value: number | null; + positiveLabel: string; + negativeLabel: string; + className?: string; + size?: "default" | "lg"; +}) { + if (value === null) { + return ; + } + + const isUp = value >= 0; + return ( + + {isUp ? : } + {isUp ? positiveLabel : negativeLabel} {formatPercent(Math.abs(value), 1)} + + ); +} + +const CustomTooltip = ({ active, payload, label }: TooltipProps) => { + if (active && payload && payload.length) { + return ( +
+
+ {label} +
+
+ {payload.map((entry, index) => { + const name = entry.name?.toString() || ""; + const isRevenue = name.includes("revenue"); + const isMargin = name.includes("margin"); + const isAov = name.includes("aov"); + + let valueFormatted = ""; + if (isRevenue || isAov) valueFormatted = formatCurrency(Number(entry.value)); + else if (isMargin) valueFormatted = `${Number(entry.value).toFixed(1)}%`; + else valueFormatted = formatNumber(Number(entry.value)); + + return ( +
+
+
+ + {name.replace(/-/g, " ")} + +
+ {valueFormatted} +
+ ); + })} +
+
+ ); + } + return null; +}; + +function LoadingBlock() { + return ( +
+ +
+ {[...Array(4)].map((_, index) => ( + + ))} +
+
+ ); +} + +export function BlackFridayDashboard() { + const currentYear = new Date().getUTCFullYear(); + const availableYears = useMemo( + () => + Array.from({ length: 6 }, (_, index) => currentYear - index).filter( + (year) => year >= 2018 + ), + [currentYear] + ); + + const [selectedYears, setSelectedYears] = useState( + availableYears.slice(0, 6) + ); + const [dataByYear, setDataByYear] = useState>({}); + const [bucketDebug, setBucketDebug] = useState([]); + const [showDebug] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [refreshToken, setRefreshToken] = useState(0); + const [realtimeSnapshot, setRealtimeSnapshot] = useState({ + last5MinUsers: 0, + last30MinUsers: 0, + lastUpdated: null, + }); + const [realtimeError, setRealtimeError] = useState(null); + + useEffect(() => { + let cancelled = false; + + const loadData = async () => { + setLoading(true); + setError(null); + + try { + const results = await Promise.all( + selectedYears.map(async (year) => { + const range = getBlackFridayRange(year); + const params = { + timeRange: "custom", + startDate: range.start.toUTC().toISO(), + endDate: range.end.toUTC().toISO(), + }; + + const debugNotes: DebugBucketNote[] = []; + + const [statsResponseRaw, financialResponseRaw] = await Promise.all([ + acotService.getStatsDetails({ + ...params, + metric: "revenue", + eventType: "PLACED_ORDER", + daily: true, + }), + acotService.getFinancials({ + timeRange: "custom", + startDate: params.startDate, + endDate: params.endDate, + }), + ]); + + const statsResponse = statsResponseRaw as StatsDetailsResponse; + const financialResponse = financialResponseRaw as FinancialsResponse; + + const days = buildDayBuckets(range.start); + const stats = Array.isArray(statsResponse?.stats) + ? statsResponse.stats + : []; + mapStatsToDays(days, stats, range.start, debugNotes); + mapFinancialTrendToDays( + days, + Array.isArray(financialResponse?.trend) ? financialResponse.trend : [], + range.start, + debugNotes + ); + finalizeMetrics(days); + + const totals = computeTotals( + days, + (financialResponse?.totals as Record) || {} + ); + + return { + year, + range, + days, + totals, + debugNotes, + } as YearMetrics & { debugNotes: DebugBucketNote[] }; + }) + ); + + if (cancelled) return; + + const record: Record = {}; + const debug: DebugBucketNote[] = []; + results.forEach((result) => { + const { debugNotes, ...rest } = result; + record[rest.year] = rest; + debug.push(...debugNotes); + }); + + setDataByYear(record); + setBucketDebug(debug); + } catch (err) { + if (cancelled) return; + const message = + typeof err === "object" && + err !== null && + "message" in err && + typeof (err as { message?: unknown }).message === "string" + ? (err as { message: string }).message + : "Failed to load Black Friday data"; + setError(message); + } finally { + if (!cancelled) { + setLoading(false); + } + } + }; + + void loadData(); + + return () => { + cancelled = true; + }; + }, [selectedYears, refreshToken]); + + useEffect(() => { + const interval = setInterval(() => { + setRefreshToken((token) => token + 1); + }, 30000); + + return () => clearInterval(interval); + }, []); + + useEffect(() => { + let cancelled = false; + + const fetchRealtime = async () => { + try { + const response = await fetch("/api/dashboard-analytics/realtime/basic", { + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Failed to fetch realtime analytics"); + } + + const result = await response.json(); + if (cancelled) return; + + const snapshot = mapRealtimeBasic((result?.data ?? {}) as Record); + setRealtimeSnapshot(snapshot); + setRealtimeError(null); + } catch (err) { + if (cancelled) return; + setRealtimeError("Realtime data unavailable"); + } + }; + + void fetchRealtime(); + const interval = setInterval(fetchRealtime, 30000); + + return () => { + cancelled = true; + clearInterval(interval); + }; + }, []); + + const sortedYears = useMemo( + () => [...selectedYears].sort((a, b) => b - a), // Current year first + [selectedYears] + ); + + const previousYears = useMemo( + () => sortedYears.filter(y => y !== currentYear), + [sortedYears, currentYear] + ); + + const yoyByYear = useMemo(() => { + const map: Record = {}; + const ascending = [...sortedYears].sort((a, b) => a - b); + ascending.forEach((year, index) => { + const current = dataByYear[year]; + const previousYear = ascending[index - 1]; + const previous = previousYear ? dataByYear[previousYear] : undefined; + + if (current && previous) { + map[year] = percentChange( + current.totals.revenue, + previous.totals.revenue + ); + } else { + map[year] = null; + } + }); + return map; + }, [dataByYear, sortedYears]); + + const chartData = useMemo(() => { + if (!sortedYears.length) return []; + + return DAY_LABELS.map((label, index) => { + const point: Record = { day: label }; + sortedYears.forEach((year, colorIndex) => { + const day = dataByYear[year]?.days[index]; + point[`${year}-revenue`] = day?.revenue ?? 0; + point[`${year}-orders`] = day?.orders ?? 0; + point[`${year}-margin`] = day?.margin ?? null; + point[`${year}-aov`] = day?.avgOrderValue ?? 0; + point[`color-${year}`] = COLOR_PALETTE[colorIndex % COLOR_PALETTE.length]; + }); + return point; + }); + }, [dataByYear, sortedYears]); + + const debugOverlay = useMemo(() => { + if (!bucketDebug.length) return []; + return bucketDebug.slice(0, 200); // keep overlay readable + }, [bucketDebug]); + + const toggleYear = (year: number) => { + setSelectedYears((prev) => { + if (prev.includes(year)) { + if (prev.length === 1) return prev; // keep at least one year selected + return prev.filter((y) => y !== year); + } + return [...prev, year].sort((a, b) => b - a); + }); + }; + + const getStrokeForYear = (year: number) => { + const index = sortedYears.indexOf(year); + return COLOR_PALETTE[index % COLOR_PALETTE.length]; + }; + + const currentYearData = dataByYear[currentYear]; + const lastYearData = dataByYear[currentYear - 1]; + + // Live status indicator + const renderLiveHeader = () => ( +
+
+
+
+ + +
+ LIVE +
+
+

Black Friday {currentYear}

+ {currentYearData?.range.label} +
+ +
+
+ {availableYears.map((year) => ( + + ))} +
+ +
+
+ ); + + // Realtime shoppers strip - more prominent + const renderRealtimeStrip = () => ( +
+
+ + On site: + {formatNumber(realtimeSnapshot.last5MinUsers)} + (5m) +
+
+
+ {formatNumber(realtimeSnapshot.last30MinUsers)} + (30m) +
+
+ + {formatLastUpdated(realtimeSnapshot.lastUpdated)} +
+ {realtimeError && ( + {realtimeError} + )} +
+ ); + + // Current year hero section - big numbers + const renderCurrentYearHero = () => { + if (!currentYearData) return null; + + const yoy = yoyByYear[currentYear]; + + return ( +
+
+ {/* Revenue - Primary metric */} +
+
+
+ + Revenue +
+
+ {formatCurrency(currentYearData.totals.revenue, 0)} +
+ {yoy !== null && ( + + )} +
+
+ + {/* Orders */} +
+
+ + Orders +
+
+ {formatNumber(currentYearData.totals.orders)} +
+ {lastYearData && ( +
+ vs {formatNumber(lastYearData.totals.orders)} LY +
+ )} +
+ + {/* Profit */} +
+
+ + Profit +
+
+ {formatCurrency(currentYearData.totals.profit, 0)} +
+ {lastYearData && ( +
+ vs {formatCurrency(lastYearData.totals.profit, 0)} LY +
+ )} +
+ + {/* Margin */} +
+
+ + Margin +
+
+ {formatPercent(currentYearData.totals.margin, 1)} +
+ {lastYearData && ( +
+ vs {formatPercent(lastYearData.totals.margin, 1)} LY +
+ )} +
+ + {/* AOV */} +
+
+ + AOV +
+
+ {formatCurrency(currentYearData.totals.avgOrderValue, 0)} +
+ {lastYearData && ( +
+ vs {formatCurrency(lastYearData.totals.avgOrderValue, 0)} LY +
+ )} +
+
+
+ ); + }; + + // Daily progress for current year + const renderCurrentYearDays = () => { + if (!currentYearData) return null; + + return ( +
+ {currentYearData.days.map((day, idx) => { + const lastYearDay = lastYearData?.days[idx]; + const change = lastYearDay ? percentChange(day.revenue, lastYearDay.revenue) : null; + const isToday = idx === Math.min( + Math.floor( + DateTime.now().setZone(BUSINESS_TIMEZONE).diff(currentYearData.range.start, 'days').days + ), + 5 + ); + + return ( +
+
+ + {day.label} + + {isToday && ( +
+ )} +
+
+ {formatCurrency(day.revenue, 0)} +
+
+ + {formatNumber(day.orders)} ord + + {change !== null && ( + = 0 ? "text-emerald-600" : "text-rose-500" + )}> + {change >= 0 ? "+" : ""}{change.toFixed(0)}% + + )} +
+
+ ); + })} +
+ ); + }; + + // Compact previous years comparison + const renderPreviousYearsComparison = () => { + if (!previousYears.length) return null; + + return ( +
+ {previousYears.slice(0, 5).map((year) => { + const data = dataByYear[year]; + if (!data) return null; + + const yoy = yoyByYear[year]; + + return ( +
+
+ {year} + {yoy !== null && ( + = 0 ? "text-emerald-600" : "text-rose-500" + )}> + {yoy >= 0 ? "+" : ""}{yoy.toFixed(1)}% + + )} +
+
+ {formatCurrency(data.totals.revenue, 0)} +
+
+ {formatNumber(data.totals.orders)} orders + {formatPercent(data.totals.margin, 0)} margin +
+
+ ); + })} +
+ ); + }; + + // Compact charts - 2x2 grid + const renderCharts = () => ( +
+ {/* Revenue Chart */} + + + + + Revenue by Day + + + +
+ + + + + `$${(value as number / 1000).toFixed(0)}k`} + axisLine={false} + tickLine={false} + tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }} + width={45} + /> + } /> + {sortedYears.map((year) => ( + + ))} + + +
+
+
+ + {/* Orders Chart */} + + + + + Orders by Day + + + +
+ + + + + formatNumber(Number(value))} + axisLine={false} + tickLine={false} + tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }} + width={40} + /> + } /> + {sortedYears.map((year) => ( + + ))} + + +
+
+
+ + {/* Margin Chart */} + + + + + Margin by Day + + + +
+ + + + + `${Number(value).toFixed(0)}%`} + axisLine={false} + tickLine={false} + tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }} + width={35} + /> + } /> + {sortedYears.map((year) => ( + + ))} + + +
+
+
+ + {/* AOV Chart */} + + + + + AOV by Day + + + +
+ + + + + `$${Number(value).toFixed(0)}`} + axisLine={false} + tickLine={false} + tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }} + width={40} + /> + } /> + {sortedYears.map((year) => ( + + ))} + + +
+
+
+
+ ); + + // Compact year comparison table + const renderComparisonTable = () => ( + + + Year-over-Year Comparison + + +
+ + + + Year + Revenue + Orders + AOV + Profit + Margin + + + + {sortedYears.map((year) => { + const entry = dataByYear[year]; + if (!entry) return null; + + const isCurrentYear = year === currentYear; + + return ( + + + {year} + + + {formatCurrency(entry.totals.revenue, 0)} + + + {formatNumber(entry.totals.orders)} + + + {formatCurrency(entry.totals.avgOrderValue, 0)} + + + {formatCurrency(entry.totals.profit, 0)} + + + + {formatPercent(entry.totals.margin, 1)} + + + + ); + })} + +
+
+
+
+ ); + + const renderDebugOverlay = () => { + if (!showDebug) return null; + return ( + + + Debug: Bucket Mapping (first 200) + Raw date - business bucket assignments + + + + + + Year + Source + Raw + Bucket + Index + + + + {debugOverlay.map((entry, idx) => ( + + {entry.year} + {entry.source} + {entry.raw} + {entry.bucket} + {entry.index} + + ))} + +
+
+
+ ); + }; + + // Chart legend + const renderLegend = () => ( +
+ {sortedYears.map((year) => ( +
+
+ + {year} + +
+ ))} +
+ ); + + return ( +
+ {/* Header with live indicator */} + {renderLiveHeader()} + + {/* Realtime shoppers */} + {renderRealtimeStrip()} + + {error && ( + + Unable to load Black Friday data + {error} + + )} + + {loading && !Object.keys(dataByYear).length ? ( + + ) : ( +
+ {/* Current year hero */} + {renderCurrentYearHero()} + + {/* Current year daily breakdown */} + {renderCurrentYearDays()} + + {/* Previous years quick comparison */} + {previousYears.length > 0 && ( +
+

Previous Years

+ {renderPreviousYearsComparison()} +
+ )} + + {/* Charts with legend */} +
+
+

Daily Trends

+ {renderLegend()} +
+ {renderCharts()} +
+ + {/* Comparison table */} + {renderComparisonTable()} + + {renderDebugOverlay()} +
+ )} +
+ ); +} + +export default BlackFridayDashboard; diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo index 9ecdd48..1b9639d 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/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.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/periodselectionpopover.tsx","./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/overview/bestsellers.tsx","./src/components/overview/forecastmetrics.tsx","./src/components/overview/overstockmetrics.tsx","./src/components/overview/overview.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/overview/vendorperformance.tsx","./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/modalwrapper.tsx","./src/components/product-import/components/providers.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/validationstepnew/index.tsx","./src/components/product-import/steps/validationstepnew/types.ts","./src/components/product-import/steps/validationstepnew/components/aivalidationdialogs.tsx","./src/components/product-import/steps/validationstepnew/components/basecellcontent.tsx","./src/components/product-import/steps/validationstepnew/components/initializingvalidation.tsx","./src/components/product-import/steps/validationstepnew/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstepnew/components/upcvalidationtableadapter.tsx","./src/components/product-import/steps/validationstepnew/components/validationcell.tsx","./src/components/product-import/steps/validationstepnew/components/validationcontainer.tsx","./src/components/product-import/steps/validationstepnew/components/validationtable.tsx","./src/components/product-import/steps/validationstepnew/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstepnew/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstepnew/hooks/useaivalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefieldvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefiltermanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useinitialvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useproductlinesfetching.tsx","./src/components/product-import/steps/validationstepnew/hooks/userowoperations.tsx","./src/components/product-import/steps/validationstepnew/hooks/usetemplatemanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniqueitemnumbersvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniquevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useupcvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidationstate.tsx","./src/components/product-import/steps/validationstepnew/hooks/validationtypes.ts","./src/components/product-import/steps/validationstepnew/types/index.ts","./src/components/product-import/steps/validationstepnew/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstepnew/utils/countryutils.ts","./src/components/product-import/steps/validationstepnew/utils/datamutations.ts","./src/components/product-import/steps/validationstepnew/utils/priceutils.ts","./src/components/product-import/steps/validationstepnew/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/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/products.tsx","./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/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.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/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/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/usedebounce.ts","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/brands.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/overview.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/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/emojiutils.ts","./src/utils/naturallanguageperiod.ts","./src/utils/productutils.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/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.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/periodselectionpopover.tsx","./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/overview/bestsellers.tsx","./src/components/overview/forecastmetrics.tsx","./src/components/overview/overstockmetrics.tsx","./src/components/overview/overview.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/overview/vendorperformance.tsx","./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/modalwrapper.tsx","./src/components/product-import/components/providers.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/validationstepnew/index.tsx","./src/components/product-import/steps/validationstepnew/types.ts","./src/components/product-import/steps/validationstepnew/components/aivalidationdialogs.tsx","./src/components/product-import/steps/validationstepnew/components/basecellcontent.tsx","./src/components/product-import/steps/validationstepnew/components/initializingvalidation.tsx","./src/components/product-import/steps/validationstepnew/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstepnew/components/upcvalidationtableadapter.tsx","./src/components/product-import/steps/validationstepnew/components/validationcell.tsx","./src/components/product-import/steps/validationstepnew/components/validationcontainer.tsx","./src/components/product-import/steps/validationstepnew/components/validationtable.tsx","./src/components/product-import/steps/validationstepnew/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstepnew/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstepnew/hooks/useaivalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefieldvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefiltermanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useinitialvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useproductlinesfetching.tsx","./src/components/product-import/steps/validationstepnew/hooks/userowoperations.tsx","./src/components/product-import/steps/validationstepnew/hooks/usetemplatemanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniqueitemnumbersvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniquevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useupcvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidationstate.tsx","./src/components/product-import/steps/validationstepnew/hooks/validationtypes.ts","./src/components/product-import/steps/validationstepnew/types/index.ts","./src/components/product-import/steps/validationstepnew/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstepnew/utils/countryutils.ts","./src/components/product-import/steps/validationstepnew/utils/datamutations.ts","./src/components/product-import/steps/validationstepnew/utils/priceutils.ts","./src/components/product-import/steps/validationstepnew/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/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/products.tsx","./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/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.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/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/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/usedebounce.ts","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/blackfridaydashboard.tsx","./src/pages/brands.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/overview.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/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/emojiutils.ts","./src/utils/naturallanguageperiod.ts","./src/utils/productutils.ts"],"version":"5.6.3"} \ No newline at end of file