From 34014298a9e168e7f16b04bc8024fc3e092753e5 Mon Sep 17 00:00:00 2001 From: Saleem Latif Date: Mon, 23 Sep 2024 16:30:48 +0500 Subject: [PATCH] feat: Updated completions and engagments tabs in analytics according to updates on the backend. --- .../AdvanceAnalyticsV2/data/hooks/index.js | 2 + .../hooks/useEnterpriseCompletionsData.js | 85 ++++++++++++++++ .../data/hooks/useEnterpriseEngagementData.js | 85 ++++++++++++++++ .../AdvanceAnalyticsV2/tabs/Completions.jsx | 94 +++++++++++------- .../AdvanceAnalyticsV2/tabs/Engagements.jsx | 98 ++++++++++++------- .../tabs/Engagements.test.jsx | 2 +- 6 files changed, 297 insertions(+), 69 deletions(-) create mode 100644 src/components/AdvanceAnalyticsV2/data/hooks/useEnterpriseCompletionsData.js create mode 100644 src/components/AdvanceAnalyticsV2/data/hooks/useEnterpriseEngagementData.js diff --git a/src/components/AdvanceAnalyticsV2/data/hooks/index.js b/src/components/AdvanceAnalyticsV2/data/hooks/index.js index 7ed4168863..59d2b48aab 100644 --- a/src/components/AdvanceAnalyticsV2/data/hooks/index.js +++ b/src/components/AdvanceAnalyticsV2/data/hooks/index.js @@ -5,6 +5,8 @@ import { advanceAnalyticsQueryKeys } from '../constants'; import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; export { default as useEnterpriseEnrollmentsData } from './useEnterpriseEnrollmentsData'; +export { default as useEnterpriseEngagementData } from './useEnterpriseEngagementData'; +export { default as useEnterpriseCompletionsData } from './useEnterpriseCompletionsData'; export const useEnterpriseAnalyticsData = ({ enterpriseCustomerUUID, diff --git a/src/components/AdvanceAnalyticsV2/data/hooks/useEnterpriseCompletionsData.js b/src/components/AdvanceAnalyticsV2/data/hooks/useEnterpriseCompletionsData.js new file mode 100644 index 0000000000..f210cd8d00 --- /dev/null +++ b/src/components/AdvanceAnalyticsV2/data/hooks/useEnterpriseCompletionsData.js @@ -0,0 +1,85 @@ +import { useQuery } from '@tanstack/react-query'; +import _ from 'lodash'; + +import { useMemo } from 'react'; +import { ANALYTICS_TABS, generateKey } from '../constants'; +import { applyGranularity, applyCalculation } from '../utils'; +import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; + +/** + Applies data transformations to the response data. + + Apply transformations to the response data based on the granularity and calculation. Data transformation is applied + only on the items with the allowed enrollment types. + + * @param {object} response - The response data returned from the API. + * @param {OpUnitType} granularity - Granularity of the data. e.g. `day`, `week`, `month`, `quarter`, `year`. + * @param {String} calculation - Calculation to apply on the data. e.g. + * `total`, `running_total`, `moving_average_3_periods`, `moving_average_7_periods`. + * @param {Array} allowedEnrollTypes - Allowed enrollment types to consider. e.g. [`certificate`, `audit`]. + */ +const applyDataTransformations = (response, granularity, calculation, allowedEnrollTypes = ['certificate', 'audit']) => { + const modifiedResponse = _.cloneDeep(response); + if (modifiedResponse?.data?.completionsOverTime) { + let completionsOverTime = []; + for (let i = 0; i < allowedEnrollTypes.length; i++) { + const data = applyGranularity( + modifiedResponse.data.completionsOverTime.filter( + completion => completion.enrollType === allowedEnrollTypes[i], + ), + 'passedDate', + 'completionCount', + granularity, + ); + completionsOverTime = completionsOverTime.concat( + applyCalculation(data, 'completionCount', calculation), + ); + } + + modifiedResponse.data.completionsOverTime = completionsOverTime; + } + + return modifiedResponse; +}; + +/** + Fetches enterprise completion data. + + * @param {String} enterpriseCustomerUUID - UUID of the enterprise customer. + * @param {Date} startDate - Start date for the data. + * @param {Date} endDate - End date for the data. + * @param {OpUnitType} granularity - Granularity of the data. e.g. `day`, `week`, `month`, `quarter`, `year`. + * @param {String} calculation - Calculation to apply on the data. e.g. + * `total`, `running_total`, `moving_average_3_periods`, `moving_average_7_periods`. + * @param {object} queryOptions - Additional options for the query. + */ +const useEnterpriseCompletionsData = ({ + enterpriseCustomerUUID, + startDate, + endDate, + granularity = undefined, + calculation = undefined, + queryOptions = {}, +}) => { + const requestOptions = { startDate, endDate }; + const response = useQuery({ + queryKey: generateKey('completions', enterpriseCustomerUUID, requestOptions), + queryFn: () => EnterpriseDataApiService.fetchAdminAnalyticsData( + enterpriseCustomerUUID, + ANALYTICS_TABS.COMPLETIONS, + requestOptions, + ), + staleTime: 0.5 * (1000 * 60 * 60), // 30 minutes. The time in milliseconds after data is considered stale. + cacheTime: 0.75 * (1000 * 60 * 60), // 45 minutes. Cache data will be garbage collected after this duration. + keepPreviousData: true, + ...queryOptions, + }); + + return useMemo(() => applyDataTransformations( + response, + granularity, + calculation, + ), [response, granularity, calculation]); +}; + +export default useEnterpriseCompletionsData; diff --git a/src/components/AdvanceAnalyticsV2/data/hooks/useEnterpriseEngagementData.js b/src/components/AdvanceAnalyticsV2/data/hooks/useEnterpriseEngagementData.js new file mode 100644 index 0000000000..cb745ff2ac --- /dev/null +++ b/src/components/AdvanceAnalyticsV2/data/hooks/useEnterpriseEngagementData.js @@ -0,0 +1,85 @@ +import { useQuery } from '@tanstack/react-query'; +import _ from 'lodash'; + +import { useMemo } from 'react'; +import { ANALYTICS_TABS, generateKey } from '../constants'; +import { applyGranularity, applyCalculation } from '../utils'; +import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; + +/** + Applies data transformations to the response data. + + Apply transformations to the response data based on the granularity and calculation. Data transformation is applied + only on the items with the allowed enrollment types. + + * @param {object} response - The response data returned from the API. + * @param {OpUnitType} granularity - Granularity of the data. e.g. `day`, `week`, `month`, `quarter`, `year`. + * @param {String} calculation - Calculation to apply on the data. e.g. + * `total`, `running_total`, `moving_average_3_periods`, `moving_average_7_periods`. + * @param {Array} allowedEnrollTypes - Allowed enrollment types to consider. e.g. [`certificate`, `audit`]. + */ +const applyDataTransformations = (response, granularity, calculation, allowedEnrollTypes = ['certificate', 'audit']) => { + const modifiedResponse = _.cloneDeep(response); + if (modifiedResponse?.data?.engagementOverTime) { + let engagementOverTime = []; + for (let i = 0; i < allowedEnrollTypes.length; i++) { + const data = applyGranularity( + modifiedResponse.data.engagementOverTime.filter( + engagement => engagement.enrollType === allowedEnrollTypes[i], + ), + 'activityDate', + 'learningTimeHours', + granularity, + ); + engagementOverTime = engagementOverTime.concat( + applyCalculation(data, 'learningTimeHours', calculation), + ); + } + + modifiedResponse.data.engagementOverTime = engagementOverTime; + } + + return modifiedResponse; +}; + +/** + Fetches enterprise engagement data. + + * @param {String} enterpriseCustomerUUID - UUID of the enterprise customer. + * @param {Date} startDate - Start date for the data. + * @param {Date} endDate - End date for the data. + * @param {OpUnitType} granularity - Granularity of the data. e.g. `day`, `week`, `month`, `quarter`, `year`. + * @param {String} calculation - Calculation to apply on the data. e.g. + * `total`, `running_total`, `moving_average_3_periods`, `moving_average_7_periods`. + * @param {object} queryOptions - Additional options for the query. + */ +const useEnterpriseEngagementData = ({ + enterpriseCustomerUUID, + startDate, + endDate, + granularity = undefined, + calculation = undefined, + queryOptions = {}, +}) => { + const requestOptions = { startDate, endDate }; + const response = useQuery({ + queryKey: generateKey('engagements', enterpriseCustomerUUID, requestOptions), + queryFn: () => EnterpriseDataApiService.fetchAdminAnalyticsData( + enterpriseCustomerUUID, + ANALYTICS_TABS.ENGAGEMENTS, + requestOptions, + ), + staleTime: 0.5 * (1000 * 60 * 60), // 30 minutes. The time in milliseconds after data is considered stale. + cacheTime: 0.75 * (1000 * 60 * 60), // 45 minutes. Cache data will be garbage collected after this duration. + keepPreviousData: true, + ...queryOptions, + }); + + return useMemo(() => applyDataTransformations( + response, + granularity, + calculation, + ), [response, granularity, calculation]); +}; + +export default useEnterpriseEngagementData; diff --git a/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx b/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx index 5d7a1adac7..9e5d35d54f 100644 --- a/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx +++ b/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx @@ -1,13 +1,14 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; import Header from '../Header'; -import { ANALYTICS_TABS, CHART_TYPES, chartColorMap } from '../data/constants'; +import { ANALYTICS_TABS, chartColorMap } from '../data/constants'; import AnalyticsTable from './AnalyticsTable'; import ChartWrapper from '../charts/ChartWrapper'; -import { useEnterpriseAnalyticsData } from '../data/hooks'; -import DownloadCSV from '../DownloadCSV'; -import { constructChartHoverTemplate } from '../data/utils'; +import { useEnterpriseCompletionsData } from '../data/hooks'; +import DownloadCSVButton from '../DownloadCSVButton'; +import { constructChartHoverTemplate, modifyDataToIntroduceEnrollTypeCount } from '../data/utils'; const Completions = ({ startDate, endDate, granularity, calculation, enterpriseId, @@ -16,7 +17,7 @@ const Completions = ({ const { isFetching, isError, data, - } = useEnterpriseAnalyticsData({ + } = useEnterpriseCompletionsData({ enterpriseCustomerUUID: enterpriseId, key: ANALYTICS_TABS.COMPLETIONS, startDate, @@ -25,6 +26,48 @@ const Completions = ({ calculation, }); + const completionsOverTimeForCSV = useMemo(() => { + const completionsOverTime = modifyDataToIntroduceEnrollTypeCount( + data?.completionsOverTime, + 'passedDate', + 'completionCount', + ); + return completionsOverTime.map(({ activityDate, certificate, audit }) => ({ + activity_date: dayjs.utc(activityDate).toISOString().split('T')[0], + certificate, + audit, + })); + }, [data]); + + const topCoursesByCompletionForCSV = useMemo(() => { + const topCoursesByCompletions = modifyDataToIntroduceEnrollTypeCount( + data?.topCoursesByCompletions, + 'courseKey', + 'completionCount', + ); + return topCoursesByCompletions.map(({ + courseKey, courseTitle, certificate, audit, + }) => ({ + course_key: courseKey, + course_title: courseTitle, + certificate, + audit, + })); + }, [data]); + + const topSubjectsByCompletionsForCSV = useMemo(() => { + const topSubjectsByCompletions = modifyDataToIntroduceEnrollTypeCount( + data?.topSubjectsByCompletions, + 'courseSubject', + 'completionCount', + ); + return topSubjectsByCompletions.map(({ courseSubject, certificate, audit }) => ({ + course_subject: courseSubject, + certificate, + audit, + })); + }, [data]); + return (
@@ -40,14 +83,9 @@ const Completions = ({ description: 'Subtitle for the completions over time chart.', })} DownloadCSVComponent={( - )} /> @@ -58,7 +96,7 @@ const Completions = ({ chartProps={{ data: data?.completionsOverTime, xKey: 'passedDate', - yKey: 'count', + yKey: 'completionCount', colorKey: 'enrollType', colorMap: chartColorMap, xAxisTitle: '', @@ -88,14 +126,9 @@ const Completions = ({ description: 'Subtitle for the top 10 courses by completions chart.', })} DownloadCSVComponent={( - )} /> @@ -106,7 +139,7 @@ const Completions = ({ chartProps={{ data: data?.topCoursesByCompletions, xKey: 'courseKey', - yKey: 'count', + yKey: 'completionCount', colorKey: 'enrollType', colorMap: chartColorMap, yAxisTitle: intl.formatMessage({ @@ -139,14 +172,9 @@ const Completions = ({ description: 'Subtitle for the top 10 subjects by completion chart.', })} DownloadCSVComponent={( - )} /> @@ -157,7 +185,7 @@ const Completions = ({ chartProps={{ data: data?.topSubjectsByCompletions, xKey: 'courseSubject', - yKey: 'count', + yKey: 'completionCount', colorKey: 'enrollType', colorMap: chartColorMap, yAxisTitle: intl.formatMessage({ diff --git a/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx b/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx index 0d18f6f912..5f5396ffa0 100644 --- a/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx +++ b/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx @@ -1,13 +1,14 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; import Header from '../Header'; -import { ANALYTICS_TABS, CHART_TYPES, chartColorMap } from '../data/constants'; +import { ANALYTICS_TABS, chartColorMap } from '../data/constants'; import AnalyticsTable from './AnalyticsTable'; import ChartWrapper from '../charts/ChartWrapper'; -import { useEnterpriseAnalyticsData } from '../data/hooks'; -import DownloadCSV from '../DownloadCSV'; -import { constructChartHoverTemplate } from '../data/utils'; +import { useEnterpriseEngagementData } from '../data/hooks'; +import DownloadCSVButton from '../DownloadCSVButton'; +import { constructChartHoverTemplate, modifyDataToIntroduceEnrollTypeCount } from '../data/utils'; const Engagements = ({ startDate, endDate, granularity, calculation, enterpriseId, @@ -15,7 +16,7 @@ const Engagements = ({ const intl = useIntl(); const { isFetching, isError, data, - } = useEnterpriseAnalyticsData({ + } = useEnterpriseEngagementData({ enterpriseCustomerUUID: enterpriseId, key: ANALYTICS_TABS.ENGAGEMENTS, startDate, @@ -24,6 +25,48 @@ const Engagements = ({ calculation, }); + const engagementOverTimeForCSV = useMemo(() => { + const engagementOverTime = modifyDataToIntroduceEnrollTypeCount( + data?.engagementOverTime, + 'activityDate', + 'learningTimeHours', + ); + return engagementOverTime.map(({ activityDate, certificate, audit }) => ({ + activity_date: dayjs.utc(activityDate).toISOString().split('T')[0], + certificate, + audit, + })); + }, [data]); + + const topCoursesByEngagementForCSV = useMemo(() => { + const topCoursesByEngagement = modifyDataToIntroduceEnrollTypeCount( + data?.topCoursesByEngagement, + 'courseKey', + 'learningTimeHours', + ); + return topCoursesByEngagement.map(({ + courseKey, courseTitle, certificate, audit, + }) => ({ + course_key: courseKey, + course_title: courseTitle, + certificate, + audit, + })); + }, [data]); + + const topSubjectsByEngagementForCSV = useMemo(() => { + const topSubjectsByEngagement = modifyDataToIntroduceEnrollTypeCount( + data?.topSubjectsByEngagement, + 'courseSubject', + 'learningTimeHours', + ); + return topSubjectsByEngagement.map(({ courseSubject, certificate, audit }) => ({ + course_subject: courseSubject, + certificate, + audit, + })); + }, [data]); + return (
@@ -39,14 +82,9 @@ const Engagements = ({ description: 'Subtitle for the learning hours over time chart.', })} DownloadCSVComponent={( - )} /> @@ -55,9 +93,9 @@ const Engagements = ({ isError={isError} chartType="LineChart" chartProps={{ - data: data?.engagementsOverTime, + data: data?.engagementOverTime, xKey: 'activityDate', - yKey: 'sum', + yKey: 'learningTimeHours', colorKey: 'enrollType', colorMap: chartColorMap, xAxisTitle: '', @@ -87,14 +125,9 @@ const Engagements = ({ description: 'Subtitle for the top 10 courses by learning hours chart.', })} DownloadCSVComponent={( - )} /> @@ -104,8 +137,8 @@ const Engagements = ({ chartType="BarChart" chartProps={{ data: data?.topCoursesByEngagement, - xKey: 'courseKey', - yKey: 'count', + xKey: 'courseTitle', + yKey: 'learningTimeHours', colorKey: 'enrollType', colorMap: chartColorMap, yAxisTitle: intl.formatMessage({ @@ -138,14 +171,9 @@ const Engagements = ({ description: 'Subtitle for the top 10 subjects by learning hours chart.', })} DownloadCSVComponent={( - )} /> @@ -156,7 +184,7 @@ const Engagements = ({ chartProps={{ data: data?.topSubjectsByEngagement, xKey: 'courseSubject', - yKey: 'count', + yKey: 'learningTimeHours', colorKey: 'enrollType', colorMap: chartColorMap, yAxisTitle: intl.formatMessage({ diff --git a/src/components/AdvanceAnalyticsV2/tabs/Engagements.test.jsx b/src/components/AdvanceAnalyticsV2/tabs/Engagements.test.jsx index a8f0b33d1f..108bf1c65d 100644 --- a/src/components/AdvanceAnalyticsV2/tabs/Engagements.test.jsx +++ b/src/components/AdvanceAnalyticsV2/tabs/Engagements.test.jsx @@ -38,7 +38,7 @@ const mockAnalyticsTableData = { ], }; const mockAnalyticsChartsData = { - engagementsOverTime: [], + engagementOverTime: [], topCoursesByEngagement: [], topSubjectsByEngagement: [], };