feat(reporting): add total time utilization component and update member time sheets to include totals

This commit is contained in:
shancds
2025-05-29 17:50:11 +05:30
parent b5288a8da2
commit 1f6bbce0ae
6 changed files with 93 additions and 14 deletions

View File

@@ -504,8 +504,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
const loggedSeconds = member.logged_time ? parseFloat(member.logged_time) : 0;
const utilizedHours = loggedSeconds / 3600;
const utilizationPercent = totalWorkingSeconds > 0 && loggedSeconds
? ((loggedSeconds / totalWorkingSeconds) * 100)
: 0;
? ((loggedSeconds / totalWorkingSeconds) * 100)
: 0;
const overUnder = utilizedHours - totalWorkingHours;
member.value = utilizedHours ? parseFloat(utilizedHours.toFixed(2)) : 0;
@@ -516,11 +516,11 @@ export default class ReportingAllocationController extends ReportingControllerBa
member.over_under_utilized_hours = overUnder.toFixed(2);
if (utilizationPercent < 90) {
member.utilization_state = 'under';
member.utilization_state = 'under';
} else if (utilizationPercent <= 110) {
member.utilization_state = 'optimal';
member.utilization_state = 'optimal';
} else {
member.utilization_state = 'over';
member.utilization_state = 'over';
}
}
@@ -528,7 +528,21 @@ export default class ReportingAllocationController extends ReportingControllerBa
? result.rows.filter(member => utilization.includes(member.utilization_state))
: result.rows;
return res.status(200).send(new ServerResponse(true, filteredRows));
// Calculate totals
const total_time_logs = filteredRows.reduce((sum, member) => sum + parseFloat(member.logged_time || '0'), 0);
const total_estimated_hours = totalWorkingHours;
const total_utilization = total_time_logs > 0 && totalWorkingSeconds > 0
? ((total_time_logs / totalWorkingSeconds) * 100).toFixed(2)
: '0.00';
return res.status(200).send(new ServerResponse(true, {
filteredRows,
totals: {
total_time_logs: ((total_time_logs / 3600).toFixed(2)).toString(),
total_estimated_hours: total_estimated_hours.toString(),
total_utilization: total_utilization.toString(),
},
}));
}
@HandleExceptions()

View File

@@ -3,7 +3,7 @@ import { toQueryString } from '@/utils/toQueryString';
import apiClient from '../api-client';
import { IServerResponse } from '@/types/common.types';
import { IAllocationViewModel } from '@/types/reporting/reporting-allocation.types';
import { IProjectLogsBreakdown, IRPTTimeMember, IRPTTimeProject, ITimeLogBreakdownReq } from '@/types/reporting/reporting.types';
import { IProjectLogsBreakdown, IRPTTimeMember, IRPTTimeMemberViewModel, IRPTTimeProject, ITimeLogBreakdownReq } from '@/types/reporting/reporting.types';
const rootUrl = `${API_BASE_URL}/reporting`;
@@ -25,7 +25,7 @@ export const reportingTimesheetApiService = {
return response.data;
},
getMemberTimeSheets: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeMember[]>> => {
getMemberTimeSheets: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeMemberViewModel>> => {
const q = toQueryString({ archived });
const response = await apiClient.post(`${rootUrl}/time-reports/members/${q}`, body);
return response.data;

View File

@@ -19,11 +19,14 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels);
interface MembersTimeSheetProps {
onTotalsUpdate: (totals: { total_time_logs: string; total_estimated_hours: string; total_utilization: string }) => void;
}
export interface MembersTimeSheetRef {
exportChart: () => void;
}
const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
const MembersTimeSheet = forwardRef<MembersTimeSheetRef, MembersTimeSheetProps>(({ onTotalsUpdate }, ref) => {
const { t } = useTranslation('time-report');
const dispatch = useAppDispatch();
const chartRef = React.useRef<ChartJS<'bar', string[], unknown>>(null);
@@ -181,8 +184,17 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
};
const res = await reportingTimesheetApiService.getMemberTimeSheets(body, archived);
console.log('Members Time Sheet Data:', res.body.totals);
if (res.done) {
setJsonData(res.body || []);
setJsonData(res.body.filteredRows || []);
const totalsRaw = res.body.totals || {};
const totals = {
total_time_logs: totalsRaw.total_time_logs ?? "0",
total_estimated_hours: totalsRaw.total_estimated_hours ?? "0",
total_utilization: totalsRaw.total_utilization ?? "0",
};
onTotalsUpdate(totals);
}
} catch (error) {
console.error('Error fetching chart data:', error);

View File

@@ -4,12 +4,18 @@ import MembersTimeSheet, { MembersTimeSheetRef } from '@/pages/reporting/time-re
import TimeReportingRightHeader from './timeReportingRightHeader/TimeReportingRightHeader';
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import { useRef } from 'react';
import { useRef, useState } from 'react';
import TotalTimeUtilization from './total-time-utilization/total-time-utilization';
import { IRPTTimeTotals } from '@/types/reporting/reporting.types';
const MembersTimeReports = () => {
const { t } = useTranslation('time-report');
const chartRef = useRef<MembersTimeSheetRef>(null);
const [totals, setTotals] = useState<IRPTTimeTotals>({
total_time_logs: "0",
total_estimated_hours: "0",
total_utilization: "0",
});
useDocumentTitle('Reporting - Allocation');
const handleExport = (type: string) => {
@@ -18,6 +24,10 @@ const MembersTimeReports = () => {
}
};
const handleTotalsUpdate = (newTotals: IRPTTimeTotals) => {
setTotals(newTotals);
};
return (
<Flex vertical>
<TimeReportingRightHeader
@@ -25,7 +35,7 @@ const MembersTimeReports = () => {
exportType={[{ key: 'png', label: 'PNG' }]}
export={handleExport}
/>
<TotalTimeUtilization totals={totals} />
<Card
style={{ borderRadius: '4px' }}
title={
@@ -41,7 +51,7 @@ const MembersTimeReports = () => {
},
}}
>
<MembersTimeSheet ref={chartRef} />
<MembersTimeSheet onTotalsUpdate={handleTotalsUpdate} ref={chartRef} />
</Card>
</Flex>
);

View File

@@ -0,0 +1,34 @@
import { Card, Flex } from 'antd';
import React, { useEffect } from 'react';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { IRPTTimeTotals } from '@/types/reporting/reporting.types';
interface TotalTimeUtilizationProps {
totals: IRPTTimeTotals;
}
const TotalTimeUtilization: React.FC<TotalTimeUtilizationProps> = ({ totals }) => {
return (
<Flex gap={16} style={{ marginBottom: '16px' }}>
<Card style={{ borderRadius: '4px', flex: 1 }}>
<div>
<div style={{ fontSize: 14, color: '#888' }}>Total Time Logs</div>
<div style={{ fontSize: 24, fontWeight: 600 }}>{totals.total_time_logs}h</div>
</div>
</Card>
<Card style={{ borderRadius: '4px', flex: 1 }}>
<div>
<div style={{ fontSize: 14, color: '#888' }}>Estimated Hours</div>
<div style={{ fontSize: 24, fontWeight: 600 }}>{totals.total_estimated_hours}h</div>
</div>
</Card>
<Card style={{ borderRadius: '4px', flex: 1 }}>
<div>
<div style={{ fontSize: 14, color: '#888' }}>Utilization (%)</div>
<div style={{ fontSize: 24, fontWeight: 600 }}>{totals.total_utilization}%</div>
</div>
</Card>
</Flex>
);
};
export default TotalTimeUtilization;

View File

@@ -411,6 +411,15 @@ export interface IRPTTimeMember {
over_under_utilized_hours?: string;
utilization_state?: string;
}
export interface IRPTTimeTotals {
total_estimated_hours?: string;
total_time_logs?: string;
total_utilization?: string;
}
export interface IRPTTimeMemberViewModel {
filteredRows?: IRPTTimeMember[];
totals?: IRPTTimeTotals;
}
export interface IMemberTaskStatGroupResonse {
team_member_name: string;