feat(reporting): add total time utilization component and update member time sheets to include totals
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user