import { ClockCircleOutlined, StopOutlined } from '@ant-design/icons'; import { Badge, Button, Dropdown, List, Tooltip, Typography, Space, Divider, theme } from 'antd'; import React, { useEffect, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { taskTimeLogsApiService, IRunningTimer } from '@/api/tasks/task-time-logs.api.service'; 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'; const { Text } = Typography; const { useToken } = theme; const TimerButton = () => { const [runningTimers, setRunningTimers] = useState([]); const [loading, setLoading] = useState(false); const [currentTimes, setCurrentTimes] = useState>({}); const [dropdownOpen, setDropdownOpen] = useState(false); const [error, setError] = useState(null); const { t } = useTranslation('navbar'); const { token } = useToken(); const dispatch = useAppDispatch(); const { socket } = useSocket(); const logError = (message: string, error?: any) => { // Production-safe error logging console.error(`[TimerButton] ${message}`, error); setError(message); }; const fetchRunningTimers = useCallback(async () => { try { setLoading(true); setError(null); const response = await taskTimeLogsApiService.getRunningTimers(); if (response && response.done) { const timers = Array.isArray(response.body) ? response.body : []; setRunningTimers(timers); } else { logError('Invalid response from getRunningTimers API'); setRunningTimers([]); } } catch (error) { logError('Error fetching running timers', error); setRunningTimers([]); } finally { setLoading(false); } }, []); const updateCurrentTimes = useCallback(() => { try { if (!Array.isArray(runningTimers) || runningTimers.length === 0) return; const newTimes: Record = {}; runningTimers.forEach(timer => { try { if (!timer || !timer.task_id || !timer.start_time) return; const startTime = moment(timer.start_time); if (!startTime.isValid()) { 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(); newTimes[timer.task_id] = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } catch (error) { logError(`Error updating time for timer ${timer?.task_id}`, error); } }); setCurrentTimes(newTimes); } catch (error) { logError('Error in updateCurrentTimes', error); } }, [runningTimers]); useEffect(() => { fetchRunningTimers(); // Set up polling to refresh timers every 30 seconds const pollInterval = setInterval(() => { fetchRunningTimers(); }, 30000); return () => clearInterval(pollInterval); }, [fetchRunningTimers]); useEffect(() => { if (runningTimers.length > 0) { updateCurrentTimes(); const interval = setInterval(updateCurrentTimes, 1000); return () => clearInterval(interval); } }, [runningTimers, updateCurrentTimes]); // Listen for timer start/stop events and project updates to refresh the count useEffect(() => { if (!socket) { logError('Socket not available'); return; } const handleTimerStart = (data: string) => { try { const parsed = typeof data === 'string' ? JSON.parse(data) : data; const { id } = parsed || {}; if (id) { // Refresh the running timers list when a new timer is started fetchRunningTimers(); } } catch (error) { logError('Error parsing timer start event', error); } }; const handleTimerStop = (data: string) => { try { const parsed = typeof data === 'string' ? JSON.parse(data) : data; const { id } = parsed || {}; if (id) { // Refresh the running timers list when a timer is stopped fetchRunningTimers(); } } catch (error) { logError('Error parsing timer stop event', error); } }; const handleProjectUpdates = () => { try { // Refresh timers when project updates are available fetchRunningTimers(); } catch (error) { logError('Error handling project updates', error); } }; try { socket.on(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart); socket.on(SocketEvents.TASK_TIMER_STOP.toString(), handleTimerStop); socket.on(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString(), handleProjectUpdates); return () => { try { socket.off(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart); socket.off(SocketEvents.TASK_TIMER_STOP.toString(), handleTimerStop); socket.off(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString(), handleProjectUpdates); } catch (error) { logError('Error cleaning up socket listeners', error); } }; } catch (error) { logError('Error setting up socket listeners', error); } }, [socket, fetchRunningTimers]); const hasRunningTimers = () => { return Array.isArray(runningTimers) && runningTimers.length > 0; }; const timerCount = () => { return Array.isArray(runningTimers) ? runningTimers.length : 0; }; const handleStopTimer = (taskId: string) => { if (!socket) { logError('Socket not available for stopping timer'); return; } if (!taskId) { logError('Invalid task ID for stopping timer'); return; } try { socket.emit(SocketEvents.TASK_TIMER_STOP.toString(), JSON.stringify({ task_id: taskId })); dispatch(updateTaskTimeTracking({ taskId, timeTracking: null })); } catch (error) { logError(`Error stopping timer for task ${taskId}`, error); } }; const renderDropdownContent = () => { try { if (error) { return (
Error loading timers
); } return (
{!Array.isArray(runningTimers) || runningTimers.length === 0 ? (
No running timers
) : ( { if (!timer || !timer.task_id) return null; return (
{timer.task_name || 'Unnamed Task'}
{timer.project_name || 'Unnamed Project'}
{timer.parent_task_name && ( Parent: {timer.parent_task_name} )}
Started:{' '} {timer.start_time ? moment(timer.start_time).format('HH:mm') : '--:--'} {currentTimes[timer.task_id] || '00:00:00'}
); }} /> )} {hasRunningTimers() && ( <>
{timerCount()} timer{timerCount() !== 1 ? 's' : ''} running
)}
); } catch (error) { logError('Error rendering dropdown content', error); return (
Error rendering timers
); } }; const handleDropdownOpenChange = (open: boolean) => { try { setDropdownOpen(open); if (open) { fetchRunningTimers(); } } catch (error) { logError('Error handling dropdown open change', error); } }; try { return ( renderDropdownContent()} trigger={['click']} placement="bottomRight" open={dropdownOpen} onOpenChange={handleDropdownOpenChange} >