feat(localization): add timer conflict handling and update translations

- Introduced a new hook `useTaskTimerWithConflictCheck` to manage timer conflicts, prompting users when a timer is already running for a different task.
- Updated localization files for Albanian, German, English, Spanish, Portuguese, and Chinese to include new translation keys related to timer conflict handling and cancellation.
- Refactored components to utilize the new timer hook, enhancing user experience by preventing overlapping timers.
This commit is contained in:
chamikaJ
2025-07-30 10:13:08 +05:30
parent c53ab511bf
commit 5cce3bc613
17 changed files with 158 additions and 27 deletions

View File

@@ -12,7 +12,7 @@ import TimeLogList from './time-log-list';
import { taskTimeLogsApiService } from '@/api/tasks/task-time-logs.api.service';
import { ITaskLogViewModel } from '@/types/tasks/task-log-view.types';
import TaskTimer from '@/components/taskListCommon/task-timer/task-timer';
import { useTaskTimer } from '@/hooks/useTaskTimer';
import { useTaskTimerWithConflictCheck } from '@/hooks/useTaskTimerWithConflictCheck';
import logger from '@/utils/errorLogger';
interface TaskDrawerTimeLogProps {
@@ -31,7 +31,7 @@ const TaskDrawerTimeLog = ({ t, refreshTrigger = 0 }: TaskDrawerTimeLogProps) =>
state => state.taskDrawerReducer
);
const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimer(
const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimerWithConflictCheck(
selectedTaskId || '',
taskFormViewModel?.task?.timer_start_time || null
);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import TaskTimer from '@/components/taskListCommon/task-timer/task-timer';
import { useTaskTimer } from '@/hooks/useTaskTimer';
import { useTaskTimerWithConflictCheck } from '@/hooks/useTaskTimerWithConflictCheck';
interface TaskTimeTrackingProps {
taskId: string;
@@ -8,7 +8,7 @@ interface TaskTimeTrackingProps {
}
const TaskTimeTracking: React.FC<TaskTimeTrackingProps> = React.memo(({ taskId, isDarkMode }) => {
const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimer(
const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimerWithConflictCheck(
taskId,
null // The hook will get the timer start time from Redux
);

View File

@@ -153,8 +153,8 @@ const Navbar = () => {
<Flex align="center">
<SwitchTeamButton />
<NotificationButton />
{/* <TimerButton /> */}
<HelpButton />
<TimerButton />
{/* <HelpButton /> */}
<ProfileButton isOwnerOrAdmin={isOwnerOrAdmin} />
</Flex>
</Flex>

View File

@@ -7,7 +7,7 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { updateTaskTimeTracking } from '@/features/tasks/tasks.slice';
import moment from 'moment';
import { format, differenceInSeconds, isValid, parseISO } from 'date-fns';
const { Text } = Typography;
const { useToken } = theme;
@@ -60,17 +60,17 @@ const TimerButton = () => {
try {
if (!timer || !timer.task_id || !timer.start_time) return;
const startTime = moment(timer.start_time);
if (!startTime.isValid()) {
const startTime = parseISO(timer.start_time);
if (!isValid(startTime)) {
logError(`Invalid start time for timer ${timer.task_id}: ${timer.start_time}`);
return;
}
const now = moment();
const duration = moment.duration(now.diff(startTime));
const hours = Math.floor(duration.asHours());
const minutes = duration.minutes();
const seconds = duration.seconds();
const now = new Date();
const totalSeconds = differenceInSeconds(now, startTime);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
newTimes[timer.task_id] =
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} catch (error) {
@@ -86,12 +86,7 @@ const TimerButton = () => {
useEffect(() => {
fetchRunningTimers();
// Set up polling to refresh timers every 30 seconds
const pollInterval = setInterval(() => {
fetchRunningTimers();
}, 30000);
return () => clearInterval(pollInterval);
// Removed periodic polling - rely on socket events for real-time updates
}, [fetchRunningTimers]);
useEffect(() => {
@@ -273,7 +268,7 @@ const TimerButton = () => {
<Text type="secondary" style={{ fontSize: 11 }}>
Started:{' '}
{timer.start_time
? moment(timer.start_time).format('HH:mm')
? format(parseISO(timer.start_time), 'HH:mm')
: '--:--'}
</Text>
<Text

View File

@@ -0,0 +1,94 @@
import { useState, useEffect, useCallback } from 'react';
import { Modal } from 'antd';
import { useTranslation } from 'react-i18next';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { taskTimeLogsApiService } from '@/api/tasks/task-time-logs.api.service';
import { useTaskTimer } from './useTaskTimer';
interface ConflictingTimer {
task_id: string;
task_name: string;
project_name: string;
}
export const useTaskTimerWithConflictCheck = (taskId: string, timerStartTime: string | null) => {
const { socket } = useSocket();
const { t: tTable } = useTranslation('task-list-table');
const { t: tCommon } = useTranslation('common');
// Ensure timerStartTime is a number or null, as required by useTaskTimer
const parsedTimerStartTime = typeof timerStartTime === 'string' && timerStartTime !== null
? Number(timerStartTime)
: timerStartTime;
const originalHook = useTaskTimer(taskId, parsedTimerStartTime as number | null);
const [isCheckingConflict, setIsCheckingConflict] = useState(false);
const checkForConflictingTimers = useCallback(async () => {
try {
const response = await taskTimeLogsApiService.getRunningTimers();
const runningTimers = response.body || [];
// Find conflicting timer (running timer for a different task)
const conflictingTimer = runningTimers.find((timer: ConflictingTimer) =>
timer.task_id !== taskId
);
return conflictingTimer;
} catch (error) {
console.error('Error checking for conflicting timers:', error);
return null;
}
}, [taskId]);
const handleStartTimerWithConflictCheck = useCallback(async () => {
if (isCheckingConflict) return;
setIsCheckingConflict(true);
try {
const conflictingTimer = await checkForConflictingTimers();
if (conflictingTimer) {
Modal.confirm({
title: tTable('timer.conflictTitle'),
content: tTable('timer.conflictMessage', {
taskName: conflictingTimer.task_name,
projectName: conflictingTimer.project_name
}),
okText: tTable('timer.stopAndStart'),
cancelText: tCommon('cancel'),
onOk: () => {
// Stop the conflicting timer
if (socket) {
socket.emit(SocketEvents.TASK_TIMER_STOP.toString(), JSON.stringify({
task_id: conflictingTimer.task_id
}));
}
// Start the new timer after a short delay
setTimeout(() => {
originalHook.handleStartTimer();
}, 100);
},
});
} else {
// No conflict, start timer directly
originalHook.handleStartTimer();
}
} catch (error) {
console.error('Error handling timer start with conflict check:', error);
// Fallback to original behavior
originalHook.handleStartTimer();
} finally {
setIsCheckingConflict(false);
}
}, [isCheckingConflict, checkForConflictingTimers, tTable, tCommon, socket, originalHook]);
return {
...originalHook,
handleStartTimer: handleStartTimerWithConflictCheck,
isCheckingConflict,
};
};