diff --git a/components/settings/unified/tabs/WorkspaceMembersTab.tsx b/components/settings/unified/tabs/WorkspaceMembersTab.tsx index d23a6ee..f030476 100644 --- a/components/settings/unified/tabs/WorkspaceMembersTab.tsx +++ b/components/settings/unified/tabs/WorkspaceMembersTab.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react' import { Button, Input, Select, toast, Spinner } from '@ciphera-net/ui' import { Plus, Trash, EnvelopeSimple, Crown, UserCircle } from '@phosphor-icons/react' import { useAuth } from '@/lib/auth/context' -import { getOrganizationMembers, sendInvitation, type OrganizationMember } from '@/lib/api/organization' +import { getOrganizationMembers, removeOrganizationMember, sendInvitation, getInvitations, revokeInvitation, type OrganizationMember, type OrganizationInvitation } from '@/lib/api/organization' import { getAuthErrorMessage } from '@ciphera-net/ui' const ROLE_OPTIONS = [ @@ -33,6 +33,7 @@ function RoleBadge({ role }: { role: string }) { export default function WorkspaceMembersTab() { const { user } = useAuth() const [members, setMembers] = useState([]) + const [invitations, setInvitations] = useState([]) const [loading, setLoading] = useState(true) const [inviteEmail, setInviteEmail] = useState('') const [inviteRole, setInviteRole] = useState('member') @@ -44,8 +45,12 @@ export default function WorkspaceMembersTab() { const loadMembers = async () => { if (!user?.org_id) return try { - const data = await getOrganizationMembers(user.org_id) - setMembers(data) + const [membersData, invitationsData] = await Promise.all([ + getOrganizationMembers(user.org_id), + getInvitations(user.org_id).catch(() => [] as OrganizationInvitation[]), + ]) + setMembers(membersData) + setInvitations(invitationsData) } catch { } finally { setLoading(false) } } @@ -68,11 +73,28 @@ export default function WorkspaceMembersTab() { } } - const handleRemove = async (_memberId: string, email: string) => { - // Member removal requires the full org settings page (auth API endpoint) - toast.message(`To remove ${email}, use Organization Settings → Members.`, { - action: { label: 'Open', onClick: () => { window.location.href = '/org-settings?tab=members' } }, - }) + const handleRemove = async (memberId: string, email: string) => { + if (!user?.org_id) return + if (!confirm(`Remove ${email} from the organization?`)) return + try { + await removeOrganizationMember(user.org_id, memberId) + toast.success(`${email} has been removed`) + loadMembers() + } catch (err) { + toast.error(getAuthErrorMessage(err as Error) || 'Failed to remove member') + } + } + + const handleRevokeInvitation = async (inviteId: string) => { + if (!user?.org_id) return + if (!confirm('Revoke this invitation?')) return + try { + await revokeInvitation(user.org_id, inviteId) + toast.success('Invitation revoked') + loadMembers() + } catch (err) { + toast.error(getAuthErrorMessage(err as Error) || 'Failed to revoke invitation') + } } if (loading) return
@@ -145,6 +167,36 @@ export default function WorkspaceMembersTab() { ))} + + {/* Pending Invitations */} + {invitations.length > 0 && ( +
+

Pending Invitations

+ {invitations.map(inv => ( +
+
+ + + + +
+ {inv.email} + {inv.role} + expires {new Date(inv.expires_at).toLocaleDateString('en-GB')} +
+
+ {canManage && ( + + )} +
+ ))} +
+ )} ) } diff --git a/lib/api/organization.ts b/lib/api/organization.ts index b48ab07..932b722 100644 --- a/lib/api/organization.ts +++ b/lib/api/organization.ts @@ -79,6 +79,13 @@ export async function getOrganizationMembers(organizationId: string): Promise { + await authFetch(`/auth/organizations/${organizationId}/members/${userId}`, { + method: 'DELETE', + }) +} + // Send an invitation export async function sendInvitation( organizationId: string,