"use client"; import { useCallback, useEffect, useState, type ChangeEvent, type KeyboardEvent, type ReactElement, type SyntheticEvent, } from "react"; import Link from "next/link"; import { UserPlus, UserX } from "lucide-react"; import { WorkspaceMemberRole } from "@mosaic/shared"; import { isValidEmail } from "@/components/workspace/validation"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { deactivateUser, fetchAdminUsers, inviteUser, updateUser, type AdminUser, type AdminUsersResponse, type AdminWorkspaceMembership, type InviteUserDto, type UpdateUserDto, } from "@/lib/api/admin"; import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces"; import { SettingsAccessDenied } from "@/components/settings/SettingsAccessDenied"; const ROLE_PRIORITY: Record = { [WorkspaceMemberRole.OWNER]: 4, [WorkspaceMemberRole.ADMIN]: 3, [WorkspaceMemberRole.MEMBER]: 2, [WorkspaceMemberRole.GUEST]: 1, }; const INITIAL_INVITE_FORM = { email: "", role: WorkspaceMemberRole.MEMBER, }; const INITIAL_DETAIL_FORM = { name: "", email: "", role: WorkspaceMemberRole.MEMBER, workspaceId: null as string | null, workspaceName: null as string | null, }; interface DetailInitialState { name: string; email: string; role: WorkspaceMemberRole; workspaceId: string | null; } function toRoleLabel(role: WorkspaceMemberRole): string { return `${role.charAt(0)}${role.slice(1).toLowerCase()}`; } function getPrimaryMembership(user: AdminUser): AdminWorkspaceMembership | null { const [firstMembership, ...restMemberships] = user.workspaceMemberships; if (!firstMembership) { return null; } return restMemberships.reduce((highest, membership) => { if (ROLE_PRIORITY[membership.role] > ROLE_PRIORITY[highest.role]) { return membership; } return highest; }, firstMembership); } export default function UsersSettingsPage(): ReactElement { const [users, setUsers] = useState([]); const [meta, setMeta] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); const [defaultWorkspaceId, setDefaultWorkspaceId] = useState(null); const [isAdmin, setIsAdmin] = useState(null); const [isInviteOpen, setIsInviteOpen] = useState(false); const [inviteForm, setInviteForm] = useState(INITIAL_INVITE_FORM); const [inviteError, setInviteError] = useState(null); const [isInviting, setIsInviting] = useState(false); const [detailTarget, setDetailTarget] = useState(null); const [detailForm, setDetailForm] = useState(INITIAL_DETAIL_FORM); const [detailInitial, setDetailInitial] = useState(null); const [detailError, setDetailError] = useState(null); const [isSavingDetails, setIsSavingDetails] = useState(false); const [deactivateTarget, setDeactivateTarget] = useState(null); const [isDeactivating, setIsDeactivating] = useState(false); const loadUsers = useCallback(async (showLoadingState: boolean): Promise => { try { if (showLoadingState) { setIsLoading(true); } else { setIsRefreshing(true); } const response = await fetchAdminUsers(1, 50); setUsers(response.data); setMeta(response.meta); setError(null); } catch (err: unknown) { setError(err instanceof Error ? err.message : "Failed to load admin users"); } finally { setIsLoading(false); setIsRefreshing(false); } }, []); useEffect(() => { fetchUserWorkspaces() .then((workspaces) => { const adminRoles: WorkspaceMemberRole[] = [ WorkspaceMemberRole.OWNER, WorkspaceMemberRole.ADMIN, ]; setDefaultWorkspaceId(workspaces[0]?.id ?? null); setIsAdmin(workspaces.some((workspace) => adminRoles.includes(workspace.role))); }) .catch(() => { setDefaultWorkspaceId(null); setIsAdmin(true); // fail open }); }, []); useEffect(() => { if (isAdmin !== true) { return; } void loadUsers(true); }, [isAdmin, loadUsers]); function resetInviteForm(): void { setInviteForm(INITIAL_INVITE_FORM); setInviteError(null); } function openUserDetails(user: AdminUser): void { const primaryMembership = getPrimaryMembership(user); const nextDetailForm = { name: user.name, email: user.email, role: primaryMembership?.role ?? WorkspaceMemberRole.MEMBER, workspaceId: primaryMembership?.workspaceId ?? null, workspaceName: primaryMembership?.workspaceName ?? null, }; setDetailTarget(user); setDetailForm(nextDetailForm); setDetailInitial({ name: nextDetailForm.name, email: nextDetailForm.email, role: nextDetailForm.role, workspaceId: nextDetailForm.workspaceId, }); setDetailError(null); } function resetUserDetails(): void { setDetailTarget(null); setDetailForm(INITIAL_DETAIL_FORM); setDetailInitial(null); setDetailError(null); } function handleUserRowKeyDown(event: KeyboardEvent, user: AdminUser): void { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openUserDetails(user); } } async function handleInviteSubmit(event: SyntheticEvent): Promise { event.preventDefault(); setInviteError(null); const email = inviteForm.email.trim(); if (!email) { setInviteError("Email is required."); return; } if (!isValidEmail(email)) { setInviteError("Please enter a valid email address."); return; } const dto: InviteUserDto = { email, role: inviteForm.role, }; if (defaultWorkspaceId) { dto.workspaceId = defaultWorkspaceId; } try { setIsInviting(true); await inviteUser(dto); setIsInviteOpen(false); resetInviteForm(); await loadUsers(false); } catch (err: unknown) { setInviteError(err instanceof Error ? err.message : "Failed to invite user"); } finally { setIsInviting(false); } } async function handleDetailSubmit(event: SyntheticEvent): Promise { event.preventDefault(); if (detailTarget === null || detailInitial === null) { return; } const name = detailForm.name.trim(); const email = detailForm.email.trim(); if (!name) { setDetailError("Name is required."); return; } if (!email) { setDetailError("Email is required."); return; } if (!isValidEmail(email)) { setDetailError("Please enter a valid email address."); return; } const didUpdateUser = name !== detailInitial.name || email !== detailInitial.email; const didUpdateRole = detailForm.workspaceId !== null && detailForm.workspaceId === detailInitial.workspaceId && detailForm.role !== detailInitial.role; if (!didUpdateUser && !didUpdateRole) { resetUserDetails(); return; } try { setIsSavingDetails(true); setDetailError(null); if (didUpdateUser) { const dto: UpdateUserDto = {}; if (name !== detailInitial.name) { dto.name = name; } if (email !== detailInitial.email) { dto.email = email; } await updateUser(detailTarget.id, dto); } if (didUpdateRole && detailForm.workspaceId !== null) { await updateWorkspaceMemberRole(detailForm.workspaceId, detailTarget.id, { role: detailForm.role, }); } resetUserDetails(); await loadUsers(false); } catch (err: unknown) { setDetailError(err instanceof Error ? err.message : "Failed to update user"); } finally { setIsSavingDetails(false); } } async function confirmDeactivate(): Promise { if (!deactivateTarget) { return; } try { setIsDeactivating(true); await deactivateUser(deactivateTarget.id); setDeactivateTarget(null); await loadUsers(false); setError(null); } catch (err: unknown) { setError(err instanceof Error ? err.message : "Failed to deactivate user"); } finally { setIsDeactivating(false); } } if (isAdmin === null) { return ( Checking permissions... ); } if (!isAdmin) { return ; } return (

Users

{meta ? {meta.total} total : null}

Invite and manage workspace users

{ if (!open && !isInviting) { resetInviteForm(); } setIsInviteOpen(open); }} > Invite User Invite a new user and assign their role for your default workspace.
{ void handleInviteSubmit(event); }} className="space-y-4" >
) => { setInviteForm((prev) => ({ ...prev, email: event.target.value })); }} placeholder="user@example.com" maxLength={255} required />
{defaultWorkspaceId ? (

Role will be applied on invite.

) : (

No default workspace found. User will be invited without workspace assignment.

)}
{inviteError ? (

{inviteError}

) : null}
← Back to Settings
{error ? (

{error}

) : null} {isLoading ? ( Loading users... ) : users.length === 0 ? ( No Users Yet Invite the first user to get started. ) : ( User Directory Click a user to view details or edit profile fields. {users.map((user) => { const primaryMembership = getPrimaryMembership(user); const isActive = user.deactivatedAt === null; return (
{ openUserDetails(user); }} onKeyDown={(event) => { handleUserRowKeyDown(event, user); }} >

{user.name || "Unnamed User"}

{user.email}

{primaryMembership ? toRoleLabel(primaryMembership.role) : "No role"} {isActive ? "Active" : "Inactive"} {isActive ? ( ) : null}
); })}
)} { if (!open && !isSavingDetails) { resetUserDetails(); } }} > User Details Edit profile details for {detailTarget?.email ?? "selected user"}.
{ void handleDetailSubmit(event); }} className="space-y-4" >
) => { setDetailForm((prev) => ({ ...prev, name: event.target.value })); }} placeholder="Full name" maxLength={255} disabled={isSavingDetails} required />
) => { setDetailForm((prev) => ({ ...prev, email: event.target.value })); }} placeholder="user@example.com" maxLength={255} disabled={isSavingDetails} required />
{detailForm.workspaceName ? (

Role updates apply to: {detailForm.workspaceName}

) : (

This user has no workspace membership. Role cannot be updated.

)}
{detailError !== null ? (

{detailError}

) : null}
{ if (!open && !isDeactivating) { setDeactivateTarget(null); } }} > Deactivate User Deactivate {deactivateTarget?.email}? They will no longer be able to access the system. Cancel { void confirmDeactivate(); }} > {isDeactivating ? "Deactivating..." : "Deactivate"}
); }