update-deps #846
| @@ -1,5 +1,3 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use rocket::serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, SqlitePool}; | ||||
|  | ||||
| @@ -7,6 +5,93 @@ use crate::tera::board::boathouse::FormBoathouseToAdd; | ||||
|  | ||||
| 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)] | ||||
| pub struct Boathouse { | ||||
|     pub id: i64, | ||||
| @@ -17,54 +102,7 @@ pub struct Boathouse { | ||||
| } | ||||
|  | ||||
| impl Boathouse { | ||||
|     pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> { | ||||
|         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); | ||||
|  | ||||
|     pub async fn get(db: &SqlitePool) -> BoathouseAisles { | ||||
|         let boathouses = sqlx::query_as!( | ||||
|             Boathouse, | ||||
|             "SELECT id, boat_id, aisle, side, level FROM boathouse" | ||||
| @@ -73,21 +111,7 @@ impl Boathouse { | ||||
|         .await | ||||
|         .unwrap(); //TODO: fixme | ||||
|  | ||||
|         for boathouse in boathouses { | ||||
|             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 | ||||
|         BoathouseAisles::from(db, boathouses).await | ||||
|     } | ||||
|  | ||||
|     pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> { | ||||
|   | ||||
| @@ -33,8 +33,7 @@ impl PartialEq for Logbook { | ||||
|  | ||||
| pub(crate) enum Filter { | ||||
|     SingleDayOnly, | ||||
|     MultiDazOnly, | ||||
|     None, | ||||
|     MultiDayOnly, | ||||
| } | ||||
|  | ||||
| #[derive(FromForm, Debug, Clone)] | ||||
| @@ -362,12 +361,13 @@ ORDER BY departure DESC | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year( | ||||
|         db: &SqlitePool, | ||||
|     pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year_tx( | ||||
|         db: &mut Transaction<'_, Sqlite>, | ||||
|         user: &User, | ||||
|         min_distance: i32, | ||||
|         year: i32, | ||||
|         filter: Filter, | ||||
|         exclude_last_log: bool, | ||||
|     ) -> Vec<LogbookWithBoatAndRowers> { | ||||
|         let logs: Vec<Logbook> = sqlx::query_as( | ||||
|                &format!(" | ||||
| @@ -378,7 +378,7 @@ ORDER BY departure DESC | ||||
|     ORDER BY arrival DESC | ||||
|             ",  user.id, min_distance, year) | ||||
|             ) | ||||
|             .fetch_all(db) | ||||
|             .fetch_all(db.deref_mut()) | ||||
|             .await | ||||
|             .unwrap(); //TODO: fixme | ||||
|  | ||||
| @@ -389,19 +389,20 @@ ORDER BY departure DESC | ||||
|             match filter { | ||||
|                 Filter::SingleDayOnly => { | ||||
|                     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 { | ||||
|                         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 | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,63 +1,64 @@ | ||||
| use crate::model::{logbook::Logbook, stat::Stat, user::User}; | ||||
| use serde::Serialize; | ||||
|  | ||||
| #[derive(Serialize, PartialEq, Debug)] | ||||
| pub(crate) enum Level { | ||||
|     NONE, | ||||
|     BRONZE, | ||||
|     SILVER, | ||||
|     GOLD, | ||||
|     DIAMOND, | ||||
|     DONE, | ||||
|     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, | ||||
|             Level::Bronze => 40_000, | ||||
|             Level::Silver => 80_000, | ||||
|             Level::Gold => 100_000, | ||||
|             Level::Diamond => 200_000, | ||||
|             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 | ||||
|         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 | ||||
|             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 | ||||
|         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 | ||||
|             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 => "-", | ||||
|             Level::Bronze => "Bronze", | ||||
|             Level::Silver => "Silber", | ||||
|             Level::Gold => "Gold", | ||||
|             Level::Diamond => "Diamant", | ||||
|             Level::Done => "", | ||||
|             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 | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ use std::cmp; | ||||
|  | ||||
| use chrono::{Datelike, Local, NaiveDate}; | ||||
| use serde::Serialize; | ||||
| use sqlx::SqlitePool; | ||||
| use sqlx::{Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use crate::model::{ | ||||
|     logbook::{Filter, Logbook, LogbookWithBoatAndRowers}, | ||||
| @@ -111,11 +111,44 @@ pub(crate) struct 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 { | ||||
|             return None; | ||||
|         }; | ||||
|         let category = agebracket.cat().to_string(); | ||||
|  | ||||
|         let year = if Local::now().month() == 1 { | ||||
|             Local::now().year() - 1 | ||||
| @@ -123,44 +156,66 @@ impl Status { | ||||
|             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 rowed_km = Stat::person_tx(db, Some(year), user).await.rowed_km; | ||||
|         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, | ||||
|                 user, | ||||
|                 agebracket.required_dist_single_day_in_km(), | ||||
|                 year, | ||||
|                 Filter::SingleDayOnly, | ||||
|                 exclude_last_log, | ||||
|             ) | ||||
|             .await; | ||||
|         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, | ||||
|                 user, | ||||
|                 agebracket.required_dist_multi_day_in_km(), | ||||
|                 year, | ||||
|                 Filter::MultiDazOnly, | ||||
|                 Filter::MultiDayOnly, | ||||
|                 exclude_last_log, | ||||
|             ) | ||||
|             .await; | ||||
|  | ||||
|         let achieved = missing_km == 0 | ||||
|             && (multi_day_trips_over_required_distance.len() >= 1 | ||||
|                 || single_day_trips_over_required_distance.len() >= 2); | ||||
|         let ret = Self::calc( | ||||
|             &agebracket, | ||||
|             rowed_km, | ||||
|             single_day_trips_over_required_distance.len(), | ||||
|             multi_day_trips_over_required_distance.len(), | ||||
|             year, | ||||
|         ); | ||||
|  | ||||
|         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, | ||||
|             ..ret | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| use std::collections::HashMap; | ||||
| use std::{collections::HashMap, ops::DerefMut}; | ||||
|  | ||||
| use crate::model::user::User; | ||||
| use chrono::Datelike; | ||||
| use serde::Serialize; | ||||
| use sqlx::{FromRow, Row, SqlitePool}; | ||||
| use sqlx::{FromRow, Row, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use super::boat::Boat; | ||||
|  | ||||
| @@ -218,7 +218,7 @@ ORDER BY rowed_km DESC, u.name; | ||||
|         .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) | ||||
|         let row = sqlx::query(&format!( | ||||
|             " | ||||
| @@ -233,7 +233,7 @@ WHERE l.distance_in_km IS NOT NULL; | ||||
| ", | ||||
|             user.id | ||||
|         )) | ||||
|         .fetch_one(db) | ||||
|         .fetch_one(db.deref_mut()) | ||||
|         .await | ||||
|         .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 { | ||||
|             Some(year) => 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 | ||||
|         )) | ||||
|         .fetch_one(db) | ||||
|         .fetch_one(db.deref_mut()) | ||||
|         .await | ||||
|         .unwrap(); | ||||
|  | ||||
| @@ -273,6 +284,13 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%'; | ||||
|             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)] | ||||
|   | ||||
| @@ -287,10 +287,8 @@ WHERE day=? | ||||
|             return Err(TripUpdateError::NotYourTrip); | ||||
|         } | ||||
|  | ||||
|         if update.trip_type != Some(4) { | ||||
|             if !update.cox.allowed_to_steer(db).await { | ||||
|                 return Err(TripUpdateError::TripTypeNotAllowed); | ||||
|             } | ||||
|         if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await { | ||||
|             return Err(TripUpdateError::TripTypeNotAllowed); | ||||
|         } | ||||
|  | ||||
|         let Some(trip_details_id) = update.trip.trip_details_id else { | ||||
|   | ||||
							
								
								
									
										58
									
								
								src/model/user/fee.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/model/user/fee.rs
									
									
									
									
									
										Normal 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -15,14 +15,25 @@ use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
| 
 | ||||
| use super::{ | ||||
|     family::Family, log::Log, logbook::Logbook, mail::Mail, notification::Notification, role::Role, | ||||
|     stat::Stat, tripdetails::TripDetails, Day, | ||||
|     family::Family, | ||||
|     log::Log, | ||||
|     logbook::Logbook, | ||||
|     mail::Mail, | ||||
|     notification::Notification, | ||||
|     personal::{equatorprice, rowingbadge}, | ||||
|     role::Role, | ||||
|     stat::Stat, | ||||
|     tripdetails::TripDetails, | ||||
|     Day, | ||||
| }; | ||||
| use crate::{ | ||||
|     tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE, | ||||
|     EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG, | ||||
|     SCHECKBUCH, STUDENT_OR_PUPIL, UNTERSTUETZEND, | ||||
| }; | ||||
| use fee::Fee; | ||||
| 
 | ||||
| mod fee; | ||||
| 
 | ||||
| #[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] | ||||
| pub struct User { | ||||
| @@ -83,62 +94,6 @@ pub enum LoginError { | ||||
|     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 { | ||||
|     pub async fn allowed_to_steer(&self, db: &SqlitePool) -> bool { | ||||
|         self.has_role(db, "cox").await || self.has_role(db, "Bootsführer").await | ||||
| @@ -440,12 +395,10 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|                 } | ||||
|             } else if self.has_role(db, "Ehrenmitglied").await { | ||||
|                 fee.add("Ehrenmitglied".into(), 0); | ||||
|             } else if halfprice { | ||||
|                 fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2); | ||||
|             } else { | ||||
|                 if halfprice { | ||||
|                     fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2); | ||||
|                 } else { | ||||
|                     fee.add("Mitgliedsbeitrag".into(), REGULAR); | ||||
|                 } | ||||
|                 fee.add("Mitgliedsbeitrag".into(), REGULAR); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @@ -1039,39 +992,76 @@ ORDER BY last_access DESC | ||||
|         smtp_pw: &str, | ||||
|     ) { | ||||
|         if self.has_role_tx(db, "scheckbuch").await { | ||||
|             let amount_trips = Logbook::completed_with_user_tx(db, &self).await.len(); | ||||
|             if amount_trips == 5 { | ||||
|                 if let Some(mail) = &self.mail { | ||||
|                     let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await; | ||||
|             let amount_trips = Logbook::completed_with_user_tx(db, self).await.len(); | ||||
|             match amount_trips { | ||||
|                 5 => { | ||||
|                     if let Some(mail) = &self.mail { | ||||
|                         let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await; | ||||
|                     } | ||||
|                     Notification::create_for_steering_people_tx( | ||||
|                         db, | ||||
|                         &format!( | ||||
|                             "Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)", | ||||
|                             self.name | ||||
|                         ), | ||||
|                         "Scheckbuch fertig", | ||||
|                         None,None | ||||
|                     ) | ||||
|                     .await; | ||||
|                 } | ||||
|                 Notification::create_for_steering_people_tx( | ||||
|             db, | ||||
|             &format!( | ||||
|                 "Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)", | ||||
|                 self.name | ||||
|             ), | ||||
|             "Scheckbuch fertig", | ||||
|             None,None | ||||
|         ) | ||||
|         .await; | ||||
|             } else if amount_trips > 5 { | ||||
|                 let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap(); | ||||
|                 Notification::create_for_role_tx( | ||||
|             db, | ||||
|             &board, | ||||
|             &format!( | ||||
|                 "Lieber Vorstand, {} hat nun bereits die {}. seiner 5 Scheckbuchausfahrten absolviert.", | ||||
|                 self.name, amount_trips | ||||
|             ), | ||||
|             "Scheckbuch überfertig", | ||||
|             None,None | ||||
|         ) | ||||
|         .await; | ||||
|                 a if a > 5 => { | ||||
|                     let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap(); | ||||
|                     Notification::create_for_role_tx( | ||||
|                         db, | ||||
|                         &board, | ||||
|                         &format!( | ||||
|                             "Lieber Vorstand, {} hat nun bereits die {}. seiner 5 Scheckbuchausfahrten absolviert.", | ||||
|                             self.name, amount_trips | ||||
|                         ), | ||||
|                         "Scheckbuch überfertig", | ||||
|                         None,None | ||||
|                     ) | ||||
|                     .await; | ||||
|                 } | ||||
|                 _ => {} | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // TODO: check fahrtenabzeichen fertig?
 | ||||
|         // TODO: check äquatorpreis geschafft?
 | ||||
|         // check fahrtenabzeichen fertig
 | ||||
|         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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @@ -408,7 +408,7 @@ async fn create_scheckbuch( | ||||
|         format!("{} created new scheckbuch: {data:?}", admin.name), | ||||
|     ) | ||||
|     .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")] | ||||
| @@ -458,7 +458,7 @@ async fn schnupper_to_scheckbuch( | ||||
|         ), | ||||
|     ) | ||||
|     .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> { | ||||
|   | ||||
| @@ -18,12 +18,12 @@ async fn index( | ||||
|         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 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; | ||||
|         let achievement = Achievements::for_user(db, &user).await; | ||||
|         if let Some(badge) = &achievement.rowingbadge { | ||||
|             rowingbadge_year = Some(badge.year); | ||||
|         } | ||||
|   | ||||
| @@ -148,13 +148,13 @@ async fn fixed<'r>( | ||||
|  | ||||
| #[derive(FromForm)] | ||||
| pub struct FormBoatDamageVerified<'r> { | ||||
|     pub desc: &'r str, | ||||
|     desc: &'r str, | ||||
| } | ||||
|  | ||||
| #[post("/<boatdamage_id>/verified", data = "<data>")] | ||||
| async fn verified<'r>( | ||||
|     db: &State<SqlitePool>, | ||||
|     data: Form<FormBoatDamageFixed<'r>>, | ||||
|     data: Form<FormBoatDamageVerified<'r>>, | ||||
|     boatdamage_id: i32, | ||||
|     techuser: TechUser, | ||||
| ) -> Flash<Redirect> { | ||||
|   | ||||
| @@ -217,7 +217,7 @@ async fn new_thirty( | ||||
|         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!( | ||||
|         "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 { | ||||
|         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(".") { | ||||
|         format_time(result) | ||||
|     } else { | ||||
|   | ||||
| @@ -312,7 +312,7 @@ async fn update( | ||||
|     let data = data.into_inner(); | ||||
|  | ||||
|     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 { | ||||
|   | ||||
| @@ -19,7 +19,7 @@ async fn cal_registered( | ||||
|         return Err("Invalid".into()); | ||||
|     }; | ||||
|  | ||||
|     if &user.user_token != uuid { | ||||
|     if user.user_token != uuid { | ||||
|         return Err("Invalid".into()); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -4,13 +4,12 @@ | ||||
| {% extends "base" %} | ||||
| {% macro show_place(aisle_name, side_name, level) %} | ||||
|     <li class="truncate p-2 flex relative w-full"> | ||||
|         {% set aisle = aisle_name ~ "-aisle" %} | ||||
|         {% set place = boathouse[aisle][side_name] %} | ||||
|         {% set place = boathouse[aisle_name][side_name].boats %} | ||||
|         {% if place[level] %} | ||||
|             {{ place[level].1.name }} | ||||
|             {{ place[level].boat.name }} | ||||
|             {% if "admin" in loggedin_user.roles %} | ||||
|                 <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 %} | ||||
|         {% elif boats | length > 0 %} | ||||
|             {% if "admin" in loggedin_user.roles %} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user