Merge pull request #137 from shancds/feature/project-finance

Feature/project finance (project-ratecard-member-add)
This commit is contained in:
Chamika J
2025-05-26 08:33:29 +05:30
committed by GitHub
11 changed files with 457 additions and 131 deletions

View File

@@ -7,12 +7,17 @@ import { useAppDispatch } from '../../../../../../hooks/useAppDispatch';
import { useTranslation } from 'react-i18next';
import { JobRoleType, IJobType, RatecardType } from '@/types/project/ratecard.types';
import {
assignMemberToRateCardRole,
deleteProjectRateCardRoleById,
fetchProjectRateCardRoles,
insertProjectRateCardRole,
updateProjectRateCardRolesByProjectId,
} from '@/features/finance/project-finance-slice';
import { useParams } from 'react-router-dom';
import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service';
import RateCardAssigneeSelector from '@/components/project-ratecard/ratecard-assignee-selector';
import { projectsApiService } from '@/api/projects/projects.api.service';
import { IProjectMemberViewModel } from '@/types/projectMember.types';
const RatecardTable: React.FC = () => {
const dispatch = useAppDispatch();
@@ -27,8 +32,42 @@ const RatecardTable: React.FC = () => {
// Local state for editing
const [roles, setRoles] = useState<JobRoleType[]>(rolesRedux);
const [addingRow, setAddingRow] = useState<boolean>(false);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [jobTitles, setJobTitles] = useState<RatecardType[]>([]);
const [members, setMembers] = useState<IProjectMemberViewModel[]>([]);
const [isLoadingMembers, setIsLoading] = useState(false);
const pagination = {
current: 1,
pageSize: 1000,
field: 'name',
order: 'asc',
};
const getProjectMembers = async () => {
if (!projectId) return;
setIsLoading(true);
try {
const res = await projectsApiService.getMembers(
projectId,
pagination.current,
pagination.pageSize,
pagination.field,
pagination.order,
null
);
if (res.done) {
setMembers(res.body?.data || []);
}
} catch (error) {
console.error('Error fetching members:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
getProjectMembers();
}, [projectId]);
// Fetch job titles for selection
useEffect(() => {
@@ -40,7 +79,6 @@ const RatecardTable: React.FC = () => {
// Sync local roles with redux roles
useEffect(() => {
console.log('Roles Redux:', rolesRedux);
setRoles(rolesRedux);
}, [rolesRedux]);
@@ -59,7 +97,6 @@ const RatecardTable: React.FC = () => {
// Save all roles (bulk update)
const handleSaveAll = () => {
if (projectId) {
// Only send roles with job_title_id and rate
const filteredRoles = roles
.filter((r) => r.job_title_id && typeof r.rate !== 'undefined')
.map((r) => ({
@@ -72,20 +109,27 @@ const RatecardTable: React.FC = () => {
};
// Handle job title select for new row
const handleSelectJobTitle = (jobTitleId: string) => {
const handleSelectJobTitle = async (jobTitleId: string) => {
const jobTitle = jobTitles.find((jt) => jt.id === jobTitleId);
if (!jobTitle) return;
// Prevent duplicates
if (!jobTitle || !projectId) return;
if (roles.some((r) => r.job_title_id === jobTitleId)) return;
setRoles([
...roles,
{
job_title_id: jobTitleId,
jobtitle: jobTitle.name || '',
rate: 0,
members: [],
},
]);
const resultAction = await dispatch(
insertProjectRateCardRole({ project_id: projectId, job_title_id: jobTitleId, rate: 0 })
);
if (insertProjectRateCardRole.fulfilled.match(resultAction)) {
const newRole = resultAction.payload;
setRoles([
...roles,
{
id: newRole.id,
job_title_id: newRole.job_title_id,
jobtitle: newRole.jobtitle,
rate: newRole.rate,
members: [], // Initialize members array
},
]);
}
setAddingRow(false);
};
@@ -102,18 +146,41 @@ const RatecardTable: React.FC = () => {
if (record.id) {
dispatch(deleteProjectRateCardRoleById(record.id));
} else {
// Remove unsaved row
setRoles(roles.filter((_, idx) => idx !== index));
}
};
// Handle member change
const handleMemberChange = async (memberId: string, rowIndex: number, record: JobRoleType) => {
if (!projectId || !record.id) return; // Ensure required IDs are present
try {
const resultAction = await dispatch(
assignMemberToRateCardRole({
project_id: projectId,
member_id: memberId,
project_rate_card_role_id: record.id,
})
);
if (assignMemberToRateCardRole.fulfilled.match(resultAction)) {
const updatedMembers = resultAction.payload; // Array of member IDs
setRoles((prev) =>
prev.map((role, idx) => {
if (idx !== rowIndex) return role;
return { ...role, members: updatedMembers?.members || [] };
})
);
}
} catch (error) {
console.error('Error assigning member:', error);
}
};
// Columns
const columns: TableProps<JobRoleType>['columns'] = [
{
title: t('jobTitleColumn'),
dataIndex: 'jobtitle',
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) {
return (
<Select
@@ -125,12 +192,12 @@ const RatecardTable: React.FC = () => {
onChange={handleSelectJobTitle}
onBlur={() => setAddingRow(false)}
filterOption={(input, option) =>
(option?.children as string).toLowerCase().includes(input.toLowerCase())
(option?.children as string)?.toLowerCase().includes(input.toLowerCase())
}
>
{jobTitles
.filter(jt => !roles.some((role) => role.job_title_id === jt.id))
.map(jt => (
.filter((jt) => !roles.some((role) => role.job_title_id === jt.id))
.map((jt) => (
<Select.Option key={jt.id} value={jt.id!}>
{jt.name}
</Select.Option>
@@ -138,19 +205,13 @@ const RatecardTable: React.FC = () => {
</Select>
);
}
return (
<span
style={{ cursor: 'pointer' }}
onClick={() => setEditingIndex(index)}
>
{text || record.name}
</span>
);
return <span>{text || record.name}</span>;
},
},
{
title: `${t('ratePerHourColumn')} (${currency})`,
dataIndex: 'rate',
align: 'right',
render: (value: number, record: JobRoleType, index: number) => (
<Input
type="number"
@@ -161,6 +222,7 @@ const RatecardTable: React.FC = () => {
boxShadow: 'none',
padding: 0,
width: 80,
textAlign: 'right',
}}
onChange={(e) => handleRateChange(e.target.value, index)}
/>
@@ -169,34 +231,31 @@ const RatecardTable: React.FC = () => {
{
title: t('membersColumn'),
dataIndex: 'members',
render: (members: string[] | null | undefined) =>
members && members.length > 0 ? (
render: (memberscol: string[] | null | undefined, record: JobRoleType, index: number) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, position: 'relative' }}>
<Avatar.Group>
{members.map((member, i) => (
<CustomAvatar key={i} avatarName={member} size={26} />
))}
{memberscol?.map((memberId, i) => {
const member = members.find((m) => m.id === memberId);
return member ? (
<CustomAvatar key={i} avatarName={member.name} size={26} />
) : null;
})}
</Avatar.Group>
) : (
<Button
shape="circle"
icon={
<PlusOutlined
style={{
fontSize: 12,
width: 22,
height: 22,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
/>
}
/>
),
<div>
<RateCardAssigneeSelector
projectId={projectId as string}
selectedMemberIds={memberscol || []}
onChange={(memberId) => handleMemberChange(memberId, index, record)}
memberlist={members}
/>
</div>
</div>
),
},
{
title: t('actions'),
key: 'actions',
align: 'center',
render: (_: any, record: JobRoleType, index: number) => (
<Popconfirm
title={t('deleteConfirm')}
@@ -204,11 +263,7 @@ const RatecardTable: React.FC = () => {
okText={t('yes')}
cancelText={t('no')}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
/>
<Button type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
),
},
@@ -219,37 +274,33 @@ const RatecardTable: React.FC = () => {
dataSource={
addingRow
? [
...roles,
{
job_title_id: '',
jobtitle: '',
rate: 0,
members: [],
},
]
...roles,
{
job_title_id: '',
jobtitle: '',
rate: 0,
members: [],
},
]
: roles
}
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}
loading={isLoading}
loading={isLoading || isLoadingMembers}
footer={() => (
<Flex gap={8}>
<Button
type="dashed"
onClick={handleAddRole}
style={{ width: 'fit-content' }}
>
<Flex gap={0}>
<Button type="dashed" onClick={handleAddRole} style={{ width: 'fit-content' }}>
{t('addRoleButton')}
</Button>
<Button
{/* <Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSaveAll}
disabled={roles.length === 0}
>
{t('saveButton') || 'Save'}
</Button>
</Button> */}
</Flex>
)}
/>