493 lines
14 KiB
Rust
493 lines
14 KiB
Rust
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<String>,
|
|
pub(crate) amount_people: Option<i64>,
|
|
last_login: Option<NaiveDateTime>, // TODO use proper timestamp (NaiveDateTime?)
|
|
pub(crate) pw: String,
|
|
pub(crate) ready: bool,
|
|
pub(crate) lat: Option<f64>,
|
|
pub(crate) lng: Option<f64>,
|
|
}
|
|
|
|
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<Self> {
|
|
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<Self> {
|
|
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<Self> {
|
|
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<i64>,
|
|
notes: Option<String>,
|
|
) -> 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<Route> {
|
|
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<DateTime<Local>> {
|
|
let Some(last_login) = &self.last_login else {
|
|
return None;
|
|
};
|
|
let datetime_utc = DateTime::<Utc>::from_naive_utc_and_offset(*last_login, Utc);
|
|
Some(datetime_utc.with_timezone(&Local))
|
|
}
|
|
|
|
pub(crate) async fn teams(&self, db: &SqlitePool) -> Vec<Team> {
|
|
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<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 {
|
|
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<AppState> {
|
|
web::routes()
|
|
}
|