From 76a8836769b98ede40c1f8218117011ad975162c Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 24 Mar 2026 09:56:51 -0400 Subject: [PATCH] Small dashboard updates --- inventory-server/src/routes/dashboard.js | 73 +++ .../src/components/dashboard/DateTime.jsx | 137 +++--- .../src/components/dashboard/EventFeed.jsx | 26 +- .../dashboard/MiniBusinessMetrics.jsx | 241 ++++++++++ .../components/dashboard/MiniEventFeed.jsx | 94 ++-- .../dashboard/MiniInventorySnapshot.jsx | 111 +++++ .../components/dashboard/MiniSalesChart.jsx | 417 +++++++++--------- .../components/dashboard/MiniStatCards.jsx | 163 +++---- .../shared/DashboardMultiStatCardMini.tsx | 144 ++++++ .../shared/DashboardStatCardMini.tsx | 2 +- .../src/components/dashboard/shared/index.ts | 6 + inventory/src/pages/SmallDashboard.tsx | 28 +- inventory/tsconfig.tsbuildinfo | 2 +- 13 files changed, 982 insertions(+), 462 deletions(-) create mode 100644 inventory/src/components/dashboard/MiniBusinessMetrics.jsx create mode 100644 inventory/src/components/dashboard/MiniInventorySnapshot.jsx create mode 100644 inventory/src/components/dashboard/shared/DashboardMultiStatCardMini.tsx diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index 77cf7c4..ac1e01d 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -989,6 +989,79 @@ router.get('/best-sellers', async (req, res) => { } }); +// GET /dashboard/year-revenue-estimate +// Returns YTD actual revenue + rest-of-year forecast revenue for a full-year estimate +router.get('/year-revenue-estimate', async (req, res) => { + const now = new Date(); + const yearStart = `${now.getFullYear()}-01-01`; + const todayISO = now.toISOString().split('T')[0]; + const yearEndISO = `${now.getFullYear()}-12-31`; + + try { + // YTD actual revenue from orders + const { rows: [ytd] } = await executeQuery(` + SELECT COALESCE(SUM(price * quantity), 0) AS revenue + FROM orders + WHERE date >= $1 AND date <= $2 AND canceled = false + `, [yearStart, todayISO]); + + // Forecast horizon + const { rows: [horizonRow] } = await executeQuery( + `SELECT MAX(forecast_date) AS max_date FROM product_forecasts` + ); + const forecastHorizonISO = horizonRow?.max_date + ? (horizonRow.max_date instanceof Date + ? horizonRow.max_date.toISOString().split('T')[0] + : horizonRow.max_date) + : todayISO; + + const clampedEnd = yearEndISO <= forecastHorizonISO ? yearEndISO : forecastHorizonISO; + + // Forecast revenue from tomorrow to clamped end + const { rows: [forecast] } = await executeQuery(` + SELECT COALESCE(SUM(pf.forecast_revenue), 0) AS revenue + FROM product_forecasts pf + JOIN product_metrics pm ON pm.pid = pf.pid + WHERE pm.is_visible = true + AND pf.forecast_date > $1 AND pf.forecast_date <= $2 + `, [todayISO, clampedEnd]); + + let eoyForecastRevenue = parseFloat(forecast.revenue) || 0; + + // If forecast doesn't cover full year, extrapolate remaining days + if (yearEndISO > forecastHorizonISO) { + const { rows: [tailRow] } = await executeQuery(` + SELECT AVG(daily_rev) AS avg_daily FROM ( + SELECT forecast_date, SUM(pf.forecast_revenue) AS daily_rev + FROM product_forecasts pf + JOIN product_metrics pm ON pm.pid = pf.pid + WHERE pm.is_visible = true + AND pf.forecast_date > ($1::date - INTERVAL '7 days') + AND pf.forecast_date <= $1 + GROUP BY forecast_date + ) sub + `, [forecastHorizonISO]); + + const baselineDaily = parseFloat(tailRow?.avg_daily) || 0; + const horizonDate = new Date(forecastHorizonISO + 'T00:00:00'); + const yearEnd = new Date(yearEndISO + 'T00:00:00'); + const extraDays = Math.round((yearEnd - horizonDate) / (1000 * 60 * 60 * 24)); + eoyForecastRevenue += baselineDaily * extraDays; + } + + const ytdRevenue = parseFloat(ytd.revenue) || 0; + + res.json({ + ytdRevenue, + eoyForecastRevenue, + yearTotal: ytdRevenue + eoyForecastRevenue, + }); + } catch (err) { + console.error('Error fetching year revenue estimate:', err); + res.status(500).json({ error: 'Failed to fetch year revenue estimate' }); + } +}); + // GET /dashboard/sales/metrics // Returns sales metrics for specified period router.get('/sales/metrics', async (req, res) => { diff --git a/inventory/src/components/dashboard/DateTime.jsx b/inventory/src/components/dashboard/DateTime.jsx index 939aa6f..e39dbe3 100644 --- a/inventory/src/components/dashboard/DateTime.jsx +++ b/inventory/src/components/dashboard/DateTime.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useQuery } from "@tanstack/react-query"; import { Card, CardContent } from '@/components/ui/card'; import { Calendar as CalendarComponent } from '@/components/ui/calendaredit'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; @@ -34,8 +35,44 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => { const [prevTime, setPrevTime] = useState(getTimeComponents(new Date())); const [isTimeChanging, setIsTimeChanging] = useState(false); const [mounted, setMounted] = useState(false); - const [weather, setWeather] = useState(null); - const [forecast, setForecast] = useState(null); + const { data: weatherData } = useQuery({ + queryKey: ["weather-current-forecast"], + queryFn: async () => { + const API_KEY = import.meta.env.VITE_OPENWEATHER_API_KEY; + const [weatherResponse, forecastResponse] = await Promise.all([ + fetch( + `https://api.openweathermap.org/data/2.5/weather?lat=43.63507&lon=-84.18995&appid=${API_KEY}&units=imperial` + ), + fetch( + `https://api.openweathermap.org/data/2.5/forecast?lat=43.63507&lon=-84.18995&appid=${API_KEY}&units=imperial` + ) + ]); + + const weather = await weatherResponse.json(); + const forecastData = await forecastResponse.json(); + + const dailyForecasts = forecastData.list.reduce((acc, item) => { + const date = new Date(item.dt * 1000).toLocaleDateString(); + if (!acc[date]) { + acc[date] = { + ...item, + precipitation: item.rain?.['3h'] || item.snow?.['3h'] || 0, + pop: item.pop * 100, + }; + } + return acc; + }, {}); + + return { + weather, + forecast: Object.values(dailyForecasts).slice(0, 5), + }; + }, + refetchInterval: 300000, + }); + + const weather = weatherData?.weather ?? null; + const forecast = weatherData?.forecast ?? null; useEffect(() => { setTimeout(() => setMounted(true), 150); @@ -43,60 +80,18 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => { const timer = setInterval(() => { const newDate = new Date(); const newTime = getTimeComponents(newDate); - + if (newTime.minutes !== prevTime.minutes) { setIsTimeChanging(true); setTimeout(() => setIsTimeChanging(false), 200); } - + setPrevTime(newTime); setDatetime(newDate); }, 1000); return () => clearInterval(timer); }, [prevTime]); - useEffect(() => { - const fetchWeatherData = async () => { - try { - const API_KEY = import.meta.env.VITE_OPENWEATHER_API_KEY; - const [weatherResponse, forecastResponse] = await Promise.all([ - fetch( - `https://api.openweathermap.org/data/2.5/weather?lat=43.63507&lon=-84.18995&appid=${API_KEY}&units=imperial` - ), - fetch( - `https://api.openweathermap.org/data/2.5/forecast?lat=43.63507&lon=-84.18995&appid=${API_KEY}&units=imperial` - ) - ]); - - const weatherData = await weatherResponse.json(); - const forecastData = await forecastResponse.json(); - - setWeather(weatherData); - - // Process forecast data to get daily forecasts with precipitation - const dailyForecasts = forecastData.list.reduce((acc, item) => { - const date = new Date(item.dt * 1000).toLocaleDateString(); - if (!acc[date]) { - acc[date] = { - ...item, - precipitation: item.rain?.['3h'] || item.snow?.['3h'] || 0, - pop: item.pop * 100 // Probability of precipitation as percentage - }; - } - return acc; - }, {}); - - setForecast(Object.values(dailyForecasts).slice(0, 5)); - } catch (error) { - console.error("Error fetching weather:", error); - } - }; - - fetchWeatherData(); - const weatherTimer = setInterval(fetchWeatherData, 300000); - return () => clearInterval(weatherTimer); - }, []); - function getTimeComponents(date) { let hours = date.getHours(); const minutes = date.getMinutes(); @@ -242,7 +237,7 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => { const WeatherDetails = () => (
- +
@@ -252,7 +247,7 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => {
- +
@@ -262,7 +257,7 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => {
- +
@@ -272,7 +267,7 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => {
- +
@@ -282,7 +277,7 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => {
- +
@@ -292,7 +287,7 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => {
- +
@@ -314,7 +309,7 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => { key={index} className={cn( getWeatherBackground(day.weather[0].id, isNight), - "p-2" + "p-2 border-white/[0.08] ring-1 ring-white/[0.05]" )} >
@@ -365,23 +360,23 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => { {/* Time Display */} -
+
- {hours} - : - {minutes} - {ampm} + {hours} + : + {minutes} + {ampm}
{/* Date and Weather Display */} -
+
- + {dateInfo.day} @@ -404,7 +399,7 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => {
{getWeatherIcon(weather.weather[0]?.id, datetime)} - + {Math.round(weather.main.temp)}°
@@ -417,7 +412,7 @@ const DateTimeWeatherDisplay = ({ scaleFactor = 1 }) => { {
{/* Calendar Display */} - + (
); -const EventDialog = ({ event, children }) => { +const EventDialog = ({ event, children, scale }) => { const eventType = EVENT_TYPES[event.metric_id]; if (!eventType) return children; const details = event.event_properties || {}; const Icon = EVENT_ICONS[event.metric_id] || Package; - return ( - - {children} - + const dialogInner = ( + <>
{Icon && } @@ -797,6 +795,24 @@ const EventDialog = ({ event, children }) => { )}
+ + ); + + return ( + + {children} + + {scale ? ( +
+ {dialogInner} +
+ ) : dialogInner}
); diff --git a/inventory/src/components/dashboard/MiniBusinessMetrics.jsx b/inventory/src/components/dashboard/MiniBusinessMetrics.jsx new file mode 100644 index 0000000..885eddb --- /dev/null +++ b/inventory/src/components/dashboard/MiniBusinessMetrics.jsx @@ -0,0 +1,241 @@ +import React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { TrendingUp, DollarSign, Percent, Briefcase } from "lucide-react"; +import { DashboardMultiStatCardMini } from "@/components/dashboard/shared"; +import { acotService } from "@/services/dashboard/acotService"; +import config from "@/config"; + +const fmtK = (value) => { + if (!value && value !== 0) return "$0"; + if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`; + if (value >= 1000) return `$${(value / 1000).toFixed(0)}K`; + return `$${value.toFixed(0)}`; +}; + +const fmtDollars = (value) => { + if (!value && value !== 0) return "$0"; + return `$${Math.round(value).toLocaleString("en-US")}`; +}; + +const fmtYear = (value) => { + if (!value && value !== 0) return "$0"; + if (value >= 1000000) return `$${(value / 1000000).toFixed(2)}M`; + if (value >= 1000) return `$${(value / 1000).toFixed(0)}K`; + return `$${value.toFixed(0)}`; +}; + +const TrendSub = ({ label, trend, suffix = "%", invert = false, decimals = 0 }) => { + if (label == null) return null; + const isPositive = trend > 0; + const isGood = invert ? !isPositive : isPositive; + const arrow = trend > 0 ? "\u2191" : trend < 0 ? "\u2193" : ""; + const trendColor = + trend == null || Math.abs(trend) < 0.1 + ? "" + : isGood + ? "text-emerald-300" + : "text-rose-300"; + const formatted = decimals > 0 + ? Math.abs(trend).toFixed(decimals) + : Math.abs(Math.round(trend)); + + return ( + + {label} + {trend != null && Math.abs(trend) >= 0.1 && ( + + {arrow}{formatted}{suffix} + + )} + + ); +}; + +const MiniBusinessMetrics = () => { + // 30d forecast revenue + const { data: forecastData, isLoading: forecastLoading } = useQuery({ + queryKey: ["mini-forecast-30d"], + queryFn: async () => { + const response = await fetch(`${config.apiUrl}/dashboard/forecast/metrics`); + if (!response.ok) throw new Error("Failed to fetch forecast"); + return response.json(); + }, + refetchInterval: 600000, + }); + + // Year revenue estimate (YTD + rest-of-year forecast) + const { data: yearData, isLoading: yearLoading } = useQuery({ + queryKey: ["mini-year-estimate"], + queryFn: async () => { + const response = await fetch(`${config.apiUrl}/dashboard/year-revenue-estimate`); + if (!response.ok) throw new Error("Failed to fetch year estimate"); + return response.json(); + }, + refetchInterval: 600000, + }); + + // Avg revenue per day (last 30 days via ACOT stats) + const { data: revenueData, isLoading: revenueLoading } = useQuery({ + queryKey: ["mini-avg-revenue-30d"], + queryFn: () => + acotService.getStatsDetails({ + timeRange: "last30days", + metric: "revenue", + daily: true, + }), + refetchInterval: 300000, + }); + + // Financials for profit margin (last 30 days) + const { data: financialData, isLoading: financialLoading } = useQuery({ + queryKey: ["mini-financials-30d"], + queryFn: () => acotService.getFinancials({ timeRange: "last30days" }), + refetchInterval: 300000, + }); + + // Payroll FTE — use employee-metrics with timeRange for true 30d window + const { data: fteData, isLoading: payrollLoading } = useQuery({ + queryKey: ["mini-fte-30d"], + queryFn: () => acotService.getEmployeeMetrics({ timeRange: "last30days" }), + refetchInterval: 300000, + }); + + const loading = + forecastLoading || yearLoading || revenueLoading || financialLoading || payrollLoading; + + // --- Avg revenue per day --- + let avgRevPerDay = 0; + let prevAvgRevPerDay = 0; + let revTrend = 0; + if (revenueData?.stats) { + const stats = Array.isArray(revenueData.stats) ? revenueData.stats : []; + if (stats.length > 0) { + avgRevPerDay = stats.reduce((sum, d) => sum + (d.revenue || 0), 0) / stats.length; + prevAvgRevPerDay = + stats.reduce((sum, d) => sum + (d.prevRevenue || 0), 0) / stats.length; + revTrend = + prevAvgRevPerDay > 0 + ? ((avgRevPerDay - prevAvgRevPerDay) / prevAvgRevPerDay) * 100 + : 0; + } + } + + // --- Profit margin --- + let margin = 0; + let prevMargin = null; + let marginTrend = null; + if (financialData?.totals) { + const t = financialData.totals; + if (Number.isFinite(t.margin)) { + margin = t.margin; + } else { + const income = + (t.grossSales || 0) - (t.refunds || 0) - (t.discounts || 0) + (t.shippingFees || 0); + const profit = income - (t.cogs || 0); + margin = income > 0 ? (profit / income) * 100 : 0; + } + + const prev = financialData.previousTotals; + if (prev) { + if (Number.isFinite(prev.margin)) { + prevMargin = prev.margin; + } else { + const prevIncome = + (prev.grossSales || 0) - + (prev.refunds || 0) - + (prev.discounts || 0) + + (prev.shippingFees || 0); + const prevProfit = prevIncome - (prev.cogs || 0); + prevMargin = prevIncome > 0 ? (prevProfit / prevIncome) * 100 : 0; + } + } + + if (financialData.comparison?.margin?.absolute != null) { + marginTrend = financialData.comparison.margin.absolute; + } else if (prevMargin != null) { + marginTrend = margin - prevMargin; + } + } + + // --- Payroll FTE (true 30d window from employee-metrics) --- + const fte = fteData?.totals?.fte ?? 0; + const prevFte = fteData?.previousTotals?.fte ?? null; + const fteTrend = fteData?.comparison?.fte?.percentage ?? null; + + const ready = + !loading && + forecastData && + yearData && + revenueData?.stats && + financialData?.totals && + fteData?.totals; + + const entries = ready + ? [ + { + icon: TrendingUp, + iconBg: "bg-blue-400", + label: "Forecast", + value: fmtK(forecastData.forecastRevenue), + sub: ( + + {fmtYear(yearData.yearTotal)} for year + + ), + }, + { + icon: DollarSign, + iconBg: "bg-emerald-400", + label: "Avg Revenue/Day", + value: fmtDollars(avgRevPerDay), + sub: ( + + ), + }, + { + icon: Percent, + iconBg: "bg-purple-400", + label: "Profit Margin", + value: `${margin.toFixed(1)}%`, + sub: + prevMargin != null ? ( + + ) : undefined, + }, + { + icon: Briefcase, + iconBg: "bg-orange-400", + label: "Payroll FTE", + value: fte.toFixed(1), + sub: + prevFte != null ? ( + + ) : undefined, + }, + ] + : []; + + return ( + + ); +}; + +export default MiniBusinessMetrics; diff --git a/inventory/src/components/dashboard/MiniEventFeed.jsx b/inventory/src/components/dashboard/MiniEventFeed.jsx index c54133e..93de05c 100644 --- a/inventory/src/components/dashboard/MiniEventFeed.jsx +++ b/inventory/src/components/dashboard/MiniEventFeed.jsx @@ -1,5 +1,6 @@ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useRef } from "react"; import axios from "axios"; +import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, @@ -135,7 +136,7 @@ const EventCard = ({ event }) => { const details = event.event_properties || {}; return ( - +
@@ -317,14 +318,28 @@ const DEFAULT_METRICS = Object.values(METRIC_IDS); const MiniEventFeed = ({ selectedMetrics = DEFAULT_METRICS, }) => { - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [lastUpdate, setLastUpdate] = useState(null); const scrollRef = useRef(null); const [showLeftArrow, setShowLeftArrow] = useState(false); const [showRightArrow, setShowRightArrow] = useState(false); + const { data: events = [], isLoading: loading, error } = useQuery({ + queryKey: ["mini-event-feed", selectedMetrics], + queryFn: async () => { + const response = await axios.get("/api/klaviyo/events/feed", { + params: { + timeRange: "today", + metricIds: JSON.stringify(selectedMetrics), + }, + }); + return (response.data.data || []).map((event) => ({ + ...event, + datetime: event.attributes?.datetime || event.datetime, + event_properties: event.attributes?.event_properties || {} + })); + }, + refetchInterval: 30000, + }); + const handleScroll = () => { if (scrollRef.current) { const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current; @@ -351,62 +366,23 @@ const MiniEventFeed = ({ } }; - const fetchEvents = useCallback(async () => { - try { - setError(null); - - if (events.length === 0) { - setLoading(true); - } - - const response = await axios.get("/api/klaviyo/events/feed", { - params: { - timeRange: "today", - metricIds: JSON.stringify(selectedMetrics), - }, - }); - - const processedEvents = (response.data.data || []).map((event) => ({ - ...event, - datetime: event.attributes?.datetime || event.datetime, - event_properties: event.attributes?.event_properties || {} - })); - - setEvents(processedEvents); - setLastUpdate(new Date()); - - // Scroll to the right after events are loaded - if (scrollRef.current) { - setTimeout(() => { - scrollRef.current.scrollTo({ - left: scrollRef.current.scrollWidth, - behavior: 'instant' - }); - handleScroll(); - }, 0); - } - } catch (error) { - console.error("Error fetching events:", error); - setError(error.message); - } finally { - setLoading(false); + // Scroll to end when events load/change + useEffect(() => { + if (scrollRef.current && events.length > 0) { + setTimeout(() => { + scrollRef.current?.scrollTo({ + left: scrollRef.current.scrollWidth, + behavior: 'instant' + }); + handleScroll(); + }, 0); } - }, [selectedMetrics]); - - useEffect(() => { - fetchEvents(); - const interval = setInterval(fetchEvents, 30000); - return () => clearInterval(interval); - }, [fetchEvents]); - - useEffect(() => { - handleScroll(); }, [events]); return (
- -
+ +
{/* Left fade edge */} {showLeftArrow && (
-
+
{loading && !events.length ? ( ) : error ? ( - + ) : !events || events.length === 0 ? (
diff --git a/inventory/src/components/dashboard/MiniInventorySnapshot.jsx b/inventory/src/components/dashboard/MiniInventorySnapshot.jsx new file mode 100644 index 0000000..2510887 --- /dev/null +++ b/inventory/src/components/dashboard/MiniInventorySnapshot.jsx @@ -0,0 +1,111 @@ +import React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Truck, Warehouse, ShoppingBag, AlertTriangle } from "lucide-react"; +import { DashboardMultiStatCardMini } from "@/components/dashboard/shared"; +import { acotService } from "@/services/dashboard/acotService"; +import config from "@/config"; + +const fmtCurrency = (value) => { + if (!value && value !== 0) return "$0"; + if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`; + if (value >= 1000) return `$${(value / 1000).toFixed(0)}K`; + return `$${value.toFixed(0)}`; +}; + +const fmtNum = (value) => { + if (!value && value !== 0) return "0"; + if (value >= 1000) return `${(value / 1000).toFixed(1)}K`; + return value.toLocaleString(); +}; + +const MiniInventorySnapshot = () => { + // Operations (shipped/picked today) + const { data: opsData, isLoading: opsLoading } = useQuery({ + queryKey: ["mini-ops-today"], + queryFn: () => acotService.getOperationsMetrics({ timeRange: "today" }), + refetchInterval: 120000, + }); + + // Stock metrics + const { data: stockData, isLoading: stockLoading } = useQuery({ + queryKey: ["mini-stock-metrics"], + queryFn: async () => { + const response = await fetch(`${config.apiUrl}/dashboard/stock/metrics`); + if (!response.ok) throw new Error("Failed to fetch stock metrics"); + return response.json(); + }, + refetchInterval: 300000, + }); + + // Replenishment metrics + const { data: replenishData, isLoading: replenishLoading } = useQuery({ + queryKey: ["mini-replenish-metrics"], + queryFn: async () => { + const response = await fetch(`${config.apiUrl}/dashboard/replenishment/metrics`); + if (!response.ok) throw new Error("Failed to fetch replenishment"); + return response.json(); + }, + refetchInterval: 300000, + }); + + // Overstock metrics + const { data: overstockData, isLoading: overstockLoading } = useQuery({ + queryKey: ["mini-overstock-metrics"], + queryFn: async () => { + const response = await fetch(`${config.apiUrl}/dashboard/overstock/metrics`); + if (!response.ok) throw new Error("Failed to fetch overstock"); + return response.json(); + }, + refetchInterval: 300000, + }); + + const loading = opsLoading || stockLoading || replenishLoading || overstockLoading; + const ready = !loading && opsData?.totals && stockData && replenishData && overstockData; + + const t = opsData?.totals; + + const entries = ready + ? [ + { + icon: Truck, + iconBg: "bg-sky-400", + label: "Shipped", + value: `${fmtNum(t.ordersShipped)}`, + sub: `${fmtNum(t.piecesPicked)} pcs picked`, + }, + { + icon: Warehouse, + iconBg: "bg-emerald-400", + label: "Stock Value", + value: fmtCurrency(stockData.totalStockCost), + sub: `${fmtNum(stockData.productsInStock)} products`, + }, + { + icon: ShoppingBag, + iconBg: "bg-amber-400", + label: "Replenish Units", + value: `${fmtNum(replenishData.unitsToReplenish)}`, + sub: `${fmtCurrency(replenishData.replenishmentCost)} cost`, + }, + { + icon: AlertTriangle, + iconBg: overstockData.overstockedProducts > 0 ? "bg-rose-400" : "bg-violet-400", + label: "Overstocked SKUs", + value: `${fmtNum(overstockData.overstockedProducts)}`, + sub: `${fmtCurrency(overstockData.totalExcessCost)} cost`, + }, + ] + : []; + + return ( + + ); +}; + +export default MiniInventorySnapshot; diff --git a/inventory/src/components/dashboard/MiniSalesChart.jsx b/inventory/src/components/dashboard/MiniSalesChart.jsx index ad95275..04c61f2 100644 --- a/inventory/src/components/dashboard/MiniSalesChart.jsx +++ b/inventory/src/components/dashboard/MiniSalesChart.jsx @@ -1,22 +1,20 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React from "react"; +import { useQuery } from "@tanstack/react-query"; import { acotService } from "@/services/dashboard/acotService"; +import { Card, CardContent } from "@/components/ui/card"; import { - Card, - CardContent, -} from "@/components/ui/card"; -import { - LineChart, - Line, + AreaChart, + Area, XAxis, YAxis, - CartesianGrid, Tooltip, ResponsiveContainer, } from "recharts"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { AlertCircle, PiggyBank, Truck } from "lucide-react"; -import { formatCurrency, processData } from "./SalesChart.jsx"; -import { METRIC_COLORS } from "@/lib/dashboard/designTokens"; +import { AlertCircle, PiggyBank, ShoppingCart } from "lucide-react"; +import { formatCurrency } from "./SalesChart.jsx"; +import { PHASE_CONFIG, PHASE_KEYS_WITH_UNKNOWN as PHASE_KEYS } from "@/utils/lifecyclePhases"; +import config from "@/config"; import { DashboardStatCardMini, DashboardStatCardMiniSkeleton, @@ -24,116 +22,80 @@ import { TOOLTIP_THEMES, } from "@/components/dashboard/shared"; -// Brighter chart colors for dark glass backgrounds -const MINI_CHART_COLORS = { - revenue: "#34d399", // emerald-400 - orders: "#60a5fa", // blue-400 +const formatCompactCurrency = (value) => { + if (!value || isNaN(value)) return "$0"; + if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`; + if (value >= 1_000) return `$${(value / 1_000).toFixed(1)}k`; + return `$${Math.round(value)}`; +}; + +const formatFullCurrency = (value) => { + if (!value || isNaN(value)) return "$0"; + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); }; const MiniSalesChart = ({ className = "" }) => { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [visibleMetrics, setVisibleMetrics] = useState({ - revenue: true, - orders: true - }); - const [summaryStats, setSummaryStats] = useState({ - totalRevenue: 0, - totalOrders: 0, - prevRevenue: 0, - prevOrders: 0, - periodProgress: 100 - }); - const [projection, setProjection] = useState(null); - const [projectionLoading, setProjectionLoading] = useState(false); - - const fetchProjection = useCallback(async () => { - if (summaryStats.periodProgress >= 100) return; - - try { - setProjectionLoading(true); - const response = await acotService.getProjection({ timeRange: "last30days" }); - setProjection(response); - } catch (error) { - console.error("Error loading projection:", error); - } finally { - setProjectionLoading(false); - } - }, [summaryStats.periodProgress]); - - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - + const { data: revenueStats, isLoading: statsLoading } = useQuery({ + queryKey: ["mini-sales-stats-30d"], + queryFn: async () => { const response = await acotService.getStatsDetails({ timeRange: "last30days", metric: "revenue", daily: true, }); + if (!response.stats) throw new Error("Invalid response format"); + const stats = Array.isArray(response.stats) ? response.stats : []; + return stats.reduce( + (acc, day) => ({ + totalRevenue: acc.totalRevenue + (Number(day.revenue) || 0), + totalOrders: acc.totalOrders + (Number(day.orders) || 0), + prevRevenue: acc.prevRevenue + (Number(day.prevRevenue) || 0), + prevOrders: acc.prevOrders + (Number(day.prevOrders) || 0), + periodProgress: day.periodProgress || 100, + }), + { totalRevenue: 0, totalOrders: 0, prevRevenue: 0, prevOrders: 0, periodProgress: 100 } + ); + }, + refetchInterval: 300000, + }); - if (!response.stats) { - throw new Error("Invalid response format"); - } + const summaryStats = revenueStats ?? { + totalRevenue: 0, totalOrders: 0, prevRevenue: 0, prevOrders: 0, periodProgress: 100, + }; - const stats = Array.isArray(response.stats) - ? response.stats - : []; + const { data: projection } = useQuery({ + queryKey: ["mini-sales-projection-30d"], + queryFn: () => acotService.getProjection({ timeRange: "last30days" }), + enabled: summaryStats.periodProgress < 100, + refetchInterval: 300000, + }); - const processedData = processData(stats); - - // Calculate totals and growth - const totals = stats.reduce((acc, day) => ({ - totalRevenue: acc.totalRevenue + (Number(day.revenue) || 0), - totalOrders: acc.totalOrders + (Number(day.orders) || 0), - prevRevenue: acc.prevRevenue + (Number(day.prevRevenue) || 0), - prevOrders: acc.prevOrders + (Number(day.prevOrders) || 0), - periodProgress: day.periodProgress || 100, - }), { - totalRevenue: 0, - totalOrders: 0, - prevRevenue: 0, - prevOrders: 0, - periodProgress: 100 + const { data: chartData, isLoading: chartLoading, error } = useQuery({ + queryKey: ["mini-sales-chart-30d"], + queryFn: async () => { + const now = new Date(); + const thirtyDaysAgo = new Date(now); + thirtyDaysAgo.setDate(now.getDate() - 30); + const params = new URLSearchParams({ + startDate: thirtyDaysAgo.toISOString(), + endDate: now.toISOString(), }); - - setData(processedData); - setSummaryStats(totals); - setError(null); - - // Fetch projection if needed - if (totals.periodProgress < 100) { - fetchProjection(); - } - } catch (error) { - console.error("Error fetching data:", error); - setError(error.message); - } finally { - setLoading(false); - } - }, [fetchProjection]); - - useEffect(() => { - fetchData(); - const intervalId = setInterval(fetchData, 300000); - return () => clearInterval(intervalId); - }, [fetchData]); + const response = await fetch(`${config.apiUrl}/dashboard/sales/metrics?${params}`); + if (!response.ok) throw new Error("Failed to fetch sales metrics"); + return response.json(); + }, + refetchInterval: 300000, + }); const formatXAxis = (value) => { if (!value) return ""; const date = new Date(value); - return date.toLocaleDateString([], { - month: "short", - day: "numeric" - }); - }; - - const toggleMetric = (metric) => { - setVisibleMetrics(prev => ({ - ...prev, - [metric]: !prev[metric] - })); + return date.toLocaleDateString([], { month: "numeric", day: "numeric" }); }; if (error) { @@ -141,16 +103,16 @@ const MiniSalesChart = ({ className = "" }) => { Error - Failed to load sales data: {error} + Failed to load sales data: {error.message} ); } - // Helper to calculate trend values (positive = up, negative = down) const getRevenueTrendValue = () => { - const current = summaryStats.periodProgress < 100 - ? (projection?.projectedRevenue || summaryStats.totalRevenue) - : summaryStats.totalRevenue; + const current = + summaryStats.periodProgress < 100 + ? projection?.projectedRevenue || summaryStats.totalRevenue + : summaryStats.totalRevenue; if (!summaryStats.prevRevenue) return 0; return ((current - summaryStats.prevRevenue) / summaryStats.prevRevenue) * 100; }; @@ -162,30 +124,18 @@ const MiniSalesChart = ({ className = "" }) => { return ((current - summaryStats.prevOrders) / summaryStats.prevOrders) * 100; }; - if (loading && !data) { - return ( -
- {/* Stat Cards */} -
- - -
+ const dailyPhase = chartData?.dailySalesByPhase || []; + const phaseBreakdown = chartData?.phaseBreakdown || []; - {/* Chart Card */} - - - - - -
- ); - } + const activePhases = phaseBreakdown + .filter((p) => p.revenue > 0) + .sort((a, b) => b.revenue - a.revenue); return (
{/* Stat Cards */}
- {loading && !data?.length ? ( + {statsLoading ? ( <> @@ -200,19 +150,15 @@ const MiniSalesChart = ({ className = "" }) => { icon={PiggyBank} iconBackground="bg-emerald-400" gradient="slate" - className={!visibleMetrics.revenue ? 'opacity-50' : ''} - onClick={() => toggleMetric('revenue')} /> toggleMetric('orders')} /> )} @@ -221,104 +167,159 @@ const MiniSalesChart = ({ className = "" }) => { {/* Chart Card */} -
- {loading && !data?.length ? ( - - ) : ( - - - - - {visibleMetrics.revenue && ( - formatCurrency(value, false)} + {chartLoading ? ( + <> +
+ +
+
+
+
+ {[48, 40, 56, 52, 44].map((w, i) => ( +
+
+
+
+ ))} +
+
+ + ) : ( + <> +
+ + + - )} - {visibleMetrics.orders && ( - )} - { - if (active && payload && payload.length) { - const timestamp = new Date(payload[0].payload.timestamp); + { + if (!active || !payload?.length) return null; + const dateStr = payload[0]?.payload?.date; + const date = dateStr ? new Date(dateStr) : null; const styles = TOOLTIP_THEMES.stone; + const items = payload + .filter((entry) => entry.value > 0) + .sort((a, b) => b.value - a.value); + const total = items.reduce((sum, entry) => sum + (entry.value || 0), 0); return (
-

- {timestamp.toLocaleDateString([], { - weekday: "short", - month: "short", - day: "numeric" - })} -

+ {date && ( +

+ {date.toLocaleDateString([], { + weekday: "short", + month: "short", + day: "numeric", + })} +

+ )}
- {payload - .filter(entry => visibleMetrics[entry.dataKey]) - .map((entry, index) => ( + {items.map((entry, index) => { + const cfg = PHASE_CONFIG[entry.dataKey] || {}; + return (
- - {entry.name} + + + + {cfg.label || entry.name} + - {entry.dataKey === 'revenue' - ? formatCurrency(entry.value) - : entry.value.toLocaleString()} + {formatFullCurrency(entry.value)}
- ))} + ); + })} + {items.length > 1 && ( +
+ Total + + {formatFullCurrency(total)} + +
+ )}
); - } - return null; - }} - /> - {visibleMetrics.revenue && ( - - )} - {visibleMetrics.orders && ( - - )} - -
- )} -
+ {PHASE_KEYS.map((phase) => { + const cfg = PHASE_CONFIG[phase]; + return ( + + ); + })} + + +
+ + {/* Phase breakdown bar + legend */} + {activePhases.length > 0 && ( +
+ {(() => { + let pos = 0; + const stops = activePhases.flatMap((p) => { + const cfg = PHASE_CONFIG[p.phase] || { color: "#94A3B8" }; + const start = pos; + pos += p.percentage; + return [`${cfg.color} ${start}%`, `${cfg.color} ${pos}%`]; + }); + return ( +
+ ); + })()} +
+ {activePhases.slice(0, 5).map((p) => { + const cfg = PHASE_CONFIG[p.phase] || { label: p.phase, color: "#94A3B8" }; + return ( + + + {cfg.label} + + ); + })} +
+
+ )} + + )}
); }; -export default MiniSalesChart; \ No newline at end of file +export default MiniSalesChart; diff --git a/inventory/src/components/dashboard/MiniStatCards.jsx b/inventory/src/components/dashboard/MiniStatCards.jsx index de87bdc..37439da 100644 --- a/inventory/src/components/dashboard/MiniStatCards.jsx +++ b/inventory/src/components/dashboard/MiniStatCards.jsx @@ -1,26 +1,19 @@ import React, { useState, useEffect, useCallback } from "react"; +import { useQuery } from "@tanstack/react-query"; import { acotService } from "@/services/dashboard/acotService"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { DateTime } from "luxon"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { DollarSign, ShoppingCart, - Package, - AlertCircle, CircleDollarSign, + Users, } from "lucide-react"; +import { processBasicData } from "./RealtimeAnalytics"; // Import the detail view components and utilities from StatCards import { @@ -58,16 +51,45 @@ const MiniStatCards = ({ description = "", compact = false, }) => { - const [stats, setStats] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [lastUpdate, setLastUpdate] = useState(null); const [timeRange, setTimeRange] = useState(initialTimeRange); const [selectedMetric, setSelectedMetric] = useState(null); const [detailDataLoading, setDetailDataLoading] = useState({}); const [detailData, setDetailData] = useState({}); - const [projection, setProjection] = useState(null); - const [projectionLoading, setProjectionLoading] = useState(false); + + // Main stats query + const statsParams = timeRange === "custom" ? { startDate, endDate } : { timeRange }; + const { data: stats, isLoading: loading, error: statsError } = useQuery({ + queryKey: ["mini-stat-cards", timeRange, startDate, endDate], + queryFn: async () => { + const response = await acotService.getStats(statsParams); + return response.stats; + }, + refetchInterval: timeRange === "today" ? 60000 : undefined, + }); + + // Projection query (depends on stats) + const { data: projection, isLoading: projectionLoading } = useQuery({ + queryKey: ["mini-stat-projection", timeRange, startDate, endDate], + queryFn: () => acotService.getProjection(statsParams), + enabled: stats?.periodProgress != null && stats.periodProgress < 100, + refetchInterval: timeRange === "today" ? 60000 : undefined, + }); + + // Realtime users query + const { data: realtimeData = { last30MinUsers: 0, last5MinUsers: 0 }, isLoading: realtimeLoading } = useQuery({ + queryKey: ["mini-realtime-users"], + queryFn: async () => { + const response = await fetch("/api/dashboard-analytics/realtime/basic", { + credentials: "include", + }); + if (!response.ok) throw new Error("Failed to fetch realtime"); + const result = await response.json(); + return processBasicData(result.data); + }, + refetchInterval: 30000, + }); + + const error = statsError?.message ?? null; // Reuse the trend calculation functions const calculateTrend = useCallback((current, previous) => { @@ -125,93 +147,6 @@ const MiniStatCards = ({ return calculateTrend(stats.averageOrderValue, stats.prevPeriodAOV); }, [stats, calculateTrend]); - // Initial load effect - useEffect(() => { - let isMounted = true; - - const loadData = async () => { - try { - setLoading(true); - setStats(null); - - const params = - timeRange === "custom" ? { startDate, endDate } : { timeRange }; - const response = await acotService.getStats(params); - - if (!isMounted) return; - - setStats(response.stats); - setLastUpdate(DateTime.now().setZone("America/New_York")); - setError(null); - } catch (error) { - console.error("Error loading data:", error); - if (isMounted) { - setError(error.message); - } - } finally { - if (isMounted) { - setLoading(false); - } - } - }; - - loadData(); - return () => { - isMounted = false; - }; - }, [timeRange, startDate, endDate]); - - // Load smart projection separately - useEffect(() => { - let isMounted = true; - - const loadProjection = async () => { - if (!stats?.periodProgress || stats.periodProgress >= 100) return; - - try { - setProjectionLoading(true); - const params = - timeRange === "custom" ? { startDate, endDate } : { timeRange }; - const response = await acotService.getProjection(params); - - if (!isMounted) return; - setProjection(response); - } catch (error) { - console.error("Error loading projection:", error); - } finally { - if (isMounted) { - setProjectionLoading(false); - } - } - }; - - loadProjection(); - return () => { - isMounted = false; - }; - }, [timeRange, startDate, endDate, stats?.periodProgress]); - - // Auto-refresh for 'today' view - useEffect(() => { - if (timeRange !== "today") return; - - const interval = setInterval(async () => { - try { - const [statsResponse, projectionResponse] = await Promise.all([ - acotService.getStats({ timeRange: "today" }), - acotService.getProjection({ timeRange: "today" }), - ]); - - setStats(statsResponse.stats); - setProjection(projectionResponse); - setLastUpdate(DateTime.now().setZone("America/New_York")); - } catch (error) { - console.error("Error auto-refreshing stats:", error); - } - }, 60000); - - return () => clearInterval(interval); - }, [timeRange]); // Add function to fetch detail data const fetchDetailData = useCallback( @@ -262,13 +197,13 @@ const MiniStatCards = ({ preloadData(); }, []); // eslint-disable-line react-hooks/exhaustive-deps - if (loading && !stats) { + if (loading) { return (
- +
); } @@ -342,14 +277,18 @@ const MiniStatCards = ({ /> setSelectedMetric("shipping")} + loading={realtimeLoading} />
diff --git a/inventory/src/components/dashboard/shared/DashboardMultiStatCardMini.tsx b/inventory/src/components/dashboard/shared/DashboardMultiStatCardMini.tsx new file mode 100644 index 0000000..1459a0b --- /dev/null +++ b/inventory/src/components/dashboard/shared/DashboardMultiStatCardMini.tsx @@ -0,0 +1,144 @@ +/** + * DashboardMultiStatCardMini + * + * A compact card that displays multiple stats vertically, styled to match + * DashboardStatCardMini's typography and layout. Use for panels like + * Operations Today or Inventory Snapshot where several related metrics + * live inside a single gradient card. + */ + +import React from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import type { LucideIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { type GradientVariant } from "./DashboardStatCardMini"; + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface MultiStatEntry { + /** Lucide icon component */ + icon: LucideIcon; + /** Tailwind bg class for the icon circle (e.g. "bg-emerald-400") */ + iconBg: string; + /** Short uppercase label */ + label: string; + /** Main display value */ + value: string | number; + /** Optional secondary line below value (string or JSX for trend indicators) */ + sub?: React.ReactNode; +} + +export interface DashboardMultiStatCardMiniProps { + /** Optional card title shown above stats */ + title?: string; + /** Array of stats to display vertically */ + entries: MultiStatEntry[]; + /** Gradient preset */ + gradient?: GradientVariant; + /** Additional className */ + className?: string; + /** Loading state — shows skeletons for this many rows */ + loading?: boolean; + /** Number of skeleton rows to show when loading (defaults to entries length or 3) */ + skeletonRows?: number; +} + +// ============================================================================= +// GRADIENT PRESETS (mirrors DashboardStatCardMini) +// ============================================================================= + +const GRADIENT_PRESETS: Record = { + slate: "bg-gradient-to-br from-slate-800/80 to-slate-700/60", + emerald: "bg-gradient-to-br from-emerald-900/80 to-emerald-700/40", + blue: "bg-gradient-to-br from-blue-900/80 to-blue-700/40", + purple: "bg-gradient-to-br from-purple-900/80 to-purple-700/40", + violet: "bg-gradient-to-br from-violet-900/80 to-violet-700/40", + amber: "bg-gradient-to-br from-amber-800/80 to-amber-600/40", + orange: "bg-gradient-to-br from-orange-900/80 to-orange-700/40", + rose: "bg-gradient-to-br from-rose-900/80 to-rose-700/40", + cyan: "bg-gradient-to-br from-cyan-900/80 to-cyan-700/40", + sky: "bg-gradient-to-br from-sky-900/80 to-sky-700/40", + custom: "", +}; + +// ============================================================================= +// INTERNAL COMPONENTS +// ============================================================================= + +const StatRow: React.FC = ({ icon: Icon, iconBg, label, value, sub }) => ( +
+
+ + {label} + +
+ {typeof value === "number" ? value.toLocaleString() : value} +
+ {sub && ( +
{sub}
+ )} +
+
+
+ +
+
+); + +const SkeletonRow: React.FC = () => ( +
+
+
+
+
+
+
+
+); + +// ============================================================================= +// MAIN COMPONENT +// ============================================================================= + +export const DashboardMultiStatCardMini: React.FC = ({ + title, + entries, + gradient = "slate", + className, + loading = false, + skeletonRows, +}) => { + const gradientClass = gradient === "custom" ? "" : GRADIENT_PRESETS[gradient]; + const rowCount = skeletonRows ?? (entries.length || 3); + + return ( + + + {title && ( + + {title} + + )} + {loading ? ( + <> + {Array.from({ length: rowCount }, (_, i) => ( + + ))} + + ) : ( + entries.map((entry, i) => ) + )} + + + ); +}; + +export default DashboardMultiStatCardMini; diff --git a/inventory/src/components/dashboard/shared/DashboardStatCardMini.tsx b/inventory/src/components/dashboard/shared/DashboardStatCardMini.tsx index a40fc61..b9c2a48 100644 --- a/inventory/src/components/dashboard/shared/DashboardStatCardMini.tsx +++ b/inventory/src/components/dashboard/shared/DashboardStatCardMini.tsx @@ -244,7 +244,7 @@ export const DashboardStatCardMini: React.FC = ({ )}
{(subtitle || trend) && ( -
+
{subtitle && ( {subtitle} diff --git a/inventory/src/components/dashboard/shared/index.ts b/inventory/src/components/dashboard/shared/index.ts index a30bbac..1f43ce7 100644 --- a/inventory/src/components/dashboard/shared/index.ts +++ b/inventory/src/components/dashboard/shared/index.ts @@ -99,6 +99,12 @@ export { type GradientVariant, } from "./DashboardStatCardMini"; +export { + DashboardMultiStatCardMini, + type DashboardMultiStatCardMiniProps, + type MultiStatEntry, +} from "./DashboardMultiStatCardMini"; + // ============================================================================= // CHART TOOLTIPS // ============================================================================= diff --git a/inventory/src/pages/SmallDashboard.tsx b/inventory/src/pages/SmallDashboard.tsx index 7bc0e45..37a6f5e 100644 --- a/inventory/src/pages/SmallDashboard.tsx +++ b/inventory/src/pages/SmallDashboard.tsx @@ -4,9 +4,12 @@ import LockButton from "@/components/dashboard/LockButton"; import PinProtection from "@/components/dashboard/PinProtection"; import DateTimeWeatherDisplay from "@/components/dashboard/DateTime"; import MiniStatCards from "@/components/dashboard/MiniStatCards"; -import MiniRealtimeAnalytics from "@/components/dashboard/MiniRealtimeAnalytics"; import MiniSalesChart from "@/components/dashboard/MiniSalesChart"; import MiniEventFeed from "@/components/dashboard/MiniEventFeed"; +// @ts-expect-error - JSX component without type declarations +import MiniBusinessMetrics from "@/components/dashboard/MiniBusinessMetrics"; +// @ts-expect-error - JSX component without type declarations +import MiniInventorySnapshot from "@/components/dashboard/MiniInventorySnapshot"; // Pin Protected Layout const PinProtectedLayout = ({ children }: { children: React.ReactNode }) => { @@ -30,7 +33,7 @@ const PinProtectedLayout = ({ children }: { children: React.ReactNode }) => { const SmallLayout = () => { const DATETIME_SCALE = 2; const STATS_SCALE = 1.65; - const ANALYTICS_SCALE = 1.65; + const PANELS_SCALE = 1.65; const SALES_SCALE = 1.65; const FEED_SCALE = 1.65; @@ -86,15 +89,22 @@ const SmallLayout = () => {
- {/* Mini Realtime Analytics */} -
-
+
- +
+
+ +
+
+ +
+
diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo index 6e46aaa..b801c89 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/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/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/importsessionapi.ts","./src/services/producteditor.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/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/importsessionapi.ts","./src/services/producteditor.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