notfiication-on-new-personal-stat #843

Merged
philipp merged 4 commits from notfiication-on-new-personal-stat into main 2025-01-09 16:19:40 +01:00
15 changed files with 410 additions and 250 deletions

View File

@ -1,5 +1,3 @@
use std::collections::HashMap;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
@ -7,6 +5,93 @@ use crate::tera::board::boathouse::FormBoathouseToAdd;
use super::boat::Boat; use super::boat::Boat;
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathousePlace {
boat: Boat,
boathouse_id: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseRack {
boats: [Option<BoathousePlace>; 12],
}
impl BoathouseRack {
fn new() -> Self {
let boats = [
None, None, None, None, None, None, None, None, None, None, None, None,
];
Self { boats }
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
self.boats[boathouse.level as usize] = Some(BoathousePlace {
boat: Boat::find_by_id(db, boathouse.boat_id as i32)
.await
.unwrap(),
boathouse_id: boathouse.id,
});
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseSide {
mountain: BoathouseRack,
water: BoathouseRack,
}
impl BoathouseSide {
fn new() -> Self {
Self {
mountain: BoathouseRack::new(),
water: BoathouseRack::new(),
}
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
match boathouse.side.as_str() {
"mountain" => self.mountain.add(db, boathouse).await,
"water" => self.water.add(db, boathouse).await,
_ => panic!("db constraint failed"),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseAisles {
mountain: BoathouseSide,
middle: BoathouseSide,
water: BoathouseSide,
}
impl BoathouseAisles {
fn new() -> Self {
Self {
mountain: BoathouseSide::new(),
middle: BoathouseSide::new(),
water: BoathouseSide::new(),
}
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
match boathouse.aisle.as_str() {
"water" => self.water.add(db, boathouse).await,
"middle" => self.middle.add(db, boathouse).await,
"mountain" => self.mountain.add(db, boathouse).await,
_ => panic!("db constraint failed"),
};
}
pub async fn from(db: &SqlitePool, boathouses: Vec<Boathouse>) -> Self {
let mut ret = BoathouseAisles::new();
for boathouse in boathouses {
ret.add(db, boathouse).await;
}
ret
}
}
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Boathouse { pub struct Boathouse {
pub id: i64, pub id: i64,
@ -17,54 +102,7 @@ pub struct Boathouse {
} }
impl Boathouse { impl Boathouse {
pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> { pub async fn get(db: &SqlitePool) -> BoathouseAisles {
let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> = HashMap::new();
let mut mountain = HashMap::new();
mountain.insert(
"mountain",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
mountain.insert(
"water",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
ret.insert("mountain-aisle", mountain);
let mut middle = HashMap::new();
middle.insert(
"mountain",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
middle.insert(
"water",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
ret.insert("middle-aisle", middle);
let mut water = HashMap::new();
water.insert(
"mountain",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
water.insert(
"water",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
ret.insert("water-aisle", water);
let boathouses = sqlx::query_as!( let boathouses = sqlx::query_as!(
Boathouse, Boathouse,
"SELECT id, boat_id, aisle, side, level FROM boathouse" "SELECT id, boat_id, aisle, side, level FROM boathouse"
@ -73,21 +111,7 @@ impl Boathouse {
.await .await
.unwrap(); //TODO: fixme .unwrap(); //TODO: fixme
for boathouse in boathouses { BoathouseAisles::from(db, boathouses).await
let aisle = ret
.get_mut(format!("{}-aisle", boathouse.aisle).as_str())
.unwrap();
let side = aisle.get_mut(boathouse.side.as_str()).unwrap();
side[boathouse.level as usize] = Some((
boathouse.id,
Boat::find_by_id(db, boathouse.boat_id as i32)
.await
.unwrap(),
));
}
ret
} }
pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> { pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> {

View File

@ -33,8 +33,7 @@ impl PartialEq for Logbook {
pub(crate) enum Filter { pub(crate) enum Filter {
SingleDayOnly, SingleDayOnly,
MultiDazOnly, MultiDayOnly,
None,
} }
#[derive(FromForm, Debug, Clone)] #[derive(FromForm, Debug, Clone)]
@ -362,12 +361,13 @@ ORDER BY departure DESC
} }
} }
pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year( pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year_tx(
db: &SqlitePool, db: &mut Transaction<'_, Sqlite>,
user: &User, user: &User,
min_distance: i32, min_distance: i32,
year: i32, year: i32,
filter: Filter, filter: Filter,
exclude_last_log: bool,
) -> Vec<LogbookWithBoatAndRowers> { ) -> Vec<LogbookWithBoatAndRowers> {
let logs: Vec<Logbook> = sqlx::query_as( let logs: Vec<Logbook> = sqlx::query_as(
&format!(" &format!("
@ -378,7 +378,7 @@ ORDER BY departure DESC
ORDER BY arrival DESC ORDER BY arrival DESC
", user.id, min_distance, year) ", user.id, min_distance, year)
) )
.fetch_all(db) .fetch_all(db.deref_mut())
.await .await
.unwrap(); //TODO: fixme .unwrap(); //TODO: fixme
@ -389,19 +389,20 @@ ORDER BY departure DESC
match filter { match filter {
Filter::SingleDayOnly => { Filter::SingleDayOnly => {
if trip_days == 0 { if trip_days == 0 {
ret.push(LogbookWithBoatAndRowers::from(db, log).await); ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
} }
} }
Filter::MultiDazOnly => { Filter::MultiDayOnly => {
if trip_days > 0 { if trip_days > 0 {
ret.push(LogbookWithBoatAndRowers::from(db, log).await); ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
}
}
Filter::None => {
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
} }
} }
} }
}
if exclude_last_log {
ret.pop();
}
ret ret
} }

View File

@ -1,63 +1,64 @@
use crate::model::{logbook::Logbook, stat::Stat, user::User};
use serde::Serialize; use serde::Serialize;
#[derive(Serialize, PartialEq, Debug)] #[derive(Serialize, PartialEq, Debug)]
pub(crate) enum Level { pub(crate) enum Level {
NONE, None,
BRONZE, Bronze,
SILVER, Silver,
GOLD, Gold,
DIAMOND, Diamond,
DONE, Done,
} }
impl Level { impl Level {
fn required_km(&self) -> i32 { fn required_km(&self) -> i32 {
match self { match self {
Level::BRONZE => 40000, Level::Bronze => 40_000,
Level::SILVER => 80000, Level::Silver => 80_000,
Level::GOLD => 100000, Level::Gold => 100_000,
Level::DIAMOND => 200000, Level::Diamond => 200_000,
Level::DONE => 0, Level::Done => 0,
Level::NONE => 0, Level::None => 0,
} }
} }
fn next_level(km: i32) -> Self { fn next_level(km: i32) -> Self {
if km < Level::BRONZE.required_km() { if km < Level::Bronze.required_km() {
Level::BRONZE Level::Bronze
} else if km < Level::SILVER.required_km() { } else if km < Level::Silver.required_km() {
Level::SILVER Level::Silver
} else if km < Level::GOLD.required_km() { } else if km < Level::Gold.required_km() {
Level::GOLD Level::Gold
} else if km < Level::DIAMOND.required_km() { } else if km < Level::Diamond.required_km() {
Level::DIAMOND Level::Diamond
} else { } else {
Level::DONE Level::Done
} }
} }
pub(crate) fn curr_level(km: i32) -> Self { pub(crate) fn curr_level(km: i32) -> Self {
if km < Level::BRONZE.required_km() { if km < Level::Bronze.required_km() {
Level::NONE Level::None
} else if km < Level::SILVER.required_km() { } else if km < Level::Silver.required_km() {
Level::BRONZE Level::Bronze
} else if km < Level::GOLD.required_km() { } else if km < Level::Gold.required_km() {
Level::SILVER Level::Silver
} else if km < Level::DIAMOND.required_km() { } else if km < Level::Diamond.required_km() {
Level::GOLD Level::Gold
} else { } else {
Level::DIAMOND Level::Diamond
} }
} }
pub(crate) fn desc(&self) -> &str { pub(crate) fn desc(&self) -> &str {
match self { match self {
Level::BRONZE => "Bronze", Level::Bronze => "Bronze",
Level::SILVER => "Silber", Level::Silver => "Silber",
Level::GOLD => "Gold", Level::Gold => "Gold",
Level::DIAMOND => "Diamant", Level::Diamond => "Diamant",
Level::DONE => "", Level::Done => "",
Level::NONE => "-", Level::None => "-",
} }
} }
} }
@ -85,3 +86,19 @@ impl Next {
} }
} }
} }
pub(crate) async fn new_level_with_last_log(
db: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
user: &User,
) -> Option<String> {
let rowed_km = Stat::total_km_tx(db, user).await.rowed_km;
if let Some(last_logbookentry) = Logbook::completed_with_user_tx(db, user).await.last() {
let last_trip_km = last_logbookentry.logbook.distance_in_km.unwrap();
if Level::curr_level(rowed_km) != Level::curr_level(rowed_km - last_trip_km as i32) {
return Some(Level::curr_level(rowed_km).desc().to_string());
}
}
None
}

View File

@ -2,7 +2,7 @@ use std::cmp;
use chrono::{Datelike, Local, NaiveDate}; use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::{Sqlite, SqlitePool, Transaction};
use crate::model::{ use crate::model::{
logbook::{Filter, Logbook, LogbookWithBoatAndRowers}, logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
@ -111,11 +111,44 @@ pub(crate) struct Status {
} }
impl Status { impl Status {
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> { fn calc(
agebracket: &AgeBracket,
rowed_km: i32,
single_day_trips_over_required_distance: usize,
multi_day_trips_over_required_distance: usize,
year: i32,
) -> Self {
let category = agebracket.cat().to_string();
let required_km = agebracket.dist_in_km();
let missing_km = cmp::max(required_km - rowed_km, 0);
let achieved = missing_km == 0
&& (multi_day_trips_over_required_distance >= 1
|| single_day_trips_over_required_distance >= 2);
Self {
year,
rowed_km,
category,
required_km,
missing_km,
multi_day_trips_over_required_distance: vec![],
single_day_trips_over_required_distance: vec![],
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,
}
}
pub(crate) async fn for_user_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
exclude_last_log: bool,
) -> Option<Self> {
let Ok(agebracket) = AgeBracket::try_from(user) else { let Ok(agebracket) = AgeBracket::try_from(user) else {
return None; return None;
}; };
let category = agebracket.cat().to_string();
let year = if Local::now().month() == 1 { let year = if Local::now().month() == 1 {
Local::now().year() - 1 Local::now().year() - 1
@ -123,44 +156,66 @@ impl Status {
Local::now().year() Local::now().year()
}; };
let rowed_km = Stat::person(db, Some(year), user).await.rowed_km; let rowed_km = Stat::person_tx(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 = let single_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year( Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
db, db,
user, user,
agebracket.required_dist_single_day_in_km(), agebracket.required_dist_single_day_in_km(),
year, year,
Filter::SingleDayOnly, Filter::SingleDayOnly,
exclude_last_log,
) )
.await; .await;
let multi_day_trips_over_required_distance = let multi_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year( Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
db, db,
user, user,
agebracket.required_dist_multi_day_in_km(), agebracket.required_dist_multi_day_in_km(),
year, year,
Filter::MultiDazOnly, Filter::MultiDayOnly,
exclude_last_log,
) )
.await; .await;
let achieved = missing_km == 0 let ret = Self::calc(
&& (multi_day_trips_over_required_distance.len() >= 1 &agebracket,
|| single_day_trips_over_required_distance.len() >= 2); rowed_km,
single_day_trips_over_required_distance.len(),
multi_day_trips_over_required_distance.len(),
year,
);
Some(Self { Some(Self {
year,
rowed_km,
category,
required_km,
missing_km,
multi_day_trips_over_required_distance, multi_day_trips_over_required_distance,
single_day_trips_over_required_distance, single_day_trips_over_required_distance,
multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(), ..ret
single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(),
achieved,
}) })
} }
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
let mut tx = db.begin().await.unwrap();
let ret = Self::for_user_tx(&mut tx, user, false).await;
tx.commit().await.unwrap();
ret
}
pub(crate) async fn completed_with_last_log(
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> bool {
if let Some(status) = Self::for_user_tx(db, user, false).await {
// if user has agebracket...
if status.achieved {
// ... and has achieved the 'Fahrtenabzeichen'
let without_last_entry = Self::for_user_tx(db, user, true).await.unwrap();
if !without_last_entry.achieved {
// ... and this wasn't the case before the last logentry
return true;
}
}
}
false
}
} }

View File

@ -1,9 +1,9 @@
use std::collections::HashMap; use std::{collections::HashMap, ops::DerefMut};
use crate::model::user::User; use crate::model::user::User;
use chrono::Datelike; use chrono::Datelike;
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool}; use sqlx::{FromRow, Row, Sqlite, SqlitePool, Transaction};
use super::boat::Boat; use super::boat::Boat;
@ -218,7 +218,7 @@ ORDER BY rowed_km DESC, u.name;
.collect() .collect()
} }
pub async fn total_km(db: &SqlitePool, user: &User) -> Stat { pub async fn total_km_tx(db: &mut Transaction<'_, Sqlite>, user: &User) -> Stat {
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let row = sqlx::query(&format!( let row = sqlx::query(&format!(
" "
@ -233,7 +233,7 @@ WHERE l.distance_in_km IS NOT NULL;
", ",
user.id user.id
)) ))
.fetch_one(db) .fetch_one(db.deref_mut())
.await .await
.unwrap(); .unwrap();
@ -244,7 +244,18 @@ WHERE l.distance_in_km IS NOT NULL;
} }
} }
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat { pub async fn total_km(db: &SqlitePool, user: &User) -> Stat {
let mut tx = db.begin().await.unwrap();
let ret = Self::total_km_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub async fn person_tx(
db: &mut Transaction<'_, Sqlite>,
year: Option<i32>,
user: &User,
) -> Stat {
let year = match year { let year = match year {
Some(year) => year, Some(year) => year,
None => chrono::Local::now().year(), None => chrono::Local::now().year(),
@ -263,7 +274,7 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
", ",
user.id user.id
)) ))
.fetch_one(db) .fetch_one(db.deref_mut())
.await .await
.unwrap(); .unwrap();
@ -273,6 +284,13 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
rowed_km: row.get("rowed_km"), rowed_km: row.get("rowed_km"),
} }
} }
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
let mut tx = db.begin().await.unwrap();
let ret = Self::person_tx(&mut tx, year, user).await;
tx.commit().await.unwrap();
ret
}
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]

View File

@ -287,11 +287,9 @@ WHERE day=?
return Err(TripUpdateError::NotYourTrip); return Err(TripUpdateError::NotYourTrip);
} }
if update.trip_type != Some(4) { if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await {
if !update.cox.allowed_to_steer(db).await {
return Err(TripUpdateError::TripTypeNotAllowed); return Err(TripUpdateError::TripTypeNotAllowed);
} }
}
let Some(trip_details_id) = update.trip.trip_details_id else { let Some(trip_details_id) = update.trip.trip_details_id else {
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove? return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?

58
src/model/user/fee.rs Normal file
View File

@ -0,0 +1,58 @@
use super::User;
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct Fee {
pub sum_in_cents: i32,
pub parts: Vec<(String, i32)>,
pub name: String,
pub user_ids: String,
pub paid: bool,
pub users: Vec<User>,
}
impl Default for Fee {
fn default() -> Self {
Self::new()
}
}
impl Fee {
pub fn new() -> Self {
Self {
sum_in_cents: 0,
name: "".into(),
parts: Vec::new(),
user_ids: "".into(),
users: Vec::new(),
paid: false,
}
}
pub fn add(&mut self, desc: String, price_in_cents: i32) {
self.sum_in_cents += price_in_cents;
self.parts.push((desc, price_in_cents));
}
pub fn add_person(&mut self, user: &User) {
if !self.name.is_empty() {
self.name.push_str(" + ");
self.user_ids.push('&');
}
self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
self.users.push(user.clone());
}
pub fn paid(&mut self) {
self.paid = true;
}
pub fn merge(&mut self, fee: Fee) {
for (desc, price_in_cents) in fee.parts {
self.add(desc, price_in_cents);
}
}
}

View File

@ -15,14 +15,25 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{ use super::{
family::Family, log::Log, logbook::Logbook, mail::Mail, notification::Notification, role::Role, family::Family,
stat::Stat, tripdetails::TripDetails, Day, log::Log,
logbook::Logbook,
mail::Mail,
notification::Notification,
personal::{equatorprice, rowingbadge},
role::Role,
stat::Stat,
tripdetails::TripDetails,
Day,
}; };
use crate::{ use crate::{
tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE, tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE,
EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG,
SCHECKBUCH, STUDENT_OR_PUPIL, UNTERSTUETZEND, SCHECKBUCH, STUDENT_OR_PUPIL, UNTERSTUETZEND,
}; };
use fee::Fee;
mod fee;
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] #[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct User { pub struct User {
@ -83,62 +94,6 @@ pub enum LoginError {
DeserializationError, DeserializationError,
} }
#[derive(Debug, Serialize)]
pub struct Fee {
pub sum_in_cents: i32,
pub parts: Vec<(String, i32)>,
pub name: String,
pub user_ids: String,
pub paid: bool,
pub users: Vec<User>,
}
impl Default for Fee {
fn default() -> Self {
Self::new()
}
}
impl Fee {
pub fn new() -> Self {
Self {
sum_in_cents: 0,
name: "".into(),
parts: Vec::new(),
user_ids: "".into(),
users: Vec::new(),
paid: false,
}
}
pub fn add(&mut self, desc: String, price_in_cents: i32) {
self.sum_in_cents += price_in_cents;
self.parts.push((desc, price_in_cents));
}
pub fn add_person(&mut self, user: &User) {
if !self.name.is_empty() {
self.name.push_str(" + ");
self.user_ids.push('&');
}
self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
self.users.push(user.clone());
}
pub fn paid(&mut self) {
self.paid = true;
}
pub fn merge(&mut self, fee: Fee) {
for (desc, price_in_cents) in fee.parts {
self.add(desc, price_in_cents);
}
}
}
impl User { impl User {
pub async fn allowed_to_steer(&self, db: &SqlitePool) -> bool { pub async fn allowed_to_steer(&self, db: &SqlitePool) -> bool {
self.has_role(db, "cox").await || self.has_role(db, "Bootsführer").await self.has_role(db, "cox").await || self.has_role(db, "Bootsführer").await
@ -440,14 +395,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
} }
} else if self.has_role(db, "Ehrenmitglied").await { } else if self.has_role(db, "Ehrenmitglied").await {
fee.add("Ehrenmitglied".into(), 0); fee.add("Ehrenmitglied".into(), 0);
} else { } else if halfprice {
if halfprice {
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2); fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
} else { } else {
fee.add("Mitgliedsbeitrag".into(), REGULAR); fee.add("Mitgliedsbeitrag".into(), REGULAR);
} }
} }
}
fee fee
} }
@ -1039,8 +992,9 @@ ORDER BY last_access DESC
smtp_pw: &str, smtp_pw: &str,
) { ) {
if self.has_role_tx(db, "scheckbuch").await { if self.has_role_tx(db, "scheckbuch").await {
let amount_trips = Logbook::completed_with_user_tx(db, &self).await.len(); let amount_trips = Logbook::completed_with_user_tx(db, self).await.len();
if amount_trips == 5 { match amount_trips {
5 => {
if let Some(mail) = &self.mail { if let Some(mail) = &self.mail {
let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await; let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await;
} }
@ -1054,7 +1008,8 @@ ORDER BY last_access DESC
None,None None,None
) )
.await; .await;
} else if amount_trips > 5 { }
a if a > 5 => {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap(); let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx( Notification::create_for_role_tx(
db, db,
@ -1068,10 +1023,45 @@ ORDER BY last_access DESC
) )
.await; .await;
} }
_ => {}
}
} }
// TODO: check fahrtenabzeichen fertig? // check fahrtenabzeichen fertig
// TODO: check äquatorpreis geschafft? if rowingbadge::Status::completed_with_last_log(db, self).await {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, zur Info: {} hat gerade alle Anforderungen für das diesjährige Fahrtenabzeichen erfüllt.",
self.name
),
"Fahrtenabzeichen geschafft",
None,None
)
.await;
Notification::create_with_tx(db, self, "Mit deiner letzten Ausfahrt hast du nun alle Anforderungen für das heurige Fahrtenzeichen erfüllt. Gratuliere! 🎉", "Fahrtenabzeichen geschafft", None, None).await;
}
// check äquatorpreis geschafft?
if let Some(level) = equatorprice::new_level_with_last_log(db, self).await {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, zur Info: {} hat gerade alle Anforderungen für den Äquatorpreis in {level} geschafft.",
self.name
),
"Äquatorpreis",
None,None
)
.await;
Notification::create_with_tx(db, self, &format!("Mit deiner letzten Ausfahrt erfüllst du nun alle Anforderungen für den Äquatorpreis in {level}. Gratuliere! 🎉"), "Äquatorpreis", None, None).await;
}
} }
} }

View File

@ -408,7 +408,7 @@ async fn create_scheckbuch(
format!("{} created new scheckbuch: {data:?}", admin.name), format!("{} created new scheckbuch: {data:?}", admin.name),
) )
.await; .await;
Flash::success(Redirect::to("/admin/user/scheckbuch"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt.")) Flash::success(Redirect::to("/admin/user/scheckbuch"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt."))
} }
#[get("/user/move/schnupperant/<id>/to/scheckbuch")] #[get("/user/move/schnupperant/<id>/to/scheckbuch")]
@ -458,7 +458,7 @@ async fn schnupper_to_scheckbuch(
), ),
) )
.await; .await;
Flash::success(Redirect::to("/admin/schnupper"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap())) Flash::success(Redirect::to("/admin/schnupper"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap()))
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {

View File

@ -18,12 +18,12 @@ async fn index(
context.insert("flash", &msg.into_inner()); context.insert("flash", &msg.into_inner());
} }
let role = Role::find_by_name(&db, "Donau Linz").await.unwrap(); let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
let users = User::all_with_role(&db, &role).await; let users = User::all_with_role(db, &role).await;
let mut people = Vec::new(); let mut people = Vec::new();
let mut rowingbadge_year = None; let mut rowingbadge_year = None;
for user in users { for user in users {
let achievement = Achievements::for_user(&db, &user).await; let achievement = Achievements::for_user(db, &user).await;
if let Some(badge) = &achievement.rowingbadge { if let Some(badge) = &achievement.rowingbadge {
rowingbadge_year = Some(badge.year); rowingbadge_year = Some(badge.year);
} }

View File

@ -148,13 +148,13 @@ async fn fixed<'r>(
#[derive(FromForm)] #[derive(FromForm)]
pub struct FormBoatDamageVerified<'r> { pub struct FormBoatDamageVerified<'r> {
pub desc: &'r str, desc: &'r str,
} }
#[post("/<boatdamage_id>/verified", data = "<data>")] #[post("/<boatdamage_id>/verified", data = "<data>")]
async fn verified<'r>( async fn verified<'r>(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<FormBoatDamageFixed<'r>>, data: Form<FormBoatDamageVerified<'r>>,
boatdamage_id: i32, boatdamage_id: i32,
techuser: TechUser, techuser: TechUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {

View File

@ -217,7 +217,7 @@ async fn new_thirty(
eprintln!("Failed to persist file: {:?}", e); eprintln!("Failed to persist file: {:?}", e);
} }
let result = data.result.trim_start_matches(|c| c == '0' || c == ' '); let result = data.result.trim_start_matches(['0', ' ']);
sqlx::query!( sqlx::query!(
"UPDATE user SET dirty_thirty = ? where id = ?", "UPDATE user SET dirty_thirty = ? where id = ?",
@ -318,7 +318,7 @@ async fn new_dozen(
if let Err(e) = data.proof.move_copy_to(file_path).await { if let Err(e) = data.proof.move_copy_to(file_path).await {
eprintln!("Failed to persist file: {:?}", e); eprintln!("Failed to persist file: {:?}", e);
} }
let result = data.result.trim_start_matches(|c| c == '0' || c == ' '); let result = data.result.trim_start_matches(['0', ' ']);
let result = if result.contains(":") || result.contains(".") { let result = if result.contains(":") || result.contains(".") {
format_time(result) format_time(result)
} else { } else {

View File

@ -312,7 +312,7 @@ async fn update(
let data = data.into_inner(); let data = data.into_inner();
let Some(logbook) = Logbook::find_by_id(db, data.id).await else { let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
return Flash::error(Redirect::to("/log"), &format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id)); return Flash::error(Redirect::to("/log"), format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
}; };
match logbook.update(db, data.clone(), &user.user).await { match logbook.update(db, data.clone(), &user.user).await {

View File

@ -19,7 +19,7 @@ async fn cal_registered(
return Err("Invalid".into()); return Err("Invalid".into());
}; };
if &user.user_token != uuid { if user.user_token != uuid {
return Err("Invalid".into()); return Err("Invalid".into());
} }

View File

@ -4,13 +4,12 @@
{% extends "base" %} {% extends "base" %}
{% macro show_place(aisle_name, side_name, level) %} {% macro show_place(aisle_name, side_name, level) %}
<li class="truncate p-2 flex relative w-full"> <li class="truncate p-2 flex relative w-full">
{% set aisle = aisle_name ~ "-aisle" %} {% set place = boathouse[aisle_name][side_name].boats %}
{% set place = boathouse[aisle][side_name] %}
{% if place[level] %} {% if place[level] %}
{{ place[level].1.name }} {{ place[level].boat.name }}
{% if "admin" in loggedin_user.roles %} {% if "admin" in loggedin_user.roles %}
<a class="btn btn-primary absolute end-0" <a class="btn btn-primary absolute end-0"
href="/board/boathouse/{{ place[level].0 }}/delete">X</a> href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X</a>
{% endif %} {% endif %}
{% elif boats | length > 0 %} {% elif boats | length > 0 %}
{% if "admin" in loggedin_user.roles %} {% if "admin" in loggedin_user.roles %}