refactor(storefront): remove supabase shim and restore customer storefronts
The legacy src/lib/supabase.ts shim returned { data: null } for every
query, so 15+ pages were silently rendering empty state — including the
two customer-facing storefronts (tuxedo, indian-river-direct) and
several v1 admin pages. Replace every caller with canonical access:
- New src/actions/storefront.ts server-action module: brand lookup,
public stops, active products, stop-by-slug, wholesale settings,
portal config. Uses the shared pg Pool and SECURITY DEFINER RPCs.
- src/actions/brand-settings.ts: getBrandSettingsPublic inlined the
brands + brand_settings LEFT JOIN wholesale_settings query (the
RPC it called did not exist; try/catch was masking the failure).
- src/actions/ai/preferences.ts: switched get/saveAIPreferences to
pool.query.
- src/actions/square-sync-ui.ts: new getSquareQueueCount action for
SquareSyncWidget (replaces shim count).
- src/app/api/{tuxedo,indian-river-direct}/schedule-pdf/route.ts:
use pool.query.
- next.config.ts: redirects /admin/{products/:id,reports,taxes,
settings/{shipping,integrations,billing}} → their v2 / settings
equivalents, then deleted those pages.
- Deleted src/lib/supabase.ts (no remaining imports).
Tests: 174/175 (unchanged from baseline; pre-existing getAdminUser
mock issue is tracked in Step 5).
This commit is contained in:
@@ -1,103 +0,0 @@
|
||||
import AdminOrdersPanel from "@/components/admin/AdminOrdersPanel";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { getAdminOrders } from "@/actions/orders";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminOrdersPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
|
||||
if (!adminUser.can_manage_orders) {
|
||||
redirect("/admin/pickup");
|
||||
}
|
||||
|
||||
const activeBrandId = await getActiveBrandId(adminUser);
|
||||
// Platform admin can browse all brands' orders; everyone else must have a brand
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return <AdminAccessDenied message="You don't have access to any brand." />;
|
||||
}
|
||||
|
||||
const { orders, stops } = await getAdminOrders();
|
||||
|
||||
const brandStops = activeBrandId
|
||||
? stops.filter((s) => s.brand_id === activeBrandId)
|
||||
: stops;
|
||||
|
||||
const brandOrders = activeBrandId
|
||||
? orders.filter(
|
||||
(o) =>
|
||||
o.stops && brandStops.some((s) => s.id === o.stop_id)
|
||||
)
|
||||
: orders;
|
||||
|
||||
// Fetch active products for this brand (for admin "New Order" item picker)
|
||||
let brandProducts: Array<{ id: string; name: string; price: number; type?: string | null; active?: boolean }> = [];
|
||||
try {
|
||||
let prodQuery = supabase
|
||||
.from("products")
|
||||
.select("id, name, price, type, active")
|
||||
.eq("active", true)
|
||||
.is("deleted_at", null)
|
||||
.order("name")
|
||||
.limit(200);
|
||||
|
||||
if (activeBrandId) {
|
||||
prodQuery = prodQuery.eq("brand_id", activeBrandId);
|
||||
}
|
||||
|
||||
const { data: prods } = await prodQuery;
|
||||
brandProducts = (prods ?? []).map((p) => ({
|
||||
id: String(p.id),
|
||||
name: String(p.name ?? ""),
|
||||
price: Number(p.price),
|
||||
type: p.type as string | null ?? null,
|
||||
active: Boolean(p.active ?? true),
|
||||
}));
|
||||
} catch {
|
||||
// non-fatal for the orders list
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
|
||||
<p className="ha-eyebrow mb-2">Operations</p>
|
||||
<PageHeader
|
||||
title="Orders"
|
||||
subtitle="View, filter, and manage customer orders."
|
||||
icon={
|
||||
<svg className="h-5 w-5 sm:h-6 sm:w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/>
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M16 10a4 4 0 0 1-8 0"/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-4 sm:px-6 md:px-8 py-4 sm:py-6">
|
||||
<div
|
||||
className="rounded-2xl border overflow-hidden"
|
||||
style={{
|
||||
borderColor: "var(--admin-border)",
|
||||
backgroundColor: "var(--admin-card-bg)",
|
||||
}}
|
||||
>
|
||||
<AdminOrdersPanel
|
||||
initialOrders={brandOrders}
|
||||
initialStops={brandStops}
|
||||
initialProducts={brandProducts}
|
||||
brandId={activeBrandId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import ProductEditForm from "@/components/admin/ProductEditForm";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { PageHeader, AdminBadge } from "@/components/admin/design-system";
|
||||
import { Package as PackageIcon } from "lucide-react";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
type ProductDetailPageProps = {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
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() 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();
|
||||
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
|
||||
if (!adminUser.can_manage_products) redirect("/admin/pickup");
|
||||
|
||||
if (adminUser?.brand_id && product?.brand_id !== adminUser.brand_id) {
|
||||
return <AdminAccessDenied />;
|
||||
}
|
||||
|
||||
if (error || !product) {
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)] px-4 sm:px-6 md:px-8 py-6 sm:py-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="text-3xl font-bold text-[var(--admin-danger)]">Product not found</h1>
|
||||
<pre className="mt-4 rounded-xl bg-[var(--admin-card-bg)] border border-[var(--admin-border)] p-4 text-sm text-[var(--admin-text-muted)]">
|
||||
{error?.message ?? "Product not found"}
|
||||
</pre>
|
||||
<Link
|
||||
href="/admin/products"
|
||||
className="mt-4 inline-block text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]"
|
||||
>
|
||||
← Back to Products
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)] px-4 sm:px-6 md:px-8 py-6 sm:py-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="ha-eyebrow mb-2">Operations</div>
|
||||
<PageHeader
|
||||
icon={<PackageIcon className="h-5 w-5" strokeWidth={1.75} />}
|
||||
title="Edit product"
|
||||
subtitle="Update product details, pricing, and availability."
|
||||
actions={
|
||||
<AdminBadge tone={product.active ? "success" : "neutral"} dot>
|
||||
{product.active ? "Active" : "Inactive"}
|
||||
</AdminBadge>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
href="/admin/products"
|
||||
className="text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] transition-colors"
|
||||
>
|
||||
← Back to Products
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl bg-[var(--admin-card-bg)] p-8 shadow-sm border border-[var(--admin-border)]">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-[var(--admin-text-muted)]">
|
||||
{product.brands?.name}
|
||||
</p>
|
||||
<h1 className="mt-2 text-3xl font-bold text-[var(--admin-text-primary)]">
|
||||
{product.name}
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-[var(--admin-text-secondary)]">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--admin-text-muted)]">Price</p>
|
||||
<p
|
||||
className="mt-1 text-2xl font-bold text-[var(--admin-text-primary)]"
|
||||
style={{ fontFamily: "var(--font-fragment-mono)", fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
${Number(product.price).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--admin-text-muted)]">Type</p>
|
||||
<p className="mt-1 text-lg font-semibold text-[var(--admin-text-primary)]">
|
||||
{product.type}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-2xl bg-[var(--admin-card-bg)] p-8 shadow-sm border border-[var(--admin-border)]">
|
||||
<h2 className="text-2xl font-bold text-[var(--admin-text-primary)]">Edit Product</h2>
|
||||
<p className="mt-1 text-[var(--admin-text-muted)]">
|
||||
Update product details, pricing, and availability.
|
||||
</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<ProductEditForm
|
||||
product={{
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
description: product.description ?? "",
|
||||
price: Number(product.price),
|
||||
type: product.type ?? "pickup",
|
||||
active: product.active,
|
||||
brand_id: product.brand_id,
|
||||
image_url: product.image_url ?? "",
|
||||
is_taxable: product.is_taxable,
|
||||
pickup_type: product.pickup_type ?? "pickup",
|
||||
}}
|
||||
brands={brands ?? []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import ReportsDashboard from "@/components/admin/ReportsDashboard";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ReportsPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
if (!adminUser.can_manage_reports) redirect("/admin/pickup");
|
||||
|
||||
const isPlatformAdmin = adminUser.role === "platform_admin";
|
||||
|
||||
const { data: brands } = await supabase
|
||||
.from("brands")
|
||||
.select("id, name")
|
||||
.order("name") as unknown as { data: { id: string; name: string }[] | null };
|
||||
|
||||
const initialBrandId = isPlatformAdmin
|
||||
? null
|
||||
: await getActiveBrandId(adminUser);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-10" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<PageHeader
|
||||
title="Reports"
|
||||
subtitle="Operational visibility across orders, fulfillment, contacts, and campaigns."
|
||||
/>
|
||||
|
||||
<ReportsDashboard
|
||||
brands={brands ?? []}
|
||||
initialBrandId={initialBrandId}
|
||||
isPlatformAdmin={isPlatformAdmin}
|
||||
brandId={adminUser.brand_id}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { getBillingOverview } from "@/actions/billing/billing-overview";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import BillingClientPage from "./BillingClientPage";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ brandId?: string }>;
|
||||
};
|
||||
|
||||
export default async function BillingPage({ params }: Props) {
|
||||
const { brandId: brandIdParam } = await params;
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandIdParam);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return <AdminAccessDenied message="You don't have access to that brand." />;
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? "";
|
||||
const isPlatformAdmin = adminUser.role === "platform_admin";
|
||||
|
||||
let resolvedBrandId = effectiveBrandId;
|
||||
if (isPlatformAdmin && !resolvedBrandId) {
|
||||
const { data: firstBrand } = await supabase
|
||||
.from("brands")
|
||||
.select("id")
|
||||
.limit(1)
|
||||
.single();
|
||||
if (firstBrand?.id) {
|
||||
resolvedBrandId = String(firstBrand.id);
|
||||
} else {
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)] px-6 py-12">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
|
||||
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<span className="text-[var(--admin-text-primary)]">Billing</span>
|
||||
</nav>
|
||||
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-8 text-center">
|
||||
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">No Brands Found</h1>
|
||||
<p className="mt-2 text-[var(--admin-text-muted)]">Create a brand in the database before accessing billing settings.</p>
|
||||
<a href="/admin" className="mt-4 inline-block rounded-xl bg-[var(--admin-accent)] hover:bg-[var(--admin-accent-hover)] px-6 py-3 text-sm font-medium text-white transition-colors">
|
||||
Back to Admin
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedBrandId) return <AdminAccessDenied />;
|
||||
|
||||
// Single source of truth for everything the billing client needs.
|
||||
const overviewRes = await getBillingOverview(resolvedBrandId);
|
||||
if (!overviewRes.success || !overviewRes.data) {
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)] px-6 py-12">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-8 text-center">
|
||||
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">Billing Unavailable</h1>
|
||||
<p className="mt-2 text-[var(--admin-text-muted)]">{overviewRes.error ?? "Could not load billing information."}</p>
|
||||
<a href="/admin" className="mt-4 inline-block rounded-xl bg-[var(--admin-accent)] hover:bg-[var(--admin-accent-hover)] px-6 py-3 text-sm font-medium text-white transition-colors">
|
||||
Back to Admin
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const overview = overviewRes.data;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
{/* Platform billing header */}
|
||||
<div className="bg-[var(--admin-bg-subtle)] border-b border-[var(--admin-border)] px-6 py-3">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">
|
||||
<span className="font-medium text-[var(--admin-text-primary)]">Route Commerce Platform Billing</span>
|
||||
{" — "}Invoiced by Cielo Hermosa, LLC · Manage your platform subscription and add-ons.
|
||||
{" "}Questions? <a href="mailto:billing@cielohermosa.com" className="text-[var(--admin-accent)] hover:text-[var(--admin-accent-hover)] underline transition-colors">billing@cielohermosa.com</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-6xl px-6 py-10">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
|
||||
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<a href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</a>
|
||||
<span>/</span>
|
||||
<span className="text-[var(--admin-text-primary)]">Billing</span>
|
||||
</nav>
|
||||
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-[var(--admin-text-primary)]">Billing & Subscription</h1>
|
||||
<p className="mt-1 text-[var(--admin-text-muted)]">
|
||||
Manage your Route Commerce subscription for {overview.brandName ?? "your brand"}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BillingClientPage overview={overview} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import IntegrationsClientPage from "./IntegrationsClientPage";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
|
||||
export const metadata = {
|
||||
title: "Integrations - Route Commerce Admin",
|
||||
description: "Configure integrations for AI, email, SMS, and payments",
|
||||
};
|
||||
|
||||
export default async function IntegrationsPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
|
||||
const isPlatformAdmin = adminUser.role === "platform_admin";
|
||||
const brandId = adminUser.brand_id ?? "";
|
||||
|
||||
// Platform admins: fetch all brands for the picker
|
||||
const brands = isPlatformAdmin
|
||||
? ((await supabase.from("brands").select("id, name").order("name")) as unknown as { data: { id: string; name: string }[] }).data ?? []
|
||||
: [];
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6 pb-10">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
|
||||
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<a href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</a>
|
||||
<span>/</span>
|
||||
<span className="text-[var(--admin-text-primary)]">Integrations</span>
|
||||
</nav>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[var(--admin-accent)] text-white">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--admin-text-primary)]">Integrations</h1>
|
||||
<p className="text-sm text-[var(--admin-text-muted)]">
|
||||
Connect AI, email, SMS, and payment providers to power your operations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IntegrationsClientPage
|
||||
brandId={brandId}
|
||||
brands={brands}
|
||||
isPlatformAdmin={isPlatformAdmin}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getShippingSettings } from "@/actions/shipping/settings";
|
||||
import ShippingSettingsForm from "@/components/admin/ShippingSettingsForm";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
|
||||
export default async function ShippingSettingsPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
if (!adminUser.can_manage_orders || adminUser.role === "store_employee") {
|
||||
redirect("/admin/pickup");
|
||||
}
|
||||
|
||||
const isPlatformAdmin = adminUser.role === "platform_admin";
|
||||
const brandId = adminUser.brand_id ?? "";
|
||||
|
||||
// Platform admins: fetch all brands for the picker
|
||||
const brands = isPlatformAdmin
|
||||
? ((await supabase.from("brands").select("id, name").order("name")) as unknown as { data: { id: string; name: string }[] }).data ?? []
|
||||
: [];
|
||||
|
||||
const effectiveBrandId = brandId || (brands[0]?.id ?? "");
|
||||
|
||||
const result = await getShippingSettings(effectiveBrandId);
|
||||
const settings = result.success ? result.settings : null;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-10" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
<div className="mx-auto max-w-2xl">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-2 text-xs mb-6" style={{ color: "var(--admin-text-muted)" }}>
|
||||
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<span style={{ color: "var(--admin-text-primary)" }}>Shipping</span>
|
||||
</nav>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl" style={{ backgroundColor: "var(--admin-accent)", color: "white" }}>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25m-2.25 0h2.25m0 0v-.375c0-.621-.504-1.125-1.125-1.125H15m-1.5-3l1.5 0l.75 0v-.375c0-.621-.504-1.125-1.125-1.125H15m0 0v-.375c0-.621-.504-1.125-1.125-1.125H12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold" style={{ color: "var(--admin-text-primary)" }}>Shipping Settings</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-sm" style={{ color: "var(--admin-text-muted)" }}>
|
||||
Configure FedEx integration for shipping fresh produce — sweet corn, onions, and more.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border p-6 shadow-lg" style={{ backgroundColor: "white", borderColor: "var(--admin-border)" }}>
|
||||
<ShippingSettingsForm
|
||||
settings={settings}
|
||||
brandId={effectiveBrandId}
|
||||
brands={brands}
|
||||
isPlatformAdmin={isPlatformAdmin}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import TaxDashboard from "@/components/admin/TaxDashboard";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ brandId?: string }>;
|
||||
};
|
||||
|
||||
export default async function TaxesPage({ params }: Props) {
|
||||
const { brandId: brandIdParam } = await params;
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandIdParam);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return <AdminAccessDenied message="You don't have access to that brand." />;
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? "";
|
||||
const isPlatformAdmin = adminUser.role === "platform_admin";
|
||||
|
||||
let resolvedBrandId = effectiveBrandId;
|
||||
if (isPlatformAdmin && !resolvedBrandId) {
|
||||
const { data: firstBrand } = await supabase
|
||||
.from("brands")
|
||||
.select("id")
|
||||
.limit(1)
|
||||
.single() as unknown as { data: { id: string } | null };
|
||||
if (firstBrand?.id) {
|
||||
resolvedBrandId = String(firstBrand.id);
|
||||
} else {
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-12" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="rounded-2xl p-8 text-center border" style={{
|
||||
backgroundColor: "white",
|
||||
borderColor: "var(--admin-border)"
|
||||
}}>
|
||||
<h1 className="text-2xl font-bold" style={{ color: "var(--admin-text-primary)" }}>No Brands Found</h1>
|
||||
<p className="mt-2" style={{ color: "var(--admin-text-muted)" }}>Create a brand in the database first.</p>
|
||||
<a href="/admin" className="mt-4 inline-block rounded-xl px-6 py-3 text-sm font-medium border transition-colors"
|
||||
style={{
|
||||
backgroundColor: "var(--admin-bg-subtle)",
|
||||
borderColor: "var(--admin-border)",
|
||||
color: "var(--admin-text-primary)"
|
||||
}}>
|
||||
Back to Admin
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedBrandId) return <AdminAccessDenied />;
|
||||
|
||||
const { data: brands } = await supabase
|
||||
.from("brands")
|
||||
.select("id, name")
|
||||
.order("name");
|
||||
|
||||
const allBrands = (brands ?? []) as Array<{ id: string; name: string }>;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
<div className="mx-auto max-w-6xl px-6 py-10">
|
||||
<PageHeader
|
||||
title="Tax Dashboard"
|
||||
subtitle="Sales tax collected on orders shipped to nexus states."
|
||||
/>
|
||||
|
||||
<TaxDashboard
|
||||
brands={allBrands}
|
||||
initialBrandId={resolvedBrandId}
|
||||
isPlatformAdmin={isPlatformAdmin}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user