diff --git a/frontend/src/pages/AccountSettingsPage.tsx b/frontend/src/pages/AccountSettingsPage.tsx index f19e169e..b04c502e 100644 --- a/frontend/src/pages/AccountSettingsPage.tsx +++ b/frontend/src/pages/AccountSettingsPage.tsx @@ -2,9 +2,7 @@ import { useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { AlertCircle, - AlertTriangle, ArrowRight, - Building2, Check, Clock, Copy, @@ -14,13 +12,11 @@ import { Mail, MessageSquareText, Palette, + Pencil, Plug, RefreshCw, Server, - Settings, - ShieldCheck, UserCog, - Users, X, } from 'lucide-react' import { PageMeta } from '@/components/common/PageMeta' @@ -36,46 +32,20 @@ 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 -} +/* ── Building blocks ─────────────────────────────────────────────────────── */ -function SettingsLinkCard({ to, icon, title, description, badge }: SettingsLinkCardProps) { +function SectionLabel({ children }: { children: React.ReactNode }) { return ( - -
-
-
{icon}
-
-
-

{title}

- {badge && ( - - {badge} - - )} -
-

{description}

-
-
- -
- +

+ {children} +

) } -function UsageStat({ +function UsageRow({ label, current, max, @@ -86,47 +56,85 @@ function UsageStat({ }) { const isUnlimited = max === null const percentage = isUnlimited ? 0 : Math.min((current / max) * 100, 100) - const isNearLimit = !isUnlimited && percentage >= 80 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}

-

+ {label} +

+ {!isUnlimited && ( +
)} - > - {current} - - / {isUnlimited ? 'Unlimited' : max} - -

- {!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 { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore() const subscription = useAuthStore((s) => s.subscription) const user = useAuthStore((s) => s.user) const refreshUser = useAuthStore((s) => s.fetchUser) @@ -180,8 +188,11 @@ 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 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 @@ -276,562 +287,405 @@ export function AccountSettingsPage() { } 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 ( <> -
-
-

Account Management

-

- Manage your account identity, billing, access, and workspace settings. -

-
- -
-
-
+
+ {/* ── Header ─────────────────────────────────────────────────────── */} +
+
+ {isEditingName ? (
- -

Account Identity

+ 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) + } + }} + /> + +
- -
-
- - {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?.display_code} - - -
-

- Share this code with teammates so they can join your account during registration. -

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

-
-
-
-
- -
-
-
-
- -

Billing & Usage

-
-

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

-
-
- -
+ ) : ( + <> +

{account?.name}

+ {isAccountOwner && ( + + )} + + )} +
+
+ + + {planLabel(plan)} plan + + {sub && ( + <> + · - - {plan.charAt(0).toUpperCase() + plan.slice(1)} Plan + {sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')} - {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)} + +
+
-
-
-

Renewal date

-

- {sub?.current_period_end ? new Date(sub.current_period_end).toLocaleDateString() : 'Not scheduled'} -

-
-
-

Branding access

-

- {limits?.custom_branding ? 'Included in your plan' : 'Upgrade required'} -

-
-
+ {/* ── Plan & Usage ───────────────────────────────────────────────── */} +
+
+ Plan & usage + + {sub?.current_period_end + ? `Renews ${new Date(sub.current_period_end).toLocaleDateString()}` + : 'No renewal scheduled'} + +
- {limits && usage && ( -
- - - -
- )} - - {plan === 'free' && ( -
- - -
- )} - {plan === 'pro' && ( -
- -
- )} + {limits && usage ? ( +
+ + +
+ ) : ( +

Plan limits unavailable.

+ )} -
-
- -

Access & Security

-
+ {plan !== 'team' && ( +
+ {plan === 'free' && } + +
+ )} +
-
-
-

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. -

-
+ {/* ── People ─────────────────────────────────────────────────────── */} + {isAccountOwner ? ( +
+ People -
-

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. -

-
-
+
+ setInviteEmail(e.target.value)} + required + className={cn(inputClass, 'flex-1 min-w-[14rem]')} + /> + + +
- {isAccountOwner && ( -
-
-
-

Need enterprise security controls?

-

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

+ {(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)} + )} +
    - - Contact Us - -
-
- )} -
-
- - -
- -
-
-

Settings Areas

-

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

+ ) : ( +
+ People +

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

-
+ + )} -
- + Settings + +
+ } - 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." + icon={} + title="Profile" + description="Your name, email, and personal preferences" />
+ + {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="Team categories" + description="Shared flow categories for your workspace" + /> + } + 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 && ( diff --git a/frontend/src/pages/account/ProfileSettingsPage.tsx b/frontend/src/pages/account/ProfileSettingsPage.tsx index 933e37c5..70a8f877 100644 --- a/frontend/src/pages/account/ProfileSettingsPage.tsx +++ b/frontend/src/pages/account/ProfileSettingsPage.tsx @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom' import { User as UserIcon, Loader2, AlertCircle, Check } from 'lucide-react' import { authApi } from '@/api/auth' import { useAuthStore } from '@/store/authStore' +import { useUserPreferencesStore } from '@/store/userPreferencesStore' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' import type { UserUpdate } from '@/types' @@ -16,6 +17,7 @@ const inputClass = cn( export function ProfileSettingsPage() { const user = useAuthStore((s) => s.user) const fetchUser = useAuthStore((s) => s.fetchUser) + const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore() const [name, setName] = useState(user?.name ?? '') const [email, setEmail] = useState(user?.email ?? '') @@ -120,6 +122,27 @@ export function ProfileSettingsPage() {
+ {/* Default export format — saved on change, not via Save Changes */} +
+ +

Pre-selected when exporting sessions.

+ +
+ {error && (