From 8f7d90e2831d30bf2d87689644e0d9c566ed36ad Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Tue, 19 Dec 2023 21:12:41 +0000 Subject: [PATCH 1/2] fix: adding country attribute mapping to sso self serve stepper --- .../SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx index 31070334ca..cd2a6e1efe 100644 --- a/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx +++ b/src/components/settings/SettingsSSOTab/steps/NewSSOConfigConfigureStep.tsx @@ -103,6 +103,14 @@ const SSOConfigConfigureStep = () => { fieldInstructions="URN of SAML attribute containing the user's email address[es]." /> + + + ); const renderSAPFields = () => ( From 2d7e691905069d068295f6743f7aa736c671f40e Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Tue, 2 Jan 2024 09:33:45 -0500 Subject: [PATCH 2/2] feat: add additional segment events (#1132) * feat: add addtional segment events * feat: close and open chip modal events * feat: naming for track events added * feat: more segment eventing work * feat: breadcrumbs and budget overview eventing added * feat: individual cancel and remind modal events * feat: cancel and remind submission events * feat: bulk cancel/remind eventing * feat: Add failed redemption, refator select all events * fix: updates how assignment configuration is retrieved * chore: tests * chore: more tests * chore: PR feedback * feat: add additional metadata across all events * chore: Update tests * chore: PR feedback with abstractions * chore: Update comments * chore: PR fixes * chore: remove extraneous code from test --- .../AssignmentStatusTableCell.jsx | 77 ++++++++++--- .../AssignmentTableCancel.jsx | 101 ++++++++++++++++-- .../AssignmentTableRemind.jsx | 97 +++++++++++++++-- .../BudgetAssignmentsTable.jsx | 2 +- .../BudgetDetailActivityTabContents.jsx | 4 +- .../BudgetDetailPageBreadcrumbs.jsx | 59 +++++++--- .../BudgetDetailPageOverviewAvailability.jsx | 48 ++++++++- .../BudgetDetailPageOverviewUtilization.jsx | 25 ++++- .../CancelAssignmentModal.jsx | 3 + .../PendingAssignmentCancelButton.jsx | 91 +++++++++++++++- .../PendingAssignmentRemindButton.jsx | 90 +++++++++++++++- .../RemindAssignmentModal.jsx | 4 +- .../FailedBadEmail.jsx | 37 +++++-- .../FailedCancellation.jsx | 41 +++++-- .../FailedRedemption.jsx | 43 ++++++-- .../FailedReminder.jsx | 42 ++++++-- .../assignments-status-chips/FailedSystem.jsx | 42 ++++++-- .../NotifyingLearner.jsx | 26 +++-- .../WaitingForLearner.jsx | 38 +++++-- .../cards/NewAssignmentModalButton.jsx | 29 +++-- .../cards/tests/CourseCard.test.jsx | 1 - .../data/hooks/index.js | 1 + .../data/hooks/useAssignmentStatusChip.jsx | 39 +++++++ .../learner-credit-management/data/utils.js | 57 ++++++++++ .../tests/BudgetDetailPage.test.jsx | 25 +++++ .../tests/BudgetDetailPageWrapper.test.jsx | 39 +++++-- src/eventTracking.js | 40 +++++++ 27 files changed, 972 insertions(+), 129 deletions(-) create mode 100644 src/components/learner-credit-management/data/hooks/useAssignmentStatusChip.jsx diff --git a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx index d68cf57d9f..8dc0c0da52 100644 --- a/src/components/learner-credit-management/AssignmentStatusTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentStatusTableCell.jsx @@ -1,5 +1,7 @@ import { Chip } from '@edx/paragon'; import PropTypes from 'prop-types'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import FailedBadEmail from './assignments-status-chips/FailedBadEmail'; import FailedCancellation from './assignments-status-chips/FailedCancellation'; import FailedRedemption from './assignments-status-chips/FailedRedemption'; @@ -8,14 +10,61 @@ import FailedSystem from './assignments-status-chips/FailedSystem'; import NotifyingLearner from './assignments-status-chips/NotifyingLearner'; import WaitingForLearner from './assignments-status-chips/WaitingForLearner'; import { capitalizeFirstLetter } from '../../utils'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; -const AssignmentStatusTableCell = ({ row }) => { +const AssignmentStatusTableCell = ({ enterpriseId, row }) => { const { original } = row; const { learnerEmail, learnerState, errorReason, } = original; + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; + + const sharedTrackEventMetadata = { + learnerState, + subsidyUuid, + assignmentConfiguration, + isSubsidyActive, + isAssignable, + catalogUuid, + aggregates, + }; + + const sendGenericTrackEvent = (eventName, eventMetadata = {}) => { + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + { + ...sharedTrackEventMetadata, + ...eventMetadata, + }, + ); + }; + + const sendErrorStateTrackEvent = (eventName, eventMetadata = {}) => { + const errorReasonMetadata = { + erroredAction: { + errorReason: errorReason?.errorReason || null, + actionType: errorReason?.actionType || null, + }, + }; + const errorStateMetadata = { + ...sharedTrackEventMetadata, + ...errorReasonMetadata, + ...eventMetadata, + }; + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + errorStateMetadata, + ); + }; + // Learner state is not available for this assignment, so don't display anything. if (!learnerState) { return null; @@ -24,43 +73,42 @@ const AssignmentStatusTableCell = ({ row }) => { // Display the appropriate status chip based on the learner state. if (learnerState === 'notifying') { return ( - + ); } if (learnerState === 'waiting') { return ( - + ); } if (learnerState === 'failed') { // If learnerState is failed but no top-level error reason is defined, return a failed system chip. if (!errorReason) { - return ; + return ; } // Determine which failure chip to display based on the top level errorReason. In most cases, the actual errorReason // code is ignored, in which case we key off the actionType. if (errorReason.actionType === 'notified') { if (errorReason.errorReason === 'email_error') { return ( - + ); } - // non-email errors on failed notifications should NOT use the FailedBadEmail chip. - return ; + return ; } if (errorReason.actionType === 'cancelled') { - return ; + return ; } if (errorReason.actionType === 'reminded') { - return ; + return ; } if (errorReason.actionType === 'redeemed') { - return ; + return ; } // In all other unexpected cases, return a failed system chip. - return ; + return ; } // Note: The given `learnerState` not officially supported with a `ModalPopup`, but display it anyway. @@ -68,6 +116,7 @@ const AssignmentStatusTableCell = ({ row }) => { }; AssignmentStatusTableCell.propTypes = { + enterpriseId: PropTypes.string.isRequired, row: PropTypes.shape({ original: PropTypes.shape({ learnerEmail: PropTypes.string, @@ -84,4 +133,8 @@ AssignmentStatusTableCell.propTypes = { }).isRequired, }; -export default AssignmentStatusTableCell; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(AssignmentStatusTableCell); diff --git a/src/components/learner-credit-management/AssignmentTableCancel.jsx b/src/components/learner-credit-management/AssignmentTableCancel.jsx index db155be926..eac61c8f31 100644 --- a/src/components/learner-credit-management/AssignmentTableCancel.jsx +++ b/src/components/learner-credit-management/AssignmentTableCancel.jsx @@ -2,8 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from '@edx/paragon'; import { DoNotDisturbOn } from '@edx/paragon/icons'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import CancelAssignmentModal from './CancelAssignmentModal'; import useCancelContentAssignments from './data/hooks/useCancelContentAssignments'; +import { transformSelectedRows, useBudgetId, useSubsidyAccessPolicy } from './data'; +import EVENT_NAMES from '../../eventTracking'; import { getActiveTableColumnFilters } from '../../utils'; const calculateTotalToCancel = ({ @@ -17,9 +21,23 @@ const calculateTotalToCancel = ({ return assignmentUuids.length; }; -const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, tableInstance }) => { - const assignmentUuids = selectedFlatRows.map(row => row.id); - const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; +const AssignmentTableCancelAction = ({ + selectedFlatRows, isEntireTableSelected, learnerStateCounts, tableInstance, enterpriseId, +}) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; + + const { + uniqueLearnerState, + uniqueAssignmentState, + uniqueContentKeys, + totalContentQuantity, + assignmentUuids, + totalSelectedRows, + } = transformSelectedRows(selectedFlatRows); const activeFilters = getActiveTableColumnFilters(tableInstance.columns); @@ -32,7 +50,66 @@ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, close, isOpen, open, - } = useCancelContentAssignments(assignmentConfigurationUuid, assignmentUuids, shouldCancelAll); + } = useCancelContentAssignments(assignmentConfiguration.uuid, assignmentUuids, shouldCancelAll); + + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_CANCEL_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_CANCEL_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_CANCEL, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const trackEvent = (eventName) => { + // constructs a learner state object for the select all state to match format of select all on page metadata + const learnerStateObject = {}; + learnerStateCounts.forEach((learnerState) => { + learnerStateObject[learnerState.learnerState] = learnerState.count; + }); + + const selectedRowsMetadata = isEntireTableSelected + ? { uniqueLearnerState: learnerStateObject, totalSelectedRows: tableInstance.itemCount } + : { + uniqueLearnerState, uniqueAssignmentState, uniqueContentKeys, totalContentQuantity, totalSelectedRows, + }; + + const trackEventMetadata = { + ...selectedRowsMetadata, + isAssignable, + isSubsidyActive, + subsidyUuid, + catalogUuid, + isEntireTableSelected, + assignmentUuids, + aggregates, + assignmentConfiguration, + isOpen: !isOpen, + }; + + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + trackEventMetadata, + ); + }; + + const openModal = () => { + open(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_CANCEL_MODAL, + ); + }; + + const closeModal = () => { + close(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_CANCEL_MODAL, + ); + }; + + const cancellationTrackEvent = () => { + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_CANCEL, + ); + }; const tableItemCount = tableInstance.itemCount; const totalToCancel = calculateTotalToCancel({ @@ -43,14 +120,15 @@ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, return ( <> - @@ -58,12 +136,21 @@ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, }; AssignmentTableCancelAction.propTypes = { + enterpriseId: PropTypes.string.isRequired, selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired, isEntireTableSelected: PropTypes.bool.isRequired, + learnerStateCounts: PropTypes.arrayOf(PropTypes.shape({ + learnerState: PropTypes.string.isRequired, + count: PropTypes.number.isRequired, + })).isRequired, tableInstance: PropTypes.shape({ itemCount: PropTypes.number.isRequired, columns: PropTypes.arrayOf(PropTypes.shape()).isRequired, }).isRequired, }; -export default AssignmentTableCancelAction; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(AssignmentTableCancelAction); diff --git a/src/components/learner-credit-management/AssignmentTableRemind.jsx b/src/components/learner-credit-management/AssignmentTableRemind.jsx index d501178a11..d4eb2f00f1 100644 --- a/src/components/learner-credit-management/AssignmentTableRemind.jsx +++ b/src/components/learner-credit-management/AssignmentTableRemind.jsx @@ -2,8 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from '@edx/paragon'; import { Mail } from '@edx/paragon/icons'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import useRemindContentAssignments from './data/hooks/useRemindContentAssignments'; import RemindAssignmentModal from './RemindAssignmentModal'; +import { transformSelectedRows, useBudgetId, useSubsidyAccessPolicy } from './data'; +import EVENT_NAMES from '../../eventTracking'; import { getActiveTableColumnFilters } from '../../utils'; const calculateTotalToRemind = ({ @@ -19,10 +23,23 @@ const calculateTotalToRemind = ({ }; const AssignmentTableRemindAction = ({ - selectedFlatRows, isEntireTableSelected, learnerStateCounts, tableInstance, + selectedFlatRows, isEntireTableSelected, learnerStateCounts, tableInstance, enterpriseId, }) => { - const assignmentUuids = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').map(({ id }) => id); - const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration; + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; + + const remindableRows = selectedFlatRows.filter(row => row.original.learnerState === 'waiting'); + const { + uniqueLearnerState, + uniqueAssignmentState, + uniqueContentKeys, + totalContentQuantity, + assignmentUuids, + totalSelectedRows, + } = transformSelectedRows(remindableRows); const activeFilters = getActiveTableColumnFilters(tableInstance.columns); @@ -35,7 +52,7 @@ const AssignmentTableRemindAction = ({ close, isOpen, open, - } = useRemindContentAssignments(assignmentConfigurationUuid, assignmentUuids, shouldRemindAll); + } = useRemindContentAssignments(assignmentConfiguration.uuid, assignmentUuids, shouldRemindAll); const selectedRemindableRowCount = calculateTotalToRemind({ assignmentUuids, @@ -43,21 +60,81 @@ const AssignmentTableRemindAction = ({ learnerStateCounts, }); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_REMIND_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_REMIND_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_REMIND, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const trackEvent = (eventName) => { + // constructs a learner state object for the select all state to match format of select all on page metadata + const learnerStateObject = {}; + learnerStateCounts.forEach((learnerState) => { + learnerStateObject[learnerState.learnerState] = learnerState.count; + }); + + const selectedRowsMetadata = isEntireTableSelected + ? { uniqueLearnerState: learnerStateObject, totalSelectedRows: selectedRemindableRowCount } + : { + uniqueLearnerState, uniqueAssignmentState, uniqueContentKeys, totalContentQuantity, totalSelectedRows, + }; + + const trackEventMetadata = { + ...selectedRowsMetadata, + isAssignable, + isSubsidyActive, + subsidyUuid, + catalogUuid, + isEntireTableSelected, + assignmentUuids, + aggregates, + assignmentConfiguration, + isOpen: !isOpen, + }; + + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + trackEventMetadata, + ); + }; + + const openModal = () => { + open(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_REMIND_MODAL, + ); + }; + + const closeModal = () => { + close(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_REMIND_MODAL, + ); + }; + + const reminderTrackEvent = () => { + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_REMIND, + ); + }; + return ( <> @@ -66,6 +143,7 @@ const AssignmentTableRemindAction = ({ AssignmentTableRemindAction.propTypes = { selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired, + enterpriseId: PropTypes.string.isRequired, isEntireTableSelected: PropTypes.bool.isRequired, learnerStateCounts: PropTypes.arrayOf(PropTypes.shape({ learnerState: PropTypes.string.isRequired, @@ -73,7 +151,12 @@ AssignmentTableRemindAction.propTypes = { })).isRequired, tableInstance: PropTypes.shape({ columns: PropTypes.arrayOf(PropTypes.shape()).isRequired, + itemCount: PropTypes.number.isRequired, }).isRequired, }; -export default AssignmentTableRemindAction; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(AssignmentTableRemindAction); diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx index 3b3c7e76d1..b9d6aa7dc3 100644 --- a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -119,7 +119,7 @@ const BudgetAssignmentsTable = ({ EmptyTableComponent={CustomDataTableEmptyState} bulkActions={[ , - , + , ]} /> ); diff --git a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx index 8c41d33eef..21e65d2de1 100644 --- a/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx +++ b/src/components/learner-credit-management/BudgetDetailActivityTabContents.jsx @@ -21,8 +21,8 @@ const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) isTopDownAssignmentEnabled, }); - // // If the budget activity overview data is loading (either the initial request OR any - // // background re-fetching), show a skeleton. + // If the budget activity overview data is loading (either the initial request OR any + // background re-fetching), show a skeleton. if (isBudgetActivityOverviewLoading || isBudgetActivityOverviewFetching || !budgetActivityOverview) { return ( <> diff --git a/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx b/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx index fd8ae54555..5d4c9f4294 100644 --- a/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx @@ -3,27 +3,60 @@ import { connect } from 'react-redux'; import { Breadcrumb } from '@edx/paragon'; import { Link } from 'react-router-dom'; import React from 'react'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import EVENT_NAMES from '../../eventTracking'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; -const BudgetDetailPageBreadcrumbs = ({ enterpriseSlug, budgetDisplayName }) => ( -
- -
-); +const BudgetDetailPageBreadcrumbs = ({ enterpriseId, enterpriseSlug, budgetDisplayName }) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + + const trackEventMetadata = {}; + if (subsidyAccessPolicy) { + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, catalogUuid, aggregates, isAssignable, + } = subsidyAccessPolicy; + Object.assign( + trackEventMetadata, + { + subsidyUuid, + assignmentConfiguration, + isSubsidyActive, + isAssignable, + catalogUuid, + aggregates, + }, + ); + } + + return ( +
+ sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BREADCRUMB_FROM_BUDGET_DETAIL_TO_BUDGETS, + trackEventMetadata, + )} + /> +
+ ); +}; const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, enterpriseSlug: state.portalConfiguration.enterpriseSlug, }); BudgetDetailPageBreadcrumbs.propTypes = { + enterpriseId: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, budgetDisplayName: PropTypes.string.isRequired, }; diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx index 39fa31dbe8..9be186816a 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -7,8 +7,10 @@ import { } from '@edx/paragon'; import { Add } from '@edx/paragon/icons'; import { generatePath, useRouteMatch, Link } from 'react-router-dom'; -import { formatPrice } from './data'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from './data'; import { configuration } from '../../config'; +import EVENT_NAMES from '../../eventTracking'; const BudgetDetail = ({ available, utilized, limit }) => { const currentProgressBarLimit = (available / limit) * 100; @@ -38,9 +40,29 @@ BudgetDetail.propTypes = { limit: PropTypes.number.isRequired, }; -const BudgetActions = ({ budgetId, isAssignable }) => { +const BudgetActions = ({ budgetId, isAssignable, enterpriseId }) => { const routeMatch = useRouteMatch(); const supportUrl = configuration.ENTERPRISE_SUPPORT_URL; + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + + const trackEventMetadata = {}; + if (subsidyAccessPolicy) { + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, catalogUuid, aggregates, + } = subsidyAccessPolicy; + Object.assign( + trackEventMetadata, + { + subsidyUuid, + assignmentConfiguration, + isSubsidyActive, + isAssignable, + catalogUuid, + aggregates, + }, + ); + } const isLargeScreenOrGreater = useMediaQuery({ query: `(min-width: ${breakpoints.small.minWidth}px)` }); @@ -53,7 +75,17 @@ const BudgetActions = ({ budgetId, isAssignable }) => { Funds from this budget are set to auto-allocate to registered learners based on settings configured with your support team.

- @@ -73,6 +105,11 @@ const BudgetActions = ({ budgetId, isAssignable }) => { pathname: generatePath(routeMatch.path, { budgetId, activeTabKey: 'catalog' }), state: { budgetActivityScrollToKey: 'catalog' }, }} + onClick={() => sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BUDGET_OVERVIEW_NEW_ASSIGNMENT, + trackEventMetadata, + )} > New course assignment @@ -84,6 +121,7 @@ const BudgetActions = ({ budgetId, isAssignable }) => { BudgetActions.propTypes = { budgetId: PropTypes.string.isRequired, isAssignable: PropTypes.bool.isRequired, + enterpriseId: PropTypes.string.isRequired, }; const BudgetDetailPageOverviewAvailability = ({ @@ -91,6 +129,7 @@ const BudgetDetailPageOverviewAvailability = ({ isAssignable, budgetTotalSummary: { available, utilized, limit }, enterpriseFeatures, + enterpriseId, }) => ( @@ -101,6 +140,7 @@ const BudgetDetailPageOverviewAvailability = ({ @@ -120,9 +160,11 @@ BudgetDetailPageOverviewAvailability.propTypes = { enterpriseFeatures: PropTypes.shape({ topDownAssignmentRealTimeLcm: PropTypes.bool, }).isRequired, + enterpriseId: PropTypes.string.isRequired, }; const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, }); diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx index 2276670b81..f4865abd7d 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx @@ -5,11 +5,13 @@ import { Stack, Collapsible, Row, Col, Button, } from '@edx/paragon'; import { ArrowDownward } from '@edx/paragon/icons'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { generatePath, useRouteMatch, Link, } from 'react-router-dom'; import { formatPrice } from './data'; +import EVENT_NAMES from '../../eventTracking'; const BudgetDetailPageOverviewUtilization = ({ budgetId, @@ -17,10 +19,15 @@ const BudgetDetailPageOverviewUtilization = ({ budgetAggregates, isAssignable, enterpriseFeatures, + enterpriseId, }) => { const routeMatch = useRouteMatch(); - const { amountAllocatedUsd, amountRedeemedUsd } = budgetAggregates; + const { + BUDGET_OVERVIEW_UTILIZATION_VIEW_ASSIGNED_TABLE, + BUDGET_OVERVIEW_UTILIZATION_VIEW_SPENT_TABLE, + BUDGET_OVERVIEW_UTILIZATION_DROPDOWN_TOGGLE, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; if (!budgetId || !enterpriseFeatures.topDownAssignmentRealTimeLcm || utilized <= 0 || !isAssignable) { return null; @@ -32,6 +39,9 @@ const BudgetDetailPageOverviewUtilization = ({ } const linkText = (type === 'assigned') ? 'View assigned activity' : 'View spent activity'; + const eventNameType = (type === 'assigned') + ? BUDGET_OVERVIEW_UTILIZATION_VIEW_ASSIGNED_TABLE + : BUDGET_OVERVIEW_UTILIZATION_VIEW_SPENT_TABLE; return ( @@ -55,6 +69,13 @@ const BudgetDetailPageOverviewUtilization = ({ className="mt-4 budget-utilization-container" styling="basic" title={
Utilization details
} + onToggle={(open) => sendEnterpriseTrackEvent( + enterpriseId, + BUDGET_OVERVIEW_UTILIZATION_DROPDOWN_TOGGLE, + { + isOpen: open, + }, + )} > @@ -121,10 +142,12 @@ BudgetDetailPageOverviewUtilization.propTypes = { enterpriseFeatures: PropTypes.shape({ topDownAssignmentRealTimeLcm: PropTypes.bool, }).isRequired, + enterpriseId: PropTypes.string.isRequired, }; const mapStateToProps = state => ({ enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, + enterpriseId: state.portalConfiguration.enterpriseId, }); export default connect(mapStateToProps)(BudgetDetailPageOverviewUtilization); diff --git a/src/components/learner-credit-management/CancelAssignmentModal.jsx b/src/components/learner-credit-management/CancelAssignmentModal.jsx index cce7b896b1..13657a184b 100644 --- a/src/components/learner-credit-management/CancelAssignmentModal.jsx +++ b/src/components/learner-credit-management/CancelAssignmentModal.jsx @@ -12,6 +12,7 @@ const CancelAssignmentModal = ({ close, isOpen, uuidCount, + trackEvent, }) => { const { successfulCancellationToast: { displayToastForAssignmentCancellation }, @@ -19,6 +20,7 @@ const CancelAssignmentModal = ({ const handleOnClick = async () => { await cancelContentAssignments(); + trackEvent(); displayToastForAssignmentCancellation(uuidCount); }; @@ -69,6 +71,7 @@ CancelAssignmentModal.propTypes = { close: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired, uuidCount: PropTypes.number, + trackEvent: PropTypes.func.isRequired, }; export default CancelAssignmentModal; diff --git a/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx index 5b2144b149..74ebab3da9 100644 --- a/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx +++ b/src/components/learner-credit-management/PendingAssignmentCancelButton.jsx @@ -4,25 +4,96 @@ import { Icon, IconButtonWithTooltip, } from '@edx/paragon'; import { DoNotDisturbOn } from '@edx/paragon/icons'; +import { connect } from 'react-redux'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import useCancelContentAssignments from './data/hooks/useCancelContentAssignments'; import CancelAssignmentModal from './CancelAssignmentModal'; +import EVENT_NAMES from '../../eventTracking'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; + +const PendingAssignmentCancelButton = ({ row, enterpriseId }) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; -const PendingAssignmentCancelButton = ({ row }) => { const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; + const { + contentKey, + contentQuantity, + learnerState, + state, + uuid, + } = row.original; + const { cancelButtonState, cancelContentAssignments, close, isOpen, open, - } = useCancelContentAssignments(row.original.assignmentConfiguration, [row.original.uuid]); + } = useCancelContentAssignments(assignmentConfiguration, [uuid]); + + const sharedTrackEventMetadata = { + subsidyUuid, + isSubsidyActive, + isAssignable, + catalogUuid, + assignmentConfiguration, + contentKey, + contentQuantity, + learnerState, + aggregates, + assignmentState: state, + isOpen: !isOpen, + }; + + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_CANCEL_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_CANCEL_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CANCEL, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const trackEvent = (eventName, eventMetadata = {}) => { + const trackEventMetadata = { + ...sharedTrackEventMetadata, + ...eventMetadata, + }; + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + trackEventMetadata, + ); + }; + + const openModal = () => { + open(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_CANCEL_MODAL, + ); + }; + + const closeModal = () => { + close(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_CANCEL_MODAL, + ); + }; + + const cancellationTrackEvent = () => { + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CANCEL, + ); + }; + return ( <> { /> ); @@ -41,11 +113,20 @@ const PendingAssignmentCancelButton = ({ row }) => { PendingAssignmentCancelButton.propTypes = { row: PropTypes.shape({ original: PropTypes.shape({ + contentKey: PropTypes.string.isRequired, + contentQuantity: PropTypes.number.isRequired, + learnerState: PropTypes.string.isRequired, + state: PropTypes.string.isRequired, assignmentConfiguration: PropTypes.string.isRequired, learnerEmail: PropTypes.string, uuid: PropTypes.string.isRequired, }).isRequired, }).isRequired, + enterpriseId: PropTypes.string.isRequired, }; -export default PendingAssignmentCancelButton; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(PendingAssignmentCancelButton); diff --git a/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx b/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx index 07f38883cf..a88843a2fd 100644 --- a/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx +++ b/src/components/learner-credit-management/PendingAssignmentRemindButton.jsx @@ -3,18 +3,88 @@ import PropTypes from 'prop-types'; import { Icon, IconButtonWithTooltip } from '@edx/paragon'; import { Mail } from '@edx/paragon/icons'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { connect } from 'react-redux'; import RemindAssignmentModal from './RemindAssignmentModal'; import useRemindContentAssignments from './data/hooks/useRemindContentAssignments'; +import EVENT_NAMES from '../../eventTracking'; +import { useBudgetId, useSubsidyAccessPolicy } from './data'; + +const PendingAssignmentRemindButton = ({ row, enterpriseId }) => { + const { subsidyAccessPolicyId } = useBudgetId(); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + const { + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, + } = subsidyAccessPolicy; -const PendingAssignmentRemindButton = ({ row }) => { const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : ''; + const { + contentKey, + contentQuantity, + learnerState, + state, + uuid, + } = row.original; + const { remindButtonState, remindContentAssignments, close, isOpen, open, - } = useRemindContentAssignments(row.original.assignmentConfiguration, [row.original.uuid]); + } = useRemindContentAssignments(assignmentConfiguration, [uuid]); + + const sharedTrackEventMetadata = { + subsidyUuid, + isSubsidyActive, + isAssignable, + catalogUuid, + assignmentConfiguration, + contentKey, + contentQuantity, + learnerState, + aggregates, + assignmentState: state, + isOpen: !isOpen, + }; + + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_REMIND_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_REMIND_MODAL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_REMIND, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const trackEvent = (eventName, eventMetadata = {}) => { + const trackEventMetadata = { + ...sharedTrackEventMetadata, + ...eventMetadata, + }; + sendEnterpriseTrackEvent( + enterpriseId, + eventName, + trackEventMetadata, + ); + }; + + const openModal = () => { + open(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_REMIND_MODAL, + ); + }; + + const closeModal = () => { + close(); + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_REMIND_MODAL, + ); + }; + + const reminderTrackEvent = () => { + trackEvent( + BUDGET_DETAILS_ASSIGNED_DATATABLE_REMIND, + ); + }; return ( <> @@ -22,16 +92,17 @@ const PendingAssignmentRemindButton = ({ row }) => { alt={`Remind learner ${emailAltText}`} data-testid={`remind-assignment-${row.original.uuid}`} iconAs={Icon} - onClick={open} + onClick={openModal} src={Mail} tooltipContent="Remind learner" tooltipPlacement="top" /> ); @@ -40,11 +111,20 @@ const PendingAssignmentRemindButton = ({ row }) => { PendingAssignmentRemindButton.propTypes = { row: PropTypes.shape({ original: PropTypes.shape({ + contentKey: PropTypes.string.isRequired, + contentQuantity: PropTypes.number.isRequired, + learnerState: PropTypes.string.isRequired, + state: PropTypes.string.isRequired, assignmentConfiguration: PropTypes.string.isRequired, learnerEmail: PropTypes.string, uuid: PropTypes.string.isRequired, }).isRequired, }).isRequired, + enterpriseId: PropTypes.string.isRequired, }; -export default PendingAssignmentRemindButton; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +export default connect(mapStateToProps)(PendingAssignmentRemindButton); diff --git a/src/components/learner-credit-management/RemindAssignmentModal.jsx b/src/components/learner-credit-management/RemindAssignmentModal.jsx index 013535e2b9..546bee7093 100644 --- a/src/components/learner-credit-management/RemindAssignmentModal.jsx +++ b/src/components/learner-credit-management/RemindAssignmentModal.jsx @@ -7,7 +7,7 @@ import { Mail } from '@edx/paragon/icons'; import { BudgetDetailPageContext } from './BudgetDetailPageWrapper'; const RemindAssignmentModal = ({ - remindButtonState, remindContentAssignments, close, isOpen, uuidCount, + remindButtonState, remindContentAssignments, close, isOpen, uuidCount, trackEvent, }) => { const { successfulReminderToast: { displayToastForAssignmentReminder }, @@ -15,6 +15,7 @@ const RemindAssignmentModal = ({ const handleOnClick = async () => { await remindContentAssignments(); + trackEvent(); displayToastForAssignmentReminder(uuidCount); }; @@ -66,6 +67,7 @@ RemindAssignmentModal.propTypes = { close: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired, uuidCount: PropTypes.number, + trackEvent: PropTypes.func.isRequired, }; export default RemindAssignmentModal; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx index b6664a8768..df5fb496c3 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedBadEmail.jsx @@ -1,30 +1,46 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedBadEmail = ({ learnerEmail }) => { - const [isOpen, open, close] = useToggle(false); +const FailedBadEmail = ({ learnerEmail, trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL_HELP_CENTER, + trackEvent, + }); return ( <> Failed: Bad email Failed: Bad email @@ -41,7 +57,11 @@ const FailedBadEmail = ({ learnerEmail }) => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • @@ -55,6 +75,7 @@ const FailedBadEmail = ({ learnerEmail }) => { FailedBadEmail.propTypes = { learnerEmail: PropTypes.string, + trackEvent: PropTypes.func.isRequired, }; FailedBadEmail.defaultProps = { diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx index f102592f4f..18bc683c56 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedCancellation.jsx @@ -1,29 +1,46 @@ import React, { useState } from 'react'; -import { Chip, useToggle, Hyperlink } from '@edx/paragon'; +import PropTypes from 'prop-types'; +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedCancellation = () => { - const [isOpen, open, close] = useToggle(false); +const FailedCancellation = ({ trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION_HELP_CENTER, + trackEvent, + }); return ( <> Failed: Cancellation Failed: Cancellation @@ -41,7 +58,11 @@ const FailedCancellation = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments
  • @@ -53,4 +74,8 @@ const FailedCancellation = () => { ); }; +FailedCancellation.propTypes = { + trackEvent: PropTypes.func.isRequired, +}; + export default FailedCancellation; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx index 6e12f9303f..ef263065c6 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedRedemption.jsx @@ -1,29 +1,48 @@ import React, { useState } from 'react'; -import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import PropTypes from 'prop-types'; + +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedRedemption = () => { - const [isOpen, open, close] = useToggle(false); +const FailedRedemption = ({ trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION_HELP_CENTER, + trackEvent, + }); + return ( <> Failed: Redemption Failed: Redemption @@ -44,7 +63,11 @@ const FailedRedemption = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • @@ -56,4 +79,8 @@ const FailedRedemption = () => { ); }; +FailedRedemption.propTypes = { + trackEvent: PropTypes.func.isRequired, +}; + export default FailedRedemption; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx index 033ae7bca4..03519222c9 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedReminder.jsx @@ -1,27 +1,45 @@ import React, { useState } from 'react'; -import { Chip, useToggle, Hyperlink } from '@edx/paragon'; +import PropTypes from 'prop-types'; +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedReminder = () => { - const [isOpen, open, close] = useToggle(false); +const FailedReminder = ({ trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER_HELP_CENTER, + trackEvent, + }); return ( <> Failed: Reminder Failed: Reminder @@ -39,7 +57,11 @@ const FailedReminder = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • @@ -51,4 +73,8 @@ const FailedReminder = () => { ); }; +FailedReminder.propTypes = { + trackEvent: PropTypes.func.isRequired, +}; + export default FailedReminder; diff --git a/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx b/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx index 43ae45e4b0..c5a1acb026 100644 --- a/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/FailedSystem.jsx @@ -1,29 +1,47 @@ import React, { useState } from 'react'; -import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import PropTypes from 'prop-types'; + +import { Chip, Hyperlink } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const FailedSystem = () => { - const [isOpen, open, close] = useToggle(false); +const FailedSystem = ({ trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM_HELP_CENTER, + trackEvent, + }); return ( <> Failed: System Failed: System @@ -38,7 +56,11 @@ const FailedSystem = () => {
  • Get more troubleshooting help at{' '} - + Help Center: Course Assignments .
  • @@ -50,4 +72,8 @@ const FailedSystem = () => { ); }; +FailedSystem.propTypes = { + trackEvent: PropTypes.func.isRequired, +}; + export default FailedSystem; diff --git a/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx b/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx index 2380fda362..94ac11d39f 100644 --- a/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/NotifyingLearner.jsx @@ -1,28 +1,39 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Chip, useToggle } from '@edx/paragon'; +import { Chip } from '@edx/paragon'; import { Send } from '@edx/paragon/icons'; import BaseModalPopup from './BaseModalPopup'; +import EVENT_NAMES from '../../../eventTracking'; +import { useAssignmentStatusChip } from '../data'; -const NotifyingLearner = ({ learnerEmail }) => { - const [isOpen, open, close] = useToggle(false); +const NotifyingLearner = ({ learnerEmail, trackEvent }) => { const [target, setTarget] = useState(null); + const { BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_NOTIFY_LEARNER } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_NOTIFY_LEARNER, + trackEvent, + }); return ( <> Notifying learner Notifying {learnerEmail ?? 'learner'} @@ -40,6 +51,7 @@ const NotifyingLearner = ({ learnerEmail }) => { NotifyingLearner.propTypes = { learnerEmail: PropTypes.string, + trackEvent: PropTypes.func.isRequired, }; export default NotifyingLearner; diff --git a/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx index f1176b3317..819f918c53 100644 --- a/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx +++ b/src/components/learner-credit-management/assignments-status-chips/WaitingForLearner.jsx @@ -1,31 +1,46 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Chip, Hyperlink, useToggle } from '@edx/paragon'; +import { Chip, Hyperlink } from '@edx/paragon'; import { Timelapse } from '@edx/paragon/icons'; import { getConfig } from '@edx/frontend-platform/config'; import BaseModalPopup from './BaseModalPopup'; -import { ASSIGNMENT_ENROLLMENT_DEADLINE } from '../data'; +import { ASSIGNMENT_ENROLLMENT_DEADLINE, useAssignmentStatusChip } from '../data'; +import EVENT_NAMES from '../../../eventTracking'; -const WaitingForLearner = ({ learnerEmail }) => { - const [isOpen, open, close] = useToggle(false); +const WaitingForLearner = ({ learnerEmail, trackEvent }) => { const [target, setTarget] = useState(null); + const { + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER_HELP_CENTER, + } = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT; + + const { + openChipModal, + closeChipModal, + isChipModalOpen, + helpCenterTrackEvent, + } = useAssignmentStatusChip({ + chipInteractionEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER, + chipHelpCenterEventName: BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER_HELP_CENTER, + trackEvent, + }); return ( <> Waiting for learner Waiting for {learnerEmail ?? 'learner'} @@ -40,7 +55,11 @@ const WaitingForLearner = ({ learnerEmail }) => {

    Need help?

    Learn more about learner enrollment in assigned courses at{' '} - + Help Center: Course Assignments .

    @@ -53,6 +72,7 @@ const WaitingForLearner = ({ learnerEmail }) => { WaitingForLearner.propTypes = { learnerEmail: PropTypes.string, + trackEvent: PropTypes.func.isRequired, }; export default WaitingForLearner; diff --git a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx index 790fc68d3f..4024990c9f 100644 --- a/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/cards/NewAssignmentModalButton.jsx @@ -47,7 +47,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { } = useContext(BudgetDetailPageContext); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); const { - subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, + subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates, } = subsidyAccessPolicy; const sharedEnterpriseTrackEventMetadata = { subsidyAccessPolicyId, @@ -55,10 +55,11 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { subsidyUuid, isSubsidyActive, isAssignable, + aggregates, contentPriceCents: course.normalizedMetadata.contentPrice * 100, contentKey: course.key, courseUuid: course.uuid, - assignmentConfigurationUuid: assignmentConfiguration.uuid, + assignmentConfiguration, }; const { mutate } = useAllocateContentAssignments(); @@ -94,11 +95,13 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { const onSuccessEnterpriseTrackEvents = ({ totalLearnersAllocated, totalLearnersAlreadyAllocated, + response, }) => { const trackEventMetadata = { ...sharedEnterpriseTrackEventMetadata, totalLearnersAllocated, totalLearnersAlreadyAllocated, + response, }; sendEnterpriseTrackEvent( enterpriseId, @@ -120,7 +123,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { setAssignButtonState('pending'); setCreateAssignmentsErrorReason(null); mutate(mutationArgs, { - onSuccess: ({ created, noChange, updated }) => { + onSuccess: (res) => { setAssignButtonState('complete'); // Ensure the budget and budgets queries are invalidated so that the relevant // queries become stale and refetches new updated data from the API. @@ -131,11 +134,12 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { queryKey: learnerCreditManagementQueryKeys.budgets(enterpriseId), }); handleCloseAssignmentModal(); - const totalLearnersAllocated = created.length + updated.length; - const totalLearnersAlreadyAllocated = noChange.length; + const totalLearnersAllocated = res.created.length + res.updated.length; + const totalLearnersAlreadyAllocated = res.noChange.length; onSuccessEnterpriseTrackEvents({ totalLearnersAllocated, totalLearnersAlreadyAllocated, + res, }); displayToastForAssignmentAllocation({ totalLearnersAllocated, @@ -167,6 +171,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { totalAllocatedLearners: learnerEmails.length, errorStatus: httpErrorStatus, errorReason, + response: err, }, ); }, @@ -185,7 +190,10 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { sendEnterpriseTrackEvent( enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_EXIT, - { assignButtonState }, + { + ...sharedEnterpriseTrackEventMetadata, + assignButtonState, + }, ); }} footerNode={( @@ -196,6 +204,10 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { onClick={() => sendEnterpriseTrackEvent( enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_HELP_CENTER, + { + ...sharedEnterpriseTrackEventMetadata, + assignButtonState, + }, )} destination={getConfig().ENTERPRISE_SUPPORT_LEARNER_CREDIT_URL} showLaunchIcon @@ -211,7 +223,10 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => { sendEnterpriseTrackEvent( enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.ASSIGNMENT_MODAL_CANCEL, - { assignButtonState }, + { + ...sharedEnterpriseTrackEventMetadata, + assignButtonState, + }, ); }} > diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 517f14fbd5..f5fed6a990 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -9,7 +9,6 @@ import { QueryClientProvider, useQueryClient } from '@tanstack/react-query'; import { AppContext } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; - import CourseCard from '../CourseCard'; import { formatPrice, diff --git a/src/components/learner-credit-management/data/hooks/index.js b/src/components/learner-credit-management/data/hooks/index.js index 63681a4030..396153dad6 100644 --- a/src/components/learner-credit-management/data/hooks/index.js +++ b/src/components/learner-credit-management/data/hooks/index.js @@ -12,3 +12,4 @@ export { default as useSuccessfulAssignmentToastContextValue } from './useSucces export { default as useSuccessfulCancellationToastContextValue } from './useSuccessfulCancellationToastContextValue'; export { default as useSuccessfulReminderToastContextValue } from './useSuccessfulReminderToastContextValue'; export { default as useEnterpriseOffer } from './useEnterpriseOffer'; +export { default as useAssignmentStatusChip } from './useAssignmentStatusChip'; diff --git a/src/components/learner-credit-management/data/hooks/useAssignmentStatusChip.jsx b/src/components/learner-credit-management/data/hooks/useAssignmentStatusChip.jsx new file mode 100644 index 0000000000..f74dc90a2a --- /dev/null +++ b/src/components/learner-credit-management/data/hooks/useAssignmentStatusChip.jsx @@ -0,0 +1,39 @@ +import { useToggle } from '@edx/paragon'; + +/** + * + * @param chipInteractionEventName {String} - The event name that will be read in Segment of a chip opening and closing + * @param chipHelpCenterEventName {String} - The event name that will be read in Segment for a help center link + * interaction + * @param trackEvent {Function} - The track event functioning that will be sending a track event + * @returns {{ + * closeChipModal: closeChipModal, + * openChipModal: openChipModal, + * helpCenterTrackEvent: helpCenterTrackEvent, + * isChipModalOpen: * + * }} + */ +export default function useAssignmentStatusChip({ chipInteractionEventName, chipHelpCenterEventName, trackEvent }) { + const [isChipModalOpen, open, close] = useToggle(false); + const openChipModal = () => { + open(); + // Note: could hardcode true given this function *always* opens + trackEvent(chipInteractionEventName, { isOpen: true }); + }; + const closeChipModal = () => { + close(); + // Note: could hardcode false given this function *always* closes + trackEvent(chipInteractionEventName, { isOpen: false }); + }; + + const helpCenterTrackEvent = () => { + trackEvent(chipHelpCenterEventName); + }; + + return { + isChipModalOpen, + openChipModal, + closeChipModal, + helpCenterTrackEvent, + }; +} diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 51746f3eca..2216a01580 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -337,3 +337,60 @@ export async function retrieveBudgetDetailActivityOverview({ } return result; } + +/** + * Takes the raw selected flat rows data from the 'Assigned' datatable and returns metadata that is used for tracking + * bulk enrollment of reminders and bulk enrollment of cancellations. + * @param {Array} selectedFlatRows An array of selectedFlatRows from the activity 'Assigned' table + * @returns {{ + * uniqueLearnerState: [String], + * totalContentQuantity: Number, + * assignmentConfigurationUuid: String, + * assignmentUuids: [String] + * uniqueContentKeys: [String], + * uniqueAssignmentState: [String], + * totalSelectedRows: Number, + * }} + */ +export const transformSelectedRows = (selectedFlatRows) => { + const assignmentUuids = selectedFlatRows.map(item => item.id); + const totalSelectedRows = selectedFlatRows.length; + + // Count of unique content keys, where the key is the course, + // and value is count of the course. + const flatMappedContentKeys = selectedFlatRows.map(item => item?.original?.contentKey); + const uniqueContentKeys = {}; + flatMappedContentKeys.forEach((courseKey) => { + uniqueContentKeys[courseKey] = (uniqueContentKeys[courseKey] || 0) + 1; + }); + + // Count of unique learner states, where the key is the learnerState, + // and value is count of the learnerState. + const flatMappedLearnerState = selectedFlatRows.map(item => item?.original?.learnerState); + const uniqueLearnerState = {}; + flatMappedLearnerState.forEach((learnerState) => { + uniqueLearnerState[learnerState] = (uniqueLearnerState[learnerState] || 0) + 1; + }); + + // Count of unique assignment states, where the key is the assignment state, + // and value is count of the assignment state. + const flatMappedAssignmentState = selectedFlatRows.map(item => item?.original?.state); + const uniqueAssignmentState = {}; + flatMappedAssignmentState.forEach((state) => { + uniqueAssignmentState[state] = (uniqueAssignmentState[state] || 0) + 1; + }); + + // Total value of all the selected rows accumulated from the contentQuantity + const totalContentQuantity = selectedFlatRows.map( + item => item.original.contentQuantity, + ).reduce((prev, next) => prev + next, 0); + + return { + uniqueAssignmentState, + uniqueLearnerState, + uniqueContentKeys, + totalContentQuantity, + assignmentUuids, + totalSelectedRows, + }; +}; diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 9052c8d93c..b3ade09d7b 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -1175,10 +1175,25 @@ describe('', () => { expect(statusChip).toBeInTheDocument(); userEvent.click(statusChip); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + // Modal popup is visible with expected text const modalPopupContents = within(screen.getByTestId('assignment-status-modalpopup-contents')); expect(modalPopupContents.getByText(expectedModalPopupHeading)).toBeInTheDocument(); expect(modalPopupContents.getByText(expectedModalPopupContent, { exact: false })).toBeInTheDocument(); + + // Help Center link clicked and modal closed + if (screen.queryByText('Help Center: Course Assignments')) { + const helpCenterLink = screen.getByText('Help Center: Course Assignments'); + userEvent.click(helpCenterLink); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); + // Click chip to close modal + userEvent.click(statusChip); + expect(sendEnterpriseTrackEvent).toHaveBeenCalled(); + } else { + userEvent.click(statusChip); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); + } }); it.each([ @@ -1332,6 +1347,8 @@ describe('', () => { userEvent.click(catalogTab); }); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + await waitFor(() => { expect(screen.getByTestId('budget-detail-catalog-tab-contents')).toBeInTheDocument(); }); @@ -1516,6 +1533,7 @@ describe('', () => { const cancelBulkActionButton = screen.getByText('Cancel (2)'); expect(cancelBulkActionButton).toBeInTheDocument(); userEvent.click(cancelBulkActionButton); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const modalDialog = screen.getByRole('dialog'); expect(modalDialog).toBeInTheDocument(); const cancelDialogButton = getButtonElement('Cancel assignments (2)'); @@ -1528,6 +1546,7 @@ describe('', () => { await waitFor( () => expect(screen.getByText('Assignments canceled (2)')).toBeInTheDocument(), ); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); it('reminds assignments in bulk', async () => { @@ -1608,6 +1627,7 @@ describe('', () => { expect(modalDialog).toBeInTheDocument(); const remindDialogButton = getButtonElement('Send reminders (2)'); userEvent.click(remindDialogButton); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); await waitFor( () => expect( EnterpriseAccessApiService.remindAllContentAssignments, @@ -1616,6 +1636,7 @@ describe('', () => { await waitFor( () => expect(screen.getByText('Reminders sent (2)')).toBeInTheDocument(), ); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); it('cancels a single assignment', async () => { @@ -1666,6 +1687,7 @@ describe('', () => { const cancelIconButton = screen.getByTestId('cancel-assignment-test-uuid'); expect(cancelIconButton).toBeInTheDocument(); userEvent.click(cancelIconButton); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const modalDialog = screen.getByRole('dialog'); expect(modalDialog).toBeInTheDocument(); const cancelDialogButton = getButtonElement('Cancel assignment'); @@ -1673,6 +1695,7 @@ describe('', () => { await waitFor( () => expect(screen.getByText('Assignment canceled')).toBeInTheDocument(), ); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); it('reminds a single assignment', async () => { EnterpriseAccessApiService.remindContentAssignments.mockResolvedValueOnce({ status: 200 }); @@ -1722,6 +1745,7 @@ describe('', () => { const remindIconButton = screen.getByTestId('remind-assignment-test-uuid'); expect(remindIconButton).toBeInTheDocument(); userEvent.click(remindIconButton); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); const modalDialog = screen.getByRole('dialog'); expect(modalDialog).toBeInTheDocument(); const remindDialogButton = getButtonElement('Send reminder'); @@ -1729,5 +1753,6 @@ describe('', () => { await waitFor( () => expect(screen.getByText('Reminder sent')).toBeInTheDocument(), ); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); }); diff --git a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx index 96c7ed5fc2..7ce956ab94 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx @@ -7,9 +7,12 @@ import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import '@testing-library/jest-dom/extend-expect'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import BudgetDetailPageWrapper, { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; -import { getButtonElement } from '../../test/testUtils'; +import { getButtonElement, queryClient } from '../../test/testUtils'; +import BudgetDetailPageBreadcrumbs from '../BudgetDetailPageBreadcrumbs'; const mockStore = configureMockStore([thunk]); const getMockStore = store => mockStore(store); @@ -26,19 +29,27 @@ const defaultStoreState = { }, }; +jest.mock('@edx/frontend-enterprise-utils', () => ({ + ...jest.requireActual('@edx/frontend-enterprise-utils'), + sendEnterpriseTrackEvent: jest.fn(), +})); + const MockBudgetDetailPageWrapper = ({ initialStoreState = defaultStoreState, children, }) => { const store = getMockStore(initialStoreState); return ( - - - - {children} - - - + + + + + {children} + + + + + ); }; @@ -263,4 +274,16 @@ describe('', () => { expect(screen.queryByText(expectedToastMessage)).not.toBeInTheDocument(); }); }); + it('calls segment event on breadcrumb click', () => { + const mockBudgetDisplayName = 'Test Budget'; + renderWithRouter( + + + , + ); + const previousBreadcrumb = screen.getByText('Budgets'); + + userEvent.click(previousBreadcrumb); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/eventTracking.js b/src/eventTracking.js index ed4244786f..d77196db3e 100644 --- a/src/eventTracking.js +++ b/src/eventTracking.js @@ -103,14 +103,54 @@ export const SUBSCRIPTION_EVENTS = { }; export const LEARNER_CREDIT_MANAGEMENT_EVENTS = { + BREADCRUMB_FROM_BUDGET_DETAIL_TO_BUDGETS: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.breadcrumb_budget_detail_to_budgets.clicked`, TAB_CHANGED: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.tab.changed`, + // Budget Overview + BUDGET_OVERVIEW_CONTACT_US: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.contact_us.clicked`, + BUDGET_OVERVIEW_NEW_ASSIGNMENT: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.new_assignment.clicked`, + BUDGET_OVERVIEW_UTILIZATION_DROPDOWN_TOGGLE: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.utilization_dropdown.toggled`, + BUDGET_OVERVIEW_UTILIZATION_VIEW_SPENT_TABLE: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.view_spent_activity.clicked`, + BUDGET_OVERVIEW_UTILIZATION_VIEW_ASSIGNED_TABLE: `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_detail.view_assigned_activity.clicked`, // Activity tab + // Activity tab assigned datatable BUDGET_DETAILS_ASSIGNED_DATATABLE_SORT_BY_OR_FILTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table.changed`, BUDGET_DETAILS_ASSIGNED_DATATABLE_VIEW_COURSE: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_view_course.clicked`, BUDGET_DETAILS_ASSIGNED_DATATABLE_ACTIONS_REFRESH: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_refresh.clicked`, + // Activity tab assigned table remind + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_REMIND_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_remind_modal_open.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_REMIND_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_remind_modal_close.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_REMIND: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_remind.clicked`, + // Activity tab assigned table bulk remind + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_REMIND_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_remind_modal_open.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_REMIND_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_remind_modal_close.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_REMIND: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_remind.clicked`, + // Activity tab assigned table cancel + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_CANCEL_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_cancel_modal_open.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_CANCEL_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_cancel_modal_close.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CANCEL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_cancel.clicked`, + // Activity tab assigned table bulk cancel + BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_CANCEL_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_cancel_modal_open.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_CANCEL_MODAL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_cancel_modal_close.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_CANCEL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_bulk_cancel.clicked`, + // Activity tab spent datatable BUDGET_DETAILS_SPENT_DATATABLE_SORT_BY_OR_FILTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.spent_table.changed`, BUDGET_DETAILS_SPENT_DATATABLE_VIEW_COURSE: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.spent_table_view_course.clicked`, EMPTY_STATE_CTA: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.empty_state_cta_to_catalog.clicked`, + // Activity tab chips + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_NOTIFY_LEARNER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_notify_learner.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_waiting_for_learner.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_cancellation.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_system.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_bad_email.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_reminder.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_redemption.clicked`, + // Activity tab chips help center links + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_WAITING_FOR_LEARNER_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_waiting_for_learner_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_CANCELLATION_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_cancellation_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_SYSTEM_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_system_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_EMAIL_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_bad_email_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REMINDER_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_reminder_help_center.clicked`, + BUDGET_DETAILS_ASSIGNED_DATATABLE_CHIP_FAILED_REDEMPTION_HELP_CENTER: `${BUDGET_DETAIL_ACTIVITY_TAB_PREFIX}.assigned_table_chip_failed_redemption_help_center.clicked`, // Catalog tab // Catalog tab search VIEW_COURSE: `${BUDGET_DETAIL_SEARCH_PREFIX}.view_course.clicked`,