Skip to content

Commit

Permalink
feat: assignment dashboard card messaging updates and alert on expiri…
Browse files Browse the repository at this point in the history
…ng assignments (#1105)
  • Loading branch information
adamstankiewicz authored Jun 24, 2024
1 parent 036eca6 commit 4bffff7
Show file tree
Hide file tree
Showing 21 changed files with 784 additions and 381 deletions.
2 changes: 2 additions & 0 deletions src/components/app/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ export const COURSE_MODES_MAP = {
HONOR: 'honor',
PAID_EXECUTIVE_EDUCATION: 'paid-executive-education',
};

export const ENROLL_BY_DATE_WARNING_THRESHOLD_DAYS = 10;
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ describe('useEnterpriseCourseEnrollments', () => {
enrollBy: mockContentAssignment.earliestPossibleExpiration.date,
isCanceledAssignment: false,
isExpiredAssignment: false,
isExpiringAssignment: false,
assignmentConfiguration: mockContentAssignment.assignmentConfiguration,
uuid: mockContentAssignment.uuid,
learnerAcknowledged: mockContentAssignment.learnerAcknowledged,
Expand Down
8 changes: 6 additions & 2 deletions src/components/app/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { logError } from '@edx/frontend-platform/logging';

import { ASSIGNMENT_TYPES, POLICY_TYPES } from '../../enterprise-user-subsidy/enterprise-offers/data/constants';
import { LICENSE_STATUS } from '../../enterprise-user-subsidy/data/constants';
import { getBrandColorsFromCSSVariables } from '../../../utils/common';
import { getBrandColorsFromCSSVariables, isTodayWithinDateThreshold } from '../../../utils/common';
import { COURSE_STATUSES, SUBSIDY_TYPE } from '../../../constants';
import { LATE_ENROLLMENTS_BUFFER_DAYS } from '../../../config/constants';
import { COURSE_AVAILABILITY_MAP, COURSE_MODES_MAP } from './constants';
import { COURSE_AVAILABILITY_MAP, COURSE_MODES_MAP, ENROLL_BY_DATE_WARNING_THRESHOLD_DAYS } from './constants';
import { features } from '../../../config';

/**
Expand Down Expand Up @@ -379,6 +379,10 @@ export const transformLearnerContentAssignment = (learnerContentAssignment, ente
enrollBy: assignmentEnrollByDeadline,
isCanceledAssignment,
isExpiredAssignment,
isExpiringAssignment: isTodayWithinDateThreshold({
date: assignmentEnrollByDeadline,
days: ENROLL_BY_DATE_WARNING_THRESHOLD_DAYS,
}),
assignmentConfiguration: learnerContentAssignment.assignmentConfiguration,
uuid: learnerContentAssignment.uuid,
learnerAcknowledged: learnerContentAssignment.learnerAcknowledged,
Expand Down
5 changes: 0 additions & 5 deletions src/components/dashboard/DashboardPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,6 @@ const DashboardPage = () => {
onClose={handleSubscriptionLicenseActivationAlertClose}
className="mt-3"
dismissible
closeLabel={intl.formatMessage({
id: 'enterprise.dashboard.course.assignment.alert.dismiss.button.label',
defaultMessage: 'Dismiss',
description: 'Dismiss button label for the course assignment alert',
})}
>
<FormattedMessage
id="enterprise.dashboard.tab.courses.license.activated"
Expand Down
2 changes: 2 additions & 0 deletions src/components/dashboard/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ export const BUDGET_STATUSES = {

export const EXPIRED_SUBSCRIPTION_MODAL_LOCALSTORAGE_KEY = ({ uuid }) => (`hasSeenSubscriptionLicenseExpiredModal-${uuid}`);
export const EXPIRING_SUBSCRIPTION_MODAL_LOCALSTORAGE_KEY = ({ uuid, threshold }) => (`${SEEN_SUBSCRIPTION_EXPIRATION_MODAL_COOKIE_PREFIX}${threshold}-${uuid}`);

export const ASSIGNMENTS_EXPIRING_WARNING_LOCALSTORAGE_KEY = 'enterprise.learner-portal.assignment-expiration-alert.dismissed.assignment.uuids';
32 changes: 31 additions & 1 deletion src/components/dashboard/data/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BUDGET_STATUSES } from './constants';
import { ASSIGNMENTS_EXPIRING_WARNING_LOCALSTORAGE_KEY, BUDGET_STATUSES } from './constants';

/**
* Determines whether there are any unacknowledged assignments.
Expand All @@ -10,6 +10,36 @@ export function getHasUnacknowledgedAssignments(assignments) {
return assignments.some((assignment) => !assignment.learnerAcknowledged);
}

export function getExpiringAssignmentsAcknowledgementState(assignments) {
const alreadyAcknowledgedExpiringAssignments = JSON.parse(
global.localStorage.getItem(ASSIGNMENTS_EXPIRING_WARNING_LOCALSTORAGE_KEY),
) || [];

const expiringAssignments = [];
const unacknowledgedExpiringAssignments = [];
const acknowledgedExpiringAssignments = [];

assignments.forEach((assignment) => {
if (!assignment.isExpiringAssignment) {
return;
}
expiringAssignments.push(assignment);
if (alreadyAcknowledgedExpiringAssignments.includes(assignment.uuid)) {
acknowledgedExpiringAssignments.push(assignment);
} else {
unacknowledgedExpiringAssignments.push(assignment);
}
});

return {
expiringAssignments,
unacknowledgedExpiringAssignments,
hasUnacknowledgedExpiringAssignments: unacknowledgedExpiringAssignments.length > 0,
acknowledgedExpiringAssignments,
hasAcknowledgedExpiringAssignments: acknowledgedExpiringAssignments.length > 0,
};
}

// Utility function to check the budget status
export const getStatusMetadata = ({
isPlanApproachingExpiry,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,106 @@
import PropTypes from 'prop-types';
import { Alert, Button, MailtoLink } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import { Info, Warning } from '@openedx/paragon/icons';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';

import { getContactEmail } from '../../../../utils/common';
import { ASSIGNMENT_TYPES } from '../../../enterprise-user-subsidy/enterprise-offers/data/constants';
import { useEnterpriseCustomer } from '../../../app/data';

const alertMessagingByVariant = {
[ASSIGNMENT_TYPES.CANCELED]: {
heading: (
<FormattedMessage
id="enterprise.dashboard.course.assignment.canceled.alert.heading"
defaultMessage="Course assignment canceled"
description="Heading for the alert that appears when a course assignment is canceled."
/>
),
text: (
<FormattedMessage
id="enterprise.dashboard.course.assignment.canceled.alert.text"
defaultMessage="Your learning administrator canceled one or more course assignments below."
description="Text for the alert that appears when a course assignment is canceled."
/>
),
variant: 'danger',
hasContactAdministrator: true,
isDismissable: true,
icon: Info,
},
[ASSIGNMENT_TYPES.EXPIRED]: {
heading: (
<FormattedMessage
id="enterprise.dashboard.course.assignment.deadline.passed.alert.heading"
defaultMessage="Deadline passed"
description="Heading for the alert that appears when a course assignment deadline has passed."
/>
),
text: (
<FormattedMessage
id="enterprise.dashboard.course.assignment.deadline.passed.alert.text"
defaultMessage="Deadline to enroll into one or more courses below has passed."
description="Text for the alert that appears when a course assignment deadline has passed."
/>
),
variant: 'danger',
hasContactAdministrator: true,
isDismissable: true,
icon: Info,
},
[ASSIGNMENT_TYPES.EXPIRING]: {
heading: (
<FormattedMessage
id="enterprise.dashboard.course.assignment.expiring.alert.heading"
defaultMessage="Enrollment deadlines approaching"
description="Heading for the alert that appears when a course assignment is expiring soon."
/>
),
text: (
<FormattedMessage
id="enterprise.dashboard.course.assignment.expiring.alert.text"
defaultMessage="One or more of your assigned courses is approaching their deadline to enroll. Please enroll before the course is removed."
description="Text for the alert that appears when a course assignment is expiring soon."
/>
),
variant: 'warning',
hasContactAdministrator: false,
isDismissable: true,
icon: Warning,
},
};

const CourseAssignmentAlert = ({
showAlert,
onClose,
variant,
isAcknowledgingAssignments,
}) => {
const intl = useIntl();
const heading = variant === ASSIGNMENT_TYPES.CANCELED ? (
<FormattedMessage
id="enterprise.dashboard.course.assignment.cancelled.alert.heading"
defaultMessage="Course assignment canceled"
description="Heading for the alert that appears when a course assignment is canceled."
/>
) : (
<FormattedMessage
id="enterprise.dashboard.course.assignment.deadline.passed.alert.heading"
defaultMessage="Deadline passed"
description="Heading for the alert that appears when a course assignment deadline has passed."
/>
);

const text = variant === ASSIGNMENT_TYPES.CANCELED ? (
<FormattedMessage
id="enterprise.dashboard.course.assignment.cancelled.alert.text"
defaultMessage="Your learning administrator canceled one or more course assignments below."
description="Text for the alert that appears when a course assignment is canceled."
/>
) : (
<FormattedMessage
id="enterprise.dashboard.course.assignment.deadline.passed.alert.text"
defaultMessage="Deadline to enroll into one or more courses below has passed."
description="Text for the alert that appears when a course assignment deadline has passed."
/>
);

const { data: enterpriseCustomer } = useEnterpriseCustomer();
const adminEmail = getContactEmail(enterpriseCustomer);
const alertMessaging = alertMessagingByVariant[variant];
if (!alertMessaging) {
return null;
}

const alertActions = alertMessaging.hasContactAdministrator ? [
<Button as={MailtoLink} className="text-nowrap" to={adminEmail}>
<FormattedMessage
id="enterprise.dashboard.course.assignment.alert.contact.admin.button"
defaultMessage="Contact administrator"
description="Contact adminstrator button label for the course enrollemnt assignment alert"
/>
</Button>,
] : [];

return (
<Alert
variant="danger"
variant={alertMessaging.variant}
show={showAlert}
icon={Info}
dismissible
actions={[
<Button as={MailtoLink} className="text-nowrap" to={adminEmail}>
<FormattedMessage
id="enterprise.dashboard.course.assignment.alert.contact.admin.button"
defaultMessage="Contact administrator"
description="Conatact adminstrator button label for the course enrollemnt assignment alert"
/>
</Button>,
]}
icon={alertMessaging.icon}
dismissible={alertMessaging.isDismissable}
actions={alertActions}
onClose={onClose}
closeLabel={isAcknowledgingAssignments
? intl.formatMessage({
Expand All @@ -73,15 +114,15 @@ const CourseAssignmentAlert = ({
description: 'Dismiss button label for the course assignment alert',
})}
>
<Alert.Heading>{heading}</Alert.Heading>
<p>{text}</p>
<Alert.Heading>{alertMessaging.heading}</Alert.Heading>
<p>{alertMessaging.text}</p>
</Alert>
);
};

CourseAssignmentAlert.propTypes = {
onClose: PropTypes.func,
variant: PropTypes.oneOf([ASSIGNMENT_TYPES.CANCELED, ASSIGNMENT_TYPES.EXPIRED]),
variant: PropTypes.oneOf([ASSIGNMENT_TYPES.CANCELED, ASSIGNMENT_TYPES.EXPIRED, ASSIGNMENT_TYPES.EXPIRING]),
showAlert: PropTypes.bool,
isAcknowledgingAssignments: PropTypes.bool,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ const CourseEnrollments = ({ children }) => {
assignments,
showCanceledAssignmentsAlert,
showExpiredAssignmentsAlert,
showExpiringAssignmentsAlert,
handleAcknowledgeExpiringAssignments,
handleAcknowledgeAssignments,
isAcknowledgingAssignments,
} = useContentAssignments(allEnrollmentsByStatus.assigned);
Expand Down Expand Up @@ -110,6 +112,24 @@ const CourseEnrollments = ({ children }) => {
enterpriseCustomer={enterpriseCustomer}
/>
)}
{shouldShowMarkSavedForLaterCourseSuccess && (
<CourseEnrollmentsAlert variant="success" onClose={() => setShouldShowMarkSavedForLaterCourseSuccess(false)}>
<FormattedMessage
id="enterprise.dashboard.course.enrollment.saved.for.later.alert.text"
defaultMessage="Your course was saved for later."
description="Message when a course is saved for later."
/>
</CourseEnrollmentsAlert>
)}
{shouldShowMoveToInProgressCourseSuccess && (
<CourseEnrollmentsAlert variant="success" onClose={() => setShouldShowMoveToInProgressCourseSuccess(false)}>
<FormattedMessage
id="enterprise.dashboard.course.enrollment.moved.to.progress.alert.text"
defaultMessage="Your course was moved to In Progress."
description="Message when a course is moved to In Progress."
/>
</CourseEnrollmentsAlert>
)}
{features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && (
<>
<CourseAssignmentAlert
Expand All @@ -128,24 +148,11 @@ const CourseEnrollments = ({ children }) => {
})}
isAcknowledgingAssignments={isAcknowledgingAssignments}
/>
{shouldShowMarkSavedForLaterCourseSuccess && (
<CourseEnrollmentsAlert variant="success" onClose={() => setShouldShowMarkSavedForLaterCourseSuccess(false)}>
<FormattedMessage
id="enterprise.dashboard.course.enrollment.saved.for.later.alert.text"
defaultMessage="Your course was saved for later."
description="Message when a course is saved for later."
/>
</CourseEnrollmentsAlert>
)}
{shouldShowMoveToInProgressCourseSuccess && (
<CourseEnrollmentsAlert variant="success" onClose={() => setShouldShowMoveToInProgressCourseSuccess(false)}>
<FormattedMessage
id="enterprise.dashboard.course.enrollment.moved.to.progress.alert.text"
defaultMessage="Your course was moved to In Progress."
description="Message when a course is moved to In Progress."
/>
</CourseEnrollmentsAlert>
)}
<CourseAssignmentAlert
showAlert={showExpiringAssignmentsAlert}
variant={ASSIGNMENT_TYPES.EXPIRING}
onClose={handleAcknowledgeExpiringAssignments}
/>
<CourseSection
title={isFirstVisit ? intl.formatMessage({
id: 'enterprise.dashboard.course.enrollments.assigned.section.title.for.first.visit',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import { Button } from '@openedx/paragon';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

import BaseCourseCard from './BaseCourseCard';
import { COURSE_STATUSES } from '../data';
Expand Down Expand Up @@ -29,7 +30,11 @@ const AssignedCourseCard = (props) => {
// background) should be using the brand variant.
variant="inverse-brand"
>
Enroll
<FormattedMessage
id="enterprise.learner-portal.dashboard.courses.assignments.assignment.go-to-enrollment"
defaultMessage="Go to enrollment"
description="Button text for assigned course card to go to course about page to continue with enrollment"
/>
</Button>
);

Expand All @@ -39,6 +44,7 @@ const AssignedCourseCard = (props) => {
type={COURSE_STATUSES.assigned}
hasViewCertificateLink={false}
canUnenroll={false}
externalCourseLink={false}
{...props}
/>
);
Expand Down
Loading

0 comments on commit 4bffff7

Please sign in to comment.