make points + notes updateable
All checks were successful
CI/CD Pipeline / test (push) Successful in 3m16s

This commit is contained in:
Philipp Hofer 2025-04-11 19:51:01 +02:00
parent 6be4f1f883
commit 17690d7e6f
4 changed files with 182 additions and 18 deletions

View File

@ -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)

View File

@ -89,6 +89,39 @@ impl Station {
Ok(())
}
pub(crate) async fn team_update(
&self,
db: &SqlitePool,
team: &Team,
points: Option<i64>,
notes: Option<String>,
) -> 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
)

View File

@ -8,7 +8,7 @@ pub(crate) struct Rating {
pub(crate) team_id: i64,
pub(crate) station_id: i64,
pub(crate) points: Option<i64>,
notes: Option<String>,
pub(crate) notes: Option<String>,
arrived_at: NaiveDateTime,
started_at: Option<NaiveDateTime>,
left_at: Option<NaiveDateTime>,
@ -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<i64>,
notes: Option<String>,
) -> 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,

View File

@ -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<i64>,
notes: Option<String>,
}
async fn team_update(
State(db): State<Arc<SqlitePool>>,
session: Session,
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
Form(form): Form<TeamUpdateForm>,
) -> 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<AppState> {
Router::new()
.route("/", get(view))
@ -303,6 +418,7 @@ pub(super) fn routes() -> Router<AppState> {
.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)]