import { useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { AlertCircle, ArrowRight, Check, Clock, Copy, CreditCard, Crown, FolderTree, Loader2, Mail, MessageSquareText, Palette, Pencil, Plug, RefreshCw, Server, Shield, Wand2, UserCog, 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 { SeatCounterWidget } from '@/components/admin/SeatCounterWidget' import { useAuthStore } from '@/store/authStore' import { CheckoutButton } from '@/components/subscription/CheckoutButton' import { toast } from '@/lib/toast' /* ── Building blocks ─────────────────────────────────────────────────────── */ function SectionLabel({ children }: { children: React.ReactNode }) { return (

{children}

) } function UsageRow({ 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 isAtLimit = !isUnlimited && current >= max const isNearLimit = !isUnlimited && percentage >= 80 const numeralColor = isAtLimit ? 'text-danger' : isNearLimit ? 'text-warning' : 'text-foreground' const barColor = isAtLimit ? 'bg-danger' : isNearLimit ? 'bg-warning' : 'bg-primary' return (
{label}
{!isUnlimited && (
)}
{current} / {isUnlimited ? '∞' : max}
) } interface SettingsRowProps { to: string icon: React.ReactNode title: string description: string status?: { label: string; tone: 'positive' | 'neutral' | 'warning' } } function SettingsRow({ to, icon, title, description, status }: SettingsRowProps) { return ( {icon}
{title}
{description}
{status && ( {status.label} )} ) } function formatShortDate(value: string | null | undefined) { if (!value) return 'Never' return new Date(value).toLocaleDateString() } function planLabel(plan: string) { return plan.charAt(0).toUpperCase() + plan.slice(1) } /* ── Page ────────────────────────────────────────────────────────────────── */ export function AccountSettingsPage() { const { isAccountOwner } = usePermissions() const { plan, limits, usage } = useSubscription() 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) 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(null) useEffect(() => { loadData() }, []) // eslint-disable-line react-hooks/exhaustive-deps -- initial account load; mutations call loadData explicitly 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 memberCount = members.length || (isAccountOwner ? 1 : undefined) 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) { const resp = (err as { response?: { status?: number data?: { detail?: { code?: string; role?: string; current?: number; limit?: number } } } }).response if (resp?.status === 402 && resp?.data?.detail?.code === 'seat_limit_exceeded') { const d = resp.data.detail const label = d.role === 'l1_tech' ? 'L1' : 'Engineer' toast.warning( `${label} seats full: ${d.current}/${d.limit}. Upgrade your plan to add more.`, ) } else { 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 (
) } if (error) { return (
{error}
) } const sub = subscription?.subscription const ownerName = ownerMember?.name ?? (user?.account_role === 'owner' ? user.name : null) const inputClass = cn( 'rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground', 'focus:border-primary/40 focus:outline-hidden focus:ring-1 focus:ring-primary/20' ) return ( <>
{/* ── Header ─────────────────────────────────────────────────────── */}
{isEditingName ? (
setEditedName(e.target.value)} className={cn(inputClass, 'text-2xl font-bold font-heading py-1')} autoFocus onKeyDown={(e) => { if (e.key === 'Enter') handleSaveName() if (e.key === 'Escape') { setEditedName(account?.name ?? '') setIsEditingName(false) } }} />
) : ( <>

{account?.name}

{isAccountOwner && ( )} )}
{planLabel(plan)} plan {sub && ( <> · {sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')} )} {ownerName && ( <> · Owned by {ownerName} )} {memberCount !== undefined && ( <> · {memberCount} {memberCount === 1 ? 'member' : 'members'} )} <> · Created {formatShortDate(account?.created_at)}
{/* ── Plan & Usage ───────────────────────────────────────────────── */}
Plan & usage {sub?.current_period_end ? `Renews ${new Date(sub.current_period_end).toLocaleDateString()}` : 'No renewal scheduled'}
{limits && usage ? (
{(() => { const seatCount = usage.user_count ?? (isAccountOwner ? members.length : null) return seatCount !== null ? ( ) : null })()}
) : (

Plan limits unavailable.

)} {plan !== 'enterprise' && (
{plan === 'free' && }
)}
{/* ── People ─────────────────────────────────────────────────────── */} {isAccountOwner ? (
People
setInviteEmail(e.target.value)} required className={cn(inputClass, 'flex-1 min-w-[14rem]')} />
{(members.length > 0 || pendingInvites.length > 0) && (
    {members.map((member) => (
  • {member.name} {!member.is_active && ( Inactive )}
    {member.email} {member.last_login && ( · Last seen {formatShortDate(member.last_login)} )}
    {member.account_role === 'owner' ? ( Owner ) : ( )} {member.account_role !== 'owner' && ( handleRemoveMember(member.id)} confirmLabel="Remove?" className="rounded 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}`} > )}
  • ))} {pendingInvites.map((invite) => (
  • {invite.email}
    Pending {invite.expires_at && ( {' '} · Expires {new Date(invite.expires_at).toLocaleDateString()} )}
    {invite.role}
  • ))}
)} {account?.display_code && (
Share to invite during signup: {account.display_code}
)}
) : (
People

Membership and invites are managed by the account owner {ownerName && ({ownerName})}. Contact your admin to make changes.

)} {/* ── Settings ───────────────────────────────────────────────────── */}
Settings
} title="Profile" description="Your name, email, and personal preferences" /> } title="Billing" description="Subscription, payment method, and invoices" />
{isAccountOwner && (
} title="Branding" description="Logo, accent color, and company name" status={ limits?.custom_branding ? { label: 'Included', tone: 'positive' } : { label: 'Plan gated', tone: 'warning' } } /> } title="Integrations" description="Connect PSA tools and external systems" /> } title="Chat retention" description="Conversation retention and assistant data lifecycle" /> } title="Session security" description="Session-expiration policy and active sessions" /> } title="Team categories" description="Shared flow categories for your workspace" /> } title="L1 AI build categories" description="Which problem types the L1 assistant may build trees for" /> } title="Target lists" description="Saved server and device lists for the team" />
)}
Need help? Send feedback or report a bug
{/* ── Account actions (transfer / delete / leave) ────────────────── */}
{isAccountOwner ? ( <>

Transfer ownership

Move ownership of this account to another member.

Delete account

Permanently delete the account and all data. Cannot be undone.

) : (

Leave account

Leave this account and create a personal one.

)}
{showTransferModal && ( setShowTransferModal(false)} onTransferred={() => { setShowTransferModal(false) loadData() }} /> )} {showLeaveModal && account && ( setShowLeaveModal(false)} /> )} {showDeleteModal && setShowDeleteModal(false)} />}
) } export default AccountSettingsPage