diff --git a/src/components/BudgetExpiryAlertAndModal/data/expiryThresholds.js b/src/components/BudgetExpiryAlertAndModal/data/expiryThresholds.js index b91463fa38..3908c36594 100644 --- a/src/components/BudgetExpiryAlertAndModal/data/expiryThresholds.js +++ b/src/components/BudgetExpiryAlertAndModal/data/expiryThresholds.js @@ -93,26 +93,26 @@ const expiryThresholds = { 30: ({ intl, date }) => ({ notificationTemplate: { title: intl.formatMessage({ - id: 'adminPostal.learnerCredit.expiresInThirtyDaysNotification.title', + id: 'adminPortal.learnerCredit.expiresInThirtyDaysNotification.title', defaultMessage: 'Your Learner Credit plan expires in less than 30 days', description: 'Title for the notification that the Learner Credit plan is expiring in less than 30 days.', }), variant: 'danger', message: intl.formatMessage({ - id: 'adminPostal.learnerCredit.expiresInThirtyDaysNotification.message', - defaultMessage: 'When your plan expires you will lose access to administrative functions and the remaining balance of your plan{apostrophe}s budget(s) will be unusable. Contact support today to renew your plan.', + id: 'adminPortal.learnerCredit.expiresInThirtyDaysNotification.message', + defaultMessage: "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.", description: 'Message for the notification that the Learner Credit plan is expiring in less than 30 days.', - }, { aposrophe: "'" }), + }), }, modalTemplate: { title: intl.formatMessage({ - id: 'adminPostal.learnerCredit.expiresInThirtyDaysModal.title', + id: 'adminPortal.learnerCredit.expiresInThirtyDaysModal.title', defaultMessage: 'Your Learner Credit plan expires in less than 30 days', description: 'Title for the modal that the Learner Credit plan is expiring in less than 30 days.', }), message: parse(sanitizeHTML( intl.formatMessage({ - id: 'adminPostal.learnerCredit.expiresInThirtyDaysModal.message', + id: 'adminPortal.learnerCredit.expiresInThirtyDaysModal.message', defaultMessage: 'Your Learner Credit plan expires {date}. Contact support today to renew your plan and keep people learning.', description: 'Message for the modal that the Learner Credit plan is expiring in less than 30 days.', }, { date }), @@ -161,10 +161,10 @@ const expiryThresholds = { message: parse(sanitizeHTML( intl.formatMessage({ id: 'adminPortal.learnerCreditPlan.expiredModal.message', - defaultMessage: `Your Learner Credit plan expired on {date}. You no longer have access to administrative functions and the remaining balance of your plan{apostrophe}s budget(s) are no longer available to spend. + defaultMessage: `Your Learner Credit plan expired on {date}. You no longer have access to administrative functions and the remaining balance of your plan's budget(s) are no longer available to spend. Please contact your representative if you have any questions or concerns.`, description: 'Message for the modal that the Learner Credit plan has expired.', - }, { date, apostrophe: "'" }), + }, { date }), )), }, variant: PLAN_EXPIRY_VARIANTS.expired, diff --git a/src/components/ContentHighlights/DeleteArchivedHighlightsDialogs.jsx b/src/components/ContentHighlights/DeleteArchivedHighlightsDialogs.jsx index a1db17a147..f75a2e0705 100644 --- a/src/components/ContentHighlights/DeleteArchivedHighlightsDialogs.jsx +++ b/src/components/ContentHighlights/DeleteArchivedHighlightsDialogs.jsx @@ -95,12 +95,8 @@ const DeleteArchivedCoursesDialogs = ({

diff --git a/src/components/ContentHighlights/DeleteHighlightSet.jsx b/src/components/ContentHighlights/DeleteHighlightSet.jsx index 4cdbf67ffc..501edce973 100644 --- a/src/components/ContentHighlights/DeleteHighlightSet.jsx +++ b/src/components/ContentHighlights/DeleteHighlightSet.jsx @@ -172,12 +172,8 @@ const DeleteHighlightSet = ({ enterpriseId, enterpriseSlug }) => {

diff --git a/src/components/ContentHighlights/HighlightStepper/HighlightStepperTitle.jsx b/src/components/ContentHighlights/HighlightStepper/HighlightStepperTitle.jsx index beb00dea6b..4d7fcf7eb3 100644 --- a/src/components/ContentHighlights/HighlightStepper/HighlightStepperTitle.jsx +++ b/src/components/ContentHighlights/HighlightStepper/HighlightStepperTitle.jsx @@ -32,14 +32,8 @@ const HighlightStepperTitle = () => (

diff --git a/src/components/NotFoundPage/index.jsx b/src/components/NotFoundPage/index.jsx index cb7dc56f52..3cdc38a0e3 100644 --- a/src/components/NotFoundPage/index.jsx +++ b/src/components/NotFoundPage/index.jsx @@ -12,17 +12,15 @@ export const NotFound = () => (

diff --git a/src/components/learner-credit-management/AssignmentEnrollByDateCell.jsx b/src/components/learner-credit-management/AssignmentEnrollByDateCell.jsx new file mode 100644 index 0000000000..d13f627ce7 --- /dev/null +++ b/src/components/learner-credit-management/AssignmentEnrollByDateCell.jsx @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import { + Icon, IconButtonWithTooltip, Stack, +} from '@openedx/paragon'; +import { Warning } from '@openedx/paragon/icons'; +import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; +import { ENROLL_BY_DATE_DAYS_THRESHOLD, formatDate } from './data'; +import { isTodayWithinDateThreshold } from '../../utils'; + +const messages = defineMessages({ + tooltipContent: { + id: 'lcm.budget.detail.page.assignments.table.columns.enroll-by-date.data.tooltip-content', + defaultMessage: 'Enrollment deadline approaching', + description: 'On hover tool tip message for an upcoming enrollment date', + }, +}); + +const ExpiringIconButtonWithTooltip = () => { + const intl = useIntl(); + return ( + + ); +}; + +const AssignmentEnrollByDateCell = ({ row }) => { + const { original: { earliestPossibleExpiration: { date } } } = row; + + const formattedEnrollByDate = formatDate(date); + const isAssignmentExpiringSoon = isTodayWithinDateThreshold({ + days: ENROLL_BY_DATE_DAYS_THRESHOLD, + date, + }); + + return ( + + {isAssignmentExpiringSoon && } +
+ {formattedEnrollByDate} +
+
+ ); +}; + +AssignmentEnrollByDateCell.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + earliestPossibleExpiration: PropTypes.shape({ + date: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; + +export default AssignmentEnrollByDateCell; diff --git a/src/components/learner-credit-management/AssignmentEnrollByDateHeader.jsx b/src/components/learner-credit-management/AssignmentEnrollByDateHeader.jsx new file mode 100644 index 0000000000..7ddb4c1ce7 --- /dev/null +++ b/src/components/learner-credit-management/AssignmentEnrollByDateHeader.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { + Stack, + Icon, + IconButtonWithTooltip, +} from '@openedx/paragon'; +import { InfoOutline } from '@openedx/paragon/icons'; +import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + header: { + id: 'lcm.budget.detail.page.assignments.table.columns.enroll-by-date.header', + defaultMessage: 'Enroll-by date', + description: 'Column header for the enroll-by date column in the assignments table', + }, + headerTooltipContent: { + id: 'lcm.budget.detail.page.assignments.table.columns.enroll-by-date.header.tooltip-content', + defaultMessage: 'Failure to enroll by midnight of enrollment deadline date will release funds back into the budget', + description: 'On hover tool tip message for the enroll-by date column', + }, +}); + +const AssignmentEnrollByDateHeader = () => { + const intl = useIntl(); + return ( + + + {intl.formatMessage(messages.header)} + + + + ); +}; + +export default AssignmentEnrollByDateHeader; diff --git a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx index f39ba436d9..d6cb6a1bd5 100644 --- a/src/components/learner-credit-management/BudgetAssignmentsTable.jsx +++ b/src/components/learner-credit-management/BudgetAssignmentsTable.jsx @@ -12,6 +12,8 @@ import AssignmentTableCancelAction from './AssignmentTableCancel'; import { DEFAULT_PAGE, PAGE_SIZE, formatPrice } from './data'; import AssignmentRecentActionTableCell from './AssignmentRecentActionTableCell'; import AssignmentsTableRefreshAction from './AssignmentsTableRefreshAction'; +import AssignmentEnrollByDateCell from './AssignmentEnrollByDateCell'; +import AssignmentEnrollByDateHeader from './AssignmentEnrollByDateHeader'; const FilterStatus = (rest) => ; @@ -111,6 +113,13 @@ const BudgetAssignmentsTable = ({ Cell: AssignmentRecentActionTableCell, disableFilters: true, }, + { + Header: AssignmentEnrollByDateHeader, + accessor: 'earliestPossibleExpiration', + Cell: AssignmentEnrollByDateCell, + disableFilters: true, + disableSortBy: true, + }, ]} additionalColumns={[ { diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx index 64fde7a8a0..35c0b50480 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -13,12 +13,15 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { configuration } from '../../config'; import { BudgetDetailPageContext } from './BudgetDetailPageWrapper'; import { - useBudgetId, useSubsidyAccessPolicy, useEnterpriseCustomer, useEnterpriseGroup, + useBudgetId, + useSubsidyAccessPolicy, + useEnterpriseCustomer, + useEnterpriseGroup, + isLmsBudget, } from './data'; import EVENT_NAMES from '../../eventTracking'; import { LEARNER_CREDIT_ROUTE } from './constants'; import { BUDGET_STATUSES } from '../EnterpriseApp/data/constants'; -import isLmsBudget from './utils'; import BudgetDetail from './BudgetDetail'; const BudgetActions = ({ @@ -111,9 +114,8 @@ const BudgetActions = ({

diff --git a/src/components/learner-credit-management/BudgetStatusSubtitle.jsx b/src/components/learner-credit-management/BudgetStatusSubtitle.jsx index f68c54fb84..de07b19a6b 100644 --- a/src/components/learner-credit-management/BudgetStatusSubtitle.jsx +++ b/src/components/learner-credit-management/BudgetStatusSubtitle.jsx @@ -7,8 +7,12 @@ import { import { GroupAdd, Groups, ManageAccounts } from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { formatDate, useEnterpriseCustomer, useEnterpriseGroup } from './data'; -import isLmsBudget from './utils'; +import { + formatDate, + useEnterpriseCustomer, + useEnterpriseGroup, + isLmsBudget, +} from './data'; const BudgetStatusSubtitle = ({ badgeVariant, status, isAssignable, term, date, policy, enterpriseUUID, diff --git a/src/components/learner-credit-management/CancelAssignmentModal.jsx b/src/components/learner-credit-management/CancelAssignmentModal.jsx index a33f942665..a6273dc77f 100644 --- a/src/components/learner-credit-management/CancelAssignmentModal.jsx +++ b/src/components/learner-credit-management/CancelAssignmentModal.jsx @@ -53,9 +53,8 @@ const CancelAssignmentModal = ({

diff --git a/src/components/learner-credit-management/RemindAssignmentModal.jsx b/src/components/learner-credit-management/RemindAssignmentModal.jsx index 384062a860..cf6e7f7819 100644 --- a/src/components/learner-credit-management/RemindAssignmentModal.jsx +++ b/src/components/learner-credit-management/RemindAssignmentModal.jsx @@ -47,9 +47,8 @@ const RemindAssignmentModal = ({

diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentAllocationHelpCollapsibles.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentAllocationHelpCollapsibles.jsx index c114d8cc94..c04f0ee9e4 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentAllocationHelpCollapsibles.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentAllocationHelpCollapsibles.jsx @@ -78,24 +78,15 @@ const AssignmentAllocationHelpCollapsibles = ({ enterpriseId, course }) => (
  • diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryEmptyState.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryEmptyState.jsx index 8c53d4a6a0..06f73615ef 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryEmptyState.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryEmptyState.jsx @@ -6,9 +6,8 @@ const AssignmentModalSummaryEmptyState = () => (
    diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryErrorState.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryErrorState.jsx index c6de52aa90..d1832820ee 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryErrorState.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryErrorState.jsx @@ -10,9 +10,8 @@ const AssignmentModalSummaryErrorState = () => (
    diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index 14dbc543f4..e7d6b7756f 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -68,6 +68,9 @@ export const ASSIGNMENT_STATUS_MODAL_MAX_WIDTH = 480; export const MEMBERS_TABLE_PAGE_SIZE = 10; +// Enroll-by date warning message threshold by days +export const ENROLL_BY_DATE_DAYS_THRESHOLD = 10; + // Query Key factory for the learner credit management module, intended to be used with `@tanstack/react-query`. // Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories. export const learnerCreditManagementQueryKeys = { diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 0c6b0c2116..a647dc2e4a 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -545,3 +545,8 @@ export const getTranslatedBudgetTerm = (intl, term) => { return ''; } }; + +export const isLmsBudget = ( + activeIntegrationsLength, + isUniversalGroup, +) => activeIntegrationsLength > 0 && isUniversalGroup; diff --git a/src/components/learner-credit-management/members-tab/MemberEnrollmentsTableColumnHeader.jsx b/src/components/learner-credit-management/members-tab/MemberEnrollmentsTableColumnHeader.jsx index 84c6a7f3ff..fd39695adb 100644 --- a/src/components/learner-credit-management/members-tab/MemberEnrollmentsTableColumnHeader.jsx +++ b/src/components/learner-credit-management/members-tab/MemberEnrollmentsTableColumnHeader.jsx @@ -8,9 +8,9 @@ import { import { InfoOutline } from '@openedx/paragon/icons'; const MemberEnrollmentsTableColumnHeader = () => ( - - -

    Enrollments

    + + + Enrollments ( - - -

    Status

    + + + Status ', () => { await waitFor(() => expect(screen.queryByText('dukesilver@test.com')).toBeInTheDocument()); userEvent.click(screen.getByText('Waiting for member')); await waitFor(() => expect(screen.queryByText('Waiting for dukesilver@test.com')).toBeInTheDocument()); - screen.debug(undefined, 10000000); screen.getByText('This member must accept their invitation to browse this budget\'s catalog and enroll using their ' + 'member permissions by logging in or creating an account within 90 days.'); // click again to close it out diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index 852d5b8b10..9488bb35f4 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -12,6 +12,7 @@ import { renderWithRouter, sendEnterpriseTrackEvent } from '@edx/frontend-enterp import { act } from 'react-dom/test-utils'; import { v4 as uuidv4 } from 'uuid'; import { faker } from '@faker-js/faker'; +import dayjs from 'dayjs'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import BudgetDetailPage from '../BudgetDetailPage'; @@ -139,6 +140,7 @@ const mockLearnerContentAssignment = { actions: [mockSuccessfulLinkedLearnerAction, mockSuccessfulNotifiedAction], errorReason: null, assignmentConfiguration: expect.any(Object), + earliestPossibleExpiration: { date: dayjs().add(5, 'days').toISOString() }, }; const createMockLearnerContentAssignment = () => ({ ...mockLearnerContentAssignment, @@ -2018,6 +2020,7 @@ describe('', () => { actions: [mockSuccessfulNotifiedAction], errorReason: null, state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(5, 'days').toISOString() }, }, { uuid: 'test-uuid2', @@ -2028,6 +2031,7 @@ describe('', () => { actions: [mockSuccessfulNotifiedAction], errorReason: null, state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(5, 'days').toISOString() }, }, ], learnerStateCounts: [ @@ -2109,6 +2113,7 @@ describe('', () => { actions: [mockSuccessfulNotifiedAction], errorReason: null, state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(5, 'days').toISOString() }, }, { uuid: 'test-uuid2', @@ -2119,6 +2124,7 @@ describe('', () => { actions: [mockSuccessfulNotifiedAction], errorReason: null, state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(5, 'days').toISOString() }, }, { uuid: 'test-uuid3', @@ -2129,6 +2135,7 @@ describe('', () => { actions: [mockSuccessfulNotifiedAction], errorReason: null, state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(5, 'days').toISOString() }, }, ], learnerStateCounts: [ @@ -2209,6 +2216,7 @@ describe('', () => { actions: [mockSuccessfulNotifiedAction], errorReason: null, state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(5, 'days').toISOString() }, }, ], learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], @@ -2278,6 +2286,7 @@ describe('', () => { actions: [mockSuccessfulNotifiedAction], errorReason: null, state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(5, 'days').toISOString() }, }, ], learnerStateCounts: [{ learnerState: 'waiting', count: 1 }], @@ -2361,4 +2370,106 @@ describe('', () => { expect(screen.getByText('• Enroll via Integrated Learning Platform')).toBeInTheDocument(); expect(screen.getByText('Manage edX in your integrated learning platform')).toBeInTheDocument(); }); + it.each([ + { + modifiedDayOffset: 5, + }, + { + modifiedDayOffset: 15, + }, + ])('displays upcoming expiring allocation (%s)', async ({ modifiedDayOffset }) => { + useParams.mockReturnValue({ + enterpriseSlug: 'test-enterprise-slug', + enterpriseAppPage: 'test-enterprise-page', + budgetId: mockSubsidyAccessPolicyUUID, + activeTabKey: 'activity', + }); + useBudgetRedemptions.mockReturnValue({ + isLoading: false, + budgetRedemptions: mockEmptyBudgetRedemptions, + fetchBudgetRedemptions: jest.fn(), + }); + useSubsidyAccessPolicy.mockReturnValue({ + isInitialLoading: false, + data: mockAssignableSubsidyAccessPolicy, + }); + useEnterpriseGroupLearners.mockReturnValue({ + data: { + count: 0, + currentPage: 1, + next: null, + numPages: 1, + results: [], + }, + }); + useBudgetDetailActivityOverview.mockReturnValue({ + isLoading: false, + data: { + contentAssignments: { count: 1 }, + spentTransactions: { count: 0 }, + }, + }); + useBudgetContentAssignments.mockReturnValue({ + isLoading: false, + contentAssignments: { + count: 3, + results: [ + { + uuid: 'test-uuid1', + contentKey: mockCourseKey, + contentQuantity: -19900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-10-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(modifiedDayOffset, 'days').toISOString() }, + }, + { + uuid: 'test-uuid2', + contentKey: mockCourseKey, + contentQuantity: -29900, + learnerState: 'waiting', + recentAction: { actionType: 'assigned', timestamp: '2023-11-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(12, 'days').toISOString() }, + }, + { + uuid: 'test-uuid3', + contentKey: mockCourseKey, + contentQuantity: -29900, + learnerState: 'notifying', + recentAction: { actionType: 'assigned', timestamp: '2023-11-27' }, + actions: [mockSuccessfulNotifiedAction], + errorReason: null, + state: 'allocated', + earliestPossibleExpiration: { date: dayjs().add(15, 'days').toISOString() }, + }, + ], + learnerStateCounts: [ + { learnerState: 'waiting', count: 3 }, + ], + numPages: 1, + currentPage: 1, + }, + fetchContentAssignments: jest.fn(), + }); + renderWithRouter(); + const enrollByDateTooltip = screen.getByTestId('enroll-by-date-tooltip'); + const expiringAllocationTooltip = screen.queryByTestId('upcoming-allocation-expiration-tooltip'); + + expect(screen.getByText('Enroll-by date')).toBeTruthy(); + + if (expiringAllocationTooltip) { + userEvent.hover(expiringAllocationTooltip); + await waitFor(() => expect(screen.getByText('Enrollment deadline approaching')).toBeTruthy()); + } + + userEvent.hover(enrollByDateTooltip); + await waitFor(() => expect(screen.getByText( + 'Failure to enroll by midnight of enrollment deadline date will release funds back into the budget', + )).toBeTruthy()); + }); }); diff --git a/src/components/learner-credit-management/utils.js b/src/components/learner-credit-management/utils.js deleted file mode 100644 index 58e82d8c00..0000000000 --- a/src/components/learner-credit-management/utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function isLmsBudget(activeIntegrationsLength, isUniversalGroup) { - return activeIntegrationsLength > 0 && isUniversalGroup; -} diff --git a/src/components/settings/SettingsAppearanceTab/index.jsx b/src/components/settings/SettingsAppearanceTab/index.jsx index 03d91374ab..4d6a630f3e 100644 --- a/src/components/settings/SettingsAppearanceTab/index.jsx +++ b/src/components/settings/SettingsAppearanceTab/index.jsx @@ -100,9 +100,8 @@ export const SettingsAppearanceTab = ({

    diff --git a/src/components/subscriptions/expiration/SubscriptionExpirationBanner.jsx b/src/components/subscriptions/expiration/SubscriptionExpirationBanner.jsx index 34e4dbbdac..992a62a798 100644 --- a/src/components/subscriptions/expiration/SubscriptionExpirationBanner.jsx +++ b/src/components/subscriptions/expiration/SubscriptionExpirationBanner.jsx @@ -36,9 +36,9 @@ const SubscriptionExpirationBanner = ({ isSubscriptionPlanDetails }) => { {intl.formatMessage({ id: 'admin.portal.subscription.expiration.banner.plan.expired.heading', - defaultMessage: 'This subscription plan{apostrophe}s end date has passed', + defaultMessage: "This subscription plan's end date has passed", description: 'Heading for expired plan message in subscription expiration banner.', - }, { apostrophe: "'" })} + })} {intl.formatMessage({ id: 'admin.portal.subscription.expiration.banner.plan.expired.message', @@ -51,9 +51,9 @@ const SubscriptionExpirationBanner = ({ isSubscriptionPlanDetails }) => { {intl.formatMessage({ id: 'admin.portal.subscription.expiration.banner.plan.approaching.heading', - defaultMessage: 'This subscription plan{apostrophe}s end date is approaching', + defaultMessage: "This subscription plan's end date is approaching", description: 'Heading for approaching plan message in subscription expiration banner.', - }, { apostrophe: "'" })} + })} {intl.formatMessage( { diff --git a/src/components/subscriptions/expiration/SubscriptionExpiringModal.jsx b/src/components/subscriptions/expiration/SubscriptionExpiringModal.jsx index fb82ac4b71..b88399db2a 100644 --- a/src/components/subscriptions/expiration/SubscriptionExpiringModal.jsx +++ b/src/components/subscriptions/expiration/SubscriptionExpiringModal.jsx @@ -58,12 +58,10 @@ const SubscriptionExpiringModal = ({ { intl.formatMessage({ id: 'admin.portal.subscription.expiration.modal.body.p1', - defaultMessage: `It{apostrophe}s time to renew your subscription contract with edX! + defaultMessage: `It's time to renew your subscription contract with edX! The edX customer support team is here to help. Get in touch today to minimize access disruptions for your learners.`, description: 'Body paragraph 1 for the subscription expiring modal in the admin portal.', - }, { - apostrophe: "'", }) }

    diff --git a/src/utils.js b/src/utils.js index 5ed45c0eb5..f4488f4141 100644 --- a/src/utils.js +++ b/src/utils.js @@ -561,6 +561,21 @@ function isArchivedContent(content) { return courseRunStatuses.every(status => ARCHIVABLE_STATUSES.includes(status)); } +/** + * Helper function utilizing dayjs's 'isBetween' function to determine + * if the date passed is between today and an offset amount of days + * + * @param date + * @param days + * @returns {boolean} + */ +function isTodayWithinDateThreshold({ date, days }) { + const dateToCheck = dayjs(date); + const today = dayjs(); + const offsetDays = dateToCheck.subtract(days, 'days'); + return today.isBetween(offsetDays, dateToCheck); +} + export { camelCaseDict, camelCaseDictArray, @@ -604,4 +619,5 @@ export { i18nFormatTimestamp, i18nFormatPassedTimestamp, i18nFormatProgressStatus, + isTodayWithinDateThreshold, };