From 3249ff0a559e7dc5c6be2fdfc07484258a4129af Mon Sep 17 00:00:00 2001 From: Nora Date: Fri, 26 Jun 2026 04:44:15 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20react-doctor=20unused-file=2027=E2=86=92?= =?UTF-8?q?0=20(-27=20more=20dead=20files,=20wholesale=20consolidate)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/ai/preferences.ts | 107 ---- src/actions/locations.ts | 281 --------- src/actions/route-trace/types.ts | 157 ----- src/actions/settings/features.ts | 73 --- src/actions/stops/get-stop-customers.ts | 70 --- src/actions/stops/get-stop-details.ts | 115 ---- src/actions/stripe-connect.ts | 258 -------- src/actions/wholesale/customers.ts | 99 --- src/actions/wholesale/deposits.ts | 89 --- src/actions/wholesale/index.ts | 76 --- src/actions/wholesale/notifications.ts | 94 --- src/actions/wholesale/orders.ts | 135 ---- src/actions/wholesale/products.ts | 105 ---- src/actions/wholesale/scope.ts | 71 --- src/actions/wholesale/settings.ts | 91 --- src/actions/wholesale/types.ts | 153 ----- src/actions/wholesale/webhooks.ts | 96 --- src/components/admin/AddLocationModal.tsx | 288 --------- src/components/admin/AddStopModal.tsx | 419 ------------- src/components/admin/AdvancedAIPanel.tsx | 574 ------------------ src/components/admin/AdvancedIntegrations.tsx | 387 ------------ src/components/admin/AdvancedPayments.tsx | 471 -------------- src/components/admin/AdvancedShipping.tsx | 212 ------- src/components/admin/AdvancedSquareSync.tsx | 184 ------ src/components/admin/EditLocationModal.tsx | 301 --------- src/components/admin/SquareSyncWidget.tsx | 267 -------- src/components/storefront/HeroSection.tsx | 179 ------ 27 files changed, 5352 deletions(-) delete mode 100644 src/actions/ai/preferences.ts delete mode 100644 src/actions/locations.ts delete mode 100644 src/actions/route-trace/types.ts delete mode 100644 src/actions/settings/features.ts delete mode 100644 src/actions/stops/get-stop-customers.ts delete mode 100644 src/actions/stops/get-stop-details.ts delete mode 100644 src/actions/stripe-connect.ts delete mode 100644 src/actions/wholesale/customers.ts delete mode 100644 src/actions/wholesale/deposits.ts delete mode 100644 src/actions/wholesale/index.ts delete mode 100644 src/actions/wholesale/notifications.ts delete mode 100644 src/actions/wholesale/orders.ts delete mode 100644 src/actions/wholesale/products.ts delete mode 100644 src/actions/wholesale/scope.ts delete mode 100644 src/actions/wholesale/settings.ts delete mode 100644 src/actions/wholesale/types.ts delete mode 100644 src/actions/wholesale/webhooks.ts delete mode 100644 src/components/admin/AddLocationModal.tsx delete mode 100644 src/components/admin/AddStopModal.tsx delete mode 100644 src/components/admin/AdvancedAIPanel.tsx delete mode 100644 src/components/admin/AdvancedIntegrations.tsx delete mode 100644 src/components/admin/AdvancedPayments.tsx delete mode 100644 src/components/admin/AdvancedShipping.tsx delete mode 100644 src/components/admin/AdvancedSquareSync.tsx delete mode 100644 src/components/admin/EditLocationModal.tsx delete mode 100644 src/components/admin/SquareSyncWidget.tsx delete mode 100644 src/components/storefront/HeroSection.tsx diff --git a/src/actions/ai/preferences.ts b/src/actions/ai/preferences.ts deleted file mode 100644 index 19b78e5..0000000 --- a/src/actions/ai/preferences.ts +++ /dev/null @@ -1,107 +0,0 @@ -"use server"; -import { requireAuth } from "@/lib/admin-permissions"; -import { pool } from "@/lib/db"; - -export type AIAuthConfig = { - provider: string; - api_key: string; - organization_id: string; - base_url: string; - model: string; - max_tokens: number; -}; - -export async function getAIPreferences( - brandId: string, -): Promise<{ - api_key?: string; - organization_id?: string; - base_url?: string; - model?: string; - max_tokens?: number; -} | null> { - await requireAuth(); - - const { rows } = await pool.query<{ - api_key: string; - organization_id: string; - base_url: string; - model: string; - max_tokens: number; - }>( - `SELECT api_key, organization_id, base_url, model, max_tokens - FROM brand_ai_settings - WHERE brand_id = $1 - LIMIT 1`, - [brandId], - ); - - return rows[0] ?? null; -} - -export async function saveAIPreferences( - brandId: string, - config: AIAuthConfig, -): Promise<{ success: boolean; error?: string }> { - await requireAuth(); - - try { - await pool.query( - `INSERT INTO brand_ai_settings ( - brand_id, provider, api_key, organization_id, base_url, model, max_tokens, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now()) - ON CONFLICT (brand_id) DO UPDATE SET - provider = EXCLUDED.provider, - api_key = EXCLUDED.api_key, - organization_id = EXCLUDED.organization_id, - base_url = EXCLUDED.base_url, - model = EXCLUDED.model, - max_tokens = EXCLUDED.max_tokens, - updated_at = EXCLUDED.updated_at`, - [ - brandId, - config.provider, - config.api_key || null, - config.organization_id || null, - config.base_url || null, - config.model || "gpt-4o-mini", - config.max_tokens || 4000, - ], - ); - return { success: true }; - } catch (err) { - const message = err instanceof Error ? err.message : "Unknown error"; - return { success: false, error: message }; - } -} - -export async function testAIConnection( - config: AIAuthConfig, -): Promise<{ ok: boolean; message: string }> { - await requireAuth(); - - if (!config.api_key?.trim()) { - return { ok: false, message: "API key is required" }; - } - - try { - const baseUrl = config.base_url?.trim() || "https://api.openai.com/v1"; - const url = `${baseUrl.replace(/\/$/, "")}/models`; - - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${config.api_key}`, - }, - }); - - if (response.ok) { - return { ok: true, message: "Connection successful! Your API key is valid." }; - } - if (response.status === 401) { - return { ok: false, message: "Invalid API key. Please check and try again." }; - } - return { ok: false, message: `Error: ${response.status} ${response.statusText}` }; - } catch { - return { ok: false, message: "Could not connect. Check your network and API key." }; - } -} \ No newline at end of file diff --git a/src/actions/locations.ts b/src/actions/locations.ts deleted file mode 100644 index cdfec39..0000000 --- a/src/actions/locations.ts +++ /dev/null @@ -1,281 +0,0 @@ -"use server"; - -import { revalidateTag } from "next/cache"; -import { getAdminUser } from "@/lib/admin-permissions"; -import { getActiveBrandId } from "@/lib/brand-scope"; -import { pool } from "@/lib/db"; -import { getSession } from "@/lib/auth"; - -export type LocationInput = { - name: string; - address?: string | null; - city?: string | null; - state?: string | null; - zip?: string | null; - phone?: string | null; - contact_name?: string | null; - contact_email?: string | null; - notes?: string | null; - active?: boolean; -}; - -export type Location = { - id: string; - brand_id: string; - name: string; - address: string | null; - city: string | null; - state: string | null; - zip: string | null; - phone: string | null; - contact_name: string | null; - contact_email: string | null; - notes: string | null; - active: boolean; - deleted_at: string | null; - created_at: string; - updated_at: string; - slug: string | null; -}; - -export type LocationWithCount = Location & { stop_count: number }; - -// ── Create (single) ────────────────────────────────────────────────────────── -export async function createLocation( - brandId: string, - input: LocationInput -): Promise<{ success: true; id: string; slug: string } | { success: false; error: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_stops) { - return { success: false, error: "Not authorized to manage locations" }; - } - const activeBrandId = await getActiveBrandId(adminUser, brandId); - if (!activeBrandId && adminUser.role !== "platform_admin") { - return { success: false, error: "Brand access required" }; - } - const effectiveBrandId = activeBrandId; - if (!effectiveBrandId) return { success: false, error: "No brand selected" }; - - try { - const { rows } = await pool.query<{ id: string; slug: string }>( - `SELECT * FROM admin_create_location( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 - )`, - [ - effectiveBrandId, - input.name, - input.address ?? null, - input.city ?? null, - input.state ?? null, - input.zip ?? null, - input.phone ?? null, - input.contact_name ?? null, - input.contact_email ?? null, - input.notes ?? null, - ], - ); - const data = rows[0]; - if (!data) { - return { success: false, error: "Insert failed" }; - } - revalidateTag("locations", "default"); - revalidateTag(`brand:${effectiveBrandId}:locations`, "default"); - return { success: true, id: data.id, slug: data.slug }; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Insert failed", - }; - } -} - -// ── Create (batch) ─────────────────────────────────────────────────────────── -export async function createLocationsBatch( - brandId: string, - locations: LocationInput[] -): Promise<{ success: boolean; created: number; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, created: 0, error: "Not authenticated" }; - if (!adminUser.can_manage_stops) { - return { success: false, created: 0, error: "Not authorized" }; - } - const activeBrandId = await getActiveBrandId(adminUser, brandId); - if (!activeBrandId && adminUser.role !== "platform_admin") { - return { success: false, created: 0, error: "Brand access required" }; - } - const effectiveBrandId = activeBrandId; - if (!effectiveBrandId) return { success: false, created: 0, error: "No brand selected" }; - - let inserted: { id?: string }[] = []; - try { - const { rows } = await pool.query<{ id?: string }>( - "SELECT * FROM admin_create_locations_batch($1, $2::jsonb)", - [effectiveBrandId, JSON.stringify(locations)], - ); - inserted = rows; - } catch (err) { - return { - success: false, - created: 0, - error: err instanceof Error ? err.message : "Insert failed", - }; - } - - revalidateTag("locations", "default"); - revalidateTag(`brand:${effectiveBrandId}:locations`, "default"); - return { - success: true, - created: Array.isArray(inserted) ? inserted.length : locations.length, - }; -} - -// ── Update (partial) ───────────────────────────────────────────────────────── -export async function updateLocation( - locationId: string, - brandId: string, - updates: Partial -): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_stops) return { success: false, error: "Not authorized" }; - - let data: { success?: boolean; error?: string } = {}; - try { - const { rows } = await pool.query<{ success?: boolean; error?: string }>( - "SELECT * FROM admin_update_location($1, $2, $3::jsonb)", - [locationId, brandId, JSON.stringify(updates)], - ); - data = rows[0] ?? {}; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Update failed", - }; - } - - if (!data.success) { - return { success: false, error: data.error ?? "Update failed" }; - } - - revalidateTag("locations", "default"); - revalidateTag(`brand:${brandId}:locations`, "default"); - return { success: true }; -} - -// ── Delete (soft) ──────────────────────────────────────────────────────────── -export async function deleteLocation( - locationId: string, - brandId: string -): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_stops) return { success: false, error: "Not authorized" }; - - let data: { success?: boolean; error?: string } = {}; - try { - const { rows } = await pool.query<{ success?: boolean; error?: string }>( - "SELECT * FROM admin_delete_location($1, $2)", - [locationId, brandId], - ); - data = rows[0] ?? {}; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Delete failed", - }; - } - - if (!data.success) { - return { success: false, error: data.error ?? "Delete failed" }; - } - - revalidateTag("locations", "default"); - revalidateTag(`brand:${brandId}:locations`, "default"); - return { success: true }; -} - -// ── Read (admin, by brand_id) ──────────────────────────────────────────────── -export async function adminListLocations( - brandId: string -): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return []; - - const activeBrandId = await getActiveBrandId(adminUser, brandId); - const effectiveBrandId = activeBrandId; - - try { - const { rows } = await pool.query( - "SELECT * FROM admin_list_locations($1)", - [effectiveBrandId], - ); - return Array.isArray(rows) ? rows : []; - } catch { - return []; - } -} - -// ── Read (public, by brand slug) ───────────────────────────────────────────── -export type PublicLocation = Pick< - Location, - "id" | "name" | "address" | "city" | "state" | "zip" | "phone" | "slug" -> & { stop_count: number }; - -export async function getPublicLocationsForBrand( - brandSlug: string -): Promise { - -await getSession(); if (!brandSlug) return []; - - try { - const { rows } = await pool.query( - "SELECT * FROM get_locations_for_brand($1)", - [brandSlug], - ); - return Array.isArray(rows) ? rows : []; - } catch { - return []; - } -} - -// ── Attach a stop to a location ────────────────────────────────────────────── -export async function attachStopToLocation( - stopId: string, - locationId: string, - brandId: string -): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_stops) return { success: false, error: "Not authorized" }; - - let data: { success?: boolean; error?: string } = {}; - try { - const { rows } = await pool.query<{ success?: boolean; error?: string }>( - "SELECT * FROM admin_attach_location_to_stop($1, $2, $3)", - [stopId, locationId, brandId], - ); - data = rows[0] ?? {}; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Attach failed", - }; - } - - if (!data.success) { - return { success: false, error: data.error ?? "Attach failed" }; - } - - revalidateTag("stops", "default"); - revalidateTag("locations", "default"); - revalidateTag(`brand:${brandId}:stops`, "default"); - revalidateTag(`brand:${brandId}:locations`, "default"); - return { success: true }; -} diff --git a/src/actions/route-trace/types.ts b/src/actions/route-trace/types.ts deleted file mode 100644 index 8164e43..0000000 --- a/src/actions/route-trace/types.ts +++ /dev/null @@ -1,157 +0,0 @@ -export interface HarvestLot { - id: string; - brand_id: string; - lot_number: string; - crop_type: string; - variety: string | null; - harvest_date: string; - field_location: string | null; - worker_name: string | null; - packer_name: string | null; - quantity_lbs: number | null; - quantity_used_lbs: number | null; - status: "active" | "in_transit" | "at_shed" | "packed" | "delivered"; - notes: string | null; - source_stop_id: string | null; - destination_stop_id: string | null; - created_at: string; - updated_at: string; - bin_id: string | null; - container_id: string | null; - field_block: string | null; - pallets: number | null; - yield_estimate_lbs: number | null; - yield_unit: string | null; -} - -export interface LotEvent { - id: string; - event_type: string; - event_time: string; - location: string | null; - notes: string | null; - created_by_name: string | null; - created_at: string; - bin_id: string | null; -} - -export interface LotDetail { - lot_id: string; - lot_number: string; - crop_type: string; - variety: string | null; - harvest_date: string; - field_location: string | null; - worker_name: string | null; - packer_name: string | null; - quantity_lbs: number | null; - quantity_used_lbs: number | null; - status: string; - notes: string | null; - source_stop_id: string | null; - destination_stop_id: string | null; - created_at: string; - updated_at: string; - bin_id: string | null; - container_id: string | null; - field_block: string | null; - pallets: number | null; - yield_estimate_lbs: number | null; - yield_unit: string | null; - events: LotEvent[]; -} - -export interface RouteTraceStats { - active_count: number; - in_transit_count: number; - at_shed_count: number; - total_lots_today: number; - total_harvested_today: number; - total_lots: number; -} - -export interface RecentLotEvent { - event_id: string; - event_type: string; - event_time: string; - location: string | null; - bin_id: string | null; - notes: string | null; - created_by_name: string | null; - lot_id: string; - lot_number: string; - crop_type: string; - status: string; -} - -export interface LotOrder { - id: string; - customer_name: string; - order_date: string; - stop_name: string; - item_quantity: number | null; - item_notes: string | null; - fulfillment: string | null; - lot_quantity_used: number | null; -} - -export interface TraceChain { - lot: HarvestLot; - events: LotEvent[]; - orders: Array<{ id: string; customer_name: string; order_date: string; stop_name: string }>; -} - -export interface HaulingLot { - lot_id: string; - lot_number: string; - crop_type: string; - harvest_date: string; - status: string; - field_location: string | null; - worker_name: string | null; - quantity_lbs: number | null; - yield_unit: string | null; - bin_id: string | null; - container_id: string | null; - field_block: string | null; - pallets: number | null; - destination_stop_id: string | null; - destination_stop_name: string | null; - destination_stop_time: string | null; -} - -export interface FieldYieldSummary { - field_location: string; - field_block: string; - total_yield_estimate: number; - total_quantity_lbs: number; - active_lots: number; - yield_unit: string | null; -} - -export interface InventoryByCrop { - crop_type: string; - status: string; - total_lbs: number; - total_estimate: number; - lot_count: number; - yield_unit: string | null; -} - -export interface CreateLotData { - crop_type: string; - variety?: string; - harvest_date: string; - field_location?: string; - worker_name?: string; - packer_name?: string; - quantity_lbs?: number; - notes?: string; - destination_stop_id?: string; - bin_id?: string; - container_id?: string; - field_block?: string; - yield_estimate_lbs?: number; - yield_unit?: string; - pallets?: number; -} diff --git a/src/actions/settings/features.ts b/src/actions/settings/features.ts deleted file mode 100644 index b69cb7f..0000000 --- a/src/actions/settings/features.ts +++ /dev/null @@ -1,73 +0,0 @@ -"use server"; - -import { getAdminUser } from "@/lib/admin-permissions"; -import { withBrand } from "@/db/client"; -import { brandSettings } from "@/db/schema"; -import { eq } from "drizzle-orm"; -import { revalidatePath } from "next/cache"; -import { getSession } from "@/lib/auth"; -import { - invalidateBrandFeatureCache, - type BrandFeatureKey, -} from "@/lib/feature-flags"; - -export type ToggleFeatureResult = - | { success: true } - | { success: false; error: string }; - -/** - * Toggle an add-on feature flag for a brand. The new schema stores feature - * flags as a JSONB blob in `brand_settings.feature_flags` — see - * `src/lib/feature-flags.ts` for the read path. The legacy RPC - * `set_brand_feature(p_brand_id, p_feature_key, p_enabled)` and the - * `brand_features` table it wrote to are gone. - */ -export async function toggleBrandFeature( - brandId: string, - featureKey: BrandFeatureKey, - enabled: boolean -): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") { - return { success: false, error: "Not authorized" }; - } - if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) { - return { success: false, error: "Not authorized for this brand" }; - } - - try { - await withBrand(brandId, async (db) => { - const existing = await db - .select({ featureFlags: brandSettings.featureFlags }) - .from(brandSettings) - .where(eq(brandSettings.brandId, brandId)) - .limit(1); - const current = (existing[0]?.featureFlags ?? {}) as Record; - const next = { ...current, [featureKey]: enabled }; - if (existing.length === 0) { - // No row yet — bootstrap with the brand name from tenants. - await db - .insert(brandSettings) - .values({ brandId: brandId, featureFlags: next }); - } else { - await db - .update(brandSettings) - .set({ featureFlags: next, updatedAt: new Date() }) - .where(eq(brandSettings.brandId, brandId)); - } - }); - - invalidateBrandFeatureCache(brandId); - revalidatePath("/admin/settings/apps"); - revalidatePath("/admin"); - - return { success: true }; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Failed to toggle feature", - }; - } -} diff --git a/src/actions/stops/get-stop-customers.ts b/src/actions/stops/get-stop-customers.ts deleted file mode 100644 index 680da97..0000000 --- a/src/actions/stops/get-stop-customers.ts +++ /dev/null @@ -1,70 +0,0 @@ -"use server"; - -/** - * List the customers with pending pickups for a given stop. - * - * TODO(migration): the SaaS rebuild's `orders` table - * (db/schema/orders.ts) doesn't carry a `stop_id` column, so this - * helper queries the legacy `orders` table directly via `pool.query` - * for the customer-name / email / phone. When the new schema grows a - * `stop_id` reference, switch this read to a Drizzle query. - */ - -import { getAdminUser } from "@/lib/admin-permissions"; -import { assertBrandAccess } from "@/lib/brand-scope"; -import { pool } from "@/lib/db"; -import { getSession } from "@/lib/auth"; - -export type StopCustomer = { - id: string; - customer_name: string; - customer_email: string | null; - customer_phone: string | null; - pickup_complete: boolean; -}; - -export type GetStopPendingCustomersResult = - | { success: true; customers: StopCustomer[] } - | { success: false; error: string }; - -export async function getStopPendingCustomers( - stopId: string -): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_stops) return { success: false, error: "Not authorized" }; - - // Resolve the stop's brand so we can scope-check. - const { rows: stopRows } = await pool.query<{ brand_id: string | null }>( - "SELECT brand_id::text AS brand_id FROM stops WHERE id = $1 LIMIT 1", - [stopId] - ); - const brandId = stopRows[0]?.brand_id ?? null; - if (brandId) { - try { assertBrandAccess(adminUser, brandId); } catch { - return { success: false, error: "Brand access denied" }; - } - } - - try { - const { rows } = await pool.query( - `SELECT id::text AS id, - COALESCE(customer_name, '') AS customer_name, - customer_email, - customer_phone, - COALESCE(pickup_complete, false) AS pickup_complete - FROM orders - WHERE stop_id = $1 - AND COALESCE(pickup_complete, false) = false - ORDER BY created_at DESC`, - [stopId] - ); - return { success: true, customers: rows }; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Failed to load customers", - }; - } -} diff --git a/src/actions/stops/get-stop-details.ts b/src/actions/stops/get-stop-details.ts deleted file mode 100644 index c06ad2e..0000000 --- a/src/actions/stops/get-stop-details.ts +++ /dev/null @@ -1,115 +0,0 @@ -"use server"; - -import { getAdminUser } from "@/lib/admin-permissions"; -import { pool } from "@/lib/db"; -import { getSession } from "@/lib/auth"; - -export type StopDetail = { - id: string; - city: string; - state: string; - date: string; - time: string; - location: string; - slug: string; - active: boolean; - brand_id: string; - address: string | null; - zip: string | null; - cutoff_time: string | null; - brands: { name: string; slug: string } | { name: string; slug: string }[] | null; -}; - -export type AssignedProduct = { - id: string; - product_id: string; - products: { name: string; type: string; price: number } | null; -}; - -export type StopDetailsResult = - | { - success: true; - stop: StopDetail; - allProducts: { id: string; name: string; type: string; price: number }[]; - assignedProducts: AssignedProduct[]; - brands: { id: string; name: string; slug: string }[]; - /** admin_users.user_id of the caller, forwarded to RPCs that authorise via user_id lookup */ - callerUid: string; - } - | { success: false; error: string }; - -/** - * Fetch a single stop with its brand, all candidate products, currently - * assigned products, and the list of brands (for the brand switcher in the - * edit form). Mirrors the data the old `/admin/stops/[id]` page server - * component loaded, so the modal can be a drop-in replacement. - */ -export async function getStopDetails(stopId: string): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_stops) { - return { success: false, error: "Not authorized" }; - } - - // 1. Stop + brand — legacy schema, raw SQL (the Drizzle stops table - // maps to the new schema which doesn't have city/state/date/etc). - const stopRes = await pool.query( - `SELECT s.*, b.name AS brand_name, b.slug AS brand_slug - FROM stops s - LEFT JOIN brands b ON b.id = s.brand_id - WHERE s.id = $1 - LIMIT 1`, - [stopId], - ); - const stopRow = stopRes.rows[0]; - if (!stopRow) { - return { success: false, error: "Stop not found" }; - } - const stop: StopDetail = { - ...stopRow, - brands: stopRow.brand_name - ? { name: stopRow.brand_name, slug: stopRow.brand_slug } - : null, - }; - - // Brand-scope check for brand_admin - if (adminUser.brand_id && stop.brand_id !== adminUser.brand_id) { - return { success: false, error: "Not authorized for this brand" }; - } - - // 2 + 3 + 4. Candidate products, assigned products, and brand switcher - // list — all independent reads, fetched in parallel. - const [ - { rows: allProducts }, - { rows: productStops }, - { rows: brands }, - ] = await Promise.all([ - pool.query<{ id: string; name: string; type: string; price: number }>( - `SELECT id, name, type, price - FROM products - WHERE brand_id = $1 AND active = true`, - [stop.brand_id], - ), - pool.query( - `SELECT ps.id, ps.product_id, - json_build_object('id', p.id, 'name', p.name, 'type', p.type, 'price', p.price) AS products - FROM product_stops ps - LEFT JOIN products p ON p.id = ps.product_id - WHERE ps.stop_id = $1`, - [stopId], - ), - pool.query<{ id: string; name: string; slug: string }>( - `SELECT id, name, slug FROM brands ORDER BY name`, - ), - ]); - - return { - success: true, - stop, - allProducts: allProducts ?? [], - assignedProducts: (productStops ?? []) as AssignedProduct[], - brands: brands ?? [], - callerUid: adminUser.user_id, - }; -} diff --git a/src/actions/stripe-connect.ts b/src/actions/stripe-connect.ts deleted file mode 100644 index e03af78..0000000 --- a/src/actions/stripe-connect.ts +++ /dev/null @@ -1,258 +0,0 @@ -"use server"; - -import { getAdminUser } from "@/lib/admin-permissions"; -import { pool } from "@/lib/db"; -import Stripe from "stripe"; -import { getSession } from "@/lib/auth"; - -// Stripe API version type -type StripeApiVersion = "2026-06-24.dahlia"; - -/** - * Get Stripe Connect status for a brand. - * Checks if brand has a stripe_user_id (connected account). - */ -export async function getStripeConnectStatus(brandId: string): Promise<{ - is_connected: boolean; - account_id?: string; - charges_enabled?: boolean; - payouts_enabled?: boolean; - details_submitted?: boolean; - error?: string; -}> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { is_connected: false, error: "Not authenticated" }; - - // Get brand's payment settings via SECURITY DEFINER RPC - const { rows } = await pool.query<{ stripe_user_id: string | null }>( - "SELECT * FROM get_brand_payment_settings($1)", - [brandId] - ); - - const stripeUserId = rows[0]?.stripe_user_id ?? null; - - if (!stripeUserId) { - return { is_connected: false }; - } - - // Verify the account exists and get status - try { - const stripeKey = process.env.STRIPE_SECRET_KEY; - if (!stripeKey) { - return { is_connected: true, account_id: stripeUserId }; - } - - const stripe = new Stripe(stripeKey, { apiVersion: "2026-06-24.dahlia" as StripeApiVersion }); - const account = await stripe.accounts.retrieve(stripeUserId); - - return { - is_connected: true, - account_id: stripeUserId, - charges_enabled: account.charges_enabled, - payouts_enabled: account.payouts_enabled, - details_submitted: account.details_submitted, - }; - } catch { - // If we can't verify, assume connected but with stale data - return { - is_connected: true, - account_id: stripeUserId, - charges_enabled: false, - payouts_enabled: false, - details_submitted: false, - }; - } -} - -/** - * Create a new Stripe Connect Express account for a brand - * and return an onboarding link. - */ -export async function createStripeConnectLink(brandId: string): Promise<{ - success: boolean; - url?: string; - account_id?: string; - error?: string; -}> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") { - return { success: false, error: "Not authorized" }; - } - - const stripeKey = process.env.STRIPE_SECRET_KEY; - if (!stripeKey) { - return { success: false, error: "Stripe is not configured on this platform" }; - } - - try { - const stripe = new Stripe(stripeKey, { apiVersion: "2026-06-24.dahlia" as StripeApiVersion }); - const origin = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"; - - // Create Express account - const account = await stripe.accounts.create({ - type: "express", - capabilities: { - card_payments: { requested: true }, - transfers: { requested: true }, - }, - metadata: { - brand_id: brandId, - }, - }); - - // Create account link for onboarding - const accountLink = await stripe.accountLinks.create({ - account: account.id, - refresh_url: `${origin}/admin/advanced?stripe_refresh=true`, - return_url: `${origin}/admin/advanced?stripe_connected=true`, - type: "account_onboarding", - }); - - return { - success: true, - url: accountLink.url, - account_id: account.id, - }; - } catch (err) { - const message = err instanceof Error ? err.message : "Failed to create Stripe account"; - return { success: false, error: message }; - } -} - -/** - * Create a new account link for an existing Stripe Connect account - * (for refreshing expired links or re-onboarding) - */ -export async function refreshStripeConnectLink(brandId: string): Promise<{ - success: boolean; - url?: string; - error?: string; -}> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") { - return { success: false, error: "Not authorized" }; - } - - // Get existing account ID - const status = await getStripeConnectStatus(brandId); - if (!status.is_connected || !status.account_id) { - // No existing account, create new one - return createStripeConnectLink(brandId); - } - - const stripeKey = process.env.STRIPE_SECRET_KEY; - if (!stripeKey) { - return { success: false, error: "Stripe is not configured on this platform" }; - } - - try { - const stripe = new Stripe(stripeKey, { apiVersion: "2026-06-24.dahlia" as StripeApiVersion }); - const origin = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"; - - const accountLink = await stripe.accountLinks.create({ - account: status.account_id, - refresh_url: `${origin}/admin/advanced?stripe_refresh=true`, - return_url: `${origin}/admin/advanced?stripe_connected=true`, - type: "account_onboarding", - }); - - return { - success: true, - url: accountLink.url, - }; - } catch (err) { - const message = err instanceof Error ? err.message : "Failed to refresh onboarding link"; - return { success: false, error: message }; - } -} - -/** - * Save Stripe Connect account ID to brand settings - */ -export async function saveStripeConnectAccount(brandId: string, accountId: string): Promise<{ - success: boolean; - error?: string; -}> { - -await getSession(); try { - // Save to payment_settings via SECURITY DEFINER RPC - await pool.query( - "SELECT set_stripe_connect_account($1, $2)", - [brandId, accountId] - ); - return { success: true }; - } catch (e: unknown) { - const error = e instanceof Error ? e.message : String(e); - return { success: false, error: `Failed to save: ${error}` }; - } -} - -/** - * Disconnect Stripe Connect account from a brand - */ -export async function disconnectStripeConnect(brandId: string): Promise<{ - success: boolean; - error?: string; -}> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") { - return { success: false, error: "Not authorized" }; - } - - try { - // SECURITY DEFINER RPC disconnects the Stripe Connect account - await pool.query( - "SELECT disconnect_stripe_connect($1)", - [brandId] - ); - return { success: true }; - } catch { - return { success: false, error: "Failed to disconnect Stripe account" }; - } -} - -/** - * Create a Stripe Express dashboard login link - */ -export async function createStripeDashboardLink(brandId: string): Promise<{ - success: boolean; - url?: string; - error?: string; -}> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") { - return { success: false, error: "Not authorized" }; - } - - const status = await getStripeConnectStatus(brandId); - if (!status.is_connected || !status.account_id) { - return { success: false, error: "No Stripe account connected" }; - } - - const stripeKey = process.env.STRIPE_SECRET_KEY; - if (!stripeKey) { - return { success: false, error: "Stripe is not configured" }; - } - - try { - const stripe = new Stripe(stripeKey, { apiVersion: "2026-06-24.dahlia" as StripeApiVersion }); - const loginLink = await stripe.accounts.createLoginLink(status.account_id); - - return { - success: true, - url: loginLink.url, - }; - } catch (err) { - const message = err instanceof Error ? err.message : "Failed to create dashboard link"; - return { success: false, error: message }; - } -} \ No newline at end of file diff --git a/src/actions/wholesale/customers.ts b/src/actions/wholesale/customers.ts deleted file mode 100644 index 1909da0..0000000 --- a/src/actions/wholesale/customers.ts +++ /dev/null @@ -1,99 +0,0 @@ -"use server"; - -import { pool } from "@/lib/db"; -import { getAdminUser } from "@/lib/admin-permissions"; -import { enforceBrandScope, resolveBrandId } from "./scope"; -import type { WholesaleCustomer } from "./types"; -import { getSession } from "@/lib/auth"; - -export async function getWholesaleCustomers(brandId?: string): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return []; - const bid = await resolveBrandId(adminUser, brandId); - - try { - const { rows } = await pool.query( - "SELECT * FROM get_wholesale_customers($1)", - [bid] - ); - return rows; - } catch { - return []; - } -} - -export async function saveWholesaleCustomer(params: { - brandId: string; - userId?: string; - companyName?: string; - contactName?: string; - email?: string; - phone?: string; - accountStatus?: string; - creditLimit?: number; - depositsEnabled?: boolean; - depositThreshold?: number; - depositPercentage?: number; - orderEmail?: string; - invoiceEmail?: string; - adminNotes?: string; -}): Promise<{ success: boolean; error?: string; id?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - const { error } = await enforceBrandScope(adminUser, params.brandId); - if (error) return { success: false, error }; - - try { - const { rows } = await pool.query<{ id: string }>( - "SELECT * FROM upsert_wholesale_customer($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", - [ - params.brandId, - params.userId ?? null, - params.companyName ?? null, - params.contactName ?? null, - params.email ?? null, - params.phone ?? null, - params.accountStatus ?? "active", - params.creditLimit ?? 0, - params.depositsEnabled ?? false, - params.depositThreshold ?? null, - params.depositPercentage ?? null, - params.orderEmail ?? null, - params.invoiceEmail ?? null, - params.adminNotes ?? null, - ] - ); - const data = Array.isArray(rows) ? rows[0] : rows; - return { success: true, id: data?.id }; - } catch { - return { success: false, error: "Failed to save customer" }; - } -} - -export async function deleteWholesaleCustomer(customerId: string, brandId?: string): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - const { error } = await enforceBrandScope(adminUser, brandId); - if (error) return { success: false, error }; - - try { - const { rows } = await pool.query<{ success: boolean; error?: string }>( - "SELECT * FROM delete_wholesale_customer($1)", - [customerId] - ); - const data = Array.isArray(rows) ? rows[0] : rows; - if (!data?.success) { - return { success: false, error: data?.error ?? "Delete failed" }; - } - return { success: true }; - } catch (e: unknown) { - return { success: false, error: e instanceof Error ? e.message : "Failed to delete customer" }; - } -} \ No newline at end of file diff --git a/src/actions/wholesale/deposits.ts b/src/actions/wholesale/deposits.ts deleted file mode 100644 index 6c5c5c9..0000000 --- a/src/actions/wholesale/deposits.ts +++ /dev/null @@ -1,89 +0,0 @@ -"use server"; - -import { pool } from "@/lib/db"; -import { getAdminUser } from "@/lib/admin-permissions"; -import { getActiveBrandId } from "@/lib/brand-scope"; -import { enqueueWholesaleWebhook } from "./webhooks"; -import { getSession } from "@/lib/auth"; - -export async function recordWholesaleDeposit( - orderId: string, - amount: number, - method: string = "cash", - reference?: string, - brandId?: string -): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - const activeBrandId = await getActiveBrandId(adminUser, brandId); - let data: { success?: boolean; error?: string } | null = null; - try { - const { rows } = await pool.query<{ success: boolean; error?: string }>( - "SELECT * FROM record_wholesale_deposit($1, $2, $3, $4, $5, $6)", - [orderId, amount, method, reference ?? null, adminUser.user_id, activeBrandId] - ); - data = Array.isArray(rows) ? rows[0] : rows; - if (!data?.success) { - return { success: false, error: data?.error ?? "Failed to record deposit" }; - } - } catch (e: unknown) { - return { success: false, error: e instanceof Error ? e.message : "Failed to record deposit" }; - } - - // Fire webhook — fire-and-forget - enqueueWholesaleWebhook("deposit_recorded", orderId, { order_id: orderId, amount }, activeBrandId ?? undefined).catch(() => {}); - - return { success: true }; -} - -export async function bulkFulfillWholesaleOrders( - orderIds: string[] -): Promise<{ success: boolean; count?: number; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - try { - const { rows } = await pool.query<{ success: boolean; count?: number; error?: string }>( - "SELECT * FROM bulk_fulfill_wholesale_orders($1, $2, $3)", - [orderIds, adminUser.user_id, await getActiveBrandId(adminUser)] - ); - const data = Array.isArray(rows) ? rows[0] : rows; - if (!data?.success) { - return { success: false, error: data?.error ?? "Failed to bulk fulfill orders" }; - } - return { success: true, count: data.count }; - } catch (e: unknown) { - return { success: false, error: e instanceof Error ? e.message : "Failed to bulk fulfill orders" }; - } -} - -export async function bulkRecordWholesaleDeposit( - orderIds: string[], - amount: number, - method: string = "cash", - reference?: string -): Promise<{ success: boolean; count?: number; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - try { - const { rows } = await pool.query<{ success: boolean; count?: number; error?: string }>( - "SELECT * FROM bulk_record_wholesale_deposit($1, $2, $3, $4, $5, $6)", - [orderIds, amount, method, reference ?? null, adminUser.user_id, await getActiveBrandId(adminUser)] - ); - const data = Array.isArray(rows) ? rows[0] : rows; - if (!data?.success) { - return { success: false, error: data?.error ?? "Failed to bulk record deposits" }; - } - return { success: true, count: data.count }; - } catch (e: unknown) { - return { success: false, error: e instanceof Error ? e.message : "Failed to bulk record deposits" }; - } -} \ No newline at end of file diff --git a/src/actions/wholesale/index.ts b/src/actions/wholesale/index.ts deleted file mode 100644 index 8f94ad2..0000000 --- a/src/actions/wholesale/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Barrel re-export for the wholesale action set. - * - * Source modules live next to this file: - * - orders.ts — order CRUD + dashboard stats - * - customers.ts — wholesale customer CRUD - * - products.ts — wholesale product CRUD - * - settings.ts — wholesale_settings + public read - * - deposits.ts — deposit recording + bulk actions - * - notifications.ts — email/SMS notification queue - * - webhooks.ts — outbound webhook settings + dispatch - * - scope.ts — internal brand-scoping helpers (NOT re-exported here) - * - types.ts — shared type definitions - * - * Existing imports of `@/actions/wholesale` keep working. New code should - * import directly from the focused modules to keep the dependency graph - * tight. - */ - -export type { - WholesaleOrder, - WholesaleCustomer, - WholesaleProduct, - NotificationRecipient, - WholesaleSettings, - WholesaleDashboardStats, - WholesaleNotification, - WebhookSettings, -} from "./types"; - -export { - getWholesaleOrders, - getWholesalePickupOrders, - getWholesaleDashboardStats, - markWholesaleOrderFulfilled, - updateWholesaleOrderStatus, - deleteWholesaleOrder, -} from "./orders"; - -export { - getWholesaleCustomers, - saveWholesaleCustomer, - deleteWholesaleCustomer, -} from "./customers"; - -export { - getWholesaleProducts, - saveWholesaleProduct, - deleteWholesaleProduct, -} from "./products"; - -export { - getWholesaleSettings, - getWholesaleSettingsPublic, - saveWholesaleSettings, -} from "./settings"; - -export { - recordWholesaleDeposit, - bulkFulfillWholesaleOrders, - bulkRecordWholesaleDeposit, -} from "./deposits"; - -export { - getWholesaleNotificationStats, - getWholesalePendingNotifications, - markWholesaleNotificationSent, - enqueueWholesaleNotification, -} from "./notifications"; - -export { - getWebhookSettings, - saveWebhookSettings, - enqueueWholesaleWebhook, - getRecentWebhookActivity, -} from "./webhooks"; \ No newline at end of file diff --git a/src/actions/wholesale/notifications.ts b/src/actions/wholesale/notifications.ts deleted file mode 100644 index d1f3b81..0000000 --- a/src/actions/wholesale/notifications.ts +++ /dev/null @@ -1,94 +0,0 @@ -"use server"; - -import { pool } from "@/lib/db"; -import { getAdminUser } from "@/lib/admin-permissions"; -import type { WholesaleNotification } from "./types"; -import { getSession } from "@/lib/auth"; - -export async function getWholesaleNotificationStats( - brandId: string -): Promise<{ pending: number; sent: number; failed: number; total: number }> { - -await getSession(); try { - const { rows } = await pool.query<{ pending: number; sent: number; failed: number; total: number }>( - "SELECT * FROM get_wholesale_notification_stats($1)", - [brandId] - ); - return rows[0] ?? { pending: 0, sent: 0, failed: 0, total: 0 }; - } catch { - return { pending: 0, sent: 0, failed: 0, total: 0 }; - } -} - -export async function getWholesalePendingNotifications( - brandId: string, - limit = 50 -): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return []; - if (!adminUser.can_manage_orders) return []; - - try { - const { rows } = await pool.query( - "SELECT * FROM get_wholesale_pending_notifications($1, $2)", - [brandId, limit] - ); - return rows; - } catch { - return []; - } -} - -export async function markWholesaleNotificationSent( - notificationId: string, - error?: string -): Promise<{ success: boolean }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false }; - if (!adminUser.can_manage_orders) return { success: false }; - - try { - await pool.query( - "SELECT mark_wholesale_notification_sent($1, $2)", - [notificationId, error ?? null] - ); - return { success: true }; - } catch { - return { success: false }; - } -} - -export async function enqueueWholesaleNotification(params: { - brandId: string; - customerId: string; - orderId: string; - type: "order_confirmation" | "deposit_received" | "order_fulfilled"; - emailTo: string; - emailCc?: string; - subject: string; - bodyHtml?: string; - bodyText?: string; -}): Promise<{ success: boolean; error?: string }> { - -await getSession(); try { - await pool.query( - "SELECT enqueue_wholesale_notification($1, $2, $3, $4, $5, $6, $7, $8, $9)", - [ - params.brandId, - params.customerId, - params.orderId, - params.type, - params.emailTo, - params.emailCc ?? null, - params.subject, - params.bodyHtml ?? null, - params.bodyText ?? null, - ] - ); - return { success: true }; - } catch { - return { success: false, error: "Failed to enqueue notification" }; - } -} \ No newline at end of file diff --git a/src/actions/wholesale/orders.ts b/src/actions/wholesale/orders.ts deleted file mode 100644 index 4863390..0000000 --- a/src/actions/wholesale/orders.ts +++ /dev/null @@ -1,135 +0,0 @@ -"use server"; - -import { pool } from "@/lib/db"; -import { getAdminUser } from "@/lib/admin-permissions"; -import { getActiveBrandId } from "@/lib/brand-scope"; -import { enforceBrandScope, resolveBrandId } from "./scope"; -import { enqueueWholesaleWebhook } from "./webhooks"; -import type { WholesaleDashboardStats, WholesaleOrder } from "./types"; -import { getSession } from "@/lib/auth"; - -export async function getWholesaleOrders(brandId?: string): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return []; - const bid = await resolveBrandId(adminUser, brandId); - - try { - const { rows } = await pool.query( - "SELECT * FROM get_wholesale_orders($1)", - [bid] - ); - return rows; - } catch { - return []; - } -} - -export async function getWholesalePickupOrders(brandId?: string): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return []; - const bid = await resolveBrandId(adminUser, brandId); - - try { - const { rows } = await pool.query( - "SELECT * FROM get_wholesale_pickup_orders($1)", - [bid] - ); - return rows; - } catch { - return []; - } -} - -export async function getWholesaleDashboardStats(brandId?: string): Promise { - -await getSession(); const orders = await getWholesaleOrders(brandId); - const today = new Date().toISOString().split("T")[0]; - - const open = orders.filter(o => ["pending", "awaiting_deposit", "confirmed"].includes(o.status)); - const pickupToday = orders.filter(o => o.anticipated_pickup_date === today && o.fulfillment_status !== "fulfilled"); - const pastDue = orders.filter(o => o.anticipated_pickup_date && o.anticipated_pickup_date < today && o.fulfillment_status !== "fulfilled"); - const totalUnpaid = open.reduce((sum, o) => sum + Number(o.balance_due), 0); - const awaitingDep = orders.filter(o => o.status === "awaiting_deposit"); - const fulfilledToday = orders.filter(o => o.fulfillment_status === "fulfilled" && o.updated_at?.startsWith(today)); - - return { - open_orders: open.length, - pickup_today: pickupToday.length, - past_due: pastDue.length, - total_unpaid: totalUnpaid, - awaiting_deposit: awaitingDep.length, - fulfilled_today: fulfilledToday.length, - }; -} - -export async function markWholesaleOrderFulfilled(orderId: string, brandId?: string): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - const { brandId: resolved, error } = await enforceBrandScope(adminUser, brandId); - if (error) return { success: false, error }; - - try { - const { rows } = await pool.query( - "SELECT * FROM mark_wholesale_order_fulfilled($1, $2)", - [orderId, adminUser.user_id] - ); - if (!rows || rows.length === 0) { - return { success: false, error: "Failed to mark fulfilled" }; - } - } catch { - return { success: false, error: "Failed to mark fulfilled" }; - } - - enqueueWholesaleWebhook("order_fulfilled", orderId, { order_id: orderId }, resolved ?? undefined).catch(() => {}); - - return { success: true }; -} - -export async function updateWholesaleOrderStatus( - orderId: string, - status: "pending" | "confirmed" | "cancelled", - brandId?: string -): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - const { error } = await enforceBrandScope(adminUser, brandId); - if (error) return { success: false, error }; - - try { - await pool.query( - "SELECT * FROM update_wholesale_order_status($1, $2)", - [orderId, status] - ); - return { success: true }; - } catch { - return { success: false, error: "Failed to update order status" }; - } -} - -export async function deleteWholesaleOrder(orderId: string, brandId?: string): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - const { error } = await enforceBrandScope(adminUser, brandId); - if (error) return { success: false, error }; - - try { - await pool.query( - "SELECT * FROM delete_wholesale_order($1)", - [orderId] - ); - return { success: true }; - } catch { - return { success: false, error: "Failed to delete order" }; - } -} \ No newline at end of file diff --git a/src/actions/wholesale/products.ts b/src/actions/wholesale/products.ts deleted file mode 100644 index f11b952..0000000 --- a/src/actions/wholesale/products.ts +++ /dev/null @@ -1,105 +0,0 @@ -"use server"; - -import { pool } from "@/lib/db"; -import { getAdminUser } from "@/lib/admin-permissions"; -import { enforceBrandScope, resolveBrandId } from "./scope"; -import type { WholesaleProduct } from "./types"; -import { getSession } from "@/lib/auth"; - -export async function getWholesaleProducts(brandId?: string): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return []; - const bid = await resolveBrandId(adminUser, brandId); - - try { - const { rows } = await pool.query( - "SELECT * FROM get_wholesale_products($1)", - [bid] - ); - return rows; - } catch { - return []; - } -} - -export async function saveWholesaleProduct(params: { - brandId: string; - id?: string; - name: string; - description?: string; - unitType?: string; - availability?: string; - qtyAvailable?: number; - priceTiers?: Array<{ min_qty: number; max_qty: number; price: number }>; - hpSku?: string; - hpItemId?: string; - internalNotes?: string; - handlingInstructions?: string; - storageWarning?: string; - productLabel?: string; - packStyle?: string; - containerType?: string; - defaultPickupLocation?: string; -}): Promise<{ success: boolean; error?: string; id?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_products) return { success: false, error: "Not authorized" }; - - const { error } = await enforceBrandScope(adminUser, params.brandId); - if (error) return { success: false, error }; - - try { - const { rows } = await pool.query<{ id: string }>( - "SELECT * FROM upsert_wholesale_product($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11, $12, $13, $14, $15, $16, $17)", - [ - params.brandId, - params.id ?? null, - params.name, - params.description ?? null, - params.unitType ?? "each", - params.availability ?? "unavailable", - params.qtyAvailable ?? 0, - JSON.stringify(params.priceTiers ?? []), - params.hpSku ?? null, - params.hpItemId ?? null, - params.internalNotes ?? null, - params.handlingInstructions ?? null, - params.storageWarning ?? null, - params.productLabel ?? null, - params.packStyle ?? null, - params.containerType ?? null, - params.defaultPickupLocation ?? null, - ] - ); - const data = Array.isArray(rows) ? rows[0] : rows; - return { success: true, id: data?.id }; - } catch { - return { success: false, error: "Failed to save product" }; - } -} - -export async function deleteWholesaleProduct(productId: string, brandId?: string): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_products) return { success: false, error: "Not authorized" }; - - const { error } = await enforceBrandScope(adminUser, brandId); - if (error) return { success: false, error }; - - try { - const { rows } = await pool.query<{ success: boolean; error?: string }>( - "SELECT * FROM delete_wholesale_product($1)", - [productId] - ); - const data = Array.isArray(rows) ? rows[0] : rows; - if (!data?.success) { - return { success: false, error: data?.error ?? "Delete failed" }; - } - return { success: true }; - } catch (e: unknown) { - return { success: false, error: e instanceof Error ? e.message : "Failed to delete product" }; - } -} \ No newline at end of file diff --git a/src/actions/wholesale/scope.ts b/src/actions/wholesale/scope.ts deleted file mode 100644 index 53f0763..0000000 --- a/src/actions/wholesale/scope.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Brand scoping helpers shared across the wholesale actions. - * - * No "use server" so this can also be imported from non-action contexts - * (e.g. middleware, tests). - */ - -import { getAdminUser } from "@/lib/admin-permissions"; -import { getActiveBrandId } from "@/lib/brand-scope"; -import { getSession } from "@/lib/auth"; - -/** - * Resolves the effective brand_id for an action, enforcing brand scoping. - * - * platform_admin → null (means "all brands" — passes to RPC unchanged) - * brand_admin → their own brand_id only; rejects attempts to operate on other brands - * store_employee → their own brand_id - * multi_brand_admin → active brand from cookie/URL/legacy, must be in brand_ids - * unauthenticated → null (actions should already bail out earlier) - * - * This prevents brand_admin from seeing or modifying another brand's data - * even if they manually pass a different brandId to the action. - */ -export async function resolveBrandId( - adminUser: Awaited>, - requestedBrandId?: string -): Promise { - -await getSession(); if (!adminUser) return null; - - if (adminUser.role === "platform_admin") { - // platform_admin can operate on all brands — pass null (= all brands) to RPC - return null; - } - - // For non-platform-admin: resolve the active brand (validates against brand_ids) - const activeBrandId = await getActiveBrandId(adminUser, requestedBrandId); - - if (requestedBrandId && activeBrandId !== requestedBrandId) { - // Brand admin trying to operate on another brand's data — block it - return null; // caller should check and return unauthorized - } - - return activeBrandId; -} - -/** - * Like resolveBrandId but returns null for platform_admin AND throws an error - * if a brand_admin tries to operate outside their brand. - * Use for mutating actions (save, delete, fulfill) where cross-brand access must be blocked. - */ -export async function enforceBrandScope( - adminUser: Awaited>, - requestedBrandId?: string -): Promise<{ brandId: string | null; error?: string }> { - -await getSession(); if (!adminUser) return { brandId: null, error: "Not authenticated" }; - - if (adminUser.role === "platform_admin") { - return { brandId: null }; // unrestricted - } - - // For non-platform-admin: resolve the active brand (validates against brand_ids) - const activeBrandId = await getActiveBrandId(adminUser, requestedBrandId); - - if (requestedBrandId && activeBrandId !== requestedBrandId) { - return { brandId: null, error: "Not authorized to operate on this brand" }; - } - - return { brandId: activeBrandId }; -} \ No newline at end of file diff --git a/src/actions/wholesale/settings.ts b/src/actions/wholesale/settings.ts deleted file mode 100644 index ddc07f1..0000000 --- a/src/actions/wholesale/settings.ts +++ /dev/null @@ -1,91 +0,0 @@ -"use server"; - -import { pool } from "@/lib/db"; -import { getAdminUser } from "@/lib/admin-permissions"; -import { getActiveBrandId } from "@/lib/brand-scope"; -import type { NotificationRecipient, WholesaleSettings } from "./types"; -import { getSession } from "@/lib/auth"; - -export async function getWholesaleSettings(brandId?: string): Promise { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return null; - const bid = await getActiveBrandId(adminUser, brandId); - if (!bid && adminUser.role !== "platform_admin") { - return null; - } - - try { - const { rows } = await pool.query( - "SELECT * FROM get_wholesale_settings($1)", - [bid] - ); - return rows[0] ?? null; - } catch { - return null; - } -} - -export async function getWholesaleSettingsPublic(brandId: string): Promise<{ invoice_business_address: string | null } | null> { - -await getSession(); try { - const { rows } = await pool.query<{ invoice_business_address: string | null }>( - "SELECT invoice_business_address FROM get_wholesale_settings($1)", - [brandId] - ); - return rows[0] ?? null; - } catch { - return null; - } -} - -export async function saveWholesaleSettings(params: { - brandId: string; - requireApproval?: boolean; - minOrderAmount?: number; - onlinePaymentEnabled?: boolean; - wholesaleEnabled?: boolean; - squareSyncEnabled?: boolean; - pickupLocation?: string; - fobLocation?: string; - fromEmail?: string; - invoiceBusinessName?: string; - invoiceBusinessAddress?: string; - invoiceBusinessPhone?: string; - invoiceBusinessEmail?: string; - invoiceBusinessWebsite?: string; - notificationEmail?: string; - notificationRecipients?: NotificationRecipient[]; -}): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - try { - await pool.query( - "SELECT upsert_wholesale_settings($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)", - [ - params.brandId, - params.requireApproval ?? null, - params.minOrderAmount ?? null, - params.onlinePaymentEnabled ?? null, - params.wholesaleEnabled ?? null, - params.pickupLocation ?? null, - params.fobLocation ?? null, - params.fromEmail ?? null, - params.invoiceBusinessName ?? null, - params.invoiceBusinessAddress ?? null, - params.invoiceBusinessPhone ?? null, - params.invoiceBusinessEmail ?? null, - params.invoiceBusinessWebsite ?? null, - params.notificationEmail ?? null, - params.notificationRecipients ? JSON.stringify(params.notificationRecipients) : null, - params.squareSyncEnabled ?? null, - ] - ); - return { success: true }; - } catch { - return { success: false, error: "Failed to save settings" }; - } -} \ No newline at end of file diff --git a/src/actions/wholesale/types.ts b/src/actions/wholesale/types.ts deleted file mode 100644 index 04d7884..0000000 --- a/src/actions/wholesale/types.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Shared types for wholesale actions. - * - * Pure type definitions — no runtime imports, no "use server" so it can - * be imported into both server and client code. - */ - -export type WholesaleOrder = { - id: string; - customer_id: string; - status: string; - fulfillment_status: string; - payment_status: string; - anticipated_pickup_date: string | null; - subtotal: number; - deposit_required: number; - deposit_paid: number; - balance_due: number; - created_at: string; - updated_at: string; - invoice_number: string | null; - assigned_employee_id: string | null; - company_name: string; - contact_name: string | null; - customer_email: string; - customer_phone: string | null; - items: Array<{ - id: string; - product_name: string; - quantity: number; - unit_price: number; - line_total: number; - }>; - fulfilled_at: string | null; -}; - -export type WholesaleCustomer = { - id: string; - user_id: string | null; - company_name: string; - contact_name: string | null; - email: string; - phone: string | null; - account_status: string; - credit_limit: number; - deposits_enabled: boolean; - deposit_threshold: number | null; - deposit_percentage: number | null; - order_email: string | null; - invoice_email: string | null; - admin_notes: string | null; - role: string; - created_at: string; - deleted_at: string | null; -}; - -export type WholesaleProduct = { - id: string; - name: string; - description: string | null; - unit_type: string; - unit_type_custom: string | null; - availability: string; - qty_available: number; - season_start: string | null; - season_end: string | null; - price_tiers: Array<{ min_qty: number; max_qty: number; price: number }>; - hp_sku: string | null; - hp_item_id: string | null; - handling_instructions: string | null; - storage_warning: string | null; - loading_notes: string | null; - product_label: string | null; - pack_style: string | null; - container_type: string | null; - container_size_code: string | null; - units_per_container: number | null; - default_pickup_location: string | null; - created_at: string; - deleted_at: string | null; -}; - -export type NotificationRecipient = { - email: string; - name?: string; - active: boolean; - notification_types?: ( - | "order_confirmation" - | "deposit_received" - | "order_fulfilled" - | "price_sheet" - | "unclaimed_pickup" - )[]; -}; - -export type WholesaleSettings = { - id: string; - brand_id: string; - portal_page_id: string | null; - price_sheet_page_id: string | null; - require_approval: boolean; - min_order_amount: number | null; - online_payment_enabled: boolean; - wholesale_enabled: boolean; - square_sync_enabled: boolean; - pickup_location: string; - fob_location: string; - from_email: string; - invoice_business_name: string; - invoice_business_address: string | null; - invoice_business_phone: string | null; - invoice_business_email: string | null; - invoice_business_website: string | null; - notification_email: string | null; - notification_recipients: NotificationRecipient[]; - last_invoice_number: number; -}; - -export type WholesaleDashboardStats = { - open_orders: number; - pickup_today: number; - past_due: number; - total_unpaid: number; - awaiting_deposit: number; - fulfilled_today: number; -}; - -export type WholesaleNotification = { - id: string; - type: "order_confirmation" | "deposit_received" | "order_fulfilled"; - email_to: string; - email_cc: string | null; - subject: string; - body_html: string | null; - body_text: string | null; - brand_id: string; - customer_id: string; - order_id: string | null; - status: string; - invoice_business_name: string | null; - invoice_business_email: string | null; - created_at: string; -}; - -export type WebhookSettings = { - id: string; - brand_id: string; - url: string; - secret: string; - enabled: boolean; - created_at: string; - updated_at: string; -}; \ No newline at end of file diff --git a/src/actions/wholesale/webhooks.ts b/src/actions/wholesale/webhooks.ts deleted file mode 100644 index aa66b74..0000000 --- a/src/actions/wholesale/webhooks.ts +++ /dev/null @@ -1,96 +0,0 @@ -"use server"; - -import { pool } from "@/lib/db"; -import { getAdminUser } from "@/lib/admin-permissions"; -import type { WebhookSettings } from "./types"; -import { getSession } from "@/lib/auth"; - -export async function getWebhookSettings(brandId: string): Promise { - -await getSession(); try { - const { rows } = await pool.query( - "SELECT * FROM wholesale_webhook_settings WHERE brand_id = $1 LIMIT 1", - [brandId] - ); - return rows[0] ?? null; - } catch { - return null; - } -} - -export async function saveWebhookSettings(params: { - brandId: string; - url?: string; - secret?: string; - enabled?: boolean; -}): Promise<{ success: boolean; error?: string }> { - -await getSession(); const adminUser = await getAdminUser(); - if (!adminUser) return { success: false, error: "Not authenticated" }; - if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; - - try { - await pool.query( - "SELECT upsert_wholesale_webhook_settings($1::jsonb)", - [ - JSON.stringify({ - brand_id: params.brandId, - url: params.url ?? "", - secret: params.secret ?? "", - enabled: params.enabled ?? false, - }), - ] - ); - return { success: true }; - } catch { - return { success: false, error: "Failed to save webhook settings" }; - } -} - -export async function enqueueWholesaleWebhook( - eventType: "order_created" | "order_fulfilled" | "deposit_recorded" | "order_paid", - orderId: string | null = null, - payload: Record | null = null, - brandId?: string -): Promise<{ success: boolean; logId?: string }> { - -await getSession(); try { - const { rows } = await pool.query<{ id: string }>( - "SELECT enqueue_wholesale_webhook($1, $2, $3::jsonb, $4)", - [eventType, orderId, payload ? JSON.stringify(payload) : null, brandId ?? null] - ); - const data = Array.isArray(rows) ? rows[0] : rows; - return { success: true, logId: data?.id }; - } catch { - return { success: false }; - } -} - -export async function getRecentWebhookActivity(brandId: string, limit = 10): Promise> { - -await getSession(); try { - const { rows } = await pool.query<{ - id: string; - event_type: string; - order_id: string | null; - status: string; - attempts: number; - created_at: string; - response: string | null; - }>( - "SELECT * FROM wholesale_sync_log WHERE brand_id = $1 ORDER BY created_at DESC LIMIT $2", - [brandId, limit] - ); - return rows; - } catch { - return []; - } -} \ No newline at end of file diff --git a/src/components/admin/AddLocationModal.tsx b/src/components/admin/AddLocationModal.tsx deleted file mode 100644 index 025d876..0000000 --- a/src/components/admin/AddLocationModal.tsx +++ /dev/null @@ -1,288 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { createLocation } from "@/actions/locations"; -import GlassModal from "@/components/admin/GlassModal"; - -const inputStyle = { - background: "rgba(0, 0, 0, 0.02)", - border: "1px solid rgba(0, 0, 0, 0.06)", - outline: "none", -}; - -function handleFocus(e: React.FocusEvent) { - e.target.style.background = "rgba(16, 185, 129, 0.04)"; - e.target.style.border = "1px solid rgba(16, 185, 129, 0.5)"; - e.target.style.boxShadow = "0 0 0 4px rgba(16, 185, 129, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04)"; -} -function handleBlur(e: React.FocusEvent) { - e.target.style.background = "rgba(0, 0, 0, 0.02)"; - e.target.style.border = "1px solid rgba(0, 0, 0, 0.06)"; - e.target.style.boxShadow = "none"; -} - -type Props = { - isOpen: boolean; - onClose: () => void; - brandId: string; - onSuccess?: (locationId: string) => void; -}; - -export default function AddLocationModal({ isOpen, onClose, brandId, onSuccess }: Props) { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const [name, setName] = useState(""); - const [address, setAddress] = useState(""); - const [city, setCity] = useState(""); - const [stateVal, setStateVal] = useState(""); - const [zip, setZip] = useState(""); - const [phone, setPhone] = useState(""); - const [contactName, setContactName] = useState(""); - const [contactEmail, setContactEmail] = useState(""); - const [notes, setNotes] = useState(""); - - const reset = useCallback(() => { - setName(""); - setAddress(""); - setCity(""); - setStateVal(""); - setZip(""); - setPhone(""); - setContactName(""); - setContactEmail(""); - setNotes(""); - setError(null); - }, []); - - const handleClose = useCallback(() => { - if (loading) return; - reset(); - onClose(); - }, [loading, reset, onClose]); - - const handleSubmit = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - if (!name.trim()) { - setError("Venue name is required."); - return; - } - setLoading(true); - try { - const result = await createLocation(brandId, { - name: name.trim(), - address: address.trim() || null, - city: city.trim() || null, - state: stateVal.trim().toUpperCase() || null, - zip: zip.trim() || null, - phone: phone.trim() || null, - contact_name: contactName.trim() || null, - contact_email: contactEmail.trim() || null, - notes: notes.trim() || null, - active: true, - }); - if (result.success) { - onSuccess?.(result.id); - reset(); - onClose(); - } else { - setError(result.error ?? "Failed to create location"); - } - } catch { - setError("Network error. Please try again."); - } finally { - setLoading(false); - } - }, - [brandId, name, address, city, stateVal, zip, phone, contactName, contactEmail, notes, onSuccess, reset, onClose] - ); - - if (!isOpen) return null; - - return ( - -
- {error && ( -
- {error} -
- )} - -
- - setName(e.target.value)} - placeholder="Tractor Supply" - className="w-full rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 transition-all" - style={inputStyle} - onFocus={handleFocus} - onBlur={handleBlur} - required - /> -
- -
- - setAddress(e.target.value)} - placeholder="13778 E I-25 Frontage Rd" - className="w-full rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 transition-all" - style={inputStyle} - onFocus={handleFocus} - onBlur={handleBlur} - /> -
- -
-
- - setCity(e.target.value)} - placeholder="Wellington" - className="w-full rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 transition-all" - style={inputStyle} - onFocus={handleFocus} - onBlur={handleBlur} - /> -
-
- - setStateVal(e.target.value.toUpperCase())} - placeholder="CO" - maxLength={2} - className="w-full rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 transition-all" - style={inputStyle} - onFocus={handleFocus} - onBlur={handleBlur} - /> -
-
- - setZip(e.target.value)} - placeholder="80549" - className="w-full rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 transition-all" - style={inputStyle} - onFocus={handleFocus} - onBlur={handleBlur} - /> -
-
- -
-
- - setPhone(e.target.value)} - placeholder="(970) 555-1234" - className="w-full rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 transition-all" - style={inputStyle} - onFocus={handleFocus} - onBlur={handleBlur} - /> -
-
- - setContactName(e.target.value)} - placeholder="Store manager" - className="w-full rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 transition-all" - style={inputStyle} - onFocus={handleFocus} - onBlur={handleBlur} - /> -
-
- -
- - setContactEmail(e.target.value)} - placeholder="manager@example.com" - className="w-full rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 transition-all" - style={inputStyle} - onFocus={handleFocus} - onBlur={handleBlur} - /> -
- -
- -