aequatorpreis #730
@@ -31,6 +31,12 @@ impl PartialEq for Logbook {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) enum Filter {
 | 
			
		||||
    SingleDayOnly,
 | 
			
		||||
    MultiDazOnly,
 | 
			
		||||
    None,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(FromForm, Debug, Clone)]
 | 
			
		||||
pub struct LogToAdd {
 | 
			
		||||
    pub boat_id: i32,
 | 
			
		||||
@@ -293,6 +299,93 @@ ORDER BY departure DESC
 | 
			
		||||
        ret
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn year_first_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
 | 
			
		||||
        let log: Option<Self> = sqlx::query_as(
 | 
			
		||||
               &format!("
 | 
			
		||||
    SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
 | 
			
		||||
    FROM logbook
 | 
			
		||||
    JOIN rower ON logbook.id = rower.logbook_id
 | 
			
		||||
    WHERE arrival is not null AND rower_id = {}
 | 
			
		||||
    ORDER BY arrival 
 | 
			
		||||
    LIMIT 1
 | 
			
		||||
            ",  user.id)
 | 
			
		||||
            )
 | 
			
		||||
            .fetch_optional(db)
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap(); //TODO: fixme
 | 
			
		||||
 | 
			
		||||
        if let Some(log) = log {
 | 
			
		||||
            Some(log.arrival.unwrap().year())
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn year_last_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
 | 
			
		||||
        let log: Option<Self> = sqlx::query_as(
 | 
			
		||||
               &format!("
 | 
			
		||||
    SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
 | 
			
		||||
    FROM logbook
 | 
			
		||||
    JOIN rower ON logbook.id = rower.logbook_id
 | 
			
		||||
    WHERE arrival is not null AND rower_id = {}
 | 
			
		||||
    ORDER BY arrival DESC
 | 
			
		||||
    LIMIT 1
 | 
			
		||||
            ",  user.id)
 | 
			
		||||
            )
 | 
			
		||||
            .fetch_optional(db)
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap(); //TODO: fixme
 | 
			
		||||
 | 
			
		||||
        if let Some(log) = log {
 | 
			
		||||
            Some(log.arrival.unwrap().year())
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year(
 | 
			
		||||
        db: &SqlitePool,
 | 
			
		||||
        user: &User,
 | 
			
		||||
        min_distance: i32,
 | 
			
		||||
        year: i32,
 | 
			
		||||
        filter: Filter,
 | 
			
		||||
    ) -> Vec<LogbookWithBoatAndRowers> {
 | 
			
		||||
        let logs: Vec<Logbook> = sqlx::query_as(
 | 
			
		||||
               &format!("
 | 
			
		||||
    SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
 | 
			
		||||
    FROM logbook
 | 
			
		||||
    JOIN rower ON logbook.id = rower.logbook_id
 | 
			
		||||
    WHERE arrival is not null AND rower_id = {} AND logtype = 1 AND distance_in_km >= {} AND arrival like '{}-%' 
 | 
			
		||||
    ORDER BY arrival DESC
 | 
			
		||||
            ",  user.id, min_distance, year)
 | 
			
		||||
            )
 | 
			
		||||
            .fetch_all(db)
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap(); //TODO: fixme
 | 
			
		||||
 | 
			
		||||
        let mut ret = Vec::new();
 | 
			
		||||
        for log in logs {
 | 
			
		||||
            let trip_days = log.arrival.unwrap() - log.departure;
 | 
			
		||||
            let trip_days = trip_days.num_days();
 | 
			
		||||
            match filter {
 | 
			
		||||
                Filter::SingleDayOnly => {
 | 
			
		||||
                    if trip_days == 0 {
 | 
			
		||||
                        ret.push(LogbookWithBoatAndRowers::from(db, log).await);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Filter::MultiDazOnly => {
 | 
			
		||||
                    if trip_days > 0 {
 | 
			
		||||
                        ret.push(LogbookWithBoatAndRowers::from(db, log).await);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Filter::None => {
 | 
			
		||||
                    ret.push(LogbookWithBoatAndRowers::from(db, log).await);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        ret
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn completed(db: &SqlitePool) -> Vec<LogbookWithBoatAndRowers> {
 | 
			
		||||
        let year = chrono::Local::now().year();
 | 
			
		||||
        Self::completed_in_year(db, year).await
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ pub mod logbook;
 | 
			
		||||
pub mod logtype;
 | 
			
		||||
pub mod mail;
 | 
			
		||||
pub mod notification;
 | 
			
		||||
pub mod personal;
 | 
			
		||||
pub mod role;
 | 
			
		||||
pub mod rower;
 | 
			
		||||
pub mod stat;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										87
									
								
								src/model/personal/equatorprice.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/model/personal/equatorprice.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, PartialEq, Debug)]
 | 
			
		||||
pub(crate) enum Level {
 | 
			
		||||
    NONE,
 | 
			
		||||
    BRONZE,
 | 
			
		||||
    SILVER,
 | 
			
		||||
    GOLD,
 | 
			
		||||
    DIAMOND,
 | 
			
		||||
    DONE,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Level {
 | 
			
		||||
    fn required_km(&self) -> i32 {
 | 
			
		||||
        match self {
 | 
			
		||||
            Level::BRONZE => 40000,
 | 
			
		||||
            Level::SILVER => 80000,
 | 
			
		||||
            Level::GOLD => 100000,
 | 
			
		||||
            Level::DIAMOND => 200000,
 | 
			
		||||
            Level::DONE => 0,
 | 
			
		||||
            Level::NONE => 0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn next_level(km: i32) -> Self {
 | 
			
		||||
        if km < Level::BRONZE.required_km() {
 | 
			
		||||
            Level::BRONZE
 | 
			
		||||
        } else if km < Level::SILVER.required_km() {
 | 
			
		||||
            Level::SILVER
 | 
			
		||||
        } else if km < Level::GOLD.required_km() {
 | 
			
		||||
            Level::GOLD
 | 
			
		||||
        } else if km < Level::DIAMOND.required_km() {
 | 
			
		||||
            Level::DIAMOND
 | 
			
		||||
        } else {
 | 
			
		||||
            Level::DONE
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn curr_level(km: i32) -> Self {
 | 
			
		||||
        if km < Level::BRONZE.required_km() {
 | 
			
		||||
            Level::NONE
 | 
			
		||||
        } else if km < Level::SILVER.required_km() {
 | 
			
		||||
            Level::BRONZE
 | 
			
		||||
        } else if km < Level::GOLD.required_km() {
 | 
			
		||||
            Level::SILVER
 | 
			
		||||
        } else if km < Level::DIAMOND.required_km() {
 | 
			
		||||
            Level::GOLD
 | 
			
		||||
        } else {
 | 
			
		||||
            Level::DIAMOND
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn desc(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
            Level::BRONZE => "Bronze",
 | 
			
		||||
            Level::SILVER => "Silber",
 | 
			
		||||
            Level::GOLD => "Gold",
 | 
			
		||||
            Level::DIAMOND => "Diamant",
 | 
			
		||||
            Level::DONE => "",
 | 
			
		||||
            Level::NONE => "-",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
pub(crate) struct Next {
 | 
			
		||||
    level: Level,
 | 
			
		||||
    desc: String,
 | 
			
		||||
    missing_km: i32,
 | 
			
		||||
    required_km: i32,
 | 
			
		||||
    rowed_km: i32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Next {
 | 
			
		||||
    pub(crate) fn new(rowed_km: i32) -> Self {
 | 
			
		||||
        let level = Level::next_level(rowed_km);
 | 
			
		||||
        let required_km = level.required_km();
 | 
			
		||||
        let missing_km = required_km - rowed_km;
 | 
			
		||||
        Self {
 | 
			
		||||
            desc: level.desc().to_string(),
 | 
			
		||||
            level,
 | 
			
		||||
            missing_km,
 | 
			
		||||
            required_km,
 | 
			
		||||
            rowed_km,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								src/model/personal/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/model/personal/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
use chrono::{Datelike, Local};
 | 
			
		||||
use equatorprice::Level;
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
use sqlx::SqlitePool;
 | 
			
		||||
 | 
			
		||||
use super::{logbook::Logbook, stat::Stat, user::User};
 | 
			
		||||
 | 
			
		||||
pub(crate) mod equatorprice;
 | 
			
		||||
pub(crate) mod rowingbadge;
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
pub(crate) struct Achievements {
 | 
			
		||||
    pub(crate) equatorprice: equatorprice::Next,
 | 
			
		||||
    pub(crate) curr_equatorprice_name: String,
 | 
			
		||||
    pub(crate) new_equatorprice_this_season: bool,
 | 
			
		||||
    pub(crate) rowingbadge: Option<rowingbadge::Status>,
 | 
			
		||||
    pub(crate) all_time_km: i32,
 | 
			
		||||
    pub(crate) year_first_mentioned: Option<i32>,
 | 
			
		||||
    pub(crate) year_last_mentioned: Option<i32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Achievements {
 | 
			
		||||
    pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Self {
 | 
			
		||||
        let rowed_km = Stat::total_km(db, user).await.rowed_km;
 | 
			
		||||
        let rowed_km_this_season = if Local::now().month() == 1 {
 | 
			
		||||
            Stat::person(db, Some(Local::now().year() - 1), user)
 | 
			
		||||
                .await
 | 
			
		||||
                .rowed_km
 | 
			
		||||
                + Stat::person(db, Some(Local::now().year()), user)
 | 
			
		||||
                    .await
 | 
			
		||||
                    .rowed_km
 | 
			
		||||
        } else {
 | 
			
		||||
            Stat::person(db, Some(Local::now().year()), user)
 | 
			
		||||
                .await
 | 
			
		||||
                .rowed_km
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let new_equatorprice_this_season =
 | 
			
		||||
            Level::curr_level(rowed_km) != Level::curr_level(rowed_km - rowed_km_this_season);
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            equatorprice: equatorprice::Next::new(rowed_km),
 | 
			
		||||
            curr_equatorprice_name: equatorprice::Level::curr_level(rowed_km).desc().to_string(),
 | 
			
		||||
            new_equatorprice_this_season,
 | 
			
		||||
            rowingbadge: rowingbadge::Status::for_user(db, user).await,
 | 
			
		||||
            all_time_km: rowed_km,
 | 
			
		||||
            year_first_mentioned: Logbook::year_first_logbook_entry(db, user).await,
 | 
			
		||||
            year_last_mentioned: Logbook::year_last_logbook_entry(db, user).await,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										166
									
								
								src/model/personal/rowingbadge.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/model/personal/rowingbadge.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
			
		||||
use std::cmp;
 | 
			
		||||
 | 
			
		||||
use chrono::{Datelike, Local, NaiveDate};
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
use sqlx::SqlitePool;
 | 
			
		||||
 | 
			
		||||
use crate::model::{
 | 
			
		||||
    logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
 | 
			
		||||
    stat::Stat,
 | 
			
		||||
    user::User,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum AgeBracket {
 | 
			
		||||
    Till14,
 | 
			
		||||
    From14Till18,
 | 
			
		||||
    From19Till30,
 | 
			
		||||
    From31Till60,
 | 
			
		||||
    From61Till75,
 | 
			
		||||
    From76,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AgeBracket {
 | 
			
		||||
    fn cat(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
            AgeBracket::Till14 => "Schülerinnen und Schüler bis 14 Jahre",
 | 
			
		||||
            AgeBracket::From14Till18 => "Juniorinnen und Junioren, Para-Ruderer bis 18 Jahre",
 | 
			
		||||
            AgeBracket::From19Till30 => "Frauen und Männer, Para-Ruderer bis 30 Jahre",
 | 
			
		||||
            AgeBracket::From31Till60 => "Frauen und Männer, Para-Ruderer von 31 bis 60 Jahre",
 | 
			
		||||
            AgeBracket::From61Till75 => "Frauen und Männer, Para-Ruderer von 61 bis 75 Jahre",
 | 
			
		||||
            AgeBracket::From76 => "Frauen und Männer, Para-Ruderer ab 76 Jahre",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn dist_in_km(&self) -> i32 {
 | 
			
		||||
        match self {
 | 
			
		||||
            AgeBracket::Till14 => 500,
 | 
			
		||||
            AgeBracket::From14Till18 => 1000,
 | 
			
		||||
            AgeBracket::From19Till30 => 1200,
 | 
			
		||||
            AgeBracket::From31Till60 => 1000,
 | 
			
		||||
            AgeBracket::From61Till75 => 800,
 | 
			
		||||
            AgeBracket::From76 => 600,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn required_dist_multi_day_in_km(&self) -> i32 {
 | 
			
		||||
        match self {
 | 
			
		||||
            AgeBracket::Till14 => 60,
 | 
			
		||||
            AgeBracket::From14Till18 => 60,
 | 
			
		||||
            AgeBracket::From19Till30 => 80,
 | 
			
		||||
            AgeBracket::From31Till60 => 80,
 | 
			
		||||
            AgeBracket::From61Till75 => 80,
 | 
			
		||||
            AgeBracket::From76 => 80,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn required_dist_single_day_in_km(&self) -> i32 {
 | 
			
		||||
        match self {
 | 
			
		||||
            AgeBracket::Till14 => 30,
 | 
			
		||||
            AgeBracket::From14Till18 => 30,
 | 
			
		||||
            AgeBracket::From19Till30 => 40,
 | 
			
		||||
            AgeBracket::From31Till60 => 40,
 | 
			
		||||
            AgeBracket::From61Till75 => 40,
 | 
			
		||||
            AgeBracket::From76 => 40,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TryFrom<&User> for AgeBracket {
 | 
			
		||||
    type Error = String;
 | 
			
		||||
 | 
			
		||||
    fn try_from(value: &User) -> Result<Self, Self::Error> {
 | 
			
		||||
        let Some(birthdate) = value.birthdate.clone() else {
 | 
			
		||||
            return Err("User has no birthdate".to_string());
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let Ok(birthdate) = NaiveDate::parse_from_str(&birthdate, "%Y-%m-%d") else {
 | 
			
		||||
            return Err("Birthdate in wrong format...".to_string());
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let today = Local::now().date_naive();
 | 
			
		||||
 | 
			
		||||
        let age = today.year() - birthdate.year();
 | 
			
		||||
        if age <= 14 {
 | 
			
		||||
            Ok(AgeBracket::Till14)
 | 
			
		||||
        } else if age <= 18 {
 | 
			
		||||
            Ok(AgeBracket::From14Till18)
 | 
			
		||||
        } else if age <= 30 {
 | 
			
		||||
            Ok(AgeBracket::From19Till30)
 | 
			
		||||
        } else if age <= 60 {
 | 
			
		||||
            Ok(AgeBracket::From31Till60)
 | 
			
		||||
        } else if age <= 75 {
 | 
			
		||||
            Ok(AgeBracket::From61Till75)
 | 
			
		||||
        } else {
 | 
			
		||||
            Ok(AgeBracket::From76)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
pub(crate) struct Status {
 | 
			
		||||
    pub(crate) year: i32,
 | 
			
		||||
    rowed_km: i32,
 | 
			
		||||
    category: String,
 | 
			
		||||
    required_km: i32,
 | 
			
		||||
    missing_km: i32,
 | 
			
		||||
    multi_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
 | 
			
		||||
    multi_day_trips_required_distance: i32,
 | 
			
		||||
    single_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
 | 
			
		||||
    single_day_trips_required_distance: i32,
 | 
			
		||||
    achieved: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Status {
 | 
			
		||||
    pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
 | 
			
		||||
        let Ok(agebracket) = AgeBracket::try_from(user) else {
 | 
			
		||||
            return None;
 | 
			
		||||
        };
 | 
			
		||||
        let category = agebracket.cat().to_string();
 | 
			
		||||
 | 
			
		||||
        let year = if Local::now().month() == 1 {
 | 
			
		||||
            Local::now().year() - 1
 | 
			
		||||
        } else {
 | 
			
		||||
            Local::now().year()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let rowed_km = Stat::person(db, Some(year), user).await.rowed_km;
 | 
			
		||||
        let required_km = agebracket.dist_in_km();
 | 
			
		||||
        let missing_km = cmp::max(required_km - rowed_km, 0);
 | 
			
		||||
 | 
			
		||||
        let single_day_trips_over_required_distance =
 | 
			
		||||
            Logbook::completed_wanderfahrten_with_user_over_km_in_year(
 | 
			
		||||
                db,
 | 
			
		||||
                user,
 | 
			
		||||
                agebracket.required_dist_single_day_in_km(),
 | 
			
		||||
                year,
 | 
			
		||||
                Filter::SingleDayOnly,
 | 
			
		||||
            )
 | 
			
		||||
            .await;
 | 
			
		||||
        let multi_day_trips_over_required_distance =
 | 
			
		||||
            Logbook::completed_wanderfahrten_with_user_over_km_in_year(
 | 
			
		||||
                db,
 | 
			
		||||
                user,
 | 
			
		||||
                agebracket.required_dist_multi_day_in_km(),
 | 
			
		||||
                year,
 | 
			
		||||
                Filter::MultiDazOnly,
 | 
			
		||||
            )
 | 
			
		||||
            .await;
 | 
			
		||||
 | 
			
		||||
        let achieved = missing_km == 0
 | 
			
		||||
            && (multi_day_trips_over_required_distance.len() >= 1
 | 
			
		||||
                || single_day_trips_over_required_distance.len() >= 2);
 | 
			
		||||
 | 
			
		||||
        Some(Self {
 | 
			
		||||
            year,
 | 
			
		||||
            rowed_km,
 | 
			
		||||
            category,
 | 
			
		||||
            required_km,
 | 
			
		||||
            missing_km,
 | 
			
		||||
            multi_day_trips_over_required_distance,
 | 
			
		||||
            single_day_trips_over_required_distance,
 | 
			
		||||
            multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(),
 | 
			
		||||
            single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(),
 | 
			
		||||
            achieved,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -195,6 +195,32 @@ ORDER BY rowed_km DESC, u.name;
 | 
			
		||||
        })
 | 
			
		||||
        .collect()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn total_km(db: &SqlitePool, user: &User) -> Stat {
 | 
			
		||||
        //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
 | 
			
		||||
        let row = sqlx::query(&format!(
 | 
			
		||||
            "
 | 
			
		||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km 
 | 
			
		||||
FROM (
 | 
			
		||||
    SELECT * FROM user 
 | 
			
		||||
    WHERE id={}
 | 
			
		||||
) u
 | 
			
		||||
INNER JOIN rower r ON u.id = r.rower_id
 | 
			
		||||
INNER JOIN logbook l ON r.logbook_id = l.id
 | 
			
		||||
WHERE l.distance_in_km IS NOT NULL;
 | 
			
		||||
",
 | 
			
		||||
            user.id
 | 
			
		||||
        ))
 | 
			
		||||
        .fetch_one(db)
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
 | 
			
		||||
        Stat {
 | 
			
		||||
            name: row.get("name"),
 | 
			
		||||
            rowed_km: row.get("rowed_km"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
 | 
			
		||||
        let year = match year {
 | 
			
		||||
            Some(year) => year,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										46
									
								
								src/tera/board/achievement.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/tera/board/achievement.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
use crate::model::{
 | 
			
		||||
    personal::Achievements,
 | 
			
		||||
    role::Role,
 | 
			
		||||
    user::{User, UserWithDetails, VorstandUser},
 | 
			
		||||
};
 | 
			
		||||
use rocket::{get, request::FlashMessage, routes, Route, State};
 | 
			
		||||
use rocket_dyn_templates::{tera::Context, Template};
 | 
			
		||||
use sqlx::SqlitePool;
 | 
			
		||||
 | 
			
		||||
#[get("/achievement")]
 | 
			
		||||
async fn index(
 | 
			
		||||
    db: &State<SqlitePool>,
 | 
			
		||||
    admin: VorstandUser,
 | 
			
		||||
    flash: Option<FlashMessage<'_>>,
 | 
			
		||||
) -> Template {
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    if let Some(msg) = flash {
 | 
			
		||||
        context.insert("flash", &msg.into_inner());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let role = Role::find_by_name(&db, "Donau Linz").await.unwrap();
 | 
			
		||||
    let users = User::all_with_role(&db, &role).await;
 | 
			
		||||
    let mut people = Vec::new();
 | 
			
		||||
    let mut rowingbadge_year = None;
 | 
			
		||||
    for user in users {
 | 
			
		||||
        let achievement = Achievements::for_user(&db, &user).await;
 | 
			
		||||
        if let Some(badge) = &achievement.rowingbadge {
 | 
			
		||||
            rowingbadge_year = Some(badge.year);
 | 
			
		||||
        }
 | 
			
		||||
        people.push((user, achievement));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    context.insert("people", &people);
 | 
			
		||||
    context.insert("rowingbadge_year", &rowingbadge_year.unwrap());
 | 
			
		||||
 | 
			
		||||
    context.insert(
 | 
			
		||||
        "loggedin_user",
 | 
			
		||||
        &UserWithDetails::from_user(admin.into_inner(), db).await,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Template::render("achievement", context.into_json())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn routes() -> Vec<Route> {
 | 
			
		||||
    routes![index]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
use rocket::Route;
 | 
			
		||||
 | 
			
		||||
pub mod achievement;
 | 
			
		||||
pub mod boathouse;
 | 
			
		||||
 | 
			
		||||
pub fn routes() -> Vec<Route> {
 | 
			
		||||
    let mut ret = Vec::new();
 | 
			
		||||
    ret.append(&mut boathouse::routes());
 | 
			
		||||
    ret.append(&mut achievement::routes());
 | 
			
		||||
    ret
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ use crate::{
 | 
			
		||||
    model::{
 | 
			
		||||
        logbook::Logbook,
 | 
			
		||||
        notification::Notification,
 | 
			
		||||
        personal::Achievements,
 | 
			
		||||
        role::Role,
 | 
			
		||||
        user::{User, UserWithDetails},
 | 
			
		||||
    },
 | 
			
		||||
@@ -62,6 +63,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
 | 
			
		||||
        context.insert("last_trips", &last_trips);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    context.insert("achievements", &Achievements::for_user(db, &user).await);
 | 
			
		||||
    context.insert("notifications", &Notification::for_user(db, &user).await);
 | 
			
		||||
    context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
 | 
			
		||||
    context.insert("costs_scheckbuch", &SCHECKBUCH);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										100
									
								
								templates/achievement.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								templates/achievement.html.tera
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
{% import "includes/macros" as macros %}
 | 
			
		||||
{% import "includes/forms/log" as log %}
 | 
			
		||||
{% extends "base" %}
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <link rel="stylesheet" href="/public/table.css" />
 | 
			
		||||
    <div class="w-full">
 | 
			
		||||
        <h1 class="h1">Abzeichen für {{ rowingbadge_year }}</h1>
 | 
			
		||||
        <div class="text-black dark:text-white">
 | 
			
		||||
            <table id="basic">
 | 
			
		||||
                <thead>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th>Name</th>
 | 
			
		||||
                        <th>Erster Log</th>
 | 
			
		||||
                        <th>Letzter Log</th>
 | 
			
		||||
                        <th>Gesamt-KM</th>
 | 
			
		||||
                        <th>Äquatorpreis (ÄP)</th>
 | 
			
		||||
                        <th>
 | 
			
		||||
                            ÄP diese
 | 
			
		||||
                            <br>
 | 
			
		||||
                            Saison bekommen
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th>
 | 
			
		||||
                            Fahrtenabzeichen (FA)
 | 
			
		||||
                            <br>
 | 
			
		||||
                            geschafft
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <th>FA - KM</th>
 | 
			
		||||
                        <th>FA - fehlende KM</th>
 | 
			
		||||
                        <th>Eintagesausfahrten</th>
 | 
			
		||||
                        <th>Mehrtagesausfahrten</th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    {% for person in people %}
 | 
			
		||||
                        {% set user = person.0 %}
 | 
			
		||||
                        {% set achievement = person.1 %}
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>{{ user.name }}</td>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if achievement.year_first_mentioned %}{{ achievement.year_first_mentioned }}{% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if achievement.year_last_mentioned %}{{ achievement.year_last_mentioned }}{% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <td>{{ achievement.all_time_km }}</td>
 | 
			
		||||
                            <td>{{ achievement.curr_equatorprice_name }}</td>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if achievement.new_equatorprice_this_season %}
 | 
			
		||||
                                    🎉
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                    -
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                            {% if achievement.rowingbadge %}
 | 
			
		||||
                                {% set badge = achievement.rowingbadge %}
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    {% if badge.achieved %}
 | 
			
		||||
                                        ja
 | 
			
		||||
                                    {% else %}
 | 
			
		||||
                                        nein
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </td>
 | 
			
		||||
                                <td>{{ badge.rowed_km }} / {{ badge.required_km }}</td>
 | 
			
		||||
                                <td>{{ badge.missing_km }}</td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    <details>
 | 
			
		||||
                                        <summary>
 | 
			
		||||
                                            > {{ badge.single_day_trips_required_distance }} km: {{ badge.single_day_trips_over_required_distance | length }} / 2
 | 
			
		||||
                                        </summary>
 | 
			
		||||
                                        {% for log in badge.single_day_trips_over_required_distance %}
 | 
			
		||||
                                            {{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, hide_type=true) }}
 | 
			
		||||
                                        {% endfor %}
 | 
			
		||||
                                    </details>
 | 
			
		||||
                                </td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    <details>
 | 
			
		||||
                                        <summary>
 | 
			
		||||
                                            > {{ badge.multi_day_trips_required_distance }} km: {{ badge.multi_day_trips_over_required_distance | length }} / 1
 | 
			
		||||
                                        </summary>
 | 
			
		||||
                                        {% for log in badge.multi_day_trips_over_required_distance %}
 | 
			
		||||
                                            {{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, hide_type=true) }}
 | 
			
		||||
                                        {% endfor %}
 | 
			
		||||
                                    </details>
 | 
			
		||||
                                </td>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                                <td>Geb.datum fehlt 👻</td>
 | 
			
		||||
                                <td>Geb.datum fehlt 👻</td>
 | 
			
		||||
                                <td>Geb.datum fehlt 👻</td>
 | 
			
		||||
                                <td>Geb.datum fehlt 👻</td>
 | 
			
		||||
                                <td>Geb.datum fehlt 👻</td>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <script src="/public/jstable.min.js"></script>
 | 
			
		||||
    <script src="/public/table.js"></script>
 | 
			
		||||
{% endblock content %}
 | 
			
		||||
@@ -173,13 +173,13 @@
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endmacro show %}
 | 
			
		||||
{% macro show_old(log, state, allowed_to_close=false, allowed_to_edit=false, index) %}
 | 
			
		||||
{% macro show_old(log, state, allowed_to_close=false, allowed_to_edit=false, index, hide_type=false) %}
 | 
			
		||||
    <div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
 | 
			
		||||
         data-filterable="true"
 | 
			
		||||
         data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}">
 | 
			
		||||
        <details>
 | 
			
		||||
            <summary style="list-style: none;">
 | 
			
		||||
                {% if log.logtype %}
 | 
			
		||||
                {% if log.logtype and not hide_type %}
 | 
			
		||||
                    <div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
 | 
			
		||||
                        {% if log.logtype == 1 %}
 | 
			
		||||
                            Wanderfahrt
 | 
			
		||||
 
 | 
			
		||||
@@ -74,6 +74,138 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
 | 
			
		||||
                <dialog id="call-for-action" class="max-w-screen-sm dark:bg-primary-600 dark:text-white rounded-md" onclick="document.getElementById('call-for-action').close()">
 | 
			
		||||
		<div onclick="event.stopPropagation();" class="p-3">
 | 
			
		||||
		    <button type="button" onclick="document.getElementById('call-for-action').close()" title="Schließen" class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3"><svg class="inline h-5 w-5" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
 | 
			
		||||
    <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
 | 
			
		||||
</svg>
 | 
			
		||||
</button>
 | 
			
		||||
<div class="mt-8">
 | 
			
		||||
<p>
 | 
			
		||||
                    Du hast Ideen für sinnvolle neue Funktionen für diese Ruderapp? Melde dich bei Philipp, Marie oder <a href="mailto:it@rudernlinz.at" class="underline">it@rudernlinz.at</a>.
 | 
			
		||||
		    </p>
 | 
			
		||||
<p class="mt-3">
 | 
			
		||||
		    
 | 
			
		||||
Wenn du darüber hinaus Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird (😀), dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.
 | 
			
		||||
</p>
 | 
			
		||||
		    </div>
 | 
			
		||||
		    </div>
 | 
			
		||||
                </dialog>
 | 
			
		||||
                <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
 | 
			
		||||
                     role="alert">
 | 
			
		||||
                    <h2 class="h2">
 | 
			
		||||
                        Persönliches
 | 
			
		||||
                        <span class="text-xl"
 | 
			
		||||
                              onclick="document.getElementById('call-for-action').showModal()">💡</span>
 | 
			
		||||
                    </h2>
 | 
			
		||||
                    <div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
 | 
			
		||||
                        <div class="py-3">
 | 
			
		||||
                            <details>
 | 
			
		||||
                                <summary>
 | 
			
		||||
                                    <h3 class="inline">
 | 
			
		||||
                                        <span class="text-xl">
 | 
			
		||||
                                            {% if achievements.rowingbadge and achievements.rowingbadge.achieved %}
 | 
			
		||||
                                                🎉
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
                                                📋
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                        Fahrtenabzeichen
 | 
			
		||||
                                        {% if achievements.rowingbadge %}{{ achievements.rowingbadge.year }}{% endif %}
 | 
			
		||||
                                        <span><a href="http://www.rudern.at/OFFICE/Downloads/Ausschreibungen/2022/Wanderfahrten//Fahrtenabzeichen%20%C3%84quatorpreis%20und%20Danubius%202022.pdf"
 | 
			
		||||
   target="_blank"
 | 
			
		||||
   class="w-6 h-6 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2 text-white">?</a></span>
 | 
			
		||||
                                    </h3>
 | 
			
		||||
                                </summary>
 | 
			
		||||
                                {% if achievements.rowingbadge %}
 | 
			
		||||
                                    {% set badge = achievements.rowingbadge %}
 | 
			
		||||
                                    <div class="mb-3">{{ badge.category }}</div>
 | 
			
		||||
                                    <label for="rowingbadge" class="label">Kilometer ({{ badge.rowed_km }} / {{ badge.required_km }} km)</label>
 | 
			
		||||
                                    <progress id="rowingbadge"
 | 
			
		||||
                                              class="w-full block my-3"
 | 
			
		||||
                                              value="{{ badge.rowed_km }}"
 | 
			
		||||
                                              max="{{ badge.required_km }}"></progress>
 | 
			
		||||
                                    <h4 class="font-bold mt-4">Wanderfahrten</h4>
 | 
			
		||||
                                    <div>Nur eine der folgenden Optionen muss erreicht werden:</div>
 | 
			
		||||
                                    <ol class="list-decimal ml-4 my-3">
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            {% if badge.multi_day_trips_over_required_distance | length >= 1 %}
 | 
			
		||||
                                                ✅
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
                                                ❌
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                            1 mehrtägige Wanderfahrt > {{ badge.multi_day_trips_required_distance }} km
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            {% if badge.single_day_trips_over_required_distance | length >= 2 %}
 | 
			
		||||
                                                ✅
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
                                                ❌
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                            2 eintägige Wanderfahrten > {{ badge.single_day_trips_required_distance }} km
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                    </ol>
 | 
			
		||||
                                    <details>
 | 
			
		||||
                                        <summary>Details zu den Wanderfahrten</summary>
 | 
			
		||||
                                        <div class="mt-3">
 | 
			
		||||
                                            {% for log in badge.single_day_trips_over_required_distance %}
 | 
			
		||||
                                                {{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}
 | 
			
		||||
                                            {% endfor %}
 | 
			
		||||
                                            {% for log in badge.multi_day_trips_over_required_distance %}
 | 
			
		||||
                                                {{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}
 | 
			
		||||
                                            {% endfor %}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </details>
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                    <div class="mt-3">
 | 
			
		||||
                                        Wir haben leider kein Geburtsdatum von dir und können dir leider deinen heurigen Status für das Fahrtenabzeichen nicht anzeigen. Wenn du dein Geburtsdatum an <a href="mailto:it@rudernlinz.at" class="underline">it@rudernlinz.at</a> schreibst, lässt sich das ändern :-)
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </details>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="py-3">
 | 
			
		||||
                            <details>
 | 
			
		||||
                                <summary>
 | 
			
		||||
                                    <h3 class="mb-3 inline">
 | 
			
		||||
                                        {% set price = achievements.equatorprice %}
 | 
			
		||||
                                        <span class="text-xl">
 | 
			
		||||
                                            {% if achievements.curr_equatorprice_name == "-" %}
 | 
			
		||||
                                                📋
 | 
			
		||||
                                            {% elif achievements.curr_equatorprice_name == "Bronze" %}
 | 
			
		||||
                                                🥉
 | 
			
		||||
                                            {% elif achievements.curr_equatorprice_name == "Silber" %}
 | 
			
		||||
                                                🥈
 | 
			
		||||
                                            {% elif achievements.curr_equatorprice_name == "Gold" %}
 | 
			
		||||
                                                🥇
 | 
			
		||||
                                            {% elif achievements.curr_equatorprice_name == "Diamant" %}
 | 
			
		||||
                                                💍
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                        Äquatorpreis
 | 
			
		||||
                                        <span><a href="http://www.rudern.at/OFFICE/Downloads/Ausschreibungen/2022/Wanderfahrten//Fahrtenabzeichen%20%C3%84quatorpreis%20und%20Danubius%202022.pdf"
 | 
			
		||||
   target="_blank"
 | 
			
		||||
   class="w-6 h-6 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2 text-white">?</a></span>
 | 
			
		||||
                                    </h3>
 | 
			
		||||
                                </summary>
 | 
			
		||||
                                <div class="mt-3">
 | 
			
		||||
                                    {% if price.level == "DONE" %}
 | 
			
		||||
                                        Gratuliere, du hast alles in deinem Rudererleben erreicht, was es (beim Äquatorpreis) zu erreichen gibt.
 | 
			
		||||
                                    {% else %}
 | 
			
		||||
                                        <label for="equatorprice" class="label">{{ price.desc }} ({{ price.rowed_km }} / {{ price.required_km }} km)</label>
 | 
			
		||||
                                        <progress id="equatorprice"
 | 
			
		||||
                                                  class="w-full block my-3"
 | 
			
		||||
                                                  value="{{ price.rowed_km }}"
 | 
			
		||||
                                                  max="{{ price.required_km }}"></progress>
 | 
			
		||||
                                        <details>
 | 
			
		||||
                                            <summary>Details</summary>
 | 
			
		||||
                                            Du bist insgesamt {{ price.rowed_km }} km gerudert. Um den Äquatorpreis in {{ price.desc }} zu erhalten, benötigst du noch {{ price.missing_km }} km um die notwendigen {{ price.required_km }} km zu erreichen.
 | 
			
		||||
                                        </details>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </details>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
 | 
			
		||||
                     role="alert">
 | 
			
		||||
                    <h2 class="h2">Aktives Vereinsmitglied</h2>
 | 
			
		||||
@@ -172,6 +304,10 @@
 | 
			
		||||
                            <a href="/admin/notification"
 | 
			
		||||
                               class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="py-1">
 | 
			
		||||
                            <a href="/board/achievement"
 | 
			
		||||
                               class="block w-100 py-2 hover:text-primary-600">Abzeichen</a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user