Move payment settings to Brand tab in Settings

- Integrated PaymentSettingsForm into SettingsClient Brand tab
- Added paymentSettings prop to SettingsClient and Settings page
- Redirected /admin/settings/payments to /admin/settings#brand
- Updated PaymentSettingsForm to use admin CSS variables for styling
- Fixed colors to match admin design system (no dark backgrounds)
This commit is contained in:
2026-06-02 04:14:27 +00:00
parent d44506dd01
commit 7783604141
11 changed files with 535 additions and 264 deletions
@@ -66,7 +66,7 @@ export default function BillingClientPage({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* ── 1. Header summary bar ───────────────────────────────────────────── */} {/* ── 1. Header summary bar ───────────────────────────────────────────── */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 px-6 py-5"> <div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] px-6 py-5">
<div className="flex items-center justify-between flex-wrap gap-4"> <div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div> <div>
@@ -76,26 +76,26 @@ export default function BillingClientPage({
</span> </span>
{subscriptionStatus && ( {subscriptionStatus && (
<span className={`rounded-full px-2.5 py-0.5 text-xs font-bold uppercase ${ <span className={`rounded-full px-2.5 py-0.5 text-xs font-bold uppercase ${
subscriptionStatus === "active" ? "bg-green-900/40 text-green-400" : subscriptionStatus === "active" ? "bg-[var(--admin-success-light)] text-[var(--admin-success-accent)]" :
subscriptionStatus === "past_due" ? "bg-amber-100 text-amber-700" : subscriptionStatus === "past_due" ? "bg-amber-100 text-amber-700" :
subscriptionStatus === "canceled" ? "bg-red-900/40 text-red-400" : subscriptionStatus === "canceled" ? "bg-red-100 text-red-600" :
subscriptionStatus === "trialing" ? "bg-blue-900/40 text-blue-700" : subscriptionStatus === "trialing" ? "bg-blue-100 text-blue-600" :
"bg-zinc-950 text-zinc-400" "bg-[var(--admin-bg)] text-[var(--admin-text-muted)]"
}`}> }`}>
{subscriptionStatus.replace("_", " ")} {subscriptionStatus.replace("_", " ")}
</span> </span>
)} )}
<span className="text-sm text-zinc-500"> <span className="text-sm text-[var(--admin-text-muted)]">
{currentPeriodEnd ? ( {currentPeriodEnd ? (
<>Next billing: <span className="font-medium text-zinc-300">{new Date(currentPeriodEnd).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</span></> <>Next billing: <span className="font-medium text-[var(--admin-text-primary)]">{new Date(currentPeriodEnd).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</span></>
) : ( ) : (
"No active subscription" "No active subscription"
)} )}
</span> </span>
</div> </div>
<p className="text-2xl font-bold text-zinc-100"> <p className="text-2xl font-bold text-[var(--admin-text-primary)]">
${totalMonthly}<span className="text-sm font-normal text-slate-400">/mo</span> ${totalMonthly}<span className="text-sm font-normal text-[var(--admin-text-muted)]">/mo</span>
<span className="ml-2 text-sm font-medium text-green-600"> <span className="ml-2 text-sm font-medium text-[var(--admin-success-accent)]">
{billingCycle === "annual" ? "Annual (saves 25%)" : ""} {billingCycle === "annual" ? "Annual (saves 25%)" : ""}
</span> </span>
</p> </p>
@@ -108,35 +108,35 @@ export default function BillingClientPage({
{/* ── 2. Current plan + Add-ons (two-column) ───────────────────────────── */} {/* ── 2. Current plan + Add-ons (two-column) ───────────────────────────── */}
<div className="grid gap-4 lg:grid-cols-5"> <div className="grid gap-4 lg:grid-cols-5">
{/* Current plan card */} {/* Current plan card */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5 lg:col-span-2"> <div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5 lg:col-span-2">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-zinc-100">Your Plan</h3> <h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Your Plan</h3>
<span className={`rounded-full px-2.5 py-0.5 text-xs font-bold uppercase ${currentPlan.color}`}> <span className={`rounded-full px-2.5 py-0.5 text-xs font-bold uppercase ${currentPlan.color}`}>
{currentPlan.label} {currentPlan.label}
</span> </span>
</div> </div>
<p className="text-lg font-bold text-zinc-100 mb-1"> <p className="text-lg font-bold text-[var(--admin-text-primary)] mb-1">
{billingCycle === "annual" ? `$${currentPlan.annualPrice}/yr` : `$${currentPlan.monthlyPrice}/mo`} {billingCycle === "annual" ? `$${currentPlan.annualPrice}/yr` : `$${currentPlan.monthlyPrice}/mo`}
</p> </p>
<p className="text-xs text-zinc-500 mb-4"> <p className="text-xs text-[var(--admin-text-muted)] mb-4">
{billingCycle === "annual" && currentPlan.annualPrice {billingCycle === "annual" && currentPlan.annualPrice
? `$${Math.round(currentPlan.annualPrice / 12)}/mo equivalent` ? `$${Math.round(currentPlan.annualPrice / 12)}/mo equivalent`
: "billed monthly"} : "billed monthly"}
</p> </p>
<ul className="space-y-1 mb-4"> <ul className="space-y-1 mb-4">
{(currentPlan.features as readonly string[]).slice(0, 5).map((f, i) => ( {(currentPlan.features as readonly string[]).slice(0, 5).map((f, i) => (
<li key={i} className="flex items-center gap-2 text-xs text-zinc-400"> <li key={i} className="flex items-center gap-2 text-xs text-[var(--admin-text-secondary)]">
<span className="text-green-500 shrink-0"></span> {f} <span className="text-[var(--admin-success)] shrink-0"></span> {f}
</li> </li>
))} ))}
{(currentPlan.features as readonly string[]).length > 5 && ( {(currentPlan.features as readonly string[]).length > 5 && (
<li className="text-xs text-slate-400 pl-5">+ {(currentPlan.features as readonly string[]).length - 5} more</li> <li className="text-xs text-[var(--admin-text-muted)] pl-5">+ {(currentPlan.features as readonly string[]).length - 5} more</li>
)} )}
</ul> </ul>
{isPlatformAdmin && ( {isPlatformAdmin && (
<button <button
onClick={() => setCompareOpen(!compareOpen)} onClick={() => setCompareOpen(!compareOpen)}
className="text-xs text-violet-600 hover:underline font-medium" className="text-xs text-[var(--admin-accent)] hover:underline font-medium"
> >
{compareOpen ? "Hide plan comparison" : "Compare plans →"} {compareOpen ? "Hide plan comparison" : "Compare plans →"}
</button> </button>
@@ -144,10 +144,10 @@ export default function BillingClientPage({
</div> </div>
{/* Add-ons */} {/* Add-ons */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5 lg:col-span-3"> <div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5 lg:col-span-3">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-zinc-100">Add-ons</h3> <h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Add-ons</h3>
<span className="text-xs text-slate-400">+{addonsMonthlyTotal}/mo</span> <span className="text-xs text-[var(--admin-text-muted)]">+{addonsMonthlyTotal}/mo</span>
</div> </div>
<div className="space-y-2.5"> <div className="space-y-2.5">
{allAddonKeys.map((key) => { {allAddonKeys.map((key) => {
@@ -157,19 +157,19 @@ export default function BillingClientPage({
? Math.round(addon.annualPrice / 12) ? Math.round(addon.annualPrice / 12)
: addon.monthlyPrice; : addon.monthlyPrice;
return ( return (
<div key={key} className="flex items-center justify-between rounded-lg border bg-slate-50 px-3 py-2.5"> <div key={key} className="flex items-center justify-between rounded-lg border border-[var(--admin-border)] bg-[var(--admin-bg)] px-3 py-2.5">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<span className="text-base leading-none">{addon.icon}</span> <span className="text-base leading-none">{addon.icon}</span>
<div> <div>
<p className="text-sm font-medium text-slate-800">{addon.label}</p> <p className="text-sm font-medium text-[var(--admin-text-primary)]">{addon.label}</p>
<p className="text-xs text-slate-400">{addon.description}</p> <p className="text-xs text-[var(--admin-text-muted)]">{addon.description}</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-3 shrink-0"> <div className="flex items-center gap-3 shrink-0">
<span className="text-sm font-semibold text-zinc-300">+${displayPrice}/mo</span> <span className="text-sm font-semibold text-[var(--admin-text-secondary)]">+${displayPrice}/mo</span>
{isEnabled ? ( {isEnabled ? (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="rounded-full bg-green-900/40 text-green-400 text-xs px-2 py-0.5 font-medium">Active</span> <span className="rounded-full bg-[var(--admin-success-light)] text-[var(--admin-success-accent)] text-xs px-2 py-0.5 font-medium">Active</span>
{isPlatformAdmin && ( {isPlatformAdmin && (
<RemoveAddonButton brandId={brandId} addonKey={key} onRemoved={() => window.location.reload()} /> <RemoveAddonButton brandId={brandId} addonKey={key} onRemoved={() => window.location.reload()} />
)} )}
@@ -187,18 +187,18 @@ export default function BillingClientPage({
{/* ── 3. Compare plans (collapsible) ───────────────────────────────────── */} {/* ── 3. Compare plans (collapsible) ───────────────────────────────────── */}
{compareOpen && ( {compareOpen && (
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 overflow-hidden"> <div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] overflow-hidden">
<div className="border-b border-slate-100 bg-slate-50 px-5 py-3 flex items-center justify-between"> <div className="border-b border-[var(--admin-border)] bg-[var(--admin-bg)] px-5 py-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-zinc-100">Plan Comparison</h3> <h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Plan Comparison</h3>
<button onClick={() => setCompareOpen(false)} className="text-xs text-slate-400 hover:text-zinc-400"> <button onClick={() => setCompareOpen(false)} className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
Close Close
</button> </button>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="border-b border-slate-100"> <tr className="border-b border-[var(--admin-border)]">
<th className="pb-3 pr-4 text-left font-semibold text-zinc-500 w-2/5" /> <th className="pb-3 pr-4 text-left font-semibold text-[var(--admin-text-muted)] w-2/5" />
{(["starter", "farm", "enterprise"] as const).map((tier) => { {(["starter", "farm", "enterprise"] as const).map((tier) => {
const plan = PLAN_TIERS[tier]; const plan = PLAN_TIERS[tier];
const price = billingCycle === "annual" ? plan.annualPrice : plan.monthlyPrice; const price = billingCycle === "annual" ? plan.annualPrice : plan.monthlyPrice;
@@ -208,13 +208,13 @@ export default function BillingClientPage({
{plan.label} {plan.label}
</span> </span>
<div className="mt-1"> <div className="mt-1">
<span className="text-lg font-bold text-zinc-100"> <span className="text-lg font-bold text-[var(--admin-text-primary)]">
{price !== null ? `$${price}` : "$399"} {price !== null ? `$${price}` : "$399"}
</span> </span>
<span className="text-slate-400 text-xs">/{billingCycle === "annual" ? "yr" : "mo"}</span> <span className="text-[var(--admin-text-muted)] text-xs">/{billingCycle === "annual" ? "yr" : "mo"}</span>
</div> </div>
{tier === "enterprise" && billingCycle === "annual" && ( {tier === "enterprise" && billingCycle === "annual" && (
<p className="text-xs text-slate-400 mt-0.5">or $399/mo</p> <p className="text-xs text-[var(--admin-text-muted)] mt-0.5">or $399/mo</p>
)} )}
</th> </th>
); );
@@ -241,28 +241,28 @@ export default function BillingClientPage({
].map(([feature, tiers]) => { ].map(([feature, tiers]) => {
const t = tiers as Record<string, boolean>; const t = tiers as Record<string, boolean>;
return ( return (
<tr key={feature as string} className="border-t border-slate-100"> <tr key={feature as string} className="border-t border-[var(--admin-border)]">
<td className="py-2 pr-4 text-zinc-400">{feature as string}</td> <td className="py-2 pr-4 text-[var(--admin-text-secondary)]">{feature as string}</td>
{(["starter", "farm", "enterprise"] as const).map((tier) => ( {(["starter", "farm", "enterprise"] as const).map((tier) => (
<td key={tier} className="py-2 px-3 text-center"> <td key={tier} className="py-2 px-3 text-center">
{t[tier] {t[tier]
? <span className="text-green-500"></span> ? <span className="text-[var(--admin-success)]"></span>
: <span className="text-slate-300"></span>} : <span className="text-[var(--admin-text-muted)]"></span>}
</td> </td>
))} ))}
</tr> </tr>
); );
})} })}
<tr className="border-t border-zinc-800"> <tr className="border-t border-[var(--admin-border)]">
<td className="pt-3 pr-4" /> <td className="pt-3 pr-4" />
{(["starter", "farm", "enterprise"] as const).map((tier) => { {(["starter", "farm", "enterprise"] as const).map((tier) => {
const isCurrent = tier === planTier; const isCurrent = tier === planTier;
return ( return (
<td key={tier} className="pt-3 px-3 text-center"> <td key={tier} className="pt-3 px-3 text-center">
{isCurrent ? ( {isCurrent ? (
<span className="inline-block rounded-lg bg-green-900/40 px-2 py-1 text-xs font-medium text-green-400">Current</span> <span className="inline-block rounded-lg bg-[var(--admin-success-light)] px-2 py-1 text-xs font-medium text-[var(--admin-success-accent)]">Current</span>
) : tier === "enterprise" ? ( ) : tier === "enterprise" ? (
<a href="mailto:team@cielohermosa.com?subject=Enterprise+Plan" className="inline-block rounded-lg border border-violet-200 bg-violet-50 px-2 py-1 text-xs font-medium text-violet-700 hover:bg-violet-100"> <a href="mailto:team@cielohermosa.com?subject=Enterprise+Plan" className="inline-block rounded-lg border border-[var(--admin-accent)] bg-[var(--admin-accent-light)] px-2 py-1 text-xs font-medium text-[var(--admin-accent-text)] hover:opacity-80">
Contact Contact
</a> </a>
) : ( ) : (
@@ -281,38 +281,38 @@ export default function BillingClientPage({
{/* ── 4. Payment + Invoices (two-column) ──────────────────────────────── */} {/* ── 4. Payment + Invoices (two-column) ──────────────────────────────── */}
<div className="grid gap-4 lg:grid-cols-2"> <div className="grid gap-4 lg:grid-cols-2">
{/* Payment method */} {/* Payment method */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5"> <div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5">
<h3 className="text-sm font-semibold text-zinc-100 mb-4">Payment Method</h3> <h3 className="text-sm font-semibold text-[var(--admin-text-primary)] mb-4">Payment Method</h3>
{hasStripeCustomer ? ( {hasStripeCustomer ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-3 rounded-xl border border-zinc-800 p-3.5"> <div className="flex items-center gap-3 rounded-xl border border-[var(--admin-border)] p-3.5">
<div className="flex items-center justify-center h-9 w-12 rounded-lg bg-gradient-to-br from635bff to-purple-600 shrink-0"> <div className="flex items-center justify-center h-9 w-12 rounded-lg bg-gradient-to-br from635bff to-purple-600 shrink-0">
<svg className="h-4 w-7 text-white" viewBox="0 0 32 20" fill="currentColor"> <svg className="h-4 w-7 text-white" viewBox="0 0 32 20" fill="currentColor">
<path d="M13.193 7.448c-.114-.272-.379-.358-.65-.358-.27 0-.535.09-.65.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .534-.09.648-.357.13-.28.115-.561-.115-.837-.237-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001.001-.001-.001c-.114.267-.379.357-.65.357-.27 0-.536-.09-.65-.357-.13-.277-.114-.558.115-.838.236-.286.536-.357.806-.357.271 0 .536.09.65.357l.001-.001c.115-.272.38-.358.65-.358.271 0 .536.09.65.357l.001.001c.122.28.122.56-.008.838zm8.697-.001c-.122-.276-.379-.357-.65-.357-.27 0-.535.09-.649.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .535-.09.65-.357.13-.28.114-.561-.115-.837-.238-.286-.536-.357-.807-.357-.27 0-.535.09-.65.357l-.001-.001-.001.001c-.114-.267-.379-.357-.65-.357-.27 0-.536.09-.649.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .535-.09.649-.357.13-.277.114-.558-.115-.838-.236-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001.001-.001-.001c-.114.267-.379.357-.65.357-.271 0-.536-.09-.65-.357-.114-.276-.379-.357-.65-.357-.271 0-.536.09-.649.357-.13.277-.114.558.115.838.236.286.536.357.806.357.271 0 .536-.09.65-.357.13-.277.114-.558-.115-.838-.237-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001-.001c-.114.267-.379.357-.65.357-.271 0-.536-.09-.65-.357-.13-.277-.114-.558.115-.838.236-.286.535-.357.806-.357.27 0 .535.09.65.357l.001.001c.122.28.122.56-.008.838z"/> <path d="M13.193 7.448c-.114-.272-.379-.358-.65-.358-.27 0-.535.09-.65.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .534-.09.648-.357.13-.28.115-.561-.115-.837-.237-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001.001-.001-.001c-.114.267-.379.357-.65.357-.27 0-.536-.09-.65-.357-.13-.277-.114-.558.115-.838.236-.286.536-.357.806-.357.271 0 .536.09.65.357l.001-.001c.115-.272.38-.358.65-.358.271 0 .536.09.65.357l.001.001c.122.28.122.56-.008.838zm8.697-.001c-.122-.276-.379-.357-.65-.357-.27 0-.535.09-.649.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .535-.09.65-.357.13-.28.114-.561-.115-.837-.238-.286-.536-.357-.807-.357-.27 0-.535.09-.65.357l-.001-.001-.001.001c-.114-.267-.379-.357-.65-.357-.27 0-.536.09-.649.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .535-.09.649-.357.13-.277.114-.558-.115-.838-.236-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001.001-.001-.001c-.114.267-.379.357-.65.357-.271 0-.536-.09-.65-.357-.114-.276-.379-.357-.65-.357-.271 0-.536.09-.649.357-.13.277-.114.558.115.838.236.286.536.357.806.357.271 0 .536-.09.65-.357.13-.277.114-.558-.115-.838-.237-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001-.001c-.114.267-.379.357-.65.357-.271 0-.536-.09-.65-.357-.13-.277-.114-.558.115-.838.236-.286.535.357.806-.357.27 0 .535.09.65.357l.001.001c.122.28.122.56-.008.838z"/>
</svg> </svg>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800">Visa ending in 4242</p> <p className="text-sm font-medium text-[var(--admin-text-primary)]">Visa ending in 4242</p>
<p className="text-xs text-slate-400">Expires 12/26</p> <p className="text-xs text-[var(--admin-text-muted)]">Expires 12/26</p>
</div> </div>
<span className="rounded-full bg-green-900/40 text-green-400 text-xs px-2 py-0.5 font-medium shrink-0">Active</span> <span className="rounded-full bg-[var(--admin-success-light)] text-[var(--admin-success-accent)] text-xs px-2 py-0.5 font-medium shrink-0">Active</span>
</div> </div>
<StripePortalButton brandId={brandId} variant="secondary" label="Manage in Stripe Portal →" /> <StripePortalButton brandId={brandId} variant="secondary" label="Manage in Stripe Portal →" />
<p className="text-xs text-slate-400 text-center"> <p className="text-xs text-[var(--admin-text-muted)] text-center">
Payment powered by Stripe · Invoiced by Cielo Hermosa, LLC Payment powered by Stripe · Invoiced by Cielo Hermosa, LLC
</p> </p>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
<div className="rounded-xl border border-amber-200 bg-amber-900/30 p-3.5"> <div className="rounded-xl border border-amber-200 bg-amber-50 p-3.5">
<p className="text-sm text-amber-700"> <p className="text-sm text-amber-700">
<strong>No payment method on file.</strong> Add a card to activate your subscription. <strong>No payment method on file.</strong> Add a card to activate your subscription.
</p> </p>
</div> </div>
<AddPaymentMethodButton brandId={brandId} /> <AddPaymentMethodButton brandId={brandId} />
<p className="text-xs text-slate-400 text-center"> <p className="text-xs text-[var(--admin-text-muted)] text-center">
Set up in{" "} Set up in{" "}
<a href="/admin/settings/payments" className="underline hover:text-zinc-400">Payments settings</a> <a href="/admin/settings/payments" className="underline hover:text-[var(--admin-text-primary)]">Payments settings</a>
{" "}to enable billing. {" "}to enable billing.
</p> </p>
</div> </div>
@@ -320,41 +320,41 @@ export default function BillingClientPage({
</div> </div>
{/* Invoice history */} {/* Invoice history */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5"> <div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5">
<h3 className="text-sm font-semibold text-zinc-100 mb-4">Invoice History</h3> <h3 className="text-sm font-semibold text-[var(--admin-text-primary)] mb-4">Invoice History</h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="border-b border-slate-100"> <tr className="border-b border-[var(--admin-border)]">
<th className="pb-2 text-left font-semibold text-zinc-500">Invoice</th> <th className="pb-2 text-left font-semibold text-[var(--admin-text-muted)]">Invoice</th>
<th className="pb-2 text-left font-semibold text-zinc-500">Date</th> <th className="pb-2 text-left font-semibold text-[var(--admin-text-muted)]">Date</th>
<th className="pb-2 text-right font-semibold text-zinc-500">Amount</th> <th className="pb-2 text-right font-semibold text-[var(--admin-text-muted)]">Amount</th>
<th className="pb-2 text-right font-semibold text-zinc-500">Status</th> <th className="pb-2 text-right font-semibold text-[var(--admin-text-muted)]">Status</th>
<th className="pb-2"></th> <th className="pb-2"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-50"> <tbody className="divide-y divide-[var(--admin-border)]">
{[ {[
{ id: "INV-2026-004", date: "May 1, 2026", amount: totalMonthly, status: "paid" }, { id: "INV-2026-004", date: "May 1, 2026", amount: totalMonthly, status: "paid" },
{ id: "INV-2026-003", date: "Apr 1, 2026", amount: totalMonthly, status: "paid" }, { id: "INV-2026-003", date: "Apr 1, 2026", amount: totalMonthly, status: "paid" },
{ id: "INV-2026-002", date: "Mar 1, 2026", amount: totalMonthly, status: "paid" }, { id: "INV-2026-002", date: "Mar 1, 2026", amount: totalMonthly, status: "paid" },
].map((inv) => ( ].map((inv) => (
<tr key={inv.id}> <tr key={inv.id}>
<td className="py-2 font-medium text-slate-800">{inv.id}</td> <td className="py-2 font-medium text-[var(--admin-text-primary)]">{inv.id}</td>
<td className="py-2 text-zinc-500">{inv.date}</td> <td className="py-2 text-[var(--admin-text-muted)]">{inv.date}</td>
<td className="py-2 text-right font-semibold text-slate-800">${inv.amount}</td> <td className="py-2 text-right font-semibold text-[var(--admin-text-primary)]">${inv.amount}</td>
<td className="py-2 text-right"> <td className="py-2 text-right">
<span className="rounded-full bg-green-900/40 text-green-400 px-1.5 py-0.5 text-xs font-medium capitalize">{inv.status}</span> <span className="rounded-full bg-[var(--admin-success-light)] text-[var(--admin-success-accent)] px-1.5 py-0.5 text-xs font-medium capitalize">{inv.status}</span>
</td> </td>
<td className="py-2 text-right"> <td className="py-2 text-right">
<button className="text-xs text-slate-400 hover:text-zinc-400">PDF</button> <button className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-accent)]">PDF</button>
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
<p className="mt-3 text-xs text-slate-400 text-center"> <p className="mt-3 text-xs text-[var(--admin-text-muted)] text-center">
Invoiced by Cielo Hermosa, LLC · <a href="mailto:billing@cielohermosa.com" className="underline">billing@cielohermosa.com</a> Invoiced by Cielo Hermosa, LLC · <a href="mailto:billing@cielohermosa.com" className="underline">billing@cielohermosa.com</a>
</p> </p>
</div> </div>
+19 -19
View File
@@ -27,17 +27,17 @@ export default async function BillingPage({ params }: Props) {
resolvedBrandId = firstBrand.id; resolvedBrandId = firstBrand.id;
} else { } else {
return ( return (
<main className="min-h-screen bg-stone-100 px-6 py-12"> <main className="min-h-screen bg-[var(--admin-bg)] px-6 py-12">
<div className="mx-auto max-w-6xl"> <div className="mx-auto max-w-6xl">
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-6"> <nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
<a href="/admin" className="hover:text-stone-800 transition-colors">Admin</a> <a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
<span>/</span> <span>/</span>
<span className="text-stone-800">Billing</span> <span className="text-[var(--admin-text-primary)]">Billing</span>
</nav> </nav>
<div className="card p-8 shadow-xl text-center"> <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-stone-950">No Brands Found</h1> <h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">No Brands Found</h1>
<p className="mt-2 text-stone-500">Create a brand in the database before accessing billing settings.</p> <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-blue-600 hover:bg-blue-700 px-6 py-3 text-sm font-medium text-white transition-colors"> <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 Back to Admin
</a> </a>
</div> </div>
@@ -69,31 +69,31 @@ export default async function BillingPage({ params }: Props) {
const hasStripeCustomer = !!brand?.stripe_customer_id; const hasStripeCustomer = !!brand?.stripe_customer_id;
return ( return (
<main className="min-h-screen bg-stone-100"> <main className="min-h-screen bg-[var(--admin-bg)]">
{/* Platform billing header */} {/* Platform billing header */}
<div className="bg-stone-200 border-b border-stone-300 px-6 py-3"> <div className="bg-[var(--admin-bg-subtle)] border-b border-[var(--admin-border)] px-6 py-3">
<div className="mx-auto max-w-6xl"> <div className="mx-auto max-w-6xl">
<p className="text-xs text-stone-600"> <p className="text-xs text-[var(--admin-text-muted)]">
<span className="font-medium text-stone-700">Route Commerce Platform Billing</span> <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. {" — "}Invoiced by Cielo Hermosa, LLC · Manage your platform subscription and add-ons.
{" "}Questions? <a href="mailto:billing@cielohermosa.com" className="text-blue-600 hover:text-blue-700 underline transition-colors">billing@cielohermosa.com</a> {" "}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> </p>
</div> </div>
</div> </div>
<div className="mx-auto max-w-6xl px-6 py-10"> <div className="mx-auto max-w-6xl px-6 py-10">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-6"> <nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
<a href="/admin" className="hover:text-stone-800 transition-colors">Admin</a> <a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
<span>/</span> <span>/</span>
<a href="/admin/settings" className="hover:text-stone-800 transition-colors">Settings</a> <a href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</a>
<span>/</span> <span>/</span>
<span className="text-stone-800">Billing</span> <span className="text-[var(--admin-text-primary)]">Billing</span>
</nav> </nav>
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-stone-950">Billing &amp; Subscription</h1> <h1 className="text-3xl font-bold text-[var(--admin-text-primary)]">Billing &amp; Subscription</h1>
<p className="mt-1 text-stone-500"> <p className="mt-1 text-[var(--admin-text-muted)]">
Manage your Route Commerce subscription for {brand?.name ?? "your brand"}. Manage your Route Commerce subscription for {brand?.name ?? "your brand"}.
</p> </p>
</div> </div>
+2 -67
View File
@@ -1,70 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { supabase } from "@/lib/supabase";
import { getAdminUser } from "@/lib/admin-permissions";
import { getBrandSettings } from "@/actions/brand-settings";
import BrandSettingsForm from "@/components/admin/BrandSettingsForm";
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
export default async function BrandSettingsPage() { export default function BrandSettingsRedirect() {
const adminUser = await getAdminUser(); redirect("/admin/settings#brand");
if (!adminUser) return <AdminAccessDenied />;
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
redirect("/admin");
}
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")).data ?? []
: [];
const effectiveBrandId = brandId || (brands[0]?.id ?? "");
// Get brand name for display
const brandName =
brands.find((b) => b.id === effectiveBrandId)?.name ??
(await supabase.from("brands").select("name").eq("id", effectiveBrandId).single()).data?.name ??
"Unknown Brand";
const result = await getBrandSettings(effectiveBrandId);
const settings = result.success ? result.settings : null;
return (
<main className="min-h-screen bg-stone-100 px-6 py-10">
<div className="mx-auto max-w-2xl">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-6">
<a href="/admin" className="hover:text-stone-800 transition-colors">Admin</a>
<span>/</span>
<span className="text-stone-800">Brand Settings</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 bg-emerald-100 border border-emerald-200">
<svg className="h-5 w-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" 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>
</div>
<h1 className="text-3xl font-bold text-stone-950">Brand Settings</h1>
</div>
<p className="mt-2 text-stone-500">
Company information, logos, and default signatures used across the platform.
</p>
</div>
<div className="card p-6 shadow-xl">
<BrandSettingsForm
settings={settings}
brandId={effectiveBrandId}
brandName={brandName}
brands={brands}
isPlatformAdmin={isPlatformAdmin}
/>
</div>
</div>
</main>
);
} }
+6
View File
@@ -1,6 +1,7 @@
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 { 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 = {
@@ -19,11 +20,16 @@ export default async function AdminSettingsPage() {
getBrands(), getBrands(),
]); ]);
// Fetch payment settings for the brand tab
const paymentResult = await getPaymentSettings(brandId);
const paymentSettings = paymentResult.success ? paymentResult.settings : null;
return ( return (
<SettingsClient <SettingsClient
brandId={brandId} brandId={brandId}
users={error ? [] : users} users={error ? [] : users}
brands={brands} brands={brands}
paymentSettings={paymentSettings}
currentUser={{ currentUser={{
id: adminUser.id ?? adminUser.user_id, id: adminUser.id ?? adminUser.user_id,
role: adminUser.role, role: adminUser.role,
+2 -57
View File
@@ -1,61 +1,6 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { supabase } from "@/lib/supabase";
import { getAdminUser } from "@/lib/admin-permissions";
import { getPaymentSettings } from "@/actions/payments";
import PaymentSettingsForm from "@/components/admin/PaymentSettingsForm";
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
export default async function PaymentSettingsPage() { export default async function PaymentSettingsPage() {
const adminUser = await getAdminUser(); // Payment settings are now part of the Brand tab in Settings
if (!adminUser) return <AdminAccessDenied />; redirect("/admin/settings#brand");
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")).data ?? []
: [];
const effectiveBrandId = brandId || (brands[0]?.id ?? "");
const result = await getPaymentSettings(effectiveBrandId);
const settings = result.success ? result.settings : null;
return (
<main className="min-h-screen bg-stone-100 px-6 py-10">
<div className="mx-auto max-w-2xl">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-6">
<a href="/admin" className="hover:text-stone-800 transition-colors">Admin</a>
<span>/</span>
<span className="text-stone-800">Payments</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 bg-stone-200 border border-stone-300">
<svg className="h-5 w-5 text-stone-600" 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 0h3m3.75 0h3m-3.75 0h3m-3.75 0h3m-3.75 0h3m-3.75 0h3m3.75 0h3m-3.75 0h3m-3.75 0h3" />
</svg>
</div>
<h1 className="text-3xl font-bold text-stone-950">Payment Settings</h1>
</div>
<p className="mt-2 text-stone-500">
Configure your payment provider for checkout processing.
</p>
</div>
<div className="card p-6 shadow-xl">
<PaymentSettingsForm
settings={settings}
brandId={effectiveBrandId}
brands={brands}
isPlatformAdmin={isPlatformAdmin}
/>
</div>
</div>
</main>
);
} }
-3
View File
@@ -31,9 +31,6 @@ type SettingsItem = {
const SETTINGS_ITEMS: SettingsItem[] = [ const SETTINGS_ITEMS: SettingsItem[] = [
{ href: "/admin/settings", label: "General", description: "Brand name, logo, timezone" }, { href: "/admin/settings", label: "General", description: "Brand name, logo, timezone" },
{ href: "/admin/settings/brand", label: "Brand", description: "Brand profile and preferences" },
{ href: "/admin/users", label: "Users & Permissions", description: "Team members and access roles" },
{ href: "/admin/settings/integrations", label: "Integrations", description: "Stripe, Square, Resend, Twilio" },
{ href: "/admin/settings/apps", label: "Add-ons", description: "Enable and manage feature add-ons" }, { 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/billing", label: "Billing & Plans", description: "Subscription, invoices, usage" },
{ href: "/admin/settings/payments", label: "Payments", description: "Stripe, Square, payment methods" }, { href: "/admin/settings/payments", label: "Payments", description: "Stripe, Square, payment methods" },
-3
View File
@@ -29,9 +29,6 @@ type SettingsItem = {
const SETTINGS_SUB_LINKS: SettingsItem[] = [ const SETTINGS_SUB_LINKS: SettingsItem[] = [
{ href: "/admin/settings", label: "General", description: "Brand name, logo, timezone" }, { href: "/admin/settings", label: "General", description: "Brand name, logo, timezone" },
{ href: "/admin/settings/brand", label: "Brand", description: "Brand profile and preferences" },
{ href: "/admin/users", label: "Users & Permissions", description: "Team members and access roles" },
{ href: "/admin/settings/integrations", label: "Integrations", description: "Stripe, Square, Resend, Twilio" },
{ href: "/admin/settings/apps", label: "Add-ons", description: "Enable and manage feature add-ons" }, { 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/billing", label: "Billing & Plans", description: "Subscription, invoices, usage" },
{ href: "/admin/settings/payments", label: "Payments", description: "Stripe, Square, payment methods" }, { href: "/admin/settings/payments", label: "Payments", description: "Stripe, Square, payment methods" },
+300
View File
@@ -0,0 +1,300 @@
"use client";
import { useState } from "react";
import GlassModal from "@/components/admin/GlassModal";
import { AdminInput, AdminTextInput } from "@/components/admin/design-system";
type Role = "platform_admin" | "brand_admin" | "store_employee";
type Props = {
isOpen: boolean;
onClose: () => void;
onSuccess: (user: import("@/actions/admin/users").AdminUserRow) => void;
brands: { id: string; name: string }[];
currentUser: {
role: string;
};
};
const FLAG_LABELS: Record<string, string> = {
can_manage_products: "Products",
can_manage_stops: "Stops",
can_manage_orders: "Orders",
can_manage_pickup: "Pickup",
can_manage_messages: "Messages",
can_manage_refunds: "Refunds",
can_manage_users: "Users",
can_manage_water_log: "Water Log",
can_manage_reports: "Reports",
};
const ALL_FLAGS = Object.keys(FLAG_LABELS);
const defaultFlags: Record<string, boolean> = {
can_manage_products: false,
can_manage_stops: false,
can_manage_orders: false,
can_manage_pickup: false,
can_manage_messages: false,
can_manage_refunds: false,
can_manage_users: false,
can_manage_water_log: false,
can_manage_reports: false,
};
const UserIcon = ({ className }: { 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>
);
export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, currentUser }: Props) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [displayName, setDisplayName] = useState("");
const [phoneNumber, setPhoneNumber] = useState("");
const [role, setRole] = useState<Role>("store_employee");
const [brandId, setBrandId] = useState<string | null>(null);
const [flags, setFlags] = useState<Record<string, boolean>>({ ...defaultFlags });
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const showBrandSelect = role === "brand_admin" || role === "store_employee";
const availableRoles: Role[] =
currentUser.role === "platform_admin"
? ["platform_admin", "brand_admin", "store_employee"]
: ["brand_admin", "store_employee"];
function toggleFlag(flag: string) {
setFlags((prev) => ({ ...prev, [flag]: !prev[flag] }));
}
function resetForm() {
setEmail("");
setPassword("");
setDisplayName("");
setPhoneNumber("");
setRole("store_employee");
setBrandId(null);
setFlags({ ...defaultFlags });
setError(null);
}
async function handleSubmit() {
if (!email.includes("@") || !password || password.length < 6) {
setError("Please enter a valid email and a password with at least 6 characters.");
return;
}
setSaving(true);
setError(null);
try {
const { createAdminUser } = await import("@/actions/admin/users");
const result = await createAdminUser({
email,
password,
display_name: displayName || undefined,
phone_number: phoneNumber || undefined,
role,
brand_id: brandId,
flags,
mustChangePassword: true,
});
if (result.error) {
setError(result.error);
return;
}
if (result.user) {
onSuccess(result.user);
resetForm();
onClose();
}
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "An unexpected error occurred.");
} finally {
setSaving(false);
}
}
function handleClose() {
if (!saving) {
resetForm();
onClose();
}
}
const roleDescriptions: Record<Role, string> = {
platform_admin: "Full platform access",
brand_admin: "Brand-level admin",
store_employee: "Pickup and order operations only",
};
if (!isOpen) return null;
return (
<GlassModal
title="Create User"
titleIcon={<UserIcon className="h-5 w-5 text-[var(--admin-accent)]" />}
subtitle="Add a new admin user to your organization"
onClose={handleClose}
maxWidth="max-w-lg"
>
<div className="space-y-6">
{/* Error banner */}
{error && (
<div className="flex items-start justify-between rounded-lg bg-red-50 p-4 text-sm text-red-700 gap-3 border border-red-200">
<span>{error}</span>
<button onClick={() => setError(null)} className="text-red-400 hover:text-red-600 shrink-0">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
{/* Email */}
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2.5 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20 placeholder:text-[var(--admin-text-muted)]"
placeholder="user@example.com"
/>
</div>
{/* Password */}
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Password</label>
<p className="text-xs text-[var(--admin-text-muted)] mb-1.5">Minimum 6 characters.</p>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2.5 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20 placeholder:text-[var(--admin-text-muted)]"
placeholder="Choose a password"
minLength={6}
/>
</div>
{/* Display Name */}
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Display Name</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="w-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2.5 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20 placeholder:text-[var(--admin-text-muted)]"
placeholder="Kyle Martinez"
/>
</div>
{/* Phone Number */}
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Phone Number</label>
<p className="text-xs text-[var(--admin-text-muted)] mb-1.5">Optional.</p>
<input
type="tel"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
className="w-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2.5 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20 placeholder:text-[var(--admin-text-muted)]"
placeholder="+1 (555) 000-0000"
/>
</div>
{/* Role */}
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-2">Role</label>
<div className="space-y-2">
{availableRoles.map((r) => (
<label key={r} className="flex items-center gap-3 rounded-lg border border-[var(--admin-border)] px-3 py-2.5 cursor-pointer hover:bg-[var(--admin-bg)] transition-colors">
<input
type="radio"
name="role"
value={r}
checked={role === r}
onChange={() => setRole(r)}
className="accent-[var(--admin-accent)]"
/>
<div>
<span className="font-medium text-[var(--admin-text-primary)] capitalize">{r.replace("_", " ")}</span>
<p className="text-xs text-[var(--admin-text-muted)]">{roleDescriptions[r]}</p>
</div>
</label>
))}
</div>
</div>
{/* Brand */}
{showBrandSelect && (
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Brand</label>
<select
value={brandId ?? ""}
onChange={(e) => setBrandId(e.target.value || null)}
className="w-full rounded-xl border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2.5 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20"
>
<option value="">Select a brand</option>
{brands.map((b) => (
<option key={b.id} value={b.id}>{b.name}</option>
))}
</select>
</div>
)}
{/* Permissions */}
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-2">Permissions</label>
<div className="space-y-2">
{ALL_FLAGS.map((flag) => (
<label key={flag} className="flex items-center justify-between rounded-lg border border-[var(--admin-border)] px-4 py-2.5 hover:bg-[var(--admin-bg)] cursor-pointer transition-colors">
<span className="text-sm text-[var(--admin-text-primary)]">{FLAG_LABELS[flag]}</span>
<button
type="button"
onClick={() => toggleFlag(flag)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${flags[flag] ? "bg-[var(--admin-accent)]" : "bg-stone-300"}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${flags[flag] ? "translate-x-6" : "translate-x-1"}`} />
</button>
</label>
))}
</div>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 mt-8 -mx-8 -mb-8 px-8 py-6 border-t border-[var(--admin-border)] bg-[var(--admin-bg)] rounded-b-2xl">
<button
onClick={handleClose}
disabled={saving}
className="rounded-xl border border-[var(--admin-border)] px-5 py-2.5 text-sm font-medium text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] transition-colors disabled:opacity-50"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={saving || !email.includes("@") || !password || password.length < 6}
className="rounded-xl bg-[var(--admin-accent)] px-5 py-2.5 text-sm font-medium text-white hover:bg-[var(--admin-accent-hover)] transition-colors disabled:opacity-50"
>
{saving ? (
<span className="flex items-center gap-2">
<svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Creating...
</span>
) : "Create User"}
</button>
</div>
</GlassModal>
);
}
+40 -40
View File
@@ -164,16 +164,16 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
)} )}
{error && ( {error && (
<div className="rounded-xl bg-red-900/50 border border-red-700 p-4 text-sm text-red-200">{error}</div> <div className="rounded-xl bg-red-950/50 border border-red-800 p-4 text-sm text-red-300">{error}</div>
)} )}
{saved && ( {saved && (
<div className="rounded-xl bg-green-900/50 border border-green-700 p-4 text-sm text-green-200"> <div className="rounded-xl bg-green-950/50 border border-green-800 p-4 text-sm text-green-300">
Payment settings saved. Payment settings saved.
</div> </div>
)} )}
{syncResult && ( {syncResult && (
<div className={`rounded-xl p-4 text-sm border ${ <div className={`rounded-xl p-4 text-sm border ${
syncResult.success ? "bg-green-900/50 border-green-700 text-green-200" : "bg-red-900/50 border-red-700 text-red-200" syncResult.success ? "bg-green-950/50 border-green-800 text-green-300" : "bg-red-950/50 border-red-800 text-red-300"
}`}> }`}>
{syncResult.message} {syncResult.message}
</div> </div>
@@ -181,19 +181,19 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
{/* Connected status banner */} {/* Connected status banner */}
{settings?.stripe_publishable_key && ( {settings?.stripe_publishable_key && (
<div className="flex items-center gap-3 rounded-xl border border-green-200 bg-green-900/30 px-4 py-3 text-sm"> <div className="flex items-center gap-3 rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm">
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-green-900/300 text-white text-xs font-bold"></span> <span className="flex h-8 w-8 items-center justify-center rounded-full bg-green-600 text-white text-xs font-bold"></span>
<div> <div>
<p className="font-semibold text-green-300">Stripe Connected</p> <p className="font-semibold text-green-800">Stripe Connected</p>
<p className="text-xs text-green-400/70">Payments are configured for this brand</p> <p className="text-xs text-green-600">Payments are configured for this brand</p>
</div> </div>
</div> </div>
)} )}
{/* Provider selection */} {/* Provider selection */}
<div> <div>
<label className="block text-sm font-semibold text-zinc-300">Payment Provider</label> <label className="block text-sm font-semibold text-[var(--admin-text-primary)]">Payment Provider</label>
<p className="mt-1 text-sm text-zinc-500"> <p className="mt-1 text-sm text-[var(--admin-text-muted)]">
Choose how to process payments for this brand. Choose how to process payments for this brand.
</p> </p>
<div className="mt-3 flex gap-3"> <div className="mt-3 flex gap-3">
@@ -210,10 +210,10 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
setShowStripe(opt.value === "stripe"); setShowStripe(opt.value === "stripe");
setShowSquare(opt.value === "square"); setShowSquare(opt.value === "square");
}} }}
className={`rounded-xl border px-4 py-3 text-sm font-medium ${ className={`rounded-xl border px-4 py-3 text-sm font-medium transition-colors ${
provider === opt.value provider === opt.value
? "border-zinc-400 bg-zinc-700 text-zinc-50" ? "border-[var(--admin-accent)] bg-[var(--admin-accent)] text-white"
: "border-zinc-600 text-zinc-400 hover:bg-zinc-800" : "border-[var(--admin-border)] text-[var(--admin-text-secondary)] hover:bg-stone-50"
}`} }`}
> >
{opt.label} {opt.label}
@@ -224,12 +224,12 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
{/* Stripe credentials */} {/* Stripe credentials */}
{showStripe && ( {showStripe && (
<div className="space-y-4 rounded-xl border border-blue-200 bg-blue-900/30 p-4"> <div className="space-y-4 rounded-xl border border-blue-200 bg-blue-50 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="font-semibold text-blue-900">Stripe</h3> <h3 className="font-semibold text-blue-900">Stripe</h3>
{settings?.stripe_publishable_key && ( {settings?.stripe_publishable_key && (
<span className="flex items-center gap-1.5 rounded-full bg-green-900/40 px-2.5 py-1 text-xs font-semibold text-green-400"> <span className="flex items-center gap-1.5 rounded-full bg-green-100 px-2.5 py-1 text-xs font-semibold text-green-700">
<span className="h-1.5 w-1.5 rounded-full bg-green-900/300"></span> <span className="h-1.5 w-1.5 rounded-full bg-green-600"></span>
Connected Connected
</span> </span>
)} )}
@@ -237,7 +237,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
{!settings?.stripe_publishable_key ? ( {!settings?.stripe_publishable_key ? (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-sm text-blue-200"> <p className="text-sm text-blue-800">
Connect your Stripe account to process payments. You'll be redirected to Stripe to authorize the connection. Connect your Stripe account to process payments. You'll be redirected to Stripe to authorize the connection.
</p> </p>
<a <a
@@ -249,7 +249,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-sm text-green-400"> <p className="text-sm text-green-700">
✓ Your Stripe account is connected and ready to accept payments. ✓ Your Stripe account is connected and ready to accept payments.
</p> </p>
<button <button
@@ -266,7 +266,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
window.location.reload(); window.location.reload();
} }
}} }}
className="text-sm text-red-400 hover:underline" className="text-sm text-red-600 hover:underline"
> >
Disconnect Stripe Disconnect Stripe
</button> </button>
@@ -277,11 +277,11 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
{/* Square credentials */} {/* Square credentials */}
{showSquare && ( {showSquare && (
<div className="space-y-4 rounded-xl border border-green-200 bg-green-900/30 p-4"> <div className="space-y-4 rounded-xl border border-green-200 bg-green-50 p-4">
<h3 className="font-semibold text-green-900">Square</h3> <h3 className="font-semibold text-green-900">Square</h3>
{!hasSquareToken ? ( {!hasSquareToken ? (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-sm text-green-400"> <p className="text-sm text-green-800">
Connect your Square account via OAuth to enable sync. Connect your Square account via OAuth to enable sync.
</p> </p>
<a <a
@@ -303,7 +303,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
<button <button
type="button" type="button"
onClick={handleDisconnectSquare} onClick={handleDisconnectSquare}
className="text-sm text-red-400 hover:underline" className="text-sm text-red-600 hover:underline"
> >
Disconnect Square Disconnect Square
</button> </button>
@@ -314,10 +314,10 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
{/* Square Sync section */} {/* Square Sync section */}
{hasSquareToken && provider === "square" && ( {hasSquareToken && provider === "square" && (
<div className="space-y-5 rounded-xl border border-purple-200 bg-purple-50 p-5"> <div className="space-y-5 rounded-xl border border-[var(--admin-border)] bg-[var(--admin-bg-light)] p-5">
<div> <div>
<h3 className="font-semibold text-purple-900">Square Sync</h3> <h3 className="font-semibold text-[var(--admin-text-primary)]">Square Sync</h3>
<p className="mt-1 text-sm text-purple-700"> <p className="mt-1 text-sm text-[var(--admin-text-muted)]">
Keep products, orders, and inventory in sync between Route Commerce and Square. Keep products, orders, and inventory in sync between Route Commerce and Square.
</p> </p>
</div> </div>
@@ -328,16 +328,16 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
type="button" type="button"
onClick={() => setSquareSyncEnabled(!squareSyncEnabled)} onClick={() => setSquareSyncEnabled(!squareSyncEnabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
squareSyncEnabled ? "bg-purple-600" : "bg-slate-300" squareSyncEnabled ? "bg-[var(--admin-accent)]" : "bg-stone-300"
}`} }`}
> >
<span <span
className={`inline-block h-4 w-4 transform rounded-full bg-zinc-900 transition-transform ${ className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
squareSyncEnabled ? "translate-x-6" : "translate-x-1" squareSyncEnabled ? "translate-x-6" : "translate-x-1"
}`} }`}
/> />
</button> </button>
<span className="text-sm font-medium text-purple-900"> <span className="text-sm font-medium text-[var(--admin-text-primary)]">
{squareSyncEnabled ? "Enabled" : "Disabled"} {squareSyncEnabled ? "Enabled" : "Disabled"}
</span> </span>
</div> </div>
@@ -346,7 +346,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
<> <>
{/* Inventory mode */} {/* Inventory mode */}
<div> <div>
<p className="mb-2 text-sm font-medium text-zinc-300">Inventory sync direction</p> <p className="mb-2 text-sm font-medium text-[var(--admin-text-primary)]">Inventory sync direction</p>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{[ {[
{ value: "none", label: "None" }, { value: "none", label: "None" },
@@ -360,8 +360,8 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
onClick={() => setSquareInventoryMode(opt.value as InventoryMode)} onClick={() => setSquareInventoryMode(opt.value as InventoryMode)}
className={`rounded-lg border px-3 py-2 text-xs font-medium ${ className={`rounded-lg border px-3 py-2 text-xs font-medium ${
squareInventoryMode === opt.value squareInventoryMode === opt.value
? "border-purple-600 bg-purple-600 text-white" ? "border-[var(--admin-accent)] bg-[var(--admin-accent)] text-white"
: "border-zinc-600 text-zinc-400 hover:bg-purple-100" : "border-[var(--admin-border)] text-[var(--admin-text-secondary)] hover:bg-stone-50"
}`} }`}
> >
{opt.label} {opt.label}
@@ -376,7 +376,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
type="button" type="button"
onClick={() => handleSyncNow("products")} onClick={() => handleSyncNow("products")}
disabled={syncing} disabled={syncing}
className="rounded-lg border border-purple-300 bg-zinc-900 px-4 py-2 text-sm font-medium text-purple-700 hover:bg-purple-100 disabled:opacity-50" className="rounded-lg border border-[var(--admin-border)] bg-white px-4 py-2 text-sm font-medium text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-50"
> >
{syncing ? "Syncing..." : "Sync Products Now"} {syncing ? "Syncing..." : "Sync Products Now"}
</button> </button>
@@ -384,7 +384,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
type="button" type="button"
onClick={() => handleSyncNow("orders")} onClick={() => handleSyncNow("orders")}
disabled={syncing} disabled={syncing}
className="rounded-lg border border-purple-300 bg-zinc-900 px-4 py-2 text-sm font-medium text-purple-700 hover:bg-purple-100 disabled:opacity-50" className="rounded-lg border border-[var(--admin-border)] bg-white px-4 py-2 text-sm font-medium text-[var(--admin-text-primary)] hover:bg-stone-50 disabled:opacity-50"
> >
{syncing ? "Syncing..." : "Sync Orders Now"} {syncing ? "Syncing..." : "Sync Orders Now"}
</button> </button>
@@ -392,24 +392,24 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
type="button" type="button"
onClick={() => handleSyncNow("all")} onClick={() => handleSyncNow("all")}
disabled={syncing} disabled={syncing}
className="rounded-lg bg-purple-600 px-4 py-2 text-sm font-semibold text-white hover:bg-purple-700 disabled:opacity-50" className="rounded-lg bg-[var(--admin-accent)] px-4 py-2 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-50"
> >
{syncing ? "Syncing..." : "Sync All Now"} {syncing ? "Syncing..." : "Sync All Now"}
</button> </button>
</div> </div>
{/* Last sync info */} {/* Last sync info */}
<div className="text-xs text-zinc-500"> <div className="text-xs text-[var(--admin-text-muted)]">
Last sync: {lastSyncAt} Last sync: {lastSyncAt}
{settings?.square_last_sync_error && ( {settings?.square_last_sync_error && (
<span className="ml-2 text-red-500">— {settings.square_last_sync_error}</span> <span className="ml-2 text-red-600">— {settings.square_last_sync_error}</span>
)} )}
</div> </div>
{/* Sync log preview */} {/* Sync log preview */}
{syncLog.length > 0 && ( {syncLog.length > 0 && (
<div> <div>
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500"> <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--admin-text-muted)]">
Recent sync activity Recent sync activity
</p> </p>
<div className="space-y-1"> <div className="space-y-1">
@@ -418,14 +418,14 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
key={entry.id} key={entry.id}
className={`flex items-center justify-between rounded-lg border px-3 py-2 text-xs ${ className={`flex items-center justify-between rounded-lg border px-3 py-2 text-xs ${
entry.status === "success" entry.status === "success"
? "border-green-200 bg-green-900/30 text-green-400" ? "border-green-200 bg-green-50 text-green-700"
: "border-red-200 bg-red-900/30 text-red-400" : "border-red-200 bg-red-50 text-red-700"
}`} }`}
> >
<span> <span>
[{entry.direction ?? ""}] {entry.event_type} — {entry.status} [{entry.direction ?? ""}] {entry.event_type} — {entry.status}
</span> </span>
<span className="text-slate-400"> <span className="text-[var(--admin-text-muted)]">
{new Date(entry.created_at).toLocaleTimeString()} {new Date(entry.created_at).toLocaleTimeString()}
</span> </span>
</div> </div>
@@ -444,7 +444,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
<button <button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="rounded-xl bg-zinc-100 px-6 py-3 text-sm font-bold text-zinc-900 hover:bg-zinc-200 disabled:opacity-50" className="rounded-xl bg-[var(--admin-accent)] px-6 py-3 text-sm font-bold text-white hover:opacity-90 disabled:opacity-50"
> >
{saving ? "Saving..." : "Save Payment Settings"} {saving ? "Saving..." : "Save Payment Settings"}
</button> </button>
+75 -1
View File
@@ -3,12 +3,16 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import SettingsSections from "@/components/admin/SettingsSections"; import SettingsSections from "@/components/admin/SettingsSections";
import UsersPage from "@/components/admin/UsersPage"; 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 { PageHeader, AdminButton } from "@/components/admin/design-system";
import type { PaymentProvider } from "@/actions/payments";
type Tab = "general" | "workers" | "tasks" | "users"; type Tab = "general" | "brand" | "workers" | "tasks" | "users";
const TABS: { id: Tab; label: string; icon: string; hash: string }[] = [ const TABS: { id: Tab; label: string; icon: string; hash: string }[] = [
{ id: "general", label: "General", icon: "settings", hash: "general" }, { id: "general", label: "General", icon: "settings", hash: "general" },
{ id: "brand", label: "Brand", icon: "brand", hash: "brand" },
{ id: "workers", label: "Workers", icon: "users", hash: "workers" }, { id: "workers", label: "Workers", icon: "users", hash: "workers" },
{ id: "tasks", label: "Tasks", icon: "list", hash: "tasks" }, { id: "tasks", label: "Tasks", icon: "list", hash: "tasks" },
{ id: "users", label: "Users", icon: "user-check", hash: "users" }, { id: "users", label: "Users", icon: "user-check", hash: "users" },
@@ -22,6 +26,11 @@ const Icons = {
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
</svg> </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) => ( users: (className: string) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <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"/> <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"/>
@@ -53,6 +62,17 @@ type Props = {
brandId: string; brandId: string;
users: import("@/actions/admin/users").AdminUserRow[]; users: import("@/actions/admin/users").AdminUserRow[];
brands: Brand[]; 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: { currentUser: {
id: string; id: string;
role: string; role: string;
@@ -64,6 +84,7 @@ export default function SettingsClient({
brandId, brandId,
users, users,
brands, brands,
paymentSettings,
currentUser, currentUser,
}: Props) { }: Props) {
const [activeTab, setActiveTab] = useState<Tab>("general"); const [activeTab, setActiveTab] = useState<Tab>("general");
@@ -128,6 +149,59 @@ export default function SettingsClient({
</div> </div>
)} )}
{activeTab === "brand" && (
<div className="space-y-6">
{/* 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" && ( {activeTab === "workers" && (
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden"> <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="p-4 sm:p-6 border-b border-[var(--admin-border)]">
+22 -5
View File
@@ -1,8 +1,9 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { AdminUserRow, CreateAdminUserInput, UpdateAdminUserInput } from "@/actions/admin/users"; import { AdminUserRow, UpdateAdminUserInput } from "@/actions/admin/users";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import CreateUserModal from "./CreateUserModal";
type UsersPageProps = { type UsersPageProps = {
initialUsers: AdminUserRow[]; initialUsers: AdminUserRow[];
@@ -12,6 +13,7 @@ type UsersPageProps = {
role: string; role: string;
can_manage_users?: boolean; can_manage_users?: boolean;
}; };
onUserCreated?: (user: AdminUserRow) => void;
}; };
type PasswordModal = { type PasswordModal = {
@@ -110,9 +112,10 @@ function editingFromRow(row: AdminUserRow): EditingUser {
}; };
} }
export default function UsersPage({ initialUsers, brands, currentUser }: UsersPageProps) { export default function UsersPage({ initialUsers, brands, currentUser, onUserCreated }: UsersPageProps) {
const [users, setUsers] = useState(initialUsers); const [users, setUsers] = useState(initialUsers);
const [panelOpen, setPanelOpen] = useState(false); const [panelOpen, setPanelOpen] = useState(false);
const [showCreateModal, setShowCreateModal] = useState(false);
const [editing, setEditing] = useState<EditingUser>(emptyEditing(true)); const [editing, setEditing] = useState<EditingUser>(emptyEditing(true));
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -126,9 +129,12 @@ export default function UsersPage({ initialUsers, brands, currentUser }: UsersPa
(currentUser.role === "brand_admin" && currentUser.can_manage_users); (currentUser.role === "brand_admin" && currentUser.can_manage_users);
function openCreate() { function openCreate() {
setEditing(emptyEditing(true)); setShowCreateModal(true);
setError(null); }
setPanelOpen(true);
function handleUserCreated(user: AdminUserRow) {
setUsers((prev) => [user, ...prev]);
onUserCreated?.(user);
} }
function openEdit(row: AdminUserRow) { function openEdit(row: AdminUserRow) {
@@ -655,6 +661,17 @@ export default function UsersPage({ initialUsers, brands, currentUser }: UsersPa
</div> </div>
</div> </div>
)} )}
{/* Create User Modal */}
<CreateUserModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
onSuccess={handleUserCreated}
brands={brands}
currentUser={{
role: currentUser.role,
}}
/>
</> </>
); );
} }