From f1887e42b7fc39623b56184c17258ef9a3f578fa Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Mon, 16 Sep 2024 17:04:59 -0400 Subject: [PATCH] feat: default stale start dates to today (#1180) --- src/components/app/data/constants.js | 3 ++ .../hooks/useEnterpriseCourseEnrollments.js | 2 +- src/components/app/data/utils.js | 5 +- .../course-header/CourseImportantDates.jsx | 8 ++-- src/components/course/data/constants.js | 6 ++- src/components/course/data/utils.jsx | 46 ++++++++++++++++--- .../course-cards/BaseCourseCard.jsx | 12 ++++- .../course-enrollments/data/hooks.js | 1 - 8 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/components/app/data/constants.js b/src/components/app/data/constants.js index 61e83b43cf..65624c3e09 100644 --- a/src/components/app/data/constants.js +++ b/src/components/app/data/constants.js @@ -64,3 +64,6 @@ export const ASSIGNMENT_TYPES = { ERRORED: 'errored', EXPIRING: 'expiring', }; + +// When the start date is before this number of days before today, display the alternate start date (fixed to today). +export const START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS = 14; diff --git a/src/components/app/data/hooks/useEnterpriseCourseEnrollments.js b/src/components/app/data/hooks/useEnterpriseCourseEnrollments.js index 0f85f6c8e6..003268dce3 100644 --- a/src/components/app/data/hooks/useEnterpriseCourseEnrollments.js +++ b/src/components/app/data/hooks/useEnterpriseCourseEnrollments.js @@ -73,7 +73,7 @@ export default function useEnterpriseCourseEnrollments(queryOptions = {}) { }, enabled: isEnabled, }); - + // TODO: Talk about how we don't have access to weeksToComplete on the dashboard page. const allEnrollmentsByStatus = useMemo(() => transformAllEnrollmentsByStatus({ enterpriseCourseEnrollments, requests, diff --git a/src/components/app/data/utils.js b/src/components/app/data/utils.js index 837aa00098..b488e9c2f6 100644 --- a/src/components/app/data/utils.js +++ b/src/components/app/data/utils.js @@ -317,7 +317,6 @@ export const canUnenrollCourseEnrollment = (courseEnrollment) => { */ export const transformCourseEnrollment = (rawCourseEnrollment) => { const courseEnrollment = { ...rawCourseEnrollment }; - // Return the fields expected by the component(s) courseEnrollment.title = courseEnrollment.displayName; courseEnrollment.microMastersTitle = courseEnrollment.micromastersTitle; @@ -409,7 +408,6 @@ export const transformLearnerContentAssignment = (learnerContentAssignment, ente courseKey = parentContentKey; courseRunId = contentKey; } - return { linkToCourse: `/${enterpriseSlug}/course/${courseKey}`, courseRunId, @@ -510,7 +508,6 @@ export function getAvailableCourseRuns({ course, lateEnrollmentBufferDays }) { if (!course?.courseRuns) { return []; } - // These are the standard rules used for determining whether a run is "available". const standardAvailableCourseRunsFilter = (courseRun) => ( courseRun.isMarketable && !isArchived(courseRun) && courseRun.isEnrollable @@ -879,7 +876,7 @@ export function transformCourseMetadataByAllocatedCourseRunAssignments({ courseRuns: courseMetadata.courseRuns.filter( courseRun => allocatedCourseRunAssignmentKeys.includes(courseRun.key), ), - availableCourseRuns: courseMetadata.courseRuns.filter( + availableCourseRuns: courseMetadata.availableCourseRuns.filter( courseRun => allocatedCourseRunAssignmentKeys.includes(courseRun.key), ), }; diff --git a/src/components/course/course-header/CourseImportantDates.jsx b/src/components/course/course-header/CourseImportantDates.jsx index f598562931..ebdaa81eea 100644 --- a/src/components/course/course-header/CourseImportantDates.jsx +++ b/src/components/course/course-header/CourseImportantDates.jsx @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import { DATE_FORMAT, DATETIME_FORMAT, + getNormalizedStartDate, getSoonestEarliestPossibleExpirationData, hasCourseStarted, useIsCourseAssigned, @@ -88,11 +89,12 @@ const CourseImportantDates = () => { // Match soonest expiring assignment to the corresponding course start date from course metadata let soonestExpiringAllocatedAssignmentCourseStartDate = null; if (soonestExpiringAssignment) { - soonestExpiringAllocatedAssignmentCourseStartDate = courseMetadata.availableCourseRuns.find( + const soonestExpiringAllocatedAssignment = courseMetadata.availableCourseRuns.find( (courseRun) => courseRun.key === soonestExpiringAssignment?.contentKey, - )?.start; + ); + soonestExpiringAllocatedAssignmentCourseStartDate = soonestExpiringAllocatedAssignment + && getNormalizedStartDate(soonestExpiringAllocatedAssignment); } - // Parse logic of date existence and labels const enrollByDate = soonestExpirationDate ?? null; const courseStartDate = soonestExpiringAllocatedAssignmentCourseStartDate diff --git a/src/components/course/data/constants.js b/src/components/course/data/constants.js index 3663d9966e..eb02ded1da 100644 --- a/src/components/course/data/constants.js +++ b/src/components/course/data/constants.js @@ -1,8 +1,12 @@ import GetSmarterLogo from '../../../assets/icons/getsmarter-header-icon.svg'; +// The SELF and INSTRUCTOR values are keys/value pairs used specifically for pacing sourced from the +// enterprise_course_enrollments API. export const COURSE_PACING_MAP = { SELF_PACED: 'self_paced', INSTRUCTOR_PACED: 'instructor_paced', + INSTRUCTOR: 'instructor', + SELF: 'self', }; export const SUBSIDY_DISCOUNT_TYPE_MAP = { @@ -122,5 +126,5 @@ export const DISABLED_ENROLL_USER_MESSAGES = { /* eslint-enable max-len */ export const DATE_FORMAT = 'MMM D, YYYY'; -export const DATETIME_FORMAT = 'MMM D, YYYY h:mm, a'; +export const DATETIME_FORMAT = 'MMM D, YYYY h:mma'; export const ZERO_PRICE = 0.00; diff --git a/src/components/course/data/utils.jsx b/src/components/course/data/utils.jsx index a2864cc0af..5f4873d655 100644 --- a/src/components/course/data/utils.jsx +++ b/src/components/course/data/utils.jsx @@ -26,11 +26,16 @@ import { findHighestLevelEntitlementSku, findHighestLevelSkuByEntityModeType, isEnrollmentUpgradeable, + START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS, } from '../../app/data'; -export function hasCourseStarted(start) { - return dayjs(start).isBefore(dayjs()); -} +/** + * Determines if the course has already started. Mostly used around text formatting for tense + * + * @param start + * @returns {boolean} + */ +export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs()); export function findUserEnrollmentForCourseRun({ userEnrollments, key }) { return userEnrollments.find( @@ -59,13 +64,43 @@ export function hasTimeToComplete(courseRun) { } export function isCourseSelfPaced(pacingType) { - return pacingType === COURSE_PACING_MAP.SELF_PACED; + return [COURSE_PACING_MAP.SELF_PACED, COURSE_PACING_MAP.SELF].includes(pacingType); } export function isCourseInstructorPaced(pacingType) { - return pacingType === COURSE_PACING_MAP.INSTRUCTOR_PACED; + return [COURSE_PACING_MAP.INSTRUCTOR_PACED, COURSE_PACING_MAP.INSTRUCTOR].includes(pacingType); } +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. + * + * @param {String} start + * @param {String} pacingType + * @param {String} end + * @param {Number} weeksToComplete + * @returns {string} + */ +export const getNormalizedStartDate = ({ + start, pacingType, end, weeksToComplete, +}) => { + const todayToIso = dayjs().toISOString(); + if (!start) { + 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 function getDefaultProgram(programs = []) { if (programs.length === 0) { return undefined; @@ -932,7 +967,6 @@ export function getSoonestEarliestPossibleExpirationData({ if (dateFormat) { soonestExpirationDate = dayjs(soonestExpirationDate).format(dateFormat); } - return { soonestExpirationDate, soonestExpirationReason: sortedByExpirationDate[0].earliestPossibleExpiration.reason, diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/BaseCourseCard.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/BaseCourseCard.jsx index 874803e4fd..b9f96b9246 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/BaseCourseCard.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/BaseCourseCard.jsx @@ -33,6 +33,7 @@ import { useEnterpriseCustomer, } from '../../../../app/data'; import { isCourseEnded, isDefinedAndNotNull, isTodayWithinDateThreshold } from '../../../../../utils/common'; +import { getNormalizedStartDate } from '../../../../course/data'; const messages = defineMessages({ statusBadgeLabelInProgress: { @@ -401,8 +402,15 @@ const BaseCourseCard = ({ }; const renderStartDate = () => { - const formattedStartDate = startDate ? dayjs(startDate).format('MMMM Do, YYYY') : null; - const isCourseStarted = dayjs(startDate) <= dayjs(); + // TODO: Determine if its worth exposing weeks_to_complete in assignments to utilize this function effectively + const courseStartDate = getNormalizedStartDate({ + start: startDate, + pacingType: pacing, + end: endDate, + weeksToComplete: null, + }); + const formattedStartDate = dayjs(courseStartDate).format('MMMM Do, YYYY'); + const isCourseStarted = dayjs(courseStartDate).isBefore(dayjs()); if (formattedStartDate && !isCourseStarted) { return Starts {formattedStartDate}; } diff --git a/src/components/dashboard/main-content/course-enrollments/data/hooks.js b/src/components/dashboard/main-content/course-enrollments/data/hooks.js index 3a40078440..1aaa7eafe1 100644 --- a/src/components/dashboard/main-content/course-enrollments/data/hooks.js +++ b/src/components/dashboard/main-content/course-enrollments/data/hooks.js @@ -498,7 +498,6 @@ export function useCourseEnrollmentsBySection(courseEnrollmentsByStatus) { ]), [courseEnrollmentsByStatus], ); - const completedCourseEnrollments = useMemo( () => sortedEnrollmentsByEnrollmentDate(courseEnrollmentsByStatus.completed), [courseEnrollmentsByStatus.completed],