feat: add progress tracking for course completion metrics

- Introduced a new module for progress tracking in the LMS service.
- Implemented `calculate_course_completion` function to compute total lessons, completed lessons, and progress percentage for a user in a specific course.
- Updated the main.rs file to include the new progress tracking module.
- Enhanced the Excel import functionality in the Question Bank to support various question types and improved error handling.
- Added a new dependency on the `xlsx` library for handling Excel files in the frontend.
- Modified the course settings page to include a branded certificate template with additional organization details.
- Updated the package.json and package-lock.json files to include the new `xlsx` dependency.
- Changed the default state for ingestRag in the Admin Shared Materials page to true.
This commit is contained in:
2026-04-22 10:08:27 -04:00
parent 1c67d0dac2
commit 77eceee2f3
13 changed files with 703 additions and 238 deletions
@@ -0,0 +1,73 @@
use sqlx::{PgPool, Row};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct CourseCompletionMetrics {
pub total_lessons: i64,
pub completed_lessons: i64,
pub progress_percentage: f64,
pub completed: bool,
}
pub async fn calculate_course_completion(
pool: &PgPool,
user_id: Uuid,
course_id: Uuid,
) -> Result<CourseCompletionMetrics, sqlx::Error> {
let row = sqlx::query(
r#"
SELECT
COALESCE(totals.total_lessons, 0)::bigint AS total_lessons,
LEAST(
COALESCE(totals.total_lessons, 0)::bigint,
COALESCE(graded.graded_done, 0)::bigint + COALESCE(ungraded.ungraded_done, 0)::bigint
) AS completed_lessons
FROM (SELECT 1) seed
LEFT JOIN LATERAL (
SELECT COUNT(*)::bigint AS total_lessons
FROM lessons l
JOIN modules m ON m.id = l.module_id
WHERE m.course_id = $2
) totals ON TRUE
LEFT JOIN LATERAL (
SELECT COUNT(DISTINCT ug.lesson_id)::bigint AS graded_done
FROM user_grades ug
JOIN lessons l ON l.id = ug.lesson_id
JOIN modules m ON m.id = l.module_id
WHERE ug.user_id = $1
AND m.course_id = $2
AND l.is_graded = true
AND (ug.score * 100.0) >= COALESCE(l.passing_percentage::double precision, 60.0)
) graded ON TRUE
LEFT JOIN LATERAL (
SELECT COUNT(DISTINCT li.lesson_id)::bigint AS ungraded_done
FROM lesson_interactions li
JOIN lessons l ON l.id = li.lesson_id
JOIN modules m ON m.id = l.module_id
WHERE li.user_id = $1
AND li.event_type = 'complete'
AND m.course_id = $2
AND l.is_graded = false
) ungraded ON TRUE
"#,
)
.bind(user_id)
.bind(course_id)
.fetch_one(pool)
.await?;
let total_lessons = row.get::<i64, _>("total_lessons");
let completed_lessons = row.get::<i64, _>("completed_lessons");
let progress_percentage = if total_lessons > 0 {
(completed_lessons as f64 / total_lessons as f64) * 100.0
} else {
0.0
};
Ok(CourseCompletionMetrics {
total_lessons,
completed_lessons,
progress_percentage,
completed: total_lessons > 0 && completed_lessons >= total_lessons,
})
}