import { useCallback, useEffect, useState } from 'react' import { Copy, UserPlus } from 'lucide-react' import { toast } from 'sonner' import { AppShell } from '@/components/AppShell' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { api } from '@/lib/api' import { useMembers } from '@/lib/useDirectory' import { useAuth } from '@/store/auth' interface Invitation { id: string email: string scopeType: string scopeId: string role: string status: string token: string createdAtUtc: string } const ROLES = ['Member', 'TeamOwner', 'Viewer', 'Owner'] as const export function MembersPage() { const organizationId = useAuth((s) => s.organizationId) const members = useMembers(organizationId) const [invitations, setInvitations] = useState([]) const [email, setEmail] = useState('') const [role, setRole] = useState('Member') const [busy, setBusy] = useState(false) const loadInvitations = useCallback(async () => { if (!organizationId) return try { setInvitations(await api.get(`/api/identity/invitations?organizationId=${organizationId}`)) } catch { setInvitations([]) // non-owners simply don't see the invitations panel } }, [organizationId]) useEffect(() => { void loadInvitations() }, [loadInvitations]) async function invite() { if (!organizationId || !email.trim()) return setBusy(true) try { await api.post('/api/identity/invitations', { email, scopeType: 'Organization', scopeId: organizationId, role, organizationId, }) setEmail('') toast.success('Invitation created โ€” copy the join token below.') await loadInvitations() } catch (err) { toast.error((err as Error).message) } finally { setBusy(false) } } async function copyToken(invitation: Invitation) { await navigator.clipboard.writeText(invitation.token) toast.success('Join token copied โ€” share it; they accept on the login page.') } return (

Members

Who's in the org, and who's invited.

Invite someone V1 sends no email โ€” share the join token; they redeem it from the login page.
setEmail(e.target.value)} className="w-64" placeholder="dev@company.com" />
{invitations.length > 0 && ( Invitations {invitations.map((invitation) => (
{invitation.email}
{invitation.role} ยท {new Date(invitation.createdAtUtc).toLocaleDateString()}
{invitation.status} {invitation.status === 'Pending' && ( )}
))}
)} Members ({members.length}) {members.map((member) => (
{member.displayName.slice(0, 2).toUpperCase()} {member.displayName} {member.email} {member.role ?? 'Member'}
))}
) }