diff --git a/src/main.rs b/src/main.rs index 1109418..b5c5e71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -916,10 +916,9 @@ async fn delete_match( .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - // Revert each player's rating - for (player_id, match_type, rating_before, _rating_after) in &participants { - let rating_col = if match_type == "doubles" { "doubles_rating" } else { "singles_rating" }; - sqlx::query(&format!("UPDATE players SET {} = ? WHERE id = ?", rating_col)) + // Revert each player's rating (UNIFIED: always use singles_rating) + for (player_id, _match_type, rating_before, _rating_after) in &participants { + sqlx::query("UPDATE players SET singles_rating = ? WHERE id = ?") .bind(rating_before) .bind(player_id) .execute(&state.pool) @@ -1298,7 +1297,8 @@ async fn create_match( .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create match: {}", e)))?; let is_doubles = match_data.match_type == "doubles"; - let rating_col = if is_doubles { "doubles" } else { "singles" }; + // UNIFIED RATING: Always use singles_rating as the unified rating column + let rating_col = "singles"; let mut team1_players = vec![match_data.team1_player1]; if is_doubles { if let Some(p2) = match_data.team1_player2 { team1_players.push(p2); } } @@ -1575,10 +1575,10 @@ async fn players_list_handler(State(state): State) -> Html { /// /// **Parameters:** None /// -/// **Returns:** HTML page with dual leaderboards +/// **Returns:** HTML page with unified leaderboard async fn leaderboard_handler(State(state): State) -> Html { - // Only include players who have played at least one match - let singles: Vec<(i64, String, f64)> = sqlx::query_as( + // UNIFIED RATING: Single leaderboard using singles_rating as unified rating + let players: Vec<(i64, String, f64)> = sqlx::query_as( r#"SELECT DISTINCT p.id, p.name, p.singles_rating FROM players p JOIN match_participants mp ON p.id = mp.player_id @@ -1588,27 +1588,7 @@ async fn leaderboard_handler(State(state): State) -> Html { .await .unwrap_or_default(); - let doubles: Vec<(i64, String, f64)> = sqlx::query_as( - r#"SELECT DISTINCT p.id, p.name, p.doubles_rating - FROM players p - JOIN match_participants mp ON p.id = mp.player_id - ORDER BY p.doubles_rating DESC LIMIT 10"# - ) - .fetch_all(&state.pool) - .await - .unwrap_or_default(); - - let singles_rows: String = singles.iter().enumerate() - .map(|(i, (id, name, rating))| { - let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" }; - format!(r#"
-
{}{}. {}
- {:.0} -
"#, medal, i + 1, id, name, rating) - }) - .collect(); - - let doubles_rows: String = doubles.iter().enumerate() + let player_rows: String = players.iter().enumerate() .map(|(i, (id, name, rating))| { let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" }; format!(r#"
@@ -1627,9 +1607,7 @@ async fn leaderboard_handler(State(state): State) -> Html { Leaderboard - Pickleball ELO @@ -1637,20 +1615,17 @@ async fn leaderboard_handler(State(state): State) -> Html {

🏓 Leaderboard

{} -
-
-

📊 Top Singles

-
{}
-
-
-

📊 Top Doubles

-
{}
-
-
+

📊 Top Players (Unified ELO)

+
{}
+ +

+ Unified rating: singles and doubles matches both contribute to one rating.
+ Learn how ratings work → +

- "#, COMMON_CSS, nav_html(), singles_rows, doubles_rows); + "#, COMMON_CSS, nav_html(), player_rows); Html(html) } @@ -1659,15 +1634,15 @@ async fn leaderboard_handler(State(state): State) -> Html { /// /// **Endpoint:** `GET /api/leaderboard` /// -/// **Description:** Provides leaderboard data as JSON for external applications, including top 10 in singles and doubles with ratings and uncertainty (RD). +/// **Description:** Provides unified leaderboard data as JSON for external applications. /// /// **Parameters:** None /// -/// **Returns:** JSON object with `singles` and `doubles` arrays, each containing rank, name, rating, and RD +/// **Returns:** JSON object with `players` array containing rank, name, and rating async fn api_leaderboard_handler(State(state): State) -> axum::Json { - // Only include players who have played at least one match - let singles: Vec<(String, f64, f64)> = sqlx::query_as( - r#"SELECT DISTINCT p.name, p.singles_rating, p.singles_rd + // UNIFIED RATING: Single leaderboard + let players: Vec<(String, f64)> = sqlx::query_as( + r#"SELECT DISTINCT p.name, p.singles_rating FROM players p JOIN match_participants mp ON p.id = mp.player_id ORDER BY p.singles_rating DESC LIMIT 10"# @@ -1676,22 +1651,9 @@ async fn api_leaderboard_handler(State(state): State) -> axum::Json = sqlx::query_as( - r#"SELECT DISTINCT p.name, p.doubles_rating, p.doubles_rd - FROM players p - JOIN match_participants mp ON p.id = mp.player_id - ORDER BY p.doubles_rating DESC LIMIT 10"# - ) - .fetch_all(&state.pool) - .await - .unwrap_or_default(); - axum::Json(serde_json::json!({ - "singles": singles.iter().enumerate().map(|(i, (n, r, rd))| { - serde_json::json!({"rank": i+1, "name": n, "rating": *r as i32, "rd": *rd as i32}) - }).collect::>(), - "doubles": doubles.iter().enumerate().map(|(i, (n, r, rd))| { - serde_json::json!({"rank": i+1, "name": n, "rating": *r as i32, "rd": *rd as i32}) + "players": players.iter().enumerate().map(|(i, (n, r))| { + serde_json::json!({"rank": i+1, "name": n, "rating": *r as i32}) }).collect::>(), })) }