feat: Implement Ratecard Drawer and Finance Table

This commit is contained in:
shancds
2025-05-14 22:20:50 +05:30
parent 19deef9298
commit 6847eec603
20 changed files with 1659 additions and 1 deletions

View File

@@ -0,0 +1,44 @@
import React, { useEffect, useMemo, useState } from 'react';
import FinanceTableWrapper from './finance-table/finance-table-wrapper';
import { fetchData } from '../../../../../utils/fetchData';
const FinanceTab = ({
groupType,
}: {
groupType: 'status' | 'priority' | 'phases';
}) => {
// Save each table's list according to the groups
const [statusTables, setStatusTables] = useState<any[]>([]);
const [priorityTables, setPriorityTables] = useState<any[]>([]);
const [activeTablesList, setActiveTablesList] = useState<any[]>([]);
// Fetch data for status tables
useMemo(() => {
fetchData('/finance-mock-data/finance-task-status.json', setStatusTables);
}, []);
// Fetch data for priority tables
useMemo(() => {
fetchData(
'/finance-mock-data/finance-task-priority.json',
setPriorityTables
);
}, []);
// Update activeTablesList based on groupType and fetched data
useEffect(() => {
if (groupType === 'status') {
setActiveTablesList(statusTables);
} else if (groupType === 'priority') {
setActiveTablesList(priorityTables);
}
}, [groupType, priorityTables, statusTables]);
return (
<div>
<FinanceTableWrapper activeTablesList={activeTablesList} />
</div>
);
};
export default FinanceTab;

View File

@@ -0,0 +1,253 @@
import React, { useEffect, useState } from 'react';
import { Checkbox, Flex, Typography } from 'antd';
import { themeWiseColor } from '../../../../../../utils/themeWiseColor';
import { useAppSelector } from '../../../../../../hooks/useAppSelector';
import { useTranslation } from 'react-i18next';
import { useAppDispatch } from '../../../../../../hooks/useAppDispatch';
import { toggleFinanceDrawer } from '@/features/finance/finance-slice';
import { financeTableColumns } from '@/lib/project/project-view-finance-table-columns';
import FinanceTable from './finance-table';
import FinanceDrawer from '@/features/finance/finance-drawer/finance-drawer';
const FinanceTableWrapper = ({
activeTablesList,
}: {
activeTablesList: any;
}) => {
const [isScrolling, setIsScrolling] = useState(false);
//? this state for inside this state individualy in finance table only display the data of the last table's task when a task is clicked The selectedTask state does not synchronize across tables so thats why move the selectedTask state to a parent component
const [selectedTask, setSelectedTask] = useState(null);
// localization
const { t } = useTranslation('project-view-finance');
const dispatch = useAppDispatch();
// function on task click
const onTaskClick = (task: any) => {
setSelectedTask(task);
dispatch(toggleFinanceDrawer());
};
// trigger the table scrolling
useEffect(() => {
const tableContainer = document.querySelector('.tasklist-container');
const handleScroll = () => {
if (tableContainer) {
setIsScrolling(tableContainer.scrollLeft > 0);
}
};
// add the scroll event listener
tableContainer?.addEventListener('scroll', handleScroll);
// cleanup on unmount
return () => {
tableContainer?.removeEventListener('scroll', handleScroll);
};
}, []);
// get theme data from theme reducer
const themeMode = useAppSelector((state) => state.themeReducer.mode);
// get tasklist and currently using currency from finance reducer
const { currency } = useAppSelector((state) => state.financeReducer);
// totals of all the tasks
const totals = activeTablesList.reduce(
(
acc: {
hours: number;
cost: number;
fixedCost: number;
totalBudget: number;
totalActual: number;
variance: number;
},
table: { tasks: any[] }
) => {
table.tasks.forEach((task: any) => {
acc.hours += task.hours || 0;
acc.cost += task.cost || 0;
acc.fixedCost += task.fixedCost || 0;
acc.totalBudget += task.totalBudget || 0;
acc.totalActual += task.totalActual || 0;
acc.variance += task.variance || 0;
});
return acc;
},
{
hours: 0,
cost: 0,
fixedCost: 0,
totalBudget: 0,
totalActual: 0,
variance: 0,
}
);
const renderFinancialTableHeaderContent = (columnKey: any) => {
switch (columnKey) {
case 'hours':
return (
<Typography.Text style={{ fontSize: 18 }}>
{totals.hours}
</Typography.Text>
);
case 'cost':
return (
<Typography.Text style={{ fontSize: 18 }}>
{totals.cost}
</Typography.Text>
);
case 'fixedCost':
return (
<Typography.Text style={{ fontSize: 18 }}>
{totals.fixedCost}
</Typography.Text>
);
case 'totalBudget':
return (
<Typography.Text style={{ fontSize: 18 }}>
{totals.totalBudget}
</Typography.Text>
);
case 'totalActual':
return (
<Typography.Text style={{ fontSize: 18 }}>
{totals.totalActual}
</Typography.Text>
);
case 'variance':
return (
<Typography.Text
style={{
color: totals.variance < 0 ? '#FF0000' : '#6DC376',
fontSize: 18,
}}
>
{totals.variance}
</Typography.Text>
);
default:
return null;
}
};
// layout styles for table and the columns
const customColumnHeaderStyles = (key: string) =>
`px-2 text-left ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && 'sticky left-[48px] z-10'} ${key === 'members' && `sticky left-[288px] z-10 ${isScrolling ? 'after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[68px] after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
const customColumnStyles = (key: string) =>
`px-2 text-left ${key === 'totalRow' && `sticky left-0 z-10 ${isScrolling ? 'after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[68px] after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`}`;
return (
<>
<Flex
vertical
className="tasklist-container min-h-0 max-w-full overflow-x-auto"
>
<table>
<tbody>
<tr
style={{
height: 56,
fontWeight: 600,
backgroundColor: themeWiseColor(
'#fafafa',
'#1d1d1d',
themeMode
),
borderBlockEnd: `2px solid rgb(0 0 0 / 0.05)`,
}}
>
<td
style={{ width: 32, paddingInline: 16 }}
className={customColumnHeaderStyles('selector')}
>
<Checkbox />
</td>
{financeTableColumns.map((col) => (
<td
key={col.key}
style={{
minWidth: col.width,
paddingInline: 16,
textAlign:
col.type === 'hours' || col.type === 'currency'
? 'center'
: 'start',
}}
className={`${customColumnHeaderStyles(col.key)} before:constent relative before:absolute before:left-0 before:top-1/2 before:h-[36px] before:w-0.5 before:-translate-y-1/2 ${themeMode === 'dark' ? 'before:bg-white/10' : 'before:bg-black/5'}`}
>
<Typography.Text>
{t(`${col.name}Column`)}{' '}
{col.type === 'currency' && `(${currency.toUpperCase()})`}
</Typography.Text>
</td>
))}
</tr>
<tr
style={{
height: 56,
fontWeight: 500,
backgroundColor: themeWiseColor(
'#fbfbfb',
'#141414',
themeMode
),
}}
>
<td
colSpan={3}
style={{
paddingInline: 16,
backgroundColor: themeWiseColor(
'#fbfbfb',
'#141414',
themeMode
),
}}
className={customColumnStyles('totalRow')}
>
<Typography.Text style={{ fontSize: 18 }}>
{t('totalText')}
</Typography.Text>
</td>
{financeTableColumns.map(
(col) =>
(col.type === 'hours' || col.type === 'currency') && (
<td
key={col.key}
style={{
minWidth: col.width,
paddingInline: 16,
textAlign: 'end',
}}
>
{renderFinancialTableHeaderContent(col.key)}
</td>
)
)}
</tr>
{activeTablesList.map((table: any, index: number) => (
<FinanceTable
key={index}
table={table}
isScrolling={isScrolling}
onTaskClick={onTaskClick}
/>
))}
</tbody>
</table>
</Flex>
{selectedTask && <FinanceDrawer task={selectedTask} />}
</>
);
};
export default FinanceTableWrapper;

View File

@@ -0,0 +1,287 @@
import { Avatar, Checkbox, Flex, Input, Tooltip, Typography } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import CustomAvatar from '../../../../../../components/CustomAvatar';
import { useAppSelector } from '../../../../../../hooks/useAppSelector';
import {
DollarCircleOutlined,
DownOutlined,
RightOutlined,
} from '@ant-design/icons';
import { themeWiseColor } from '../../../../../../utils/themeWiseColor';
import { colors } from '../../../../../../styles/colors';
import { financeTableColumns } from '@/lib/project/project-view-finance-table-columns';
type FinanceTableProps = {
table: any;
isScrolling: boolean;
onTaskClick: (task: any) => void;
};
const FinanceTable = ({
table,
isScrolling,
onTaskClick,
}: FinanceTableProps) => {
const [isCollapse, setIsCollapse] = useState<boolean>(false);
const [selectedTask, setSelectedTask] = useState(null);
// get theme data from theme reducer
const themeMode = useAppSelector((state) => state.themeReducer.mode);
// totals of the current table
const totals = useMemo(
() => ({
hours: (table?.tasks || []).reduce(
(sum: any, task: { hours: any }) => sum + task.hours,
0
),
cost: (table?.tasks || []).reduce(
(sum: any, task: { cost: any }) => sum + task.cost,
0
),
fixedCost: (table?.tasks || []).reduce(
(sum: any, task: { fixedCost: any }) => sum + task.fixedCost,
0
),
totalBudget: (table?.tasks || []).reduce(
(sum: any, task: { totalBudget: any }) => sum + task.totalBudget,
0
),
totalActual: (table?.tasks || []).reduce(
(sum: any, task: { totalActual: any }) => sum + task.totalActual,
0
),
variance: (table?.tasks || []).reduce(
(sum: any, task: { variance: any }) => sum + task.variance,
0
),
}),
[table]
);
useEffect(() => {
console.log('Selected Task:', selectedTask);
}, [selectedTask]);
const renderFinancialTableHeaderContent = (columnKey: any) => {
switch (columnKey) {
case 'hours':
return (
<Typography.Text style={{ color: colors.darkGray }}>
{totals.hours}
</Typography.Text>
);
case 'cost':
return (
<Typography.Text style={{ color: colors.darkGray }}>
{totals.cost}
</Typography.Text>
);
case 'fixedCost':
return (
<Typography.Text style={{ color: colors.darkGray }}>
{totals.fixedCost}
</Typography.Text>
);
case 'totalBudget':
return (
<Typography.Text style={{ color: colors.darkGray }}>
{totals.totalBudget}
</Typography.Text>
);
case 'totalActual':
return (
<Typography.Text style={{ color: colors.darkGray }}>
{totals.totalActual}
</Typography.Text>
);
case 'variance':
return (
<Typography.Text
style={{
color:
totals.variance < 0
? '#FF0000'
: themeWiseColor('#6DC376', colors.darkGray, themeMode),
}}
>
{totals.variance}
</Typography.Text>
);
default:
return null;
}
};
const renderFinancialTableColumnContent = (columnKey: any, task: any) => {
switch (columnKey) {
case 'task':
return (
<Tooltip title={task.task}>
<Flex gap={8} align="center">
<Typography.Text
ellipsis={{ expanded: false }}
style={{ maxWidth: 160 }}
>
{task.task}
</Typography.Text>
{task.isbBillable && <DollarCircleOutlined />}
</Flex>
</Tooltip>
);
case 'members':
return (
<Avatar.Group>
{task.members.map((member: any) => (
<CustomAvatar avatarName={member.name} size={26} />
))}
</Avatar.Group>
);
case 'hours':
return <Typography.Text>{task.hours}</Typography.Text>;
case 'cost':
return <Typography.Text>{task.cost}</Typography.Text>;
case 'fixedCost':
return (
<Input
value={task.fixedCost}
style={{
background: 'transparent',
border: 'none',
boxShadow: 'none',
textAlign: 'right',
padding: 0,
}}
/>
);
case 'totalBudget':
return (
<Input
value={task.totalBudget}
style={{
background: 'transparent',
border: 'none',
boxShadow: 'none',
textAlign: 'right',
padding: 0,
}}
/>
);
case 'totalActual':
return <Typography.Text>{task.totalActual}</Typography.Text>;
case 'variance':
return (
<Typography.Text
style={{
color: task.variance < 0 ? '#FF0000' : '#6DC376',
}}
>
{task.variance}
</Typography.Text>
);
default:
return null;
}
};
// layout styles for table and the columns
const customColumnHeaderStyles = (key: string) =>
`px-2 text-left ${key === 'tableTitle' && `sticky left-0 z-10 ${isScrolling ? 'after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[40px] after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`}`;
const customColumnStyles = (key: string) =>
`px-2 text-left ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && 'sticky left-[48px] z-10'} ${key === 'members' && `sticky left-[288px] z-10 ${isScrolling ? 'after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[52px] after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
return (
<>
{/* header row */}
<tr
style={{
height: 40,
backgroundColor: themeWiseColor(
table.color_code,
table.color_code_dark,
themeMode
),
fontWeight: 600,
}}
className="group"
>
<td
colSpan={3}
style={{
width: 48,
textTransform: 'capitalize',
textAlign: 'left',
paddingInline: 16,
backgroundColor: themeWiseColor(
table.color_code,
table.color_code_dark,
themeMode
),
cursor: 'pointer',
}}
className={customColumnHeaderStyles('tableTitle')}
onClick={(e) => setIsCollapse((prev) => !prev)}
>
<Flex gap={8} align="center" style={{ color: colors.darkGray }}>
{isCollapse ? <RightOutlined /> : <DownOutlined />}
{table.name} ({table.tasks.length})
</Flex>
</td>
{financeTableColumns.map(
(col) =>
col.key !== 'task' &&
col.key !== 'members' && (
<td
key={col.key}
style={{
width: col.width,
paddingInline: 16,
textAlign: 'end',
}}
>
{renderFinancialTableHeaderContent(col.key)}
</td>
)
)}
</tr>
{/* task rows */}
{table.tasks.map((task: any) => (
<tr
key={task.taskId}
style={{ height: 52 }}
className={`${isCollapse ? 'hidden' : 'static'} cursor-pointer border-b-[1px] ${themeMode === 'dark' ? 'hover:bg-[#000000]' : 'hover:bg-[#f8f7f9]'} `}
onClick={() => onTaskClick(task)}
>
<td
style={{ paddingInline: 16 }}
className={customColumnStyles('selector')}
>
<Checkbox />
</td>
{financeTableColumns.map((col) => (
<td
key={col.key}
className={customColumnStyles(col.key)}
style={{
width: col.width,
paddingInline: 16,
textAlign:
col.type === 'hours' || col.type === 'currency'
? 'end'
: 'start',
}}
>
{renderFinancialTableColumnContent(col.key, task)}
</td>
))}
</tr>
))}
</>
);
};
export default FinanceTable;

View File

@@ -0,0 +1,56 @@
import { CaretDownFilled } from '@ant-design/icons';
import { Flex, Select } from 'antd';
import React from 'react';
import { useSelectedProject } from '../../../../../hooks/useSelectedProject';
import { useAppSelector } from '../../../../../hooks/useAppSelector';
import { useTranslation } from 'react-i18next';
type GroupByFilterDropdownProps = {
activeGroup: 'status' | 'priority' | 'phases';
setActiveGroup: (group: 'status' | 'priority' | 'phases') => void;
};
const GroupByFilterDropdown = ({
activeGroup,
setActiveGroup,
}: GroupByFilterDropdownProps) => {
// localization
const { t } = useTranslation('project-view-finance');
const handleChange = (value: string) => {
setActiveGroup(value as 'status' | 'priority' | 'phases');
};
// get selected project from useSelectedPro
const selectedProject = useSelectedProject();
//get phases details from phases slice
const phase =
useAppSelector((state) => state.phaseReducer.phaseList).find(
(phase) => phase?.projectId === selectedProject?.projectId
) || null;
const groupDropdownMenuItems = [
{ key: 'status', value: 'status', label: t('statusText') },
{ key: 'priority', value: 'priority', label: t('priorityText') },
{
key: 'phase',
value: 'phase',
label: phase ? phase?.phase : t('phaseText'),
},
];
return (
<Flex align="center" gap={4} style={{ marginInlineStart: 12 }}>
{t('groupByText')}:
<Select
defaultValue={'status'}
options={groupDropdownMenuItems}
onChange={handleChange}
suffixIcon={<CaretDownFilled />}
/>
</Flex>
);
};
export default GroupByFilterDropdown;

View File

@@ -0,0 +1,86 @@
import { Button, ConfigProvider, Flex, Select, Typography } from 'antd';
import React from 'react';
import GroupByFilterDropdown from './group-by-filter-dropdown';
import { DownOutlined } from '@ant-design/icons';
import { useAppDispatch } from '../../../../../hooks/useAppDispatch';
import { useTranslation } from 'react-i18next';
import { changeCurrency, toggleImportRatecardsDrawer } from '@/features/finance/finance-slice';
type ProjectViewFinanceHeaderProps = {
activeTab: 'finance' | 'ratecard';
setActiveTab: (tab: 'finance' | 'ratecard') => void;
activeGroup: 'status' | 'priority' | 'phases';
setActiveGroup: (group: 'status' | 'priority' | 'phases') => void;
};
const ProjectViewFinanceHeader = ({
activeTab,
setActiveTab,
activeGroup,
setActiveGroup,
}: ProjectViewFinanceHeaderProps) => {
// localization
const { t } = useTranslation('project-view-finance');
const dispatch = useAppDispatch();
return (
<ConfigProvider wave={{ disabled: true }}>
<Flex gap={16} align="center" justify="space-between">
<Flex gap={16} align="center">
<Flex>
<Button
className={`${activeTab === 'finance' && 'border-[#1890ff] text-[#1890ff]'} rounded-r-none`}
onClick={() => setActiveTab('finance')}
>
{t('financeText')}
</Button>
<Button
className={`${activeTab === 'ratecard' && 'border-[#1890ff] text-[#1890ff]'} rounded-l-none`}
onClick={() => setActiveTab('ratecard')}
>
{t('ratecardSingularText')}
</Button>
</Flex>
{activeTab === 'finance' && (
<GroupByFilterDropdown
activeGroup={activeGroup}
setActiveGroup={setActiveGroup}
/>
)}
</Flex>
{activeTab === 'finance' ? (
<Button type="primary" icon={<DownOutlined />} iconPosition="end">
{t('exportButton')}
</Button>
) : (
<Flex gap={8} align="center">
<Flex gap={8} align="center">
<Typography.Text>{t('currencyText')}</Typography.Text>
<Select
defaultValue={'lkr'}
options={[
{ value: 'lkr', label: 'LKR' },
{ value: 'usd', label: 'USD' },
{ value: 'inr', label: 'INR' },
]}
onChange={(value) => dispatch(changeCurrency(value))}
/>
</Flex>
<Button
type="primary"
onClick={() => dispatch(toggleImportRatecardsDrawer())}
>
{t('importButton')}
</Button>
</Flex>
)}
</Flex>
</ConfigProvider>
);
};
export default ProjectViewFinanceHeader;

View File

@@ -0,0 +1,32 @@
import { Flex } from 'antd';
import React, { useState } from 'react';
import ProjectViewFinanceHeader from './project-view-finance-header/project-view-finance-header';
import FinanceTab from './finance-tab/finance-tab';
import RatecardTab from './ratecard-tab/ratecard-tab';
type FinanceTabType = 'finance' | 'ratecard';
type GroupTypes = 'status' | 'priority' | 'phases';
const ProjectViewFinance = () => {
const [activeTab, setActiveTab] = useState<FinanceTabType>('finance');
const [activeGroup, setActiveGroup] = useState<GroupTypes>('status');
return (
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
<ProjectViewFinanceHeader
activeTab={activeTab}
setActiveTab={setActiveTab}
activeGroup={activeGroup}
setActiveGroup={setActiveGroup}
/>
{activeTab === 'finance' ? (
<FinanceTab groupType={activeGroup} />
) : (
<RatecardTab />
)}
</Flex>
);
};
export default ProjectViewFinance;

View File

@@ -0,0 +1,38 @@
import React from 'react';
import RatecardTable from './reatecard-table/ratecard-table';
import { Button, Flex, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import ImportRatecardsDrawer from '@/features/finance/ratecard-drawer/import-ratecards-drawer';
const RatecardTab = () => {
// localization
const { t } = useTranslation('project-view-finance');
return (
<Flex vertical gap={8}>
<RatecardTable />
<Typography.Text
type="danger"
style={{ display: 'block', marginTop: '10px' }}
>
{t('ratecardImportantNotice')}
</Typography.Text>
<Button
type="primary"
style={{
marginTop: '10px',
width: 'fit-content',
alignSelf: 'flex-end',
}}
>
{t('saveButton')}
</Button>
{/* import ratecards drawer */}
<ImportRatecardsDrawer />
</Flex>
);
};
export default RatecardTab;

View File

@@ -0,0 +1,150 @@
import { Avatar, Button, Input, Table, TableProps } from 'antd';
import React, { useState } from 'react';
import CustomAvatar from '../../../../../../components/CustomAvatar';
import { PlusOutlined } from '@ant-design/icons';
import { useAppSelector } from '../../../../../../hooks/useAppSelector';
import { useTranslation } from 'react-i18next';
import { JobRoleType } from '@/types/project/ratecard.types';
const initialJobRolesList: JobRoleType[] = [
{
jobId: 'J001',
jobTitle: 'Project Manager',
ratePerHour: 50,
members: ['Alice Johnson', 'Bob Smith'],
},
{
jobId: 'J002',
jobTitle: 'Senior Software Engineer',
ratePerHour: 40,
members: ['Charlie Brown', 'Diana Prince'],
},
{
jobId: 'J003',
jobTitle: 'Junior Software Engineer',
ratePerHour: 25,
members: ['Eve Davis', 'Frank Castle'],
},
{
jobId: 'J004',
jobTitle: 'UI/UX Designer',
ratePerHour: 30,
members: null,
},
];
const RatecardTable: React.FC = () => {
const [roles, setRoles] = useState<JobRoleType[]>(initialJobRolesList);
// localization
const { t } = useTranslation('project-view-finance');
// get currently using currency from finance reducer
const currency = useAppSelector(
(state) => state.financeReducer.currency
).toUpperCase();
const handleAddRole = () => {
const newRole: JobRoleType = {
jobId: `J00${roles.length + 1}`,
jobTitle: 'New Role',
ratePerHour: 0,
members: [],
};
setRoles([...roles, newRole]);
};
const columns: TableProps<JobRoleType>['columns'] = [
{
title: t('jobTitleColumn'),
dataIndex: 'jobTitle',
render: (text: string, record: JobRoleType, index: number) => (
<Input
value={text}
placeholder="Enter job title"
style={{
background: 'transparent',
border: 'none',
boxShadow: 'none',
padding: 0,
color: '#1890ff',
}}
onChange={(e) => {
const updatedRoles = [...roles];
updatedRoles[index].jobTitle = e.target.value;
setRoles(updatedRoles);
}}
/>
),
},
{
title: `${t('ratePerHourColumn')} (${currency})`,
dataIndex: 'ratePerHour',
render: (text: number, record: JobRoleType, index: number) => (
<Input
type="number"
value={text}
style={{
background: 'transparent',
border: 'none',
boxShadow: 'none',
padding: 0,
}}
onChange={(e) => {
const updatedRoles = [...roles];
updatedRoles[index].ratePerHour = parseInt(e.target.value, 10) || 0;
setRoles(updatedRoles);
}}
/>
),
},
{
title: t('membersColumn'),
dataIndex: 'members',
render: (members: string[]) =>
members?.length > 0 ? (
<Avatar.Group>
{members.map((member, i) => (
<CustomAvatar key={i} avatarName={member} size={26} />
))}
</Avatar.Group>
) : (
<Button
shape="circle"
icon={
<PlusOutlined
style={{
fontSize: 12,
width: 22,
height: 22,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
/>
}
/>
),
},
];
return (
<Table
dataSource={roles}
columns={columns}
rowKey={(record) => record.jobId}
pagination={false}
footer={() => (
<Button
type="dashed"
onClick={handleAddRole}
style={{ width: 'fit-content' }}
>
{t('addRoleButton')}
</Button>
)}
/>
);
};
export default RatecardTable;