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.billable,
tc.fixed_cost, tc.fixed_cost,
tc.sub_tasks_count, 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 CASE
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN ( WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.estimated_seconds) SELECT SUM(sub_tc.estimated_seconds)
FROM task_costs sub_tc 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 ELSE tc.estimated_seconds
END as 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 ( WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.total_time_logged_seconds) SELECT SUM(sub_tc.total_time_logged_seconds)
FROM task_costs sub_tc 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 ELSE tc.total_time_logged_seconds
END as 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 ( WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.estimated_cost) SELECT SUM(sub_tc.estimated_cost)
FROM task_costs sub_tc 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 ELSE tc.estimated_cost
END as 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 ( WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.actual_cost_from_logs) SELECT SUM(sub_tc.actual_cost_from_logs)
FROM task_costs sub_tc 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 ELSE tc.actual_cost_from_logs
END as actual_cost_from_logs END as actual_cost_from_logs
@@ -860,12 +860,12 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
tc.billable, tc.billable,
tc.fixed_cost, tc.fixed_cost,
tc.sub_tasks_count, 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 CASE
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN ( WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.estimated_seconds) SELECT SUM(sub_tc.estimated_seconds)
FROM task_costs sub_tc 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 ELSE tc.estimated_seconds
END as 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 ( WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.total_time_logged_seconds) SELECT SUM(sub_tc.total_time_logged_seconds)
FROM task_costs sub_tc 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 ELSE tc.total_time_logged_seconds
END as 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 ( WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.estimated_cost) SELECT SUM(sub_tc.estimated_cost)
FROM task_costs sub_tc 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 ELSE tc.estimated_cost
END as 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 ( WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
SELECT SUM(sub_tc.actual_cost_from_logs) SELECT SUM(sub_tc.actual_cost_from_logs)
FROM task_costs sub_tc 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 ELSE tc.actual_cost_from_logs
END as 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 }>) => { updateTaskFixedCost: (state, action: PayloadAction<{ taskId: string; groupId: string; fixedCost: number }>) => {
const { taskId, groupId, fixedCost } = action.payload; const { taskId, groupId, fixedCost } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId); const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) { if (group) {
const task = group.tasks.find(t => t.id === taskId); // Recursive function to find and update a task in the hierarchy
if (task) { const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
task.fixed_cost = fixedCost; for (const task of tasks) {
// Don't recalculate here - let the backend handle it and we'll refresh 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 }>) => { updateTaskEstimatedCost: (state, action: PayloadAction<{ taskId: string; groupId: string; estimatedCost: number }>) => {
const { taskId, groupId, estimatedCost } = action.payload; const { taskId, groupId, estimatedCost } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId); const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) { if (group) {
const task = group.tasks.find(t => t.id === taskId); // Recursive function to find and update a task in the hierarchy
if (task) { const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
task.estimated_cost = estimatedCost; for (const task of tasks) {
// Recalculate task costs after updating estimated cost if (task.id === targetId) {
const { totalBudget, totalActual, variance } = calculateTaskCosts(task); task.estimated_cost = estimatedCost;
task.total_budget = totalBudget; // Recalculate task costs after updating estimated cost
task.total_actual = totalActual; const { totalBudget, totalActual, variance } = calculateTaskCosts(task);
task.variance = variance; 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 }>) => { updateTaskTimeLogged: (state, action: PayloadAction<{ taskId: string; groupId: string; timeLoggedSeconds: number; timeLoggedString: string }>) => {
const { taskId, groupId, timeLoggedSeconds, timeLoggedString } = action.payload; const { taskId, groupId, timeLoggedSeconds, timeLoggedString } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId); const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) { if (group) {
const task = group.tasks.find(t => t.id === taskId); // Recursive function to find and update a task in the hierarchy
if (task) { const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
task.total_time_logged_seconds = timeLoggedSeconds; for (const task of tasks) {
task.total_time_logged = timeLoggedString; if (task.id === targetId) {
// Recalculate task costs after updating time logged task.total_time_logged_seconds = timeLoggedSeconds;
const { totalBudget, totalActual, variance } = calculateTaskCosts(task); task.total_time_logged = timeLoggedString;
task.total_budget = totalBudget; // Recalculate task costs after updating time logged
task.total_actual = totalActual; const { totalBudget, totalActual, variance } = calculateTaskCosts(task);
task.variance = variance; 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 }>) => { toggleTaskExpansion: (state, action: PayloadAction<{ taskId: string; groupId: string }>) => {
const { taskId, groupId } = action.payload; const { taskId, groupId } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId); const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) { if (group) {
const task = group.tasks.find(t => t.id === taskId); // Recursive function to find and toggle a task in the hierarchy
if (task) { const findAndToggleTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
task.show_sub_tasks = !task.show_sub_tasks; 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>) => { updateProjectFinanceCurrency: (state, action: PayloadAction<string>) => {
@@ -200,26 +256,56 @@ export const projectFinancesSlice = createSlice({
.addCase(updateTaskFixedCostAsync.fulfilled, (state, action) => { .addCase(updateTaskFixedCostAsync.fulfilled, (state, action) => {
const { taskId, groupId, fixedCost } = action.payload; const { taskId, groupId, fixedCost } = action.payload;
const group = state.taskGroups.find(g => g.group_id === groupId); const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) { if (group) {
const task = group.tasks.find(t => t.id === taskId); // Recursive function to find and update a task in the hierarchy
if (task) { const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
task.fixed_cost = fixedCost; for (const task of tasks) {
// Don't recalculate here - trigger a refresh instead for accuracy 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) => { .addCase(fetchSubTasks.fulfilled, (state, action) => {
const { parentTaskId, subTasks } = action.payload; 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 // Find the parent task in any group and add the subtasks
for (const group of state.taskGroups) { for (const group of state.taskGroups) {
const parentTask = group.tasks.find(t => t.id === parentTaskId); if (findAndUpdateTask(group.tasks, parentTaskId)) {
if (parentTask) {
parentTask.sub_tasks = subTasks.map(subTask => ({
...subTask,
is_sub_task: true,
parent_task_id: parentTaskId
}));
parentTask.show_sub_tasks = true;
break; 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 { 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 { useAppSelector } from '@/hooks/useAppSelector';
import { import {
DollarCircleOutlined, DollarCircleOutlined,
@@ -24,6 +24,8 @@ import { useParams } from 'react-router-dom';
import { useAuthService } from '@/hooks/useAuth'; import { useAuthService } from '@/hooks/useAuth';
import { canEditFixedCost } from '@/utils/finance-permissions'; import { canEditFixedCost } from '@/utils/finance-permissions';
import './finance-table.css'; import './finance-table.css';
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
import { fetchPriorities } from '@/features/taskAttributes/taskPrioritySlice';
type FinanceTableProps = { type FinanceTableProps = {
table: IProjectFinanceGroup; table: IProjectFinanceGroup;
@@ -41,6 +43,7 @@ const FinanceTable = ({
const [selectedTask, setSelectedTask] = useState<IProjectFinanceTask | null>(null); const [selectedTask, setSelectedTask] = useState<IProjectFinanceTask | null>(null);
const [editingFixedCostValue, setEditingFixedCostValue] = useState<number | null>(null); const [editingFixedCostValue, setEditingFixedCostValue] = useState<number | null>(null);
const [tasks, setTasks] = useState<IProjectFinanceTask[]>(table.tasks); const [tasks, setTasks] = useState<IProjectFinanceTask[]>(table.tasks);
const [hoveredTaskId, setHoveredTaskId] = useState<string | null>(null);
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null); const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -159,8 +162,10 @@ const FinanceTable = ({
if (!taskId || !projectId) return; if (!taskId || !projectId) return;
dispatch(setSelectedTaskId(taskId)); dispatch(setSelectedTaskId(taskId));
dispatch(setShowTaskDrawer(true)); dispatch(fetchPhasesByProjectId(projectId));
dispatch(fetchPriorities());
dispatch(fetchTask({ taskId, projectId })); dispatch(fetchTask({ taskId, projectId }));
dispatch(setShowTaskDrawer(true));
}; };
// Handle task expansion/collapse // Handle task expansion/collapse
@@ -208,35 +213,112 @@ const FinanceTable = ({
setEditingFixedCostValue(null); 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) { switch (columnKey) {
case FinanceTableColumnKeys.TASK: case FinanceTableColumnKeys.TASK:
return ( return (
<Tooltip title={task.name}> <Tooltip title={task.name}>
<Flex gap={8} align="center"> <Flex gap={8} align="center" style={{ paddingLeft: getTaskIndentation(level) }}>
{/* Indentation for subtasks */}
{task.is_sub_task && <div style={{ width: 20 }} />}
{/* Expand/collapse icon for parent tasks */} {/* Expand/collapse icon for parent tasks */}
{task.sub_tasks_count > 0 && ( {task.sub_tasks_count > 0 && (
<div <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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleTaskExpansion(task); handleTaskExpansion(task);
}} }}
> >
{task.show_sub_tasks ? <DownOutlined /> : <RightOutlined />} {task.show_sub_tasks ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
</div> </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 */} {/* Task name */}
<Typography.Text <Typography.Text
className="finance-table-task-name"
ellipsis={{ expanded: false }} ellipsis={{ expanded: false }}
style={{ 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', 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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -251,7 +333,7 @@ const FinanceTable = ({
> >
{task.name} {task.name}
</Typography.Text> </Typography.Text>
{task.billable && <DollarCircleOutlined />} {task.billable && <DollarCircleOutlined style={{ fontSize: 12, flexShrink: 0 }} />}
</Flex> </Flex>
</Tooltip> </Tooltip>
); );
@@ -277,11 +359,11 @@ const FinanceTable = ({
</div> </div>
); );
case FinanceTableColumnKeys.HOURS: 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: 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: 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: case FinanceTableColumnKeys.FIXED_COST:
return selectedTask?.id === task.id && hasEditPermission ? ( return selectedTask?.id === task.id && hasEditPermission ? (
<InputNumber <InputNumber
@@ -300,7 +382,7 @@ const FinanceTable = ({
immediateSaveFixedCost(editingFixedCostValue, task.id); immediateSaveFixedCost(editingFixedCostValue, task.id);
}} }}
autoFocus 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, ',')} formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
parser={(value) => Number(value!.replace(/\$\s?|(,*)/g, ''))} parser={(value) => Number(value!.replace(/\$\s?|(,*)/g, ''))}
min={0} min={0}
@@ -313,7 +395,8 @@ const FinanceTable = ({
cursor: hasEditPermission ? 'pointer' : 'default', cursor: hasEditPermission ? 'pointer' : 'default',
width: '100%', width: '100%',
display: 'block', display: 'block',
opacity: hasEditPermission ? 1 : 0.7 opacity: hasEditPermission ? 1 : 0.7,
fontSize: Math.max(12, 14 - level * 0.5)
}} }}
onClick={hasEditPermission ? (e) => { onClick={hasEditPermission ? (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -328,7 +411,8 @@ const FinanceTable = ({
return ( return (
<Typography.Text <Typography.Text
style={{ 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)) : {task.variance < 0 ? '+' + formatNumber(Math.abs(task.variance)) :
@@ -337,11 +421,11 @@ const FinanceTable = ({
</Typography.Text> </Typography.Text>
); );
case FinanceTableColumnKeys.TOTAL_BUDGET: 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: 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: 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: default:
return null; return null;
} }
@@ -363,32 +447,29 @@ const FinanceTable = ({
return parts.join(' '); return parts.join(' ');
}; };
// Flatten tasks to include subtasks for rendering // Generate flattened task list with all nested levels
const flattenedTasks = useMemo(() => { const flattenedTasks = useMemo(() => {
const flattened: IProjectFinanceTask[] = []; const flattened: React.ReactElement[] = [];
tasks.forEach(task => { tasks.forEach(task => {
// Add the parent task flattened.push(...renderTaskHierarchy(task, 0));
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);
});
}
}); });
return flattened; 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(() => { const totals = useMemo(() => {
return tasks.reduce( return tasks.reduce(
(acc, task) => { (acc, task) => {
// Calculate actual cost from logs (total_actual - fixed_cost) // Calculate actual cost from logs (total_actual - fixed_cost)
const actualCostFromLogs = (task.total_actual || 0) - (task.fixed_cost || 0); 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 { return {
hours: acc.hours + (task.estimated_seconds || 0), hours: acc.hours + (task.estimated_seconds || 0),
total_time_logged: acc.total_time_logged + (task.total_time_logged_seconds || 0), total_time_logged: acc.total_time_logged + (task.total_time_logged_seconds || 0),
@@ -480,43 +561,8 @@ const FinanceTable = ({
)} )}
</tr> </tr>
{/* task rows */} {/* task rows with recursive hierarchy */}
{!isCollapse && flattenedTasks.map((task, idx) => ( {!isCollapse && flattenedTasks}
<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>
))}
</> </>
); );
}; };