diff --git a/worklenz-frontend/public/locales/alb/common.json b/worklenz-frontend/public/locales/alb/common.json index 5af25f69..8b005314 100644 --- a/worklenz-frontend/public/locales/alb/common.json +++ b/worklenz-frontend/public/locales/alb/common.json @@ -5,5 +5,6 @@ "signup-failed": "Regjistrimi dështoi. Ju lutemi sigurohuni që të gjitha fushat e nevojshme janë plotësuar dhe provoni përsëri.", "reconnecting": "Jeni shkëputur nga serveri.", "connection-lost": "Lidhja me serverin dështoi. Ju lutemi kontrolloni lidhjen tuaj me internet.", - "connection-restored": "U lidhët me serverin me sukses" + "connection-restored": "U lidhët me serverin me sukses", + "cancel": "Anulo" } diff --git a/worklenz-frontend/public/locales/alb/task-list-table.json b/worklenz-frontend/public/locales/alb/task-list-table.json index bf2542cf..5d7756eb 100644 --- a/worklenz-frontend/public/locales/alb/task-list-table.json +++ b/worklenz-frontend/public/locales/alb/task-list-table.json @@ -136,5 +136,11 @@ "dependencies": "Detyra ka varësi", "recurring": "Detyrë përsëritëse" } + }, + + "timer": { + "conflictTitle": "Kronómetr Tashë Në Ecuri", + "conflictMessage": "Ju keni një kronómetr në ecuri për \"{{taskName}}\" në projektin \"{{projectName}}\". Dëshironi ta ndaloni atë kronómetr dhe të filloni një të ri për këtë detyrë?", + "stopAndStart": "Ndalo & Fillo Kronómetr të Ri" } } diff --git a/worklenz-frontend/public/locales/de/common.json b/worklenz-frontend/public/locales/de/common.json index 937ad4a9..10ad2bc1 100644 --- a/worklenz-frontend/public/locales/de/common.json +++ b/worklenz-frontend/public/locales/de/common.json @@ -5,5 +5,6 @@ "signup-failed": "Registrierung fehlgeschlagen. Bitte füllen Sie alle erforderlichen Felder aus und versuchen Sie es erneut.", "reconnecting": "Vom Server getrennt.", "connection-lost": "Verbindung zum Server fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.", - "connection-restored": "Erfolgreich mit dem Server verbunden" + "connection-restored": "Erfolgreich mit dem Server verbunden", + "cancel": "Abbrechen" } diff --git a/worklenz-frontend/public/locales/de/task-list-table.json b/worklenz-frontend/public/locales/de/task-list-table.json index 21353b54..4adfd5f7 100644 --- a/worklenz-frontend/public/locales/de/task-list-table.json +++ b/worklenz-frontend/public/locales/de/task-list-table.json @@ -136,5 +136,11 @@ "dependencies": "Aufgabe hat Abhängigkeiten", "recurring": "Wiederkehrende Aufgabe" } + }, + + "timer": { + "conflictTitle": "Timer läuft bereits", + "conflictMessage": "Sie haben einen Timer für \"{{taskName}}\" im Projekt \"{{projectName}}\" laufen. Möchten Sie diesen Timer stoppen und einen neuen für diese Aufgabe starten?", + "stopAndStart": "Stoppen & Neuen Timer starten" } } diff --git a/worklenz-frontend/public/locales/en/common.json b/worklenz-frontend/public/locales/en/common.json index 815560be..641bd765 100644 --- a/worklenz-frontend/public/locales/en/common.json +++ b/worklenz-frontend/public/locales/en/common.json @@ -5,5 +5,6 @@ "signup-failed": "Signup failed. Please ensure all required fields are filled and try again.", "reconnecting": "Disconnected from server.", "connection-lost": "Failed to connect to server. Please check your internet connection.", - "connection-restored": "Connected to server successfully" + "connection-restored": "Connected to server successfully", + "cancel": "Cancel" } diff --git a/worklenz-frontend/public/locales/en/task-list-table.json b/worklenz-frontend/public/locales/en/task-list-table.json index 6d05d6b9..f20fdadc 100644 --- a/worklenz-frontend/public/locales/en/task-list-table.json +++ b/worklenz-frontend/public/locales/en/task-list-table.json @@ -136,5 +136,11 @@ "dependencies": "Task has dependencies", "recurring": "Recurring task" } + }, + + "timer": { + "conflictTitle": "Timer Already Running", + "conflictMessage": "You have a timer running for \"{{taskName}}\" in project \"{{projectName}}\". Would you like to stop that timer and start a new one for this task?", + "stopAndStart": "Stop & Start New Timer" } } diff --git a/worklenz-frontend/public/locales/es/common.json b/worklenz-frontend/public/locales/es/common.json index 583e8670..2f04e775 100644 --- a/worklenz-frontend/public/locales/es/common.json +++ b/worklenz-frontend/public/locales/es/common.json @@ -5,5 +5,6 @@ "signup-failed": "Error al registrarse. Por favor asegúrate de llenar todos los campos requeridos e intenta nuevamente.", "reconnecting": "Reconectando al servidor...", "connection-lost": "Conexión perdida. Intentando reconectarse...", - "connection-restored": "Conexión restaurada. Reconectando al servidor..." + "connection-restored": "Conexión restaurada. Reconectando al servidor...", + "cancel": "Cancelar" } diff --git a/worklenz-frontend/public/locales/es/task-list-table.json b/worklenz-frontend/public/locales/es/task-list-table.json index 0ff70a2f..53d99bb8 100644 --- a/worklenz-frontend/public/locales/es/task-list-table.json +++ b/worklenz-frontend/public/locales/es/task-list-table.json @@ -136,5 +136,11 @@ "dependencies": "La tarea tiene dependencias", "recurring": "Tarea recurrente" } + }, + + "timer": { + "conflictTitle": "Temporizador Ya En Ejecución", + "conflictMessage": "Tiene un temporizador ejecutándose para \"{{taskName}}\" en el proyecto \"{{projectName}}\". ¿Le gustaría detener ese temporizador e iniciar uno nuevo para esta tarea?", + "stopAndStart": "Detener e Iniciar Nuevo Temporizador" } } diff --git a/worklenz-frontend/public/locales/pt/common.json b/worklenz-frontend/public/locales/pt/common.json index ce540a28..0e192458 100644 --- a/worklenz-frontend/public/locales/pt/common.json +++ b/worklenz-frontend/public/locales/pt/common.json @@ -5,5 +5,6 @@ "signup-failed": "Falha no cadastro. Por favor, certifique-se de que todos os campos obrigatórios estão preenchidos e tente novamente.", "reconnecting": "Reconectando ao servidor...", "connection-lost": "Conexão perdida. Tentando reconectar...", - "connection-restored": "Conexão restaurada. Reconectando ao servidor..." + "connection-restored": "Conexão restaurada. Reconectando ao servidor...", + "cancel": "Cancelar" } diff --git a/worklenz-frontend/public/locales/pt/task-list-table.json b/worklenz-frontend/public/locales/pt/task-list-table.json index 6726b32c..d519ec8f 100644 --- a/worklenz-frontend/public/locales/pt/task-list-table.json +++ b/worklenz-frontend/public/locales/pt/task-list-table.json @@ -136,5 +136,11 @@ "dependencies": "A tarefa tem dependências", "recurring": "Tarefa recorrente" } + }, + + "timer": { + "conflictTitle": "Temporizador Já Em Execução", + "conflictMessage": "Você tem um temporizador executando para \"{{taskName}}\" no projeto \"{{projectName}}\". Gostaria de parar esse temporizador e iniciar um novo para esta tarefa?", + "stopAndStart": "Parar e Iniciar Novo Temporizador" } } diff --git a/worklenz-frontend/public/locales/zh/common.json b/worklenz-frontend/public/locales/zh/common.json index 520ee5e2..44821492 100644 --- a/worklenz-frontend/public/locales/zh/common.json +++ b/worklenz-frontend/public/locales/zh/common.json @@ -5,5 +5,6 @@ "signup-failed": "注册失败。请确保填写所有必填字段并重试。", "reconnecting": "与服务器断开连接。", "connection-lost": "无法连接到服务器。请检查您的互联网连接。", - "connection-restored": "成功连接到服务器" + "connection-restored": "成功连接到服务器", + "cancel": "取消" } \ No newline at end of file diff --git a/worklenz-frontend/public/locales/zh/task-list-table.json b/worklenz-frontend/public/locales/zh/task-list-table.json index 8d3dc9bb..e61fe403 100644 --- a/worklenz-frontend/public/locales/zh/task-list-table.json +++ b/worklenz-frontend/public/locales/zh/task-list-table.json @@ -129,5 +129,11 @@ "dependencies": "任务有依赖项", "recurring": "重复任务" } + }, + + "timer": { + "conflictTitle": "计时器已在运行", + "conflictMessage": "您在项目\"{{projectName}}\"中的\"{{taskName}}\"任务正在运行计时器。您是否要停止该计时器并为此任务启动新的计时器?", + "stopAndStart": "停止并启动新计时器" } } \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-drawer/shared/time-log/task-drawer-time-log.tsx b/worklenz-frontend/src/components/task-drawer/shared/time-log/task-drawer-time-log.tsx index 61f245cf..c4ff27f9 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/time-log/task-drawer-time-log.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/time-log/task-drawer-time-log.tsx @@ -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 ); diff --git a/worklenz-frontend/src/components/task-list-v2/TaskTimeTracking.tsx b/worklenz-frontend/src/components/task-list-v2/TaskTimeTracking.tsx index 3bb44f57..77094d67 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskTimeTracking.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskTimeTracking.tsx @@ -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 = 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 ); diff --git a/worklenz-frontend/src/features/navbar/navbar.tsx b/worklenz-frontend/src/features/navbar/navbar.tsx index 1630c25e..ca0cd06c 100644 --- a/worklenz-frontend/src/features/navbar/navbar.tsx +++ b/worklenz-frontend/src/features/navbar/navbar.tsx @@ -153,8 +153,8 @@ const Navbar = () => { - {/* */} - + + {/* */} diff --git a/worklenz-frontend/src/features/navbar/timers/timer-button.tsx b/worklenz-frontend/src/features/navbar/timers/timer-button.tsx index 2735c34a..3a4ea12b 100644 --- a/worklenz-frontend/src/features/navbar/timers/timer-button.tsx +++ b/worklenz-frontend/src/features/navbar/timers/timer-button.tsx @@ -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 = () => { Started:{' '} {timer.start_time - ? moment(timer.start_time).format('HH:mm') + ? format(parseISO(timer.start_time), 'HH:mm') : '--:--'} { + 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, + }; +}; \ No newline at end of file