use super::{generate_random_alphanumeric, team::Team}; use crate::{ AppState, admin::route::Route, models::rating::{Rating, TeamsAtStationLocation}, }; use axum::Router; use chrono::{DateTime, Local, NaiveDateTime, Utc}; use futures::{StreamExt, stream}; use maud::{Markup, Render, html}; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool}; mod web; #[derive(FromRow, Debug, Serialize, Deserialize)] pub(crate) struct Station { pub(crate) id: i64, pub(crate) name: String, notes: Option, pub(crate) amount_people: Option, last_login: Option, // TODO use proper timestamp (NaiveDateTime?) pub(crate) pw: String, pub(crate) ready: bool, pub(crate) lat: Option, pub(crate) lng: Option, } impl Render for Station { fn render(&self) -> Markup { html! { a href=(format!("/admin/station/{}", self.id)){ (self.name) } @if self.crewless() { em data-tooltip="Station ohne Stationsbetreuer" data-placement="bottom" { small { "🤖" } } } } } } impl Station { pub(crate) async fn all(db: &SqlitePool) -> Vec { sqlx::query_as::<_, Self>( "SELECT id, name, notes, amount_people, last_login, ready, pw, lat, lng FROM station;", ) .fetch_all(db) .await .unwrap() } pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { sqlx::query_as!( Self, "SELECT id, name, notes, amount_people, last_login, ready, pw, lat, lng FROM station WHERE id = ?", id ) .fetch_one(db) .await .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 { let station = sqlx::query_as!( Self, "SELECT id, name, notes, amount_people, last_login, ready, pw, lat, lng FROM station WHERE id = ? AND pw = ?", id, code ) .fetch_one(db) .await .ok()?; sqlx::query!( "UPDATE station SET last_login = CURRENT_TIMESTAMP WHERE id = ?", station.id ) .execute(db) .await .unwrap(); Some(station) } pub async fn switch_ready(&self, db: &SqlitePool) { let new_ready_status = !self.ready; sqlx::query!( "UPDATE station SET ready = ? WHERE id = ?", new_ready_status, self.id ) .execute(db) .await .unwrap(); } pub(crate) async fn create(db: &SqlitePool, name: &str) -> Result<(), String> { let code = generate_random_alphanumeric(8); let station_id = sqlx::query!( "INSERT INTO station(name, pw) VALUES (?, ?) RETURNING id", name, code ) .fetch_one(db) .await .map_err(|e| e.to_string())?; let mut routes = Route::all(db).await.into_iter(); if let Some(route) = routes.next() { if routes.next().is_none() { // Just one route exists -> use it for new station let station = Station::find_by_id(db, station_id.id) .await .expect("just created"); route.add_station(db, &station).await?; } } Ok(()) } pub(crate) async fn new_team_waiting( &self, db: &SqlitePool, team: &Team, ) -> Result<(), String> { let teams = TeamsAtStationLocation::for_station(db, self).await; if !teams.not_yet_here.contains(team) { return Err(format!( "Kann Team nicht der Warteschlange hinzufügen, weil das Team {} nicht zu deiner Station kommen soll.", team.name )); } Rating::create(db, self, team).await?; Ok(()) } pub(crate) async fn team_update( &self, db: &SqlitePool, team: &Team, points: Option, notes: Option, ) -> Result<(), String> { let notes = match notes { Some(n) if n.is_empty() => None, Some(n) => Some(n), None => None, }; let teams = TeamsAtStationLocation::for_station(db, self).await; let waiting_teams: Vec<&Team> = teams.waiting.iter().map(|(team, _)| team).collect(); let doing_teams: Vec<&Team> = teams.doing.iter().map(|(team, _)| team).collect(); let finished_teams: Vec<&Team> = teams .left_not_yet_rated .iter() .map(|(team, _)| team) .collect(); let finished_and_rated_teams: Vec<&Team> = teams.left_and_rated.iter().map(|(team, _)| team).collect(); if !waiting_teams.contains(&team) && !doing_teams.contains(&team) && !finished_teams.contains(&team) && !finished_and_rated_teams.contains(&team) { return Err( "Es können nur Teams bewertet werden, die zumindest schon bei der Station sind." .to_string(), ); } Rating::update(db, self, team, points, notes).await?; Ok(()) } pub(crate) async fn remove_team_waiting( &self, db: &SqlitePool, team: &Team, ) -> Result<(), String> { let teams = TeamsAtStationLocation::for_station(db, self).await; let waiting_teams: Vec<&Team> = teams.waiting.iter().map(|(team, _)| team).collect(); if !waiting_teams.contains(&team) { return Err(format!( "Kann Team nicht von der Warteschlange gelöscht werden, weil das Team {} nicht in der Warteschlange ist.", team.name )); } Rating::delete(db, self, team).await?; Ok(()) } pub(crate) async fn team_starting(&self, db: &SqlitePool, team: &Team) -> Result<(), String> { let teams = TeamsAtStationLocation::for_station(db, self).await; let waiting_teams: Vec<&Team> = teams.waiting.iter().map(|(team, _)| team).collect(); if !waiting_teams.contains(&team) { return Err(format!( "Team kann nicht starten, weil das Team {} nicht in der Warteschlange ist.", team.name )); } sqlx::query!( "UPDATE rating SET started_at = CURRENT_TIMESTAMP WHERE team_id = ? AND station_id = ?", team.id, self.id ) .execute(db) .await .unwrap(); Ok(()) } pub(crate) async fn remove_team_doing( &self, db: &SqlitePool, team: &Team, ) -> Result<(), String> { let teams = TeamsAtStationLocation::for_station(db, self).await; let doing_teams: Vec<&Team> = teams.doing.iter().map(|(team, _)| team).collect(); if !doing_teams.contains(&team) { return Err(format!( "Team kann nicht zur Warteschlange hinzugefügt werden, weil das Team {} aktuell nicht an deiner Station arbeitet.", team.name )); } sqlx::query!( "UPDATE rating SET started_at = NULL WHERE team_id = ? AND station_id = ?", team.id, self.id ) .execute(db) .await .unwrap(); Ok(()) } pub(crate) async fn team_finished(&self, db: &SqlitePool, team: &Team) -> Result<(), String> { let teams = TeamsAtStationLocation::for_station(db, self).await; let doing_teams: Vec<&Team> = teams.doing.iter().map(|(team, _)| team).collect(); if !doing_teams.contains(&team) { return Err(format!( "Team kann nicht beenden, weil das Team {} nicht an der Station arbeitet.", team.name )); } sqlx::query!( "UPDATE rating SET left_at = CURRENT_TIMESTAMP WHERE team_id = ? AND station_id = ?", team.id, self.id ) .execute(db) .await .unwrap(); Ok(()) } pub(crate) async fn remove_team_left( &self, db: &SqlitePool, team: &Team, ) -> Result<(), String> { let teams = TeamsAtStationLocation::for_station(db, self).await; let left_and_rated_teams: Vec<&Team> = teams.left_and_rated.iter().map(|(team, _)| team).collect(); let left_not_yet_rated_teams: Vec<&Team> = teams .left_not_yet_rated .iter() .map(|(team, _)| team) .collect(); if !left_and_rated_teams.contains(&team) && !left_not_yet_rated_teams.contains(&team) { return Err(format!( "Team kann nicht zur Arbeitsposition hinzugefügt werden, weil das Team {} aktuell nicht feritg ist", team.name )); } sqlx::query!( "UPDATE rating SET left_at = NULL, points=NULL WHERE team_id = ? AND station_id = ?", team.id, self.id ) .execute(db) .await .unwrap(); Ok(()) } async fn update_name(&self, db: &SqlitePool, name: &str) { sqlx::query!("UPDATE station SET name = ? WHERE id = ?", name, self.id) .execute(db) .await .unwrap(); } async fn update_notes(&self, db: &SqlitePool, notes: &str) { sqlx::query!("UPDATE station SET notes = ? WHERE id = ?", notes, self.id) .execute(db) .await .unwrap(); } async fn update_amount_people(&self, db: &SqlitePool, amount_people: i64) { sqlx::query!( "UPDATE station SET amount_people = ? WHERE id = ?", amount_people, self.id ) .execute(db) .await .unwrap(); } async fn update_amount_people_reset(&self, db: &SqlitePool) { sqlx::query!( "UPDATE station SET amount_people = NULL WHERE id = ?", self.id ) .execute(db) .await .unwrap(); } async fn update_location(&self, db: &SqlitePool, lat: f64, lng: f64) { sqlx::query!( "UPDATE station SET lat = ?, lng = ? WHERE id = ?", lat, lng, self.id ) .execute(db) .await .unwrap(); } async fn update_location_clear(&self, db: &SqlitePool) { sqlx::query!( "UPDATE station SET lat = NULL, lng=NULL WHERE id = ?", self.id ) .execute(db) .await .unwrap(); } async fn delete(&self, db: &SqlitePool) -> Result<(), String> { sqlx::query!("DELETE FROM station WHERE id = ?", self.id) .execute(db) .await .map_err(|e| e.to_string())?; Ok(()) } async fn routes(&self, db: &SqlitePool) -> Vec { sqlx::query_as::<_, Route>( " SELECT r.id, r.name FROM route r JOIN route_station rs ON r.id = rs.route_id WHERE rs.station_id = ? ORDER BY rs.pos ", ) .bind(self.id) .fetch_all(db) .await .unwrap() } pub async fn is_in_route(&self, db: &SqlitePool, route: &Route) -> bool { for r in self.routes(db).await { if r.id == route.id { return true; } } false } pub(crate) fn local_last_login(&self) -> Option> { let Some(last_login) = &self.last_login else { return None; }; let datetime_utc = DateTime::::from_naive_utc_and_offset(*last_login, Utc); Some(datetime_utc.with_timezone(&Local)) } pub(crate) async fn teams(&self, db: &SqlitePool) -> Vec { sqlx::query_as::<_, Team>( "SELECT DISTINCT t.id, t.name, t.notes, t.amount_people, t.first_station_id, t.route_id FROM team t JOIN route_station rs ON t.route_id = rs.route_id WHERE rs.station_id = ? ORDER BY LOWER(t.name);", ) .bind(self.id) .fetch_all(db) .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 { None } else { Some(entry) } }) .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 { web::routes() }