Skip to content

Commit 3fa18a6

Browse files
authored
fix dashboard card enrollment association and display (#2792)
* fix run selection logic * fix loading order / skeletons on org dashboard * fix tests * fix display issue with course start countdown * treat a valid certificate as the equivalent of completed status * Remove extraneous comments added by Copilot * rename variable * simplify "only contract scoped runs" test, add another test that ensures correct run assignment
1 parent ec3bab8 commit 3fa18a6

File tree

9 files changed

+331
-119
lines changed

9 files changed

+331
-119
lines changed

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ describe.each([
558558
status: EnrollmentStatus.Completed,
559559
mode: EnrollmentMode.Verified,
560560
grades: [],
561+
run: mitxonline.factories.courses.courseRun(),
561562
}
562563
renderWithProviders(
563564
<DashboardCard
@@ -631,6 +632,7 @@ describe.each([
631632
status: status,
632633
mode: EnrollmentMode.Audit,
633634
grades: [],
635+
run: mitxonline.factories.courses.courseRun(),
634636
}
635637
renderWithProviders(
636638
<DashboardCard titleAction="marketing" dashboardResource={course} />,

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,8 @@ const UpgradeBanner: React.FC<
449449
}
450450

451451
const CountdownRoot = styled.div(({ theme }) => ({
452-
width: "142px",
453-
marginRight: "32px",
452+
width: "100%",
453+
paddingRight: "32px",
454454
display: "flex",
455455
justifyContent: "center",
456456
alignSelf: "end",
@@ -539,6 +539,10 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
539539
const run = isCourse ? dashboardResource.run : undefined
540540
const coursewareId = isCourse ? dashboardResource.coursewareId : null
541541
const readableId = isCourse ? dashboardResource.readableId : null
542+
const hasValidCertificate = isCourse ? !!run?.certificate?.link : false
543+
const enrollmentStatus = hasValidCertificate
544+
? EnrollmentStatus.Completed
545+
: enrollment?.status
542546

543547
// Title link logic
544548
const coursewareUrl = run?.coursewareUrl
@@ -599,9 +603,7 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
599603
) : (
600604
<TitleText>{title}</TitleText>
601605
)}
602-
{isCourse &&
603-
enrollment?.status === EnrollmentStatus.Completed &&
604-
run?.certificate?.link ? (
606+
{isCourse && run?.certificate?.link ? (
605607
<SubtitleLink href={run.certificate.link}>
606608
<RiAwardLine size="16px" />
607609
View Certificate
@@ -625,15 +627,15 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
625627
) : isCourse ? (
626628
<>
627629
<EnrollmentStatusIndicator
628-
status={enrollment?.status}
630+
status={enrollmentStatus}
629631
showNotComplete={showNotComplete}
630632
/>
631633
<CoursewareButton
632634
data-testid="courseware-button"
633635
coursewareId={coursewareId}
634636
readableId={readableId}
635637
startDate={run?.startDate}
636-
enrollmentStatus={enrollment?.status}
638+
enrollmentStatus={enrollmentStatus}
637639
href={buttonHref ?? run?.coursewareUrl}
638640
endDate={run?.endDate}
639641
noun={noun}

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
mitxonlineCourse,
1818
mitxonlineProgram,
1919
programEnrollmentsToPrograms,
20+
selectBestEnrollment,
21+
transformEnrollmentToDashboard,
2022
userEnrollmentsToDashboardCourses,
2123
} from "./transform"
2224
import { DashboardCard } from "./DashboardCard"
@@ -249,10 +251,9 @@ interface ProgramEnrollmentDisplayProps {
249251
const ProgramEnrollmentDisplay: React.FC<ProgramEnrollmentDisplayProps> = ({
250252
programId,
251253
}) => {
252-
const { data: userCourses, isLoading: userEnrollmentsLoading } = useQuery({
253-
...enrollmentQueries.courseRunEnrollmentsList(),
254-
select: userEnrollmentsToDashboardCourses,
255-
})
254+
const { data: rawEnrollments, isLoading: userEnrollmentsLoading } = useQuery(
255+
enrollmentQueries.courseRunEnrollmentsList(),
256+
)
256257
const { data: rawProgram, isLoading: programLoading } = useQuery(
257258
programsQueries.programDetail({ id: programId }),
258259
)
@@ -277,6 +278,19 @@ const ProgramEnrollmentDisplay: React.FC<ProgramEnrollmentDisplayProps> = ({
277278
programEnrollmentsLoading ||
278279
programCoursesLoading
279280

281+
// Group enrollments by course ID for efficient lookup
282+
const enrollmentsByCourseId = (rawEnrollments || []).reduce(
283+
(acc, enrollment) => {
284+
const courseId = enrollment.run.course.id
285+
if (!acc[courseId]) {
286+
acc[courseId] = []
287+
}
288+
acc[courseId].push(enrollment)
289+
return acc
290+
},
291+
{} as Record<number, typeof rawEnrollments>,
292+
)
293+
280294
// Build sections from requirement tree
281295
const requirementSections =
282296
program?.reqTree
@@ -286,12 +300,30 @@ const ProgramEnrollmentDisplay: React.FC<ProgramEnrollmentDisplayProps> = ({
286300
const sectionCourses = (rawProgramCourses?.results || [])
287301
.filter((course) => courseIds.includes(course.id))
288302
.map((course) => {
289-
const enrollment = userCourses?.find((dashboardCourse) =>
290-
course.courseruns.some(
291-
(run) => run.courseware_id === dashboardCourse.coursewareId,
292-
),
293-
)?.enrollment
294-
return mitxonlineCourse(course, enrollment)
303+
// Find all enrollments for this course
304+
const courseEnrollments = enrollmentsByCourseId[course.id] || []
305+
306+
if (courseEnrollments.length === 0) {
307+
// No enrollment - use first run
308+
return mitxonlineCourse(course)
309+
}
310+
311+
// If multiple enrollments exist, select the best one
312+
const bestEnrollment =
313+
courseEnrollments.length > 1
314+
? selectBestEnrollment(courseEnrollments)
315+
: courseEnrollments[0]
316+
317+
// Find the matching run from course.courseruns
318+
const matchingRun = course.courseruns.find(
319+
(run) => run.id === bestEnrollment.run.id,
320+
)
321+
322+
return mitxonlineCourse(
323+
course,
324+
transformEnrollmentToDashboard(bestEnrollment),
325+
matchingRun,
326+
)
295327
})
296328

297329
return {

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const dashboardCourse: PartialFactory<DashboardCourse> = (...overrides) => {
5050
status: faker.helpers.arrayElement(Object.values(EnrollmentStatus)),
5151
mode: faker.helpers.arrayElement(Object.values(EnrollmentMode)),
5252
grades: [],
53+
run: factories.courses.courseRun(),
5354
},
5455
},
5556
...overrides,

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.test.tsx

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
7575
) ?? "",
7676
},
7777
grades: apiData.grades,
78+
run: apiData.run,
7879
},
7980
} satisfies DashboardResource)
8081
},
@@ -545,12 +546,13 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
545546
})
546547
})
547548

548-
test("selects enrollment with highest grade when multiple enrollments exist for same course", () => {
549+
test("selects enrollment with highest grade when multiple enrollments exist for same course in same contract", () => {
549550
const orgId = faker.number.int()
550551
const contracts = createTestContracts(orgId, 1)
551552
const contractId = contracts[0].id
552553

553554
// Create a course with 2 runs, both tied to the same contract
555+
// (e.g., pilot run and soft launch run both in same contract)
554556
const course = factories.courses.course({
555557
id: 123,
556558
title: "Test Course",
@@ -574,6 +576,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
574576
grades: [factories.enrollment.grade({ grade: 0.65, passed: true })],
575577
b2b_contract_id: contractId,
576578
b2b_organization_id: orgId,
579+
certificate: null,
577580
})
578581

579582
const enrollmentHighGrade = factories.enrollment.courseEnrollment({
@@ -584,6 +587,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
584587
grades: [factories.enrollment.grade({ grade: 0.95, passed: true })],
585588
b2b_contract_id: contractId,
586589
b2b_organization_id: orgId,
590+
certificate: null,
587591
})
588592

589593
const transformedCourses = organizationCoursesWithContracts({
@@ -601,6 +605,69 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
601605
expect(transformedCourse.enrollment?.grades[0].grade).toBe(0.95)
602606
expect(transformedCourse.enrollment?.id).toBe(enrollmentHighGrade.id)
603607
})
608+
609+
test("prioritizes enrollment with certificate over grade when multiple enrollments exist", () => {
610+
const orgId = faker.number.int()
611+
const contracts = createTestContracts(orgId, 1)
612+
const contractId = contracts[0].id
613+
614+
const course = factories.courses.course({
615+
id: 123,
616+
title: "Test Course",
617+
})
618+
const run1 = factories.courses.courseRun({
619+
id: 1,
620+
b2b_contract: contractId,
621+
})
622+
const run2 = factories.courses.courseRun({
623+
id: 2,
624+
b2b_contract: contractId,
625+
})
626+
course.courseruns = [run1, run2]
627+
628+
// Enrollment with higher grade but no certificate
629+
const enrollmentHighGradeNoCert = factories.enrollment.courseEnrollment({
630+
run: {
631+
id: run1.id,
632+
course: { id: course.id, title: course.title },
633+
},
634+
grades: [factories.enrollment.grade({ grade: 0.95, passed: true })],
635+
b2b_contract_id: contractId,
636+
b2b_organization_id: orgId,
637+
certificate: null,
638+
})
639+
640+
// Enrollment with lower/no grade but has certificate
641+
const enrollmentWithCert = factories.enrollment.courseEnrollment({
642+
run: {
643+
id: run2.id,
644+
course: { id: course.id, title: course.title },
645+
},
646+
grades: [],
647+
b2b_contract_id: contractId,
648+
b2b_organization_id: orgId,
649+
certificate: {
650+
uuid: "test-cert-uuid",
651+
link: "/certificate/test-link/",
652+
},
653+
})
654+
655+
const transformedCourses = organizationCoursesWithContracts({
656+
courses: [course],
657+
contracts,
658+
enrollments: [enrollmentHighGradeNoCert, enrollmentWithCert],
659+
})
660+
661+
expect(transformedCourses).toHaveLength(1)
662+
const transformedCourse = transformedCourses[0]
663+
664+
// Should select the enrollment with the certificate, even though it has no grade
665+
expect(transformedCourse.enrollment).toBeDefined()
666+
expect(transformedCourse.enrollment?.id).toBe(enrollmentWithCert.id)
667+
expect(transformedCourse.enrollment?.certificate?.uuid).toBe(
668+
"test-cert-uuid",
669+
)
670+
})
604671
})
605672

606673
describe("transformEnrollmentToDashboard", () => {
@@ -629,6 +696,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
629696
link: "/certificate/course/test123/",
630697
},
631698
grades: enrollment.grades,
699+
run: enrollment.run,
632700
})
633701
})
634702

0 commit comments

Comments
 (0)