diff --git a/inventory/src/components/dashboard/mobile/StudioMobile.tsx b/inventory/src/components/dashboard/mobile/StudioMobile.tsx
new file mode 100644
index 0000000..e0d3439
--- /dev/null
+++ b/inventory/src/components/dashboard/mobile/StudioMobile.tsx
@@ -0,0 +1,1071 @@
+/**
+ * StudioMobile — the /mobile dashboard, Sorbet Studio style.
+ *
+ * One scrolling page tuned to a phone viewport (~420px). Bento-style hero with
+ * a featured revenue tile, then light-chrome cards. Charts and lists here are
+ * mobile-specific simplifications — plain SVG lines instead of recharts, no
+ * axes chrome, no tables — because the full dashboard components don't survive
+ * being squeezed to phone width. Data hooks mirror NightboardSmall/StatCards
+ * one-to-one; only the presentation is new. Deliberately lighter than the full
+ * dashboard and a little richer than the kiosk: scroll buys some depth, but
+ * every section should still read at a glance.
+ */
+import React from "react";
+import { useQuery } from "@tanstack/react-query";
+import { acotService } from "@/services/dashboard/acotService";
+import { apiClient } from "@/utils/apiClient";
+import { apiFetch } from "@/utils/api";
+import config from "@/config";
+import { STUDIO_TILES } from "@/lib/dashboard/designTokens";
+import { useNow } from "@/hooks/useNow";
+// @ts-expect-error - JSX module without type declarations
+import { processBasicData } from "@/components/dashboard/RealtimeAnalytics";
+// @ts-expect-error - JSX module without type declarations
+import { EventDialog } from "@/components/dashboard/EventFeed.jsx";
+
+// ── palette (Sorbet Studio, hex-pinned like the tokens file) ─────────────────
+const C = {
+ ground: "#f8f7f5",
+ ink: "#2b2925",
+ muted: "#7c7870",
+ faint: "#a09b92",
+ border: "#e7e5e1",
+ hairline: "#f0eeea",
+ coral: "#e06a4e",
+ teal: "#0f8f77",
+ up: "#237a4d",
+ down: "#b3503f",
+} as const;
+
+// ── formatters (same conventions as NightboardSmall) ─────────────────────────
+const fmtMoney = (v: number | null | undefined) =>
+ v == null || isNaN(v)
+ ? "—"
+ : new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: 0,
+ }).format(v);
+
+const fmtCompact = (v: number | null | undefined) => {
+ if (v == null || isNaN(v)) return "—";
+ if (Math.abs(v) >= 1_000_000) return `$${(v / 1_000_000).toFixed(2)}M`;
+ if (Math.abs(v) >= 1_000) return `$${(v / 1_000).toFixed(0)}K`;
+ return `$${Math.round(v)}`;
+};
+
+const fmtCount = (v: number | null | undefined) => {
+ if (v == null || isNaN(v)) return "—";
+ if (v >= 10_000) return `${(v / 1_000).toFixed(1)}K`;
+ return v.toLocaleString();
+};
+
+const trendPct = (cur: number, prev: number) =>
+ prev > 0 ? ((cur - prev) / prev) * 100 : null;
+
+const fmtTime = (iso: string | undefined) =>
+ iso
+ ? new Date(iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })
+ : "";
+
+const hourLabel = (h: number) => `${h % 12 || 12} ${h < 12 ? "AM" : "PM"}`;
+
+// ── metric ids for the event feed (same Klaviyo metrics as EventFeed) ────────
+const METRIC_IDS = {
+ PLACED_ORDER: "Y8cqcF",
+ SHIPPED_ORDER: "VExpdL",
+ ACCOUNT_CREATED: "TeeypV",
+ CANCELED_ORDER: "YjVMNg",
+ NEW_BLOG_POST: "YcxeDr",
+ PAYMENT_REFUNDED: "R7XUYh",
+} as const;
+
+const EVENT_META: Record
= {
+ [METRIC_IDS.PLACED_ORDER]: { label: "Order", dot: C.up },
+ [METRIC_IDS.SHIPPED_ORDER]: { label: "Shipped", dot: "#3f7fae" },
+ [METRIC_IDS.ACCOUNT_CREATED]: { label: "Account", dot: "#6b5fc7" },
+ [METRIC_IDS.CANCELED_ORDER]: { label: "Canceled", dot: C.down },
+ [METRIC_IDS.PAYMENT_REFUNDED]: { label: "Refund", dot: "#c9822a" },
+ [METRIC_IDS.NEW_BLOG_POST]: { label: "Blog", dot: C.muted },
+};
+
+const fmtEventMoney = (amount: string | number | undefined) => {
+ const num = typeof amount === "string" ? parseFloat(amount) : amount;
+ if (num == null || isNaN(num)) return "";
+ return `${num < 0 ? "-" : ""}$${Math.abs(num).toFixed(2)}`;
+};
+
+const shipMethod = (method: string | undefined) => {
+ if (!method) return "Digital";
+ if (method.includes("usps")) return "USPS";
+ if (method.includes("fedex")) return "FedEx";
+ if (method.includes("ups")) return "UPS";
+ return "Standard";
+};
+
+// ── response shapes (acotService is untyped JS; mirrors what the API returns) ─
+interface EventProps {
+ ShippingName?: string;
+ OrderId?: string | number;
+ TotalAmount?: string | number;
+ ShipMethod?: string;
+ FirstName?: string;
+ LastName?: string;
+ EmailAddress?: string;
+ CancelReason?: string;
+ FromOrder?: string | number;
+ PaymentAmount?: string | number;
+ PaymentName?: string;
+ title?: string;
+ description?: string;
+}
+
+interface FeedEvent {
+ id: string;
+ metric_id: string;
+ datetime?: string;
+ attributes?: { datetime?: string; event_properties?: EventProps };
+ event_properties: EventProps;
+}
+
+interface TypeSlice {
+ count?: number;
+}
+
+interface TodayStats {
+ revenue: number;
+ orderCount: number;
+ itemCount: number;
+ averageOrderValue: number;
+ averageItemsPerOrder: number;
+ periodProgress: number;
+ prevPeriodRevenue: number;
+ prevPeriodOrders: number;
+ prevPeriodAOV: number;
+ projectedRevenue?: number;
+ orderTypes?: { preOrders?: TypeSlice; localPickup?: TypeSlice; heldItems?: TypeSlice };
+ shipping?: { shippedCount?: number; locations?: { total?: number } };
+ refunds?: { total?: number; count?: number };
+ canceledOrders?: { total?: number; count?: number };
+}
+
+interface Projection {
+ projectedRevenue?: number;
+ projectedOrders?: number;
+}
+
+interface DailyRow {
+ date?: string;
+ timestamp?: string;
+ revenue?: number;
+ orders?: number;
+ prevRevenue?: number;
+ prevOrders?: number;
+ periodProgress?: number;
+}
+
+interface FinancialsResponse {
+ totals?: Record;
+ previousTotals?: Record;
+}
+
+interface TopProduct {
+ id: number | string;
+ name: string;
+ ImgThumb?: string;
+ totalQuantity: number;
+ orderCount: number;
+ totalRevenue: number;
+}
+
+// ── tiny presentational atoms ────────────────────────────────────────────────
+// Beyond a few hundred percent the number is an artifact (day-boundary
+// projections, near-zero baselines) — on a glance page, show nothing.
+const trendVisible = (pct: number | null | undefined): pct is number =>
+ pct != null && isFinite(pct) && Math.abs(pct) >= 0.1 && Math.abs(pct) < 500;
+
+const TrendChip = ({
+ pct,
+ invert = false,
+ big = false,
+}: {
+ pct: number | null | undefined;
+ invert?: boolean;
+ big?: boolean;
+}) => {
+ if (!trendVisible(pct)) return null;
+ const good = invert ? pct < 0 : pct > 0;
+ return (
+
+ {pct > 0 ? "▲" : "▼"} {Math.abs(pct).toFixed(1)}%
+
+ );
+};
+
+/** Percentage-point delta (for margin) — same voice as TrendChip. */
+const PpChip = ({ delta }: { delta: number | null | undefined }) => {
+ if (delta == null || !isFinite(delta) || Math.abs(delta) < 0.1) return null;
+ return (
+ 0 ? C.up : C.down }}>
+ {delta > 0 ? "▲" : "▼"} {Math.abs(delta).toFixed(1)}pp
+
+ );
+};
+
+/** Uppercase eyebrow above a bare strip — the page's only section chrome. */
+const Eyebrow = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+/** White section card with a slim bordered header — chrome kept, weight shed. */
+const SectionCard = ({
+ title,
+ aside,
+ children,
+ className = "",
+}: {
+ title: string;
+ aside?: React.ReactNode;
+ children: React.ReactNode;
+ className?: string;
+}) => (
+
+
+ {title}
+ {aside}
+
+ {children}
+
+);
+
+const QuietCell = ({
+ label,
+ value,
+ sub,
+}: {
+ label: string;
+ value: React.ReactNode;
+ sub?: React.ReactNode;
+}) => (
+
+
+ {label}
+
+
{value}
+ {sub != null && (
+
+ {sub}
+
+ )}
+
+);
+
+/** gap-px strip of QuietCells on a hairline ground */
+const QuietStrip = ({ cols, children }: { cols: 2 | 3; children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+/**
+ * Mobile chart: a plain SVG polyline with a soft area fill and an emphasized
+ * endpoint — no recharts, no axes chrome. Optional dashed previous-period
+ * series shares the same scale (one axis, one story).
+ */
+const MobileLine = ({
+ values,
+ prevValues,
+ height,
+ stroke,
+ fill,
+ prevStroke,
+ startLabel,
+ endLabel,
+ labelColor,
+ baseline,
+}: {
+ values: number[];
+ prevValues?: number[];
+ height: number;
+ stroke: string;
+ fill: string;
+ prevStroke?: string;
+ startLabel?: string;
+ endLabel?: string;
+ labelColor: string;
+ baseline: string;
+}) => {
+ if (values.length < 2) return ;
+ const W = 360;
+ const labelSpace = startLabel || endLabel ? 14 : 0;
+ const H = height - labelSpace;
+ const max = Math.max(...values, ...(prevValues || []), 1);
+ const toXY = (vals: number[]) => {
+ const step = vals.length > 1 ? W / (vals.length - 1) : W;
+ return vals.map((v, i) => ({
+ x: i * step,
+ y: H - 3 - (v / max) * (H - 6),
+ }));
+ };
+ const cur = toXY(values);
+ const prev = prevValues && prevValues.length > 1 ? toXY(prevValues) : null;
+ const curStr = cur.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
+ const prevStr = prev?.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
+ const last = cur[cur.length - 1];
+ return (
+
+ );
+};
+
+// ── main component ───────────────────────────────────────────────────────────
+const StudioMobile = () => {
+ const now = useNow();
+
+ // Today stats (same source as StatCards)
+ const { data: stats, dataUpdatedAt } = useQuery({
+ queryKey: ["mobile-stats-today"],
+ queryFn: async () => {
+ const res = (await acotService.getStats({ timeRange: "today" })) as { stats: TodayStats };
+ return res.stats;
+ },
+ refetchInterval: 60_000,
+ });
+
+ const { data: projection } = useQuery({
+ queryKey: ["mobile-projection-today"],
+ queryFn: async () =>
+ (await acotService.getProjection({ timeRange: "today" })) as Projection,
+ enabled: stats != null && stats.periodProgress < 100,
+ refetchInterval: 60_000,
+ });
+
+ // Today financials for the profit tile (same source as the StatCards hero)
+ const { data: financialsToday } = useQuery({
+ queryKey: ["mobile-financials-today"],
+ queryFn: async () =>
+ (await acotService.getFinancials({ timeRange: "today" })) as FinancialsResponse,
+ refetchInterval: 120_000,
+ });
+
+ // Ops for the shipped tile
+ const { data: opsData } = useQuery({
+ queryKey: ["mobile-ops-today"],
+ queryFn: async () =>
+ // @ts-expect-error - acotService is a JS file, TypeScript can't infer the param type
+ (await acotService.getOperationsMetrics({ timeRange: "today" })) as {
+ totals?: { ordersShipped?: number; piecesPicked?: number };
+ },
+ refetchInterval: 120_000,
+ });
+
+ const { data: realtime } = useQuery({
+ queryKey: ["mobile-realtime-users"],
+ queryFn: async () => {
+ const response = await apiFetch("/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) as { last30MinUsers: number; last5MinUsers: number };
+ },
+ refetchInterval: 30_000,
+ });
+
+ // Event feed (same source as EventFeed)
+ const { data: events = [] } = useQuery({
+ queryKey: ["mobile-event-feed"],
+ queryFn: async () => {
+ const response = await apiClient.get("/api/klaviyo/events/feed", {
+ params: {
+ timeRange: "today",
+ metricIds: JSON.stringify(Object.values(METRIC_IDS)),
+ },
+ });
+ return (response.data.data || []).map((event: FeedEvent) => ({
+ ...event,
+ datetime: event.attributes?.datetime || event.datetime,
+ event_properties: event.attributes?.event_properties || {},
+ }));
+ },
+ refetchInterval: 30_000,
+ });
+
+ // Top sellers (same source as ProductGrid)
+ const { data: products = [] } = useQuery({
+ queryKey: ["mobile-top-products"],
+ queryFn: async () => {
+ const res = (await acotService.getProducts({ timeRange: "today" })) as {
+ stats?: { products?: { list?: TopProduct[] } };
+ };
+ return res.stats?.products?.list || [];
+ },
+ refetchInterval: 300_000,
+ });
+
+ // 30-day dailies (chart + totals), financials, forecast
+ const { data: daily30 = [] } = useQuery({
+ queryKey: ["mobile-daily-30d"],
+ queryFn: async () => {
+ const res = (await acotService.getStatsDetails({
+ timeRange: "last30days",
+ metric: "revenue",
+ daily: true,
+ })) as { stats?: DailyRow[] };
+ return Array.isArray(res.stats) ? res.stats : [];
+ },
+ refetchInterval: 300_000,
+ });
+
+ const { data: financials30 } = useQuery({
+ queryKey: ["mobile-financials-30d"],
+ queryFn: async () =>
+ (await acotService.getFinancials({ timeRange: "last30days" })) as FinancialsResponse,
+ refetchInterval: 300_000,
+ });
+
+ // Inventory band
+ const { data: stockData } = useQuery({
+ queryKey: ["mobile-stock-metrics"],
+ queryFn: async () => {
+ const response = await apiFetch(`${config.apiUrl}/dashboard/stock/metrics`);
+ if (!response.ok) throw new Error("Failed to fetch stock metrics");
+ return response.json();
+ },
+ refetchInterval: 600_000,
+ });
+
+ const { data: replenishData } = useQuery({
+ queryKey: ["mobile-replenish-metrics"],
+ queryFn: async () => {
+ const response = await apiFetch(`${config.apiUrl}/dashboard/replenishment/metrics`);
+ if (!response.ok) throw new Error("Failed to fetch replenishment");
+ return response.json();
+ },
+ refetchInterval: 600_000,
+ });
+
+ const { data: overstockData } = useQuery({
+ queryKey: ["mobile-overstock-metrics"],
+ queryFn: async () => {
+ const response = await apiFetch(`${config.apiUrl}/dashboard/overstock/metrics`);
+ if (!response.ok) throw new Error("Failed to fetch overstock");
+ return response.json();
+ },
+ refetchInterval: 600_000,
+ });
+
+ // ── derived values ─────────────────────────────────────────────────────────
+ const inProgress = stats != null && stats.periodProgress < 100;
+ // Minutes into the business day a projection multiplies noise ("▲ 9,914%").
+ // Glanceability beats completeness here: hide projection-based chips until
+ // enough of the day exists to project from.
+ const projReliable = stats != null && stats.periodProgress >= 10;
+ const projRevenue = projection?.projectedRevenue ?? stats?.projectedRevenue ?? null;
+ const revTrend =
+ stats && projReliable && stats.prevPeriodRevenue > 0
+ ? trendPct(
+ inProgress ? projRevenue ?? stats.revenue : stats.revenue,
+ stats.prevPeriodRevenue
+ )
+ : null;
+ const projOrders =
+ projection?.projectedOrders ??
+ (stats && stats.periodProgress > 0
+ ? Math.round(stats.orderCount / (stats.periodProgress / 100))
+ : null);
+ const ordersTrend =
+ stats && projReliable && stats.prevPeriodOrders > 0
+ ? trendPct(
+ inProgress ? projOrders ?? stats.orderCount : stats.orderCount,
+ stats.prevPeriodOrders
+ )
+ : null;
+ const aovTrend =
+ stats && stats.prevPeriodAOV > 0
+ ? trendPct(stats.averageOrderValue, stats.prevPeriodAOV)
+ : null;
+
+ const profit = financialsToday?.totals?.profit ?? null;
+ const marginToday = financialsToday?.totals?.margin ?? null;
+ // mid-period the raw profit always trails yesterday; scale by projected
+ // revenue the same way the StatCards hero does
+ const profitTrend = (() => {
+ const prev = financialsToday?.previousTotals?.profit;
+ if (!projReliable || profit == null || prev == null || prev <= 0) return null;
+ const scaled =
+ inProgress && stats && stats.revenue > 0 && projRevenue
+ ? profit * (projRevenue / stats.revenue)
+ : profit;
+ return trendPct(scaled, prev);
+ })();
+
+ const ops = opsData?.totals;
+
+ // Hourly revenue shape for the hero curve, bucketed from today's placed-order
+ // feed events (the stats/details endpoint only returns one daily row for
+ // "today", and the feed is uncapped — every order today is already here).
+ // Sums won't exactly reconcile with stats.revenue (cancels/refunds); the
+ // curve is the day's shape, not a ledger.
+ const hourlyCurve = React.useMemo(() => {
+ const totals = new Array(24).fill(0) as number[];
+ let any = false;
+ for (const e of events) {
+ if (e.metric_id !== METRIC_IDS.PLACED_ORDER || !e.datetime) continue;
+ const amt = parseFloat(String(e.event_properties?.TotalAmount ?? ""));
+ if (!Number.isFinite(amt)) continue;
+ totals[new Date(e.datetime).getHours()] += amt;
+ any = true;
+ }
+ if (!any) return null;
+ // business day runs 1 AM → 1 AM (see shared/business-time)
+ const nowH = now.getHours();
+ const values: number[] = [];
+ let h = 1;
+ for (let i = 0; i < 24; i++) {
+ values.push(totals[h]);
+ if (h === nowH) break;
+ h = (h + 1) % 24;
+ }
+ return { values, start: hourLabel(1), end: hourLabel(nowH) };
+ }, [events, now]);
+
+ const daily30Rev = daily30.map((r) => Number(r.revenue) || 0);
+ const daily30Prev = daily30.map((r) => Number(r.prevRevenue) || 0);
+ const sum30 = daily30.reduce<{
+ revenue: number;
+ prevRevenue: number;
+ orders: number;
+ prevOrders: number;
+ }>(
+ (acc, d) => ({
+ revenue: acc.revenue + (Number(d.revenue) || 0),
+ prevRevenue: acc.prevRevenue + (Number(d.prevRevenue) || 0),
+ orders: acc.orders + (Number(d.orders) || 0),
+ prevOrders: acc.prevOrders + (Number(d.prevOrders) || 0),
+ }),
+ { revenue: 0, prevRevenue: 0, orders: 0, prevOrders: 0 }
+ );
+ const avgPerDay = daily30.length ? sum30.revenue / daily30.length : null;
+ const rev30Trend = daily30.length ? trendPct(sum30.revenue, sum30.prevRevenue) : null;
+ const orders30Trend = daily30.length ? trendPct(sum30.orders, sum30.prevOrders) : null;
+ const prevAvgPerDay = daily30.length ? sum30.prevRevenue / daily30.length : null;
+ const avg30Trend =
+ avgPerDay != null && prevAvgPerDay != null ? trendPct(avgPerDay, prevAvgPerDay) : null;
+
+ // margin with the same income-based fallback the nightboard band uses
+ const marginFrom = (t: Record | undefined) => {
+ if (!t) return null;
+ if (Number.isFinite(t.margin)) return t.margin;
+ const income = (t.grossSales || 0) - (t.refunds || 0) - (t.discounts || 0) + (t.shippingFees || 0);
+ return income > 0 ? ((income - (t.cogs || 0)) / income) * 100 : 0;
+ };
+ const margin30 = marginFrom(financials30?.totals);
+ const prevMargin30 = marginFrom(financials30?.previousTotals);
+ const margin30Delta =
+ margin30 != null && prevMargin30 != null ? margin30 - prevMargin30 : null;
+
+ const hour = now.getHours();
+ const greeting = hour < 12 ? "Good morning" : hour < 17 ? "Good afternoon" : "Good evening";
+ const dateLine = now.toLocaleDateString("en-US", {
+ weekday: "long",
+ month: "long",
+ day: "numeric",
+ });
+
+ const tiles = STUDIO_TILES;
+ const rankTones = [tiles.peach, tiles.mint, tiles.lemon, tiles.lilac];
+
+ return (
+
+
+ {/* header — actives live in the hero's realtime tile, so no chip here */}
+
+
{greeting}
+
+ {dateLine}
+
+
+
+ {/* hero — bento: featured revenue + pastel tiles */}
+
+
+
+ Revenue · today
+
+
+
+ {fmtMoney(stats?.revenue)}
+
+
+
+ {stats != null && (
+
+ {inProgress && projReliable && projRevenue != null
+ ? `proj ${fmtCompact(projRevenue)} · `
+ : ""}
+ {stats.prevPeriodRevenue > 0 ? `prev ${fmtCompact(stats.prevPeriodRevenue)}` : ""}
+
+ )}
+ {hourlyCurve != null && hourlyCurve.values.length >= 2 && (
+
+
+
+ )}
+
+
+
+
+ Orders
+
+
+ {stats ? fmtCount(stats.orderCount) : "—"}
+
+
+
+
+ {stats
+ ? `${trendVisible(ordersTrend) ? "· " : ""}${fmtCount(stats.itemCount)} items`
+ : "…"}
+
+
+
+
+
+
+ Gross profit
+
+
+ {profit != null ? fmtMoney(profit) : "—"}
+
+
+
+
+ {marginToday != null && Number.isFinite(marginToday)
+ ? `${trendVisible(profitTrend) ? "· " : ""}${marginToday.toFixed(1)}% margin`
+ : "…"}
+
+
+
+
+
+
+ AOV
+
+
+ {stats?.averageOrderValue != null
+ ? `$${stats.averageOrderValue.toFixed(2)}`
+ : "—"}
+
+
+
+
+ {stats?.averageItemsPerOrder != null
+ ? `${trendVisible(aovTrend) ? "· " : ""}${stats.averageItemsPerOrder.toFixed(1)} items/order`
+ : "…"}
+
+
+
+
+
+
+ Active now
+
+
+
+ {realtime ? fmtCount(realtime.last5MinUsers) : "—"}
+
+
+ {realtime ? `${fmtCount(realtime.last30MinUsers)} last 30 min` : "…"}
+
+
+
+
+ {/* order flow strip */}
+
Order flow · today
+
+
+
+
+
+
+
+
+
+ {/* live feed */}
+
+ today
+
+ }
+ >
+ {events.length === 0 ? (
+
+ Nothing yet today
+
+ ) : (
+
+ {events.slice(0, 40).map((event) => {
+ const meta = EVENT_META[event.metric_id] || { label: "Event", dot: C.muted };
+ const d = event.event_properties || {};
+ let name: string | undefined;
+ let detail: React.ReactNode = null;
+ if (event.metric_id === METRIC_IDS.PLACED_ORDER) {
+ name = d.ShippingName;
+ detail = <>#{d.OrderId} · {fmtEventMoney(d.TotalAmount)}>;
+ } else if (event.metric_id === METRIC_IDS.SHIPPED_ORDER) {
+ name = d.ShippingName;
+ detail = <>#{d.OrderId} · {shipMethod(d.ShipMethod)}>;
+ } else if (event.metric_id === METRIC_IDS.ACCOUNT_CREATED) {
+ name = [d.FirstName, d.LastName].filter(Boolean).join(" ") || d.EmailAddress;
+ detail = d.EmailAddress;
+ } else if (event.metric_id === METRIC_IDS.CANCELED_ORDER) {
+ name = d.ShippingName;
+ detail = (
+ <>
+ #{d.OrderId} · {fmtEventMoney(d.TotalAmount)}
+ {d.CancelReason ? ` · ${d.CancelReason}` : ""}
+ >
+ );
+ } else if (event.metric_id === METRIC_IDS.PAYMENT_REFUNDED) {
+ name = d.ShippingName;
+ detail = <>#{d.FromOrder} · {fmtEventMoney(d.PaymentAmount)}>;
+ } else if (event.metric_id === METRIC_IDS.NEW_BLOG_POST) {
+ name = d.title;
+ detail = d.description;
+ }
+ return (
+
+
+
+
+
+ {name || meta.label}
+
+
+ {meta.label}
+ {detail ? <> · {detail}> : null}
+
+
+
+ {fmtTime(event.datetime)}
+
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* top sellers */}
+
+ today
+
+ }
+ >
+ {products.length === 0 ? (
+
+ No sales yet today
+
+ ) : (
+
+ )}
+
+
+ {/* last 30 days */}
+
+
+
+ this 30d
+
+
+
+ prev
+
+
+ }
+ >
+
+
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+
+ {/* inventory strip */}
+
Inventory
+
+
+
+
+
+
+ {/* footer stamp */}
+
+ {dataUpdatedAt
+ ? `Updated ${new Date(dataUpdatedAt).toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ })} · refreshes automatically`
+ : "Loading…"}
+
+
+
+ );
+};
+
+export default StudioMobile;
diff --git a/inventory/src/components/settings/GlobalSettings.tsx b/inventory/src/components/settings/GlobalSettings.tsx
index 1dc86fc..c524339 100644
--- a/inventory/src/components/settings/GlobalSettings.tsx
+++ b/inventory/src/components/settings/GlobalSettings.tsx
@@ -89,21 +89,6 @@ export function GlobalSettings() {
);
- } else if (setting.setting_key === 'default_forecast_method') {
- return (
-
- );
} else if (setting.setting_key.includes('threshold')) {
// Percentage inputs
return (
diff --git a/inventory/src/components/settings/ProductSettings.tsx b/inventory/src/components/settings/ProductSettings.tsx
index 09b0585..ab8f2c5 100644
--- a/inventory/src/components/settings/ProductSettings.tsx
+++ b/inventory/src/components/settings/ProductSettings.tsx
@@ -6,7 +6,6 @@ import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import config from '../../config';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Search } from 'lucide-react';
import { Switch } from "@/components/ui/switch";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -25,7 +24,6 @@ interface ProductSetting {
lead_time_days: number | null;
days_of_stock: number | null;
safety_stock: number;
- forecast_method: string | null;
exclude_from_forecast: boolean;
updated_at: string;
product_name?: string; // Added for display purposes
@@ -86,7 +84,6 @@ export function ProductSettings() {
lead_time_days: setting.lead_time_days,
days_of_stock: setting.days_of_stock,
safety_stock: setting.safety_stock,
- forecast_method: setting.forecast_method,
exclude_from_forecast: setting.exclude_from_forecast
})
});
@@ -195,7 +192,6 @@ export function ProductSettings() {
Lead Time (days)
Days of Stock
Safety Stock
- Forecast Method
Exclude
Actions
@@ -231,21 +227,6 @@ export function ProductSettings() {
onChange={(e) => updateSetting(setting.pid, 'safety_stock', parseInt(e.target.value) || 0)}
/>
-
-
-
({}));
console.error('Auth check failed:', response.status, errorData);
- // Any failed /me response means the session is invalid — logout
- logout();
+ // Only a genuine auth rejection ends the session. Server errors and
+ // rate limits (500/429/etc.) are transient — keep the session.
+ if (response.status === 401 || response.status === 403) {
+ logout();
+ } else {
+ setError(errorData.error || `Auth check failed (${response.status})`);
+ }
return;
}
diff --git a/inventory/src/hooks/useNow.ts b/inventory/src/hooks/useNow.ts
new file mode 100644
index 0000000..f79e137
--- /dev/null
+++ b/inventory/src/hooks/useNow.ts
@@ -0,0 +1,26 @@
+import { useEffect, useState } from "react";
+
+/**
+ * A Date that stays current: re-renders every `intervalMs` (default 1 min)
+ * and immediately when the tab becomes visible again — so greetings and
+ * date lines roll over even if the page sat open (or backgrounded on a
+ * phone) across a morning/afternoon/midnight boundary.
+ */
+export function useNow(intervalMs = 60_000): Date {
+ const [now, setNow] = useState(() => new Date());
+
+ useEffect(() => {
+ const update = () => setNow(new Date());
+ const id = setInterval(update, intervalMs);
+ const onVisibility = () => {
+ if (document.visibilityState === "visible") update();
+ };
+ document.addEventListener("visibilitychange", onVisibility);
+ return () => {
+ clearInterval(id);
+ document.removeEventListener("visibilitychange", onVisibility);
+ };
+ }, [intervalMs]);
+
+ return now;
+}
diff --git a/inventory/src/pages/MobileDashboard.tsx b/inventory/src/pages/MobileDashboard.tsx
new file mode 100644
index 0000000..f366021
--- /dev/null
+++ b/inventory/src/pages/MobileDashboard.tsx
@@ -0,0 +1,69 @@
+import React, { useEffect, useState } from "react";
+import { Navigate } from "react-router-dom";
+import PinProtection from "@/components/dashboard/PinProtection";
+import StudioMobile from "@/components/dashboard/mobile/StudioMobile";
+import PageLoading from "@/components/ui/page-loading";
+import { apiFetch } from "@/utils/api";
+
+// Pin Protected Layout
+const PinProtectedLayout = ({ children }: { children: React.ReactNode }) => {
+ const [isPinVerified, setIsPinVerified] = useState(() => {
+ return sessionStorage.getItem("pinVerified") === "true";
+ });
+
+ const handlePinSuccess = () => {
+ setIsPinVerified(true);
+ sessionStorage.setItem("pinVerified", "true");
+ };
+
+ if (!isPinVerified) {
+ return ;
+ }
+
+ return <>{children}>;
+};
+
+// Same three-way gate as /small: office IP gets PIN, authenticated users skip
+// PIN, everyone else is bounced to login. Identity comes from
+// /api/dashboard/whoami (behind Caddy's office-IP allowlist for the kiosk case).
+type Identity = "probing" | "kiosk" | "authenticated" | "anonymous";
+
+const AccessGate = ({ children }: { children: React.ReactNode }) => {
+ const [identity, setIdentity] = useState("probing");
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const res = await apiFetch("/api/dashboard/whoami");
+ if (cancelled) return;
+ if (res.status === 401) {
+ setIdentity("anonymous");
+ return;
+ }
+ const body = await res.json();
+ setIdentity(body.is_kiosk ? "kiosk" : "authenticated");
+ } catch {
+ if (!cancelled) setIdentity("anonymous");
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ if (identity === "probing") return ;
+ if (identity === "anonymous") return ;
+ if (identity === "kiosk") return {children};
+ return <>{children}>;
+};
+
+export function MobileDashboard() {
+ return (
+
+
+
+ );
+}
+
+export default MobileDashboard;
diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo
index 404a8b1..29c2e44 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/create-po/addproductsdialog.tsx","./src/components/create-po/confirmationview.tsx","./src/components/create-po/lineitemstable.tsx","./src/components/create-po/pofloatingselectionbar.tsx","./src/components/create-po/reviewmatchesdialog.tsx","./src/components/create-po/supplierselector.tsx","./src/components/create-po/constants.ts","./src/components/create-po/parsespreadsheet.ts","./src/components/create-po/resolveidentifiers.ts","./src/components/create-po/types.ts","./src/components/dashboard/customerservicedashboard.tsx","./src/components/dashboard/financialoverview.tsx","./src/components/dashboard/operationsmetrics.tsx","./src/components/dashboard/payrollmetrics.tsx","./src/components/dashboard/periodselectionpopover.tsx","./src/components/dashboard/nightboard/nightboardsmall.tsx","./src/components/dashboard/shared/businessrangeselect.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/horizontabs.tsx","./src/components/dashboard/shared/studiocontrols.tsx","./src/components/dashboard/shared/index.ts","./src/components/dashboard/studio/sparkline.tsx","./src/components/dashboard/studio/studiostattile.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/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/mappingsignature.ts","./src/components/product-import/steps/validationstep/utils/priceutils.ts","./src/components/product-import/steps/validationstep/utils/upcutils.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/productsummarycards.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/statusbadge.tsx","./src/components/products/columndefinitions.ts","./src/components/purchase-orders/categorymetricscard.tsx","./src/components/purchase-orders/filtercontrols.tsx","./src/components/purchase-orders/ordermetricscard.tsx","./src/components/purchase-orders/paginationcontrols.tsx","./src/components/purchase-orders/pipelinecard.tsx","./src/components/purchase-orders/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.tsx","./src/components/settings/auditlog.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/globalsettings.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/productsettings.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/settings/vendorsettings.tsx","./src/components/shared/index.ts","./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/authed-image.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.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/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/config/dashboard.ts","./src/config/uploads.ts","./src/contexts/authcontext.tsx","./src/contexts/dashboardscrollcontext.tsx","./src/contexts/importsessioncontext.tsx","./src/hooks/use-mobile.tsx","./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/createpurchaseorder.tsx","./src/pages/dashboard.tsx","./src/pages/discountsimulator.tsx","./src/pages/forecasting.tsx","./src/pages/htslookup.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/newsletter.tsx","./src/pages/overview.tsx","./src/pages/producteditor.tsx","./src/pages/productlines.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/repeatorders.tsx","./src/pages/settings.tsx","./src/pages/smalldashboard.tsx","./src/pages/speclookup.tsx","./src/services/apiv2.ts","./src/services/importauditlogapi.ts","./src/services/importsessionapi.ts","./src/services/producteditor.ts","./src/services/producteditorauditlog.ts","./src/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/importsession.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/api.ts","./src/utils/apiclient.ts","./src/utils/businesstime.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/create-po/addproductsdialog.tsx","./src/components/create-po/confirmationview.tsx","./src/components/create-po/lineitemstable.tsx","./src/components/create-po/pofloatingselectionbar.tsx","./src/components/create-po/reviewmatchesdialog.tsx","./src/components/create-po/supplierselector.tsx","./src/components/create-po/constants.ts","./src/components/create-po/parsespreadsheet.ts","./src/components/create-po/resolveidentifiers.ts","./src/components/create-po/types.ts","./src/components/dashboard/customerservicedashboard.tsx","./src/components/dashboard/financialoverview.tsx","./src/components/dashboard/operationsmetrics.tsx","./src/components/dashboard/payrollmetrics.tsx","./src/components/dashboard/periodselectionpopover.tsx","./src/components/dashboard/mobile/studiomobile.tsx","./src/components/dashboard/nightboard/nightboardsmall.tsx","./src/components/dashboard/shared/businessrangeselect.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/horizontabs.tsx","./src/components/dashboard/shared/studiocontrols.tsx","./src/components/dashboard/shared/index.ts","./src/components/dashboard/studio/sparkline.tsx","./src/components/dashboard/studio/studiostattile.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/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/mappingsignature.ts","./src/components/product-import/steps/validationstep/utils/priceutils.ts","./src/components/product-import/steps/validationstep/utils/upcutils.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/productsummarycards.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/statusbadge.tsx","./src/components/products/columndefinitions.ts","./src/components/purchase-orders/categorymetricscard.tsx","./src/components/purchase-orders/filtercontrols.tsx","./src/components/purchase-orders/ordermetricscard.tsx","./src/components/purchase-orders/paginationcontrols.tsx","./src/components/purchase-orders/pipelinecard.tsx","./src/components/purchase-orders/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.tsx","./src/components/settings/auditlog.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/globalsettings.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/productsettings.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/settings/vendorsettings.tsx","./src/components/shared/index.ts","./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/authed-image.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.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/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/config/dashboard.ts","./src/config/uploads.ts","./src/contexts/authcontext.tsx","./src/contexts/dashboardscrollcontext.tsx","./src/contexts/importsessioncontext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/usedebounce.ts","./src/hooks/useimportautosave.ts","./src/hooks/usenow.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/createpurchaseorder.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/mobiledashboard.tsx","./src/pages/newsletter.tsx","./src/pages/overview.tsx","./src/pages/producteditor.tsx","./src/pages/productlines.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/repeatorders.tsx","./src/pages/settings.tsx","./src/pages/smalldashboard.tsx","./src/pages/speclookup.tsx","./src/services/apiv2.ts","./src/services/importauditlogapi.ts","./src/services/importsessionapi.ts","./src/services/producteditor.ts","./src/services/producteditorauditlog.ts","./src/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/importsession.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/api.ts","./src/utils/apiclient.ts","./src/utils/businesstime.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