feat(project-finance): enhance project finance view and calculations
- Added a new SQL view `project_finance_view` to aggregate project financial data. - Updated `project-finance-controller.ts` to fetch and group tasks by status, priority, or phases, including financial calculations for estimated costs, actual costs, and variances. - Enhanced frontend components to display total time logged, estimated costs, and fixed costs in the finance table. - Introduced new utility functions for formatting hours and calculating totals. - Updated localization files to include new financial columns in English, Spanish, and Portuguese. - Implemented Redux slice for managing project finance state and actions for updating task costs.
This commit is contained in:
@@ -10,26 +10,28 @@ interface FinanceTabProps {
|
||||
|
||||
const FinanceTab = ({
|
||||
groupType,
|
||||
taskGroups,
|
||||
taskGroups = [],
|
||||
loading
|
||||
}: FinanceTabProps) => {
|
||||
// Transform taskGroups into the format expected by FinanceTableWrapper
|
||||
const activeTablesList = taskGroups.map(group => ({
|
||||
id: group.group_id,
|
||||
name: group.group_name,
|
||||
const activeTablesList = (taskGroups || []).map(group => ({
|
||||
group_id: group.group_id,
|
||||
group_name: group.group_name,
|
||||
color_code: group.color_code,
|
||||
color_code_dark: group.color_code_dark,
|
||||
tasks: group.tasks.map(task => ({
|
||||
taskId: task.id,
|
||||
task: task.name,
|
||||
tasks: (group.tasks || []).map(task => ({
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
hours: task.estimated_hours || 0,
|
||||
cost: 0, // TODO: Calculate based on rate and hours
|
||||
fixedCost: 0, // TODO: Add fixed cost field
|
||||
totalBudget: 0, // TODO: Calculate total budget
|
||||
totalActual: task.actual_hours || 0,
|
||||
totalActual: task.total_actual || 0,
|
||||
variance: 0, // TODO: Calculate variance
|
||||
members: task.members || [],
|
||||
isbBillable: task.billable
|
||||
isbBillable: task.billable,
|
||||
total_time_logged: task.total_time_logged || 0,
|
||||
estimated_cost: task.estimated_cost || 0
|
||||
}))
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from "react";
|
||||
import { Card, Col, Row, Spin } from "antd";
|
||||
import { useThemeContext } from "../../../../../context/theme-context";
|
||||
import { FinanceTable } from "./finance-table";
|
||||
import { IFinanceTable } from "./finance-table.interface";
|
||||
import { Card, Col, Row } from "antd";
|
||||
|
||||
import { IProjectFinanceGroup } from "../../../../../types/project/project-finance.types";
|
||||
import FinanceTable from "./finance-table/finance-table";
|
||||
|
||||
interface Props {
|
||||
activeTablesList: IProjectFinanceGroup[];
|
||||
@@ -32,7 +31,7 @@ export const FinanceTableWrapper: React.FC<Props> = ({ activeTablesList, loading
|
||||
<h3>{table.group_name}</h3>
|
||||
</div>
|
||||
<FinanceTable
|
||||
table={table as unknown as IFinanceTable}
|
||||
table={table}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Checkbox, Flex, Typography } from 'antd';
|
||||
import { Checkbox, Flex, Tooltip, Typography } from 'antd';
|
||||
import { themeWiseColor } from '../../../../../../utils/themeWiseColor';
|
||||
import { useAppSelector } from '../../../../../../hooks/useAppSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
import { convertToHoursMinutes, formatHoursToReadable } from '@/utils/format-hours-to-readable';
|
||||
|
||||
interface FinanceTableWrapperProps {
|
||||
activeTablesList: {
|
||||
@@ -26,6 +27,8 @@ interface FinanceTableWrapperProps {
|
||||
variance: number;
|
||||
members: any[];
|
||||
isbBillable: boolean;
|
||||
total_time_logged: number;
|
||||
estimated_cost: number;
|
||||
}[];
|
||||
}[];
|
||||
loading: boolean;
|
||||
@@ -72,6 +75,8 @@ const FinanceTableWrapper: React.FC<FinanceTableWrapperProps> = ({
|
||||
totalBudget: number;
|
||||
totalActual: number;
|
||||
variance: number;
|
||||
total_time_logged: number;
|
||||
estimated_cost: number;
|
||||
},
|
||||
table: { tasks: any[] }
|
||||
) => {
|
||||
@@ -82,6 +87,8 @@ const FinanceTableWrapper: React.FC<FinanceTableWrapperProps> = ({
|
||||
acc.totalBudget += task.totalBudget || 0;
|
||||
acc.totalActual += task.totalActual || 0;
|
||||
acc.variance += task.variance || 0;
|
||||
acc.total_time_logged += task.total_time_logged || 0;
|
||||
acc.estimated_cost += task.estimated_cost || 0;
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
@@ -92,15 +99,21 @@ const FinanceTableWrapper: React.FC<FinanceTableWrapperProps> = ({
|
||||
totalBudget: 0,
|
||||
totalActual: 0,
|
||||
variance: 0,
|
||||
total_time_logged: 0,
|
||||
estimated_cost: 0,
|
||||
}
|
||||
);
|
||||
|
||||
console.log("totals", totals);
|
||||
|
||||
const renderFinancialTableHeaderContent = (columnKey: any) => {
|
||||
switch (columnKey) {
|
||||
case 'hours':
|
||||
return (
|
||||
<Typography.Text style={{ fontSize: 18 }}>
|
||||
{totals.hours}
|
||||
<Tooltip title={convertToHoursMinutes(totals.hours)}>
|
||||
{formatHoursToReadable(totals.hours)}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
);
|
||||
case 'cost':
|
||||
@@ -138,6 +151,18 @@ const FinanceTableWrapper: React.FC<FinanceTableWrapperProps> = ({
|
||||
{totals.variance}
|
||||
</Typography.Text>
|
||||
);
|
||||
case 'total_time_logged':
|
||||
return (
|
||||
<Typography.Text style={{ fontSize: 18 }}>
|
||||
{totals.total_time_logged?.toFixed(2)}
|
||||
</Typography.Text>
|
||||
);
|
||||
case 'estimated_cost':
|
||||
return (
|
||||
<Typography.Text style={{ fontSize: 18 }}>
|
||||
{`${currency.toUpperCase()} ${totals.estimated_cost?.toFixed(2)}`}
|
||||
</Typography.Text>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -189,7 +214,7 @@ const FinanceTableWrapper: React.FC<FinanceTableWrapperProps> = ({
|
||||
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`)}{' '}
|
||||
{t(`${col.name}`)}{' '}
|
||||
{col.type === 'currency' && `(${currency.toUpperCase()})`}
|
||||
</Typography.Text>
|
||||
</td>
|
||||
|
||||
@@ -1,283 +1,250 @@
|
||||
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 { Checkbox, Flex, Input, InputNumber, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import {
|
||||
DollarCircleOutlined,
|
||||
DownOutlined,
|
||||
RightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { themeWiseColor } from '../../../../../../utils/themeWiseColor';
|
||||
import { colors } from '../../../../../../styles/colors';
|
||||
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||
import { colors } from '@/styles/colors';
|
||||
import { financeTableColumns } from '@/lib/project/project-view-finance-table-columns';
|
||||
import Avatars from '@/components/avatars/avatars';
|
||||
import { IProjectFinanceGroup, IProjectFinanceTask } from '@/types/project/project-finance.types';
|
||||
import { updateTaskFixedCost } from '@/features/projects/finance/project-finance.slice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
|
||||
type FinanceTableProps = {
|
||||
table: any;
|
||||
isScrolling: boolean;
|
||||
onTaskClick: (task: any) => void;
|
||||
table: IProjectFinanceGroup;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
const FinanceTable = ({
|
||||
table,
|
||||
isScrolling,
|
||||
onTaskClick,
|
||||
loading,
|
||||
}: FinanceTableProps) => {
|
||||
const [isCollapse, setIsCollapse] = useState<boolean>(false);
|
||||
const [selectedTask, setSelectedTask] = useState(null);
|
||||
const [selectedTask, setSelectedTask] = useState<IProjectFinanceTask | null>(null);
|
||||
const [tasks, setTasks] = useState<IProjectFinanceTask[]>(table.tasks);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Get the latest task groups from Redux store
|
||||
const taskGroups = useAppSelector((state) => state.projectFinances.taskGroups);
|
||||
|
||||
// Update local state when table.tasks or Redux store changes
|
||||
useEffect(() => {
|
||||
const updatedGroup = taskGroups.find(g => g.group_id === table.group_id);
|
||||
if (updatedGroup) {
|
||||
setTasks(updatedGroup.tasks);
|
||||
} else {
|
||||
setTasks(table.tasks);
|
||||
}
|
||||
}, [table.tasks, taskGroups, table.group_id]);
|
||||
|
||||
// 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]
|
||||
);
|
||||
const formatNumber = (value: number | undefined | null) => {
|
||||
if (value === undefined || value === null) return '0.00';
|
||||
return value.toFixed(2);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Selected Task:', selectedTask);
|
||||
}, [selectedTask]);
|
||||
|
||||
const renderFinancialTableHeaderContent = (columnKey: any) => {
|
||||
const renderFinancialTableHeaderContent = (columnKey: string) => {
|
||||
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>
|
||||
);
|
||||
return <Typography.Text>{formatNumber(totals.hours)}</Typography.Text>;
|
||||
case 'total_time_logged':
|
||||
return <Typography.Text>{formatNumber(totals.total_time_logged)}</Typography.Text>;
|
||||
case 'estimated_cost':
|
||||
return <Typography.Text>{formatNumber(totals.estimated_cost)}</Typography.Text>;
|
||||
case 'totalBudget':
|
||||
return (
|
||||
<Typography.Text style={{ color: colors.darkGray }}>
|
||||
{totals.totalBudget}
|
||||
</Typography.Text>
|
||||
);
|
||||
return <Typography.Text>{formatNumber(totals.total_budget)}</Typography.Text>;
|
||||
case 'totalActual':
|
||||
return (
|
||||
<Typography.Text style={{ color: colors.darkGray }}>
|
||||
{totals.totalActual}
|
||||
</Typography.Text>
|
||||
);
|
||||
return <Typography.Text>{formatNumber(totals.total_actual)}</Typography.Text>;
|
||||
case 'variance':
|
||||
return (
|
||||
<Typography.Text
|
||||
style={{
|
||||
color:
|
||||
totals.variance < 0
|
||||
? '#FF0000'
|
||||
: themeWiseColor('#6DC376', colors.darkGray, themeMode),
|
||||
}}
|
||||
>
|
||||
{totals.variance}
|
||||
</Typography.Text>
|
||||
);
|
||||
return <Typography.Text>{formatNumber(totals.variance)}</Typography.Text>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderFinancialTableColumnContent = (columnKey: any, task: any) => {
|
||||
const handleFixedCostChange = (value: number | null, taskId: string) => {
|
||||
dispatch(updateTaskFixedCost({ taskId, groupId: table.group_id, fixedCost: value || 0 }));
|
||||
};
|
||||
|
||||
const renderFinancialTableColumnContent = (columnKey: string, task: IProjectFinanceTask) => {
|
||||
switch (columnKey) {
|
||||
case 'task':
|
||||
return (
|
||||
<Tooltip title={task.task}>
|
||||
<Tooltip title={task.name}>
|
||||
<Flex gap={8} align="center">
|
||||
<Typography.Text
|
||||
ellipsis={{ expanded: false }}
|
||||
style={{ maxWidth: 160 }}
|
||||
>
|
||||
{task.task}
|
||||
{task.name}
|
||||
</Typography.Text>
|
||||
|
||||
{task.isbBillable && <DollarCircleOutlined />}
|
||||
{task.billable && <DollarCircleOutlined />}
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'members':
|
||||
return (
|
||||
task?.assignees && <Avatars members={task.assignees} />
|
||||
return task.members && (
|
||||
<Avatars
|
||||
members={task.members.map(member => ({
|
||||
...member,
|
||||
avatar_url: member.avatar_url || undefined
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
case 'hours':
|
||||
return <Typography.Text>{task.hours}</Typography.Text>;
|
||||
case 'cost':
|
||||
return <Typography.Text>{task.cost}</Typography.Text>;
|
||||
return <Typography.Text>{formatNumber(task.estimated_hours / 60)}</Typography.Text>;
|
||||
case 'total_time_logged':
|
||||
return <Typography.Text>{formatNumber(task.total_time_logged / 60)}</Typography.Text>;
|
||||
case 'estimated_cost':
|
||||
return <Typography.Text>{formatNumber(task.estimated_cost)}</Typography.Text>;
|
||||
case 'fixedCost':
|
||||
return (
|
||||
<Input
|
||||
value={task.fixedCost}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
textAlign: 'right',
|
||||
padding: 0,
|
||||
return selectedTask?.id === task.id ? (
|
||||
<InputNumber
|
||||
value={task.fixed_cost}
|
||||
onBlur={(e) => {
|
||||
handleFixedCostChange(Number(e.target.value), task.id);
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
autoFocus
|
||||
style={{ width: '100%', textAlign: 'right' }}
|
||||
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
|
||||
parser={(value) => Number(value!.replace(/\$\s?|(,*)/g, ''))}
|
||||
min={0}
|
||||
precision={2}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text>{formatNumber(task.fixed_cost)}</Typography.Text>
|
||||
);
|
||||
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>
|
||||
);
|
||||
return <Typography.Text>{formatNumber(task.variance)}</Typography.Text>;
|
||||
case 'totalBudget':
|
||||
return <Typography.Text>{formatNumber(task.total_budget)}</Typography.Text>;
|
||||
case 'totalActual':
|
||||
return <Typography.Text>{formatNumber(task.total_actual)}</Typography.Text>;
|
||||
case 'cost':
|
||||
return <Typography.Text>{formatNumber(task.cost || 0)}</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]'}`;
|
||||
// Calculate totals for the current table
|
||||
const totals = useMemo(() => {
|
||||
return tasks.reduce(
|
||||
(acc, task) => ({
|
||||
hours: acc.hours + (task.estimated_hours / 60),
|
||||
total_time_logged: acc.total_time_logged + (task.total_time_logged / 60),
|
||||
estimated_cost: acc.estimated_cost + (task.estimated_cost || 0),
|
||||
total_budget: acc.total_budget + (task.total_budget || 0),
|
||||
total_actual: acc.total_actual + (task.total_actual || 0),
|
||||
variance: acc.variance + (task.variance || 0)
|
||||
}),
|
||||
{
|
||||
hours: 0,
|
||||
total_time_logged: 0,
|
||||
estimated_cost: 0,
|
||||
total_budget: 0,
|
||||
total_actual: 0,
|
||||
variance: 0
|
||||
}
|
||||
);
|
||||
}, [tasks]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* header row */}
|
||||
<tr
|
||||
style={{
|
||||
height: 40,
|
||||
backgroundColor: themeWiseColor(
|
||||
table.color_code,
|
||||
table.color_code_dark,
|
||||
themeMode
|
||||
),
|
||||
fontWeight: 600,
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<td
|
||||
colSpan={3}
|
||||
<Skeleton active loading={loading}>
|
||||
<>
|
||||
{/* header row */}
|
||||
<tr
|
||||
style={{
|
||||
width: 48,
|
||||
textTransform: 'capitalize',
|
||||
textAlign: 'left',
|
||||
paddingInline: 16,
|
||||
height: 40,
|
||||
backgroundColor: themeWiseColor(
|
||||
table.color_code,
|
||||
table.color_code_dark,
|
||||
themeMode
|
||||
),
|
||||
cursor: 'pointer',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
className={customColumnHeaderStyles('tableTitle')}
|
||||
onClick={(e) => setIsCollapse((prev) => !prev)}
|
||||
className="group"
|
||||
>
|
||||
<Flex gap={8} align="center" style={{ color: colors.darkGray }}>
|
||||
{isCollapse ? <RightOutlined /> : <DownOutlined />}
|
||||
{table.name} ({table.tasks.length})
|
||||
</Flex>
|
||||
</td>
|
||||
<td
|
||||
colSpan={3}
|
||||
style={{
|
||||
width: 48,
|
||||
textTransform: 'capitalize',
|
||||
textAlign: 'left',
|
||||
paddingInline: 16,
|
||||
backgroundColor: themeWiseColor(
|
||||
table.color_code,
|
||||
table.color_code_dark,
|
||||
themeMode
|
||||
),
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setIsCollapse((prev) => !prev)}
|
||||
>
|
||||
<Flex gap={8} align="center" style={{ color: colors.darkGray }}>
|
||||
{isCollapse ? <RightOutlined /> : <DownOutlined />}
|
||||
{table.group_name} ({tasks.length})
|
||||
</Flex>
|
||||
</td>
|
||||
|
||||
{financeTableColumns.map(
|
||||
(col) =>
|
||||
col.key !== 'task' &&
|
||||
col.key !== 'members' && (
|
||||
{financeTableColumns.map(
|
||||
(col) =>
|
||||
col.key !== 'task' &&
|
||||
col.key !== 'members' && (
|
||||
<td
|
||||
key={`header-${col.key}`}
|
||||
style={{
|
||||
width: col.width,
|
||||
paddingInline: 16,
|
||||
textAlign: 'end',
|
||||
}}
|
||||
>
|
||||
{renderFinancialTableHeaderContent(col.key)}
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
</tr>
|
||||
|
||||
{/* task rows */}
|
||||
{!isCollapse && tasks.map((task, idx) => (
|
||||
<tr
|
||||
key={task.id}
|
||||
style={{
|
||||
height: 40,
|
||||
background: idx % 2 === 0 ? '#232323' : '#181818',
|
||||
transition: 'background 0.2s',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.background = '#333'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = idx % 2 === 0 ? '#232323' : '#181818'}
|
||||
onClick={() => setSelectedTask(task)}
|
||||
>
|
||||
<td style={{ width: 48, paddingInline: 16 }}>
|
||||
<Checkbox />
|
||||
</td>
|
||||
{financeTableColumns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
key={`${task.id}-${col.key}`}
|
||||
style={{
|
||||
width: col.width,
|
||||
paddingInline: 16,
|
||||
textAlign: 'end',
|
||||
textAlign: col.type === 'string' ? 'left' : 'right',
|
||||
}}
|
||||
>
|
||||
{renderFinancialTableHeaderContent(col.key)}
|
||||
{renderFinancialTableColumnContent(col.key, task)}
|
||||
</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>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
|
||||
@@ -1,61 +1,33 @@
|
||||
import { Flex } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
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';
|
||||
import { projectFinanceApiService } from '@/api/project-finance-ratecard/project-finance.api.service';
|
||||
import { IProjectFinanceGroup } from '@/types/project/project-finance.types';
|
||||
|
||||
type FinanceTabType = 'finance' | 'ratecard';
|
||||
type GroupTypes = 'status' | 'priority' | 'phases';
|
||||
|
||||
interface TaskGroup {
|
||||
group_id: string;
|
||||
group_name: string;
|
||||
tasks: any[];
|
||||
}
|
||||
|
||||
interface FinanceTabProps {
|
||||
groupType: GroupTypes;
|
||||
taskGroups: TaskGroup[];
|
||||
loading: boolean;
|
||||
}
|
||||
import { fetchProjectFinances, setActiveTab, setActiveGroup } from '@/features/projects/finance/project-finance.slice';
|
||||
import { RootState } from '@/app/store';
|
||||
|
||||
const ProjectViewFinance = () => {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const [activeTab, setActiveTab] = useState<FinanceTabType>('finance');
|
||||
const [activeGroup, setActiveGroup] = useState<GroupTypes>('status');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [taskGroups, setTaskGroups] = useState<IProjectFinanceGroup[]>([]);
|
||||
|
||||
const fetchTasks = async () => {
|
||||
if (!projectId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await projectFinanceApiService.getProjectTasks(projectId, activeGroup);
|
||||
if (response.done) {
|
||||
setTaskGroups(response.body);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching tasks:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { activeTab, activeGroup, loading, taskGroups } = useAppSelector((state: RootState) => state.projectFinances);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
}, [projectId, activeGroup]);
|
||||
if (projectId) {
|
||||
dispatch(fetchProjectFinances({ projectId, groupBy: activeGroup }));
|
||||
}
|
||||
}, [projectId, activeGroup, dispatch]);
|
||||
|
||||
return (
|
||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||
<ProjectViewFinanceHeader
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
setActiveTab={(tab) => dispatch(setActiveTab(tab))}
|
||||
activeGroup={activeGroup}
|
||||
setActiveGroup={setActiveGroup}
|
||||
setActiveGroup={(group) => dispatch(setActiveGroup(group))}
|
||||
/>
|
||||
|
||||
{activeTab === 'finance' ? (
|
||||
|
||||
Reference in New Issue
Block a user