'use client'; import { useEffect, useMemo, useState } from 'react'; type UserRecord = { id: string; name: string; email: string; status: 'invited' | 'active' | 'inactive' | 'suspended'; roleId: string | null; roleName: string; createdAt: string; updatedAt: string; lastLoginAt: string | null; emailVerifiedAt: string | null; }; type RoleOption = { id: string; name: string; }; type Props = { initialUsers: UserRecord[]; initialTotal: number; initialPage: number; initialPageSize: number; initialTotalPages: number; availableRoles: RoleOption[]; }; type PanelMode = 'create' | 'edit' | null; function formatLastSeen(value: string | null, status: UserRecord['status']) { if (status === 'invited') return 'Pending invite'; if (!value) return 'Never'; const diffMs = Date.now() - new Date(value).getTime(); const minutes = Math.max(1, Math.floor(diffMs / 60000)); if (minutes < 60) return 'Just now'; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours} hour${hours === 1 ? '' : 's'} ago`; const days = Math.floor(hours / 24); return `${days} day${days === 1 ? '' : 's'} ago`; } function formatStatusLabel(status: UserRecord['status']) { switch (status) { case 'active': return 'Active'; case 'invited': return 'Invited'; case 'inactive': return 'Inactive'; case 'suspended': return 'Suspended'; } } function getStatusTone(status: UserRecord['status']) { switch (status) { case 'active': return 'success'; case 'invited': return 'warning'; case 'suspended': return 'error'; case 'inactive': return 'muted'; } } function getRoleTone(roleName: string) { const normalized = roleName.toLowerCase(); if (normalized.includes('admin')) return 'admin'; if (normalized.includes('editor')) return 'editor'; return 'agent'; } function initials(name: string) { return name .split(/\s+/) .slice(0, 2) .map((part) => part[0]?.toUpperCase() || '') .join(''); } const roleDescriptions: Record = { Admin: 'Full system access, including billing, user management, and API settings.', Editor: 'Manage broadcasts and message templates, but cannot change system settings.', Agent: 'Limited to handling conversations, contacts, and operational response flow.', }; export function UsersManagementBoard({ initialUsers, initialTotal, initialPage, initialPageSize, initialTotalPages, availableRoles, }: Props) { const [users, setUsers] = useState(initialUsers); const [total, setTotal] = useState(initialTotal); const [page, setPage] = useState(initialPage); const [pageSize] = useState(initialPageSize); const [totalPages, setTotalPages] = useState(initialTotalPages); const [search, setSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [roleFilter, setRoleFilter] = useState('all'); const [showFilters, setShowFilters] = useState(false); const [panelMode, setPanelMode] = useState(null); const [editingUserId, setEditingUserId] = useState(null); const [feedback, setFeedback] = useState(''); const [inviteName, setInviteName] = useState(''); const [inviteEmail, setInviteEmail] = useState(''); const [inviteRoleId, setInviteRoleId] = useState(availableRoles[0]?.id || ''); const [editName, setEditName] = useState(''); const [editEmail, setEditEmail] = useState(''); const [editRoleId, setEditRoleId] = useState(''); const [editStatus, setEditStatus] = useState('active'); const [isSaving, setIsSaving] = useState(false); const [isLoading, setIsLoading] = useState(false); const editingUser = useMemo( () => users.find((user) => user.id === editingUserId) ?? null, [editingUserId, users], ); const stats = useMemo(() => { const totalUsers = total; const pendingInvites = users.filter((user) => user.status === 'invited').length; const activeRecently = users.filter((user) => { if (!user.lastLoginAt) return false; return Date.now() - new Date(user.lastLoginAt).getTime() <= 24 * 60 * 60 * 1000; }).length; return { totalUsers, pendingInvites, activeRecently }; }, [total, users]); const visibleRoles = useMemo(() => { const base = availableRoles.map((role) => role.name); return base.slice(0, 3); }, [availableRoles]); useEffect(() => { const timeout = window.setTimeout(() => { setDebouncedSearch(search.trim()); setPage(1); }, 250); return () => window.clearTimeout(timeout); }, [search]); useEffect(() => { const controller = new AbortController(); async function loadUsers() { try { setIsLoading(true); const params = new URLSearchParams(); params.set('page', String(page)); params.set('limit', String(pageSize)); if (debouncedSearch) params.set('search', debouncedSearch); if (statusFilter !== 'all') params.set('status', statusFilter); if (roleFilter !== 'all') params.set('roleId', roleFilter); const response = await fetch(`/api/users?${params.toString()}`, { signal: controller.signal, cache: 'no-store', }); const payload = await response.json(); if (!response.ok) { setFeedback(payload.message || 'Failed to load users'); return; } setUsers(payload.items); setTotal(payload.total); setTotalPages(payload.totalPages); } catch (error) { if ((error as Error).name !== 'AbortError') { setFeedback('Failed to load users'); } } finally { setIsLoading(false); } } loadUsers(); return () => controller.abort(); }, [debouncedSearch, page, pageSize, roleFilter, statusFilter]); function closePanel() { setPanelMode(null); setEditingUserId(null); } function openCreatePanel() { setPanelMode('create'); setEditingUserId(null); setFeedback(''); } function beginEdit(user: UserRecord) { setPanelMode('edit'); setEditingUserId(user.id); setEditName(user.name); setEditEmail(user.email); setEditRoleId(user.roleId || ''); setEditStatus(user.status); setFeedback(''); } async function submitInvite() { setIsSaving(true); try { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: inviteName, email: inviteEmail, roleId: inviteRoleId || undefined, }), }); const payload = await response.json(); if (!response.ok) { setFeedback(payload.message || 'Failed to invite user'); return; } setFeedback( payload.emailSent ? `Invitation sent to ${payload.email}.` : `User invited, but email was not sent. Activation link: ${payload.invitationUrl}`, ); setInviteName(''); setInviteEmail(''); setInviteRoleId(availableRoles[0]?.id || ''); closePanel(); setPage(1); setDebouncedSearch(''); setSearch(''); } finally { setIsSaving(false); } } async function submitEdit() { if (!editingUser) return; setIsSaving(true); try { const response = await fetch(`/api/users/${editingUser.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: editName, email: editEmail, roleId: editRoleId || undefined, status: editStatus, }), }); const payload = await response.json(); if (!response.ok) { setFeedback(payload.message || 'Failed to update user'); return; } setUsers((current) => current.map((user) => user.id === payload.id ? { ...user, name: payload.name, email: payload.email, status: payload.status, roleId: payload.roleId, roleName: payload.roleName, lastLoginAt: payload.lastLoginAt, emailVerifiedAt: payload.emailVerifiedAt, } : user, ), ); setFeedback(`User ${payload.email} updated.`); closePanel(); } finally { setIsSaving(false); } } async function resendInvite(user: UserRecord) { setIsSaving(true); try { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: user.name, email: user.email, roleId: user.roleId || undefined, }), }); const payload = await response.json(); if (!response.ok) { setFeedback(payload.message || 'Failed to resend invite'); return; } setFeedback( payload.emailSent ? `Invitation resent to ${payload.email}.` : `Invite refreshed, but email was not sent. Activation link: ${payload.invitationUrl}`, ); } finally { setIsSaving(false); } } function resetFilters() { setSearch(''); setDebouncedSearch(''); setStatusFilter('all'); setRoleFilter('all'); setPage(1); } function exportUsers() { const params = new URLSearchParams(); if (debouncedSearch) params.set('search', debouncedSearch); if (statusFilter !== 'all') params.set('status', statusFilter); if (roleFilter !== 'all') params.set('roleId', roleFilter); window.location.href = `/api/users/export${params.size ? `?${params.toString()}` : ''}`; } return (

Users

User Management

Manage team access levels, roles, and security permissions.

Total Users

{stats.totalUsers}

trending_up Directory accounts

group

Pending Invites

{stats.pendingInvites}

schedule Awaiting response

mail

Active Sessions

{stats.activeRecently}

Current real-time activity

bolt
{panelMode === 'create' ? (

Settings / Team Management

Add New Team Member

Invite a new administrator or support agent to your business dashboard.

person_add Profile Information

) : null} {panelMode === 'edit' && editingUser ? (

Edit User

Update user access and account status

Change display details, role assignment, and lifecycle state without leaving the directory.

{formatStatusLabel(editingUser.status)}
{editingUser.emailVerifiedAt ? 'Verified' : 'Pending Verification'} Email verification
{formatLastSeen(editingUser.lastLoginAt, editingUser.status)} Last activity
warning

Danger Zone

Deleting this user is not enabled yet. For now, use `Suspended` status to revoke access safely.

) : null} {feedback ?

{feedback}

: null}

Team Members

{showFilters ? (
) : null}
{users.map((user) => ( ))} {users.length === 0 ? ( ) : null}
Name / Email Role Last Active Status Actions
{initials(user.name)}

{user.name}

{user.email}
{user.roleName.toUpperCase()} {formatLastSeen(user.lastLoginAt, user.status)}
{formatStatusLabel(user.status)}
{user.status === 'invited' ? ( ) : null}
No users matched the current filters.

Showing {users.length === 0 ? 0 : (page - 1) * pageSize + 1} to {(page - 1) * pageSize + users.length} of {total} team members

{Array.from({ length: totalPages }, (_, index) => index + 1) .slice(Math.max(0, page - 2), Math.max(0, page - 2) + 3) .map((pageNumber) => ( ))}
{isLoading ?
: null}
Role Permissions

Quick overview of what each team member can access across the WhatsApp Business platform.

{visibleRoles.map((roleName) => (
{getRoleTone(roleName) === 'admin' ? 'verified_user' : getRoleTone(roleName) === 'editor' ? 'edit_note' : 'support_agent'} {roleName.toUpperCase()}

{roleDescriptions[roleName] || 'Operational access based on the assigned role matrix.'}

))}
); }