From 5e4d78c6f5d434bbe6c0f144776556bc432f4bd1 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 2 Jun 2025 09:19:58 +0530 Subject: [PATCH 1/6] refactor(task-details-form): enhance progress input handling and improve assignee rendering - Added `InlineMember` type import for better type management. - Enhanced the `Avatars` component to handle multiple sources for assignee names, improving flexibility in data handling. --- .../shared/info-tab/task-details-form.tsx | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx index a2dcaef1..23dac128 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-details-form.tsx @@ -30,6 +30,7 @@ import TaskDrawerProgress from './details/task-drawer-progress/task-drawer-progr import { useAppSelector } from '@/hooks/useAppSelector'; import logger from '@/utils/errorLogger'; import TaskDrawerRecurringConfig from './details/task-drawer-recurring-config/task-drawer-recurring-config'; +import { InlineMember } from '@/types/teamMembers/inlineMember.types'; interface TaskDetailsFormProps { taskFormViewModel?: ITaskFormViewModel | null; @@ -45,29 +46,32 @@ const ConditionalProgressInput = ({ task, form }: ConditionalProgressInputProps) const { project } = useAppSelector(state => state.projectReducer); const hasSubTasks = task?.sub_tasks_count > 0; const isSubTask = !!task?.parent_task_id; - - // Add more aggressive logging and checks - logger.debug(`Task ${task.id} status: hasSubTasks=${hasSubTasks}, isSubTask=${isSubTask}, modes: time=${project?.use_time_progress}, manual=${project?.use_manual_progress}, weighted=${project?.use_weighted_progress}`); - + // STRICT RULE: Never show progress input for parent tasks with subtasks // This is the most important check and must be done first if (hasSubTasks) { logger.debug(`Task ${task.id} has ${task.sub_tasks_count} subtasks. Hiding progress input.`); return null; } - + // Only for tasks without subtasks, determine which input to show based on project mode if (project?.use_time_progress) { // In time-based mode, show progress input ONLY for tasks without subtasks - return ; + return ( + + ); } else if (project?.use_manual_progress) { // In manual mode, show progress input ONLY for tasks without subtasks - return ; + return ( + + ); } else if (project?.use_weighted_progress && isSubTask) { // In weighted mode, show weight input for subtasks - return ; + return ( + + ); } - + return null; }; @@ -148,7 +152,13 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) => - + @@ -160,10 +170,7 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) => {taskFormViewModel?.task && ( - + )} From 24fa837a39ba4fc5e95d22724b4404c9290138ef Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 2 Jun 2025 13:07:50 +0530 Subject: [PATCH 2/6] feat(auth): enhance login and verification processes with detailed debug logging - Added comprehensive debug logging to the login strategy and verification endpoint to track authentication flow and errors. - Improved title determination logic for login and signup success/failure messages based on authentication status. - Implemented middleware for logging request details on the login route to aid in debugging. --- .../src/controllers/auth-controller.ts | 28 +++++++++++++-- .../passport-local-login.ts | 36 +++++++++++++++---- worklenz-backend/src/routes/auth/index.ts | 14 +++++++- 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/worklenz-backend/src/controllers/auth-controller.ts b/worklenz-backend/src/controllers/auth-controller.ts index 8364d59c..b2d24c16 100644 --- a/worklenz-backend/src/controllers/auth-controller.ts +++ b/worklenz-backend/src/controllers/auth-controller.ts @@ -35,8 +35,32 @@ export default class AuthController extends WorklenzControllerBase { const auth_error = errors.length > 0 ? errors[0] : null; const message = messages.length > 0 ? messages[0] : null; - const midTitle = req.query.strategy === "login" ? "Login Failed!" : "Signup Failed!"; - const title = req.query.strategy ? midTitle : null; + // Debug logging + console.log("=== VERIFY ENDPOINT HIT ==="); + console.log("Verify endpoint - Strategy:", req.query.strategy); + console.log("Verify endpoint - Authenticated:", req.isAuthenticated()); + console.log("Verify endpoint - User:", !!req.user); + console.log("Verify endpoint - User ID:", req.user?.id); + console.log("Verify endpoint - Auth error:", auth_error); + console.log("Verify endpoint - Success message:", message); + console.log("Verify endpoint - Flash errors:", errors); + console.log("Verify endpoint - Flash messages:", messages); + console.log("Verify endpoint - Session ID:", req.sessionID); + console.log("Verify endpoint - Session passport:", (req.session as any).passport); + console.log("Verify endpoint - Session flash:", (req.session as any).flash); + + // Determine title based on authentication status and strategy + let title = null; + if (req.query.strategy) { + if (auth_error) { + // Show failure title only when there's an actual error + title = req.query.strategy === "login" ? "Login Failed!" : "Signup Failed!"; + } else if (req.isAuthenticated() && message) { + // Show success title when authenticated and there's a success message + title = req.query.strategy === "login" ? "Login Successful!" : "Signup Successful!"; + } + // If no error and not authenticated, don't show any title (this might be a redirect without completion) + } if (req.user) req.user.build_v = FileConstants.getRelease(); diff --git a/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts b/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts index 7d29fae8..f399b326 100644 --- a/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts +++ b/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts @@ -3,13 +3,23 @@ import { Strategy as LocalStrategy } from "passport-local"; import { log_error } from "../../shared/utils"; import db from "../../config/db"; import { Request } from "express"; +import { ERROR_KEY, SUCCESS_KEY } from "./passport-constants"; async function handleLogin(req: Request, email: string, password: string, done: any) { + console.log("=== LOGIN STRATEGY STARTED ==="); console.log("Login attempt for:", email); + console.log("Password provided:", !!password); + console.log("Request body:", req.body); + + // Clear any existing flash messages + (req.session as any).flash = {}; if (!email || !password) { - console.log("Missing credentials"); - return done(null, false, { message: "Please enter both email and password" }); + console.log("Missing credentials - email:", !!email, "password:", !!password); + const errorMsg = "Please enter both email and password"; + console.log("Setting error flash message:", errorMsg); + req.flash(ERROR_KEY, errorMsg); + return done(null, false); } try { @@ -24,18 +34,30 @@ async function handleLogin(req: Request, email: string, password: string, done: const [data] = result.rows; if (!data?.password) { - console.log("No account found"); - return done(null, false, { message: "No account found with this email" }); + console.log("No account found for email:", email); + const errorMsg = "No account found with this email"; + console.log("Setting error flash message:", errorMsg); + req.flash(ERROR_KEY, errorMsg); + return done(null, false); } const passwordMatch = bcrypt.compareSync(password, data.password); - console.log("Password match:", passwordMatch); + console.log("Password match result:", passwordMatch); if (passwordMatch && email === data.email) { delete data.password; - return done(null, data, {message: "User successfully logged in"}); + console.log("Login successful for user:", data.id); + const successMsg = "User successfully logged in"; + console.log("Setting success flash message:", successMsg); + req.flash(SUCCESS_KEY, successMsg); + return done(null, data); } - return done(null, false, { message: "Incorrect email or password" }); + + console.log("Password mismatch or email mismatch"); + const errorMsg = "Incorrect email or password"; + console.log("Setting error flash message:", errorMsg); + req.flash(ERROR_KEY, errorMsg); + return done(null, false); } catch (error) { console.error("Login error:", error); log_error(error, req.body); diff --git a/worklenz-backend/src/routes/auth/index.ts b/worklenz-backend/src/routes/auth/index.ts index 1d34fb27..5c57d314 100644 --- a/worklenz-backend/src/routes/auth/index.ts +++ b/worklenz-backend/src/routes/auth/index.ts @@ -17,7 +17,19 @@ const options = (key: string): passport.AuthenticateOptions => ({ successRedirect: `/secure/verify?strategy=${key}` }); -authRouter.post("/login", passport.authenticate("local-login", options("login"))); +// Debug middleware for login +const loginDebugMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => { + console.log("=== LOGIN ROUTE HIT ==="); + console.log("Request method:", req.method); + console.log("Request URL:", req.url); + console.log("Request body:", req.body); + console.log("Content-Type:", req.headers["content-type"]); + console.log("Session ID:", req.sessionID); + console.log("Is authenticated before:", req.isAuthenticated()); + next(); +}; + +authRouter.post("/login", loginDebugMiddleware, passport.authenticate("local-login", options("login"))); authRouter.post("/signup", signUpValidator, passwordValidator, passport.authenticate("local-signup", options("signup"))); authRouter.post("/signup/check", signUpValidator, passwordValidator, safeControllerFunction(AuthController.status_check)); authRouter.get("/verify", AuthController.verify); From 69f50095795d054702cfa02175b58cc3584b8fdb Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 2 Jun 2025 13:20:40 +0530 Subject: [PATCH 3/6] refactor(auth): remove debug logging and enhance session middleware - Eliminated extensive debug logging from the login strategy and verification endpoint to streamline the authentication process. - Updated session middleware to improve cookie handling, enabling proxy support and adjusting session creation behavior. - Ensured secure cookie settings for cross-origin requests in production environments. --- .../src/controllers/auth-controller.ts | 14 -------------- .../src/middlewares/session-middleware.ts | 17 ++++++++++------- .../passport-strategies/passport-local-login.ts | 15 --------------- worklenz-backend/src/routes/auth/index.ts | 14 +------------- 4 files changed, 11 insertions(+), 49 deletions(-) diff --git a/worklenz-backend/src/controllers/auth-controller.ts b/worklenz-backend/src/controllers/auth-controller.ts index b2d24c16..4fea4f59 100644 --- a/worklenz-backend/src/controllers/auth-controller.ts +++ b/worklenz-backend/src/controllers/auth-controller.ts @@ -35,20 +35,6 @@ export default class AuthController extends WorklenzControllerBase { const auth_error = errors.length > 0 ? errors[0] : null; const message = messages.length > 0 ? messages[0] : null; - // Debug logging - console.log("=== VERIFY ENDPOINT HIT ==="); - console.log("Verify endpoint - Strategy:", req.query.strategy); - console.log("Verify endpoint - Authenticated:", req.isAuthenticated()); - console.log("Verify endpoint - User:", !!req.user); - console.log("Verify endpoint - User ID:", req.user?.id); - console.log("Verify endpoint - Auth error:", auth_error); - console.log("Verify endpoint - Success message:", message); - console.log("Verify endpoint - Flash errors:", errors); - console.log("Verify endpoint - Flash messages:", messages); - console.log("Verify endpoint - Session ID:", req.sessionID); - console.log("Verify endpoint - Session passport:", (req.session as any).passport); - console.log("Verify endpoint - Session flash:", (req.session as any).flash); - // Determine title based on authentication status and strategy let title = null; if (req.query.strategy) { diff --git a/worklenz-backend/src/middlewares/session-middleware.ts b/worklenz-backend/src/middlewares/session-middleware.ts index cb6cd624..a0452bee 100644 --- a/worklenz-backend/src/middlewares/session-middleware.ts +++ b/worklenz-backend/src/middlewares/session-middleware.ts @@ -5,12 +5,15 @@ import { isProduction } from "../shared/utils"; // eslint-disable-next-line @typescript-eslint/no-var-requires const pgSession = require("connect-pg-simple")(session); +// For cross-origin requests, we need special cookie settings +const isHttps = process.env.NODE_ENV === "production" || process.env.FORCE_HTTPS === "true"; + export default session({ - name: process.env.SESSION_NAME, + name: process.env.SESSION_NAME || "worklenz.sid", secret: process.env.SESSION_SECRET || "development-secret-key", - proxy: false, + proxy: true, // Enable proxy support for proper session handling resave: false, - saveUninitialized: true, + saveUninitialized: false, // Changed to false to prevent unnecessary session creation rolling: true, store: new pgSession({ pool: db.pool, @@ -18,10 +21,10 @@ export default session({ }), cookie: { path: "/", - // secure: isProduction(), - // httpOnly: isProduction(), - // sameSite: "none", - // domain: isProduction() ? ".worklenz.com" : undefined, + secure: isHttps, // Only secure in production with HTTPS + httpOnly: true, // Enable httpOnly for security + sameSite: isHttps ? "none" : false, // Use "none" for HTTPS cross-origin, disable for HTTP + domain: undefined, // Don't set domain for cross-origin requests maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days } }); \ No newline at end of file diff --git a/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts b/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts index f399b326..d71c4a36 100644 --- a/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts +++ b/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts @@ -6,18 +6,11 @@ import { Request } from "express"; import { ERROR_KEY, SUCCESS_KEY } from "./passport-constants"; async function handleLogin(req: Request, email: string, password: string, done: any) { - console.log("=== LOGIN STRATEGY STARTED ==="); - console.log("Login attempt for:", email); - console.log("Password provided:", !!password); - console.log("Request body:", req.body); - // Clear any existing flash messages (req.session as any).flash = {}; if (!email || !password) { - console.log("Missing credentials - email:", !!email, "password:", !!password); const errorMsg = "Please enter both email and password"; - console.log("Setting error flash message:", errorMsg); req.flash(ERROR_KEY, errorMsg); return done(null, false); } @@ -29,33 +22,25 @@ async function handleLogin(req: Request, email: string, password: string, done: AND google_id IS NULL AND is_deleted IS FALSE;`; const result = await db.query(q, [email]); - console.log("User query result count:", result.rowCount); const [data] = result.rows; if (!data?.password) { - console.log("No account found for email:", email); const errorMsg = "No account found with this email"; - console.log("Setting error flash message:", errorMsg); req.flash(ERROR_KEY, errorMsg); return done(null, false); } const passwordMatch = bcrypt.compareSync(password, data.password); - console.log("Password match result:", passwordMatch); if (passwordMatch && email === data.email) { delete data.password; - console.log("Login successful for user:", data.id); const successMsg = "User successfully logged in"; - console.log("Setting success flash message:", successMsg); req.flash(SUCCESS_KEY, successMsg); return done(null, data); } - console.log("Password mismatch or email mismatch"); const errorMsg = "Incorrect email or password"; - console.log("Setting error flash message:", errorMsg); req.flash(ERROR_KEY, errorMsg); return done(null, false); } catch (error) { diff --git a/worklenz-backend/src/routes/auth/index.ts b/worklenz-backend/src/routes/auth/index.ts index 5c57d314..1d34fb27 100644 --- a/worklenz-backend/src/routes/auth/index.ts +++ b/worklenz-backend/src/routes/auth/index.ts @@ -17,19 +17,7 @@ const options = (key: string): passport.AuthenticateOptions => ({ successRedirect: `/secure/verify?strategy=${key}` }); -// Debug middleware for login -const loginDebugMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => { - console.log("=== LOGIN ROUTE HIT ==="); - console.log("Request method:", req.method); - console.log("Request URL:", req.url); - console.log("Request body:", req.body); - console.log("Content-Type:", req.headers["content-type"]); - console.log("Session ID:", req.sessionID); - console.log("Is authenticated before:", req.isAuthenticated()); - next(); -}; - -authRouter.post("/login", loginDebugMiddleware, passport.authenticate("local-login", options("login"))); +authRouter.post("/login", passport.authenticate("local-login", options("login"))); authRouter.post("/signup", signUpValidator, passwordValidator, passport.authenticate("local-signup", options("signup"))); authRouter.post("/signup/check", signUpValidator, passwordValidator, safeControllerFunction(AuthController.status_check)); authRouter.get("/verify", AuthController.verify); From cfa0af24aeb99cfaee0028b1250cb0d91f3bd397 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 2 Jun 2025 13:29:05 +0530 Subject: [PATCH 4/6] refactor(session-middleware): improve cookie handling and security settings - Updated session middleware to use secure cookies in production environments. - Adjusted sameSite attribute to "lax" for standard handling of same-origin requests. - Removed unnecessary comments and streamlined cookie settings for clarity. --- .../src/middlewares/session-middleware.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/worklenz-backend/src/middlewares/session-middleware.ts b/worklenz-backend/src/middlewares/session-middleware.ts index a0452bee..fea60018 100644 --- a/worklenz-backend/src/middlewares/session-middleware.ts +++ b/worklenz-backend/src/middlewares/session-middleware.ts @@ -5,15 +5,12 @@ import { isProduction } from "../shared/utils"; // eslint-disable-next-line @typescript-eslint/no-var-requires const pgSession = require("connect-pg-simple")(session); -// For cross-origin requests, we need special cookie settings -const isHttps = process.env.NODE_ENV === "production" || process.env.FORCE_HTTPS === "true"; - export default session({ name: process.env.SESSION_NAME || "worklenz.sid", secret: process.env.SESSION_SECRET || "development-secret-key", - proxy: true, // Enable proxy support for proper session handling + proxy: true, resave: false, - saveUninitialized: false, // Changed to false to prevent unnecessary session creation + saveUninitialized: false, rolling: true, store: new pgSession({ pool: db.pool, @@ -21,10 +18,9 @@ export default session({ }), cookie: { path: "/", - secure: isHttps, // Only secure in production with HTTPS - httpOnly: true, // Enable httpOnly for security - sameSite: isHttps ? "none" : false, // Use "none" for HTTPS cross-origin, disable for HTTP - domain: undefined, // Don't set domain for cross-origin requests + secure: isProduction(), // Use secure cookies in production + httpOnly: true, + sameSite: "lax", // Standard setting for same-origin requests maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days } }); \ No newline at end of file From bd7773393503e72f1617d8b3489fb9d76001f276 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Thu, 5 Jun 2025 11:11:16 +0530 Subject: [PATCH 5/6] feat(timers): add running timers feature to the navbar - Introduced a new `TimerButton` component to display and manage running timers. - Implemented API service method `getRunningTimers` to fetch active timers. - Updated the navbar to replace the HelpButton with the TimerButton for better functionality. - Enhanced timer display with real-time updates and socket event handling for timer start/stop actions. --- .../api/tasks/task-time-logs.api.service.ts | 15 + .../src/features/navbar/navbar.tsx | 5 +- .../features/navbar/timers/timer-button.tsx | 275 ++++++++++++++++++ 3 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 worklenz-frontend/src/features/navbar/timers/timer-button.tsx diff --git a/worklenz-frontend/src/api/tasks/task-time-logs.api.service.ts b/worklenz-frontend/src/api/tasks/task-time-logs.api.service.ts index f4565837..37673590 100644 --- a/worklenz-frontend/src/api/tasks/task-time-logs.api.service.ts +++ b/worklenz-frontend/src/api/tasks/task-time-logs.api.service.ts @@ -5,6 +5,16 @@ import { ITaskLogViewModel } from "@/types/tasks/task-log-view.types"; const rootUrl = `${API_BASE_URL}/task-time-log`; +export interface IRunningTimer { + task_id: string; + start_time: string; + task_name: string; + project_id: string; + project_name: string; + parent_task_id?: string; + parent_task_name?: string; +} + export const taskTimeLogsApiService = { getByTask: async (id: string) : Promise> => { const response = await apiClient.get(`${rootUrl}/task/${id}`); @@ -26,6 +36,11 @@ export const taskTimeLogsApiService = { return response.data; }, + getRunningTimers: async (): Promise> => { + const response = await apiClient.get(`${rootUrl}/running-timers`); + return response.data; + }, + exportToExcel(taskId: string) { window.location.href = `${import.meta.env.VITE_API_URL}${API_BASE_URL}/task-time-log/export/${taskId}`; }, diff --git a/worklenz-frontend/src/features/navbar/navbar.tsx b/worklenz-frontend/src/features/navbar/navbar.tsx index 41d7c0e7..430318d3 100644 --- a/worklenz-frontend/src/features/navbar/navbar.tsx +++ b/worklenz-frontend/src/features/navbar/navbar.tsx @@ -5,7 +5,6 @@ import { Col, ConfigProvider, Flex, Menu, MenuProps, Alert } from 'antd'; import { createPortal } from 'react-dom'; import InviteTeamMembers from '../../components/common/invite-team-members/invite-team-members'; -import HelpButton from './help/HelpButton'; import InviteButton from './invite/InviteButton'; import MobileMenuButton from './mobileMenu/MobileMenuButton'; import NavbarLogo from './navbar-logo'; @@ -22,6 +21,7 @@ import { useAuthService } from '@/hooks/useAuth'; import { authApiService } from '@/api/auth/auth.api.service'; import { ISUBSCRIPTION_TYPE } from '@/shared/constants'; import logger from '@/utils/errorLogger'; +import TimerButton from './timers/timer-button'; const Navbar = () => { const [current, setCurrent] = useState('home'); @@ -90,6 +90,7 @@ const Navbar = () => { }, [location]); return ( + { - + diff --git a/worklenz-frontend/src/features/navbar/timers/timer-button.tsx b/worklenz-frontend/src/features/navbar/timers/timer-button.tsx new file mode 100644 index 00000000..c4a229e8 --- /dev/null +++ b/worklenz-frontend/src/features/navbar/timers/timer-button.tsx @@ -0,0 +1,275 @@ +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 { t } = useTranslation('navbar'); + const { token } = useToken(); + const dispatch = useAppDispatch(); + const { socket } = useSocket(); + + const fetchRunningTimers = useCallback(async () => { + try { + setLoading(true); + const response = await taskTimeLogsApiService.getRunningTimers(); + if (response.done) { + setRunningTimers(response.body || []); + } + } catch (error) { + console.error('Error fetching running timers:', error); + } 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); + }; + + 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]); + + // Listen for timer start/stop events and project updates to refresh the count + useEffect(() => { + if (!socket) return; + + const handleTimerStart = (data: string) => { + try { + const { id } = typeof data === 'string' ? JSON.parse(data) : data; + if (id) { + // Refresh the running timers list when a new timer is started + fetchRunningTimers(); + } + } catch (error) { + console.error('Error parsing timer start event:', error); + } + }; + + const handleTimerStop = (data: string) => { + try { + const { id } = typeof data === 'string' ? JSON.parse(data) : data; + if (id) { + // Refresh the running timers list when a timer is stopped + fetchRunningTimers(); + } + } catch (error) { + console.error('Error parsing timer stop event:', error); + } + }; + + const handleProjectUpdates = () => { + // Refresh timers when project updates are available + fetchRunningTimers(); + }; + + 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); + }; + }, [socket, fetchRunningTimers]); + + const hasRunningTimers = () => { + return runningTimers.length > 0; + }; + + const timerCount = () => { + return runningTimers.length; + }; + + const handleStopTimer = (taskId: string) => { + if (!socket) return; + + socket.emit(SocketEvents.TASK_TIMER_STOP.toString(), JSON.stringify({ task_id: taskId })); + dispatch(updateTaskTimeTracking({ taskId, timeTracking: null })); + }; + + 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 + +
+ + )} +
+ ); + + return ( + dropdownContent} + trigger={['click']} + placement="bottomRight" + open={dropdownOpen} + onOpenChange={(open) => { + setDropdownOpen(open); + if (open) { + fetchRunningTimers(); + } + }} + > + +