feat(project-finance): enhance task cost tracking and UI updates

- Added `actual_cost_from_logs` to task data structure for improved cost tracking.
- Updated SQL queries in ProjectFinanceController to ensure accurate fixed cost updates and task hierarchy recalculations.
- Enhanced the project finance slice to optimize task hierarchy recalculations, ensuring accurate financial data representation.
- Modified FinanceTable component to reflect changes in cost calculations, preventing double counting and improving UI responsiveness.
This commit is contained in:
chamikaJ
2025-06-11 12:28:25 +05:30
parent 06488d80ff
commit c5bac36c53
5 changed files with 135 additions and 113 deletions

View File

@@ -360,6 +360,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
Number(task.total_time_logged_seconds) || 0 Number(task.total_time_logged_seconds) || 0
), ),
estimated_cost: Number(task.estimated_cost) || 0, estimated_cost: Number(task.estimated_cost) || 0,
actual_cost_from_logs: Number(task.actual_cost_from_logs) || 0,
fixed_cost: Number(task.fixed_cost) || 0, fixed_cost: Number(task.fixed_cost) || 0,
total_budget: Number(task.total_budget) || 0, total_budget: Number(task.total_budget) || 0,
total_actual: Number(task.total_actual) || 0, total_actual: Number(task.total_actual) || 0,
@@ -426,14 +427,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
.send(new ServerResponse(false, null, "Cannot update fixed cost for parent tasks. Fixed cost is calculated from subtasks.")); .send(new ServerResponse(false, null, "Cannot update fixed cost for parent tasks. Fixed cost is calculated from subtasks."));
} }
const q = ` // Update only the specific subtask's fixed cost
const updateQuery = `
UPDATE tasks UPDATE tasks
SET fixed_cost = $1, updated_at = NOW() SET fixed_cost = $1, updated_at = NOW()
WHERE id = $2 WHERE id = $2
RETURNING id, name, fixed_cost; RETURNING id, name, fixed_cost;
`; `;
const result = await db.query(q, [fixed_cost, taskId]); const result = await db.query(updateQuery, [fixed_cost, taskId]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
return res return res
@@ -441,7 +443,10 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
.send(new ServerResponse(false, null, "Task not found")); .send(new ServerResponse(false, null, "Task not found"));
} }
return res.status(200).send(new ServerResponse(true, result.rows[0])); return res.status(200).send(new ServerResponse(true, {
updated_task: result.rows[0],
message: "Fixed cost updated successfully."
}));
} }
@HandleExceptions() @HandleExceptions()
@@ -839,6 +844,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
Number(task.total_time_logged_seconds) || 0 Number(task.total_time_logged_seconds) || 0
), ),
estimated_cost: Number(task.estimated_cost) || 0, estimated_cost: Number(task.estimated_cost) || 0,
actual_cost_from_logs: Number(task.actual_cost_from_logs) || 0,
fixed_cost: Number(task.fixed_cost) || 0, fixed_cost: Number(task.fixed_cost) || 0,
total_budget: Number(task.total_budget) || 0, total_budget: Number(task.total_budget) || 0,
total_actual: Number(task.total_actual) || 0, total_actual: Number(task.total_actual) || 0,
@@ -1161,6 +1167,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
Number(task.total_time_logged_seconds) || 0 Number(task.total_time_logged_seconds) || 0
), ),
estimated_cost: Number(task.estimated_cost) || 0, estimated_cost: Number(task.estimated_cost) || 0,
actual_cost_from_logs: Number(task.actual_cost_from_logs) || 0,
fixed_cost: Number(task.fixed_cost) || 0, fixed_cost: Number(task.fixed_cost) || 0,
total_budget: Number(task.total_budget) || 0, total_budget: Number(task.total_budget) || 0,
total_actual: Number(task.total_actual) || 0, total_actual: Number(task.total_actual) || 0,

View File

@@ -78,52 +78,65 @@ const hasTaskChanged = (oldTask: IProjectFinanceTask, newTask: IProjectFinanceTa
// Optimized recursive calculation for task hierarchy with memoization // Optimized recursive calculation for task hierarchy with memoization
const recalculateTaskHierarchy = (tasks: IProjectFinanceTask[]): IProjectFinanceTask[] => { const recalculateTaskHierarchy = (tasks: IProjectFinanceTask[]): IProjectFinanceTask[] => {
return tasks.map(task => { return tasks.map(task => {
const cacheKey = generateTaskCacheKey(task); // If task has loaded subtasks, recalculate from subtasks
const cached = taskCalculationCache.get(cacheKey);
// If task has subtasks, first recalculate all subtasks recursively
if (task.sub_tasks && task.sub_tasks.length > 0) { if (task.sub_tasks && task.sub_tasks.length > 0) {
const updatedSubTasks = recalculateTaskHierarchy(task.sub_tasks); const updatedSubTasks = recalculateTaskHierarchy(task.sub_tasks);
// Calculate parent task totals from subtasks // Calculate totals from subtasks only (for time and costs from logs)
const subtaskTotals = updatedSubTasks.reduce((acc, subtask) => ({ const subtaskTotals = updatedSubTasks.reduce((acc, subtask) => ({
estimated_cost: acc.estimated_cost + (subtask.estimated_cost || 0), estimated_cost: acc.estimated_cost + (subtask.estimated_cost || 0),
fixed_cost: acc.fixed_cost + (subtask.fixed_cost || 0), fixed_cost: acc.fixed_cost + (subtask.fixed_cost || 0),
total_actual: acc.total_actual + (subtask.total_actual || 0), actual_cost_from_logs: acc.actual_cost_from_logs + (subtask.actual_cost_from_logs || 0),
estimated_seconds: acc.estimated_seconds + (subtask.estimated_seconds || 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) total_time_logged_seconds: acc.total_time_logged_seconds + (subtask.total_time_logged_seconds || 0)
}), { }), {
estimated_cost: 0, estimated_cost: 0,
fixed_cost: 0, fixed_cost: 0,
total_actual: 0, actual_cost_from_logs: 0,
estimated_seconds: 0, estimated_seconds: 0,
total_time_logged_seconds: 0 total_time_logged_seconds: 0
}); });
// For parent tasks with loaded subtasks: use ONLY the subtask totals
// The parent's original values were backend-aggregated, now we use frontend subtask aggregation
const totalFixedCost = subtaskTotals.fixed_cost; // Only subtask fixed costs
const totalEstimatedCost = subtaskTotals.estimated_cost; // Only subtask estimated costs
const totalActualCostFromLogs = subtaskTotals.actual_cost_from_logs; // Only subtask logged costs
const totalActual = totalActualCostFromLogs + totalFixedCost;
// Update parent task with aggregated values // Update parent task with aggregated values
const updatedTask = { const updatedTask = {
...task, ...task,
sub_tasks: updatedSubTasks, sub_tasks: updatedSubTasks,
estimated_cost: subtaskTotals.estimated_cost, estimated_cost: totalEstimatedCost,
fixed_cost: subtaskTotals.fixed_cost, fixed_cost: totalFixedCost,
total_actual: subtaskTotals.total_actual, actual_cost_from_logs: totalActualCostFromLogs,
total_actual: totalActual,
estimated_seconds: subtaskTotals.estimated_seconds, estimated_seconds: subtaskTotals.estimated_seconds,
total_time_logged_seconds: subtaskTotals.total_time_logged_seconds, total_time_logged_seconds: subtaskTotals.total_time_logged_seconds,
total_budget: subtaskTotals.estimated_cost + subtaskTotals.fixed_cost, total_budget: totalEstimatedCost + totalFixedCost,
variance: subtaskTotals.total_actual - (subtaskTotals.estimated_cost + subtaskTotals.fixed_cost) variance: totalActual - (totalEstimatedCost + totalFixedCost)
}; };
// Cache the result
taskCalculationCache.set(cacheKey, {
task: { ...task },
result: updatedTask,
timestamp: Date.now()
});
return updatedTask; return updatedTask;
} }
// For parent tasks without loaded subtasks, trust backend-calculated values
if (task.sub_tasks_count > 0 && (!task.sub_tasks || task.sub_tasks.length === 0)) {
// Parent task with unloaded subtasks - backend has already calculated aggregated values
const { totalBudget, totalActual, variance } = calculateTaskCosts(task);
return {
...task,
total_budget: totalBudget,
total_actual: totalActual,
variance: variance
};
}
// For leaf tasks, check cache first // For leaf tasks, check cache first
const cacheKey = generateTaskCacheKey(task);
const cached = taskCalculationCache.get(cacheKey);
if (cached && !hasTaskChanged(cached.task, task)) { if (cached && !hasTaskChanged(cached.task, task)) {
return { ...cached.result, ...task }; // Merge with current task to preserve other properties return { ...cached.result, ...task }; // Merge with current task to preserve other properties
} }
@@ -137,7 +150,7 @@ const recalculateTaskHierarchy = (tasks: IProjectFinanceTask[]): IProjectFinance
variance: variance variance: variance
}; };
// Cache the result // Cache the result only for leaf tasks
taskCalculationCache.set(cacheKey, { taskCalculationCache.set(cacheKey, {
task: { ...task }, task: { ...task },
result: updatedTask, result: updatedTask,
@@ -340,7 +353,12 @@ export const projectFinancesSlice = createSlice({
}) })
.addCase(fetchProjectFinances.fulfilled, (state, action) => { .addCase(fetchProjectFinances.fulfilled, (state, action) => {
state.loading = false; state.loading = false;
state.taskGroups = action.payload.groups; // Apply hierarchy recalculation to ensure parent tasks show correct aggregated values
const recalculatedGroups = action.payload.groups.map(group => ({
...group,
tasks: recalculateTaskHierarchy(group.tasks)
}));
state.taskGroups = recalculatedGroups;
state.projectRateCards = action.payload.project_rate_cards; state.projectRateCards = action.payload.project_rate_cards;
state.project = action.payload.project; state.project = action.payload.project;
// Clear cache when fresh data is loaded // Clear cache when fresh data is loaded
@@ -369,16 +387,20 @@ export const projectFinancesSlice = createSlice({
}); });
}; };
// Update groups while preserving expansion state // Update groups while preserving expansion state and applying hierarchy recalculation
const updatedTaskGroups = action.payload.groups.map(newGroup => { const updatedTaskGroups = action.payload.groups.map(newGroup => {
const existingGroup = state.taskGroups.find(g => g.group_id === newGroup.group_id); const existingGroup = state.taskGroups.find(g => g.group_id === newGroup.group_id);
if (existingGroup) { if (existingGroup) {
const tasksWithExpansion = preserveExpansionState(existingGroup.tasks, newGroup.tasks);
return { return {
...newGroup, ...newGroup,
tasks: preserveExpansionState(existingGroup.tasks, newGroup.tasks) tasks: recalculateTaskHierarchy(tasksWithExpansion)
}; };
} }
return newGroup; return {
...newGroup,
tasks: recalculateTaskHierarchy(newGroup.tasks)
};
}); });
// Update data without changing loading state for silent refresh // Update data without changing loading state for silent refresh
@@ -393,30 +415,20 @@ export const projectFinancesSlice = createSlice({
const group = state.taskGroups.find(g => g.group_id === groupId); const group = state.taskGroups.find(g => g.group_id === groupId);
if (group) { if (group) {
// Recursive function to find and update a task in the hierarchy // Update the specific task's fixed cost and recalculate the entire hierarchy
const findAndUpdateTask = (tasks: IProjectFinanceTask[], targetId: string): boolean => { const result = updateTaskAndRecalculateHierarchy(
for (const task of tasks) { group.tasks,
if (task.id === targetId) { taskId,
task.fixed_cost = fixedCost; (task) => ({
// Recalculate financial values immediately for UI responsiveness ...task,
const totalBudget = (task.estimated_cost || 0) + fixedCost; fixed_cost: 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); if (result.updated) {
group.tasks = result.tasks;
clearCalculationCache();
}
} }
}) })
.addCase(fetchSubTasks.fulfilled, (state, action) => { .addCase(fetchSubTasks.fulfilled, (state, action) => {
@@ -447,6 +459,8 @@ export const projectFinancesSlice = createSlice({
// 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) {
if (findAndUpdateTask(group.tasks, parentTaskId)) { if (findAndUpdateTask(group.tasks, parentTaskId)) {
// Recalculate the hierarchy after adding subtasks to ensure parent values are correct
group.tasks = recalculateTaskHierarchy(group.tasks);
break; break;
} }
} }

View File

@@ -73,7 +73,7 @@ const FinanceTableWrapper: React.FC<FinanceTableWrapperProps> = ({ activeTablesL
// Parent task - use its aggregated values which already include subtask totals // Parent task - use its aggregated values which already include subtask totals
return { return {
hours: acc.hours + (task.estimated_seconds || 0), hours: acc.hours + (task.estimated_seconds || 0),
cost: acc.cost + ((task.total_actual || 0) - (task.fixed_cost || 0)), cost: acc.cost + (task.actual_cost_from_logs || 0),
fixedCost: acc.fixedCost + (task.fixed_cost || 0), fixedCost: acc.fixedCost + (task.fixed_cost || 0),
totalBudget: acc.totalBudget + (task.total_budget || 0), totalBudget: acc.totalBudget + (task.total_budget || 0),
totalActual: acc.totalActual + (task.total_actual || 0), totalActual: acc.totalActual + (task.total_actual || 0),
@@ -85,7 +85,7 @@ const FinanceTableWrapper: React.FC<FinanceTableWrapperProps> = ({ activeTablesL
// Leaf task - use its individual values // Leaf task - use its individual values
return { return {
hours: acc.hours + (task.estimated_seconds || 0), hours: acc.hours + (task.estimated_seconds || 0),
cost: acc.cost + ((task.total_actual || 0) - (task.fixed_cost || 0)), cost: acc.cost + (task.actual_cost_from_logs || 0),
fixedCost: acc.fixedCost + (task.fixed_cost || 0), fixedCost: acc.fixedCost + (task.fixed_cost || 0),
totalBudget: acc.totalBudget + (task.total_budget || 0), totalBudget: acc.totalBudget + (task.total_budget || 0),
totalActual: acc.totalActual + (task.total_actual || 0), totalActual: acc.totalActual + (task.total_actual || 0),

View File

@@ -48,8 +48,6 @@ 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 billableFilter = useAppSelector((state) => state.projectFinances.billableFilter);
// Auth and permissions // Auth and permissions
const auth = useAuthService(); const auth = useAuthService();
@@ -71,7 +69,7 @@ const FinanceTable = ({
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if (selectedTask && !(event.target as Element)?.closest('.fixed-cost-input')) { if (selectedTask && !(event.target as Element)?.closest('.fixed-cost-input')) {
// Save current value before closing // Save current value before closing if it has changed
if (editingFixedCostValue !== null) { if (editingFixedCostValue !== null) {
immediateSaveFixedCost(editingFixedCostValue, selectedTask.id); immediateSaveFixedCost(editingFixedCostValue, selectedTask.id);
} else { } else {
@@ -85,7 +83,7 @@ const FinanceTable = ({
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
}; };
}, [selectedTask, editingFixedCostValue]); }, [selectedTask, editingFixedCostValue, tasks]);
// Cleanup timeout on unmount // Cleanup timeout on unmount
useEffect(() => { useEffect(() => {
@@ -169,9 +167,11 @@ const FinanceTable = ({
try { try {
// Update the task fixed cost - this will automatically trigger hierarchical recalculation // Update the task fixed cost - this will automatically trigger hierarchical recalculation
// The Redux slice handles parent task updates through recalculateTaskHierarchy
await dispatch(updateTaskFixedCostAsync({ taskId, groupId: table.group_id, fixedCost })).unwrap(); await dispatch(updateTaskFixedCostAsync({ taskId, groupId: table.group_id, fixedCost })).unwrap();
// No need for manual parent calculations or API refetch - the Redux slice handles it efficiently setSelectedTask(null);
setEditingFixedCostValue(null);
} catch (error) { } catch (error) {
console.error('Failed to update fixed cost:', error); console.error('Failed to update fixed cost:', error);
} }
@@ -211,7 +211,24 @@ const FinanceTable = ({
// Set new timeout // Set new timeout
saveTimeoutRef.current = setTimeout(() => { saveTimeoutRef.current = setTimeout(() => {
if (value !== null) { // Find the current task to check if value actually changed
const findTask = (tasks: IProjectFinanceTask[], id: string): IProjectFinanceTask | null => {
for (const task of tasks) {
if (task.id === id) return task;
if (task.sub_tasks) {
const found = findTask(task.sub_tasks, id);
if (found) return found;
}
}
return null;
};
const currentTask = findTask(tasks, taskId);
const currentFixedCost = currentTask?.fixed_cost || 0;
const newFixedCost = value || 0;
// Only save if the value actually changed
if (newFixedCost !== currentFixedCost && value !== null) {
handleFixedCostChange(value, taskId); handleFixedCostChange(value, taskId);
setSelectedTask(null); setSelectedTask(null);
setEditingFixedCostValue(null); setEditingFixedCostValue(null);
@@ -227,11 +244,30 @@ const FinanceTable = ({
saveTimeoutRef.current = null; saveTimeoutRef.current = null;
} }
if (value !== null) { // Find the current task to check if value actually changed
const findTask = (tasks: IProjectFinanceTask[], id: string): IProjectFinanceTask | null => {
for (const task of tasks) {
if (task.id === id) return task;
if (task.sub_tasks) {
const found = findTask(task.sub_tasks, id);
if (found) return found;
}
}
return null;
};
const currentTask = findTask(tasks, taskId);
const currentFixedCost = currentTask?.fixed_cost || 0;
const newFixedCost = value || 0;
// Only save if the value actually changed
if (newFixedCost !== currentFixedCost && value !== null) {
handleFixedCostChange(value, taskId); handleFixedCostChange(value, taskId);
} else {
// Just close the editor without saving
setSelectedTask(null);
setEditingFixedCostValue(null);
} }
setSelectedTask(null);
setEditingFixedCostValue(null);
}; };
// Calculate indentation based on nesting level // Calculate indentation based on nesting level
@@ -453,7 +489,7 @@ const FinanceTable = ({
case FinanceTableColumnKeys.TOTAL_ACTUAL: case FinanceTableColumnKeys.TOTAL_ACTUAL:
return <Typography.Text style={{ fontSize: Math.max(12, 14 - level * 0.5) }}>{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 style={{ fontSize: Math.max(12, 14 - level * 0.5) }}>{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.actual_cost_from_logs || 0)}</Typography.Text>;
default: default:
return null; return null;
} }
@@ -489,7 +525,7 @@ const FinanceTable = ({
// Calculate totals for the current table // Calculate totals for the current table
// Optimized calculation that avoids double counting in nested hierarchies // Optimized calculation that avoids double counting in nested hierarchies
const totals = useMemo(() => { const totals = useMemo(() => {
const calculateTaskTotalsFlat = (taskList: IProjectFinanceTask[]): any => { const calculateTaskTotalsRecursive = (taskList: IProjectFinanceTask[]): any => {
let totals = { let totals = {
hours: 0, hours: 0,
total_time_logged: 0, total_time_logged: 0,
@@ -502,24 +538,24 @@ const FinanceTable = ({
}; };
for (const task of taskList) { for (const task of taskList) {
// For parent tasks with subtasks, only count the aggregated values (no double counting)
// For leaf tasks, count their individual values
if (task.sub_tasks && task.sub_tasks.length > 0) { if (task.sub_tasks && task.sub_tasks.length > 0) {
// Parent task - use its aggregated values which already include subtask totals // Parent task with loaded subtasks - only count the subtasks recursively
totals.hours += task.estimated_seconds || 0; // This completely avoids the parent's aggregated values to prevent double counting
totals.total_time_logged += task.total_time_logged_seconds || 0; const subtaskTotals = calculateTaskTotalsRecursive(task.sub_tasks);
totals.estimated_cost += task.estimated_cost || 0; totals.hours += subtaskTotals.hours;
totals.actual_cost_from_logs += (task.total_actual || 0) - (task.fixed_cost || 0); totals.total_time_logged += subtaskTotals.total_time_logged;
totals.fixed_cost += task.fixed_cost || 0; totals.estimated_cost += subtaskTotals.estimated_cost;
totals.total_budget += task.total_budget || 0; totals.actual_cost_from_logs += subtaskTotals.actual_cost_from_logs;
totals.total_actual += task.total_actual || 0; totals.fixed_cost += subtaskTotals.fixed_cost;
totals.variance += task.variance || 0; totals.total_budget += subtaskTotals.total_budget;
totals.total_actual += subtaskTotals.total_actual;
totals.variance += subtaskTotals.variance;
} else { } else {
// Leaf task - use its individual values // Leaf task or parent task without loaded subtasks - use its values directly
totals.hours += task.estimated_seconds || 0; totals.hours += task.estimated_seconds || 0;
totals.total_time_logged += task.total_time_logged_seconds || 0; totals.total_time_logged += task.total_time_logged_seconds || 0;
totals.estimated_cost += task.estimated_cost || 0; totals.estimated_cost += task.estimated_cost || 0;
totals.actual_cost_from_logs += (task.total_actual || 0) - (task.fixed_cost || 0); totals.actual_cost_from_logs += task.actual_cost_from_logs || 0;
totals.fixed_cost += task.fixed_cost || 0; totals.fixed_cost += task.fixed_cost || 0;
totals.total_budget += task.total_budget || 0; totals.total_budget += task.total_budget || 0;
totals.total_actual += task.total_actual || 0; totals.total_actual += task.total_actual || 0;
@@ -530,7 +566,7 @@ const FinanceTable = ({
return totals; return totals;
}; };
return calculateTaskTotalsFlat(tasks); return calculateTaskTotalsRecursive(tasks);
}, [tasks]); }, [tasks]);
// Format the totals for display // Format the totals for display
@@ -602,42 +638,6 @@ const FinanceTable = ({
{/* task rows with recursive hierarchy */} {/* task rows with recursive hierarchy */}
{!isCollapse && flattenedTasks} {!isCollapse && flattenedTasks}
{/* Group totals row */}
{!isCollapse && tasks.length > 0 && (
<tr
style={{
height: 40,
backgroundColor: themeWiseColor('#f0f0f0', '#2a2a2a', themeMode),
fontWeight: 600,
borderTop: `1px solid ${themeMode === 'dark' ? '#404040' : '#e0e0e0'}`,
}}
className={`group-totals ${themeMode === 'dark' ? 'dark' : ''}`}
>
{financeTableColumns.map((col) => (
<td
key={`total-${col.key}`}
style={{
width: col.width,
paddingInline: 16,
textAlign: col.key === FinanceTableColumnKeys.TASK || col.key === FinanceTableColumnKeys.MEMBERS ? 'left' : 'right',
backgroundColor: themeWiseColor('#f0f0f0', '#2a2a2a', themeMode),
fontSize: 14,
fontWeight: 600,
}}
className={customColumnStyles(col.key)}
>
{col.key === FinanceTableColumnKeys.TASK ? (
<Typography.Text style={{ fontSize: 14, fontWeight: 600 }}>
Group Total
</Typography.Text>
) : col.key === FinanceTableColumnKeys.MEMBERS ? null : (
renderFinancialTableHeaderContent(col.key)
)}
</td>
))}
</tr>
)}
</> </>
); );
}; };

View File

@@ -34,6 +34,7 @@ export interface IProjectFinanceTask {
total_time_logged_seconds: number; total_time_logged_seconds: number;
total_time_logged: string; // Formatted time string like "4h 30m 12s" total_time_logged: string; // Formatted time string like "4h 30m 12s"
estimated_cost: number; estimated_cost: number;
actual_cost_from_logs: number;
members: IProjectFinanceMember[]; members: IProjectFinanceMember[];
billable: boolean; billable: boolean;
fixed_cost: number; fixed_cost: number;