Add W-L records to player cards and leaderboard

This commit is contained in:
Split 2026-02-26 13:07:07 -05:00
parent 1b74470fcb
commit a1f96b9af4
7 changed files with 94 additions and 21 deletions

View File

@ -720,3 +720,4 @@ called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, mes
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Migration error: error returned from database: (code: 1) no such column: rating
Migration error: error returned from database: (code: 1) no such column: rating
Migration error: error returned from database: (code: 1) no such column: rating

View File

@ -1081,3 +1081,16 @@ Starting Pickleball ELO Tracker Server on port 3000...
Add Player: http://localhost:3000/players/new
🎾 Record Match: http://localhost:3000/matches/new
🏓 Pickleball ELO Tracker v3.0
==============================
Starting Pickleball ELO Tracker Server on port 3000...
✅ Server running at http://localhost:3000
📊 Leaderboard: http://localhost:3000/leaderboard
📜 Match History: http://localhost:3000/matches
👥 Players: http://localhost:3000/players
⚖️ Team Balancer: http://localhost:3000/balance
Add Player: http://localhost:3000/players/new
🎾 Record Match: http://localhost:3000/matches/new

Binary file not shown.

View File

@ -205,6 +205,8 @@ struct PlayerData {
email: String,
rating_display: String,
has_email: bool,
wins: i64,
losses: i64,
}
#[derive(Template, Clone, Debug)]
@ -1578,16 +1580,30 @@ async fn create_match(
///
/// **Returns:** HTML page with players table
async fn players_list_handler(State(state): State<AppState>) -> impl IntoResponse {
// UNIFIED RATING: use rating field (merged singles/doubles)
let players: Vec<(i64, String, Option<String>, f64)> = sqlx::query_as(
"SELECT id, name, email, rating FROM players ORDER BY rating DESC"
// UNIFIED RATING: use rating field (merged singles/doubles) with W-L record
let players: Vec<(i64, String, Option<String>, f64, i64, i64)> = sqlx::query_as(
r#"SELECT
p.id, p.name, p.email, p.rating,
COALESCE(SUM(CASE WHEN
(mp.team = 1 AND m.team1_score > m.team2_score) OR
(mp.team = 2 AND m.team2_score > m.team1_score)
THEN 1 ELSE 0 END), 0) as wins,
COALESCE(SUM(CASE WHEN
(mp.team = 1 AND m.team1_score < m.team2_score) OR
(mp.team = 2 AND m.team2_score < m.team1_score)
THEN 1 ELSE 0 END), 0) as losses
FROM players p
LEFT JOIN match_participants mp ON p.id = mp.player_id
LEFT JOIN matches m ON mp.match_id = m.id
GROUP BY p.id
ORDER BY p.rating DESC"#
)
.fetch_all(&state.pool)
.await
.unwrap_or_default();
let players = players.into_iter()
.map(|(id, name, email, rating)| {
.map(|(id, name, email, rating, wins, losses)| {
let has_email = email.is_some();
PlayerData {
id,
@ -1597,6 +1613,8 @@ async fn players_list_handler(State(state): State<AppState>) -> impl IntoRespons
email: email.unwrap_or_default(),
rating_display: format!("{:.1}", rating),
has_email,
wins,
losses,
}
})
.collect();
@ -1614,11 +1632,22 @@ async fn players_list_handler(State(state): State<AppState>) -> impl IntoRespons
///
/// **Returns:** HTML page with unified leaderboard
async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse {
// UNIFIED RATING: Single leaderboard using rating field
let players: Vec<(i64, String, f64, Option<String>)> = sqlx::query_as(
r#"SELECT DISTINCT p.id, p.name, p.rating, p.email
// UNIFIED RATING: Single leaderboard using rating field with W-L record
let players: Vec<(i64, String, f64, Option<String>, i64, i64)> = sqlx::query_as(
r#"SELECT
p.id, p.name, p.rating, p.email,
COALESCE(SUM(CASE WHEN
(mp.team = 1 AND m.team1_score > m.team2_score) OR
(mp.team = 2 AND m.team2_score > m.team1_score)
THEN 1 ELSE 0 END), 0) as wins,
COALESCE(SUM(CASE WHEN
(mp.team = 1 AND m.team1_score < m.team2_score) OR
(mp.team = 2 AND m.team2_score < m.team1_score)
THEN 1 ELSE 0 END), 0) as losses
FROM players p
JOIN match_participants mp ON p.id = mp.player_id
LEFT JOIN match_participants mp ON p.id = mp.player_id
LEFT JOIN matches m ON mp.match_id = m.id
GROUP BY p.id
ORDER BY p.rating DESC"#
)
.fetch_all(&state.pool)
@ -1626,7 +1655,7 @@ async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse
.unwrap_or_default();
let leaderboard = players.into_iter().enumerate()
.map(|(i, (id, name, rating, email))| {
.map(|(i, (id, name, rating, email, wins, losses))| {
let has_email = email.is_some();
let player_data = PlayerData {
id,
@ -1636,6 +1665,8 @@ async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse
email: email.unwrap_or_default(),
rating_display: format!("{:.1}", rating),
has_email,
wins,
losses,
};
((i + 1) as i32, player_data)
})

View File

@ -2,12 +2,23 @@
<h3 class="pitt-primary font-bold text-xl mb-2">
<a href="/players/{{ player.id }}" class="hover:underline">{{ player.name }}</a>
</h3>
<div class="text-3xl font-bold pitt-primary mb-4">{{ player.singles_rating | round(1) }}</div>
{% if player.email %}
<p class="text-sm text-gray-600">📧 {{ player.email }}</p>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<div class="text-xs text-gray-500 uppercase">Rating</div>
<div class="text-2xl font-bold pitt-primary">{{ player.rating_display }}</div>
</div>
<div>
<div class="text-xs text-gray-500 uppercase">Record</div>
<div class="text-2xl font-bold">
<span class="text-green-600">{{ player.wins }}</span>-<span class="text-red-600">{{ player.losses }}</span>
</div>
</div>
</div>
{% if player.has_email %}
<p class="text-sm text-gray-600 mb-4">📧 {{ player.email }}</p>
{% endif %}
<div class="flex gap-2 mt-4">
<a href="/players/{{ player.id }}" class="btn-primary text-sm">View Profile</a>
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm">Edit</a>
<div class="flex gap-2">
<a href="/players/{{ player.id }}" class="btn-primary text-sm flex-1">View Profile</a>
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm flex-1">Edit</a>
</div>
</div>

View File

@ -17,11 +17,17 @@
<a href="/players/{{ player.id }}/edit" class="btn-warning">✏️ Edit</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-gradient-to-br from-blue-50 to-blue-100 p-6 rounded-lg">
<div class="text-sm text-gray-600 mb-1">ELO Rating</div>
<div class="text-4xl font-bold pitt-primary">{{ player.rating_display }}</div>
</div>
<div class="bg-gradient-to-br from-purple-50 to-purple-100 p-6 rounded-lg">
<div class="text-sm text-gray-600 mb-1">Record</div>
<div class="text-4xl font-bold">
<span class="text-green-600">{{ player.wins }}</span><span class="text-gray-400">-</span><span class="text-red-600">{{ player.losses }}</span>
</div>
</div>
<div class="bg-gradient-to-br from-green-50 to-green-100 p-6 rounded-lg">
<div class="text-sm text-gray-600 mb-1">Matches Played</div>
<div class="text-4xl font-bold text-green-700">{{ match_count }}</div>

View File

@ -18,13 +18,24 @@
<h3 class="pitt-primary font-bold text-xl mb-2">
<a href="/players/{{ player.id }}" class="hover:underline">{{ player.name }}</a>
</h3>
<div class="text-3xl font-bold pitt-primary mb-4">{{ player.rating_display }}</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<div class="text-xs text-gray-500 uppercase">Rating</div>
<div class="text-2xl font-bold pitt-primary">{{ player.rating_display }}</div>
</div>
<div>
<div class="text-xs text-gray-500 uppercase">Record</div>
<div class="text-2xl font-bold">
<span class="text-green-600">{{ player.wins }}</span>-<span class="text-red-600">{{ player.losses }}</span>
</div>
</div>
</div>
{% if player.has_email %}
<p class="text-sm text-gray-600">📧 {{ player.email }}</p>
<p class="text-sm text-gray-600 mb-4">📧 {{ player.email }}</p>
{% endif %}
<div class="flex gap-2 mt-4">
<a href="/players/{{ player.id }}" class="btn-primary text-sm">View Profile</a>
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm">Edit</a>
<div class="flex gap-2">
<a href="/players/{{ player.id }}" class="btn-primary text-sm flex-1">View Profile</a>
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm flex-1">Edit</a>
</div>
</div>
{% endfor %}