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.
This commit is contained in:
chamikaJ
2025-06-11 12:55:53 +05:30
parent 6492a4672b
commit 5ce9e66fea

View File

@@ -17,38 +17,70 @@ const TimerButton = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [currentTimes, setCurrentTimes] = useState<Record<string, string>>({}); const [currentTimes, setCurrentTimes] = useState<Record<string, string>>({});
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
const { t } = useTranslation('navbar'); const { t } = useTranslation('navbar');
const { token } = useToken(); const { token } = useToken();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { socket } = useSocket(); const { socket } = useSocket();
const logError = (message: string, error?: any) => {
// Production-safe error logging
console.error(`[TimerButton] ${message}`, error);
setError(message);
};
const fetchRunningTimers = useCallback(async () => { const fetchRunningTimers = useCallback(async () => {
try { try {
setLoading(true); setLoading(true);
setError(null);
const response = await taskTimeLogsApiService.getRunningTimers(); 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) { } catch (error) {
console.error('Error fetching running timers:', error); logError('Error fetching running timers', error);
setRunningTimers([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, []);
const updateCurrentTimes = () => { const updateCurrentTimes = useCallback(() => {
const newTimes: Record<string, string> = {}; try {
runningTimers.forEach(timer => { if (!Array.isArray(runningTimers) || runningTimers.length === 0) return;
const startTime = moment(timer.start_time);
const now = moment(); const newTimes: Record<string, string> = {};
const duration = moment.duration(now.diff(startTime)); runningTimers.forEach(timer => {
const hours = Math.floor(duration.asHours()); try {
const minutes = duration.minutes(); if (!timer || !timer.task_id || !timer.start_time) return;
const seconds = duration.seconds();
newTimes[timer.task_id] = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; const startTime = moment(timer.start_time);
}); if (!startTime.isValid()) {
setCurrentTimes(newTimes); 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(() => { useEffect(() => {
fetchRunningTimers(); fetchRunningTimers();
@@ -67,209 +99,281 @@ const TimerButton = () => {
const interval = setInterval(updateCurrentTimes, 1000); const interval = setInterval(updateCurrentTimes, 1000);
return () => clearInterval(interval); return () => clearInterval(interval);
} }
}, [runningTimers]); }, [runningTimers, updateCurrentTimes]);
// Listen for timer start/stop events and project updates to refresh the count // Listen for timer start/stop events and project updates to refresh the count
useEffect(() => { useEffect(() => {
if (!socket) return; if (!socket) {
logError('Socket not available');
return;
}
const handleTimerStart = (data: string) => { const handleTimerStart = (data: string) => {
try { try {
const { id } = typeof data === 'string' ? JSON.parse(data) : data; const parsed = typeof data === 'string' ? JSON.parse(data) : data;
const { id } = parsed || {};
if (id) { if (id) {
// Refresh the running timers list when a new timer is started // Refresh the running timers list when a new timer is started
fetchRunningTimers(); fetchRunningTimers();
} }
} catch (error) { } catch (error) {
console.error('Error parsing timer start event:', error); logError('Error parsing timer start event', error);
} }
}; };
const handleTimerStop = (data: string) => { const handleTimerStop = (data: string) => {
try { try {
const { id } = typeof data === 'string' ? JSON.parse(data) : data; const parsed = typeof data === 'string' ? JSON.parse(data) : data;
const { id } = parsed || {};
if (id) { if (id) {
// Refresh the running timers list when a timer is stopped // Refresh the running timers list when a timer is stopped
fetchRunningTimers(); fetchRunningTimers();
} }
} catch (error) { } catch (error) {
console.error('Error parsing timer stop event:', error); logError('Error parsing timer stop event', error);
} }
}; };
const handleProjectUpdates = () => { const handleProjectUpdates = () => {
// Refresh timers when project updates are available try {
fetchRunningTimers(); // Refresh timers when project updates are available
fetchRunningTimers();
} catch (error) {
logError('Error handling project updates', error);
}
}; };
socket.on(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart); try {
socket.on(SocketEvents.TASK_TIMER_STOP.toString(), handleTimerStop); socket.on(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart);
socket.on(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString(), handleProjectUpdates); socket.on(SocketEvents.TASK_TIMER_STOP.toString(), handleTimerStop);
socket.on(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString(), handleProjectUpdates);
return () => { return () => {
socket.off(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart); try {
socket.off(SocketEvents.TASK_TIMER_STOP.toString(), handleTimerStop); socket.off(SocketEvents.TASK_TIMER_START.toString(), handleTimerStart);
socket.off(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString(), handleProjectUpdates); 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]); }, [socket, fetchRunningTimers]);
const hasRunningTimers = () => { const hasRunningTimers = () => {
return runningTimers.length > 0; return Array.isArray(runningTimers) && runningTimers.length > 0;
}; };
const timerCount = () => { const timerCount = () => {
return runningTimers.length; return Array.isArray(runningTimers) ? runningTimers.length : 0;
}; };
const handleStopTimer = (taskId: string) => { 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 })); if (!taskId) {
dispatch(updateTaskTimeTracking({ taskId, timeTracking: null })); 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 = ( const renderDropdownContent = () => {
<div try {
style={{ if (error) {
width: 350, return (
maxHeight: 400, <div style={{ padding: 16, textAlign: 'center', width: 350 }}>
overflow: 'auto', <Text type="danger">Error loading timers</Text>
backgroundColor: token.colorBgElevated,
borderRadius: token.borderRadius,
boxShadow: token.boxShadowSecondary,
border: `1px solid ${token.colorBorderSecondary}`
}}
>
{runningTimers.length === 0 ? (
<div style={{ padding: 16, textAlign: 'center' }}>
<Text type="secondary">No running timers</Text>
</div>
) : (
<List
dataSource={runningTimers}
renderItem={(timer) => (
<List.Item
style={{
padding: '12px 16px',
borderBottom: `1px solid ${token.colorBorderSecondary}`,
backgroundColor: 'transparent'
}}
>
<div style={{ width: '100%' }}>
<Space direction="vertical" size={4} style={{ width: '100%' }}>
<Text strong style={{ fontSize: 14, color: token.colorText }}>
{timer.task_name}
</Text>
<div style={{
display: 'inline-block',
backgroundColor: token.colorPrimaryBg,
color: token.colorPrimary,
padding: '2px 8px',
borderRadius: token.borderRadiusSM,
fontSize: 11,
fontWeight: 500,
marginTop: 2
}}>
{timer.project_name}
</div>
{timer.parent_task_name && (
<Text type="secondary" style={{ fontSize: 11 }}>
Parent: {timer.parent_task_name}
</Text>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<Text type="secondary" style={{ fontSize: 11 }}>
Started: {moment(timer.start_time).format('HH:mm')}
</Text>
<Text
strong
style={{
fontSize: 14,
color: token.colorPrimary,
fontFamily: 'monospace'
}}
>
{currentTimes[timer.task_id] || '00:00:00'}
</Text>
</div>
</div>
<Button
size="small"
icon={<StopOutlined />}
onClick={(e) => {
e.stopPropagation();
handleStopTimer(timer.task_id);
}}
style={{
backgroundColor: token.colorErrorBg,
borderColor: token.colorError,
color: token.colorError,
fontWeight: 500
}}
>
Stop
</Button>
</div>
</Space>
</div>
</List.Item>
)}
/>
)}
{runningTimers.length > 0 && (
<>
<Divider style={{ margin: 0, borderColor: token.colorBorderSecondary }} />
<div
style={{
padding: '8px 16px',
textAlign: 'center',
backgroundColor: token.colorFillQuaternary,
borderBottomLeftRadius: token.borderRadius,
borderBottomRightRadius: token.borderRadius
}}
>
<Text type="secondary" style={{ fontSize: 11 }}>
{runningTimers.length} timer{runningTimers.length !== 1 ? 's' : ''} running
</Text>
</div> </div>
</> );
)} }
</div>
);
return ( return (
<Dropdown <div
popupRender={() => dropdownContent} style={{
trigger={['click']} width: 350,
placement="bottomRight" maxHeight: 400,
open={dropdownOpen} overflow: 'auto',
onOpenChange={(open) => { backgroundColor: token.colorBgElevated,
setDropdownOpen(open); borderRadius: token.borderRadius,
if (open) { boxShadow: token.boxShadowSecondary,
fetchRunningTimers(); border: `1px solid ${token.colorBorderSecondary}`
} }}
}} >
> {!Array.isArray(runningTimers) || runningTimers.length === 0 ? (
<Tooltip title="Running Timers"> <div style={{ padding: 16, textAlign: 'center' }}>
<Text type="secondary">No running timers</Text>
</div>
) : (
<List
dataSource={runningTimers}
renderItem={(timer) => {
if (!timer || !timer.task_id) return null;
return (
<List.Item
style={{
padding: '12px 16px',
borderBottom: `1px solid ${token.colorBorderSecondary}`,
backgroundColor: 'transparent'
}}
>
<div style={{ width: '100%' }}>
<Space direction="vertical" size={4} style={{ width: '100%' }}>
<Text strong style={{ fontSize: 14, color: token.colorText }}>
{timer.task_name || 'Unnamed Task'}
</Text>
<div style={{
display: 'inline-block',
backgroundColor: token.colorPrimaryBg,
color: token.colorPrimary,
padding: '2px 8px',
borderRadius: token.borderRadiusSM,
fontSize: 11,
fontWeight: 500,
marginTop: 2
}}>
{timer.project_name || 'Unnamed Project'}
</div>
{timer.parent_task_name && (
<Text type="secondary" style={{ fontSize: 11 }}>
Parent: {timer.parent_task_name}
</Text>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<Text type="secondary" style={{ fontSize: 11 }}>
Started: {timer.start_time ? moment(timer.start_time).format('HH:mm') : '--:--'}
</Text>
<Text
strong
style={{
fontSize: 14,
color: token.colorPrimary,
fontFamily: 'monospace'
}}
>
{currentTimes[timer.task_id] || '00:00:00'}
</Text>
</div>
</div>
<Button
size="small"
icon={<StopOutlined />}
onClick={(e) => {
e.stopPropagation();
handleStopTimer(timer.task_id);
}}
style={{
backgroundColor: token.colorErrorBg,
borderColor: token.colorError,
color: token.colorError,
fontWeight: 500
}}
>
Stop
</Button>
</div>
</Space>
</div>
</List.Item>
);
}}
/>
)}
{hasRunningTimers() && (
<>
<Divider style={{ margin: 0, borderColor: token.colorBorderSecondary }} />
<div
style={{
padding: '8px 16px',
textAlign: 'center',
backgroundColor: token.colorFillQuaternary,
borderBottomLeftRadius: token.borderRadius,
borderBottomRightRadius: token.borderRadius
}}
>
<Text type="secondary" style={{ fontSize: 11 }}>
{timerCount()} timer{timerCount() !== 1 ? 's' : ''} running
</Text>
</div>
</>
)}
</div>
);
} catch (error) {
logError('Error rendering dropdown content', error);
return (
<div style={{ padding: 16, textAlign: 'center', width: 350 }}>
<Text type="danger">Error rendering timers</Text>
</div>
);
}
};
const handleDropdownOpenChange = (open: boolean) => {
try {
setDropdownOpen(open);
if (open) {
fetchRunningTimers();
}
} catch (error) {
logError('Error handling dropdown open change', error);
}
};
try {
return (
<Dropdown
popupRender={() => renderDropdownContent()}
trigger={['click']}
placement="bottomRight"
open={dropdownOpen}
onOpenChange={handleDropdownOpenChange}
>
<Tooltip title="Running Timers">
<Button
style={{ height: '62px', width: '60px' }}
type="text"
icon={
hasRunningTimers() ? (
<Badge count={timerCount()}>
<ClockCircleOutlined style={{ fontSize: 20 }} />
</Badge>
) : (
<ClockCircleOutlined style={{ fontSize: 20 }} />
)
}
loading={loading}
/>
</Tooltip>
</Dropdown>
);
} catch (error) {
logError('Error rendering TimerButton', error);
return (
<Tooltip title="Timer Error">
<Button <Button
style={{ height: '62px', width: '60px' }} style={{ height: '62px', width: '60px' }}
type="text" type="text"
icon={ icon={<ClockCircleOutlined style={{ fontSize: 20 }} />}
hasRunningTimers() ? ( disabled
<Badge count={timerCount()}>
<ClockCircleOutlined style={{ fontSize: 20 }} />
</Badge>
) : (
<ClockCircleOutlined style={{ fontSize: 20 }} />
)
}
loading={loading}
/> />
</Tooltip> </Tooltip>
</Dropdown> );
); }
}; };
export default TimerButton; export default TimerButton;