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),