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.
This commit is contained in:
chamiakJ
2025-06-03 10:41:20 +05:30
parent 0f5946134c
commit e59216af54
7 changed files with 811 additions and 122 deletions

View File

@@ -147,6 +147,10 @@ const TimeWiseFilter = () => {
format={'MMM DD, YYYY'} format={'MMM DD, YYYY'}
onChange={handleDateRangeChange} onChange={handleDateRangeChange}
value={customRange ? [dayjs(customRange[0]), dayjs(customRange[1])] : null} value={customRange ? [dayjs(customRange[0]), dayjs(customRange[1])] : null}
disabledDate={(current) => {
// Disable dates after today
return current && current > dayjs().endOf('day');
}}
/> />
<Button <Button

View File

@@ -1,10 +1,10 @@
import { fetchReportingProjects, setNoCategory, setSelectOrDeselectAllCategories, setSelectOrDeselectCategory } from '@/features/reporting/time-reports/time-reports-overview.slice'; import { fetchReportingProjects, setNoCategory, setSelectOrDeselectAllCategories, setSelectOrDeselectCategory } from '@/features/reporting/time-reports/time-reports-overview.slice';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { CaretDownFilled } from '@ant-design/icons'; import { CaretDownFilled, FilterOutlined, CheckCircleFilled } from '@ant-design/icons';
import { Button, Card, Checkbox, Divider, Dropdown, Input, theme } from 'antd'; import { Button, Card, Checkbox, Divider, Dropdown, Input, theme, Space } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox'; import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const Categories: React.FC = () => { const Categories: React.FC = () => {
@@ -19,10 +19,36 @@ const Categories: React.FC = () => {
); );
const { token } = theme.useToken(); const { token } = theme.useToken();
// Calculate active filters count
const activeFiltersCount = useMemo(() => {
const selectedCategories = categories.filter(category => category.selected).length;
return selectedCategories + (noCategory ? 1 : 0);
}, [categories, noCategory]);
// Check if all options are selected
const isAllSelected = categories.length > 0 && categories.every(category => category.selected) && noCategory;
const isNoneSelected = categories.length > 0 && !categories.some(category => category.selected) && !noCategory;
const filteredItems = categories.filter(item => const filteredItems = categories.filter(item =>
item.name?.toLowerCase().includes(searchText.toLowerCase()) 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 // Handle checkbox change for individual items
const handleCheckboxChange = async (key: string, checked: boolean) => { const handleCheckboxChange = async (key: string, checked: boolean) => {
await dispatch(setSelectOrDeselectCategory({ id: key, selected: checked })); await dispatch(setSelectOrDeselectCategory({ id: key, selected: checked }));
@@ -36,7 +62,23 @@ const Categories: React.FC = () => {
await dispatch(setNoCategory(isChecked)); await dispatch(setNoCategory(isChecked));
await dispatch(setSelectOrDeselectAllCategories(isChecked)); await dispatch(setSelectOrDeselectAllCategories(isChecked));
await dispatch(fetchReportingProjects()); await dispatch(fetchReportingProjects());
};
// Handle select all button click
const handleSelectAllClick = async () => {
const newValue = !isAllSelected;
setSelectAll(newValue);
await dispatch(setNoCategory(newValue));
await dispatch(setSelectOrDeselectAllCategories(newValue));
await dispatch(fetchReportingProjects());
};
// Handle clear all
const handleClearAll = async () => {
setSelectAll(false);
await dispatch(setNoCategory(false));
await dispatch(setSelectOrDeselectAllCategories(false));
await dispatch(fetchReportingProjects());
}; };
const handleNoCategoryChange = async (checked: boolean) => { const handleNoCategoryChange = async (checked: boolean) => {
@@ -44,6 +86,12 @@ const Categories: React.FC = () => {
await dispatch(fetchReportingProjects()); await dispatch(fetchReportingProjects());
}; };
const getButtonText = () => {
if (isNoneSelected) return t('categories');
if (isAllSelected) return `All ${t('categories')}`;
return `${t('categories')} (${activeFiltersCount})`;
};
return ( return (
<div> <div>
<Dropdown <Dropdown
@@ -52,43 +100,102 @@ const Categories: React.FC = () => {
trigger={['click']} trigger={['click']}
dropdownRender={() => ( dropdownRender={() => (
<div style={{ <div style={{
background: token.colorBgContainer, background: colors.dropdownBg,
borderRadius: token.borderRadius, borderRadius: '8px',
boxShadow: token.boxShadow, boxShadow: isDark
? '0 6px 16px 0 rgba(0, 0, 0, 0.32), 0 3px 6px -4px rgba(0, 0, 0, 0.32), 0 9px 28px 8px rgba(0, 0, 0, 0.20)'
: '0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)',
border: `1px solid ${colors.dropdownBorder}`,
padding: '4px 0', padding: '4px 0',
maxHeight: '330px', maxHeight: '330px',
display: 'flex', display: 'flex',
flexDirection: 'column' flexDirection: 'column'
}}> }}>
<div style={{ padding: '8px', flexShrink: 0 }}> {/* Header */}
<div style={{
padding: '4px 4px 2px',
fontWeight: 600,
fontSize: '12px',
color: colors.headerText,
borderBottom: `1px solid ${colors.borderColor}`,
marginBottom: '2px'
}}>
{t('searchByCategory')}
</div>
{/* Search */}
<div style={{ padding: '4px 8px', flexShrink: 0 }}>
<Input <Input
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
placeholder={t('searchByCategory')} placeholder={t('searchByCategory')}
value={searchText} value={searchText}
onChange={e => setSearchText(e.target.value)} onChange={e => setSearchText(e.target.value)}
style={{ fontSize: '14px' }}
/> />
</div> </div>
{/* Actions */}
{categories.length > 0 && ( {categories.length > 0 && (
<div style={{ padding: '0 12px', flexShrink: 0 }}> <div style={{ padding: '2px 8px', marginBottom: '2px' }}>
<Checkbox <Space size="small">
onClick={e => e.stopPropagation()} <Button
onChange={handleSelectAllChange} type="link"
checked={selectAll} size="small"
> onClick={handleSelectAllClick}
{t('selectAll')} disabled={isAllSelected}
</Checkbox> style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isAllSelected ? colors.linkDisabled : colors.linkActive
}}
>
{t('selectAll')}
</Button>
<Divider type="vertical" style={{ margin: '0 2px' }} />
<Button
type="link"
size="small"
onClick={handleClearAll}
disabled={isNoneSelected}
style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isNoneSelected ? colors.linkDisabled : colors.errorColor
}}
>
{t('clearAll')}
</Button>
</Space>
</div> </div>
)} )}
<div style={{ padding: '8px 12px 4px 12px', flexShrink: 0 }}>
{/* No Category Option */}
<div style={{
padding: '4px 8px',
display: 'flex',
alignItems: 'center',
gap: '6px',
borderRadius: '4px',
transition: 'background-color 0.2s'
}}>
<Checkbox <Checkbox
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
checked={noCategory} checked={noCategory}
onChange={e => handleNoCategoryChange(e.target.checked)} onChange={e => handleNoCategoryChange(e.target.checked)}
style={{ fontSize: '14px' }}
> >
{t('noCategory')} <span style={{ marginLeft: '2px', fontSize: '14px' }}>{t('noCategory')}</span>
</Checkbox> </Checkbox>
{noCategory && (
<CheckCircleFilled style={{ color: colors.successColor, fontSize: '10px' }} />
)}
</div> </div>
<Divider style={{ margin: '4px 0', flexShrink: 0 }} />
<Divider style={{ margin: '2px 0', flexShrink: 0 }} />
{/* Items */}
<div style={{ <div style={{
overflowY: 'auto', overflowY: 'auto',
flex: 1 flex: 1
@@ -98,21 +205,30 @@ const Categories: React.FC = () => {
<div <div
key={item.id} key={item.id}
style={{ style={{
padding: '8px 12px', padding: '4px 8px',
cursor: 'pointer' cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
borderRadius: '4px',
transition: 'background-color 0.2s'
}} }}
> >
<Checkbox <Checkbox
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
checked={item.selected} checked={item.selected}
onChange={e => handleCheckboxChange(item.id || '', e.target.checked)} onChange={e => handleCheckboxChange(item.id || '', e.target.checked)}
style={{ fontSize: '14px' }}
> >
{item.name} <span style={{ marginLeft: '2px', fontSize: '14px' }}>{item.name}</span>
</Checkbox> </Checkbox>
{item.selected && (
<CheckCircleFilled style={{ color: colors.successColor, fontSize: '10px' }} />
)}
</div> </div>
)) ))
) : ( ) : (
<div style={{ padding: '8px 12px' }}> <div style={{ padding: '4px 8px', fontSize: '14px', color: colors.headerText }}>
{t('noCategories')} {t('noCategories')}
</div> </div>
)} )}
@@ -126,8 +242,45 @@ const Categories: React.FC = () => {
} }
}} }}
> >
<Button loading={loadingCategories}> <Button
{t('categories')} <CaretDownFilled /> loading={loadingCategories}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
height: '32px',
borderColor: colors.buttonBorder,
color: colors.buttonText,
fontWeight: activeFiltersCount > 0 ? 500 : 400,
transition: 'all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)',
backgroundColor: colors.buttonBg,
}}
onMouseEnter={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimaryHover;
e.currentTarget.style.boxShadow = `0 2px 4px ${token.colorPrimary}20`;
}
}}
onMouseLeave={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimary;
e.currentTarget.style.boxShadow = 'none';
}
}}
>
<FilterOutlined
style={{
fontSize: '14px',
color: activeFiltersCount > 0 ? token.colorPrimary : token.colorTextTertiary
}}
/>
<span>{getButtonText()}</span>
<CaretDownFilled
style={{
fontSize: '10px',
marginLeft: '2px'
}}
/>
</Button> </Button>
</Dropdown> </Dropdown>
</div> </div>

View File

@@ -1,10 +1,10 @@
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { setSelectOrDeselectAllMembers, setSelectOrDeselectMember } from '@/features/reporting/time-reports/time-reports-overview.slice'; 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 { 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 { useTranslation } from 'react-i18next';
const Members: React.FC = () => { const Members: React.FC = () => {
@@ -16,11 +16,36 @@ const Members: React.FC = () => {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [selectAll, setSelectAll] = useState(true); 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 // Filter members based on search text
const filteredMembers = members.filter(member => const filteredMembers = members.filter(member =>
member.name?.toLowerCase().includes(searchText.toLowerCase()) 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 // Handle checkbox change for individual members
const handleCheckboxChange = (id: string, checked: boolean) => { const handleCheckboxChange = (id: string, checked: boolean) => {
dispatch(setSelectOrDeselectMember({ id, selected: checked })); dispatch(setSelectOrDeselectMember({ id, selected: checked }));
@@ -33,6 +58,25 @@ const Members: React.FC = () => {
dispatch(setSelectOrDeselectAllMembers(isChecked)); 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 ( return (
<Dropdown <Dropdown
menu={undefined} menu={undefined}
@@ -41,33 +85,79 @@ const Members: React.FC = () => {
dropdownRender={() => ( dropdownRender={() => (
<div <div
style={{ style={{
background: token.colorBgContainer, background: colors.dropdownBg,
borderRadius: token.borderRadius, borderRadius: '8px',
boxShadow: token.boxShadow, boxShadow: isDark
? '0 6px 16px 0 rgba(0, 0, 0, 0.32), 0 3px 6px -4px rgba(0, 0, 0, 0.32), 0 9px 28px 8px rgba(0, 0, 0, 0.20)'
: '0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)',
border: `1px solid ${colors.dropdownBorder}`,
padding: '4px 0', padding: '4px 0',
maxHeight: '330px', maxHeight: '330px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}} }}
> >
<div style={{ padding: '8px', flexShrink: 0 }}> {/* Header */}
<div style={{
padding: '4px 4px 2px',
fontWeight: 600,
fontSize: '12px',
color: colors.headerText,
borderBottom: `1px solid ${colors.borderColor}`,
marginBottom: '2px'
}}>
{t('searchByMember')}
</div>
{/* Search */}
<div style={{ padding: '4px 8px', flexShrink: 0 }}>
<Input <Input
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
placeholder={t('searchByMember')} placeholder={t('searchByMember')}
value={searchText} value={searchText}
onChange={e => setSearchText(e.target.value)} onChange={e => setSearchText(e.target.value)}
style={{ fontSize: '14px' }}
/> />
</div> </div>
<div style={{ padding: '0 12px', flexShrink: 0 }}>
<Checkbox {/* Actions */}
onClick={e => e.stopPropagation()} <div style={{ padding: '2px 8px', marginBottom: '2px' }}>
onChange={handleSelectAllChange} <Space size="small">
checked={selectAll} <Button
> type="link"
{t('selectAll')} size="small"
</Checkbox> onClick={handleSelectAllClick}
disabled={isAllSelected}
style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isAllSelected ? colors.linkDisabled : colors.linkActive
}}
>
{t('selectAll')}
</Button>
<Divider type="vertical" style={{ margin: '0 2px' }} />
<Button
type="link"
size="small"
onClick={handleClearAll}
disabled={isNoneSelected}
style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isNoneSelected ? colors.linkDisabled : colors.errorColor
}}
>
{t('clearAll')}
</Button>
</Space>
</div> </div>
<Divider style={{ margin: '4px 0', flexShrink: 0 }} />
<Divider style={{ margin: '2px 0', flexShrink: 0 }} />
{/* Items */}
<div <div
style={{ style={{
overflowY: 'auto', overflowY: 'auto',
@@ -78,32 +168,72 @@ const Members: React.FC = () => {
<div <div
key={member.id} key={member.id}
style={{ style={{
padding: '8px 12px', padding: '4px 8px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '8px', gap: '6px',
cursor: 'pointer', cursor: 'pointer',
'&:hover': { borderRadius: '4px',
backgroundColor: token.colorBgTextHover, transition: 'background-color 0.2s'
},
}} }}
> >
<Avatar src={member.avatar_url} alt={member.name} /> <Avatar src={member.avatar_url} alt={member.name} size="small" />
<Checkbox <Checkbox
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
checked={member.selected} checked={member.selected}
onChange={e => handleCheckboxChange(member.id, e.target.checked)} onChange={e => handleCheckboxChange(member.id, e.target.checked)}
style={{ fontSize: '14px' }}
> >
{member.name} <span style={{ marginLeft: '2px', fontSize: '14px' }}>{member.name}</span>
</Checkbox> </Checkbox>
{member.selected && (
<CheckCircleFilled style={{ color: colors.successColor, fontSize: '10px', marginLeft: 'auto' }} />
)}
</div> </div>
))} ))}
</div> </div>
</div> </div>
)} )}
> >
<Button loading={loadingMembers}> <Button
{t('members')} <CaretDownFilled /> loading={loadingMembers}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
height: '32px',
borderColor: colors.buttonBorder,
color: colors.buttonText,
fontWeight: activeFiltersCount > 0 ? 500 : 400,
transition: 'all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)',
backgroundColor: colors.buttonBg,
}}
onMouseEnter={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimaryHover;
e.currentTarget.style.boxShadow = `0 2px 4px ${token.colorPrimary}20`;
}
}}
onMouseLeave={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimary;
e.currentTarget.style.boxShadow = 'none';
}
}}
>
<FilterOutlined
style={{
fontSize: '14px',
color: activeFiltersCount > 0 ? token.colorPrimary : token.colorTextTertiary
}}
/>
<span>{getButtonText()}</span>
<CaretDownFilled
style={{
fontSize: '10px',
marginLeft: '2px'
}}
/>
</Button> </Button>
</Dropdown> </Dropdown>
); );

View File

@@ -1,10 +1,10 @@
import { setSelectOrDeselectAllProjects, setSelectOrDeselectProject } from '@/features/reporting/time-reports/time-reports-overview.slice'; import { setSelectOrDeselectAllProjects, setSelectOrDeselectProject } from '@/features/reporting/time-reports/time-reports-overview.slice';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { CaretDownFilled } from '@ant-design/icons'; import { CaretDownFilled, FilterOutlined, CheckCircleFilled } from '@ant-design/icons';
import { Button, Checkbox, Divider, Dropdown, Input, theme } from 'antd'; import { Button, Checkbox, Divider, Dropdown, Input, theme, Space } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox'; import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const Projects: React.FC = () => { const Projects: React.FC = () => {
@@ -17,11 +17,36 @@ const Projects: React.FC = () => {
const { projects, loadingProjects } = useAppSelector(state => state.timeReportsOverviewReducer); const { projects, loadingProjects } = useAppSelector(state => state.timeReportsOverviewReducer);
const { token } = theme.useToken(); 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 // Filter items based on search text
const filteredItems = projects.filter(item => const filteredItems = projects.filter(item =>
item.name?.toLowerCase().includes(searchText.toLowerCase()) 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 // Handle checkbox change for individual items
const handleCheckboxChange = (key: string, checked: boolean) => { const handleCheckboxChange = (key: string, checked: boolean) => {
dispatch(setSelectOrDeselectProject({ id: key, selected: checked })); dispatch(setSelectOrDeselectProject({ id: key, selected: checked }));
@@ -34,6 +59,25 @@ const Projects: React.FC = () => {
dispatch(setSelectOrDeselectAllProjects(isChecked)); 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 ( return (
<div> <div>
<Dropdown <Dropdown
@@ -42,32 +86,78 @@ const Projects: React.FC = () => {
trigger={['click']} trigger={['click']}
dropdownRender={() => ( dropdownRender={() => (
<div style={{ <div style={{
background: token.colorBgContainer, background: colors.dropdownBg,
borderRadius: token.borderRadius, borderRadius: '8px',
boxShadow: token.boxShadow, boxShadow: isDark
? '0 6px 16px 0 rgba(0, 0, 0, 0.32), 0 3px 6px -4px rgba(0, 0, 0, 0.32), 0 9px 28px 8px rgba(0, 0, 0, 0.20)'
: '0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)',
border: `1px solid ${colors.dropdownBorder}`,
padding: '4px 0', padding: '4px 0',
maxHeight: '330px', maxHeight: '330px',
display: 'flex', display: 'flex',
flexDirection: 'column' flexDirection: 'column'
}}> }}>
<div style={{ padding: '8px', flexShrink: 0 }}> {/* Header */}
<div style={{
padding: '4px 4px 2px',
fontWeight: 600,
fontSize: '12px',
color: colors.headerText,
borderBottom: `1px solid ${colors.borderColor}`,
marginBottom: '2px'
}}>
{t('searchByProject')}
</div>
{/* Search */}
<div style={{ padding: '4px 8px', flexShrink: 0 }}>
<Input <Input
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
placeholder={t('searchByProject')} placeholder={t('searchByProject')}
value={searchText} value={searchText}
onChange={e => setSearchText(e.target.value)} onChange={e => setSearchText(e.target.value)}
style={{ fontSize: '14px' }}
/> />
</div> </div>
<div style={{ padding: '0 12px', flexShrink: 0 }}>
<Checkbox {/* Actions */}
onClick={e => e.stopPropagation()} <div style={{ padding: '2px 8px', marginBottom: '2px' }}>
onChange={handleSelectAllChange} <Space size="small">
checked={selectAll} <Button
> type="link"
{t('selectAll')} size="small"
</Checkbox> onClick={handleSelectAllClick}
disabled={isAllSelected}
style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isAllSelected ? colors.linkDisabled : colors.linkActive
}}
>
{t('selectAll')}
</Button>
<Divider type="vertical" style={{ margin: '0 2px' }} />
<Button
type="link"
size="small"
onClick={handleClearAll}
disabled={isNoneSelected}
style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isNoneSelected ? colors.linkDisabled : colors.errorColor
}}
>
{t('clearAll')}
</Button>
</Space>
</div> </div>
<Divider style={{ margin: '4px 0', flexShrink: 0 }} />
<Divider style={{ margin: '2px 0', flexShrink: 0 }} />
{/* Items */}
<div style={{ <div style={{
overflowY: 'auto', overflowY: 'auto',
flex: 1 flex: 1
@@ -76,20 +166,26 @@ const Projects: React.FC = () => {
<div <div
key={item.id} key={item.id}
style={{ style={{
padding: '8px 12px', padding: '4px 8px',
cursor: 'pointer', cursor: 'pointer',
'&:hover': { display: 'flex',
backgroundColor: token.colorBgTextHover alignItems: 'center',
} gap: '6px',
borderRadius: '4px',
transition: 'background-color 0.2s'
}} }}
> >
<Checkbox <Checkbox
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
checked={item.selected} checked={item.selected}
onChange={e => handleCheckboxChange(item.id || '', e.target.checked)} onChange={e => handleCheckboxChange(item.id || '', e.target.checked)}
style={{ fontSize: '14px' }}
> >
{item.name} <span style={{ marginLeft: '2px', fontSize: '14px' }}>{item.name}</span>
</Checkbox> </Checkbox>
{item.selected && (
<CheckCircleFilled style={{ color: colors.successColor, fontSize: '10px' }} />
)}
</div> </div>
))} ))}
</div> </div>
@@ -102,8 +198,45 @@ const Projects: React.FC = () => {
} }
}} }}
> >
<Button loading={loadingProjects}> <Button
{t('projects')} <CaretDownFilled /> loading={loadingProjects}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
height: '32px',
borderColor: colors.buttonBorder,
color: colors.buttonText,
fontWeight: activeFiltersCount > 0 ? 500 : 400,
transition: 'all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)',
backgroundColor: colors.buttonBg,
}}
onMouseEnter={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimaryHover;
e.currentTarget.style.boxShadow = `0 2px 4px ${token.colorPrimary}20`;
}
}}
onMouseLeave={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimary;
e.currentTarget.style.boxShadow = 'none';
}
}}
>
<FilterOutlined
style={{
fontSize: '14px',
color: activeFiltersCount > 0 ? token.colorPrimary : token.colorTextTertiary
}}
/>
<span>{getButtonText()}</span>
<CaretDownFilled
style={{
fontSize: '10px',
marginLeft: '2px'
}}
/>
</Button> </Button>
</Dropdown> </Dropdown>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { CaretDownFilled } from '@ant-design/icons'; import { CaretDownFilled, FilterOutlined, CheckCircleFilled } from '@ant-design/icons';
import { Button, Checkbox, Divider, Dropdown, Input, theme } from 'antd'; import { Button, Checkbox, Divider, Dropdown, Input, theme, Space } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useMemo } from 'react';
import type { CheckboxChangeEvent } from 'antd/es/checkbox'; import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ISelectableTeam } from '@/types/reporting/reporting-filters.types'; import { ISelectableTeam } from '@/types/reporting/reporting-filters.types';
@@ -21,10 +21,35 @@ const Team: React.FC = () => {
const { teams, loadingTeams } = useAppSelector(state => state.timeReportsOverviewReducer); 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 => const filteredItems = teams.filter(item =>
item.name?.toLowerCase().includes(searchText.toLowerCase()) 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) => { const handleCheckboxChange = async (key: string, checked: boolean) => {
dispatch(setSelectOrDeselectTeam({ id: key, selected: checked })); dispatch(setSelectOrDeselectTeam({ id: key, selected: checked }));
await dispatch(fetchReportingCategories()); await dispatch(fetchReportingCategories());
@@ -39,6 +64,29 @@ const Team: React.FC = () => {
await dispatch(fetchReportingProjects()); 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 ( return (
<div> <div>
<Dropdown <Dropdown
@@ -47,32 +95,78 @@ const Team: React.FC = () => {
trigger={['click']} trigger={['click']}
dropdownRender={() => ( dropdownRender={() => (
<div style={{ <div style={{
background: token.colorBgContainer, background: colors.dropdownBg,
borderRadius: token.borderRadius, borderRadius: '8px',
boxShadow: token.boxShadow, boxShadow: isDark
? '0 6px 16px 0 rgba(0, 0, 0, 0.32), 0 3px 6px -4px rgba(0, 0, 0, 0.32), 0 9px 28px 8px rgba(0, 0, 0, 0.20)'
: '0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)',
border: `1px solid ${colors.dropdownBorder}`,
padding: '4px 0', padding: '4px 0',
maxHeight: '330px', maxHeight: '330px',
display: 'flex', display: 'flex',
flexDirection: 'column' flexDirection: 'column'
}}> }}>
<div style={{ padding: '8px', flexShrink: 0 }}> {/* Header */}
<div style={{
padding: '4px 4px 2px',
fontWeight: 600,
fontSize: '12px',
color: colors.headerText,
borderBottom: `1px solid ${colors.borderColor}`,
marginBottom: '2px'
}}>
{t('searchByName')}
</div>
{/* Search */}
<div style={{ padding: '4px 8px', flexShrink: 0 }}>
<Input <Input
placeholder={t('searchByName')} placeholder={t('searchByName')}
value={searchText} value={searchText}
onChange={e => setSearchText(e.target.value)} onChange={e => setSearchText(e.target.value)}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
style={{ fontSize: '14px' }}
/> />
</div> </div>
<div style={{ padding: '0 12px', flexShrink: 0 }}>
<Checkbox {/* Actions */}
onClick={e => e.stopPropagation()} <div style={{ padding: '2px 8px', marginBottom: '2px' }}>
onChange={handleSelectAllChange} <Space size="small">
checked={selectAll} <Button
> type="link"
{t('selectAll')} size="small"
</Checkbox> onClick={handleSelectAllClick}
disabled={isAllSelected}
style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isAllSelected ? colors.linkDisabled : colors.linkActive
}}
>
{t('selectAll')}
</Button>
<Divider type="vertical" style={{ margin: '0 2px' }} />
<Button
type="link"
size="small"
onClick={handleClearAll}
disabled={isNoneSelected}
style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isNoneSelected ? colors.linkDisabled : colors.errorColor
}}
>
{t('clearAll')}
</Button>
</Space>
</div> </div>
<Divider style={{ margin: '4px 0', flexShrink: 0 }} />
<Divider style={{ margin: '2px 0', flexShrink: 0 }} />
{/* Items */}
<div style={{ <div style={{
overflowY: 'auto', overflowY: 'auto',
flex: 1 flex: 1
@@ -81,17 +175,26 @@ const Team: React.FC = () => {
<div <div
key={item.id} key={item.id}
style={{ style={{
padding: '8px 12px', padding: '4px 8px',
cursor: 'pointer' cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
borderRadius: '4px',
transition: 'background-color 0.2s'
}} }}
> >
<Checkbox <Checkbox
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
checked={item.selected} checked={item.selected}
onChange={e => handleCheckboxChange(item.id || '', e.target.checked)} onChange={e => handleCheckboxChange(item.id || '', e.target.checked)}
style={{ fontSize: '14px' }}
> >
{item.name} <span style={{ marginLeft: '2px', fontSize: '14px' }}>{item.name}</span>
</Checkbox> </Checkbox>
{item.selected && (
<CheckCircleFilled style={{ color: colors.successColor, fontSize: '10px' }} />
)}
</div> </div>
))} ))}
</div> </div>
@@ -104,8 +207,45 @@ const Team: React.FC = () => {
} }
}} }}
> >
<Button loading={loadingTeams}> <Button
{t('teams')} <CaretDownFilled /> loading={loadingTeams}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
height: '32px',
borderColor: colors.buttonBorder,
color: colors.buttonText,
fontWeight: activeFiltersCount > 0 ? 500 : 400,
transition: 'all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)',
backgroundColor: colors.buttonBg,
}}
onMouseEnter={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimaryHover;
e.currentTarget.style.boxShadow = `0 2px 4px ${token.colorPrimary}20`;
}
}}
onMouseLeave={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimary;
e.currentTarget.style.boxShadow = 'none';
}
}}
>
<FilterOutlined
style={{
fontSize: '14px',
color: activeFiltersCount > 0 ? token.colorPrimary : token.colorTextTertiary
}}
/>
<span>{getButtonText()}</span>
<CaretDownFilled
style={{
fontSize: '10px',
marginLeft: '2px'
}}
/>
</Button> </Button>
</Dropdown> </Dropdown>
</div> </div>

View File

@@ -1,12 +1,11 @@
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { setSelectOrDeselectAllMembers, setSelectOrDeselectAllUtilization, setSelectOrDeselectMember, setSelectOrDeselectUtilization } from '@/features/reporting/time-reports/time-reports-overview.slice'; 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 { 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 { useTranslation } from 'react-i18next';
import { id } from 'date-fns/locale';
const Utilization: React.FC = () => { const Utilization: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -17,10 +16,36 @@ const Utilization: React.FC = () => {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [selectAll, setSelectAll] = useState(true); 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 // Filter members based on search text
const filteredItems = utilization.filter(item => const filteredItems = utilization.filter(item =>
item.name?.toLowerCase().includes(searchText.toLowerCase()) 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 // Handle checkbox change for individual members
const handleCheckboxChange = (id: string, selected: boolean) => { const handleCheckboxChange = (id: string, selected: boolean) => {
dispatch(setSelectOrDeselectUtilization({ id, selected })); dispatch(setSelectOrDeselectUtilization({ id, selected }));
@@ -32,6 +57,25 @@ const Utilization: React.FC = () => {
dispatch(setSelectOrDeselectAllUtilization(isChecked)); 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 ( return (
<Dropdown <Dropdown
menu={undefined} menu={undefined}
@@ -40,27 +84,68 @@ const Utilization: React.FC = () => {
dropdownRender={() => ( dropdownRender={() => (
<div <div
style={{ style={{
background: token.colorBgContainer, background: colors.dropdownBg,
borderRadius: token.borderRadius, borderRadius: '8px',
boxShadow: token.boxShadow, boxShadow: isDark
? '0 6px 16px 0 rgba(0, 0, 0, 0.32), 0 3px 6px -4px rgba(0, 0, 0, 0.32), 0 9px 28px 8px rgba(0, 0, 0, 0.20)'
: '0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)',
border: `1px solid ${colors.dropdownBorder}`,
padding: '4px 0', padding: '4px 0',
maxHeight: '330px', maxHeight: '330px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}} }}
> >
<div style={{ padding: '8px', flexShrink: 0 }}> {/* Header */}
<div style={{
padding: '4px 4px 2px',
fontWeight: 600,
fontSize: '12px',
color: colors.headerText,
borderBottom: `1px solid ${colors.borderColor}`,
marginBottom: '2px'
}}>
{t('utilization')}
</div> </div>
<div style={{ padding: '0 12px', flexShrink: 0 }}>
<Checkbox {/* Actions */}
onClick={e => e.stopPropagation()} <div style={{ padding: '2px 8px', marginBottom: '2px' }}>
onChange={handleSelectAll} <Space size="small">
checked={selectAll} <Button
> type="link"
{t('selectAll')} size="small"
</Checkbox> onClick={handleSelectAllClick}
disabled={isAllSelected}
style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isAllSelected ? colors.linkDisabled : colors.linkActive
}}
>
{t('selectAll')}
</Button>
<Divider type="vertical" style={{ margin: '0 2px' }} />
<Button
type="link"
size="small"
onClick={handleClearAll}
disabled={isNoneSelected}
style={{
padding: '0 2px',
height: 'auto',
fontSize: '11px',
color: isNoneSelected ? colors.linkDisabled : colors.errorColor
}}
>
{t('clearAll')}
</Button>
</Space>
</div> </div>
<Divider style={{ margin: '4px 0', flexShrink: 0 }} />
<Divider style={{ margin: '2px 0', flexShrink: 0 }} />
{/* Items */}
<div <div
style={{ style={{
overflowY: 'auto', overflowY: 'auto',
@@ -71,31 +156,71 @@ const Utilization: React.FC = () => {
<div <div
key={index} key={index}
style={{ style={{
padding: '8px 12px', padding: '4px 8px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '8px', gap: '6px',
cursor: 'pointer', cursor: 'pointer',
'&:hover': { borderRadius: '4px',
backgroundColor: token.colorBgTextHover, transition: 'background-color 0.2s'
},
}} }}
> >
<Checkbox <Checkbox
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
checked={ut.selected} checked={ut.selected}
onChange={e => handleCheckboxChange(ut.id, e.target.checked)} onChange={e => handleCheckboxChange(ut.id, e.target.checked)}
style={{ fontSize: '14px' }}
> >
{ut.name} <span style={{ marginLeft: '2px', fontSize: '14px' }}>{ut.name}</span>
</Checkbox> </Checkbox>
{ut.selected && (
<CheckCircleFilled style={{ color: colors.successColor, fontSize: '10px', marginLeft: 'auto' }} />
)}
</div> </div>
))} ))}
</div> </div>
</div> </div>
)} )}
> >
<Button loading={loadingUtilization}> <Button
{t('utilization')} <CaretDownFilled /> loading={loadingUtilization}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
height: '32px',
borderColor: colors.buttonBorder,
color: colors.buttonText,
fontWeight: activeFiltersCount > 0 ? 500 : 400,
transition: 'all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)',
backgroundColor: colors.buttonBg,
}}
onMouseEnter={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimaryHover;
e.currentTarget.style.boxShadow = `0 2px 4px ${token.colorPrimary}20`;
}
}}
onMouseLeave={(e) => {
if (activeFiltersCount > 0) {
e.currentTarget.style.borderColor = token.colorPrimary;
e.currentTarget.style.boxShadow = 'none';
}
}}
>
<FilterOutlined
style={{
fontSize: '14px',
color: activeFiltersCount > 0 ? token.colorPrimary : token.colorTextTertiary
}}
/>
<span>{getButtonText()}</span>
<CaretDownFilled
style={{
fontSize: '10px',
marginLeft: '2px'
}}
/>
</Button> </Button>
</Dropdown> </Dropdown>
); );

View File

@@ -14,7 +14,11 @@ interface headerState {
export: (key: string) => void; export: (key: string) => void;
} }
const TimeReportingRightHeader: React.FC<headerState> = ({ title, exportType, export: exportFn }) => { const TimeReportingRightHeader: React.FC<headerState> = ({
title,
exportType,
export: exportFn,
}) => {
const { t } = useTranslation('time-report'); const { t } = useTranslation('time-report');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { archived } = useAppSelector(state => state.timeReportsOverviewReducer); const { archived } = useAppSelector(state => state.timeReportsOverviewReducer);
@@ -22,7 +26,7 @@ const TimeReportingRightHeader: React.FC<headerState> = ({ title, exportType, ex
const menuItems = exportType.map(item => ({ const menuItems = exportType.map(item => ({
key: item.key, key: item.key,
label: item.label, label: item.label,
onClick: () => exportFn(item.key) onClick: () => exportFn(item.key),
})); }));
return ( return (
@@ -36,7 +40,7 @@ const TimeReportingRightHeader: React.FC<headerState> = ({ title, exportType, ex
</Checkbox> </Checkbox>
</Button> </Button>
<TimeWiseFilter /> <TimeWiseFilter />
<Dropdown menu={{ items: menuItems }}> <Dropdown menu={{ items: menuItems }}>
<Button type="primary" icon={<DownOutlined />} iconPosition="end"> <Button type="primary" icon={<DownOutlined />} iconPosition="end">
{t('export')} {t('export')}
</Button> </Button>