From e59216af54c572aa00aa63fbd82eb9ffb905e72a Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Tue, 3 Jun 2025 10:41:20 +0530 Subject: [PATCH] feat(reporting-filters): enhance filter components with improved UI and functionality - Added date disabling functionality to the TimeWiseFilter component to prevent selection of future dates. - Updated Categories, Members, Projects, Team, and Utilization components to include active filters count and improved button text display. - Enhanced dropdown menus with theme-aware styles and added clear all and select all functionalities for better user experience. - Refactored components to utilize memoization for performance optimization and maintainability. --- .../components/reporting/time-wise-filter.tsx | 4 + .../timeReports/page-header/categories.tsx | 201 +++++++++++++++--- .../timeReports/page-header/members.tsx | 180 +++++++++++++--- .../timeReports/page-header/projects.tsx | 179 ++++++++++++++-- .../timeReports/page-header/team.tsx | 182 ++++++++++++++-- .../timeReports/page-header/utilization.tsx | 175 ++++++++++++--- .../TimeReportingRightHeader.tsx | 12 +- 7 files changed, 811 insertions(+), 122 deletions(-) diff --git a/worklenz-frontend/src/components/reporting/time-wise-filter.tsx b/worklenz-frontend/src/components/reporting/time-wise-filter.tsx index 5ef6f93b..4a6529ff 100644 --- a/worklenz-frontend/src/components/reporting/time-wise-filter.tsx +++ b/worklenz-frontend/src/components/reporting/time-wise-filter.tsx @@ -147,6 +147,10 @@ const TimeWiseFilter = () => { format={'MMM DD, YYYY'} onChange={handleDateRangeChange} value={customRange ? [dayjs(customRange[0]), dayjs(customRange[1])] : null} + disabledDate={(current) => { + // Disable dates after today + return current && current > dayjs().endOf('day'); + }} /> + + + )} -
+ + {/* No Category Option */} +
e.stopPropagation()} checked={noCategory} onChange={e => handleNoCategoryChange(e.target.checked)} + style={{ fontSize: '14px' }} > - {t('noCategory')} + {t('noCategory')} + {noCategory && ( + + )}
- + + + + {/* Items */}
{
e.stopPropagation()} checked={item.selected} onChange={e => handleCheckboxChange(item.id || '', e.target.checked)} + style={{ fontSize: '14px' }} > - {item.name} + {item.name} + {item.selected && ( + + )}
)) ) : ( -
+
{t('noCategories')}
)} @@ -126,8 +242,45 @@ const Categories: React.FC = () => { } }} > -
diff --git a/worklenz-frontend/src/pages/reporting/timeReports/page-header/members.tsx b/worklenz-frontend/src/pages/reporting/timeReports/page-header/members.tsx index 499a8b1a..b60b411e 100644 --- a/worklenz-frontend/src/pages/reporting/timeReports/page-header/members.tsx +++ b/worklenz-frontend/src/pages/reporting/timeReports/page-header/members.tsx @@ -1,10 +1,10 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppSelector } from '@/hooks/useAppSelector'; import { setSelectOrDeselectAllMembers, setSelectOrDeselectMember } from '@/features/reporting/time-reports/time-reports-overview.slice'; -import { Button, Checkbox, Divider, Dropdown, Input, Avatar, theme } from 'antd'; +import { Button, Checkbox, Divider, Dropdown, Input, Avatar, theme, Space } from 'antd'; import { CheckboxChangeEvent } from 'antd/es/checkbox'; -import { CaretDownFilled } from '@ant-design/icons'; +import { CaretDownFilled, FilterOutlined, CheckCircleFilled } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; const Members: React.FC = () => { @@ -16,11 +16,36 @@ const Members: React.FC = () => { const [searchText, setSearchText] = useState(''); const [selectAll, setSelectAll] = useState(true); + // Calculate active filters count + const activeFiltersCount = useMemo(() => { + return members.filter(member => member.selected).length; + }, [members]); + + // Check if all options are selected + const isAllSelected = members.length > 0 && members.every(member => member.selected); + const isNoneSelected = members.length > 0 && !members.some(member => member.selected); + // Filter members based on search text const filteredMembers = members.filter(member => member.name?.toLowerCase().includes(searchText.toLowerCase()) ); + // Theme-aware colors + const isDark = token.colorBgContainer !== '#ffffff'; + const colors = { + headerText: isDark ? token.colorTextSecondary : '#262626', + borderColor: isDark ? token.colorBorder : '#f0f0f0', + linkActive: token.colorPrimary, + linkDisabled: isDark ? token.colorTextDisabled : '#d9d9d9', + successColor: token.colorSuccess, + errorColor: token.colorError, + buttonBorder: activeFiltersCount > 0 ? token.colorPrimary : token.colorBorder, + buttonText: activeFiltersCount > 0 ? token.colorPrimary : token.colorTextSecondary, + buttonBg: activeFiltersCount > 0 ? (isDark ? token.colorPrimaryBg : '#f6ffed') : 'transparent', + dropdownBg: token.colorBgElevated, + dropdownBorder: token.colorBorderSecondary, + }; + // Handle checkbox change for individual members const handleCheckboxChange = (id: string, checked: boolean) => { dispatch(setSelectOrDeselectMember({ id, selected: checked })); @@ -33,6 +58,25 @@ const Members: React.FC = () => { dispatch(setSelectOrDeselectAllMembers(isChecked)); }; + // Handle select all button click + const handleSelectAllClick = () => { + const newValue = !isAllSelected; + setSelectAll(newValue); + dispatch(setSelectOrDeselectAllMembers(newValue)); + }; + + // Handle clear all + const handleClearAll = () => { + setSelectAll(false); + dispatch(setSelectOrDeselectAllMembers(false)); + }; + + const getButtonText = () => { + if (isNoneSelected) return t('members'); + if (isAllSelected) return `All ${t('members')}`; + return `${t('members')} (${activeFiltersCount})`; + }; + return ( { dropdownRender={() => (
-
+ {/* Header */} +
+ {t('searchByMember')} +
+ + {/* Search */} +
e.stopPropagation()} placeholder={t('searchByMember')} value={searchText} onChange={e => setSearchText(e.target.value)} + style={{ fontSize: '14px' }} />
-
- e.stopPropagation()} - onChange={handleSelectAllChange} - checked={selectAll} - > - {t('selectAll')} - + + {/* Actions */} +
+ + + + +
- + + + + {/* Items */}
{
- + e.stopPropagation()} checked={member.selected} onChange={e => handleCheckboxChange(member.id, e.target.checked)} + style={{ fontSize: '14px' }} > - {member.name} + {member.name} + {member.selected && ( + + )}
))}
)} > - ); diff --git a/worklenz-frontend/src/pages/reporting/timeReports/page-header/projects.tsx b/worklenz-frontend/src/pages/reporting/timeReports/page-header/projects.tsx index bb1712cf..5502fa6b 100644 --- a/worklenz-frontend/src/pages/reporting/timeReports/page-header/projects.tsx +++ b/worklenz-frontend/src/pages/reporting/timeReports/page-header/projects.tsx @@ -1,10 +1,10 @@ import { setSelectOrDeselectAllProjects, setSelectOrDeselectProject } from '@/features/reporting/time-reports/time-reports-overview.slice'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppSelector } from '@/hooks/useAppSelector'; -import { CaretDownFilled } from '@ant-design/icons'; -import { Button, Checkbox, Divider, Dropdown, Input, theme } from 'antd'; +import { CaretDownFilled, FilterOutlined, CheckCircleFilled } from '@ant-design/icons'; +import { Button, Checkbox, Divider, Dropdown, Input, theme, Space } from 'antd'; import { CheckboxChangeEvent } from 'antd/es/checkbox'; -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const Projects: React.FC = () => { @@ -17,11 +17,36 @@ const Projects: React.FC = () => { const { projects, loadingProjects } = useAppSelector(state => state.timeReportsOverviewReducer); const { token } = theme.useToken(); + // Calculate active filters count + const activeFiltersCount = useMemo(() => { + return projects.filter(project => project.selected).length; + }, [projects]); + + // Check if all options are selected + const isAllSelected = projects.length > 0 && projects.every(project => project.selected); + const isNoneSelected = projects.length > 0 && !projects.some(project => project.selected); + // Filter items based on search text const filteredItems = projects.filter(item => item.name?.toLowerCase().includes(searchText.toLowerCase()) ); + // Theme-aware colors + const isDark = token.colorBgContainer !== '#ffffff'; + const colors = { + headerText: isDark ? token.colorTextSecondary : '#262626', + borderColor: isDark ? token.colorBorder : '#f0f0f0', + linkActive: token.colorPrimary, + linkDisabled: isDark ? token.colorTextDisabled : '#d9d9d9', + successColor: token.colorSuccess, + errorColor: token.colorError, + buttonBorder: activeFiltersCount > 0 ? token.colorPrimary : token.colorBorder, + buttonText: activeFiltersCount > 0 ? token.colorPrimary : token.colorTextSecondary, + buttonBg: activeFiltersCount > 0 ? (isDark ? token.colorPrimaryBg : '#f6ffed') : 'transparent', + dropdownBg: token.colorBgElevated, + dropdownBorder: token.colorBorderSecondary, + }; + // Handle checkbox change for individual items const handleCheckboxChange = (key: string, checked: boolean) => { dispatch(setSelectOrDeselectProject({ id: key, selected: checked })); @@ -34,6 +59,25 @@ const Projects: React.FC = () => { dispatch(setSelectOrDeselectAllProjects(isChecked)); }; + // Handle select all button click + const handleSelectAllClick = () => { + const newValue = !isAllSelected; + setSelectAll(newValue); + dispatch(setSelectOrDeselectAllProjects(newValue)); + }; + + // Handle clear all + const handleClearAll = () => { + setSelectAll(false); + dispatch(setSelectOrDeselectAllProjects(false)); + }; + + const getButtonText = () => { + if (isNoneSelected) return t('projects'); + if (isAllSelected) return `All ${t('projects')}`; + return `${t('projects')} (${activeFiltersCount})`; + }; + return (
{ trigger={['click']} dropdownRender={() => (
-
+ {/* Header */} +
+ {t('searchByProject')} +
+ + {/* Search */} +
e.stopPropagation()} placeholder={t('searchByProject')} value={searchText} onChange={e => setSearchText(e.target.value)} + style={{ fontSize: '14px' }} />
-
- e.stopPropagation()} - onChange={handleSelectAllChange} - checked={selectAll} - > - {t('selectAll')} - + + {/* Actions */} +
+ + + + +
- + + + + {/* Items */}
{
e.stopPropagation()} checked={item.selected} onChange={e => handleCheckboxChange(item.id || '', e.target.checked)} + style={{ fontSize: '14px' }} > - {item.name} + {item.name} + {item.selected && ( + + )}
))}
@@ -102,8 +198,45 @@ const Projects: React.FC = () => { } }} > -
diff --git a/worklenz-frontend/src/pages/reporting/timeReports/page-header/team.tsx b/worklenz-frontend/src/pages/reporting/timeReports/page-header/team.tsx index 0ceb342c..778a2465 100644 --- a/worklenz-frontend/src/pages/reporting/timeReports/page-header/team.tsx +++ b/worklenz-frontend/src/pages/reporting/timeReports/page-header/team.tsx @@ -1,6 +1,6 @@ -import { CaretDownFilled } from '@ant-design/icons'; -import { Button, Checkbox, Divider, Dropdown, Input, theme } from 'antd'; -import React, { useEffect, useState } from 'react'; +import { CaretDownFilled, FilterOutlined, CheckCircleFilled } from '@ant-design/icons'; +import { Button, Checkbox, Divider, Dropdown, Input, theme, Space } from 'antd'; +import React, { useEffect, useState, useMemo } from 'react'; import type { CheckboxChangeEvent } from 'antd/es/checkbox'; import { useTranslation } from 'react-i18next'; import { ISelectableTeam } from '@/types/reporting/reporting-filters.types'; @@ -21,10 +21,35 @@ const Team: React.FC = () => { const { teams, loadingTeams } = useAppSelector(state => state.timeReportsOverviewReducer); + // Calculate active filters count + const activeFiltersCount = useMemo(() => { + return teams.filter(team => team.selected).length; + }, [teams]); + + // Check if all options are selected + const isAllSelected = teams.length > 0 && teams.every(team => team.selected); + const isNoneSelected = teams.length > 0 && !teams.some(team => team.selected); + const filteredItems = teams.filter(item => item.name?.toLowerCase().includes(searchText.toLowerCase()) ); + // Theme-aware colors + const isDark = token.colorBgContainer !== '#ffffff'; + const colors = { + headerText: isDark ? token.colorTextSecondary : '#262626', + borderColor: isDark ? token.colorBorder : '#f0f0f0', + linkActive: token.colorPrimary, + linkDisabled: isDark ? token.colorTextDisabled : '#d9d9d9', + successColor: token.colorSuccess, + errorColor: token.colorError, + buttonBorder: activeFiltersCount > 0 ? token.colorPrimary : token.colorBorder, + buttonText: activeFiltersCount > 0 ? token.colorPrimary : token.colorTextSecondary, + buttonBg: activeFiltersCount > 0 ? (isDark ? token.colorPrimaryBg : '#f6ffed') : 'transparent', + dropdownBg: token.colorBgElevated, + dropdownBorder: token.colorBorderSecondary, + }; + const handleCheckboxChange = async (key: string, checked: boolean) => { dispatch(setSelectOrDeselectTeam({ id: key, selected: checked })); await dispatch(fetchReportingCategories()); @@ -39,6 +64,29 @@ const Team: React.FC = () => { await dispatch(fetchReportingProjects()); }; + // Handle clear all + const handleClearAll = async () => { + setSelectAll(false); + dispatch(setSelectOrDeselectAllTeams(false)); + await dispatch(fetchReportingCategories()); + await dispatch(fetchReportingProjects()); + }; + + // Handle select all button click + const handleSelectAllClick = async () => { + const newValue = !isAllSelected; + setSelectAll(newValue); + dispatch(setSelectOrDeselectAllTeams(newValue)); + await dispatch(fetchReportingCategories()); + await dispatch(fetchReportingProjects()); + }; + + const getButtonText = () => { + if (isNoneSelected) return t('teams'); + if (isAllSelected) return `All ${t('teams')}`; + return `${t('teams')} (${activeFiltersCount})`; + }; + return (
{ trigger={['click']} dropdownRender={() => (
-
+ {/* Header */} +
+ {t('searchByName')} +
+ + {/* Search */} +
setSearchText(e.target.value)} onClick={e => e.stopPropagation()} + style={{ fontSize: '14px' }} />
-
- e.stopPropagation()} - onChange={handleSelectAllChange} - checked={selectAll} - > - {t('selectAll')} - + + {/* Actions */} +
+ + + + +
- + + + + {/* Items */}
{
e.stopPropagation()} checked={item.selected} onChange={e => handleCheckboxChange(item.id || '', e.target.checked)} + style={{ fontSize: '14px' }} > - {item.name} + {item.name} + {item.selected && ( + + )}
))}
@@ -104,8 +207,45 @@ const Team: React.FC = () => { } }} > -
diff --git a/worklenz-frontend/src/pages/reporting/timeReports/page-header/utilization.tsx b/worklenz-frontend/src/pages/reporting/timeReports/page-header/utilization.tsx index 14ed2f2e..e3b964c9 100644 --- a/worklenz-frontend/src/pages/reporting/timeReports/page-header/utilization.tsx +++ b/worklenz-frontend/src/pages/reporting/timeReports/page-header/utilization.tsx @@ -1,12 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppSelector } from '@/hooks/useAppSelector'; import { setSelectOrDeselectAllMembers, setSelectOrDeselectAllUtilization, setSelectOrDeselectMember, setSelectOrDeselectUtilization } from '@/features/reporting/time-reports/time-reports-overview.slice'; -import { Button, Checkbox, Divider, Dropdown, Input, Avatar, theme } from 'antd'; +import { Button, Checkbox, Divider, Dropdown, Input, Avatar, theme, Space } from 'antd'; import { CheckboxChangeEvent } from 'antd/es/checkbox'; -import { CaretDownFilled } from '@ant-design/icons'; +import { CaretDownFilled, FilterOutlined, CheckCircleFilled } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; -import { id } from 'date-fns/locale'; const Utilization: React.FC = () => { const dispatch = useAppDispatch(); @@ -17,10 +16,36 @@ const Utilization: React.FC = () => { const [searchText, setSearchText] = useState(''); const [selectAll, setSelectAll] = useState(true); + // Calculate active filters count + const activeFiltersCount = useMemo(() => { + return utilization.filter(item => item.selected).length; + }, [utilization]); + + // Check if all options are selected + const isAllSelected = utilization.length > 0 && utilization.every(item => item.selected); + const isNoneSelected = utilization.length > 0 && !utilization.some(item => item.selected); + // Filter members based on search text const filteredItems = utilization.filter(item => item.name?.toLowerCase().includes(searchText.toLowerCase()) ); + + // Theme-aware colors + const isDark = token.colorBgContainer !== '#ffffff'; + const colors = { + headerText: isDark ? token.colorTextSecondary : '#262626', + borderColor: isDark ? token.colorBorder : '#f0f0f0', + linkActive: token.colorPrimary, + linkDisabled: isDark ? token.colorTextDisabled : '#d9d9d9', + successColor: token.colorSuccess, + errorColor: token.colorError, + buttonBorder: activeFiltersCount > 0 ? token.colorPrimary : token.colorBorder, + buttonText: activeFiltersCount > 0 ? token.colorPrimary : token.colorTextSecondary, + buttonBg: activeFiltersCount > 0 ? (isDark ? token.colorPrimaryBg : '#f6ffed') : 'transparent', + dropdownBg: token.colorBgElevated, + dropdownBorder: token.colorBorderSecondary, + }; + // Handle checkbox change for individual members const handleCheckboxChange = (id: string, selected: boolean) => { dispatch(setSelectOrDeselectUtilization({ id, selected })); @@ -32,6 +57,25 @@ const Utilization: React.FC = () => { dispatch(setSelectOrDeselectAllUtilization(isChecked)); }; + // Handle select all button click + const handleSelectAllClick = () => { + const newValue = !isAllSelected; + setSelectAll(newValue); + dispatch(setSelectOrDeselectAllUtilization(newValue)); + }; + + // Handle clear all + const handleClearAll = () => { + setSelectAll(false); + dispatch(setSelectOrDeselectAllUtilization(false)); + }; + + const getButtonText = () => { + if (isNoneSelected) return t('utilization'); + if (isAllSelected) return `All ${t('utilization')}`; + return `${t('utilization')} (${activeFiltersCount})`; + }; + return ( { dropdownRender={() => (
-
+ {/* Header */} +
+ {t('utilization')}
-
- e.stopPropagation()} - onChange={handleSelectAll} - checked={selectAll} - > - {t('selectAll')} - + + {/* Actions */} +
+ + + + +
- + + + + {/* Items */}
{
e.stopPropagation()} checked={ut.selected} onChange={e => handleCheckboxChange(ut.id, e.target.checked)} + style={{ fontSize: '14px' }} > - {ut.name} + {ut.name} + {ut.selected && ( + + )}
))}
)} > - ); diff --git a/worklenz-frontend/src/pages/reporting/timeReports/timeReportingRightHeader/TimeReportingRightHeader.tsx b/worklenz-frontend/src/pages/reporting/timeReports/timeReportingRightHeader/TimeReportingRightHeader.tsx index d3c35835..d2594db0 100644 --- a/worklenz-frontend/src/pages/reporting/timeReports/timeReportingRightHeader/TimeReportingRightHeader.tsx +++ b/worklenz-frontend/src/pages/reporting/timeReports/timeReportingRightHeader/TimeReportingRightHeader.tsx @@ -14,7 +14,11 @@ interface headerState { export: (key: string) => void; } -const TimeReportingRightHeader: React.FC = ({ title, exportType, export: exportFn }) => { +const TimeReportingRightHeader: React.FC = ({ + title, + exportType, + export: exportFn, +}) => { const { t } = useTranslation('time-report'); const dispatch = useAppDispatch(); const { archived } = useAppSelector(state => state.timeReportsOverviewReducer); @@ -22,7 +26,7 @@ const TimeReportingRightHeader: React.FC = ({ title, exportType, ex const menuItems = exportType.map(item => ({ key: item.key, label: item.label, - onClick: () => exportFn(item.key) + onClick: () => exportFn(item.key), })); return ( @@ -36,8 +40,8 @@ const TimeReportingRightHeader: React.FC = ({ title, exportType, ex - -