From 5ce9e66fea6ab9b3c83528cc8e1139442fa9149b Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Wed, 11 Jun 2025 12:55:53 +0530 Subject: [PATCH] feat(timer-button): enhance error handling and improve timer updates - Added error state management and logging for API calls and timer updates. - Refactored timer update logic to handle invalid data and improve robustness. - Updated dropdown rendering to display error messages and handle empty states more gracefully. - Improved socket event handling with error logging for better debugging. --- .../features/navbar/timers/timer-button.tsx | 448 +++++++++++------- 1 file changed, 276 insertions(+), 172 deletions(-) diff --git a/worklenz-frontend/src/features/navbar/timers/timer-button.tsx b/worklenz-frontend/src/features/navbar/timers/timer-button.tsx index b9e050f0..847e63e7 100644 --- a/worklenz-frontend/src/features/navbar/timers/timer-button.tsx +++ b/worklenz-frontend/src/features/navbar/timers/timer-button.tsx @@ -17,38 +17,70 @@ const TimerButton = () => { 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.done) { - setRunningTimers(response.body || []); + + if (response && response.done) { + const timers = Array.isArray(response.body) ? response.body : []; + setRunningTimers(timers); + } else { + logError('Invalid response from getRunningTimers API'); + setRunningTimers([]); } } catch (error) { - console.error('Error fetching running timers:', error); + logError('Error fetching running timers', error); + setRunningTimers([]); } finally { setLoading(false); } }, []); - const updateCurrentTimes = () => { - const newTimes: Record = {}; - runningTimers.forEach(timer => { - const startTime = moment(timer.start_time); - 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')}`; - }); - setCurrentTimes(newTimes); - }; + 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(); @@ -67,209 +99,281 @@ const TimerButton = () => { const interval = setInterval(updateCurrentTimes, 1000); return () => clearInterval(interval); } - }, [runningTimers]); + }, [runningTimers, updateCurrentTimes]); // Listen for timer start/stop events and project updates to refresh the count useEffect(() => { - if (!socket) return; + if (!socket) { + logError('Socket not available'); + return; + } const handleTimerStart = (data: string) => { try { - const { id } = typeof data === 'string' ? JSON.parse(data) : data; + 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) { - console.error('Error parsing timer start event:', error); + logError('Error parsing timer start event', error); } }; const handleTimerStop = (data: string) => { try { - const { id } = typeof data === 'string' ? JSON.parse(data) : data; + 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) { - console.error('Error parsing timer stop event:', error); + logError('Error parsing timer stop event', error); } }; const handleProjectUpdates = () => { - // Refresh timers when project updates are available - fetchRunningTimers(); + try { + // Refresh timers when project updates are available + fetchRunningTimers(); + } catch (error) { + logError('Error handling project updates', error); + } }; - socket.on(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart); - socket.on(SocketEvents.TASK_TIMER_STOP.toString(), handleTimerStop); - socket.on(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString(), handleProjectUpdates); + 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 () => { - socket.off(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart); - socket.off(SocketEvents.TASK_TIMER_STOP.toString(), handleTimerStop); - socket.off(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 runningTimers.length > 0; + return Array.isArray(runningTimers) && runningTimers.length > 0; }; const timerCount = () => { - return runningTimers.length; + return Array.isArray(runningTimers) ? runningTimers.length : 0; }; const handleStopTimer = (taskId: string) => { - if (!socket) return; + if (!socket) { + logError('Socket not available for stopping timer'); + return; + } - socket.emit(SocketEvents.TASK_TIMER_STOP.toString(), JSON.stringify({ task_id: taskId })); - dispatch(updateTaskTimeTracking({ taskId, timeTracking: null })); + 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 dropdownContent = ( -
- {runningTimers.length === 0 ? ( -
- No running timers -
- ) : ( - ( - -
- - - {timer.task_name} - -
- {timer.project_name} -
- {timer.parent_task_name && ( - - Parent: {timer.parent_task_name} - - )} -
-
-
- - Started: {moment(timer.start_time).format('HH:mm')} - - - {currentTimes[timer.task_id] || '00:00:00'} - -
-
- -
-
-
-
- )} - /> - )} - {runningTimers.length > 0 && ( - <> - -
- - {runningTimers.length} timer{runningTimers.length !== 1 ? 's' : ''} running - + const renderDropdownContent = () => { + try { + if (error) { + return ( +
+ Error loading timers
- - )} -
- ); + ); + } - return ( - dropdownContent} - trigger={['click']} - placement="bottomRight" - open={dropdownOpen} - onOpenChange={(open) => { - setDropdownOpen(open); - if (open) { - fetchRunningTimers(); - } - }} - > - + 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} + > + +