feat(project-ratecard-member): add members to the project ratecard func (FE)

This commit is contained in:
shancds
2025-05-23 09:34:02 +05:30
parent 1a5f6d54ed
commit 22d0fc7049
2 changed files with 68 additions and 82 deletions

View File

@@ -9,8 +9,7 @@ import Button from 'antd/es/button';
import Empty from 'antd/es/empty'; import Empty from 'antd/es/empty';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import SingleAvatar from '../common/single-avatar/single-avatar'; import SingleAvatar from '../common/single-avatar/single-avatar';
import { JobRoleType } from '@/types/project/ratecard.types'; import { IProjectMemberViewModel } from '@/types/projectMember.types';
import { IProjectMembersViewModel, IProjectMemberViewModel } from '@/types/projectMember.types';
interface RateCardAssigneeSelectorProps { interface RateCardAssigneeSelectorProps {
projectId: string; projectId: string;
@@ -23,13 +22,17 @@ const RateCardAssigneeSelector = ({
projectId, projectId,
onChange, onChange,
selectedMemberIds = [], selectedMemberIds = [],
memberlist, memberlist = [],
}: RateCardAssigneeSelectorProps) => { }: RateCardAssigneeSelectorProps) => {
const membersInputRef = useRef<InputRef>(null); const membersInputRef = useRef<InputRef>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [members, setMembers] = useState<IProjectMemberViewModel[]>(memberlist || []); const [members, setMembers] = useState<IProjectMemberViewModel[]>(memberlist);
const [isLoading, setIsLoading] = useState(false);
const filteredMembers = members.filter(member => useEffect(() => {
setMembers(memberlist);
}, [memberlist]);
const filteredMembers = members.filter((member) =>
member.name?.toLowerCase().includes(searchQuery.toLowerCase()) member.name?.toLowerCase().includes(searchQuery.toLowerCase())
); );
@@ -38,45 +41,40 @@ const RateCardAssigneeSelector = ({
<Input <Input
ref={membersInputRef} ref={membersInputRef}
value={searchQuery} value={searchQuery}
onChange={e => setSearchQuery(e.currentTarget.value)} onChange={(e) => setSearchQuery(e.currentTarget.value)}
placeholder="Search members" placeholder="Search members"
/> />
<List style={{ padding: 0, overflow: 'auto' }}> <List style={{ padding: 0, maxHeight: 200, overflow: 'auto' }}>
{filteredMembers.length ? ( {filteredMembers.length ? (
filteredMembers.map(member => ( filteredMembers.map((member) => (
<List.Item <List.Item
key={member.id} key={member.id}
style={{ style={{
display: 'flex', display: 'flex',
gap: 8, gap: 8,
alignItems: 'flex-start', // changed from 'l' to 'flex-start' for left alignment alignItems: 'center',
padding: '4px 8px', padding: '4px 8px',
border: 'none', border: 'none',
cursor: member.pending_invitation ? 'not-allowed' : 'pointer',
opacity: member.pending_invitation ? 0.5 : 1, opacity: member.pending_invitation ? 0.5 : 1,
justifyContent: 'flex-start', // ensure content is aligned left justifyContent: 'flex-start',
textAlign: 'left', // ensure text is aligned left textAlign: 'left',
}} }}
onClick={() => !member.pending_invitation && onChange?.(member.id || '')}
> >
<Checkbox <Checkbox
checked={selectedMemberIds.includes(member.id || '')} checked={selectedMemberIds.includes(member.id || '')}
disabled={member.pending_invitation} disabled={member.pending_invitation}
onChange={() => onChange?.(member.id || '')} onChange={() => onChange?.(member.id || '')}
/> />
<div> <SingleAvatar
<SingleAvatar avatarUrl={member.avatar_url}
avatarUrl={member.avatar_url} name={member.name}
name={member.name} email={member.email}
email={member.email} />
/>
</div>
<span>{member.name}</span> <span>{member.name}</span>
{/* <span style={{ fontSize: 12, color: '#888' }}>{member.email}</span> */}
</List.Item> </List.Item>
)) ))
) : ( ) : (
<Empty /> <Empty description="No members found" />
)} )}
</List> </List>
</Card> </Card>
@@ -87,7 +85,7 @@ const RateCardAssigneeSelector = ({
overlayClassName="custom-dropdown" overlayClassName="custom-dropdown"
trigger={['click']} trigger={['click']}
dropdownRender={() => dropdownContent} dropdownRender={() => dropdownContent}
onOpenChange={open => { onOpenChange={(open) => {
if (open) setTimeout(() => membersInputRef.current?.focus(), 0); if (open) setTimeout(() => membersInputRef.current?.focus(), 0);
}} }}
> >
@@ -101,4 +99,4 @@ const RateCardAssigneeSelector = ({
); );
}; };
export default RateCardAssigneeSelector; export default RateCardAssigneeSelector;

View File

@@ -18,9 +18,7 @@ import RateCardAssigneeSelector from '@/components/project-ratecard/ratecard-ass
import { projectsApiService } from '@/api/projects/projects.api.service'; import { projectsApiService } from '@/api/projects/projects.api.service';
import { IProjectMemberViewModel } from '@/types/projectMember.types'; import { IProjectMemberViewModel } from '@/types/projectMember.types';
const RatecardTable: React.FC = () => { const RatecardTable: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation('project-view-finance'); const { t } = useTranslation('project-view-finance');
const { projectId } = useParams(); const { projectId } = useParams();
@@ -33,16 +31,17 @@ const RatecardTable: React.FC = () => {
// Local state for editing // Local state for editing
const [roles, setRoles] = useState<JobRoleType[]>(rolesRedux); const [roles, setRoles] = useState<JobRoleType[]>(rolesRedux);
const [addingRow, setAddingRow] = useState<boolean>(false); const [addingRow, setAddingRow] = useState<boolean>(false);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [jobTitles, setJobTitles] = useState<RatecardType[]>([]); const [jobTitles, setJobTitles] = useState<RatecardType[]>([]);
const [members, setMembers] = useState<IProjectMemberViewModel[]>([]); const [members, setMembers] = useState<IProjectMemberViewModel[]>([]);
const [isLoadingMembers, setIsLoading] = useState(false); const [isLoadingMembers, setIsLoading] = useState(false);
const pagination = { const pagination = {
current: 1, current: 1,
pageSize: 100, pageSize: 1000,
field: 'name', field: 'name',
order: 'asc', order: 'asc',
}; };
const getProjectMembers = async () => { const getProjectMembers = async () => {
if (!projectId) return; if (!projectId) return;
setIsLoading(true); setIsLoading(true);
@@ -67,8 +66,7 @@ const RatecardTable: React.FC = () => {
useEffect(() => { useEffect(() => {
getProjectMembers(); getProjectMembers();
// eslint-disable-next-line }, [projectId]);
}, []);
// Fetch job titles for selection // Fetch job titles for selection
useEffect(() => { useEffect(() => {
@@ -81,7 +79,7 @@ const RatecardTable: React.FC = () => {
// Sync local roles with redux roles // Sync local roles with redux roles
useEffect(() => { useEffect(() => {
setRoles(rolesRedux); setRoles(rolesRedux);
}, [rolesRedux, dispatch]); }, [rolesRedux]);
// Fetch roles on mount // Fetch roles on mount
useEffect(() => { useEffect(() => {
@@ -98,7 +96,6 @@ const RatecardTable: React.FC = () => {
// Save all roles (bulk update) // Save all roles (bulk update)
const handleSaveAll = () => { const handleSaveAll = () => {
if (projectId) { if (projectId) {
// Only send roles with job_title_id and rate
const filteredRoles = roles const filteredRoles = roles
.filter((r) => r.job_title_id && typeof r.rate !== 'undefined') .filter((r) => r.job_title_id && typeof r.rate !== 'undefined')
.map((r) => ({ .map((r) => ({
@@ -114,14 +111,11 @@ const RatecardTable: React.FC = () => {
const handleSelectJobTitle = async (jobTitleId: string) => { const handleSelectJobTitle = async (jobTitleId: string) => {
const jobTitle = jobTitles.find((jt) => jt.id === jobTitleId); const jobTitle = jobTitles.find((jt) => jt.id === jobTitleId);
if (!jobTitle || !projectId) return; if (!jobTitle || !projectId) return;
// Prevent duplicates
if (roles.some((r) => r.job_title_id === jobTitleId)) return; if (roles.some((r) => r.job_title_id === jobTitleId)) return;
// Dispatch and wait for result
const resultAction = await dispatch( const resultAction = await dispatch(
insertProjectRateCardRole({ project_id: projectId, job_title_id: jobTitleId, rate: 0 }) insertProjectRateCardRole({ project_id: projectId, job_title_id: jobTitleId, rate: 0 })
); );
// If fulfilled, update local state with returned id
if (insertProjectRateCardRole.fulfilled.match(resultAction)) { if (insertProjectRateCardRole.fulfilled.match(resultAction)) {
const newRole = resultAction.payload; const newRole = resultAction.payload;
setRoles([ setRoles([
@@ -131,6 +125,7 @@ const RatecardTable: React.FC = () => {
job_title_id: newRole.job_title_id, job_title_id: newRole.job_title_id,
jobtitle: newRole.jobtitle, jobtitle: newRole.jobtitle,
rate: newRole.rate, rate: newRole.rate,
members: [], // Initialize members array
}, },
]); ]);
} }
@@ -150,18 +145,40 @@ const RatecardTable: React.FC = () => {
if (record.id) { if (record.id) {
dispatch(deleteProjectRateCardRoleById(record.id)); dispatch(deleteProjectRateCardRoleById(record.id));
} else { } else {
// Remove unsaved row
setRoles(roles.filter((_, idx) => idx !== index)); setRoles(roles.filter((_, idx) => idx !== index));
} }
}; };
// Handle member change
const handleMemberChange = (memberId: string, rowIndex: number, record: JobRoleType) => {
if (!projectId && !memberId) return;
setRoles((prev) =>
prev.map((role, idx) => {
if (idx !== rowIndex) return role;
const members = Array.isArray(role.members) ? [...role.members] : [];
const memberIdx = members.indexOf(memberId);
if (memberIdx > -1) {
members.splice(memberIdx, 1); // Remove if exists
} else {
members.push(memberId); // Add if not exists
}
return { ...role, members };
})
);
// Log the required values
console.log({
project_id: projectId,
id: memberId,
project_rate_card_role_id: record.id,
});
};
// Columns // Columns
const columns: TableProps<JobRoleType>['columns'] = [ const columns: TableProps<JobRoleType>['columns'] = [
{ {
title: t('jobTitleColumn'), title: t('jobTitleColumn'),
dataIndex: 'jobtitle', dataIndex: 'jobtitle',
render: (text: string, record: JobRoleType, index: number) => { render: (text: string, record: JobRoleType, index: number) => {
// Only show Select if addingRow and this is the last row (new row)
if (addingRow && index === roles.length) { if (addingRow && index === roles.length) {
return ( return (
<Select <Select
@@ -173,12 +190,12 @@ const RatecardTable: React.FC = () => {
onChange={handleSelectJobTitle} onChange={handleSelectJobTitle}
onBlur={() => setAddingRow(false)} onBlur={() => setAddingRow(false)}
filterOption={(input, option) => filterOption={(input, option) =>
(option?.children as string).toLowerCase().includes(input.toLowerCase()) (option?.children as string)?.toLowerCase().includes(input.toLowerCase())
} }
> >
{jobTitles {jobTitles
.filter(jt => !roles.some((role) => role.job_title_id === jt.id)) .filter((jt) => !roles.some((role) => role.job_title_id === jt.id))
.map(jt => ( .map((jt) => (
<Select.Option key={jt.id} value={jt.id!}> <Select.Option key={jt.id} value={jt.id!}>
{jt.name} {jt.name}
</Select.Option> </Select.Option>
@@ -186,14 +203,7 @@ const RatecardTable: React.FC = () => {
</Select> </Select>
); );
} }
return ( return <span>{text || record.name}</span>;
<span
style={{ cursor: 'pointer' }}
onClick={() => setEditingIndex(index)}
>
{text || record.name}
</span>
);
}, },
}, },
{ {
@@ -222,16 +232,18 @@ const RatecardTable: React.FC = () => {
render: (memberscol: string[] | null | undefined, record: JobRoleType, index: number) => ( render: (memberscol: string[] | null | undefined, record: JobRoleType, index: number) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, position: 'relative' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 4, position: 'relative' }}>
<Avatar.Group> <Avatar.Group>
{memberscol && memberscol.length > 0 && {memberscol?.map((memberId, i) => {
memberscol.map((member, i) => ( const member = members.find((m) => m.id === memberId);
<CustomAvatar key={i} avatarName={member} size={26} /> return member ? (
))} <CustomAvatar key={i} avatarName={member.name} size={26} />
) : null;
})}
</Avatar.Group> </Avatar.Group>
<div> <div>
<RateCardAssigneeSelector <RateCardAssigneeSelector
projectId={projectId as string} projectId={projectId as string}
selectedMemberIds={memberscol || []} selectedMemberIds={memberscol || []}
onChange={memberId => handleMemberChange(memberId, index)} onChange={(memberId) => handleMemberChange(memberId, index, record)}
memberlist={members} memberlist={members}
/> />
</div> </div>
@@ -248,32 +260,12 @@ const RatecardTable: React.FC = () => {
okText={t('yes')} okText={t('yes')}
cancelText={t('no')} cancelText={t('no')}
> >
<Button <Button type="text" danger icon={<DeleteOutlined />} />
type="text"
danger
icon={<DeleteOutlined />}
/>
</Popconfirm> </Popconfirm>
), ),
}, },
]; ];
const handleMemberChange = (memberId: string, rowIndex: number) => {
setRoles(prev =>
prev.map((role, idx) => {
if (idx !== rowIndex) return role;
const members = Array.isArray(role.members) ? [...role.members] : [];
const memberIdx = members.indexOf(memberId);
if (memberIdx > -1) {
members.splice(memberIdx, 1); // remove if exists
} else {
members.push(memberId); // add if not exists
}
return { ...role, members };
})
);
};
return ( return (
<Table <Table
dataSource={ dataSource={
@@ -290,16 +282,12 @@ const RatecardTable: React.FC = () => {
: roles : roles
} }
columns={columns} columns={columns}
rowKey={(record, idx) => record.id || record.job_title_id || idx} rowKey={(record, idx) => record.id || record.job_title_id || String(idx)}
pagination={false} pagination={false}
loading={isLoading} loading={isLoading || isLoadingMembers}
footer={() => ( footer={() => (
<Flex gap={8}> <Flex gap={8}>
<Button <Button type="dashed" onClick={handleAddRole} style={{ width: 'fit-content' }}>
type="dashed"
onClick={handleAddRole}
style={{ width: 'fit-content' }}
>
{t('addRoleButton')} {t('addRoleButton')}
</Button> </Button>
<Button <Button