-
-
- Renewal -
- - {formatDate(account.subscription?.current_period_end ?? null)} - -
- Created -
- {formatDate(account.created_at)} -
diff --git a/frontend/src/components/admin/ActionMenu.tsx b/frontend/src/components/admin/ActionMenu.tsx
index 6cabd654..978f3638 100644
--- a/frontend/src/components/admin/ActionMenu.tsx
+++ b/frontend/src/components/admin/ActionMenu.tsx
@@ -54,7 +54,7 @@ export function ActionMenu({ items }: ActionMenuProps) {
onClick={() => setOpen(!open)}
className={cn(
'rounded-md p-1.5 text-muted-foreground transition-colors',
- 'hover:bg-accent hover:text-foreground'
+ 'hover:bg-elevated hover:text-foreground'
)}
>
| ({ |
|---|
| - + | ))}
|
diff --git a/frontend/src/components/admin/Pagination.tsx b/frontend/src/components/admin/Pagination.tsx
index 1e967ed8..dacd6f3f 100644
--- a/frontend/src/components/admin/Pagination.tsx
+++ b/frontend/src/components/admin/Pagination.tsx
@@ -43,7 +43,7 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
@@ -59,7 +59,7 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
'px-2',
p === page
? 'bg-primary text-white'
- : 'text-muted-foreground hover:bg-accent hover:text-foreground'
+ : 'text-muted-foreground hover:bg-elevated hover:text-foreground'
)}
>
{p}
@@ -69,7 +69,7 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
diff --git a/frontend/src/components/admin/StatusBadge.tsx b/frontend/src/components/admin/StatusBadge.tsx
index 0f2e4b54..4a7b6ff3 100644
--- a/frontend/src/components/admin/StatusBadge.tsx
+++ b/frontend/src/components/admin/StatusBadge.tsx
@@ -6,22 +6,26 @@ interface StatusBadgeProps {
variant?: BadgeVariant
children: React.ReactNode
className?: string
+ title?: string
}
const variantClasses: Record
+
+
+ )
+}
+
+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 (
+
+
+ {icon}
+
+
+
+
+ {title}+ {badge && ( + + {badge} + + )} +{description} +
+
+ )
+}
+
+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{label} ++ {current} + + / {isUnlimited ? 'Unlimited' : max} + + + {!isUnlimited && ( +
+
+
+ )}
+
-
-
-
-
-
- Account Settings-- Manage your account, subscription, and team - -
- {/* Account Info Section */}
-
- Account Information- -
- {/* Account Name */}
-
+
-
- {isEditingName ? (
-
-
- {/* Display Code */}
-
- 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} - -
+
+
- {/* Subscription Section */}
- Account Management++ Manage your account identity, billing, access, and workspace settings. +
- Subscription- -
- {/* Plan & Status */}
-
-
-
-
- {sub?.current_period_end && (
- - Current period ends: {new Date(sub.current_period_end).toLocaleDateString()} - - )} - - {/* Usage Stats */} - {limits && usage && ( -
-
+
-
- {/* Team Members Section (owners only) */}
- {isAccountOwner && (
-
+
-
+
- )}
- {/* Upgrade buttons */}
- {plan === 'free' && (
- Account Identity
-
- )}
- {plan === 'pro' && (
-
-
- )}
-
-
-
-
- {members.length === 0 ? (
- Team Members-No team members yet. - ) : ( -
- {members.map((member) => (
-
+ )
+}
+
+export default UsersPage
diff --git a/frontend/src/pages/admin/UsersPage.tsx b/frontend/src/pages/admin/UsersPage.tsx
deleted file mode 100644
index 09cc1bfb..00000000
--- a/frontend/src/pages/admin/UsersPage.tsx
+++ /dev/null
@@ -1,1108 +0,0 @@
-import { useCallback, useEffect, useMemo, useState } from 'react'
-import { useNavigate } from 'react-router-dom'
-import {
- ArrowRightLeft,
- Building2,
- CalendarClock,
- Check,
- Copy,
- Crown,
- ExternalLink,
- Loader2,
- Mail,
- Plus,
- Search,
- Shield,
- Sparkles,
- UserCheck,
- UserPlus,
- UserX,
- Users,
-} from 'lucide-react'
-import { Button } from '@/components/ui/Button'
-import { Input } from '@/components/ui/Input'
-import { EmptyState, PageHeader, Pagination, SearchInput, StatusBadge, ActionMenu } from '@/components/admin'
-import { Modal } from '@/components/common/Modal'
-import { adminApi } from '@/api/admin'
-import { toast } from '@/lib/toast'
-import { cn } from '@/lib/utils'
-import type {
- AccountFeatureOverrideResponse,
- AccountOverrideResponse,
- AdminAccountListItem,
- AdminAccountMember,
- AdminUserListItem,
-} from '@/types/admin'
-
-type UserRecord = AdminUserListItem | (AdminAccountMember & {
- account_id: string
- account_name: string
- account_display_code: string
-})
-
-function formatDate(value: string | null) {
- if (!value) return 'Never'
- return new Date(value).toLocaleDateString()
-}
-
-function buildMemberRecord(account: AdminAccountListItem, member: AdminAccountMember): UserRecord {
- return {
- ...member,
- account_id: account.id,
- account_name: account.name,
- account_display_code: account.display_code,
- }
-}
-
-function UsageMini({
- label,
- current,
-}: {
- label: string
- current: number
-}) {
- return (
-
-
)
}
diff --git a/frontend/src/pages/admin/AccountsPage.tsx b/frontend/src/pages/admin/AccountsPage.tsx
new file mode 100644
index 00000000..4baf6fbe
--- /dev/null
+++ b/frontend/src/pages/admin/AccountsPage.tsx
@@ -0,0 +1,821 @@
+import { useCallback, useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import {
+ Building2,
+ Check,
+ Copy,
+ ExternalLink,
+ Loader2,
+ Mail,
+ Plus,
+ Search,
+ Sparkles,
+ UserPlus,
+} from 'lucide-react'
+import { Button } from '@/components/ui/Button'
+import { Input } from '@/components/ui/Input'
+import {
+ DataTable,
+ EmptyState,
+ PageHeader,
+ Pagination,
+ SearchInput,
+ StatusBadge,
+ ActionMenu,
+ type Column,
+} from '@/components/admin'
+import { Modal } from '@/components/common/Modal'
+import { adminApi } from '@/api/admin'
+import { toast } from '@/lib/toast'
+import { cn } from '@/lib/utils'
+import type {
+ AdminAccountListItem,
+ AdminUserListItem,
+} from '@/types/admin'
+
+function formatDate(value: string | null) {
+ if (!value) return 'Never'
+ return new Date(value).toLocaleDateString()
+}
+
+function planBadgeVariant(status: string | undefined): 'success' | 'warning' | 'destructive' | 'default' {
+ switch (status) {
+ case 'active': return 'success'
+ case 'trialing': return 'warning'
+ case 'past_due': return 'warning'
+ case 'canceled': return 'destructive'
+ default: return 'default'
+ }
+}
+
+export function UsersPage() {
+ const navigate = useNavigate()
+
+ const [accounts, setAccounts] = useState
-
- {/* Invite Member Section (owners only) */}
- {isAccountOwner && (
- {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+
+
+
+
+
+
+
+
+
+
+ {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)} +
+
+
+ ),
+ },
+ {
+ key: 'plan',
+ header: 'Plan',
+ render: (account) => (
+ + {account.display_code} + {account.owner ? ` · ${account.owner.name}` : ''} + +
+
+
+ ),
+ },
+ {
+ key: 'role',
+ header: 'Role',
+ render: (user) => (
+ {user.email} +
+ {user.is_super_admin &&
+ ),
+ className: 'w-[140px]',
+ },
+ {
+ key: 'account',
+ header: 'Account',
+ render: (user) => (
+
+ {user.account_name || 'No account'}
+ {user.account_display_code && (
+ {user.account_display_code}
+ )}
+
+ ),
+ },
+ {
+ key: 'status',
+ header: 'Status',
+ render: (user) => (
+
+
+ ),
+ className: 'w-[140px]',
+ },
+ {
+ key: 'last_login',
+ header: 'Last Login',
+ render: (user) => (
+ {formatDate(user.last_login)}
+ ),
+ className: 'w-[100px]',
+ },
+ {
+ key: 'actions',
+ header: '',
+ render: (user) => (
+
+
+ }
+ />
+
+ {/* Filters */}
+
+
+
+ {/* Accounts table */}
+
+
+
+
+
+
+
+
+ + {accountsLoading ? 'Loading...' : `${total} accounts`} ++
+
+
+
+
+ Global People Search++ Find a user across all accounts by name or email. + +
+
+ ) : !peopleLoading ? (
+
+
+ )
+ ) : (
+ Type a name or email to search. + )} +
+
+
+
+ )}
+ >
+
+
+
+
+ setCreateAccountForm((form) => ({ ...form, name: e.target.value }))}
+ placeholder="Acme MSP"
+ />
+
+
+
+
+
+
+
+
+
+ )}
+ >
+
+
+
+
+ setCreateForm((form) => ({ ...form, name: e.target.value }))}
+ placeholder="Full name"
+ />
+
+
+
+ setCreateForm((form) => ({ ...form, email: e.target.value }))}
+ placeholder="user@example.com"
+ />
+
+
+
+
+
+ {createForm.account_mode === 'existing' && (
+ <>
+
+
+ setCreateForm((form) => ({ ...form, account_display_code: e.target.value }))}
+ placeholder="e.g. ABC12345"
+ />
+
+
+
+
+
+ >
+ )}
+
+ setCreateForm((form) => ({ ...form, send_email: e.target.checked }))}
+ className="rounded border-border bg-card"
+ />
+
+
+
+
+
+ )}
+ >
+
+
+
+ This password will not be shown again. Copy it now.
+
+
+
+
+
+
+
+ {tempPassword}
+
+
+ + The user will be required to change this password on first login. + +
+
+
+
+ )}
+ >
+
+
+
+
+ setInviteForm((form) => ({ ...form, email: e.target.value }))}
+ placeholder="user@example.com"
+ />
+
+
+
+ setInviteForm((form) => ({ ...form, account_display_code: e.target.value }))}
+ placeholder="e.g. ABC12345"
+ />
+
+
+
+
+
+
-
- )
-}
-
-export function UsersPage() {
- const navigate = useNavigate()
-
- const [accounts, setAccounts] = useState{label} -{current} -
-
- )
-
- const accountTotalPages = Math.max(1, Math.ceil(total / accountPageSize))
- const peopleTotalPages = Math.max(1, Math.ceil(peopleTotal / peoplePageSize))
-
- return (
-
-
-
-
- {user.name} - {user.is_super_admin &&{user.email} -
- Platform role: {user.role}
- Account: {user.account_name || 'No account'}
- {user.account_display_code && Code: {user.account_display_code}}
- Last login: {formatDate(user.last_login)}
-
-
-
-
-
- )
-}
-
-export default UsersPage
diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx
index fec74d59..22333b03 100644
--- a/frontend/src/router.tsx
+++ b/frontend/src/router.tsx
@@ -63,7 +63,7 @@ const AccountSettingsPage = lazyWithRetry(() => import('@/pages/AccountSettingsP
// Admin pages
const AdminLayout = lazyWithRetry(() => import('@/components/admin/AdminLayout'))
const AdminDashboardPage = lazyWithRetry(() => import('@/pages/admin/DashboardPage'))
-const AdminAccountsPage = lazyWithRetry(() => import('@/pages/admin/UsersPage'))
+const AdminAccountsPage = lazyWithRetry(() => import('@/pages/admin/AccountsPage'))
const AdminAccountDetailPage = lazyWithRetry(() => import('@/pages/admin/AccountDetailPage'))
const AdminUserDetailPage = lazyWithRetry(() => import('@/pages/admin/UserDetailPage'))
const AdminInviteCodesPage = lazyWithRetry(() => import('@/pages/admin/InviteCodesPage'))
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - Customer Account Management --- Search customers, inspect subscription health, and manage account-level SaaS controls from here. - -
-
-
-
-
- {accounts.length > 0 ? (
-
-
-
-
- Customer Accounts-- {accountsLoading ? 'Loading account records...' : `${total} customer accounts match the current filters.`} - -
- {accounts.map((account) => {
- const limitOverride = limitOverrideByAccount.get(account.id)
- const featureOverrideCount = featureOverrideCounts.get(account.id) ?? 0
-
- return (
-
- ) : !accountsLoading ? (
-
-
-
-
-
-
-
-
- {account.subscription ? (
- <>
-
-
-
-
- {account.owner && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Members-
- {account.members.length > 0 ? (
- account.members.slice(0, 3).map((member) => renderUserRow(buildMemberRecord(account, member)))
- ) : (
-
-
- No members found in this account.
-
- )}
- {account.members.length > 3 && (
-
- +{account.members.length - 3} more members in this account
-
- )}
-
-
-
-
-
-
-
- Global People Search-- Find a user anywhere across customer accounts without leaving the account management view. - -
- {people.map((person) => renderUserRow(person))}
-
- ) : !peopleLoading ? (
-
-
- )
- ) : (
- Type a name or email to search individual users globally. - )} -
-
-
-
- )}
- >
-
-
-
-
- setCreateAccountForm((form) => ({ ...form, name: e.target.value }))}
- placeholder="Acme MSP"
- />
-
-
-
-
-
-
-
-
-
- )}
- >
-
-
- - Changing role for {roleModalUser?.name} - - -
-
-
-
- )}
- >
-
-
- - Moving {moveModalUser?.name} to a new account. - -
-
- setDisplayCode(e.target.value)}
- placeholder="e.g. ABC12345"
- />
-
-
-
-
-
- )}
- >
-
-
-
-
- setCreateForm((form) => ({ ...form, name: e.target.value }))}
- placeholder="Full name"
- />
-
-
-
- setCreateForm((form) => ({ ...form, email: e.target.value }))}
- placeholder="user@example.com"
- />
-
-
-
-
-
- {createForm.account_mode === 'existing' && (
- <>
-
-
- setCreateForm((form) => ({ ...form, account_display_code: e.target.value }))}
- placeholder="e.g. ABC12345"
- />
-
-
-
-
-
- >
- )}
-
- setCreateForm((form) => ({ ...form, send_email: e.target.checked }))}
- className="rounded border-border bg-card"
- />
-
-
-
-
-
- )}
- >
-
-
-
- This password will not be shown again. Copy it now.
-
-
-
-
-
-
-
- {tempPassword}
-
-
- - The user will be required to change this password on first login. - -
-
-
-
- )}
- >
-
-
-
-
- setInviteForm((form) => ({ ...form, email: e.target.value }))}
- placeholder="user@example.com"
- />
-
-
-
- setInviteForm((form) => ({ ...form, account_display_code: e.target.value }))}
- placeholder="e.g. ABC12345"
- />
-
-
-
-
-
-
-
-
-
- )}
- >
-
-
- - Updating plan for {planModalAccount?.name}. - - -
-
-
-
- )}
- >
-
-
- - Starting or extending trial for {trialModalAccount?.name}. - -
-
- setTrialDays(e.target.value)}
- />
-
- |