diff --git a/src/components/Reports/Reports.jsx b/src/components/Reports/Reports.jsx
index 3432ae800e..d991ee27b8 100644
--- a/src/components/Reports/Reports.jsx
+++ b/src/components/Reports/Reports.jsx
@@ -708,7 +708,7 @@ endDate: moment()
fontSize={15}
isPermissionPage
darkMode={darkMode}
- defaultText="Click this to see only people who logged/contributed a minimum of 10 tangible hours. This is used for identifying actual contributors vs. people who never started, were immediately terminated, etc."
+ defaultText="This report only shows Team Members who have contributed more than 10 Hours."
/>
diff --git a/src/components/Reports/TotalReport/TotalContributorsReport.jsx b/src/components/Reports/TotalReport/TotalContributorsReport.jsx
index 5371db1232..9923786981 100644
--- a/src/components/Reports/TotalReport/TotalContributorsReport.jsx
+++ b/src/components/Reports/TotalReport/TotalContributorsReport.jsx
@@ -1,235 +1,237 @@
-import axios from 'axios';
import PropTypes from 'prop-types';
-import { useCallback, useEffect, useMemo, useState } from 'react';
-import { ENDPOINTS } from '~/utils/URL';
+import { useState } from 'react';
+import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
import Loading from '../../common/Loading';
import EditableInfoModal from '../../UserProfile/EditableModal/EditableInfoModal';
-import {
- getCachedData,
- logApiRequest,
- logApiResponse,
- setCachedData,
- validateUserList,
-} from './cacheUtils';
-import { generateBarData as generateBarDataUtil } from './generateBarData';
+import useContributorsData from './useContributorsData';
import styles from './TotalReport.module.css';
import TotalReportBarGraph from './TotalReportBarGraph';
-function TotalContributorsReport({ startDate, endDate, userProfiles, darkMode, userRole }) {
- const [contributors, setContributors] = useState([]);
- const [loading, setLoading] = useState(true);
- const [timeEntries, setTimeEntries] = useState([]);
- const [showMonthly, setShowMonthly] = useState(false);
- const [showYearly, setShowYearly] = useState(false);
- const [contributorsInMonth, setContributorsInMonth] = useState([]);
- const [contributorsInYear, setContributorsInYear] = useState([]);
-
- const fromDate = useMemo(() => startDate.toLocaleDateString('en-CA'), [startDate]);
- const toDate = useMemo(() => endDate.toLocaleDateString('en-CA'), [endDate]);
- const userList = useMemo(() => {
- return userProfiles?.map(({ _id }) => _id) || [];
- }, [userProfiles]);
-
- // Fetch time entries for the selected period
- const loadTimeEntriesForPeriod = useCallback(async (controller) => {
- const reportName = 'TotalContributorsReport';
- const url = ENDPOINTS.TIME_ENTRIES_REPORTS;
-
- if (!url) {
- return;
- }
+const MIN_DATE = new Date('01/01/2010');
- // Validate userList
- if (!validateUserList(userList, userProfiles, reportName)) {
- setTimeEntries([]);
- setLoading(false);
- return;
- }
+const formatDate = date =>
+ date.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' });
- // Check cache with date range key
- const cacheKey = `${reportName}_${fromDate}_${toDate}`;
- const cached = getCachedData(cacheKey, reportName);
- if (cached.data) {
- setTimeEntries(cached.data);
- setLoading(false);
- return;
- }
+const getDateRangeLabel = (startDate, endDate) => `${formatDate(startDate)} to ${formatDate(endDate)}`;
- try {
- logApiRequest(reportName, url, { users: userList, fromDate, toDate }, {
- usersCount: userList?.length,
- });
-
- const response = await axios.post(
- url,
- { users: userList, fromDate, toDate },
- { signal: controller.signal }
- );
-
- logApiResponse(reportName, response.data?.length);
-
- const mappedTimeEntries = response.data.map(entry => ({
- userId: entry.personId,
- hours: entry.hours,
- minutes: entry.minutes,
- isTangible: entry.isTangible,
- date: entry.dateOfWork,
- }));
-
- setTimeEntries(mappedTimeEntries);
- setCachedData(cacheKey, mappedTimeEntries, reportName);
- } catch (error) {
- // eslint-disable-next-line import/no-named-as-default-member
- if (!axios.isCancel(error)) {
- // eslint-disable-next-line no-console
- console.error(`${reportName} API Error:`, error);
- setTimeEntries([]);
- }
- }
- }, [fromDate, toDate, userList, userProfiles]);
-
- // Group time entries by user and calculate total hours
- const sumByUser = useCallback((entries) => {
- return entries.reduce((acc, { userId, hours = 0, minutes = 0, isTangible = false }) => {
- if (!acc[userId]) {
- acc[userId] = {
- userId,
- hours: 0,
- minutes: 0,
- tangibleTime: 0,
- };
- }
- const numHours = parseFloat(hours) || 0;
- const numMinutes = parseFloat(minutes) || 0;
- acc[userId].hours += numHours;
- acc[userId].minutes += numMinutes;
- if (isTangible) {
- acc[userId].tangibleTime += numHours + numMinutes / 60;
- }
- return acc;
- }, {});
- }, []);
-
- // Filter users who have contributed more than 10 hours
- const filterContributors = useCallback((users) => {
- return users.filter(user =>
- (user.hours + user.minutes/60) >= 10
- );
- }, []);
-
- // Group entries by time range (month/year)
- const groupByTimeRange = useCallback((entries, timeRange) => {
- return entries.reduce((acc, entry) => {
- const date = new Date(entry.date);
- const key = timeRange === 'month'
- ? `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
- : `${date.getFullYear()}`;
-
- if (!acc[key]) {
- acc[key] = [];
- }
- acc[key].push(entry);
- return acc;
- }, {});
- }, []);
-
- // Generate summary data for the specified time range
- const summaryOfTimeRange = useCallback((timeRange) => {
- const groupedEntries = Object.entries(groupByTimeRange(timeEntries, timeRange));
- return groupedEntries.map(([key, entries]) => {
- const groupedUsers = Object.values(sumByUser(entries));
- const contributedUsers = filterContributors(groupedUsers);
- return { timeRange: key, usersOfTime: contributedUsers };
- });
- }, [timeEntries, groupByTimeRange, sumByUser, filterContributors]);
-
- // Generate bar chart data
- const generateBarData = useCallback((groupedDate, isYear = false) => {
- return generateBarDataUtil(groupedDate, isYear, startDate, endDate, 'usersOfTime');
- }, [startDate, endDate]);
-
- // Check if we should show monthly/yearly summaries
- const checkPeriodForSummary = useCallback(() => {
- const oneMonth = 1000 * 60 * 60 * 24 * 31;
- const diffDate = endDate - startDate;
- if (diffDate > oneMonth) {
- setContributorsInMonth(generateBarData(summaryOfTimeRange('month')));
- setContributorsInYear(generateBarData(summaryOfTimeRange('year'), true));
- if (diffDate <= oneMonth * 12) {
- setShowMonthly(true);
- }
- if (startDate.getFullYear() !== endDate.getFullYear()) {
- setShowYearly(true);
- }
+// Renders the summary stats and charts for a single period.
+function ContributorsPeriodPanel({ startDate, endDate, data, idSuffix, title }) {
+ const {
+ contributors,
+ totalTangibleTime,
+ showMonthly,
+ showYearly,
+ contributorsInMonth,
+ contributorsInYear,
+ } = data;
+ const dateRangeLabel = getDateRangeLabel(startDate, endDate);
+
+ return (
+
-
Contributors Report
+
+ Contributors Report
+
-
- In the period from {startDate.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' })} to {endDate.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' })}:
-
-
-
{contributors.length}
-
members have contributed more than 10 hours.
-
-
-
{totalTangibleTime.toFixed(2)}
-
hours of tangible time have been logged.
-
-
- {showMonthly && contributorsInMonth.length > 0 && (
-
- )}
- {showYearly && contributorsInYear.length > 0 && (
-
+
+
+
+
+ Compare to another period
+
+ {comparisonEnabled && (
+
+
+ Compare Start Date
+
+
+
+ Compare End Date
+
+
+
)}
+
+ {comparisonEnabled ? (
+
+
+ {comparison.loading ? (
+
+
+
+ ) : (
+
+ )}
+
+ ) : (
+
+ )}
);
}
@@ -237,9 +239,11 @@ function TotalContributorsReport({ startDate, endDate, userProfiles, darkMode, u
TotalContributorsReport.propTypes = {
startDate: PropTypes.instanceOf(Date).isRequired,
endDate: PropTypes.instanceOf(Date).isRequired,
- userProfiles: PropTypes.arrayOf(PropTypes.shape({
- _id: PropTypes.string,
- })),
+ userProfiles: PropTypes.arrayOf(
+ PropTypes.shape({
+ _id: PropTypes.string,
+ }),
+ ),
darkMode: PropTypes.bool,
userRole: PropTypes.string,
};
@@ -250,4 +254,4 @@ TotalContributorsReport.defaultProps = {
userRole: '',
};
-export default TotalContributorsReport;
\ No newline at end of file
+export default TotalContributorsReport;
diff --git a/src/components/Reports/TotalReport/TotalReport.module.css b/src/components/Reports/TotalReport/TotalReport.module.css
index d94ed96449..78e03e9ba4 100644
--- a/src/components/Reports/TotalReport/TotalReport.module.css
+++ b/src/components/Reports/TotalReport/TotalReport.module.css
@@ -39,6 +39,63 @@
font-weight: 400;
}
+.comparisonControls {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin: 12px 0 8px;
+}
+
+.comparisonToggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ margin: 0;
+}
+
+.comparePickers {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 16px;
+}
+
+.comparePickerItem {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.comparisonGrid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 28px;
+ align-items: start;
+}
+
+.comparisonPanel {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ min-width: 0;
+}
+
+.panelTitle {
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 1.3;
+ margin-bottom: 0;
+ color: steelblue;
+}
+
+@media screen and (width <= 700px) {
+ .comparisonGrid {
+ grid-template-columns: 1fr;
+ }
+}
+
.totalDetail {
margin-top: 20px;
}
@@ -65,6 +122,39 @@
font-size: inherit;
}
+.contributorPeriodLabel {
+ font-size: 17px;
+ line-height: 1.45;
+}
+
+.contributorSummaryMetrics {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.contributorSummaryItem {
+ display: grid;
+ grid-template-columns: 112px minmax(0, 1fr);
+ align-items: center;
+ column-gap: 14px;
+ min-height: 44px;
+}
+
+.contributorSummaryNumber {
+ margin-right: 0;
+ font-size: 30px;
+ line-height: 1.15;
+ text-align: right;
+}
+
+.contributorSummaryText {
+ flex: 1 1 auto;
+ min-width: 0;
+ font-size: 18px;
+ line-height: 1.45;
+}
+
@media screen and (width <= 1100px) {
.totalTitle,
.totalNumber,
@@ -75,6 +165,24 @@
.totalText {
font-size: 12px;
}
+
+ .contributorPeriodLabel {
+ font-size: 16px;
+ }
+
+ .contributorSummaryItem {
+ grid-template-columns: 96px minmax(0, 1fr);
+ column-gap: 12px;
+ }
+
+ .contributorSummaryNumber {
+ font-size: 26px;
+ }
+
+ .contributorSummaryText {
+ font-size: 16px;
+ line-height: 1.45;
+ }
}
@media screen and (width <= 700px) {
@@ -87,12 +195,42 @@
.totalText {
font-size: 14px;
}
+
+ .comparisonPanel {
+ gap: 12px;
+ }
+
+ .contributorPeriodLabel {
+ font-size: 16px;
+ }
+
+ .contributorSummaryItem {
+ grid-template-columns: 104px minmax(0, 1fr);
+ }
+
+ .contributorSummaryNumber {
+ font-size: 28px;
+ }
+
+ .contributorSummaryText {
+ font-size: 16px;
+ line-height: 1.45;
+ }
}
@media screen and (width <= 550px) {
.totalContainer {
padding: 15px;
}
+
+ .contributorSummaryItem {
+ grid-template-columns: 88px minmax(0, 1fr);
+ column-gap: 10px;
+ }
+
+ .contributorSummaryNumber {
+ font-size: 24px;
+ }
}
/* Dark Mode Styles - Using global dark-mode class for consistency across builds */
@@ -105,6 +243,10 @@
color: #60a5fa;
}
+:global(.dark-mode) .panelTitle {
+ color: #60a5fa;
+}
+
:global(.dark-mode) .totalPeriod {
color: #d1d5db;
}
@@ -134,4 +276,3 @@
:global(.dark-mode) .totalWarning {
color: #fbbf24;
}
-
diff --git a/src/components/Reports/TotalReport/TotalReportBarGraph.jsx b/src/components/Reports/TotalReport/TotalReportBarGraph.jsx
index 7f809eb5a7..2f7d35a40d 100644
--- a/src/components/Reports/TotalReport/TotalReportBarGraph.jsx
+++ b/src/components/Reports/TotalReport/TotalReportBarGraph.jsx
@@ -3,23 +3,52 @@ import * as d3 from 'd3';
import styles from './TotalReportBarGraph.module.css';
import { useSelector } from 'react-redux';
-function TotalReportBarGraph({ barData, range }) {
- const svgId = `svg-container-${range}`;
+function TotalReportBarGraph({
+ barData,
+ range,
+ idSuffix = '',
+ fallbackLabel = '',
+}) {
+ const svgId = `svg-container-${range}${idSuffix}`;
const darkMode = useSelector(state => state.theme.darkMode);
const containerRef = useRef(null);
+ const isComparisonChart = idSuffix.includes('compare');
+ const barColor = isComparisonChart
+ ? darkMode
+ ? '#f59e0b'
+ : '#d97706'
+ : darkMode
+ ? '#4a90e2'
+ : '#5b6ee1';
+ const aboveBarLabelColor = darkMode ? '#f8fafc' : '#1f2937';
+ const insideLabelMinHeight = 34;
+
+ const getReadableLabel = label => {
+ const text = label === null || label === undefined ? '' : String(label);
+ if (!text || text.includes('NaN') || text === 'Invalid Date') {
+ return fallbackLabel || 'Selected date range';
+ }
+ return text;
+ };
const drawChart = (data, darkmode) => {
- data.sort((a, b) => (a.label > b.label ? 1 : -1));
+ const normalizedData = data
+ .map(item => ({
+ ...item,
+ label: getReadableLabel(item.label),
+ value: Number.isNaN(Number(item.value)) ? 0 : Number(item.value),
+ }))
+ .sort((a, b) => (a.label > b.label ? 1 : -1));
const container = containerRef.current;
- const { width: containerWidth } = container.getBoundingClientRect();
- const containerHeight = containerWidth;
+ const { width: containerWidth } = container.getBoundingClientRect();
+ const containerHeight = Math.max(containerWidth * 0.75, 320);
const margin = { top: 10, right: 8, bottom: 100, left: 20 };
const width = containerWidth - margin.left - margin.right;
const height = containerHeight - margin.top - margin.bottom;
- const maxValue = Math.max(...data.map(d => d.value));
+ const maxValue = Math.max(...normalizedData.map(d => d.value), 1);
const svg = d3
// eslint-disable-next-line testing-library/no-node-access
@@ -37,7 +66,7 @@ function TotalReportBarGraph({ barData, range }) {
const xScale = d3
.scaleBand()
- .domain(data.map(d => d.label))
+ .domain(normalizedData.map(d => d.label))
.range([0, width])
.padding(0.4);
@@ -46,43 +75,32 @@ function TotalReportBarGraph({ barData, range }) {
.domain([0, maxValue])
.range([height, 0]);
- const colorScale = d3
- .scaleLinear()
- .domain([0, maxValue])
- .range(darkMode ? ['#4a90e2', '#003366'] : ['#f5a3a3', '#c3b6f7']);
- data.forEach(d => {
+ normalizedData.forEach(d => {
const x = xScale(d.label);
const barHeight = height - yScale(d.value);
const y = yScale(d.value);
+ const labelFitsInside = barHeight >= insideLabelMinHeight;
-
chart
.append('rect')
.attr('x', x)
.attr('y', y)
.attr('width', xScale.bandwidth())
.attr('height', barHeight)
- .attr('fill', colorScale(d.value));
-
-
- const yText =
- barHeight >= 30
- ? y + barHeight / 2
- :
- y - 10;
+ .attr('fill', barColor);
chart
.append('text')
.attr('x', x + xScale.bandwidth() / 2)
- .attr('y', yText)
+ .attr('y', labelFitsInside ? y + barHeight / 2 : Math.max(y - 8, 14))
.attr('text-anchor', 'middle')
- .style('fill', darkmode ? 'white' : 'black')
- .style('font-size', '20px')
+ .attr('dominant-baseline', labelFitsInside ? 'middle' : 'baseline')
+ .style('fill', labelFitsInside ? 'white' : aboveBarLabelColor)
+ .style('font-size', '24px')
.style('font-weight', 'bold')
- .text(Number.isNaN(d.value) ? '' : d.value);
+ .text(d.value > 0 ? d.value : '');
});
-
chart
.append('g')
.attr('transform', `translate(0, ${height})`)
@@ -92,7 +110,7 @@ function TotalReportBarGraph({ barData, range }) {
.style('text-anchor', 'middle')
.style('font-size', '14px')
.style('fill', 'black')
- .style('fill', darkmode ? 'white' : 'black');
+ .style('fill', darkmode ? 'white' : 'black');
};
useEffect(() => {
diff --git a/src/components/Reports/TotalReport/useContributorsData.js b/src/components/Reports/TotalReport/useContributorsData.js
new file mode 100644
index 0000000000..73637e69ef
--- /dev/null
+++ b/src/components/Reports/TotalReport/useContributorsData.js
@@ -0,0 +1,229 @@
+import axios from 'axios';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { ENDPOINTS } from '~/utils/URL';
+import {
+ getCachedData,
+ logApiRequest,
+ logApiResponse,
+ setCachedData,
+ validateUserList,
+} from './cacheUtils';
+import { generateBarData as generateBarDataUtil } from './generateBarData';
+
+const REPORT_NAME = 'TotalContributorsReport';
+const ONE_MONTH = 1000 * 60 * 60 * 24 * 31;
+
+// Group time entries by user and calculate total hours
+const sumByUser = entries => {
+ return entries.reduce((acc, { userId, hours = 0, minutes = 0, isTangible = false }) => {
+ if (!acc[userId]) {
+ acc[userId] = { userId, hours: 0, minutes: 0, tangibleTime: 0 };
+ }
+ const numHours = parseFloat(hours) || 0;
+ const numMinutes = parseFloat(minutes) || 0;
+ acc[userId].hours += numHours;
+ acc[userId].minutes += numMinutes;
+ if (isTangible) {
+ acc[userId].tangibleTime += numHours + numMinutes / 60;
+ }
+ return acc;
+ }, {});
+};
+
+// Filter users who have contributed more than 10 hours
+const filterContributors = users => users.filter(user => user.hours + user.minutes / 60 >= 10);
+
+// Group entries by time range (month/year)
+const groupByTimeRange = (entries, timeRange) => {
+ return entries.reduce((acc, entry) => {
+ const date = new Date(entry.date);
+ const key =
+ timeRange === 'month'
+ ? `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
+ : `${date.getFullYear()}`;
+ if (!acc[key]) {
+ acc[key] = [];
+ }
+ acc[key].push(entry);
+ return acc;
+ }, {});
+};
+
+// Generate summary data for the specified time range
+const summaryOfTimeRange = (entries, timeRange) => {
+ const groupedEntries = Object.entries(groupByTimeRange(entries, timeRange));
+ return groupedEntries.map(([key, rangeEntries]) => {
+ const groupedUsers = Object.values(sumByUser(rangeEntries));
+ const contributedUsers = filterContributors(groupedUsers);
+ return { timeRange: key, usersOfTime: contributedUsers };
+ });
+};
+
+/**
+ * Encapsulates fetching + processing the Contributors Report data for a single
+ * date range. Returns the computed summary so it can be rendered for both the
+ * primary period and an optional comparison period.
+ *
+ * @param {Object} params
+ * @param {Date} params.startDate
+ * @param {Date} params.endDate
+ * @param {Array} params.userProfiles
+ * @param {boolean} params.enabled - When false, no fetching/processing happens
+ * (used to skip work for the comparison period until the user enables it).
+ */
+export default function useContributorsData({ startDate, endDate, userProfiles, enabled = true }) {
+ const [loading, setLoading] = useState(true);
+ const [contributors, setContributors] = useState([]);
+ const [showMonthly, setShowMonthly] = useState(false);
+ const [showYearly, setShowYearly] = useState(false);
+ const [contributorsInMonth, setContributorsInMonth] = useState([]);
+ const [contributorsInYear, setContributorsInYear] = useState([]);
+
+ const fromDate = useMemo(() => startDate.toLocaleDateString('en-CA'), [startDate]);
+ const toDate = useMemo(() => endDate.toLocaleDateString('en-CA'), [endDate]);
+ const userList = useMemo(() => userProfiles?.map(({ _id }) => _id) || [], [userProfiles]);
+
+ const resetReportData = useCallback(() => {
+ setContributors([]);
+ setShowMonthly(false);
+ setShowYearly(false);
+ setContributorsInMonth([]);
+ setContributorsInYear([]);
+ }, []);
+
+ const processTimeEntries = useCallback(
+ entries => {
+ if (entries.length === 0) {
+ resetReportData();
+ return;
+ }
+
+ const groupedUsers = Object.values(sumByUser(entries));
+ setContributors(filterContributors(groupedUsers));
+
+ const diffDate = endDate - startDate;
+ let monthly = false;
+ let yearly = false;
+ let monthData = [];
+ let yearData = [];
+ if (diffDate > ONE_MONTH) {
+ monthData = generateBarDataUtil(
+ summaryOfTimeRange(entries, 'month'),
+ false,
+ startDate,
+ endDate,
+ 'usersOfTime',
+ );
+ yearData = generateBarDataUtil(
+ summaryOfTimeRange(entries, 'year'),
+ true,
+ startDate,
+ endDate,
+ 'usersOfTime',
+ );
+ if (diffDate <= ONE_MONTH * 12) {
+ monthly = true;
+ }
+ if (startDate.getFullYear() !== endDate.getFullYear()) {
+ yearly = true;
+ }
+ }
+
+ setShowMonthly(monthly);
+ setShowYearly(yearly);
+ setContributorsInMonth(monthData);
+ setContributorsInYear(yearData);
+ },
+ [endDate, resetReportData, startDate],
+ );
+
+ // Fetch time entries for the selected period
+ const loadTimeEntriesForPeriod = useCallback(
+ async controller => {
+ const url = ENDPOINTS.TIME_ENTRIES_REPORTS;
+ if (!url) {
+ return;
+ }
+
+ if (!validateUserList(userList, userProfiles, REPORT_NAME)) {
+ resetReportData();
+ setLoading(false);
+ return;
+ }
+
+ const cacheKey = `${REPORT_NAME}_${fromDate}_${toDate}`;
+ const cached = getCachedData(cacheKey, REPORT_NAME);
+ if (cached.data) {
+ processTimeEntries(cached.data);
+ setLoading(false);
+ return;
+ }
+
+ try {
+ logApiRequest(
+ REPORT_NAME,
+ url,
+ { users: userList, fromDate, toDate },
+ { usersCount: userList?.length },
+ );
+
+ const response = await axios.post(
+ url,
+ { users: userList, fromDate, toDate },
+ { signal: controller.signal },
+ );
+
+ logApiResponse(REPORT_NAME, response.data?.length);
+
+ const mappedTimeEntries = response.data.map(entry => ({
+ userId: entry.personId,
+ hours: entry.hours,
+ minutes: entry.minutes,
+ isTangible: entry.isTangible,
+ date: entry.dateOfWork,
+ }));
+
+ processTimeEntries(mappedTimeEntries);
+ setCachedData(cacheKey, mappedTimeEntries, REPORT_NAME);
+ } catch (error) {
+ // eslint-disable-next-line import/no-named-as-default-member
+ if (!axios.isCancel(error)) {
+ // eslint-disable-next-line no-console
+ console.error(`${REPORT_NAME} API Error:`, error);
+ resetReportData();
+ }
+ }
+ },
+ [fromDate, processTimeEntries, resetReportData, toDate, userList, userProfiles],
+ );
+
+ // Load data when the date range changes
+ useEffect(() => {
+ if (!enabled) {
+ setLoading(false);
+ return undefined;
+ }
+ if (!userList || userList.length === 0) {
+ return undefined;
+ }
+
+ setLoading(true);
+ const controller = new AbortController();
+ loadTimeEntriesForPeriod(controller).then(() => {
+ setLoading(false);
+ });
+ return () => controller.abort();
+ }, [enabled, loadTimeEntriesForPeriod, userList]);
+
+ const totalTangibleTime = contributors.reduce((acc, obj) => acc + Number(obj.tangibleTime), 0);
+
+ return {
+ loading,
+ contributors,
+ totalTangibleTime,
+ showMonthly,
+ showYearly,
+ contributorsInMonth,
+ contributorsInYear,
+ };
+}