use super::{CreateError, LastContactTeam, Team}; use crate::{ admin::{route::Route, station::Station}, er, models::rating::Rating, partials::page, suc, AppState, }; 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::{collections::HashMap, sync::Arc}; use tower_sessions::Session; #[derive(Deserialize)] struct CreateForm { name: String, route_id: i64, } async fn create( State(db): State>, session: Session, Form(form): Form, ) -> impl IntoResponse { let Some(route) = Route::find_by_id(&db, form.route_id).await else { er!(session, t!("nonexisting_route", id = form.route_id)); return Redirect::to("/admin/team"); }; let id = match Team::create(&db, &form.name, &route).await { Ok(id) => { suc!(session, t!("team_created", team = form.name)); id } Err(CreateError::DuplicateName(e)) => { er!( session, t!("team_not_created_duplicate_name", team = form.name, err = e) ); return Redirect::to("/admin/team"); } Err(CreateError::NoStationForRoute) => { er!( session, t!( "team_not_created_no_station_in_route", team = form.name, route = route.name ) ); return Redirect::to("/admin/team"); } }; Redirect::to(&format!("/admin/team/{id}")) } async fn delete( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, ) -> impl IntoResponse { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Redirect::to("/admin/team"); }; match team.delete(&db).await { Ok(()) => suc!(session, t!("team_deleted", team = team.name)), Err(e) => er!( session, t!("team_not_deleted_already_in_use", team = team.name, err = e) ), } Redirect::to("/admin/team") } async fn quick(db: Arc, team: &Team, stations: Vec, redirect: &str) -> Markup { html! { h1 { a href=(format!("/admin/team/{}", team.id)) { "↩️" } (t!("ratings")) " " (t!("team")) " " (team.name) } form action=(format!("/admin/team/{}/quick", team.id)) method="post" { input type="hidden" name="redirect" value=(redirect); table { thead { tr { th { (t!("station")) } th { (t!("points")) } } } tbody { @for station in &stations { tr { td { a href=(format!("/admin/station/{}", station.id)) { (station.name) } } td { @if let Some(rating) = Rating::find_by_team_and_station(&db, team, station).await { a href=(format!("/s/{}/{}", station.id, station.pw)){ @if let Some(points) = rating.points { em data-tooltip=(t!("already_entered")) { (points) } } @else { em data-tooltip=(t!("team_currently_at_station")) { "?" } } } } @else { input type="number" min="0" max="10" name=(station.id); } } } } } } input type="submit" value=(t!("save")); } } } async fn quick_crewless( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, ) -> Result { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Err(Redirect::to("/admin/team")); }; let stations: Vec = team.route(&db).await.crewless_stations(&db).await; let content = quick(db, &team, stations, "/crewless").await; Ok(page(content, session, true).await) } async fn quick_all( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, ) -> Result { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Err(Redirect::to("/admin/team")); }; let stations = team.route(&db).await.stations(&db).await; let content = quick(db, &team, stations, "").await; Ok(page(content, session, true).await) } #[derive(Deserialize, Debug)] struct QuickUpdate { redirect: String, #[serde(flatten)] fields: HashMap, } async fn quick_post( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, Form(form): Form, ) -> impl IntoResponse { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Redirect::to("/admin/team"); }; let mut ret = String::new(); let mut amount_succ = 0; for (station_id, points) in &form.fields { let Ok(station_id) = station_id.parse::() else { ret.push_str(&format!( "Skipped stationid={station_id} because this id can't be parsed as i64" )); continue; }; let Ok(points) = points.parse::() else { ret.push_str(&format!( "Skipped stationid={station_id} because points {points} can't be parsed as i64", )); continue; }; let Some(station) = Station::find_by_id(&db, station_id).await else { ret.push_str(&format!( "Skipped stationid={station_id} because this station does not exist" )); continue; }; if Rating::find_by_team_and_station(&db, &team, &station) .await .is_some() { let msg: String = t!( "error_rating_team_already_rated", team = team.name, station = station.name ) .into(); ret.push_str(&msg); continue; } Rating::create_quick(&db, &team, &station, points).await; amount_succ += 1; } if !ret.is_empty() { // TODO: properly log warnings println!("{ret}"); } if amount_succ == 0 { suc!(session, t!("funny_you_entered_no_rating")); } else { suc!(session, t!("entered_n_ratings", amount = amount_succ)); } Redirect::to(&format!("/admin/team/{id}/quick{}", form.redirect)) } async fn view( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, ) -> Result { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Err(Redirect::to("/admin/team")); }; let first_station = team.first_station(&db).await; let last_station = team.last_station(&db).await; let routes = Route::all(&db).await; let stations = team.route(&db).await.crewful_stations(&db).await; // maybe switch to maud-display impl of team let content = html! { h1 { a href="/admin/team" { "↩️" } (t!("team")) " " (team.name) } article { details { summary { (t!("edit_teamname")) " ✏️" } form action=(format!("/admin/team/{}/name", team.id)) method="post" { input type="text" name="name" value=(team.name) required; input type="submit" value=(t!("save")); } } } table { tbody { tr { th scope="row" { (t!("notes")) }; td { @match &team.notes { Some(notes) => { (notes) details { summary { "✏️" } form action=(format!("/admin/team/{}/notes", team.id)) method="post" { textarea name="notes" required rows="10" { (notes) }; input type="submit" value=(t!("save")); } } }, None => details { summary { (t!("add_new_note")) } form action=(format!("/admin/team/{}/notes", team.id)) method="post" { textarea name="notes" required rows="10" {}; input type="submit" value=(t!("save")); } } } } } tr { th scope="row" { (t!("amount_teammembers")) }; td { @match team.amount_people { Some(amount) => (amount), None => "?", } details { summary { "✏️" } form action=(format!("/admin/team/{}/amount-people", team.id)) method="post" { input type="number" name="amount_people" min="0" max="10"; input type="submit" value=(t!("save")); } a href=(format!("/admin/team/{}/amount-people-reset", team.id)) { button class="error" { em data-tooltip=(t!("not_sure_about_amount_team")) { "?" } } } } } } tr { th scope="row" { (t!("route")) }; td { a href=(format!("/admin/route/{}", &team.route(&db).await.id)) { (&team.route(&db).await.name) } @if routes.len() > 1 { details { summary { "✏️" } form action=(format!("/admin/team/{}/update-route", team.id)) method="post" { select name="route_id" aria-label=(t!("select_route")) required { @for route in &routes { @if route.id != team.route(&db).await.id { option value=(route.id) { (route.name) } } } } input type="submit" value=(t!("save")); } } } } } tr { th scope="row" { (t!("first_station")) article { (t!("first_station_expl")) } }; td { a href=(format!("/admin/station/{}", first_station.id)) { (first_station.name) } @if stations.len() > 1 { details { summary { "✏️" } form action=(format!("/admin/team/{}/update-first-station", team.id)) method="post" { select name="first_station_id" aria-label=(t!("select_station")) required { @for station in &stations { @if station.id != first_station.id { option value=(station.id) { (station.name) @let amount_start_teams = Team::all_with_first_station(&db, station).await.len(); @if amount_start_teams > 0 { @if amount_start_teams == 1 { (t!("already_has_1_start_team")) }@else{ (t!("already_has_n_start_team", amount=amount_start_teams)) } } } } } } input type="submit" value=(t!("save")); } } } } } @if let Some(last_station) = last_station { tr { th scope="row" { (t!("last_station")) }; td { a href=(format!("/admin/station/{}", last_station.id)) { (last_station.name) } @if stations.len() > 1 { details { summary { "✏️" } form action=(format!("/admin/team/{}/update-last-station", team.id)) method="post" { select name="last_station_id" aria-label=(t!("select_station")) required { @for station in &stations { @if station.id != last_station.id { option value=(station.id) { (station.name) } } } } input type="submit" value=(t!("save")); } } } } } } } } a href=(format!("/admin/team/{}/quick", team.id)){ button { (t!("ratings")) } } hr; a href=(format!("/admin/team/{}/quick/crewless", team.id)){ button { (t!("rate_crewless_stations")) } } }; Ok(page(content, session, true).await) } #[derive(Deserialize)] struct UpdateNameForm { name: String, } async fn update_name( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, Form(form): Form, ) -> impl IntoResponse { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Redirect::to("/admin/team"); }; team.update_name(&db, &form.name).await; suc!( session, t!("new_team_name", old = team.name, new = form.name) ); Redirect::to(&format!("/admin/team/{id}")) } #[derive(Deserialize)] struct UpdateNotesForm { notes: String, } async fn update_notes( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, Form(form): Form, ) -> impl IntoResponse { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Redirect::to("/admin/team"); }; team.update_notes(&db, &form.notes).await; suc!(session, t!("notes_edited", team = team.name)); Redirect::to(&format!("/admin/team/{id}")) } #[derive(Deserialize)] struct UpdateAmountPeopleForm { amount_people: i64, } async fn update_amount_people( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, Form(form): Form, ) -> impl IntoResponse { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Redirect::to("/admin/team"); }; team.update_amount_people(&db, form.amount_people).await; suc!(session, t!("amount_teammembers_edited", team = team.name)); Redirect::to(&format!("/admin/team/{id}")) } #[derive(Deserialize)] struct UpdateRouteForm { route_id: i64, } async fn update_route( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, Form(form): Form, ) -> impl IntoResponse { // TODO: move sanity checks into mod.rs let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Redirect::to("/admin/team"); }; let Some(route) = Route::find_by_id(&db, form.route_id).await else { er!(session, t!("nonexisting_route", id = form.route_id)); return Redirect::to(&format!("/admin/team/{id}")); }; match team.update_route(&db, &route).await { Ok(new_first_station_name) => suc!( session, t!( "route_edited", team = team.name, route = route.name, first_station = new_first_station_name ) ), Err(()) => er!( session, t!( "team_not_edited_route_has_no_stations", team = team.name, route = route.name ) ), } Redirect::to(&format!("/admin/team/{id}")) } #[derive(Deserialize)] struct UpdateFirstStationForm { first_station_id: i64, } async fn update_first_station( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, Form(form): Form, ) -> impl IntoResponse { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Redirect::to("/admin/team"); }; let Some(station) = Station::find_by_id(&db, form.first_station_id).await else { er!( session, t!("nonexisting_station", id = form.first_station_id) ); return Redirect::to(&format!("/admin/team/{id}")); }; if !station.is_in_route(&db, &team.route(&db).await).await { er!( session, t!( "first_station_not_edited_not_on_route", station = station.name, team = team.name, route = team.route(&db).await.name ) ); return Redirect::to(&format!("/admin/team/{id}")); } team.update_first_station(&db, &station).await; suc!( session, t!( "changed_first_station", team = team.name, station = station.name ) ); Redirect::to(&format!("/admin/team/{id}")) } #[derive(Deserialize)] struct UpdateLastStationForm { last_station_id: i64, } async fn update_last_station( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, Form(form): Form, ) -> impl IntoResponse { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Redirect::to("/admin/team"); }; let Some(station) = Station::find_by_id(&db, form.last_station_id).await else { er!( session, t!("nonexisting_station", id = form.last_station_id) ); return Redirect::to(&format!("/admin/team/{id}")); }; if !station.is_in_route(&db, &team.route(&db).await).await { er!( session, t!( "last_station_not_edited_not_on_route", station = station.name, team = team.name, route = team.route(&db).await.name ) ); return Redirect::to(&format!("/admin/team/{id}")); } team.update_last_station(&db, &station).await; suc!( session, t!( "changed_last_station", team = team.name, station = station.name ) ); Redirect::to(&format!("/admin/team/{id}")) } async fn update_amount_people_reset( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, ) -> impl IntoResponse { let Some(team) = Team::find_by_id(&db, id).await else { er!(session, t!("nonexisting_team", id = id)); return Redirect::to("/admin/team"); }; team.update_amount_people_reset(&db).await; suc!(session, t!("amount_teammembers_edited", team = team.name)); Redirect::to(&format!("/admin/team/{id}")) } async fn lost(State(db): State>, session: Session) -> Markup { let losts = LastContactTeam::all_sort_missing(&db).await; let content = html! { h1 { a href="/admin/team" { "↩️" } (t!("last_contact_team")) } div class="overflow-auto" { table { thead { tr { td { (t!("team")) } td { (t!("time")) } td { (t!("station")) } } } tbody { @for lost in &losts { tr { td { a href=(format!("/admin/team/{}", lost.team.id)) { (lost.team.name) } } td { @if let Some(time) = lost.local_last_contact() { (time) }@else{ (t!("not_yet_seen")) } } td { @if let Some(station) = &lost.station { a href=(format!("/admin/station/{}", station.id)) { (station.name) } }@else{ (t!("not_yet_seen")) } } } } } } } }; page(content, session, false).await } async fn index(State(db): State>, session: Session) -> Markup { let teams = Team::all(&db).await; let routes = Route::all(&db).await; let content = html! { h1 { a href="/admin" { "↩️" } (t!("teams")) } article { em { (t!("teams")) } "sind eine Menge an Personen, die verschiedene " a href="/admin/station" { "Stationen" } " ablaufen. Welche Stationen, entscheidet sich je nachdem, welcher " a href="/admin/route" { "Route" } " sie zugewiesen sind." } @if teams.is_empty() { article class="warning" { (t!("no_teams")) @if !routes.is_empty() { (t!("change_that_below")) } } } article { h2 { (t!("new_team")) } @if routes.is_empty() { article class="error" { (t!("route_needed_before_creating_teams")) (PreEscaped(" → ")) a role="button" href="/admin/route" { (t!("create_route")) } } } @else { form action="/admin/team" method="post" { @if routes.len() == 1 { fieldset role="group" { input type="text" name="name" placeholder=(t!("teamname")) required; input type="hidden" name="route_id" value=(routes[0].id) ; input type="submit" value="Neues Team"; } } @else { input type="text" name="name" placeholder=(t!("teamname")) required; select name="route_id" aria-label=(t!("select_route")) required { @for route in &routes { option value=(route.id) { (route.name) } } } input type="submit" value=(t!("new_team")); } } } } a href="/admin/team/lost" { button class="outline" { (t!("have_i_lost_groups")) } } @for route in &routes { h2 { (route.name) } ol { @for team in &route.teams(&db).await{ li { a href=(format!("/admin/team/{}", team.id)){ (team.name) } a href=(format!("/admin/team/{}/delete", team.id)) onclick=(format!("return confirm('{}');", t!("confirm_delete_team"))) { "🗑️" } } } } } }; page(content, session, false).await } pub(super) fn routes() -> Router { Router::new() .route("/", get(index)) .route("/", post(create)) .route("/lost", get(lost)) .route("/{id}", get(view)) .route("/{id}/delete", get(delete)) .route("/{id}/quick", get(quick_all)) .route("/{id}/quick/crewless", get(quick_crewless)) .route("/{id}/quick", post(quick_post)) .route("/{id}/name", post(update_name)) .route("/{id}/notes", post(update_notes)) .route("/{id}/amount-people", post(update_amount_people)) .route("/{id}/amount-people-reset", get(update_amount_people_reset)) .route("/{id}/update-route", post(update_route)) .route("/{id}/update-first-station", post(update_first_station)) .route("/{id}/update-last-station", post(update_last_station)) }