feat(reporting): add total time utilization component and update member time sheets to include totals
This commit is contained in:
@@ -528,7 +528,21 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
? result.rows.filter(member => utilization.includes(member.utilization_state))
|
? result.rows.filter(member => utilization.includes(member.utilization_state))
|
||||||
: result.rows;
|
: 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()
|
@HandleExceptions()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { toQueryString } from '@/utils/toQueryString';
|
|||||||
import apiClient from '../api-client';
|
import apiClient from '../api-client';
|
||||||
import { IServerResponse } from '@/types/common.types';
|
import { IServerResponse } from '@/types/common.types';
|
||||||
import { IAllocationViewModel } from '@/types/reporting/reporting-allocation.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`;
|
const rootUrl = `${API_BASE_URL}/reporting`;
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export const reportingTimesheetApiService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getMemberTimeSheets: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeMember[]>> => {
|
getMemberTimeSheets: async (body = {}, archived = false): Promise<IServerResponse<IRPTTimeMemberViewModel>> => {
|
||||||
const q = toQueryString({ archived });
|
const q = toQueryString({ archived });
|
||||||
const response = await apiClient.post(`${rootUrl}/time-reports/members/${q}`, body);
|
const response = await apiClient.post(`${rootUrl}/time-reports/members/${q}`, body);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -19,11 +19,14 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels);
|
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 {
|
export interface MembersTimeSheetRef {
|
||||||
exportChart: () => void;
|
exportChart: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
|
const MembersTimeSheet = forwardRef<MembersTimeSheetRef, MembersTimeSheetProps>(({ onTotalsUpdate }, ref) => {
|
||||||
const { t } = useTranslation('time-report');
|
const { t } = useTranslation('time-report');
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const chartRef = React.useRef<ChartJS<'bar', string[], unknown>>(null);
|
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);
|
const res = await reportingTimesheetApiService.getMemberTimeSheets(body, archived);
|
||||||
|
console.log('Members Time Sheet Data:', res.body.totals);
|
||||||
if (res.done) {
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching chart data:', 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 TimeReportingRightHeader from './timeReportingRightHeader/TimeReportingRightHeader';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
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 MembersTimeReports = () => {
|
||||||
const { t } = useTranslation('time-report');
|
const { t } = useTranslation('time-report');
|
||||||
const chartRef = useRef<MembersTimeSheetRef>(null);
|
const chartRef = useRef<MembersTimeSheetRef>(null);
|
||||||
|
const [totals, setTotals] = useState<IRPTTimeTotals>({
|
||||||
|
total_time_logs: "0",
|
||||||
|
total_estimated_hours: "0",
|
||||||
|
total_utilization: "0",
|
||||||
|
});
|
||||||
useDocumentTitle('Reporting - Allocation');
|
useDocumentTitle('Reporting - Allocation');
|
||||||
|
|
||||||
const handleExport = (type: string) => {
|
const handleExport = (type: string) => {
|
||||||
@@ -18,6 +24,10 @@ const MembersTimeReports = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTotalsUpdate = (newTotals: IRPTTimeTotals) => {
|
||||||
|
setTotals(newTotals);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
<TimeReportingRightHeader
|
<TimeReportingRightHeader
|
||||||
@@ -25,7 +35,7 @@ const MembersTimeReports = () => {
|
|||||||
exportType={[{ key: 'png', label: 'PNG' }]}
|
exportType={[{ key: 'png', label: 'PNG' }]}
|
||||||
export={handleExport}
|
export={handleExport}
|
||||||
/>
|
/>
|
||||||
|
<TotalTimeUtilization totals={totals} />
|
||||||
<Card
|
<Card
|
||||||
style={{ borderRadius: '4px' }}
|
style={{ borderRadius: '4px' }}
|
||||||
title={
|
title={
|
||||||
@@ -41,7 +51,7 @@ const MembersTimeReports = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MembersTimeSheet ref={chartRef} />
|
<MembersTimeSheet onTotalsUpdate={handleTotalsUpdate} ref={chartRef} />
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</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;
|
over_under_utilized_hours?: string;
|
||||||
utilization_state?: 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 {
|
export interface IMemberTaskStatGroupResonse {
|
||||||
team_member_name: string;
|
team_member_name: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user