feat(project-finance): enhance fixed cost calculations and parent task updates
- Updated SQL queries in ProjectFinanceController to aggregate fixed costs from current tasks and their descendants, improving financial accuracy. - Introduced a new async thunk to update task fixed costs with recalculation, ensuring UI responsiveness and accurate parent task totals. - Implemented recursive functions in the project finance slice to maintain accurate financial data for parent tasks based on subtasks. - Enhanced the FinanceTable component to support these updates, ensuring totals reflect the latest calculations across task hierarchies.
This commit is contained in:
@@ -174,7 +174,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
tc.phase_id,
|
tc.phase_id,
|
||||||
tc.assignees,
|
tc.assignees,
|
||||||
tc.billable,
|
tc.billable,
|
||||||
tc.fixed_cost,
|
-- Fixed cost aggregation: include current task + all descendants
|
||||||
|
CASE
|
||||||
|
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
|
||||||
|
SELECT SUM(sub_tc.fixed_cost)
|
||||||
|
FROM task_costs sub_tc
|
||||||
|
WHERE sub_tc.root_id = tc.id
|
||||||
|
)
|
||||||
|
ELSE tc.fixed_cost
|
||||||
|
END as fixed_cost,
|
||||||
tc.sub_tasks_count,
|
tc.sub_tasks_count,
|
||||||
-- For parent tasks, sum values from descendants only (exclude parent task itself)
|
-- For parent tasks, sum values from descendants only (exclude parent task itself)
|
||||||
CASE
|
CASE
|
||||||
@@ -688,7 +696,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
tc.phase_id,
|
tc.phase_id,
|
||||||
tc.assignees,
|
tc.assignees,
|
||||||
tc.billable,
|
tc.billable,
|
||||||
tc.fixed_cost,
|
-- Fixed cost aggregation: include current task + all descendants
|
||||||
|
CASE
|
||||||
|
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
|
||||||
|
SELECT SUM(sub_tc.fixed_cost)
|
||||||
|
FROM task_costs sub_tc
|
||||||
|
WHERE sub_tc.root_id = tc.id
|
||||||
|
)
|
||||||
|
ELSE tc.fixed_cost
|
||||||
|
END as fixed_cost,
|
||||||
tc.sub_tasks_count,
|
tc.sub_tasks_count,
|
||||||
-- For subtasks that have their own sub-subtasks, sum values from descendants only
|
-- For subtasks that have their own sub-subtasks, sum values from descendants only
|
||||||
CASE
|
CASE
|
||||||
@@ -932,7 +948,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
tc.phase_id,
|
tc.phase_id,
|
||||||
tc.assignees,
|
tc.assignees,
|
||||||
tc.billable,
|
tc.billable,
|
||||||
tc.fixed_cost,
|
-- Fixed cost aggregation: include current task + all descendants
|
||||||
|
CASE
|
||||||
|
WHEN tc.level = 0 AND tc.sub_tasks_count > 0 THEN (
|
||||||
|
SELECT SUM(sub_tc.fixed_cost)
|
||||||
|
FROM task_costs sub_tc
|
||||||
|
WHERE sub_tc.root_id = tc.id
|
||||||
|
)
|
||||||
|
ELSE tc.fixed_cost
|
||||||
|
END as fixed_cost,
|
||||||
tc.sub_tasks_count,
|
tc.sub_tasks_count,
|
||||||
-- For parent tasks, sum values from descendants only (exclude parent task itself)
|
-- For parent tasks, sum values from descendants only (exclude parent task itself)
|
||||||
CASE
|
CASE
|
||||||
|
|||||||
@@ -106,6 +106,26 @@ export const updateTaskFixedCostAsync = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const updateTaskFixedCostWithRecalculation = createAsyncThunk(
|
||||||
|
'projectFinances/updateTaskFixedCostWithRecalculation',
|
||||||
|
async ({ taskId, groupId, fixedCost, projectId, groupBy, billableFilter }: {
|
||||||
|
taskId: string;
|
||||||
|
groupId: string;
|
||||||
|
fixedCost: number;
|
||||||
|
projectId: string;
|
||||||
|
groupBy: GroupTypes;
|
||||||
|
billableFilter?: BillableFilterType;
|
||||||
|
}, { dispatch }) => {
|
||||||
|
// Update the fixed cost
|
||||||
|
await projectFinanceApiService.updateTaskFixedCost(taskId, fixedCost);
|
||||||
|
|
||||||
|
// Trigger a silent refresh to get accurate calculations from backend
|
||||||
|
dispatch(fetchProjectFinancesSilent({ projectId, groupBy, billableFilter }));
|
||||||
|
|
||||||
|
return { taskId, groupId, fixedCost };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const projectFinancesSlice = createSlice({
|
export const projectFinancesSlice = createSlice({
|
||||||
name: 'projectFinances',
|
name: 'projectFinances',
|
||||||
initialState,
|
initialState,
|
||||||
@@ -231,6 +251,78 @@ export const projectFinancesSlice = createSlice({
|
|||||||
if (state.project) {
|
if (state.project) {
|
||||||
state.project.currency = action.payload;
|
state.project.currency = action.payload;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
updateParentTaskCalculations: (state, action: PayloadAction<{ taskId: string; groupId: string }>) => {
|
||||||
|
const { taskId, groupId } = action.payload;
|
||||||
|
const group = state.taskGroups.find(g => g.group_id === groupId);
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
// Recursive function to recalculate parent task totals
|
||||||
|
const recalculateParentTotals = (tasks: IProjectFinanceTask[], targetId: string): boolean => {
|
||||||
|
for (const task of tasks) {
|
||||||
|
if (task.id === targetId) {
|
||||||
|
// If this task has subtasks, recalculate its totals from subtasks
|
||||||
|
if (task.sub_tasks && task.sub_tasks.length > 0) {
|
||||||
|
const subtaskTotals = task.sub_tasks.reduce((acc, subtask) => ({
|
||||||
|
estimated_cost: acc.estimated_cost + (subtask.estimated_cost || 0),
|
||||||
|
fixed_cost: acc.fixed_cost + (subtask.fixed_cost || 0),
|
||||||
|
total_actual: acc.total_actual + (subtask.total_actual || 0),
|
||||||
|
estimated_seconds: acc.estimated_seconds + (subtask.estimated_seconds || 0),
|
||||||
|
total_time_logged_seconds: acc.total_time_logged_seconds + (subtask.total_time_logged_seconds || 0)
|
||||||
|
}), {
|
||||||
|
estimated_cost: 0,
|
||||||
|
fixed_cost: 0,
|
||||||
|
total_actual: 0,
|
||||||
|
estimated_seconds: 0,
|
||||||
|
total_time_logged_seconds: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update parent task with aggregated values
|
||||||
|
task.estimated_cost = subtaskTotals.estimated_cost;
|
||||||
|
task.fixed_cost = subtaskTotals.fixed_cost;
|
||||||
|
task.total_actual = subtaskTotals.total_actual;
|
||||||
|
task.estimated_seconds = subtaskTotals.estimated_seconds;
|
||||||
|
task.total_time_logged_seconds = subtaskTotals.total_time_logged_seconds;
|
||||||
|
task.total_budget = task.estimated_cost + task.fixed_cost;
|
||||||
|
task.variance = task.total_actual - task.total_budget;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search in subtasks recursively and recalculate if found
|
||||||
|
if (task.sub_tasks && recalculateParentTotals(task.sub_tasks, targetId)) {
|
||||||
|
// After updating subtask, recalculate this parent's totals
|
||||||
|
if (task.sub_tasks && task.sub_tasks.length > 0) {
|
||||||
|
const subtaskTotals = task.sub_tasks.reduce((acc, subtask) => ({
|
||||||
|
estimated_cost: acc.estimated_cost + (subtask.estimated_cost || 0),
|
||||||
|
fixed_cost: acc.fixed_cost + (subtask.fixed_cost || 0),
|
||||||
|
total_actual: acc.total_actual + (subtask.total_actual || 0),
|
||||||
|
estimated_seconds: acc.estimated_seconds + (subtask.estimated_seconds || 0),
|
||||||
|
total_time_logged_seconds: acc.total_time_logged_seconds + (subtask.total_time_logged_seconds || 0)
|
||||||
|
}), {
|
||||||
|
estimated_cost: 0,
|
||||||
|
fixed_cost: 0,
|
||||||
|
total_actual: 0,
|
||||||
|
estimated_seconds: 0,
|
||||||
|
total_time_logged_seconds: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
task.estimated_cost = subtaskTotals.estimated_cost;
|
||||||
|
task.fixed_cost = subtaskTotals.fixed_cost;
|
||||||
|
task.total_actual = subtaskTotals.total_actual;
|
||||||
|
task.estimated_seconds = subtaskTotals.estimated_seconds;
|
||||||
|
task.total_time_logged_seconds = subtaskTotals.total_time_logged_seconds;
|
||||||
|
task.total_budget = task.estimated_cost + task.fixed_cost;
|
||||||
|
task.variance = task.total_actual - task.total_budget;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
recalculateParentTotals(group.tasks, taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
@@ -248,8 +340,39 @@ export const projectFinancesSlice = createSlice({
|
|||||||
state.loading = false;
|
state.loading = false;
|
||||||
})
|
})
|
||||||
.addCase(fetchProjectFinancesSilent.fulfilled, (state, action) => {
|
.addCase(fetchProjectFinancesSilent.fulfilled, (state, action) => {
|
||||||
|
// Helper function to preserve expansion state and sub_tasks during updates
|
||||||
|
const preserveExpansionState = (existingTasks: IProjectFinanceTask[], newTasks: IProjectFinanceTask[]): IProjectFinanceTask[] => {
|
||||||
|
return newTasks.map(newTask => {
|
||||||
|
const existingTask = existingTasks.find(t => t.id === newTask.id);
|
||||||
|
if (existingTask) {
|
||||||
|
// Preserve expansion state and subtasks
|
||||||
|
const updatedTask = {
|
||||||
|
...newTask,
|
||||||
|
show_sub_tasks: existingTask.show_sub_tasks,
|
||||||
|
sub_tasks: existingTask.sub_tasks ?
|
||||||
|
preserveExpansionState(existingTask.sub_tasks, newTask.sub_tasks || []) :
|
||||||
|
newTask.sub_tasks
|
||||||
|
};
|
||||||
|
return updatedTask;
|
||||||
|
}
|
||||||
|
return newTask;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update groups while preserving expansion state
|
||||||
|
const updatedTaskGroups = action.payload.groups.map(newGroup => {
|
||||||
|
const existingGroup = state.taskGroups.find(g => g.group_id === newGroup.group_id);
|
||||||
|
if (existingGroup) {
|
||||||
|
return {
|
||||||
|
...newGroup,
|
||||||
|
tasks: preserveExpansionState(existingGroup.tasks, newGroup.tasks)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return newGroup;
|
||||||
|
});
|
||||||
|
|
||||||
// Update data without changing loading state for silent refresh
|
// Update data without changing loading state for silent refresh
|
||||||
state.taskGroups = action.payload.groups;
|
state.taskGroups = updatedTaskGroups;
|
||||||
state.projectRateCards = action.payload.project_rate_cards;
|
state.projectRateCards = action.payload.project_rate_cards;
|
||||||
state.project = action.payload.project;
|
state.project = action.payload.project;
|
||||||
})
|
})
|
||||||
@@ -263,7 +386,44 @@ export const projectFinancesSlice = createSlice({
|
|||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
if (task.id === targetId) {
|
if (task.id === targetId) {
|
||||||
task.fixed_cost = fixedCost;
|
task.fixed_cost = fixedCost;
|
||||||
// Don't recalculate here - trigger a refresh instead for accuracy
|
// Recalculate financial values immediately for UI responsiveness
|
||||||
|
const totalBudget = (task.estimated_cost || 0) + fixedCost;
|
||||||
|
const totalActual = task.total_actual || 0;
|
||||||
|
const variance = totalActual - totalBudget;
|
||||||
|
|
||||||
|
task.total_budget = totalBudget;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(updateTaskFixedCostWithRecalculation.fulfilled, (state, action) => {
|
||||||
|
const { taskId, groupId, fixedCost } = action.payload;
|
||||||
|
const group = state.taskGroups.find(g => g.group_id === groupId);
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
// 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;
|
||||||
|
// Immediate calculation for UI responsiveness
|
||||||
|
const totalBudget = (task.estimated_cost || 0) + fixedCost;
|
||||||
|
const totalActual = task.total_actual || 0;
|
||||||
|
const variance = totalActual - totalBudget;
|
||||||
|
|
||||||
|
task.total_budget = totalBudget;
|
||||||
|
task.variance = variance;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +481,8 @@ export const {
|
|||||||
updateTaskEstimatedCost,
|
updateTaskEstimatedCost,
|
||||||
updateTaskTimeLogged,
|
updateTaskTimeLogged,
|
||||||
toggleTaskExpansion,
|
toggleTaskExpansion,
|
||||||
updateProjectFinanceCurrency
|
updateProjectFinanceCurrency,
|
||||||
|
updateParentTaskCalculations
|
||||||
} = projectFinancesSlice.actions;
|
} = projectFinancesSlice.actions;
|
||||||
|
|
||||||
export default projectFinancesSlice.reducer;
|
export default projectFinancesSlice.reducer;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import Avatars from '@/components/avatars/avatars';
|
|||||||
import { IProjectFinanceGroup, IProjectFinanceTask } from '@/types/project/project-finance.types';
|
import { IProjectFinanceGroup, IProjectFinanceTask } from '@/types/project/project-finance.types';
|
||||||
import {
|
import {
|
||||||
updateTaskFixedCostAsync,
|
updateTaskFixedCostAsync,
|
||||||
|
updateTaskFixedCostWithRecalculation,
|
||||||
|
updateParentTaskCalculations,
|
||||||
fetchProjectFinancesSilent,
|
fetchProjectFinancesSilent,
|
||||||
toggleTaskExpansion,
|
toggleTaskExpansion,
|
||||||
fetchSubTasks
|
fetchSubTasks
|
||||||
@@ -50,6 +52,7 @@ const FinanceTable = ({
|
|||||||
// Get the latest task groups from Redux store
|
// Get the latest task groups from Redux store
|
||||||
const taskGroups = useAppSelector((state) => state.projectFinances.taskGroups);
|
const taskGroups = useAppSelector((state) => state.projectFinances.taskGroups);
|
||||||
const activeGroup = useAppSelector((state) => state.projectFinances.activeGroup);
|
const activeGroup = useAppSelector((state) => state.projectFinances.activeGroup);
|
||||||
|
const billableFilter = useAppSelector((state) => state.projectFinances.billableFilter);
|
||||||
|
|
||||||
// Auth and permissions
|
// Auth and permissions
|
||||||
const auth = useAuthService();
|
const auth = useAuthService();
|
||||||
@@ -144,12 +147,21 @@ const FinanceTable = ({
|
|||||||
const fixedCost = value || 0;
|
const fixedCost = value || 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Make the API call to persist the change
|
// First update the task fixed cost
|
||||||
await dispatch(updateTaskFixedCostAsync({ taskId, groupId: table.group_id, fixedCost })).unwrap();
|
await dispatch(updateTaskFixedCostAsync({ taskId, groupId: table.group_id, fixedCost })).unwrap();
|
||||||
|
|
||||||
// Silent refresh the data to get accurate calculations from backend without loading animation
|
// Then update parent task calculations to reflect the change
|
||||||
|
dispatch(updateParentTaskCalculations({ taskId, groupId: table.group_id }));
|
||||||
|
|
||||||
|
// Finally, trigger a silent refresh to ensure backend consistency
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
dispatch(fetchProjectFinancesSilent({ projectId, groupBy: activeGroup }));
|
setTimeout(() => {
|
||||||
|
dispatch(fetchProjectFinancesSilent({
|
||||||
|
projectId,
|
||||||
|
groupBy: activeGroup,
|
||||||
|
billableFilter
|
||||||
|
}));
|
||||||
|
}, 100); // Small delay to allow UI update to complete first
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update fixed cost:', error);
|
console.error('Failed to update fixed cost:', error);
|
||||||
@@ -459,39 +471,57 @@ const FinanceTable = ({
|
|||||||
}, [tasks, selectedTask, editingFixedCostValue, hasEditPermission, themeMode, hoveredTaskId]);
|
}, [tasks, selectedTask, editingFixedCostValue, hasEditPermission, themeMode, hoveredTaskId]);
|
||||||
|
|
||||||
// Calculate totals for the current table
|
// Calculate totals for the current table
|
||||||
// Since the backend already aggregates subtask values into parent tasks,
|
// Recursively calculate totals including all subtasks
|
||||||
// we only need to sum the parent tasks (tasks without is_sub_task flag)
|
|
||||||
const totals = useMemo(() => {
|
const totals = useMemo(() => {
|
||||||
return tasks.reduce(
|
const calculateTaskTotalsRecursively = (taskList: IProjectFinanceTask[]): any => {
|
||||||
(acc, task) => {
|
return taskList.reduce(
|
||||||
// Calculate actual cost from logs (total_actual - fixed_cost)
|
(acc, task) => {
|
||||||
const actualCostFromLogs = (task.total_actual || 0) - (task.fixed_cost || 0);
|
// 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
|
// Add current task values
|
||||||
// Parent tasks contain the sum of their subtasks' values
|
const taskTotals = {
|
||||||
// So we can safely sum all parent tasks (which are the tasks in this array)
|
hours: acc.hours + (task.estimated_seconds || 0),
|
||||||
return {
|
total_time_logged: acc.total_time_logged + (task.total_time_logged_seconds || 0),
|
||||||
hours: acc.hours + (task.estimated_seconds || 0),
|
estimated_cost: acc.estimated_cost + (task.estimated_cost || 0),
|
||||||
total_time_logged: acc.total_time_logged + (task.total_time_logged_seconds || 0),
|
actual_cost_from_logs: acc.actual_cost_from_logs + actualCostFromLogs,
|
||||||
estimated_cost: acc.estimated_cost + (task.estimated_cost || 0),
|
fixed_cost: acc.fixed_cost + (task.fixed_cost || 0),
|
||||||
actual_cost_from_logs: acc.actual_cost_from_logs + actualCostFromLogs,
|
total_budget: acc.total_budget + (task.total_budget || 0),
|
||||||
fixed_cost: acc.fixed_cost + (task.fixed_cost || 0),
|
total_actual: acc.total_actual + (task.total_actual || 0),
|
||||||
total_budget: acc.total_budget + (task.total_budget || 0),
|
variance: acc.variance + (task.variance || 0)
|
||||||
total_actual: acc.total_actual + (task.total_actual || 0),
|
};
|
||||||
variance: acc.variance + (task.variance || 0)
|
|
||||||
};
|
// If task has subtasks, recursively add their totals
|
||||||
},
|
if (task.sub_tasks && task.sub_tasks.length > 0) {
|
||||||
{
|
const subTaskTotals = calculateTaskTotalsRecursively(task.sub_tasks);
|
||||||
hours: 0,
|
return {
|
||||||
total_time_logged: 0,
|
hours: taskTotals.hours + subTaskTotals.hours,
|
||||||
estimated_cost: 0,
|
total_time_logged: taskTotals.total_time_logged + subTaskTotals.total_time_logged,
|
||||||
actual_cost_from_logs: 0,
|
estimated_cost: taskTotals.estimated_cost + subTaskTotals.estimated_cost,
|
||||||
fixed_cost: 0,
|
actual_cost_from_logs: taskTotals.actual_cost_from_logs + subTaskTotals.actual_cost_from_logs,
|
||||||
total_budget: 0,
|
fixed_cost: taskTotals.fixed_cost + subTaskTotals.fixed_cost,
|
||||||
total_actual: 0,
|
total_budget: taskTotals.total_budget + subTaskTotals.total_budget,
|
||||||
variance: 0
|
total_actual: taskTotals.total_actual + subTaskTotals.total_actual,
|
||||||
}
|
variance: taskTotals.variance + subTaskTotals.variance
|
||||||
);
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskTotals;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hours: 0,
|
||||||
|
total_time_logged: 0,
|
||||||
|
estimated_cost: 0,
|
||||||
|
actual_cost_from_logs: 0,
|
||||||
|
fixed_cost: 0,
|
||||||
|
total_budget: 0,
|
||||||
|
total_actual: 0,
|
||||||
|
variance: 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return calculateTaskTotalsRecursively(tasks);
|
||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
|
|
||||||
// Format the totals for display
|
// Format the totals for display
|
||||||
|
|||||||
@@ -55,15 +55,48 @@ const ProjectViewFinance = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const totals = taskGroups.reduce((acc, group) => {
|
const calculateTaskTotalsRecursively = (tasks: any[]): any => {
|
||||||
group.tasks.forEach(task => {
|
return tasks.reduce((acc, task) => {
|
||||||
acc.totalEstimatedCost += task.estimated_cost || 0;
|
// Add current task values
|
||||||
acc.totalFixedCost += task.fixed_cost || 0;
|
const taskTotals = {
|
||||||
acc.totalBudget += task.total_budget || 0;
|
totalEstimatedCost: acc.totalEstimatedCost + (task.estimated_cost || 0),
|
||||||
acc.totalActualCost += task.total_actual || 0;
|
totalFixedCost: acc.totalFixedCost + (task.fixed_cost || 0),
|
||||||
acc.totalVariance += task.variance || 0;
|
totalBudget: acc.totalBudget + (task.total_budget || 0),
|
||||||
|
totalActualCost: acc.totalActualCost + (task.total_actual || 0),
|
||||||
|
totalVariance: acc.totalVariance + (task.variance || 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// If task has subtasks, recursively add their totals
|
||||||
|
if (task.sub_tasks && task.sub_tasks.length > 0) {
|
||||||
|
const subTaskTotals = calculateTaskTotalsRecursively(task.sub_tasks);
|
||||||
|
return {
|
||||||
|
totalEstimatedCost: taskTotals.totalEstimatedCost + subTaskTotals.totalEstimatedCost,
|
||||||
|
totalFixedCost: taskTotals.totalFixedCost + subTaskTotals.totalFixedCost,
|
||||||
|
totalBudget: taskTotals.totalBudget + subTaskTotals.totalBudget,
|
||||||
|
totalActualCost: taskTotals.totalActualCost + subTaskTotals.totalActualCost,
|
||||||
|
totalVariance: taskTotals.totalVariance + subTaskTotals.totalVariance
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskTotals;
|
||||||
|
}, {
|
||||||
|
totalEstimatedCost: 0,
|
||||||
|
totalFixedCost: 0,
|
||||||
|
totalBudget: 0,
|
||||||
|
totalActualCost: 0,
|
||||||
|
totalVariance: 0
|
||||||
});
|
});
|
||||||
return acc;
|
};
|
||||||
|
|
||||||
|
const totals = taskGroups.reduce((acc, group) => {
|
||||||
|
const groupTotals = calculateTaskTotalsRecursively(group.tasks);
|
||||||
|
return {
|
||||||
|
totalEstimatedCost: acc.totalEstimatedCost + groupTotals.totalEstimatedCost,
|
||||||
|
totalFixedCost: acc.totalFixedCost + groupTotals.totalFixedCost,
|
||||||
|
totalBudget: acc.totalBudget + groupTotals.totalBudget,
|
||||||
|
totalActualCost: acc.totalActualCost + groupTotals.totalActualCost,
|
||||||
|
totalVariance: acc.totalVariance + groupTotals.totalVariance
|
||||||
|
};
|
||||||
}, {
|
}, {
|
||||||
totalEstimatedCost: 0,
|
totalEstimatedCost: 0,
|
||||||
totalFixedCost: 0,
|
totalFixedCost: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user