diff --git a/package-lock.json b/package-lock.json
index a07c931..8cc24ce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@open-craft/frontend-app-learning-paths",
- "version": "0.1.5",
+ "version": "0.1.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@open-craft/frontend-app-learning-paths",
- "version": "0.1.5",
+ "version": "0.1.6",
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
diff --git a/package.json b/package.json
index ea98869..1fb472c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@open-craft/frontend-app-learning-paths",
- "version": "0.1.5",
+ "version": "0.1.6",
"description": "Frontend application template",
"repository": {
"type": "git",
diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx
index 186dfcb..6c9cf53 100644
--- a/src/learningpath/CourseCard.jsx
+++ b/src/learningpath/CourseCard.jsx
@@ -31,6 +31,8 @@ export const CourseCard = ({
endDate,
status,
percent,
+ hasOptionalCompletion,
+ hasUnearnedOptionalCompletion,
checkingEnrollment,
} = course;
@@ -135,9 +137,14 @@ export const CourseCard = ({
label={`${progressBarPercent}%`}
variant="primary"
/>
-
content completed
+
+ {hasOptionalCompletion ? 'Required content completed' : 'content completed'}
+
>
)}
+ {status.toLowerCase() === 'completed' && hasUnearnedOptionalCompletion && (
+ You've completed all required content. Remaining content is optional.
+ )}
@@ -195,6 +202,8 @@ CourseCard.propTypes = {
endDate: PropTypes.string,
status: PropTypes.string.isRequired,
percent: PropTypes.number.isRequired,
+ hasOptionalCompletion: PropTypes.bool,
+ hasUnearnedOptionalCompletion: PropTypes.bool,
checkingEnrollment: PropTypes.bool,
}).isRequired,
relatedLearningPaths: PropTypes.arrayOf(PropTypes.shape({
diff --git a/src/learningpath/LearningPathCard.jsx b/src/learningpath/LearningPathCard.jsx
index 90ea28f..7e530e2 100644
--- a/src/learningpath/LearningPathCard.jsx
+++ b/src/learningpath/LearningPathCard.jsx
@@ -29,6 +29,8 @@ const LearningPathCard = ({ learningPath, showFilters = false }) => {
minDate,
maxDate,
percent,
+ hasOptionalCompletion,
+ hasUnearnedOptionalCompletion,
org,
} = learningPath;
@@ -123,9 +125,14 @@ const LearningPathCard = ({ learningPath, showFilters = false }) => {
label={`${progressBarPercent}%`}
variant="primary"
/>
- content completed
+
+ {hasOptionalCompletion ? 'Required content completed' : 'content completed'}
+
>
)}
+ {status.toLowerCase() === 'completed' && hasUnearnedOptionalCompletion && (
+ You've completed all required content. Remaining content is optional.
+ )}
@@ -159,6 +166,8 @@ LearningPathCard.propTypes = {
minDate: PropTypes.instanceOf(Date),
maxDate: PropTypes.instanceOf(Date),
percent: PropTypes.number,
+ hasOptionalCompletion: PropTypes.bool,
+ hasUnearnedOptionalCompletion: PropTypes.bool,
org: PropTypes.string,
}).isRequired,
showFilters: PropTypes.bool,
diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js
index cb3c136..378826f 100644
--- a/src/learningpath/data/api.js
+++ b/src/learningpath/data/api.js
@@ -69,26 +69,12 @@ export async function fetchCourseDetails(courseId) {
});
}
-export async function fetchCourseCompletion(courseId) {
- try {
- const { username } = getAuthenticatedUser();
- const client = getAuthenticatedHttpClient();
- const response = await client.get(
- `${getConfig().LMS_BASE_URL}/completion-aggregator/v1/course/${encodeURIComponent(courseId)}/?username=${username}`,
- );
- return response.data.results?.[0]?.completion?.percent ?? 0.0;
- } catch (error) {
- // Handle API errors - they indicate the user is not enrolled or did not complete any XBlocks.
- return 0.0;
- }
-}
-
export async function fetchAllCourseCompletions() {
const { username } = getAuthenticatedUser();
const client = getAuthenticatedHttpClient();
let allResults = [];
- let nextUrl = `${getConfig().LMS_BASE_URL}/completion-aggregator/v1/course/?username=${username}&page_size=10000`;
+ let nextUrl = `${getConfig().LMS_BASE_URL}/completion-aggregator/v1/course/?username=${username}&page_size=10000&include_optional=true`;
while (nextUrl) {
// eslint-disable-next-line no-await-in-loop
@@ -103,6 +89,7 @@ export async function fetchAllCourseCompletions() {
return camelCaseObject(allResults.map(item => ({
course_key: item.course_key,
completion: item.completion,
+ optional_completion: item.optional_completion,
})));
}
diff --git a/src/learningpath/data/dataUtils.js b/src/learningpath/data/dataUtils.js
index 7dd6560..0c2a6ce 100644
--- a/src/learningpath/data/dataUtils.js
+++ b/src/learningpath/data/dataUtils.js
@@ -22,12 +22,19 @@ export const calculateCompletionStatus = (completion) => {
* @returns {Object} Course object with completion status
*/
export const addCompletionStatus = (course, completionsMap, courseId) => {
- const completion = completionsMap[courseId]?.percent || 0;
+ const completionData = completionsMap[courseId];
+ const completion = completionData?.completion?.percent || 0;
const { status, percent } = calculateCompletionStatus(completion);
+ const optionalPossible = completionData?.optionalCompletion?.possible ?? 0;
+ const optionalEarned = completionData?.optionalCompletion?.earned ?? 0;
+ const hasOptionalCompletion = optionalPossible > 0;
+ const hasUnearnedOptionalCompletion = optionalPossible > optionalEarned;
return {
...course,
status,
percent,
+ hasOptionalCompletion,
+ hasUnearnedOptionalCompletion,
};
};
@@ -39,7 +46,10 @@ export const addCompletionStatus = (course, completionsMap, courseId) => {
export const createCompletionsMap = (completions) => {
const completionsMap = {};
completions?.forEach?.(item => {
- completionsMap[item.courseKey] = item.completion;
+ completionsMap[item.courseKey] = {
+ completion: item.completion,
+ optionalCompletion: item.optionalCompletion,
+ };
});
return completionsMap;
};
diff --git a/src/learningpath/data/queries.js b/src/learningpath/data/queries.js
index 1d97aaf..a7b00e9 100644
--- a/src/learningpath/data/queries.js
+++ b/src/learningpath/data/queries.js
@@ -17,7 +17,6 @@ export const QUERY_KEYS = {
LEARNER_DASHBOARD: ['learnerDashboard'],
COURSE_DETAILS: (courseId) => ['course', courseId],
COURSE_COMPLETIONS: ['courseCompletions'],
- COURSE_COMPLETION: (courseId) => ['courseCompletion', courseId],
COURSE_ENROLLMENT_STATUS: (courseId) => ['courseEnrollmentStatus', courseId],
ORGANIZATIONS: ['organizations'],
CREDENTIAL_CONFIGURATION: (learningContextKey) => ['credentialConfiguration', learningContextKey],
@@ -70,9 +69,19 @@ export const useLearningPaths = () => {
};
}
+ let hasOptionalCompletion = false;
+ let hasUnearnedOptionalCompletion = false;
const totalCompletion = lp.steps.reduce((sum, step) => {
- const completion = completionsMap[step.courseKey];
- return sum + (completion?.percent ?? 0);
+ const completionData = completionsMap[step.courseKey];
+ const optionalPossible = completionData?.optionalCompletion?.possible ?? 0;
+ const optionalEarned = completionData?.optionalCompletion?.earned ?? 0;
+ if (optionalPossible > 0) {
+ hasOptionalCompletion = true;
+ if (optionalPossible > optionalEarned) {
+ hasUnearnedOptionalCompletion = true;
+ }
+ }
+ return sum + (completionData?.completion?.percent ?? 0);
}, 0);
const percent = totalCompletion / totalCourses;
@@ -104,6 +113,8 @@ export const useLearningPaths = () => {
minDate,
maxDate,
percent,
+ hasOptionalCompletion,
+ hasUnearnedOptionalCompletion,
type: 'learning_path',
org: lp.key.match(/path-v1:([^+]+)/)[1],
enrollmentDate: lp.enrollmentDate ? new Date(lp.enrollmentDate) : null,
@@ -215,12 +226,6 @@ export const useLearnerDashboard = () => {
});
};
-export const useCourseCompletions = () => useQuery({
- queryKey: QUERY_KEYS.COURSE_COMPLETIONS,
- queryFn: api.fetchAllCourseCompletions,
- staleTime: STALE_TIMES.COMPLETIONS,
-});
-
export const useCoursesByIds = (courseIds) => {
const queryClient = useQueryClient();
@@ -314,12 +319,6 @@ export const usePrefetchCourseDetail = (courseId) => {
staleTime: STALE_TIMES.COURSE_DETAIL,
});
- queryClient.fetchQuery({
- queryKey: QUERY_KEYS.COURSE_COMPLETION(courseId),
- queryFn: () => api.fetchCourseCompletion(courseId),
- staleTime: STALE_TIMES.COMPLETIONS,
- });
-
queryClient.prefetchQuery({
queryKey: QUERY_KEYS.CREDENTIAL_CONFIGURATION(courseId),
queryFn: () => api.fetchCredentialConfiguration(courseId),