From 75391641fda99a0210db80901536b284d3fe8ffe Mon Sep 17 00:00:00 2001 From: MRNafisiA Date: Fri, 2 May 2025 15:53:27 +0330 Subject: [PATCH 01/12] increase the memory limit to prevent crashing during build time. --- worklenz-frontend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worklenz-frontend/Dockerfile b/worklenz-frontend/Dockerfile index a32f879e..46a87fa7 100644 --- a/worklenz-frontend/Dockerfile +++ b/worklenz-frontend/Dockerfile @@ -12,7 +12,7 @@ COPY . . RUN echo "window.VITE_API_URL='${VITE_API_URL:-http://backend:3000}';" > ./public/env-config.js && \ echo "window.VITE_SOCKET_URL='${VITE_SOCKET_URL:-ws://backend:3000}';" >> ./public/env-config.js -RUN npm run build +RUN NODE_OPTIONS="--max-old-space-size=4096" npm run build FROM node:22-alpine AS production From cef4bffd691790e6c42bc19806f09ef6fe8033f9 Mon Sep 17 00:00:00 2001 From: Chamika J <75464293+chamikaJ@users.noreply.github.com> Date: Wed, 21 May 2025 08:28:09 +0530 Subject: [PATCH 02/12] Update README.md updated logo URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6885d95..20dffb21 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Worklenz Logo + Worklenz Logo
Worklenz From 32248f8424a81c5ae71278ccf4dcce2116527161 Mon Sep 17 00:00:00 2001 From: kithmina1999 Date: Wed, 28 May 2025 09:32:32 +0530 Subject: [PATCH 03/12] Update README.md to include video guides for local and remote deployment - Added a section for a video guide on local Docker deployment. - Included a video guide for deploying Worklenz to a remote server. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index a6885d95..1805e6a3 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,7 @@ docker-compose up -d docker-compose down ``` + ## MinIO Integration The project uses MinIO as an S3-compatible object storage service, which provides an open-source alternative to AWS S3 for development and production. @@ -403,6 +404,10 @@ This script generates properly configured environment files for both development - Frontend: http://localhost:5000 - Backend API: http://localhost:3000 (or https://localhost:3000 with SSL) +4. Video Guide + + For a visual walkthrough of the local Docker deployment process, check out our [step-by-step video guide](https://www.youtube.com/watch?v=AfwAKxJbqLg). + ### Remote Server Deployment When deploying to a remote server: @@ -428,6 +433,10 @@ When deploying to a remote server: - Frontend: http://your-server-hostname:5000 - Backend API: http://your-server-hostname:3000 +4. Video Guide + + For a complete walkthrough of deploying Worklenz to a remote server, check out our [deployment video guide](https://www.youtube.com/watch?v=CAZGu2iOXQs&t=10s). + ### Environment Configuration The Docker setup uses environment variables to configure the services: From 102be2c24aea234e27b5ade54fdb94371fa30a0d Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Tue, 27 May 2025 22:40:19 -0400 Subject: [PATCH 04/12] Generate random passwords in update-docker-env.sh --- update-docker-env.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/update-docker-env.sh b/update-docker-env.sh index 77ab1beb..12044bd1 100755 --- a/update-docker-env.sh +++ b/update-docker-env.sh @@ -73,8 +73,8 @@ cat > worklenz-backend/.env << EOL NODE_ENV=production PORT=3000 SESSION_NAME=worklenz.sid -SESSION_SECRET=change_me_in_production -COOKIE_SECRET=change_me_in_production +SESSION_SECRET=$(openssl rand -base64 48) +COOKIE_SECRET=$(openssl rand -base64 48) # CORS SOCKET_IO_CORS=${FRONTEND_URL} @@ -92,7 +92,7 @@ LOGIN_SUCCESS_REDIRECT="${FRONTEND_URL}/auth/authenticating" DB_HOST=db DB_PORT=5432 DB_USER=postgres -DB_PASSWORD=password +DB_PASSWORD=$(openssl rand -base64 48) DB_NAME=worklenz_db DB_MAX_CLIENTS=50 USE_PG_NATIVE=true @@ -123,7 +123,7 @@ SLACK_WEBHOOK= COMMIT_BUILD_IMMEDIATELY=true # JWT Secret -JWT_SECRET=change_me_in_production +JWT_SECRET=$(openssl rand -base64 48) EOL echo "Environment configuration updated for ${HOSTNAME} with" $([ "$USE_SSL" = "true" ] && echo "HTTPS/WSS" || echo "HTTP/WS") @@ -138,4 +138,4 @@ echo "Frontend URL: ${FRONTEND_URL}" echo "API URL: ${HTTP_PREFIX}${HOSTNAME}:3000" echo "Socket URL: ${WS_PREFIX}${HOSTNAME}:3000" echo "MinIO Dashboard URL: ${MINIO_DASHBOARD_URL}" -echo "CORS is configured to allow requests from: ${FRONTEND_URL}" \ No newline at end of file +echo "CORS is configured to allow requests from: ${FRONTEND_URL}" From 24fa837a39ba4fc5e95d22724b4404c9290138ef Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 2 Jun 2025 13:07:50 +0530 Subject: [PATCH 05/12] 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 06/12] 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 07/12] 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 09f44a5685e1b258efe8da9427be75e9d745e3db Mon Sep 17 00:00:00 2001 From: kithmina1999 Date: Thu, 5 Jun 2025 10:40:06 +0530 Subject: [PATCH 08/12] fix: change DB_PASSWORD to static value for development Using a static password simplifies development environment setup. The previous random password generation caused issues during local testing and debugging. --- update-docker-env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update-docker-env.sh b/update-docker-env.sh index 12044bd1..7852e86f 100755 --- a/update-docker-env.sh +++ b/update-docker-env.sh @@ -92,7 +92,7 @@ LOGIN_SUCCESS_REDIRECT="${FRONTEND_URL}/auth/authenticating" DB_HOST=db DB_PORT=5432 DB_USER=postgres -DB_PASSWORD=$(openssl rand -base64 48) +DB_PASSWORD=password DB_NAME=worklenz_db DB_MAX_CLIENTS=50 USE_PG_NATIVE=true From bd7773393503e72f1617d8b3489fb9d76001f276 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Thu, 5 Jun 2025 11:11:16 +0530 Subject: [PATCH 09/12] 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(); + } + }} + > + +