mirror of
https://github.com/Xevion/banner.git
synced 2026-01-31 12:23:33 -06:00
feat: implement interactive data table with sorting and column control
Replaces static course table with TanStack Table featuring sortable columns, column visibility management, and server-side sort handling. Adds reusable data-table primitives adapted for Svelte 5 runes.
This commit is contained in:
+46
-24
@@ -21,10 +21,15 @@ pub async fn search_courses(
|
||||
campus: Option<&str>,
|
||||
limit: i32,
|
||||
offset: i32,
|
||||
order_by: &str,
|
||||
) -> Result<(Vec<Course>, i64)> {
|
||||
// Build WHERE clauses dynamically via parameter binding + COALESCE trick:
|
||||
// each optional filter uses ($N IS NULL OR column = $N) so NULL means "no filter".
|
||||
let courses = sqlx::query_as::<_, Course>(
|
||||
//
|
||||
// ORDER BY is interpolated as a string since column names can't be bound as
|
||||
// parameters. The caller must provide a safe, pre-validated clause (see
|
||||
// `sort_clause` in routes.rs).
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT *
|
||||
FROM courses
|
||||
@@ -36,22 +41,24 @@ pub async fn search_courses(
|
||||
AND ($6::bool = false OR max_enrollment > enrollment)
|
||||
AND ($7::text IS NULL OR instructional_method = $7)
|
||||
AND ($8::text IS NULL OR campus = $8)
|
||||
ORDER BY subject, course_number, sequence_number
|
||||
ORDER BY {order_by}
|
||||
LIMIT $9 OFFSET $10
|
||||
"#,
|
||||
)
|
||||
.bind(term_code)
|
||||
.bind(subject)
|
||||
.bind(title_query)
|
||||
.bind(course_number_low)
|
||||
.bind(course_number_high)
|
||||
.bind(open_only)
|
||||
.bind(instructional_method)
|
||||
.bind(campus)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(db_pool)
|
||||
.await?;
|
||||
"#
|
||||
);
|
||||
|
||||
let courses = sqlx::query_as::<_, Course>(&query)
|
||||
.bind(term_code)
|
||||
.bind(subject)
|
||||
.bind(title_query)
|
||||
.bind(course_number_low)
|
||||
.bind(course_number_high)
|
||||
.bind(open_only)
|
||||
.bind(instructional_method)
|
||||
.bind(campus)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(db_pool)
|
||||
.await?;
|
||||
|
||||
let total: (i64,) = sqlx::query_as(
|
||||
r#"
|
||||
@@ -102,10 +109,25 @@ pub async fn get_course_by_crn(
|
||||
pub async fn get_course_instructors(
|
||||
db_pool: &PgPool,
|
||||
course_id: i32,
|
||||
) -> Result<Vec<(String, String, Option<String>, bool, Option<f32>, Option<i32>)>> {
|
||||
let rows: Vec<(String, String, Option<String>, bool, Option<f32>, Option<i32>)> =
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
) -> Result<
|
||||
Vec<(
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
bool,
|
||||
Option<f32>,
|
||||
Option<i32>,
|
||||
)>,
|
||||
> {
|
||||
let rows: Vec<(
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
bool,
|
||||
Option<f32>,
|
||||
Option<i32>,
|
||||
)> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT i.banner_id, i.display_name, i.email, ci.is_primary,
|
||||
rp.avg_rating, rp.num_ratings
|
||||
FROM course_instructors ci
|
||||
@@ -114,10 +136,10 @@ pub async fn get_course_instructors(
|
||||
WHERE ci.course_id = $1
|
||||
ORDER BY ci.is_primary DESC, i.display_name
|
||||
"#,
|
||||
)
|
||||
.bind(course_id)
|
||||
.fetch_all(db_pool)
|
||||
.await?;
|
||||
)
|
||||
.bind(course_id)
|
||||
.fetch_all(db_pool)
|
||||
.await?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
|
||||
+15
-15
@@ -28,21 +28,22 @@ pub async fn batch_upsert_rmp_professors(
|
||||
|
||||
let legacy_ids: Vec<i32> = deduped.iter().map(|p| p.legacy_id).collect();
|
||||
let graphql_ids: Vec<&str> = deduped.iter().map(|p| p.graphql_id.as_str()).collect();
|
||||
let first_names: Vec<String> = deduped.iter().map(|p| p.first_name.trim().to_string()).collect();
|
||||
let first_name_refs: Vec<&str> = first_names.iter().map(|s| s.as_str()).collect();
|
||||
let last_names: Vec<String> = deduped.iter().map(|p| p.last_name.trim().to_string()).collect();
|
||||
let last_name_refs: Vec<&str> = last_names.iter().map(|s| s.as_str()).collect();
|
||||
let departments: Vec<Option<&str>> = deduped
|
||||
let first_names: Vec<String> = deduped
|
||||
.iter()
|
||||
.map(|p| p.department.as_deref())
|
||||
.map(|p| p.first_name.trim().to_string())
|
||||
.collect();
|
||||
let first_name_refs: Vec<&str> = first_names.iter().map(|s| s.as_str()).collect();
|
||||
let last_names: Vec<String> = deduped
|
||||
.iter()
|
||||
.map(|p| p.last_name.trim().to_string())
|
||||
.collect();
|
||||
let last_name_refs: Vec<&str> = last_names.iter().map(|s| s.as_str()).collect();
|
||||
let departments: Vec<Option<&str>> = deduped.iter().map(|p| p.department.as_deref()).collect();
|
||||
let avg_ratings: Vec<Option<f32>> = deduped.iter().map(|p| p.avg_rating).collect();
|
||||
let avg_difficulties: Vec<Option<f32>> = deduped.iter().map(|p| p.avg_difficulty).collect();
|
||||
let num_ratings: Vec<i32> = deduped.iter().map(|p| p.num_ratings).collect();
|
||||
let would_take_again_pcts: Vec<Option<f32>> = deduped
|
||||
.iter()
|
||||
.map(|p| p.would_take_again_pct)
|
||||
.collect();
|
||||
let would_take_again_pcts: Vec<Option<f32>> =
|
||||
deduped.iter().map(|p| p.would_take_again_pct).collect();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -129,11 +130,10 @@ pub async fn auto_match_instructors(db_pool: &PgPool) -> Result<u64> {
|
||||
}
|
||||
|
||||
// Load all RMP professors
|
||||
let professors: Vec<(i32, String, String)> = sqlx::query_as(
|
||||
"SELECT legacy_id, first_name, last_name FROM rmp_professors",
|
||||
)
|
||||
.fetch_all(db_pool)
|
||||
.await?;
|
||||
let professors: Vec<(i32, String, String)> =
|
||||
sqlx::query_as("SELECT legacy_id, first_name, last_name FROM rmp_professors")
|
||||
.fetch_all(db_pool)
|
||||
.await?;
|
||||
|
||||
// Build a lookup: (normalized_last, normalized_first) -> list of legacy_ids
|
||||
let mut rmp_index: HashMap<(String, String), Vec<i32>> = HashMap::new();
|
||||
|
||||
Reference in New Issue
Block a user