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 {
a href=(format!("/admin/station/{}", station.id)){
(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()
}
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> {
// TODO: switch to macro
sqlx::query_as::<_, Station>(
@ -200,6 +216,7 @@ DROP TABLE temp_pos;",
JOIN route_station rs ON s.id = rs.station_id
LEFT JOIN 'team' g ON s.id = g.first_station_id
WHERE rs.route_id = {}
AND (s.amount_people != 0 OR s.amount_people is NULL)
GROUP BY s.id
ORDER BY team_count ASC, s.id ASC
LIMIT 1",

View File

@ -1,12 +1,12 @@
use super::Route;
use crate::{AppState, admin::station::Station, err, page, succ};
use crate::{admin::station::Station, err, page, succ, AppState};
use axum::{
Form, Router,
extract::State,
response::{IntoResponse, Redirect},
routing::{get, post},
Form, Router,
};
use maud::{Markup, PreEscaped, html};
use maud::{html, Markup, PreEscaped};
use serde::Deserialize;
use sqlx::SqlitePool;
use std::sync::Arc;
@ -162,6 +162,11 @@ async fn view(
@for (idx, station) in cur_stations.into_iter().enumerate() {
li {
(station.name)
@if station.crewless() {
em data-tooltip="Station ohne Stationsbetreuer" {
small { "🤖" }
}
}
@if idx > 0 {
a href=(format!("/admin/route/{}/move-station-higher/{}", route.id, station.id)){
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) name: String,
notes: Option<String>,
amount_people: Option<i64>,
pub(crate) amount_people: Option<i64>,
last_login: Option<NaiveDateTime>, // TODO use proper timestamp (NaiveDateTime?)
pub(crate) pw: String,
pub(crate) ready: bool,
@ -45,6 +45,13 @@ impl Station {
.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> {
let station = sqlx::query_as!(
Self,

View File

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

View File

@ -95,29 +95,14 @@ async fn delete(
Redirect::to("/admin/team")
}
async fn quick(
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;
// maybe switch to maud-display impl of team
let content = html! {
async fn quick(db: Arc<SqlitePool>, team: &Team, stations: Vec<Station>, redirect: &str) -> Markup {
html! {
h1 {
a href=(format!("/admin/team/{}", team.id)) { "↩️" }
"Bewertungen Team " (team.name)
}
form action=(format!("/admin/team/{}/quick", team.id)) method="post" {
input type="hidden" name="redirect" value=(redirect);
table {
thead {
tr {
@ -155,12 +140,53 @@ async fn quick(
}
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)
}
#[derive(Deserialize, Debug)]
struct QuickUpdate {
redirect: String,
#[serde(flatten)]
fields: HashMap<String, String>,
}
@ -228,7 +254,7 @@ async fn quick_post(
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(
@ -247,7 +273,7 @@ async fn view(
let first_station = team.first_station(&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
let content = html! {
@ -378,9 +404,13 @@ async fn view(
}
a href=(format!("/admin/team/{}/quick", team.id)){
button {
"Stations-Bewertungen für Team "
(team.name)
" eingeben"
"Bewertungen"
}
}
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("/{id}", get(view))
.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}/name", post(update_name))
.route("/{id}/notes", post(update_notes))