From 8bd88539654dec1ff32ba55fc27a25b09d48148c Mon Sep 17 00:00:00 2001 From: Cyril Nxumalo Date: Tue, 27 Feb 2024 07:19:26 +0200 Subject: [PATCH] feat: display warning when plan is expiring --- src/components/Admin/Admin.test.jsx | 11 + .../Admin/__snapshots__/Admin.test.jsx.snap | 10993 ---------------- src/components/Admin/index.jsx | 21 +- .../data/constants.js | 3 + .../data/expiryThresholds.js | 72 + .../data/hooks/useExpiry.jsx | 66 + .../BudgetExpiryAlertAndModal/data/utils.js | 41 + .../BudgetExpiryAlertAndModal/index.jsx | 137 + .../EnterpriseApp/data/constants.js | 1 + .../EnterpriseSubsidiesContext/data/hooks.js | 2 + .../BudgetDetailPageBreadcrumbs.jsx | 6 +- .../BudgetDetailPageHeader.jsx | 104 +- .../BudgetDetailPageOverviewAvailability.jsx | 75 +- .../BudgetOverviewContent.jsx | 117 + .../SubBudgetCard.jsx | 13 +- .../data/tests/constants.js | 27 +- .../learner-credit-management/data/utils.js | 19 +- .../tests/BudgetDetailPageWrapper.test.jsx | 2 +- src/eventTracking.js | 6 + 19 files changed, 582 insertions(+), 11134 deletions(-) delete mode 100644 src/components/Admin/__snapshots__/Admin.test.jsx.snap create mode 100644 src/components/BudgetExpiryAlertAndModal/data/constants.js create mode 100644 src/components/BudgetExpiryAlertAndModal/data/expiryThresholds.js create mode 100644 src/components/BudgetExpiryAlertAndModal/data/hooks/useExpiry.jsx create mode 100644 src/components/BudgetExpiryAlertAndModal/data/utils.js create mode 100644 src/components/BudgetExpiryAlertAndModal/index.jsx create mode 100644 src/components/learner-credit-management/BudgetOverviewContent.jsx diff --git a/src/components/Admin/Admin.test.jsx b/src/components/Admin/Admin.test.jsx index 91dda8471f..7dc0672d58 100644 --- a/src/components/Admin/Admin.test.jsx +++ b/src/components/Admin/Admin.test.jsx @@ -20,10 +20,20 @@ jest.mock('@edx/frontend-enterprise-utils', () => { }); }); +jest.mock('../EnterpriseSubsidiesContext/data/hooks', () => ({ + ...jest.requireActual('../EnterpriseSubsidiesContext/data/hooks'), + useEnterpriseBudgets: jest.fn().mockReturnValue({ + data: [], + }), +})); + const mockStore = configureMockStore([thunk]); const store = mockStore({ portalConfiguration: { enterpriseId: 'test-enterprise-id', + enterpriseFeatures: { + topDownAssignmentRealTimeLcm: true, + }, }, table: {}, csv: {}, @@ -472,6 +482,7 @@ describe('', () => { }); }); }); + describe('reset form button', () => { it('should not be present if there is no query', () => { const wrapper = mount(( diff --git a/src/components/Admin/__snapshots__/Admin.test.jsx.snap b/src/components/Admin/__snapshots__/Admin.test.jsx.snap deleted file mode 100644 index 3ea85ed607..0000000000 --- a/src/components/Admin/__snapshots__/Admin.test.jsx.snap +++ /dev/null @@ -1,10993 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders correctly calls fetchDashboardAnalytics prop 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
- Loading... -
- - - ‌ - -
-
- - - ‌ - -
-
- - - ‌ - -
-
- - - ‌ - -
-
-
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders # courses enrolled by learners 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders # of courses completed by learner 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders # of courses completed by learner in past week 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders collapsible cards 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders full report 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders inactive learners past month 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders inactive learners past week 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders learners not enrolled in an active course 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders registered but not enrolled report 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard analytics data renders top active learners 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with dashboard insights data renders dashboard insights data correctly 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
- - -
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with error state 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
- - - - - -
-
-
- Hey, nice to see you -
-

- Try refreshing your screen - Network Error -

-
-
-
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with loading state 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
- Loading... -
- - - ‌ - -
-
- - - ‌ - -
-
- - - ‌ - -
-
- - - ‌ - -
-
-
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; - -exports[` renders correctly with no dashboard analytics data 1`] = ` -
-
- Loading... -
- - - ‌ - -
-
- - - ‌ - -
-
-
-`; - -exports[` renders correctly with no dashboard insights data 1`] = ` -
-
-
-

- Learner Progress Report -

-
-
- edX logo -
-
-
-
-
-

- Overview -

-
-
-
-
-
-
-
-
-
-
-

- - 3 - - - - - - -

-

- total number of learners registered -

-
-
-
-
- -
- -
-
-
-
-
-
-
-

- - 1 - - - - - - -

-

- learners enrolled in at least one course -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- active learners in the past week -

-
-
- -
-
-
-
-
-
-

- - 1 - - - - - - -

-

- course completions -

-
-
- -
-
-
-
-
-
- Loading... - - Loading - -
-
-
-
-
-
-
-

- Full Report -

-
-
-
-
-
-
-
-
-
-
-
-
- Showing data as of - July 31, 2018 -
-
- -
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-`; diff --git a/src/components/Admin/index.jsx b/src/components/Admin/index.jsx index d6e958a2ca..ae86d8ab16 100644 --- a/src/components/Admin/index.jsx +++ b/src/components/Admin/index.jsx @@ -27,6 +27,7 @@ import EmbeddedSubscription from './EmbeddedSubscription'; import { withLocation, withParams } from '../../hoc'; import AIAnalyticsSummary from './AIAnalyticsSummary'; import AIAnalyticsSummarySkeleton from './AIAnalyticsSummarySkeleton'; +import BudgetExpiryAlertAndModal from '../BudgetExpiryAlertAndModal/index'; class Admin extends React.Component { componentDidMount() { @@ -308,6 +309,9 @@ class Admin extends React.Component { <> +
+ +
@@ -367,22 +371,21 @@ class Admin extends React.Component {
{lastUpdatedDate && ( - <> - Showing data as of {formatTimestamp({ timestamp: lastUpdatedDate })} - + <> + Showing data as of {formatTimestamp({ timestamp: lastUpdatedDate })} + )} -
{this.renderDownloadButton()}
{this.displaySearchBar() && ( - this.props.searchEnrollmentsList()} - tableData={this.getTableData() ? this.getTableData().results : []} - /> + this.props.searchEnrollmentsList()} + tableData={this.getTableData() ? this.getTableData().results : []} + /> )} )} diff --git a/src/components/BudgetExpiryAlertAndModal/data/constants.js b/src/components/BudgetExpiryAlertAndModal/data/constants.js new file mode 100644 index 0000000000..af7a72845f --- /dev/null +++ b/src/components/BudgetExpiryAlertAndModal/data/constants.js @@ -0,0 +1,3 @@ +export const PLAN_EXPIRY_MODAL_TITLE = 'Plan expiry model'; + +export const SEEN_ENTERPRISE_EXPIRATION_MODAL_COOKIE_PREFIX = 'seen-enterprise-expiration-modal-'; diff --git a/src/components/BudgetExpiryAlertAndModal/data/expiryThresholds.js b/src/components/BudgetExpiryAlertAndModal/data/expiryThresholds.js new file mode 100644 index 0000000000..81aff9e42a --- /dev/null +++ b/src/components/BudgetExpiryAlertAndModal/data/expiryThresholds.js @@ -0,0 +1,72 @@ +import sanitizeHTML from 'sanitize-html'; +import parse from 'html-react-parser'; + +const expiryThresholds = { + 120: ({ date }) => ({ + notificationTemplate: { + title: 'Your Learner Credit plan expires soon', + variant: 'info', + message: `Your Learner Credit plan expires ${date}. Contact support today to renew your plan and keep people learning.`, + dismissible: true, + }, + modalTemplate: { + title: 'Your plan expires soon', + message: `Your Learner Credit plan expires ${date}. Contact support today to renew your plan and keep people learning.`, + }, + }), + 90: ({ date }) => ({ + notificationTemplate: { + title: 'Reminder: Your Learner Credit plan expires soon', + variant: 'info', + message: `Your Learner Credit plan expires ${date}. Contact support today to renew your plan and keep people learning.`, + dismissible: true, + }, + modalTemplate: { + title: 'Reminder: Your plan expires soon', + message: `Your Learner Credit plan expires ${date}. Contact support today to renew your plan and keep people learning.`, + }, + }), + 60: ({ date }) => ({ + notificationTemplate: { + title: `Your Learner Credit plan expires ${date}`, + variant: 'warning', + message: 'When your Learner Credit plan expires, you will no longer have access to administrative functions and the remaining balance of your budget(s) will be unusable. Contact support today to renew your plan.', + dismissible: true, + }, + modalTemplate: { + title: `Your Learner Credit plan expires ${date}`, + message: `Your Learner Credit plan expires ${date}. Contact support today to renew your plan and keep people learning.`, + }, + }), + 30: ({ date }) => ({ + notificationTemplate: { + title: 'Your Learner Credit plan expires in less than 30 days', + variant: 'danger', + message: 'When your plan expires you will lose access to administrative functions and the remaining balance of your plan’s budget(s) will be unusable. Contact support today to renew your plan.', + }, + modalTemplate: { + title: 'Your Learner Credit plan expires in less than 30 days', + message: `Your Learner Credit plan expires ${date}. Contact support today to renew your plan and keep people learning.`, + }, + }), + 10: ({ date, days, hours }) => ({ + notificationTemplate: { + title: `Reminder: Your Learner Credit plan expires ${date}`, + variant: 'danger', + message: parse(sanitizeHTML(`Your Learner Credit plan expires in ${days} days and ${hours} hours. Contact support today to renew your plan.`)), + }, + modalTemplate: { + title: `Reminder: Your Learner Credit plan expires ${date}`, + message: `Your Learner Credit plan expires ${date}. Contact support today to renew your plan and keep people learning.`, + }, + }), + 0: ({ date }) => ({ + notificationTemplate: {}, + modalTemplate: { + title: 'Your Learner Credit plan has expired', + message: `Your Learner Credit plan expired on ${date}. You are no longer able to assign courses to learners. Please contact your representative if you have any questions or concerns.`, + }, + }), +}; + +export default expiryThresholds; diff --git a/src/components/BudgetExpiryAlertAndModal/data/hooks/useExpiry.jsx b/src/components/BudgetExpiryAlertAndModal/data/hooks/useExpiry.jsx new file mode 100644 index 0000000000..c4ce9f214e --- /dev/null +++ b/src/components/BudgetExpiryAlertAndModal/data/hooks/useExpiry.jsx @@ -0,0 +1,66 @@ +import { useState, useEffect } from 'react'; +import { getEnterpriseBudgetExpiringCookieName, isPlanApproachingExpiry } from '../utils'; + +const useExpiry = (enterpriseId, budgets, modalOpen, modalClose) => { + const [isExpiring, setIsExpiring] = useState(false); + const [notification, setNotification] = useState(null); + const [expirationThreshold, setExpirationThreshold] = useState(null); + const [modal, setModal] = useState(null); + + useEffect(() => { + if (!budgets || budgets.length === 0) { + return; + } + + // Find the budget with the earliest expiry date + const earliestExpiryBudget = budgets.reduce( + (earliestBudget, currentBudget) => (currentBudget.end < earliestBudget.end ? currentBudget : earliestBudget), + budgets[0], + ); + + // Determine the notification based on the expiry date + const { isPlanExpiring, expiryThresholdKey, expiryThreshold } = isPlanApproachingExpiry(earliestExpiryBudget.end); + + setExpirationThreshold({ + isPlanExpiring, + expiryThresholdKey, + expiryThreshold, + }); + + const seenCurrentExpiringModalCookieName = getEnterpriseBudgetExpiringCookieName({ + expirationThreshold: expiryThresholdKey, + enterpriseId, + }); + + const isDismissed = global.localStorage.getItem(seenCurrentExpiringModalCookieName); + + if (isPlanExpiring) { + const { notificationTemplate, modalTemplate } = expiryThreshold; + + setIsExpiring(isPlanExpiring); + setNotification(notificationTemplate); + setModal(modalTemplate); + + if (!isDismissed) { + modalOpen(); + } + } + }, [budgets, enterpriseId, isExpiring, modalOpen]); + + const dismissModal = () => { + const seenCurrentExpirationModalCookieName = getEnterpriseBudgetExpiringCookieName({ + expirationThreshold: expirationThreshold.expiryThresholdKey, + enterpriseId, + }); + + global.localStorage.setItem(seenCurrentExpirationModalCookieName, 'true'); + + modalClose(); + }; + + return { + isExpiring, notification, modal, dismissModal, + }; +}; + +export default useExpiry; diff --git a/src/components/BudgetExpiryAlertAndModal/data/utils.js b/src/components/BudgetExpiryAlertAndModal/data/utils.js new file mode 100644 index 0000000000..c3f70f6252 --- /dev/null +++ b/src/components/BudgetExpiryAlertAndModal/data/utils.js @@ -0,0 +1,41 @@ +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { SEEN_ENTERPRISE_EXPIRATION_MODAL_COOKIE_PREFIX } from './constants'; +import ExpiryThresholds from './expiryThresholds'; + +dayjs.extend(duration); + +export const getEnterpriseBudgetExpiringCookieName = ({ + expirationThreshold, enterpriseId, +}) => `${SEEN_ENTERPRISE_EXPIRATION_MODAL_COOKIE_PREFIX}${expirationThreshold}-${enterpriseId}`; + +export const isPlanApproachingExpiry = (endDateStr) => { + const endDate = dayjs(endDateStr); + const today = dayjs(); + const durationDiff = dayjs.duration(endDate.diff(today)); + + // Find the appropriate threshold + const thresholdKeys = Object.keys(ExpiryThresholds).sort((a, b) => a - b); + const expiryThresholdKey = thresholdKeys.find((key) => durationDiff.asDays() <= key && durationDiff.asDays() >= 0); + + if (!expiryThresholdKey) { + return { + isPlanExpiring: false, + threshold: {}, + }; + } + + // Call the function in expiryThresholds with appropriate arguments + const expiryThreshold = ExpiryThresholds[expiryThresholdKey]({ + date: endDate.format('MMM D, YYYY'), + days: durationDiff.days(), + hours: durationDiff.hours(), + minutes: durationDiff.minutes(), + }); + + return { + isPlanExpiring: true, + expiryThresholdKey, + expiryThreshold, + }; +}; diff --git a/src/components/BudgetExpiryAlertAndModal/index.jsx b/src/components/BudgetExpiryAlertAndModal/index.jsx new file mode 100644 index 0000000000..ecc0a684c8 --- /dev/null +++ b/src/components/BudgetExpiryAlertAndModal/index.jsx @@ -0,0 +1,137 @@ +import React, { useMemo } from 'react'; +import { + ActionRow, + Alert, AlertModal, + Button, Hyperlink, + useToggle, +} from '@edx/paragon'; + +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { matchPath, useLocation } from 'react-router-dom'; +import { useEnterpriseBudgets } from '../EnterpriseSubsidiesContext/data/hooks'; +import { configuration } from '../../config'; +import EVENT_NAMES from '../../eventTracking'; + +import useExpiry from './data/hooks/useExpiry'; + +const BudgetExpiryAlertAndModal = ({ enterpriseUUID, enterpriseFeatures }) => { + const [modalIsOpen, modalOpen, modalClose] = useToggle(false); + const [alertIsOpen, , alertClose] = useToggle(true); + + const location = useLocation(); + + const budgetDetailRouteMatch = matchPath( + '/:enterpriseSlug/admin/learner-credit/:budgetId', + location.pathname, + ); + + const supportUrl = configuration.ENTERPRISE_SUPPORT_URL; + + const { data: budgets } = useEnterpriseBudgets({ + isTopDownAssignmentEnabled: enterpriseFeatures.topDownAssignmentRealTimeLcm, + enterpriseId: enterpriseUUID, + enablePortalLearnerCreditManagementScreen: true, + queryOptions: { + select: (data) => { + if (!budgetDetailRouteMatch?.params?.budgetId) { + return data.budgets; + } + + return data.budgets.filter(budget => budget.id === budgetDetailRouteMatch.params.budgetId); + }, + }, + }); + + const { + isExpiring, notification, modal, dismissModal, + } = useExpiry( + enterpriseUUID, + budgets, + modalOpen, + modalClose, + ); + + const trackEventMetadata = useMemo(() => { + if (!isExpiring) { return {}; } + return { + isExpiring, + notification, + }; + }, [isExpiring, notification]); + + return ( + <> + {isExpiring && ( + sendEnterpriseTrackEvent( + enterpriseUUID, + EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BUDGET_EXPIRY_ALERT_CONTACT_SUPPORT, + trackEventMetadata, + )} + className="flex-shrink-0" + > + Contact support + , + ]} + dismissible={notification.dismissible} + onClose={() => alertClose()} + > + {notification.title} +

{notification.message}

+
+ )} + + {isExpiring && ( + + + + + )} + > +

+ {modal.message} +

+
+ )} + + ); +}; + +const mapStateToProps = state => ({ + enterpriseUUID: state.portalConfiguration.enterpriseId, + enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, +}); + +BudgetExpiryAlertAndModal.propTypes = { + enterpriseUUID: PropTypes.string.isRequired, + enterpriseFeatures: PropTypes.shape({ + topDownAssignmentRealTimeLcm: PropTypes.bool.isRequired, + }), +}; + +export default connect(mapStateToProps)(BudgetExpiryAlertAndModal); diff --git a/src/components/EnterpriseApp/data/constants.js b/src/components/EnterpriseApp/data/constants.js index c19fcc0a62..a17c95b6eb 100644 --- a/src/components/EnterpriseApp/data/constants.js +++ b/src/components/EnterpriseApp/data/constants.js @@ -17,6 +17,7 @@ export const ROUTE_NAMES = { export const BUDGET_STATUSES = { active: 'Active', expired: 'Expired', + expiring: 'Expiring', scheduled: 'Scheduled', retired: 'Retired', }; diff --git a/src/components/EnterpriseSubsidiesContext/data/hooks.js b/src/components/EnterpriseSubsidiesContext/data/hooks.js index 0770ae1e8f..ba5fee9e72 100644 --- a/src/components/EnterpriseSubsidiesContext/data/hooks.js +++ b/src/components/EnterpriseSubsidiesContext/data/hooks.js @@ -110,6 +110,7 @@ export const useEnterpriseBudgets = ({ enablePortalLearnerCreditManagementScreen, enterpriseId, isTopDownAssignmentEnabled, + queryOptions = {}, }) => useQuery({ queryKey: learnerCreditManagementQueryKeys.budgets(enterpriseId), queryFn: (args) => fetchEnterpriseBudgets({ @@ -118,6 +119,7 @@ export const useEnterpriseBudgets = ({ enterpriseId, enablePortalLearnerCreditManagementScreen, }), + ...queryOptions, }); export const useCustomerAgreement = ({ enterpriseId }) => { diff --git a/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx b/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx index 5d4c9f4294..c7650c3486 100644 --- a/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageBreadcrumbs.jsx @@ -8,7 +8,7 @@ import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; import EVENT_NAMES from '../../eventTracking'; import { useBudgetId, useSubsidyAccessPolicy } from './data'; -const BudgetDetailPageBreadcrumbs = ({ enterpriseId, enterpriseSlug, budgetDisplayName }) => { +const BudgetDetailPageBreadcrumbs = ({ enterpriseId, enterpriseSlug, displayName }) => { const { subsidyAccessPolicyId } = useBudgetId(); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); @@ -39,7 +39,7 @@ const BudgetDetailPageBreadcrumbs = ({ enterpriseId, enterpriseSlug, budgetDispl to: `/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}`, }]} linkAs={Link} - activeLabel={budgetDisplayName} + activeLabel={displayName} clickHandler={() => sendEnterpriseTrackEvent( enterpriseId, EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BREADCRUMB_FROM_BUDGET_DETAIL_TO_BUDGETS, @@ -58,7 +58,7 @@ const mapStateToProps = state => ({ BudgetDetailPageBreadcrumbs.propTypes = { enterpriseId: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, - budgetDisplayName: PropTypes.string.isRequired, + displayName: PropTypes.string.isRequired, }; export default connect(mapStateToProps)(BudgetDetailPageBreadcrumbs); diff --git a/src/components/learner-credit-management/BudgetDetailPageHeader.jsx b/src/components/learner-credit-management/BudgetDetailPageHeader.jsx index 7c359ecc17..f7170e14bf 100644 --- a/src/components/learner-credit-management/BudgetDetailPageHeader.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageHeader.jsx @@ -1,117 +1,33 @@ import React from 'react'; import { - Stack, Card, Badge, Skeleton, + Stack, } from '@edx/paragon'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; import { useBudgetId, useSubsidyAccessPolicy, - useBudgetDetailHeaderData, useEnterpriseOffer, - formatDate, - useSubsidySummaryAnalyticsApi, } from './data'; import BudgetDetailPageBreadcrumbs from './BudgetDetailPageBreadcrumbs'; -import BudgetDetailPageOverviewAvailability from './BudgetDetailPageOverviewAvailability'; -import BudgetDetailPageOverviewUtilization from './BudgetDetailPageOverviewUtilization'; -import { BUDGET_TYPES } from '../EnterpriseApp/data/constants'; +import BudgetOverviewContent from './BudgetOverviewContent'; +import BudgetExpiryAlertAndModal from '../BudgetExpiryAlertAndModal'; -const BudgetStatusBadge = ({ - badgeVariant, status, term, date, -}) => ( - - {status} - {(term && date) && ( - {term} {formatDate(date)} - )} - -); - -const BudgetDetailPageHeader = ({ enterpriseUUID, enterpriseFeatures }) => { +const BudgetDetailPageHeader = () => { const { subsidyAccessPolicyId, enterpriseOfferId } = useBudgetId(); - const budgetType = (enterpriseOfferId !== null) ? BUDGET_TYPES.ecommerce : BUDGET_TYPES.policy; - - const { isLoading: isLoadingSubsidySummary, subsidySummary } = useSubsidySummaryAnalyticsApi( - enterpriseUUID, - enterpriseOfferId, - budgetType, - ); - const { isLoading: isLoadingEnterpriseOffer, data: enterpriseOfferMetadata } = useEnterpriseOffer(enterpriseOfferId); + const { data: enterpriseOfferMetadata } = useEnterpriseOffer(enterpriseOfferId); const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); - const policyOrOfferId = subsidyAccessPolicyId || enterpriseOfferId; - const { - budgetId, - budgetDisplayName, - budgetTotalSummary, - budgetAggregates, - status, - badgeVariant, - term, - date, - isAssignable, - } = useBudgetDetailHeaderData({ - subsidyAccessPolicy, - subsidySummary, - budgetId: policyOrOfferId, - enterpriseOfferMetadata, - isTopDownAssignmentEnabled: enterpriseFeatures.topDownAssignmentRealTimeLcm, - }); - - if (!subsidyAccessPolicy && (isLoadingSubsidySummary || isLoadingEnterpriseOffer)) { - return ( -
- - Loading budget header data -
- ); - } + const displayName = subsidyAccessPolicy?.displayName || enterpriseOfferMetadata?.displayName || 'Overview'; return ( - - - -

{budgetDisplayName}

- - - -
-
+ + +
); }; -const mapStateToProps = state => ({ - enterpriseUUID: state.portalConfiguration.enterpriseId, - enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, -}); - -BudgetDetailPageHeader.propTypes = { - enterpriseUUID: PropTypes.string.isRequired, - enterpriseFeatures: PropTypes.shape({ - topDownAssignmentRealTimeLcm: PropTypes.bool, - }).isRequired, -}; - -BudgetStatusBadge.propTypes = { - badgeVariant: PropTypes.string.isRequired, - status: PropTypes.string.isRequired, - term: PropTypes.string.isRequired, - date: PropTypes.string.isRequired, -}; - -export default connect(mapStateToProps)(BudgetDetailPageHeader); +export default (BudgetDetailPageHeader); diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx index 525ea2606a..36e7539f24 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { connect } from 'react-redux'; import { - Button, Col, Hyperlink, ProgressBar, Row, Stack, useMediaQuery, breakpoints, + breakpoints, Button, Col, Hyperlink, ProgressBar, Stack, Row, useMediaQuery, } from '@edx/paragon'; import { Add } from '@edx/paragon/icons'; import { generatePath, useParams, Link } from 'react-router-dom'; @@ -12,10 +12,27 @@ import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from './data'; import { configuration } from '../../config'; import EVENT_NAMES from '../../eventTracking'; import { LEARNER_CREDIT_ROUTE } from './constants'; +import { BUDGET_STATUSES } from '../EnterpriseApp/data/constants'; -const BudgetDetail = ({ available, utilized, limit }) => { +const BudgetDetail = ({ + available, utilized, limit, status, +}) => { const currentProgressBarLimit = (available / limit) * 100; + if (status === BUDGET_STATUSES.expired) { + return ( + +

Spent

+ + {formatPrice(utilized)} + + Unspent {formatPrice(available)} + + +
+ ); + } + return (

Available

@@ -39,9 +56,15 @@ BudgetDetail.propTypes = { available: PropTypes.number.isRequired, utilized: PropTypes.number.isRequired, limit: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, }; -const BudgetActions = ({ budgetId, isAssignable, enterpriseId }) => { +const BudgetActions = ({ + budgetId, + isAssignable, + enterpriseId, + status, +}) => { const { enterpriseSlug, enterpriseAppPage } = useParams(); const supportUrl = configuration.ENTERPRISE_SUPPORT_URL; const { subsidyAccessPolicyId } = useBudgetId(); @@ -67,11 +90,37 @@ const BudgetActions = ({ budgetId, isAssignable, enterpriseId }) => { const isLargeScreenOrGreater = useMediaQuery({ query: `(min-width: ${breakpoints.small.minWidth}px)` }); - if (!isAssignable) { + if (status === BUDGET_STATUSES.expired) { return (

Get people learning using this budget

+

+ This budget has expired. To create a new budget, please contact support. +

+ +
+
+ ); + } + + if (!isAssignable) { + return ( +
+
+

Keep people learning with a new plan

Funds from this budget are set to auto-allocate to registered learners based on settings configured with your support team. @@ -123,6 +172,7 @@ BudgetActions.propTypes = { budgetId: PropTypes.string.isRequired, isAssignable: PropTypes.bool.isRequired, enterpriseId: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, }; const BudgetDetailPageOverviewAvailability = ({ @@ -131,37 +181,38 @@ const BudgetDetailPageOverviewAvailability = ({ budgetTotalSummary: { available, utilized, limit }, enterpriseFeatures, enterpriseId, + status, }) => ( - + ); -const budgetTotalSummaryShape = { - utilized: PropTypes.number.isRequired, - available: PropTypes.number.isRequired, - limit: PropTypes.number.isRequired, -}; - BudgetDetailPageOverviewAvailability.propTypes = { budgetId: PropTypes.string.isRequired, - budgetTotalSummary: PropTypes.shape(budgetTotalSummaryShape).isRequired, + budgetTotalSummary: PropTypes.shape({ + utilized: PropTypes.number.isRequired, + available: PropTypes.number.isRequired, + limit: PropTypes.number.isRequired, + }).isRequired, isAssignable: PropTypes.bool.isRequired, enterpriseFeatures: PropTypes.shape({ topDownAssignmentRealTimeLcm: PropTypes.bool, }).isRequired, enterpriseId: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, }; const mapStateToProps = state => ({ diff --git a/src/components/learner-credit-management/BudgetOverviewContent.jsx b/src/components/learner-credit-management/BudgetOverviewContent.jsx new file mode 100644 index 0000000000..c0fa8c62bc --- /dev/null +++ b/src/components/learner-credit-management/BudgetOverviewContent.jsx @@ -0,0 +1,117 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Badge, Card, Skeleton, Stack, +} from '@edx/paragon'; + +import { connect } from 'react-redux'; +import BudgetDetailPageOverviewAvailability from './BudgetDetailPageOverviewAvailability'; +import BudgetDetailPageOverviewUtilization from './BudgetDetailPageOverviewUtilization'; +import { + formatDate, + useBudgetDetailHeaderData, + useBudgetId, + useEnterpriseOffer, useSubsidyAccessPolicy, + useSubsidySummaryAnalyticsApi, +} from './data'; +import { BUDGET_TYPES } from '../EnterpriseApp/data/constants'; + +const BudgetStatusBadge = ({ + badgeVariant, status, term, date, +}) => ( + + {status} + {(term && date) && ( + {term} {formatDate(date)} + )} + +); + +const BudgetOverviewContent = ({ + enterpriseUUID, + enterpriseFeatures, +}) => { + const { subsidyAccessPolicyId, enterpriseOfferId } = useBudgetId(); + + const budgetType = (enterpriseOfferId !== null) ? BUDGET_TYPES.ecommerce : BUDGET_TYPES.policy; + + const { isLoading: isLoadingSubsidySummary, subsidySummary } = useSubsidySummaryAnalyticsApi( + enterpriseUUID, + enterpriseOfferId, + budgetType, + ); + + const { isLoading: isLoadingEnterpriseOffer, data: enterpriseOfferMetadata } = useEnterpriseOffer(enterpriseOfferId); + const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId); + + const policyOrOfferId = subsidyAccessPolicyId || enterpriseOfferId; + + const { + budgetId, + budgetDisplayName, + budgetTotalSummary, + budgetAggregates, + status, + badgeVariant, + term, + date, + isAssignable, + } = useBudgetDetailHeaderData({ + subsidyAccessPolicy, + subsidySummary, + budgetId: policyOrOfferId, + enterpriseOfferMetadata, + isTopDownAssignmentEnabled: enterpriseFeatures.topDownAssignmentRealTimeLcm, + }); + + if (!subsidyAccessPolicy && (isLoadingSubsidySummary || isLoadingEnterpriseOffer)) { + return ( +

+ + Loading budget header data +
+ ); + } + + return ( + + +

{budgetDisplayName}

+ + + +
+
+ ); +}; + +const mapStateToProps = state => ({ + enterpriseUUID: state.portalConfiguration.enterpriseId, + enterpriseFeatures: state.portalConfiguration.enterpriseFeatures, +}); + +BudgetStatusBadge.propTypes = { + badgeVariant: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + term: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, +}; + +BudgetOverviewContent.propTypes = { + enterpriseUUID: PropTypes.string.isRequired, + enterpriseFeatures: PropTypes.shape({ + topDownAssignmentRealTimeLcm: PropTypes.bool, + }).isRequired, +}; + +export default connect(mapStateToProps)(BudgetOverviewContent); diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx index f27e0f0c77..6f4a4fc3ef 100644 --- a/src/components/learner-credit-management/SubBudgetCard.jsx +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -106,12 +106,12 @@ const BaseSubBudgetCard = ({ title={{budgetType}} subtitle={{subtitle}} actions={ - budgetLabel.status !== BUDGET_STATUSES.scheduled - ? renderActions(budgetId) - : undefined - } + budgetLabel.status !== BUDGET_STATUSES.scheduled + ? renderActions(budgetId) + : undefined + } className={classNames('align-items-center', { - 'mb-4.5': budgetLabel.status !== BUDGET_STATUSES.active, + 'mb-4.5': budgetLabel.status !== BUDGET_STATUSES.active && budgetLabel.status !== BUDGET_STATUSES.expiring, })} /> ); @@ -155,7 +155,8 @@ const BaseSubBudgetCard = ({ {renderCardHeader(displayName || 'Overview', id)} - {budgetLabel.status === BUDGET_STATUSES.active && renderCardSection()} + {(budgetLabel.status === BUDGET_STATUSES.active || budgetLabel.status === BUDGET_STATUSES.expiring) + && renderCardSection()} diff --git a/src/components/learner-credit-management/data/tests/constants.js b/src/components/learner-credit-management/data/tests/constants.js index f2e7b2dbe0..76206356f8 100644 --- a/src/components/learner-credit-management/data/tests/constants.js +++ b/src/components/learner-credit-management/data/tests/constants.js @@ -1,10 +1,13 @@ +const today = Date.now(); + export const mockEnterpriseOfferId = '123'; + export const mockSubsidyAccessPolicyUUID = 'c17de32e-b80b-468f-b994-85e68fd32751'; export const mockAssignableSubsidyAccessPolicy = { uuid: mockSubsidyAccessPolicyUUID, - subsidyActiveDatetime: '2023-11-01T13:06:46Z', - subsidyExpirationDatetime: '2024-02-29T13:06:59Z', + subsidyActiveDatetime: new Date(today).toISOString(), + subsidyExpirationDatetime: new Date(today + 130 * 24 * 60 * 60 * 1000).toISOString(), policyType: 'AssignedLearnerCreditAccessPolicy', displayName: 'Assignable Learner Credit', spendLimit: 10000 * 100, @@ -22,8 +25,8 @@ export const mockAssignableSubsidyAccessPolicy = { export const mockAssignableSubsidyAccessPolicyWithNoUtilization = { uuid: mockSubsidyAccessPolicyUUID, - subsidyActiveDatetime: '2023-11-01T13:06:46Z', - subsidyExpirationDatetime: '2024-02-29T13:06:59Z', + subsidyActiveDatetime: new Date(today).toISOString(), + subsidyExpirationDatetime: new Date(today + 130 * 24 * 60 * 60 * 1000).toISOString(), policyType: 'AssignedLearnerCreditAccessPolicy', displayName: 'Assignable Learner Credit', spendLimit: 10000 * 100, @@ -41,8 +44,8 @@ export const mockAssignableSubsidyAccessPolicyWithNoUtilization = { export const mockAssignableSubsidyAccessPolicyWithSpendNoAllocations = { uuid: mockSubsidyAccessPolicyUUID, - subsidyActiveDatetime: '2023-11-01T13:06:46Z', - subsidyExpirationDatetime: '2024-02-29T13:06:59Z', + subsidyActiveDatetime: new Date(today).toISOString(), + subsidyExpirationDatetime: new Date(today + 130 * 24 * 60 * 60 * 1000).toISOString(), policyType: 'AssignedLearnerCreditAccessPolicy', displayName: 'Assignable Learner Credit', spendLimit: 10000 * 100, @@ -60,8 +63,8 @@ export const mockAssignableSubsidyAccessPolicyWithSpendNoAllocations = { export const mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed = { uuid: mockSubsidyAccessPolicyUUID, - subsidyActiveDatetime: '2023-11-01T13:06:46Z', - subsidyExpirationDatetime: '2024-02-29T13:06:59Z', + subsidyActiveDatetime: new Date(today).toISOString(), + subsidyExpirationDatetime: new Date(today + 130 * 24 * 60 * 60 * 1000).toISOString(), policyType: 'AssignedLearnerCreditAccessPolicy', displayName: 'Assignable Learner Credit', spendLimit: 10000 * 100, @@ -79,8 +82,8 @@ export const mockAssignableSubsidyAccessPolicyWithSpendNoRedeemed = { export const mockPerLearnerSpendLimitSubsidyAccessPolicy = { uuid: mockSubsidyAccessPolicyUUID, - subsidyActiveDatetime: '2023-11-01T13:06:46Z', - subsidyExpirationDatetime: '2024-02-29T13:06:59Z', + subsidyActiveDatetime: new Date(today).toISOString(), + subsidyExpirationDatetime: new Date(today + 130 * 24 * 60 * 60 * 1000).toISOString(), policyType: 'PerLearnerSpendCreditAccessPolicy', displayName: 'Per Learner Spend Limit', spendLimit: 10000 * 100, @@ -105,7 +108,7 @@ export const mockSubsidySummary = { export const mockEnterpriseOfferMetadata = { id: 99511, - startDatetime: '2022-09-01T00:00:00Z', - endDatetime: '2024-09-01T00:00:00Z', + startDatetime: new Date(today).toISOString(), + endDatetime: new Date(today + 130 * 24 * 60 * 60 * 1000).toISOString(), displayName: 'Test Display Name', }; diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 9f35a3ed05..ce6c23ee9a 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -12,6 +12,7 @@ import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import EnterpriseDataApiService from '../../../data/services/EnterpriseDataApiService'; import SubsidyApiService from '../../../data/services/EnterpriseSubsidyApiService'; +import { isPlanApproachingExpiry } from '../../BudgetExpiryAlertAndModal/data/utils'; /** * Transforms subsidy (offer or Subsidy) summary from API for display in the UI, guarding @@ -179,6 +180,15 @@ export const getBudgetStatus = ({ }; } + if (isPlanApproachingExpiry(endDateStr).isPlanExpiring) { + return { + status: BUDGET_STATUSES.expiring, + badgeVariant: 'warning', + term: 'Expiring', + date: endDateStr, + }; + } + // Check if budget is current (today's date between start/end dates) if (currentDate >= startDate && currentDate <= endDate) { return { @@ -218,10 +228,11 @@ export const formatPrice = (price, options = {}) => { */ export const orderBudgets = (budgets) => { const statusOrder = { - Active: 0, - Scheduled: 1, - Expired: 2, - Retired: 3, + Expiring: 0, + Active: 1, + Scheduled: 2, + Expired: 3, + Retired: 4, }; budgets?.sort((budgetA, budgetB) => { diff --git a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx index 7ce956ab94..acc6fc728f 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx @@ -278,7 +278,7 @@ describe('', () => { const mockBudgetDisplayName = 'Test Budget'; renderWithRouter( - + , ); const previousBreadcrumb = screen.getByText('Budgets'); diff --git a/src/eventTracking.js b/src/eventTracking.js index d77196db3e..e98b23c77c 100644 --- a/src/eventTracking.js +++ b/src/eventTracking.js @@ -34,6 +34,9 @@ const BUDGET_DETAIL_CATALOG_TAB_PREFIX = `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.bu const BUDGET_DETAIL_SEARCH_PREFIX = `${BUDGET_DETAIL_CATALOG_TAB_PREFIX}.search`; const BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX = `${BUDGET_DETAIL_CATALOG_TAB_PREFIX}.assignment_modal`; +// BudgetExpiry +const LEARNER_CREDIT_BUDGET_EXPIRY_PREFIX = `${LEARNER_CREDIT_MANAGEMENT_PREFIX}.budget_expiry`; + export const SUBSCRIPTION_TABLE_EVENTS = { // Pagination PAGINATION_NEXT: `${SUBSCRIPTION_TABLE_PREFIX}.pagination.next.clicked`, @@ -166,6 +169,9 @@ export const LEARNER_CREDIT_MANAGEMENT_EVENTS = { ASSIGNMENT_ALLOCATION_LEARNER_ASSIGNMENT: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.assignment_allocation.assigned`, EMAIL_ADDRESS_VALIDATION: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.email_validation.changed`, ASSIGNMENT_ALLOCATION_ERROR: `${BUDGET_DETAIL_ASSIGNMENT_MODAL_PREFIX}.assignment_allocation.errored`, + // Budget Expiry + BUDGET_EXPIRY_ALERT_CONTACT_SUPPORT: `${LEARNER_CREDIT_BUDGET_EXPIRY_PREFIX}.alert.contact_support.clicked`, + BUDGET_EXPIRY_MODAL_CONTACT_SUPPORT: `${LEARNER_CREDIT_BUDGET_EXPIRY_PREFIX}.modal.contact_support.clicked`, }; const EVENT_NAMES = {