aequatorpreis #729

Merged
philipp merged 4 commits from aequatorpreis into staging 2024-09-04 14:28:47 +02:00
10 changed files with 457 additions and 37 deletions
Showing only changes of commit 6df24f0f22 - Show all commits

View File

@ -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

View File

@ -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,
} }
} }
} }

View File

@ -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,
} }
} }
} }

View File

@ -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,
})
} }
} }

View File

@ -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,

View 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]
}

View File

@ -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
} }

View 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 %}

View File

@ -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

View File

@ -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 %}&#127881;{% 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 %}
&#9989;
{% else %}
&#10060;
{% endif %}
1 mehrtägige Wanderfahrt > {{ badge.multi_day_trips_required_distance }} km
</li>
<li>
{% if badge.single_day_trips_over_required_distance | length >= 2 %}
&#9989;
{% else %}
&#10060;
{% 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 %}