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:
@@ -66,7 +66,7 @@ export default function BillingClientPage({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* ── 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 gap-4">
|
||||
<div>
|
||||
@@ -76,26 +76,26 @@ export default function BillingClientPage({
|
||||
</span>
|
||||
{subscriptionStatus && (
|
||||
<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 === "canceled" ? "bg-red-900/40 text-red-400" :
|
||||
subscriptionStatus === "trialing" ? "bg-blue-900/40 text-blue-700" :
|
||||
"bg-zinc-950 text-zinc-400"
|
||||
subscriptionStatus === "canceled" ? "bg-red-100 text-red-600" :
|
||||
subscriptionStatus === "trialing" ? "bg-blue-100 text-blue-600" :
|
||||
"bg-[var(--admin-bg)] text-[var(--admin-text-muted)]"
|
||||
}`}>
|
||||
{subscriptionStatus.replace("_", " ")}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-zinc-500">
|
||||
<span className="text-sm text-[var(--admin-text-muted)]">
|
||||
{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"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-zinc-100">
|
||||
${totalMonthly}<span className="text-sm font-normal text-slate-400">/mo</span>
|
||||
<span className="ml-2 text-sm font-medium text-green-600">
|
||||
<p className="text-2xl font-bold text-[var(--admin-text-primary)]">
|
||||
${totalMonthly}<span className="text-sm font-normal text-[var(--admin-text-muted)]">/mo</span>
|
||||
<span className="ml-2 text-sm font-medium text-[var(--admin-success-accent)]">
|
||||
{billingCycle === "annual" ? "Annual (saves 25%)" : ""}
|
||||
</span>
|
||||
</p>
|
||||
@@ -108,35 +108,35 @@ export default function BillingClientPage({
|
||||
{/* ── 2. Current plan + Add-ons (two-column) ───────────────────────────── */}
|
||||
<div className="grid gap-4 lg:grid-cols-5">
|
||||
{/* 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">
|
||||
<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}`}>
|
||||
{currentPlan.label}
|
||||
</span>
|
||||
</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`}
|
||||
</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
|
||||
? `$${Math.round(currentPlan.annualPrice / 12)}/mo equivalent`
|
||||
: "billed monthly"}
|
||||
</p>
|
||||
<ul className="space-y-1 mb-4">
|
||||
{(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">
|
||||
<span className="text-green-500 shrink-0">✓</span> {f}
|
||||
<li key={i} className="flex items-center gap-2 text-xs text-[var(--admin-text-secondary)]">
|
||||
<span className="text-[var(--admin-success)] shrink-0">✓</span> {f}
|
||||
</li>
|
||||
))}
|
||||
{(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>
|
||||
{isPlatformAdmin && (
|
||||
<button
|
||||
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 →"}
|
||||
</button>
|
||||
@@ -144,10 +144,10 @@ export default function BillingClientPage({
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<h3 className="text-sm font-semibold text-zinc-100">Add-ons</h3>
|
||||
<span className="text-xs text-slate-400">+{addonsMonthlyTotal}/mo</span>
|
||||
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Add-ons</h3>
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">+{addonsMonthlyTotal}/mo</span>
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
{allAddonKeys.map((key) => {
|
||||
@@ -157,19 +157,19 @@ export default function BillingClientPage({
|
||||
? Math.round(addon.annualPrice / 12)
|
||||
: addon.monthlyPrice;
|
||||
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">
|
||||
<span className="text-base leading-none">{addon.icon}</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-800">{addon.label}</p>
|
||||
<p className="text-xs text-slate-400">{addon.description}</p>
|
||||
<p className="text-sm font-medium text-[var(--admin-text-primary)]">{addon.label}</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">{addon.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<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 ? (
|
||||
<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 && (
|
||||
<RemoveAddonButton brandId={brandId} addonKey={key} onRemoved={() => window.location.reload()} />
|
||||
)}
|
||||
@@ -187,18 +187,18 @@ export default function BillingClientPage({
|
||||
|
||||
{/* ── 3. Compare plans (collapsible) ───────────────────────────────────── */}
|
||||
{compareOpen && (
|
||||
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 overflow-hidden">
|
||||
<div className="border-b border-slate-100 bg-slate-50 px-5 py-3 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-zinc-100">Plan Comparison</h3>
|
||||
<button onClick={() => setCompareOpen(false)} className="text-xs text-slate-400 hover:text-zinc-400">
|
||||
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] overflow-hidden">
|
||||
<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-[var(--admin-text-primary)]">Plan Comparison</h3>
|
||||
<button onClick={() => setCompareOpen(false)} className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
✕ Close
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-100">
|
||||
<th className="pb-3 pr-4 text-left font-semibold text-zinc-500 w-2/5" />
|
||||
<tr className="border-b border-[var(--admin-border)]">
|
||||
<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) => {
|
||||
const plan = PLAN_TIERS[tier];
|
||||
const price = billingCycle === "annual" ? plan.annualPrice : plan.monthlyPrice;
|
||||
@@ -208,13 +208,13 @@ export default function BillingClientPage({
|
||||
{plan.label}
|
||||
</span>
|
||||
<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"}
|
||||
</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>
|
||||
{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>
|
||||
);
|
||||
@@ -241,28 +241,28 @@ export default function BillingClientPage({
|
||||
].map(([feature, tiers]) => {
|
||||
const t = tiers as Record<string, boolean>;
|
||||
return (
|
||||
<tr key={feature as string} className="border-t border-slate-100">
|
||||
<td className="py-2 pr-4 text-zinc-400">{feature as string}</td>
|
||||
<tr key={feature as string} className="border-t border-[var(--admin-border)]">
|
||||
<td className="py-2 pr-4 text-[var(--admin-text-secondary)]">{feature as string}</td>
|
||||
{(["starter", "farm", "enterprise"] as const).map((tier) => (
|
||||
<td key={tier} className="py-2 px-3 text-center">
|
||||
{t[tier]
|
||||
? <span className="text-green-500">✓</span>
|
||||
: <span className="text-slate-300">—</span>}
|
||||
? <span className="text-[var(--admin-success)]">✓</span>
|
||||
: <span className="text-[var(--admin-text-muted)]">—</span>}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr className="border-t border-zinc-800">
|
||||
<tr className="border-t border-[var(--admin-border)]">
|
||||
<td className="pt-3 pr-4" />
|
||||
{(["starter", "farm", "enterprise"] as const).map((tier) => {
|
||||
const isCurrent = tier === planTier;
|
||||
return (
|
||||
<td key={tier} className="pt-3 px-3 text-center">
|
||||
{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" ? (
|
||||
<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
|
||||
</a>
|
||||
) : (
|
||||
@@ -281,38 +281,38 @@ export default function BillingClientPage({
|
||||
{/* ── 4. Payment + Invoices (two-column) ──────────────────────────────── */}
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{/* Payment method */}
|
||||
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5">
|
||||
<h3 className="text-sm font-semibold text-zinc-100 mb-4">Payment Method</h3>
|
||||
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5">
|
||||
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)] mb-4">Payment Method</h3>
|
||||
{hasStripeCustomer ? (
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-800">Visa ending in 4242</p>
|
||||
<p className="text-xs text-slate-400">Expires 12/26</p>
|
||||
<p className="text-sm font-medium text-[var(--admin-text-primary)]">Visa ending in 4242</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">Expires 12/26</p>
|
||||
</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>
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
<strong>No payment method on file.</strong> Add a card to activate your subscription.
|
||||
</p>
|
||||
</div>
|
||||
<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{" "}
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -320,41 +320,41 @@ export default function BillingClientPage({
|
||||
</div>
|
||||
|
||||
{/* Invoice history */}
|
||||
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5">
|
||||
<h3 className="text-sm font-semibold text-zinc-100 mb-4">Invoice History</h3>
|
||||
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5">
|
||||
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)] mb-4">Invoice History</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-100">
|
||||
<th className="pb-2 text-left font-semibold text-zinc-500">Invoice</th>
|
||||
<th className="pb-2 text-left font-semibold text-zinc-500">Date</th>
|
||||
<th className="pb-2 text-right font-semibold text-zinc-500">Amount</th>
|
||||
<th className="pb-2 text-right font-semibold text-zinc-500">Status</th>
|
||||
<tr className="border-b border-[var(--admin-border)]">
|
||||
<th className="pb-2 text-left font-semibold text-[var(--admin-text-muted)]">Invoice</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-[var(--admin-text-muted)]">Amount</th>
|
||||
<th className="pb-2 text-right font-semibold text-[var(--admin-text-muted)]">Status</th>
|
||||
<th className="pb-2"></th>
|
||||
</tr>
|
||||
</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-003", date: "Apr 1, 2026", amount: totalMonthly, status: "paid" },
|
||||
{ id: "INV-2026-002", date: "Mar 1, 2026", amount: totalMonthly, status: "paid" },
|
||||
].map((inv) => (
|
||||
<tr key={inv.id}>
|
||||
<td className="py-2 font-medium text-slate-800">{inv.id}</td>
|
||||
<td className="py-2 text-zinc-500">{inv.date}</td>
|
||||
<td className="py-2 text-right font-semibold text-slate-800">${inv.amount}</td>
|
||||
<td className="py-2 font-medium text-[var(--admin-text-primary)]">{inv.id}</td>
|
||||
<td className="py-2 text-[var(--admin-text-muted)]">{inv.date}</td>
|
||||
<td className="py-2 text-right font-semibold text-[var(--admin-text-primary)]">${inv.amount}</td>
|
||||
<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 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>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -27,17 +27,17 @@ export default async function BillingPage({ params }: Props) {
|
||||
resolvedBrandId = firstBrand.id;
|
||||
} else {
|
||||
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">
|
||||
<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>
|
||||
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
|
||||
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<span className="text-stone-800">Billing</span>
|
||||
<span className="text-[var(--admin-text-primary)]">Billing</span>
|
||||
</nav>
|
||||
<div className="card p-8 shadow-xl text-center">
|
||||
<h1 className="text-2xl font-bold text-stone-950">No Brands Found</h1>
|
||||
<p className="mt-2 text-stone-500">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">
|
||||
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-8 text-center">
|
||||
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">No Brands Found</h1>
|
||||
<p className="mt-2 text-[var(--admin-text-muted)]">Create a brand in the database before accessing billing settings.</p>
|
||||
<a href="/admin" className="mt-4 inline-block rounded-xl bg-[var(--admin-accent)] hover:bg-[var(--admin-accent-hover)] px-6 py-3 text-sm font-medium text-white transition-colors">
|
||||
Back to Admin
|
||||
</a>
|
||||
</div>
|
||||
@@ -69,31 +69,31 @@ export default async function BillingPage({ params }: Props) {
|
||||
const hasStripeCustomer = !!brand?.stripe_customer_id;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-stone-100">
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
{/* 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">
|
||||
<p className="text-xs text-stone-600">
|
||||
<span className="font-medium text-stone-700">Route Commerce Platform Billing</span>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">
|
||||
<span className="font-medium text-[var(--admin-text-primary)]">Route Commerce Platform Billing</span>
|
||||
{" — "}Invoiced by Cielo Hermosa, LLC · Manage your platform subscription and add-ons.
|
||||
{" "}Questions? <a href="mailto:billing@cielohermosa.com" className="text-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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-6xl px-6 py-10">
|
||||
{/* 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>
|
||||
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
|
||||
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<a href="/admin/settings" className="hover:text-stone-800 transition-colors">Settings</a>
|
||||
<a href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</a>
|
||||
<span>/</span>
|
||||
<span className="text-stone-800">Billing</span>
|
||||
<span className="text-[var(--admin-text-primary)]">Billing</span>
|
||||
</nav>
|
||||
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-stone-950">Billing & Subscription</h1>
|
||||
<p className="mt-1 text-stone-500">
|
||||
<h1 className="text-3xl font-bold text-[var(--admin-text-primary)]">Billing & Subscription</h1>
|
||||
<p className="mt-1 text-[var(--admin-text-muted)]">
|
||||
Manage your Route Commerce subscription for {brand?.name ?? "your brand"}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,70 +1,5 @@
|
||||
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() {
|
||||
const adminUser = await getAdminUser();
|
||||
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>
|
||||
);
|
||||
}
|
||||
export default function BrandSettingsRedirect() {
|
||||
redirect("/admin/settings#brand");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAdminUsers, getBrands } from "@/actions/admin/users";
|
||||
import { getPaymentSettings } from "@/actions/payments";
|
||||
import SettingsClient from "@/components/admin/SettingsClient";
|
||||
|
||||
export const metadata = {
|
||||
@@ -19,11 +20,16 @@ export default async function AdminSettingsPage() {
|
||||
getBrands(),
|
||||
]);
|
||||
|
||||
// Fetch payment settings for the brand tab
|
||||
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,
|
||||
|
||||
@@ -1,61 +1,6 @@
|
||||
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() {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
if (!adminUser.can_manage_orders || adminUser.role === "store_employee") redirect("/admin/pickup");
|
||||
|
||||
const isPlatformAdmin = adminUser.role === "platform_admin";
|
||||
const brandId = adminUser.brand_id ?? "";
|
||||
|
||||
// Platform admins: fetch all brands for the picker
|
||||
const brands = isPlatformAdmin
|
||||
? (await supabase.from("brands").select("id, name").order("name")).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>
|
||||
);
|
||||
}
|
||||
// Payment settings are now part of the Brand tab in Settings
|
||||
redirect("/admin/settings#brand");
|
||||
}
|
||||
@@ -31,9 +31,6 @@ type SettingsItem = {
|
||||
|
||||
const SETTINGS_ITEMS: SettingsItem[] = [
|
||||
{ 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/billing", label: "Billing & Plans", description: "Subscription, invoices, usage" },
|
||||
{ href: "/admin/settings/payments", label: "Payments", description: "Stripe, Square, payment methods" },
|
||||
|
||||
@@ -29,9 +29,6 @@ type SettingsItem = {
|
||||
|
||||
const SETTINGS_SUB_LINKS: SettingsItem[] = [
|
||||
{ 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/billing", label: "Billing & Plans", description: "Subscription, invoices, usage" },
|
||||
{ href: "/admin/settings/payments", label: "Payments", description: "Stripe, Square, payment methods" },
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -164,16 +164,16 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
)}
|
||||
|
||||
{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 && (
|
||||
<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.
|
||||
</div>
|
||||
)}
|
||||
{syncResult && (
|
||||
<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}
|
||||
</div>
|
||||
@@ -181,19 +181,19 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
|
||||
{/* Connected status banner */}
|
||||
{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">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-green-900/300 text-white text-xs font-bold">✓</span>
|
||||
<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-600 text-white text-xs font-bold">✓</span>
|
||||
<div>
|
||||
<p className="font-semibold text-green-300">Stripe Connected</p>
|
||||
<p className="text-xs text-green-400/70">Payments are configured for this brand</p>
|
||||
<p className="font-semibold text-green-800">Stripe Connected</p>
|
||||
<p className="text-xs text-green-600">Payments are configured for this brand</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Provider selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-zinc-300">Payment Provider</label>
|
||||
<p className="mt-1 text-sm text-zinc-500">
|
||||
<label className="block text-sm font-semibold text-[var(--admin-text-primary)]">Payment Provider</label>
|
||||
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
|
||||
Choose how to process payments for this brand.
|
||||
</p>
|
||||
<div className="mt-3 flex gap-3">
|
||||
@@ -210,10 +210,10 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
setShowStripe(opt.value === "stripe");
|
||||
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
|
||||
? "border-zinc-400 bg-zinc-700 text-zinc-50"
|
||||
: "border-zinc-600 text-zinc-400 hover:bg-zinc-800"
|
||||
? "border-[var(--admin-accent)] bg-[var(--admin-accent)] text-white"
|
||||
: "border-[var(--admin-border)] text-[var(--admin-text-secondary)] hover:bg-stone-50"
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
@@ -224,12 +224,12 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
|
||||
{/* Stripe credentials */}
|
||||
{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">
|
||||
<h3 className="font-semibold text-blue-900">Stripe</h3>
|
||||
{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="h-1.5 w-1.5 rounded-full bg-green-900/300"></span>
|
||||
<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-600"></span>
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
@@ -237,7 +237,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
|
||||
{!settings?.stripe_publishable_key ? (
|
||||
<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.
|
||||
</p>
|
||||
<a
|
||||
@@ -249,7 +249,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
</div>
|
||||
) : (
|
||||
<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.
|
||||
</p>
|
||||
<button
|
||||
@@ -266,7 +266,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
className="text-sm text-red-400 hover:underline"
|
||||
className="text-sm text-red-600 hover:underline"
|
||||
>
|
||||
Disconnect Stripe
|
||||
</button>
|
||||
@@ -277,11 +277,11 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
|
||||
{/* Square credentials */}
|
||||
{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>
|
||||
{!hasSquareToken ? (
|
||||
<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.
|
||||
</p>
|
||||
<a
|
||||
@@ -303,7 +303,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDisconnectSquare}
|
||||
className="text-sm text-red-400 hover:underline"
|
||||
className="text-sm text-red-600 hover:underline"
|
||||
>
|
||||
Disconnect Square
|
||||
</button>
|
||||
@@ -314,10 +314,10 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
|
||||
{/* Square Sync section */}
|
||||
{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>
|
||||
<h3 className="font-semibold text-purple-900">Square Sync</h3>
|
||||
<p className="mt-1 text-sm text-purple-700">
|
||||
<h3 className="font-semibold text-[var(--admin-text-primary)]">Square Sync</h3>
|
||||
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
|
||||
Keep products, orders, and inventory in sync between Route Commerce and Square.
|
||||
</p>
|
||||
</div>
|
||||
@@ -328,16 +328,16 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
type="button"
|
||||
onClick={() => setSquareSyncEnabled(!squareSyncEnabled)}
|
||||
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
|
||||
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"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-sm font-medium text-purple-900">
|
||||
<span className="text-sm font-medium text-[var(--admin-text-primary)]">
|
||||
{squareSyncEnabled ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
</div>
|
||||
@@ -346,7 +346,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
<>
|
||||
{/* Inventory mode */}
|
||||
<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">
|
||||
{[
|
||||
{ value: "none", label: "None" },
|
||||
@@ -360,8 +360,8 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
onClick={() => setSquareInventoryMode(opt.value as InventoryMode)}
|
||||
className={`rounded-lg border px-3 py-2 text-xs font-medium ${
|
||||
squareInventoryMode === opt.value
|
||||
? "border-purple-600 bg-purple-600 text-white"
|
||||
: "border-zinc-600 text-zinc-400 hover:bg-purple-100"
|
||||
? "border-[var(--admin-accent)] bg-[var(--admin-accent)] text-white"
|
||||
: "border-[var(--admin-border)] text-[var(--admin-text-secondary)] hover:bg-stone-50"
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
@@ -376,7 +376,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
type="button"
|
||||
onClick={() => handleSyncNow("products")}
|
||||
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"}
|
||||
</button>
|
||||
@@ -384,7 +384,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
type="button"
|
||||
onClick={() => handleSyncNow("orders")}
|
||||
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"}
|
||||
</button>
|
||||
@@ -392,24 +392,24 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
type="button"
|
||||
onClick={() => handleSyncNow("all")}
|
||||
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"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Last sync info */}
|
||||
<div className="text-xs text-zinc-500">
|
||||
<div className="text-xs text-[var(--admin-text-muted)]">
|
||||
Last sync: {lastSyncAt}
|
||||
{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>
|
||||
|
||||
{/* Sync log preview */}
|
||||
{syncLog.length > 0 && (
|
||||
<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
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
@@ -418,14 +418,14 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
key={entry.id}
|
||||
className={`flex items-center justify-between rounded-lg border px-3 py-2 text-xs ${
|
||||
entry.status === "success"
|
||||
? "border-green-200 bg-green-900/30 text-green-400"
|
||||
: "border-red-200 bg-red-900/30 text-red-400"
|
||||
? "border-green-200 bg-green-50 text-green-700"
|
||||
: "border-red-200 bg-red-50 text-red-700"
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
[{entry.direction ?? "—"}] {entry.event_type} — {entry.status}
|
||||
</span>
|
||||
<span className="text-slate-400">
|
||||
<span className="text-[var(--admin-text-muted)]">
|
||||
{new Date(entry.created_at).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -444,7 +444,7 @@ export default function PaymentSettingsForm({ settings, brandId, brands = [], is
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import SettingsSections from "@/components/admin/SettingsSections";
|
||||
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" | "workers" | "tasks" | "users";
|
||||
type Tab = "general" | "brand" | "workers" | "tasks" | "users";
|
||||
|
||||
const TABS: { id: Tab; label: string; icon: string; hash: string }[] = [
|
||||
{ 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: "tasks", label: "Tasks", icon: "list", hash: "tasks" },
|
||||
{ id: "users", label: "Users", icon: "user-check", hash: "users" },
|
||||
@@ -22,6 +26,11 @@ const Icons = {
|
||||
<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"/>
|
||||
@@ -53,6 +62,17 @@ type Props = {
|
||||
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;
|
||||
@@ -64,6 +84,7 @@ export default function SettingsClient({
|
||||
brandId,
|
||||
users,
|
||||
brands,
|
||||
paymentSettings,
|
||||
currentUser,
|
||||
}: Props) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("general");
|
||||
@@ -128,6 +149,59 @@ export default function SettingsClient({
|
||||
</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" && (
|
||||
<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)]">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
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 CreateUserModal from "./CreateUserModal";
|
||||
|
||||
type UsersPageProps = {
|
||||
initialUsers: AdminUserRow[];
|
||||
@@ -12,6 +13,7 @@ type UsersPageProps = {
|
||||
role: string;
|
||||
can_manage_users?: boolean;
|
||||
};
|
||||
onUserCreated?: (user: AdminUserRow) => void;
|
||||
};
|
||||
|
||||
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 [panelOpen, setPanelOpen] = useState(false);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editing, setEditing] = useState<EditingUser>(emptyEditing(true));
|
||||
const [saving, setSaving] = useState(false);
|
||||
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);
|
||||
|
||||
function openCreate() {
|
||||
setEditing(emptyEditing(true));
|
||||
setError(null);
|
||||
setPanelOpen(true);
|
||||
setShowCreateModal(true);
|
||||
}
|
||||
|
||||
function handleUserCreated(user: AdminUserRow) {
|
||||
setUsers((prev) => [user, ...prev]);
|
||||
onUserCreated?.(user);
|
||||
}
|
||||
|
||||
function openEdit(row: AdminUserRow) {
|
||||
@@ -655,6 +661,17 @@ export default function UsersPage({ initialUsers, brands, currentUser }: UsersPa
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create User Modal */}
|
||||
<CreateUserModal
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onSuccess={handleUserCreated}
|
||||
brands={brands}
|
||||
currentUser={{
|
||||
role: currentUser.role,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user