feat: Implement Ratecard Drawer and Finance Table
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user