0ac4beaaa8
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports)
333 lines
28 KiB
Markdown
333 lines
28 KiB
Markdown
# QA Audit Inventory — 2026-06-25
|
||
|
||
> Generated for [Loop 010 — full product evaluation](https://signals.forwardfuture.com/loop-library/loops/full-product-evaluation-loop/).
|
||
> Scope: Admin shell (all 19 modules) + public/auth pages, tested as `platform_admin` via `dev_session` cookie.
|
||
> Source: 5 parallel `explore` subagents enumerated every UI control, server action, state, and workflow.
|
||
|
||
## Inventory organization
|
||
|
||
- `R1`–`R4`: Role/identity assumptions
|
||
- `A1`–`A11`: Admin shell, layout, dashboard
|
||
- `M1`–`M11`: Admin modules (orders, stops, products, pickup, shipping, communications, wholesale, settings, analytics, users, import)
|
||
- `P1`–`P19`: Public + auth pages
|
||
- `T1`–`T6`: Cross-cutting patterns, design system, server-action catalog
|
||
|
||
Each item: route → purpose → buttons → forms → modals → states → workflows → server actions.
|
||
|
||
---
|
||
|
||
## R — Roles & identity
|
||
|
||
### R1 — `dev_session` cookie impersonation
|
||
**File:** `src/lib/admin-permissions.ts` lines 36-39.
|
||
Gated by `NODE_ENV !== "production"`. Values: `platform_admin`, `brand_admin`, `store_employee`. Returns admin user via `buildDevAdmin(role)` bypassing Neon Auth.
|
||
|
||
### R2 — Real Neon Auth (Better Auth)
|
||
**File:** `src/lib/auth.ts`. Email/password sign-in, password reset, OAuth via `signInWithGoogleAction`. Session via `getSession()`. **Not exercised** in this audit (auth is stubbed).
|
||
|
||
### R3 — RBAC permission flags
|
||
**File:** `admin_users` table; flags: `can_manage_orders`, `can_manage_products`, `can_manage_stops`, `can_manage_customers`, `can_manage_wholesale`, `can_manage_billing`, `can_manage_settings`, `can_manage_water_log`, `can_manage_time_tracking`, `can_manage_route_trace`, `can_manage_reports`, `can_manage_communications`, `can_manage_pickup`, `can_manage_messages`, `can_manage_refunds`, `can_manage_users`, `active`. Default brand_admin = orders/products/stops/customers/pickup/messages/reports = `true`, rest `false`.
|
||
|
||
### R4 — Tenant scoping (brand isolation)
|
||
- App-layer: `effectiveBrandId = brandId ?? adminUser.brand_id ?? null`; threaded through page → client.
|
||
- DB: `current_brand_id()` GUC + RLS policies on most tables (`brand_id = current_brand_id() OR is_platform_admin()`).
|
||
- Platform admin: `brand_id IS NULL`, sees all brands via `withPlatformAdmin()`.
|
||
|
||
---
|
||
|
||
## A — Admin shell, layout, dashboard
|
||
|
||
### A1 — `/admin/layout.tsx`
|
||
Server root layout. Gates access via `getAdminUser()`, redirects on `must_change_password`. Mounts: `AdminSidebar` (desktop), `MobileTabBar` (mobile via v2 layout), `CommandPalette`, `PWAInstallPrompt`, `ToastContainer`, `RouteAnnouncer`, `SmoothViewTransition`. Brand/brand-list/addons fetched with empty fallbacks on error.
|
||
|
||
### A2 — `/admin/page.tsx` (legacy dashboard)
|
||
Redirects to `/admin/v2`. Two paths: `role === "store_employee"` → 2-card landing (Pickup Lookup + Wholesale); otherwise fetches `getDashboardStats()` + `getBillingOverview()` server-side, renders `DashboardClient`.
|
||
|
||
### A3 — `/admin/v2/layout.tsx` + `/admin/v2/page.tsx` (mobile-first dashboard)
|
||
Mobile "Today" dashboard. Fetches summary/stops/orders in parallel. `Promise.all` failure → silent empty state. Renders `EmptyState` ("No brand selected" / "Nothing to show yet"). `pending_fulfillment > 0` → amber callout link `/admin/v2/orders?status=placed`. Top 5 today stops + top 5 pending orders. Pull-to-refresh wrapper.
|
||
|
||
### A4 — `AdminShell.tsx`
|
||
Responsive shell: `useMediaQuery("(min-width: 1024px)")` swaps desktop sidebar vs mobile (`OfflineBanner` + `MobileTabBar`). Mounts `OfflineBanner` only on mobile. `pb-20` mobile bottom padding for tab bar.
|
||
|
||
### A5 — `AdminSidebar.tsx`
|
||
Desktop nav + mobile slide-in. Nav groups: Workspace / Operations / Communications / Growth / Tracking / Insights / Settings. Brand selector pill. Sign-out button (`signOutAction`). Hamburger on `<lg`. Focus trap + arrow-key navigation + body scroll lock + ESC closes. `requiresPlatformAdmin` filter; `enabledAddons` hides Water Log/Route Trace when disabled.
|
||
|
||
### A6 — `MobileTabBar.tsx`
|
||
Bottom nav for `/admin/v2/*`. 5 tabs: Home / Orders / Stops / Products / More (opens `MoreSheet`). Active highlight via `pathname` match.
|
||
|
||
### A7 — `MoreSheet.tsx`
|
||
Right-anchored native `<dialog>`. Sections: Operations (Pickup, Shipping, Reports, Analytics), Marketing (Communications, Sales), Settings (Settings, Advanced). Saves focus on open, restores on close.
|
||
|
||
### A8 — `CommandPalette.tsx`
|
||
Cmd/Ctrl+K modal. Fuzzy search over static entry list (`./command-palette-data.ts`). Up to 8 results. ↑↓ to navigate, Enter to select, ESC closes. `body.style.overflow = "hidden"` while open. Honors `prefers-reduced-motion`.
|
||
|
||
### A9 — `BrandSelector.tsx`
|
||
Pill dropdown for active brand. Calls `setActiveBrand(brandId)` server action, then `router.refresh()`. Outside-click + ESC close. "All brands" option for platform_admin only. Multi-brand badge.
|
||
|
||
### A10 — `AdminAccessDenied.tsx`
|
||
Static card with "Go to Login" + "Return to homepage" links.
|
||
|
||
### A11 — `OfflineBanner.tsx`
|
||
Sticky top banner: warning-soft when syncing, danger-soft when offline. Polls pendingCount every 1 s. Hidden pre-mount to avoid hydration mismatch.
|
||
|
||
---
|
||
|
||
## M — Admin modules
|
||
|
||
### M1 — Orders
|
||
- `/admin/orders` (legacy) → `AdminOrdersPanel`: KPI tiles, status tabs (all/pending/picked_up), search (name/phone/id), stop multi-filter dropdown, table with checkbox + bulk pickup, pagination (PAGE_SIZE 20), `+ New Order` modal. Modal fields: customer name (required), email (optional), phone (optional), stop (optional), items[product+qty+price+fulfillment]. Workflows: create→full-page-reload, single pickup (optimistic), bulk pickup (loop).
|
||
- `/admin/orders/[id]` → `OrderEditForm` + `OrderPaymentSection` + `OrderPickupAction`. Edit fields: items qty/price, discount (must be ≥ 0), customer name (required), email/phone, status (pending/confirmed/cancelled — **fulfilled not exposed**), pickup toggle, internal notes. Payment fields: processor (none/manual/stripe/square/cash/venmo/other), payment status, transaction id, refund amount (0 < x ≤ remaining balance) + reason.
|
||
- `/admin/orders/new` → redirect to `/admin/orders?new=true` (opens modal).
|
||
- `/admin/v2/orders` → card-style list, `?status=` filter (placed/ready/picked-up/cancelled), LIMIT 50, no pagination.
|
||
- `/admin/v2/orders/[id]` → mobile detail with `FulfillmentTimeline` + sticky action bar (`OrderActionButtons`).
|
||
|
||
### M2 — Stops
|
||
- `/admin/stops` → `AdminStopsPanel`: status tabs (all/active/draft/inactive), search city/location, `Add Stop` modal, `Upload Schedule` modal, per-row Edit/Publish/Duplicate/Delete. Delete uses inline popover confirm. PAGE_SIZE 50.
|
||
- `/admin/stops/[id]` → product assignment + edit form + `MessageCustomersSection`.
|
||
- `/admin/stops/new` → inline `NewStopForm` (not modal). Fields: city/state/location/date/time (required), brand select, active, address/zip/cutoff (optional). Also supports `?duplicate={id}`.
|
||
- `/admin/v2/stops` → card list bucketed by Morning/Afternoon/Evening/Anytime, `?date=` filter, LIMIT 200.
|
||
- Modals: `AddStopModal`, `EditStopModal`, `AddLocationModal`, `EditLocationModal`, `LocationsTab`.
|
||
|
||
### M3 — Products
|
||
- `/admin/products` → `ProductsClient` (legacy): list with brand scoping (platform_admin sees all). No client pagination.
|
||
- `/admin/products/new` → `NewProductForm`: name (required), description, price, type (pickup/shipping/both), brand (locked for non-platform_admin), taxable, pickup_type, active, image upload (drag/drop, 5MB cap, client-side resize to 1200px).
|
||
- `/admin/products/import` → CSV upload → `parseProductCSV` → preview → `importProductsBatch`.
|
||
- `/admin/v2/products` → mobile card list with `?filter=` (all/in-stock/low/out/hidden) + `StockAdjustButton` per card.
|
||
|
||
### M4 — Pickup
|
||
- `/admin/pickup` → `DriverPickupPanel`. Pending orders (filter by stop + search), per-order `Pick Up` button (calls `markPickupComplete`). Picked-up section collapsible (last 72h). Toast `✓ Order picked up` 3s.
|
||
|
||
### M5 — Shipping
|
||
- `/admin/shipping` → `ShippingFulfillmentPanel`. Pre-loaded via `getShippingOrders()`. (Details in deeper inspection.)
|
||
- `/admin/settings/shipping` (advanced subtab) → `AdvancedShipping`: static UI mock — no server action calls. Carrier select (fedex/ups/usps/dhl), account/api fields, eye toggle, **Test Connection is simulated (1s setTimeout), Save Settings is simulated (500ms setTimeout)**.
|
||
|
||
### M6 — Communications (Harvest Reach)
|
||
Unified hub at `/admin/communications` with 8 tabs:
|
||
- **Campaigns**: list with type/status filters. `New Campaign` modal (name + campaignType). Edit panel: name, type, template, audience target (stop/zip_code/customer_history/all_customers), stop_id/dates, subject, body, schedule (now/later + datetime-local). Save Draft / Schedule Campaign / Send Campaign buttons. `upsertCampaign`, `deleteCampaign`, `getCampaignTemplates`, `getCommunicationSegments`, `previewCampaignAudience`, `sendCampaign` server actions.
|
||
- **Compose**: legacy redirect target.
|
||
- **Templates**: list + edit (template_type, subject, body_text, body_html, campaign_type). Server: `getCommunicationTemplates`, `getTemplateById`.
|
||
- **Contacts**: search + source filter, pagination (50/page, offset), per-row delete (confirm), export CSV (`exportContacts`). Bulk: none. Card layout on `<sm`.
|
||
- **Segments**: redirect → `?tab=segments`.
|
||
- **Logs**: `MessageLogPanel` — search by email/subject/status, status filter, pagination (20/page, numeric 1-5). Stats cards: total/delivered/failed/pending. `getMessageLogs` (server-side limit 100).
|
||
- **Analytics**: redirect → `?tab=analytics`.
|
||
- **Settings** (`/admin/communications/settings`): `CommunicationSettingsForm` — sender email/name, reply-to, footerHtml (supports `{unsubscribe_url}` placeholder).
|
||
- **Abandoned Carts** (`/admin/communications/abandoned-carts`): 3-step sequence (1h/24h/48h). Stats (total/active/recovered/expired), filter, pagination (25/page, 7×7 prev/next), View/Close/Resend actions per cart. Modal: cart detail with items + timestamps.
|
||
- **Welcome Sequence** (`/admin/communications/welcome-sequence`): 4-email dashboard.
|
||
|
||
Server actions: `getCommunicationCampaigns`, `getCommunicationTemplates`, `getCommunicationContacts`, `getMessageLogs`, `getCommunicationSettings`, `upsertCommunicationSettings`, `getAbandonedCarts`, `manuallyCloseAbandonedCart`, `resendAbandonedCartEmail`, `sendStopBlast`, `previewContactImport`, `importContactsBatch`, `uploadContactsToBucket`, `processBucketImport`, `listImportHistory`, `exportContacts`.
|
||
|
||
### M7 — Wholesale
|
||
- `/admin/wholesale` → `WholesaleClient` (mounted by `getActiveBrandId`). Dashboard tabs: Customers / Orders / Pricing / Settings.
|
||
- Customers: list with search, status filter (active/inactive/suspended/pending), pagination, bulk "Send Price Sheet" (counts).
|
||
- Orders: list with status filter, bulk Fulfill / bulk Deposit (with confirms).
|
||
- Pricing: per-customer price overrides.
|
||
- Settings: `WholesaleSettingsForm` — require_approval, min_order_amount, online_payment_enabled, pickup_location, fob_location, from_email, invoice_business_name.
|
||
|
||
Server actions: `getWholesaleCustomers`, `getWholesaleOrders`, `updateWholesaleCustomer`, `sendPriceSheetToCustomer`, `bulkFulfillOrders`, `recordWholesaleDeposit`, `getWholesaleSettings`, `updateWholesaleSettings`.
|
||
|
||
### M8 — Settings (`/admin/settings`)
|
||
Main hub. Tabs (URL anchor): brand / addons / users / billing.
|
||
- **Brand** (`/admin/settings` `#brand`): `BrandSettingsForm` — company info, address, 4 logo uploads, email/invoice branding, storefront customization (tagline, hero image, about, footer), brand colors (4 pickers), tax settings (collect toggle + nexus states), feature toggles (wholesale/zip/schedule-pdf/text-alerts), schedule PDF footer.
|
||
- **Add-ons** (`/admin/settings` `#addons`): `BrandFeatureCards` — grid of cards from `ADDON_CATALOG`. Enable/Disable toggle (with GlassModal confirm) + optimistic with rollback on failure.
|
||
- **Users** (`/admin/settings` `#users`): list of admin users + `CreateUserModal`. Create fields: email (required, must include @), password (≥6), display name, phone, role (3-way radio gated by caller), brand (required when brand_admin/store_employee), 9 permission toggles. Surfaces temp password on success.
|
||
- **Billing**: tier display + Stripe portal link (if customer_id set).
|
||
- **Payments** (`/admin/settings/payments`): `PaymentSettingsForm` — provider radio (None/Stripe/Square), Stripe OAuth button (`/api/stripe/oauth`), Square OAuth (`/api/stripe/oauth` → Square). Square Location ID validated `L` prefix. Inventory Mode radio (None/RC→Square/Square→RC/Bidirectional). Sync Products/Orders/All Now buttons. Wholesale Webhook Log.
|
||
- **Square Sync** (`/admin/settings/square-sync`): `SquareSyncSettingsClient` — provider/account/last sync/status grid + sync settings + manual sync + last 50 sync log entries.
|
||
- **AI** (`/admin/settings/ai`): link cards only.
|
||
- **Advanced** (`/admin/advanced`): aggregator cards linking to sub-pages.
|
||
- **Integrations** (`AdvancedIntegrations`): Resend + Twilio credential cards (Test Connection + Save).
|
||
- **AI Tools** (`AdvancedAIPanel`): provider cards (OpenAI/Anthropic/Google/xAI/Custom), API key (eye toggle), org ID (OpenAI only), custom endpoint (Custom only), model chips with cost display, Test + Save.
|
||
- **Square Sync** (`AdvancedSquareSync`): static stub — App ID/Access Token/Location ID, sync toggles hardcoded ON, **Test/Save purely client-side setTimeout**.
|
||
- **Shipping** (`AdvancedShipping`): static mock — same as M5.
|
||
- **Webhooks**: "Coming Soon" placeholder.
|
||
- **Payments** (`AdvancedPayments`): Stripe Connect onboarding — Connect with Stripe / Complete Setup / Stripe Dashboard / Disconnect. `createStripeConnectLink`, `refreshStripeConnectLink`, `disconnectStripeConnect`, `createStripeDashboardLink`, `getStripeConnectStatus`.
|
||
|
||
### M9 — Analytics (`/admin/analytics`)
|
||
`AnalyticsDashboard`. KPI cards (revenue, orders, customers, AOV), revenue chart, top products, recent orders, customer growth ring, conversion funnel. Period selector (7D/30D/90D/1Y) — **purely visual, no server refetch**. Refresh + Export Report (no-op). Server: `getAnalyticsMetrics`, `getRevenueChart`, `getTopProducts`, `getRecentOrders`, `getCustomerGrowth`, `getConversionFunnel`.
|
||
|
||
### M10 — Users (`/admin/users`)
|
||
Redirect → `/admin/settings#users`.
|
||
|
||
### M11 — Other admin modules
|
||
- **Import Center** (`/admin/import`): `ImportCenterClient` — 4-step wizard (Upload → Analyze → Preview → Import). Accepts CSV/XLSX/XLS/TXT (≤5,000 rows / 10 MB). AI-powered type detection (products/orders/contacts/stops). Per-column mapping dropdowns. Brand select (platform_admin only). Server: `analyzeImport`, `executeImport`.
|
||
- **Sales import** (`/admin/sales/import`): CSV → `parseOrderCSV` → `importOrdersBatch`. Brand ID (UUID text input) required.
|
||
- **Time Tracking** (`/admin/time-tracking`): `TimeTrackingAdminPanel`. Tabs: Summary / Workers / Tasks / Logs / Settings. Workers modal (name, role, language, active). Tasks modal (name, name_es, unit, active). Logs: date range + worker + task filters, pagination (50/page offset). Export CSV → `/api/time-tracking/export`. Tuxedo-only.
|
||
- **Water Log** (`/admin/water-log`): `WaterLogAdminPanel`. Tabs inline: Headgates / Users / Entries. Add Headgate (name, unit, notes) inline. Add User (name, role=irrigator/water_admin, language=en/es, phone). 4-digit PIN auto-generated + shown in `PinBanner`. Per-headgate: Edit (link), Rotate token, Delete. Per-entry: date range + headgate + user + method filters. Export CSV (5,000 entries). Preview Report = `window.alert` of text. Settings: `/admin/water-log/settings` — admin portal toggle, session duration slider (1-168h), coarse permission flags (edit/delete/export), alert phone. Tuxedo + `can_manage_water_log`.
|
||
- **Headgates** (`/admin/water-log/headgates`): `HeadgatesManager`. Add inline + Edit modal + QR modal (Preview/Print/Download tabs). Bulk QR print via `POST /api/water-qr-sheet` → new window. Per-headgate QR endpoints: `/api/water-qr-label`, `/api/water-qr?token=...`.
|
||
- **Route Trace** (`/admin/route-trace`, `/admin/route-trace/lots`, `/admin/route-trace/lookup`, `/admin/route-trace/settings`): all render the same `RouteTracePage` shell. Server: `getRouteTraceStats`, `getRouteTraceLots`, `getHarvestLotsReadyToHaul`, `getFieldYieldSummary`, `getInventoryByCrop`, `getRecentLotEvents`. Lot detail at `/admin/route-trace/lots/[id]` with timeline + order fulfillment. New lot → modal in Lots tab.
|
||
- **Launch Checklist** (`/admin/launch-checklist`): 8 categories × 4 tasks each (32 tasks). Progress is **purely cosmetic** (hardcoded 20% / 7 of 32). Checkbox buttons have no handlers. Export PDF / Mark All Complete / Mark Launch Complete all no-ops.
|
||
|
||
---
|
||
|
||
## P — Public + auth pages
|
||
|
||
### P1 — `/login`
|
||
Email/password + Google OAuth + dev-mode shortcuts (Platform/Brand/Store buttons).
|
||
- Form: email (required, autoComplete=username), password (required, autoComplete=current-password).
|
||
- Buttons: Sign in (submit), Continue with Google (OAuth), 3 dev shortcuts.
|
||
- Links: `/forgot-password` (Forgot?).
|
||
- States: loading (sign-in/Google), error (inline role=alert), success → redirect `/admin`.
|
||
- Dev shortcuts only when `NODE_ENV !== "production"`.
|
||
|
||
### P2 — `/change-password`
|
||
Force password update.
|
||
- Form: new password (required, minLength 8), confirm password (required).
|
||
- Buttons: Update Password (submit), "Sign out instead" link → `/logout`.
|
||
- Server: `POST /api/auth/change-password`.
|
||
|
||
### P3 — `/reset-password`
|
||
New password from reset link.
|
||
- Form: new password + confirm (both required, ≥ 8 chars, must match).
|
||
- Server: `POST /api/auth/reset-password`.
|
||
|
||
### P4 — `/logout`
|
||
Effect-only: spinner + "Signing out..." → calls `/api/auth/sign-out` → `/login`.
|
||
|
||
### P5 — `/maintenance`
|
||
Static splash. No controls. mailto + `/contact` links.
|
||
|
||
### P6 — `/` (homepage)
|
||
Marketing landing (`LandingPageClient` → `HeroSection`). No forms. CTA links: `/login`, `/brands`, `/contact`. Scroll indicator.
|
||
|
||
### P7 — `/pricing`
|
||
Pricing page. BillingToggle (Monthly/Annual, aria-pressed, "-25%" badge). 3 plan CTA buttons → `/admin`. Compare table expand/collapse. 8 FAQ accordion toggles. No form/select/textarea.
|
||
|
||
### P8 — `/contact`
|
||
Contact form (simulated setTimeout submit).
|
||
- Fields: name (required), email (required), topic select (General/Orders/Wholesale/Partner/Tech), message textarea (required, rows 5).
|
||
- Buttons: Send Message (submit, spinner), Send another message (success state).
|
||
- tel + mailto links.
|
||
- States: idle, isSubmitting, success.
|
||
|
||
### P9 — `/blog`
|
||
Static. Newsletter form (decorative — no submit handler). "Read More" links to `/blog/getting-started-with-route-commerce` (slug not implemented). 3 "Download →" buttons (no handlers).
|
||
|
||
### P10 — `/changelog`
|
||
Static timeline. Links: `/admin`, `/`, RSS feed `/api/feed/changelog.xml`, `/waitlist`.
|
||
|
||
### P11 — `/roadmap`
|
||
Static 3-column roadmap.
|
||
- Suggestion form: feature title (text), description (textarea), category (select). **No submit handler.**
|
||
- 6 Upvote buttons — **no handlers.**
|
||
- Anchor jump `#suggest`.
|
||
|
||
### P12 — `/security`
|
||
Static. mailto security@routecommerce.com for vulnerability reports.
|
||
|
||
### P13 — `/privacy-policy` + `/terms-and-conditions`
|
||
Pure static legal text.
|
||
|
||
### P14 — `/brands`
|
||
Partner brands showcase. Per-brand `<Link href="/${slug}">` "Visit Store" + "View Stops". 3D-tilt parallax.
|
||
|
||
### P15 — `/waitlist`
|
||
`<WaitlistForm>`.
|
||
- Fields: name (optional text), email (required), referral select (Google/Social/Friend/Event/Podcast/Blog/Other).
|
||
- Button: Join the Waitlist (spinner → success card).
|
||
- Server: `POST /api/waitlist`.
|
||
- States: idle, isSubmitting, error, success.
|
||
|
||
### P16 — `/cart`
|
||
`<CartClient>`.
|
||
- Per-item buttons: − (decrease qty), + (increase qty), Remove.
|
||
- Modal: "Choose Pickup Stop" (stop buttons + Cancel).
|
||
- Buttons: Continue to Checkout (state-gated: Cart is Empty / Select Pickup Stop First / Remove Incompatible Items First / Choose Correct Stop / Continue Shopping).
|
||
- States: empty, stop mismatch alert, incompatible items, availability error, loading stops.
|
||
|
||
### P17 — `/checkout`
|
||
`<CheckoutClient>` + Stripe Express iframe.
|
||
- Fields: customer_name (text), customer_email (required), customer_phone (tel optional), stop_id (select required when no stop), shipping_address/city/state (maxLength 2 upper)/postal_code (when ship items).
|
||
- Buttons: Change pickup stop, "Use secure hosted checkout →" (`POST createRetailStripeCheckoutSession`), `<Link href="/">` Back to storefront.
|
||
- States: empty cart, hostedLoading, hostedError, hasPickup/hasShed/hasStop/hasShip branching.
|
||
|
||
### P18 — Auth API routes
|
||
| Route | Method | Behavior |
|
||
|---|---|---|
|
||
| `/api/auth/sign-in` | POST | `{success}` or 400/401/500 with `{error,message}`. |
|
||
| `/api/auth/sign-out` | POST | Server sign-out + redirect `/login`. |
|
||
| `/api/auth/forgot-password` | POST | Always `{success:true}` (anti-enumeration). Calls `requestPasswordReset`. |
|
||
| `/api/auth/reset-password` | POST | Min 8 chars. |
|
||
| `/api/auth/change-password` | POST | Auth-required; uses `setUserPassword`. |
|
||
| `/api/auth/[...nextauth]` | GET/POST | Neon Auth handler proxy. |
|
||
|
||
### P19 — Other public pages
|
||
- `/water` — likely the field portal; brief enumeration.
|
||
- `/test-simple.html` — diagnostic HTML.
|
||
- `/api/feed/changelog.xml` — referenced but not in scope.
|
||
|
||
---
|
||
|
||
## T — Cross-cutting patterns
|
||
|
||
### T1 — Server-action catalog (top-level actions touched by the audited routes)
|
||
`getAdminUser`, `getAdminOrders`, `getAdminOrderDetail`, `markPickupComplete`, `createAdminOrder`, `updateOrder`, `updateOrderItem`, `deleteOrderItem`, `createRefund`, `getShippingOrders`, `createStop`, `updateStop`, `deleteStop`, `publishStop`, `assignProductToStop`, `unassignProductFromStop`, `createLocation`, `updateLocation`, `deleteLocation`, `createProduct`, `uploadProductImage`, `importProductsBatch`, `parseProductCSV`, `analyzeImport`, `executeImport`, `getAnalyticsMetrics`, `getRevenueChart`, `getTopProducts`, `getRecentOrders`, `getCustomerGrowth`, `getConversionFunnel`, `getCommunicationCampaigns`, `getCommunicationTemplates`, `getCommunicationContacts`, `getMessageLogs`, `getCommunicationSettings`, `upsertCommunicationSettings`, `getAbandonedCarts`, `manuallyCloseAbandonedCart`, `resendAbandonedCartEmail`, `sendStopBlast`, `previewContactImport`, `importContactsBatch`, `exportContacts`, `getWholesaleCustomers`, `getWholesaleOrders`, `getWholesaleSettings`, `updateWholesaleSettings`, `sendPriceSheetToCustomer`, `bulkFulfillOrders`, `recordWholesaleDeposit`, `createAdminUser`, `getAdminUsers`, `getBrands`, `getPaymentSettings`, `savePaymentSettings`, `syncSquareNow`, `getSyncLog`, `getResendCredentials`, `saveResendCredentials`, `testResendConnection`, `getTwilioCredentials`, `saveTwilioCredentials`, `testTwilioConnection`, `getStripeConnectStatus`, `createStripeConnectLink`, `refreshStripeConnectLink`, `disconnectStripeConnect`, `createStripeDashboardLink`, `getWaterIrrigators`, `getWaterHeadgatesAdmin`, `getWaterEntries`, `createWaterHeadgate`, `updateWaterHeadgate`, `deleteWaterHeadgate`, `regenerateHeadgateToken`, `createWaterUser`, `resetWaterIrrigatorPin`, `deleteWaterUser`, `getWaterAdminSettings`, `saveWaterAdminSettings`, `regenerateAdminPin`, `getTimeTrackingWorkers`, `getTimeTrackingTasks`, `getTimeTrackingSummary`, `getWorkerTimeLogs`, `createTimeWorker`, `updateTimeWorker`, `deleteTimeWorker`, `resetTimeWorkerPin`, `createTimeTask`, `updateTimeTask`, `deleteTimeTask`, `getRouteTraceStats`, `getRouteTraceLots`, `getRouteTraceLotDetail`, `getLotOrders`, `getHarvestLotsReadyToHaul`, `getFieldYieldSummary`, `getInventoryByCrop`, `getRecentLotEvents`, `signOutAction`, `signInWithGoogleAction`, `toggleBrandFeature`.
|
||
|
||
### T2 — Server-action categories
|
||
- **CRUD**: orders, stops, products, customers, campaigns, templates, contacts, wholesale_*, time_tracking_*, water_log_*, harvest_lots.
|
||
- **Bulk**: `bulkFulfillOrders`, `sendPriceSheetToCustomer` (wholesale); `markPickupComplete` loop (orders).
|
||
- **Auth**: sign-in, sign-out, password reset/change, OAuth.
|
||
- **Integration side-effects**: `syncSquareNow`, `sendCampaign`, `sendStopBlast`, `savePaymentSettings` → Stripe OAuth URL generation.
|
||
- **Reads**: `get*` actions returning typed rows for page/server-component hydration.
|
||
|
||
### T3 — Server-component vs client-component split
|
||
- Every route's `page.tsx` is `"use server"` and resolves `getAdminUser()` + brand + initial datasets in parallel.
|
||
- Heavy interactive panels (orders, stops, products, communications, settings, water-log) are `"use client"` components mounted by their page.
|
||
- v2 dashboard surfaces (`/admin/v2/*`) lean more on server components with `pool.query` directly.
|
||
|
||
### T4 — Auth gating patterns
|
||
- Page-level: `if (!adminUser) redirect("/login")` or `<AdminAccessDenied />`.
|
||
- Permission check: `if (!adminUser.can_manage_X) redirect("/admin/pickup")`.
|
||
- Brand-scope: `effectiveBrandId = brandId ?? adminUser.brand_id ?? firstBrandId`.
|
||
- Direct DB queries in pages use `getActiveBrandId()` helper; admin queries use `pool.query` with explicit `WHERE brand_id = $1` or `1=1` for platform_admin.
|
||
|
||
### T5 — Modal inventory
|
||
| Modal | Triggered from | Server action |
|
||
|---|---|---|
|
||
| `GlassModal` shell | many | n/a (UI only) |
|
||
| `ElegantModal` shell | some | n/a |
|
||
| `AdminOrdersPanel` New Order modal | inline fixed overlay | `createAdminOrder` |
|
||
| `AddStopModal` | `AdminStopsPanel` Add | `createStop` |
|
||
| `EditStopModal` | inline | `createStop` / `updateStop` |
|
||
| `NewStopForm` (inline, not modal) | `/admin/stops/new` | `createStop` |
|
||
| `AddLocationModal` / `EditLocationModal` | `LocationsTab` | `createLocation` / `updateLocation` |
|
||
| `CreateUserModal` | settings Users | `createAdminUser` |
|
||
| `CampaignListPanel` New Campaign | `/admin/communications` | `upsertCampaign` |
|
||
| `AbandonedCartDashboard` detail | abandoned carts page | read-only |
|
||
| `BrandFeatureCards` Enable confirm | settings Add-ons | `toggleBrandFeature` |
|
||
| `HeadgatesManager` Edit/QR | headgates page | `updateWaterHeadgate`, `regenerateHeadgateToken` |
|
||
| Cart stop picker | `/cart` | n/a |
|
||
| `UpgradePlanModal` (lazy) | `DashboardClient` / `DashboardUpgradeButton` | n/a |
|
||
| `CommandPalette` | global Cmd+K | n/a |
|
||
|
||
### T6 — Edge cases inventory (extracted for Phase 3)
|
||
- **Auth/role**: dev_session values, Neon Auth failures, must_change_password redirect, role-mismatched user without brand links.
|
||
- **Empty/zero states**: 0 brands, 0 stops, 0 orders, 0 contacts, 0 campaigns, 0 templates, 0 wholesale customers.
|
||
- **Long data**: very long product names, unicode (αβγ δεζ 🌿🍊 谢谢), special chars (`<script>`-style), URL-encoded emails.
|
||
- **Pagination boundaries**: page 0, last page, beyond last page, single result, exactly PAGE_SIZE, 0 results.
|
||
- **Brand switching**: platform_admin with no active brand, brand with no admin links, multi-brand admin.
|
||
- **Date/time**: past stops, future stops, today, before cutoff, after cutoff, DST boundaries, leap years (2024 just passed).
|
||
- **Status enums**: every allowed value + one disallowed value per field for boundary tests.
|
||
- **Stop states**: active/paused/closed, public/private, with/without cutoff, with/without address.
|
||
- **Order states**: all 4 statuses × 3 fulfillment types × with/without stop × with/without customer.
|
||
- **Order items**: zero items (free order), many items, mixed pickup/ship within one order.
|
||
- **Customers**: only email, only phone, both, no source, no metadata.
|
||
- **Bulk actions**: 0 selected, 1 selected, all selected, partial success.
|
||
- **Network failures**: timeouts, partial responses (not testable here — would need test API endpoints).
|
||
- **Image upload**: missing file, oversized file, wrong type, network failure mid-upload.
|
||
- **CSV import**: malformed CSV, missing columns, duplicate rows, very large file (>5 MB → bucket path).
|
||
- **Modal stacking**: open modal over modal.
|
||
- **Browser back/forward**: `/admin/orders?new=true` modal state preserved?
|
||
- **Concurrent edits**: two admin tabs editing same order.
|
||
- **Offline mode**: `OfflineBanner` state transitions + queued mutations.
|
||
- **Permission transitions**: admin user disabled mid-session.
|
||
|
||
---
|
||
|
||
## Items intentionally not enumerated in this run
|
||
|
||
- Server actions in `src/actions/` (catalogued by name in T1; full parameter signatures to be reviewed during acceptance-criteria phase).
|
||
- JSON API route handlers (`src/app/api/**/route.ts` beyond auth) — 62 routes. Out of scope for UI inventory; they will be exercised indirectly via the admin UI but not unit-tested separately unless a specific bug surfaces.
|
||
- Wholesale portal (`/wholesale/*`) — out of scope per user.
|
||
- Brand storefronts (`/tuxedo/*`, `/indian-river-direct/*`) — out of scope per user.
|
||
- Database RPCs (PL/pgSQL functions) — out of scope for inventory; exercised indirectly via server actions.
|
||
- Background jobs (cron, email automation) — exercised via curl on their API endpoints only if a bug is suspected. |