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:
@@ -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.",
|
"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.",
|
"reconnecting": "Jeni shkëputur nga serveri.",
|
||||||
"connection-lost": "Lidhja me serverin dështoi. Ju lutemi kontrolloni lidhjen tuaj me internet.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,5 +136,11 @@
|
|||||||
"dependencies": "Detyra ka varësi",
|
"dependencies": "Detyra ka varësi",
|
||||||
"recurring": "Detyrë përsëritëse"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
"signup-failed": "Registrierung fehlgeschlagen. Bitte füllen Sie alle erforderlichen Felder aus und versuchen Sie es erneut.",
|
"signup-failed": "Registrierung fehlgeschlagen. Bitte füllen Sie alle erforderlichen Felder aus und versuchen Sie es erneut.",
|
||||||
"reconnecting": "Vom Server getrennt.",
|
"reconnecting": "Vom Server getrennt.",
|
||||||
"connection-lost": "Verbindung zum Server fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,5 +136,11 @@
|
|||||||
"dependencies": "Aufgabe hat Abhängigkeiten",
|
"dependencies": "Aufgabe hat Abhängigkeiten",
|
||||||
"recurring": "Wiederkehrende Aufgabe"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
"signup-failed": "Signup failed. Please ensure all required fields are filled and try again.",
|
"signup-failed": "Signup failed. Please ensure all required fields are filled and try again.",
|
||||||
"reconnecting": "Disconnected from server.",
|
"reconnecting": "Disconnected from server.",
|
||||||
"connection-lost": "Failed to connect to server. Please check your internet connection.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,5 +136,11 @@
|
|||||||
"dependencies": "Task has dependencies",
|
"dependencies": "Task has dependencies",
|
||||||
"recurring": "Recurring task"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
"signup-failed": "Error al registrarse. Por favor asegúrate de llenar todos los campos requeridos e intenta nuevamente.",
|
"signup-failed": "Error al registrarse. Por favor asegúrate de llenar todos los campos requeridos e intenta nuevamente.",
|
||||||
"reconnecting": "Reconectando al servidor...",
|
"reconnecting": "Reconectando al servidor...",
|
||||||
"connection-lost": "Conexión perdida. Intentando reconectarse...",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,5 +136,11 @@
|
|||||||
"dependencies": "La tarea tiene dependencias",
|
"dependencies": "La tarea tiene dependencias",
|
||||||
"recurring": "Tarea recurrente"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
"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...",
|
"reconnecting": "Reconectando ao servidor...",
|
||||||
"connection-lost": "Conexão perdida. Tentando reconectar...",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,5 +136,11 @@
|
|||||||
"dependencies": "A tarefa tem dependências",
|
"dependencies": "A tarefa tem dependências",
|
||||||
"recurring": "Tarefa recorrente"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
"signup-failed": "注册失败。请确保填写所有必填字段并重试。",
|
"signup-failed": "注册失败。请确保填写所有必填字段并重试。",
|
||||||
"reconnecting": "与服务器断开连接。",
|
"reconnecting": "与服务器断开连接。",
|
||||||
"connection-lost": "无法连接到服务器。请检查您的互联网连接。",
|
"connection-lost": "无法连接到服务器。请检查您的互联网连接。",
|
||||||
"connection-restored": "成功连接到服务器"
|
"connection-restored": "成功连接到服务器",
|
||||||
|
"cancel": "取消"
|
||||||
}
|
}
|
||||||
@@ -129,5 +129,11 @@
|
|||||||
"dependencies": "任务有依赖项",
|
"dependencies": "任务有依赖项",
|
||||||
"recurring": "重复任务"
|
"recurring": "重复任务"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"timer": {
|
||||||
|
"conflictTitle": "计时器已在运行",
|
||||||
|
"conflictMessage": "您在项目\"{{projectName}}\"中的\"{{taskName}}\"任务正在运行计时器。您是否要停止该计时器并为此任务启动新的计时器?",
|
||||||
|
"stopAndStart": "停止并启动新计时器"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ import TimeLogList from './time-log-list';
|
|||||||
import { taskTimeLogsApiService } from '@/api/tasks/task-time-logs.api.service';
|
import { taskTimeLogsApiService } from '@/api/tasks/task-time-logs.api.service';
|
||||||
import { ITaskLogViewModel } from '@/types/tasks/task-log-view.types';
|
import { ITaskLogViewModel } from '@/types/tasks/task-log-view.types';
|
||||||
import TaskTimer from '@/components/taskListCommon/task-timer/task-timer';
|
import TaskTimer from '@/components/taskListCommon/task-timer/task-timer';
|
||||||
import { useTaskTimer } from '@/hooks/useTaskTimer';
|
import { useTaskTimerWithConflictCheck } from '@/hooks/useTaskTimerWithConflictCheck';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
|
|
||||||
interface TaskDrawerTimeLogProps {
|
interface TaskDrawerTimeLogProps {
|
||||||
@@ -31,7 +31,7 @@ const TaskDrawerTimeLog = ({ t, refreshTrigger = 0 }: TaskDrawerTimeLogProps) =>
|
|||||||
state => state.taskDrawerReducer
|
state => state.taskDrawerReducer
|
||||||
);
|
);
|
||||||
|
|
||||||
const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimer(
|
const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimerWithConflictCheck(
|
||||||
selectedTaskId || '',
|
selectedTaskId || '',
|
||||||
taskFormViewModel?.task?.timer_start_time || null
|
taskFormViewModel?.task?.timer_start_time || null
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TaskTimer from '@/components/taskListCommon/task-timer/task-timer';
|
import TaskTimer from '@/components/taskListCommon/task-timer/task-timer';
|
||||||
import { useTaskTimer } from '@/hooks/useTaskTimer';
|
import { useTaskTimerWithConflictCheck } from '@/hooks/useTaskTimerWithConflictCheck';
|
||||||
|
|
||||||
interface TaskTimeTrackingProps {
|
interface TaskTimeTrackingProps {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
@@ -8,7 +8,7 @@ interface TaskTimeTrackingProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TaskTimeTracking: React.FC<TaskTimeTrackingProps> = React.memo(({ taskId, isDarkMode }) => {
|
const TaskTimeTracking: React.FC<TaskTimeTrackingProps> = React.memo(({ taskId, isDarkMode }) => {
|
||||||
const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimer(
|
const { started, timeString, handleStartTimer, handleStopTimer } = useTaskTimerWithConflictCheck(
|
||||||
taskId,
|
taskId,
|
||||||
null // The hook will get the timer start time from Redux
|
null // The hook will get the timer start time from Redux
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ const Navbar = () => {
|
|||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
<SwitchTeamButton />
|
<SwitchTeamButton />
|
||||||
<NotificationButton />
|
<NotificationButton />
|
||||||
{/* <TimerButton /> */}
|
<TimerButton />
|
||||||
<HelpButton />
|
{/* <HelpButton /> */}
|
||||||
<ProfileButton isOwnerOrAdmin={isOwnerOrAdmin} />
|
<ProfileButton isOwnerOrAdmin={isOwnerOrAdmin} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import { updateTaskTimeTracking } from '@/features/tasks/tasks.slice';
|
import { updateTaskTimeTracking } from '@/features/tasks/tasks.slice';
|
||||||
import moment from 'moment';
|
import { format, differenceInSeconds, isValid, parseISO } from 'date-fns';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { useToken } = theme;
|
const { useToken } = theme;
|
||||||
@@ -60,17 +60,17 @@ const TimerButton = () => {
|
|||||||
try {
|
try {
|
||||||
if (!timer || !timer.task_id || !timer.start_time) return;
|
if (!timer || !timer.task_id || !timer.start_time) return;
|
||||||
|
|
||||||
const startTime = moment(timer.start_time);
|
const startTime = parseISO(timer.start_time);
|
||||||
if (!startTime.isValid()) {
|
if (!isValid(startTime)) {
|
||||||
logError(`Invalid start time for timer ${timer.task_id}: ${timer.start_time}`);
|
logError(`Invalid start time for timer ${timer.task_id}: ${timer.start_time}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = moment();
|
const now = new Date();
|
||||||
const duration = moment.duration(now.diff(startTime));
|
const totalSeconds = differenceInSeconds(now, startTime);
|
||||||
const hours = Math.floor(duration.asHours());
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
const minutes = duration.minutes();
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
const seconds = duration.seconds();
|
const seconds = totalSeconds % 60;
|
||||||
newTimes[timer.task_id] =
|
newTimes[timer.task_id] =
|
||||||
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -86,12 +86,7 @@ const TimerButton = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRunningTimers();
|
fetchRunningTimers();
|
||||||
|
|
||||||
// Set up polling to refresh timers every 30 seconds
|
// Removed periodic polling - rely on socket events for real-time updates
|
||||||
const pollInterval = setInterval(() => {
|
|
||||||
fetchRunningTimers();
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
return () => clearInterval(pollInterval);
|
|
||||||
}, [fetchRunningTimers]);
|
}, [fetchRunningTimers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -273,7 +268,7 @@ const TimerButton = () => {
|
|||||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||||
Started:{' '}
|
Started:{' '}
|
||||||
{timer.start_time
|
{timer.start_time
|
||||||
? moment(timer.start_time).format('HH:mm')
|
? format(parseISO(timer.start_time), 'HH:mm')
|
||||||
: '--:--'}
|
: '--:--'}
|
||||||
</Text>
|
</Text>
|
||||||
<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