From 7ec48922540673891e963329a4e32316480c4984 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 12 Sep 2024 09:57:30 -0400 Subject: [PATCH] refactor: Updates logic for start date threshold --- .../BudgetDetailPageOverviewAvailability.jsx | 3 +- .../BudgetDetailPageOverviewUtilization.jsx | 3 +- .../AssignmentModalmportantDates.jsx | 13 +++--- .../NewAssignmentModalButton.jsx | 3 +- .../NewAssignmentModalDropdown.jsx | 8 ++-- .../cards/data/useCourseCardMetadata.jsx | 2 +- .../learner-credit-management/constants.js | 20 --------- .../data/constants.js | 27 +++++++++++ .../data/hooks/usePathToCatalogTab.js | 3 +- .../learner-credit-management/data/utils.js | 45 +++++++++++++++---- 10 files changed, 81 insertions(+), 46 deletions(-) delete mode 100644 src/components/learner-credit-management/constants.js diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx index c66b5856af..d55d29be5f 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewAvailability.jsx @@ -18,10 +18,9 @@ import { useSubsidyAccessPolicy, useEnterpriseCustomer, useEnterpriseGroup, - isLmsBudget, + isLmsBudget, LEARNER_CREDIT_ROUTE, } from './data'; import EVENT_NAMES from '../../eventTracking'; -import { LEARNER_CREDIT_ROUTE } from './constants'; import { BUDGET_STATUSES } from '../EnterpriseApp/data/constants'; import BudgetDetail from './BudgetDetail'; import { useEnterpriseBudgets } from '../EnterpriseSubsidiesContext/data/hooks'; diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx index bd996e0696..a1c5f511f7 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx @@ -9,9 +9,8 @@ import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import { generatePath, useParams, Link } from 'react-router-dom'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; -import { formatPrice } from './data'; +import { formatPrice, LEARNER_CREDIT_ROUTE } from './data'; import EVENT_NAMES from '../../eventTracking'; -import { LEARNER_CREDIT_ROUTE } from './constants'; const BudgetDetailPageOverviewUtilization = ({ budgetId, diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx index a168ebd381..bf09625a51 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalmportantDates.jsx @@ -4,7 +4,9 @@ import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; -import { setStaleCourseStartDates, hasCourseStarted, SHORT_MONTH_DATE_FORMAT } from '../data'; +import { + getNormalizedEnrollByDate, getNormalizedStartDate, hasCourseStarted, SHORT_MONTH_DATE_FORMAT, +} from '../data'; const messages = defineMessages({ importantDates: { @@ -47,12 +49,11 @@ AssignmentModalImportantDate.propTypes = { const AssignmentModalImportantDates = ({ courseRun }) => { const intl = useIntl(); - const enrollByDate = courseRun.enrollBy - ? dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT) - : null; - const courseStartDate = courseRun.start - ? setStaleCourseStartDates({ start: courseRun.start }) + const normalizedEnrollByDate = getNormalizedEnrollByDate(courseRun); + const enrollByDate = normalizedEnrollByDate + ? dayjs(normalizedEnrollByDate).format(SHORT_MONTH_DATE_FORMAT) : null; + const courseStartDate = getNormalizedStartDate(courseRun); const courseHasStartedLabel = hasCourseStarted(courseStartDate) ? intl.formatMessage(messages.courseStarted) : intl.formatMessage(messages.courseStarts); diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx index fe81b96fca..6904e9ea6e 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalButton.jsx @@ -19,7 +19,7 @@ import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import AssignmentModalContent from './AssignmentModalContent'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; import { - getAssignableCourseRuns, + getAssignableCourseRuns, LEARNER_CREDIT_ROUTE, learnerCreditManagementQueryKeys, useBudgetId, useSubsidyAccessPolicy, @@ -27,7 +27,6 @@ import { import CreateAllocationErrorAlertModals from './CreateAllocationErrorAlertModals'; import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; import EVENT_NAMES from '../../../eventTracking'; -import { LEARNER_CREDIT_ROUTE } from '../constants'; import NewAssignmentModalDropdown from './NewAssignmentModalDropdown'; const useAllocateContentAssignments = () => useMutation({ diff --git a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx index d5bc4015dd..eeaf485433 100644 --- a/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx +++ b/src/components/learner-credit-management/assignment-modal/NewAssignmentModalDropdown.jsx @@ -4,7 +4,8 @@ import PropTypes from 'prop-types'; import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import { useState } from 'react'; import { - setStaleCourseStartDates, + getNormalizedEnrollByDate, + getNormalizedStartDate, SHORT_MONTH_DATE_FORMAT, } from '../data'; @@ -32,6 +33,7 @@ const NewAssignmentModalDropdown = ({ } return 'text-muted'; }; + const startLabel = ({ start }) => (dayjs(start).isBefore(dayjs()) ? 'Started' : 'Starts'); return ( @@ -50,9 +52,9 @@ const NewAssignmentModalDropdown = ({ onMouseUp={() => setClickedDropdownItem(null)} > - Enroll by {dayjs(courseRun.enrollBy).format(SHORT_MONTH_DATE_FORMAT)} + Enroll by {dayjs(getNormalizedEnrollByDate(courseRun)).format(SHORT_MONTH_DATE_FORMAT)} - Starts {dayjs(setStaleCourseStartDates({ start: courseRun.start })).format(SHORT_MONTH_DATE_FORMAT)} + {startLabel(courseRun)} {dayjs(getNormalizedStartDate(courseRun)).format(SHORT_MONTH_DATE_FORMAT)} diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx index 5234940924..96781822f1 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx @@ -3,8 +3,8 @@ import { AppContext } from '@edx/frontend-platform/react'; import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png'; import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; -import CARD_TEXT from '../../constants'; import { + CARD_TEXT, getAssignableCourseRuns, EXEC_ED_COURSE_TYPE, formatDate, diff --git a/src/components/learner-credit-management/constants.js b/src/components/learner-credit-management/constants.js deleted file mode 100644 index a344b21fff..0000000000 --- a/src/components/learner-credit-management/constants.js +++ /dev/null @@ -1,20 +0,0 @@ -const CARD_TEXT = { - BADGE: { - course: 'Course', - execEd: 'Executive Education', - }, - BUTTON_ACTION: { - viewCourse: 'View course', - assign: 'Assign', - }, - ENROLLMENT: { - text: 'Learner must enroll by', - }, - PRICE: { - subText: 'Per learner price', - }, -}; - -export const LEARNER_CREDIT_ROUTE = '/:enterpriseSlug/admin/:enterpriseAppPage/:budgetId/:activeTabKey'; - -export default CARD_TEXT; diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index e6b192df81..fcd51b3b65 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -22,6 +22,12 @@ export const API_FIELDS_BY_TABLE_COLUMN_ACCESSOR = { courseListPrice: 'course_list_price', }; +// Course pace text +export const COURSE_PACING_MAP = { + SELF_PACED: 'self_paced', + INSTRUCTOR_PACED: 'instructor_paced', +}; + // Percentage where messaging (e.g., Alert) on low remaining balance will begin appearing export const LOW_REMAINING_BALANCE_PERCENT_THRESHOLD = 0.75; @@ -46,6 +52,24 @@ export const BUDGET_DETAIL_TAB_LABELS = { [BUDGET_DETAIL_MEMBERS_TAB]: 'Members', }; +// Card text for used in useCourseCardMetadata +export const CARD_TEXT = { + BADGE: { + course: 'Course', + execEd: 'Executive Education', + }, + BUTTON_ACTION: { + viewCourse: 'View course', + assign: 'Assign', + }, + ENROLLMENT: { + text: 'Learner must enroll by', + }, + PRICE: { + subText: 'Per learner price', + }, +}; + // Facet filters export const LEARNING_TYPE_REFINEMENT = 'learning_type'; export const LANGUAGE_REFINEMENT = 'language'; @@ -93,3 +117,6 @@ export const learnerCreditManagementQueryKeys = { budgetGroupLearners: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'group learners'], enterpriseCustomer: (enterpriseId) => [...learnerCreditManagementQueryKeys.all, 'enterpriseCustomer', enterpriseId], }; + +// Route to learner credit +export const LEARNER_CREDIT_ROUTE = '/:enterpriseSlug/admin/:enterpriseAppPage/:budgetId/:activeTabKey'; diff --git a/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js b/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js index cc98165e99..c9a709729b 100644 --- a/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js +++ b/src/components/learner-credit-management/data/hooks/usePathToCatalogTab.js @@ -1,7 +1,8 @@ import { useParams, generatePath } from 'react-router-dom'; import useBudgetId from './useBudgetId'; -import { LEARNER_CREDIT_ROUTE } from '../../constants'; + +import { LEARNER_CREDIT_ROUTE } from '../constants'; const usePathToCatalogTab = () => { const { budgetId } = useBudgetId(); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 3cfcc29bef..4ae83076dd 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -8,7 +8,7 @@ import { NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, ASSIGNMENT_ENROLLMENT_DEADLINE, DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION, - START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, + START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, COURSE_PACING_MAP, } from './constants'; import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; @@ -556,15 +556,13 @@ export const isLmsBudget = ( /** * Determines if the course has already started. Mostly used around text formatting for tense - * Introduces 'jitter' in the form of a 30 second offset to take into account any additional - * formatting that takes place down stream related to setting values to today's date through dayjs() * * This should also help with reducing 'flaky' tests. * * @param start * @returns {boolean} */ -export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs().subtract(30, 'seconds')); +export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs()); /** * Returns assignable course runs within the threshold of within the subsidies expiration date @@ -590,6 +588,16 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime return assignableCourseRuns; }; +export const isCourseSelfPaced = ({ pacingType }) => pacingType === COURSE_PACING_MAP.SELF_PACED; + +export const hasTimeToComplete = ({ end, weeksToComplete }) => { + const today = dayjs(); + const differenceInWeeks = dayjs(end).diff(today, 'week'); + return weeksToComplete <= differenceInWeeks; +}; + +const isWithinMinimumStartDateThreshold = ({ start }) => dayjs(start).isBefore(dayjs().subtract(START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, 'days')); + /** * If the start date of the course is before today offset by the START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS * then return today's formatted date. Otherwise, pass-through the start date in ISO format. @@ -600,12 +608,31 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime * @param format * @returns {string} */ -export const setStaleCourseStartDates = ({ start }) => { +export const getNormalizedStartDate = ({ + start, pacingType, end, weeksToComplete, +}) => { + const todayToIso = dayjs().toISOString(); if (!start) { - return dayjs().toISOString(); + return todayToIso; + } + const startDateIso = dayjs(start).toISOString(); + if (isCourseSelfPaced({ pacingType })) { + if (hasTimeToComplete({ end, weeksToComplete }) || isWithinMinimumStartDateThreshold({ start })) { + // always today's date (incentives enrollment) + return todayToIso; + } + return startDateIso; + } + return startDateIso; +}; + +export const getNormalizedEnrollByDate = ({ enrollBy }) => { + if (!enrollBy) { + return null; } - if (dayjs(start).isBefore(dayjs().subtract(START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, 'days'))) { - return dayjs().toISOString(); + const ninetyDayAllocationOffset = dayjs().add(DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION, 'days'); + if (dayjs(enrollBy).isAfter(ninetyDayAllocationOffset)) { + return ninetyDayAllocationOffset.toISOString(); } - return dayjs(start).toISOString(); + return enrollBy; };