feat: Add bulk student enrollment functionality to courses with new API endpoint and UI.

This commit is contained in:
2026-02-17 22:50:17 -03:00
parent f9e78a265a
commit 4c96d6b225
6 changed files with 281 additions and 7 deletions
+99
View File
@@ -15,6 +15,105 @@ use sqlx::{PgPool, Row};
use std::env;
use uuid::Uuid;
#[derive(Deserialize)]
pub struct BulkEnrollPayload {
pub course_id: Uuid,
pub emails: Vec<String>,
}
#[derive(Serialize)]
pub struct BulkEnrollResponse {
pub successful_emails: Vec<String>,
pub failed_emails: Vec<String>,
pub already_enrolled_emails: Vec<String>,
}
pub async fn bulk_enroll_users(
Org(org_ctx): Org,
claims: Claims,
State(pool): State<PgPool>,
Json(payload): Json<BulkEnrollPayload>,
) -> Result<Json<BulkEnrollResponse>, StatusCode> {
if claims.role != "admin" && claims.role != "instructor" {
return Err(StatusCode::FORBIDDEN);
}
let mut successful_emails = Vec::new();
let mut failed_emails = Vec::new();
let mut already_enrolled_emails = Vec::new();
for email in payload.emails {
// 1. Find user by email in the organization
let user: Option<User> = sqlx::query_as("SELECT * FROM users WHERE email = $1")
.bind(&email)
.fetch_optional(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match user {
Some(user) => {
// 2. Check if already enrolled
let is_enrolled: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM enrollments WHERE user_id = $1 AND course_id = $2)"
)
.bind(user.id)
.bind(payload.course_id)
.fetch_one(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if is_enrolled {
already_enrolled_emails.push(email);
continue;
}
// 3. Enroll (Admin enrollment ignores payment)
let mut tx = pool
.begin()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Set session context for audit
crate::db_util::set_session_context(
&mut tx,
Some(claims.sub),
Some(org_ctx.id),
None,
None,
Some("BULK_ENROLL".to_string()),
)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let res = sqlx::query("SELECT * FROM fn_enroll_student($1, $2, $3)")
.bind(org_ctx.id)
.bind(user.id)
.bind(payload.course_id)
.execute(&mut *tx)
.await;
if res.is_ok() {
tx.commit()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
successful_emails.push(email);
} else {
failed_emails.push(email);
}
}
None => {
failed_emails.push(email);
}
}
}
Ok(Json(BulkEnrollResponse {
successful_emails,
failed_emails,
already_enrolled_emails,
}))
}
pub async fn enroll_user(
Org(org_ctx): Org,
claims: Claims,
+1
View File
@@ -51,6 +51,7 @@ async fn main() {
let protected_routes = Router::new()
.route("/enroll", post(handlers::enroll_user))
.route("/bulk-enroll", post(handlers::bulk_enroll_users))
.route("/enrollments/{id}", get(handlers::get_user_enrollments))
.route(
"/payments/preference",