574 lines
19 KiB
Rust
574 lines
19 KiB
Rust
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<Arc<SqlitePool>>,
|
|
session: Session,
|
|
Form(form): Form<CreateForm>,
|
|
) -> 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<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path(id): axum::extract::Path<i64>,
|
|
) -> 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<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path(id): axum::extract::Path<i64>,
|
|
) -> Result<Markup, impl IntoResponse> {
|
|
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<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path(id): axum::extract::Path<i64>,
|
|
Form(form): Form<UpdateNameForm>,
|
|
) -> 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<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path(id): axum::extract::Path<i64>,
|
|
Form(form): Form<UpdateNotesForm>,
|
|
) -> 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<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path(id): axum::extract::Path<i64>,
|
|
Form(form): Form<UpdateAmountPeopleForm>,
|
|
) -> 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<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path(id): axum::extract::Path<i64>,
|
|
) -> 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<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path(id): axum::extract::Path<i64>,
|
|
Form(form): Form<UpdateLocationForm>,
|
|
) -> 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<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path(id): axum::extract::Path<i64>,
|
|
) -> 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<Arc<SqlitePool>>, 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<AppState> {
|
|
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))
|
|
}
|