From 61461bb776bade5fb932658dc2834100a25ea1e2 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 14 Jul 2025 13:26:28 +0530 Subject: [PATCH] feat(user-activity): enhance user activity logs with additional data and improved queries - Added optional fields for project color, task status, and status color in IUserRecentTask and IUserTimeLoggedTask interfaces. - Optimized SQL queries to include team filtering and additional data such as project color and task status. - Updated frontend components to support new data fields and improved styling for better user experience. - Enhanced dark mode detection and styling in task activity lists. - Implemented refetching of data on tab change in the user activity feed. --- .../user-activity-logs-controller.ts | 70 ++++--- .../public/locales/alb/home.json | 9 +- worklenz-frontend/public/locales/de/home.json | 9 +- worklenz-frontend/public/locales/en/home.json | 16 +- worklenz-frontend/public/locales/es/home.json | 9 +- worklenz-frontend/public/locales/pt/home.json | 9 +- worklenz-frontend/public/locales/zh/home.json | 14 +- .../user-activity-feed/task-activity-list.tsx | 175 ++++++++++++---- .../time-logged-task-list.tsx | 193 ++++++++++++++---- .../user-activity-feed/user-activity-feed.tsx | 71 +++++-- .../src/types/home/user-activity.types.ts | 8 + 11 files changed, 466 insertions(+), 117 deletions(-) diff --git a/worklenz-backend/src/controllers/user-activity-logs-controller.ts b/worklenz-backend/src/controllers/user-activity-logs-controller.ts index c1d859d2..eeacb4b4 100644 --- a/worklenz-backend/src/controllers/user-activity-logs-controller.ts +++ b/worklenz-backend/src/controllers/user-activity-logs-controller.ts @@ -16,6 +16,9 @@ interface IUserRecentTask { project_name: string; last_activity_at: string; activity_count: number; + project_color?: string; + task_status?: string; + status_color?: string; } interface IUserTimeLoggedTask { @@ -27,6 +30,11 @@ interface IUserTimeLoggedTask { 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 { @@ -36,23 +44,30 @@ export default class UserActivityLogsController extends WorklenzControllerBase { return res.status(401).send(new ServerResponse(false, null, "Unauthorized")); } - const { id: userId } = req.user; + 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 tal.id, tal.task_id, t.name AS task_name, tal.project_id, p.name AS project_name, - tal.attribute_type, tal.log_type, tal.old_value, tal.new_value, - tal.prev_string, tal.next_string, tal.created_at AS last_activity_at, - (SELECT COUNT(*) FROM task_activity_logs WHERE task_id = tal.task_id AND user_id = $1) AS activity_count + 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 - JOIN tasks t ON tal.task_id = t.id - JOIN projects p ON tal.project_id = p.id - WHERE tal.user_id = $1 - ORDER BY tal.created_at DESC - LIMIT $2 OFFSET $3; + 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, [userId, limit, offset]); + const result = await db.query(q, [teamId, userId, limit, offset]); const tasks: IUserRecentTask[] = result.rows; return res.status(200).send(new ServerResponse(true, tasks)); @@ -64,29 +79,38 @@ export default class UserActivityLogsController extends WorklenzControllerBase { return res.status(401).send(new ServerResponse(false, null, "Unauthorized")); } - const { id: userId } = req.user; + 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) AS logged_by_timer + 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 - JOIN tasks t ON twl.task_id = t.id - JOIN projects p ON t.project_id = p.id - WHERE twl.user_id = $1 - GROUP BY twl.task_id, t.name, t.project_id, p.name + 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 $2 OFFSET $3; + LIMIT $3 OFFSET $4; `; - const result = await db.query(q, [userId, limit, offset]); + 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-frontend/public/locales/alb/home.json b/worklenz-frontend/public/locales/alb/home.json index 438ad788..9dd14e4e 100644 --- a/worklenz-frontend/public/locales/alb/home.json +++ b/worklenz-frontend/public/locales/alb/home.json @@ -46,6 +46,13 @@ "recentTasks": "Detyrat e Fundit", "timeLoggedTasks": "Koha e Regjistruar", "noRecentTasks": "Asnjë detyrë e fundit", - "noTimeLoggedTasks": "Asnjë detyrë me kohë të regjistruar" + "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 00da8fcf..03381614 100644 --- a/worklenz-frontend/public/locales/de/home.json +++ b/worklenz-frontend/public/locales/de/home.json @@ -46,6 +46,13 @@ "recentTasks": "Aktuelle Aufgaben", "timeLoggedTasks": "Erfasste Zeit", "noRecentTasks": "Keine aktuellen Aufgaben", - "noTimeLoggedTasks": "Keine Aufgaben mit erfasster Zeit" + "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 4d64626e..290316f0 100644 --- a/worklenz-frontend/public/locales/en/home.json +++ b/worklenz-frontend/public/locales/en/home.json @@ -46,6 +46,20 @@ "recentTasks": "Recent Tasks", "timeLoggedTasks": "Time Logged", "noRecentTasks": "No recent tasks", - "noTimeLoggedTasks": "No time logged 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 3eb1388c..63403089 100644 --- a/worklenz-frontend/public/locales/es/home.json +++ b/worklenz-frontend/public/locales/es/home.json @@ -45,6 +45,13 @@ "recentTasks": "Tareas Recientes", "timeLoggedTasks": "Tiempo Registrado", "noRecentTasks": "No hay tareas recientes", - "noTimeLoggedTasks": "No hay tareas con tiempo registrado" + "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 71fa4b02..9a84464c 100644 --- a/worklenz-frontend/public/locales/pt/home.json +++ b/worklenz-frontend/public/locales/pt/home.json @@ -45,6 +45,13 @@ "recentTasks": "Tarefas Recentes", "timeLoggedTasks": "Tempo Registrado", "noRecentTasks": "Nenhuma tarefa recente", - "noTimeLoggedTasks": "Nenhuma tarefa com tempo registrado" + "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/pages/home/user-activity-feed/task-activity-list.tsx b/worklenz-frontend/src/pages/home/user-activity-feed/task-activity-list.tsx index 9a9d7ed7..9182a5a0 100644 --- 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 @@ -1,7 +1,8 @@ -import React, { useCallback } from 'react'; -import { List, Typography, Tooltip, Space, Tag } from 'antd'; +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, @@ -17,7 +18,18 @@ interface TaskActivityListProps { } 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) => { @@ -28,50 +40,141 @@ const TaskActivityList: React.FC = React.memo(({ tasks }) [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={{ - padding: '12px 0', - borderBottom: '1px solid #f0f0f0', - cursor: 'pointer', - transition: 'all 0.2s ease', + style={listItemStyles} + onMouseEnter={(e) => { + Object.assign(e.currentTarget.style, listItemHoverStyles); }} - aria-label={`Recent task: ${item.task_name}`} + onMouseLeave={(e) => { + Object.assign(e.currentTarget.style, listItemStyles); + }} + aria-label={`${t('recentTaskAriaLabel')} ${item.task_name}`} > - -
- - - {item.task_name} - - - Activity - - - - {moment(item.last_activity_at).fromNow()} +
+
+ +
+
+
+ + {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()} + + + +
-
- - {item.project_name} - - - {item.activity_count} {item.activity_count === 1 ? 'activity' : 'activities'} - -
- +
)} /> 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 index c4da1825..0185c2c8 100644 --- 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 @@ -1,7 +1,8 @@ -import React, { useCallback } from 'react'; -import { List, Typography, Tag, Tooltip, Space } from 'antd'; +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, @@ -17,7 +18,18 @@ interface TimeLoggedTaskListProps { } 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) => { @@ -28,49 +40,158 @@ const TimeLoggedTaskList: React.FC = React.memo(({ task [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={{ - padding: '12px 0', - borderBottom: '1px solid #f0f0f0', - cursor: 'pointer', - transition: 'all 0.2s ease', + style={listItemStyles} + onMouseEnter={(e) => { + Object.assign(e.currentTarget.style, listItemHoverStyles); }} - aria-label={`Time logged task: ${item.task_name}`} + onMouseLeave={(e) => { + Object.assign(e.currentTarget.style, listItemStyles); + }} + aria-label={`${t('timeLoggedTaskAriaLabel')} ${item.task_name}`} > - -
- - - {item.task_name} - - - Time Log - - - - {item.total_time_logged_string} - - {item.logged_by_timer && ( - - Timer - - )} - - - {moment(item.last_logged_at).fromNow()} - - - +
+
+
- - {item.project_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()} + + + +
+
+
)} /> 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 index 77e817cc..fa014202 100644 --- 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 @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback } from 'react'; +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'; @@ -24,28 +24,62 @@ const UserActivityFeed: React.FC = () => { data: recentTasksData, isLoading: loadingRecentTasks, error: recentTasksError, + refetch: refetchRecentTasks, } = useGetUserRecentTasksQuery( { limit: 10 }, - { skip: activeTab !== ActivityFeedType.RECENT_TASKS } + { + skip: false, + refetchOnMountOrArgChange: true + } ); const { data: timeLoggedTasksData, isLoading: loadingTimeLoggedTasks, error: timeLoggedTasksError, + refetch: refetchTimeLoggedTasks, } = useGetUserTimeLoggedTasksQuery( { limit: 10 }, - { skip: activeTab !== ActivityFeedType.TIME_LOGGED_TASKS } + { + skip: false, + refetchOnMountOrArgChange: true + } ); const recentTasks = useMemo(() => { if (!recentTasksData) return []; - return Array.isArray(recentTasksData) ? recentTasksData : []; + // 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 []; - return Array.isArray(timeLoggedTasksData) ? timeLoggedTasksData : []; + // 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( @@ -79,25 +113,34 @@ const UserActivityFeed: React.FC = () => { [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 (recentTasksError) { - return ; - } if (loadingRecentTasks) { return ; } + if (recentTasksError) { + return ; + } if (recentTasks.length === 0) { return ; } return ; } else { - if (timeLoggedTasksError) { - return ; - } if (loadingTimeLoggedTasks) { return ; } + if (timeLoggedTasksError) { + return ; + } if (timeLoggedTasks.length === 0) { return ; } @@ -106,16 +149,12 @@ const UserActivityFeed: React.FC = () => { }; return ( - +
- - {t('Recent Activity')} -
{renderContent()} diff --git a/worklenz-frontend/src/types/home/user-activity.types.ts b/worklenz-frontend/src/types/home/user-activity.types.ts index 66256193..35fc382f 100644 --- a/worklenz-frontend/src/types/home/user-activity.types.ts +++ b/worklenz-frontend/src/types/home/user-activity.types.ts @@ -5,6 +5,9 @@ export interface IUserRecentTask { project_name: string; last_activity_at: string; activity_count: number; + project_color?: string; + task_status?: string; + status_color?: string; } export interface IUserTimeLoggedTask { @@ -16,6 +19,11 @@ export interface IUserTimeLoggedTask { 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 {