import { useCallback, useEffect, useMemo, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faKey, faToggleOn, faUserPlus, faUserShield } from '@fortawesome/pro-duotone-svg-icons'; import { Avatar } from '@/components/base/avatar/avatar'; import { Badge } from '@/components/base/badges/badges'; import { Button } from '@/components/base/buttons/button'; import { Select } from '@/components/base/select/select'; import { Table, TableCard } from '@/components/application/table/table'; import { SlideoutMenu } from '@/components/application/slideout-menus/slideout-menu'; import { TopBar } from '@/components/layout/top-bar'; import { EmployeeCreateForm, emptyEmployeeCreateFormValues, type EmployeeCreateFormValues, type RoleOption, } from '@/components/forms/employee-create-form'; import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; import { getInitials } from '@/lib/format'; // /settings/team — in-place employee management. Fetches real roles + SIP // seats from the platform and uses the sidecar's /api/team/members // endpoint to create workspace members directly with a temp password // that the admin hands out (no email invitations — see // feedback-no-invites in memory). Also lets admins change a member's // role via updateWorkspaceMemberRole. type MemberRole = { id: string; label: string; }; type WorkspaceMember = { id: string; name: { firstName: string; lastName: string } | null; userEmail: string; avatarUrl: string | null; // Platform returns null (not []) for members with no role assigned — // optional-chain when reading. roles: MemberRole[] | null; }; type CreatedMemberResponse = { id: string; userEmail: string; firstName: string; lastName: string; roleId: string; }; // Combined query — workspace members + assignable roles. Bundled to // save a round-trip and keep the table consistent across the join. const TEAM_QUERY = `{ workspaceMembers(first: 100) { edges { node { id name { firstName lastName } userEmail avatarUrl roles { id label } } } } getRoles { id label description canBeAssignedToUsers } }`; export const TeamSettingsPage = () => { const [members, setMembers] = useState([]); const [roles, setRoles] = useState([]); const [loading, setLoading] = useState(true); const [createOpen, setCreateOpen] = useState(false); const [createValues, setCreateValues] = useState( emptyEmployeeCreateFormValues, ); const [isCreating, setIsCreating] = useState(false); const fetchData = useCallback(async () => { try { const data = await apiClient.graphql<{ workspaceMembers: { edges: { node: WorkspaceMember }[] }; getRoles: { id: string; label: string; description: string | null; canBeAssignedToUsers: boolean; }[]; }>(TEAM_QUERY, undefined, { silent: true }); setMembers(data.workspaceMembers.edges.map((e) => e.node)); const assignable = data.getRoles.filter((r) => r.canBeAssignedToUsers); setRoles( assignable.map((r) => ({ id: r.id, label: r.label, supportingText: r.description ?? undefined, })), ); } catch { // silently fail } finally { setLoading(false); } }, []); useEffect(() => { fetchData(); }, [fetchData]); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); const PAGE_SIZE = 10; const filtered = useMemo(() => { if (!search.trim()) return members; const q = search.toLowerCase(); return members.filter((m) => { const name = `${m.name?.firstName ?? ''} ${m.name?.lastName ?? ''}`.toLowerCase(); return name.includes(q) || m.userEmail.toLowerCase().includes(q); }); }, [members, search]); const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); const handleResetPassword = (member: WorkspaceMember) => { notify.info('Password Reset', `Password reset link would be sent to ${member.userEmail}`); }; const handleChangeRole = async (memberId: string, newRoleId: string) => { try { await apiClient.graphql( `mutation UpdateRole($workspaceMemberId: UUID!, $roleId: UUID!) { updateWorkspaceMemberRole(workspaceMemberId: $workspaceMemberId, roleId: $roleId) { id } }`, { workspaceMemberId: memberId, roleId: newRoleId }, ); const newRole = roles.find((r) => r.id === newRoleId); setMembers((prev) => prev.map((m) => m.id === memberId ? { ...m, roles: newRole ? [{ id: newRole.id, label: newRole.label }] : [] } : m, ), ); notify.success('Role updated', newRole ? `Role set to ${newRole.label}` : 'Role updated'); } catch (err) { console.error('[team] role update failed', err); } }; const handleCreateMember = async (close: () => void) => { const firstName = createValues.firstName.trim(); const email = createValues.email.trim(); if (!firstName) { notify.error('First name is required'); return; } if (!email) { notify.error('Email is required'); return; } if (!createValues.password) { notify.error('Temporary password is required'); return; } if (!createValues.roleId) { notify.error('Pick a role'); return; } setIsCreating(true); try { await apiClient.post('/api/team/members', { firstName, lastName: createValues.lastName.trim(), email, password: createValues.password, roleId: createValues.roleId, }); notify.success( 'Employee created', `${firstName} ${createValues.lastName.trim()}`.trim() || email, ); setCreateValues(emptyEmployeeCreateFormValues); await fetchData(); close(); } catch (err) { console.error('[team] create failed', err); } finally { setIsCreating(false); } }; return (
{ setSearch(e.target.value); setPage(1); }} className="w-full rounded-lg border border-secondary bg-primary px-3 py-1.5 text-sm text-primary placeholder:text-placeholder outline-none focus:border-brand focus:ring-2 focus:ring-brand-100" />
} /> {loading ? (

Loading employees...

) : ( <> {(member) => { const firstName = member.name?.firstName ?? ''; const lastName = member.name?.lastName ?? ''; const fullName = `${firstName} ${lastName}`.trim() || 'Unnamed'; const initials = getInitials(firstName || '?', lastName || '?'); const memberRoles = member.roles ?? []; const currentRoleId = memberRoles[0]?.id ?? null; return (
{fullName}
{member.userEmail} {roles.length > 0 ? (
) : memberRoles.length > 0 ? ( {memberRoles[0].label} ) : ( No role )}
Active
); }}
{totalPages > 1 && (
{(page - 1) * PAGE_SIZE + 1}–{Math.min(page * PAGE_SIZE, filtered.length)} of{' '} {filtered.length}
)} )}
{({ close }) => ( <>

Add employee

Create supervisors, CC agents and admins in place

The employee logs in with this email and the temporary password you set. Share both with them directly — no email is sent.

)}
); };