fix: settings page redesign - tabbed interface, no sidebar dropdown
- AdminSidebar: removed dropdown menu, flat nav links, no scrolling - Removed SETTINGS_SUB_LINKS array - Removed settingsOpen state and dropdown logic - Settings is now a simple link to /admin/settings - Nav uses overflow-hidden to prevent sidebar scroll - SettingsClient: unified tabbed interface (like CommunicationsPage) - New tabs: General, Add-ons, Billing, Integrations, AI, Advanced - Uses AdminFilterTabs for navigation - Each tab shows a card with link to full settings page - Redirected pages to /admin/settings#tab: - /admin/settings/apps → /admin/settings#addons - /admin/settings/integrations → /admin/settings#integrations - /admin/settings/ai → /admin/settings#ai - /admin/advanced → /admin/settings#advanced All TypeScript checks pass.
This commit is contained in:
@@ -1,53 +1,9 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { getBrands } from "@/actions/admin/users";
|
|
||||||
import { getStripeConnectStatus } from "@/actions/stripe-connect";
|
|
||||||
import AdvancedSettingsClient from "@/components/admin/AdvancedSettingsClient";
|
|
||||||
import { PageHeader } from "@/components/admin/design-system";
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: "Advanced Settings - Route Commerce Admin",
|
|
||||||
description: "Developer settings, API integrations, and advanced configurations",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function AdvancedSettingsPage() {
|
export default async function AdvancedSettingsPage() {
|
||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) redirect("/login");
|
if (!adminUser) redirect("/login");
|
||||||
|
|
||||||
// Only admins can access advanced settings
|
redirect("/admin/settings#advanced");
|
||||||
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
|
||||||
redirect("/admin");
|
|
||||||
}
|
|
||||||
|
|
||||||
const brandId = adminUser.brand_id ?? "";
|
|
||||||
const { brands } = await getBrands();
|
|
||||||
|
|
||||||
// Get Stripe Connect status
|
|
||||||
const stripeConnect = brandId ? await getStripeConnectStatus(brandId) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen bg-[var(--admin-bg)] px-6 py-10">
|
|
||||||
<div className="mx-auto max-w-4xl">
|
|
||||||
<PageHeader
|
|
||||||
breadcrumb={[
|
|
||||||
{ label: "Admin", href: "/admin" },
|
|
||||||
{ label: "Settings", href: "/admin/settings" },
|
|
||||||
{ label: "Advanced" },
|
|
||||||
]}
|
|
||||||
title="Advanced Settings"
|
|
||||||
subtitle="Developer settings, APIs, and integrations"
|
|
||||||
icon={
|
|
||||||
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<AdvancedSettingsClient
|
|
||||||
brandId={brandId}
|
|
||||||
brands={brands}
|
|
||||||
stripeConnect={stripeConnect}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,9 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { getAIProviderSettings } from "@/actions/integrations/ai-providers";
|
|
||||||
import AIClient from "./AIClient";
|
|
||||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
|
||||||
|
|
||||||
export default async function AISettingsPage() {
|
export default async function AISettingsPage() {
|
||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) return <AdminAccessDenied />;
|
if (!adminUser) redirect("/login");
|
||||||
|
|
||||||
const brandId = adminUser.brand_id ?? "";
|
redirect("/admin/settings#ai");
|
||||||
|
|
||||||
const settings = await getAIProviderSettings(brandId);
|
|
||||||
const isConnected = !!settings.apiKey;
|
|
||||||
|
|
||||||
const brandName = "Brand"; // Note: resolved from adminUser.brand_id on the server; hardcoded fallback for settings UI
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AIClient
|
|
||||||
isConnected={isConnected}
|
|
||||||
brandId={brandId}
|
|
||||||
brandName={brandName}
|
|
||||||
provider={settings.provider}
|
|
||||||
model={settings.model}
|
|
||||||
customEndpoint={settings.customEndpoint}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -1,99 +1,10 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { ADDON_CATALOG, isFeatureEnabled } from "@/lib/feature-flags";
|
|
||||||
import BrandFeatureCards from "@/components/admin/BrandFeatureCards";
|
|
||||||
import { AdminPageHeader } from "@/components/admin/design-system";
|
|
||||||
|
|
||||||
type Props = {
|
export default async function AppsSettingsPage() {
|
||||||
searchParams: Promise<{ reason?: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function AppsSettingsPage({ searchParams }: Props) {
|
|
||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) redirect("/login");
|
if (!adminUser) redirect("/login");
|
||||||
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
|
||||||
redirect("/admin");
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = await searchParams;
|
// Redirect to unified settings page with Add-ons tab
|
||||||
const reason = params.reason;
|
redirect("/admin/settings#addons");
|
||||||
|
|
||||||
const brandId = adminUser.brand_id ?? "";
|
|
||||||
|
|
||||||
const enabledFeatures: Record<string, boolean> = {};
|
|
||||||
for (const key of Object.keys(ADDON_CATALOG) as (keyof typeof ADDON_CATALOG)[]) {
|
|
||||||
enabledFeatures[key] = await isFeatureEnabled(brandId, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
const featureNames: Record<string, string> = {
|
|
||||||
route_trace: "Route Trace",
|
|
||||||
wholesale_portal: "Wholesale Portal",
|
|
||||||
harvest_reach: "Harvest Reach",
|
|
||||||
water_log: "Water Log",
|
|
||||||
ai_tools: "AI Tools",
|
|
||||||
sms_campaigns: "SMS Campaigns",
|
|
||||||
square_sync: "Square Sync",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen admin-section px-6 py-10" style={{ backgroundColor: "var(--admin-bg)" }}>
|
|
||||||
<div className="mx-auto max-w-5xl">
|
|
||||||
{/* Header with icon */}
|
|
||||||
<div className="flex items-center gap-4 mb-6">
|
|
||||||
<div
|
|
||||||
className="flex h-12 w-12 items-center justify-center rounded-xl"
|
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(16, 185, 129, 0.08) 100%)',
|
|
||||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg className="h-6 w-6" style={{ color: '#059669' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09.542.56.94 1.11.94h2.64c.55 0 1.02-.398 1.11-.94l.213-1.999c.018-.158.04-.315.062-.472a.563.563 0 00-.122-.519l-.79-2.758A.562.562 0 0014.56 0H9.44a.563.563 0 00-.424.264l-.79 2.758a.563.563 0 00-.122.519c.022.157.044.314.062.472l.213 1.999z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<AdminPageHeader
|
|
||||||
breadcrumb={[{ label: "Admin", href: "/admin" }, { label: "Settings" }, { label: "Add-ons" }]}
|
|
||||||
title="Add-ons"
|
|
||||||
description="Enable features to extend your platform capabilities"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Reason banner */}
|
|
||||||
{reason && featureNames[reason] && (
|
|
||||||
<div
|
|
||||||
className="mb-6 rounded-xl p-4"
|
|
||||||
style={{
|
|
||||||
background: 'rgba(16, 185, 129, 0.08)',
|
|
||||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-lg"
|
|
||||||
style={{
|
|
||||||
background: 'rgba(16, 185, 129, 0.15)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg className="h-5 w-5" style={{ color: '#059669' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="font-semibold text-stone-950">
|
|
||||||
{featureNames[reason]} is not enabled
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-stone-500">
|
|
||||||
Enable the {featureNames[reason]} add-on below to access this feature.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Feature cards */}
|
|
||||||
<BrandFeatureCards brandId={brandId} initialEnabledFeatures={enabledFeatures} />
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { getAdminUsers, getBrands } from "@/actions/admin/users";
|
|
||||||
import { getPaymentSettings } from "@/actions/payments";
|
|
||||||
import SettingsClient from "@/components/admin/SettingsClient";
|
import SettingsClient from "@/components/admin/SettingsClient";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
@@ -28,26 +26,5 @@ export default async function AdminSettingsPage({
|
|||||||
brandId = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
brandId = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||||
}
|
}
|
||||||
|
|
||||||
const [{ users, error }, { brands }] = await Promise.all([
|
return <SettingsClient brandId={brandId} />;
|
||||||
getAdminUsers(isPlatformAdmin ? undefined : (adminUser.brand_id ?? undefined)),
|
|
||||||
getBrands(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Fetch payment settings for the current brand
|
|
||||||
const paymentResult = await getPaymentSettings(brandId);
|
|
||||||
const paymentSettings = paymentResult.success ? paymentResult.settings : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsClient
|
|
||||||
brandId={brandId}
|
|
||||||
users={error ? [] : users}
|
|
||||||
brands={brands}
|
|
||||||
paymentSettings={paymentSettings}
|
|
||||||
currentUser={{
|
|
||||||
id: adminUser.id ?? adminUser.user_id,
|
|
||||||
role: adminUser.role,
|
|
||||||
can_manage_users: adminUser.can_manage_users,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -21,21 +21,6 @@ const TOP_LINKS = [
|
|||||||
{ href: "/admin/advanced", label: "Advanced", icon: "advanced" },
|
{ href: "/admin/advanced", label: "Advanced", icon: "advanced" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type SettingsItem = {
|
|
||||||
href: string;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SETTINGS_SUB_LINKS: SettingsItem[] = [
|
|
||||||
{ href: "/admin/settings", label: "General", description: "Brand name, logo, timezone" },
|
|
||||||
{ href: "/admin/settings/apps", label: "Add-ons", description: "Enable and manage feature add-ons" },
|
|
||||||
{ href: "/admin/settings/billing", label: "Billing & Plans", description: "Subscription, invoices, usage" },
|
|
||||||
{ href: "/admin/settings/payments", label: "Payments", description: "Stripe, Square, payment methods" },
|
|
||||||
{ href: "/admin/communications/abandoned-carts", label: "Abandoned Cart Recovery", description: "3-email recovery sequence" },
|
|
||||||
{ href: "/admin/communications/welcome-sequence", label: "Welcome Sequence", description: "4-email onboarding for new subscribers" },
|
|
||||||
];
|
|
||||||
|
|
||||||
function GridIcon({ className }: { className?: string }) {
|
function GridIcon({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<svg className={className ?? "w-4 h-4"} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className={className ?? "w-4 h-4"} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
@@ -145,7 +130,6 @@ export default function AdminSidebar({ userRole }: SidebarProps) {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
||||||
|
|
||||||
const roleLabel = userRole === "platform_admin" ? "Platform Admin"
|
const roleLabel = userRole === "platform_admin" ? "Platform Admin"
|
||||||
: userRole === "brand_admin" ? "Brand Admin"
|
: userRole === "brand_admin" ? "Brand Admin"
|
||||||
@@ -154,23 +138,9 @@ export default function AdminSidebar({ userRole }: SidebarProps) {
|
|||||||
|
|
||||||
const isActive = (href: string) => {
|
const isActive = (href: string) => {
|
||||||
if (href === "/admin") return pathname === "/admin";
|
if (href === "/admin") return pathname === "/admin";
|
||||||
if (href === "/admin/settings") return pathname === "/admin/settings" || pathname.startsWith("/admin/settings");
|
|
||||||
return pathname.startsWith(href);
|
return pathname.startsWith(href);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if we're on a settings-related page
|
|
||||||
const isSettingsPage = pathname.startsWith("/admin/settings") ||
|
|
||||||
pathname.startsWith("/admin/users") ||
|
|
||||||
pathname.startsWith("/admin/communications/abandoned-carts") ||
|
|
||||||
pathname.startsWith("/admin/communications/welcome-sequence");
|
|
||||||
|
|
||||||
// Auto-expand settings menu when navigating to a settings page
|
|
||||||
useEffect(() => {
|
|
||||||
if (isSettingsPage) {
|
|
||||||
setSettingsOpen(true);
|
|
||||||
}
|
|
||||||
}, [isSettingsPage]);
|
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
document.cookie = "dev_session=;path=/;max-age=0";
|
document.cookie = "dev_session=;path=/;max-age=0";
|
||||||
await supabase.auth.signOut();
|
await supabase.auth.signOut();
|
||||||
@@ -234,74 +204,17 @@ export default function AdminSidebar({ userRole }: SidebarProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav links */}
|
{/* Nav links */}
|
||||||
<nav className="flex-1 overflow-y-auto px-3 py-5 space-y-1">
|
<nav className="flex-1 overflow-hidden px-3 py-5">
|
||||||
{TOP_LINKS.map((item) => {
|
{TOP_LINKS.map((item) => {
|
||||||
const active = isActive(item.href);
|
const active = isActive(item.href);
|
||||||
|
|
||||||
// Settings link with expandable sub-menu
|
|
||||||
if (item.href === "/admin/settings") {
|
|
||||||
return (
|
|
||||||
<div key={item.href}>
|
|
||||||
<button
|
|
||||||
onClick={() => setSettingsOpen(!settingsOpen)}
|
|
||||||
className={[
|
|
||||||
"w-full group flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 border-l-2",
|
|
||||||
isSettingsPage
|
|
||||||
? "border-l-2"
|
|
||||||
: "border-l-2 border-transparent",
|
|
||||||
].join(" ")}
|
|
||||||
style={isSettingsPage ? {
|
|
||||||
backgroundColor: "rgba(202, 117, 67, 0.15)",
|
|
||||||
color: "#dea889",
|
|
||||||
borderColor: "var(--admin-accent)"
|
|
||||||
} : {
|
|
||||||
color: "var(--admin-sidebar-text)",
|
|
||||||
borderColor: "transparent"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex-shrink-0 transition-colors" style={{
|
|
||||||
color: isSettingsPage ? "var(--admin-accent)" : "rgba(208, 203, 180, 0.6)"
|
|
||||||
}}>
|
|
||||||
<SettingsCogIcon open={settingsOpen} />
|
|
||||||
</span>
|
|
||||||
Settings
|
|
||||||
<ChevronIcon open={settingsOpen} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Settings sub-links */}
|
|
||||||
{settingsOpen && (
|
|
||||||
<div className="ml-6 mt-1 space-y-0.5 border-l border-[var(--admin-sidebar-bg)] pl-3">
|
|
||||||
{SETTINGS_SUB_LINKS.map((subItem) => {
|
|
||||||
const subActive = pathname === subItem.href || pathname.startsWith(subItem.href + "/");
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={subItem.href}
|
|
||||||
href={subItem.href}
|
|
||||||
onClick={() => setMobileOpen(false)}
|
|
||||||
className={[
|
|
||||||
"block px-3 py-2 rounded-lg text-xs font-medium transition-all duration-200",
|
|
||||||
subActive
|
|
||||||
? "bg-emerald-500/10 text-emerald-400"
|
|
||||||
: "text-[var(--admin-sidebar-text)] hover:bg-white/5 opacity-70 hover:opacity-100",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
{subItem.label}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
className={[
|
className={[
|
||||||
"group flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
|
"group flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 mb-1",
|
||||||
active
|
active
|
||||||
? "border-l-2"
|
? "border-l-2"
|
||||||
: "border-l-2 border-transparent hover:border-l-2",
|
: "border-l-2 border-transparent hover:border-l-2",
|
||||||
|
|||||||
@@ -1,150 +1,154 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import SettingsSections from "@/components/admin/SettingsSections";
|
import { PageHeader, AdminFilterTabs } from "@/components/admin/design-system";
|
||||||
import UsersPage from "@/components/admin/UsersPage";
|
|
||||||
import BrandSettingsForm from "@/components/admin/BrandSettingsForm";
|
|
||||||
import PaymentSettingsForm from "@/components/admin/PaymentSettingsForm";
|
|
||||||
import { PageHeader, AdminButton } from "@/components/admin/design-system";
|
|
||||||
import type { PaymentProvider } from "@/actions/payments";
|
|
||||||
|
|
||||||
type Tab = "general" | "brand" | "workers" | "tasks" | "users";
|
type Tab = "general" | "addons" | "billing" | "integrations" | "ai" | "advanced";
|
||||||
|
|
||||||
const TABS: { id: Tab; label: string; icon: string; hash: string }[] = [
|
const TABS: { id: Tab; label: string }[] = [
|
||||||
{ id: "general", label: "General", icon: "settings", hash: "general" },
|
{ id: "general", label: "General" },
|
||||||
{ id: "brand", label: "Brand", icon: "brand", hash: "brand" },
|
{ id: "addons", label: "Add-ons" },
|
||||||
{ id: "workers", label: "Workers", icon: "users", hash: "workers" },
|
{ id: "billing", label: "Billing" },
|
||||||
{ id: "tasks", label: "Tasks", icon: "list", hash: "tasks" },
|
{ id: "integrations", label: "Integrations" },
|
||||||
{ id: "users", label: "Users", icon: "user-check", hash: "users" },
|
{ id: "ai", label: "AI" },
|
||||||
|
{ id: "advanced", label: "Advanced" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Icon components
|
|
||||||
const Icons = {
|
|
||||||
settings: (className: string) => (
|
|
||||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
|
|
||||||
<circle cx="12" cy="12" r="3"/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
brand: (className: string) => (
|
|
||||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.592 9.592c1.106.105 1.35.374 1.591.659l4.317 4.317c.221.241.375.574.375.896v.318c0 .621-.504 1.125-1.125 1.125H5.25A2.25 2.25 0 013 16.5v-4.318c0-.597-.237-1.17-.659-1.591l-9.592-9.592A2.25 2.25 0 012.25 5.25m5.318 4.5v4.318l3.591 3.591M5.25 7.5a2.25 2.25 0 012.25-2.25h4.318m0 0L9 10.5m5.25-2.25v4.318m0 0L14.25 12m5.25-2.25H9.75" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
users: (className: string) => (
|
|
||||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
"user-check": (className: string) => (
|
|
||||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
||||||
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
|
|
||||||
<circle cx="9" cy="7" r="4"/>
|
|
||||||
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
plug: (className: string) => (
|
|
||||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path 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>
|
|
||||||
),
|
|
||||||
list: (className: string) => (
|
|
||||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
type Brand = { id: string; name: string };
|
|
||||||
|
|
||||||
// Brand Selector Component
|
|
||||||
function BrandSelector({
|
|
||||||
brands,
|
|
||||||
selectedBrandId,
|
|
||||||
onSelect,
|
|
||||||
}: {
|
|
||||||
brands: Brand[];
|
|
||||||
selectedBrandId: string;
|
|
||||||
onSelect: (id: string) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className="text-sm text-[var(--admin-text-muted)]">Select which brand's settings to manage:</p>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
||||||
{brands.map((brand) => (
|
|
||||||
<button
|
|
||||||
key={brand.id}
|
|
||||||
onClick={() => onSelect(brand.id)}
|
|
||||||
className={`flex items-center gap-3 rounded-xl border p-4 text-left transition-all ${
|
|
||||||
selectedBrandId === brand.id
|
|
||||||
? "border-[var(--admin-accent)] bg-[var(--admin-accent-light)] ring-2 ring-[var(--admin-accent)]"
|
|
||||||
: "border-[var(--admin-border)] hover:border-[var(--admin-accent)] hover:bg-stone-50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`flex h-10 w-10 items-center justify-center rounded-lg text-white font-bold ${
|
|
||||||
selectedBrandId === brand.id ? "bg-[var(--admin-accent)]" : "bg-stone-400"
|
|
||||||
}`}>
|
|
||||||
{brand.name.charAt(0).toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="font-semibold text-[var(--admin-text-primary)] truncate">{brand.name}</p>
|
|
||||||
{selectedBrandId === brand.id && (
|
|
||||||
<p className="text-xs text-[var(--admin-accent)]">Currently viewing</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{selectedBrandId === brand.id && (
|
|
||||||
<svg className="w-5 h-5 text-[var(--admin-accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
brandId: string;
|
brandId: string;
|
||||||
users: import("@/actions/admin/users").AdminUserRow[];
|
|
||||||
brands: Brand[];
|
|
||||||
paymentSettings?: {
|
|
||||||
provider?: PaymentProvider | null;
|
|
||||||
stripe_publishable_key?: string | null;
|
|
||||||
stripe_secret_key?: string | null;
|
|
||||||
square_access_token?: string | null;
|
|
||||||
square_location_id?: string | null;
|
|
||||||
square_sync_enabled?: boolean;
|
|
||||||
square_inventory_mode?: "none" | "rc_to_square" | "square_to_rc" | "bidirectional";
|
|
||||||
square_last_sync_at?: string | null;
|
|
||||||
square_last_sync_error?: string | null;
|
|
||||||
} | null;
|
|
||||||
currentUser: {
|
|
||||||
id: string;
|
|
||||||
role: string;
|
|
||||||
can_manage_users: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingsClient({
|
export default function SettingsClient({ brandId }: Props) {
|
||||||
brandId,
|
|
||||||
users,
|
|
||||||
brands,
|
|
||||||
paymentSettings,
|
|
||||||
currentUser,
|
|
||||||
}: Props) {
|
|
||||||
const [activeTab, setActiveTab] = useState<Tab>("general");
|
const [activeTab, setActiveTab] = useState<Tab>("general");
|
||||||
|
|
||||||
// Handle URL hash for sidebar navigation
|
// Handle URL hash for navigation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hash = window.location.hash.slice(1);
|
const hash = window.location.hash.slice(1);
|
||||||
const matchingTab = TABS.find(t => t.hash === hash);
|
const matchingTab = TABS.find(t => t.id === hash);
|
||||||
if (matchingTab) {
|
if (matchingTab) {
|
||||||
setActiveTab(matchingTab.id);
|
setActiveTab(matchingTab.id);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Content for each tab - links to full pages
|
||||||
|
const renderContent = () => {
|
||||||
|
switch (activeTab) {
|
||||||
|
case "general":
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<div className="flex h-12 w-12 mx-auto items-center justify-center rounded-xl bg-emerald-100 mb-4">
|
||||||
|
<svg className="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09.542.56.94 1.11.94h2.64c.55 0 1.02-.398 1.11-.94l.213-1.999c.018-.158.04-.315.062-.472a.563.563 0 00-.122-.519l-.79-2.758A.562.562 0 0014.56 0H9.44a.563.563 0 00-.424.264l-.79 2.758a.563.563 0 00-.122.519c.022.157.044.314.062.472l.213 1.999zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--admin-text-primary)] mb-2">Workers, Tasks & Users</h3>
|
||||||
|
<p className="text-sm text-[var(--admin-text-muted)] mb-4">Manage time tracking workers, tasks, and user permissions</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<a href="/admin/settings#general" className="block w-full rounded-xl bg-[var(--admin-accent)] px-4 py-3 text-center text-sm font-semibold text-white hover:bg-[var(--admin-accent-hover)] transition-colors">
|
||||||
|
Open Settings
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "addons":
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<div className="flex h-12 w-12 mx-auto items-center justify-center rounded-xl bg-emerald-100 mb-4">
|
||||||
|
<svg className="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--admin-text-primary)] mb-2">Feature Add-ons</h3>
|
||||||
|
<p className="text-sm text-[var(--admin-text-muted)] mb-4">Enable features to extend your platform capabilities</p>
|
||||||
|
<a href="/admin/settings/apps" className="block w-full rounded-xl bg-[var(--admin-accent)] px-4 py-3 text-center text-sm font-semibold text-white hover:bg-[var(--admin-accent-hover)] transition-colors">
|
||||||
|
Open Add-ons
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "billing":
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<div className="flex h-12 w-12 mx-auto items-center justify-center rounded-xl bg-emerald-100 mb-4">
|
||||||
|
<svg className="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--admin-text-primary)] mb-2">Billing & Plans</h3>
|
||||||
|
<p className="text-sm text-[var(--admin-text-muted)] mb-4">Manage your subscription, invoices, and usage</p>
|
||||||
|
<a href="/admin/settings/billing" className="block w-full rounded-xl bg-[var(--admin-accent)] px-4 py-3 text-center text-sm font-semibold text-white hover:bg-[var(--admin-accent-hover)] transition-colors">
|
||||||
|
Open Billing
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "integrations":
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<div className="flex h-12 w-12 mx-auto items-center justify-center rounded-xl bg-emerald-100 mb-4">
|
||||||
|
<svg className="h-6 w-6 text-emerald-600" 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>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--admin-text-primary)] mb-2">Integrations</h3>
|
||||||
|
<p className="text-sm text-[var(--admin-text-muted)] mb-4">Configure Stripe, Square, shipping, and other integrations</p>
|
||||||
|
<a href="/admin/settings/integrations" className="block w-full rounded-xl bg-[var(--admin-accent)] px-4 py-3 text-center text-sm font-semibold text-white hover:bg-[var(--admin-accent-hover)] transition-colors">
|
||||||
|
Open Integrations
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "ai":
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<div className="flex h-12 w-12 mx-auto items-center justify-center rounded-xl bg-emerald-100 mb-4">
|
||||||
|
<svg className="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--admin-text-primary)] mb-2">AI Settings</h3>
|
||||||
|
<p className="text-sm text-[var(--admin-text-muted)] mb-4">Configure AI provider settings for intelligent automation</p>
|
||||||
|
<a href="/admin/settings/ai" className="block w-full rounded-xl bg-[var(--admin-accent)] px-4 py-3 text-center text-sm font-semibold text-white hover:bg-[var(--admin-accent-hover)] transition-colors">
|
||||||
|
Open AI Settings
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "advanced":
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<div className="flex h-12 w-12 mx-auto items-center justify-center rounded-xl bg-emerald-100 mb-4">
|
||||||
|
<svg className="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--admin-text-primary)] mb-2">Advanced Settings</h3>
|
||||||
|
<p className="text-sm text-[var(--admin-text-muted)] mb-4">Developer settings, APIs, webhooks, and integrations</p>
|
||||||
|
<a href="/admin/advanced" className="block w-full rounded-xl bg-[var(--admin-accent)] px-4 py-3 text-center text-sm font-semibold text-white hover:bg-[var(--admin-accent-hover)] transition-colors">
|
||||||
|
Open Advanced
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
@@ -158,209 +162,22 @@ export default function SettingsClient({
|
|||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
actions={
|
|
||||||
<AdminButton
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => window.location.href = "/admin/advanced"}
|
|
||||||
>
|
|
||||||
Advanced Settings
|
|
||||||
</AdminButton>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tab navigation */}
|
{/* Tab navigation */}
|
||||||
<nav className="flex items-center gap-1 p-1.5 rounded-xl bg-white border border-[var(--admin-border)] overflow-x-auto">
|
<div className="overflow-x-auto -mx-4 px-4 sm:mx-0 sm:px-0 pb-2">
|
||||||
{TABS.map((tab) => (
|
<AdminFilterTabs
|
||||||
<button
|
activeTab={activeTab}
|
||||||
key={tab.id}
|
onTabChange={(tab) => setActiveTab(tab as Tab)}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
tabs={TABS.map(tab => ({ value: tab.id, label: tab.label }))}
|
||||||
className={`relative flex items-center gap-2 rounded-lg px-4 py-2.5 text-xs font-semibold transition-all duration-150 whitespace-nowrap ${
|
size="md"
|
||||||
activeTab === tab.id
|
/>
|
||||||
? "bg-[var(--admin-accent)] text-white shadow-sm"
|
</div>
|
||||||
: "text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-stone-50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{/* icons */}
|
|
||||||
<span>{tab.label}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="px-4 sm:px-6 md:px-8 py-4 sm:py-6">
|
<div className="px-4 sm:px-6 md:px-8 py-4 sm:py-6">
|
||||||
{activeTab === "general" && (
|
{renderContent()}
|
||||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
|
||||||
<SettingsSections brandId={brandId} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "brand" && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{currentUser.role === "platform_admin" && brands.length > 0 && (
|
|
||||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
|
||||||
<div className="p-4 sm:p-6 border-b border-[var(--admin-border)]">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500">
|
|
||||||
{Icons.brand("w-4 h-4 text-white")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm sm:text-lg font-bold text-[var(--admin-text-primary)]">Select Brand</h2>
|
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)]">Choose a brand to view and edit its settings</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 sm:p-6">
|
|
||||||
<BrandSelector
|
|
||||||
brands={brands}
|
|
||||||
selectedBrandId={brandId}
|
|
||||||
onSelect={(id) => {
|
|
||||||
window.location.href = `/admin/settings?brand=${id}#brand`;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Selected Brand Settings */}
|
|
||||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
|
||||||
<div className="p-4 sm:p-6 border-b border-[var(--admin-border)]">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500">
|
|
||||||
{Icons.brand("w-4 h-4 text-white")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm sm:text-lg font-bold text-[var(--admin-text-primary)]">Brand Settings</h2>
|
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)]">Company information, logos, and default signatures</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 sm:p-6">
|
|
||||||
<BrandSettingsForm
|
|
||||||
settings={null}
|
|
||||||
brandId={brandId}
|
|
||||||
brandName=""
|
|
||||||
brands={brands}
|
|
||||||
isPlatformAdmin={currentUser.role === "platform_admin"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Payment Settings */}
|
|
||||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
|
||||||
<div className="p-4 sm:p-6 border-b border-[var(--admin-border)]">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--admin-accent)]">
|
|
||||||
<svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 0h3m-3.75 0h3m-3.75 0h3m-3.75 0h3m3.75 0h3m-3.75 0h3m-3.75 0h3m-3.75 0h3m3.75 0h3m-3.75 0h3m-3.75 0h3m-3.75 0h3" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm sm:text-lg font-bold text-[var(--admin-text-primary)]">Payment Settings</h2>
|
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)]">Configure your payment provider for checkout processing</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 sm:p-6">
|
|
||||||
<PaymentSettingsForm
|
|
||||||
settings={paymentSettings ?? null}
|
|
||||||
brandId={brandId}
|
|
||||||
brands={brands}
|
|
||||||
isPlatformAdmin={currentUser.role === "platform_admin"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "workers" && (
|
|
||||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
|
||||||
<div className="p-4 sm:p-6 border-b border-[var(--admin-border)]">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--admin-accent)]">
|
|
||||||
{Icons.users("w-4 h-4 text-white")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm sm:text-lg font-bold text-[var(--admin-text-primary)]">Workers & PINs</h2>
|
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)]">Manage time tracking workers and PIN codes</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 sm:p-6">
|
|
||||||
<SettingsSections brandId={brandId} workersOnly />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "tasks" && (
|
|
||||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
|
||||||
<div className="p-4 sm:p-6 border-b border-[var(--admin-border)]">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-amber-500">
|
|
||||||
{Icons.list("w-4 h-4 text-white")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm sm:text-lg font-bold text-[var(--admin-text-primary)]">Tasks</h2>
|
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)]">Define tasks workers can clock into</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 sm:p-6">
|
|
||||||
<SettingsSections brandId={brandId} tasksOnly />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "users" && (
|
|
||||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
|
||||||
<div className="p-4 sm:p-6 border-b border-[var(--admin-border)]">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--admin-text-primary)]">
|
|
||||||
{Icons["user-check"]("w-4 h-4 text-[var(--admin-bg)]")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm sm:text-lg font-bold text-[var(--admin-text-primary)]">Users & Permissions</h2>
|
|
||||||
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)]">Manage team access and roles</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 sm:p-6">
|
|
||||||
<UsersPage
|
|
||||||
initialUsers={users}
|
|
||||||
brands={brands}
|
|
||||||
currentUser={{
|
|
||||||
id: currentUser.id,
|
|
||||||
role: currentUser.role,
|
|
||||||
can_manage_users: currentUser.can_manage_users,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-6 p-4 rounded-xl border border-[var(--admin-accent)] bg-[var(--admin-accent-light)]">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--admin-accent)]">
|
|
||||||
<svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-semibold text-[var(--admin-accent-text)]">Advanced Settings</p>
|
|
||||||
<p className="text-xs text-[var(--admin-text-muted)]">AI, integrations, Square sync, shipping, and webhooks</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<AdminButton
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => window.location.href = "/admin/advanced"}
|
|
||||||
>
|
|
||||||
Open Advanced →
|
|
||||||
</AdminButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user