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.
This commit is contained in:
@@ -16,6 +16,9 @@ interface IUserRecentTask {
|
|||||||
project_name: string;
|
project_name: string;
|
||||||
last_activity_at: string;
|
last_activity_at: string;
|
||||||
activity_count: number;
|
activity_count: number;
|
||||||
|
project_color?: string;
|
||||||
|
task_status?: string;
|
||||||
|
status_color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUserTimeLoggedTask {
|
interface IUserTimeLoggedTask {
|
||||||
@@ -27,6 +30,11 @@ interface IUserTimeLoggedTask {
|
|||||||
total_time_logged_string: string;
|
total_time_logged_string: string;
|
||||||
last_logged_at: string;
|
last_logged_at: string;
|
||||||
logged_by_timer: boolean;
|
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 {
|
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"));
|
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;
|
const { offset = 0, limit = 10 } = req.query;
|
||||||
|
|
||||||
|
// Optimized query with better performance and team filtering
|
||||||
const q = `
|
const q = `
|
||||||
SELECT tal.id, tal.task_id, t.name AS task_name, tal.project_id, p.name AS project_name,
|
SELECT DISTINCT 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,
|
MAX(tal.created_at) AS last_activity_at,
|
||||||
tal.prev_string, tal.next_string, tal.created_at AS last_activity_at,
|
COUNT(DISTINCT tal.id) AS activity_count,
|
||||||
(SELECT COUNT(*) FROM task_activity_logs WHERE task_id = tal.task_id AND user_id = $1) 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
|
FROM task_activity_logs tal
|
||||||
JOIN tasks t ON tal.task_id = t.id
|
INNER JOIN tasks t ON tal.task_id = t.id AND t.archived = FALSE
|
||||||
JOIN projects p ON tal.project_id = p.id
|
INNER JOIN projects p ON tal.project_id = p.id AND p.team_id = $1
|
||||||
WHERE tal.user_id = $1
|
WHERE tal.user_id = $2
|
||||||
ORDER BY tal.created_at DESC
|
AND tal.created_at >= NOW() - INTERVAL '30 days'
|
||||||
LIMIT $2 OFFSET $3;
|
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;
|
const tasks: IUserRecentTask[] = result.rows;
|
||||||
|
|
||||||
return res.status(200).send(new ServerResponse(true, tasks));
|
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"));
|
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;
|
const { offset = 0, limit = 10 } = req.query;
|
||||||
|
|
||||||
|
// Optimized query with better performance, team filtering, and useful additional data
|
||||||
const q = `
|
const q = `
|
||||||
SELECT twl.task_id, t.name AS task_name, t.project_id, p.name AS project_name,
|
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,
|
SUM(twl.time_spent) AS total_time_logged,
|
||||||
MAX(twl.created_at) AS last_logged_at,
|
MAX(twl.created_at) AS last_logged_at,
|
||||||
MAX(twl.logged_by_timer) AS logged_by_timer
|
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
|
FROM task_work_log twl
|
||||||
JOIN tasks t ON twl.task_id = t.id
|
INNER JOIN tasks t ON twl.task_id = t.id AND t.archived = FALSE
|
||||||
JOIN projects p ON t.project_id = p.id
|
INNER JOIN projects p ON t.project_id = p.id AND p.team_id = $1
|
||||||
WHERE twl.user_id = $1
|
WHERE twl.user_id = $2
|
||||||
GROUP BY twl.task_id, t.name, t.project_id, p.name
|
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
|
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 => ({
|
const tasks: IUserTimeLoggedTask[] = result.rows.map(task => ({
|
||||||
...task,
|
...task,
|
||||||
total_time_logged_string: formatDuration(moment.duration(task.total_time_logged, "seconds")),
|
total_time_logged_string: formatDuration(moment.duration(task.total_time_logged, "seconds")),
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(new ServerResponse(true, tasks));
|
return res.status(200).send(new ServerResponse(true, tasks));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,13 @@
|
|||||||
"recentTasks": "Detyrat e Fundit",
|
"recentTasks": "Detyrat e Fundit",
|
||||||
"timeLoggedTasks": "Koha e Regjistruar",
|
"timeLoggedTasks": "Koha e Regjistruar",
|
||||||
"noRecentTasks": "Asnjë detyrë e fundit",
|
"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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,13 @@
|
|||||||
"recentTasks": "Aktuelle Aufgaben",
|
"recentTasks": "Aktuelle Aufgaben",
|
||||||
"timeLoggedTasks": "Erfasste Zeit",
|
"timeLoggedTasks": "Erfasste Zeit",
|
||||||
"noRecentTasks": "Keine aktuellen Aufgaben",
|
"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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,20 @@
|
|||||||
"recentTasks": "Recent Tasks",
|
"recentTasks": "Recent Tasks",
|
||||||
"timeLoggedTasks": "Time Logged",
|
"timeLoggedTasks": "Time Logged",
|
||||||
"noRecentTasks": "No recent tasks",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,13 @@
|
|||||||
"recentTasks": "Tareas Recientes",
|
"recentTasks": "Tareas Recientes",
|
||||||
"timeLoggedTasks": "Tiempo Registrado",
|
"timeLoggedTasks": "Tiempo Registrado",
|
||||||
"noRecentTasks": "No hay tareas recientes",
|
"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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,13 @@
|
|||||||
"recentTasks": "Tarefas Recentes",
|
"recentTasks": "Tarefas Recentes",
|
||||||
"timeLoggedTasks": "Tempo Registrado",
|
"timeLoggedTasks": "Tempo Registrado",
|
||||||
"noRecentTasks": "Nenhuma tarefa recente",
|
"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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,18 @@
|
|||||||
"list": "列表",
|
"list": "列表",
|
||||||
"calendar": "日历",
|
"calendar": "日历",
|
||||||
"tasks": "任务",
|
"tasks": "任务",
|
||||||
"refresh": "刷新"
|
"refresh": "刷新",
|
||||||
|
"recentActivity": "最近活动",
|
||||||
|
"recentTasks": "最近任务",
|
||||||
|
"timeLoggedTasks": "时间记录",
|
||||||
|
"noRecentTasks": "没有最近任务",
|
||||||
|
"noTimeLoggedTasks": "没有时间记录任务",
|
||||||
|
"activityTag": "活动",
|
||||||
|
"timeLogTag": "时间记录",
|
||||||
|
"timerTag": "计时器",
|
||||||
|
"activitySingular": "活动",
|
||||||
|
"activityPlural": "活动",
|
||||||
|
"recentTaskAriaLabel": "最近任务:",
|
||||||
|
"timeLoggedTaskAriaLabel": "时间记录任务:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { List, Typography, Tooltip, Space, Tag } from 'antd';
|
import { List, Typography, Tooltip, Space, Tag, theme } from 'antd';
|
||||||
import { FileTextOutlined } from '@ant-design/icons';
|
import { FileTextOutlined } from '@ant-design/icons';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import {
|
import {
|
||||||
setSelectedTaskId,
|
setSelectedTaskId,
|
||||||
@@ -17,7 +18,18 @@ interface TaskActivityListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TaskActivityList: React.FC<TaskActivityListProps> = React.memo(({ tasks }) => {
|
const TaskActivityList: React.FC<TaskActivityListProps> = React.memo(({ tasks }) => {
|
||||||
|
const { t } = useTranslation('home');
|
||||||
const dispatch = useAppDispatch();
|
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(
|
const handleTaskClick = useCallback(
|
||||||
(taskId: string, projectId: string) => {
|
(taskId: string, projectId: string) => {
|
||||||
@@ -28,50 +40,141 @@ const TaskActivityList: React.FC<TaskActivityListProps> = React.memo(({ tasks })
|
|||||||
[dispatch]
|
[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 (
|
return (
|
||||||
<List
|
<List
|
||||||
dataSource={tasks}
|
dataSource={tasks}
|
||||||
|
style={{ background: 'transparent' }}
|
||||||
|
split={false}
|
||||||
renderItem={item => (
|
renderItem={item => (
|
||||||
<List.Item
|
<List.Item
|
||||||
onClick={() => handleTaskClick(item.task_id, item.project_id)}
|
onClick={() => handleTaskClick(item.task_id, item.project_id)}
|
||||||
style={{
|
style={listItemStyles}
|
||||||
padding: '12px 0',
|
onMouseEnter={(e) => {
|
||||||
borderBottom: '1px solid #f0f0f0',
|
Object.assign(e.currentTarget.style, listItemHoverStyles);
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
}}
|
}}
|
||||||
aria-label={`Recent task: ${item.task_name}`}
|
onMouseLeave={(e) => {
|
||||||
|
Object.assign(e.currentTarget.style, listItemStyles);
|
||||||
|
}}
|
||||||
|
aria-label={`${t('recentTaskAriaLabel')} ${item.task_name}`}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={iconStyles}>
|
||||||
<FileTextOutlined style={{ color: '#1890ff' }} />
|
<FileTextOutlined />
|
||||||
<Text strong ellipsis style={{ flex: 1 }}>
|
</div>
|
||||||
{item.task_name}
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
</Text>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
|
||||||
<Tag color="geekblue" style={{ marginLeft: 4, fontWeight: 500 }}>
|
<Text ellipsis style={taskNameStyles}>
|
||||||
Activity
|
{item.task_name}
|
||||||
</Tag>
|
|
||||||
<Tooltip title={moment(item.last_activity_at).format('MMMM Do YYYY, h:mm:ss a')}>
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{moment(item.last_activity_at).fromNow()}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
<Tag style={tagStyles}>
|
||||||
|
{t('activityTag')}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text style={metaTextStyles}>
|
||||||
|
{item.project_name}
|
||||||
|
</Text>
|
||||||
|
<Space size={16}>
|
||||||
|
<span style={activityCountStyles}>
|
||||||
|
{item.activity_count} {item.activity_count === 1 ? t('activitySingular') : t('activityPlural')}
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
title={moment(item.last_activity_at).format('MMMM Do YYYY, h:mm:ss a')}
|
||||||
|
placement="topRight"
|
||||||
|
>
|
||||||
|
<Text style={timeTextStyles}>
|
||||||
|
{moment(item.last_activity_at).fromNow()}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{item.project_name}
|
|
||||||
</Text>
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{item.activity_count} {item.activity_count === 1 ? 'activity' : 'activities'}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { List, Typography, Tag, Tooltip, Space } from 'antd';
|
import { List, Typography, Tag, Tooltip, Space, theme } from 'antd';
|
||||||
import { ClockCircleOutlined } from '@ant-design/icons';
|
import { ClockCircleOutlined } from '@ant-design/icons';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import {
|
import {
|
||||||
setSelectedTaskId,
|
setSelectedTaskId,
|
||||||
@@ -17,7 +18,18 @@ interface TimeLoggedTaskListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TimeLoggedTaskList: React.FC<TimeLoggedTaskListProps> = React.memo(({ tasks }) => {
|
const TimeLoggedTaskList: React.FC<TimeLoggedTaskListProps> = React.memo(({ tasks }) => {
|
||||||
|
const { t } = useTranslation('home');
|
||||||
const dispatch = useAppDispatch();
|
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(
|
const handleTaskClick = useCallback(
|
||||||
(taskId: string, projectId: string) => {
|
(taskId: string, projectId: string) => {
|
||||||
@@ -28,49 +40,158 @@ const TimeLoggedTaskList: React.FC<TimeLoggedTaskListProps> = React.memo(({ task
|
|||||||
[dispatch]
|
[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 (
|
return (
|
||||||
<List
|
<List
|
||||||
dataSource={tasks}
|
dataSource={tasks}
|
||||||
|
style={{ background: 'transparent' }}
|
||||||
|
split={false}
|
||||||
renderItem={item => (
|
renderItem={item => (
|
||||||
<List.Item
|
<List.Item
|
||||||
onClick={() => handleTaskClick(item.task_id, item.project_id)}
|
onClick={() => handleTaskClick(item.task_id, item.project_id)}
|
||||||
style={{
|
style={listItemStyles}
|
||||||
padding: '12px 0',
|
onMouseEnter={(e) => {
|
||||||
borderBottom: '1px solid #f0f0f0',
|
Object.assign(e.currentTarget.style, listItemHoverStyles);
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
}}
|
}}
|
||||||
aria-label={`Time logged task: ${item.task_name}`}
|
onMouseLeave={(e) => {
|
||||||
|
Object.assign(e.currentTarget.style, listItemStyles);
|
||||||
|
}}
|
||||||
|
aria-label={`${t('timeLoggedTaskAriaLabel')} ${item.task_name}`}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={iconStyles}>
|
||||||
<ClockCircleOutlined style={{ color: '#52c41a' }} />
|
<ClockCircleOutlined />
|
||||||
<Text strong ellipsis style={{ flex: 1 }}>
|
|
||||||
{item.task_name}
|
|
||||||
</Text>
|
|
||||||
<Tag color="lime" style={{ marginLeft: 4, fontWeight: 500 }}>
|
|
||||||
Time Log
|
|
||||||
</Tag>
|
|
||||||
<Space>
|
|
||||||
<Text strong style={{ color: '#52c41a', fontSize: 12 }}>
|
|
||||||
{item.total_time_logged_string}
|
|
||||||
</Text>
|
|
||||||
{item.logged_by_timer && (
|
|
||||||
<Tag color="green">
|
|
||||||
Timer
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
<Tooltip title={moment(item.last_logged_at).format('MMMM Do YYYY, h:mm:ss a')}>
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{moment(item.last_logged_at).fromNow()}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
{item.project_name}
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
|
||||||
</Text>
|
<Text ellipsis style={taskNameStyles}>
|
||||||
</Space>
|
{item.task_name}
|
||||||
|
</Text>
|
||||||
|
<Tag style={timeLogTagStyles}>
|
||||||
|
{t('timeLogTag')}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text style={metaTextStyles}>
|
||||||
|
{item.project_name}
|
||||||
|
</Text>
|
||||||
|
<Space size={12} align="center">
|
||||||
|
<span style={timeLoggedStyles}>
|
||||||
|
{item.total_time_logged_string}
|
||||||
|
</span>
|
||||||
|
{item.logged_by_timer && (
|
||||||
|
<Tag style={timerTagStyles}>
|
||||||
|
{t('timerTag')}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
<Tooltip
|
||||||
|
title={moment(item.last_logged_at).format('MMMM Do YYYY, h:mm:ss a')}
|
||||||
|
placement="topRight"
|
||||||
|
>
|
||||||
|
<Text style={timeTextStyles}>
|
||||||
|
{moment(item.last_logged_at).fromNow()}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 { Card, Segmented, Skeleton, Empty, Typography, Alert } from 'antd';
|
||||||
import { ClockCircleOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
import { ClockCircleOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -24,28 +24,62 @@ const UserActivityFeed: React.FC = () => {
|
|||||||
data: recentTasksData,
|
data: recentTasksData,
|
||||||
isLoading: loadingRecentTasks,
|
isLoading: loadingRecentTasks,
|
||||||
error: recentTasksError,
|
error: recentTasksError,
|
||||||
|
refetch: refetchRecentTasks,
|
||||||
} = useGetUserRecentTasksQuery(
|
} = useGetUserRecentTasksQuery(
|
||||||
{ limit: 10 },
|
{ limit: 10 },
|
||||||
{ skip: activeTab !== ActivityFeedType.RECENT_TASKS }
|
{
|
||||||
|
skip: false,
|
||||||
|
refetchOnMountOrArgChange: true
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: timeLoggedTasksData,
|
data: timeLoggedTasksData,
|
||||||
isLoading: loadingTimeLoggedTasks,
|
isLoading: loadingTimeLoggedTasks,
|
||||||
error: timeLoggedTasksError,
|
error: timeLoggedTasksError,
|
||||||
|
refetch: refetchTimeLoggedTasks,
|
||||||
} = useGetUserTimeLoggedTasksQuery(
|
} = useGetUserTimeLoggedTasksQuery(
|
||||||
{ limit: 10 },
|
{ limit: 10 },
|
||||||
{ skip: activeTab !== ActivityFeedType.TIME_LOGGED_TASKS }
|
{
|
||||||
|
skip: false,
|
||||||
|
refetchOnMountOrArgChange: true
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const recentTasks = useMemo(() => {
|
const recentTasks = useMemo(() => {
|
||||||
if (!recentTasksData) return [];
|
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]);
|
}, [recentTasksData]);
|
||||||
|
|
||||||
const timeLoggedTasks = useMemo(() => {
|
const timeLoggedTasks = useMemo(() => {
|
||||||
if (!timeLoggedTasksData) return [];
|
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]);
|
}, [timeLoggedTasksData]);
|
||||||
|
|
||||||
const segmentOptions = useMemo(
|
const segmentOptions = useMemo(
|
||||||
@@ -79,25 +113,34 @@ const UserActivityFeed: React.FC = () => {
|
|||||||
[dispatch]
|
[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 = () => {
|
const renderContent = () => {
|
||||||
if (activeTab === ActivityFeedType.RECENT_TASKS) {
|
if (activeTab === ActivityFeedType.RECENT_TASKS) {
|
||||||
if (recentTasksError) {
|
|
||||||
return <Alert message={t('Error Loading Recent Tasks')} type="error" showIcon />;
|
|
||||||
}
|
|
||||||
if (loadingRecentTasks) {
|
if (loadingRecentTasks) {
|
||||||
return <Skeleton active />;
|
return <Skeleton active />;
|
||||||
}
|
}
|
||||||
|
if (recentTasksError) {
|
||||||
|
return <Alert message={t('Error Loading Recent Tasks')} type="error" showIcon />;
|
||||||
|
}
|
||||||
if (recentTasks.length === 0) {
|
if (recentTasks.length === 0) {
|
||||||
return <Empty description={t('No Recent Tasks')} />;
|
return <Empty description={t('No Recent Tasks')} />;
|
||||||
}
|
}
|
||||||
return <TaskActivityList tasks={recentTasks} />;
|
return <TaskActivityList tasks={recentTasks} />;
|
||||||
} else {
|
} else {
|
||||||
if (timeLoggedTasksError) {
|
|
||||||
return <Alert message={t('Error Loading Time Logged Tasks')} type="error" showIcon />;
|
|
||||||
}
|
|
||||||
if (loadingTimeLoggedTasks) {
|
if (loadingTimeLoggedTasks) {
|
||||||
return <Skeleton active />;
|
return <Skeleton active />;
|
||||||
}
|
}
|
||||||
|
if (timeLoggedTasksError) {
|
||||||
|
return <Alert message={t('Error Loading Time Logged Tasks')} type="error" showIcon />;
|
||||||
|
}
|
||||||
if (timeLoggedTasks.length === 0) {
|
if (timeLoggedTasks.length === 0) {
|
||||||
return <Empty description={t('No Time Logged Tasks')} />;
|
return <Empty description={t('No Time Logged Tasks')} />;
|
||||||
}
|
}
|
||||||
@@ -106,16 +149,12 @@ const UserActivityFeed: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card title={t('Recent Activity')}>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Title level={5} style={{ marginBottom: 12 }}>
|
|
||||||
{t('Recent Activity')}
|
|
||||||
</Title>
|
|
||||||
<Segmented
|
<Segmented
|
||||||
options={segmentOptions}
|
options={segmentOptions}
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export interface IUserRecentTask {
|
|||||||
project_name: string;
|
project_name: string;
|
||||||
last_activity_at: string;
|
last_activity_at: string;
|
||||||
activity_count: number;
|
activity_count: number;
|
||||||
|
project_color?: string;
|
||||||
|
task_status?: string;
|
||||||
|
status_color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserTimeLoggedTask {
|
export interface IUserTimeLoggedTask {
|
||||||
@@ -16,6 +19,11 @@ export interface IUserTimeLoggedTask {
|
|||||||
total_time_logged_string: string;
|
total_time_logged_string: string;
|
||||||
last_logged_at: string;
|
last_logged_at: string;
|
||||||
logged_by_timer: boolean;
|
logged_by_timer: boolean;
|
||||||
|
project_color?: string;
|
||||||
|
task_status?: string;
|
||||||
|
status_color?: string;
|
||||||
|
log_entries_count?: number;
|
||||||
|
estimated_time?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActivityFeedType {
|
export enum ActivityFeedType {
|
||||||
|
|||||||
Reference in New Issue
Block a user