From 17690d7e6f5b84658d4807b87d9da59966189bd7 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Fri, 11 Apr 2025 19:51:01 +0200 Subject: [PATCH] make points + notes updateable --- README.md | 8 +-- src/admin/station/mod.rs | 35 +++++++++- src/models/rating.rs | 23 ++++++- src/station.rs | 134 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 182 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index fcd177d..a56df61 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,6 @@ - [ ] Rating view - [x] make plan how i want to handle station-login, then write tests! - [ ] simple rating entry - - [x] "new group here" (team_id, auto ARRIVED_AT) - - [x] create - - [x] delete - - [ ] if no group currently here -> ask to start - - [x] "group started" (auto STARTED_AT) - - [x] "group finished" (auto LEFT_AT - - [ ] "group rated" (points, notes) // also updateable - [ ] improve messages, especially for `/s` - [ ] Highscore list @@ -31,3 +24,4 @@ - Rangliste - How long have teams spent on average at each station? - Aggregated wait time (started_at - arrived_at) for each station +- station-view: maybe add configuration to instantly start groups (no waiting possible) diff --git a/src/admin/station/mod.rs b/src/admin/station/mod.rs index 0d742cc..6acb6e2 100644 --- a/src/admin/station/mod.rs +++ b/src/admin/station/mod.rs @@ -89,6 +89,39 @@ impl Station { Ok(()) } + pub(crate) async fn team_update( + &self, + db: &SqlitePool, + team: &Team, + points: Option, + notes: Option, + ) -> Result<(), String> { + let notes = match notes { + Some(n) if n.is_empty() => None, + Some(n) => Some(n), + None => None, + }; + let teams = TeamsAtStationLocation::for_station(db, self).await; + + let waiting_teams: Vec<&Team> = teams.waiting.iter().map(|(team, _)| team).collect(); + let doing_teams: Vec<&Team> = teams.doing.iter().map(|(team, _)| team).collect(); + let finished_teams: Vec<&Team> = teams.left.iter().map(|(team, _)| team).collect(); + + if !waiting_teams.contains(&team) + && !doing_teams.contains(&team) + && !finished_teams.contains(&team) + { + return Err( + "Es können nur Teams bewertet werden, die zumindest schon bei der Station sind." + .to_string(), + ); + } + + Rating::update(db, self, team, points, notes).await?; + + Ok(()) + } + pub(crate) async fn remove_team_waiting( &self, db: &SqlitePool, @@ -200,7 +233,7 @@ impl Station { } sqlx::query!( - "UPDATE rating SET left_at = NULL WHERE team_id = ? AND station_id = ?", + "UPDATE rating SET left_at = NULL, points=NULL WHERE team_id = ? AND station_id = ?", team.id, self.id ) diff --git a/src/models/rating.rs b/src/models/rating.rs index 2ae9840..2abeb85 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -8,7 +8,7 @@ pub(crate) struct Rating { pub(crate) team_id: i64, pub(crate) station_id: i64, pub(crate) points: Option, - notes: Option, + pub(crate) notes: Option, arrived_at: NaiveDateTime, started_at: Option, left_at: Option, @@ -30,6 +30,27 @@ impl Rating { .map_err(|e| e.to_string())?; Ok(()) } + + pub(crate) async fn update( + db: &SqlitePool, + station: &Station, + team: &Team, + points: Option, + notes: Option, + ) -> Result<(), String> { + sqlx::query!( + "UPDATE rating SET points = ?, notes = ? WHERE station_id = ? AND team_id = ?", + points, + notes, + station.id, + team.id + ) + .execute(db) + .await + .map_err(|e| e.to_string())?; + Ok(()) + } + pub(crate) async fn delete( db: &SqlitePool, station: &Station, diff --git a/src/station.rs b/src/station.rs index 1150221..67a99e8 100644 --- a/src/station.rs +++ b/src/station.rs @@ -54,9 +54,25 @@ async fn view( " (seit " (rating.local_time_arrived_at()) ")" - a href=(format!("/s/{id}/{code}/remove-waiting/{}", team.id)) - onclick="return confirm('Bist du sicher, dass das Team noch nicht bei dir ist? Das kann _NICHT_ mehr rückgängig gemacht werden.');" { - "🗑️" + details { + summary { "✏️" } + article { + form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" { + label { + "Notizen" + @if let Some(notes) = &rating.notes { + input type="text" name="notes" value=(notes); + } @else { + input type="text" name="notes"; + } + } + input type="submit" value="Notizen speichern"; + } + a href=(format!("/s/{id}/{code}/remove-waiting/{}", team.id)) + onclick="return confirm('Bist du sicher, dass das Team noch nicht bei dir ist? Das kann _NICHT_ mehr rückgängig gemacht werden.');" { + "🗑️" + } + } } a href=(format!("/s/{id}/{code}/team-starting/{}", team.id)) { button { "Team startet" } @@ -89,9 +105,25 @@ async fn view( " (seit " (rating.local_time_doing()) ")" - a href=(format!("/s/{id}/{code}/remove-doing/{}", team.id)) - onclick="return confirm('Bist du sicher, dass das Team noch nicht bei dir arbeitet? Das Team wird zurück auf die Warte-Position gesetzt');" { - "🗑️" + details { + summary { "✏️" } + article { + form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" { + label { + "Notizen" + @if let Some(notes) = &rating.notes { + input type="text" name="notes" value=(notes); + } @else { + input type="text" name="notes"; + } + } + input type="submit" value="Notizen speichern"; + } + a href=(format!("/s/{id}/{code}/remove-doing/{}", team.id)) + onclick="return confirm('Bist du sicher, dass das Team noch nicht bei dir arbeitet? Das Team wird zurück auf die Warte-Position gesetzt');" { + "🗑️" + } + } } a href=(format!("/s/{id}/{code}/team-finished/{}", team.id)) { button { "Team fertig" } @@ -110,10 +142,55 @@ async fn view( (team.name) " (gegangen um " (rating.local_time_left()) + @if let Some(points) = rating.points { + ", " + (points) + " Punkte" + } + @if let Some(notes) = &rating.notes{ + ", Notizen: " + (notes) + } ")" - a href=(format!("/s/{id}/{code}/remove-left/{}", team.id)) - onclick="return confirm('Bist du sicher, dass das Team noch nicht bei dir fertig ist? Das Team wird zurück auf die Arbeits-Position gesetzt');" { - "🗑️" + + details open[rating.points.is_none()] { + summary { "✏️" } + article { + @if rating.points.is_none() { + article class="warning" { + "Noch keine Punkte für diese Gruppe vergeben. Gib sie hier ein und drücke dann auf " + em { "Speichern" } + } + } + form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" { + label { + @if let Some(points) = rating.points { + span { (points) " Punkte" } + input type="range" name="points" min="0" max="10" value=(points) + onchange="if(!confirm('Du hast die Gruppe bereits bewertet. Bist du sicher, dass du deine Bewertung nochmal ändern möchtest?')) { this.value = this.defaultValue; this.previousElementSibling.textContent = this.defaultValue + ' Punkte'; }" + oninput="this.previousElementSibling.textContent = this.value + ' Punkte'" {} + } @else { + span { "0 Punkte" } + input type="range" name="points" min="0" max="10" value="0" oninput="this.previousElementSibling.textContent = this.value + ' Punkte'" {} + } + + } + label { + "Notizen" + @if let Some(notes) = &rating.notes { + input type="text" name="notes" value=(notes); + } @else { + input type="text" name="notes"; + } + } + input type="submit" value="Speichern"; + } + + a href=(format!("/s/{id}/{code}/remove-left/{}", team.id)) + onclick="return confirm('Bist du sicher, dass das Team noch nicht bei dir fertig ist? Das Team wird zurück auf die Arbeits-Position gesetzt');" { + "🗑️" + } + } } } } @@ -294,6 +371,44 @@ async fn remove_left( Redirect::to(&format!("/s/{id}/{code}")) } +#[derive(Deserialize)] +struct TeamUpdateForm { + points: Option, + notes: Option, +} +async fn team_update( + State(db): State>, + session: Session, + axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>, + Form(form): Form, +) -> impl IntoResponse { + let Some(station) = Station::login(&db, id, &code).await else { + err!( + session, + "Falscher Quick-Einlogg-Link. Bitte nochmal scannen oder neu eingeben." + ); + return Redirect::to("/s/{id}/{code}"); + }; + let Some(team) = Team::find_by_id(&db, team_id).await else { + err!( + session, + "Konnte das Team nicht updaten, weil ein Team mit ID {} nicht existiert", + team_id + ); + return Redirect::to("/s/{id}/{code}"); + }; + + match station + .team_update(&db, &team, form.points, form.notes) + .await + { + Ok(()) => succ!(session, "Team bearbeitet"), + Err(e) => err!(session, "{e}"), + } + + Redirect::to(&format!("/s/{id}/{code}")) +} + pub(super) fn routes() -> Router { Router::new() .route("/", get(view)) @@ -303,6 +418,7 @@ pub(super) fn routes() -> Router { .route("/remove-doing/{team_id}", get(remove_doing)) .route("/team-finished/{team_id}", get(team_finished)) .route("/remove-left/{team_id}", get(remove_left)) + .route("/team-update/{team_id}", post(team_update)) } #[cfg(test)]