From 27b2ded94ef43365acae6dae1212da06fa38e01c Mon Sep 17 00:00:00 2001 From: Nora Date: Fri, 26 Jun 2026 04:38:44 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20react-doctor=20unused-file=20123?= =?UTF-8?q?=E2=86=9227=20(-96=20dead=20files=20removed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/schema/audit.ts | 36 - src/actions/admin/audit.ts | 56 - src/actions/square-inventory.ts | 215 -- src/app/admin/settings/ai/AIClient.tsx | 1931 ----------------- .../admin/settings/billing/BillingClient.tsx | 66 - .../integrations/IntegrationsClient.tsx | 653 ------ .../change-password/ChangePasswordForm.tsx | 129 -- src/components/admin/AdminHeader.tsx | 218 -- src/components/admin/AdminStopsPanel.tsx | 349 --- .../admin/AdvancedSettingsClient.tsx | 240 -- src/components/admin/BrandFeatureCards.tsx | 189 -- src/components/admin/CartHydration.tsx | 24 - .../admin/CommunicationsLoading.tsx | 276 --- src/components/admin/CommunicationsNav.tsx | 120 - src/components/admin/DashboardHeader.tsx | 61 - .../admin/DashboardUpgradeButton.tsx | 36 - src/components/admin/ElegantModal.tsx | 93 - .../admin/HarvestReach/HarvestReachNav.tsx | 82 - src/components/admin/IntegrationsInner.tsx | 336 --- src/components/admin/LocationsTab.tsx | 387 ---- src/components/admin/OrderTableBody.tsx | 125 -- .../admin/ProductAssignmentForm.tsx | 142 -- src/components/admin/ProductFilterBar.tsx | 50 - src/components/admin/ProductSyncBanner.tsx | 101 - src/components/admin/ProductTableBody.tsx | 249 --- src/components/admin/ProductTableClient.tsx | 292 --- .../admin/SquareSyncWidgetWrapper.tsx | 9 - src/components/admin/StatsStrip.tsx | 31 - src/components/admin/StopDetailModal.tsx | 394 ---- src/components/admin/StopMessagingForm.tsx | 240 -- src/components/admin/StopTableBody.tsx | 58 - src/components/admin/StopsCalendarClient.tsx | 754 ------- src/components/admin/StopsHeaderActions.tsx | 73 - src/components/admin/StopsLocationsTabs.tsx | 107 - src/components/admin/StopsViewClient.tsx | 132 -- src/components/admin/TabSwitcher.tsx | 38 - src/components/admin/TaxQuarterlySummary.tsx | 95 - .../admin/design-system/AdminModal.tsx | 79 - .../admin/design-system/ToastExport.ts | 6 - src/components/admin/shared/DataTable.tsx | 123 -- src/components/admin/shared/FilterBar.tsx | 109 - src/components/admin/shared/PageHeader.tsx | 42 - src/components/admin/shared/StatusBadge.tsx | 51 - src/components/admin/shared/index.ts | 4 - src/components/admin/stops/StopsCalendar.tsx | 432 ---- .../admin/stops/StopsDashboardClient.tsx | 236 -- src/components/admin/stops/StopsList.tsx | 72 - src/components/admin/stops/StopsLocations.tsx | 412 ---- src/components/admin/stops/types.ts | 70 - src/components/changelog/ChangelogFeed.tsx | 118 - src/components/landing/FeaturesAndStats.tsx | 683 ------ src/components/landing/TestimonialsAndCTA.tsx | 670 ------ .../notifications/InAppNotificationCenter.tsx | 219 -- src/components/onboarding/OnboardingFlow.tsx | 316 --- .../providers/AnalyticsProvider.tsx | 15 - src/components/providers/ErrorBoundary.tsx | 58 - src/components/referral/ReferralSystem.tsx | 199 -- src/components/route-trace/LotCreateForm.tsx | 310 --- .../route-trace/PublicTraceActions.tsx | 12 - src/components/shared/EmptyState.tsx | 27 - src/components/shared/LoadingSkeleton.tsx | 121 -- src/components/shared/SectionHeader.tsx | 26 - src/components/shared/ThemeToggle.tsx | 44 - src/components/storefront/BreadcrumbNav.tsx | 98 - .../storefront/IndianRiverGrainOverlay.tsx | 21 - .../storefront/IndianRiverPaginatedStops.tsx | 179 -- src/components/storefront/ZipCodeSearch.tsx | 120 - .../wholesale/admin/AddRecipientForm.tsx | 52 - .../wholesale/admin/CustomerPricingPanel.tsx | 124 -- .../wholesale/admin/CustomersTab.tsx | 531 ----- .../wholesale/admin/DashboardTab.tsx | 126 -- src/components/wholesale/admin/OrdersTab.tsx | 538 ----- .../wholesale/admin/PriceSheetModal.tsx | 101 - .../wholesale/admin/ProductsTab.tsx | 262 --- .../wholesale/admin/SettingsTab.tsx | 287 --- .../wholesale/admin/StatusBadge.tsx | 25 - .../admin/WebhookSettingsSection.tsx | 165 -- .../wholesale/admin/WholesaleIcon.tsx | 9 - .../admin/WholesaleLoadingSkeleton.tsx | 47 - src/components/wholesale/admin/types.ts | 29 - src/config/brands.ts | 37 - src/lib/admin-permissions-service.ts | 76 - src/lib/auth-guards.ts | 19 - src/lib/billing.ts | 308 --- src/lib/data-service.ts | 146 -- src/lib/error-handler.ts | 94 - src/lib/fedex-auth.ts | 77 - src/lib/oauth/square.ts | 89 - src/lib/oauth/stripe.ts | 82 - src/lib/route-trace-data.ts | 59 - src/lib/route-trace-units.ts | 11 - src/lib/safe-html.tsx | 40 - src/lib/seo.ts | 218 -- src/lib/svc-headers.ts | 16 - src/lib/validation.ts | 337 --- supabase/push-migrations.js | 155 -- 96 files changed, 17319 deletions(-) delete mode 100644 db/schema/audit.ts delete mode 100644 src/actions/admin/audit.ts delete mode 100644 src/actions/square-inventory.ts delete mode 100644 src/app/admin/settings/ai/AIClient.tsx delete mode 100644 src/app/admin/settings/billing/BillingClient.tsx delete mode 100644 src/app/admin/settings/integrations/IntegrationsClient.tsx delete mode 100644 src/app/change-password/ChangePasswordForm.tsx delete mode 100644 src/components/admin/AdminHeader.tsx delete mode 100644 src/components/admin/AdminStopsPanel.tsx delete mode 100644 src/components/admin/AdvancedSettingsClient.tsx delete mode 100644 src/components/admin/BrandFeatureCards.tsx delete mode 100644 src/components/admin/CartHydration.tsx delete mode 100644 src/components/admin/CommunicationsLoading.tsx delete mode 100644 src/components/admin/CommunicationsNav.tsx delete mode 100644 src/components/admin/DashboardHeader.tsx delete mode 100644 src/components/admin/DashboardUpgradeButton.tsx delete mode 100644 src/components/admin/ElegantModal.tsx delete mode 100644 src/components/admin/HarvestReach/HarvestReachNav.tsx delete mode 100644 src/components/admin/IntegrationsInner.tsx delete mode 100644 src/components/admin/LocationsTab.tsx delete mode 100644 src/components/admin/OrderTableBody.tsx delete mode 100644 src/components/admin/ProductAssignmentForm.tsx delete mode 100644 src/components/admin/ProductFilterBar.tsx delete mode 100644 src/components/admin/ProductSyncBanner.tsx delete mode 100644 src/components/admin/ProductTableBody.tsx delete mode 100644 src/components/admin/ProductTableClient.tsx delete mode 100644 src/components/admin/SquareSyncWidgetWrapper.tsx delete mode 100644 src/components/admin/StatsStrip.tsx delete mode 100644 src/components/admin/StopDetailModal.tsx delete mode 100644 src/components/admin/StopMessagingForm.tsx delete mode 100644 src/components/admin/StopTableBody.tsx delete mode 100644 src/components/admin/StopsCalendarClient.tsx delete mode 100644 src/components/admin/StopsHeaderActions.tsx delete mode 100644 src/components/admin/StopsLocationsTabs.tsx delete mode 100644 src/components/admin/StopsViewClient.tsx delete mode 100644 src/components/admin/TabSwitcher.tsx delete mode 100644 src/components/admin/TaxQuarterlySummary.tsx delete mode 100644 src/components/admin/design-system/AdminModal.tsx delete mode 100644 src/components/admin/design-system/ToastExport.ts delete mode 100644 src/components/admin/shared/DataTable.tsx delete mode 100644 src/components/admin/shared/FilterBar.tsx delete mode 100644 src/components/admin/shared/PageHeader.tsx delete mode 100644 src/components/admin/shared/StatusBadge.tsx delete mode 100644 src/components/admin/shared/index.ts delete mode 100644 src/components/admin/stops/StopsCalendar.tsx delete mode 100644 src/components/admin/stops/StopsDashboardClient.tsx delete mode 100644 src/components/admin/stops/StopsList.tsx delete mode 100644 src/components/admin/stops/StopsLocations.tsx delete mode 100644 src/components/admin/stops/types.ts delete mode 100644 src/components/changelog/ChangelogFeed.tsx delete mode 100644 src/components/landing/FeaturesAndStats.tsx delete mode 100644 src/components/landing/TestimonialsAndCTA.tsx delete mode 100644 src/components/notifications/InAppNotificationCenter.tsx delete mode 100644 src/components/onboarding/OnboardingFlow.tsx delete mode 100644 src/components/providers/AnalyticsProvider.tsx delete mode 100644 src/components/providers/ErrorBoundary.tsx delete mode 100644 src/components/referral/ReferralSystem.tsx delete mode 100644 src/components/route-trace/LotCreateForm.tsx delete mode 100644 src/components/route-trace/PublicTraceActions.tsx delete mode 100644 src/components/shared/EmptyState.tsx delete mode 100644 src/components/shared/LoadingSkeleton.tsx delete mode 100644 src/components/shared/SectionHeader.tsx delete mode 100644 src/components/shared/ThemeToggle.tsx delete mode 100644 src/components/storefront/BreadcrumbNav.tsx delete mode 100644 src/components/storefront/IndianRiverGrainOverlay.tsx delete mode 100644 src/components/storefront/IndianRiverPaginatedStops.tsx delete mode 100644 src/components/storefront/ZipCodeSearch.tsx delete mode 100644 src/components/wholesale/admin/AddRecipientForm.tsx delete mode 100644 src/components/wholesale/admin/CustomerPricingPanel.tsx delete mode 100644 src/components/wholesale/admin/CustomersTab.tsx delete mode 100644 src/components/wholesale/admin/DashboardTab.tsx delete mode 100644 src/components/wholesale/admin/OrdersTab.tsx delete mode 100644 src/components/wholesale/admin/PriceSheetModal.tsx delete mode 100644 src/components/wholesale/admin/ProductsTab.tsx delete mode 100644 src/components/wholesale/admin/SettingsTab.tsx delete mode 100644 src/components/wholesale/admin/StatusBadge.tsx delete mode 100644 src/components/wholesale/admin/WebhookSettingsSection.tsx delete mode 100644 src/components/wholesale/admin/WholesaleIcon.tsx delete mode 100644 src/components/wholesale/admin/WholesaleLoadingSkeleton.tsx delete mode 100644 src/components/wholesale/admin/types.ts delete mode 100644 src/config/brands.ts delete mode 100644 src/lib/admin-permissions-service.ts delete mode 100644 src/lib/auth-guards.ts delete mode 100644 src/lib/billing.ts delete mode 100644 src/lib/data-service.ts delete mode 100644 src/lib/error-handler.ts delete mode 100644 src/lib/fedex-auth.ts delete mode 100644 src/lib/oauth/square.ts delete mode 100644 src/lib/oauth/stripe.ts delete mode 100644 src/lib/route-trace-data.ts delete mode 100644 src/lib/route-trace-units.ts delete mode 100644 src/lib/safe-html.tsx delete mode 100644 src/lib/seo.ts delete mode 100644 src/lib/svc-headers.ts delete mode 100644 src/lib/validation.ts delete mode 100644 supabase/push-migrations.js diff --git a/db/schema/audit.ts b/db/schema/audit.ts deleted file mode 100644 index 6efa836..0000000 --- a/db/schema/audit.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Audit log. Source of truth: `db/migrations/0001_init.sql`. - */ -import { - pgTable, - uuid, - text, - jsonb, - timestamp, - index, -} from "drizzle-orm/pg-core"; -import { brands } from "./brands"; - -export const auditLog = pgTable( - "audit_log", - { - id: uuid("id").primaryKey().defaultRandom(), - brandId: uuid("brand_id").references(() => brands.id, { - onDelete: "cascade", - }), - userId: uuid("user_id"), // FK to neon_auth.user(id) — plain UUID, enforced at DB level - action: text("action").notNull(), - targetType: text("target_type"), - targetId: uuid("target_id"), - payload: jsonb("payload"), - createdAt: timestamp("created_at", { withTimezone: true }) - .notNull() - .defaultNow(), - }, - (t) => ({ - brandIdx: index("audit_log_brand_idx").on(t.brandId, t.createdAt), - }), -); - -export type AuditLog = typeof auditLog.$inferSelect; -export type NewAuditLog = typeof auditLog.$inferInsert; diff --git a/src/actions/admin/audit.ts b/src/actions/admin/audit.ts deleted file mode 100644 index ff0e095..0000000 --- a/src/actions/admin/audit.ts +++ /dev/null @@ -1,56 +0,0 @@ -"use server"; - -import { pool } from "@/lib/db"; -import { getSession } from "@/lib/auth"; - -type AdminActionPayload = { - action_type: "create" | "update" | "delete"; - admin_id?: string; - admin_email?: string; - affected_user_id?: string; - brand_id?: string; - details?: Record; -}; - -type UserActivityPayload = { - user_id: string; - activity_type: "login" | "logout" | "password_change" | "profile_update" | "email_change"; - details?: Record; - ip_address?: string; - user_agent?: string; -}; - -export async function logAdminAction(payload: AdminActionPayload): Promise { - -await getSession(); try { - await pool.query("SELECT log_admin_action($1::jsonb)", [ - JSON.stringify({ - action_type: payload.action_type, - admin_id: payload.admin_id ?? null, - admin_email: payload.admin_email ?? null, - affected_user_id: payload.affected_user_id ?? null, - brand_id: payload.brand_id ?? null, - details: payload.details ?? {}, - }), - ]); - } catch { - // logging failed silently - } -} - -export async function logUserActivity(payload: UserActivityPayload): Promise { - -await getSession(); try { - await pool.query("SELECT log_user_activity($1::jsonb)", [ - JSON.stringify({ - user_id: payload.user_id, - activity_type: payload.activity_type, - details: payload.details ?? {}, - ip_address: payload.ip_address ?? null, - user_agent: payload.user_agent ?? null, - }), - ]); - } catch { - // logging failed silently - } -} diff --git a/src/actions/square-inventory.ts b/src/actions/square-inventory.ts deleted file mode 100644 index 8a7a006..0000000 --- a/src/actions/square-inventory.ts +++ /dev/null @@ -1,215 +0,0 @@ -"use server"; - -import { getAdminUser } from "@/lib/admin-permissions"; -import { getPaymentSettings } from "@/actions/payments"; -import { withBrand } from "@/db/client"; -import { products } from "@/db/schema"; -import { inArray } from "drizzle-orm"; -import { getSession } from "@/lib/auth"; - -function getSquareBaseUrl() { - return process.env.SQUARE_ENVIRONMENT === "production" - ? "https://connect.squareup.com" - : "https://connect.squareupsandbox.com"; -} - -async function getSquareCatalogItemVariation( - accessToken: string, - catalogObjectId: string -) { - const baseUrl = getSquareBaseUrl(); - const response = await fetch( - `${baseUrl}/v2/catalog/object/${catalogObjectId}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - "Square-Version": "2025-01-16", - }, - } - ); - if (!response.ok) throw new Error(`Catalog object fetch failed: ${await response.text()}`); - return response.json(); -} - -async function batchUpdateSquareInventory( - accessToken: string, - locationId: string, - updates: Array<{ - catalogObjectId: string; - quantity: number; - type: "AVAILABLE" | "ON_HAND" | "SOLD_OUT"; - }> -) { - const baseUrl = getSquareBaseUrl(); - const response = await fetch( - `${baseUrl}/v2/inventory/changes/batch-create`, - { - method: "POST", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - "Square-Version": "2025-01-16", - }, - body: JSON.stringify({ - idempotency_key: crypto.randomUUID(), - changes: updates.map((u) => ({ - type: "ADJUSTMENT", - physical_count: { - catalog_object_id: u.catalogObjectId, - location_id: locationId, - quantity: String(u.quantity), - measurement_unit: { quantity_unit: u.type }, - }, - occurred_at: new Date().toISOString(), - })), - }), - } - ); - if (!response.ok) { - const err = await response.text(); - throw new Error(`Batch inventory update failed: ${err}`); - } - return response.json(); -} - -export type SyncResult = { - success: boolean; - synced: number; - errors: string[]; -}; - -/** - * Sync inventory from Route Commerce products TO Square. - * Reduces Square inventory for each product sold. - * Called after an RC order is placed or updated. - * - * @param brandId - brand to sync for - * @param items - array of { productId, quantity } representing sold items - */ -export async function syncInventoryToSquare( - brandId: string, - items: Array<{ productId: string; quantity: number }> -): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, synced: 0, errors: ["Not authenticated"] }; - - const settingsResult = await getPaymentSettings(brandId); - if (!settingsResult.success || !settingsResult.settings) { - return { success: false, synced: 0, errors: ["Could not load payment settings"] }; - } - const settings = settingsResult.settings; - - if ( - !settings.square_access_token || - !settings.square_location_id || - !settings.square_sync_enabled || - !["rc_to_square", "bidirectional"].includes(settings.square_inventory_mode) - ) { - return { success: true, synced: 0, errors: [] }; // Not enabled, no-op - } - - const errors: string[] = []; - const synced = 0; - - // Build product name → quantity map - const itemQtyMap = new Map(items.map((i) => [i.productId, i.quantity])); - - try { - // Fetch product details from RC - const productIds = items.map((i) => i.productId); - const rows = await withBrand(brandId, (db) => - db - .select({ id: products.id, name: products.name }) - .from(products) - .where(inArray(products.id, productIds)) - ); - if (rows.length === 0 && productIds.length > 0) { - return { success: false, synced: 0, errors: ["Failed to fetch products from RC"] }; - } - - // Find Square catalog items matching product names and reduce quantity - const updates: Array<{ catalogObjectId: string; quantity: number; type: "AVAILABLE" | "ON_HAND" | "SOLD_OUT" }> = []; - - for (const product of rows) { - const qty = itemQtyMap.get(product.id) ?? 0; - if (qty <= 0) continue; - - // In a real implementation, we'd maintain a mapping of RC product ID → Square catalog object ID - // For now, we skip this without the mapping (requires manual catalog linking) - // A future improvement would store square_catalog_object_id on RC products table - errors.push(`Product "${product.name}": inventory sync requires Square catalog object ID mapping (not yet implemented)`); - } - } catch (err) { - errors.push(`Inventory sync error: ${String(err)}`); - } - - return { success: errors.length === 0, synced, errors }; -} - -/** - * Sync inventory from Square TO Route Commerce (polling). - * Fetches Square inventory counts and updates RC product inventory. - */ -export async function syncInventoryFromSquare(brandId: string): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, synced: 0, errors: ["Not authenticated"] }; - if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) { - return { success: false, synced: 0, errors: ["Not authorized"] }; - } - - const settingsResult = await getPaymentSettings(brandId); - if (!settingsResult.success || !settingsResult.settings) { - return { success: false, synced: 0, errors: ["Could not load payment settings"] }; - } - const settings = settingsResult.settings; - - if ( - !settings.square_access_token || - !settings.square_location_id || - !settings.square_sync_enabled || - !["square_to_rc", "bidirectional"].includes(settings.square_inventory_mode) - ) { - return { success: true, synced: 0, errors: [] }; // Not enabled, no-op - } - - const errors: string[] = []; - const synced = 0; - - try { - // Fetch Square inventory counts for the location - const baseUrl = getSquareBaseUrl(); - const response = await fetch( - `${baseUrl}/v2/inventory/${settings.square_location_id}/counts`, - { - method: "GET", - headers: { - Authorization: `Bearer ${settings.square_access_token}`, - "Content-Type": "application/json", - "Square-Version": "2025-01-16", - }, - } - ); - - if (!response.ok) { - return { success: false, synced: 0, errors: [`Square inventory fetch failed: ${await response.text()}`] }; - } - - const data = await response.json(); - - // For each inventory count, update RC product if we have a mapping - for (const count of data.counts ?? []) { - // count.catalog_object_id, count.quantity, count.calculated_at - // Would need a mapping table to update correct RC product - // This is noted as a future enhancement - errors.push(`Inventory item ${count.catalog_object_id}: requires Square catalog object ID mapping`); - } - } catch (err) { - errors.push(`Inventory sync error: ${String(err)}`); - } - - return { success: errors.length === 0, synced, errors }; -} diff --git a/src/app/admin/settings/ai/AIClient.tsx b/src/app/admin/settings/ai/AIClient.tsx deleted file mode 100644 index da83174..0000000 --- a/src/app/admin/settings/ai/AIClient.tsx +++ /dev/null @@ -1,1931 +0,0 @@ -"use client"; - -import { useEffect, useRef, useState } from "react"; -import Link from "next/link"; -import { AdminCard } from "@/components/admin/design-system"; -import { setAIProviderSettings } from "@/actions/integrations/ai-providers"; - -// ── Pure helpers (module scope) ────────────────────────────────────────────── - -function copyToClipboard(text: string) { - navigator.clipboard.writeText(text); -} - -const reportTypes = [ - { value: "orders-by-stop", label: "Orders by Stop" }, - { value: "sales-by-product", label: "Sales by Product" }, - { value: "fulfillment", label: "Fulfillment" }, - { value: "pickup-status", label: "Pickup Status" }, - { value: "contact-growth", label: "Contact Growth" }, - { value: "campaigns", label: "Campaign Activity" }, -]; - -const exampleQueries = [ - "Which customers haven't ordered in 45 days?", - "What products are trending this month?", - "Who are my top customers by revenue?", - "Show recent orders from the last 7 days", - "Which customers are at risk of churning?", -]; - -// ── Header Icon Component ───────────────────────────────────────────────────── - -function AIHeaderIcon() { - return ( -
- - - -
- ); -} - -// ── Tool Card Component ─────────────────────────────────────────────────────── - -type ToolCardProps = { - tool: AITool; - isConnected: boolean; - onOpen: (tool: AITool) => void; -}; - -function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) { - const isReady = tool.status === "available" && isConnected; - - return ( - -
- {tool.icon} -
-
-

{tool.title}

- {tool.badge && ( - - {tool.badge} - - )} - {tool.status === "available" && ( - - {isConnected ? "Ready" : "Disabled"} - - )} - {tool.status === "experimental" && ( - - Experimental - - )} - {tool.status === "coming_soon" && ( - - Coming Soon - - )} -
- {tool.module} -

{tool.description}

-
-
-
- {tool.status === "available" && ( - - )} - {tool.status === "coming_soon" && ( - - )} - {tool.status === "experimental" && ( - - )} -
-
- ); -} - -// ── Connection Status Banner ─────────────────────────────────────────────────── - -function ConnectionStatusBanner({ isConnected, availableCount, onConfigure }: { - isConnected: boolean; - availableCount: number; - onConfigure: () => void; -}) { - if (isConnected) { - return ( - -
-
- - - -
-
-

AI Connected

-

{availableCount} tool{availableCount !== 1 ? "s" : ""} ready to use

-
- - Active - -
-
- ); - } - - return ( - -
-
- - - -
-
-

AI Not Configured

-

Add your API key to enable AI tools.

-
- -
-
- ); -} - -type ToolStatus = "available" | "coming_soon" | "experimental"; - -type AITool = { - id: string; - title: string; - description: string; - icon: string; - module: string; - status: ToolStatus; - badge?: string; -}; - -const AI_TOOLS: AITool[] = [ - { - id: "campaign-writer", - title: "Campaign Idea Generator", - description: "Generate email campaign topics, angles, and subject lines based on your products, season, and customer segments.", - icon: "📧", - module: "Harvest Reach", - status: "available", - badge: "New", - }, - { - id: "product-writer", - title: "Product Description Writer", - description: "AI-assisted product descriptions, pricing analysis, and image alt-text generation for your catalog.", - icon: "🛒", - module: "Products", - status: "available", - }, - { - id: "route-suggester", - title: "Route Optimizer", - description: "Get AI-powered suggestions for stop ordering, grouping, and delivery sequence to minimize drive time.", - icon: "🗺️", - module: "Stops & Routes", - status: "available", - }, - { - id: "customer-insights", - title: "Customer Insights", - description: "Natural language queries across orders and customers. Ask 'Which customers haven't ordered in 30 days?' or 'What products are trending this month?'", - icon: "🔍", - module: "Reports", - status: "available", - }, - { - id: "stop-blast-advisor", - title: "Stop Blast Advisor", - description: "AI suggestions for timing, content, and audience targeting for operational stop blast messages.", - icon: "📢", - module: "Harvest Reach", - status: "available", - }, - { - id: "pricing-advisor", - title: "Pricing Advisor", - description: "Analyze demand, seasonality, and historical sales to suggest optimal product prices for your market.", - icon: "💰", - module: "Products", - status: "available", - }, - { - id: "report-explainer", - title: "Report Explainer", - description: "Plain-English summaries of complex reports. Just ask 'why did sales drop this week?' and get an AI-generated breakdown.", - icon: "📊", - module: "Reports", - status: "available", - }, - { - id: "demand-forecast", - title: "Demand Forecasting", - description: "Predict order volumes and popular products for upcoming stops based on historical trends and seasonal patterns.", - icon: "📈", - module: "Orders", - status: "available", - }, -]; - -type Provider = "openai" | "anthropic" | "google" | "xai" | "custom"; - -type Props = { - isConnected: boolean; - brandId: string; - brandName: string; - provider?: Provider; - model?: string; - customEndpoint?: string; -}; - -// ── Provider Selector ───────────────────────────────────────────────────────── - -type ProviderOption = { - id: Provider; - name: string; - icon: string; - description: string; -}; - -const PROVIDERS: ProviderOption[] = [ - { id: "openai", name: "OpenAI", icon: "💩", description: "GPT-4, GPT-4o, o1" }, - { id: "anthropic", name: "Anthropic", icon: "🧠", description: "Claude 3.5 Sonnet, Opus" }, - { id: "google", name: "Google", icon: "🔴", description: "Gemini 2.0 Flash, Pro" }, - { id: "xai", name: "xAI", icon: "⚡", description: "Grok-2, Grok-1.5" }, - { id: "custom", name: "Custom", icon: "🔧", description: "Bring your own endpoint" }, -]; - -// ── Model Input ─────────────────────────────────────────────────────────────── - -function ModelInput({ - currentModel, - brandId, - onChange -}: { - currentModel: string; - brandId: string; - onChange: (model: string) => void; -}) { - // Uncontrolled input: the editable buffer is the DOM input itself. - // The parent uses `key={currentModel}` so that when the prop changes - // externally, this component fully remounts and `defaultValue` is - // re-applied from the new prop — no stale copy. - const inputRef = useRef(null); - const [saving, setSaving] = useState(false); - const [saved, setSaved] = useState(false); - const [error, setError] = useState(null); - - const handleSave = async () => { - const value = inputRef.current?.value ?? ""; - if (!value.trim()) return; - setSaving(true); - setError(null); - - const result = await setAIProviderSettings(brandId, { model: value.trim() }); - if (result.success) { - onChange(value.trim()); - setSaved(true); - setTimeout(() => setSaved(false), 2000); - } else { - setError(result.error ?? "Failed to save"); - } - setSaving(false); - }; - - return ( - -
- 🤖 -

- Model -

-
-
- - -
-

- ⚠️ Model ID must be exact — check the provider's documentation for the correct identifier. -

- {error &&

{error}

} -
- ); -} - -// ── Provider Selector Component ───────────────────────────────────────────── - -function ProviderSelector({ - currentProvider, - currentModel, - brandId, - onSelect -}: { - currentProvider: Provider; - currentModel: string; - brandId: string; - onSelect: (provider: Provider) => void; -}) { - const handleSelect = async (provider: Provider) => { - onSelect(provider); - await setAIProviderSettings(brandId, { provider }); - }; - - return ( -
-
- 💩 -

- AI Provider -

-
-
- {PROVIDERS.map((provider) => ( - - ))} -
-

- Current model: {currentModel} -

-
- ); -} - -// ── Form Input Styles (Shared) ──────────────────────────────────────────────── - -const inputBaseClass = "w-full rounded-xl px-4 py-2.5 text-sm outline-none transition-colors"; -const inputStyle = { - border: '1px solid var(--admin-border)', - backgroundColor: 'var(--admin-card-bg)', - color: 'var(--admin-text-primary)', -}; -const inputFocusStyle = { - borderColor: 'var(--admin-accent)', - boxShadow: '0 0 0 3px rgba(202, 117, 67, 0.1)', -}; - -const textareaStyle = { ...inputStyle }; -const textareaFocusStyle = { ...inputFocusStyle }; - -const btnPrimaryClass = "rounded-xl px-5 py-2.5 text-sm font-bold text-white transition-all"; -const btnPrimaryStyle = { - background: 'linear-gradient(135deg, #ca7543 0%, #d4865a 100%)', - boxShadow: '0 2px 8px rgba(202, 117, 67, 0.25)', -}; - -const labelStyle = { - display: 'block', - fontSize: '0.875rem', - fontWeight: 500, - marginBottom: '0.25rem', - color: 'var(--admin-text-primary)', -}; - -// ── Campaign Writer Tool ────────────────────────────────────────────────────── - -function CampaignWriterTool({ brandId, brandName }: { brandId: string; brandName: string }) { - const [topic, setTopic] = useState(""); - const [loading, setLoading] = useState(false); - const [results, setResults] = useState<{ angle: string; subject: string; body: string }[]>([]); - const [error, setError] = useState(null); - - async function handleGenerate() { - if (!topic.trim()) return; - setLoading(true); - setError(null); - setResults([]); - - try { - const res = await fetch("/api/ai/campaign-writer", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ topic, brandId, brandName }), - }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error ?? "Generation failed"); - setResults(data.ideas ?? []); - } catch (err) { - setError(err instanceof Error ? err.message : "Generation failed"); - } finally { - setLoading(false); - } - } - - return ( -
-
- -