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:
@@ -17,38 +17,70 @@ const TimerButton = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentTimes, setCurrentTimes] = useState<Record<string, string>>({});
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 updateCurrentTimes = useCallback(() => {
|
||||
try {
|
||||
if (!Array.isArray(runningTimers) || runningTimers.length === 0) return;
|
||||
|
||||
const newTimes: Record<string, string> = {};
|
||||
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,68 +99,107 @@ 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 = () => {
|
||||
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 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;
|
||||
}
|
||||
|
||||
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 = (
|
||||
const renderDropdownContent = () => {
|
||||
try {
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: 16, textAlign: 'center', width: 350 }}>
|
||||
<Text type="danger">Error loading timers</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 350,
|
||||
@@ -140,14 +211,17 @@ const TimerButton = () => {
|
||||
border: `1px solid ${token.colorBorderSecondary}`
|
||||
}}
|
||||
>
|
||||
{runningTimers.length === 0 ? (
|
||||
{!Array.isArray(runningTimers) || runningTimers.length === 0 ? (
|
||||
<div style={{ padding: 16, textAlign: 'center' }}>
|
||||
<Text type="secondary">No running timers</Text>
|
||||
</div>
|
||||
) : (
|
||||
<List
|
||||
dataSource={runningTimers}
|
||||
renderItem={(timer) => (
|
||||
renderItem={(timer) => {
|
||||
if (!timer || !timer.task_id) return null;
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
@@ -158,7 +232,7 @@ const TimerButton = () => {
|
||||
<div style={{ width: '100%' }}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong style={{ fontSize: 14, color: token.colorText }}>
|
||||
{timer.task_name}
|
||||
{timer.task_name || 'Unnamed Task'}
|
||||
</Text>
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
@@ -170,7 +244,7 @@ const TimerButton = () => {
|
||||
fontWeight: 500,
|
||||
marginTop: 2
|
||||
}}>
|
||||
{timer.project_name}
|
||||
{timer.project_name || 'Unnamed Project'}
|
||||
</div>
|
||||
{timer.parent_task_name && (
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
@@ -181,7 +255,7 @@ const TimerButton = () => {
|
||||
<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')}
|
||||
Started: {timer.start_time ? moment(timer.start_time).format('HH:mm') : '--:--'}
|
||||
</Text>
|
||||
<Text
|
||||
strong
|
||||
@@ -215,10 +289,11 @@ const TimerButton = () => {
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{runningTimers.length > 0 && (
|
||||
{hasRunningTimers() && (
|
||||
<>
|
||||
<Divider style={{ margin: 0, borderColor: token.colorBorderSecondary }} />
|
||||
<div
|
||||
@@ -231,26 +306,42 @@ const TimerButton = () => {
|
||||
}}
|
||||
>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{runningTimers.length} timer{runningTimers.length !== 1 ? 's' : ''} running
|
||||
{timerCount()} timer{timerCount() !== 1 ? 's' : ''} running
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
logError('Error rendering dropdown content', error);
|
||||
return (
|
||||
<Dropdown
|
||||
popupRender={() => dropdownContent}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
open={dropdownOpen}
|
||||
onOpenChange={(open) => {
|
||||
<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
|
||||
@@ -270,6 +361,19 @@ const TimerButton = () => {
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
);
|
||||
} catch (error) {
|
||||
logError('Error rendering TimerButton', error);
|
||||
return (
|
||||
<Tooltip title="Timer Error">
|
||||
<Button
|
||||
style={{ height: '62px', width: '60px' }}
|
||||
type="text"
|
||||
icon={<ClockCircleOutlined style={{ fontSize: 20 }} />}
|
||||
disabled
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default TimerButton;
|
||||
Reference in New Issue
Block a user