aequatorpreis #729
@@ -31,6 +31,12 @@ impl PartialEq for Logbook {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) enum Filter {
 | 
				
			||||||
 | 
					    SingleDayOnly,
 | 
				
			||||||
 | 
					    MultiDazOnly,
 | 
				
			||||||
 | 
					    None,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(FromForm, Debug, Clone)]
 | 
					#[derive(FromForm, Debug, Clone)]
 | 
				
			||||||
pub struct LogToAdd {
 | 
					pub struct LogToAdd {
 | 
				
			||||||
    pub boat_id: i32,
 | 
					    pub boat_id: i32,
 | 
				
			||||||
@@ -293,6 +299,49 @@ ORDER BY departure DESC
 | 
				
			|||||||
        ret
 | 
					        ret
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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> {
 | 
					    pub async fn completed(db: &SqlitePool) -> Vec<LogbookWithBoatAndRowers> {
 | 
				
			||||||
        let year = chrono::Local::now().year();
 | 
					        let year = chrono::Local::now().year();
 | 
				
			||||||
        Self::completed_in_year(db, year).await
 | 
					        Self::completed_in_year(db, year).await
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,9 @@
 | 
				
			|||||||
use serde::Serialize;
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use sqlx::SqlitePool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize)]
 | 
					#[derive(Serialize, PartialEq, Debug)]
 | 
				
			||||||
enum Level {
 | 
					pub(crate) enum Level {
 | 
				
			||||||
 | 
					    NONE,
 | 
				
			||||||
    BRONZE,
 | 
					    BRONZE,
 | 
				
			||||||
    SILVER,
 | 
					    SILVER,
 | 
				
			||||||
    GOLD,
 | 
					    GOLD,
 | 
				
			||||||
@@ -17,6 +19,7 @@ impl Level {
 | 
				
			|||||||
            Level::GOLD => 100000,
 | 
					            Level::GOLD => 100000,
 | 
				
			||||||
            Level::DIAMOND => 200000,
 | 
					            Level::DIAMOND => 200000,
 | 
				
			||||||
            Level::DONE => 0,
 | 
					            Level::DONE => 0,
 | 
				
			||||||
 | 
					            Level::NONE => 0,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,26 +36,53 @@ impl Level {
 | 
				
			|||||||
            Level::DONE
 | 
					            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)]
 | 
					#[derive(Serialize)]
 | 
				
			||||||
pub(crate) struct Next {
 | 
					pub(crate) struct Next {
 | 
				
			||||||
    level: Level,
 | 
					    level: Level,
 | 
				
			||||||
 | 
					    desc: String,
 | 
				
			||||||
    missing_km: i32,
 | 
					    missing_km: i32,
 | 
				
			||||||
    required_km: i32,
 | 
					    required_km: i32,
 | 
				
			||||||
    rowed_km: i32,
 | 
					    rowed_km: i32,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Next {
 | 
					impl Next {
 | 
				
			||||||
    pub(crate) fn rowed_km(km: i32) -> Self {
 | 
					    pub(crate) fn new(rowed_km: i32) -> Self {
 | 
				
			||||||
        let level = Level::next_level(km);
 | 
					        let level = Level::next_level(rowed_km);
 | 
				
			||||||
        let required_km = level.required_km();
 | 
					        let required_km = level.required_km();
 | 
				
			||||||
        let missing_km = required_km - km;
 | 
					        let missing_km = required_km - rowed_km;
 | 
				
			||||||
        Self {
 | 
					        Self {
 | 
				
			||||||
 | 
					            desc: level.desc().to_string(),
 | 
				
			||||||
            level,
 | 
					            level,
 | 
				
			||||||
            missing_km,
 | 
					            missing_km,
 | 
				
			||||||
            required_km,
 | 
					            required_km,
 | 
				
			||||||
            rowed_km: km,
 | 
					            rowed_km,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					use chrono::{Datelike, Local};
 | 
				
			||||||
 | 
					use equatorprice::Level;
 | 
				
			||||||
use serde::Serialize;
 | 
					use serde::Serialize;
 | 
				
			||||||
use sqlx::SqlitePool;
 | 
					use sqlx::SqlitePool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -8,14 +10,46 @@ pub(crate) mod rowingbadge;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize)]
 | 
					#[derive(Serialize)]
 | 
				
			||||||
pub(crate) struct Achievements {
 | 
					pub(crate) struct Achievements {
 | 
				
			||||||
    equatorprice: equatorprice::Next,
 | 
					    pub(crate) equatorprice: equatorprice::Next,
 | 
				
			||||||
 | 
					    pub(crate) curr_equatorprice_name: String,
 | 
				
			||||||
 | 
					    pub(crate) new_equatorprice_this_season: bool,
 | 
				
			||||||
 | 
					    pub(crate) rowingbadge: Option<rowingbadge::Status>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Achievements {
 | 
					impl Achievements {
 | 
				
			||||||
    pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Self {
 | 
					    pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Self {
 | 
				
			||||||
        let rowed_km = Stat::person(db, None, user).await.rowed_km;
 | 
					        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
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        println!(
 | 
				
			||||||
 | 
					            "old: {}; new: {}",
 | 
				
			||||||
 | 
					            rowed_km,
 | 
				
			||||||
 | 
					            rowed_km - rowed_km_this_season
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        println!(
 | 
				
			||||||
 | 
					            "old: {:?}; new: {:?}",
 | 
				
			||||||
 | 
					            Level::curr_level(rowed_km),
 | 
				
			||||||
 | 
					            Level::curr_level(rowed_km - rowed_km_this_season)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        let new_equatorprice_this_season =
 | 
				
			||||||
 | 
					            Level::curr_level(rowed_km) != Level::curr_level(rowed_km - rowed_km_this_season);
 | 
				
			||||||
 | 
					        println!("{new_equatorprice_this_season:?}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Self {
 | 
					        Self {
 | 
				
			||||||
            equatorprice: equatorprice::Next::rowed_km(rowed_km),
 | 
					            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,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,14 @@
 | 
				
			|||||||
use chrono::{Datelike, Local, NaiveDate};
 | 
					use std::cmp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::model::user::User;
 | 
					use chrono::{Datelike, Local, NaiveDate};
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use sqlx::SqlitePool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::model::{
 | 
				
			||||||
 | 
					    logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
 | 
				
			||||||
 | 
					    stat::Stat,
 | 
				
			||||||
 | 
					    user::User,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum AgeBracket {
 | 
					enum AgeBracket {
 | 
				
			||||||
    Till14,
 | 
					    Till14,
 | 
				
			||||||
@@ -11,6 +19,52 @@ enum AgeBracket {
 | 
				
			|||||||
    From76,
 | 
					    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 {
 | 
					impl TryFrom<&User> for AgeBracket {
 | 
				
			||||||
    type Error = String;
 | 
					    type Error = String;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -39,24 +93,71 @@ impl TryFrom<&User> for AgeBracket {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn cat(value: &AgeBracket) -> &str {
 | 
					#[derive(Serialize)]
 | 
				
			||||||
    match value {
 | 
					pub(crate) struct Status {
 | 
				
			||||||
        AgeBracket::Till14 => "Schülerinnen und Schüler bis 14 Jahre",
 | 
					    pub(crate) year: i32,
 | 
				
			||||||
        AgeBracket::From14Till18 => "Juniorinnen und Junioren, Para-Ruderer bis 18 Jahre",
 | 
					    rowed_km: i32,
 | 
				
			||||||
        AgeBracket::From19Till30 => "Frauen und Männer, Para-Ruderer bis 30 Jahre",
 | 
					    category: String,
 | 
				
			||||||
        AgeBracket::From31Till60 => "Frauen und Männer, Para-Ruderer von 31 bis 60 Jahre",
 | 
					    required_km: i32,
 | 
				
			||||||
        AgeBracket::From61Till75 => "Frauen und Männer, Para-Ruderer von 61 bis 75 Jahre",
 | 
					    missing_km: i32,
 | 
				
			||||||
        AgeBracket::From76 => "Frauen und Männer, Para-Ruderer ab 76 Jahre",
 | 
					    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,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn dist_in_km(value: &AgeBracket) -> u32 {
 | 
					impl Status {
 | 
				
			||||||
    match value {
 | 
					    pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
 | 
				
			||||||
        AgeBracket::Till14 => 500,
 | 
					        let Ok(agebracket) = AgeBracket::try_from(user) else {
 | 
				
			||||||
        AgeBracket::From14Till18 => 1000,
 | 
					            return None;
 | 
				
			||||||
        AgeBracket::From19Till30 => 1200,
 | 
					        };
 | 
				
			||||||
        AgeBracket::From31Till60 => 1000,
 | 
					        let category = agebracket.cat().to_string();
 | 
				
			||||||
        AgeBracket::From61Till75 => 800,
 | 
					
 | 
				
			||||||
        AgeBracket::From76 => 600,
 | 
					        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()
 | 
					        .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 {
 | 
					    pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
 | 
				
			||||||
        let year = match year {
 | 
					        let year = match year {
 | 
				
			||||||
            Some(year) => 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;
 | 
					use rocket::Route;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod achievement;
 | 
				
			||||||
pub mod boathouse;
 | 
					pub mod boathouse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn routes() -> Vec<Route> {
 | 
					pub fn routes() -> Vec<Route> {
 | 
				
			||||||
    let mut ret = Vec::new();
 | 
					    let mut ret = Vec::new();
 | 
				
			||||||
    ret.append(&mut boathouse::routes());
 | 
					    ret.append(&mut boathouse::routes());
 | 
				
			||||||
 | 
					    ret.append(&mut achievement::routes());
 | 
				
			||||||
    ret
 | 
					    ret
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										74
									
								
								templates/achievement.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								templates/achievement.html.tera
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					{% import "includes/macros" as macros %}
 | 
				
			||||||
 | 
					{% import "includes/forms/log" as log %}
 | 
				
			||||||
 | 
					{% extends "base" %}
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="/public/table.css" />
 | 
				
			||||||
 | 
					    <div class="max-w-screen-lg 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>Äquatorpreis</th>
 | 
				
			||||||
 | 
					                        <th>Fahrtenabzeichen (FA) 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.new_equatorprice_this_season %}(NEU!) {% endif %}{{ achievement.curr_equatorprice_name }}  </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>no birthdate of this person</td>
 | 
				
			||||||
 | 
					                                <td>no birthdate of this person</td>
 | 
				
			||||||
 | 
					                                <td>no birthdate of this person</td>
 | 
				
			||||||
 | 
					                                <td>no birthdate of this person</td>
 | 
				
			||||||
 | 
					                                <td>no birthdate of this person</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>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
{% endmacro show %}
 | 
					{% 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"
 | 
					    <div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
 | 
				
			||||||
         data-filterable="true"
 | 
					         data-filterable="true"
 | 
				
			||||||
         data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}">
 | 
					         data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}">
 | 
				
			||||||
        <details>
 | 
					        <details>
 | 
				
			||||||
            <summary style="list-style: none;">
 | 
					            <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">
 | 
					                    <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 %}
 | 
					                        {% if log.logtype == 1 %}
 | 
				
			||||||
                            Wanderfahrt
 | 
					                            Wanderfahrt
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -79,25 +79,79 @@
 | 
				
			|||||||
                    <h2 class="h2">Persönliches</h2>
 | 
					                    <h2 class="h2">Persönliches</h2>
 | 
				
			||||||
                    <div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
 | 
					                    <div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
 | 
				
			||||||
                        <div class="py-3">
 | 
					                        <div class="py-3">
 | 
				
			||||||
                            <h3 class="font-bold text-xl mb-3">Äquatorpreis</h3>
 | 
					                            <h3 class="font-bold text-xl">
 | 
				
			||||||
 | 
					                                {% if achievements.rowingbadge and achievements.rowingbadge.achieved %}🎉{% endif %}
 | 
				
			||||||
 | 
					                                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-7 h-7 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2">?</a></span>
 | 
				
			||||||
 | 
					                            </h3>
 | 
				
			||||||
 | 
					                            {% 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 1 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 %}
 | 
				
			||||||
 | 
					                                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 :-)
 | 
				
			||||||
 | 
					                            {% endif %}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="py-3">
 | 
				
			||||||
 | 
					                            <h3 class="font-bold text-xl mb-3">
 | 
				
			||||||
 | 
					                                Ä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-7 h-7 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2">?</a></span>
 | 
				
			||||||
 | 
					                            </h3>
 | 
				
			||||||
                            {% set price = achievements.equatorprice %}
 | 
					                            {% set price = achievements.equatorprice %}
 | 
				
			||||||
                            {% if price.level == "DONE" %}
 | 
					                            {% if price.level == "DONE" %}
 | 
				
			||||||
                                Gratuliere, du hast alles erreicht, was es beim Äquatorpreis zu erreichen gibt.
 | 
					                                Gratuliere, du hast alles in deinem Rudererleben erreicht, was es (beim Äquatorpreis) zu erreichen gibt.
 | 
				
			||||||
                            {% else %}
 | 
					                            {% else %}
 | 
				
			||||||
                                <label for="equatorprice" class="label">{{ price.level }}</label>
 | 
					                                <label for="equatorprice" class="label">{{ price.desc }} ({{ price.rowed_km }} / {{ price.required_km }} km)</label>
 | 
				
			||||||
                                <progress id="equatorprice"
 | 
					                                <progress id="equatorprice"
 | 
				
			||||||
                                          class="w-full block my-3"
 | 
					                                          class="w-full block my-3"
 | 
				
			||||||
                                          value="{{ price.rowed_km }}"
 | 
					                                          value="{{ price.rowed_km }}"
 | 
				
			||||||
                                          max="{{ price.required_km }}"></progress>
 | 
					                                          max="{{ price.required_km }}"></progress>
 | 
				
			||||||
                                <details>
 | 
					                                <details>
 | 
				
			||||||
                                    <summary>Details</summary>
 | 
					                                    <summary>Details</summary>
 | 
				
			||||||
                                    Du bist insgesamt {{ price.rowed_km }} km gerudert. Um den Äquatorpreis in {{ price.level }} zu erhalten, benötigst du noch {{ price.missing_km }} um die notwendigen {{ price.required_km }} km zu erreichen.
 | 
					                                    Du bist insgesamt {{ price.rowed_km }} km gerudert. Um den Äquatorpreis in {{ price.level }} zu erhalten, benötigst du noch {{ price.missing_km }} km um die notwendigen {{ price.required_km }} km zu erreichen.
 | 
				
			||||||
                                </details>
 | 
					                                </details>
 | 
				
			||||||
                            {% endif %}
 | 
					                            {% endif %}
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                        <div class="py-1">
 | 
					 | 
				
			||||||
                            <h3>Fahrtenabzeichen</h3>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
 | 
					                <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
 | 
				
			||||||
@@ -198,6 +252,10 @@
 | 
				
			|||||||
                            <a href="/admin/notification"
 | 
					                            <a href="/admin/notification"
 | 
				
			||||||
                               class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
 | 
					                               class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
 | 
				
			||||||
                        </li>
 | 
					                        </li>
 | 
				
			||||||
 | 
					                        <li class="py-1">
 | 
				
			||||||
 | 
					                            <a href="/board/achievement"
 | 
				
			||||||
 | 
					                               class="block w-100 py-2 hover:text-primary-600">Abzeichen</a>
 | 
				
			||||||
 | 
					                        </li>
 | 
				
			||||||
                    </ul>
 | 
					                    </ul>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user