diff --git a/worklenz-backend/src/controllers/user-activity-logs-controller.ts b/worklenz-backend/src/controllers/user-activity-logs-controller.ts new file mode 100644 index 00000000..eeacb4b4 --- /dev/null +++ b/worklenz-backend/src/controllers/user-activity-logs-controller.ts @@ -0,0 +1,117 @@ +import moment from "moment"; +import { IWorkLenzRequest } from "../interfaces/worklenz-request"; +import { IWorkLenzResponse } from "../interfaces/worklenz-response"; + +import db from "../config/db"; + +import { ServerResponse } from "../models/server-response"; +import WorklenzControllerBase from "./worklenz-controller-base"; +import HandleExceptions from "../decorators/handle-exceptions"; +import { formatDuration, formatLogText, getColor } from "../shared/utils"; + +interface IUserRecentTask { + task_id: string; + task_name: string; + project_id: string; + project_name: string; + last_activity_at: string; + activity_count: number; + project_color?: string; + task_status?: string; + status_color?: string; +} + +interface IUserTimeLoggedTask { + task_id: string; + task_name: string; + project_id: string; + project_name: string; + total_time_logged: number; + total_time_logged_string: string; + last_logged_at: string; + logged_by_timer: boolean; + project_color?: string; + task_status?: string; + status_color?: string; + log_entries_count?: number; + estimated_time?: number; +} + +export default class UserActivityLogsController extends WorklenzControllerBase { + @HandleExceptions() + public static async getRecentTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + if (!req.user) { + return res.status(401).send(new ServerResponse(false, null, "Unauthorized")); + } + + const { id: userId, team_id: teamId } = req.user; + const { offset = 0, limit = 10 } = req.query; + + // Optimized query with better performance and team filtering + const q = ` + SELECT DISTINCT tal.task_id, t.name AS task_name, tal.project_id, p.name AS project_name, + MAX(tal.created_at) AS last_activity_at, + COUNT(DISTINCT tal.id) AS activity_count, + p.color_code AS project_color, + (SELECT name FROM task_statuses WHERE id = t.status_id) AS task_status, + (SELECT color_code + FROM sys_task_status_categories + WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color + FROM task_activity_logs tal + INNER JOIN tasks t ON tal.task_id = t.id AND t.archived = FALSE + INNER JOIN projects p ON tal.project_id = p.id AND p.team_id = $1 + WHERE tal.user_id = $2 + AND tal.created_at >= NOW() - INTERVAL '30 days' + GROUP BY tal.task_id, t.name, tal.project_id, p.name, p.color_code, t.status_id + ORDER BY MAX(tal.created_at) DESC + LIMIT $3 OFFSET $4; + `; + + const result = await db.query(q, [teamId, userId, limit, offset]); + const tasks: IUserRecentTask[] = result.rows; + + return res.status(200).send(new ServerResponse(true, tasks)); + } + + @HandleExceptions() + public static async getTimeLoggedTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + if (!req.user) { + return res.status(401).send(new ServerResponse(false, null, "Unauthorized")); + } + + const { id: userId, team_id: teamId } = req.user; + const { offset = 0, limit = 10 } = req.query; + + // Optimized query with better performance, team filtering, and useful additional data + const q = ` + SELECT twl.task_id, t.name AS task_name, t.project_id, p.name AS project_name, + SUM(twl.time_spent) AS total_time_logged, + MAX(twl.created_at) AS last_logged_at, + MAX(twl.logged_by_timer::int)::boolean AS logged_by_timer, + p.color_code AS project_color, + (SELECT name FROM task_statuses WHERE id = t.status_id) AS task_status, + (SELECT color_code + FROM sys_task_status_categories + WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color, + COUNT(DISTINCT twl.id) AS log_entries_count, + (t.total_minutes * 60) AS estimated_time + FROM task_work_log twl + INNER JOIN tasks t ON twl.task_id = t.id AND t.archived = FALSE + INNER JOIN projects p ON t.project_id = p.id AND p.team_id = $1 + WHERE twl.user_id = $2 + AND twl.created_at >= NOW() - INTERVAL '90 days' + GROUP BY twl.task_id, t.name, t.project_id, p.name, p.color_code, t.status_id, t.total_minutes + HAVING SUM(twl.time_spent) > 0 + ORDER BY MAX(twl.created_at) DESC + LIMIT $3 OFFSET $4; + `; + + const result = await db.query(q, [teamId, userId, limit, offset]); + const tasks: IUserTimeLoggedTask[] = result.rows.map(task => ({ + ...task, + total_time_logged_string: formatDuration(moment.duration(task.total_time_logged, "seconds")), + })); + + return res.status(200).send(new ServerResponse(true, tasks)); + } +} diff --git a/worklenz-backend/src/routes/apis/index.ts b/worklenz-backend/src/routes/apis/index.ts index 7bd13eec..a03adf7b 100644 --- a/worklenz-backend/src/routes/apis/index.ts +++ b/worklenz-backend/src/routes/apis/index.ts @@ -1,122 +1,125 @@ -import express from "express"; - -import AccessControlsController from "../../controllers/access-controls-controller"; -import AuthController from "../../controllers/auth-controller"; -import LogsController from "../../controllers/logs-controller"; -import OverviewController from "../../controllers/overview-controller"; -import TaskPrioritiesController from "../../controllers/task-priorities-controller"; - -import attachmentsApiRouter from "./attachments-api-router"; -import clientsApiRouter from "./clients-api-router"; -import jobTitlesApiRouter from "./job-titles-api-router"; -import notificationsApiRouter from "./notifications-api-router"; -import personalOverviewApiRouter from "./personal-overview-api-router"; -import projectMembersApiRouter from "./project-members-api-router"; -import projectsApiRouter from "./projects-api-router"; -import settingsApiRouter from "./settings-api-router"; -import statusesApiRouter from "./statuses-api-router"; -import subTasksApiRouter from "./sub-tasks-api-router"; -import taskCommentsApiRouter from "./task-comments-api-router"; -import taskWorkLogApiRouter from "./task-work-log-api-router"; -import tasksApiRouter from "./tasks-api-router"; -import teamMembersApiRouter from "./team-members-api-router"; -import teamsApiRouter from "./teams-api-router"; -import timezonesApiRouter from "./timezones-api-router"; -import todoListApiRouter from "./todo-list-api-router"; -import projectStatusesApiRouter from "./project-statuses-api-router"; -import labelsApiRouter from "./labels-api-router"; -import sharedProjectsApiRouter from "./shared-projects-api-router"; -import resourceAllocationApiRouter from "./resource-allocation-api-router"; -import taskTemplatesApiRouter from "./task-templates-api-router"; -import projectInsightsApiRouter from "./project-insights-api-router"; -import passwordValidator from "../../middlewares/validators/password-validator"; -import adminCenterApiRouter from "./admin-center-api-router"; -import reportingApiRouter from "./reporting-api-router"; -import activityLogsApiRouter from "./activity-logs-api-router"; -import safeControllerFunction from "../../shared/safe-controller-function"; -import projectFoldersApiRouter from "./project-folders-api-router"; -import taskPhasesApiRouter from "./task-phases-api-router"; -import projectCategoriesApiRouter from "./project-categories-api-router"; -import homePageApiRouter from "./home-page-api-router"; -import ganttApiRouter from "./gantt-api-router"; -import projectCommentsApiRouter from "./project-comments-api-router"; -import reportingExportApiRouter from "./reporting-export-api-router"; -import projectHealthsApiRouter from "./project-healths-api-router"; -import ptTasksApiRouter from "./pt-tasks-api-router"; -import projectTemplatesApiRouter from "./project-templates-api"; -import ptTaskPhasesApiRouter from "./pt_task-phases-api-router"; -import ptStatusesApiRouter from "./pt-statuses-api-router"; -import workloadApiRouter from "./gannt-apis/workload-api-router"; -import roadmapApiRouter from "./gannt-apis/roadmap-api-router"; -import scheduleApiRouter from "./gannt-apis/schedule-api-router"; -import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router"; -import projectManagerApiRouter from "./project-managers-api-router"; -import surveyApiRouter from "./survey-api-router"; - -import billingApiRouter from "./billing-api-router"; -import taskDependenciesApiRouter from "./task-dependencies-api-router"; - -import taskRecurringApiRouter from "./task-recurring-api-router"; - import customColumnsApiRouter from "./custom-columns-api-router"; - -const api = express.Router(); - -api.use("/projects", projectsApiRouter); -api.use("/team-members", teamMembersApiRouter); -api.use("/job-titles", jobTitlesApiRouter); -api.use("/clients", clientsApiRouter); -api.use("/teams", teamsApiRouter); -api.use("/tasks", tasksApiRouter); -api.use("/settings", settingsApiRouter); -api.use("/personal-overview", personalOverviewApiRouter); -api.use("/statuses", statusesApiRouter); -api.use("/todo-list", todoListApiRouter); -api.use("/notifications", notificationsApiRouter); -api.use("/attachments", attachmentsApiRouter); -api.use("/sub-tasks", subTasksApiRouter); -api.use("/project-members", projectMembersApiRouter); -api.use("/task-time-log", taskWorkLogApiRouter); -api.use("/task-comments", taskCommentsApiRouter); -api.use("/timezones", timezonesApiRouter); -api.use("/project-statuses", projectStatusesApiRouter); -api.use("/labels", labelsApiRouter); -api.use("/resource-allocation", resourceAllocationApiRouter); -api.use("/shared/projects", sharedProjectsApiRouter); -api.use("/task-templates", taskTemplatesApiRouter); -api.use("/project-insights", projectInsightsApiRouter); -api.use("/admin-center", adminCenterApiRouter); -api.use("/reporting", reportingApiRouter); -api.use("/activity-logs", activityLogsApiRouter); -api.use("/projects-folders", projectFoldersApiRouter); -api.use("/task-phases", taskPhasesApiRouter); -api.use("/project-categories", projectCategoriesApiRouter); -api.use("/home", homePageApiRouter); -api.use("/gantt", ganttApiRouter); -api.use("/project-comments", projectCommentsApiRouter); -api.use("/reporting-export", reportingExportApiRouter); -api.use("/project-healths", projectHealthsApiRouter); -api.use("/project-templates", projectTemplatesApiRouter); -api.use("/pt-tasks", ptTasksApiRouter); -api.use("/pt-task-phases", ptTaskPhasesApiRouter); -api.use("/pt-statuses", ptStatusesApiRouter); -api.use("/workload-gannt", workloadApiRouter); -api.use("/roadmap-gannt", roadmapApiRouter); -api.use("/schedule-gannt", scheduleApiRouter); -api.use("/schedule-gannt-v2", scheduleApiV2Router); -api.use("/project-managers", projectManagerApiRouter); -api.use("/surveys", surveyApiRouter); - -api.get("/overview/:id", safeControllerFunction(OverviewController.getById)); -api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get)); -api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword)); -api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles)); -api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog)); - -api.use("/billing", billingApiRouter); -api.use("/task-dependencies", taskDependenciesApiRouter); - -api.use("/task-recurring", taskRecurringApiRouter); - -api.use("/custom-columns", customColumnsApiRouter); - -export default api; +import express from "express"; + +import AccessControlsController from "../../controllers/access-controls-controller"; +import AuthController from "../../controllers/auth-controller"; +import LogsController from "../../controllers/logs-controller"; +import OverviewController from "../../controllers/overview-controller"; +import TaskPrioritiesController from "../../controllers/task-priorities-controller"; + +import attachmentsApiRouter from "./attachments-api-router"; +import clientsApiRouter from "./clients-api-router"; +import jobTitlesApiRouter from "./job-titles-api-router"; +import notificationsApiRouter from "./notifications-api-router"; +import personalOverviewApiRouter from "./personal-overview-api-router"; +import projectMembersApiRouter from "./project-members-api-router"; +import projectsApiRouter from "./projects-api-router"; +import settingsApiRouter from "./settings-api-router"; +import statusesApiRouter from "./statuses-api-router"; +import subTasksApiRouter from "./sub-tasks-api-router"; +import taskCommentsApiRouter from "./task-comments-api-router"; +import taskWorkLogApiRouter from "./task-work-log-api-router"; +import tasksApiRouter from "./tasks-api-router"; +import teamMembersApiRouter from "./team-members-api-router"; +import teamsApiRouter from "./teams-api-router"; +import timezonesApiRouter from "./timezones-api-router"; +import todoListApiRouter from "./todo-list-api-router"; +import projectStatusesApiRouter from "./project-statuses-api-router"; +import labelsApiRouter from "./labels-api-router"; +import sharedProjectsApiRouter from "./shared-projects-api-router"; +import resourceAllocationApiRouter from "./resource-allocation-api-router"; +import taskTemplatesApiRouter from "./task-templates-api-router"; +import projectInsightsApiRouter from "./project-insights-api-router"; +import passwordValidator from "../../middlewares/validators/password-validator"; +import adminCenterApiRouter from "./admin-center-api-router"; +import reportingApiRouter from "./reporting-api-router"; +import activityLogsApiRouter from "./activity-logs-api-router"; +import safeControllerFunction from "../../shared/safe-controller-function"; +import projectFoldersApiRouter from "./project-folders-api-router"; +import taskPhasesApiRouter from "./task-phases-api-router"; +import projectCategoriesApiRouter from "./project-categories-api-router"; +import homePageApiRouter from "./home-page-api-router"; +import ganttApiRouter from "./gantt-api-router"; +import projectCommentsApiRouter from "./project-comments-api-router"; +import reportingExportApiRouter from "./reporting-export-api-router"; +import projectHealthsApiRouter from "./project-healths-api-router"; +import ptTasksApiRouter from "./pt-tasks-api-router"; +import projectTemplatesApiRouter from "./project-templates-api"; +import ptTaskPhasesApiRouter from "./pt_task-phases-api-router"; +import ptStatusesApiRouter from "./pt-statuses-api-router"; +import workloadApiRouter from "./gannt-apis/workload-api-router"; +import roadmapApiRouter from "./gannt-apis/roadmap-api-router"; +import scheduleApiRouter from "./gannt-apis/schedule-api-router"; +import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router"; +import projectManagerApiRouter from "./project-managers-api-router"; +import surveyApiRouter from "./survey-api-router"; + +import billingApiRouter from "./billing-api-router"; +import taskDependenciesApiRouter from "./task-dependencies-api-router"; + +import taskRecurringApiRouter from "./task-recurring-api-router"; + +import customColumnsApiRouter from "./custom-columns-api-router"; +import userActivityLogsApiRouter from "./user-activity-logs-api-router"; + +const api = express.Router(); + +api.use("/projects", projectsApiRouter); +api.use("/team-members", teamMembersApiRouter); +api.use("/job-titles", jobTitlesApiRouter); +api.use("/clients", clientsApiRouter); +api.use("/teams", teamsApiRouter); +api.use("/tasks", tasksApiRouter); +api.use("/settings", settingsApiRouter); +api.use("/personal-overview", personalOverviewApiRouter); +api.use("/statuses", statusesApiRouter); +api.use("/todo-list", todoListApiRouter); +api.use("/notifications", notificationsApiRouter); +api.use("/attachments", attachmentsApiRouter); +api.use("/sub-tasks", subTasksApiRouter); +api.use("/project-members", projectMembersApiRouter); +api.use("/task-time-log", taskWorkLogApiRouter); +api.use("/task-comments", taskCommentsApiRouter); +api.use("/timezones", timezonesApiRouter); +api.use("/project-statuses", projectStatusesApiRouter); +api.use("/labels", labelsApiRouter); +api.use("/resource-allocation", resourceAllocationApiRouter); +api.use("/shared/projects", sharedProjectsApiRouter); +api.use("/task-templates", taskTemplatesApiRouter); +api.use("/project-insights", projectInsightsApiRouter); +api.use("/admin-center", adminCenterApiRouter); +api.use("/reporting", reportingApiRouter); +api.use("/activity-logs", activityLogsApiRouter); +api.use("/projects-folders", projectFoldersApiRouter); +api.use("/task-phases", taskPhasesApiRouter); +api.use("/project-categories", projectCategoriesApiRouter); +api.use("/home", homePageApiRouter); +api.use("/gantt", ganttApiRouter); +api.use("/project-comments", projectCommentsApiRouter); +api.use("/reporting-export", reportingExportApiRouter); +api.use("/project-healths", projectHealthsApiRouter); +api.use("/project-templates", projectTemplatesApiRouter); +api.use("/pt-tasks", ptTasksApiRouter); +api.use("/pt-task-phases", ptTaskPhasesApiRouter); +api.use("/pt-statuses", ptStatusesApiRouter); +api.use("/workload-gannt", workloadApiRouter); +api.use("/roadmap-gannt", roadmapApiRouter); +api.use("/schedule-gannt", scheduleApiRouter); +api.use("/schedule-gannt-v2", scheduleApiV2Router); +api.use("/project-managers", projectManagerApiRouter); +api.use("/surveys", surveyApiRouter); + +api.get("/overview/:id", safeControllerFunction(OverviewController.getById)); +api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get)); +api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword)); +api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles)); +api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog)); + +api.use("/billing", billingApiRouter); +api.use("/task-dependencies", taskDependenciesApiRouter); + +api.use("/task-recurring", taskRecurringApiRouter); + +api.use("/custom-columns", customColumnsApiRouter); + +api.use("/logs", userActivityLogsApiRouter); +export default api; diff --git a/worklenz-backend/src/routes/apis/user-activity-logs-api-router.ts b/worklenz-backend/src/routes/apis/user-activity-logs-api-router.ts new file mode 100644 index 00000000..b6f07930 --- /dev/null +++ b/worklenz-backend/src/routes/apis/user-activity-logs-api-router.ts @@ -0,0 +1,11 @@ +import express from 'express'; + +import UserActivityLogsController from '../../controllers/user-activity-logs-controller'; +import safeControllerFunction from "../../shared/safe-controller-function"; + +const userActivityLogsApiRouter = express.Router(); + +userActivityLogsApiRouter.get('/user-recent-tasks', safeControllerFunction(UserActivityLogsController.getRecentTasks)); +userActivityLogsApiRouter.get('/user-time-logged-tasks', safeControllerFunction(UserActivityLogsController.getTimeLoggedTasks)); + +export default userActivityLogsApiRouter; \ No newline at end of file diff --git a/worklenz-frontend/public/locales/alb/home.json b/worklenz-frontend/public/locales/alb/home.json index 58d26e0b..9dd14e4e 100644 --- a/worklenz-frontend/public/locales/alb/home.json +++ b/worklenz-frontend/public/locales/alb/home.json @@ -41,6 +41,18 @@ "list": "Listë", "calendar": "Kalendar", "tasks": "Detyrat", - "refresh": "Rifresko" + "refresh": "Rifresko", + "recentActivity": "Aktiviteti i Fundit", + "recentTasks": "Detyrat e Fundit", + "timeLoggedTasks": "Koha e Regjistruar", + "noRecentTasks": "Asnjë detyrë e fundit", + "noTimeLoggedTasks": "Asnjë detyrë me kohë të regjistruar", + "activityTag": "Aktiviteti", + "timeLogTag": "Regjistrim Kohe", + "timerTag": "Kohëmatës", + "activitySingular": "aktivitet", + "activityPlural": "aktivitete", + "recentTaskAriaLabel": "Detyrë e fundit:", + "timeLoggedTaskAriaLabel": "Detyrë me kohë të regjistruar:" } } diff --git a/worklenz-frontend/public/locales/de/home.json b/worklenz-frontend/public/locales/de/home.json index cc868952..03381614 100644 --- a/worklenz-frontend/public/locales/de/home.json +++ b/worklenz-frontend/public/locales/de/home.json @@ -41,6 +41,18 @@ "list": "Liste", "calendar": "Kalender", "tasks": "Aufgaben", - "refresh": "Aktualisieren" + "refresh": "Aktualisieren", + "recentActivity": "Aktuelle Aktivitäten", + "recentTasks": "Aktuelle Aufgaben", + "timeLoggedTasks": "Erfasste Zeit", + "noRecentTasks": "Keine aktuellen Aufgaben", + "noTimeLoggedTasks": "Keine Aufgaben mit erfasster Zeit", + "activityTag": "Aktivität", + "timeLogTag": "Zeiterfassung", + "timerTag": "Timer", + "activitySingular": "Aktivität", + "activityPlural": "Aktivitäten", + "recentTaskAriaLabel": "Aktuelle Aufgabe:", + "timeLoggedTaskAriaLabel": "Aufgabe mit erfasster Zeit:" } } diff --git a/worklenz-frontend/public/locales/en/home.json b/worklenz-frontend/public/locales/en/home.json index ccf40936..290316f0 100644 --- a/worklenz-frontend/public/locales/en/home.json +++ b/worklenz-frontend/public/locales/en/home.json @@ -41,6 +41,25 @@ "list": "List", "calendar": "Calendar", "tasks": "Tasks", - "refresh": "Refresh" + "refresh": "Refresh", + "recentActivity": "Recent Activity", + "recentTasks": "Recent Tasks", + "timeLoggedTasks": "Time Logged", + "noRecentTasks": "No recent tasks", + "noTimeLoggedTasks": "No time logged tasks", + "activityTag": "Activity", + "timeLogTag": "Time Log", + "timerTag": "Timer", + "activitySingular": "activity", + "activityPlural": "activities", + "recentTaskAriaLabel": "Recent task:", + "timeLoggedTaskAriaLabel": "Time logged task:", + "Recent Activity": "Recent Activity", + "Recent Tasks": "Recent Tasks", + "Time Logged Tasks": "Time Logged Tasks", + "Error Loading Recent Tasks": "Error loading recent tasks", + "Error Loading Time Logged Tasks": "Error loading time logged tasks", + "No Recent Tasks": "No recent tasks", + "No Time Logged Tasks": "No time logged tasks" } } diff --git a/worklenz-frontend/public/locales/es/home.json b/worklenz-frontend/public/locales/es/home.json index cfd238f9..63403089 100644 --- a/worklenz-frontend/public/locales/es/home.json +++ b/worklenz-frontend/public/locales/es/home.json @@ -40,6 +40,18 @@ "list": "Lista", "calendar": "Calendario", "tasks": "Tareas", - "refresh": "Actualizar" + "refresh": "Actualizar", + "recentActivity": "Actividad Reciente", + "recentTasks": "Tareas Recientes", + "timeLoggedTasks": "Tiempo Registrado", + "noRecentTasks": "No hay tareas recientes", + "noTimeLoggedTasks": "No hay tareas con tiempo registrado", + "activityTag": "Actividad", + "timeLogTag": "Registro de Tiempo", + "timerTag": "Temporizador", + "activitySingular": "actividad", + "activityPlural": "actividades", + "recentTaskAriaLabel": "Tarea reciente:", + "timeLoggedTaskAriaLabel": "Tarea con tiempo registrado:" } } diff --git a/worklenz-frontend/public/locales/pt/home.json b/worklenz-frontend/public/locales/pt/home.json index b19ece5f..9a84464c 100644 --- a/worklenz-frontend/public/locales/pt/home.json +++ b/worklenz-frontend/public/locales/pt/home.json @@ -40,6 +40,18 @@ "list": "Lista", "calendar": "Calendário", "tasks": "Tarefas", - "refresh": "Atualizar" + "refresh": "Atualizar", + "recentActivity": "Atividade Recente", + "recentTasks": "Tarefas Recentes", + "timeLoggedTasks": "Tempo Registrado", + "noRecentTasks": "Nenhuma tarefa recente", + "noTimeLoggedTasks": "Nenhuma tarefa com tempo registrado", + "activityTag": "Atividade", + "timeLogTag": "Registro de Tempo", + "timerTag": "Cronômetro", + "activitySingular": "atividade", + "activityPlural": "atividades", + "recentTaskAriaLabel": "Tarefa recente:", + "timeLoggedTaskAriaLabel": "Tarefa com tempo registrado:" } } diff --git a/worklenz-frontend/public/locales/zh/home.json b/worklenz-frontend/public/locales/zh/home.json index 184b4f1a..c50e37ca 100644 --- a/worklenz-frontend/public/locales/zh/home.json +++ b/worklenz-frontend/public/locales/zh/home.json @@ -41,6 +41,18 @@ "list": "列表", "calendar": "日历", "tasks": "任务", - "refresh": "刷新" + "refresh": "刷新", + "recentActivity": "最近活动", + "recentTasks": "最近任务", + "timeLoggedTasks": "时间记录", + "noRecentTasks": "没有最近任务", + "noTimeLoggedTasks": "没有时间记录任务", + "activityTag": "活动", + "timeLogTag": "时间记录", + "timerTag": "计时器", + "activitySingular": "活动", + "activityPlural": "活动", + "recentTaskAriaLabel": "最近任务:", + "timeLoggedTaskAriaLabel": "时间记录任务:" } } \ No newline at end of file diff --git a/worklenz-frontend/src/api/home-page/user-activity.api.service.ts b/worklenz-frontend/src/api/home-page/user-activity.api.service.ts new file mode 100644 index 00000000..f37da22a --- /dev/null +++ b/worklenz-frontend/src/api/home-page/user-activity.api.service.ts @@ -0,0 +1,46 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { API_BASE_URL } from '@/shared/constants'; +import { getCsrfToken } from '../api-client'; +import { IUserRecentTask, IUserTimeLoggedTask } from '@/types/home/user-activity.types'; +import config from '@/config/env'; + +const rootUrl = '/logs'; + +export const userActivityApiService = createApi({ + reducerPath: 'userActivityApi', + baseQuery: fetchBaseQuery({ + baseUrl: `${config.apiUrl}${API_BASE_URL}`, + prepareHeaders: (headers) => { + headers.set('X-CSRF-Token', getCsrfToken() || ''); + headers.set('Content-Type', 'application/json'); + return headers; + }, + credentials: 'include', + }), + tagTypes: ['UserRecentTasks', 'UserTimeLoggedTasks'], + endpoints: (builder) => ({ + getUserRecentTasks: builder.query({ + query: ({ limit = 10, offset = 0 }) => ({ + url: `${rootUrl}/user-recent-tasks`, + params: { limit, offset }, + method: 'GET', + }), + providesTags: ['UserRecentTasks'], + }), + getUserTimeLoggedTasks: builder.query({ + query: ({ limit = 10, offset = 0 }) => ({ + url: `${rootUrl}/user-time-logged-tasks`, + params: { limit, offset }, + method: 'GET', + }), + providesTags: ['UserTimeLoggedTasks'], + }), + }), +}); + +export const { + useGetUserRecentTasksQuery, + useGetUserTimeLoggedTasksQuery, +} = userActivityApiService; + + diff --git a/worklenz-frontend/src/app/store.ts b/worklenz-frontend/src/app/store.ts index 63c738a0..4190f3a9 100644 --- a/worklenz-frontend/src/app/store.ts +++ b/worklenz-frontend/src/app/store.ts @@ -7,6 +7,7 @@ import userReducer from '@features/user/userSlice'; // Home Page import homePageReducer from '@features/home-page/home-page.slice'; +import userActivityReducer from '@features/home-page/user-activity.slice'; // Account Setup import accountSetupReducer from '@features/account-setup/account-setup.slice'; @@ -82,6 +83,7 @@ import groupingReducer from '@/features/task-management/grouping.slice'; import selectionReducer from '@/features/task-management/selection.slice'; import homePageApiService from '@/api/home-page/home-page.api.service'; import { projectsApi } from '@/api/projects/projects.v1.api.service'; +import { userActivityApiService } from '@/api/home-page/user-activity.api.service'; import projectViewReducer from '@features/project/project-view-slice'; import taskManagementFieldsReducer from '@features/task-management/taskListFields.slice'; @@ -90,7 +92,7 @@ export const store = configureStore({ middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false, - }).concat(homePageApiService.middleware, projectsApi.middleware), + }).concat(homePageApiService.middleware, projectsApi.middleware, userActivityApiService.middleware), reducer: { // Auth & User auth: authReducer, @@ -103,6 +105,9 @@ export const store = configureStore({ homePageReducer: homePageReducer, [homePageApiService.reducerPath]: homePageApiService.reducer, [projectsApi.reducerPath]: projectsApi.reducer, + userActivityReducer: userActivityReducer, + [userActivityApiService.reducerPath]: userActivityApiService.reducer, + // Core UI themeReducer: themeReducer, localesReducer: localesReducer, diff --git a/worklenz-frontend/src/features/home-page/user-activity.slice.ts b/worklenz-frontend/src/features/home-page/user-activity.slice.ts new file mode 100644 index 00000000..bfe207f2 --- /dev/null +++ b/worklenz-frontend/src/features/home-page/user-activity.slice.ts @@ -0,0 +1,59 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { ActivityFeedType } from '@/types/home/user-activity.types'; + +interface ActivityItem { + id: string; + type: string; + description: string; + timestamp: string; +} + +interface UserActivityState { + activeTab: ActivityFeedType; + activities: ActivityItem[]; + loading: boolean; + error: string | null; +} + +const initialState: UserActivityState = { + activeTab: ActivityFeedType.RECENT_TASKS, + activities: [], + loading: false, + error: null, +}; + +const userActivitySlice = createSlice({ + name: 'userActivity', + initialState, + reducers: { + setActiveTab(state, action: PayloadAction) { + state.activeTab = action.payload; + }, + fetchActivitiesStart(state) { + state.loading = true; + state.error = null; + }, + fetchActivitiesSuccess(state, action: PayloadAction) { + state.activities = action.payload; + state.loading = false; + state.error = null; + }, + fetchActivitiesFailure(state, action: PayloadAction) { + state.loading = false; + state.error = action.payload; + }, + clearActivities(state) { + state.activities = []; + }, + }, +}); + +export const { + setActiveTab, + fetchActivitiesStart, + fetchActivitiesSuccess, + fetchActivitiesFailure, + clearActivities, +} = userActivitySlice.actions; + +export default userActivitySlice.reducer; \ No newline at end of file diff --git a/worklenz-frontend/src/pages/home/home-page.tsx b/worklenz-frontend/src/pages/home/home-page.tsx index fc3874dd..0e47a4b1 100644 --- a/worklenz-frontend/src/pages/home/home-page.tsx +++ b/worklenz-frontend/src/pages/home/home-page.tsx @@ -1,14 +1,16 @@ -import { useEffect, memo, useMemo, useCallback } from 'react'; +import React, { useEffect, memo, useMemo, useCallback } from 'react'; import { useMediaQuery } from 'react-responsive'; import Col from 'antd/es/col'; import Flex from 'antd/es/flex'; +import Row from 'antd/es/row'; +import Card from 'antd/es/card'; import GreetingWithTime from './greeting-with-time'; import TasksList from '@/pages/home/task-list/tasks-list'; -import TodoList from '@/pages/home/todo-list/todo-list'; import ProjectDrawer from '@/components/projects/project-drawer/project-drawer'; import CreateProjectButton from '@/components/projects/project-create-button/project-create-button'; import RecentAndFavouriteProjectList from '@/pages/home/recent-and-favourite-project-list/recent-and-favourite-project-list'; +import TodoList from './todo-list/todo-list'; import { useDocumentTitle } from '@/hooks/useDoumentTItle'; import { useAppDispatch } from '@/hooks/useAppDispatch'; @@ -19,7 +21,7 @@ import { fetchProjectCategories } from '@/features/projects/lookups/projectCateg import { fetchProjectHealth } from '@/features/projects/lookups/projectHealth/projectHealthSlice'; import { fetchProjects } from '@/features/home-page/home-page.slice'; import { createPortal } from 'react-dom'; -import React, { Suspense } from 'react'; +import UserActivityFeed from './user-activity-feed/user-activity-feed'; const DESKTOP_MIN_WIDTH = 1024; const TASK_LIST_MIN_WIDTH = 500; @@ -45,7 +47,7 @@ const HomePage = memo(() => { console.warn('Failed to preload TaskDrawer:', error); } }; - + preloadTaskDrawer(); }, []); @@ -98,26 +100,6 @@ const HomePage = memo(() => { ); }, [isDesktop, isOwnerOrAdmin]); - const MainContent = useMemo(() => { - return isDesktop ? ( - - - - - - - - - - ) : ( - - - - - - ); - }, [isDesktop, desktopFlexStyle, sidebarFlexStyle]); - return (
@@ -125,29 +107,26 @@ const HomePage = memo(() => { {CreateProjectButtonComponent} - {MainContent} + + + + - {/* Use Suspense for lazy-loaded components with error boundary */} - Loading...
}> - {createPortal( - - - , - document.body, - 'home-task-drawer' - )} - + + + - {createPortal( - , - document.body, - 'project-drawer' - )} + - {/* Survey Modal - only shown to users who haven't completed it */} - - - + + + + + + + + {createPortal(, document.body, 'home-task-drawer')} + {createPortal( {}} />, document.body, 'project-drawer')} ); }); diff --git a/worklenz-frontend/src/pages/home/user-activity-feed/task-activity-list.tsx b/worklenz-frontend/src/pages/home/user-activity-feed/task-activity-list.tsx new file mode 100644 index 00000000..9182a5a0 --- /dev/null +++ b/worklenz-frontend/src/pages/home/user-activity-feed/task-activity-list.tsx @@ -0,0 +1,184 @@ +import React, { useCallback, useMemo } from 'react'; +import { List, Typography, Tooltip, Space, Tag, theme } from 'antd'; +import { FileTextOutlined } from '@ant-design/icons'; +import moment from 'moment'; +import { useTranslation } from 'react-i18next'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { + setSelectedTaskId, + setShowTaskDrawer, + fetchTask, +} from '@/features/task-drawer/task-drawer.slice'; +import { IUserRecentTask } from '@/types/home/user-activity.types'; + +const { Text } = Typography; + +interface TaskActivityListProps { + tasks: IUserRecentTask[]; +} + +const TaskActivityList: React.FC = React.memo(({ tasks }) => { + const { t } = useTranslation('home'); + const dispatch = useAppDispatch(); + const { token } = theme.useToken(); + + // Enhanced dark mode detection + const isDarkMode = useMemo(() => { + return token.colorBgContainer === '#1f1f1f' || + token.colorBgBase === '#141414' || + token.colorBgElevated === '#1f1f1f' || + document.documentElement.getAttribute('data-theme') === 'dark' || + document.body.classList.contains('dark'); + }, [token]); + + const handleTaskClick = useCallback( + (taskId: string, projectId: string) => { + dispatch(setSelectedTaskId(taskId)); + dispatch(setShowTaskDrawer(true)); + dispatch(fetchTask({ taskId, projectId })); + }, + [dispatch] + ); + + // Enhanced styling with theme support + const listItemStyles = useMemo(() => ({ + padding: '16px 20px', + borderBottom: isDarkMode ? '1px solid #404040' : '1px solid #f0f2f5', + cursor: 'pointer', + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + borderRadius: '8px', + margin: '0 0 2px 0', + background: isDarkMode ? 'transparent' : 'transparent', + position: 'relative' as const, + overflow: 'hidden', + }), [isDarkMode]); + + const listItemHoverStyles = useMemo(() => ({ + background: isDarkMode + ? 'linear-gradient(135deg, #2a2a2a 0%, #353535 100%)' + : 'linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%)', + borderColor: isDarkMode ? '#505050' : '#d1d9e6', + transform: 'translateY(-1px)', + boxShadow: isDarkMode + ? '0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2)' + : '0 4px 16px rgba(24, 144, 255, 0.15), 0 1px 4px rgba(0, 0, 0, 0.1)', + }), [isDarkMode]); + + const iconStyles = useMemo(() => ({ + color: isDarkMode ? '#40a9ff' : '#1890ff', + fontSize: '16px', + padding: '8px', + borderRadius: '6px', + background: isDarkMode + ? 'linear-gradient(135deg, #1a2332 0%, #2a3441 100%)' + : 'linear-gradient(135deg, #e6f7ff 0%, #f0f8ff 100%)', + border: isDarkMode ? '1px solid #40a9ff20' : '1px solid #1890ff20', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: '32px', + minHeight: '32px', + }), [isDarkMode]); + + const taskNameStyles = useMemo(() => ({ + color: isDarkMode ? '#ffffff' : '#1f2937', + fontSize: '15px', + fontWeight: 600, + lineHeight: '1.4', + }), [isDarkMode]); + + const tagStyles = useMemo(() => ({ + background: isDarkMode + ? 'linear-gradient(135deg, #1e40af 0%, #3b82f6 100%)' + : 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)', + color: isDarkMode ? '#ffffff' : '#1e40af', + border: isDarkMode ? '1px solid #3b82f6' : '1px solid #93c5fd', + borderRadius: '6px', + fontSize: '11px', + fontWeight: 600, + padding: '2px 8px', + textTransform: 'uppercase' as const, + letterSpacing: '0.5px', + }), [isDarkMode]); + + const metaTextStyles = useMemo(() => ({ + color: isDarkMode ? '#9ca3af' : '#6b7280', + fontSize: '13px', + fontWeight: 500, + }), [isDarkMode]); + + const timeTextStyles = useMemo(() => ({ + color: isDarkMode ? '#8c8c8c' : '#9ca3af', + fontSize: '12px', + fontWeight: 400, + }), [isDarkMode]); + + const activityCountStyles = useMemo(() => ({ + color: isDarkMode ? '#10b981' : '#059669', + fontSize: '12px', + fontWeight: 600, + background: isDarkMode + ? 'linear-gradient(135deg, #064e3b20 0%, #065f4620 100%)' + : 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)', + padding: '2px 6px', + borderRadius: '4px', + border: isDarkMode ? '1px solid #065f4640' : '1px solid #a7f3d040', + }), [isDarkMode]); + + return ( + ( + handleTaskClick(item.task_id, item.project_id)} + style={listItemStyles} + onMouseEnter={(e) => { + Object.assign(e.currentTarget.style, listItemHoverStyles); + }} + onMouseLeave={(e) => { + Object.assign(e.currentTarget.style, listItemStyles); + }} + aria-label={`${t('recentTaskAriaLabel')} ${item.task_name}`} + > +
+
+ +
+
+
+ + {item.task_name} + + + {t('activityTag')} + +
+
+ + {item.project_name} + + + + {item.activity_count} {item.activity_count === 1 ? t('activitySingular') : t('activityPlural')} + + + + {moment(item.last_activity_at).fromNow()} + + + +
+
+
+
+ )} + /> + ); +}); + +export default TaskActivityList; diff --git a/worklenz-frontend/src/pages/home/user-activity-feed/time-logged-task-list.tsx b/worklenz-frontend/src/pages/home/user-activity-feed/time-logged-task-list.tsx new file mode 100644 index 00000000..0185c2c8 --- /dev/null +++ b/worklenz-frontend/src/pages/home/user-activity-feed/time-logged-task-list.tsx @@ -0,0 +1,201 @@ +import React, { useCallback, useMemo } from 'react'; +import { List, Typography, Tag, Tooltip, Space, theme } from 'antd'; +import { ClockCircleOutlined } from '@ant-design/icons'; +import moment from 'moment'; +import { useTranslation } from 'react-i18next'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { + setSelectedTaskId, + setShowTaskDrawer, + fetchTask, +} from '@/features/task-drawer/task-drawer.slice'; +import { IUserTimeLoggedTask } from '@/types/home/user-activity.types'; + +const { Text } = Typography; + +interface TimeLoggedTaskListProps { + tasks: IUserTimeLoggedTask[]; +} + +const TimeLoggedTaskList: React.FC = React.memo(({ tasks }) => { + const { t } = useTranslation('home'); + const dispatch = useAppDispatch(); + const { token } = theme.useToken(); + + // Enhanced dark mode detection + const isDarkMode = useMemo(() => { + return token.colorBgContainer === '#1f1f1f' || + token.colorBgBase === '#141414' || + token.colorBgElevated === '#1f1f1f' || + document.documentElement.getAttribute('data-theme') === 'dark' || + document.body.classList.contains('dark'); + }, [token]); + + const handleTaskClick = useCallback( + (taskId: string, projectId: string) => { + dispatch(setSelectedTaskId(taskId)); + dispatch(setShowTaskDrawer(true)); + dispatch(fetchTask({ taskId, projectId })); + }, + [dispatch] + ); + + // Enhanced styling with theme support + const listItemStyles = useMemo(() => ({ + padding: '16px 20px', + borderBottom: isDarkMode ? '1px solid #404040' : '1px solid #f0f2f5', + cursor: 'pointer', + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + borderRadius: '8px', + margin: '0 0 2px 0', + background: isDarkMode ? 'transparent' : 'transparent', + position: 'relative' as const, + overflow: 'hidden', + }), [isDarkMode]); + + const listItemHoverStyles = useMemo(() => ({ + background: isDarkMode + ? 'linear-gradient(135deg, #2a2a2a 0%, #353535 100%)' + : 'linear-gradient(135deg, #f6ffed 0%, #f0fff4 100%)', + borderColor: isDarkMode ? '#505050' : '#b7eb8f', + transform: 'translateY(-1px)', + boxShadow: isDarkMode + ? '0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2)' + : '0 4px 16px rgba(82, 196, 26, 0.15), 0 1px 4px rgba(0, 0, 0, 0.1)', + }), [isDarkMode]); + + const iconStyles = useMemo(() => ({ + color: isDarkMode ? '#73d13d' : '#52c41a', + fontSize: '16px', + padding: '8px', + borderRadius: '6px', + background: isDarkMode + ? 'linear-gradient(135deg, #1b2918 0%, #273622 100%)' + : 'linear-gradient(135deg, #f6ffed 0%, #f0fff4 100%)', + border: isDarkMode ? '1px solid #52c41a20' : '1px solid #52c41a20', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: '32px', + minHeight: '32px', + }), [isDarkMode]); + + const taskNameStyles = useMemo(() => ({ + color: isDarkMode ? '#ffffff' : '#1f2937', + fontSize: '15px', + fontWeight: 600, + lineHeight: '1.4', + }), [isDarkMode]); + + const timeLogTagStyles = useMemo(() => ({ + background: isDarkMode + ? 'linear-gradient(135deg, #365314 0%, #4d7c0f 100%)' + : 'linear-gradient(135deg, #f0fff4 0%, #d9f7be 100%)', + color: isDarkMode ? '#ffffff' : '#365314', + border: isDarkMode ? '1px solid #4d7c0f' : '1px solid #95de64', + borderRadius: '6px', + fontSize: '11px', + fontWeight: 600, + padding: '2px 8px', + textTransform: 'uppercase' as const, + letterSpacing: '0.5px', + }), [isDarkMode]); + + const timerTagStyles = useMemo(() => ({ + background: isDarkMode + ? 'linear-gradient(135deg, #0f766e 0%, #14b8a6 100%)' + : 'linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%)', + color: isDarkMode ? '#ffffff' : '#0f766e', + border: isDarkMode ? '1px solid #14b8a6' : '1px solid #5eead4', + borderRadius: '6px', + fontSize: '10px', + fontWeight: 600, + padding: '1px 6px', + }), [isDarkMode]); + + const metaTextStyles = useMemo(() => ({ + color: isDarkMode ? '#9ca3af' : '#6b7280', + fontSize: '13px', + fontWeight: 500, + }), [isDarkMode]); + + const timeTextStyles = useMemo(() => ({ + color: isDarkMode ? '#8c8c8c' : '#9ca3af', + fontSize: '12px', + fontWeight: 400, + }), [isDarkMode]); + + const timeLoggedStyles = useMemo(() => ({ + color: isDarkMode ? '#73d13d' : '#52c41a', + fontSize: '13px', + fontWeight: 700, + background: isDarkMode + ? 'linear-gradient(135deg, #1b291820 0%, #27362220 100%)' + : 'linear-gradient(135deg, #f6ffed 0%, #f0fff4 100%)', + padding: '4px 8px', + borderRadius: '6px', + border: isDarkMode ? '1px solid #52c41a40' : '1px solid #b7eb8f40', + }), [isDarkMode]); + + return ( + ( + handleTaskClick(item.task_id, item.project_id)} + style={listItemStyles} + onMouseEnter={(e) => { + Object.assign(e.currentTarget.style, listItemHoverStyles); + }} + onMouseLeave={(e) => { + Object.assign(e.currentTarget.style, listItemStyles); + }} + aria-label={`${t('timeLoggedTaskAriaLabel')} ${item.task_name}`} + > +
+
+ +
+
+
+ + {item.task_name} + + + {t('timeLogTag')} + +
+
+ + {item.project_name} + + + + {item.total_time_logged_string} + + {item.logged_by_timer && ( + + {t('timerTag')} + + )} + + + {moment(item.last_logged_at).fromNow()} + + + +
+
+
+
+ )} + /> + ); +}); + +export default TimeLoggedTaskList; diff --git a/worklenz-frontend/src/pages/home/user-activity-feed/user-activity-feed.css b/worklenz-frontend/src/pages/home/user-activity-feed/user-activity-feed.css new file mode 100644 index 00000000..98faae79 --- /dev/null +++ b/worklenz-frontend/src/pages/home/user-activity-feed/user-activity-feed.css @@ -0,0 +1,19 @@ +.activity-feed-item:hover { + background-color: var(--activity-hover, #fafafa); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.activity-feed-item:active { + transform: translateY(0); + background-color: var(--activity-active, #f0f0f0); +} + +/* Dark theme support */ +[data-theme="dark"] .activity-feed-item:hover { + background-color: var(--activity-hover, #262626); +} + +[data-theme="dark"] .activity-feed-item:active { + background-color: var(--activity-active, #1f1f1f); +} diff --git a/worklenz-frontend/src/pages/home/user-activity-feed/user-activity-feed.tsx b/worklenz-frontend/src/pages/home/user-activity-feed/user-activity-feed.tsx new file mode 100644 index 00000000..fa014202 --- /dev/null +++ b/worklenz-frontend/src/pages/home/user-activity-feed/user-activity-feed.tsx @@ -0,0 +1,165 @@ +import React, { useMemo, useCallback, useEffect } from 'react'; +import { Card, Segmented, Skeleton, Empty, Typography, Alert } from 'antd'; +import { ClockCircleOutlined, UnorderedListOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; +import { useAppSelector } from '@/hooks/useAppSelector'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { ActivityFeedType } from '@/types/home/user-activity.types'; +import { setActiveTab } from '@/features/home-page/user-activity.slice'; +import { + useGetUserRecentTasksQuery, + useGetUserTimeLoggedTasksQuery, +} from '@/api/home-page/user-activity.api.service'; +import TaskActivityList from './task-activity-list'; +import TimeLoggedTaskList from './time-logged-task-list'; + +const { Title } = Typography; + +const UserActivityFeed: React.FC = () => { + const { t } = useTranslation('home'); + const dispatch = useAppDispatch(); + const { activeTab } = useAppSelector(state => state.userActivityReducer); + + const { + data: recentTasksData, + isLoading: loadingRecentTasks, + error: recentTasksError, + refetch: refetchRecentTasks, + } = useGetUserRecentTasksQuery( + { limit: 10 }, + { + skip: false, + refetchOnMountOrArgChange: true + } + ); + + const { + data: timeLoggedTasksData, + isLoading: loadingTimeLoggedTasks, + error: timeLoggedTasksError, + refetch: refetchTimeLoggedTasks, + } = useGetUserTimeLoggedTasksQuery( + { limit: 10 }, + { + skip: false, + refetchOnMountOrArgChange: true + } + ); + + const recentTasks = useMemo(() => { + if (!recentTasksData) return []; + // Handle both array and object responses from the API + if (Array.isArray(recentTasksData)) { + return recentTasksData; + } + // If it's an object with a data property (common API pattern) + if (recentTasksData && typeof recentTasksData === 'object' && 'data' in recentTasksData) { + return Array.isArray(recentTasksData.data) ? recentTasksData.data : []; + } + // If it's a different object structure, try to extract tasks + if (recentTasksData && typeof recentTasksData === 'object') { + const possibleArrays = Object.values(recentTasksData).filter(Array.isArray); + return possibleArrays.length > 0 ? possibleArrays[0] : []; + } + return []; + }, [recentTasksData]); + + const timeLoggedTasks = useMemo(() => { + if (!timeLoggedTasksData) return []; + // Handle both array and object responses from the API + if (Array.isArray(timeLoggedTasksData)) { + return timeLoggedTasksData; + } + // If it's an object with a data property (common API pattern) + if (timeLoggedTasksData && typeof timeLoggedTasksData === 'object' && 'data' in timeLoggedTasksData) { + return Array.isArray(timeLoggedTasksData.data) ? timeLoggedTasksData.data : []; + } + // If it's a different object structure, try to extract tasks + if (timeLoggedTasksData && typeof timeLoggedTasksData === 'object') { + const possibleArrays = Object.values(timeLoggedTasksData).filter(Array.isArray); + return possibleArrays.length > 0 ? possibleArrays[0] : []; + } + return []; + }, [timeLoggedTasksData]); + + const segmentOptions = useMemo( + () => [ + { + value: ActivityFeedType.RECENT_TASKS, + label: ( +
+ + {t('Recent Tasks')} +
+ ), + }, + { + value: ActivityFeedType.TIME_LOGGED_TASKS, + label: ( +
+ + {t('Time Logged Tasks')} +
+ ), + }, + ], + [t] + ); + + const handleTabChange = useCallback( + (value: ActivityFeedType) => { + dispatch(setActiveTab(value)); + }, + [dispatch] + ); + + // Refetch data when the active tab changes + useEffect(() => { + if (activeTab === ActivityFeedType.RECENT_TASKS) { + refetchRecentTasks(); + } else if (activeTab === ActivityFeedType.TIME_LOGGED_TASKS) { + refetchTimeLoggedTasks(); + } + }, [activeTab, refetchRecentTasks, refetchTimeLoggedTasks]); + + const renderContent = () => { + if (activeTab === ActivityFeedType.RECENT_TASKS) { + if (loadingRecentTasks) { + return ; + } + if (recentTasksError) { + return ; + } + if (recentTasks.length === 0) { + return ; + } + return ; + } else { + if (loadingTimeLoggedTasks) { + return ; + } + if (timeLoggedTasksError) { + return ; + } + if (timeLoggedTasks.length === 0) { + return ; + } + return ; + } + }; + + return ( + +
+ +
+ {renderContent()} +
+ ); +}; + +export default React.memo(UserActivityFeed); diff --git a/worklenz-frontend/src/types/home/user-activity.types.ts b/worklenz-frontend/src/types/home/user-activity.types.ts new file mode 100644 index 00000000..35fc382f --- /dev/null +++ b/worklenz-frontend/src/types/home/user-activity.types.ts @@ -0,0 +1,32 @@ +export interface IUserRecentTask { + task_id: string; + task_name: string; + project_id: string; + project_name: string; + last_activity_at: string; + activity_count: number; + project_color?: string; + task_status?: string; + status_color?: string; +} + +export interface IUserTimeLoggedTask { + task_id: string; + task_name: string; + project_id: string; + project_name: string; + total_time_logged: number; + total_time_logged_string: string; + last_logged_at: string; + logged_by_timer: boolean; + project_color?: string; + task_status?: string; + status_color?: string; + log_entries_count?: number; + estimated_time?: number; +} + +export enum ActivityFeedType { + RECENT_TASKS = 'recent-tasks', + TIME_LOGGED_TASKS = 'time-logged-tasks' +}