// Ant Design Components import { Avatar, Button, Card, Flex, Popconfirm, Progress, Skeleton, Table, TableProps, Tooltip, Typography, Input } from '@/shared/antd-imports'; // Icons import { DeleteOutlined, ExclamationCircleFilled, SyncOutlined } from '@/shared/antd-imports'; // React & Router import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; // Services & API import { projectsApiService } from '@/api/projects/projects.api.service'; import { projectMembersApiService } from '@/api/project-members/project-members.api.service'; import { useAuthService } from '@/hooks/useAuth'; // Types import { IProjectMembersViewModel, IProjectMemberViewModel } from '@/types/projectMember.types'; // Constants & Utils import { DEFAULT_PAGE_SIZE } from '@/shared/constants'; import { colors } from '../../../../styles/colors'; import logger from '@/utils/errorLogger'; // Components import EmptyListPlaceholder from '../../../../components/EmptyListPlaceholder'; import { useAppSelector } from '@/hooks/useAppSelector'; import { evt_project_members_visit } from '@/shared/worklenz-analytics-events'; import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; interface PaginationType { current: number; pageSize: number; field: string; order: string; total: number; pageSizeOptions: string[]; size: 'small' | 'default'; } const ProjectViewMembers = () => { // Hooks const { projectId } = useParams(); const { t } = useTranslation('project-view-members'); const auth = useAuthService(); const user = auth.getCurrentSession(); const isOwnerOrAdmin = auth.isOwnerOrAdmin(); const { trackMixpanelEvent } = useMixpanelTracking(); const { refreshTimestamp } = useAppSelector(state => state.projectReducer); // State const [isLoading, setIsLoading] = useState(false); const [members, setMembers] = useState(); const [pagination, setPagination] = useState({ current: 1, pageSize: DEFAULT_PAGE_SIZE, field: 'name', order: 'ascend', total: 0, pageSizeOptions: ['10', '20', '50', '100'], size: 'small', }); const [searchQuery, setSearchQuery] = useState(''); // <-- Add search state // API Functions const getProjectMembers = async (search: string = searchQuery) => { if (!projectId) return; setIsLoading(true); try { const offset = (pagination.current - 1) * pagination.pageSize; const res = await projectsApiService.getMembers( projectId, pagination.current, // index pagination.pageSize, // size // offset pagination.field, pagination.order, search ); if (res.done) { setMembers(res.body); setPagination(p => ({ ...p, total: res.body.total ?? 0 })); // update total from backend, default to 0 } } catch (error) { logger.error('Error fetching members:', error); } finally { setIsLoading(false); } }; const deleteMember = async (memberId: string | undefined) => { if (!memberId || !projectId) return; try { const res = await projectMembersApiService.deleteProjectMember(memberId, projectId); if (res.done) { void getProjectMembers(); } } catch (error) { logger.error('Error deleting member:', error); } }; // Helper Functions const checkDisabled = (record: IProjectMemberViewModel): boolean => { if (!isOwnerOrAdmin) return true; if (user?.team_member_id === record.team_member_id) return true; return false; }; const calculateProgressPercent = (completed: number = 0, total: number = 0): number => { if (total === 0) return 0; return Math.floor((completed / total) * 100); }; const handleTableChange = (tablePagination: any, filters: any, sorter: any) => { setPagination(prev => ({ ...prev, current: tablePagination.current, pageSize: tablePagination.pageSize, field: sorter.field || prev.field, order: sorter.order || prev.order, })); }; // Effects useEffect(() => { void getProjectMembers(); }, [ refreshTimestamp, projectId, pagination.current, pagination.pageSize, pagination.field, pagination.order, // searchQuery, // <-- Do NOT include here, search is triggered manually ]); useEffect(() => { trackMixpanelEvent(evt_project_members_visit); }, []); // Table Configuration const columns: TableProps['columns'] = [ { key: 'memberName', title: t('nameColumn'), dataIndex: 'name', sorter: true, sortOrder: pagination.order === 'ascend' && pagination.field === 'name' ? 'ascend' : pagination.order === 'descend' && pagination.field === 'name' ? 'descend' : null, render: (_, record: IProjectMemberViewModel) => ( {record.name?.charAt(0)} {record.name} ), }, { key: 'jobTitle', title: t('jobTitleColumn'), dataIndex: 'job_title', sorter: true, sortOrder: pagination.order === 'ascend' && pagination.field === 'job_title' ? 'ascend' : pagination.order === 'descend' && pagination.field === 'job_title' ? 'descend' : null, render: (_, record: IProjectMemberViewModel) => ( {record?.job_title || '-'} ), }, { key: 'email', title: t('emailColumn'), dataIndex: 'email', sorter: true, sortOrder: pagination.order === 'ascend' && pagination.field === 'email' ? 'ascend' : pagination.order === 'descend' && pagination.field === 'email' ? 'descend' : null, render: (_, record: IProjectMemberViewModel) => ( {record.email} ), }, { key: 'tasks', title: t('tasksColumn'), width: 90, render: (_, record: IProjectMemberViewModel) => ( {`${record.completed_tasks_count}/${record.all_tasks_count}`} ), }, { key: 'taskProgress', title: t('taskProgressColumn'), render: (_, record: IProjectMemberViewModel) => ( ), }, { key: 'access', title: t('accessColumn'), dataIndex: 'access', sorter: true, sortOrder: pagination.order === 'ascend' && pagination.field === 'access' ? 'ascend' : pagination.order === 'descend' && pagination.field === 'access' ? 'descend' : null, render: (_, record: IProjectMemberViewModel) => ( {record.access} ), }, { key: 'actionBtns', width: 80, render: (record: IProjectMemberViewModel) => ( } okText={t('deleteConfirmationOk')} cancelText={t('deleteConfirmationCancel')} onConfirm={() => deleteMember(record.id)} >