refactor(localization): update task-related translations and improve user activity feed
- Added new translation keys for recent tasks and time logged tasks in Albanian, German, English, Spanish, Portuguese, and Chinese localization files. - Enhanced user activity feed to switch between recent tasks and time logged tasks, improving user experience. - Updated the date formatting utility to support locale-specific formatting for better internationalization. - Refactored task activity list and time logged task list components to utilize a table layout for improved readability.
This commit is contained in:
@@ -44,7 +44,9 @@
|
|||||||
"refresh": "Rifresko",
|
"refresh": "Rifresko",
|
||||||
"recentActivity": "Aktiviteti i Fundit",
|
"recentActivity": "Aktiviteti i Fundit",
|
||||||
"recentTasks": "Detyrat e Fundit",
|
"recentTasks": "Detyrat e Fundit",
|
||||||
"timeLoggedTasks": "Koha e Regjistruar",
|
"recentTasksSegment": "Detyrat e Fundit",
|
||||||
|
"timeLogged": "Koha e Regjistruar",
|
||||||
|
"timeLoggedSegment": "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",
|
"activityTag": "Aktiviteti",
|
||||||
@@ -53,6 +55,8 @@
|
|||||||
"activitySingular": "aktivitet",
|
"activitySingular": "aktivitet",
|
||||||
"activityPlural": "aktivitete",
|
"activityPlural": "aktivitete",
|
||||||
"recentTaskAriaLabel": "Detyrë e fundit:",
|
"recentTaskAriaLabel": "Detyrë e fundit:",
|
||||||
"timeLoggedTaskAriaLabel": "Detyrë me kohë të regjistruar:"
|
"timeLoggedTaskAriaLabel": "Detyrë me kohë të regjistruar:",
|
||||||
|
"errorLoadingRecentTasks": "Gabim në ngarkimin e detyrave të fundit",
|
||||||
|
"errorLoadingTimeLoggedTasks": "Gabim në ngarkimin e detyrave me kohë të regjistruar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,9 @@
|
|||||||
"refresh": "Aktualisieren",
|
"refresh": "Aktualisieren",
|
||||||
"recentActivity": "Aktuelle Aktivitäten",
|
"recentActivity": "Aktuelle Aktivitäten",
|
||||||
"recentTasks": "Aktuelle Aufgaben",
|
"recentTasks": "Aktuelle Aufgaben",
|
||||||
"timeLoggedTasks": "Erfasste Zeit",
|
"recentTasksSegment": "Aktuelle Aufgaben",
|
||||||
|
"timeLogged": "Erfasste Zeit",
|
||||||
|
"timeLoggedSegment": "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",
|
"activityTag": "Aktivität",
|
||||||
@@ -53,6 +55,8 @@
|
|||||||
"activitySingular": "Aktivität",
|
"activitySingular": "Aktivität",
|
||||||
"activityPlural": "Aktivitäten",
|
"activityPlural": "Aktivitäten",
|
||||||
"recentTaskAriaLabel": "Aktuelle Aufgabe:",
|
"recentTaskAriaLabel": "Aktuelle Aufgabe:",
|
||||||
"timeLoggedTaskAriaLabel": "Aufgabe mit erfasster Zeit:"
|
"timeLoggedTaskAriaLabel": "Aufgabe mit erfasster Zeit:",
|
||||||
|
"errorLoadingRecentTasks": "Fehler beim Laden aktueller Aufgaben",
|
||||||
|
"errorLoadingTimeLoggedTasks": "Fehler beim Laden der Zeiterfassung"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,9 @@
|
|||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"recentActivity": "Recent Activity",
|
"recentActivity": "Recent Activity",
|
||||||
"recentTasks": "Recent Tasks",
|
"recentTasks": "Recent Tasks",
|
||||||
"timeLoggedTasks": "Time Logged",
|
"recentTasksSegment": "Recent Tasks",
|
||||||
|
"timeLogged": "Time Logged",
|
||||||
|
"timeLoggedSegment": "Time Logged",
|
||||||
"noRecentTasks": "No recent tasks",
|
"noRecentTasks": "No recent tasks",
|
||||||
"noTimeLoggedTasks": "No time logged tasks",
|
"noTimeLoggedTasks": "No time logged tasks",
|
||||||
"activityTag": "Activity",
|
"activityTag": "Activity",
|
||||||
@@ -54,12 +56,7 @@
|
|||||||
"activityPlural": "activities",
|
"activityPlural": "activities",
|
||||||
"recentTaskAriaLabel": "Recent task:",
|
"recentTaskAriaLabel": "Recent task:",
|
||||||
"timeLoggedTaskAriaLabel": "Time logged task:",
|
"timeLoggedTaskAriaLabel": "Time logged task:",
|
||||||
"Recent Activity": "Recent Activity",
|
"errorLoadingRecentTasks": "Error loading recent tasks",
|
||||||
"Recent Tasks": "Recent Tasks",
|
"errorLoadingTimeLoggedTasks": "Error loading time logged 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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,9 @@
|
|||||||
"refresh": "Actualizar",
|
"refresh": "Actualizar",
|
||||||
"recentActivity": "Actividad Reciente",
|
"recentActivity": "Actividad Reciente",
|
||||||
"recentTasks": "Tareas Recientes",
|
"recentTasks": "Tareas Recientes",
|
||||||
"timeLoggedTasks": "Tiempo Registrado",
|
"recentTasksSegment": "Tareas Recientes",
|
||||||
|
"timeLogged": "Tiempo Registrado",
|
||||||
|
"timeLoggedSegment": "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",
|
"activityTag": "Actividad",
|
||||||
@@ -52,6 +54,8 @@
|
|||||||
"activitySingular": "actividad",
|
"activitySingular": "actividad",
|
||||||
"activityPlural": "actividades",
|
"activityPlural": "actividades",
|
||||||
"recentTaskAriaLabel": "Tarea reciente:",
|
"recentTaskAriaLabel": "Tarea reciente:",
|
||||||
"timeLoggedTaskAriaLabel": "Tarea con tiempo registrado:"
|
"timeLoggedTaskAriaLabel": "Tarea con tiempo registrado:",
|
||||||
|
"errorLoadingRecentTasks": "Error al cargar tareas recientes",
|
||||||
|
"errorLoadingTimeLoggedTasks": "Error al cargar tareas con tiempo registrado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,9 @@
|
|||||||
"refresh": "Atualizar",
|
"refresh": "Atualizar",
|
||||||
"recentActivity": "Atividade Recente",
|
"recentActivity": "Atividade Recente",
|
||||||
"recentTasks": "Tarefas Recentes",
|
"recentTasks": "Tarefas Recentes",
|
||||||
"timeLoggedTasks": "Tempo Registrado",
|
"recentTasksSegment": "Tarefas Recentes",
|
||||||
|
"timeLogged": "Tempo Registrado",
|
||||||
|
"timeLoggedSegment": "Tempo Registrado",
|
||||||
"noRecentTasks": "Nenhuma tarefa recente",
|
"noRecentTasks": "Nenhuma tarefa recente",
|
||||||
"noTimeLoggedTasks": "Nenhuma tarefa com tempo registrado",
|
"noTimeLoggedTasks": "Nenhuma tarefa com tempo registrado",
|
||||||
"activityTag": "Atividade",
|
"activityTag": "Atividade",
|
||||||
@@ -52,6 +54,8 @@
|
|||||||
"activitySingular": "atividade",
|
"activitySingular": "atividade",
|
||||||
"activityPlural": "atividades",
|
"activityPlural": "atividades",
|
||||||
"recentTaskAriaLabel": "Tarefa recente:",
|
"recentTaskAriaLabel": "Tarefa recente:",
|
||||||
"timeLoggedTaskAriaLabel": "Tarefa com tempo registrado:"
|
"timeLoggedTaskAriaLabel": "Tarefa com tempo registrado:",
|
||||||
|
"errorLoadingRecentTasks": "Erro ao carregar tarefas recentes",
|
||||||
|
"errorLoadingTimeLoggedTasks": "Erro ao carregar tarefas com tempo registrado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,9 @@
|
|||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"recentActivity": "最近活动",
|
"recentActivity": "最近活动",
|
||||||
"recentTasks": "最近任务",
|
"recentTasks": "最近任务",
|
||||||
"timeLoggedTasks": "时间记录",
|
"recentTasksSegment": "最近任务",
|
||||||
|
"timeLogged": "时间记录",
|
||||||
|
"timeLoggedSegment": "时间记录",
|
||||||
"noRecentTasks": "没有最近任务",
|
"noRecentTasks": "没有最近任务",
|
||||||
"noTimeLoggedTasks": "没有时间记录任务",
|
"noTimeLoggedTasks": "没有时间记录任务",
|
||||||
"activityTag": "活动",
|
"activityTag": "活动",
|
||||||
@@ -53,6 +55,8 @@
|
|||||||
"activitySingular": "活动",
|
"activitySingular": "活动",
|
||||||
"activityPlural": "活动",
|
"activityPlural": "活动",
|
||||||
"recentTaskAriaLabel": "最近任务:",
|
"recentTaskAriaLabel": "最近任务:",
|
||||||
"timeLoggedTaskAriaLabel": "时间记录任务:"
|
"timeLoggedTaskAriaLabel": "时间记录任务:",
|
||||||
|
"errorLoadingRecentTasks": "加载最近任务时出错",
|
||||||
|
"errorLoadingTimeLoggedTasks": "加载时间记录任务时出错"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ interface UserActivityState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialState: UserActivityState = {
|
const initialState: UserActivityState = {
|
||||||
activeTab: ActivityFeedType.RECENT_TASKS,
|
activeTab: ActivityFeedType.TIME_LOGGED_TASKS,
|
||||||
activities: [],
|
activities: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ const SIDEBAR_MAX_WIDTH = 400;
|
|||||||
|
|
||||||
// Lazy load heavy components
|
// Lazy load heavy components
|
||||||
const TaskDrawer = React.lazy(() => import('@/components/task-drawer/task-drawer'));
|
const TaskDrawer = React.lazy(() => import('@/components/task-drawer/task-drawer'));
|
||||||
const SurveyPromptModal = React.lazy(() => import('@/components/survey/SurveyPromptModal').then(m => ({ default: m.SurveyPromptModal })));
|
const SurveyPromptModal = React.lazy(() =>
|
||||||
|
import('@/components/survey/SurveyPromptModal').then(m => ({ default: m.SurveyPromptModal }))
|
||||||
|
);
|
||||||
|
|
||||||
const HomePage = memo(() => {
|
const HomePage = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -109,18 +111,18 @@ const HomePage = memo(() => {
|
|||||||
|
|
||||||
<Row gutter={[24, 24]} className="mt-12">
|
<Row gutter={[24, 24]} className="mt-12">
|
||||||
<Col xs={24} lg={16}>
|
<Col xs={24} lg={16}>
|
||||||
<TasksList />
|
<Flex vertical gap={24}>
|
||||||
|
<TasksList />
|
||||||
|
|
||||||
|
<TodoList />
|
||||||
|
</Flex>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} lg={8}>
|
<Col xs={24} lg={8}>
|
||||||
<Flex vertical gap={24}>
|
<Flex vertical gap={24}>
|
||||||
<UserActivityFeed />
|
<UserActivityFeed />
|
||||||
|
|
||||||
<TodoList />
|
<RecentAndFavouriteProjectList />
|
||||||
|
|
||||||
<Card title="Recent & Favorite Projects">
|
|
||||||
<RecentAndFavouriteProjectList />
|
|
||||||
</Card>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ const TodoList = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div style={{ maxHeight: 420, overflow: 'auto' }}>
|
<div style={{ maxHeight: 300, overflow: 'auto' }}>
|
||||||
{data?.body.length === 0 ? (
|
{data?.body.length === 0 ? (
|
||||||
<EmptyListPlaceholder
|
<EmptyListPlaceholder
|
||||||
imageSrc="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/empty-box.webp"
|
imageSrc="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/empty-box.webp"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { List, Typography, Tooltip, Space, Tag, theme } from 'antd';
|
import { Table, Typography, Tooltip, theme } from 'antd';
|
||||||
import { FileTextOutlined } from '@ant-design/icons';
|
import { FileTextOutlined } from '@ant-design/icons';
|
||||||
import moment from 'moment';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { fromNow, formatDate } from '@/utils/dateUtils';
|
||||||
import {
|
import {
|
||||||
setSelectedTaskId,
|
setSelectedTaskId,
|
||||||
setShowTaskDrawer,
|
setShowTaskDrawer,
|
||||||
@@ -22,15 +22,6 @@ const TaskActivityList: React.FC<TaskActivityListProps> = React.memo(({ tasks })
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { token } = theme.useToken();
|
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) => {
|
||||||
dispatch(setSelectedTaskId(taskId));
|
dispatch(setSelectedTaskId(taskId));
|
||||||
@@ -40,143 +31,63 @@ const TaskActivityList: React.FC<TaskActivityListProps> = React.memo(({ tasks })
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Enhanced styling with theme support
|
const columns = [
|
||||||
const listItemStyles = useMemo(() => ({
|
{
|
||||||
padding: '16px 20px',
|
key: 'task',
|
||||||
borderBottom: isDarkMode ? '1px solid #404040' : '1px solid #f0f2f5',
|
render: (record: IUserRecentTask) => (
|
||||||
cursor: 'pointer',
|
<div
|
||||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
style={{
|
||||||
borderRadius: '8px',
|
display: 'flex',
|
||||||
margin: '0 0 2px 0',
|
alignItems: 'flex-start',
|
||||||
background: isDarkMode ? 'transparent' : 'transparent',
|
gap: 12,
|
||||||
position: 'relative' as const,
|
width: '100%',
|
||||||
overflow: 'hidden',
|
cursor: 'pointer',
|
||||||
}), [isDarkMode]);
|
padding: '8px 0'
|
||||||
|
|
||||||
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 (
|
|
||||||
<List
|
|
||||||
dataSource={tasks}
|
|
||||||
style={{ background: 'transparent' }}
|
|
||||||
split={false}
|
|
||||||
renderItem={item => (
|
|
||||||
<List.Item
|
|
||||||
onClick={() => handleTaskClick(item.task_id, item.project_id)}
|
|
||||||
style={listItemStyles}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
Object.assign(e.currentTarget.style, listItemHoverStyles);
|
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onClick={() => handleTaskClick(record.task_id, record.project_id)}
|
||||||
Object.assign(e.currentTarget.style, listItemStyles);
|
aria-label={`${t('tasks.recentTaskAriaLabel')} ${record.task_name}`}
|
||||||
}}
|
|
||||||
aria-label={`${t('recentTaskAriaLabel')} ${item.task_name}`}
|
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
<div style={{
|
||||||
<div style={iconStyles}>
|
marginTop: 2,
|
||||||
<FileTextOutlined />
|
color: token.colorPrimary,
|
||||||
|
fontSize: 16
|
||||||
|
}}>
|
||||||
|
<FileTextOutlined />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<Text strong style={{ fontSize: 14 }}>
|
||||||
|
{record.task_name}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
<Text ellipsis style={taskNameStyles}>
|
{record.project_name}
|
||||||
{item.task_name}
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
title={formatDate(record.last_activity_at, 'MMMM Do YYYY, h:mm:ss a')}
|
||||||
|
placement="topRight"
|
||||||
|
>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{fromNow(record.last_activity_at)}
|
||||||
</Text>
|
</Text>
|
||||||
<Tag style={tagStyles}>
|
</Tooltip>
|
||||||
{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>
|
||||||
</List.Item>
|
</div>
|
||||||
)}
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
className="custom-two-colors-row-table"
|
||||||
|
dataSource={tasks}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="task_id"
|
||||||
|
showHeader={false}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { List, Typography, Tag, Tooltip, Space, theme } from 'antd';
|
import { Table, Typography, Tag, Tooltip, Space, theme } from '@/shared/antd-imports';
|
||||||
import { ClockCircleOutlined } from '@ant-design/icons';
|
import { ClockCircleOutlined } from '@/shared/antd-imports';
|
||||||
import moment from 'moment';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { fromNow, formatDate } from '@/utils/dateUtils';
|
||||||
import {
|
import {
|
||||||
setSelectedTaskId,
|
setSelectedTaskId,
|
||||||
setShowTaskDrawer,
|
setShowTaskDrawer,
|
||||||
@@ -22,15 +22,6 @@ const TimeLoggedTaskList: React.FC<TimeLoggedTaskListProps> = React.memo(({ task
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { token } = theme.useToken();
|
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) => {
|
||||||
dispatch(setSelectedTaskId(taskId));
|
dispatch(setSelectedTaskId(taskId));
|
||||||
@@ -40,160 +31,134 @@ const TimeLoggedTaskList: React.FC<TimeLoggedTaskListProps> = React.memo(({ task
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Enhanced styling with theme support
|
const columns = [
|
||||||
const listItemStyles = useMemo(() => ({
|
{
|
||||||
padding: '16px 20px',
|
key: 'task',
|
||||||
borderBottom: isDarkMode ? '1px solid #404040' : '1px solid #f0f2f5',
|
render: (record: IUserTimeLoggedTask) => (
|
||||||
cursor: 'pointer',
|
<div
|
||||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
style={{
|
||||||
borderRadius: '8px',
|
display: 'flex',
|
||||||
margin: '0 0 2px 0',
|
alignItems: 'center',
|
||||||
background: isDarkMode ? 'transparent' : 'transparent',
|
gap: 10,
|
||||||
position: 'relative' as const,
|
width: '100%',
|
||||||
overflow: 'hidden',
|
cursor: 'pointer',
|
||||||
}), [isDarkMode]);
|
padding: '8px 0'
|
||||||
|
}}
|
||||||
|
onClick={() => handleTaskClick(record.task_id, record.project_id)}
|
||||||
|
aria-label={`${t('tasks.timeLoggedTaskAriaLabel')} ${record.task_name}`}
|
||||||
|
>
|
||||||
|
{/* Clock Icon */}
|
||||||
|
<div style={{
|
||||||
|
color: token.colorSuccess,
|
||||||
|
fontSize: 14,
|
||||||
|
flexShrink: 0
|
||||||
|
}}>
|
||||||
|
<ClockCircleOutlined />
|
||||||
|
</div>
|
||||||
|
|
||||||
const listItemHoverStyles = useMemo(() => ({
|
{/* Main Content */}
|
||||||
background: isDarkMode
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
? 'linear-gradient(135deg, #2a2a2a 0%, #353535 100%)'
|
{/* Task Name */}
|
||||||
: 'linear-gradient(135deg, #f6ffed 0%, #f0fff4 100%)',
|
<div style={{ marginBottom: 2 }}>
|
||||||
borderColor: isDarkMode ? '#505050' : '#b7eb8f',
|
<Text
|
||||||
transform: 'translateY(-1px)',
|
strong
|
||||||
boxShadow: isDarkMode
|
style={{
|
||||||
? '0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2)'
|
fontSize: 13,
|
||||||
: '0 4px 16px rgba(82, 196, 26, 0.15), 0 1px 4px rgba(0, 0, 0, 0.1)',
|
lineHeight: 1.4,
|
||||||
}), [isDarkMode]);
|
color: token.colorText
|
||||||
|
}}
|
||||||
|
ellipsis={{ tooltip: record.task_name }}
|
||||||
|
>
|
||||||
|
{record.task_name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
const iconStyles = useMemo(() => ({
|
{/* Project Name */}
|
||||||
color: isDarkMode ? '#73d13d' : '#52c41a',
|
<Text
|
||||||
fontSize: '16px',
|
type="secondary"
|
||||||
padding: '8px',
|
style={{
|
||||||
borderRadius: '6px',
|
fontSize: 11,
|
||||||
background: isDarkMode
|
lineHeight: 1.2,
|
||||||
? 'linear-gradient(135deg, #1b2918 0%, #273622 100%)'
|
display: 'block',
|
||||||
: 'linear-gradient(135deg, #f6ffed 0%, #f0fff4 100%)',
|
marginBottom: 4
|
||||||
border: isDarkMode ? '1px solid #52c41a20' : '1px solid #52c41a20',
|
}}
|
||||||
display: 'flex',
|
ellipsis={{ tooltip: record.project_name }}
|
||||||
alignItems: 'center',
|
>
|
||||||
justifyContent: 'center',
|
{record.project_name}
|
||||||
minWidth: '32px',
|
</Text>
|
||||||
minHeight: '32px',
|
</div>
|
||||||
}), [isDarkMode]);
|
|
||||||
|
|
||||||
const taskNameStyles = useMemo(() => ({
|
{/* Right Side - Time and Status */}
|
||||||
color: isDarkMode ? '#ffffff' : '#1f2937',
|
<div style={{
|
||||||
fontSize: '15px',
|
display: 'flex',
|
||||||
fontWeight: 600,
|
flexDirection: 'column',
|
||||||
lineHeight: '1.4',
|
alignItems: 'flex-end',
|
||||||
}), [isDarkMode]);
|
gap: 3,
|
||||||
|
flexShrink: 0
|
||||||
|
}}>
|
||||||
|
{/* Time Logged */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<Tag
|
||||||
|
color="success"
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '0 6px',
|
||||||
|
height: 18,
|
||||||
|
lineHeight: '16px',
|
||||||
|
borderRadius: 3
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{record.total_time_logged_string}
|
||||||
|
</Tag>
|
||||||
|
{record.logged_by_timer && (
|
||||||
|
<Tag
|
||||||
|
color="processing"
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 10,
|
||||||
|
padding: '0 4px',
|
||||||
|
height: 16,
|
||||||
|
lineHeight: '14px',
|
||||||
|
borderRadius: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('tasks.timerTag')}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
const timeLogTagStyles = useMemo(() => ({
|
{/* Time Ago */}
|
||||||
background: isDarkMode
|
<Tooltip
|
||||||
? 'linear-gradient(135deg, #365314 0%, #4d7c0f 100%)'
|
title={formatDate(record.last_logged_at, 'MMMM Do YYYY, h:mm:ss a')}
|
||||||
: 'linear-gradient(135deg, #f0fff4 0%, #d9f7be 100%)',
|
placement="topRight"
|
||||||
color: isDarkMode ? '#ffffff' : '#365314',
|
>
|
||||||
border: isDarkMode ? '1px solid #4d7c0f' : '1px solid #95de64',
|
<Text
|
||||||
borderRadius: '6px',
|
type="secondary"
|
||||||
fontSize: '11px',
|
style={{
|
||||||
fontWeight: 600,
|
fontSize: 10,
|
||||||
padding: '2px 8px',
|
lineHeight: 1,
|
||||||
textTransform: 'uppercase' as const,
|
color: token.colorTextTertiary
|
||||||
letterSpacing: '0.5px',
|
}}
|
||||||
}), [isDarkMode]);
|
>
|
||||||
|
{fromNow(record.last_logged_at)}
|
||||||
const timerTagStyles = useMemo(() => ({
|
</Text>
|
||||||
background: isDarkMode
|
</Tooltip>
|
||||||
? 'linear-gradient(135deg, #0f766e 0%, #14b8a6 100%)'
|
</div>
|
||||||
: 'linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%)',
|
</div>
|
||||||
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
|
<Table
|
||||||
|
className="custom-two-colors-row-table"
|
||||||
dataSource={tasks}
|
dataSource={tasks}
|
||||||
style={{ background: 'transparent' }}
|
columns={columns}
|
||||||
split={false}
|
rowKey="task_id"
|
||||||
renderItem={item => (
|
showHeader={false}
|
||||||
<List.Item
|
pagination={false}
|
||||||
onClick={() => handleTaskClick(item.task_id, item.project_id)}
|
size="small"
|
||||||
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}`}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
||||||
<div style={iconStyles}>
|
|
||||||
<ClockCircleOutlined />
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
|
|
||||||
<Text ellipsis style={taskNameStyles}>
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo, useCallback, useEffect } 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, Button, Tooltip } from '@/shared/antd-imports';
|
||||||
import { ClockCircleOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
import { ClockCircleOutlined, UnorderedListOutlined, SyncOutlined } from '@/shared/antd-imports';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
@@ -54,11 +54,12 @@ const UserActivityFeed: React.FC = () => {
|
|||||||
}
|
}
|
||||||
// If it's an object with a data property (common API pattern)
|
// If it's an object with a data property (common API pattern)
|
||||||
if (recentTasksData && typeof recentTasksData === 'object' && 'data' in recentTasksData) {
|
if (recentTasksData && typeof recentTasksData === 'object' && 'data' in recentTasksData) {
|
||||||
return Array.isArray(recentTasksData.data) ? recentTasksData.data : [];
|
const data = (recentTasksData as any).data;
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
}
|
}
|
||||||
// If it's a different object structure, try to extract tasks
|
// If it's a different object structure, try to extract tasks
|
||||||
if (recentTasksData && typeof recentTasksData === 'object') {
|
if (recentTasksData && typeof recentTasksData === 'object') {
|
||||||
const possibleArrays = Object.values(recentTasksData).filter(Array.isArray);
|
const possibleArrays = Object.values(recentTasksData as any).filter(Array.isArray);
|
||||||
return possibleArrays.length > 0 ? possibleArrays[0] : [];
|
return possibleArrays.length > 0 ? possibleArrays[0] : [];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -72,11 +73,12 @@ const UserActivityFeed: React.FC = () => {
|
|||||||
}
|
}
|
||||||
// If it's an object with a data property (common API pattern)
|
// If it's an object with a data property (common API pattern)
|
||||||
if (timeLoggedTasksData && typeof timeLoggedTasksData === 'object' && 'data' in timeLoggedTasksData) {
|
if (timeLoggedTasksData && typeof timeLoggedTasksData === 'object' && 'data' in timeLoggedTasksData) {
|
||||||
return Array.isArray(timeLoggedTasksData.data) ? timeLoggedTasksData.data : [];
|
const data = (timeLoggedTasksData as any).data;
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
}
|
}
|
||||||
// If it's a different object structure, try to extract tasks
|
// If it's a different object structure, try to extract tasks
|
||||||
if (timeLoggedTasksData && typeof timeLoggedTasksData === 'object') {
|
if (timeLoggedTasksData && typeof timeLoggedTasksData === 'object') {
|
||||||
const possibleArrays = Object.values(timeLoggedTasksData).filter(Array.isArray);
|
const possibleArrays = Object.values(timeLoggedTasksData as any).filter(Array.isArray);
|
||||||
return possibleArrays.length > 0 ? possibleArrays[0] : [];
|
return possibleArrays.length > 0 ? possibleArrays[0] : [];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -85,21 +87,21 @@ const UserActivityFeed: React.FC = () => {
|
|||||||
const segmentOptions = useMemo(
|
const segmentOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
value: ActivityFeedType.RECENT_TASKS,
|
value: ActivityFeedType.TIME_LOGGED_TASKS,
|
||||||
label: (
|
label: (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<UnorderedListOutlined />
|
<ClockCircleOutlined style={{ fontSize: 14 }} />
|
||||||
<span>{t('Recent Tasks')}</span>
|
{t('tasks.timeLoggedSegment')}
|
||||||
</div>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: ActivityFeedType.TIME_LOGGED_TASKS,
|
value: ActivityFeedType.RECENT_TASKS,
|
||||||
label: (
|
label: (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<ClockCircleOutlined />
|
<UnorderedListOutlined style={{ fontSize: 14 }} />
|
||||||
<span>{t('Time Logged Tasks')}</span>
|
{t('tasks.recentTasksSegment')}
|
||||||
</div>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -122,41 +124,77 @@ const UserActivityFeed: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [activeTab, refetchRecentTasks, refetchTimeLoggedTasks]);
|
}, [activeTab, refetchRecentTasks, refetchTimeLoggedTasks]);
|
||||||
|
|
||||||
const renderContent = () => {
|
const handleRefresh = useCallback(() => {
|
||||||
if (activeTab === ActivityFeedType.RECENT_TASKS) {
|
if (activeTab === ActivityFeedType.TIME_LOGGED_TASKS) {
|
||||||
if (loadingRecentTasks) {
|
refetchTimeLoggedTasks();
|
||||||
return <Skeleton active />;
|
|
||||||
}
|
|
||||||
if (recentTasksError) {
|
|
||||||
return <Alert message={t('Error Loading Recent Tasks')} type="error" showIcon />;
|
|
||||||
}
|
|
||||||
if (recentTasks.length === 0) {
|
|
||||||
return <Empty description={t('No Recent Tasks')} />;
|
|
||||||
}
|
|
||||||
return <TaskActivityList tasks={recentTasks} />;
|
|
||||||
} else {
|
} else {
|
||||||
|
refetchRecentTasks();
|
||||||
|
}
|
||||||
|
}, [activeTab, refetchRecentTasks, refetchTimeLoggedTasks]);
|
||||||
|
|
||||||
|
const isLoading = activeTab === ActivityFeedType.TIME_LOGGED_TASKS ? loadingTimeLoggedTasks : loadingRecentTasks;
|
||||||
|
const currentCount = activeTab === ActivityFeedType.TIME_LOGGED_TASKS ? timeLoggedTasks.length : recentTasks.length;
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (activeTab === ActivityFeedType.TIME_LOGGED_TASKS) {
|
||||||
if (loadingTimeLoggedTasks) {
|
if (loadingTimeLoggedTasks) {
|
||||||
return <Skeleton active />;
|
return <Skeleton active />;
|
||||||
}
|
}
|
||||||
if (timeLoggedTasksError) {
|
if (timeLoggedTasksError) {
|
||||||
return <Alert message={t('Error Loading Time Logged Tasks')} type="error" showIcon />;
|
return <Alert message={t('tasks.errorLoadingTimeLoggedTasks')} type="error" showIcon />;
|
||||||
}
|
}
|
||||||
if (timeLoggedTasks.length === 0) {
|
if (timeLoggedTasks.length === 0) {
|
||||||
return <Empty description={t('No Time Logged Tasks')} />;
|
return <Empty description={t('tasks.noTimeLoggedTasks')} />;
|
||||||
}
|
}
|
||||||
return <TimeLoggedTaskList tasks={timeLoggedTasks} />;
|
return (
|
||||||
|
<div style={{ maxHeight: 450, overflow: 'auto' }}>
|
||||||
|
<TimeLoggedTaskList tasks={timeLoggedTasks} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (activeTab === ActivityFeedType.RECENT_TASKS) {
|
||||||
|
if (loadingRecentTasks) {
|
||||||
|
return <Skeleton active />;
|
||||||
|
}
|
||||||
|
if (recentTasksError) {
|
||||||
|
return <Alert message={t('tasks.errorLoadingRecentTasks')} type="error" showIcon />;
|
||||||
|
}
|
||||||
|
if (recentTasks.length === 0) {
|
||||||
|
return <Empty description={t('tasks.noRecentTasks')} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ maxHeight: 450, overflow: 'auto' }}>
|
||||||
|
<TaskActivityList tasks={recentTasks} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('Recent Activity')}>
|
<Card
|
||||||
<div style={{ marginBottom: 16 }}>
|
title={
|
||||||
<Segmented
|
<Typography.Title level={5} style={{ marginBlockEnd: 0 }}>
|
||||||
options={segmentOptions}
|
{t('tasks.recentActivity')} ({currentCount})
|
||||||
value={activeTab}
|
</Typography.Title>
|
||||||
onChange={handleTabChange}
|
}
|
||||||
/>
|
extra={
|
||||||
</div>
|
<Tooltip title={t('tasks.refresh')}>
|
||||||
|
<Button
|
||||||
|
shape="circle"
|
||||||
|
icon={<SyncOutlined spin={isLoading} />}
|
||||||
|
onClick={handleRefresh}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Segmented
|
||||||
|
options={segmentOptions}
|
||||||
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
style={{ marginBottom: 16, width: '100%' }}
|
||||||
|
block
|
||||||
|
/>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,32 +1,59 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
|
import 'dayjs/locale/de';
|
||||||
|
import 'dayjs/locale/es';
|
||||||
|
import 'dayjs/locale/pt';
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
import { getLanguageFromLocalStorage } from './language-utils';
|
||||||
|
|
||||||
// Initialize the relativeTime plugin
|
// Initialize plugins
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.extend(localizedFormat);
|
||||||
|
|
||||||
/**
|
// Map application languages to dayjs locales
|
||||||
* Formats a date to a relative time string (e.g., "2 hours ago", "a day ago")
|
const getLocaleFromLanguage = (language: string): string => {
|
||||||
* This mimics the Angular fromNow pipe functionality
|
const localeMap: Record<string, string> = {
|
||||||
*
|
'en': 'en',
|
||||||
* @param date - The date to format (string, Date, or dayjs object)
|
'de': 'de',
|
||||||
* @returns A string representing the relative time
|
'es': 'es',
|
||||||
*/
|
'pt': 'pt',
|
||||||
export const fromNow = (date: string | Date | dayjs.Dayjs): string => {
|
'alb': 'en', // Albanian not supported by dayjs, fallback to English
|
||||||
if (!date) return '';
|
'zh': 'zh-cn'
|
||||||
return dayjs(date).fromNow();
|
};
|
||||||
|
return localeMap[language] || 'en';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a date to a specific format
|
* Formats a date to a relative time string (e.g., "2 hours ago", "a day ago")
|
||||||
|
* This mimics the Angular fromNow pipe functionality with locale support
|
||||||
|
*
|
||||||
|
* @param date - The date to format (string, Date, or dayjs object)
|
||||||
|
* @param language - Optional language override (defaults to stored language)
|
||||||
|
* @returns A string representing the relative time
|
||||||
|
*/
|
||||||
|
export const fromNow = (date: string | Date | dayjs.Dayjs, language?: string): string => {
|
||||||
|
if (!date) return '';
|
||||||
|
const currentLanguage = language || getLanguageFromLocalStorage();
|
||||||
|
const locale = getLocaleFromLanguage(currentLanguage);
|
||||||
|
return dayjs(date).locale(locale).fromNow();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a date to a specific format with locale support
|
||||||
*
|
*
|
||||||
* @param date - The date to format (string, Date, or dayjs object)
|
* @param date - The date to format (string, Date, or dayjs object)
|
||||||
* @param format - The format string (default: 'YYYY-MM-DD')
|
* @param format - The format string (default: 'YYYY-MM-DD')
|
||||||
|
* @param language - Optional language override (defaults to stored language)
|
||||||
* @returns A formatted date string
|
* @returns A formatted date string
|
||||||
*/
|
*/
|
||||||
export const formatDate = (
|
export const formatDate = (
|
||||||
date: string | Date | dayjs.Dayjs,
|
date: string | Date | dayjs.Dayjs,
|
||||||
format: string = 'YYYY-MM-DD'
|
format: string = 'YYYY-MM-DD',
|
||||||
|
language?: string
|
||||||
): string => {
|
): string => {
|
||||||
if (!date) return '';
|
if (!date) return '';
|
||||||
return dayjs(date).format(format);
|
const currentLanguage = language || getLanguageFromLocalStorage();
|
||||||
|
const locale = getLocaleFromLanguage(currentLanguage);
|
||||||
|
return dayjs(date).locale(locale).format(format);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user