aequatorpreis #730
@ -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,93 @@ ORDER BY departure DESC
|
|||||||
ret
|
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> {
|
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
|
||||||
|
@ -25,6 +25,7 @@ pub mod logbook;
|
|||||||
pub mod logtype;
|
pub mod logtype;
|
||||||
pub mod mail;
|
pub mod mail;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
|
pub mod personal;
|
||||||
pub mod role;
|
pub mod role;
|
||||||
pub mod rower;
|
pub mod rower;
|
||||||
pub mod stat;
|
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()
|
.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
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ use crate::{
|
|||||||
model::{
|
model::{
|
||||||
logbook::Logbook,
|
logbook::Logbook,
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
|
personal::Achievements,
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{User, UserWithDetails},
|
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("last_trips", &last_trips);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.insert("achievements", &Achievements::for_user(db, &user).await);
|
||||||
context.insert("notifications", &Notification::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("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||||
context.insert("costs_scheckbuch", &SCHECKBUCH);
|
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>
|
||||||
</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
|
||||||
|
@ -74,6 +74,138 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% 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"
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
role="alert">
|
role="alert">
|
||||||
<h2 class="h2">Aktives Vereinsmitglied</h2>
|
<h2 class="h2">Aktives Vereinsmitglied</h2>
|
||||||
@ -172,6 +304,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 %}
|
||||||
|
Loading…
Reference in New Issue
Block a user