Skip to content

Commit 6fde50c

Browse files
authored
Merge pull request #48 from open-craft/agrendalath/bb-9331-optional-completion
feat: display optional completion information on the cards
2 parents e1523c5 + c342389 commit 6fde50c

File tree

7 files changed

+51
-37
lines changed

7 files changed

+51
-37
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@open-craft/frontend-app-learning-paths",
3-
"version": "0.1.5",
3+
"version": "0.1.6",
44
"description": "Frontend application template",
55
"repository": {
66
"type": "git",

src/learningpath/CourseCard.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export const CourseCard = ({
3131
endDate,
3232
status,
3333
percent,
34+
hasOptionalCompletion,
35+
hasUnearnedOptionalCompletion,
3436
checkingEnrollment,
3537
} = course;
3638

@@ -135,9 +137,14 @@ export const CourseCard = ({
135137
label={`${progressBarPercent}%`}
136138
variant="primary"
137139
/>
138-
<div className="x-small text-right pt-1">content completed</div>
140+
<div className="x-small text-right pt-1">
141+
{hasOptionalCompletion ? 'Required content completed' : 'content completed'}
142+
</div>
139143
</>
140144
)}
145+
{status.toLowerCase() === 'completed' && hasUnearnedOptionalCompletion && (
146+
<div className="small">You&apos;ve completed all required content. Remaining content is optional.</div>
147+
)}
141148
</Card.Section>
142149
<Card.Footer orientation="horizontal" className="pt-3 pb-3 justify-content-between">
143150
<Col className="d-flex p-0 flex-column-reverse flex-md-row align-items-start w-100 w-md-auto">
@@ -195,6 +202,8 @@ CourseCard.propTypes = {
195202
endDate: PropTypes.string,
196203
status: PropTypes.string.isRequired,
197204
percent: PropTypes.number.isRequired,
205+
hasOptionalCompletion: PropTypes.bool,
206+
hasUnearnedOptionalCompletion: PropTypes.bool,
198207
checkingEnrollment: PropTypes.bool,
199208
}).isRequired,
200209
relatedLearningPaths: PropTypes.arrayOf(PropTypes.shape({

src/learningpath/LearningPathCard.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const LearningPathCard = ({ learningPath, showFilters = false }) => {
2929
minDate,
3030
maxDate,
3131
percent,
32+
hasOptionalCompletion,
33+
hasUnearnedOptionalCompletion,
3234
org,
3335
} = learningPath;
3436

@@ -123,9 +125,14 @@ const LearningPathCard = ({ learningPath, showFilters = false }) => {
123125
label={`${progressBarPercent}%`}
124126
variant="primary"
125127
/>
126-
<div className="x-small text-right pt-1">content completed</div>
128+
<div className="x-small text-right pt-1">
129+
{hasOptionalCompletion ? 'Required content completed' : 'content completed'}
130+
</div>
127131
</>
128132
)}
133+
{status.toLowerCase() === 'completed' && hasUnearnedOptionalCompletion && (
134+
<div className="small">You&apos;ve completed all required content. Remaining content is optional.</div>
135+
)}
129136
</Card.Section>
130137
<Card.Footer orientation="horizontal" className="pt-3 pb-3 justify-content-between">
131138
<Card.Section className="d-flex p-0 flex-column-reverse flex-md-row align-items-start w-100 w-md-auto">
@@ -159,6 +166,8 @@ LearningPathCard.propTypes = {
159166
minDate: PropTypes.instanceOf(Date),
160167
maxDate: PropTypes.instanceOf(Date),
161168
percent: PropTypes.number,
169+
hasOptionalCompletion: PropTypes.bool,
170+
hasUnearnedOptionalCompletion: PropTypes.bool,
162171
org: PropTypes.string,
163172
}).isRequired,
164173
showFilters: PropTypes.bool,

src/learningpath/data/api.js

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,26 +69,12 @@ export async function fetchCourseDetails(courseId) {
6969
});
7070
}
7171

72-
export async function fetchCourseCompletion(courseId) {
73-
try {
74-
const { username } = getAuthenticatedUser();
75-
const client = getAuthenticatedHttpClient();
76-
const response = await client.get(
77-
`${getConfig().LMS_BASE_URL}/completion-aggregator/v1/course/${encodeURIComponent(courseId)}/?username=${username}`,
78-
);
79-
return response.data.results?.[0]?.completion?.percent ?? 0.0;
80-
} catch (error) {
81-
// Handle API errors - they indicate the user is not enrolled or did not complete any XBlocks.
82-
return 0.0;
83-
}
84-
}
85-
8672
export async function fetchAllCourseCompletions() {
8773
const { username } = getAuthenticatedUser();
8874
const client = getAuthenticatedHttpClient();
8975

9076
let allResults = [];
91-
let nextUrl = `${getConfig().LMS_BASE_URL}/completion-aggregator/v1/course/?username=${username}&page_size=10000`;
77+
let nextUrl = `${getConfig().LMS_BASE_URL}/completion-aggregator/v1/course/?username=${username}&page_size=10000&include_optional=true`;
9278

9379
while (nextUrl) {
9480
// eslint-disable-next-line no-await-in-loop
@@ -103,6 +89,7 @@ export async function fetchAllCourseCompletions() {
10389
return camelCaseObject(allResults.map(item => ({
10490
course_key: item.course_key,
10591
completion: item.completion,
92+
optional_completion: item.optional_completion,
10693
})));
10794
}
10895

src/learningpath/data/dataUtils.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ export const calculateCompletionStatus = (completion) => {
2222
* @returns {Object} Course object with completion status
2323
*/
2424
export const addCompletionStatus = (course, completionsMap, courseId) => {
25-
const completion = completionsMap[courseId]?.percent || 0;
25+
const completionData = completionsMap[courseId];
26+
const completion = completionData?.completion?.percent || 0;
2627
const { status, percent } = calculateCompletionStatus(completion);
28+
const optionalPossible = completionData?.optionalCompletion?.possible ?? 0;
29+
const optionalEarned = completionData?.optionalCompletion?.earned ?? 0;
30+
const hasOptionalCompletion = optionalPossible > 0;
31+
const hasUnearnedOptionalCompletion = optionalPossible > optionalEarned;
2732
return {
2833
...course,
2934
status,
3035
percent,
36+
hasOptionalCompletion,
37+
hasUnearnedOptionalCompletion,
3138
};
3239
};
3340

@@ -39,7 +46,10 @@ export const addCompletionStatus = (course, completionsMap, courseId) => {
3946
export const createCompletionsMap = (completions) => {
4047
const completionsMap = {};
4148
completions?.forEach?.(item => {
42-
completionsMap[item.courseKey] = item.completion;
49+
completionsMap[item.courseKey] = {
50+
completion: item.completion,
51+
optionalCompletion: item.optionalCompletion,
52+
};
4353
});
4454
return completionsMap;
4555
};

src/learningpath/data/queries.js

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export const QUERY_KEYS = {
1717
LEARNER_DASHBOARD: ['learnerDashboard'],
1818
COURSE_DETAILS: (courseId) => ['course', courseId],
1919
COURSE_COMPLETIONS: ['courseCompletions'],
20-
COURSE_COMPLETION: (courseId) => ['courseCompletion', courseId],
2120
COURSE_ENROLLMENT_STATUS: (courseId) => ['courseEnrollmentStatus', courseId],
2221
ORGANIZATIONS: ['organizations'],
2322
CREDENTIAL_CONFIGURATION: (learningContextKey) => ['credentialConfiguration', learningContextKey],
@@ -70,9 +69,19 @@ export const useLearningPaths = () => {
7069
};
7170
}
7271

72+
let hasOptionalCompletion = false;
73+
let hasUnearnedOptionalCompletion = false;
7374
const totalCompletion = lp.steps.reduce((sum, step) => {
74-
const completion = completionsMap[step.courseKey];
75-
return sum + (completion?.percent ?? 0);
75+
const completionData = completionsMap[step.courseKey];
76+
const optionalPossible = completionData?.optionalCompletion?.possible ?? 0;
77+
const optionalEarned = completionData?.optionalCompletion?.earned ?? 0;
78+
if (optionalPossible > 0) {
79+
hasOptionalCompletion = true;
80+
if (optionalPossible > optionalEarned) {
81+
hasUnearnedOptionalCompletion = true;
82+
}
83+
}
84+
return sum + (completionData?.completion?.percent ?? 0);
7685
}, 0);
7786

7887
const percent = totalCompletion / totalCourses;
@@ -104,6 +113,8 @@ export const useLearningPaths = () => {
104113
minDate,
105114
maxDate,
106115
percent,
116+
hasOptionalCompletion,
117+
hasUnearnedOptionalCompletion,
107118
type: 'learning_path',
108119
org: lp.key.match(/path-v1:([^+]+)/)[1],
109120
enrollmentDate: lp.enrollmentDate ? new Date(lp.enrollmentDate) : null,
@@ -215,12 +226,6 @@ export const useLearnerDashboard = () => {
215226
});
216227
};
217228

218-
export const useCourseCompletions = () => useQuery({
219-
queryKey: QUERY_KEYS.COURSE_COMPLETIONS,
220-
queryFn: api.fetchAllCourseCompletions,
221-
staleTime: STALE_TIMES.COMPLETIONS,
222-
});
223-
224229
export const useCoursesByIds = (courseIds) => {
225230
const queryClient = useQueryClient();
226231

@@ -314,12 +319,6 @@ export const usePrefetchCourseDetail = (courseId) => {
314319
staleTime: STALE_TIMES.COURSE_DETAIL,
315320
});
316321

317-
queryClient.fetchQuery({
318-
queryKey: QUERY_KEYS.COURSE_COMPLETION(courseId),
319-
queryFn: () => api.fetchCourseCompletion(courseId),
320-
staleTime: STALE_TIMES.COMPLETIONS,
321-
});
322-
323322
queryClient.prefetchQuery({
324323
queryKey: QUERY_KEYS.CREDENTIAL_CONFIGURATION(courseId),
325324
queryFn: () => api.fetchCredentialConfiguration(courseId),

0 commit comments

Comments
 (0)