-
-
- 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
-
+ )
+}
+
+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 (
- {icon}
+ {icon}
{title}{badge && ( - + {badge} )} @@ -75,37 +75,6 @@ function SettingsLinkCard({ to, icon, title, description, badge }: SettingsLinkC ) } -function OverviewStat({ - icon, - label, - value, - tone = 'default', -}: { - icon: React.ReactNode - label: string - value: string - tone?: 'default' | 'good' | 'warn' -}) { - return ( -
-
- )
-}
-
function UsageStat({
label,
current,
@@ -135,7 +104,7 @@ function UsageStat({
{!isUnlimited && (
-
-
- {icon}
-
- {label}
-
- {value} -
+
- 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
@@ -312,72 +281,11 @@ export function AccountSettingsPage() {
<>
)}
@@ -784,7 +702,7 @@ export function AccountSettingsPage() {
toast.success('Preference saved')
}}
className={cn(
- 'mt-2 block w-full rounded-xl border border-border bg-card px-3 py-2',
+ 'mt-2 block w-full rounded-md border border-border bg-card px-3 py-2',
'text-sm text-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
diff --git a/frontend/src/pages/admin/AccountDetailPage.tsx b/frontend/src/pages/admin/AccountDetailPage.tsx
index 3d82cfd6..fef6297e 100644
--- a/frontend/src/pages/admin/AccountDetailPage.tsx
+++ b/frontend/src/pages/admin/AccountDetailPage.tsx
@@ -19,6 +19,7 @@ import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Modal } from '@/components/common/Modal'
import { EmptyState, StatusBadge } from '@/components/admin'
+import { ConfirmButton } from '@/components/common/ConfirmButton'
import { adminApi } from '@/api/admin'
import { toast } from '@/lib/toast'
import { cn } from '@/lib/utils'
@@ -57,11 +58,11 @@ export function AccountDetailPage() {
})
const [inviteLoading, setInviteLoading] = useState(false)
- const [planModalOpen, setPlanModalOpen] = useState(false)
+ const [editingPlan, setEditingPlan] = useState(false)
const [selectedPlan, setSelectedPlan] = useState('free')
const [planSaving, setPlanSaving] = useState(false)
- const [trialModalOpen, setTrialModalOpen] = useState(false)
+ const [editingTrial, setEditingTrial] = useState(false)
const [trialDays, setTrialDays] = useState('14')
const [trialSaving, setTrialSaving] = useState(false)
@@ -189,7 +190,7 @@ export function AccountDetailPage() {
try {
await adminApi.updateAccountSubscriptionPlan(account.id, selectedPlan)
toast.success(`Plan updated to ${selectedPlan}`)
- setPlanModalOpen(false)
+ setEditingPlan(false)
loadAccount()
} catch {
toast.error('Failed to update plan')
@@ -204,7 +205,7 @@ export function AccountDetailPage() {
try {
await adminApi.extendAccountTrial(account.id, parseInt(trialDays, 10))
toast.success(`Trial updated by ${trialDays} days`)
- setTrialModalOpen(false)
+ setEditingTrial(false)
loadAccount()
} catch {
toast.error('Failed to update trial')
@@ -249,7 +250,7 @@ export function AccountDetailPage() {
-
) : (
-
-
@@ -610,6 +518,14 @@ export function AccountSettingsPage() {
-
+
-
-
-
-
-
-
-
-
-
- - Account Management --- Manage your account identity, billing, access, and workspace settings from one place. - -
-
- {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'}`}
-
-
-
-
-
+
Account Management++ Manage your account identity, billing, access, and workspace settings. +
@@ -452,7 +360,7 @@ export function AccountSettingsPage() {
- Useful when admins or teammates need to join this account. + Share this code with teammates so they can join your account during registration. No pending invites right now. +No pending invites. Use the form above to invite teammates by email. )}
@@ -257,7 +258,7 @@ export function AccountDetailPage() {
))
) : (
- {account.name}-
Manage account settings, subscription, invites, and users from one place.
@@ -367,10 +368,22 @@ export function AccountDetailPage() {
-
+ {member.is_active ? (
+
- No users yet. Create or invite someone into this account.
+
@@ -414,29 +428,71 @@ export function AccountDetailPage() {
+
)}
No users yet. +Use Create User or Invite User above to add members.
-
-
-
+ {editingPlan ? (
+
+
+
+
+
+ ) : editingTrial ? (
+
+ setTrialDays(e.target.value)}
+ className="w-24"
+ placeholder="Days"
+ />
+
+
+
+ ) : (
+
+
+
+
+ )}
Pending Invites-
{account.invites.length > 0 ? (
@@ -450,8 +506,9 @@ export function AccountDetailPage() {
))
) : (
-
- No pending invites for this account.
+
@@ -561,7 +618,7 @@ export function AccountDetailPage() {
@@ -569,49 +626,6 @@ export function AccountDetailPage() {
+
)}
No pending invites. +Use Invite User above to send an invitation.
-
-
-
- )}
- >
-
-
-
-
-
- )}
- >
-
-
- setTrialDays(e.target.value)} />
-
-
+
+
+ ),
+ },
+ {
+ 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)}
- />
-
- |