Files
star/src/admin/team/mod.rs
Philipp Hofer 3a99946787
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m48s
CI/CD Pipeline / deploy (push) Successful in 6m13s
lint
2025-05-17 21:06:31 +02:00

370 lines
11 KiB
Rust

use crate::{
AppState,
admin::{route::Route, station::Station},
rating::Rating,
};
use axum::Router;
use chrono::{DateTime, Local, NaiveDateTime, Utc};
use maud::{Markup, Render, html};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqliteConnection};
mod web;
#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq, Clone)]
pub(crate) struct Team {
pub(crate) id: i64,
pub(crate) name: String,
pub(crate) notes: Option<String>,
pub(crate) amount_people: Option<i64>,
first_station_id: i64,
last_station_id: Option<i64>,
pub(crate) route_id: i64,
}
impl Render for Team {
fn render(&self) -> Markup {
html! {
a href=(format!("/admin/team/{}", self.id)){
(self.name)
}
}
}
}
#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct LastContactTeam {
team: Team,
station: Option<Station>,
last_contact_time: Option<NaiveDateTime>,
}
impl LastContactTeam {
pub(crate) async fn all_sort_missing(db: &mut SqliteConnection) -> Vec<Self> {
let rows = sqlx::query_as::<_, (i64, i64, Option<NaiveDateTime>)>(
"SELECT
t.id AS team_id,
last_contact.station_id AS station_id,
last_contact.last_contact_time AS last_contact_time
FROM
team t
LEFT JOIN (
SELECT
r.team_id,
r.station_id,
MAX(COALESCE(r.left_at, r.started_at, r.arrived_at)) AS last_contact_time
FROM
rating r
GROUP BY
r.team_id
HAVING
last_contact_time = MAX(COALESCE(r.left_at, r.started_at, r.arrived_at))
) AS last_contact ON t.id = last_contact.team_id
LEFT JOIN station s ON last_contact.station_id = s.id
ORDER BY
last_contact.last_contact_time DESC",
)
.fetch_all(&mut *db)
.await
.unwrap();
let mut ret = Vec::new();
for (team_id, station_id, last_contact_time) in rows {
ret.push(LastContactTeam {
team: Team::find_by_id(&mut *db, team_id)
.await
.expect("db constraints"),
station: Station::find_by_id(db, station_id).await,
last_contact_time,
});
}
ret
}
pub(crate) fn local_last_contact(&self) -> Option<DateTime<Local>> {
let Some(last_contact) = &self.last_contact_time else {
return None;
};
let datetime_utc = DateTime::<Utc>::from_naive_utc_and_offset(*last_contact, Utc);
Some(datetime_utc.with_timezone(&Local))
}
}
#[derive(Debug)]
pub(crate) enum CreateError {
NoStationForRoute,
DuplicateName(String),
}
impl Team {
pub(crate) async fn all(db: &mut SqliteConnection) -> Vec<Self> {
sqlx::query_as::<_, Self>(
"SELECT id, name, notes, amount_people, first_station_id, last_station_id, route_id FROM team ORDER BY name;",
)
.fetch_all(db)
.await
.unwrap()
}
pub(crate) async fn all_with_route(db: &mut SqliteConnection, route: &Route) -> Vec<Self> {
sqlx::query_as!(
Team,
"SELECT id, name, notes, amount_people, first_station_id, last_station_id, route_id FROM team WHERE route_id = ?;",
route.id
)
.fetch_all(db)
.await
.unwrap()
}
pub(crate) async fn all_with_first_station(
db: &mut SqliteConnection,
station: &Station,
) -> Vec<Self> {
sqlx::query_as!(
Team,
"select id, name, notes, amount_people, first_station_id, last_station_id, route_id from team where first_station_id = ?;",
station.id
)
.fetch_all(db)
.await
.unwrap()
}
pub(crate) async fn all_with_last_station(
db: &mut SqliteConnection,
station: &Station,
) -> Vec<Self> {
sqlx::query_as!(
Team,
"select id, name, notes, amount_people, first_station_id, last_station_id, route_id from team where last_station_id = ?;",
station.id
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn find_by_id(db: &mut SqliteConnection, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, name, notes, amount_people, first_station_id, last_station_id, route_id FROM team WHERE id = ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub(crate) async fn create(
db: &mut SqliteConnection,
name: &str,
route: &Route,
) -> Result<i64, CreateError> {
// get next station id which has the lowest amount of teams to have in the first place
// assigned
let Some(station) = route.get_next_first_station(db).await else {
return Err(CreateError::NoStationForRoute);
};
let result = sqlx::query!(
"INSERT INTO team(name, route_id, first_station_id) VALUES (?, ?, ?) RETURNING id",
name,
route.id,
station.id
)
.fetch_one(db)
.await
.map_err(|e| CreateError::DuplicateName(e.to_string()))?;
Ok(result.id.unwrap())
}
async fn update_name(&self, db: &mut SqliteConnection, name: &str) {
sqlx::query!("UPDATE team SET name = ? WHERE id = ?", name, self.id)
.execute(db)
.await
.unwrap();
}
async fn update_end_station(&self, db: &mut SqliteConnection, station: &Station) {
sqlx::query!(
"UPDATE team SET last_station_id = ? WHERE id = ?",
station.id,
self.id
)
.execute(db)
.await
.unwrap();
}
async fn update_notes(&self, db: &mut SqliteConnection, notes: &str) {
sqlx::query!("UPDATE team SET notes = ? WHERE id = ?", notes, self.id)
.execute(db)
.await
.unwrap();
}
async fn update_amount_people(&self, db: &mut SqliteConnection, amount_people: i64) {
sqlx::query!(
"UPDATE team SET amount_people = ? WHERE id = ?",
amount_people,
self.id
)
.execute(db)
.await
.unwrap();
}
async fn update_route(&self, db: &mut SqliteConnection, route: &Route) -> Result<String, ()> {
let Some(station) = route.get_next_first_station(db).await else {
return Err(());
};
sqlx::query!(
"UPDATE team SET route_id = ?, first_station_id = ? WHERE id = ?",
route.id,
station.id,
self.id
)
.execute(db)
.await
.unwrap();
Ok(station.name)
}
pub(crate) async fn update_first_station(&self, db: &mut SqliteConnection, station: &Station) {
sqlx::query!(
"UPDATE team SET first_station_id = ? WHERE id = ?",
station.id,
self.id
)
.execute(db)
.await
.unwrap();
}
async fn update_last_station(&self, db: &mut SqliteConnection, station: &Station) {
sqlx::query!(
"UPDATE team SET last_station_id = ? WHERE id = ?",
station.id,
self.id
)
.execute(db)
.await
.unwrap();
}
async fn update_amount_people_reset(&self, db: &mut SqliteConnection) {
sqlx::query!("UPDATE team SET amount_people = NULL WHERE id = ?", self.id)
.execute(db)
.await
.unwrap();
}
async fn delete(&self, db: &mut SqliteConnection) -> Result<(), String> {
sqlx::query!("DELETE FROM team WHERE id = ?", self.id)
.execute(db)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn first_station(&self, db: &mut SqliteConnection) -> Station {
Station::find_by_id(db, self.first_station_id)
.await
.expect("db constraints")
}
pub async fn last_station(&self, db: &mut SqliteConnection) -> Option<Station> {
if let Some(last_station_id) = self.last_station_id {
Station::find_by_id(db, last_station_id).await
} else {
None
}
}
pub async fn route(&self, db: &mut SqliteConnection) -> Route {
Route::find_by_id(db, self.route_id)
.await
.expect("db constraints")
}
pub async fn get_curr_points(&self, db: &mut SqliteConnection) -> i64 {
sqlx::query!(
"SELECT IFNULL(sum(points), 0) as points FROM rating WHERE team_id = ?",
self.id
)
.fetch_one(db)
.await
.unwrap()
.points
}
pub async fn been_at_station(&self, db: &mut SqliteConnection, station: &Station) -> bool {
Rating::find_by_team_and_station(db, self, station)
.await
.is_some()
}
pub(crate) async fn end_station(&self, db: &mut SqliteConnection) -> Station {
match LastContactTeam::all_sort_missing(db)
.await
.into_iter()
.find(|last_contact_team| &last_contact_team.team == self)
{
Some(last_contact_team) => {
if let Some(station) = last_contact_team.station {
// Team already made some contact with a station
match Rating::find_by_team_and_station(db, self, &station).await {
Some(rating) => {
if rating.left_at.is_none() {
rating.station(db).await
} else {
let next_station = self
.route(db)
.await
.next_station(db, &station)
.await
.unwrap_or(station.clone());
if Rating::find_by_team_and_station(db, self, &next_station)
.await
.is_some()
{
station // last station for team
} else {
next_station
}
}
}
None => self.first_station(db).await,
}
} else {
// Team has made no contact yet -> next station should be the first one
self.first_station(db).await
}
}
None => unreachable!(),
}
}
pub async fn end_run(db: &mut SqliteConnection) {
// set `last_station_id` to the next station where `left_at` is not null
let teams = Team::all(db).await;
for team in teams {
let end_station = team.end_station(db).await;
team.update_end_station(db, &end_station).await;
}
}
pub async fn restart_run(db: &mut SqliteConnection) {
sqlx::query!("UPDATE team SET last_station_id = null")
.execute(db)
.await
.unwrap();
}
}
pub(super) fn routes() -> Router<AppState> {
web::routes()
}