diff --git a/src/model/family.rs b/src/model/family.rs index 89944d1..a3ea998 100644 --- a/src/model/family.rs +++ b/src/model/family.rs @@ -1,6 +1,8 @@ use serde::Serialize; use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool}; +use super::user::User; + #[derive(FromRow, Serialize, Clone)] pub struct Family { id: i64, @@ -46,10 +48,36 @@ GROUP BY family.id;" .unwrap() } - pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { + pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id) .fetch_one(db) .await .ok() } + + pub async fn find_by_opt_id(db: &SqlitePool, id: Option) -> Option { + if let Some(id) = id { + Self::find_by_id(db, id).await + } else { + None + } + } + + pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 { + sqlx::query!( + "SELECT COUNT(*) as count FROM user WHERE family_id = ?", + self.id + ) + .fetch_one(db) + .await + .unwrap() + .count + } + + pub async fn members(&self, db: &SqlitePool) -> Vec { + sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id) + .fetch_all(db) + .await + .unwrap() + } } diff --git a/src/model/user.rs b/src/model/user.rs index aace4c8..5b3c08c 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -16,6 +16,13 @@ use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use super::{family::Family, log::Log, tripdetails::TripDetails, Day}; use crate::tera::admin::user::UserEditForm; +const RENNRUDERBEITRAG: i32 = 11000; +const BOAT_STORAGE: i32 = 4500; +const FAMILY_TWO: i32 = 30000; +const FAMILY_THREE_OR_MORE: i32 = 35000; +const STUDENT_OR_PUPIL: i32 = 8000; +const REGULAR: i32 = 22000; + #[derive(FromRow, Debug, Serialize, Deserialize)] pub struct User { pub id: i64, @@ -90,7 +97,112 @@ pub enum LoginError { DeserializationError, } +#[derive(Debug, Serialize)] +pub(crate) struct Fee { + pub(crate) sum_in_cents: i32, + pub(crate) parts: Vec<(String, i32)>, + pub(crate) family: bool, + pub(crate) name: String, +} + +impl Fee { + pub fn new() -> Self { + Self { + sum_in_cents: 0, + family: false, + name: "".into(), + parts: Vec::new(), + } + } + + pub fn family(&mut self) { + self.family = true; + } + + 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 name(&mut self, name: String) { + self.name = name; + } + + 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 fee(&self, db: &SqlitePool) -> Option { + if !self.has_role(db, "Donau Linz").await { + return None; + } + + let mut fee = Fee::new(); + + if let Some(family) = Family::find_by_opt_id(db, self.family_id).await { + fee.name(format!("{} + Familie", self.name)); + fee.family(); + for member in family.members(db).await { + fee.merge(member.fee_without_families(db).await); + } + if family.amount_family_members(db).await > 2 { + fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE); + } else { + fee.add("Familie 2 Personen".into(), FAMILY_TWO); + } + } else { + fee.name(self.name.clone()); + fee.merge(self.fee_without_families(db).await); + } + + Some(fee) + } + + async fn fee_without_families(&self, db: &SqlitePool) -> Fee { + let mut fee = Fee::new(); + + if !self.has_role(db, "Donau Linz").await { + return fee; + } + if self.has_role(db, "Rennrudern").await { + fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG); + } + + let amount_boats = self.amount_boats(db).await; + if amount_boats > 0 { + fee.add( + format!("{}x Bootsplatz", amount_boats), + amount_boats * BOAT_STORAGE, + ); + } + + if Family::find_by_opt_id(db, self.family_id).await.is_none() { + if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await { + fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL); + } else { + fee.add("Mitgliedsbeitrag".into(), REGULAR); + } + } + + fee + } + + pub async fn amount_boats(&self, db: &SqlitePool) -> i32 { + sqlx::query!( + "SELECT COUNT(*) as count FROM boat WHERE owner = ?", + self.id + ) + .fetch_one(db) + .await + .unwrap() + .count + } + pub async fn rowed_km(&self, db: &SqlitePool) -> i32 { sqlx::query!( "SELECT COALESCE(SUM(distance_in_km),0) as rowed_km @@ -640,6 +752,42 @@ impl<'r> FromRequest<'r> for DonauLinzUser { } } +#[derive(Debug, Serialize, Deserialize)] +pub struct VorstandUser(pub(crate) User); + +impl Into for VorstandUser { + fn into(self) -> User { + self.0 + } +} + +impl Deref for VorstandUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[async_trait] +impl<'r> FromRequest<'r> for VorstandUser { + type Error = LoginError; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let db = req.rocket().state::().unwrap(); + match User::from_request(req).await { + Outcome::Success(user) => { + if user.has_role(db, "Vorstand").await { + Outcome::Success(VorstandUser(user)) + } else { + Outcome::Error((Status::Forbidden, LoginError::NotACox)) + } + } + Outcome::Error(f) => Outcome::Error(f), + Outcome::Forward(f) => Outcome::Forward(f), + } + } +} #[cfg(test)] mod test { use std::collections::HashMap; diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 280013f..6b67bbb 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -3,9 +3,9 @@ use std::collections::HashMap; use crate::model::{ family::Family, role::Role, - user::{AdminUser, User, UserWithRoles}, + user::{AdminUser, Fee, User, UserWithRoles, VorstandUser}, }; -use futures::future::join_all; +use futures::future::{self, join_all}; use rocket::{ form::Form, get, post, @@ -51,40 +51,27 @@ async fn index( #[get("/user/fees")] async fn fees( db: &State, - admin: AdminUser, + admin: VorstandUser, flash: Option>, ) -> Template { - // Checks - // Fördernd -> Donau Linz - // Fördernd -> keine Family - // Unterstützend -> Donau Linz - // Unterstützend -> keine Family - // Schüler -> Donau Linz - // Student -> Donau Linz - - // Fördernd + boat: 85€ - // Unterstützend + boat: 25€ - - // select id, size_of_family, count_rennsport, amount_boats from family - // 2-Family: 220€ - // 3+-Family: 350€ - - // select student+schüler, amount_boats, rennsportbeitrag where !family - // Student, Schüler: 80€ - - // select donaulinz, amount_boats, rennsportbeitrag where !family !student ! schüler - // Normal: 220€ - - // Rennsportbeitrag: 110€ - // Bootsplatz: 45€ - let mut context = Context::new(); + + let users = User::all(db).await; + let mut fees = Vec::new(); + for user in users { + if let Some(fee) = user.fee(db).await { + fees.push(fee); + } + } + + context.insert("fees", &fees); + if let Some(msg) = flash { context.insert("flash", &msg.into_inner()); } context.insert( "loggedin_user", - &UserWithRoles::from_user(admin.user, db).await, + &UserWithRoles::from_user(admin.into(), db).await, ); Template::render("admin/user/fees", context.into_json()) diff --git a/templates/admin/user/fees.html.tera b/templates/admin/user/fees.html.tera new file mode 100644 index 0000000..930eb1e --- /dev/null +++ b/templates/admin/user/fees.html.tera @@ -0,0 +1,32 @@ +{% import "includes/macros" as macros %} + +{% extends "base" %} + +{% block content %} +
+

Ergo Challenges

+ + {% if flash %} + {{ macros::alert(message=flash.1, type=flash.0, class="my-3") }} + {% endif %} + +
+ + +
+
+ +{% endblock content%} + diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 226d310..ca1579c 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -4,13 +4,17 @@ >