diff --git a/frontend/src/pages/AccountSettingsPage.tsx b/frontend/src/pages/AccountSettingsPage.tsx
index 3c47835d..3c347df1 100644
--- a/frontend/src/pages/AccountSettingsPage.tsx
+++ b/frontend/src/pages/AccountSettingsPage.tsx
@@ -1,9 +1,32 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
-import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock, Plug, Palette, ShieldCheck } from 'lucide-react'
+import {
+ AlertCircle,
+ AlertTriangle,
+ ArrowRight,
+ Building2,
+ CalendarClock,
+ 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, AccountMember, AccountInvite } from '@/types'
+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'
@@ -17,32 +40,147 @@ 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 (
+
+
+
+
{icon}
+
+
+
{title}
+ {badge && (
+
+ {badge}
+
+ )}
+
+
{description}
+
+
+
+
+
+ )
+}
+
+function OverviewStat({
+ icon,
+ label,
+ value,
+ tone = 'default',
+}: {
+ icon: React.ReactNode
+ label: string
+ value: string
+ tone?: 'default' | 'good' | 'warn'
+}) {
+ return (
+
+
+
+ {icon}
+
+ {label}
+
+
{value}
+
+ )
+}
+
+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 (
+
+
{label}
+
+ {current}
+
+ / {isUnlimited ? 'Unlimited' : max}
+
+
+ {!isUnlimited && (
+
+ )}
+
+ )
+}
+
+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(null)
const [members, setMembers] = useState([])
const [invites, setInvites] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
+ const [copiedCode, setCopiedCode] = useState(false)
- // Account name editing
const [isEditingName, setIsEditingName] = useState(false)
const [editedName, setEditedName] = useState('')
const [isSavingName, setIsSavingName] = useState(false)
- // Modals
const [showTransferModal, setShowTransferModal] = useState(false)
const [showLeaveModal, setShowLeaveModal] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
- // Invite form
const [inviteEmail, setInviteEmail] = useState('')
const [inviteRole, setInviteRole] = useState('engineer')
const [isInviting, setIsInviting] = useState(false)
+ const [resendingId, setResendingId] = useState(null)
useEffect(() => {
loadData()
@@ -72,6 +210,18 @@ export function AccountSettingsPage() {
}
}
+ const pendingInvites = useMemo(() => invites.filter((invite) => !invite.used_at), [invites])
+ const ownerMember = useMemo(() => members.find((member) => member.account_role === 'owner') ?? null, [members])
+ const currentMembership = useMemo(() => members.find((member) => member.id === user?.id) ?? null, [members, user?.id])
+
+ 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)
@@ -83,6 +233,7 @@ export function AccountSettingsPage() {
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)
@@ -110,8 +261,6 @@ export function AccountSettingsPage() {
}
}
- const [resendingId, setResendingId] = useState(null)
-
const handleResendInvite = async (inviteId: string) => {
setResendingId(inviteId)
try {
@@ -129,8 +278,9 @@ export function AccountSettingsPage() {
const handleRemoveMember = async (userId: string) => {
try {
await accountsApi.removeMember(userId)
- setMembers(members.filter((m) => m.id !== 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)
@@ -160,622 +310,629 @@ export function AccountSettingsPage() {
return (
<>
-
-
-
-
-
-
Account Settings
-
-
- Manage your account, subscription, and team
-
-
-
-
- {/* Account Info Section */}
-
-
Account Information
-
-
- {/* Account Name */}
-
-
- {isEditingName ? (
-
-
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)
- }
- }}
- />
-
-
+
+
+
+
+
+
+
+
- ) : (
-
-
{account?.name}
- {isAccountOwner && (
-
- )}
+
+
+ Account Management
+
+
+ Manage your account identity, billing, access, and workspace settings from one place.
+
- )}
-
+
- {/* Display Code */}
-
-
-
- {account?.display_code}
-
-
-
-
-
- {/* Subscription Section */}
-
-
Subscription
-
-
- {/* Plan & Status */}
-
-
-
- {plan.charAt(0).toUpperCase() + plan.slice(1)} Plan
-
- {sub && (
-
- {sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
+
+
+ {plan.charAt(0).toUpperCase() + plan.slice(1)} plan
- )}
+ {sub && (
+
+ {sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
+
+ )}
+
+ {isAccountOwner ? 'Owner access' : `Role: ${currentMembership?.account_role ?? user?.account_role ?? 'member'}`}
+
+
- {sub?.current_period_end && (
-
- Current period ends: {new Date(sub.current_period_end).toLocaleDateString()}
-
- )}
-
- {/* Usage Stats */}
- {limits && usage && (
-
-
-
-
-
- )}
-
- {/* Upgrade buttons */}
- {plan === 'free' && (
-
-
-
-
- )}
- {plan === 'pro' && (
-
-
-
- )}
+
+ }
+ label="Members"
+ value={isAccountOwner ? String(members.length) : String(usage?.user_count ?? 1)}
+ />
+ }
+ label="Pending invites"
+ value={isAccountOwner ? String(pendingInvites.length) : 'Owner only'}
+ tone={pendingInvites.length > 0 ? 'warn' : 'default'}
+ />
+ }
+ label="Account created"
+ value={formatShortDate(account?.created_at)}
+ />
+ }
+ label="Security"
+ value={plan === 'team' ? 'SSO ready' : 'Password auth'}
+ tone={plan === 'team' ? 'good' : 'default'}
+ />
+
- {/* Team Members Section (owners only) */}
- {isAccountOwner && (
-
-
-
-
Team Members
-
+
+
+
+
+
+
Account Identity
+
- {members.length === 0 ? (
-
No team members yet.
- ) : (
-
- {members.map((member) => (
-
-
-
{member.name}
-
{member.email}
+
+
+
+ {isEditingName ? (
+
+ 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)
+ }
+ }}
+ />
+
+
-
- {member.account_role === 'owner' ? (
-
- owner
-
- ) : (
-
- )}
- {!member.is_active && (
-
- Inactive
-
- )}
- {member.account_role !== 'owner' && (
+ ) : (
+
+ {account?.name}
+ {isAccountOwner && (
)}
-
- ))}
-
- )}
-
- )}
+ )}
+
- {/* Invite Member Section (owners only) */}
- {isAccountOwner && (
-
-
-
-
Invite Member
+
+
+
+
+
+ {account?.display_code}
+
+
+
+
+ Useful when admins or teammates need to join this account.
+
+
+
+
+
+
+ {ownerMember ? ownerMember.name : user?.account_role === 'owner' ? user.email : 'Owner unavailable'}
+
+
+ {ownerMember?.email ?? 'The owner manages billing, branding, integrations, and membership changes.'}
+
+
+
+
+
+
+
+
{formatShortDate(account?.created_at)}
+
+
+
+
{formatShortDate(account?.updated_at)}
+
+
+
-