From 54a2460eacfcea567381ab88df516ec618b8c607 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 17 Jun 2026 15:06:38 -0400 Subject: [PATCH] Time unification --- .gitignore | 4 + TIME_UNIFICATION_PLAN.md | 217 +++++++ .../dashboard/acot-server/db/connection.js | 7 +- .../dashboard/acot-server/routes/customers.js | 2 + .../dashboard/acot-server/routes/discounts.js | 23 +- .../acot-server/routes/employee-metrics.js | 73 ++- .../dashboard/acot-server/routes/events.js | 132 ++-- .../acot-server/routes/operations-metrics.js | 57 +- .../acot-server/routes/payroll-metrics.js | 35 +- .../dashboard/acot-server/utils/timeUtils.js | 317 ---------- .../routes/klaviyo/campaigns.routes.js | 2 +- .../dashboard/routes/klaviyo/events.routes.js | 2 +- .../routes/klaviyo/reporting.routes.js | 2 +- .../services/klaviyo/campaigns.service.js | 2 +- .../services/klaviyo/events.service.js | 2 +- .../services/klaviyo/redis.service.js | 2 +- .../services/klaviyo/reporting.service.js | 2 +- .../dashboard/utils/time.utils.js | 448 -------------- inventory-server/docs/TIME.md | 67 +++ inventory-server/package-lock.json | 10 + inventory-server/package.json | 1 + .../scripts/calculate-metrics-new.js | 6 + .../forecast_engine.cpython-312.pyc | Bin 81621 -> 82084 bytes .../scripts/forecast/forecast_engine.py | 20 +- .../scripts/forecast/run_forecast.js | 4 + inventory-server/scripts/full-update.js | 4 + inventory-server/scripts/import-from-prod.js | 18 +- inventory-server/scripts/import/utils.js | 16 +- .../metrics-new/calculate_brand_metrics.sql | 4 + .../calculate_category_metrics.sql | 4 + .../metrics-new/calculate_vendor_metrics.sql | 4 + .../metrics-new/update_daily_snapshots.sql | 4 + .../update_lifecycle_forecasts.sql | 4 + .../metrics-new/update_periodic_metrics.sql | 4 + .../metrics-new/update_product_metrics.sql | 4 + .../scripts/metrics-new/utils/db.js | 3 + .../shared/business-time/index.js | 562 ++++++++++++++++++ inventory-server/shared/db/pg.js | 3 + inventory-server/shared/package.json | 8 +- inventory-server/src/routes/analytics.js | 23 +- inventory-server/src/routes/dashboard.js | 73 +-- inventory-server/src/routes/import.js | 5 +- inventory-server/src/routes/orders.js | 14 +- inventory-server/src/routes/products.js | 3 +- .../src/routes/purchase-orders.js | 5 +- inventory-server/src/utils/db.js | 8 +- inventory-server/src/utils/dbConnection.js | 5 +- .../components/analytics/InventoryFlow.tsx | 17 +- .../components/analytics/InventoryTrends.tsx | 17 +- .../analytics/InventoryValueTrend.tsx | 17 +- .../src/components/dashboard/DateTime.jsx | 32 +- .../dashboard/FinancialOverview.tsx | 8 +- .../components/dashboard/KlaviyoCampaigns.jsx | 22 +- .../dashboard/OperationsMetrics.tsx | 8 +- .../src/components/dashboard/ProductGrid.jsx | 24 +- .../src/components/dashboard/SalesChart.jsx | 24 +- .../src/components/dashboard/StatCards.jsx | 35 +- .../dashboard/shared/BusinessRangeSelect.tsx | 64 ++ .../dashboard/shared/HorizonTabs.tsx | 60 ++ .../forecasting/DateRangePickerQuick.tsx | 8 +- .../src/components/overview/SalesMetrics.tsx | 10 +- .../ui/date-range-picker-narrow.tsx | 135 ----- inventory/src/pages/BlackFridayDashboard.tsx | 9 +- inventory/src/pages/DiscountSimulator.tsx | 25 +- inventory/src/pages/Forecasting.tsx | 6 +- inventory/src/utils/businessTime.ts | 33 + inventory/tsconfig.tsbuildinfo | 2 +- 67 files changed, 1442 insertions(+), 1329 deletions(-) create mode 100644 TIME_UNIFICATION_PLAN.md delete mode 100644 inventory-server/dashboard/acot-server/utils/timeUtils.js delete mode 100644 inventory-server/dashboard/utils/time.utils.js create mode 100644 inventory-server/docs/TIME.md create mode 100644 inventory-server/shared/business-time/index.js create mode 100644 inventory/src/components/dashboard/shared/BusinessRangeSelect.tsx create mode 100644 inventory/src/components/dashboard/shared/HorizonTabs.tsx delete mode 100644 inventory/src/components/ui/date-range-picker-narrow.tsx create mode 100644 inventory/src/utils/businessTime.ts diff --git a/.gitignore b/.gitignore index 6bc1138..0235abd 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,10 @@ dashboard-server/meta-server/._package-lock.json dashboard-server/meta-server/._services *.tsbuildinfo +# Python bytecode +__pycache__/ +*.pyc + uploads/* uploads/**/* **/uploads/* diff --git a/TIME_UNIFICATION_PLAN.md b/TIME_UNIFICATION_PLAN.md new file mode 100644 index 0000000..aac8164 --- /dev/null +++ b/TIME_UNIFICATION_PLAN.md @@ -0,0 +1,217 @@ +# Time Unification Plan + +Audit date: 2026-06-12. Companion to `IMPORT_METRICS_FIX_PLAN.md` (import/metrics day-bucketing fixes, implemented 2026-06-11). + +> **IMPLEMENTATION STATUS (2026-06-12):** Phases 1–6 code implemented. Shared +> module at `inventory-server/shared/business-time/` (+ frontend mirror +> `inventory/src/utils/businessTime.ts`); both duplicate utils deleted; +> acot-server hour-late bug fixed; `dateStrings: true` on all mysql2 configs +> except the import pipeline (dynamic Chicago offset instead); +> `forecast_engine.py` anchors on `business_today()`; pg pools pin +> `TimeZone=America/Chicago`; `/var/www/ecosystem.config.cjs` has +> `TZ: 'America/Chicago'` (backup at `~/ecosystem.config.cjs.bak.2026-06-12`); +> supervised import+metrics cycle ran clean. Docs at +> `inventory-server/docs/TIME.md`. +> +> **Still pending (Matt):** ① `sudo -u postgres psql -c "ALTER DATABASE +> inventory_db SET timezone = 'America/Chicago';"` ② `pm2 reload +> ecosystem.config.cjs --update-env` (mind the JWT_SECRET shell-var gotcha) +> ③ flush dashboard Redis ④ frontend `npm run build` (deploys) ⑤ Phase 2 +> before/after verification + Klaviyo UTC spot-check. Deferred (non-correctness): +> GA4/Meta footnote labels. (UPDATE 2026-06-13: the two-selector-kit UI +> consolidation — `BusinessRangeSelect` + `HorizonTabs` — is now implemented; see Phase 5.) + +## The Convention (single source of truth) + +**A business day runs 1:00:00am Eastern → 12:59:59am Eastern the next day.** + +Because 1am Eastern == midnight Central year-round (both observe US DST), this is identical to: + +> **Business date = the America/Chicago calendar date.** + +Practical consequences — these are the ONLY approved idioms: + + +| Context | Correct idiom | +| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| PG `timestamptz` → business date | `(ts AT TIME ZONE 'America/Chicago')::date` — or plain `ts::date` / `CURRENT_DATE` once Phase 1 sets the session TZ | +| MySQL DATETIME literal (stores Central wall-clock) → business date | `DATE(col)` — **no hour shift** | +| MySQL WHERE bounds for a business day | Central wall-clock strings: `col >= 'YYYY-MM-DD 00:00:00' AND col < 'YYYY-MM-DD+1 00:00:00'` | +| Luxon | `dt.setZone('America/Chicago')` (real conversion). Never `keepLocalTime`, never a fixed `UTC-05:00` offset | +| Intervals | **Half-open** `[start, nextStart)`. No `-1 minute`, no `-1 ms` endpoints | +| "Today" in JS | a `businessToday()` helper (Chicago date), never `new Date().toISOString().split('T')[0]` (UTC) and never bare `CURRENT_DATE` reasoning in Node | + + +## Verified facts (2026-06-11/12) + +- MySQL (192.168.1.5) session/global TZ = SYSTEM = America/Chicago. `NOW()` = UTC-5 (CDT) ✓ live-checked. +- `_order.date_placed`, `_order.date_shipped` are DATETIME storing **Central wall-clock** ✓ (recent order: `date_placed 18:28` at 23:45 UTC). +- `timeclock.TimeStamp` stores **Central wall-clock** ✓ (8am-ET punch-ins recorded as ~07:03–07:06; `NOW()` = 08:14 at ~9:14am ET). +- `picking_ticket.createddate` — PHP-written, Central (same as all PHP DATETIME writes; spot-check during Phase 2 anyway). +- PostgreSQL `inventory_db` session TZ = **Europe/Berlin** ✓ (`SHOW timezone`). +- Node processes on netcup run with system TZ Europe/Berlin (no `TZ` in `.env`/ecosystem). +- Browsers/office = America/New_York. +- GA4 property reporting TZ = America/New_York (per Matt); API `NdaysAgo` resolves in property TZ. +- Klaviyo account TZ = Eastern; API documented to return UTC — **verify in Phase 5** (one event spot-check). +- PG columns: `orders.date`, `receivings.received_date`, `purchase_orders.date`, `products.created_at/date_online`, `product_metrics.last_calculated` = timestamptz; `stock_snapshots.snapshot_date`, `daily_product_snapshots.snapshot_date`, `purchase_orders.expected_date`, `product_forecasts.forecast_date` = DATE. + +## Known defects this plan fixes + +1. **acot-server dashboard windows are 1 hour late** (today = 2am–2am ET): `toDatabaseSqlString()` in `dashboard/acot-server/utils/timeUtils.js` passes Eastern wall-clock (via `keepLocalTime: true` + fixed `UTC-05:00`) to a Central-literal DB. +2. **Fixed `UTC-05:00` offsets** in `timeUtils.js` (`DB_TIMEZONE`) and `events.js:639` — wrong every winter (CST = UTC-6). +3. `**DATE_SUB(col, INTERVAL 1 HOUR)` daily bucketing** in operations/employee metrics — assumed Eastern storage; columns are Central, so buckets are also 1h late. +4. **payroll-metrics punch parsing**: mysql2 `timezone: 'Z'` turns `07:58` Central into `07:58Z`, then luxon renders it as 03:58 ET — punches shifted −5h before pay-period math; overnight punches (midnight–5am CT) land on the wrong day. +5. **PG routes use Berlin "today"**: every `CURRENT_DATE` / `NOW()` / `::date` / `DATE(col)` in `src/routes/`* and the window anchors in `scripts/metrics-new/*.sql` flip to the next business day at 6pm ET. +6. **JS UTC "today"** (`new Date().toISOString().split('T')[0]`) in `src/routes/dashboard.js` and others flips at 8pm ET — a third clock. +7. **DATE-column serialization off-by-one**: node-postgres returns `DATE` as a JS Date at *Berlin local midnight* (= 22:00/23:00 UTC the previous day); `d.toISOString().split('T')[0]` then emits the **previous day**. Affects `src/routes/dashboard.js` (~lines 335, 379, 478, 542, 779, 799, 812) and `src/routes/products.js:1006` wherever the value is a true DATE column. +8. **Two duplicate luxon utils disagree**: `TimeManager.getDayEnd` = next 1am −1 *minute* (59.999s hole); `timeUtils.getDayEnd` = −1 ms. `TimeManager.groupEventsByInterval` groups by `startOf('day')` in the event string's own zone — neither ET nor the 1am rule. +9. **Routes that ignore the convention**: `discounts.js` (zoneless `DateTime.fromISO` + `startOf('day')` = Berlin midnight), `customers.js` (MySQL `NOW()` windows), `events.js` hourly chart (Central hour labels shown to an Eastern office). +10. **Three mysql2 timezone configs for the same DB**: import `'-05:00'` (+ dynamic outbound fix), acot-server `'Z'`, `src/utils/dbConnection.js` + `src/routes/import.js` `'Z'`. +11. **Frontend**: custom ranges are browser-local midnight→23:59:59 (not business days); Forecasting's "Last Month" preset = *trailing* month while everywhere else it's *calendar* month; BlackFridayDashboard does UTC date math; 5 different range-option vocabularies; `date-range-picker-narrow.tsx` is dead code. + +--- + +## Phase 1 — PostgreSQL & Node clocks (highest leverage, smallest diff) + +**1.1 Set PG database timezone to business time.** + +```sql +ALTER DATABASE inventory_db SET timezone = 'America/Chicago'; +``` + +Then reconnect all pools (PM2 reload, see gotcha note at bottom; the 15-min cron picks it up on next run automatically since it opens fresh connections). + +Effect: every `CURRENT_DATE`, `NOW()::date`, `ts::date`, `DATE(ts)`, `date_trunc('day', ts)` in routes and metrics SQL becomes business-correct *without touching the queries*. Explicit `AT TIME ZONE 'America/Chicago'` expressions are unaffected (they're absolute) — leave them; they now read as documentation. + +- [ ] Run `ALTER DATABASE`; verify with `SHOW timezone` from a fresh `psql` and from the app pool (add a temporary log of `SELECT current_setting('timezone')` on pool connect). +- [ ] Belt-and-suspenders: also set it in the pool config in `src/utils/db.js` (`options: '-c TimeZone=America/Chicago'` or a `SET TIME ZONE` on connect) so a DB restore/migration can't silently revert it. Same for the pg pools in `scripts/import/`* and `scripts/forecast/run_forecast.js`. +- [ ] Sweep for anything that *wanted* UTC or Berlin semantics: `grep -rn "NOW()\|CURRENT_DATE\|CURRENT_TIMESTAMP" src/ scripts/ db/`. Audit found none — confirm. (Pipeline bookkeeping like `last_calculated >= NOW() - INTERVAL '5 minutes'` is interval-based and unaffected.) +- [ ] Check `forecast_engine.py`: any `datetime.now()`/`date.today()` used to anchor horizons should become Chicago-aware (`zoneinfo.ZoneInfo('America/Chicago')`). The engine writes `forecast_date` DATE rows — those must mean business dates. + +**1.2 Set Node process TZ to business time.** +Add `TZ=America/Chicago` to the PM2 ecosystem env for `inventory-server` and `dashboard-server` (and the cron wrapper for `full-update.js`). + +Why: (a) fixes defect #7 — node-postgres will materialize DATE columns at *Chicago* midnight, which round-trips through `toISOString().split('T')[0]` to the **same** date (Chicago is west of UTC); (b) makes incidental `new Date()` / `setHours` / `getHours` math business-aligned instead of Berlin. + +- [ ] Add `TZ` to `ecosystem.config.cjs` env blocks; `pm2 reload ecosystem.config.cjs --update-env` (NOT `pm2 restart` — see PM2 env gotcha). +- [ ] Even with TZ set, replace the `.toISOString().split('T')[0]` idiom on DATE columns with `::text` in the SQL (cheapest, unambiguous) or a `formatDateCol()` helper — do this opportunistically in Phase 4; the TZ change makes it correct immediately. +- [ ] Note: `new Date().toISOString()` still yields UTC instants — fine for instants, never for "today". `businessToday()` from the shared module (Phase 3) is the only sanctioned "today". + +**Verification (Phase 1):** + +- [ ] Before/after at ~7pm ET (the danger window): `SELECT CURRENT_DATE` must equal the Chicago date, not tomorrow. +- [ ] `GET /api/dashboard` forecast series: dates must match `SELECT forecast_date::text` directly from PG (defect #7 regression check). +- [ ] Run a full `full-update.js` cycle; diff `product_metrics` sales_30d counts for a handful of pids against hand-computed Chicago windows. + +## Phase 2 — Fix the acot-server hour-late bug (biggest visible-number impact) + +All in `dashboard/acot-server/` (= `/var/www/inventory/dashboard/acot-server/`). + +**2.1 `utils/timeUtils.js`:** + +- [ ] Replace `DB_TIMEZONE = 'UTC-05:00'` with `DB_TIMEZONE = 'America/Chicago'`. +- [ ] `toDatabaseSqlString()`: `normalized.setZone(DB_TIMEZONE).toFormat(...)` — **delete `keepLocalTime: true*`*. 1:00am ET now correctly serializes as `00:00:00` (Central). +- [ ] `parseBusinessDate()` / `formatMySQLDate()`: same zone constant; they become DST-correct automatically. +- [ ] `getDayEnd()`: switch to half-open — return next day-start; change `getTimeRangeConditions` to emit `date_placed >= ? AND date_placed < ?`. (Eliminates the −1ms endpoint and any boundary-order ambiguity.) +- [ ] Keep `BUSINESS_DAY_START_HOUR = 1` ET — it's the same convention; the *conversion* was the bug. + +**2.2 Daily bucketing in routes — remove the hand-rolled hour shift:** + +- [ ] `routes/operations-metrics.js` (~~184–214) and `routes/employee-metrics.js` (~~324–351): `DATE_FORMAT(DATE_SUB(col, INTERVAL 1 HOUR), '%Y-%m-%d')` → `DATE(col)` for `pt.createddate`, `o.date_shipped`, `tc.TimeStamp` (all Central literals — verified). Update the misleading "Eastern timezone" comments. +- [ ] Where the JS then re-parses those date strings (`DateTime.fromFormat(date, 'yyyy-MM-dd', { zone: TIMEZONE })`) keep zone ET only for *labeling*; the string itself is already the business date — pass it through. + +**2.3 `routes/events.js`:** + +- [ ] Line ~639: `DateTime.fromSQL(value, { zone: 'UTC-05:00' })` → `{ zone: 'America/Chicago' }` (or route through `parseBusinessDate`). +- [ ] Hourly chart (today/yesterday): currently `HOUR(date_placed)` returns **Central** hours; office reads the axis as Eastern. Convert for display: emit the business timestamp and format hours in ET on the frontend, or `HOUR(CONVERT_TZ(date_placed,'America/Chicago','America/New_York'))` if the MySQL tz tables are loaded (verify with `SELECT CONVERT_TZ(NOW(),'America/Chicago','America/New_York')`; fall back to `ADDTIME(date_placed, '01:00:00')` — ET = CT + 1h always). Same for the `currentHour` source. +- [ ] Previous-period custom math (~553–561) is instant-math and stays as-is once bounds are correct. + +**2.4 Routes not on the util:** + +- [ ] `routes/discounts.js` (~62–85): replace zoneless `DateTime.fromISO` + Berlin `startOf('day')`/`endOf('day')` with `getTimeRangeConditions('custom', …)` / business-day helpers. +- [ ] `routes/customers.js` (~238–244): `DATE_SUB(NOW(), INTERVAL 3 MONTH)` is a rough trailing window — acceptable; add a comment that `NOW()` here is Central and that's intended. (Or switch to bound params from the util for uniformity.) +- [ ] `routes/payroll-metrics.js`: punches must be interpreted as Central. Either (preferred, see 2.5) get strings from the driver and parse with `DateTime.fromSQL(s, { zone: 'America/Chicago' })`, or correct the `'Z'`-mangled Dates explicitly. All pay-period boundary math stays in ET — that's display convention; the parse is what's broken. Re-test: a 7:58am-CT punch must render 8:58am ET and land on the right pay-period day; test an overnight punch (e.g. 00:30 CT). + +**2.5 Driver config — make values unambiguous:** + +- [ ] `db/connection.js`: add `dateStrings: true` (and drop `timezone: 'Z'`). All DATETIME/TIMESTAMP values arrive as literal strings; every consumer parses through the shared helper with zone Chicago. This converts the implicit-zone footgun into a compile-time-visible parse step. Sweep acot-server routes for `instanceof Date` assumptions and `new Date(row.X)` on datetime fields and route them through the parser. + +**Verification (Phase 2):** + +- [ ] Live-DB boundary check: place/find an order with `date_placed` between `00:00` and `01:00` CT (i.e. 1–2am ET); `timeRange=today` must include orders from `00:00:00` CT onward. Before the fix it starts at `01:00:00` CT. +- [ ] Compare `/stats?timeRange=yesterday` totals before/after; expect ~the 1am–2am ET hour to migrate between days. +- [ ] Employee daily hours for a known day vs the punch list by hand. +- [ ] Flush the dashboard Redis cache after deploy (cached responses are keyed by `timeRange` and hold pre-fix numbers). + +## Phase 3 — One shared business-time module (kill the duplicate utils) + +Create `inventory-server/shared/business-time/` (consistent with the shared-lib location and the side-service convention — this is a leaf dependency, not a middle layer). Export, roughly: + +``` +BUSINESS_TZ = 'America/Chicago' // canonical +OFFICE_TZ = 'America/New_York' // display only +businessToday() // Chicago calendar date (DateTime) +businessDayStart(d) / businessDayRange(d) // [start, nextStart) +rangeForPreset(name, now?) // today|yesterday|last7days|...|lastMonth — one vocabulary +previousRange(name|range) +toMySqlBound(dt) // Central wall-clock 'YYYY-MM-DD HH:mm:ss' +parseMySql(s) // zone Chicago → DateTime +businessDateOf(instant) // → 'YYYY-MM-DD' +formatOffice(dt, fmt) // ET display formatting +``` + +- [ ] Port `timeUtils.js` consumers (events/operations/employee/discounts/payroll) to it; delete `dashboard/acot-server/utils/timeUtils.js`. +- [ ] Port `TimeManager` consumers (klaviyo routes + `events.service.js`, `campaigns.service.js`, `reporting.service.js`, `redis.service.js`) to it; delete `dashboard/utils/time.utils.js`. Klaviyo API bounds stay UTC ISO (that part was right). +- [ ] Fix `groupEventsByInterval` while porting: group by `businessDateOf(eventInstant)` (day) and `setZone(OFFICE_TZ)` for hour/week/month — never the event string's own offset. +- [ ] One range vocabulary: the union of today's two sets — `today, yesterday, twoDaysAgo, thisWeek, lastWeek, thisMonth, lastMonth, last7days, last30days, last90days, previous7/30/90days, custom`. Both servers accept exactly this list; unknown → 400, not a silent default. +- [ ] ESM/CJS: dashboard tree is ESM, `src/` is CJS — publish the module dual (or `.mjs` with a tiny CJS wrapper), same pattern as existing shared lib. +- [ ] Klaviyo verification (per Matt): pull one known recent order event and confirm `attributes.datetime` is UTC (`Z`/+00:00) and matches the PG `orders.date` instant. If it ever arrives as ET-naive, add an explicit zone at the single parse point in the module. + +## Phase 4 — inventory-server `src/routes` cleanup (mostly mechanical after Phase 1) + +- [ ] `dashboard.js`: replace `new Date()`/`toISOString().split('T')[0]` "today"/default-range derivations (~309–316, 1041–1043, 1115–1120) with `businessToday()` / date-only strings; emit DATE columns via `::text` (lines ~335, 379, 478, 542, 769–815). +- [ ] `analytics.js`: default ranges (~19–24, 180–184) — drop the UTC truncation dance; accept `YYYY-MM-DD` business dates, default via `businessToday()`. `o.date::date` joins become business-correct after Phase 1 — change to the explicit `AT TIME ZONE 'America/Chicago'` idiom anyway in the hot endpoints so they don't regress if session TZ ever changes. +- [ ] `orders.js`: `fromDate/toDate` (~~13–45) — treat date-only input as business-day bounds (start of `from`'s business day to start of day after `to`), not UTC midnights; `DATE(date)` windows (~~115–131) fine post-Phase 1. +- [ ] `products.js:1006`, `purchase-orders.js:719/772` (UTC-today defaults), `repeat-orders.js`, `newsletter.js`: same two substitutions (business today; `::text` for DATE output). All `CURRENT_DATE` usages are correct after Phase 1 — leave them. +- [ ] `metrics-new/*.sql`: window anchors (`CURRENT_DATE - INTERVAL '59 days'` etc., `update_product_metrics.sql:11` `_current_date`) become business-correct via Phase 1 — no edits required; add a header comment to each file stating the session-TZ assumption. +- [ ] Standardize `src/utils/dbConnection.js` and the inline config in `src/routes/import.js` to `dateStrings: true` + shared parsing (they currently use `timezone: 'Z'`; consumers are product-text-centric so risk is low, but one convention). +- [ ] Import pipeline (`scripts/import/`): keep `adjustDateForMySQL()` for outbound params (it's the correct dynamic approach) — or migrate to `dateStrings: true` + `toMySqlBound()` for symmetry. Either way, inbound DATETIME *reads* must stop being parsed at fixed `-05:00`: audit `purchase-orders.js`/`daily-deals.js` for fields that survive into PG as instants and route them through `parseMySql()`. (`orders` uses `stamp` TIMESTAMP — wire-format follows session tz = Central; same parse rule applies.) + +## Phase 5 — Frontend consolidation + +**Principle: the server owns all boundary math.** The client sends either a named range or date-only strings (`YYYY-MM-DD`, meaning business dates); it never sends `toISOString()` of a browser-local midnight. + +- [x] **Two sanctioned selector kits** — *implemented 2026-06-13* at `inventory/src/components/dashboard/shared/{BusinessRangeSelect,HorizonTabs}.tsx`. + 1. `BusinessRangeSelect` — the `TIME_RANGES` presets. Migrated StatCards (×2), SalesChart, ProductGrid, KlaviyoCampaigns (all were identical ` - - - - - {TIME_RANGES.map((option) => ( - - {option.label} - - ))} - - + } /> diff --git a/inventory/src/components/dashboard/OperationsMetrics.tsx b/inventory/src/components/dashboard/OperationsMetrics.tsx index 56852ff..2636faa 100644 --- a/inventory/src/components/dashboard/OperationsMetrics.tsx +++ b/inventory/src/components/dashboard/OperationsMetrics.tsx @@ -47,6 +47,7 @@ import { METRIC_COLORS, } from "@/components/dashboard/shared"; import { Tooltip as TooltipUI, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { toDateOnly } from "@/utils/businessTime"; type ComparisonValue = { absolute: number | null; @@ -335,8 +336,11 @@ const OperationsMetrics = () => { return; } params.timeRange = "custom"; - params.startDate = requestRange.start.toISOString(); - params.endDate = requestRange.end.toISOString(); + // Date-only strings = business dates; the server resolves them to + // business-day bounds (1am ET / Chicago midnight). Never send + // browser-local-midnight instants. + params.startDate = toDateOnly(requestRange.start); + params.endDate = toDateOnly(requestRange.end); } // @ts-expect-error - acotService is a JS file, TypeScript can't infer the param type diff --git a/inventory/src/components/dashboard/ProductGrid.jsx b/inventory/src/components/dashboard/ProductGrid.jsx index 5c67a2e..1ecce7c 100644 --- a/inventory/src/components/dashboard/ProductGrid.jsx +++ b/inventory/src/components/dashboard/ProductGrid.jsx @@ -12,16 +12,9 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { BusinessRangeSelect } from "@/components/dashboard/shared/BusinessRangeSelect"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { TIME_RANGES } from "@/lib/dashboard/constants"; import { cn } from "@/lib/utils"; import { Skeleton } from "@/components/ui/skeleton"; import { @@ -235,21 +228,10 @@ const ProductGrid = ({ )} - + /> {isSearchVisible && !error && ( diff --git a/inventory/src/components/dashboard/SalesChart.jsx b/inventory/src/components/dashboard/SalesChart.jsx index 0394dc7..df932a1 100644 --- a/inventory/src/components/dashboard/SalesChart.jsx +++ b/inventory/src/components/dashboard/SalesChart.jsx @@ -1,13 +1,7 @@ import React, { useState, useEffect, useCallback, memo } from "react"; import { acotService } from "@/services/dashboard/acotService"; import { Card, CardContent } from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { BusinessRangeSelect } from "@/components/dashboard/shared/BusinessRangeSelect"; import { Button } from "@/components/ui/button"; import { TrendingUp } from "lucide-react"; import { @@ -21,7 +15,6 @@ import { Legend, ReferenceLine, } from "recharts"; -import { TIME_RANGES } from "@/lib/dashboard/constants"; import { Table, TableHeader, @@ -463,21 +456,10 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { // Time selector for DashboardSectionHeader const timeSelector = ( - + /> ); // Actions (Details dialog) for DashboardSectionHeader diff --git a/inventory/src/components/dashboard/StatCards.jsx b/inventory/src/components/dashboard/StatCards.jsx index a1a29b7..9e45b51 100644 --- a/inventory/src/components/dashboard/StatCards.jsx +++ b/inventory/src/components/dashboard/StatCards.jsx @@ -7,13 +7,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { BusinessRangeSelect } from "@/components/dashboard/shared/BusinessRangeSelect"; import { Dialog, DialogContent, @@ -49,7 +43,6 @@ import { MapPin, } from "lucide-react"; import { DateTime } from "luxon"; -import { TIME_RANGES } from "@/lib/dashboard/constants"; import { CARD_STYLES } from "@/lib/dashboard/designTokens"; import { DashboardStatCard, @@ -1475,18 +1468,7 @@ const StatCards = ({ Last updated: {lastUpdate.toFormat("hh:mm a")} )} - + @@ -1547,18 +1529,7 @@ const StatCards = ({
- +
diff --git a/inventory/src/components/dashboard/shared/BusinessRangeSelect.tsx b/inventory/src/components/dashboard/shared/BusinessRangeSelect.tsx new file mode 100644 index 0000000..ab56f51 --- /dev/null +++ b/inventory/src/components/dashboard/shared/BusinessRangeSelect.tsx @@ -0,0 +1,64 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { TIME_RANGES } from "@/lib/dashboard/constants"; +import { cn } from "@/lib/utils"; + +/** + * BusinessRangeSelect — the single shared named-range preset selector for + * dashboards. Renders the canonical TIME_RANGES presets (Today / Yesterday / + * Last 7|30|90 Days / This|Last Week / This|Last Month). + * + * The selected `value` is a *named range* the server resolves to business-day + * (1am ET) bounds — the client sends the name, never a browser-local + * toISOString(). See inventory-server/docs/TIME.md. + * + * Replaces the duplicated + + + + + {options.map((range) => ( + + {range.label} + + ))} + + + ); +} + +export default BusinessRangeSelect; diff --git a/inventory/src/components/dashboard/shared/HorizonTabs.tsx b/inventory/src/components/dashboard/shared/HorizonTabs.tsx new file mode 100644 index 0000000..be8fd9d --- /dev/null +++ b/inventory/src/components/dashboard/shared/HorizonTabs.tsx @@ -0,0 +1,60 @@ +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +/** + * HorizonTabs — the single shared "trailing window" selector for analytics and + * overview cards. The value is a number of days; the server interprets it as a + * business-day window ending today (see inventory-server/docs/TIME.md). + * + * Replaces the per-card duplication this consolidated: + * - SalesMetrics (shadcn , 7 / 30 / 90) + * - InventoryFlow (raw button-group, 30 / 90) + * - InventoryTrends (raw button-group, 30 / 90 / 365) + * - InventoryValueTrend(raw button-group, 30 / 90 / 365) + * + * Note: ForecastMetrics keeps its own selector — its third option ("EOY") is a + * forward, calendar-anchored forecast horizon, not a trailing window. + */ + +const HORIZON_LABELS: Record = { + 7: "7D", + 14: "14D", + 30: "30D", + 90: "90D", + 365: "1Y", +}; + +export interface HorizonTabsProps { + /** Currently selected trailing window, in days. */ + value: number; + /** Called with the newly selected number of days. */ + onChange: (days: number) => void; + /** Day options to render, left → right. Defaults to the full set. */ + options?: number[]; + /** Optional className forwarded to the Tabs root. */ + className?: string; +} + +export function HorizonTabs({ + value, + onChange, + options = [7, 30, 90, 365], + className, +}: HorizonTabsProps) { + return ( + onChange(Number(v))} + className={className} + > + + {options.map((days) => ( + + {HORIZON_LABELS[days] ?? `${days}D`} + + ))} + + + ); +} + +export default HorizonTabs; diff --git a/inventory/src/components/forecasting/DateRangePickerQuick.tsx b/inventory/src/components/forecasting/DateRangePickerQuick.tsx index dbb36d1..11ae4e3 100644 --- a/inventory/src/components/forecasting/DateRangePickerQuick.tsx +++ b/inventory/src/components/forecasting/DateRangePickerQuick.tsx @@ -55,16 +55,16 @@ export function DateRangePickerQuick({ value, onChange, className }: DateRangePi />
diff --git a/inventory/src/components/overview/SalesMetrics.tsx b/inventory/src/components/overview/SalesMetrics.tsx index e30fa25..57473e0 100644 --- a/inventory/src/components/overview/SalesMetrics.tsx +++ b/inventory/src/components/overview/SalesMetrics.tsx @@ -7,7 +7,7 @@ import config from "@/config" import { formatCurrency } from "@/utils/formatCurrency" import { ClipboardList, Package, DollarSign, ShoppingCart } from "lucide-react" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { HorizonTabs } from "@/components/dashboard/shared/HorizonTabs" import { addDays, format } from "date-fns" import { PHASE_CONFIG, PHASE_KEYS_WITH_UNKNOWN as PHASE_KEYS } from "@/utils/lifecyclePhases" @@ -74,13 +74,7 @@ export function SalesMetrics() { <> Sales - setPeriod(Number(v) as Period)}> - - 7D - 30D - 90D - - + setPeriod(d as Period)} options={[7, 30, 90]} /> {isError ? ( diff --git a/inventory/src/components/ui/date-range-picker-narrow.tsx b/inventory/src/components/ui/date-range-picker-narrow.tsx deleted file mode 100644 index 10162fb..0000000 --- a/inventory/src/components/ui/date-range-picker-narrow.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { format, addDays, startOfYear, endOfYear, subDays } from "date-fns"; -import { Calendar as CalendarIcon } from "lucide-react"; -import { DateRange } from "react-day-picker"; -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Calendar } from "@/components/ui/calendar"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; - -interface DateRangePickerProps { - value: DateRange; - onChange: (range: DateRange | undefined) => void; - className?: string; - future?: boolean; -} - -export function DateRangePicker({ - value, - onChange, - className, - future = false, -}: DateRangePickerProps) { - const today = new Date(); - - const presets = future ? [ - { - label: "Next 30 days", - range: { - from: today, - to: addDays(today, 30), - }, - }, - { - label: "Next 90 days", - range: { - from: today, - to: addDays(today, 90), - }, - }, - { - label: "Rest of year", - range: { - from: today, - to: endOfYear(today), - }, - }, - ] : [ - { - label: "Last 7 days", - range: { - from: subDays(today, 7), - to: today, - }, - }, - { - label: "Last 30 days", - range: { - from: subDays(today, 30), - to: today, - }, - }, - { - label: "Last 90 days", - range: { - from: subDays(today, 90), - to: today, - }, - }, - { - label: "Year to date", - range: { - from: startOfYear(today), - to: today, - }, - }, - ]; - - return ( -
- - - - - -
- {presets.map((preset) => ( - - ))} -
- { - if (range) onChange(range); - }} - numberOfMonths={2} - /> -
-
-
- ); -} \ No newline at end of file diff --git a/inventory/src/pages/BlackFridayDashboard.tsx b/inventory/src/pages/BlackFridayDashboard.tsx index ad8911e..26e8993 100644 --- a/inventory/src/pages/BlackFridayDashboard.tsx +++ b/inventory/src/pages/BlackFridayDashboard.tsx @@ -49,6 +49,7 @@ import { acotService } from "@/services/dashboard/acotService"; import { cn } from "@/lib/utils"; import { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"; import { DateTime } from "luxon"; +import { businessTodayStr } from "@/utils/businessTime"; type BlackFridayRange = { start: DateTime; @@ -513,8 +514,12 @@ function LoadingBlock() { } export function BlackFridayDashboard() { - const currentYear = new Date().getUTCFullYear(); - const currentMonth = new Date().getUTCMonth() + 1; // getUTCMonth() returns 0-11, so add 1 + // Anchor on the BUSINESS date (Chicago calendar), not UTC. + const [businessYear, businessMonth] = businessTodayStr() + .split("-") + .map(Number); + const currentYear = businessYear; + const currentMonth = businessMonth; // Show previous year until November of current year const displayYear = currentMonth < 11 ? currentYear - 1 : currentYear; const availableYears = useMemo( diff --git a/inventory/src/pages/DiscountSimulator.tsx b/inventory/src/pages/DiscountSimulator.tsx index 530d001..8c748d7 100644 --- a/inventory/src/pages/DiscountSimulator.tsx +++ b/inventory/src/pages/DiscountSimulator.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { DateRange } from "react-day-picker"; -import { subMonths, startOfDay, endOfDay } from "date-fns"; +import { subMonths } from "date-fns"; import { useMutation, useQuery } from "@tanstack/react-query"; import { motion } from "motion/react"; @@ -20,6 +20,7 @@ import { CogsCalculationMode, } from "@/types/discount-simulator"; import { useToast } from "@/hooks/use-toast"; +import { toDateOnly } from "@/utils/businessTime"; const DEFAULT_POINT_VALUE = 0.005; const DEFAULT_REDEMPTION_RATE = 0.9; @@ -78,8 +79,9 @@ export function DiscountSimulator() { const promoDateBounds = useMemo(() => { const { from, to } = ensureDateRange(dateRange); return { - startDate: startOfDay(from).toISOString(), - endDate: endOfDay(to).toISOString(), + // Date-only strings = business dates; the server resolves bounds. + startDate: toDateOnly(from), + endDate: toDateOnly(to), }; }, [dateRange]); @@ -138,8 +140,8 @@ export function DiscountSimulator() { return { dateRange: { - start: startOfDay(from).toISOString(), - end: endOfDay(to).toISOString(), + start: toDateOnly(from), + end: toDateOnly(to), }, filters: { shipCountry: 'US', @@ -271,8 +273,13 @@ export function DiscountSimulator() { if (parsed.dateRange) { const defaultRange = getDefaultDateRange(); - const fromValue = parsed.dateRange.start ? new Date(parsed.dateRange.start) : undefined; - const toValue = parsed.dateRange.end ? new Date(parsed.dateRange.end) : undefined; + // Saved values may be date-only ('YYYY-MM-DD') or legacy ISO instants. + // Parse date-only as LOCAL (bare new Date() would treat it as UTC and + // show the previous day in US zones). + const parseSaved = (value: string) => + /^\d{4}-\d{2}-\d{2}$/.test(value) ? new Date(`${value}T00:00:00`) : new Date(value); + const fromValue = parsed.dateRange.start ? parseSaved(parsed.dateRange.start) : undefined; + const toValue = parsed.dateRange.end ? parseSaved(parsed.dateRange.end) : undefined; setDateRange({ from: fromValue ?? defaultRange.from, to: toValue ?? defaultRange.to, @@ -348,8 +355,8 @@ export function DiscountSimulator() { const { from, to } = ensureDateRange(dateRange); return JSON.stringify({ dateRange: { - start: startOfDay(from).toISOString(), - end: endOfDay(to).toISOString(), + start: toDateOnly(from), + end: toDateOnly(to), }, selectedPromoId: selectedPromoId ?? null, productPromo, diff --git a/inventory/src/pages/Forecasting.tsx b/inventory/src/pages/Forecasting.tsx index 19bf596..92b4497 100644 --- a/inventory/src/pages/Forecasting.tsx +++ b/inventory/src/pages/Forecasting.tsx @@ -38,6 +38,7 @@ import { Input } from "@/components/ui/input"; import { X, Layers, FolderTree, TrendingUp, Palette } from "lucide-react"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { toDateOnly } from "@/utils/businessTime"; type GroupMode = "line" | "category" | "designer"; @@ -126,8 +127,9 @@ export default function Forecasting() { queryFn: async () => { const params = new URLSearchParams({ brand: selectedBrand, - startDate: dateRange.from?.toISOString() || "", - endDate: dateRange.to?.toISOString() || "", + // Date-only strings = business dates (see docs/TIME.md) + startDate: dateRange.from ? toDateOnly(dateRange.from) : "", + endDate: dateRange.to ? toDateOnly(dateRange.to) : "", }); const response = await apiFetch(`/api/analytics/forecast-v2?${params}`); if (!response.ok) throw new Error("Failed to fetch forecast data"); diff --git a/inventory/src/utils/businessTime.ts b/inventory/src/utils/businessTime.ts new file mode 100644 index 0000000..22eb269 --- /dev/null +++ b/inventory/src/utils/businessTime.ts @@ -0,0 +1,33 @@ +// Business-time helpers (see inventory-server/docs/TIME.md). +// +// THE CONVENTION: a business day runs 1am Eastern → 1am Eastern, which is +// identical to the America/Chicago calendar date. The SERVER owns all +// boundary math — the client sends named ranges or date-only strings +// ('YYYY-MM-DD', meaning business dates), never toISOString() of a +// browser-local midnight. + +export const BUSINESS_TZ = 'America/Chicago'; +export const OFFICE_TZ = 'America/New_York'; + +/** The current business date as 'YYYY-MM-DD' (Chicago calendar date). */ +export function businessTodayStr(): string { + // en-CA formats as YYYY-MM-DD + return new Intl.DateTimeFormat('en-CA', { timeZone: BUSINESS_TZ }).format(new Date()); +} + +/** Business date of an instant. */ +export function businessDateOf(date: Date): string { + return new Intl.DateTimeFormat('en-CA', { timeZone: BUSINESS_TZ }).format(date); +} + +/** + * Calendar date of a browser-local Date, as 'YYYY-MM-DD' — for converting a + * locally-constructed period boundary (e.g. "March 2026" → Mar 1 local) into + * the date-only string the server resolves to business-day bounds. + */ +export function toDateOnly(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; +} diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo index 09a96d3..ebb630f 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/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/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/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-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/config/uploads.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/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/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/financialoverview.tsx","./src/components/dashboard/operationsmetrics.tsx","./src/components/dashboard/payrollmetrics.tsx","./src/components/dashboard/periodselectionpopover.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/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/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/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/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/config/uploads.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/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