feat(reporting): enhance utilization tracking and filtering in time reports

This commit is contained in:
shancds
2025-05-29 15:38:25 +05:30
parent f1920c17b4
commit b94c56f50d
7 changed files with 233 additions and 26 deletions

View File

@@ -37,6 +37,8 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
loadingProjects,
members,
loadingMembers,
utilization,
loadingUtilization,
billable,
archived,
} = useAppSelector(state => state.timeReportsOverviewReducer);
@@ -100,22 +102,24 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
const hours = member?.utilized_hours || '0.00';
const percent = parseFloat(member?.utilization_percent || '0.00');
const overUnder = member?.over_under_utilized_hours || '0.00';
let status = '';
let color = '';
if (percent < 90) {
status = 'Under';
color = '🟧';
} else if (percent <= 110) {
status = 'Optimal';
color = '🟩';
} else {
status = 'Over';
color = '🟥';
switch (member.utilization_state) {
case 'under':
color = '🟧';
break;
case 'optimal':
color = '🟩';
break;
case 'over':
color = '🟥';
break;
default:
color = '';
}
return [
`${context.dataset.label}: ${hours} h`,
`${color} Utilization: ${percent}%`,
`${status} Utilized: ${overUnder} h`
`${member.utilization_state} Utilized: ${overUnder} h`
];
},
}
@@ -163,13 +167,14 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
const selectedTeams = teams.filter(team => team.selected);
const selectedProjects = filterProjects.filter(project => project.selected);
const selectedCategories = categories.filter(category => category.selected);
const selectedMembers = members.filter(member => member.selected); // Use selected members
const selectedMembers = members.filter(member => member.selected);
const selectedUtilization = utilization.filter(item => item.selected);
const body = {
teams: selectedTeams.map(t => t.id),
projects: selectedProjects.map(project => project.id),
categories: selectedCategories.map(category => category.id),
members: selectedMembers.map(member => member.id), // Include members in the request
members: selectedMembers.map(member => member.id),
utilization: selectedUtilization.map(item => item.id),
duration,
date_range: dateRange,
billable,
@@ -189,7 +194,7 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
useEffect(() => {
fetchChartData();
}, [dispatch, duration, dateRange, billable, archived, teams, filterProjects, categories, members]);
}, [dispatch, duration, dateRange, billable, archived, teams, filterProjects, categories, members, utilization]);
const exportChart = () => {
if (chartRef.current) {

View File

@@ -9,8 +9,10 @@ import {
fetchReportingProjects,
fetchReportingCategories,
fetchReportingMembers,
fetchReportingUtilization,
} from '@/features/reporting/time-reports/time-reports-overview.slice';
import Members from './members';
import Utilization from './utilization';
const TimeReportPageHeader: React.FC = () => {
const dispatch = useAppDispatch();
@@ -21,6 +23,7 @@ const TimeReportPageHeader: React.FC = () => {
await dispatch(fetchReportingCategories());
await dispatch(fetchReportingProjects());
await dispatch(fetchReportingMembers());
await dispatch(fetchReportingUtilization());
};
fetchData();
@@ -33,6 +36,7 @@ const TimeReportPageHeader: React.FC = () => {
<Projects />
<Billable />
<Members/>
<Utilization />
</div>
);
};

View File

@@ -0,0 +1,104 @@
import React, { useState } from 'react';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { setSelectOrDeselectAllMembers, setSelectOrDeselectAllUtilization, setSelectOrDeselectMember, setSelectOrDeselectUtilization } from '@/features/reporting/time-reports/time-reports-overview.slice';
import { Button, Checkbox, Divider, Dropdown, Input, Avatar, theme } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { CaretDownFilled } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { id } from 'date-fns/locale';
const Utilization: React.FC = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation('time-report');
const { utilization, loadingUtilization } = useAppSelector(state => state.timeReportsOverviewReducer);
const { token } = theme.useToken();
const [searchText, setSearchText] = useState('');
const [selectAll, setSelectAll] = useState(true);
// Filter members based on search text
const filteredItems = utilization.filter(item =>
item.name?.toLowerCase().includes(searchText.toLowerCase())
);
// Handle checkbox change for individual members
const handleCheckboxChange = (id: string, selected: boolean) => {
dispatch(setSelectOrDeselectUtilization({ id, selected }));
};
const handleSelectAll = (e: CheckboxChangeEvent) => {
const isChecked = e.target.checked;
setSelectAll(isChecked);
dispatch(setSelectOrDeselectAllUtilization(isChecked));
};
return (
<Dropdown
menu={undefined}
placement="bottomLeft"
trigger={['click']}
dropdownRender={() => (
<div
style={{
background: token.colorBgContainer,
borderRadius: token.borderRadius,
boxShadow: token.boxShadow,
padding: '4px 0',
maxHeight: '330px',
display: 'flex',
flexDirection: 'column',
}}
>
<div style={{ padding: '8px', flexShrink: 0 }}>
</div>
<div style={{ padding: '0 12px', flexShrink: 0 }}>
<Checkbox
onClick={e => e.stopPropagation()}
onChange={handleSelectAll}
checked={selectAll}
>
{t('selectAll')}
</Checkbox>
</div>
<Divider style={{ margin: '4px 0', flexShrink: 0 }} />
<div
style={{
overflowY: 'auto',
flex: 1,
}}
>
{filteredItems.map((ut, index) => (
<div
key={index}
style={{
padding: '8px 12px',
display: 'flex',
alignItems: 'center',
gap: '8px',
cursor: 'pointer',
'&:hover': {
backgroundColor: token.colorBgTextHover,
},
}}
>
<Checkbox
onClick={e => e.stopPropagation()}
checked={ut.selected}
onChange={e => handleCheckboxChange(ut.id, e.target.checked)}
>
{ut.name}
</Checkbox>
</div>
))}
</div>
</div>
)}
>
<Button loading={loadingUtilization}>
{t('utilization')} <CaretDownFilled />
</Button>
</Dropdown>
);
};
export default Utilization;