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)}

+
+
+
-
-
- setInviteEmail(e.target.value)} - required - 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' - )} - /> - - +
+
+
+
+ +

Billing & Usage

+
+

+ Monitor plan status, renewal timing, and current account limits. +

+
- +
+ + + {plan.charAt(0).toUpperCase() + plan.slice(1)} Plan + + {sub && ( + + {sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')} + + )} +
- {/* Pending Invites */} - {invites.length > 0 && ( -
-

Pending Invites

-
- {invites - .filter((inv) => !inv.used_at) - .map((invite) => ( -
-
-

{invite.email}

-

- {invite.expires_at - ? `Expires ${new Date(invite.expires_at).toLocaleDateString()}` - : 'No expiration'} -

-
-
- - {invite.role} - -
+ +
+
+ +

Access & Security

+
+ +
+
+

Authentication

+

+ {plan === 'team' ? 'Password auth available, SSO can be enabled.' : 'Password-based authentication is active.'} +

+

+ Use profile settings to update your personal details and sign-in information. +

+
+ +
+

Single Sign-On

+

+ {plan === 'team' ? 'Enterprise-ready setup available.' : 'Available on higher-tier account setups.'} +

+

+ Contact support to configure SAML or OIDC for your organization. +

+
+
+ + {isAccountOwner && ( +
+
+
+

Need enterprise security controls?

+

+ We can help enable SSO and align account security for larger teams. +

+
+ + Contact Us + +
+
+ )} +
+
+ +
)} -
- )} - {/* Profile Settings Link */} - -
- -
-

Profile Settings

-

Update your name, email, and personal details

-
-
- - - - {/* Team Settings section (owners only) */} - {isAccountOwner && ( - <> -

- Team Settings -

- - -
- -
-

Team Categories

-

Manage flow categories for your team

+ {isAccountOwner && ( +
+
+ +

Invites

-
- - - -
- -
-

Target Lists

-

Saved server and device lists for your team

-
-
- - - - -
- -
-

Chat Retention

-

Configure AI assistant conversation retention policies

-
-
- - - - -
- -
-

Integrations

-

Connect your PSA to sync session documentation to tickets

-
-
- - - - -
- -
-

Branding

-

Customize logo, accent color, and company name

-
-
- - - - )} - - {/* Feedback Link (all users) */} - -
- -
-

Send Feedback

-

Report bugs, request features, or share your thoughts

-
-
- - - - {/* Preferences Section */} -
-
- -

Preferences

-
- -
- -

- This format will be pre-selected when exporting sessions -

- -
-
- {/* SSO Section */} - {isAccountOwner && ( -
-
- -

Single Sign-On (SSO)

- - Enterprise - -
-

- SAML and OIDC single sign-on is available for enterprise plans. Contact us to enable SSO for - your organization. -

- - Contact Us - -
- )} - - {/* Danger Zone */} -
-
- -

Danger Zone

-
- -
- {isAccountOwner ? ( - <> -
-
-

Transfer Ownership

-

Make another member the account owner

+
+ 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' + )} + /> +
+ +
- -
-
-
-

Delete Account

-

Permanently delete your account and all data

+ + + {pendingInvites.length > 0 ? ( +
+ {pendingInvites.map((invite) => ( +
+
+
+

{invite.email}

+

+ {invite.expires_at + ? `Expires ${new Date(invite.expires_at).toLocaleDateString()}` + : 'No expiration'} +

+
+
+ + {invite.role} + + +
+
+
+ ))}
- -
- - ) : ( -
-
-

Leave Account

-

Leave this account and create a personal one

-
-
+ )} + +
+
+ +

Preferences

+
+ +
+ +

+ This format will be pre-selected when exporting sessions. +

+
- )} -
-
-
+
- {/* Modals */} - {showTransferModal && ( - setShowTransferModal(false)} - onTransferred={() => { setShowTransferModal(false); loadData() }} - /> - )} - {showLeaveModal && account && ( - setShowLeaveModal(false)} - /> - )} - {showDeleteModal && ( - setShowDeleteModal(false)} /> - )} -
+
+
+ +

Danger Zone

+
+ +
+ {isAccountOwner ? ( + <> +
+
+

Transfer Ownership

+

Make another member the account owner.

+
+ +
+
+
+

Delete Account

+

Permanently delete your account and all data.

+
+ +
+ + ) : ( +
+
+

Leave Account

+

Leave this account and create a personal one.

+
+ +
+ )} +
+
+ +
+ +
+
+

Settings Areas

+

+ Common account management actions, organized the way most SaaS teams expect to find them. +

+
+ +
+ } + title="Profile Settings" + description="Update your name, email, and personal details." + /> + + {isAccountOwner && ( + } + title="Branding" + description="Customize logo, accent color, and company name." + badge={limits?.custom_branding ? 'Included' : 'Plan gated'} + /> + )} + + {isAccountOwner && ( + } + title="Integrations" + description="Connect PSA and other external systems for your team." + /> + )} + + {isAccountOwner && ( + } + title="Chat Retention" + description="Control conversation retention and assistant data lifecycle." + /> + )} + + {isAccountOwner && ( + } + title="Team Categories" + description="Manage shared flow categories for your workspace." + /> + )} + + {isAccountOwner && ( + } + title="Target Lists" + description="Maintain saved server and device lists for the team." + /> + )} + + } + title="Support & Feedback" + description="Report bugs, request features, or share product feedback." + /> +
+
+ + {showTransferModal && ( + setShowTransferModal(false)} + onTransferred={() => { + setShowTransferModal(false) + loadData() + }} + /> + )} + {showLeaveModal && account && ( + setShowLeaveModal(false)} /> + )} + {showDeleteModal && setShowDeleteModal(false)} />} +
) } -/** Small helper component for usage stat display */ -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 && ( -
-
-
- )} -
- ) -} - export default AccountSettingsPage