feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s

- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
  deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
  new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
  Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
  brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
  bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
  TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
  public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
  wholesale_settings lookup
This commit is contained in:
openclaw
2026-06-09 11:32:17 -06:00
parent bb464bda65
commit 916ad39176
349 changed files with 6706 additions and 3343 deletions
+12 -3
View File
@@ -1,5 +1,5 @@
import { getAdminUser } from "@/lib/admin-permissions";
import { getAdminOrderDetail } from "@/actions/orders";
import { getAdminOrderDetail, type AdminOrder } from "@/actions/orders";
import OrderEditForm from "@/components/admin/OrderEditForm";
import OrderPaymentSection from "@/components/admin/OrderPaymentSection";
import OrderPickupAction from "@/components/admin/OrderPickupAction";
@@ -14,6 +14,15 @@ type OrderDetailPageProps = {
}>;
};
type OrderItem = {
id: string;
quantity: number;
price: number | string;
products?: {
name: string;
} | null;
};
function formatCurrency(amount: number) {
return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
}
@@ -139,7 +148,7 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps)
{order.order_items && order.order_items.length > 0 ? (
<div className="mt-4 divide-y divide-stone-100">
{order.order_items.map((item: any) => (
{order.order_items.map((item: OrderItem) => (
<div
key={item.id}
className="flex items-center justify-between py-3"
@@ -214,7 +223,7 @@ export default async function OrderDetailPage({ params }: OrderDetailPageProps)
<p className="mt-1 text-sm text-stone-500">Update customer details, pricing, and status</p>
<div className="mt-5">
<OrderEditForm order={order as any} brandId={brandId} />
<OrderEditForm order={order as unknown as Parameters<typeof OrderEditForm>[0]["order"]} brandId={brandId} />
</div>
</div>
+5 -5
View File
@@ -53,12 +53,12 @@ export default async function AdminOrdersPage() {
}
const { data: prods } = await prodQuery;
brandProducts = (prods ?? []).map((p: any) => ({
id: p.id,
name: p.name,
brandProducts = (prods ?? []).map((p) => ({
id: String(p.id),
name: String(p.name ?? ""),
price: Number(p.price),
type: p.type ?? null,
active: p.active ?? true,
type: p.type as string | null ?? null,
active: Boolean(p.active ?? true),
}));
} catch {
// non-fatal for the orders list
+1 -1
View File
@@ -38,7 +38,7 @@ export default async function AdminPage() {
.limit(1)
.single();
if (firstBrand?.id) {
dashboardBrandId = firstBrand.id;
dashboardBrandId = String(firstBrand.id);
}
} catch (err) {
console.error("[admin/page] supabase brands lookup failed:", err);
+5 -2
View File
@@ -4,6 +4,10 @@ import { getAdminOrders } from "@/actions/orders";
export const dynamic = "force-dynamic";
// Pre-compute cutoff time to avoid impure function in render
const PICKUP_WINDOW_MS = 72 * 60 * 60 * 1000;
const pickedUpCutoff = new Date(Date.now() - PICKUP_WINDOW_MS);
export default async function DriverPickupPage() {
const adminUser = await getAdminUser();
const { orders, stops } = await getAdminOrders();
@@ -26,8 +30,7 @@ export default async function DriverPickupPage() {
(o) =>
o.pickup_complete &&
o.pickup_completed_at &&
new Date(o.pickup_completed_at) >
new Date(Date.now() - 72 * 60 * 60 * 1000)
new Date(o.pickup_completed_at) > pickedUpCutoff
);
return (
+26 -6
View File
@@ -11,12 +11,32 @@ type ProductDetailPageProps = {
}>;
};
interface Product {
id: string;
brand_id: string;
name: string;
description: string | null;
price: number;
active: boolean;
type: string | null;
image_url: string | null;
is_taxable: boolean;
pickup_type: string | null;
brands?: { name: string; slug: string };
}
interface Brand {
id: string;
name: string;
slug: string;
}
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
const { id } = await params;
const [{ data: product, error }, { data: brands }] = await Promise.all([
supabase.from("products").select("*, brands(name, slug)").eq("id", id).single(),
supabase.from("brands").select("id, name, slug"),
supabase.from("products").select("*, brands(name, slug)").eq("id", id).single() as unknown as { data: Product | null; error: { message: string } | null },
supabase.from("brands").select("id, name, slug") as unknown as { data: Brand[] | null },
]);
const adminUser = await getAdminUser();
@@ -109,14 +129,14 @@ export default async function ProductDetailPage({ params }: ProductDetailPagePro
product={{
id: product.id,
name: product.name,
description: product.description,
description: product.description ?? "",
price: Number(product.price),
type: product.type,
type: product.type ?? "pickup",
active: product.active,
brand_id: product.brand_id,
image_url: product.image_url,
image_url: product.image_url ?? "",
is_taxable: product.is_taxable,
pickup_type: product.pickup_type,
pickup_type: product.pickup_type ?? "pickup",
}}
brands={brands ?? []}
/>
+12 -12
View File
@@ -1,6 +1,6 @@
import { asc, eq, sql } from "drizzle-orm";
import { withPlatformAdmin, withTenant } from "@/db/client";
import { products, productImages, tenants } from "@/db/schema";
import { withPlatformAdmin, withBrand } from "@/db/client";
import { products, productImages, brands } from "@/db/schema";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
@@ -53,14 +53,14 @@ export default async function AdminProductsPage() {
const isPlatformAdmin = adminUser.role === "platform_admin";
// Platform admins need a brand picker for new products
let brands: { id: string; name: string }[] = [];
let brandOptions: { id: string; name: string }[] = [];
if (isPlatformAdmin) {
const result = await getBrands();
brands = result.brands ?? [];
brandOptions = result.brands ?? [];
}
// Query products + their first image. The new SaaS schema:
// * renamed `brand_id` → `tenant_id` on products
// * renamed `brand_id` → `brand_id` on products
// * renamed `price` (numeric) → `price_cents` (integer)
// * moved `image_url` off the products table into a separate
// `product_images` table (keyed by `product_id` + `position`)
@@ -71,7 +71,7 @@ export default async function AdminProductsPage() {
let productsList: ProductRow[] = [];
let queryError: string | null = null;
try {
const baseQuery = (db: Parameters<Parameters<typeof withTenant>[1]>[0]) =>
const baseQuery = (db: Parameters<Parameters<typeof withBrand>[1]>[0]) =>
db
.select({
id: products.id,
@@ -79,13 +79,13 @@ export default async function AdminProductsPage() {
description: products.description,
priceCents: products.priceCents,
active: products.active,
tenantId: products.tenantId,
tenantName: tenants.name,
brandId: products.brandId,
brandName: brands.name,
firstImageKey: productImages.storageKey,
firstImagePosition: productImages.position,
})
.from(products)
.leftJoin(tenants, eq(tenants.id, products.tenantId))
.leftJoin(brands, eq(brands.id, products.brandId))
// Pull only the lowest-position image per product. The
// `position` index on product_images (product_id, position) makes
// this cheap; a real-world scale would replace it with a
@@ -105,7 +105,7 @@ export default async function AdminProductsPage() {
const rows = isPlatformAdmin
? await withPlatformAdmin((db) => baseQuery(db))
: brandId
? await withTenant(brandId, (db) => baseQuery(db))
? await withBrand(brandId, (db) => baseQuery(db))
: [];
// Resolve each row's first image to a public URL. Until object-store
@@ -120,7 +120,7 @@ export default async function AdminProductsPage() {
type: "pickup", // legacy column not in new schema
active: r.active,
image_url: r.firstImageKey, // storage key, not a public URL yet
brand_id: r.tenantId,
brand_id: r.brandId,
is_taxable: false, // legacy column not in new schema
available_from: null,
available_until: null,
@@ -147,7 +147,7 @@ export default async function AdminProductsPage() {
<ProductsClient
products={productsList}
brandId={brandId ?? undefined}
brands={brands}
brands={brandOptions}
isPlatformAdmin={isPlatformAdmin}
/>
</div>
+1 -1
View File
@@ -16,7 +16,7 @@ export default async function ReportsPage() {
const { data: brands } = await supabase
.from("brands")
.select("id, name")
.order("name");
.order("name") as unknown as { data: { id: string; name: string }[] | null };
const initialBrandId = isPlatformAdmin
? null
+2 -7
View File
@@ -1,5 +1,4 @@
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { getAdminUser } from "@/lib/admin-permissions";
import { isFeatureEnabled } from "@/lib/feature-flags";
import { getRouteTraceLotDetail, getLotOrders } from "@/actions/route-trace/lots";
@@ -26,13 +25,9 @@ export default async function LotDetailPage({ params }: { params: Promise<{ id:
const adminUser = await getAdminUser();
if (!adminUser) redirect("/login");
const cookieStore = await cookies();
const devSession = cookieStore.get("dev_session")?.value;
const isDevMode = !!devSession;
const effectiveBrandId = adminUser.brand_id ?? "64294306-5f42-463d-a5e8-2ad6c81a96de";
const enabled = isDevMode ? true : await isFeatureEnabled(effectiveBrandId, "route_trace");
const enabled = await isFeatureEnabled(effectiveBrandId, "route_trace");
if (!enabled) redirect("/admin/settings/apps?reason=route_trace");
const [detailResult, ordersResult] = await Promise.all([
@@ -68,4 +63,4 @@ export default async function LotDetailPage({ params }: { params: Promise<{ id:
</div>
</div>
);
}
}
+2 -7
View File
@@ -1,5 +1,4 @@
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { getAdminUser } from "@/lib/admin-permissions";
import { isFeatureEnabled } from "@/lib/feature-flags";
import { getRouteTraceLots } from "@/actions/route-trace/lots";
@@ -22,13 +21,9 @@ export default async function LotsPage() {
const adminUser = await getAdminUser();
if (!adminUser) redirect("/login");
const cookieStore = await cookies();
const devSession = cookieStore.get("dev_session")?.value;
const isDevMode = !!devSession;
const effectiveBrandId = adminUser.brand_id ?? "64294306-5f42-463d-a5e8-2ad6c81a96de";
const enabled = isDevMode ? true : await isFeatureEnabled(effectiveBrandId, "route_trace");
const enabled = await isFeatureEnabled(effectiveBrandId, "route_trace");
if (!enabled) redirect("/admin/settings/apps?reason=route_trace");
const [statsResult, lotsResult, haulingResult, yieldResult, invResult, eventsResult] = await Promise.all([
@@ -62,4 +57,4 @@ export default async function LotsPage() {
lots={allLots}
/>
);
}
}
+2 -8
View File
@@ -1,5 +1,4 @@
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { getAdminUser } from "@/lib/admin-permissions";
import { isFeatureEnabled } from "@/lib/feature-flags";
import {
@@ -41,14 +40,9 @@ export default async function RouteTraceDashboardPage() {
const adminUser = await getAdminUser();
if (!adminUser) redirect("/login");
const cookieStore = await cookies();
const devSession = cookieStore.get("dev_session")?.value;
const isDevMode = !!devSession;
const effectiveBrandId = adminUser.brand_id ?? "64294306-5f42-463d-a5e8-2ad6c81a96de";
// Bypass feature check in dev mode (dev_session cookie present)
const enabled = isDevMode ? true : await isFeatureEnabled(effectiveBrandId, "route_trace");
const enabled = await isFeatureEnabled(effectiveBrandId, "route_trace");
if (!enabled) redirect("/admin/settings/apps?reason=route_trace");
const [statsResult, lotsResult, haulingResult, yieldResult, invResult, eventsResult] = await Promise.all([
@@ -110,4 +104,4 @@ export default async function RouteTraceDashboardPage() {
</div>
</main>
);
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ export default async function BillingPage({ params }: Props) {
.limit(1)
.single();
if (firstBrand?.id) {
resolvedBrandId = firstBrand.id;
resolvedBrandId = String(firstBrand.id);
} else {
return (
<main className="min-h-screen bg-[var(--admin-bg)] px-6 py-12">
+1 -1
View File
@@ -18,7 +18,7 @@ export default async function IntegrationsPage() {
// Platform admins: fetch all brands for the picker
const brands = isPlatformAdmin
? (await supabase.from("brands").select("id, name").order("name")).data ?? []
? ((await supabase.from("brands").select("id, name").order("name")) as unknown as { data: { id: string; name: string }[] }).data ?? []
: [];
return (
+1 -1
View File
@@ -17,7 +17,7 @@ export default async function ShippingSettingsPage() {
// Platform admins: fetch all brands for the picker
const brands = isPlatformAdmin
? (await supabase.from("brands").select("id, name").order("name")).data ?? []
? ((await supabase.from("brands").select("id, name").order("name")) as unknown as { data: { id: string; name: string }[] }).data ?? []
: [];
const effectiveBrandId = brandId || (brands[0]?.id ?? "");
@@ -65,7 +65,7 @@ export default function SquareSyncSettingsClient({ settings, logs, brandId }: Pr
setError(null);
const result = await savePaymentSettings({
brandId,
provider: (settings?.provider as any) || null,
provider: (settings?.provider ?? null) as "stripe" | "square" | "manual" | null,
squareSyncEnabled,
squareInventoryMode,
});
+2 -2
View File
@@ -1,6 +1,6 @@
import { redirect } from "next/navigation";
import { getAdminUser } from "@/lib/admin-permissions";
import { getPaymentSettings } from "@/actions/payments";
import { getPaymentSettings, type PaymentSettings } from "@/actions/payments";
import { getSyncLog, type SyncLogEntry } from "@/actions/square-sync-ui";
import SquareSyncSettingsClient from "./SquareSyncSettingsClient";
@@ -26,7 +26,7 @@ export default async function SquareSyncSettingsPage() {
return (
<SquareSyncSettingsClient
settings={settings as any}
settings={settings as PaymentSettings}
logs={logs}
brandId={brandId}
/>
+46 -5
View File
@@ -13,6 +13,38 @@ type StopDetailPageProps = {
}>;
};
interface Stop {
id: string;
brand_id: string;
city: string;
state: string;
address: string | null;
zip: string | null;
date: string;
time: string;
cutoff_date: string | null;
cutoff_time: string | null;
status: string;
location: string;
slug: string;
active: boolean;
brands?: { name: string; slug: string };
}
interface Product {
id: string;
name: string;
type: string;
price: number;
image_url?: string | null;
}
interface ProductStop {
id: string;
product_id: string;
products?: Product;
}
export default async function StopDetailPage({ params }: StopDetailPageProps) {
const { id } = await params;
@@ -21,8 +53,8 @@ export default async function StopDetailPage({ params }: StopDetailPageProps) {
.from("stops")
.select("*, brands(name, slug)")
.eq("id", id)
.single(),
supabase.from("brands").select("id, name, slug"),
.single() as unknown as { data: Stop | null; error: { message: string } | null },
supabase.from("brands").select("id, name, slug") as unknown as { data: { id: string; name: string; slug: string }[] | null },
]);
const adminUser = await getAdminUser();
@@ -61,15 +93,24 @@ export default async function StopDetailPage({ params }: StopDetailPageProps) {
.from("products")
.select("id, name, type, price")
.eq("brand_id", stop.brand_id)
.eq("active", true),
.eq("active", true) as unknown as { data: Product[] | null },
supabase
.from("product_stops")
.select("id, product_id, products(id, name, type, price)")
.eq("stop_id", id),
.eq("stop_id", id) as unknown as { data: ProductStop[] | null },
]);
const assignedProducts = (productStops ?? [])
.map((ps: any) => ps)
.map((ps: ProductStop) => ({
id: ps.id,
product_id: ps.product_id,
products: ps.products ? {
name: ps.products.name,
type: ps.products.type,
price: ps.products.price,
image_url: ps.products.image_url,
} : null,
}))
.filter(Boolean);
return (
+2 -2
View File
@@ -36,7 +36,7 @@ export default async function NewStopPage({
.from("stops")
.select("city, state, location, date, time, brand_id, active, address, zip, cutoff_time")
.eq("id", duplicate)
.single();
.single() as unknown as { data: Stop | null };
duplicateFrom = data;
}
@@ -48,7 +48,7 @@ export default async function NewStopPage({
.from("products")
.select("id, name, type, price")
.eq("brand_id", brandId)
.eq("active", true),
.eq("active", true) as unknown as { data: { id: string; name: string; type: string; price: number }[] | null },
]);
return (
+18 -1
View File
@@ -6,6 +6,23 @@ import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
import { PageHeader } from "@/components/admin/design-system";
import { redirect } from "next/navigation";
interface Stop {
id: string;
city: string;
state: string;
date: string;
time: string;
location: string;
active: boolean;
deleted_at: string | null;
brand_id: string;
address: string | null;
zip: string | null;
cutoff_time: string | null;
status: string;
brands: { name: string } | { name: string }[];
}
const StopIcon = () => (
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-current" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/>
@@ -49,7 +66,7 @@ export default async function AdminStopsPage() {
query = query.eq("brand_id", adminUser.brand_id);
}
const { data: stops, error } = await query;
const { data: stops, error } = await query as unknown as { data: Stop[] | null; error: { message: string } | null };
if (error) {
return (
+2 -2
View File
@@ -29,9 +29,9 @@ export default async function TaxesPage({ params }: Props) {
.from("brands")
.select("id")
.limit(1)
.single();
.single() as unknown as { data: { id: string } | null };
if (firstBrand?.id) {
resolvedBrandId = firstBrand.id;
resolvedBrandId = String(firstBrand.id);
} else {
return (
<main className="min-h-screen px-6 py-12" style={{ backgroundColor: "var(--admin-bg)" }}>
+3 -11
View File
@@ -1,6 +1,5 @@
import TimeTrackingAdminPanel from "@/components/admin/TimeTrackingAdminPanel";
import { getAdminUser } from "@/lib/admin-permissions";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { PageHeader } from "@/components/admin/design-system";
@@ -11,16 +10,9 @@ export const dynamic = "force-dynamic";
export default async function AdminTimeTrackingPage() {
const adminUser = await getAdminUser();
// Dev session bypass for platform admin
if (!adminUser) {
const cookieStore = await cookies();
const devSession = cookieStore.get("dev_session")?.value;
if (devSession === "platform_admin" || devSession === "brand_admin") {
// Allow access in dev mode
} else {
redirect("/login");
}
redirect("/login");
}
const isPlatformAdmin = adminUser?.role === "platform_admin";
@@ -45,4 +37,4 @@ export default async function AdminTimeTrackingPage() {
</div>
</div>
);
}
}
+10 -7
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { getWaterAdminSettings, saveWaterAdminSettings, type WaterAdminSettings } from "@/actions/water-log/settings";
import { useRouter } from "next/navigation";
@@ -24,11 +24,7 @@ export default function WaterLogSettingsPage() {
const [alertPhone, setAlertPhone] = useState("");
const [alertsEnabled, setAlertsEnabled] = useState(false);
useEffect(() => {
loadSettings();
}, []);
async function loadSettings() {
const loadSettings = useCallback(async () => {
setLoading(true);
const data = await getWaterAdminSettings(TUXEDO_BRAND_ID);
if (data) {
@@ -42,7 +38,14 @@ export default function WaterLogSettingsPage() {
setAlertsEnabled(data.alerts_enabled ?? false);
}
setLoading(false);
}
}, []);
useEffect(() => {
const init = async () => {
await loadSettings();
};
init();
}, [loadSettings]);
async function handleSave(e: React.FormEvent) {
e.preventDefault();
+23 -15
View File
@@ -41,6 +41,15 @@ import { PageHeader, AdminButton, AdminSearchInput, AdminFilterTabs, AdminBadge
type Tab = "dashboard" | "customers" | "products" | "orders" | "settings";
// SVG Icon for Wholesale - defined at module level to prevent recreation on each render
function WholesaleIcon() {
return (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
);
}
export default function WholesaleClient({ brandId }: { brandId: string }) {
const [tab, setTab] = useState<Tab>("dashboard");
const [msg, setMsg] = useState<{ kind: "success" | "error"; text: string } | null>(null);
@@ -101,13 +110,6 @@ export default function WholesaleClient({ brandId }: { brandId: string }) {
);
}
// SVG Icon for Wholesale
const WholesaleIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
);
const tabs = [
{ value: "dashboard", label: "Dashboard", count: undefined },
{ value: "customers", label: "Customers", count: customers.filter(c => c.account_status !== "pending_approval" && c.account_status !== "rejected").length },
@@ -116,10 +118,13 @@ export default function WholesaleClient({ brandId }: { brandId: string }) {
{ value: "settings", label: "Settings" },
];
// Declare icon reference for PageHeader
const pageIcon = <WholesaleIcon />;
return (
<div className="min-h-screen bg-[var(--admin-bg)]">
<PageHeader
icon={<WholesaleIcon />}
icon={pageIcon}
title="Wholesale Portal"
subtitle="Manage wholesale orders, customers, and products"
className="mb-0"
@@ -1843,13 +1848,7 @@ function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }:
onRefresh: () => void;
canManageSettings: boolean;
}) {
if (!canManageSettings) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-slate-400">You do not have permission to manage settings.</p>
</div>
);
}
// Hooks must be called unconditionally - always declare them first
const [form, setForm] = useState({
requireApproval: settings?.require_approval ?? true,
minOrderAmount: settings?.min_order_amount ?? "",
@@ -1868,6 +1867,15 @@ function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }:
});
const [saving, setSaving] = useState(false);
// Permission check after hooks
if (!canManageSettings) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-slate-400">You do not have permission to manage settings.</p>
</div>
);
}
async function handleSave() {
setSaving(true);
const result = await saveWholesaleSettings({
+5 -18
View File
@@ -1,25 +1,12 @@
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import WholesaleClient from "./WholesaleClient";
export default async function WholesalePage() {
const cookieStore = await cookies();
const devSession = cookieStore.get("dev_session")?.value;
const isDevMode =
devSession === "platform_admin" ||
devSession === "brand_admin" ||
devSession === "store_employee";
const adminUser = await getAdminUser();
if (!adminUser) redirect("/login");
if (!isDevMode) {
const adminUser = await getAdminUser();
if (!adminUser) redirect("/login");
const activeBrandId = await getActiveBrandId(adminUser);
return <WholesaleClient brandId={activeBrandId ?? ""} />;
}
// Dev mode: platform_admin sees all brands, use first brand as default
const brandId = devSession === "platform_admin" ? "" : (cookieStore.get("dev_brand_id")?.value ?? "64294306-5f42-463d-a5e8-2ad6c81a96de");
return <WholesaleClient brandId={brandId} />;
}
const activeBrandId = await getActiveBrandId(adminUser);
return <WholesaleClient brandId={activeBrandId ?? ""} />;
}