use crate::{ admin::station::Station, er, err, models::rating::{Rating, TeamsAtStationLocation}, partials::page, suc, succ, AppState, }; use axum::{ extract::State, response::{IntoResponse, Redirect}, routing::{get, post}, Form, Router, }; use maud::{html, Markup}; use serde::Deserialize; use sqlx::SqlitePool; use std::sync::Arc; use tower_sessions::Session; #[derive(Deserialize)] struct CreateForm { name: String, } async fn create( State(db): State>, session: Session, Form(form): Form, ) -> impl IntoResponse { match Station::create(&db, &form.name).await { Ok(()) => suc!(session, t!("station_create_succ", name = form.name)), Err(e) => er!( session, t!( "station_create_err_duplicate_name", name = form.name, err = e ) ), } Redirect::to("/admin/station") } async fn delete( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, ) -> impl IntoResponse { let Some(station) = Station::find_by_id(&db, id).await else { er!(session, t!("station_delete_err_nonexisting", id = id)); return Redirect::to("/admin/station"); }; match station.delete(&db).await { Ok(()) => suc!(session, t!("station_delete_succ", name = station.name)), Err(e) => er!( session, t!( "station_delete_err_already_used", name = station.name, err = e ) ), } Redirect::to("/admin/station") } async fn view( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, ) -> Result { let Some(station) = Station::find_by_id(&db, id).await else { err!( session, "Station mit ID {id} konnte nicht geöffnet werden, da sie nicht existiert" ); return Err(Redirect::to("/admin/station")); }; let ratings = Rating::for_station(&db, &station).await; // maybe switch to maud-display impl of station let content = html! { h1 { a href="/admin/station" { "↩️" } "Station " (station.name) } article { details { summary { "Stationsname bearbeiten ✏️" } form action=(format!("/admin/station/{}/name", station.id)) method="post" { input type="text" name="name" value=(station.name) required; input type="submit" value="Speichern"; } } } table { tbody { tr { th scope="row" { "Notizen" }; td { @match station.notes { Some(ref notes) => { (notes) details { summary { "✏️" } form action=(format!("/admin/station/{}/notes", station.id)) method="post" { textarea name="notes" required rows="10" { (notes) }; input type="submit" value="Speichern"; } } }, None => details { summary { "Neue Notiz hinzufügen" } form action=(format!("/admin/station/{}/notes", station.id)) method="post" { textarea name="notes" required rows="10" {}; input type="submit" value="Speichern"; } } } } } tr { th scope="row" { "Stations-Link" }; td { a href=(format!("/s/{}/{}", station.id, station.pw)) { "Login-Link" } article class="warning" { (format!("Diesen Link nur Betreuern der Station {} geben! Mit diesem Link erhält man die Berechtigung, Teams zu bewerten.", station.name)) } } } tr { th scope="row" { "Anzahl Stationsbetreuer" }; td { @match station.amount_people { Some(amount) => (amount), None => "?", } details { summary { "✏️" } form action=(format!("/admin/station/{}/amount-people", station.id)) method="post" { input type="number" name="amount_people" min="0" max="10"; input type="submit" value="Speichern"; } button class="error" { a href=(format!("/admin/station/{}/amount-people-reset", station.id)) { em data-tooltip="Ich weiß noch nicht wv. Personen benötigt werden." { "?" } } } } } } tr { th scope="row" { "Letzter Zugriff eines Stationsbetreuers" }; td { @match station.local_last_login() { Some(last_login) => (last_login), None => "noch nicht eingeloggt :-(", } } } } } @if !ratings.is_empty() { h2 { "Bewertungen" } div class="overflow-auto" { table { thead { tr { th { "Team" } th { "Punkte" } th { "Notizen" } th { em data-placement="bottom" data-tooltip="Angekommen" { "👋" } } th { em data-placement="bottom" data-tooltip="Begonnen" { "🎬" } } th { em data-placement="bottom" data-tooltip="Gegangen" { "🚶‍♂️" } } } } tbody { @for rating in ratings { tr { td { a href=(format!("/admin/team/{}", rating.team_id)) { (rating.team(&db).await.name) } } td { @if let Some(points) = rating.points { (points) } } td { @if let Some(ref notes) = rating.notes{ (notes) } } td { (rating.local_time_arrived_at()) } td { (rating.local_time_doing()) } td { (rating.local_time_left()) } } } } } } } @if let (Some(_), Some(_)) = (station.lat, station.lng) { article { "Um einen neuen Standort zu wählen, auf einen Punkt in der Karte klicken" } } @else{ article { "Um einen Standort zu wählen, auf einen Punkt in der Karte klicken" } } @if station.lat.is_some() && station.lng.is_some() { a href=(format!("/admin/station/{}/location-clear", station.id)) onclick="return confirm('Bist du sicher, dass du den Standort der Station löschen willst?');"{ "Standort löschen" } } form action=(format!("/admin/station/{}/location", station.id)) method="post" { input type="hidden" name="lat" id="lat"; input type="hidden" name="lng" id="lng"; input type="submit" value="Neuen Standort speichern" style="display: None;" id="location-submit"; } div id="map" style="height: 500px" {} script { r#" const map = L.map('map').setView([48.511445, 14.505301], 14); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); // Variable to store the current marker let currentMarker = null; map.on('click', function(e) { const lat = e.latlng.lat; const lng = e.latlng.lng; // Update the coordinates display if (currentMarker !== null) { map.removeLayer(currentMarker); } const myIcon = L.icon({ iconUrl: '/marker.png', iconAnchor: [12, 41] }); document.getElementById('lat').value = lat; document.getElementById('lng').value = lng; document.getElementById('location-submit').style.display = 'block'; document.getElementById('location-submit').scrollIntoView(); currentMarker = L.marker([lat, lng], {icon: myIcon}).addTo(map); }); "# } @if let (Some(lat), Some(lng)) = (station.lat, station.lng) { script { (format!(" const lat = {lat}; const lng = {lng}; const myIcon = L.icon({{ iconUrl: '/marker.png', iconAnchor: [12, 41] }}); currentMarker = L.marker([lat, lng], {{icon: myIcon}}).addTo(map); map.setView([lat, lng], 14); ")); } } }; 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(station) = Station::find_by_id(&db, id).await else { err!( session, "Station mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert" ); return Redirect::to("/admin/station"); }; station.update_name(&db, &form.name).await; succ!( session, "Station '{}' heißt ab sofort '{}'.", station.name, form.name ); Redirect::to(&format!("/admin/station/{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(station) = Station::find_by_id(&db, id).await else { err!( session, "Station mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert" ); return Redirect::to("/admin/station"); }; station.update_notes(&db, &form.notes).await; succ!( session, "Notizen für die Station '{}' wurden erfolgreich bearbeitet!", station.name ); Redirect::to(&format!("/admin/station/{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(station) = Station::find_by_id(&db, id).await else { err!( session, "Station mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert" ); return Redirect::to("/admin/station"); }; station.update_amount_people(&db, form.amount_people).await; succ!( session, "Anzahl an Betreuer für die Station '{}' wurden erfolgreich bearbeitet!", station.name ); Redirect::to(&format!("/admin/station/{id}")) } async fn update_amount_people_reset( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, ) -> impl IntoResponse { let Some(station) = Station::find_by_id(&db, id).await else { err!( session, "Station mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert" ); return Redirect::to("/admin/station"); }; station.update_amount_people_reset(&db).await; succ!( session, "Anzahl an Betreuer für die Station '{}' wurden erfolgreich bearbeitet!", station.name ); Redirect::to(&format!("/admin/station/{id}")) } #[derive(Deserialize)] struct UpdateLocationForm { lat: f64, lng: f64, } async fn update_location( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, Form(form): Form, ) -> impl IntoResponse { let Some(station) = Station::find_by_id(&db, id).await else { err!( session, "Station mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert" ); return Redirect::to("/admin/station"); }; station.update_location(&db, form.lat, form.lng).await; succ!( session, "Standort für die Station '{}' wurden erfolgreich bearbeitet!", station.name ); Redirect::to(&format!("/admin/station/{id}")) } async fn update_location_clear( State(db): State>, session: Session, axum::extract::Path(id): axum::extract::Path, ) -> impl IntoResponse { let Some(station) = Station::find_by_id(&db, id).await else { err!( session, "Station mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert" ); return Redirect::to("/admin/station"); }; station.update_location_clear(&db).await; succ!( session, "Standort für die Station '{}' wurden erfolgreich gelöscht!", station.name ); Redirect::to(&format!("/admin/station/{id}")) } async fn index(State(db): State>, session: Session) -> Markup { let stations = Station::all(&db).await; let content = html! { h1 { a href="/admin" { "↩️" } (t!("stations")) } article { em { (t!("stations")) " " } (t!("stations_expl_without_first_word")) } table { thead { tr { th { "Station" } th { "Fortschritt" } th { "" } } } tbody { @for station in &stations { @let status = TeamsAtStationLocation::for_station(&db, station).await; tr { td { @if station.ready { em data-tooltip="Station bereit!" { small { "🟢 " } } } @if station.routes(&db).await.is_empty() { em data-tooltip=(t!("station_warning_not_assigned_route")) { "⚠️ " } } a href=(format!("/admin/station/{}", station.id)){ (station.name) } } td { em data-tooltip=(format!("{}/{} Teams (davon {} wartend + {} aktiv)", status.total_teams-status.not_yet_here.len() as i64, status.total_teams, status.waiting.len(), status.doing.len())) { progress value=(status.total_teams-status.not_yet_here.len() as i64) max=(status.total_teams) {} } } td { a href=(format!("/admin/station/{}/delete", station.id)) onclick=(format!("return confirm('{}');", t!("station_confirm_deletion"))) { "🗑️" } } } } } } @if stations.is_empty() { article class="warning" { (t!("station_hint_create_first")) } } h2 { (t!("station_new")) } form action="/admin/station" method="post" { fieldset role="group" { input type="text" name="name" placeholder=(t!("station_name")) required; input type="submit" value=(t!("station_new")); } } }; page(content, session, false).await } pub(super) fn routes() -> Router { Router::new() .route("/", get(index)) .route("/", post(create)) .route("/{id}", get(view)) .route("/{id}/delete", get(delete)) .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}/location", post(update_location)) .route("/{id}/location-clear", get(update_location_clear)) }