628 lines
25 KiB
Rust
628 lines
25 KiB
Rust
use crate::{
|
|
admin::{team::Team, RunStatus},
|
|
er, err,
|
|
models::rating::TeamsAtStationLocation,
|
|
partials, suc, AppState, Station,
|
|
};
|
|
use axum::{
|
|
extract::State,
|
|
response::{IntoResponse, Redirect},
|
|
routing::{get, post},
|
|
Form, Router,
|
|
};
|
|
use maud::{html, Markup, PreEscaped};
|
|
use serde::Deserialize;
|
|
use sqlx::SqlitePool;
|
|
use std::sync::Arc;
|
|
use tower_sessions::Session;
|
|
|
|
async fn view(
|
|
State(db): State<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>,
|
|
) -> Markup {
|
|
let Some(station) = Station::login(&db, id, &code).await else {
|
|
let content = html! {
|
|
article class="error" {
|
|
(t!("invalid_rating_code"))
|
|
}
|
|
};
|
|
return partials::page(content, session, false).await;
|
|
};
|
|
|
|
let teams = TeamsAtStationLocation::for_station(&db, &station).await;
|
|
let teams_on_the_way = station.teams_on_the_way(&db).await;
|
|
let status = RunStatus::curr(&db).await;
|
|
|
|
let content = html! {
|
|
h1 {
|
|
(t!("station"))
|
|
" "
|
|
(station.name)
|
|
}
|
|
@if let (Some(lat), Some(lng)) = (station.lat, station.lng) {
|
|
article {
|
|
details open[(!station.ready)]{
|
|
summary { (t!("infos")) }
|
|
"👋"
|
|
(t!("station_info"))
|
|
" "
|
|
@let first_teams = Team::all_with_first_station(&db, &station).await;
|
|
@if first_teams.is_empty() {
|
|
(t!("station_has_no_teams_to_take_to_start"))
|
|
} @else{
|
|
@if first_teams.len() == 1 {
|
|
(t!("station_should_take_one_teams_to_start"))
|
|
} @else {
|
|
(t!("station_should_take_n_teams_to_start", amount=first_teams.len()))
|
|
}
|
|
ol {
|
|
@for team in first_teams {
|
|
li {
|
|
(team.name)
|
|
ul {
|
|
@if let Some(amount_people) = team.amount_people {
|
|
li {
|
|
(amount_people)
|
|
" "
|
|
@if amount_people == 1 {
|
|
(t!("person"))
|
|
} @else {
|
|
(t!("people"))
|
|
}
|
|
}
|
|
}
|
|
@if let Some(notes) = team.notes {
|
|
li {
|
|
(notes)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
(t!("your_station_is_here"))
|
|
div id="map" style="height: 500px" {}
|
|
script { (format!("
|
|
const map = L.map('map').setView([{lat}, {lng}], 14);
|
|
|
|
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
|
|
attribution: '© OpenStreetMap contributors'
|
|
}}).addTo(map);
|
|
|
|
|
|
const myIcon = L.icon({{
|
|
iconUrl: '/marker.png',
|
|
iconAnchor: [12, 41]
|
|
}});
|
|
currentMarker = L.marker([{lat}, {lng}], {{icon: myIcon}}).addTo(map);
|
|
map.setView([{lat}, {lng}], 14);
|
|
"))
|
|
}
|
|
div {
|
|
sub {
|
|
a href=(format!("https://www.google.com/maps?q={lat},{lng}")) target="_blank" {
|
|
"Google Maps Navigation"
|
|
}
|
|
}
|
|
}
|
|
hr;
|
|
"In diesem Tool solltest du diese 3 Dinge vermerken:"
|
|
ol {
|
|
li {
|
|
b { "Ein Team kommt zu deiner Station:" }
|
|
" Du wählst das entsprechende Team aus und klickst auf "
|
|
em { (t!("team_is_here")) }
|
|
". Das Team ist nun im Wartemodus (⏳)."
|
|
}
|
|
li {
|
|
b { "Das Team beginnt mit der Aufgabe bei deiner Station:" }
|
|
" Du klickst beim entsprechenden Team auf "
|
|
em { (t!("team_starting")) }
|
|
". Das Team ist nun im aktiven Modus (🎬)."
|
|
}
|
|
li {
|
|
b { "Das Team hat deine Station beendet und ist gegangen:" }
|
|
" Du klickst beim entsprechenden Team auf "
|
|
em { (t!("team_finished")) }
|
|
". Bitte schau, dass du das immer zeitnah erledigst, damit die nächste Station informiert werden kann, dass ein Team auf dem Weg ist."
|
|
}
|
|
}
|
|
"Zu jedem Zeitpunkt kannst du mit Klick auf ✏️ Notizen zu den Teams machen. In aller Ruhe kannst du unter dem Punkt "
|
|
em { (t!("to_rate")) }
|
|
" die Teams, die schon bei dir waren, bewerten."
|
|
hr;
|
|
a href=(format!("/s/{id}/{code}/ready")){
|
|
@if station.ready {
|
|
(t!("station_not_yet_ready"))
|
|
} @else {
|
|
button { (t!("station_ready")) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
article {
|
|
@if teams.total_teams == 1 {
|
|
(t!("one_team_should_come_to_station"))
|
|
} @else{
|
|
(t!("n_teams_should_come_to_station", amount=teams.total_teams))
|
|
}
|
|
progress value=(teams.total_teams-teams.not_yet_here.len() as i64) max=(teams.total_teams) {}
|
|
@if status == RunStatus::HasEnded {
|
|
@let teams_to_take_home = Team::all_with_last_station(&db, &station).await;
|
|
@if !teams_to_take_home.is_empty() {
|
|
"Bitte nimm folgende Teams mit heim:"
|
|
ol {
|
|
@for team in teams_to_take_home {
|
|
li { (team.name) }
|
|
}
|
|
}
|
|
}@else {
|
|
"Du musst keine Teams mit heim nehmen"
|
|
}
|
|
|
|
}
|
|
}
|
|
@for team in teams_on_the_way {
|
|
article {
|
|
(t!("team_on_the_way_to_your_station", team=team.team.name, time=team.left))
|
|
form action=(format!("/s/{id}/{code}/new-waiting")) method="post" {
|
|
input type="hidden" name="team_id" value=(team.team.id);
|
|
input type="submit" value=(t!("team_is_here"));
|
|
}
|
|
}
|
|
}
|
|
@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=(t!("select_team")) required {
|
|
@for team in &teams.not_yet_here {
|
|
option value=(team.id) {
|
|
(team.name)
|
|
}
|
|
}
|
|
}
|
|
input type="submit" value=(t!("team_is_here"));
|
|
}
|
|
}
|
|
}
|
|
h2 { (t!("teams_at_your_station")) }
|
|
@if !teams.doing.is_empty() {
|
|
@for (team, rating) in teams.doing {
|
|
article {
|
|
details {
|
|
summary {
|
|
em data-tooltip=(t!("state_active")) { (t!("state_active_icon")) " " }
|
|
(team.name)
|
|
small {
|
|
" ("
|
|
(t!("since_time", time=rating.local_time_doing()))
|
|
")"
|
|
}
|
|
"✏️"
|
|
a href=(format!("/s/{id}/{code}/team-finished/{}", team.id)) {
|
|
button { (t!("team_finished")) }
|
|
}
|
|
}
|
|
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=(t!("save_notes"));
|
|
}
|
|
a href=(format!("/s/{id}/{code}/remove-doing/{}", team.id))
|
|
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_active", team=team.name))) {
|
|
"🗑️"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@if !teams.waiting.is_empty() {
|
|
@for (team, rating) in teams.waiting {
|
|
article {
|
|
details {
|
|
summary {
|
|
em data-tooltip=(t!("state_waiting")) { (t!("state_waiting_icon")) " "}
|
|
(team.name)
|
|
small {
|
|
" ("
|
|
(t!("since_time", time=rating.local_time_arrived_at()))
|
|
")"
|
|
}
|
|
"✏️"
|
|
a href=(format!("/s/{id}/{code}/team-starting/{}", team.id)) {
|
|
button { (t!("team_starting")) }
|
|
}
|
|
}
|
|
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=(t!("save_notes"));
|
|
}
|
|
a href=(format!("/s/{id}/{code}/remove-waiting/{}", team.id))
|
|
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_waiting", team=team.name))) {
|
|
"🗑️"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@if !teams.left_not_yet_rated.is_empty() {
|
|
h2 { (t!("to_rate")) }
|
|
article class="warning" {
|
|
@if teams.left_not_yet_rated.len() == 1 {
|
|
(t!("info_single_team_not_yet_rated"))
|
|
} @else {
|
|
(t!("info_multiple_teams_not_yet_rated"))
|
|
}
|
|
}
|
|
@for (team, rating) in teams.left_not_yet_rated {
|
|
article {
|
|
em data-tooltip=(t!("state_to_rate")) { (t!("state_to_rate_icon")) " " }
|
|
(team.name)
|
|
small {
|
|
" ("
|
|
(t!("left_at", time=rating.local_time_left()))
|
|
")"
|
|
}
|
|
form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" {
|
|
label {
|
|
@if let Some(points) = rating.points {
|
|
span { (points) " " (t!("points")) }
|
|
input type="range" name="points" min="0" max="10" value=(points)
|
|
onchange=(format!("if(!confirm('{}')) {{ this.value = this.defaultValue; this.previousElementSibling.textContent = this.defaultValue + ' Punkte'; }}", t!("confirm_rating_change")))
|
|
oninput=(format!("this.previousElementSibling.textContent = this.value + ' {}'", t!("points"))) {}
|
|
} @else {
|
|
span { "0 " (t!("points")) }
|
|
input type="range" name="points" min="0" max="10" value="0" oninput="this.previousElementSibling.textContent = this.value + ' Punkte'" {}
|
|
}
|
|
|
|
}
|
|
label {
|
|
(t!("notes"))
|
|
@if let Some(notes) = &rating.notes {
|
|
input type="text" name="notes" value=(notes);
|
|
} @else {
|
|
input type="text" name="notes";
|
|
}
|
|
}
|
|
input type="submit" value=(t!("save_notes"));
|
|
}
|
|
a href=(format!("/s/{id}/{code}/remove-left/{}", team.id))
|
|
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_finished"))) {
|
|
"🗑️"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
h2 { (t!("history")) }
|
|
@if !teams.left_and_rated.is_empty() {
|
|
@for (team, rating) in teams.left_and_rated {
|
|
article {
|
|
details {
|
|
summary {
|
|
em data-tooltip=(t!("state_rated")) { (t!("state_rated_icon")) " " }
|
|
(team.name)
|
|
(PreEscaped(" → "))
|
|
(rating.points.unwrap())
|
|
" "
|
|
(t!("points"))
|
|
}
|
|
small {
|
|
" ("
|
|
(t!("arrived_at_started_at_left_at", arrived=rating.local_time_arrived_at(), active=rating.local_time_doing(), left=rating.local_time_left()))
|
|
")"
|
|
}
|
|
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=(format!("if(!confirm('{}')) {{ this.value = this.defaultValue; this.previousElementSibling.textContent = this.defaultValue + ' Punkte'; }}", t!("confirm_rating_change")))
|
|
oninput=(format!("this.previousElementSibling.textContent = this.value + ' {}'", t!("points"))) {}
|
|
}
|
|
|
|
}
|
|
label {
|
|
(t!("notes"))
|
|
@if let Some(notes) = &rating.notes {
|
|
input type="text" name="notes" value=(notes);
|
|
} @else {
|
|
input type="text" name="notes";
|
|
}
|
|
}
|
|
input type="submit" value=(t!("save_notes"));
|
|
}
|
|
a href=(format!("/s/{id}/{code}/remove-left/{}", team.id))
|
|
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_finished"))) {
|
|
"🗑️"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} @else {
|
|
(t!("no_teams_rated_yet"))
|
|
}
|
|
|
|
};
|
|
|
|
let use_map = station.lat.is_some() && station.lng.is_some();
|
|
|
|
partials::page(content, session, use_map).await
|
|
}
|
|
|
|
async fn ready(
|
|
State(db): State<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>,
|
|
) -> impl IntoResponse {
|
|
let Some(station) = Station::login(&db, id, &code).await else {
|
|
er!(session, t!("invalid_rating_code"));
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
station.switch_ready(&db).await;
|
|
|
|
suc!(session, t!("succ_change"));
|
|
Redirect::to(&format!("/s/{id}/{code}"))
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
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 {
|
|
er!(session, t!("invalid_rating_code"));
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
let Some(team) = Team::find_by_id(&db, form.team_id).await else {
|
|
er!(
|
|
session,
|
|
t!("cant_add_nonexisting_team_to_waiting", id = form.team_id)
|
|
);
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
match station.new_team_waiting(&db, &team).await {
|
|
Ok(()) => suc!(session, t!("team_added_to_waiting", team = team.name)),
|
|
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 {
|
|
er!(session, t!("invalid_rating_code"));
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
|
er!(
|
|
session,
|
|
t!("cant_remove_nonexisting_team_from_waiting", id = team_id)
|
|
);
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
match station.remove_team_waiting(&db, &team).await {
|
|
Ok(()) => suc!(session, t!("team_removed_from_waiting", team = team.name)),
|
|
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 {
|
|
er!(session, t!("invalid_rating_code"));
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
|
er!(
|
|
session,
|
|
t!("cant_add_nonexisting_team_to_active", id = team_id)
|
|
);
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
match station.team_starting(&db, &team).await {
|
|
Ok(()) => suc!(session, t!("team_added_to_active", team = team.name)),
|
|
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 {
|
|
er!(session, t!("invalid_rating_code"));
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
|
er!(
|
|
session,
|
|
t!("cant_remove_nonexisting_team_from_active", id = team_id)
|
|
);
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
match station.remove_team_doing(&db, &team).await {
|
|
Ok(()) => suc!(session, t!("team_removed_from_active", team = team.name)),
|
|
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 {
|
|
er!(session, t!("invalid_rating_code"));
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
|
er!(
|
|
session,
|
|
t!("cant_add_nonexisting_team_to_finished", id = team_id)
|
|
);
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
match station.team_finished(&db, &team).await {
|
|
Ok(()) => suc!(session, t!("team_added_to_finished")),
|
|
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 {
|
|
er!(session, t!("invalid_rating_code"));
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
|
er!(
|
|
session,
|
|
t!("cant_remove_nonexisting_team_from_finished", id = team_id)
|
|
);
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
match station.remove_team_left(&db, &team).await {
|
|
Ok(()) => suc!(session, t!("team_removed_from_finished")),
|
|
Err(e) => err!(session, "{e}"),
|
|
}
|
|
|
|
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 {
|
|
er!(session, t!("invalid_rating_code"));
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
|
er!(session, t!("cant_update_nonexisting_team", id = team_id));
|
|
return Redirect::to("/s/{id}/{code}");
|
|
};
|
|
|
|
match station
|
|
.team_update(&db, &team, form.points, form.notes)
|
|
.await
|
|
{
|
|
Ok(()) => suc!(session, t!("rating_updated", team = team.name)),
|
|
Err(e) => err!(session, "{e}"),
|
|
}
|
|
|
|
Redirect::to(&format!("/s/{id}/{code}"))
|
|
}
|
|
|
|
pub(super) fn routes() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/", get(view))
|
|
.route("/ready", get(ready))
|
|
.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))
|
|
.route("/team-update/{team_id}", post(team_update))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::{router, testdb, Station};
|
|
|
|
use sqlx::SqlitePool;
|
|
|
|
use axum_test::TestServer;
|
|
|
|
#[sqlx::test]
|
|
async fn test_wrong_station() {
|
|
let pool = testdb!();
|
|
|
|
Station::create(&pool, "Teststation").await.unwrap();
|
|
|
|
let server = TestServer::new(router(pool)).unwrap();
|
|
|
|
let response = server.get("/s/1/wrong-pw").await;
|
|
|
|
response.assert_text_contains(t!("invalid_rating_code"));
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn test_correct_station() {
|
|
let pool = testdb!();
|
|
|
|
Station::create(&pool, "42-Station").await.unwrap();
|
|
let stations = Station::all(&pool).await;
|
|
let station = stations.last().unwrap();
|
|
|
|
let server = TestServer::new(router(pool)).unwrap();
|
|
|
|
let response = server.get(&format!("/s/1/{}", station.pw)).await;
|
|
|
|
response.assert_text_contains("42-Station");
|
|
}
|
|
}
|