feat(ratecard): add 'Add All' and 'Remove All' buttons, enhance role management, and implement drawer close logic
This commit is contained in:
@@ -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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user