import { DeleteOutlined, EditOutlined, ExclamationCircleFilled, SearchOutlined, SyncOutlined, UserSwitchOutlined, } from '@ant-design/icons'; import { Avatar, Badge, Button, Card, Flex, Input, Popconfirm, Table, TableProps, Tooltip, Typography, } from 'antd'; import { createPortal } from 'react-dom'; import { useEffect, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useDocumentTitle } from '@/hooks/useDoumentTItle'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; import UpdateMemberDrawer from '@/components/settings/update-member-drawer'; import { toggleInviteMemberDrawer, toggleUpdateMemberDrawer, } from '@features/settings/member/memberSlice'; import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types'; import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types'; import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS } from '@/shared/constants'; import { teamMembersApiService } from '@/api/team-members/teamMembers.api.service'; import { colors } from '@/styles/colors'; const TeamMembersSettings = () => { const { t } = useTranslation('settings/team-members'); const dispatch = useAppDispatch(); const { socket } = useSocket(); const refreshTeamMembers = useAppSelector(state => state.memberReducer.refreshTeamMembers); // Listen to refresh flag useDocumentTitle(t('title') || 'Team Members'); const [model, setModel] = useState({ total: 0, data: [] }); const [searchQuery, setSearchQuery] = useState(''); const [isLoading, setIsLoading] = useState(false); const [selectedMemberId, setSelectedMemberId] = useState(null); const [pagination, setPagination] = useState({ current: 1, pageSize: DEFAULT_PAGE_SIZE, field: 'name', order: 'asc', }); const getTeamMembers = useCallback(async () => { try { setIsLoading(true); const res = await teamMembersApiService.get( pagination.current, pagination.pageSize, pagination.field, pagination.order, searchQuery ); if (res.done) { setModel(res.body); } } catch (error) { console.error('Error fetching team members:', error); } finally { setIsLoading(false); } }, [pagination, searchQuery]); const handleStatusChange = async (record: ITeamMemberViewModel) => { try { setIsLoading(true); const res = await teamMembersApiService.toggleMemberActiveStatus( record.id || '', record.active as boolean, record.email || '' ); if (res.done) { await getTeamMembers(); } } finally { setIsLoading(false); } }; const handleDeleteMember = async (record: ITeamMemberViewModel) => { if (!record.id) return; try { setIsLoading(true); const res = await teamMembersApiService.delete(record.id); if (res.done) { await getTeamMembers(); } } finally { setIsLoading(false); } }; const handleRoleUpdate = useCallback((memberId: string, newRoleName: string) => { setModel(prevModel => ({ ...prevModel, data: prevModel.data?.map(member => member.id === memberId ? { ...member, role_name: newRoleName } : member ), })); }, []); const handleRefresh = useCallback(() => { setIsLoading(true); getTeamMembers().finally(() => setIsLoading(false)); }, [getTeamMembers]); const handleMemberClick = useCallback( (memberId: string) => { setSelectedMemberId(memberId); dispatch(toggleUpdateMemberDrawer()); }, [dispatch] ); const handleTableChange = useCallback((newPagination: any, filters: any, sorter: any) => { setPagination(prev => ({ ...prev, current: newPagination.current, pageSize: newPagination.pageSize, field: sorter.field || 'name', order: sorter.order === 'ascend' ? 'asc' : 'desc', })); }, []); useEffect(() => { if (socket) { const handleRoleChange = (data: { memberId: string; role_name: string }) => { handleRoleUpdate(data.memberId, data.role_name); }; socket.on(SocketEvents.TEAM_MEMBER_ROLE_CHANGE.toString(), handleRoleChange); return () => { socket.off(SocketEvents.TEAM_MEMBER_ROLE_CHANGE.toString(), handleRoleChange); }; } }, [socket, handleRoleUpdate]); useEffect(() => { handleRefresh(); }, [refreshTeamMembers, handleRefresh]); useEffect(() => { getTeamMembers(); }, [getTeamMembers]); const getColor = useCallback((role: string | undefined) => { switch (role?.toLowerCase()) { case 'owner': return colors.skyBlue; case 'member': return colors.lightGray; case 'admin': return colors.yellow; default: return colors.darkGray; } }, []); const columns: TableProps['columns'] = [ { key: 'name', dataIndex: 'name', title: t('nameColumn'), defaultSortOrder: 'ascend', sorter: true, onCell: (record: ITeamMemberViewModel) => ({ onClick: () => handleMemberClick(record.id || ''), style: { cursor: 'pointer' }, }), render: (_, record: ITeamMemberViewModel) => ( {record.name?.charAt(0)} {record.name} {record.is_online && } {!record.active && ( {t('deactivatedText')} )} ), }, { key: 'projects_count', dataIndex: 'projects_count', title: t('projectsColumn'), sorter: true, onCell: (record: ITeamMemberViewModel) => ({ onClick: () => handleMemberClick(record.id || ''), style: { cursor: 'pointer' }, }), render: (_, record: ITeamMemberViewModel) => ( {record.projects_count} ), }, { key: 'email', dataIndex: 'email', title: t('emailColumn'), sorter: true, onCell: (record: ITeamMemberViewModel) => ({ onClick: () => handleMemberClick(record.id || ''), style: { cursor: 'pointer' }, }), render: (_, record: ITeamMemberViewModel) => (
{record.email} {record.pending_invitation && ( {t('pendingInvitationText')} )}
), }, { key: 'role_name', dataIndex: 'role_name', title: t('teamAccessColumn'), sorter: true, onCell: (record: ITeamMemberViewModel) => ({ onClick: () => handleMemberClick(record.id || ''), style: { cursor: 'pointer' }, }), render: (_, record: ITeamMemberViewModel) => ( {record.role_name} ), }, { key: 'actionBtns', width: 120, render: (record: ITeamMemberViewModel) => record.role_name !== 'owner' && ( record.id} onChange={handleTableChange} loading={isLoading} pagination={{ current: pagination.current, pageSize: pagination.pageSize, showSizeChanger: true, defaultPageSize: DEFAULT_PAGE_SIZE, pageSizeOptions: PAGE_SIZE_OPTIONS, size: 'small', total: model.total, showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} items`, }} scroll={{ x: 'max-content' }} /> {createPortal( , document.body )} ); }; export default TeamMembersSettings;