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([]) const [accountsLoading, setAccountsLoading] = useState(true) const [accountSearch, setAccountSearch] = useState('') const [planFilter, setPlanFilter] = useState('all') const [statusFilter, setStatusFilter] = useState('all') const [page, setPage] = useState(1) const [total, setTotal] = useState(0) const accountPageSize = 12 const [showArchived, setShowArchived] = useState(false) const [people, setPeople] = useState([]) const [peopleLoading, setPeopleLoading] = useState(false) const [peopleSearch, setPeopleSearch] = useState('') const [peoplePage, setPeoplePage] = useState(1) const [peopleTotal, setPeopleTotal] = useState(0) const peoplePageSize = 12 const [showCreateModal, setShowCreateModal] = useState(false) const [createForm, setCreateForm] = useState({ email: '', name: '', account_mode: 'personal' as 'existing' | 'personal', account_display_code: '', account_role: 'engineer' as 'owner' | 'admin' | 'engineer' | 'viewer', send_email: true, }) const [createLoading, setCreateLoading] = useState(false) const [tempPassword, setTempPassword] = useState(null) const [copied, setCopied] = useState(false) const [showInviteModal, setShowInviteModal] = useState(false) const [inviteForm, setInviteForm] = useState({ email: '', account_display_code: '', role: 'engineer' as 'engineer' | 'viewer', }) const [inviteLoading, setInviteLoading] = useState(false) const [showCreateAccountModal, setShowCreateAccountModal] = useState(false) const [createAccountForm, setCreateAccountForm] = useState({ name: '', plan: 'free' as 'free' | 'pro' | 'team', owner_email: '' }) const [createAccountLoading, setCreateAccountLoading] = useState(false) const fetchAccounts = useCallback(async () => { setAccountsLoading(true) try { const accountsData = await adminApi.listAccounts({ page, size: accountPageSize, search: accountSearch || undefined, plan: planFilter !== 'all' ? planFilter : undefined, status: statusFilter !== 'all' ? statusFilter : undefined, include_archived: showArchived || undefined, }) setAccounts(accountsData.items) setTotal(accountsData.total) } catch { toast.error('Failed to load accounts') } finally { setAccountsLoading(false) } }, [accountPageSize, accountSearch, page, planFilter, showArchived, statusFilter]) const fetchPeople = useCallback(async () => { if (!peopleSearch.trim()) { setPeopleLoading(false) setPeople([]) setPeopleTotal(0) return } setPeopleLoading(true) try { const data = await adminApi.listUsers({ page: peoplePage, size: peoplePageSize, search: peopleSearch || undefined, include_archived: showArchived || undefined, }) setPeople(data.items) setPeopleTotal(data.total) } catch { toast.error('Failed to load people search') } finally { setPeopleLoading(false) } }, [peoplePage, peoplePageSize, peopleSearch, showArchived]) useEffect(() => { fetchAccounts() }, [fetchAccounts]) useEffect(() => { fetchPeople() }, [fetchPeople]) const handleCreateUser = async () => { if (!createForm.email || !createForm.name) return if (createForm.account_mode === 'existing' && !createForm.account_display_code) { toast.error('Account display code is required') return } setCreateLoading(true) try { const result = await adminApi.createUser({ email: createForm.email, name: createForm.name, account_mode: createForm.account_mode, account_display_code: createForm.account_mode === 'existing' ? createForm.account_display_code : undefined, account_role: createForm.account_mode === 'existing' ? createForm.account_role : undefined, send_email: createForm.send_email, }) setShowCreateModal(false) setTempPassword(result.temporary_password) setCopied(false) toast.success(result.email_sent ? 'User created and welcome email sent' : 'User created') setCreateForm({ email: '', name: '', account_mode: 'personal', account_display_code: '', account_role: 'engineer', send_email: true, }) fetchAccounts() } catch (err: unknown) { if (err && typeof err === 'object' && 'response' in err) { const axiosErr = err as { response?: { data?: { detail?: string } } } toast.error(axiosErr.response?.data?.detail || 'Failed to create user') } else { toast.error('Failed to create user') } } finally { setCreateLoading(false) } } const handleCopyPassword = async () => { if (!tempPassword) return await navigator.clipboard.writeText(tempPassword) setCopied(true) setTimeout(() => setCopied(false), 2000) } const handleInviteUser = async () => { if (!inviteForm.email || !inviteForm.account_display_code) return setInviteLoading(true) try { const result = await adminApi.createInvite({ email: inviteForm.email, account_display_code: inviteForm.account_display_code, role: inviteForm.role, }) setShowInviteModal(false) setInviteForm({ email: '', account_display_code: '', role: 'engineer' }) toast.success(result.email_sent ? 'Invite sent' : 'Invite created (email not configured)') fetchAccounts() } catch (err: unknown) { if (err && typeof err === 'object' && 'response' in err) { const axiosErr = err as { response?: { data?: { detail?: string } } } toast.error(axiosErr.response?.data?.detail || 'Failed to send invite') } else { toast.error('Failed to send invite') } } finally { setInviteLoading(false) } } const handleCreateAccount = async () => { if (!createAccountForm.name.trim()) return setCreateAccountLoading(true) try { const created = await adminApi.createAccount({ name: createAccountForm.name.trim(), plan: createAccountForm.plan, owner_email: createAccountForm.owner_email.trim() || undefined, }) toast.success('Account created') setShowCreateAccountModal(false) setCreateAccountForm({ name: '', plan: 'free', owner_email: '' }) navigate(`/admin/accounts/${created.id}`) } catch (err: unknown) { if (err && typeof err === 'object' && 'response' in err) { const axiosErr = err as { response?: { data?: { detail?: string } } } toast.error(axiosErr.response?.data?.detail || 'Failed to create account') } else { toast.error('Failed to create account') } } finally { setCreateAccountLoading(false) } } const accountColumns: Column[] = [ { key: 'name', header: 'Account', render: (account) => (

{account.display_code} {account.owner ? ` · ${account.owner.name}` : ''}

), }, { key: 'plan', header: 'Plan', render: (account) => ( {account.subscription?.plan ?? 'free'} ), className: 'w-[100px]', }, { key: 'status', header: 'Status', render: (account) => { if (!account.subscription) { return No subscription } return ( {account.subscription.status} ) }, className: 'w-[120px]', }, { key: 'members', header: 'Members', render: (account) => ( {account.active_member_count} / {account.member_count} ), className: 'w-[100px]', }, { key: 'usage', header: 'Usage', render: (account) => ( {account.usage.tree_count} flows · {account.usage.session_count_this_month} sessions ), className: 'w-[160px]', }, { key: 'created', header: 'Created', render: (account) => ( {formatDate(account.created_at)} ), className: 'w-[100px]', }, { key: 'actions', header: '', render: (account) => ( , onClick: () => navigate(`/admin/accounts/${account.id}`), }, ...(account.owner ? [{ label: 'View Owner', icon: , onClick: () => navigate(`/admin/users/${account.owner?.id}`), }] : []), ]} /> ), className: 'w-[48px]', }, ] const peopleColumns: Column[] = [ { key: 'name', header: 'Name', render: (user) => (

{user.email}

), }, { key: 'role', header: 'Role', render: (user) => (
{user.is_super_admin && Super Admin} {user.role}
), 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) => (
{user.is_active ? 'Active' : 'Inactive'} {user.deleted_at && Archived}
), className: 'w-[140px]', }, { key: 'last_login', header: 'Last Login', render: (user) => ( {formatDate(user.last_login)} ), className: 'w-[100px]', }, { key: 'actions', header: '', render: (user) => ( , onClick: () => navigate(`/admin/users/${user.id}`), }, ]} /> ), className: 'w-[48px]', }, ] const accountTotalPages = Math.max(1, Math.ceil(total / accountPageSize)) const peopleTotalPages = Math.max(1, Math.ceil(peopleTotal / peoplePageSize)) return (
} /> {/* Filters */}
{ setAccountSearch(value) setPage(1) }} placeholder="Search accounts, owners, or codes..." className="w-full sm:max-w-sm" />
{/* Accounts table */}

{accountsLoading ? 'Loading...' : `${total} accounts`}

a.id} isLoading={accountsLoading} skeletonRows={6} emptyState={ } title="No accounts found" description="Adjust the filters or clear the search." /> } />
{/* Global people search */}

Global People Search

Find a user across all accounts by name or email.

{ setPeopleSearch(value) setPeoplePage(1) }} placeholder="Search by name, email, or account..." className="max-w-sm" /> {peopleSearch.trim() ? ( people.length > 0 ? (
p.id} isLoading={peopleLoading} skeletonRows={4} emptyState={ } title="No matching people" description="Try another name or email." /> } />
) : !peopleLoading ? ( } title="No matching people" description="Try another name or email." /> ) : (
Searching...
) ) : (

Type a name or email to search.

)}
{/* Create Account modal */} setShowCreateAccountModal(false)} title="Create Account" size="sm" footer={(
)} >
setCreateAccountForm((form) => ({ ...form, name: e.target.value }))} placeholder="Acme MSP" />
setCreateAccountForm((form) => ({ ...form, owner_email: e.target.value }))} placeholder="owner@example.com" />

Must be an existing user.

{/* Create User modal */} setShowCreateModal(false)} title="Create User" size="sm" footer={(
)} >
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" />
{/* Temp password modal */} setTempPassword(null)} title="User Created" size="sm" footer={(
)} >
This password will not be shown again. Copy it now.
{tempPassword}

The user will be required to change this password on first login.

{/* Invite User modal */} setShowInviteModal(false)} title="Invite User" size="sm" footer={(
)} >
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 default UsersPage