use std::cmp; use chrono::{Datelike, Local, NaiveDate}; use serde::Serialize; use sqlx::{Sqlite, SqlitePool, Transaction}; 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 { 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, multi_day_trips_required_distance: i32, single_day_trips_over_required_distance: Vec, single_day_trips_required_distance: i32, achieved: bool, } impl Status { 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 { let Ok(agebracket) = AgeBracket::try_from(user) else { return None; }; let year = if Local::now().month() == 1 { Local::now().year() - 1 } else { Local::now().year() }; 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_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_tx( db, user, agebracket.required_dist_multi_day_in_km(), year, Filter::MultiDayOnly, exclude_last_log, ) .await; let ret = Self::calc( &agebracket, rowed_km, single_day_trips_over_required_distance.len(), multi_day_trips_over_required_distance.len(), year, ); Some(Self { multi_day_trips_over_required_distance, single_day_trips_over_required_distance, ..ret }) } pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option { 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 } }