feat(ratecard): add 'Add All' and 'Remove All' buttons, enhance role management, and implement drawer close logic

This commit is contained in:
shancds
2025-05-21 12:26:01 +05:30
parent 4386aabeda
commit db1108a48d
2 changed files with 95 additions and 31 deletions

View File

@@ -16,5 +16,9 @@
"createRatecardErrorMessage": "Create Rate Card failed!", "createRatecardErrorMessage": "Create Rate Card failed!",
"updateRatecardSuccessMessage": "Update Rate Card success!", "updateRatecardSuccessMessage": "Update Rate Card success!",
"updateRatecardErrorMessage": "Update Rate Card failed!", "updateRatecardErrorMessage": "Update Rate Card failed!",
"currency": "Currency" "currency": "Currency",
"actionsColumn": "Actions",
"addAllButton": "Add All",
"removeAllButton": "Remove All"
} }

View File

@@ -3,10 +3,9 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAppSelector } from '../../../hooks/useAppSelector'; import { useAppSelector } from '../../../hooks/useAppSelector';
import { useAppDispatch } from '../../../hooks/useAppDispatch'; import { useAppDispatch } from '../../../hooks/useAppDispatch';
import { clearDrawerRatecard, fetchRateCardById, fetchRateCards, toggleRatecardDrawer, updateRateCard } from '../finance-slice'; import { deleteRateCard, fetchRateCardById, fetchRateCards, toggleRatecardDrawer, updateRateCard } from '../finance-slice';
import { RatecardType, IJobType } from '@/types/project/ratecard.types'; import { RatecardType, IJobType } from '@/types/project/ratecard.types';
import { IJobTitlesViewModel } from '@/types/job.types'; import { IJobTitlesViewModel } from '@/types/job.types';
import { DEFAULT_PAGE_SIZE } from '@/shared/constants';
import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service'; import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
@@ -31,7 +30,7 @@ const RatecardDrawer = ({
const [ratecardsList, setRatecardsList] = useState<RatecardType[]>([]); const [ratecardsList, setRatecardsList] = useState<RatecardType[]>([]);
// initial Job Roles List (dummy data) // initial Job Roles List (dummy data)
const [roles, setRoles] = useState<IJobType[]>([]); const [roles, setRoles] = useState<IJobType[]>([]);
const [addingRowIndex, setAddingRowIndex] = useState<number | null>(null);
const { t } = useTranslation('settings/ratecard-settings'); const { t } = useTranslation('settings/ratecard-settings');
// get drawer state from client reducer // get drawer state from client reducer
const drawerLoading = useAppSelector(state => state.financeReducer.isFinanceDrawerloading); const drawerLoading = useAppSelector(state => state.financeReducer.isFinanceDrawerloading);
@@ -49,13 +48,14 @@ const RatecardDrawer = ({
const [jobTitles, setJobTitles] = useState<IJobTitlesViewModel>({}); const [jobTitles, setJobTitles] = useState<IJobTitlesViewModel>({});
const [pagination, setPagination] = useState<PaginationType>({ const [pagination, setPagination] = useState<PaginationType>({
current: 1, current: 1,
pageSize: DEFAULT_PAGE_SIZE, pageSize: 10000,
field: 'name', field: 'name',
order: 'desc', order: 'desc',
total: 0, total: 0,
pageSizeOptions: ['5', '10', '15', '20', '50', '100'], pageSizeOptions: ['5', '10', '15', '20', '50', '100'],
size: 'small', size: 'small',
}); });
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
const getJobTitles = useMemo(() => { const getJobTitles = useMemo(() => {
return async () => { return async () => {
@@ -102,23 +102,33 @@ const RatecardDrawer = ({
// Add All handler // Add All handler
const handleAddAllRoles = () => { const handleAddAllRoles = () => {
if (!jobTitles.data) return; if (!jobTitles.data) return;
// Filter out job titles already in roles // Get current job_title_ids in roles
const existingIds = new Set(roles.map(r => r.job_title_id)); const existingIds = new Set(roles.map(r => r.job_title_id));
// Only add job titles not already present
const newRoles = jobTitles.data const newRoles = jobTitles.data
.filter(jt => !existingIds.has(jt.id!)) .filter(jt => jt.id && !existingIds.has(jt.id))
.map(jt => ({ .map(jt => ({
jobtitle: jt.name, jobtitle: jt.name,
rate_card_id: ratecardId, rate_card_id: ratecardId,
job_title_id: jt.id!, job_title_id: jt.id!,
rate: 0, rate: 0,
})); }));
setRoles([...roles, ...newRoles]); // Prevent any accidental duplicates by merging and filtering again
const mergedRoles = [...roles, ...newRoles].filter(
(role, idx, arr) =>
arr.findIndex(r => r.job_title_id === role.job_title_id) === idx
);
setRoles(mergedRoles);
}; };
// add new job role handler
const handleAddRole = () => { const handleAddRole = () => {
setIsAddingRole(true); // Only allow adding if there are job titles not already in roles
setSelectedJobTitleId(undefined); const existingIds = new Set(roles.map(r => r.job_title_id));
const availableJobTitles = jobTitles.data?.filter(jt => !existingIds.has(jt.id!));
if (availableJobTitles && availableJobTitles.length > 0) {
setRoles([...roles, { job_title_id: '', rate: 0 }]);
setAddingRowIndex(roles.length); // index of the new row
}
}; };
const handleDeleteRole = (index: number) => { const handleDeleteRole = (index: number) => {
const updatedRoles = [...roles]; const updatedRoles = [...roles];
@@ -126,6 +136,12 @@ const RatecardDrawer = ({
setRoles(updatedRoles); setRoles(updatedRoles);
}; };
const handleSelectJobTitle = (jobTitleId: string) => { const handleSelectJobTitle = (jobTitleId: string) => {
// Prevent duplicate job_title_id
if (roles.some(role => role.job_title_id === jobTitleId)) {
setIsAddingRole(false);
setSelectedJobTitleId(undefined);
return;
}
const jobTitle = jobTitles.data?.find(jt => jt.id === jobTitleId); const jobTitle = jobTitles.data?.find(jt => jt.id === jobTitleId);
if (jobTitle) { if (jobTitle) {
const newRole = { const newRole = {
@@ -143,12 +159,14 @@ const RatecardDrawer = ({
const handleSave = async () => { const handleSave = async () => {
if (type === 'update' && ratecardId) { if (type === 'update' && ratecardId) {
try { try {
// Filter out roles with no jobtitle or empty jobtitle
const filteredRoles = roles.filter(role => role.jobtitle && role.jobtitle.trim() !== '');
await dispatch(updateRateCard({ await dispatch(updateRateCard({
id: ratecardId, id: ratecardId,
body: { body: {
name, name,
currency, currency,
jobRolesList: roles, jobRolesList: filteredRoles,
}, },
}) as any); }) as any);
// Refresh the rate cards list in Redux // Refresh the rate cards list in Redux
@@ -177,24 +195,56 @@ const RatecardDrawer = ({
{ {
title: t('jobTitleColumn'), title: t('jobTitleColumn'),
dataIndex: 'jobtitle', dataIndex: 'jobtitle',
render: (text: string, record: any, index: number) => ( render: (text: string, record: any, index: number) => {
<Input if (index === addingRowIndex || index === editingRowIndex) {
value={text} return (
placeholder="Enter job title" <Select
style={{ showSearch
background: 'transparent', autoFocus
border: 'none', placeholder={t('selectJobTitle')}
boxShadow: 'none', style={{ minWidth: 150 }}
padding: 0, value={record.job_title_id || undefined}
color: '#1890ff', onChange={value => {
}} // Prevent duplicate job_title_id
onChange={(e) => { if (roles.some((role, idx) => role.job_title_id === value && idx !== index)) {
const updatedRoles = [...roles]; return;
updatedRoles[index].jobtitle = e.target.value; }
setRoles(updatedRoles); const updatedRoles = [...roles];
}} const selectedJob = jobTitles.data?.find(jt => jt.id === value);
/> updatedRoles[index].job_title_id = value;
), updatedRoles[index].jobtitle = selectedJob?.name || '';
setRoles(updatedRoles);
setEditingRowIndex(null);
setAddingRowIndex(null);
}}
onBlur={() => {
setEditingRowIndex(null);
setAddingRowIndex(null);
}}
filterOption={(input, option) =>
(option?.children as string).toLowerCase().includes(input.toLowerCase())
}
>
{jobTitles.data
?.filter(jt => !roles.some((role, idx) => role.job_title_id === jt.id && idx !== index))
.map(jt => (
<Select.Option key={jt.id} value={jt.id}>
{jt.name}
</Select.Option>
))}
</Select>
);
}
// Render as clickable text for existing rows
return (
<span
style={{ cursor: 'pointer' }}
onClick={() => setEditingRowIndex(index)}
>
{record.jobtitle}
</span>
);
},
}, },
{ {
title: `${t('ratePerHourColumn')} (${currency})`, title: `${t('ratePerHourColumn')} (${currency})`,
@@ -230,10 +280,21 @@ const RatecardDrawer = ({
), ),
}, },
]; ];
const handleDrawerClose = async () => {
if (
drawerRatecard &&
(drawerRatecard.jobRolesList?.length === 0 || !drawerRatecard.jobRolesList) &&
name === 'Untitled Rate Card'
) {
await dispatch(deleteRateCard(drawerRatecard.id as string));
}
dispatch(toggleRatecardDrawer());
};
return ( return (
<Drawer <Drawer
loading={drawerLoading} loading={drawerLoading}
onClose={handleDrawerClose}
title={ title={
<Flex align="center" justify="space-between"> <Flex align="center" justify="space-between">
<Typography.Text style={{ fontWeight: 500, fontSize: 16 }}> <Typography.Text style={{ fontWeight: 500, fontSize: 16 }}>
@@ -272,7 +333,6 @@ const RatecardDrawer = ({
</Flex> </Flex>
} }
open={isDrawerOpen} open={isDrawerOpen}
onClose={() => dispatch(toggleRatecardDrawer())}
width={700} width={700}
footer={ footer={
<Flex justify="end" gap={16} style={{ marginTop: 16 }}> <Flex justify="end" gap={16} style={{ marginTop: 16 }}>