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:
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -153,8 +153,8 @@ const Navbar = () => {
|
||||
<Flex align="center">
|
||||
<SwitchTeamButton />
|
||||
<NotificationButton />
|
||||
{/* <TimerButton /> */}
|
||||
<HelpButton />
|
||||
<TimerButton />
|
||||
{/* <HelpButton /> */}
|
||||
<ProfileButton isOwnerOrAdmin={isOwnerOrAdmin} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -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
|
||||
|
||||
94
worklenz-frontend/src/hooks/useTaskTimerWithConflictCheck.ts
Normal file
94
worklenz-frontend/src/hooks/useTaskTimerWithConflictCheck.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user