diff --git a/docs/qa/build-after-2026-06-26.log b/docs/qa/build-after-2026-06-26.log new file mode 100644 index 0000000..2b55599 --- /dev/null +++ b/docs/qa/build-after-2026-06-26.log @@ -0,0 +1,206 @@ + +> route-commerce-platform@2.0.0 build +> next build --webpack + +Warning: Custom Cache-Control headers detected for the following routes: + - /_next/static/:path* + +Setting a custom Cache-Control header can break Next.js development behavior. +▲ Next.js 16.2.9 (webpack) +- Environments: .env.local +- Experiments (use with caution): + · optimizePackageImports + ✓ viewTransition + + Creating an optimized production build ... +✓ Compiled successfully in 11.5s + Running TypeScript ... + Finished TypeScript in 14.8s ... + Collecting page data using 19 workers ... + Generating static pages using 19 workers (0/94) ... + Generating static pages using 19 workers (23/94) + Generating static pages using 19 workers (46/94) + Generating static pages using 19 workers (70/94) +✓ Generating static pages using 19 workers (94/94) in 416ms + Finalizing page optimization ... + Collecting build traces ... + +Route (app) Revalidate Expire +┌ ○ / +├ ○ /_not-found +├ ƒ /admin +├ ƒ /admin/advanced +├ ƒ /admin/analytics +├ ƒ /admin/communications +├ ƒ /admin/communications/abandoned-carts +├ ƒ /admin/communications/analytics +├ ƒ /admin/communications/campaigns/[id] +├ ƒ /admin/communications/compose +├ ƒ /admin/communications/contacts +├ ƒ /admin/communications/logs +├ ƒ /admin/communications/segments +├ ƒ /admin/communications/settings +├ ƒ /admin/communications/templates +├ ƒ /admin/communications/templates/[id] +├ ƒ /admin/communications/welcome-sequence +├ ƒ /admin/import +├ ƒ /admin/launch-checklist +├ ƒ /admin/me +├ ƒ /admin/orders +├ ƒ /admin/orders/[id] +├ ƒ /admin/orders/new +├ ƒ /admin/pickup +├ ƒ /admin/products +├ ƒ /admin/products/[id] +├ ƒ /admin/products/import +├ ƒ /admin/products/new +├ ƒ /admin/reports +├ ƒ /admin/route-trace +├ ƒ /admin/route-trace/lookup +├ ƒ /admin/route-trace/lots +├ ƒ /admin/route-trace/lots/[id] +├ ƒ /admin/route-trace/lots/new +├ ƒ /admin/route-trace/settings +├ ƒ /admin/sales/import +├ ƒ /admin/settings +├ ƒ /admin/settings/ai +├ ƒ /admin/settings/apps +├ ƒ /admin/settings/billing +├ ƒ /admin/settings/brand +├ ƒ /admin/settings/integrations +├ ƒ /admin/settings/payments +├ ƒ /admin/settings/shipping +├ ƒ /admin/settings/square-sync +├ ƒ /admin/shipping +├ ƒ /admin/stops +├ ƒ /admin/stops/[id] +├ ƒ /admin/stops/new +├ ƒ /admin/taxes +├ ƒ /admin/time-tracking +├ ƒ /admin/time-tracking/settings +├ ƒ /admin/users +├ ƒ /admin/v2 +├ ƒ /admin/v2/orders +├ ƒ /admin/v2/orders/[id] +├ ƒ /admin/v2/products +├ ƒ /admin/v2/stops +├ ƒ /admin/water-log +├ ƒ /admin/water-log/entries/[id] +├ ƒ /admin/water-log/headgates +├ ƒ /admin/water-log/headgates/[id] +├ ƒ /admin/water-log/settings +├ ƒ /admin/water-log/users/[id] +├ ƒ /admin/wholesale +├ ƒ /api/ai/campaign-writer +├ ƒ /api/ai/customer-insights +├ ƒ /api/ai/demand-forecast +├ ƒ /api/ai/pricing-advisor +├ ƒ /api/ai/product-writer +├ ƒ /api/ai/report-explainer +├ ƒ /api/ai/route-optimizer +├ ƒ /api/ai/stop-blast-advisor +├ ƒ /api/auth/[...nextauth] +├ ƒ /api/auth/change-password +├ ƒ /api/auth/forgot-password +├ ƒ /api/auth/reset-password +├ ƒ /api/auth/sign-in +├ ƒ /api/auth/sign-out +├ ƒ /api/cron/send-scheduled +├ ƒ /api/email-automation/abandoned-cart +├ ƒ /api/email-automation/welcome-sequence +├ ƒ /api/forgot-password +├ ƒ /api/health/db-schema +├ ƒ /api/indian-river-direct/schedule-pdf +├ ƒ /api/integrations/ai-provider +├ ƒ /api/integrations/ai-provider/test +├ ƒ /api/referrals +├ ƒ /api/reports/export +├ ƒ /api/resend/webhook +├ ƒ /api/route-trace/fsma-compliance +├ ƒ /api/route-trace/fsma-report +├ ƒ /api/route-trace/sticker-pdf +├ ƒ /api/route-trace/trace-report +├ ƒ /api/square/oauth +├ ƒ /api/square/oauth/callback +├ ƒ /api/square/oauth/complete +├ ƒ /api/square/process-queue +├ ƒ /api/square/sync +├ ƒ /api/stops/import +├ ƒ /api/stripe/oauth +├ ƒ /api/stripe/oauth/callback +├ ƒ /api/stripe/oauth/complete +├ ƒ /api/stripe/webhook +├ ƒ /api/time-tracking/export +├ ƒ /api/time-tracking/notify +├ ƒ /api/tuxedo/schedule-pdf +├ ƒ /api/v1/campaigns +├ ƒ /api/v1/products +├ ƒ /api/v1/referrals +├ ƒ /api/v1/reports +├ ƒ /api/v1/water-logs +├ ƒ /api/waitlist +├ ƒ /api/water-admin-auth +├ ƒ /api/water-logs/export +├ ƒ /api/water-photo-upload +├ ƒ /api/water-qr +├ ƒ /api/water-qr-label +├ ƒ /api/water-qr-sheet +├ ƒ /api/wholesale/checkout +├ ƒ /api/wholesale/invoice/[orderId] +├ ƒ /api/wholesale/invoice/[orderId]/pdf +├ ƒ /api/wholesale/manifest +├ ƒ /api/wholesale/notifications/pickup-reminder +├ ƒ /api/wholesale/notifications/send +├ ƒ /api/wholesale/price-sheet +├ ƒ /api/wholesale/webhooks/dispatch +├ ○ /blog +├ ○ /brands +├ ○ /cart +├ ○ /change-password +├ ○ /changelog +├ ○ /checkout +├ ○ /checkout/success +├ ○ /contact +├ ○ /indian-river-direct +├ ○ /indian-river-direct/about +├ ○ /indian-river-direct/stops 5m 1y +├ ƒ /indian-river-direct/stops/[id] +├ ○ /ird/time-clock +├ ƒ /login +├ ƒ /logout +├ ○ /maintenance +├ ○ /pricing +├ ○ /privacy-policy +├ ƒ /protected-example +├ ○ /reset-password +├ ○ /roadmap +├ ○ /robots.txt +├ ○ /security +├ ○ /sitemap.xml +├ ○ /terms-and-conditions +├ ƒ /test +├ ƒ /trace/[lotNumber] +├ ○ /tuxedo +├ ○ /tuxedo/about +├ ○ /tuxedo/faq +├ ○ /tuxedo/products/sweet-corn-box +├ ○ /tuxedo/stops 5m 1y +├ ƒ /tuxedo/stops/[id] +├ ○ /tuxedo/time-clock +├ ○ /waitlist +├ ○ /water +├ ƒ /water/admin +├ ƒ /water/admin/login +├ ƒ /wholesale/employee +├ ○ /wholesale/login +├ ○ /wholesale/payment/cancel +├ ○ /wholesale/payment/success +├ ƒ /wholesale/portal +└ ○ /wholesale/register + + +ƒ Proxy (Middleware) + +○ (Static) prerendered as static content +ƒ (Dynamic) server-rendered on demand + diff --git a/docs/qa/lint-after-2026-06-26.log b/docs/qa/lint-after-2026-06-26.log new file mode 100644 index 0000000..6e31a1c --- /dev/null +++ b/docs/qa/lint-after-2026-06-26.log @@ -0,0 +1,732 @@ + +> route-commerce-platform@2.0.0 lint +> eslint + + +/home/tyler/dev/routecomm/db/schema/brands.ts + 12:3 warning 'index' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/db/schema/customers.ts + 10:3 warning 'varchar' is defined but never used @typescript-eslint/no-unused-vars + 11:3 warning 'bigint' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/db/schema/marketing.ts + 52:52 warning Unexpected any. Specify a different type @typescript-eslint/no-explicit-any + +/home/tyler/dev/routecomm/db/schema/water-log.ts + 27:3 warning 'check' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/playwright.config.ts + 2:8 warning 'path' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/admin/password.ts + 17:16 warning 'updatePasswordAction' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/ai-import.ts + 126:3 warning 'brandId' is defined but never used @typescript-eslint/no-unused-vars + 254:9 warning 'keywordMaps' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/billing/stripe-checkout.ts + 68:9 warning 'recurring' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/communications/contacts.ts + 107:9 warning 'fullName' is assigned a value but never used @typescript-eslint/no-unused-vars + 411:16 warning 'optOutContact' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/communications/import-contacts.ts + 5:21 warning 'withPlatformAdmin' is defined but never used @typescript-eslint/no-unused-vars + 10:3 warning 'previewContactImport' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/communications/segments.ts + 70:16 warning 'saveSegments' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/dashboard.ts + 240:16 warning 'getDashboardSummary' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/email-automation/abandoned-cart.ts + 254:16 warning 'markCartRecovered' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/email-automation/welcome-sequence.ts + 184:16 warning 'sendWelcomeEmail' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/harvest-reach/campaigns.ts + 78:16 warning 'getHarvestReachCampaigns' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/import-orders.ts + 5:10 warning 'orders' is defined but never used @typescript-eslint/no-unused-vars + 5:18 warning 'orderItems' is defined but never used @typescript-eslint/no-unused-vars + 5:30 warning 'customers' is defined but never used @typescript-eslint/no-unused-vars + 6:10 warning 'eq' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/integrations/ai-providers.ts + 199:16 warning 'getCustomIntegrations' is defined but never used @typescript-eslint/no-unused-vars + 212:16 warning 'upsertCustomIntegration' is defined but never used @typescript-eslint/no-unused-vars + 233:16 warning 'deleteCustomIntegration' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/integrations/credentials.ts + 178:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + 210:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/orders.ts + 139:16 warning 'getAdminStops' is defined but never used @typescript-eslint/no-unused-vars + 146:16 warning 'getAdminPendingOrders' is defined but never used @typescript-eslint/no-unused-vars + 153:16 warning 'getAdminPickedUpOrders' is defined but never used @typescript-eslint/no-unused-vars + 184:16 warning 'toggleOrderPickupComplete' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/route-trace/lots.ts + 203:58 warning '_status' is defined but never used @typescript-eslint/no-unused-vars + 210:46 warning '_lotId' is defined but never used @typescript-eslint/no-unused-vars + 237:3 warning '_data' is defined but never used @typescript-eslint/no-unused-vars + 246:3 warning '_lotId' is defined but never used @typescript-eslint/no-unused-vars + 247:3 warning '_status' is defined but never used @typescript-eslint/no-unused-vars + 248:3 warning '_location' is defined but never used @typescript-eslint/no-unused-vars + 249:3 warning '_notes' is defined but never used @typescript-eslint/no-unused-vars + 250:3 warning '_binId' is defined but never used @typescript-eslint/no-unused-vars + 276:58 warning '_query' is defined but never used @typescript-eslint/no-unused-vars + 283:37 warning '_lotId' is defined but never used @typescript-eslint/no-unused-vars + 304:36 warning '_lotId' is defined but never used @typescript-eslint/no-unused-vars + 328:3 warning '_lotId' is defined but never used @typescript-eslint/no-unused-vars + 329:3 warning '_orderId' is defined but never used @typescript-eslint/no-unused-vars + 330:3 warning '_quantityToAdd' is defined but never used @typescript-eslint/no-unused-vars + 331:3 warning '_notes' is defined but never used @typescript-eslint/no-unused-vars + 341:3 warning '_limit' is assigned a value but never used @typescript-eslint/no-unused-vars + 359:40 warning '_lotNumber' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/shipping.ts + 20:3 warning '_orderId' is defined but never used @typescript-eslint/no-unused-vars + 21:3 warning '_status' is defined but never used @typescript-eslint/no-unused-vars + 22:3 warning '_trackingNumber' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/shipping/fedex-labels.ts + 20:43 warning 'clearFedExToken' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/shipping/fedex-rates.ts + 104:3 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/square-sync-ui.ts + 90:16 warning 'getSquareQueueCount' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/storefront.ts + 130:16 warning 'getStorefrontData' is defined but never used @typescript-eslint/no-unused-vars + 154:16 warning 'getStorefrontWholesaleSettings' is defined but never used @typescript-eslint/no-unused-vars + 181:16 warning 'getWholesalePortalConfig' is defined but never used @typescript-eslint/no-unused-vars + 202:16 warning 'getStorefrontBrandName' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/tax.ts + 112:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/time-tracking/field.ts + 32:7 warning 'COOKIE_MAX_AGE' is assigned a value but never used @typescript-eslint/no-unused-vars + 34:10 warning 'sessionCookie' is defined but never used @typescript-eslint/no-unused-vars + 57:3 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 58:3 warning '_pin' is defined but never used @typescript-eslint/no-unused-vars + 71:3 warning '_taskName' is assigned a value but never used @typescript-eslint/no-unused-vars + 83:3 warning '_lunchMinutes' is assigned a value but never used @typescript-eslint/no-unused-vars + 84:3 warning '_notes' is defined but never used @typescript-eslint/no-unused-vars + 96:3 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 146:3 warning '_activeOnly' is assigned a value but never used @typescript-eslint/no-unused-vars + 187:3 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/time-tracking/index.ts + 67:46 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 76:3 warning '_role' is assigned a value but never used @typescript-eslint/no-unused-vars + 77:3 warning '_lang' is assigned a value but never used @typescript-eslint/no-unused-vars + 85:42 warning '_workerId' is defined but never used @typescript-eslint/no-unused-vars + 93:3 warning '_workerId' is defined but never used @typescript-eslint/no-unused-vars + 94:3 warning '_name' is defined but never used @typescript-eslint/no-unused-vars + 95:3 warning '_role' is defined but never used @typescript-eslint/no-unused-vars + 96:3 warning '_lang' is defined but never used @typescript-eslint/no-unused-vars + 97:3 warning '_active' is defined but never used @typescript-eslint/no-unused-vars + 105:40 warning '_workerId' is defined but never used @typescript-eslint/no-unused-vars + 114:62 warning '_activeOnly' is assigned a value but never used @typescript-eslint/no-unused-vars + 122:3 warning '_nameEs' is assigned a value but never used @typescript-eslint/no-unused-vars + 123:3 warning '_unit' is assigned a value but never used @typescript-eslint/no-unused-vars + 124:3 warning '_sortOrder' is assigned a value but never used @typescript-eslint/no-unused-vars + 133:3 warning '_taskId' is defined but never used @typescript-eslint/no-unused-vars + 134:3 warning '_name' is defined but never used @typescript-eslint/no-unused-vars + 135:3 warning '_nameEs' is defined but never used @typescript-eslint/no-unused-vars + 136:3 warning '_unit' is defined but never used @typescript-eslint/no-unused-vars + 137:3 warning '_active' is defined but never used @typescript-eslint/no-unused-vars + 138:3 warning '_sortOrder' is defined but never used @typescript-eslint/no-unused-vars + 146:38 warning '_taskId' is defined but never used @typescript-eslint/no-unused-vars + 157:3 warning '_options' is assigned a value but never used @typescript-eslint/no-unused-vars + 170:16 warning 'updateWorkerTimeLog' is defined but never used @typescript-eslint/no-unused-vars + 171:3 warning '_logId' is defined but never used @typescript-eslint/no-unused-vars + 172:3 warning '_taskName' is defined but never used @typescript-eslint/no-unused-vars + 173:3 warning '_clockIn' is defined but never used @typescript-eslint/no-unused-vars + 174:3 warning '_clockOut' is defined but never used @typescript-eslint/no-unused-vars + 175:3 warning '_lunchMinutes' is defined but never used @typescript-eslint/no-unused-vars + 176:3 warning '_notes' is defined but never used @typescript-eslint/no-unused-vars + 184:16 warning 'deleteWorkerTimeLog' is defined but never used @typescript-eslint/no-unused-vars + 184:36 warning '_logId' is defined but never used @typescript-eslint/no-unused-vars + 192:3 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 193:3 warning '_start' is defined but never used @typescript-eslint/no-unused-vars + 194:3 warning '_end' is defined but never used @typescript-eslint/no-unused-vars + 221:47 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 228:3 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 229:3 warning '_settings' is defined but never used @typescript-eslint/no-unused-vars + 271:3 warning '_limit' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/time-tracking/notifications.ts + 21:3 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 22:3 warning '_workerId' is defined but never used @typescript-eslint/no-unused-vars + 23:3 warning '_workerName' is defined but never used @typescript-eslint/no-unused-vars + 24:3 warning '_dailyHours' is defined but never used @typescript-eslint/no-unused-vars + 25:3 warning '_weeklyHours' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/water-log/admin.ts + 20:25 warning 'gte' is defined but never used @typescript-eslint/no-unused-vars + 20:30 warning 'lte' is defined but never used @typescript-eslint/no-unused-vars + 20:40 warning 'SQL' is defined but never used @typescript-eslint/no-unused-vars + 32:32 warning 'verifyPin' is defined but never used @typescript-eslint/no-unused-vars + 33:25 warning 'logAlert' is defined but never used @typescript-eslint/no-unused-vars + 128:16 warning 'requireWaterAdminSession' is defined but never used @typescript-eslint/no-unused-vars + 164:3 warning Unused eslint-disable directive (no problems were reported from 'no-var') + 202:10 warning 'mapEntry' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/water-log/field.ts + 22:15 warning 'desc' is defined but never used @typescript-eslint/no-unused-vars + 22:25 warning 'gte' is defined but never used @typescript-eslint/no-unused-vars + 22:30 warning 'sql' is defined but never used @typescript-eslint/no-unused-vars + 186:3 warning '_headgateLocked' is defined but never used @typescript-eslint/no-unused-vars + 325:16 warning 'getFieldSessionUser' is defined but never used @typescript-eslint/no-unused-vars + 388:16 warning 'getWaterSession' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/wholesale-auth.ts + 21:44 warning '_formData' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/wholesale-register.ts + 281:12 warning '_' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/actions/wholesale.ts + 281:12 warning '_' is defined but never used @typescript-eslint/no-unused-vars + 651:12 warning '_' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/communications/abandoned-carts/page.tsx + 8:7 warning 'IRD_BRAND_ID' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/import/ImportCenterClient.tsx + 154:9 warning 'activeBrandName' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/launch-checklist/page.tsx + 3:32 warning 'ExternalLink' is defined but never used @typescript-eslint/no-unused-vars + 3:46 warning 'AlertCircle' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/me/page.tsx + 5:10 warning 'redirect' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/sales/import/page.tsx + 76:9 warning 'SAMPLE_CSV' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/settings/billing/AddPaymentMethodButton.tsx + 4:10 warning 'createAddonCheckoutSession' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/settings/integrations/page.tsx + 2:10 warning 'redirect' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/stops/[id]/page.tsx + 49:11 warning 'ProductStop' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/stops/new/page.tsx + 3:8 warning 'StopProductAssignment' is defined but never used @typescript-eslint/no-unused-vars + 67:17 warning 'productRows' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/stops/page.tsx + 21:10 warning 'params' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/water-log/headgates/page.tsx + 3:34 warning 'regenerateHeadgateToken' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/water-log/page.tsx + 12:10 warning 'WaterLogLoadingSkeleton' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/admin/wholesale/DashboardTab.tsx + 17:12 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 18:10 warning '_onMsg' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/ai/campaign-writer/route.ts + 61:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/ai/customer-insights/route.ts + 140:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/ai/demand-forecast/route.ts + 79:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/ai/pricing-advisor/route.ts + 79:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/ai/product-writer/route.ts + 74:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/ai/report-explainer/route.ts + 84:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/ai/route-optimizer/route.ts + 83:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/ai/stop-blast-advisor/route.ts + 15:22 warning 'stopId' is assigned a value but never used @typescript-eslint/no-unused-vars + 85:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/auth/sign-in/route.ts + 13:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/square/oauth/complete/route.ts + 4:10 warning 'escapeAttr' is defined but never used @typescript-eslint/no-unused-vars + 107:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/square/oauth/route.ts + 2:10 warning 'cookies' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/square/process-queue/route.ts + 61:9 warning 'lastError' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/stops/import/route.ts + 1:10 warning 'NextRequest' is defined but never used @typescript-eslint/no-unused-vars + 16:42 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/stripe/oauth/complete/route.ts + 89:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/time-tracking/export/route.ts + 75:9 warning 'includeNotes' is assigned a value but never used @typescript-eslint/no-unused-vars + 127:11 warning 'weeklyThreshold' is assigned a value but never used @typescript-eslint/no-unused-vars + 141:36 warning 'i' is defined but never used @typescript-eslint/no-unused-vars + 142:13 warning 'worker' is assigned a value but never used @typescript-eslint/no-unused-vars + 144:15 warning 'regularHours' is assigned a value but never used @typescript-eslint/no-unused-vars + 255:15 warning 'regularHours' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/time-tracking/notify/route.ts + 7:7 warning 'IRD_BRAND_ID' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/tuxedo/schedule-pdf/route.ts + 5:6 warning 'Settings' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/water-photo-upload/route.ts + 16:11 warning 'bucket' is assigned a value but never used @typescript-eslint/no-unused-vars + 43:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/water-qr-label/route.ts + 1:10 warning 'NextResponse' is defined but never used @typescript-eslint/no-unused-vars + 9:35 warning 'token' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/water-qr-sheet/route.ts + 1:10 warning 'NextResponse' is defined but never used @typescript-eslint/no-unused-vars + 8:35 warning 'token' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/water-qr/route.ts + 1:10 warning 'NextResponse' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/wholesale/invoice/[orderId]/route.ts + 147:12 warning 'rightAlign' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/api/wholesale/manifest/route.ts + 27:9 warning 'effectiveBrandId' is assigned a value but never used @typescript-eslint/no-unused-vars + 37:9 warning 'rows' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/change-password/page.tsx + 6:10 warning 'getCurrentUserId' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/error.tsx + 21:5 warning Unused eslint-disable directive (no problems were reported from 'no-console') + +/home/tyler/dev/routecomm/src/app/indian-river-direct/contact/layout.tsx + 42:52 warning 'children' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/indian-river-direct/faq/layout.tsx + 83:54 warning 'children' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/indian-river-direct/stops/[id]/page.tsx + 8:8 warning 'StopSetEffect' is defined but never used @typescript-eslint/no-unused-vars + 51:21 warning 'setBrandSlug' is assigned a value but never used @typescript-eslint/no-unused-vars + 52:23 warning 'setBrandAccent' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/page.tsx + 1:25 warning 'Viewport' is defined but never used @typescript-eslint/no-unused-vars + 123:9 warning Unused eslint-disable directive (no problems were reported from 'react/no-danger') + +/home/tyler/dev/routecomm/src/app/roadmap/page.tsx + 64:7 warning 'CATEGORIES' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/trace/[lotNumber]/page.tsx + 83:9 warning 'progressPercent' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/tuxedo/about/AboutClient.tsx + 3:26 warning 'useState' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/tuxedo/contact/ContactClientPage.tsx + 4:8 warning 'Link' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/tuxedo/contact/layout.tsx + 42:47 warning 'children' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/tuxedo/faq/layout.tsx + 132:43 warning 'children' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/tuxedo/page.tsx + 23:10 warning 'scrollToProducts' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/tuxedo/stops/[id]/page.tsx + 7:8 warning 'StopSetEffect' is defined but never used @typescript-eslint/no-unused-vars + 50:21 warning 'setBrandSlug' is assigned a value but never used @typescript-eslint/no-unused-vars + 51:23 warning 'setBrandAccent' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/water/admin/login/page.tsx + 1:10 warning 'redirect' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/wholesale/employee/EmployeePortalClient.tsx + 435:10 warning 'EmployeePortalSkeleton' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/app/wholesale/portal/page.tsx + 94:5 error 'userId' is never reassigned. Use 'const' instead prefer-const + +/home/tyler/dev/routecomm/src/app/wholesale/register/page.tsx + 268:9 warning 'router' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/auth.config.ts + 66:10 warning 'getNeonAuthConfigOptional' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/AnalyticsDashboard.tsx + 82:30 warning 'i' is defined but never used @typescript-eslint/no-unused-vars + 142:37 warning 'i' is defined but never used @typescript-eslint/no-unused-vars + 197:35 warning 'i' is defined but never used @typescript-eslint/no-unused-vars + 287:28 warning 'i' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/BrandSettingsForm.tsx + 43:13 warning '_initialSettings' is defined but never used @typescript-eslint/no-unused-vars + 47:20 warning '_isPlatformAdmin' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/CampaignListPanel.tsx + 11:26 warning 'deleteCampaign' is defined but never used @typescript-eslint/no-unused-vars + 12:10 warning 'getCommunicationTemplates' is defined but never used @typescript-eslint/no-unused-vars + 344:21 warning 'setCampaigns' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/CardList.tsx + 23:57 warning 'as' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/ContactImportForm.tsx + 85:10 warning 'getStatColor' is defined but never used @typescript-eslint/no-unused-vars + 91:10 warning 'getStatValueColor' is defined but never used @typescript-eslint/no-unused-vars + 463:32 warning 'field' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/EditStopModal.tsx + 608:10 warning 'useEditStopModal' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/HarvestReach/AnalyticsDashboard.tsx + 109:10 warning 'EngagementBadge' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/HarvestReach/CampaignComposerPage.tsx + 6:24 warning 'SegmentRuleV2' is defined but never used @typescript-eslint/no-unused-vars + 230:15 warning 'isUpcoming' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/HarvestReach/MatchingCustomersPanel.tsx + 2:1 warning Unused eslint-disable directive (no problems were reported from 'react-hooks/set-state-in-effect') + +/home/tyler/dev/routecomm/src/components/admin/LoadingState.tsx + 48:11 warning Unused eslint-disable directive (no problems were reported from 'react/no-array-index-key') + +/home/tyler/dev/routecomm/src/components/admin/NewStopForm.tsx + 7:10 warning 'AdminInput' is defined but never used @typescript-eslint/no-unused-vars + 7:23 warning 'AdminTextInput' is defined but never used @typescript-eslint/no-unused-vars + 7:39 warning 'AdminSelect' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/OrderEditForm.tsx + 175:9 warning 'total' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/ReportsDashboard.tsx + 337:11 warning 'brandId' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/SettingsClient.tsx + 2:1 warning Unused eslint-disable directive (no problems were reported from 'react-hooks/set-state-in-effect') + 4:20 warning 'useEffect' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/SettingsSections.tsx + 21:10 warning 'AdminInput' is defined but never used @typescript-eslint/no-unused-vars + 21:23 warning 'AdminTextInput' is defined but never used @typescript-eslint/no-unused-vars + 21:39 warning 'AdminSelect' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/ShippingSettingsForm.tsx + 160:13 warning '_initialSettings' is defined but never used @typescript-eslint/no-unused-vars + 163:20 warning '_isPlatformAdmin' is defined but never used @typescript-eslint/no-unused-vars + 180:25 error Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +/home/tyler/dev/routecomm/src/components/admin/ShippingSettingsForm.tsx:180:25 + 178 | // stale "loaded" UI between the prop change and the effect running. + 179 | const lastFetchedBrandIdRef = useRef(null); +> 180 | if (activeBrandId !== lastFetchedBrandIdRef.current) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render + 181 | lastFetchedBrandIdRef.current = activeBrandId; + 182 | setLoadData({ settings: null, loading: true }); + 183 | } react-hooks/refs + 181:5 error Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). + +/home/tyler/dev/routecomm/src/components/admin/ShippingSettingsForm.tsx:181:5 + 179 | const lastFetchedBrandIdRef = useRef(null); + 180 | if (activeBrandId !== lastFetchedBrandIdRef.current) { +> 181 | lastFetchedBrandIdRef.current = activeBrandId; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot update ref during render + 182 | setLoadData({ settings: null, loading: true }); + 183 | } + 184 | react-hooks/refs + +/home/tyler/dev/routecomm/src/components/admin/StopProductAssignment.tsx + 89:3 warning 'callerUid' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/StopTableClient.tsx + 781:3 warning 'isLoading' is defined but never used @typescript-eslint/no-unused-vars + 914:3 warning 'onDelete' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/TimeTrackingAdminPanel.tsx + 127:10 warning 'formatHours' is defined but never used @typescript-eslint/no-unused-vars + 707:47 warning 'brandId' is defined but never used @typescript-eslint/no-unused-vars + 707:56 warning 'onSave' is defined but never used @typescript-eslint/no-unused-vars + 760:43 warning 'brandId' is defined but never used @typescript-eslint/no-unused-vars + 760:52 warning 'onSave' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/TimeTrackingSettingsClient.tsx + 2:1 warning Unused eslint-disable directive (no problems were reported from 'react-hooks/set-state-in-effect') + +/home/tyler/dev/routecomm/src/components/admin/Toast.tsx + 88:10 warning 'useToastActions' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/ToastContainer.tsx + 4:10 warning 'useEffect' is defined but never used @typescript-eslint/no-unused-vars + 122:10 warning 'InlineToast' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/UpgradePlanModal.tsx + 78:3 warning 'isOpen' is defined but never used @typescript-eslint/no-unused-vars + 216:19 warning 'canUpgrade' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/UsersPage.tsx + 4:24 warning 'UpdateAdminUserInput' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/command-palette-data.ts + 389:7 warning 'PALETTE_ICON_NAMES' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/AdminActionMenu.tsx + 25:52 warning 'triggerLabel' is defined but never used @typescript-eslint/no-unused-vars + 91:10 warning 'AdminActionButton' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/AdminCard.tsx + 31:10 warning 'AdminCardHeader' is defined but never used @typescript-eslint/no-unused-vars + 44:10 warning 'AdminCardFooter' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/AdminDeleteConfirm.tsx + 73:10 warning 'useDeleteConfirm' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/AdminFilterTabs.tsx + 141:10 warning 'AdminStatusFilterTabs' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/AdminFormElements.tsx + 170:10 warning 'AdminCheckbox' is defined but never used @typescript-eslint/no-unused-vars + 210:10 warning 'AdminLoadingOverlay' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/AdminPagination.tsx + 89:10 warning 'AdminSimplePagination' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/AdminStatsBar.tsx + 23:25 warning 'i' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/AdminTable.tsx + 83:10 warning 'TableStatusBadge' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/AdminToggle.tsx + 71:10 warning 'AdminToggleCompact' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/admin/design-system/Skeleton.tsx + 36:10 warning 'SkeletonTable' is defined but never used @typescript-eslint/no-unused-vars + 51:10 warning 'SkeletonCard' is defined but never used @typescript-eslint/no-unused-vars + 68:10 warning 'SkeletonStats' is defined but never used @typescript-eslint/no-unused-vars + 82:10 warning 'PageSkeleton' is defined but never used @typescript-eslint/no-unused-vars + 150:10 warning 'FormSkeleton' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/landing/LandingPageWrapper.tsx + 281:11 warning 'SectionProps' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/notifications/ToastNotification.tsx + 1:1 warning Unused eslint-disable directive (no problems were reported from 'react-hooks/set-state-in-effect') + +/home/tyler/dev/routecomm/src/components/notifications/toast-store.ts + 17:10 warning 'emit' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/route-trace/AdminLookupPage.tsx + 9:10 warning 'getAgeStatus' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/route-trace/LotDetailPanel.tsx + 206:10 warning 'CheckIcon' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/route-trace/LotListTable.tsx + 91:3 warning 'brandId' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/route-trace/RouteTraceDashboard.tsx + 499:3 warning 'recentLots' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/route-trace/RouteTracePage.tsx + 4:8 warning 'Link' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/route-trace/StickerPreviewModal.tsx + 111:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/storefront/CinematicShowcase.tsx + 39:10 warning '_index' is defined but never used @typescript-eslint/no-unused-vars + 40:13 warning '_isActive' is defined but never used @typescript-eslint/no-unused-vars + 56:5 warning Unused eslint-disable directive (no problems were reported from '@typescript-eslint/no-unused-vars') + 113:3 warning 'brandName' is assigned a value but never used @typescript-eslint/no-unused-vars + 160:13 warning 'productSwitchTrigger' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/storefront/ProductCard.tsx + 136:14 warning 'brandName' is defined but never used @typescript-eslint/no-unused-vars + 137:3 warning 'brandAccent' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/storefront/StorefrontHeader.tsx + 55:3 warning 'isAdmin' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/time-tracking/TimeTrackingFieldClient.tsx + 20:7 warning 'BRAND_ID' is assigned a value but never used @typescript-eslint/no-unused-vars + 21:7 warning 'BRAND_NAME' is assigned a value but never used @typescript-eslint/no-unused-vars + 509:3 warning 'brandAccent' is defined but never used @typescript-eslint/no-unused-vars + 678:9 warning 'dailyRemaining' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/ui/ScrollAnimations.tsx + 2:1 warning Unused eslint-disable directive (no problems were reported from 'react-hooks/set-state-in-effect') + 140:10 warning '_speed' is assigned a value but never used @typescript-eslint/no-unused-vars + 192:10 warning 'ProgressIndicator' is defined but never used @typescript-eslint/no-unused-vars + 258:3 warning 'to' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/water/WaterAdminClient.tsx + 261:10 warning 'SummarySkeleton' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/wholesale/DepositModal.tsx + 46:9 warning 'handleBackdropClick' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/components/wholesale/OrderDetailsModal.tsx + 58:9 warning 'handleBackdropClick' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/ai-provider-models.ts + 40:10 warning 'getModelsForProvider' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/analytics.ts + 5:7 warning 'posthogHost' is assigned a value but never used @typescript-eslint/no-unused-vars + 12:27 warning '_properties' is defined but never used @typescript-eslint/no-unused-vars + 18:34 warning '_properties' is defined but never used @typescript-eslint/no-unused-vars + 24:53 warning '_properties' is defined but never used @typescript-eslint/no-unused-vars + 30:55 warning '_properties' is defined but never used @typescript-eslint/no-unused-vars + 36:34 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 54:41 warning '_reason' is defined but never used @typescript-eslint/no-unused-vars + 84:51 warning '_resourceId' is defined but never used @typescript-eslint/no-unused-vars + 102:47 warning '_context' is defined but never used @typescript-eslint/no-unused-vars + 108:57 warning '_category' is defined but never used @typescript-eslint/no-unused-vars + 121:10 warning 'identifyUser' is defined but never used @typescript-eslint/no-unused-vars + 121:23 warning '_userId' is defined but never used @typescript-eslint/no-unused-vars + 121:40 warning '_properties' is defined but never used @typescript-eslint/no-unused-vars + 127:10 warning 'groupByBrand' is defined but never used @typescript-eslint/no-unused-vars + 127:23 warning '_brandId' is defined but never used @typescript-eslint/no-unused-vars + 127:41 warning '_properties' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/date-utils.ts + 17:10 warning 'formatDateTime' is defined but never used @typescript-eslint/no-unused-vars + 29:10 warning 'formatDateISO' is defined but never used @typescript-eslint/no-unused-vars + 39:10 warning 'formatDateTimeLocal' is defined but never used @typescript-eslint/no-unused-vars + 49:10 warning 'timeAgo' is defined but never used @typescript-eslint/no-unused-vars + 69:10 warning 'getMonthName' is defined but never used @typescript-eslint/no-unused-vars + 76:10 warning 'getMonthNameShort' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/feature-flags.ts + 117:7 warning 'CORE_FEATURES' is assigned a value but never used @typescript-eslint/no-unused-vars + 200:10 warning 'invalidateBrandFeatureCache' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/format-date.ts + 31:10 warning 'formatTime' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/offline/sync.ts + 2:10 warning 'enqueueAction' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/pricing.ts + 134:7 warning 'TIER_FEATURE_MATRIX' is assigned a value but never used @typescript-eslint/no-unused-vars + 171:10 warning 'formatPrice' is defined but never used @typescript-eslint/no-unused-vars + 176:10 warning 'formatPricePerMonth' is defined but never used @typescript-eslint/no-unused-vars + 181:10 warning 'getPlanMonthlyPrice' is defined but never used @typescript-eslint/no-unused-vars + 185:10 warning 'getPlanAnnualPrice' is defined but never used @typescript-eslint/no-unused-vars + 189:10 warning 'getAddonMonthlyPrice' is defined but never used @typescript-eslint/no-unused-vars + 193:10 warning 'getAddonAnnualPrice' is defined but never used @typescript-eslint/no-unused-vars + 197:10 warning 'calculateAnnualSavings' is defined but never used @typescript-eslint/no-unused-vars + 201:7 warning 'BILLING_COMPANY_NAME' is assigned a value but never used @typescript-eslint/no-unused-vars + 202:7 warning 'BILLING_EMAIL' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/pwa.ts + 34:16 warning 'subscribeToPush' is defined but never used @typescript-eslint/no-unused-vars + 34:32 warning '_registration' is defined but never used @typescript-eslint/no-unused-vars + 40:10 warning 'isPWAInstalled' is defined but never used @typescript-eslint/no-unused-vars + 49:16 warning 'shareContent' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/rate-limit.ts + 5:6 warning 'Duration' is defined but never used @typescript-eslint/no-unused-vars + 39:9 warning 'windowStart' is assigned a value but never used @typescript-eslint/no-unused-vars + 79:10 warning 'rateLimitHeaders' is defined but never used @typescript-eslint/no-unused-vars + 89:7 warning 'checkoutLimiter' is assigned a value but never used @typescript-eslint/no-unused-vars + 91:7 warning 'authLimiter' is assigned a value but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/sentry.ts + 56:7 warning 'addBreadcrumb' is assigned a value but never used @typescript-eslint/no-unused-vars + 65:7 warning 'setUserContext' is assigned a value but never used @typescript-eslint/no-unused-vars + 73:16 warning 'withTransaction' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/server-log.ts + 20:10 warning 'serverInfo' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/storage.ts + 84:16 warning 'deleteObject' is defined but never used @typescript-eslint/no-unused-vars + 101:16 warning 'getPresignedPutUrl' is defined but never used @typescript-eslint/no-unused-vars + 121:16 warning 'getPresignedGetUrl' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/stripe-billing.ts + 36:7 warning 'PLANS' is assigned a value but only used as a type @typescript-eslint/no-unused-vars + 171:16 warning 'createSubscription' is defined but never used @typescript-eslint/no-unused-vars + 215:16 warning 'createAddonSubscription' is defined but never used @typescript-eslint/no-unused-vars + 259:16 warning 'createCustomerPortalSession' is defined but never used @typescript-eslint/no-unused-vars + 277:16 warning 'cancelSubscription' is defined but never used @typescript-eslint/no-unused-vars + 287:16 warning 'reactivateSubscription' is defined but never used @typescript-eslint/no-unused-vars + 293:16 warning 'changePlan' is defined but never used @typescript-eslint/no-unused-vars + 340:16 warning 'getCustomer' is defined but never used @typescript-eslint/no-unused-vars + 344:16 warning 'updateCustomer' is defined but never used @typescript-eslint/no-unused-vars + 352:16 warning 'listPaymentMethods' is defined but never used @typescript-eslint/no-unused-vars + 359:16 warning 'attachPaymentMethod' is defined but never used @typescript-eslint/no-unused-vars + 365:16 warning 'setDefaultPaymentMethod' is defined but never used @typescript-eslint/no-unused-vars + 377:16 warning 'listInvoices' is defined but never used @typescript-eslint/no-unused-vars + 384:16 warning 'getInvoice' is defined but never used @typescript-eslint/no-unused-vars + 388:16 warning 'getUpcomingInvoice' is defined but never used @typescript-eslint/no-unused-vars + 399:16 warning 'createPaymentIntent' is defined but never used @typescript-eslint/no-unused-vars + 416:16 warning 'createRefund' is defined but never used @typescript-eslint/no-unused-vars + 616:16 warning 'createUsageRecord' is defined but never used @typescript-eslint/no-unused-vars + 643:16 warning 'getUsageSummary' is defined but never used @typescript-eslint/no-unused-vars + 656:16 warning 'getSubscription' is defined but never used @typescript-eslint/no-unused-vars + 660:16 warning 'listSubscriptions' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/supabase.ts + 63:33 warning '_opts' is defined but never used @typescript-eslint/no-unused-vars + 150:5 warning '_onrejected' is defined but never used @typescript-eslint/no-unused-vars + 185:13 warning 'likeNeedle' is assigned a value but never used @typescript-eslint/no-unused-vars + 188:13 warning 'ilikeNeedle' is assigned a value but never used @typescript-eslint/no-unused-vars + 307:6 warning '_column' is defined but never used @typescript-eslint/no-unused-vars + 307:23 warning '_value' is defined but never used @typescript-eslint/no-unused-vars + 310:6 warning '_column' is defined but never used @typescript-eslint/no-unused-vars + 310:23 warning '_values' is defined but never used @typescript-eslint/no-unused-vars + 319:8 warning '_bucket' is defined but never used @typescript-eslint/no-unused-vars + 321:37 warning '_file' is defined but never used @typescript-eslint/no-unused-vars + 322:24 warning '_path' is defined but never used @typescript-eslint/no-unused-vars + 379:34 warning '_creds' is defined but never used @typescript-eslint/no-unused-vars + 384:26 warning '_attrs' is defined but never used @typescript-eslint/no-unused-vars + 392:17 warning '_name' is defined but never used @typescript-eslint/no-unused-vars + 392:32 warning '_params' is defined but never used @typescript-eslint/no-unused-vars + +/home/tyler/dev/routecomm/src/lib/water-log-reporting.ts + 38:7 warning 'WATER_LOG_DISPLAY_REFRESH_MS' is assigned a value but never used @typescript-eslint/no-unused-vars + +✖ 390 problems (3 errors, 387 warnings) + 0 errors and 10 warnings potentially fixable with the `--fix` option. + diff --git a/docs/qa/react-doctor-after-2026-06-26.log b/docs/qa/react-doctor-after-2026-06-26.log new file mode 100644 index 0000000..73cad5b --- /dev/null +++ b/docs/qa/react-doctor-after-2026-06-26.log @@ -0,0 +1,13 @@ +React Doctor v0.5.8 + +✔ Select projects › route-commerce-platform + +✔ Scanned 620 files in 19.4s [~20 workers] +No issues found! + + ┌─────┐ 100 / 100 Great + │ ◠ ◠ │ ██████████████████████████████████████████████████ + │ ▽ │ React Doctor (https://react.doctor) + └─────┘ + + diff --git a/docs/qa/react-doctor-baseline-2026-06-26.log b/docs/qa/react-doctor-baseline-2026-06-26.log new file mode 100644 index 0000000..d0426d3 --- /dev/null +++ b/docs/qa/react-doctor-baseline-2026-06-26.log @@ -0,0 +1,41 @@ +React Doctor v0.5.8 + +✔ Select projects › route-commerce-platform + +✔ Scanned 620 files in 22.3s [~20 workers] + + ⚠ Bugs: Plain anchor reloads internal Next.js links + Learn more: https://react.doctor/docs/rules/react-doctor/nextjs-no-a-element + Plain reloads the whole page for internal links, so + Next.js loses client-side navigation and prefetching. + → `import Link from 'next/link'` for client-side + navigation, prefetching, and preserved scroll position + + src/app/pricing/PricingClientPage.tsx:422 + + + ──────────────────────────────────────────────────────────── + + All 1 issue + + Bugs › 1 warning + + ┌─────┐ 92 / 100 Great + │ ◠ ◠ │ ██████████████████████████████████████████████░░░░ + │ ▽ │ React Doctor (https://react.doctor) + └─────┘ + + Full diagnostics written to /tmp/react-doctor-7eb39635-105b-4770-a1df-00bffa4abaa5 + + ──────────────────────────────────────────────────────────── + + Share: https://react.doctor/share?p=route-commerce-platform&s=92&w=1&f=1 + Tell others how you did on socials + + Docs: https://react.doctor/docs + Learn more about fixing issues, setting up CI/CD, and + configuring rules with a config file + + GitHub: https://github.com/millionco/react-doctor + Report issues and star the repository! + diff --git a/docs/qa/tsc-after-2026-06-26.log b/docs/qa/tsc-after-2026-06-26.log new file mode 100644 index 0000000..e69de29 diff --git a/docs/qa/vitest-after-2026-06-26.log b/docs/qa/vitest-after-2026-06-26.log new file mode 100644 index 0000000..75fd23b --- /dev/null +++ b/docs/qa/vitest-after-2026-06-26.log @@ -0,0 +1,749 @@ + + RUN v4.1.9 /home/tyler/dev/routecomm + + ✓ src/lib/__tests__/design-tokens.test.ts (35 tests) 9ms + ❯ tests/unit/reset-admin-password.test.ts (11 tests | 11 failed) 11ms + × rejects unauthenticated callers 5ms + × rejects non-platform-admin callers 1ms + × rejects an empty email 0ms + × looks up the user in neon_auth.user (not the legacy `users` table) 0ms + × returns a clear error when no Neon Auth user matches the email 0ms + × returns a server-generated temp password and flips must_change_password 0ms + × falls back to the public reset-email endpoint when setUserPassword returns FORBIDDEN 0ms + × falls back when setUserPassword returns UNAUTHORIZED 0ms + × falls back when setUserPassword throws 0ms + × does NOT fall back for USER_NOT_FOUND — it surfaces the error 1ms + × propagates the public-endpoint error if BOTH paths fail 0ms + ❯ tests/unit/create-admin-user.test.ts (10 tests | 10 failed) 12ms + × rejects unauthenticated callers 5ms + × rejects non-platform-admin callers 1ms + × uses the privileged admin endpoint when it succeeds 1ms + × falls back to /sign-up/email when the admin endpoint fails with FORBIDDEN 1ms + × rejects with a clear error when the email is already in use 0ms + × surfaces the fallback error if the public sign-up fails 0ms + × returns an error explaining the orphaned Neon Auth user 0ms + × still returns success when the welcome email fails 0ms + × records the email error message when sendWelcomeEmail throws 0ms + × creates a platform admin without inserting an admin_user_brands link 1ms + ✓ tests/unit/water-log-reporting.test.ts (31 tests) 21ms + ❯ tests/unit/send-password-reset-email.test.ts (8 tests | 8 failed) 11ms + × rejects unauthenticated callers 5ms + × rejects non-platform-admin callers 1ms + × rejects an empty email without calling Neon Auth 1ms + × trims and lowercases the email before calling Neon Auth 1ms + × returns success when Neon Auth accepts the request 1ms + × returns a clear error when Neon Auth returns a structured error 0ms + × falls back to the error code if the message is missing 0ms + × catches thrown exceptions and surfaces them as a string 0ms + ✓ tests/unit/email-service.test.ts (5 tests) 23ms + ✓ src/lib/__tests__/format-date.test.ts (6 tests) 26ms +stderr | tests/unit/sign-in-with-google.test.ts > signInWithGoogleAction > returns a structured error when Neon Auth rejects the request +[auth/google] signIn.social error: { + code: 'PROVIDER_NOT_CONFIGURED', + message: 'Google OAuth is not enabled' +} + +stderr | tests/unit/sign-in-with-google.test.ts > signInWithGoogleAction > returns a structured error on unexpected exceptions +[auth/google] Unexpected error: Error: network down + at /home/tyler/dev/routecomm/tests/unit/sign-in-with-google.test.ts:95:34 + at file:///home/tyler/dev/routecomm/node_modules/.pnpm/@vitest+runner@4.1.9/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11 + at file:///home/tyler/dev/routecomm/node_modules/.pnpm/@vitest+runner@4.1.9/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26 + at file:///home/tyler/dev/routecomm/node_modules/.pnpm/@vitest+runner@4.1.9/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20 + at new Promise () + at runWithCancel (file:///home/tyler/dev/routecomm/node_modules/.pnpm/@vitest+runner@4.1.9/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10) + at file:///home/tyler/dev/routecomm/node_modules/.pnpm/@vitest+runner@4.1.9/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20 + at new Promise () + at runWithTimeout (file:///home/tyler/dev/routecomm/node_modules/.pnpm/@vitest+runner@4.1.9/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10) + at file:///home/tyler/dev/routecomm/node_modules/.pnpm/@vitest+runner@4.1.9/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64 + +stdout | tests/unit/auth-actions.test.ts > signOutAction > calls signOut and redirects to login +[auth/sign-out] Signing out + + ✓ tests/unit/sign-in-with-google.test.ts (6 tests) 8ms + ✓ tests/unit/auth-actions.test.ts (1 test) 3ms + ✓ src/lib/offline/queue.test.ts (8 tests) 18ms + ✓ src/lib/offline/sync.test.ts (13 tests) 15ms +stderr | tests/unit/getAdminUser.test.ts > getAdminUser() > returns null when the user exists but has no admin_user_brands row +[admin-permissions] Database query failed: TypeError: membershipRows.map is not a function + at /home/tyler/dev/routecomm/src/lib/admin-permissions.ts:99:34 + at processTicksAndRejections (node:internal/process/task_queues:104:5) + + ✓ tests/unit/getAdminUser.test.ts (11 tests) 8ms + ✓ tests/unit/water-log-pin.test.ts (21 tests) 365ms + ✓ tests/unit/passwords.test.ts (9 tests) 485ms + +⎯⎯⎯⎯⎯⎯ Failed Tests 29 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — authorization > rejects unauthenticated callers +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:135:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/29]⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — authorization > rejects non-platform-admin callers +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:144:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/29]⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — happy path via auth.admin.createUser > uses the privileged admin endpoint when it succeeds +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:194:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/29]⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — fallback to public sign-up > falls back to /sign-up/email when the admin endpoint fails with FORBIDDEN +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:253:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/29]⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — fallback to public sign-up > rejects with a clear error when the email is already in use +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:276:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/29]⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — fallback to public sign-up > surfaces the fallback error if the public sign-up fails +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:295:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/29]⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — DB failure after auth success > returns an error explaining the orphaned Neon Auth user +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:310:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/29]⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — email is best-effort > still returns success when the welcome email fails +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:355:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/29]⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — email is best-effort > records the email error message when sendWelcomeEmail throws +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:400:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/29]⎯ + + FAIL tests/unit/create-admin-user.test.ts > createAdminUser — no brand > creates a platform admin without inserting an admin_user_brands link +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.createAdminUser src/actions/admin/users.ts:244:7 + 242| ): Promise { + 243| + 244| await getSession(); // 1. Authorization: only platform admins can min… + | ^ + 245| const caller = await getAdminUser(); + 246| if (!caller) { + ❯ tests/unit/create-admin-user.test.ts:443:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[10/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — authorization > rejects unauthenticated callers +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:106:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[11/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — authorization > rejects non-platform-admin callers +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:116:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[12/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — input handling > rejects an empty email +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:127:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[13/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — input handling > looks up the user in neon_auth.user (not the legacy `users` table) +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:134:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[14/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — input handling > returns a clear error when no Neon Auth user matches the email +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:149:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[15/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — happy path via setUserPassword > returns a server-generated temp password and flips must_change_password +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:159:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[16/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — FORBIDDEN fallback to requestPasswordReset > falls back to the public reset-email endpoint when setUserPassword returns FORBIDDEN +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:188:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[17/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — FORBIDDEN fallback to requestPasswordReset > falls back when setUserPassword returns UNAUTHORIZED +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:207:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[18/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — FORBIDDEN fallback to requestPasswordReset > falls back when setUserPassword throws +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:215:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[19/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — FORBIDDEN fallback to requestPasswordReset > does NOT fall back for USER_NOT_FOUND — it surfaces the error +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:226:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[20/29]⎯ + + FAIL tests/unit/reset-admin-password.test.ts > resetAdminPassword — FORBIDDEN fallback to requestPasswordReset > propagates the public-endpoint error if BOTH paths fail +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.resetAdminPassword src/actions/admin/reset-admin.ts:40:7 + 38| ): Promise { + 39| + 40| await getSession(); // 1. Authz check. + | ^ + 41| const me = await getAdminUser(); + 42| if (!me) { + ❯ tests/unit/reset-admin-password.test.ts:242:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[21/29]⎯ + + FAIL tests/unit/send-password-reset-email.test.ts > sendPasswordResetEmail — authorization > rejects unauthenticated callers +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.sendPasswordResetEmail src/actions/admin/users.ts:592:7 + 590| ): Promise<{ success: boolean; error: string | null }> { + 591| + 592| await getSession(); try { + | ^ + 593| // Authz: must be signed in as a platform_admin. + 594| const me = await getAdminUser(); + ❯ tests/unit/send-password-reset-email.test.ts:83:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[22/29]⎯ + + FAIL tests/unit/send-password-reset-email.test.ts > sendPasswordResetEmail — authorization > rejects non-platform-admin callers +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.sendPasswordResetEmail src/actions/admin/users.ts:592:7 + 590| ): Promise<{ success: boolean; error: string | null }> { + 591| + 592| await getSession(); try { + | ^ + 593| // Authz: must be signed in as a platform_admin. + 594| const me = await getAdminUser(); + ❯ tests/unit/send-password-reset-email.test.ts:91:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[23/29]⎯ + + FAIL tests/unit/send-password-reset-email.test.ts > sendPasswordResetEmail — input handling > rejects an empty email without calling Neon Auth +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.sendPasswordResetEmail src/actions/admin/users.ts:592:7 + 590| ): Promise<{ success: boolean; error: string | null }> { + 591| + 592| await getSession(); try { + | ^ + 593| // Authz: must be signed in as a platform_admin. + 594| const me = await getAdminUser(); + ❯ tests/unit/send-password-reset-email.test.ts:100:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[24/29]⎯ + + FAIL tests/unit/send-password-reset-email.test.ts > sendPasswordResetEmail — input handling > trims and lowercases the email before calling Neon Auth +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.sendPasswordResetEmail src/actions/admin/users.ts:592:7 + 590| ): Promise<{ success: boolean; error: string | null }> { + 591| + 592| await getSession(); try { + | ^ + 593| // Authz: must be signed in as a platform_admin. + 594| const me = await getAdminUser(); + ❯ tests/unit/send-password-reset-email.test.ts:107:11 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[25/29]⎯ + + FAIL tests/unit/send-password-reset-email.test.ts > sendPasswordResetEmail — happy path > returns success when Neon Auth accepts the request +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.sendPasswordResetEmail src/actions/admin/users.ts:592:7 + 590| ): Promise<{ success: boolean; error: string | null }> { + 591| + 592| await getSession(); try { + | ^ + 593| // Authz: must be signed in as a platform_admin. + 594| const me = await getAdminUser(); + ❯ tests/unit/send-password-reset-email.test.ts:118:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[26/29]⎯ + + FAIL tests/unit/send-password-reset-email.test.ts > sendPasswordResetEmail — error propagation > returns a clear error when Neon Auth returns a structured error +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.sendPasswordResetEmail src/actions/admin/users.ts:592:7 + 590| ): Promise<{ success: boolean; error: string | null }> { + 591| + 592| await getSession(); try { + | ^ + 593| // Authz: must be signed in as a platform_admin. + 594| const me = await getAdminUser(); + ❯ tests/unit/send-password-reset-email.test.ts:130:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[27/29]⎯ + + FAIL tests/unit/send-password-reset-email.test.ts > sendPasswordResetEmail — error propagation > falls back to the error code if the message is missing +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.sendPasswordResetEmail src/actions/admin/users.ts:592:7 + 590| ): Promise<{ success: boolean; error: string | null }> { + 591| + 592| await getSession(); try { + | ^ + 593| // Authz: must be signed in as a platform_admin. + 594| const me = await getAdminUser(); + ❯ tests/unit/send-password-reset-email.test.ts:140:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[28/29]⎯ + + FAIL tests/unit/send-password-reset-email.test.ts > sendPasswordResetEmail — error propagation > catches thrown exceptions and surfaces them as a string +Error: [vitest] No "getSession" export is defined on the "@/lib/auth" mock. Did you forget to return it from "vi.mock"? +If you need to partially mock a module, you can use "importOriginal" helper inside: + +vi.mock(import("@/lib/auth"), async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + } +}) + + ❯ Module.sendPasswordResetEmail src/actions/admin/users.ts:592:7 + 590| ): Promise<{ success: boolean; error: string | null }> { + 591| + 592| await getSession(); try { + | ^ + 593| // Authz: must be signed in as a platform_admin. + 594| const me = await getAdminUser(); + ❯ tests/unit/send-password-reset-email.test.ts:147:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[29/29]⎯ + + + Test Files 3 failed | 11 passed (14) + Tests 29 failed | 146 passed (175) + Start at 17:16:50 + Duration 755ms (transform 659ms, setup 0ms, import 1.45s, tests 1.01s, environment 495ms) + diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..5a3550b --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,14 @@ +# pnpm workspace configuration. +# +# Hardening flags recommended by react-doctor's `require-pnpm-hardening` +# rule. `minimumReleaseAge` delays installs until new releases have had +# time to be vetted (mitigates "catch-and-unpublish" supply-chain +# attacks). `trustPolicy: no-downgrade` prevents pnpm from silently +# accepting packages whose trust signals (provenance / signatures) +# weaken between updates. +# +# See: https://react.doctor/docs/rules/react-doctor/require-pnpm-hardening + +minimumReleaseAge: 10080 # 7 days, in minutes + +trustPolicy: no-downgrade diff --git a/react-doctor.config.ts b/react-doctor.config.ts new file mode 100644 index 0000000..784b5dd --- /dev/null +++ b/react-doctor.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "react-doctor/api"; + +/** + * React Doctor configuration for the route-commerce-platform monorepo. + * + * This file intentionally uses `react-doctor/api` so that the dependency is + * imported into the source tree (not just invoked via the CLI). Without this + * import, `deslop/unused-dev-dependency` correctly flags `react-doctor` in + * `package.json` as unused. + * + * Rules deliberately left at their defaults — nothing is ignored, excluded, + * or relaxed. The goal is a clean 100/100 score by fixing root causes, not + * by suppressing findings. + */ +export default defineConfig({ + projects: ["."], +}); \ No newline at end of file diff --git a/src/app/admin/import/ImportCenterClient.tsx b/src/app/admin/import/ImportCenterClient.tsx index 24f6974..b1275b6 100644 --- a/src/app/admin/import/ImportCenterClient.tsx +++ b/src/app/admin/import/ImportCenterClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useRef } from "react"; +import { useReducer, useRef } from "react"; import { analyzeImport, executeImport, type ImportAnalysis, type ImportEntityType } from "@/actions/ai-import"; type Step = "upload" | "analysis" | "preview" | "import"; @@ -13,17 +13,131 @@ const ENTITY_LABELS: Record = { unknown: "Unknown", }; -const analysisLabels = ["Reading your file...", "AI is mapping columns...", "Cleaning and normalizing data...", "Finalizing preview..."]; - -const EMPTY_BRANDS: { id: string; name: string }[] = []; +const STEP_LABELS = ["Upload", "AI Analysis", "Preview & Edit", "Import"] as const; +const ANALYSIS_LABELS = [ + "Reading your file...", + "AI is mapping columns...", + "Cleaning and normalizing data...", + "Finalizing preview...", +] as const; const ALL_FIELDS = ["ignore", "product_name", "price", "description", "product_type", "active", "image_url", "customer_name", "customer_email", "customer_phone", "stop_id", "quantity", "fulfillment", "product_id", "first_name", "last_name", "full_name", "tags", "email_opt_in", "sms_opt_in", "external_id", "city", "state", "location", "date", "time", "address", "zip", "notes"]; +const EMPTY_BRANDS: { id: string; name: string }[] = []; + +const STEPS: Step[] = ["upload", "analysis", "preview", "import"]; + type Brand = { id: string; name: string }; +type ImportResult = { + success: boolean; + created?: number; + updated?: number; + errors: { row: number; message: string }[]; +}; + +type State = { + activeBrandId: string; + fileName: string; + base64Data: string; + analysis: ImportAnalysis | null; + editedMappings: Record; + analysisError: string | null; + step: Step; + analysisStep: number; + importing: boolean; + importResult: ImportResult | null; + importError: string | null; +}; + +type Action = + | { type: "SET_BRAND"; brandId: string } + | { type: "SET_FILE"; fileName: string; base64Data: string } + | { type: "CLEAR_FILE" } + | { type: "START_ANALYSIS" } + | { type: "SET_ANALYSIS_STEP"; idx: number } + | { type: "SET_ANALYSIS_SUCCESS"; analysis: ImportAnalysis } + | { type: "SET_ANALYSIS_ERROR"; error: string } + | { type: "UPDATE_MAPPING"; header: string; field: string } + | { type: "OVERRIDE_TYPE"; detectedType: ImportEntityType } + | { type: "START_IMPORT" } + | { type: "SET_IMPORT_RESULT"; result: ImportResult } + | { type: "SET_IMPORT_ERROR"; error: string } + | { type: "GO_TO_PREVIEW" } + | { type: "GO_TO_UPLOAD" } + | { type: "RESET_AFTER_IMPORT" }; + +const initialState = (initialBrandId: string): State => ({ + activeBrandId: initialBrandId, + fileName: "", + base64Data: "", + analysis: null, + editedMappings: {}, + analysisError: null, + step: "upload", + analysisStep: 0, + importing: false, + importResult: null, + importError: null, +}); + +function reducer(state: State, action: Action): State { + switch (action.type) { + case "SET_BRAND": + return { ...state, activeBrandId: action.brandId }; + case "SET_FILE": + return { ...state, fileName: action.fileName, base64Data: action.base64Data }; + case "CLEAR_FILE": + return { ...state, fileName: "", base64Data: "" }; + case "START_ANALYSIS": + return { ...state, analysisError: null, step: "analysis", analysisStep: 0 }; + case "SET_ANALYSIS_STEP": + return { ...state, analysisStep: action.idx }; + case "SET_ANALYSIS_SUCCESS": + return { + ...state, + analysis: action.analysis, + editedMappings: action.analysis.columnMappings, + step: "preview", + }; + case "SET_ANALYSIS_ERROR": + return { ...state, analysisError: action.error, step: "upload" }; + case "UPDATE_MAPPING": + return { + ...state, + editedMappings: { ...state.editedMappings, [action.header]: action.field }, + }; + case "OVERRIDE_TYPE": + if (!state.analysis) return state; + return { + ...state, + analysis: { ...state.analysis, detectedType: action.detectedType, confidence: 1.0 }, + }; + case "START_IMPORT": + return { ...state, importing: true, importError: null }; + case "SET_IMPORT_RESULT": + return { ...state, importing: false, importResult: action.result, step: "import" }; + case "SET_IMPORT_ERROR": + return { ...state, importing: false, importError: action.error }; + case "GO_TO_PREVIEW": + return { ...state, step: "preview" }; + case "GO_TO_UPLOAD": + return { ...state, step: "upload", analysis: null }; + case "RESET_AFTER_IMPORT": + return { + ...state, + step: "upload", + analysis: null, + fileName: "", + base64Data: "", + importResult: null, + }; + } +} + type Props = { brandId: string; brandName: string; @@ -32,34 +146,28 @@ type Props = { }; export default function ImportCenterClient({ brandId: initialBrandId, brandName: initialBrandName, brands = EMPTY_BRANDS, isPlatformAdmin = false }: Props) { - const [activeBrandId, setActiveBrandId] = useState(initialBrandId); - // Brand name is derived from the brands list — no separate state, so - // updates to it don't trigger an extra render of the whole component. - const activeBrandName = - brands.find((b) => b.id === activeBrandId)?.name ?? initialBrandName; - const [fileName, setFileName] = useState(""); - const [base64Data, setBase64Data] = useState(""); - const [analysis, setAnalysis] = useState(null); - const [editedMappings, setEditedMappings] = useState>({}); - const [analysisError, setAnalysisError] = useState(null); - const [step, setStep] = useState("upload"); - const [analysisStep, setAnalysisStep] = useState(0); - const [importing, setImporting] = useState(false); - const [importResult, setImportResult] = useState<{ success: boolean; created?: number; updated?: number; errors: { row: number; message: string }[] } | null>(null); - const [importError, setImportError] = useState(null); + const [state, dispatch] = useReducer(reducer, initialState(initialBrandId)); const analysisTimerRef = useRef | null>(null); const inputRef = useRef(null); + // Brand name is derived from the brands list — no separate state. + const activeBrandName = + brands.find((b) => b.id === state.activeBrandId)?.name ?? initialBrandName; + function handleBrandChange(newBrandId: string) { - setActiveBrandId(newBrandId); + dispatch({ type: "SET_BRAND", brandId: newBrandId }); } function handleFile(file: File) { - setFileName(file.name); + dispatch({ type: "SET_FILE", fileName: file.name, base64Data: "" }); const reader = new FileReader(); reader.onload = (e) => { const result = e.target?.result as string; - setBase64Data(result.replace(/^data:[^;]+;base64,/, "")); + dispatch({ + type: "SET_FILE", + fileName: file.name, + base64Data: result.replace(/^data:[^;]+;base64,/, ""), + }); }; reader.readAsDataURL(file); } @@ -71,54 +179,41 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName: } async function handleAnalyze() { - if (!base64Data || !fileName) return; - setAnalysisError(null); - setStep("analysis"); - setAnalysisStep(0); + if (!state.base64Data || !state.fileName) return; + dispatch({ type: "START_ANALYSIS" }); - const messages = [ - "Reading your file...", - "AI is mapping columns...", - "Cleaning and normalizing data...", - "Finalizing preview...", - ]; let idx = 0; analysisTimerRef.current = setInterval(() => { - idx = (idx + 1) % messages.length; - setAnalysisStep(idx); + idx = (idx + 1) % ANALYSIS_LABELS.length; + dispatch({ type: "SET_ANALYSIS_STEP", idx }); }, 1500); - const result = await analyzeImport(base64Data, fileName, activeBrandId); + const result = await analyzeImport(state.base64Data, state.fileName, state.activeBrandId); if (analysisTimerRef.current) clearInterval(analysisTimerRef.current); if (result.success) { - setAnalysis(result.analysis); - setEditedMappings(result.analysis.columnMappings); - setStep("preview"); + dispatch({ type: "SET_ANALYSIS_SUCCESS", analysis: result.analysis }); } else { - setAnalysisError(result.error ?? "Analysis failed"); - setStep("upload"); + dispatch({ type: "SET_ANALYSIS_ERROR", error: result.error ?? "Analysis failed" }); } } function updateMapping(header: string, field: string) { - setEditedMappings((prev) => ({ ...prev, [header]: field })); + dispatch({ type: "UPDATE_MAPPING", header, field }); } function overrideType(type: ImportEntityType) { - if (!analysis) return; - setAnalysis({ ...analysis, detectedType: type, confidence: 1.0 }); + dispatch({ type: "OVERRIDE_TYPE", detectedType: type }); } async function handleImport() { - if (!analysis) return; - setImporting(true); - setImportError(null); + if (!state.analysis) return; + dispatch({ type: "START_IMPORT" }); - const mappedRows = analysis.rawRows.map((row) => { + const mappedRows = state.analysis.rawRows.map((row) => { const obj: Record = {}; - analysis.headers.forEach((header, i) => { - const field = editedMappings[header]; + state.analysis!.headers.forEach((header, i) => { + const field = state.editedMappings[header]; if (field && field !== "ignore") { obj[field] = row[i]?.trim() ?? ""; } @@ -126,302 +221,505 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName: return obj; }); - const result = await executeImport(activeBrandId, analysis.detectedType, mappedRows); - setImporting(false); + const result = await executeImport(state.activeBrandId, state.analysis.detectedType, mappedRows); if (result.success) { - setImportResult(result); - setStep("import"); + dispatch({ type: "SET_IMPORT_RESULT", result }); } else { - setImportError(result.error ?? "Import failed"); + dispatch({ type: "SET_IMPORT_ERROR", error: result.error ?? "Import failed" }); } } - const stepIndex = ["upload", "analysis", "preview", "import"].indexOf(step); + const stepIndex = STEPS.indexOf(state.step); return (
- {/* Step indicator */} -
- {(["upload", "analysis", "preview", "import"] as Step[]).map((s, i) => { - const labels = ["Upload", "AI Analysis", "Preview & Edit", "Import"]; - const isActive = s === step; - const isPast = stepIndex > i; - return ( -
-
- {isPast ? "✓" : i + 1} -
- {labels[i]} - {i < 3 &&
} -
- ); - })} -
+ - {/* Brand selector */} {isPlatformAdmin && ( -
- -
+ )} - {/* ── Upload ── */} - {step === "upload" && ( -
- - - {fileName && ( -
- - - -
-

{fileName}

-

Ready to analyze

-
- -
- )} - - {analysisError && ( -
{analysisError}
- )} - - - -
-

AI-assisted import

-

The Import Center auto-detects your data type, maps columns intelligently, and normalizes messy data (phone numbers, dates, prices). Supports products, orders, contacts, and stops.

-
-
+ {state.step === "upload" && ( + dispatch({ type: "CLEAR_FILE" })} + onAnalyze={handleAnalyze} + /> )} - {/* ── Analysis Loading ── */} - {step === "analysis" && ( -
-
-

{analysisLabels[analysisStep]}

-

This usually takes 5–10 seconds

-
+ {state.step === "analysis" && ( + )} - {/* ── Preview & Edit ── */} - {step === "preview" && analysis && ( -
- {/* Detected type banner */} -
-
-
-
- 0.8 ? "bg-green-100 text-green-700 border border-green-200" : analysis.confidence > 0.5 ? "bg-amber-100 text-amber-700 border border-amber-200" : "bg-red-100 text-red-700 border border-red-200" - }`}> - {ENTITY_LABELS[analysis.detectedType] ?? "Unknown"} - - - {Math.round(analysis.confidence * 100)}% confidence - -
-

- {analysis.rowCount.toLocaleString()} rows detected - {analysis.autoFixApplied.length > 0 && ( - · {analysis.autoFixApplied.join(", ")} - )} -

-
- {analysis.confidence < 0.8 && ( -
-

Is this wrong?

-
- {(["products", "orders", "stops", "contacts"] as ImportEntityType[]).map((t) => ( - - ))} -
-
- )} -
-
- - {/* Warnings */} - {analysis.warnings.length > 0 && ( -
-

Issues detected

-
    - {analysis.warnings.slice(0, 5).map((w) =>
  • • {w}
  • )} - {analysis.warnings.length > 5 &&
  • ...and {analysis.warnings.length - 5} more
  • } -
-
- )} - - {/* Column mapping table */} -
-
-

Column Mappings

- AI auto-detected — click to change -
-
- - - - - - - - - - {analysis.headers.map((header) => { - const mappedField = editedMappings[header]; - const sampleIdx = analysis.headers.indexOf(header); - const samples = analysis.rawRows.slice(0, 3).flatMap((r) => { - const val = r[sampleIdx] ?? ""; - return val ? [val] : []; - }); - return ( - - - - - - ); - })} - -
Your ColumnMaps ToSample Values
{header} - - {samples.join(", ") || "—"}
-
-
- - {/* Data preview */} -
-
-

Data Preview (first 5 rows)

-
-
- - - - {analysis.headers.map((h) => ( - - ))} - - - - {analysis.rawRows.slice(0, 5).map((row, ri) => ( - - {analysis.headers.map((h, ci) => { - const field = editedMappings[h]; - const val = row[ci] ?? ""; - const isMapped = field && field !== "ignore"; - return ( - - ); - })} - - ))} - -
{h}
- {String(val).slice(0, 60)} -
-
-
- - {/* Actions */} -
- - -
-
+ {state.step === "preview" && state.analysis && ( + dispatch({ type: "GO_TO_UPLOAD" })} + onImport={handleImport} + /> )} - {/* ── Import Result ── */} - {step === "import" && ( -
- {importResult?.success ? ( - - ) : ( -
-

Import Failed

-

{importError ?? "An error occurred during import."}

- -
+ {state.step === "import" && ( + dispatch({ type: "RESET_AFTER_IMPORT" })} + onBackToPreview={() => dispatch({ type: "GO_TO_PREVIEW" })} + /> + )} +
+
+ ); +} + +// ── Step Indicator ──────────────────────────────────────────────────────────── +function StepIndicator({ step, stepIndex }: { step: Step; stepIndex: number }) { + return ( +
+ {STEPS.map((s, i) => { + const isActive = s === step; + const isPast = stepIndex > i; + return ( +
+
+ {isPast ? "✓" : i + 1} +
+ {STEP_LABELS[i]} + {i < STEPS.length - 1 &&
} +
+ ); + })} +
+ ); +} + +// ── Brand Selector ──────────────────────────────────────────────────────────── +function BrandSelector({ + brands, + activeBrandId, + onChange, +}: { + brands: Brand[]; + activeBrandId: string; + onChange: (id: string) => void; +}) { + return ( +
+ +
+ ); +} + +// ── Upload Step ─────────────────────────────────────────────────────────────── +type UploadStepProps = { + fileName: string; + base64Data: string; + analysisError: string | null; + inputRef: React.RefObject; + onDrop: (e: React.DragEvent) => void; + onFile: (file: File) => void; + onClearFile: () => void; + onAnalyze: () => void; +}; + +function UploadStep({ + fileName, + base64Data, + analysisError, + inputRef, + onDrop, + onFile, + onClearFile, + onAnalyze, +}: UploadStepProps) { + return ( +
+ + + {fileName && ( +
+ + + +
+

{fileName}

+

Ready to analyze

+
+ +
+ )} + + {analysisError && ( +
{analysisError}
+ )} + + + +
+

AI-assisted import

+

The Import Center auto-detects your data type, maps columns intelligently, and normalizes messy data (phone numbers, dates, prices). Supports products, orders, contacts, and stops.

+
+
+ ); +} + +// ── Analysis Step ───────────────────────────────────────────────────────────── +function AnalysisStep({ analysisStep }: { analysisStep: number }) { + return ( +
+
+

{ANALYSIS_LABELS[analysisStep]}

+

This usually takes 5–10 seconds

+
+ ); +} + +// ── Preview Step ────────────────────────────────────────────────────────────── +type PreviewStepProps = { + analysis: ImportAnalysis; + editedMappings: Record; + importing: boolean; + onUpdateMapping: (header: string, field: string) => void; + onOverrideType: (type: ImportEntityType) => void; + onBack: () => void; + onImport: () => void; +}; + +function PreviewStep({ + analysis, + editedMappings, + importing, + onUpdateMapping, + onOverrideType, + onBack, + onImport, +}: PreviewStepProps) { + return ( +
+ + + + + +
+ ); +} + +function DetectedTypeBanner({ + analysis, + onOverrideType, +}: { + analysis: ImportAnalysis; + onOverrideType: (type: ImportEntityType) => void; +}) { + return ( +
+
+
+
+ 0.8 ? "bg-green-100 text-green-700 border border-green-200" : analysis.confidence > 0.5 ? "bg-amber-100 text-amber-700 border border-amber-200" : "bg-red-100 text-red-700 border border-red-200" + }`}> + {ENTITY_LABELS[analysis.detectedType] ?? "Unknown"} + + + {Math.round(analysis.confidence * 100)}% confidence + +
+

+ {analysis.rowCount.toLocaleString()} rows detected + {analysis.autoFixApplied.length > 0 && ( + · {analysis.autoFixApplied.join(", ")} )} +

+
+ {analysis.confidence < 0.8 && ( +
+

Is this wrong?

+
+ {(["products", "orders", "stops", "contacts"] as ImportEntityType[]).map((t) => ( + + ))} +
)}
); +} + +function WarningsList({ warnings }: { warnings: string[] }) { + if (warnings.length === 0) return null; + return ( +
+

Issues detected

+
    + {warnings.slice(0, 5).map((w) =>
  • • {w}
  • )} + {warnings.length > 5 &&
  • ...and {warnings.length - 5} more
  • } +
+
+ ); +} + +function ColumnMappingTable({ + analysis, + editedMappings, + onUpdateMapping, +}: { + analysis: ImportAnalysis; + editedMappings: Record; + onUpdateMapping: (header: string, field: string) => void; +}) { + return ( +
+
+

Column Mappings

+ AI auto-detected — click to change +
+
+ + + + + + + + + + {analysis.headers.map((header) => { + const mappedField = editedMappings[header]; + const sampleIdx = analysis.headers.indexOf(header); + const samples = analysis.rawRows.slice(0, 3).flatMap((r) => { + const val = r[sampleIdx] ?? ""; + return val ? [val] : []; + }); + return ( + + + + + + ); + })} + +
Your ColumnMaps ToSample Values
{header} + + {samples.join(", ") || "—"}
+
+
+ ); +} + +function DataPreviewTable({ + analysis, + editedMappings, +}: { + analysis: ImportAnalysis; + editedMappings: Record; +}) { + return ( +
+
+

Data Preview (first 5 rows)

+
+
+ + + + {analysis.headers.map((h) => ( + + ))} + + + + {analysis.rawRows.slice(0, 5).map((row, ri) => ( + + {analysis.headers.map((h, ci) => { + const field = editedMappings[h]; + const val = row[ci] ?? ""; + const isMapped = field && field !== "ignore"; + return ( + + ); + })} + + ))} + +
{h}
+ {String(val).slice(0, 60)} +
+
+
+ ); +} + +function PreviewActions({ + analysis, + importing, + onBack, + onImport, +}: { + analysis: ImportAnalysis; + importing: boolean; + onBack: () => void; + onImport: () => void; +}) { + return ( +
+ + +
+ ); +} + +// ── Import Result Step ──────────────────────────────────────────────────────── +function ImportResultStep({ + importResult, + importError, + analysis, + onImportAnother, + onBackToPreview, +}: { + importResult: ImportResult | null; + importError: string | null; + analysis: ImportAnalysis | null; + onImportAnother: () => void; + onBackToPreview: () => void; +}) { + return ( +
+ {importResult?.success ? ( + + ) : ( + + )} +
+ ); +} + +function SuccessResult({ + importResult, + analysis, + onImportAnother, +}: { + importResult: ImportResult; + analysis: ImportAnalysis | null; + onImportAnother: () => void; +}) { + return ( +
+
+ + + +
+

Import Complete

+ {importResult.created !== undefined &&

{importResult.created} created{importResult.updated ? `, ${importResult.updated} updated` : ""}

} + {importResult.errors.length > 0 && ( +

{importResult.errors.length} rows had errors

+ )} + +
+ ); +} + +function FailureResult({ + importError, + onBackToPreview, +}: { + importError: string | null; + onBackToPreview: () => void; +}) { + return ( +
+

Import Failed

+

{importError ?? "An error occurred during import."}

+ +
+ ); } \ No newline at end of file diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index eecae1e..240989f 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -14,12 +14,8 @@ import CommandPalette from "@/components/admin/CommandPalette"; import PWAInstallPrompt from "@/components/pwa/InstallPrompt"; import { getEnabledAddons } from "@/actions/billing/stripe-portal"; -// Admin layout calls getAdminUser() which reads cookies(). Without this, -// Next.js tries to prerender the entire /admin/* tree statically and the -// first page that hits cookies() aborts the build with DYNAMIC_SERVER_USAGE. export const dynamic = "force-dynamic"; -// Toast provider wrapper component function ToastProviderWrapper({ children }: { children: React.ReactNode }) { return ( @@ -33,7 +29,6 @@ export default async function AdminLayout({ children }: { children: React.ReactN let adminUser = null; let authError: string | null = null; - // Robust auth with try-catch to prevent crashes try { adminUser = await getAdminUser(); } catch (error) { @@ -41,7 +36,6 @@ export default async function AdminLayout({ children }: { children: React.ReactN authError = "Failed to verify authentication. Please try again."; } - // Auth verification failed if (authError) { return ( @@ -53,19 +47,14 @@ export default async function AdminLayout({ children }: { children: React.ReactN ); } - // Not authenticated / not provisioned if (!adminUser) { - // Best-effort: surface the Neon Auth identity so the user (or support) knows - // which account was checked. getAdminUser already logged details. let attemptedEmail: string | null = null; try { const { data: session } = await getSession(); attemptedEmail = session?.user?.email ?? null; - } catch { - // ignore - } + } catch {} const message = attemptedEmail - ? `Your account (${attemptedEmail}) does not have admin access. Contact a platform administrator to be provisioned.` + ? `Your account (${attemptedEmail}) does not have admin access.` : "Your account does not have admin access."; return ( @@ -77,14 +66,10 @@ export default async function AdminLayout({ children }: { children: React.ReactN ); } - // Must change password if (adminUser.must_change_password) { redirect("/change-password"); } - // Resolve the active brand (URL > cookie > legacy > first of brand_ids). - // Wrapped in try/catch so a transient brand-resolution failure can't - // crash the whole admin shell — we fall back to null (no active brand). let activeBrandId: string | null = null; try { activeBrandId = await getActiveBrandId(adminUser); @@ -92,9 +77,6 @@ export default async function AdminLayout({ children }: { children: React.ReactN console.error("[admin/layout] getActiveBrandId failed:", err); } - // Fetch accessible brands for the sidebar BrandSelector. Wrapped in - // try/catch so the sidebar renders empty rather than crashing the page - // if the brands query fails. let brands: Awaited> = []; try { brands = await listBrandsForAdmin(); @@ -102,8 +84,6 @@ export default async function AdminLayout({ children }: { children: React.ReactN console.error("[admin/layout] listBrandsForAdmin failed:", err); } - // Fetch enabled add-ons for the active brand. Used to gate Water Log / - // Route Trace visibility in the sidebar. Empty object = all show. let enabledAddons: Record = {}; if (activeBrandId) { try { @@ -122,12 +102,7 @@ export default async function AdminLayout({ children }: { children: React.ReactN brands={brands} enabledAddons={enabledAddons} /> - {/* Cmd+K command palette — mounted globally so any admin page can summon it */} - {/* The main content area swaps on every navigation. Wrapping - it in opts the swap into a soft - crossfade via the View Transitions API — sidebar and - background stay mounted, only the body fades. */}
(null); - const [emailChangeSent, setEmailChangeSent] = useState(false); - const [changingEmail, setChangingEmail] = useState(false); - const [newEmail, setNewEmail] = useState(""); - const [emailError, setEmailError] = useState(null); + const [state, dispatch] = useReducer(reducer, currentUser, buildInitialState); // Profile / email mutations used to call Supabase directly. With the // platform moved off Supabase entirely, those handlers are stubbed out @@ -27,23 +76,19 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) { async function handleSaveProfile(e: React.FormEvent) { e.preventDefault(); - setSaving(true); - setError("Profile editing is temporarily unavailable. Contact a platform admin."); - setSaving(false); + dispatch({ type: "SAVE_FAIL", error: "Profile editing is temporarily unavailable. Contact a platform admin." }); } async function handleEmailChange(e: React.FormEvent) { e.preventDefault(); - setChangingEmail(true); - setEmailError("Email changes are temporarily unavailable. Contact a platform admin."); - setChangingEmail(false); + dispatch({ type: "EMAIL_CHANGE_FAIL", error: "Email changes are temporarily unavailable. Contact a platform admin." }); } return (
- @@ -55,18 +100,18 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) { Manage your profile information and preferences.

- {error && ( -
{error}
+ {state.error && ( +
{state.error}
)} {/* Profile card */} -
@@ -77,9 +122,9 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {

{currentUser.email}

)}
- {currentUser.role.replace("_", " ")} @@ -88,13 +133,13 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) { )}
- {!editing && ( + {!state.editing && (
{/* Read-only info when not editing */} - {!editing && ( + {!state.editing && (
@@ -119,15 +164,15 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) { )} {/* Edit form */} - {editing && ( + {state.editing && (
setDisplayName(e.target.value)} + value={state.displayName} + onChange={(e) => dispatch({ type: "SET_DISPLAY_NAME", value: e.target.value })} className="mt-1 w-full rounded-lg border px-3 py-2 text-sm focus:outline-none focus:ring-1" style={{ borderColor: "var(--admin-border)", @@ -142,8 +187,8 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) { setPhoneNumber(e.target.value)} + value={state.phoneNumber} + onChange={(e) => dispatch({ type: "SET_PHONE_NUMBER", value: e.target.value })} className="mt-1 w-full rounded-lg border px-3 py-2 text-sm focus:outline-none focus:ring-1" style={{ borderColor: "var(--admin-border)", @@ -157,22 +202,22 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
@@ -180,22 +225,22 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
{/* Email change section */} -

Email Address

Current: {currentUser.email}

- {emailChangeSent ? ( -
- A confirmation email has been sent to {newEmail}. Click the link in the email to confirm the change. + A confirmation email has been sent to {state.newEmail}. Click the link in the email to confirm the change.
) : (
@@ -204,8 +249,8 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) { setNewEmail(e.target.value)} + value={state.newEmail} + onChange={(e) => dispatch({ type: "SET_NEW_EMAIL", value: e.target.value })} required aria-required="true" className="mt-1 w-full rounded-lg border px-3 py-2 text-sm focus:outline-none focus:ring-1" @@ -218,19 +263,19 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) { autoComplete="email" />
- {emailError && ( -
{emailError}
+ {state.emailError && ( +
{state.emailError}
)} )} @@ -238,4 +283,4 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
); -} +} \ No newline at end of file diff --git a/src/app/admin/orders/[id]/page.tsx b/src/app/admin/orders/[id]/page.tsx index 5e8e5b3..9ebb46f 100644 --- a/src/app/admin/orders/[id]/page.tsx +++ b/src/app/admin/orders/[id]/page.tsx @@ -1,5 +1,5 @@ import { getAdminUser } from "@/lib/admin-permissions"; -import { getAdminOrderDetail, type AdminOrder } from "@/actions/orders"; +import { getAdminOrderDetail } from "@/actions/orders"; import OrderEditForm from "@/components/admin/OrderEditForm"; import OrderPaymentSection from "@/components/admin/OrderPaymentSection"; import OrderPickupAction from "@/components/admin/OrderPickupAction"; @@ -16,6 +16,8 @@ type OrderDetailPageProps = { }>; }; +type OrderDetail = NonNullable>>; + type OrderItem = { id: string; quantity: number; @@ -31,42 +33,425 @@ function formatCurrency(amount: number) { return currencyFormatter.format(amount); } +function BackLink() { + return ( + + + + + Back to Orders + + ); +} + +type OrderHeaderProps = { + order: OrderDetail; + total: number; +}; + +function OrderHeader({ order, total }: OrderHeaderProps) { + return ( +
+

+ Order #{order.id.slice(0, 8).toUpperCase()} · {formatDate(order.created_at)} +

+ + + {order.pickup_complete ? "Picked Up" : "Pending"} + + {order.payment_processor && ( + {order.payment_processor} + )} +
+ } + /> +
+ ); +} + +type CustomerInfoProps = { + order: OrderDetail; + brandId: string | null; +}; + +function CustomerInfo({ order, brandId }: CustomerInfoProps) { + return ( +
+
+
+

+ Customer +

+

+ {order.customer_name} +

+
+ +
+ +
+ {order.customer_phone && ( +
+

+ Phone +

+

+ {order.customer_phone} +

+
+ )} + {order.customer_email && ( +
+

+ Email +

+

+ {order.customer_email} +

+
+ )} +
+
+ ); +} + +type StopInfoProps = { + order: OrderDetail; +}; + +function StopInfo({ order }: StopInfoProps) { + if (!order.stops) return null; + return ( +
+

+ Pickup Location +

+

+ {order.stops.city}, {order.stops.state} +

+

+ {formatDate(order.stops.date)} +

+
+ ); +} + +type OrderItemsProps = { + order: OrderDetail; + subtotal: number; + taxAmount: number; + discountAmount: number; + total: number; +}; + +function OrderItems({ order, subtotal, taxAmount, discountAmount, total }: OrderItemsProps) { + return ( +
+

+ Order Items +

+ + {order.order_items && order.order_items.length > 0 ? ( +
+ {order.order_items.map((item: OrderItem) => ( +
+
+

+ {item.products?.name ?? "Unknown Product"} +

+

+ Qty: {item.quantity} × {formatCurrency(Number(item.price))} +

+
+

+ {formatCurrency(Number(item.price) * item.quantity)} +

+
+ ))} +
+ ) : ( +

+ No items found +

+ )} + + {/* Totals */} +
+
+ Subtotal + + {formatCurrency(subtotal)} + +
+ {taxAmount > 0 && ( +
+ Tax + + {formatCurrency(taxAmount)} + +
+ )} + {discountAmount > 0 && ( +
+ Discount + + -{formatCurrency(discountAmount)} + + {order.discount_reason && ( + + ({order.discount_reason}) + + )} +
+ )} +
+ Total + {formatCurrency(total)} +
+
+
+ ); +} + +type PaymentSectionProps = { + order: OrderDetail; + brandId: string | null; + total: number; +}; + +function PaymentSection({ order, brandId, total }: PaymentSectionProps) { + return ( +
+

+ Payment & Refunds +

+

+ Record payment details and manage refunds +

+ +
+ +
+
+ ); +} + +type EditOrderSectionProps = { + order: OrderDetail; + brandId: string | null; +}; + +function EditOrderSection({ order, brandId }: EditOrderSectionProps) { + return ( +
+

+ Edit Order +

+

+ Update customer details, pricing, and status +

+ +
+ [0]["order"]} brandId={brandId} /> +
+
+ ); +} + +type InternalNotesProps = { + order: OrderDetail; +}; + +function InternalNotes({ order }: InternalNotesProps) { + if (!order.internal_notes) return null; + return ( +
+

+ Internal Notes +

+

+ {order.internal_notes} +

+
+ ); +} + +function OrderNotFound() { + return ( +
+
+ + ← Back to Orders + +
+

+ Order not found +

+
+
+
+ ); +} + export default async function OrderDetailPage({ params }: OrderDetailPageProps) { const { id } = await params; const order = await getAdminOrderDetail(id); if (!order) { - return ( -
-
- - ← Back to Orders - -
-

- Order not found -

-
-
-
- ); + return ; } const adminUser = await getAdminUser(); @@ -91,325 +476,20 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps) style={{ backgroundColor: "var(--admin-bg)" }} >
- {/* Back link */} - - - - - Back to Orders - - - {/* Header */} -
-

- Order #{order.id.slice(0, 8).toUpperCase()} · {formatDate(order.created_at)} -

- - - {order.pickup_complete ? "Picked Up" : "Pending"} - - {order.payment_processor && ( - {order.payment_processor} - )} -
- } - /> -
- - {/* Customer info */} -
-
-
-

- Customer -

-

- {order.customer_name} -

-
- -
- -
- {order.customer_phone && ( -
-

- Phone -

-

- {order.customer_phone} -

-
- )} - {order.customer_email && ( -
-

- Email -

-

- {order.customer_email} -

-
- )} -
-
- - {/* Stop info */} - {order.stops && ( -
-

- Pickup Location -

-

- {order.stops.city}, {order.stops.state} -

-

- {formatDate(order.stops.date)} -

-
- )} - - {/* Order items */} -
-

- Order Items -

- - {order.order_items && order.order_items.length > 0 ? ( -
- {order.order_items.map((item: OrderItem) => ( -
-
-

- {item.products?.name ?? "Unknown Product"} -

-

- Qty: {item.quantity} × {formatCurrency(Number(item.price))} -

-
-

- {formatCurrency(Number(item.price) * item.quantity)} -

-
- ))} -
- ) : ( -

- No items found -

- )} - - {/* Totals */} -
-
- Subtotal - - {formatCurrency(subtotal)} - -
- {taxAmount > 0 && ( -
- Tax - - {formatCurrency(taxAmount)} - -
- )} - {discount_amount > 0 && ( -
- Discount - - -{formatCurrency(discount_amount)} - - {order.discount_reason && ( - - ({order.discount_reason}) - - )} -
- )} -
- Total - {formatCurrency(total)} -
-
-
- - {/* Payment & Refunds */} -
-

- Payment & Refunds -

-

- Record payment details and manage refunds -

- -
- -
-
- - {/* Edit form */} -
-

- Edit Order -

-

- Update customer details, pricing, and status -

- -
- [0]["order"]} brandId={brandId} /> -
-
- - {/* Internal notes */} - {order.internal_notes && ( -
-

- Internal Notes -

-

- {order.internal_notes} -

-
- )} + + + + + + + +
); diff --git a/src/app/admin/products/import/page.tsx b/src/app/admin/products/import/page.tsx index baf7872..a659e9e 100644 --- a/src/app/admin/products/import/page.tsx +++ b/src/app/admin/products/import/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useRef } from "react"; +import { useReducer, useRef } from "react"; import Link from "next/link"; import { parseProductCSV } from "@/lib/csv-parsers"; import { importProductsBatch } from "@/actions/import-products"; @@ -16,54 +16,103 @@ type PreviewRow = { _warnings: string[]; }; -export default function ProductImportPage() { - const [csvText, setCsvText] = useState(""); - const [preview, setPreview] = useState(null); - const [parseErrors, setParseErrors] = useState<{ row: number; error: string }[]>([]); - const [importing, setImporting] = useState(false); - const [result, setResult] = useState<{ created: number; updated: number; errors: { product: string; error: string }[] } | null>(null); - const [brandId, setBrandId] = useState(""); - const fileRef = useRef(null); +type ImportResult = { + created: number; + updated: number; + errors: { product: string; error: string }[]; +}; - const SAMPLE_CSV = `name,description,price,type,active,image_url +type State = { + csvText: string; + preview: PreviewRow[] | null; + parseErrors: { row: number; error: string }[]; + importing: boolean; + result: ImportResult | null; + brandId: string; +}; + +type Action = + | { type: "SET_CSV_TEXT"; value: string } + | { type: "SET_BRAND_ID"; value: string } + | { type: "PARSE_SUCCESS"; rows: PreviewRow[]; errors: { row: number; error: string }[] } + | { type: "PARSE_FAIL"; error: string } + | { type: "START_IMPORT" } + | { type: "IMPORT_SUCCESS"; result: ImportResult } + | { type: "IMPORT_FAIL"; error: string }; + +const initialState: State = { + csvText: "", + preview: null, + parseErrors: [], + importing: false, + result: null, + brandId: "", +}; + +function reducer(state: State, action: Action): State { + switch (action.type) { + case "SET_CSV_TEXT": + return { ...state, csvText: action.value }; + case "SET_BRAND_ID": + return { ...state, brandId: action.value }; + case "PARSE_SUCCESS": + return { ...state, preview: action.rows, parseErrors: action.errors }; + case "PARSE_FAIL": + return { ...state, preview: null, parseErrors: [{ row: 0, error: action.error }] }; + case "START_IMPORT": + return { ...state, importing: true }; + case "IMPORT_SUCCESS": + return { ...state, importing: false, result: action.result }; + case "IMPORT_FAIL": + return { + ...state, + importing: false, + result: { created: 0, updated: 0, errors: [{ product: "", error: action.error }] }, + }; + } +} + +const SAMPLE_CSV = `name,description,price,type,active,image_url Dozen Sweet Corn,Fresh picked sweet corn,12.99,Pickup,TRUE, Corn & Butter Bundle,Dozen corn with herb butter,18.99,Pickup & Shipping,TRUE, Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE, `; +export default function ProductImportPage() { + const [state, dispatch] = useReducer(reducer, initialState); + const fileRef = useRef(null); + function handleFile(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { const text = ev.target?.result as string; - setCsvText(text); + dispatch({ type: "SET_CSV_TEXT", value: text }); handlePreview(text); }; reader.readAsText(file); } async function handlePreview(text?: string) { - const toParse = text ?? csvText; + const toParse = text ?? state.csvText; if (!toParse.trim()) return; const parseResult = await parseProductCSV(toParse); if (!parseResult.success) { - setPreview(null); - setParseErrors([{ row: 0, error: parseResult.error }]); + dispatch({ type: "PARSE_FAIL", error: parseResult.error }); return; } - setPreview(parseResult.rows); - setParseErrors(parseResult.errors); + dispatch({ type: "PARSE_SUCCESS", rows: parseResult.rows, errors: parseResult.errors }); } async function handleImport() { - if (!preview || !brandId) return; - setImporting(true); + if (!state.preview || !state.brandId) return; + dispatch({ type: "START_IMPORT" }); const importResult = await importProductsBatch( - brandId, - preview.map((r) => ({ + state.brandId, + state.preview.map((r) => ({ name: r.name, description: r.description, price: r.price, @@ -72,11 +121,13 @@ Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE, image_url: r.image_url, })) ); - setImporting(false); if (importResult.success) { - setResult({ created: importResult.created, updated: importResult.updated, errors: importResult.errors }); + dispatch({ + type: "IMPORT_SUCCESS", + result: { created: importResult.created, updated: importResult.updated, errors: importResult.errors }, + }); } else { - setResult({ created: 0, updated: 0, errors: [{ product: "", error: importResult.error }] }); + dispatch({ type: "IMPORT_FAIL", error: importResult.error }); } } @@ -95,7 +146,7 @@ Citrus Gift Box,Seasonal citrus assortment,34.99,Shipping,TRUE,