From 1f6bbce0ae7d9ead30cce99eb08098d70e245617 Mon Sep 17 00:00:00 2001 From: shancds Date: Thu, 29 May 2025 17:50:11 +0530 Subject: [PATCH] feat(reporting): add total time utilization component and update member time sheets to include totals --- .../reporting-allocation-controller.ts | 26 ++++++++++---- .../reporting.timesheet.api.service.ts | 4 +-- .../members-time-sheet/members-time-sheet.tsx | 16 +++++++-- .../timeReports/members-time-reports.tsx | 18 +++++++--- .../total-time-utilization.tsx | 34 +++++++++++++++++++ .../src/types/reporting/reporting.types.ts | 9 +++++ 6 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 worklenz-frontend/src/pages/reporting/timeReports/total-time-utilization/total-time-utilization.tsx diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 9add9696..98feafd9 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -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() diff --git a/worklenz-frontend/src/api/reporting/reporting.timesheet.api.service.ts b/worklenz-frontend/src/api/reporting/reporting.timesheet.api.service.ts index 1529d46b..488a0619 100644 --- a/worklenz-frontend/src/api/reporting/reporting.timesheet.api.service.ts +++ b/worklenz-frontend/src/api/reporting/reporting.timesheet.api.service.ts @@ -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> => { + getMemberTimeSheets: async (body = {}, archived = false): Promise> => { const q = toQueryString({ archived }); const response = await apiClient.post(`${rootUrl}/time-reports/members/${q}`, body); return response.data; diff --git a/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx b/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx index b5096183..4c517925 100644 --- a/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx +++ b/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx @@ -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((_, ref) => { +const MembersTimeSheet = forwardRef(({ onTotalsUpdate }, ref) => { const { t } = useTranslation('time-report'); const dispatch = useAppDispatch(); const chartRef = React.useRef>(null); @@ -181,8 +184,17 @@ const MembersTimeSheet = forwardRef((_, 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); diff --git a/worklenz-frontend/src/pages/reporting/timeReports/members-time-reports.tsx b/worklenz-frontend/src/pages/reporting/timeReports/members-time-reports.tsx index cdbcebd1..e777eed6 100644 --- a/worklenz-frontend/src/pages/reporting/timeReports/members-time-reports.tsx +++ b/worklenz-frontend/src/pages/reporting/timeReports/members-time-reports.tsx @@ -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(null); - + const [totals, setTotals] = useState({ + 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 ( { exportType={[{ key: 'png', label: 'PNG' }]} export={handleExport} /> - + { }, }} > - + ); diff --git a/worklenz-frontend/src/pages/reporting/timeReports/total-time-utilization/total-time-utilization.tsx b/worklenz-frontend/src/pages/reporting/timeReports/total-time-utilization/total-time-utilization.tsx new file mode 100644 index 00000000..ce760d4c --- /dev/null +++ b/worklenz-frontend/src/pages/reporting/timeReports/total-time-utilization/total-time-utilization.tsx @@ -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 = ({ totals }) => { + return ( + + +
+
Total Time Logs
+
{totals.total_time_logs}h
+
+
+ +
+
Estimated Hours
+
{totals.total_estimated_hours}h
+
+
+ +
+
Utilization (%)
+
{totals.total_utilization}%
+
+
+
+ ); +}; + +export default TotalTimeUtilization; \ No newline at end of file diff --git a/worklenz-frontend/src/types/reporting/reporting.types.ts b/worklenz-frontend/src/types/reporting/reporting.types.ts index 6ca74391..a0ff7bf7 100644 --- a/worklenz-frontend/src/types/reporting/reporting.types.ts +++ b/worklenz-frontend/src/types/reporting/reporting.types.ts @@ -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;