work on /s

This commit is contained in:
Philipp Hofer 2025-04-11 15:06:46 +02:00
parent 1899088400
commit 2f5129c325
8 changed files with 592 additions and 79 deletions

View File

@ -11,7 +11,16 @@
- [x] Route_station - [x] Route_station
- [x] Team - [x] Team
- [ ] Rating view - [ ] Rating view
- [ ] make plan how i want to handle station-login, then write tests! - [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 - [ ] Highscore list
## Fancy features ## Fancy features

View File

@ -35,8 +35,8 @@ CREATE TABLE team (
); );
CREATE TABLE rating ( CREATE TABLE rating (
team_id INTEGER, team_id INTEGER NOT NULL,
station_id INTEGER, station_id INTEGER NOT NULL,
points INTEGER, points INTEGER,
notes TEXT, notes TEXT,
arrived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, arrived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

View File

@ -1,4 +1,9 @@
use crate::{admin::route::Route, AppState}; use super::team::Team;
use crate::{
admin::route::Route,
models::rating::{Rating, TeamsAtStationLocation},
AppState,
};
use axum::Router; use axum::Router;
use chrono::{DateTime, Local, NaiveDateTime, Utc}; use chrono::{DateTime, Local, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -70,6 +75,144 @@ impl Station {
Ok(()) Ok(())
} }
pub(crate) async fn new_team_waiting(
&self,
db: &SqlitePool,
team: &Team,
) -> Result<(), String> {
let teams = TeamsAtStationLocation::for_station(db, &self).await;
if !teams.not_yet_here.contains(team) {
return Err(format!("Kann Team nicht der Warteschlange hinzufügen, weil das Team {} nicht zu deiner Station kommen soll.", team.name));
}
Rating::create(db, &self, team).await?;
Ok(())
}
pub(crate) async fn remove_team_waiting(
&self,
db: &SqlitePool,
team: &Team,
) -> Result<(), String> {
let teams = TeamsAtStationLocation::for_station(db, &self).await;
let waiting_teams: Vec<&Team> = teams.waiting.iter().map(|(team, _)| team).collect();
if !waiting_teams.contains(&team) {
return Err(format!("Kann Team nicht von der Warteschlange gelöscht werden, weil das Team {} nicht in der Warteschlange ist.", team.name));
}
Rating::delete(db, &self, team).await?;
Ok(())
}
pub(crate) async fn team_starting(&self, db: &SqlitePool, team: &Team) -> Result<(), String> {
let teams = TeamsAtStationLocation::for_station(db, &self).await;
let waiting_teams: Vec<&Team> = teams.waiting.iter().map(|(team, _)| team).collect();
if !waiting_teams.contains(&team) {
return Err(format!(
"Team kann nicht starten, weil das Team {} nicht in der Warteschlange ist.",
team.name
));
}
sqlx::query!(
"UPDATE rating SET started_at = CURRENT_TIMESTAMP WHERE team_id = ? AND station_id = ?",
team.id,
self.id
)
.execute(db)
.await
.unwrap();
Ok(())
}
pub(crate) async fn remove_team_doing(
&self,
db: &SqlitePool,
team: &Team,
) -> Result<(), String> {
let teams = TeamsAtStationLocation::for_station(db, &self).await;
let doing_teams: Vec<&Team> = teams.doing.iter().map(|(team, _)| team).collect();
if !doing_teams.contains(&team) {
return Err(format!(
"Team kann nicht zur Warteschlange hinzugefügt werden, weil das Team {} aktuell nicht an deiner Station arbeitet.",
team.name
));
}
sqlx::query!(
"UPDATE rating SET started_at = NULL WHERE team_id = ? AND station_id = ?",
team.id,
self.id
)
.execute(db)
.await
.unwrap();
Ok(())
}
pub(crate) async fn team_finished(&self, db: &SqlitePool, team: &Team) -> Result<(), String> {
let teams = TeamsAtStationLocation::for_station(db, &self).await;
let doing_teams: Vec<&Team> = teams.doing.iter().map(|(team, _)| team).collect();
if !doing_teams.contains(&team) {
return Err(format!(
"Team kann nicht beenden, weil das Team {} nicht an der Station arbeitet.",
team.name
));
}
sqlx::query!(
"UPDATE rating SET left_at = CURRENT_TIMESTAMP WHERE team_id = ? AND station_id = ?",
team.id,
self.id
)
.execute(db)
.await
.unwrap();
Ok(())
}
pub(crate) async fn remove_team_left(
&self,
db: &SqlitePool,
team: &Team,
) -> Result<(), String> {
let teams = TeamsAtStationLocation::for_station(db, &self).await;
let left_teams: Vec<&Team> = teams.left.iter().map(|(team, _)| team).collect();
if !left_teams.contains(&team) {
return Err(format!(
"Team kann nicht zur Arbeitsposition hinzugefügt werden, weil das Team {} aktuell nicht feritg ist",
team.name
));
}
sqlx::query!(
"UPDATE rating SET left_at = NULL WHERE team_id = ? AND station_id = ?",
team.id,
self.id
)
.execute(db)
.await
.unwrap();
Ok(())
}
async fn update_name(&self, db: &SqlitePool, name: &str) { async fn update_name(&self, db: &SqlitePool, name: &str) {
sqlx::query!("UPDATE station SET name = ? WHERE id = ?", name, self.id) sqlx::query!("UPDATE station SET name = ? WHERE id = ?", name, self.id)
.execute(db) .execute(db)
@ -167,6 +310,19 @@ impl Station {
let datetime_utc = DateTime::<Utc>::from_naive_utc_and_offset(last_login.clone(), Utc); let datetime_utc = DateTime::<Utc>::from_naive_utc_and_offset(last_login.clone(), Utc);
Some(datetime_utc.with_timezone(&Local)) Some(datetime_utc.with_timezone(&Local))
} }
pub(crate) async fn teams(&self, db: &SqlitePool) -> Vec<Team> {
sqlx::query_as::<_, Team>(
"SELECT DISTINCT t.id, t.name, t.notes, t.amount_people, t.first_station_id, t.route_id
FROM team t
JOIN route_station rs ON t.route_id = rs.route_id
WHERE rs.station_id = ?;",
)
.bind(self.id)
.fetch_all(db)
.await
.unwrap()
}
} }
pub(super) fn routes() -> Router<AppState> { pub(super) fn routes() -> Router<AppState> {

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
AppState,
admin::{route::Route, station::Station}, admin::{route::Route, station::Station},
AppState,
}; };
use axum::Router; use axum::Router;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -8,7 +8,7 @@ use sqlx::{FromRow, SqlitePool};
mod web; mod web;
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct Team { pub(crate) struct Team {
pub(crate) id: i64, pub(crate) id: i64,
pub(crate) name: String, pub(crate) name: String,

View File

@ -4,7 +4,7 @@ extern crate rust_i18n;
i18n!("locales", fallback = "de-AT"); i18n!("locales", fallback = "de-AT");
use admin::station::Station; use admin::station::Station;
use axum::{Router, body::Body, extract::FromRef, response::Response, routing::get}; use axum::{body::Body, extract::FromRef, response::Response, routing::get, Router};
use partials::page; use partials::page;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use std::sync::Arc; use std::sync::Arc;
@ -12,6 +12,7 @@ use tokio::net::TcpListener;
use tower_sessions::{MemoryStore, SessionManagerLayer}; use tower_sessions::{MemoryStore, SessionManagerLayer};
pub(crate) mod admin; pub(crate) mod admin;
pub(crate) mod models;
mod partials; mod partials;
pub(crate) mod station; pub(crate) mod station;
@ -121,7 +122,7 @@ pub async fn start(listener: TcpListener, db: SqlitePool) {
let state = AppState { db: Arc::new(db) }; let state = AppState { db: Arc::new(db) };
let app = Router::new() let app = Router::new()
.nest("/s", station::routes()) // TODO: maybe switch to "/" .nest("/s/{id}/{code}", station::routes()) // TODO: maybe switch to "/"
.nest("/admin", admin::routes()) .nest("/admin", admin::routes())
.route("/pico.css", get(serve_pico_css)) .route("/pico.css", get(serve_pico_css))
.route("/style.css", get(serve_my_css)) .route("/style.css", get(serve_my_css))

1
src/models/mod.rs Normal file
View File

@ -0,0 +1 @@
pub(crate) mod rating;

137
src/models/rating.rs Normal file
View File

@ -0,0 +1,137 @@
use crate::{admin::team::Team, Station};
use chrono::{DateTime, Local, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub(crate) struct Rating {
pub(crate) team_id: i64,
pub(crate) station_id: i64,
pub(crate) points: Option<i64>,
notes: Option<String>,
arrived_at: NaiveDateTime,
started_at: Option<NaiveDateTime>,
left_at: Option<NaiveDateTime>,
}
impl Rating {
pub(crate) async fn create(
db: &SqlitePool,
station: &Station,
team: &Team,
) -> Result<(), String> {
sqlx::query!(
"INSERT INTO rating(team_id, station_id) VALUES (?, ?)",
team.id,
station.id
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
pub(crate) async fn delete(
db: &SqlitePool,
station: &Station,
team: &Team,
) -> Result<(), String> {
sqlx::query!(
"DELETE FROM rating WHERE team_id = ? AND station_id = ?",
team.id,
station.id
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
pub(crate) async fn all_with_station(db: &SqlitePool, station: &Station) -> Vec<Self> {
sqlx::query_as::<_, Self>(
"SELECT team_id, station_id, points, notes, arrived_at, started_at, left_at FROM rating WHERE station_id = ?;",
)
.bind(station.id)
.fetch_all(db)
.await
.unwrap()
}
pub async fn find_by_team_and_station(
db: &SqlitePool,
team: &Team,
station: &Station,
) -> Option<Self> {
sqlx::query_as!(Self, "SELECT team_id, station_id, points, notes, arrived_at, started_at, left_at FROM rating WHERE team_id = ? AND station_id = ?", team.id, station.id)
.fetch_one(db)
.await
.ok()
}
pub(crate) fn local_time_arrived_at(&self) -> String {
let datetime_utc = DateTime::<Utc>::from_naive_utc_and_offset(self.arrived_at.clone(), Utc);
let datetime_local = datetime_utc.with_timezone(&Local);
datetime_local.format("%H:%M").to_string()
}
pub(crate) fn local_time_doing(&self) -> String {
let Some(started_at) = self.started_at else {
return String::from("noch nicht gestartet");
};
let datetime_utc = DateTime::<Utc>::from_naive_utc_and_offset(started_at.clone(), Utc);
let datetime_local = datetime_utc.with_timezone(&Local);
datetime_local.format("%H:%M").to_string()
}
pub(crate) fn local_time_left(&self) -> String {
let Some(left_at) = self.left_at else {
return String::from("noch nicht fertig");
};
let datetime_utc = DateTime::<Utc>::from_naive_utc_and_offset(left_at.clone(), Utc);
let datetime_local = datetime_utc.with_timezone(&Local);
datetime_local.format("%H:%M").to_string()
}
}
pub(crate) struct TeamsAtStationLocation {
pub(crate) total_teams: i64,
pub(crate) not_yet_here: Vec<Team>,
pub(crate) waiting: Vec<(Team, Rating)>,
pub(crate) doing: Vec<(Team, Rating)>,
pub(crate) left: Vec<(Team, Rating)>,
}
impl TeamsAtStationLocation {
pub(crate) async fn for_station(db: &SqlitePool, station: &Station) -> TeamsAtStationLocation {
let teams = station.teams(&db).await;
let total_teams = teams.len() as i64;
let mut not_yet_here = Vec::new();
let mut waiting = Vec::new();
let mut doing = Vec::new();
let mut left = Vec::new();
for team in teams {
match Rating::find_by_team_and_station(db, &team, &station).await {
Some(rating) => {
if rating.left_at.is_some() {
left.push((team, rating));
} else if rating.started_at.is_some() {
doing.push((team, rating));
} else {
waiting.push((team, rating));
}
}
None => not_yet_here.push(team),
}
}
TeamsAtStationLocation {
total_teams,
not_yet_here,
waiting,
doing,
left,
}
}
}

View File

@ -1,76 +1,19 @@
use crate::{AppState, Station, partials}; use crate::{
use axum::{Router, extract::State, routing::get}; admin::team::Team, err, models::rating::TeamsAtStationLocation, partials, succ, AppState,
use maud::{Markup, html}; Station,
};
use axum::{
extract::State,
response::{IntoResponse, Redirect},
routing::{get, post},
Form, Router,
};
use maud::{html, Markup};
use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use std::sync::Arc; use std::sync::Arc;
use tower_sessions::Session; use tower_sessions::Session;
//async fn view(
// State(db): State<Arc<SqlitePool>>,
// session: Session,
// jar: CookieJar,
// pjar: PrivateCookieJar,
// axum::extract::Path(id): axum::extract::Path<i64>,
//) -> Result<(CookieJar, PrivateCookieJar, Markup), (CookieJar, PrivateCookieJar, impl IntoResponse)>
//{
// // Station selector
// let (mut jar, current_station_cookie) = get_station_cookie(&db, jar).await;
// if current_station_cookie.is_none() {
// jar = jar.add(Cookie::new("station_id", id.to_string()));
// }
// if let Some(current_station_cookie) = current_station_cookie {
// if current_station_cookie.id != id {
// // user has a cookie, which is a different station than she is trying to access
// if let Some(station) = Station::find_by_id(&db, id).await {
// jar = jar.remove(Cookie::from("station_id"));
// // trying to access valid station id
// err!(session, "Du hast versucht eine neue Station zu öffnen obwohl du bereits eine andere Station offen hattest. Welche möchtest du nun verwenden?");
// return Ok((
// jar,
// pjar,
// decide_between_stations(&current_station_cookie, &station, session).await,
// ));
// } else {
// // user trying to access _in_valid station id -> make her aware + redirect to old
// err!(session, "Du hast versucht eine Station öffnen, die es nicht gibt. Nachdem du vorher schonmal eine andere Station (die es gibt) geöffnet hattest, bist du nun zu dieser weitergeleitet worden. Wenn du das nicht willst, logg dich bitte aus.");
// return Err((
// jar,
// pjar,
// Redirect::to(&format!("/s/{}", current_station_cookie.id)),
// ));
// }
// }
// }
// let station = Station::find_by_id(&db, id).await.unwrap();
//
// let mut pjar = pjar;
//
// // PW Checker
// if let Some(pw) = pjar.get("pw") {
// if pw.value() != station.pw {
// pjar = pjar.remove(Cookie::from("station_id"));
// err!(session, "Du hattest einen falschen Code für Station {} gespeichert. Bitte gibt den richtigen ein:", station.name );
// return Err((jar, pjar, Redirect::to(&format!("/s/code",))));
// }
// } else {
// return Err((jar, pjar, Redirect::to(&format!("/s/code",))));
// }
//
// let content = html! {
// nav {
// ul {
// li { strong { (format!("Station {}", station.name)) } }
// }
// ul {
// li { a href="/s/station-logout" { "Logout" } }
// }
// }
// h1 { "test" }
// };
//
// Ok((jar, pjar, partials::page(content, session, false).await))
//}
async fn view( async fn view(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
session: Session, session: Session,
@ -85,13 +28,279 @@ async fn view(
return partials::page(content, session, false).await; return partials::page(content, session, false).await;
}; };
let teams = TeamsAtStationLocation::for_station(&db, &station).await;
//pub(crate) not_yet_here: Vec<Team>,
//pub(crate) waiting: Vec<(Team, Rating)>,
//pub(crate) doing: Vec<(Team, Rating)>,
//pub(crate) left: Vec<(Team, Rating)>,
let content = html! { let content = html! {
h1 { (format!("Station {}", station.name)) } h1 { (format!("Station {}", station.name)) }
article {
"Insgesamt sollten "
(teams.total_teams)
" Teams zu deiner Station kommen."
progress value=(teams.total_teams-teams.not_yet_here.len() as i64) max=(teams.total_teams) {}
}
h2 { "Teams aktuell bei dir" }
@if !teams.waiting.is_empty() {
(teams.waiting.len())
" Teams warten an deiner Station:"
ol {
@for (team, rating) in teams.waiting {
li {
(team.name)
" (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.');" {
"🗑️"
}
a href=(format!("/s/{id}/{code}/team-starting/{}", team.id)) {
button { "Team startet" }
}
}
}
}
}
@if !teams.not_yet_here.is_empty() {
form action=(format!("/s/{id}/{code}/new-waiting")) method="post" {
fieldset role="group" {
select name="team_id" aria-label="Team auswählen" required {
@for team in &teams.not_yet_here {
option value=(team.id) {
(team.name)
}
}
}
input type="submit" value="Neues Team da";
}
}
}
@if !teams.doing.is_empty() {
(teams.doing.len())
" Teams arbeiten an deiner Station:"
ol {
@for (team, rating) in teams.doing {
li {
(team.name)
" (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');" {
"🗑️"
}
a href=(format!("/s/{id}/{code}/team-finished/{}", team.id)) {
button { "Team fertig" }
}
}
}
}
}
@if !teams.left.is_empty() {
h2 { "Teams die bei dir waren" }
(teams.left.len())
" Teams waren schon bei dir"
ol {
@for (team, rating) in teams.left {
li {
(team.name)
" (gegangen um "
(rating.local_time_left())
")"
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');" {
"🗑️"
}
}
}
}
}
}; };
partials::page(content, session, false).await partials::page(content, session, false).await
} }
pub(super) fn routes() -> Router<AppState> { #[derive(Deserialize)]
Router::new().route("/{id}/{code}", get(view)) struct NewWaitingForm {
team_id: i64,
}
async fn new_waiting(
State(db): State<Arc<SqlitePool>>,
session: Session,
axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>,
Form(form): Form<NewWaitingForm>,
) -> 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, form.team_id).await else {
err!(session, "Konnte das Team der Warteschlange nicht hinzufügen, weil ein Team mit ID {} nicht existiert", form.team_id);
return Redirect::to("/s/{id}/{code}");
};
match station.new_team_waiting(&db, &team).await {
Ok(()) => succ!(session, "Team der Warteschlange hinzugefügt"),
Err(e) => err!(session, "{e}"),
}
Redirect::to(&format!("/s/{id}/{code}"))
}
async fn remove_waiting(
State(db): State<Arc<SqlitePool>>,
session: Session,
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
) -> 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 von der Warteschlange entfernen, weil ein Team mit ID {} nicht existiert", team_id);
return Redirect::to("/s/{id}/{code}");
};
match station.remove_team_waiting(&db, &team).await {
Ok(()) => succ!(session, "Team der Warteschlange gelöscht"),
Err(e) => err!(session, "{e}"),
}
Redirect::to(&format!("/s/{id}/{code}"))
}
async fn team_starting(
State(db): State<Arc<SqlitePool>>,
session: Session,
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
) -> 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,
"Team kann nicht starten, weil ein Team mit ID {} nicht existiert",
team_id
);
return Redirect::to("/s/{id}/{code}");
};
match station.team_starting(&db, &team).await {
Ok(()) => succ!(session, "Team der Warteschlange gelöscht"),
Err(e) => err!(session, "{e}"),
}
Redirect::to(&format!("/s/{id}/{code}"))
}
async fn remove_doing(
State(db): State<Arc<SqlitePool>>,
session: Session,
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
) -> 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 zur Warteschlange hinzufügen, weil ein Team mit ID {} nicht existiert", team_id);
return Redirect::to("/s/{id}/{code}");
};
match station.remove_team_doing(&db, &team).await {
Ok(()) => succ!(session, "Team zur Warteschlange hinzugefügt"),
Err(e) => err!(session, "{e}"),
}
Redirect::to(&format!("/s/{id}/{code}"))
}
async fn team_finished(
State(db): State<Arc<SqlitePool>>,
session: Session,
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
) -> 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,
"Team kann nicht beenden, weil ein Team mit ID {} nicht existiert",
team_id
);
return Redirect::to("/s/{id}/{code}");
};
match station.team_finished(&db, &team).await {
Ok(()) => succ!(session, "Team erfolgreich beendet"),
Err(e) => err!(session, "{e}"),
}
Redirect::to(&format!("/s/{id}/{code}"))
}
async fn remove_left(
State(db): State<Arc<SqlitePool>>,
session: Session,
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
) -> 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 zur Arbeits-Position hinzufügen, weil ein Team mit ID {} nicht existiert", team_id);
return Redirect::to("/s/{id}/{code}");
};
match station.remove_team_left(&db, &team).await {
Ok(()) => succ!(session, "Team zur Arbeitsposition hinzugefügt"),
Err(e) => err!(session, "{e}"),
}
Redirect::to(&format!("/s/{id}/{code}"))
}
pub(super) fn routes() -> Router<AppState> {
Router::new()
.route("/", get(view))
.route("/new-waiting", post(new_waiting))
.route("/remove-waiting/{team_id}", get(remove_waiting))
.route("/team-starting/{team_id}", get(team_starting))
.route("/remove-doing/{team_id}", get(remove_doing))
.route("/team-finished/{team_id}", get(team_finished))
.route("/remove-left/{team_id}", get(remove_left))
} }