"use client"; export const dynamic = "force-dynamic"; import { useEffect, useMemo, useState } from "react"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { useQueryClient } from "@tanstack/react-query"; import { Building2, Copy, UserPlus, Users } from "lucide-react"; import { ApiError } from "@/api/mutator"; import { type listBoardsApiV1BoardsGetResponse, useListBoardsApiV1BoardsGet, } from "@/api/generated/boards/boards"; import { type getMyOrgApiV1OrganizationsMeGetResponse, type getMyMembershipApiV1OrganizationsMeMemberGetResponse, type getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse, type listOrgInvitesApiV1OrganizationsMeInvitesGetResponse, type listOrgMembersApiV1OrganizationsMeMembersGetResponse, getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryKey, getListOrgInvitesApiV1OrganizationsMeInvitesGetQueryKey, getListOrgMembersApiV1OrganizationsMeMembersGetQueryKey, useCreateOrgInviteApiV1OrganizationsMeInvitesPost, useGetMyOrgApiV1OrganizationsMeGet, useGetMyMembershipApiV1OrganizationsMeMemberGet, useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet, useListOrgInvitesApiV1OrganizationsMeInvitesGet, useListOrgMembersApiV1OrganizationsMeMembersGet, useRevokeOrgInviteApiV1OrganizationsMeInvitesInviteIdDelete, useUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut, useUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch, } from "@/api/generated/organizations/organizations"; import type { BoardRead, OrganizationBoardAccessSpec, OrganizationInviteRead, OrganizationMemberRead, } from "@/api/generated/model"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { cn } from "@/lib/utils"; const formatTimestamp = (value?: string | null) => { if (!value) return "—"; const date = new Date(`${value}${value.endsWith("Z") ? "" : "Z"}`); if (Number.isNaN(date.getTime())) return "—"; return date.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; type AccessScope = "all" | "custom"; type BoardAccessState = Record; const buildAccessList = ( access: BoardAccessState, ): OrganizationBoardAccessSpec[] => Object.entries(access) .filter(([, entry]) => entry.read || entry.write) .map(([boardId, entry]) => ({ board_id: boardId, can_read: entry.read || entry.write, can_write: entry.write, })); const summarizeAccess = (allRead: boolean, allWrite: boolean) => { if (allRead || allWrite) { if (allRead && allWrite) return "All boards: read + write"; if (allWrite) return "All boards: write"; return "All boards: read"; } return "Selected boards"; }; const roleBadgeVariant = (role: string) => { if (role === "admin" || role === "owner") return "accent" as const; return "outline" as const; }; const defaultBoardAccess: BoardAccessState = {}; const initialsFrom = (value?: string | null) => { if (!value) return "?"; const parts = value.trim().split(/\s+/).filter(Boolean); if (parts.length === 0) return "?"; if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); }; function BoardAccessEditor({ boards, scope, onScopeChange, allRead, allWrite, onAllReadChange, onAllWriteChange, access, onAccessChange, disabled, emptyMessage, }: { boards: BoardRead[]; scope: AccessScope; onScopeChange: (scope: AccessScope) => void; allRead: boolean; allWrite: boolean; onAllReadChange: (next: boolean) => void; onAllWriteChange: (next: boolean) => void; access: BoardAccessState; onAccessChange: (next: BoardAccessState) => void; disabled?: boolean; emptyMessage?: string; }) { const handleAllReadToggle = () => { if (disabled) return; const next = !allRead; onAllReadChange(next); if (!next && allWrite) { onAllWriteChange(false); } }; const handleAllWriteToggle = () => { if (disabled) return; const next = !allWrite; onAllWriteChange(next); if (next && !allRead) { onAllReadChange(true); } }; const updateBoardAccess = ( boardId: string, next: { read: boolean; write: boolean }, ) => { onAccessChange({ ...access, [boardId]: { read: next.read || next.write, write: next.write, }, }); }; const handleBoardReadToggle = (boardId: string) => { if (disabled) return; const current = access[boardId] ?? { read: false, write: false }; const nextRead = !current.read; const nextWrite = nextRead ? current.write : false; updateBoardAccess(boardId, { read: nextRead, write: nextWrite }); }; const handleBoardWriteToggle = (boardId: string) => { if (disabled) return; const current = access[boardId] ?? { read: false, write: false }; const nextWrite = !current.write; const nextRead = nextWrite ? true : current.read; updateBoardAccess(boardId, { read: nextRead, write: nextWrite }); }; return (

Board access

{scope === "all" ? (
Write access implies read permissions.
) : (
{boards.length === 0 ? (
{emptyMessage ?? "No boards available yet."}
) : (
{boards.map((board) => { const entry = access[board.id] ?? { read: false, write: false, }; return ( ); })}
Board Read Write
{board.name}
{board.slug}
handleBoardReadToggle(board.id)} disabled={disabled} /> handleBoardWriteToggle(board.id)} disabled={disabled} />
)}
)}
); } export default function OrganizationPage() { const { isSignedIn } = useAuth(); const queryClient = useQueryClient(); const [inviteDialogOpen, setInviteDialogOpen] = useState(false); const [inviteEmail, setInviteEmail] = useState(""); const [inviteRole, setInviteRole] = useState("member"); const [inviteScope, setInviteScope] = useState("all"); const [inviteAllRead, setInviteAllRead] = useState(true); const [inviteAllWrite, setInviteAllWrite] = useState(false); const [inviteAccess, setInviteAccess] = useState(defaultBoardAccess); const [inviteError, setInviteError] = useState(null); const [copiedInviteId, setCopiedInviteId] = useState(null); const [accessDialogOpen, setAccessDialogOpen] = useState(false); const [activeMemberId, setActiveMemberId] = useState(null); const [accessScope, setAccessScope] = useState("all"); const [accessAllRead, setAccessAllRead] = useState(false); const [accessAllWrite, setAccessAllWrite] = useState(false); const [accessRole, setAccessRole] = useState("member"); const [accessMap, setAccessMap] = useState(defaultBoardAccess); const [accessError, setAccessError] = useState(null); const orgQuery = useGetMyOrgApiV1OrganizationsMeGet< getMyOrgApiV1OrganizationsMeGetResponse, ApiError >({ query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", }, }); const membersQuery = useListOrgMembersApiV1OrganizationsMeMembersGet< listOrgMembersApiV1OrganizationsMeMembersGetResponse, ApiError >( { limit: 200 }, { query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", }, }, ); const boardsQuery = useListBoardsApiV1BoardsGet< listBoardsApiV1BoardsGetResponse, ApiError >( { limit: 200 }, { query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", }, }, ); const membershipQuery = useGetMyMembershipApiV1OrganizationsMeMemberGet< getMyMembershipApiV1OrganizationsMeMemberGetResponse, ApiError >({ query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", }, }); const isAdmin = membershipQuery.data?.status === 200 && (membershipQuery.data.data.role === "admin" || membershipQuery.data.data.role === "owner"); const invitesQuery = useListOrgInvitesApiV1OrganizationsMeInvitesGet< listOrgInvitesApiV1OrganizationsMeInvitesGetResponse, ApiError >( { limit: 200 }, { query: { enabled: Boolean(isSignedIn && isAdmin), refetchOnMount: "always", retry: false, }, }, ); const members = useMemo(() => { if (membersQuery.data?.status !== 200) return []; return membersQuery.data.data.items ?? []; }, [membersQuery.data]); const invites = useMemo(() => { if (invitesQuery.data?.status !== 200) return []; return invitesQuery.data.data.items ?? []; }, [invitesQuery.data]); const boards = useMemo(() => { if (boardsQuery.data?.status !== 200) return []; return boardsQuery.data.data.items ?? []; }, [boardsQuery.data]); const memberDetailsQuery = useGetOrgMemberApiV1OrganizationsMeMembersMemberIdGet< getOrgMemberApiV1OrganizationsMeMembersMemberIdGetResponse, ApiError >(activeMemberId ?? "", { query: { enabled: Boolean(activeMemberId && accessDialogOpen), }, }); const createInviteMutation = useCreateOrgInviteApiV1OrganizationsMeInvitesPost({ mutation: { onSuccess: (result) => { if (result.status === 200) { setInviteEmail(""); setInviteRole("member"); setInviteScope("all"); setInviteAllRead(true); setInviteAllWrite(false); setInviteAccess(defaultBoardAccess); setInviteError(null); queryClient.invalidateQueries({ queryKey: getListOrgInvitesApiV1OrganizationsMeInvitesGetQueryKey( { limit: 200, }, ), }); setInviteDialogOpen(false); } }, onError: (err) => { setInviteError(err.message || "Unable to create invite."); }, }, }); const revokeInviteMutation = useRevokeOrgInviteApiV1OrganizationsMeInvitesInviteIdDelete({ mutation: { onSuccess: () => { queryClient.invalidateQueries({ queryKey: getListOrgInvitesApiV1OrganizationsMeInvitesGetQueryKey({ limit: 200, }), }); }, }, }); const updateMemberAccessMutation = useUpdateMemberAccessApiV1OrganizationsMeMembersMemberIdAccessPut( { mutation: { onSuccess: () => { queryClient.invalidateQueries({ queryKey: getListOrgMembersApiV1OrganizationsMeMembersGetQueryKey( { limit: 200, }, ), }); if (activeMemberId) { queryClient.invalidateQueries({ queryKey: getGetOrgMemberApiV1OrganizationsMeMembersMemberIdGetQueryKey( activeMemberId, ), }); } }, }, }, ); const updateMemberRoleMutation = useUpdateOrgMemberApiV1OrganizationsMeMembersMemberIdPatch({ mutation: { onSuccess: () => { queryClient.invalidateQueries({ queryKey: getListOrgMembersApiV1OrganizationsMeMembersGetQueryKey({ limit: 200, }), }); }, }, }); useEffect(() => { if (memberDetailsQuery.data?.status !== 200) return; const member = memberDetailsQuery.data.data; setAccessRole(member.role); const isAll = member.all_boards_read || member.all_boards_write; setAccessScope(isAll ? "all" : "custom"); setAccessAllRead(member.all_boards_read); setAccessAllWrite(member.all_boards_write); const nextAccess: BoardAccessState = {}; for (const entry of member.board_access ?? []) { nextAccess[entry.board_id] = { read: entry.can_read || entry.can_write, write: entry.can_write, }; } setAccessMap(nextAccess); setAccessError(null); }, [memberDetailsQuery.data]); useEffect(() => { if (!accessDialogOpen) { setActiveMemberId(null); setAccessError(null); } }, [accessDialogOpen]); useEffect(() => { if (!inviteDialogOpen) { setInviteError(null); } }, [inviteDialogOpen]); const orgName = orgQuery.data?.status === 200 ? orgQuery.data.data.name : "Organization"; const handleInviteSubmit = (event: React.FormEvent) => { event.preventDefault(); if (!isSignedIn || !isAdmin) return; const trimmedEmail = inviteEmail.trim().toLowerCase(); if (!trimmedEmail || !trimmedEmail.includes("@")) { setInviteError("Enter a valid email address."); return; } const hasAllAccess = inviteScope === "all" && (inviteAllRead || inviteAllWrite); const inviteAccessList = buildAccessList(inviteAccess); const hasCustomAccess = inviteScope === "custom" && inviteAccessList.length > 0; if (!hasAllAccess && !hasCustomAccess) { setInviteError("Select read or write access for at least one board."); return; } setInviteError(null); createInviteMutation.mutate({ data: { invited_email: trimmedEmail, role: inviteRole, all_boards_read: inviteScope === "all" ? inviteAllRead : false, all_boards_write: inviteScope === "all" ? inviteAllWrite : false, board_access: inviteScope === "custom" ? inviteAccessList : [], }, }); }; const handleCopyInvite = async (invite: OrganizationInviteRead) => { try { const baseUrl = typeof window !== "undefined" ? window.location.origin : ""; const inviteUrl = baseUrl ? `${baseUrl}/invite?token=${invite.token}` : invite.token; let copied = false; if (typeof navigator !== "undefined" && navigator.clipboard) { try { await navigator.clipboard.writeText(inviteUrl); copied = true; } catch { copied = false; } } if (!copied && typeof document !== "undefined") { const textarea = document.createElement("textarea"); textarea.value = inviteUrl; textarea.setAttribute("readonly", "true"); textarea.style.position = "absolute"; textarea.style.left = "-9999px"; document.body.appendChild(textarea); textarea.select(); copied = document.execCommand("copy"); document.body.removeChild(textarea); } if (copied) { setCopiedInviteId(invite.id); setTimeout(() => setCopiedInviteId(null), 2000); return; } if (typeof window !== "undefined") { window.prompt("Copy invite link:", inviteUrl); } } catch { setCopiedInviteId(null); } }; const openAccessDialog = (memberId: string) => { setActiveMemberId(memberId); setAccessDialogOpen(true); }; const handleSaveAccess = async () => { if (!activeMemberId || !isAdmin) return; const hasAllAccess = accessScope === "all" && (accessAllRead || accessAllWrite); const accessList = buildAccessList(accessMap); const hasCustomAccess = accessScope === "custom" && accessList.length > 0; if (!hasAllAccess && !hasCustomAccess) { setAccessError("Select read or write access for at least one board."); return; } setAccessError(null); try { if (memberDetailsQuery.data?.status === 200) { const member = memberDetailsQuery.data.data; if (member.role !== accessRole) { await updateMemberRoleMutation.mutateAsync({ memberId: member.id, data: { role: accessRole }, }); } } await updateMemberAccessMutation.mutateAsync({ memberId: activeMemberId, data: { all_boards_read: accessScope === "all" ? accessAllRead : false, all_boards_write: accessScope === "all" ? accessAllWrite : false, board_access: accessScope === "custom" ? accessList : [], }, }); setAccessDialogOpen(false); } catch (err) { setAccessError( err instanceof Error ? err.message : "Unable to update member access.", ); } }; const memberAccessSummary = (member: OrganizationMemberRead) => summarizeAccess(member.all_boards_read, member.all_boards_write); const memberDisplay = (member: OrganizationMemberRead) => { const primary = member.user?.name || member.user?.preferred_name || member.user?.email || member.user_id; const secondary = member.user?.email ?? "No email on file"; return { primary, secondary, initials: initialsFrom(primary), }; }; return (

Sign in to manage your organization.

Organization

{orgName}

Manage members and board access across your workspace.

{members.length} {" "} members {boards.length} {" "} boards {invites.length} {" "} pending

Members & invites

Invite teammates and tune their board permissions.

{members.length + invites.length} total
{membersQuery.isLoading ? ( ) : null} {members.map((member) => { const display = memberDisplay(member); return ( ); })} {isAdmin && invitesQuery.isLoading ? ( ) : null} {isAdmin ? invites.map((invite) => ( )) : null} {!membersQuery.isLoading && (!isAdmin || !invitesQuery.isLoading) && members.length === 0 && (!isAdmin || invites.length === 0) ? ( ) : null}
Member Status Access Actions
Loading members...
{display.initials}
{display.primary}
{display.secondary}
{member.role} {memberAccessSummary(member)} {isAdmin ? ( ) : ( Admin only )}
Loading invites...
{initialsFrom(invite.invited_email)}
{invite.invited_email}
Invited {formatTimestamp(invite.created_at)}
Pending {invite.role}
{summarizeAccess( invite.all_boards_read, invite.all_boards_write, )}
No members or invites yet.
Invite a member Grant access to all boards or select specific workspaces. {isAdmin ? (
setInviteEmail(event.target.value)} placeholder="name@company.com" type="email" required />
{inviteError ? (

{inviteError}

) : null} ) : (
Only organization admins can invite new members.
)}
Manage member access Adjust board permissions and role for this teammate. {memberDetailsQuery.isLoading ? (
Loading member access...
) : memberDetailsQuery.data?.status === 200 ? (

{memberDetailsQuery.data.data.user?.name || memberDetailsQuery.data.data.user?.preferred_name || memberDetailsQuery.data.data.user?.email || memberDetailsQuery.data.data.user_id}

{memberDetailsQuery.data.data.user?.email ?? "No email on file"}

{accessError ? (

{accessError}

) : null}
) : (
Unable to load member access.
)}
); }