UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

405 lines 19 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { RegisterClass } from '@memberjunction/global'; import { LearnWorldsBaseAction } from '../learnworlds-base.action.js'; import { BaseAction } from '@memberjunction/actions'; /** * Action to retrieve detailed progress information for a LearnWorlds user */ let GetLearnWorldsUserProgressAction = class GetLearnWorldsUserProgressAction extends LearnWorldsBaseAction { /** * Description of the action */ get Description() { return 'Retrieves comprehensive learning progress for a user in LearnWorlds, including course completion, time spent, and detailed unit/lesson progress'; } /** * Typed public method for direct (non-framework) callers. * Sets company context, fetches progress, and returns a strongly-typed result. * Throws on error. */ async GetUserProgress(params, contextUser) { this.SetCompanyContext(params.CompanyID); const { UserID: userId, CourseID: courseId, IncludeUnitDetails: includeUnitDetails = false, IncludeLessonDetails: includeLessonDetails = false } = params; if (!userId) { throw new Error('UserID parameter is required'); } this.validatePathSegment(userId, 'UserID'); if (courseId) { this.validatePathSegment(courseId, 'CourseID'); } let userProgress; if (courseId) { const courseProgress = await this.getCourseProgress(userId, courseId, includeUnitDetails, includeLessonDetails, contextUser); userProgress = { userId, userEmail: '', totalCourses: 1, coursesCompleted: courseProgress.status === 'completed' ? 1 : 0, coursesInProgress: courseProgress.status === 'in_progress' ? 1 : 0, coursesNotStarted: courseProgress.status === 'not_started' ? 1 : 0, overallProgressPercentage: courseProgress.progressPercentage, totalTimeSpent: courseProgress.totalTimeSpent, totalCertificatesEarned: courseProgress.certificateEarned ? 1 : 0, courses: [courseProgress], }; } else { userProgress = await this.getAllCoursesProgress(userId, includeUnitDetails, includeLessonDetails, contextUser); } // Try to get user email try { const userInfo = await this.makeLearnWorldsRequest(`users/${userId}`, 'GET', undefined, contextUser); userProgress.userEmail = userInfo.email || ''; } catch (error) { console.warn(`Failed to fetch user info for userId ${userId}:`, error instanceof Error ? error.message : error); } const analytics = this.calculateLearningAnalytics(userProgress); Object.assign(userProgress, analytics); const summary = this.createProgressSummary(userProgress); return { UserProgress: userProgress, Summary: summary, }; } /** * Framework entry-point – thin wrapper around the typed public method. */ async InternalRunAction(params) { const { Params, ContextUser } = params; this.params = Params; try { const typedParams = this.extractGetUserProgressParams(Params); const result = await this.GetUserProgress(typedParams, ContextUser); this.setOutputParam(Params, 'UserProgress', result.UserProgress); this.setOutputParam(Params, 'Summary', result.Summary); const courseId = typedParams.CourseID; const message = courseId ? `Successfully retrieved progress for course ${courseId}` : `Successfully retrieved progress for ${result.UserProgress.totalCourses} courses`; return this.buildSuccessResult(message, Params); } catch (error) { const msg = error instanceof Error ? error.message : 'Unknown error occurred'; return this.buildErrorResult('ERROR', msg, Params); } } /** * Extract typed params from framework ActionParam[] */ extractGetUserProgressParams(params) { return { CompanyID: this.getRequiredStringParam(params, 'CompanyID'), UserID: this.getRequiredStringParam(params, 'UserID'), CourseID: this.getOptionalStringParam(params, 'CourseID'), IncludeUnitDetails: this.getOptionalBooleanParam(params, 'IncludeUnitDetails', false), IncludeLessonDetails: this.getOptionalBooleanParam(params, 'IncludeLessonDetails', false), }; } /** * Get progress for a specific course */ async getCourseProgress(userId, courseId, includeUnits, includeLessons, contextUser) { const progressResponse = await this.makeLearnWorldsRequest(`users/${userId}/courses/${courseId}/progress`, 'GET', undefined, contextUser); const progress = this.mapCourseProgress(progressResponse); if (includeUnits) { try { const unitsResponse = await this.makeLearnWorldsRequest(`users/${userId}/courses/${courseId}/units`, 'GET', undefined, contextUser); if (unitsResponse.data) { progress.unitProgress = await Promise.all(unitsResponse.data.map((unit) => this.mapUnitProgress(unit, includeLessons))); } } catch (error) { console.warn(`Units endpoint unavailable for user ${userId}, course ${courseId}:`, error instanceof Error ? error.message : error); } } return progress; } /** * Get progress for all user courses */ async getAllCoursesProgress(userId, includeUnits, includeLessons, contextUser) { const enrollmentsData = await this.makeLearnWorldsPaginatedRequest(`users/${userId}/courses`, {}, contextUser); const courseProgressResults = await this.processInBatches(enrollmentsData, (enrollment) => this.getCourseProgress(userId, enrollment.course_id || enrollment.course?.id || '', includeUnits, includeLessons, contextUser).catch(() => null)); const validCourses = courseProgressResults.filter((p) => p !== null); return this.aggregateCourseProgress(userId, validCourses); } /** * Aggregate individual course progress into overall user learning progress */ aggregateCourseProgress(userId, validCourses) { const completed = validCourses.filter((c) => c.status === 'completed').length; const inProgress = validCourses.filter((c) => c.status === 'in_progress').length; const notStarted = validCourses.filter((c) => c.status === 'not_started').length; const totalTime = validCourses.reduce((sum, c) => sum + c.totalTimeSpent, 0); const totalCerts = validCourses.filter((c) => c.certificateEarned).length; let overallProgress = 0; if (validCourses.length > 0) { const totalProgress = validCourses.reduce((sum, c) => sum + c.progressPercentage, 0); overallProgress = Math.round(totalProgress / validCourses.length); } const coursesWithQuizzes = validCourses.filter((c) => c.quizScoreAverage !== undefined); const avgQuizScore = coursesWithQuizzes.length > 0 ? coursesWithQuizzes.reduce((sum, c) => sum + (c.quizScoreAverage || 0), 0) / coursesWithQuizzes.length : undefined; return { userId, userEmail: '', totalCourses: validCourses.length, coursesCompleted: completed, coursesInProgress: inProgress, coursesNotStarted: notStarted, overallProgressPercentage: overallProgress, totalTimeSpent: totalTime, totalCertificatesEarned: totalCerts, averageQuizScore: avgQuizScore, courses: validCourses, }; } /** * Map course progress data from API response */ mapCourseProgress(data) { const progressData = { percentage: data.progress?.percentage ?? data.percentage, completed_units: data.progress?.completed_units ?? data.completed_units, total_units: data.progress?.total_units ?? data.total_units, time_spent: data.progress?.time_spent ?? data.time_spent, }; const progress = this.calculateProgress(progressData); const status = this.determineCourseStatus(data); return { courseId: data.course_id || data.course?.id || '', courseTitle: data.course_title || data.course?.title || 'Unknown Course', enrollmentId: data.enrollment_id || data.id || '', enrolledAt: this.parseLearnWorldsDate(data.enrolled_at || data.created || ''), lastAccessedAt: data.last_accessed_at ? this.parseLearnWorldsDate(data.last_accessed_at) : undefined, progressPercentage: progress.percentage, completedUnits: progress.completedUnits, totalUnits: progress.totalUnits, completedLessons: data.completed_lessons || 0, totalLessons: data.total_lessons || 0, totalTimeSpent: progress.timeSpent, averageSessionTime: data.average_session_time || 0, estimatedTimeToComplete: this.calculateEstimatedTimeToComplete(data), quizScoreAverage: data.quiz_score_average, assignmentsCompleted: data.assignments_completed, assignmentsTotal: data.assignments_total, currentGrade: data.current_grade || data.grade, status, completedAt: data.completed_at ? this.parseLearnWorldsDate(data.completed_at) : undefined, expiresAt: data.expires_at ? this.parseLearnWorldsDate(data.expires_at) : undefined, certificateEarned: data.certificate_earned || false, certificateUrl: data.certificate_url, }; } /** * Map unit progress data from API response */ async mapUnitProgress(unitData, includeLessons) { const unitProgress = { unitId: unitData.id || unitData.unit_id || '', unitTitle: unitData.title || unitData.name || '', unitType: unitData.type || 'section', order: unitData.order || unitData.position || 0, progressPercentage: unitData.progress_percentage || 0, completedLessons: unitData.completed_lessons || 0, totalLessons: unitData.total_lessons || 0, timeSpent: unitData.time_spent || 0, status: this.determineUnitStatus(unitData), startedAt: unitData.started_at ? this.parseLearnWorldsDate(unitData.started_at) : undefined, completedAt: unitData.completed_at ? this.parseLearnWorldsDate(unitData.completed_at) : undefined, }; if (includeLessons && unitData.lessons) { unitProgress.lessons = unitData.lessons.map((lesson) => this.mapLessonProgress(lesson)); } return unitProgress; } /** * Map lesson progress data from API response */ mapLessonProgress(lessonData) { return { lessonId: lessonData.id || lessonData.lesson_id || '', lessonTitle: lessonData.title || lessonData.name || '', lessonType: this.mapLessonType(lessonData.type || ''), order: lessonData.order || lessonData.position || 0, completed: lessonData.completed || false, progressPercentage: lessonData.progress_percentage || (lessonData.completed ? 100 : 0), timeSpent: lessonData.time_spent || 0, startedAt: lessonData.started_at ? this.parseLearnWorldsDate(lessonData.started_at) : undefined, completedAt: lessonData.completed_at ? this.parseLearnWorldsDate(lessonData.completed_at) : undefined, lastAccessedAt: lessonData.last_accessed_at ? this.parseLearnWorldsDate(lessonData.last_accessed_at) : undefined, videoWatchTime: lessonData.video_watch_time, videoTotalTime: lessonData.video_total_time, quizScore: lessonData.quiz_score, quizMaxScore: lessonData.quiz_max_score, quizAttempts: lessonData.quiz_attempts, assignmentSubmitted: lessonData.assignment_submitted, assignmentGrade: lessonData.assignment_grade, }; } /** * Determine course status from data */ determineCourseStatus(data) { if (data.expired || (data.expires_at && new Date(data.expires_at) < new Date())) { return 'expired'; } if (data.completed || data.progress_percentage === 100) { return 'completed'; } if ((data.progress_percentage != null && data.progress_percentage > 0) || data.started) { return 'in_progress'; } return 'not_started'; } /** * Determine unit status */ determineUnitStatus(data) { if (data.completed || data.progress_percentage === 100) { return 'completed'; } if ((data.progress_percentage != null && data.progress_percentage > 0) || data.started) { return 'in_progress'; } return 'not_started'; } /** * Map lesson type string to typed union */ mapLessonType(type) { const typeMap = { video: 'video', text: 'text', quiz: 'quiz', exam: 'quiz', assignment: 'assignment', scorm: 'scorm', interactive: 'interactive', multimedia: 'interactive', }; return typeMap[type?.toLowerCase()] || 'text'; } /** * Calculate estimated time to complete */ calculateEstimatedTimeToComplete(data) { if (data.estimated_time_to_complete) { return data.estimated_time_to_complete; } if (data.total_time_spent && data.progress_percentage != null && data.progress_percentage > 0 && data.progress_percentage < 100) { const timePerPercent = data.total_time_spent / data.progress_percentage; const remainingPercent = 100 - data.progress_percentage; return Math.round(timePerPercent * remainingPercent); } return undefined; } /** * Calculate learning analytics and return the computed values (no mutation). */ calculateLearningAnalytics(progress) { if (progress.courses.length === 0) return {}; const lastDates = progress.courses.map((c) => c.lastAccessedAt).filter((d) => d !== undefined); if (lastDates.length === 0) return {}; const lastLearningDate = new Date(Math.max(...lastDates.map((d) => d.getTime()))); const daysSinceLastActivity = Math.floor((new Date().getTime() - lastLearningDate.getTime()) / (1000 * 60 * 60 * 24)); const learningStreak = daysSinceLastActivity <= 1 ? 1 : 0; return { lastLearningDate, learningStreak }; } /** * Create progress summary */ createProgressSummary(progress) { const activeCourses = progress.courses.filter((c) => c.status === 'in_progress'); const recentActivity = progress.courses .filter((c) => c.lastAccessedAt) .sort((a, b) => (b.lastAccessedAt?.getTime() || 0) - (a.lastAccessedAt?.getTime() || 0)) .slice(0, 5); return { overview: this.buildProgressOverview(progress), performance: this.buildPerformanceMetrics(progress), currentFocus: activeCourses.map((c) => ({ courseTitle: c.courseTitle, progress: `${c.progressPercentage}%`, timeSpent: this.formatDuration(c.totalTimeSpent), estimatedTimeToComplete: c.estimatedTimeToComplete ? this.formatDuration(c.estimatedTimeToComplete) : 'N/A', })), recentActivity: recentActivity.map((c) => ({ courseTitle: c.courseTitle, lastAccessed: c.lastAccessedAt, progress: `${c.progressPercentage}%`, })), achievements: this.buildAchievementsSummary(progress), }; } buildProgressOverview(progress) { return { totalCourses: progress.totalCourses, completedCourses: progress.coursesCompleted, inProgressCourses: progress.coursesInProgress, notStartedCourses: progress.coursesNotStarted, overallProgress: `${progress.overallProgressPercentage}%`, certificatesEarned: progress.totalCertificatesEarned, totalLearningTime: this.formatDuration(progress.totalTimeSpent), }; } buildPerformanceMetrics(progress) { return { averageQuizScore: progress.averageQuizScore ? `${Math.round(progress.averageQuizScore)}%` : 'N/A', averageCourseProgress: `${progress.overallProgressPercentage}%`, completionRate: progress.totalCourses > 0 ? `${Math.round((progress.coursesCompleted / progress.totalCourses) * 100)}%` : '0%', }; } buildAchievementsSummary(progress) { return { totalCertificates: progress.totalCertificatesEarned, coursesWithCertificates: progress.courses .filter((c) => c.certificateEarned) .map((c) => ({ courseTitle: c.courseTitle, completedAt: c.completedAt, })), }; } /** * Define the parameters for this action */ get Params() { const baseParams = this.getCommonLMSParams(); const specificParams = [ { Name: 'UserID', Type: 'Input', Value: null, }, { Name: 'CourseID', Type: 'Input', Value: null, }, { Name: 'IncludeUnitDetails', Type: 'Input', Value: false, }, { Name: 'IncludeLessonDetails', Type: 'Input', Value: false, }, ]; return [...baseParams, ...specificParams]; } }; GetLearnWorldsUserProgressAction = __decorate([ RegisterClass(BaseAction, 'GetLearnWorldsUserProgressAction') ], GetLearnWorldsUserProgressAction); export { GetLearnWorldsUserProgressAction }; //# sourceMappingURL=get-user-progress.action.js.map