Files
resolutionflow/frontend/src/pages/AccountSettingsPage.tsx
chihlasm a71f082e25 feat: extract admin account management rework from PR 124 (#138)
* feat: reorganize admin panel around accounts

* feat: expand admin customer account controls

* feat: add admin account detail management

* fix: remove unused admin account icon import

* refactor: design critique fixes for account pages

- Admin accounts: replace dense card grid with compact DataTable
- Account settings: remove redundant hero card, stat grid, header pills
- Fix bg-accent (orange) misuse on decorative elements across 7 files
- Add ConfirmButton for destructive actions (deactivate, remove member)
- Replace single-field modals with inline editing (plan, trial)
- Add contextual help: display code tooltip, improved empty states
- Non-owner aside explanation for hidden owner-only sections
- Admin sidebar: group 11 items into 5 labeled sections
- Rename UsersPage.tsx → AccountsPage.tsx to match route
- Fix border radius consistency, hide zero-count badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use get_admin_db for all new admin account endpoints

All admin endpoints query across tenants without a tenant context.
get_db (app-role, subject to RLS) was never imported and would crash
at runtime — replace all 6 occurrences with get_admin_db (BYPASSRLS).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 04:44:51 -04:00

857 lines
36 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import {
AlertCircle,
AlertTriangle,
ArrowRight,
Building2,
Check,
Clock,
Copy,
Crown,
FolderTree,
Loader2,
Mail,
MessageSquareText,
Palette,
Plug,
RefreshCw,
Server,
Settings,
ShieldCheck,
UserCog,
Users,
X,
} from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { accountsApi } from '@/api/accounts'
import type { Account, AccountInvite, AccountMember } from '@/types'
import { TransferOwnershipModal } from '@/components/account/TransferOwnershipModal'
import { LeaveAccountModal } from '@/components/account/LeaveAccountModal'
import { DeleteAccountModal } from '@/components/account/DeleteAccountModal'
import { Button } from '@/components/ui/Button'
import { ConfirmButton } from '@/components/common/ConfirmButton'
import { Spinner } from '@/components/common/Spinner'
import { cn } from '@/lib/utils'
import { usePermissions } from '@/hooks/usePermissions'
import { useSubscription } from '@/hooks/useSubscription'
import { useAuthStore } from '@/store/authStore'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { CheckoutButton } from '@/components/subscription/CheckoutButton'
import { toast } from '@/lib/toast'
interface SettingsLinkCardProps {
to: string
icon: React.ReactNode
title: string
description: string
badge?: string
}
function SettingsLinkCard({ to, icon, title, description, badge }: SettingsLinkCardProps) {
return (
<Link
to={to}
className="group rounded-2xl border border-border bg-card p-5 transition-colors hover:border-border-hover"
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3">
<div className="rounded-xl bg-elevated p-2 text-muted-foreground">{icon}</div>
<div>
<div className="flex items-center gap-2">
<h3 className="text-base font-semibold text-foreground">{title}</h3>
{badge && (
<span className="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
{badge}
</span>
)}
</div>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
</div>
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:text-foreground" />
</div>
</Link>
)
}
function UsageStat({
label,
current,
max,
}: {
label: string
current: number
max: number | null
}) {
const isUnlimited = max === null
const percentage = isUnlimited ? 0 : Math.min((current / max) * 100, 100)
const isNearLimit = !isUnlimited && percentage >= 80
const isAtLimit = !isUnlimited && current >= max
return (
<div className="rounded-xl border border-border bg-card/50 p-4">
<p className="text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground">{label}</p>
<p
className={cn(
'mt-2 text-xl font-semibold',
isAtLimit ? 'text-danger' : isNearLimit ? 'text-warning' : 'text-foreground'
)}
>
{current}
<span className="ml-1 text-sm font-normal text-muted-foreground">
/ {isUnlimited ? 'Unlimited' : max}
</span>
</p>
{!isUnlimited && (
<div className="mt-3 h-2 overflow-hidden rounded-full bg-muted">
<div
className={cn(
'h-full rounded-full transition-all',
isAtLimit ? 'bg-danger' : isNearLimit ? 'bg-warning' : 'bg-primary'
)}
style={{ width: `${percentage}%` }}
/>
</div>
)}
</div>
)
}
function formatShortDate(value: string | null | undefined) {
if (!value) return 'Never'
return new Date(value).toLocaleDateString()
}
export function AccountSettingsPage() {
const { isAccountOwner } = usePermissions()
const { plan, limits, usage } = useSubscription()
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
const subscription = useAuthStore((s) => s.subscription)
const user = useAuthStore((s) => s.user)
const refreshUser = useAuthStore((s) => s.fetchUser)
const [account, setAccount] = useState<Account | null>(null)
const [members, setMembers] = useState<AccountMember[]>([])
const [invites, setInvites] = useState<AccountInvite[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [copiedCode, setCopiedCode] = useState(false)
const [isEditingName, setIsEditingName] = useState(false)
const [editedName, setEditedName] = useState('')
const [isSavingName, setIsSavingName] = useState(false)
const [showTransferModal, setShowTransferModal] = useState(false)
const [showLeaveModal, setShowLeaveModal] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [inviteEmail, setInviteEmail] = useState('')
const [inviteRole, setInviteRole] = useState('engineer')
const [isInviting, setIsInviting] = useState(false)
const [resendingId, setResendingId] = useState<string | null>(null)
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
setIsLoading(true)
setError(null)
try {
const accountData = await accountsApi.getMyAccount()
setAccount(accountData)
setEditedName(accountData.name)
if (isAccountOwner) {
const [membersData, invitesData] = await Promise.all([
accountsApi.getMembers(),
accountsApi.getInvites(),
])
setMembers(membersData)
setInvites(invitesData)
}
} catch (err) {
setError('Failed to load account information')
console.error(err)
} finally {
setIsLoading(false)
}
}
const pendingInvites = useMemo(() => invites.filter((invite) => !invite.used_at), [invites])
const ownerMember = useMemo(() => members.find((member) => member.account_role === 'owner') ?? null, [members])
const handleCopyDisplayCode = async () => {
if (!account?.display_code) return
await navigator.clipboard.writeText(account.display_code)
setCopiedCode(true)
toast.success('Display code copied')
setTimeout(() => setCopiedCode(false), 2000)
}
const handleSaveName = async () => {
if (!editedName.trim() || editedName === account?.name) {
setIsEditingName(false)
return
}
setIsSavingName(true)
try {
const updated = await accountsApi.updateMyAccount({ name: editedName.trim() })
setAccount(updated)
setIsEditingName(false)
toast.success('Account name updated')
await refreshUser()
} catch (err) {
toast.error('Failed to update account name')
console.error('Failed to update account name:', err)
} finally {
setIsSavingName(false)
}
}
const handleInvite = async (e: React.FormEvent) => {
e.preventDefault()
if (!inviteEmail.trim()) return
setIsInviting(true)
try {
await accountsApi.createInvite({ email: inviteEmail.trim(), role: inviteRole })
toast.success(`Invitation sent to ${inviteEmail}`)
setInviteEmail('')
const invitesData = await accountsApi.getInvites()
setInvites(invitesData)
} catch (err) {
toast.error('Failed to send invitation')
console.error(err)
} finally {
setIsInviting(false)
}
}
const handleResendInvite = async (inviteId: string) => {
setResendingId(inviteId)
try {
await accountsApi.resendInvite(inviteId)
toast.success('Invite resent with a new code')
const invitesData = await accountsApi.getInvites()
setInvites(invitesData)
} catch {
toast.error('Failed to resend invite')
} finally {
setResendingId(null)
}
}
const handleRemoveMember = async (userId: string) => {
try {
await accountsApi.removeMember(userId)
setMembers((current) => current.filter((member) => member.id !== userId))
toast.success('Member removed')
await refreshUser()
} catch (err) {
toast.error('Failed to remove member')
console.error('Failed to remove member:', err)
}
}
if (isLoading) {
return (
<div className="flex justify-center py-12">
<Spinner />
</div>
)
}
if (error) {
return (
<div className="rounded-md border border-danger/20 bg-danger-dim p-4 text-danger">
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
{error}
</div>
</div>
)
}
const sub = subscription?.subscription
return (
<>
<PageMeta title="Account Settings" />
<div className="space-y-8">
<div>
<h1 className="text-2xl font-bold font-heading text-foreground">Account Management</h1>
<p className="mt-1 text-muted-foreground">
Manage your account identity, billing, access, and workspace settings.
</p>
</div>
<div className="grid gap-6 xl:grid-cols-[1.25fr_0.95fr]">
<section className="space-y-6">
<div className="rounded-2xl border border-border bg-card p-5 sm:p-6">
<div className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Account Identity</h2>
</div>
<div className="mt-5 space-y-5">
<div>
<label className="block text-sm font-medium text-foreground">Account Name</label>
{isEditingName ? (
<div className="mt-2 flex items-center gap-2">
<input
type="text"
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className={cn(
'flex-1 rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveName()
if (e.key === 'Escape') {
setEditedName(account?.name ?? '')
setIsEditingName(false)
}
}}
/>
<Button onClick={handleSaveName} loading={isSavingName} size="icon-sm">
<Check className="h-4 w-4" />
</Button>
<Button
variant="secondary"
size="icon-sm"
onClick={() => {
setEditedName(account?.name ?? '')
setIsEditingName(false)
}}
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<div className="mt-2 flex items-center gap-2">
<span className="text-sm text-foreground">{account?.name}</span>
{isAccountOwner && (
<button
onClick={() => setIsEditingName(true)}
className="rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
Edit
</button>
)}
</div>
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-foreground">Display Code</label>
<div className="mt-2 flex items-center gap-2">
<code className="rounded-md border border-border bg-card/50 px-3 py-2 font-mono text-sm text-foreground">
{account?.display_code}
</code>
<Button variant="secondary" size="icon-sm" onClick={handleCopyDisplayCode} title="Copy display code">
{copiedCode ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
<p className="mt-2 text-xs text-muted-foreground">
Share this code with teammates so they can join your account during registration.
</p>
</div>
<div>
<label className="block text-sm font-medium text-foreground">Account Owner</label>
<div className="mt-2 text-sm text-foreground">
{ownerMember ? ownerMember.name : user?.account_role === 'owner' ? user.email : 'Owner unavailable'}
</div>
<p className="mt-2 text-xs text-muted-foreground">
{ownerMember?.email ?? 'The owner manages billing, branding, integrations, and membership changes.'}
</p>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-foreground">Created</label>
<p className="mt-2 text-sm text-muted-foreground">{formatShortDate(account?.created_at)}</p>
</div>
<div>
<label className="block text-sm font-medium text-foreground">Last updated</label>
<p className="mt-2 text-sm text-muted-foreground">{formatShortDate(account?.updated_at)}</p>
</div>
</div>
</div>
</div>
<div className="rounded-2xl border border-border bg-card p-5 sm:p-6">
<div className="flex items-center justify-between gap-3">
<div>
<div className="flex items-center gap-2">
<Crown className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Billing & Usage</h2>
</div>
<p className="mt-1 text-sm text-muted-foreground">
Monitor plan status, renewal timing, and current account limits.
</p>
</div>
</div>
<div className="mt-5 flex flex-wrap items-center gap-3">
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium',
plan === 'free' && 'bg-muted text-muted-foreground',
plan === 'pro' && 'bg-accent-dim text-accent-text',
plan === 'team' && 'bg-accent-dim text-accent-text'
)}
>
<Crown className="h-3.5 w-3.5" />
{plan.charAt(0).toUpperCase() + plan.slice(1)} Plan
</span>
{sub && (
<span
className={cn(
'inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium',
sub.status === 'active' && 'bg-success-dim text-success',
sub.status === 'trialing' && 'bg-info-dim text-info',
sub.status === 'past_due' && 'bg-warning-dim text-warning',
sub.status === 'canceled' && 'bg-danger-dim text-danger',
sub.status === 'orphaned' && 'bg-muted text-muted-foreground'
)}
>
{sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
</span>
)}
</div>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
<div className="rounded-xl border border-border bg-card/50 p-4">
<p className="text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground">Renewal date</p>
<p className="mt-2 text-sm text-foreground">
{sub?.current_period_end ? new Date(sub.current_period_end).toLocaleDateString() : 'Not scheduled'}
</p>
</div>
<div className="rounded-xl border border-border bg-card/50 p-4">
<p className="text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground">Branding access</p>
<p className="mt-2 text-sm text-foreground">
{limits?.custom_branding ? 'Included in your plan' : 'Upgrade required'}
</p>
</div>
</div>
{limits && usage && (
<div className="mt-5 grid gap-3 sm:grid-cols-3">
<UsageStat label="Flows" current={usage.tree_count} max={limits.max_trees} />
<UsageStat label="Sessions / month" current={usage.session_count_this_month} max={limits.max_sessions_per_month} />
<UsageStat label="Seats" current={usage.user_count} max={limits.max_users} />
</div>
)}
{plan === 'free' && (
<div className="mt-5 flex flex-wrap gap-3">
<CheckoutButton plan="pro" />
<CheckoutButton plan="team" />
</div>
)}
{plan === 'pro' && (
<div className="mt-5">
<CheckoutButton plan="team" />
</div>
)}
</div>
<div className="rounded-2xl border border-border bg-card p-5 sm:p-6">
<div className="flex items-center gap-2">
<ShieldCheck className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Access & Security</h2>
</div>
<div className="mt-5 grid gap-4 md:grid-cols-2">
<div className="rounded-xl border border-border bg-card/50 p-4">
<p className="text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground">Authentication</p>
<p className="mt-2 text-sm text-foreground">
{plan === 'team' ? 'Password auth available, SSO can be enabled.' : 'Password-based authentication is active.'}
</p>
<p className="mt-2 text-xs text-muted-foreground">
Use profile settings to update your personal details and sign-in information.
</p>
</div>
<div className="rounded-xl border border-border bg-card/50 p-4">
<p className="text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground">Single Sign-On</p>
<p className="mt-2 text-sm text-foreground">
{plan === 'team' ? 'Enterprise-ready setup available.' : 'Available on higher-tier account setups.'}
</p>
<p className="mt-2 text-xs text-muted-foreground">
Contact support to configure SAML or OIDC for your organization.
</p>
</div>
</div>
{isAccountOwner && (
<div className="mt-5 rounded-xl border border-border bg-card/50 p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-medium text-foreground">Need enterprise security controls?</p>
<p className="text-sm text-muted-foreground">
We can help enable SSO and align account security for larger teams.
</p>
</div>
<a
href="mailto:support@resolutionflow.com?subject=SSO%20Setup%20Request"
className={cn(
'inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium',
'bg-muted border border-border text-foreground transition-colors hover:border-border-hover'
)}
>
Contact Us
</a>
</div>
</div>
)}
</div>
</section>
<aside className="space-y-6">
{!isAccountOwner && (
<div className="rounded-2xl border border-border bg-card/50 p-5">
<p className="text-sm text-muted-foreground">
Membership, invites, branding, and integrations are managed by the account owner. Contact your admin to make changes.
</p>
</div>
)}
{isAccountOwner && (
<div className="rounded-2xl border border-border bg-card p-5 sm:p-6">
<div className="flex items-center gap-2">
<Users className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Membership</h2>
</div>
{members.length === 0 ? (
<p className="mt-4 text-sm text-muted-foreground">No team members yet. Use the invite form below to add your first teammate.</p>
) : (
<div className="mt-4 space-y-3">
{members.map((member) => (
<div key={member.id} className="rounded-xl border border-border bg-card/50 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-foreground">{member.name}</p>
<p className="text-xs text-muted-foreground">{member.email}</p>
<p className="mt-2 text-xs text-muted-foreground">
Last login: {formatShortDate(member.last_login)}
</p>
</div>
<div className="flex items-center gap-2">
{!member.is_active && (
<span className="rounded-full bg-danger-dim px-2 py-0.5 text-xs text-danger">
Inactive
</span>
)}
{member.account_role === 'owner' ? (
<span className="rounded-full bg-accent-dim px-2.5 py-0.5 text-xs font-medium text-accent-text">
owner
</span>
) : (
<select
value={member.account_role}
aria-label={`Role for ${member.name}`}
onChange={async (e) => {
try {
const updated = await accountsApi.updateMemberRole(member.id, e.target.value)
setMembers((current) =>
current.map((entry) =>
entry.id === member.id ? { ...entry, account_role: updated.account_role } : entry
)
)
toast.success(`Role updated to ${updated.account_role}`)
} catch {
toast.error('Failed to update role')
}
}}
className={cn(
'rounded-md border border-border bg-card px-2 py-0.5 text-xs',
'text-foreground focus:border-primary focus:outline-hidden'
)}
>
<option value="engineer">engineer</option>
<option value="viewer">viewer</option>
</select>
)}
{member.account_role !== 'owner' && (
<ConfirmButton
onConfirm={() => handleRemoveMember(member.id)}
confirmLabel="Remove?"
className="p-1 text-muted-foreground hover:text-danger"
confirmClassName="rounded px-1.5 py-0.5 text-xs font-medium text-danger bg-danger-dim"
aria-label={`Remove ${member.name}`}
>
<X className="h-4 w-4" />
</ConfirmButton>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{isAccountOwner && (
<div className="rounded-2xl border border-border bg-card p-5 sm:p-6">
<div className="flex items-center gap-2">
<Mail className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Invites</h2>
</div>
<form onSubmit={handleInvite} className="mt-4 space-y-3">
<input
type="email"
placeholder="Email address"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
required
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
/>
<div className="flex gap-3">
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
className={cn(
'min-w-[140px] rounded-md border border-border bg-card px-3 py-2',
'text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
>
<option value="engineer">Engineer</option>
<option value="viewer">Viewer</option>
</select>
<Button type="submit" disabled={!inviteEmail.trim()} loading={isInviting} className="flex-1">
{isInviting ? 'Sending...' : 'Send Invite'}
</Button>
</div>
</form>
{pendingInvites.length > 0 ? (
<div className="mt-5 space-y-3">
{pendingInvites.map((invite) => (
<div key={invite.id} className="rounded-xl border border-border bg-card/50 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-foreground">{invite.email}</p>
<p className="mt-1 text-xs text-muted-foreground">
{invite.expires_at
? `Expires ${new Date(invite.expires_at).toLocaleDateString()}`
: 'No expiration'}
</p>
</div>
<div className="flex items-center gap-2">
<span className="rounded-full bg-muted px-2.5 py-0.5 text-xs text-muted-foreground">
{invite.role}
</span>
<button
onClick={() => handleResendInvite(invite.id)}
disabled={resendingId === invite.id}
className="p-1 text-muted-foreground hover:text-foreground disabled:opacity-50"
aria-label={`Resend invite to ${invite.email}`}
>
{resendingId === invite.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</button>
</div>
</div>
</div>
))}
</div>
) : (
<p className="mt-4 text-sm text-muted-foreground">No pending invites. Use the form above to invite teammates by email.</p>
)}
</div>
)}
<div className="rounded-2xl border border-border bg-card p-5 sm:p-6">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Preferences</h2>
</div>
<div className="mt-4">
<label htmlFor="export-format" className="block text-sm font-medium text-foreground">
Default Export Format
</label>
<p className="text-sm text-muted-foreground">
This format will be pre-selected when exporting sessions.
</p>
<select
id="export-format"
value={defaultExportFormat}
onChange={(e) => {
setDefaultExportFormat(e.target.value as 'markdown' | 'text' | 'html')
toast.success('Preference saved')
}}
className={cn(
'mt-2 block w-full rounded-md border border-border bg-card px-3 py-2',
'text-sm text-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
>
<option value="markdown">Markdown (.md)</option>
<option value="text">Plain Text (.txt)</option>
<option value="html">HTML (.html)</option>
</select>
</div>
</div>
<div className="rounded-2xl border border-danger/20 p-5 sm:p-6">
<div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-danger" />
<h2 className="text-lg font-semibold text-foreground">Danger Zone</h2>
</div>
<div className="mt-4 space-y-3">
{isAccountOwner ? (
<>
<div className="flex items-center justify-between gap-3 rounded-xl border border-border bg-card/40 p-4">
<div>
<p className="text-sm font-medium text-foreground">Transfer Ownership</p>
<p className="text-xs text-muted-foreground">Make another member the account owner.</p>
</div>
<Button
variant="secondary"
size="sm"
onClick={() => setShowTransferModal(true)}
className="border-warning/30 text-warning hover:bg-warning-dim"
>
Transfer
</Button>
</div>
<div className="flex items-center justify-between gap-3 rounded-xl border border-border bg-card/40 p-4">
<div>
<p className="text-sm font-medium text-foreground">Delete Account</p>
<p className="text-xs text-muted-foreground">Permanently delete your account and all data.</p>
</div>
<Button variant="destructive" size="sm" onClick={() => setShowDeleteModal(true)}>
Delete
</Button>
</div>
</>
) : (
<div className="flex items-center justify-between gap-3 rounded-xl border border-border bg-card/40 p-4">
<div>
<p className="text-sm font-medium text-foreground">Leave Account</p>
<p className="text-xs text-muted-foreground">Leave this account and create a personal one.</p>
</div>
<Button variant="destructive" size="sm" onClick={() => setShowLeaveModal(true)}>
Leave
</Button>
</div>
)}
</div>
</div>
</aside>
</div>
<section className="space-y-4">
<div>
<h2 className="text-lg font-semibold text-foreground">Settings Areas</h2>
<p className="text-sm text-muted-foreground">
Common account management actions, organized the way most SaaS teams expect to find them.
</p>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<SettingsLinkCard
to="/account/profile"
icon={<UserCog className="h-5 w-5" />}
title="Profile Settings"
description="Update your name, email, and personal details."
/>
{isAccountOwner && (
<SettingsLinkCard
to="/account/branding"
icon={<Palette className="h-5 w-5" />}
title="Branding"
description="Customize logo, accent color, and company name."
badge={limits?.custom_branding ? 'Included' : 'Plan gated'}
/>
)}
{isAccountOwner && (
<SettingsLinkCard
to="/account/integrations"
icon={<Plug className="h-5 w-5" />}
title="Integrations"
description="Connect PSA and other external systems for your team."
/>
)}
{isAccountOwner && (
<SettingsLinkCard
to="/account/chat-retention"
icon={<Clock className="h-5 w-5" />}
title="Chat Retention"
description="Control conversation retention and assistant data lifecycle."
/>
)}
{isAccountOwner && (
<SettingsLinkCard
to="/account/categories"
icon={<FolderTree className="h-5 w-5" />}
title="Team Categories"
description="Manage shared flow categories for your workspace."
/>
)}
{isAccountOwner && (
<SettingsLinkCard
to="/account/target-lists"
icon={<Server className="h-5 w-5" />}
title="Target Lists"
description="Maintain saved server and device lists for the team."
/>
)}
<SettingsLinkCard
to="/feedback"
icon={<MessageSquareText className="h-5 w-5" />}
title="Support & Feedback"
description="Report bugs, request features, or share product feedback."
/>
</div>
</section>
{showTransferModal && (
<TransferOwnershipModal
members={members}
onClose={() => setShowTransferModal(false)}
onTransferred={() => {
setShowTransferModal(false)
loadData()
}}
/>
)}
{showLeaveModal && account && (
<LeaveAccountModal accountName={account.name} onClose={() => setShowLeaveModal(false)} />
)}
{showDeleteModal && <DeleteAccountModal onClose={() => setShowDeleteModal(false)} />}
</div>
</>
)
}
export default AccountSettingsPage