show stations if a team is supposed to be on their way to them
Some checks failed
CI/CD Pipeline / test (push) Successful in 5m38s
CI/CD Pipeline / deploy (push) Has been cancelled

This commit is contained in:
Philipp Hofer 2025-04-21 15:07:38 +02:00
parent b3bf50fed1
commit 4a0f6c0285
5 changed files with 144 additions and 2 deletions

View File

@ -235,6 +235,69 @@ DROP TABLE temp_pos;",
.expect("db constraints"), .expect("db constraints"),
) )
} }
pub async fn prev_station(&self, db: &SqlitePool, station: &Station) -> Option<Station> {
if station.crewless() {
return None;
}
let ret = sqlx::query_as::<_, Station>(
"
WITH RECURSIVE find_previous AS (
-- Get position of station
SELECT pos, 0 AS steps
FROM route_station
WHERE route_id = ? AND station_id = ?
UNION ALL
-- Keep looking for previous positions until we find one with amount_people > 0
SELECT
CASE
-- If we're at the first position, wrap around to the last
WHEN prev.pos = (SELECT MIN(pos) FROM route_station WHERE route_id = ?) THEN
(SELECT MAX(pos) FROM route_station WHERE route_id = ?)
-- Otherwise, get the previous position
ELSE
(SELECT MAX(pos) FROM route_station WHERE route_id = ? AND pos < prev.pos)
END AS pos,
prev.steps + 1 AS steps
FROM find_previous prev
-- Stop when we've checked all positions in the route
WHERE prev.steps < (SELECT COUNT(*) FROM route_station WHERE route_id = ?)
)
SELECT s.id, s.name, s.notes, s.amount_people, s.last_login, s.ready, s.pw, s.lat, s.lng
FROM find_previous fp
JOIN route_station rs ON rs.route_id = ? AND rs.pos = fp.pos
JOIN station s ON s.id = rs.station_id
WHERE (s.amount_people > 0 OR s.amount_people is NULL)
AND fp.steps > 0 -- Skip the starting position
ORDER BY fp.steps
LIMIT 1;
);",
)
.bind(self.id)
.bind(station.id)
.bind(self.id)
.bind(self.id)
.bind(self.id)
.bind(self.id)
.bind(self.id)
.bind(self.id)
.bind(station.id)
.fetch_optional(db)
.await
.unwrap();
// Don't return same station as prev station
if let Some(prev) = &ret {
if prev.id == station.id {
return None;
}
}
ret
}
} }
pub(super) fn routes() -> Router<AppState> { pub(super) fn routes() -> Router<AppState> {

View File

@ -6,6 +6,7 @@ use crate::{
}; };
use axum::Router; use axum::Router;
use chrono::{DateTime, Local, NaiveDateTime, Utc}; use chrono::{DateTime, Local, NaiveDateTime, Utc};
use futures::{stream, StreamExt};
use maud::{html, Markup, Render}; use maud::{html, Markup, Render};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
@ -427,6 +428,63 @@ ORDER BY LOWER(t.name);",
.await .await
.unwrap() .unwrap()
} }
pub(crate) async fn left_teams(&self, db: &SqlitePool) -> Vec<Team> {
sqlx::query_as::<_, Team>(
"SELECT t.id, t.name, t.notes, t.amount_people, t.first_station_id, t.route_id
FROM team t
JOIN rating r ON t.id = r.team_id
WHERE r.station_id = ?
AND r.left_at IS NOT NULL;",
)
.bind(self.id)
.fetch_all(db)
.await
.unwrap()
}
pub async fn teams_on_the_way(&self, db: &SqlitePool) -> Vec<TeamOnTheWay> {
let mut ret = Vec::new();
let teams = self.teams(db).await;
let missing_teams: Vec<Team> = stream::iter(teams)
.filter_map(|entry| async move {
if !entry.been_at_station(db, &self).await {
Some(entry)
} else {
None
}
})
.collect()
.await;
for team in missing_teams {
let route = team.route(db).await;
let Some(prev_station) = route.prev_station(db, self).await else {
continue;
};
let left_teams_of_prev_station = prev_station.left_teams(db).await;
if left_teams_of_prev_station.contains(&team) {
// team not yet at `self`, but already left `prev_station`
let rating = Rating::find_by_team_and_station(db, &team, &prev_station)
.await
.unwrap();
ret.push(TeamOnTheWay {
left: rating.local_time_left(),
team: team.clone(),
});
}
}
ret
}
}
pub struct TeamOnTheWay {
pub(crate) team: Team,
pub(crate) left: String,
//avg_time_in_secs: i64,
} }
pub(super) fn routes() -> Router<AppState> { pub(super) fn routes() -> Router<AppState> {

View File

@ -1,5 +1,6 @@
use crate::{ use crate::{
admin::{route::Route, station::Station}, admin::{route::Route, station::Station},
models::rating::Rating,
AppState, AppState,
}; };
use axum::Router; use axum::Router;
@ -9,7 +10,7 @@ use sqlx::{FromRow, SqlitePool};
mod web; mod web;
#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq)] #[derive(FromRow, Debug, Serialize, Deserialize, PartialEq, Clone)]
pub(crate) struct Team { pub(crate) struct Team {
pub(crate) id: i64, pub(crate) id: i64,
pub(crate) name: String, pub(crate) name: String,
@ -227,6 +228,12 @@ impl Team {
.unwrap() .unwrap()
.points .points
} }
pub async fn been_at_station(&self, db: &SqlitePool, station: &Station) -> bool {
Rating::find_by_team_and_station(db, &self, station)
.await
.is_some()
}
} }
pub(super) fn routes() -> Router<AppState> { pub(super) fn routes() -> Router<AppState> {

View File

@ -11,7 +11,7 @@ pub(crate) struct Rating {
pub(crate) notes: Option<String>, pub(crate) notes: Option<String>,
arrived_at: NaiveDateTime, arrived_at: NaiveDateTime,
started_at: Option<NaiveDateTime>, started_at: Option<NaiveDateTime>,
left_at: Option<NaiveDateTime>, pub(crate) left_at: Option<NaiveDateTime>,
} }
impl Rating { impl Rating {

View File

@ -29,6 +29,7 @@ async fn view(
}; };
let teams = TeamsAtStationLocation::for_station(&db, &station).await; let teams = TeamsAtStationLocation::for_station(&db, &station).await;
let teams_on_the_way = station.teams_on_the_way(&db).await;
let content = html! { let content = html! {
h1 { (format!("Station {}", station.name)) } h1 { (format!("Station {}", station.name)) }
@ -109,6 +110,19 @@ async fn view(
" Teams zu deiner Station kommen." " Teams zu deiner Station kommen."
progress value=(teams.total_teams-teams.not_yet_here.len() as i64) max=(teams.total_teams) {} progress value=(teams.total_teams-teams.not_yet_here.len() as i64) max=(teams.total_teams) {}
} }
@for team in teams_on_the_way {
article {
"Team "
(team.team.name)
" ist seit "
(team.left)
" auf dem Weg zu deiner Station."
form action=(format!("/s/{id}/{code}/new-waiting")) method="post" {
input type="hidden" name="team_id" value=(team.team.id);
input type="submit" value="Team ist da";
}
}
}
@if !teams.not_yet_here.is_empty() { @if !teams.not_yet_here.is_empty() {
form action=(format!("/s/{id}/{code}/new-waiting")) method="post" { form action=(format!("/s/{id}/{code}/new-waiting")) method="post" {
fieldset role="group" { fieldset role="group" {