feat(task-management): enhance task timer synchronization and color handling

- Updated `CustomColordLabel` and `CustomNumberLabel` components to improve color handling by removing the alpha channel logic and implementing a dynamic text color based on background luminance.
- Enhanced task management slice to preserve timer state when fetching tasks, ensuring active timers are maintained across updates.
- Modified socket handlers to synchronize timer state between task slices, improving consistency in task time tracking.
- Refactored `useTaskTimer` hook to streamline local and Redux state synchronization for timer management.
This commit is contained in:
chamikaJ
2025-07-15 13:30:59 +05:30
parent 6d8c475e67
commit d970cbb626
5 changed files with 58 additions and 20 deletions

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { Label } from '@/types/task-management.types'; import { Label } from '@/types/task-management.types';
import { ITaskLabel } from '@/types/tasks/taskLabel.types'; import { ITaskLabel } from '@/types/tasks/taskLabel.types';
import { ALPHA_CHANNEL } from '@/shared/constants';
interface CustomColordLabelProps { interface CustomColordLabelProps {
label: Label | ITaskLabel; label: Label | ITaskLabel;
@@ -15,11 +14,26 @@ const CustomColordLabel = React.forwardRef<HTMLSpanElement, CustomColordLabelPro
label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name; label.name && label.name.length > 10 ? `${label.name.substring(0, 10)}...` : label.name;
// Handle different color property names for different types // Handle different color property names for different types
const baseColor = (label as Label).color || (label as ITaskLabel).color_code || '#6b7280'; // Default to gray-500 if no color const backgroundColor = (label as Label).color || (label as ITaskLabel).color_code || '#6b7280'; // Default to gray-500 if no color
// Add alpha channel to the base color // Function to determine if we should use white or black text based on background color
const backgroundColor = baseColor + ALPHA_CHANNEL; const getTextColor = (bgColor: string): string => {
const textColor = baseColor; // Remove # if present
const color = bgColor.replace('#', '');
// Convert to RGB
const r = parseInt(color.substr(0, 2), 16);
const g = parseInt(color.substr(2, 2), 16);
const b = parseInt(color.substr(4, 2), 16);
// Calculate luminance
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
// Return white for dark backgrounds, black for light backgrounds
return luminance > 0.5 ? '#000000' : '#ffffff';
};
const textColor = getTextColor(backgroundColor);
return ( return (
<Tooltip title={label.name}> <Tooltip title={label.name}>
@@ -29,7 +43,7 @@ const CustomColordLabel = React.forwardRef<HTMLSpanElement, CustomColordLabelPro
style={{ style={{
backgroundColor, backgroundColor,
color: textColor, color: textColor,
border: `1px solid ${baseColor}`, border: `1px solid ${backgroundColor}`,
}} }}
> >
<span className="truncate">{truncatedName}</span> <span className="truncate">{truncatedName}</span>

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { NumbersColorMap, ALPHA_CHANNEL } from '@/shared/constants'; import { NumbersColorMap } from '@/shared/constants';
interface CustomNumberLabelProps { interface CustomNumberLabelProps {
labelList: string[]; labelList: string[];
@@ -12,24 +12,17 @@ interface CustomNumberLabelProps {
const CustomNumberLabel = React.forwardRef<HTMLSpanElement, CustomNumberLabelProps>( const CustomNumberLabel = React.forwardRef<HTMLSpanElement, CustomNumberLabelProps>(
({ labelList, namesString, isDarkMode = false, color }, ref) => { ({ labelList, namesString, isDarkMode = false, color }, ref) => {
// Use provided color, or fall back to NumbersColorMap based on first digit // Use provided color, or fall back to NumbersColorMap based on first digit
const baseColor = color || (() => { const backgroundColor = color || (() => {
const firstDigit = namesString.match(/\d/)?.[0] || '0'; const firstDigit = namesString.match(/\d/)?.[0] || '0';
return NumbersColorMap[firstDigit] || NumbersColorMap['0']; return NumbersColorMap[firstDigit] || NumbersColorMap['0'];
})(); })();
// Add alpha channel to the base color
const backgroundColor = baseColor + ALPHA_CHANNEL;
return ( return (
<Tooltip title={labelList.join(', ')}> <Tooltip title={labelList.join(', ')}>
<span <span
ref={ref} ref={ref}
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium cursor-help" className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium cursor-help"
style={{ style={{ backgroundColor, color: 'white' }}
backgroundColor,
color: baseColor,
border: `1px solid ${baseColor}`,
}}
> >
{namesString} {namesString}
</span> </span>

View File

@@ -959,8 +959,26 @@ const taskManagementSlice = createSlice({
.addCase(fetchTasksV3.fulfilled, (state, action) => { .addCase(fetchTasksV3.fulfilled, (state, action) => {
state.loading = false; state.loading = false;
const { allTasks, groups, grouping } = action.payload; const { allTasks, groups, grouping } = action.payload;
tasksAdapter.setAll(state as EntityState<Task, string>, allTasks || []); // Ensure allTasks is an array
state.ids = (allTasks || []).map(task => task.id); // Also update ids // Preserve existing timer state from old tasks before replacing
const oldTasks = state.entities;
const tasksWithTimers = (allTasks || []).map(task => {
const oldTask = oldTasks[task.id];
if (oldTask?.timeTracking?.activeTimer) {
// Preserve the timer state from the old task
return {
...task,
timeTracking: {
...task.timeTracking,
activeTimer: oldTask.timeTracking.activeTimer
}
};
}
return task;
});
tasksAdapter.setAll(state as EntityState<Task, string>, tasksWithTimers); // Ensure allTasks is an array
state.ids = tasksWithTimers.map(task => task.id); // Also update ids
state.groups = groups; state.groups = groups;
state.grouping = grouping; state.grouping = grouping;
}) })

View File

@@ -33,6 +33,7 @@ import {
updateTaskDescription, updateTaskDescription,
updateSubTasks, updateSubTasks,
updateTaskProgress, updateTaskProgress,
updateTaskTimeTracking,
} from '@/features/tasks/tasks.slice'; } from '@/features/tasks/tasks.slice';
import { import {
addTask, addTask,
@@ -936,6 +937,8 @@ export const useTaskSocketHandlers = () => {
const { task_id, start_time } = typeof data === 'string' ? JSON.parse(data) : data; const { task_id, start_time } = typeof data === 'string' ? JSON.parse(data) : data;
if (!task_id) return; if (!task_id) return;
const timerTimestamp = start_time ? (typeof start_time === 'number' ? start_time : parseInt(start_time)) : Date.now();
// Update the task-management slice to include timer state // Update the task-management slice to include timer state
const currentTask = store.getState().taskManagement.entities[task_id]; const currentTask = store.getState().taskManagement.entities[task_id];
if (currentTask) { if (currentTask) {
@@ -943,13 +946,16 @@ export const useTaskSocketHandlers = () => {
...currentTask, ...currentTask,
timeTracking: { timeTracking: {
...currentTask.timeTracking, ...currentTask.timeTracking,
activeTimer: start_time ? (typeof start_time === 'number' ? start_time : parseInt(start_time)) : Date.now(), activeTimer: timerTimestamp,
}, },
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
}; };
dispatch(updateTask(updatedTask)); dispatch(updateTask(updatedTask));
} }
// Also update the tasks slice activeTimers to keep both slices in sync
dispatch(updateTaskTimeTracking({ taskId: task_id, timeTracking: timerTimestamp }));
} catch (error) { } catch (error) {
logger.error('Error handling timer start event:', error); logger.error('Error handling timer start event:', error);
} }
@@ -975,6 +981,9 @@ export const useTaskSocketHandlers = () => {
}; };
dispatch(updateTask(updatedTask)); dispatch(updateTask(updatedTask));
} }
// Also update the tasks slice activeTimers to keep both slices in sync
dispatch(updateTaskTimeTracking({ taskId: task_id, timeTracking: null }));
} catch (error) { } catch (error) {
logger.error('Error handling timer stop event:', error); logger.error('Error handling timer stop event:', error);
} }

View File

@@ -50,7 +50,11 @@ export const useTaskTimer = (taskId: string, initialStartTime: number | null) =>
// Timer management effect // Timer management effect
useEffect(() => { useEffect(() => {
if (started && localStarted && reduxStartTime) { if (started && reduxStartTime) {
// Sync local state with Redux state
if (!localStarted) {
setLocalStarted(true);
}
clearTimerInterval(); clearTimerInterval();
timerTick(); timerTick();
intervalRef.current = setInterval(timerTick, 1000); intervalRef.current = setInterval(timerTick, 1000);