+
+
+ {task.name}
-
onTaskDragStart(e, task.id!, groupId)}
- onDragOver={e => {
- e.preventDefault();
- const rect = e.currentTarget.getBoundingClientRect();
- const offsetY = e.clientY - rect.top;
- const isDown = offsetY > rect.height / 2;
- setIsDown(isDown);
- onTaskDragOver(e, groupId, isDown ? idx + 1 : idx);
+
+
+
+
+
onTaskDrop(e, groupId, idx)}
- onDragEnd={onDragEnd} // <-- add this
- onClick={e => handleCardClick(e, task.id!)}
- >
-
-
- {task.labels?.map(label => (
-
- {label.name}
-
- ))}
+ onClick={handleDateClick}
+ title={t('clickToChangeDate')}
+ >
+ {isUpdating ? (
+
+ ) : selectedDate ? (
+ format(selectedDate, 'MMM d, yyyy')
+ ) : (
+ t('noDueDate')
+ )}
+
+ {/* Custom Calendar Popup */}
+ {showDatePicker && dropdownPosition && (
+
+ e.stopPropagation()}
+ >
+
+
+
+ {calendarMonth.toLocaleString('default', { month: 'long' })} {year}
+
+
-
-
-
-
-
- {isUpdating ? (
-
- ) : (
- selectedDate ? format(selectedDate, 'MMM d, yyyy') : t('noDueDate')
- )}
-
- {/* Custom Calendar Popup */}
- {showDatePicker && dropdownPosition && (
-
- e.stopPropagation()}
- >
-
-
-
- {calendarMonth.toLocaleString('default', { month: 'long' })} {year}
-
-
-
-
- {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(d => (
-
{d}
- ))}
- {weeks.map((week, i) => (
-
- {week.map((date, j) => {
- const isSelected = date && selectedDate && date.toDateString() === selectedDate.toDateString();
- const isToday = date && date.toDateString() === today.toDateString();
- return (
-
- );
- })}
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
- {(task.sub_tasks_count ?? 0) > 0 && (
-
- )}
+
+ {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(d => (
+
+ {d}
+ ))}
+ {weeks.map((week, i) => (
+
+ {week.map((date, j) => {
+ const isSelected =
+ date &&
+ selectedDate &&
+ date.toDateString() === selectedDate.toDateString();
+ const isToday =
+ date && date.toDateString() === today.toDateString();
+ return (
+
+ );
+ })}
+
+ ))}
-
+
+
+
+
+
+
+
+
+
+
+ )}
-
-
- {/* Loading state */}
- {task.sub_tasks_loading && (
-
- )}
- {/* Loaded subtasks */}
- {!task.sub_tasks_loading && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0 && (
-
- {task.sub_tasks.map(sub => (
- - handleCardClick(e, sub.id!)} className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-50 dark:hover:bg-gray-800">
- {sub.priority_color || sub.priority_color_dark ? (
-
- ) : null}
- {sub.name}
-
- {sub.end_date ? format(new Date(sub.end_date), 'MMM d, yyyy') : ''}
-
-
- {sub.names && sub.names.length > 0 && (
-
- )}
-
-
-
- ))}
-
- )}
- {/* Empty state */}
- {!task.sub_tasks_loading && (!Array.isArray(task.sub_tasks) || task.sub_tasks.length === 0) && (
-
{t('noSubtasks', 'No subtasks')}
- )}
-
+
+
+
+ {(task.sub_tasks_count ?? 0) > 0 && (
+
+ )}
+
+
+
+
+
+ {/* Loading state */}
+ {task.sub_tasks_loading && (
+
+ )}
+ {/* Loaded subtasks */}
+ {!task.sub_tasks_loading &&
+ Array.isArray(task.sub_tasks) &&
+ task.sub_tasks.length > 0 && (
+
+ {task.sub_tasks.map(sub => (
+ - handleCardClick(e, sub.id!)}
+ className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-50 dark:hover:bg-gray-800"
+ >
+ {sub.priority_color || sub.priority_color_dark ? (
+
+ ) : null}
+
+ {sub.name}
+
+
+ {sub.end_date ? format(new Date(sub.end_date), 'MMM d, yyyy') : ''}
+
+
+ {sub.names && sub.names.length > 0 && (
+
+ )}
+
+
+
+ ))}
+
+ )}
+ {/* Empty state */}
+ {!task.sub_tasks_loading &&
+ (!Array.isArray(task.sub_tasks) || task.sub_tasks.length === 0) && (
+
+ {t('noSubtasks', 'No subtasks')}
+
+ )}
- >
+
+
+ >
);
-});
+ }
+);
TaskCard.displayName = 'TaskCard';
-export default TaskCard;
\ No newline at end of file
+export default TaskCard;
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskProgressCircle.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskProgressCircle.tsx
index 044b62578..9ad7acddd 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskProgressCircle.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/TaskProgressCircle.tsx
@@ -1,52 +1,72 @@
-import { IProjectTask } from "@/types/project/projectTasksViewModel.types";
+import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
// Add a simple circular progress component
-const TaskProgressCircle: React.FC<{ task: IProjectTask; size?: number }> = ({ task, size = 28 }) => {
- const progress = typeof task.complete_ratio === 'number'
- ? task.complete_ratio
- : (typeof task.progress === 'number' ? task.progress : 0);
- const strokeWidth = 1.5;
- const radius = (size - strokeWidth) / 2;
- const circumference = 2 * Math.PI * radius;
- const offset = circumference - (progress / 100) * circumference;
- return (
-
+ );
};
-export default TaskProgressCircle;
\ No newline at end of file
+export default TaskProgressCircle;
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/index.ts b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/index.ts
index 1ddc40f17..c5d954c48 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/index.ts
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/index.ts
@@ -1,3 +1,3 @@
export { default } from './EnhancedKanbanBoardNativeDnD';
export { default as TaskCard } from './TaskCard';
-export { default as KanbanGroup } from './KanbanGroup';
\ No newline at end of file
+export { default as KanbanGroup } from './KanbanGroup';
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSection.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSection.tsx
index 7aec4fe7b..bfbc77da4 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSection.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanCreateSection.tsx
@@ -68,7 +68,8 @@ const EnhancedKanbanCreateSection: React.FC = () => {
!dropdownRef.current.contains(event.target as Node) &&
inputRef.current &&
!inputRef.current.contains(event.target as Node) &&
- (!categoryDropdownRef.current || !categoryDropdownRef.current.contains(event.target as Node))
+ (!categoryDropdownRef.current ||
+ !categoryDropdownRef.current.contains(event.target as Node))
) {
setIsAdding(false);
setSectionName('');
@@ -109,7 +110,11 @@ const EnhancedKanbanCreateSection: React.FC = () => {
setIsAdding(true);
setSectionName('');
// Default to first category if available
- if (statusCategories && statusCategories.length > 0 && typeof statusCategories[0].id === 'string') {
+ if (
+ statusCategories &&
+ statusCategories.length > 0 &&
+ typeof statusCategories[0].id === 'string'
+ ) {
setSelectedCategoryId(statusCategories[0].id);
} else {
setSelectedCategoryId('');
@@ -217,10 +222,15 @@ const EnhancedKanbanCreateSection: React.FC = () => {
style={{ minWidth: 80 }}
onClick={() => setShowCategoryDropdown(v => !v)}
>
-
+
{selectedCategory?.name || t('changeCategory')}
-
+
{showCategoryDropdown && (
{
style={{ zIndex: 1000 }}
>
- {statusCategories.filter(cat => typeof cat.id === 'string').map(cat => (
-
- ))}
+ {statusCategories
+ .filter(cat => typeof cat.id === 'string')
+ .map(cat => (
+
+ ))}
)}
@@ -263,7 +277,12 @@ const EnhancedKanbanCreateSection: React.FC = () => {
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.css b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.css
index a8d563dcf..3a7fdac4d 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.css
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.css
@@ -15,7 +15,9 @@
html.light .enhanced-kanban-task-card {
border: 1.5px solid #e1e4e8 !important; /* Asana-like light border */
- box-shadow: 0 1px 4px 0 rgba(60, 64, 67, 0.08), 0 0.5px 1.5px 0 rgba(60, 64, 67, 0.03);
+ box-shadow:
+ 0 1px 4px 0 rgba(60, 64, 67, 0.08),
+ 0 0.5px 1.5px 0 rgba(60, 64, 67, 0.03);
background: #fff !important;
}
diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx
index 82f79c55a..165e87eba 100644
--- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx
+++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx
@@ -182,14 +182,24 @@ const EnhancedKanbanTaskCard: React.FC = React.memo
isDarkMode={themeMode === 'dark'}
size={24}
/>
-
+
{/* Subtask Section - only show if count > 1 */}
{task.sub_tasks_count != null && Number(task.sub_tasks_count) > 1 && (
-
+
)}
{task.comments_count && task.comments_count > 1 && (
-
+
{task.comments_count}
)}
{task.attachments_count && task.attachments_count > 1 && (
-
+
{task.attachments_count}
diff --git a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskListBoard.tsx b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskListBoard.tsx
index 9ea40133d..0f330aa78 100644
--- a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskListBoard.tsx
+++ b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskListBoard.tsx
@@ -22,7 +22,7 @@ import { RootState } from '@/app/store';
import { fetchTaskGroups, reorderTasks } from '@/features/tasks/tasks.slice';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { AppDispatch } from '@/app/store';
-import { useAuthService } from '@/hooks/useAuth';
+import { useAuthService } from '@/hooks/useAuth';
import useIsProjectManager from '@/hooks/useIsProjectManager';
import KanbanGroup from './kanbanGroup';
import KanbanTaskCard from './kanbanTaskCard';
diff --git a/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx b/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx
index 4c742efe7..5fc7ed1a8 100644
--- a/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx
+++ b/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx
@@ -124,19 +124,19 @@ const ProjectGroupList: React.FC = ({
e.stopPropagation();
console.log('Opening project drawer from project group for project:', projectId);
trackMixpanelEvent(evt_projects_settings_click);
-
+
// Set project ID first
dispatch(setProjectId(projectId));
-
+
// Then fetch project data
dispatch(fetchProjectData(projectId))
.unwrap()
- .then((projectData) => {
+ .then(projectData => {
console.log('Project data fetched successfully from project group:', projectData);
// Open drawer after data is fetched
dispatch(toggleProjectDrawer());
})
- .catch((error) => {
+ .catch(error => {
console.error('Failed to fetch project data from project group:', error);
// Still open drawer even if fetch fails, so user can see error state
dispatch(toggleProjectDrawer());
diff --git a/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx b/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx
index 4a319a445..a9f678c76 100644
--- a/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx
+++ b/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx
@@ -47,19 +47,19 @@ export const ActionButtons: React.FC = ({
if (record.id) {
console.log('Opening project drawer for project:', record.id);
trackMixpanelEvent(evt_projects_settings_click);
-
+
// Set project ID first
dispatch(setProjectId(record.id));
-
+
// Then fetch project data
dispatch(fetchProjectData(record.id))
.unwrap()
- .then((projectData) => {
+ .then(projectData => {
console.log('Project data fetched successfully:', projectData);
// Open drawer after data is fetched
dispatch(toggleProjectDrawer());
})
- .catch((error) => {
+ .catch(error => {
console.error('Failed to fetch project data:', error);
// Still open drawer even if fetch fails, so user can see error state
dispatch(toggleProjectDrawer());
diff --git a/worklenz-frontend/src/components/project-task-filters/create-status-button/create-status-button.tsx b/worklenz-frontend/src/components/project-task-filters/create-status-button/create-status-button.tsx
index 29aefae57..fd63c6a11 100644
--- a/worklenz-frontend/src/components/project-task-filters/create-status-button/create-status-button.tsx
+++ b/worklenz-frontend/src/components/project-task-filters/create-status-button/create-status-button.tsx
@@ -19,11 +19,7 @@ const CreateStatusButton = () => {
className="borderless-icon-btn"
style={{ backgroundColor: colors.transparent, boxShadow: 'none' }}
onClick={() => dispatch(toggleDrawer())}
- icon={
-
- }
+ icon={}
/>
);
diff --git a/worklenz-frontend/src/components/project-task-filters/create-status-drawer/create-status-drawer.tsx b/worklenz-frontend/src/components/project-task-filters/create-status-drawer/create-status-drawer.tsx
index f51925cfa..c4450e245 100644
--- a/worklenz-frontend/src/components/project-task-filters/create-status-drawer/create-status-drawer.tsx
+++ b/worklenz-frontend/src/components/project-task-filters/create-status-drawer/create-status-drawer.tsx
@@ -14,7 +14,11 @@ import { toggleDrawer } from '@/features/projects/status/StatusSlice';
import './create-status-drawer.css';
-import { createStatus, fetchStatusesCategories, fetchStatuses } from '@/features/taskAttributes/taskStatusSlice';
+import {
+ createStatus,
+ fetchStatusesCategories,
+ fetchStatuses,
+} from '@/features/taskAttributes/taskStatusSlice';
import { ITaskStatusCategory } from '@/types/status.types';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
import useTabSearchParam from '@/hooks/useTabSearchParam';
diff --git a/worklenz-frontend/src/components/project-task-filters/delete-status-drawer/delete-status-drawer.tsx b/worklenz-frontend/src/components/project-task-filters/delete-status-drawer/delete-status-drawer.tsx
index 3b6b7fd0a..79b61eed5 100644
--- a/worklenz-frontend/src/components/project-task-filters/delete-status-drawer/delete-status-drawer.tsx
+++ b/worklenz-frontend/src/components/project-task-filters/delete-status-drawer/delete-status-drawer.tsx
@@ -12,10 +12,7 @@ import { deleteStatusToggleDrawer } from '@/features/projects/status/DeleteStatu
import { Drawer, Alert, Card, Select, Button, Typography, Badge } from '@/shared/antd-imports';
import { DownOutlined } from '@/shared/antd-imports';
import { useSelector } from 'react-redux';
-import {
- deleteSection,
- IGroupBy,
-} from '@features/enhanced-kanban/enhanced-kanban.slice';
+import { deleteSection, IGroupBy } from '@features/enhanced-kanban/enhanced-kanban.slice';
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service';
import logger from '@/utils/errorLogger';
diff --git a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx
index ca56a7611..34312c043 100644
--- a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx
+++ b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/column-configuration-modal.tsx
@@ -1,5 +1,14 @@
import React, { useState, useEffect } from 'react';
-import { Modal, Checkbox, Button, Flex, Typography, Space, Divider, message } from '@/shared/antd-imports';
+import {
+ Modal,
+ Checkbox,
+ Button,
+ Flex,
+ Typography,
+ Space,
+ Divider,
+ message,
+} from '@/shared/antd-imports';
import { SettingOutlined, UpOutlined, DownOutlined } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
diff --git a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/members-filter-dropdown.tsx b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/members-filter-dropdown.tsx
index 051f4522e..597327f3f 100644
--- a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/members-filter-dropdown.tsx
+++ b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/members-filter-dropdown.tsx
@@ -13,7 +13,7 @@ import {
List,
Space,
Typography,
- InputRef
+ InputRef,
} from '@/shared/antd-imports';
import { useAppDispatch } from '@/hooks/useAppDispatch';
diff --git a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/sort-filter-dropdown.tsx b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/sort-filter-dropdown.tsx
index c86d2ff8a..095437b4c 100644
--- a/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/sort-filter-dropdown.tsx
+++ b/worklenz-frontend/src/components/project-task-filters/filter-dropdowns/sort-filter-dropdown.tsx
@@ -1,4 +1,8 @@
-import { CaretDownFilled, SortAscendingOutlined, SortDescendingOutlined } from '@/shared/antd-imports';
+import {
+ CaretDownFilled,
+ SortAscendingOutlined,
+ SortDescendingOutlined,
+} from '@/shared/antd-imports';
import Badge from 'antd/es/badge';
import Button from 'antd/es/button';
diff --git a/worklenz-frontend/src/components/projects/import-ratecards-drawer/ImportRateCardsDrawer.tsx b/worklenz-frontend/src/components/projects/import-ratecards-drawer/ImportRateCardsDrawer.tsx
index b24098fe6..69b23297c 100644
--- a/worklenz-frontend/src/components/projects/import-ratecards-drawer/ImportRateCardsDrawer.tsx
+++ b/worklenz-frontend/src/components/projects/import-ratecards-drawer/ImportRateCardsDrawer.tsx
@@ -25,7 +25,8 @@ const ImportRateCardsDrawer: React.FC = () => {
const fallbackCurrency = useAppSelector(state => state.financeReducer.currency);
const currency = (projectCurrency || fallbackCurrency || 'USD').toUpperCase();
- const rolesRedux = useAppSelector(state => state.projectFinanceRateCardReducer.rateCardRoles) || [];
+ const rolesRedux =
+ useAppSelector(state => state.projectFinanceRateCardReducer.rateCardRoles) || [];
// Loading states
const isRatecardsLoading = useAppSelector(state => state.financeReducer.isRatecardsLoading);
diff --git a/worklenz-frontend/src/components/projects/project-drawer/project-client-section/project-client-section.tsx b/worklenz-frontend/src/components/projects/project-drawer/project-client-section/project-client-section.tsx
index d8e2a4b85..8714fef1a 100644
--- a/worklenz-frontend/src/components/projects/project-drawer/project-client-section/project-client-section.tsx
+++ b/worklenz-frontend/src/components/projects/project-drawer/project-client-section/project-client-section.tsx
@@ -3,7 +3,15 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
import { IClientsViewModel } from '@/types/client.types';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { QuestionCircleOutlined } from '@/shared/antd-imports';
-import { AutoComplete, Flex, Form, FormInstance, Spin, Tooltip, Typography } from '@/shared/antd-imports';
+import {
+ AutoComplete,
+ Flex,
+ Form,
+ FormInstance,
+ Spin,
+ Tooltip,
+ Typography,
+} from '@/shared/antd-imports';
import { TFunction } from 'i18next';
import { useState } from 'react';
diff --git a/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx b/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx
index 2b6ef0c0e..db16d3e82 100644
--- a/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx
+++ b/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx
@@ -137,7 +137,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
if (drawerVisible && projectId && project && !projectLoading) {
console.log('Populating form with project data:', project);
setEditMode(true);
-
+
try {
form.setFieldsValue({
...project,
@@ -148,7 +148,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
use_weighted_progress: project.use_weighted_progress || false,
use_time_progress: project.use_time_progress || false,
});
-
+
setSelectedProjectManager(project.project_manager || null);
setLoading(false);
console.log('Form populated successfully with project data');
@@ -286,7 +286,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
(visible: boolean) => {
console.log('Drawer visibility changed:', visible, 'Project ID:', projectId);
setDrawerVisible(visible);
-
+
if (!visible) {
resetForm();
} else if (visible && !projectId) {
diff --git a/worklenz-frontend/src/components/reporting/drawers/overview-team-info/projects-tab/reporting-overview-projects-table.tsx b/worklenz-frontend/src/components/reporting/drawers/overview-team-info/projects-tab/reporting-overview-projects-table.tsx
index a6afb9eba..f555b4fb2 100644
--- a/worklenz-frontend/src/components/reporting/drawers/overview-team-info/projects-tab/reporting-overview-projects-table.tsx
+++ b/worklenz-frontend/src/components/reporting/drawers/overview-team-info/projects-tab/reporting-overview-projects-table.tsx
@@ -1,5 +1,12 @@
import { useEffect, useState, useMemo } from 'react';
-import { Button, ConfigProvider, Flex, PaginationProps, Table, TableColumnsType } from '@/shared/antd-imports';
+import {
+ Button,
+ ConfigProvider,
+ Flex,
+ PaginationProps,
+ Table,
+ TableColumnsType,
+} from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
import { createPortal } from 'react-dom';
import { ExpandAltOutlined } from '@/shared/antd-imports';
diff --git a/worklenz-frontend/src/components/reporting/time-reports/page-header/Billable.tsx b/worklenz-frontend/src/components/reporting/time-reports/page-header/Billable.tsx
index 5b273132e..769d61d25 100644
--- a/worklenz-frontend/src/components/reporting/time-reports/page-header/Billable.tsx
+++ b/worklenz-frontend/src/components/reporting/time-reports/page-header/Billable.tsx
@@ -65,12 +65,10 @@ const Billable: React.FC = () => {
successColor: isDark ? '#52c41a' : '#52c41a',
errorColor: isDark ? '#ff4d4f' : '#ff4d4f',
buttonBorder: isDark ? '#303030' : '#d9d9d9',
- buttonText: activeFiltersCount > 0
- ? (isDark ? 'white' : '#262626')
- : (isDark ? '#d9d9d9' : '#595959'),
- buttonBg: activeFiltersCount > 0
- ? (isDark ? '#434343' : '#f5f5f5')
- : (isDark ? '#141414' : 'white'),
+ buttonText:
+ activeFiltersCount > 0 ? (isDark ? 'white' : '#262626') : isDark ? '#d9d9d9' : '#595959',
+ buttonBg:
+ activeFiltersCount > 0 ? (isDark ? '#434343' : '#f5f5f5') : isDark ? '#141414' : 'white',
dropdownBg: isDark ? '#1f1f1f' : 'white',
dropdownBorder: isDark ? '#303030' : '#d9d9d9',
};
diff --git a/worklenz-frontend/src/components/reporting/time-reports/page-header/Categories.tsx b/worklenz-frontend/src/components/reporting/time-reports/page-header/Categories.tsx
index 31f7b09be..01b53da47 100644
--- a/worklenz-frontend/src/components/reporting/time-reports/page-header/Categories.tsx
+++ b/worklenz-frontend/src/components/reporting/time-reports/page-header/Categories.tsx
@@ -17,7 +17,7 @@ import {
CaretDownFilled,
FilterOutlined,
CheckCircleFilled,
- CheckboxChangeEvent
+ CheckboxChangeEvent,
} from '@/shared/antd-imports';
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -60,12 +60,10 @@ const Categories: React.FC = () => {
successColor: isDark ? '#52c41a' : '#52c41a',
errorColor: isDark ? '#ff4d4f' : '#ff4d4f',
buttonBorder: isDark ? '#303030' : '#d9d9d9',
- buttonText: activeFiltersCount > 0
- ? (isDark ? 'white' : '#262626')
- : (isDark ? '#d9d9d9' : '#595959'),
- buttonBg: activeFiltersCount > 0
- ? (isDark ? '#434343' : '#f5f5f5')
- : (isDark ? '#141414' : 'white'),
+ buttonText:
+ activeFiltersCount > 0 ? (isDark ? 'white' : '#262626') : isDark ? '#d9d9d9' : '#595959',
+ buttonBg:
+ activeFiltersCount > 0 ? (isDark ? '#434343' : '#f5f5f5') : isDark ? '#141414' : 'white',
dropdownBg: isDark ? '#1f1f1f' : 'white',
dropdownBorder: isDark ? '#303030' : '#d9d9d9',
};
diff --git a/worklenz-frontend/src/components/reporting/time-reports/page-header/Members.tsx b/worklenz-frontend/src/components/reporting/time-reports/page-header/Members.tsx
index 066d840a3..5a4949bdb 100644
--- a/worklenz-frontend/src/components/reporting/time-reports/page-header/Members.tsx
+++ b/worklenz-frontend/src/components/reporting/time-reports/page-header/Members.tsx
@@ -55,12 +55,10 @@ const Members: React.FC = () => {
successColor: isDark ? '#52c41a' : '#52c41a',
errorColor: isDark ? '#ff4d4f' : '#ff4d4f',
buttonBorder: isDark ? '#303030' : '#d9d9d9',
- buttonText: activeFiltersCount > 0
- ? (isDark ? 'white' : '#262626')
- : (isDark ? '#d9d9d9' : '#595959'),
- buttonBg: activeFiltersCount > 0
- ? (isDark ? '#434343' : '#f5f5f5')
- : (isDark ? '#141414' : 'white'),
+ buttonText:
+ activeFiltersCount > 0 ? (isDark ? 'white' : '#262626') : isDark ? '#d9d9d9' : '#595959',
+ buttonBg:
+ activeFiltersCount > 0 ? (isDark ? '#434343' : '#f5f5f5') : isDark ? '#141414' : 'white',
dropdownBg: isDark ? '#1f1f1f' : 'white',
dropdownBorder: isDark ? '#303030' : '#d9d9d9',
};
diff --git a/worklenz-frontend/src/components/reporting/time-reports/page-header/Projects.tsx b/worklenz-frontend/src/components/reporting/time-reports/page-header/Projects.tsx
index 33f7ec35c..fcb81b528 100644
--- a/worklenz-frontend/src/components/reporting/time-reports/page-header/Projects.tsx
+++ b/worklenz-frontend/src/components/reporting/time-reports/page-header/Projects.tsx
@@ -54,12 +54,10 @@ const Projects: React.FC = () => {
successColor: isDark ? '#52c41a' : '#52c41a',
errorColor: isDark ? '#ff4d4f' : '#ff4d4f',
buttonBorder: isDark ? '#303030' : '#d9d9d9',
- buttonText: activeFiltersCount > 0
- ? (isDark ? 'white' : '#262626')
- : (isDark ? '#d9d9d9' : '#595959'),
- buttonBg: activeFiltersCount > 0
- ? (isDark ? '#434343' : '#f5f5f5')
- : (isDark ? '#141414' : 'white'),
+ buttonText:
+ activeFiltersCount > 0 ? (isDark ? 'white' : '#262626') : isDark ? '#d9d9d9' : '#595959',
+ buttonBg:
+ activeFiltersCount > 0 ? (isDark ? '#434343' : '#f5f5f5') : isDark ? '#141414' : 'white',
dropdownBg: isDark ? '#1f1f1f' : 'white',
dropdownBorder: isDark ? '#303030' : '#d9d9d9',
};
diff --git a/worklenz-frontend/src/components/reporting/time-reports/page-header/Team.tsx b/worklenz-frontend/src/components/reporting/time-reports/page-header/Team.tsx
index c8b24fbdc..619779d02 100644
--- a/worklenz-frontend/src/components/reporting/time-reports/page-header/Team.tsx
+++ b/worklenz-frontend/src/components/reporting/time-reports/page-header/Team.tsx
@@ -55,12 +55,10 @@ const Team: React.FC = () => {
successColor: isDark ? '#52c41a' : '#52c41a',
errorColor: isDark ? '#ff4d4f' : '#ff4d4f',
buttonBorder: isDark ? '#303030' : '#d9d9d9',
- buttonText: activeFiltersCount > 0
- ? (isDark ? 'white' : '#262626')
- : (isDark ? '#d9d9d9' : '#595959'),
- buttonBg: activeFiltersCount > 0
- ? (isDark ? '#434343' : '#f5f5f5')
- : (isDark ? '#141414' : 'white'),
+ buttonText:
+ activeFiltersCount > 0 ? (isDark ? 'white' : '#262626') : isDark ? '#d9d9d9' : '#595959',
+ buttonBg:
+ activeFiltersCount > 0 ? (isDark ? '#434343' : '#f5f5f5') : isDark ? '#141414' : 'white',
dropdownBg: isDark ? '#1f1f1f' : 'white',
dropdownBorder: isDark ? '#303030' : '#d9d9d9',
};
diff --git a/worklenz-frontend/src/components/reporting/time-reports/page-header/TimeReportPageHeader.tsx b/worklenz-frontend/src/components/reporting/time-reports/page-header/TimeReportPageHeader.tsx
index b11b85b0d..3c96c83e2 100644
--- a/worklenz-frontend/src/components/reporting/time-reports/page-header/TimeReportPageHeader.tsx
+++ b/worklenz-frontend/src/components/reporting/time-reports/page-header/TimeReportPageHeader.tsx
@@ -1,4 +1,3 @@
-
import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import Team from './Team';
@@ -28,7 +27,7 @@ const TimeReportPageHeader: React.FC = () => {
await dispatch(fetchReportingTeams());
await dispatch(fetchReportingCategories());
await dispatch(fetchReportingProjects());
-
+
// Only fetch members and utilization data for members time sheet
if (isMembersTimeSheet) {
await dispatch(fetchReportingMembers());
@@ -45,7 +44,7 @@ const TimeReportPageHeader: React.FC = () => {
- {isMembersTimeSheet && }
+ {isMembersTimeSheet && }
{isMembersTimeSheet && }
);
diff --git a/worklenz-frontend/src/components/reporting/time-reports/page-header/Utilization.tsx b/worklenz-frontend/src/components/reporting/time-reports/page-header/Utilization.tsx
index 91c075127..9c2e75958 100644
--- a/worklenz-frontend/src/components/reporting/time-reports/page-header/Utilization.tsx
+++ b/worklenz-frontend/src/components/reporting/time-reports/page-header/Utilization.tsx
@@ -56,12 +56,10 @@ const Utilization: React.FC = () => {
successColor: isDark ? '#52c41a' : '#52c41a',
errorColor: isDark ? '#ff4d4f' : '#ff4d4f',
buttonBorder: isDark ? '#303030' : '#d9d9d9',
- buttonText: activeFiltersCount > 0
- ? (isDark ? 'white' : '#262626')
- : (isDark ? '#d9d9d9' : '#595959'),
- buttonBg: activeFiltersCount > 0
- ? (isDark ? '#434343' : '#f5f5f5')
- : (isDark ? '#141414' : 'white'),
+ buttonText:
+ activeFiltersCount > 0 ? (isDark ? 'white' : '#262626') : isDark ? '#d9d9d9' : '#595959',
+ buttonBg:
+ activeFiltersCount > 0 ? (isDark ? '#434343' : '#f5f5f5') : isDark ? '#141414' : 'white',
dropdownBg: isDark ? '#1f1f1f' : 'white',
dropdownBorder: isDark ? '#303030' : '#d9d9d9',
};
diff --git a/worklenz-frontend/src/components/reporting/time-reports/total-time-utilization/total-time-utilization.tsx b/worklenz-frontend/src/components/reporting/time-reports/total-time-utilization/total-time-utilization.tsx
index 47ba171ab..d6f119098 100644
--- a/worklenz-frontend/src/components/reporting/time-reports/total-time-utilization/total-time-utilization.tsx
+++ b/worklenz-frontend/src/components/reporting/time-reports/total-time-utilization/total-time-utilization.tsx
@@ -9,24 +9,74 @@ import {
ArrowDownOutlined,
CheckCircleOutlined,
} from '@/shared/antd-imports';
-import React, { useMemo } from 'react';
+import React, { useMemo, useEffect, useState } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useTranslation } from 'react-i18next';
import { IRPTTimeTotals } from '@/types/reporting/reporting.types';
+import { useReportingUtilization } from '@/hooks/useUtilizationCalculation';
+import dayjs from 'dayjs';
interface TotalTimeUtilizationProps {
totals: IRPTTimeTotals;
+ dateRange?: string[];
}
-const TotalTimeUtilization: React.FC
= ({ totals }) => {
+const TotalTimeUtilization: React.FC = ({ totals, dateRange }) => {
const { t } = useTranslation('time-report');
const themeMode = useAppSelector(state => state.themeReducer.mode);
const isDark = themeMode === 'dark';
+ const [holidayInfo, setHolidayInfo] = useState<{ count: number; adjustedHours: number } | null>(
+ null
+ );
+
+ // Get current date range or default to this month
+ const currentDateRange = useMemo(() => {
+ if (dateRange && dateRange.length >= 2) {
+ return {
+ from: dayjs(dateRange[0]).format('YYYY-MM-DD'),
+ to: dayjs(dateRange[1]).format('YYYY-MM-DD'),
+ };
+ }
+ return {
+ from: dayjs().startOf('month').format('YYYY-MM-DD'),
+ to: dayjs().endOf('month').format('YYYY-MM-DD'),
+ };
+ }, [dateRange]);
+
+ // Temporarily disable holiday integration to prevent API spam
+ // TODO: Re-enable once backend endpoints are properly implemented
+ const holidayIntegrationEnabled = false;
+
+ useEffect(() => {
+ if (!holidayIntegrationEnabled) {
+ // For now, just show a placeholder holiday count
+ setHolidayInfo({
+ count: 0,
+ adjustedHours: parseFloat(totals.total_estimated_hours || '0'),
+ });
+ return;
+ }
+
+ // Holiday integration code will be re-enabled once backend is ready
+ // ... (previous holiday calculation code)
+ }, [
+ currentDateRange.from,
+ currentDateRange.to,
+ totals.total_estimated_hours,
+ holidayIntegrationEnabled,
+ ]);
const utilizationData = useMemo(() => {
const timeLogged = parseFloat(totals.total_time_logs || '0');
- const estimatedHours = parseFloat(totals.total_estimated_hours || '0');
- const utilizationPercent = parseFloat(totals.total_utilization || '0');
+ let estimatedHours = parseFloat(totals.total_estimated_hours || '0');
+
+ // Use holiday-adjusted hours if available
+ if (holidayInfo?.adjustedHours && holidayInfo.adjustedHours > 0) {
+ estimatedHours = holidayInfo.adjustedHours;
+ }
+
+ // Recalculate utilization with holiday adjustment
+ const utilizationPercent = estimatedHours > 0 ? (timeLogged / estimatedHours) * 100 : 0;
// Determine utilization status and color
let status: 'under' | 'optimal' | 'over' = 'optimal';
@@ -49,13 +99,13 @@ const TotalTimeUtilization: React.FC = ({ totals }) =
return {
timeLogged,
estimatedHours,
- utilizationPercent,
+ utilizationPercent: Math.round(utilizationPercent * 100) / 100,
status,
statusColor,
statusIcon,
statusText,
};
- }, [totals, t]);
+ }, [totals, t, holidayInfo]);
const getThemeColors = useMemo(
() => ({
@@ -201,7 +251,7 @@ const TotalTimeUtilization: React.FC = ({ totals }) =
lineHeight: 1,
}}
>
- {totals.total_estimated_hours}h
+ {utilizationData.estimatedHours.toFixed(1)}h
= ({ totals }) =
marginTop: '2px',
}}
>
- {t('basedOnWorkingSchedule')}
+ {holidayInfo?.count
+ ? `${t('basedOnWorkingSchedule')} (${holidayInfo.count} ${t('holidaysExcluded')})`
+ : t('basedOnWorkingSchedule')}