feature: crewless stations; Fixes #2
All checks were successful
CI/CD Pipeline / test (push) Successful in 5m38s
CI/CD Pipeline / deploy (push) Successful in 3m58s

This commit is contained in:
Philipp Hofer 2025-04-20 22:43:41 +02:00
parent fb867ff6f2
commit f5a2901b16
6 changed files with 116 additions and 42 deletions

View File

@ -45,6 +45,11 @@ async fn highscore(State(db): State<Arc<SqlitePool>>, session: Session) -> Marku
td { td {
a href=(format!("/admin/station/{}", station.id)){ a href=(format!("/admin/station/{}", station.id)){
(station.name) (station.name)
@if station.crewless() {
em data-tooltip="Station ohne Stationsbetreuer" data-placement="bottom" {
small { "🤖" }
}
}
} }
} }
} }

View File

@ -150,6 +150,22 @@ DROP TABLE temp_pos;",
.unwrap() .unwrap()
} }
pub(crate) async fn crewless_stations(&self, db: &SqlitePool) -> Vec<Station> {
self.stations(db)
.await
.into_iter()
.filter(|s| s.crewless())
.collect()
}
pub(crate) async fn crewful_stations(&self, db: &SqlitePool) -> Vec<Station> {
self.stations(db)
.await
.into_iter()
.filter(|s| !s.crewless())
.collect()
}
async fn stations_not_in_route(&self, db: &SqlitePool) -> Vec<Station> { async fn stations_not_in_route(&self, db: &SqlitePool) -> Vec<Station> {
// TODO: switch to macro // TODO: switch to macro
sqlx::query_as::<_, Station>( sqlx::query_as::<_, Station>(
@ -200,6 +216,7 @@ DROP TABLE temp_pos;",
JOIN route_station rs ON s.id = rs.station_id JOIN route_station rs ON s.id = rs.station_id
LEFT JOIN 'team' g ON s.id = g.first_station_id LEFT JOIN 'team' g ON s.id = g.first_station_id
WHERE rs.route_id = {} WHERE rs.route_id = {}
AND (s.amount_people != 0 OR s.amount_people is NULL)
GROUP BY s.id GROUP BY s.id
ORDER BY team_count ASC, s.id ASC ORDER BY team_count ASC, s.id ASC
LIMIT 1", LIMIT 1",

View File

@ -1,12 +1,12 @@
use super::Route; use super::Route;
use crate::{AppState, admin::station::Station, err, page, succ}; use crate::{admin::station::Station, err, page, succ, AppState};
use axum::{ use axum::{
Form, Router,
extract::State, extract::State,
response::{IntoResponse, Redirect}, response::{IntoResponse, Redirect},
routing::{get, post}, routing::{get, post},
Form, Router,
}; };
use maud::{Markup, PreEscaped, html}; use maud::{html, Markup, PreEscaped};
use serde::Deserialize; use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use std::sync::Arc; use std::sync::Arc;
@ -162,6 +162,11 @@ async fn view(
@for (idx, station) in cur_stations.into_iter().enumerate() { @for (idx, station) in cur_stations.into_iter().enumerate() {
li { li {
(station.name) (station.name)
@if station.crewless() {
em data-tooltip="Station ohne Stationsbetreuer" {
small { "🤖" }
}
}
@if idx > 0 { @if idx > 0 {
a href=(format!("/admin/route/{}/move-station-higher/{}", route.id, station.id)){ a href=(format!("/admin/route/{}/move-station-higher/{}", route.id, station.id)){
em data-tooltip=(format!("{} nach vor reihen", station.name)) { em data-tooltip=(format!("{} nach vor reihen", station.name)) {

View File

@ -16,7 +16,7 @@ pub(crate) struct Station {
pub(crate) id: i64, pub(crate) id: i64,
pub(crate) name: String, pub(crate) name: String,
notes: Option<String>, notes: Option<String>,
amount_people: Option<i64>, pub(crate) amount_people: Option<i64>,
last_login: Option<NaiveDateTime>, // TODO use proper timestamp (NaiveDateTime?) last_login: Option<NaiveDateTime>, // TODO use proper timestamp (NaiveDateTime?)
pub(crate) pw: String, pub(crate) pw: String,
pub(crate) ready: bool, pub(crate) ready: bool,
@ -45,6 +45,13 @@ impl Station {
.ok() .ok()
} }
pub fn crewless(&self) -> bool {
if let Some(amount_people) = self.amount_people {
return amount_people == 0;
}
false
}
pub async fn login(db: &SqlitePool, id: i64, code: &str) -> Option<Self> { pub async fn login(db: &SqlitePool, id: i64, code: &str) -> Option<Self> {
let station = sqlx::query_as!( let station = sqlx::query_as!(
Self, Self,

View File

@ -126,6 +126,7 @@ async fn view(
} }
} }
} }
@if !station.crewless() {
tr { tr {
th scope="row" { "Stations-Link" }; th scope="row" { "Stations-Link" };
td { td {
@ -138,6 +139,7 @@ async fn view(
} }
} }
} }
}
tr { tr {
th scope="row" { "Anzahl Stationsbetreuer" }; th scope="row" { "Anzahl Stationsbetreuer" };
td { td {
@ -161,6 +163,7 @@ async fn view(
} }
} }
} }
@if !station.crewless() {
tr { tr {
th scope="row" { "Letzter Zugriff eines Stationsbetreuers" }; th scope="row" { "Letzter Zugriff eines Stationsbetreuers" };
td { td {
@ -172,6 +175,7 @@ async fn view(
} }
} }
} }
}
@if !ratings.is_empty() { @if !ratings.is_empty() {
h2 { "Bewertungen" } h2 { "Bewertungen" }
@ -672,6 +676,11 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
a href=(format!("/admin/station/{}", station.id)){ a href=(format!("/admin/station/{}", station.id)){
(station.name) (station.name)
} }
@if station.crewless() {
em data-tooltip="Station ohne Stationsbetreuer" {
small { "🤖" }
}
}
} }
td { 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())) { 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())) {

View File

@ -95,29 +95,14 @@ async fn delete(
Redirect::to("/admin/team") Redirect::to("/admin/team")
} }
async fn quick( async fn quick(db: Arc<SqlitePool>, team: &Team, stations: Vec<Station>, redirect: &str) -> Markup {
State(db): State<Arc<SqlitePool>>, html! {
session: Session,
axum::extract::Path(id): axum::extract::Path<i64>,
) -> Result<Markup, impl IntoResponse> {
let Some(team) = Team::find_by_id(&db, id).await else {
err!(
session,
"Team mit ID {id} konnte nicht geöffnet werden, da sie nicht existiert"
);
return Err(Redirect::to("/admin/team"));
};
let stations = team.route(&db).await.stations(&db).await;
// maybe switch to maud-display impl of team
let content = html! {
h1 { h1 {
a href=(format!("/admin/team/{}", team.id)) { "↩️" } a href=(format!("/admin/team/{}", team.id)) { "↩️" }
"Bewertungen Team " (team.name) "Bewertungen Team " (team.name)
} }
form action=(format!("/admin/team/{}/quick", team.id)) method="post" { form action=(format!("/admin/team/{}/quick", team.id)) method="post" {
input type="hidden" name="redirect" value=(redirect);
table { table {
thead { thead {
tr { tr {
@ -155,12 +140,53 @@ async fn quick(
} }
input type="submit" value="Speichern"; input type="submit" value="Speichern";
} }
}
}
async fn quick_crewless(
State(db): State<Arc<SqlitePool>>,
session: Session,
axum::extract::Path(id): axum::extract::Path<i64>,
) -> Result<Markup, impl IntoResponse> {
let Some(team) = Team::find_by_id(&db, id).await else {
err!(
session,
"Team mit ID {id} konnte nicht geöffnet werden, da sie nicht existiert"
);
return Err(Redirect::to("/admin/team"));
}; };
let stations: Vec<Station> = 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<Arc<SqlitePool>>,
session: Session,
axum::extract::Path(id): axum::extract::Path<i64>,
) -> Result<Markup, impl IntoResponse> {
let Some(team) = Team::find_by_id(&db, id).await else {
err!(
session,
"Team mit ID {id} konnte nicht geöffnet werden, da sie nicht existiert"
);
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) Ok(page(content, session, true).await)
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct QuickUpdate { struct QuickUpdate {
redirect: String,
#[serde(flatten)] #[serde(flatten)]
fields: HashMap<String, String>, fields: HashMap<String, String>,
} }
@ -228,7 +254,7 @@ async fn quick_post(
succ!(session, "Erfolgreich {amount_succ} Bewertungen eingetragen"); succ!(session, "Erfolgreich {amount_succ} Bewertungen eingetragen");
} }
Redirect::to(&format!("/admin/team/{id}/quick")) Redirect::to(&format!("/admin/team/{id}/quick{}", form.redirect))
} }
async fn view( async fn view(
@ -247,7 +273,7 @@ async fn view(
let first_station = team.first_station(&db).await; let first_station = team.first_station(&db).await;
let routes = Route::all(&db).await; let routes = Route::all(&db).await;
let stations = team.route(&db).await.stations(&db).await; let stations = team.route(&db).await.crewful_stations(&db).await;
// maybe switch to maud-display impl of team // maybe switch to maud-display impl of team
let content = html! { let content = html! {
@ -378,9 +404,13 @@ async fn view(
} }
a href=(format!("/admin/team/{}/quick", team.id)){ a href=(format!("/admin/team/{}/quick", team.id)){
button { button {
"Stations-Bewertungen für Team " "Bewertungen"
(team.name) }
" eingeben" }
hr;
a href=(format!("/admin/team/{}/quick/crewless", team.id)){
button {
"Unbemannte Bewertungen"
} }
} }
}; };
@ -743,7 +773,8 @@ pub(super) fn routes() -> Router<AppState> {
.route("/lost", get(lost)) .route("/lost", get(lost))
.route("/{id}", get(view)) .route("/{id}", get(view))
.route("/{id}/delete", get(delete)) .route("/{id}/delete", get(delete))
.route("/{id}/quick", get(quick)) .route("/{id}/quick", get(quick_all))
.route("/{id}/quick/crewless", get(quick_crewless))
.route("/{id}/quick", post(quick_post)) .route("/{id}/quick", post(quick_post))
.route("/{id}/name", post(update_name)) .route("/{id}/name", post(update_name))
.route("/{id}/notes", post(update_notes)) .route("/{id}/notes", post(update_notes))