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:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user