diff --git a/src/admin/route/mod.rs b/src/admin/route/mod.rs index c8d470f..11a754c 100644 --- a/src/admin/route/mod.rs +++ b/src/admin/route/mod.rs @@ -235,6 +235,69 @@ DROP TABLE temp_pos;", .expect("db constraints"), ) } + + pub async fn prev_station(&self, db: &SqlitePool, station: &Station) -> Option { + 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 { diff --git a/src/admin/station/mod.rs b/src/admin/station/mod.rs index e9328bb..328e89b 100644 --- a/src/admin/station/mod.rs +++ b/src/admin/station/mod.rs @@ -6,6 +6,7 @@ use crate::{ }; use axum::Router; use chrono::{DateTime, Local, NaiveDateTime, Utc}; +use futures::{stream, StreamExt}; use maud::{html, Markup, Render}; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool}; @@ -427,6 +428,63 @@ ORDER BY LOWER(t.name);", .await .unwrap() } + + pub(crate) async fn left_teams(&self, db: &SqlitePool) -> Vec { + 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 { + let mut ret = Vec::new(); + + let teams = self.teams(db).await; + + let missing_teams: Vec = 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 { diff --git a/src/admin/team/mod.rs b/src/admin/team/mod.rs index a547e6c..252506e 100644 --- a/src/admin/team/mod.rs +++ b/src/admin/team/mod.rs @@ -1,5 +1,6 @@ use crate::{ admin::{route::Route, station::Station}, + models::rating::Rating, AppState, }; use axum::Router; @@ -9,7 +10,7 @@ use sqlx::{FromRow, SqlitePool}; mod web; -#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq)] +#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq, Clone)] pub(crate) struct Team { pub(crate) id: i64, pub(crate) name: String, @@ -227,6 +228,12 @@ impl Team { .unwrap() .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 { diff --git a/src/models/rating.rs b/src/models/rating.rs index 7c7364d..cee6997 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -11,7 +11,7 @@ pub(crate) struct Rating { pub(crate) notes: Option, arrived_at: NaiveDateTime, started_at: Option, - left_at: Option, + pub(crate) left_at: Option, } impl Rating { diff --git a/src/station.rs b/src/station.rs index 28e09e8..e4180de 100644 --- a/src/station.rs +++ b/src/station.rs @@ -29,6 +29,7 @@ async fn view( }; let teams = TeamsAtStationLocation::for_station(&db, &station).await; + let teams_on_the_way = station.teams_on_the_way(&db).await; let content = html! { h1 { (format!("Station {}", station.name)) } @@ -109,6 +110,19 @@ async fn view( " Teams zu deiner Station kommen." 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() { form action=(format!("/s/{id}/{code}/new-waiting")) method="post" { fieldset role="group" {