refactor(project-finance): improve task cost calculations and UI hierarchy

- Updated SQL queries in the ProjectFinanceController to exclude parent tasks from descendant cost calculations, ensuring accurate financial data aggregation.
- Refactored the project finance slice to implement recursive task updates for fixed costs, estimated costs, and time logged, enhancing task management efficiency.
- Enhanced the FinanceTable component to visually represent task hierarchy with improved indentation and hover effects, improving user experience and clarity.
- Added responsive styles for nested tasks and adjusted task name styling for better readability across different levels.
This commit is contained in:
chamikaJ
2025-06-09 11:24:49 +05:30
parent 49196aac2e
commit 509fcc8f64
4 changed files with 316 additions and 122 deletions

View File

@@ -176,12 +176,12 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
tc.billable,
tc.fixed_cost,
tc.sub_tasks_count,
-- For parent tasks, sum values from all descendants including self
-- For parent tasks, sum values from descendants only (exclude parent task itself)
CASE
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.estimated_seconds)
FROM task_costs sub_tc
WHERE sub_tc.root_id = tc.id
WHERE sub_tc.root_id = tc.id AND sub_tc.id != tc.id
)
ELSE tc.estimated_seconds
END as estimated_seconds,
@@ -189,7 +189,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.total_time_logged_seconds)
FROM task_costs sub_tc
WHERE sub_tc.root_id = tc.id
WHERE sub_tc.root_id = tc.id AND sub_tc.id != tc.id
)
ELSE tc.total_time_logged_seconds
END as total_time_logged_seconds,
@@ -197,7 +197,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.estimated_cost)
FROM task_costs sub_tc
WHERE sub_tc.root_id = tc.id
WHERE sub_tc.root_id = tc.id AND sub_tc.id != tc.id
)
ELSE tc.estimated_cost
END as estimated_cost,
@@ -205,7 +205,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.actual_cost_from_logs)
FROM task_costs sub_tc
WHERE sub_tc.root_id = tc.id
WHERE sub_tc.root_id = tc.id AND sub_tc.id != tc.id
)
ELSE tc.actual_cost_from_logs
END as actual_cost_from_logs
@@ -860,12 +860,12 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
tc.billable,
tc.fixed_cost,
tc.sub_tasks_count,
-- For parent tasks, sum values from all descendants including self
-- For parent tasks, sum values from descendants only (exclude parent task itself)
CASE
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.estimated_seconds)
FROM task_costs sub_tc
WHERE sub_tc.root_id = tc.id
WHERE sub_tc.root_id = tc.id AND sub_tc.id != tc.id
)
ELSE tc.estimated_seconds
END as estimated_seconds,
@@ -873,7 +873,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.total_time_logged_seconds)
FROM task_costs sub_tc
WHERE sub_tc.root_id = tc.id
WHERE sub_tc.root_id = tc.id AND sub_tc.id != tc.id
)
ELSE tc.total_time_logged_seconds
END as total_time_logged_seconds,
@@ -881,7 +881,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.estimated_cost)
FROM task_costs sub_tc
WHERE sub_tc.root_id = tc.id
WHERE sub_tc.root_id = tc.id AND sub_tc.id != tc.id
)
ELSE tc.estimated_cost
END as estimated_cost,
@@ -889,7 +889,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.actual_cost_from_logs)
FROM task_costs sub_tc
WHERE sub_tc.root_id = tc.id
WHERE sub_tc.root_id = tc.id AND sub_tc.id != tc.id
)
ELSE tc.actual_cost_from_logs
END as actual_cost_from_logs

View File

@@ -122,53 +122,109 @@ export const projectFinancesSlice = createSlice({
updateTaskFixedCost: (state, action: PayloadAction<{ taskId: string; groupId: string; fixedCost: number }>) => {
const { taskId, groupId, fixedCost } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) {
const task = group.tasks.find(t => t.id === taskId);
if (task) {
task.fixed_cost = fixedCost;
// Don't recalculate here - let the backend handle it and we'll refresh
}
// Recursive function to find and update a task in the hierarchy
const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
for (const task of tasks) {
if (task.id === targetId) {
task.fixed_cost = fixedCost;
// Don't recalculate here - let the backend handle it and we'll refresh
return true;
}
// Search in subtasks recursively
if (task.sub_tasks && findAndUpdateTask(task.sub_tasks, targetId)) {
return true;
}
}
return false;
};
findAndUpdateTask(group.tasks, taskId);
}
},
updateTaskEstimatedCost: (state, action: PayloadAction<{ taskId: string; groupId: string; estimatedCost: number }>) => {
const { taskId, groupId, estimatedCost } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) {
const task = group.tasks.find(t => t.id === taskId);
if (task) {
task.estimated_cost = estimatedCost;
// Recalculate task costs after updating estimated cost
const { totalBudget, totalActual, variance } = calculateTaskCosts(task);
task.total_budget = totalBudget;
task.total_actual = totalActual;
task.variance = variance;
}
// Recursive function to find and update a task in the hierarchy
const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
for (const task of tasks) {
if (task.id === targetId) {
task.estimated_cost = estimatedCost;
// Recalculate task costs after updating estimated cost
const { totalBudget, totalActual, variance } = calculateTaskCosts(task);
task.total_budget = totalBudget;
task.total_actual = totalActual;
task.variance = variance;
return true;
}
// Search in subtasks recursively
if (task.sub_tasks && findAndUpdateTask(task.sub_tasks, targetId)) {
return true;
}
}
return false;
};
findAndUpdateTask(group.tasks, taskId);
}
},
updateTaskTimeLogged: (state, action: PayloadAction<{ taskId: string; groupId: string; timeLoggedSeconds: number; timeLoggedString: string }>) => {
const { taskId, groupId, timeLoggedSeconds, timeLoggedString } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) {
const task = group.tasks.find(t => t.id === taskId);
if (task) {
task.total_time_logged_seconds = timeLoggedSeconds;
task.total_time_logged = timeLoggedString;
// Recalculate task costs after updating time logged
const { totalBudget, totalActual, variance } = calculateTaskCosts(task);
task.total_budget = totalBudget;
task.total_actual = totalActual;
task.variance = variance;
}
// Recursive function to find and update a task in the hierarchy
const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
for (const task of tasks) {
if (task.id === targetId) {
task.total_time_logged_seconds = timeLoggedSeconds;
task.total_time_logged = timeLoggedString;
// Recalculate task costs after updating time logged
const { totalBudget, totalActual, variance } = calculateTaskCosts(task);
task.total_budget = totalBudget;
task.total_actual = totalActual;
task.variance = variance;
return true;
}
// Search in subtasks recursively
if (task.sub_tasks && findAndUpdateTask(task.sub_tasks, targetId)) {
return true;
}
}
return false;
};
findAndUpdateTask(group.tasks, taskId);
}
},
toggleTaskExpansion: (state, action: PayloadAction<{ taskId: string; groupId: string }>) => {
const { taskId, groupId } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) {
const task = group.tasks.find(t => t.id === taskId);
if (task) {
task.show_sub_tasks = !task.show_sub_tasks;
}
// Recursive function to find and toggle a task in the hierarchy
const findAndToggleTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
for (const task of tasks) {
if (task.id === targetId) {
task.show_sub_tasks = !task.show_sub_tasks;
return true;
}
// Search in subtasks recursively
if (task.sub_tasks && findAndToggleTask(task.sub_tasks, targetId)) {
return true;
}
}
return false;
};
findAndToggleTask(group.tasks, taskId);
}
},
updateProjectFinanceCurrency: (state, action: PayloadAction<string>) => {
@@ -200,26 +256,56 @@ export const projectFinancesSlice = createSlice({
.addCase(updateTaskFixedCostAsync.fulfilled, (state, action) => {
const { taskId, groupId, fixedCost } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) {
const task = group.tasks.find(t => t.id === taskId);
if (task) {
task.fixed_cost = fixedCost;
// Don't recalculate here - trigger a refresh instead for accuracy
}
// Recursive function to find and update a task in the hierarchy
const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
for (const task of tasks) {
if (task.id === targetId) {
task.fixed_cost = fixedCost;
// Don't recalculate here - trigger a refresh instead for accuracy
return true;
}
// Search in subtasks recursively
if (task.sub_tasks && findAndUpdateTask(task.sub_tasks, targetId)) {
return true;
}
}
return false;
};
findAndUpdateTask(group.tasks, taskId);
}
})
.addCase(fetchSubTasks.fulfilled, (state, action) => {
const { parentTaskId, subTasks } = action.payload;
// Recursive function to find and update a task in the hierarchy
const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
for (const task of tasks) {
if (task.id === targetId) {
// Found the parent task, add subtasks
task.sub_tasks = subTasks.map(subTask => ({
...subTask,
is_sub_task: true,
parent_task_id: targetId
}));
task.show_sub_tasks = true;
return true;
}
// Search in subtasks recursively
if (task.sub_tasks && findAndUpdateTask(task.sub_tasks, targetId)) {
return true;
}
}
return false;
};
// Find the parent task in any group and add the subtasks
for (const group of state.taskGroups) {
const parentTask = group.tasks.find(t => t.id === parentTaskId);
if (parentTask) {
parentTask.sub_tasks = subTasks.map(subTask => ({
...subTask,
is_sub_task: true,
parent_task_id: parentTaskId
}));
parentTask.show_sub_tasks = true;
if (findAndUpdateTask(group.tasks, parentTaskId)) {
break;
}
}

View File

@@ -1 +1,63 @@
/* Finance Table Styles */
/* Finance Table Styles */
/* Enhanced hierarchy visual indicators */
.finance-table-task-row {
transition: all 0.2s ease-in-out;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.dark .finance-table-task-row {
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
/* Hover effect is now handled by inline styles in the component for consistency */
/* Nested task styling */
.finance-table-nested-task {
/* No visual connectors, just clean indentation */
}
/* Expand/collapse button styling */
.finance-table-expand-btn {
transition: all 0.2s ease-in-out;
border-radius: 2px;
padding: 2px;
}
.finance-table-expand-btn:hover {
background: rgba(0, 0, 0, 0.05);
transform: scale(1.1);
}
.dark .finance-table-expand-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
/* Task name styling for different levels */
.finance-table-task-name {
transition: all 0.2s ease-in-out;
}
.finance-table-task-name:hover {
color: #40a9ff !important;
}
/* Fixed cost input styling */
.fixed-cost-input {
border-radius: 4px;
}
.fixed-cost-input:focus {
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* Responsive adjustments for nested content */
@media (max-width: 768px) {
.finance-table-nested-task {
padding-left: 12px;
}
.finance-table-task-name {
font-size: 12px !important;
}
}

View File

@@ -1,5 +1,5 @@
import { Flex, InputNumber, Skeleton, Tooltip, Typography } from 'antd';
import { useEffect, useMemo, useState, useRef } from 'react';
import React, { useEffect, useMemo, useState, useRef } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import {
DollarCircleOutlined,
@@ -24,6 +24,8 @@ import { useParams } from 'react-router-dom';
import { useAuthService } from '@/hooks/useAuth';
import { canEditFixedCost } from '@/utils/finance-permissions';
import './finance-table.css';
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
type FinanceTableProps = {
table: IProjectFinanceGroup;
@@ -41,6 +43,7 @@ const FinanceTable = ({
const [selectedTask, setSelectedTask] = useState<IProjectFinanceTask | null>(null);
const [editingFixedCostValue, setEditingFixedCostValue] = useState<number | null>(null);
const [tasks, setTasks] = useState<IProjectFinanceTask[]>(table.tasks);
const [hoveredTaskId, setHoveredTaskId] = useState<string | null>(null);
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const dispatch = useAppDispatch();
@@ -159,8 +162,10 @@ const FinanceTable = ({
if (!taskId || !projectId) return;
dispatch(setSelectedTaskId(taskId));
dispatch(setShowTaskDrawer(true));
dispatch(fetchPhasesByProjectId(projectId));
dispatch(fetchPriorities());
dispatch(fetchTask({ taskId, projectId }));
dispatch(setShowTaskDrawer(true));
};
// Handle task expansion/collapse
@@ -208,35 +213,112 @@ const FinanceTable = ({
setEditingFixedCostValue(null);
};
const renderFinancialTableColumnContent = (columnKey: FinanceTableColumnKeys, task: IProjectFinanceTask) => {
// Calculate indentation based on nesting level
const getTaskIndentation = (level: number) => level * 32; // 32px per level for better visibility
// Recursive function to render task hierarchy
const renderTaskHierarchy = (task: IProjectFinanceTask, level: number = 0): React.ReactElement[] => {
const elements: React.ReactElement[] = [];
// Add the current task
const isHovered = hoveredTaskId === task.id;
const rowIndex = elements.length;
const defaultBg = rowIndex % 2 === 0 ? themeWiseColor('#fafafa', '#232323', themeMode) : themeWiseColor('#ffffff', '#181818', themeMode);
const hoverBg = themeMode === 'dark' ? 'rgba(64, 169, 255, 0.08)' : 'rgba(24, 144, 255, 0.04)';
elements.push(
<tr
key={task.id}
style={{
height: 40,
background: isHovered ? hoverBg : defaultBg,
transition: 'background 0.2s',
}}
className={`finance-table-task-row ${level > 0 ? 'finance-table-nested-task' : ''} ${themeMode === 'dark' ? 'dark' : ''}`}
onMouseEnter={() => setHoveredTaskId(task.id)}
onMouseLeave={() => setHoveredTaskId(null)}
>
{financeTableColumns.map((col) => (
<td
key={`${task.id}-${col.key}`}
style={{
width: col.width,
paddingInline: 16,
textAlign: col.type === 'string' ? 'left' : 'right',
backgroundColor: (col.key === FinanceTableColumnKeys.TASK || col.key === FinanceTableColumnKeys.MEMBERS) ?
(isHovered ? hoverBg : defaultBg) :
(isHovered ? hoverBg : 'transparent'),
cursor: 'default'
}}
className={customColumnStyles(col.key)}
onClick={
col.key === FinanceTableColumnKeys.FIXED_COST
? (e) => e.stopPropagation()
: undefined
}
>
{renderFinancialTableColumnContent(col.key, task, level)}
</td>
))}
</tr>
);
// Add subtasks recursively if they are expanded and loaded
if (task.show_sub_tasks && task.sub_tasks) {
task.sub_tasks.forEach(subTask => {
elements.push(...renderTaskHierarchy(subTask, level + 1));
});
}
return elements;
};
const renderFinancialTableColumnContent = (columnKey: FinanceTableColumnKeys, task: IProjectFinanceTask, level: number = 0) => {
switch (columnKey) {
case FinanceTableColumnKeys.TASK:
return (
<Tooltip title={task.name}>
<Flex gap={8} align="center">
{/* Indentation for subtasks */}
{task.is_sub_task && <div style={{ width: 20 }} />}
<Flex gap={8} align="center" style={{ paddingLeft: getTaskIndentation(level) }}>
{/* Expand/collapse icon for parent tasks */}
{task.sub_tasks_count > 0 && (
<div
style={{ cursor: 'pointer', width: 16, display: 'flex', justifyContent: 'center' }}
className="finance-table-expand-btn"
style={{
cursor: 'pointer',
width: 18,
height: 18,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexShrink: 0,
zIndex: 1
}}
onClick={(e) => {
e.stopPropagation();
handleTaskExpansion(task);
}}
>
{task.show_sub_tasks ? <DownOutlined /> : <RightOutlined />}
{task.show_sub_tasks ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
</div>
)}
{/* Spacer for tasks without subtasks to align with those that have expand icons */}
{task.sub_tasks_count === 0 && level > 0 && (
<div style={{ width: 18, height: 18, flexShrink: 0 }} />
)}
{/* Task name */}
<Typography.Text
className="finance-table-task-name"
ellipsis={{ expanded: false }}
style={{
maxWidth: task.is_sub_task ? 140 : (task.sub_tasks_count > 0 ? 140 : 160),
maxWidth: Math.max(100, 200 - getTaskIndentation(level) - (task.sub_tasks_count > 0 ? 26 : 18)),
cursor: 'pointer',
color: '#1890ff'
color: '#1890ff',
fontSize: Math.max(12, 14 - level * 0.3), // Slightly smaller font for deeper levels
opacity: Math.max(0.85, 1 - level * 0.03), // Slightly faded for deeper levels
fontWeight: level > 0 ? 400 : 500 // Slightly lighter weight for nested tasks
}}
onClick={(e) => {
e.stopPropagation();
@@ -251,7 +333,7 @@ const FinanceTable = ({
>
{task.name}
</Typography.Text>
{task.billable && <DollarCircleOutlined />}
{task.billable && <DollarCircleOutlined style={{ fontSize: 12, flexShrink: 0 }} />}
</Flex>
</Tooltip>
);
@@ -277,11 +359,11 @@ const FinanceTable = ({
</div>
);
case FinanceTableColumnKeys.HOURS:
return <Typography.Text>{task.estimated_hours}</Typography.Text>;
return <Typography.Text style={{ fontSize: Math.max(12, 14 - level * 0.5) }}>{task.estimated_hours}</Typography.Text>;
case FinanceTableColumnKeys.TOTAL_TIME_LOGGED:
return <Typography.Text>{task.total_time_logged}</Typography.Text>;
return <Typography.Text style={{ fontSize: Math.max(12, 14 - level * 0.5) }}>{task.total_time_logged}</Typography.Text>;
case FinanceTableColumnKeys.ESTIMATED_COST:
return <Typography.Text>{formatNumber(task.estimated_cost)}</Typography.Text>;
return <Typography.Text style={{ fontSize: Math.max(12, 14 - level * 0.5) }}>{formatNumber(task.estimated_cost)}</Typography.Text>;
case FinanceTableColumnKeys.FIXED_COST:
return selectedTask?.id === task.id && hasEditPermission ? (
<InputNumber
@@ -300,7 +382,7 @@ const FinanceTable = ({
immediateSaveFixedCost(editingFixedCostValue, task.id);
}}
autoFocus
style={{ width: '100%', textAlign: 'right' }}
style={{ width: '100%', textAlign: 'right', fontSize: Math.max(12, 14 - level * 0.5) }}
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={(value) => Number(value!.replace(/\$\s?|(,*)/g, ''))}
min={0}
@@ -313,7 +395,8 @@ const FinanceTable = ({
cursor: hasEditPermission ? 'pointer' : 'default',
width: '100%',
display: 'block',
opacity: hasEditPermission ? 1 : 0.7
opacity: hasEditPermission ? 1 : 0.7,
fontSize: Math.max(12, 14 - level * 0.5)
}}
onClick={hasEditPermission ? (e) => {
e.stopPropagation();
@@ -328,7 +411,8 @@ const FinanceTable = ({
return (
<Typography.Text
style={{
color: task.variance > 0 ? '#FF0000' : '#6DC376'
color: task.variance > 0 ? '#FF0000' : '#6DC376',
fontSize: Math.max(12, 14 - level * 0.5)
}}
>
{task.variance < 0 ? '+' + formatNumber(Math.abs(task.variance)) :
@@ -337,11 +421,11 @@ const FinanceTable = ({
</Typography.Text>
);
case FinanceTableColumnKeys.TOTAL_BUDGET:
return <Typography.Text>{formatNumber(task.total_budget)}</Typography.Text>;
return <Typography.Text style={{ fontSize: Math.max(12, 14 - level * 0.5) }}>{formatNumber(task.total_budget)}</Typography.Text>;
case FinanceTableColumnKeys.TOTAL_ACTUAL:
return <Typography.Text>{formatNumber(task.total_actual)}</Typography.Text>;
return <Typography.Text style={{ fontSize: Math.max(12, 14 - level * 0.5) }}>{formatNumber(task.total_actual)}</Typography.Text>;
case FinanceTableColumnKeys.COST:
return <Typography.Text>{formatNumber((task.total_actual || 0) - (task.fixed_cost || 0))}</Typography.Text>;
return <Typography.Text style={{ fontSize: Math.max(12, 14 - level * 0.5) }}>{formatNumber((task.total_actual || 0) - (task.fixed_cost || 0))}</Typography.Text>;
default:
return null;
}
@@ -363,32 +447,29 @@ const FinanceTable = ({
return parts.join(' ');
};
// Flatten tasks to include subtasks for rendering
// Generate flattened task list with all nested levels
const flattenedTasks = useMemo(() => {
const flattened: IProjectFinanceTask[] = [];
const flattened: React.ReactElement[] = [];
tasks.forEach(task => {
// Add the parent task
flattened.push(task);
// Add subtasks if they are expanded and loaded
if (task.show_sub_tasks && task.sub_tasks) {
task.sub_tasks.forEach(subTask => {
flattened.push(subTask);
});
}
flattened.push(...renderTaskHierarchy(task, 0));
});
return flattened;
}, [tasks]);
}, [tasks, selectedTask, editingFixedCostValue, hasEditPermission, themeMode, hoveredTaskId]);
// Calculate totals for the current table (only count parent tasks to avoid double counting)
// Calculate totals for the current table
// Since the backend already aggregates subtask values into parent tasks,
// we only need to sum the parent tasks (tasks without is_sub_task flag)
const totals = useMemo(() => {
return tasks.reduce(
(acc, task) => {
// Calculate actual cost from logs (total_actual - fixed_cost)
const actualCostFromLogs = (task.total_actual || 0) - (task.fixed_cost || 0);
// The backend already handles aggregation for parent tasks with subtasks
// Parent tasks contain the sum of their subtasks' values
// So we can safely sum all parent tasks (which are the tasks in this array)
return {
hours: acc.hours + (task.estimated_seconds || 0),
total_time_logged: acc.total_time_logged + (task.total_time_logged_seconds || 0),
@@ -480,43 +561,8 @@ const FinanceTable = ({
)}
</tr>
{/* task rows */}
{!isCollapse && flattenedTasks.map((task, idx) => (
<tr
key={task.id}
style={{
height: 40,
background: idx % 2 === 0 ? themeWiseColor('#fafafa', '#232323', themeMode) : themeWiseColor('#ffffff', '#181818', themeMode),
transition: 'background 0.2s',
}}
className={themeMode === 'dark' ? 'dark' : ''}
onMouseEnter={e => e.currentTarget.style.background = themeWiseColor('#f0f0f0', '#333', themeMode)}
onMouseLeave={e => e.currentTarget.style.background = idx % 2 === 0 ? themeWiseColor('#fafafa', '#232323', themeMode) : themeWiseColor('#ffffff', '#181818', themeMode)}
>
{financeTableColumns.map((col) => (
<td
key={`${task.id}-${col.key}`}
style={{
width: col.width,
paddingInline: 16,
textAlign: col.type === 'string' ? 'left' : 'right',
backgroundColor: (col.key === FinanceTableColumnKeys.TASK || col.key === FinanceTableColumnKeys.MEMBERS) ?
(idx % 2 === 0 ? themeWiseColor('#fafafa', '#232323', themeMode) : themeWiseColor('#ffffff', '#181818', themeMode)) :
'transparent',
cursor: 'default'
}}
className={customColumnStyles(col.key)}
onClick={
col.key === FinanceTableColumnKeys.FIXED_COST
? (e) => e.stopPropagation()
: undefined
}
>
{renderFinancialTableColumnContent(col.key, task)}
</td>
))}
</tr>
))}
{/* task rows with recursive hierarchy */}
{!isCollapse && flattenedTasks}
</>
);
};