Compare commits
	
		
			5 Commits
		
	
	
		
			ff795ce66c
			...
			519cd1985d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 519cd1985d | |||
| 3df6791b6b | |||
| cb892e1c0c | |||
| b893989dce | |||
| 267becfbce | 
| @@ -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<Self> { | ||||
|     pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { | ||||
|         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<i64>) -> Option<Self> { | ||||
|         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<User> { | ||||
|         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() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ use std::ops::{Deref, DerefMut}; | ||||
|  | ||||
| use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; | ||||
| use chrono::{Datelike, Local, NaiveDate}; | ||||
| use chrono_tz::Etc::UTC; | ||||
| use log::info; | ||||
| use rocket::{ | ||||
|     async_trait, | ||||
| @@ -16,6 +17,15 @@ 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; | ||||
| const UNTERSTUETZEND: i32 = 2500; | ||||
| const FOERDERND: i32 = 8500; | ||||
|  | ||||
| #[derive(FromRow, Debug, Serialize, Deserialize)] | ||||
| pub struct User { | ||||
|     pub id: i64, | ||||
| @@ -90,7 +100,109 @@ 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) name: String, | ||||
| } | ||||
|  | ||||
| impl Fee { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             sum_in_cents: 0, | ||||
|             name: "".into(), | ||||
|             parts: Vec::new(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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<Fee> { | ||||
|         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)); | ||||
|             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 self.has_role(db, "Unterstützend").await { | ||||
|             fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND); | ||||
|         } else if self.has_role(db, "Förderndes Mitglied").await { | ||||
|             fee.add("Förderndes Mitglied".into(), FOERDERND); | ||||
|         } else 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<User> 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<Self, Self::Error> { | ||||
|         let db = req.rocket().state::<SqlitePool>().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; | ||||
|   | ||||
| @@ -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<SqlitePool>, | ||||
|     admin: AdminUser, | ||||
|     admin: VorstandUser, | ||||
|     flash: Option<FlashMessage<'_>>, | ||||
| ) -> 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()) | ||||
|   | ||||
							
								
								
									
										32
									
								
								templates/admin/user/fees.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								templates/admin/user/fees.html.tera
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| {% import "includes/macros" as macros %} | ||||
|  | ||||
| {% extends "base" %} | ||||
|  | ||||
| {% block content %} | ||||
| 	<div class="max-w-screen-lg w-full"> | ||||
|     <h1 class="h1">Ergo Challenges</h1> | ||||
|  | ||||
|     {% if flash %} | ||||
|         {{ macros::alert(message=flash.1, type=flash.0, class="my-3") }} | ||||
|     {% endif %} | ||||
|  | ||||
|     <div class="grid gap-3"> | ||||
|       <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||
|         <h2 class="h2">Gebühren</h2> | ||||
|          <div class="text-sm p-3"> | ||||
| 		{% for fee in fees | sort(attribute="name") %} | ||||
| 			<b>{{ fee.name }}: {{ fee.sum_in_cents / 100 }}€</b><br /> | ||||
| 			{% for p in fee.parts %} | ||||
| 				 {{ p.0 }} ({{ p.1 / 100 }}€) {% if not loop.last %} + {% endif %} | ||||
| 			{% endfor %} | ||||
| 			<hr /> | ||||
| 		{% endfor %} | ||||
|  | ||||
|         </div> | ||||
|       </div> | ||||
|      | ||||
|     </div> | ||||
| 	</div> | ||||
|  | ||||
| {% endblock content%} | ||||
|  | ||||
| @@ -4,13 +4,17 @@ | ||||
|   > | ||||
|     <div class="max-w-screen-xl w-full flex justify-between items-center"> | ||||
|       <div class="w-1/3 truncate"> | ||||
|         <a href="/"> | ||||
|         {% if "Donau Linz" in loggedin_user.roles %} | ||||
| 	        <a href="/planned"> | ||||
|         {% else %} | ||||
|         	<a href="/"> | ||||
| 	{% endif %} | ||||
|           Hü | ||||
|           {{ loggedin_user.name }} | ||||
|         </a> | ||||
|       </div> | ||||
|    | ||||
|       <div><!-- | ||||
|       <div> | ||||
|         <a | ||||
|           href="https://wiki.rudernlinz.at/ruderassistent#faq" | ||||
|           target="_blank" | ||||
| @@ -48,7 +52,7 @@ | ||||
|           class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" | ||||
|           data-sidebar="true" | ||||
|           data-trigger="sidebar" | ||||
|           data-header="Logbuch" | ||||
|           data-header="Menü" | ||||
|           data-body="#mobile-menu" | ||||
|         > | ||||
|           {% include "includes/book" %} | ||||
| @@ -121,7 +125,7 @@ | ||||
|           </svg> | ||||
|           <span class="sr-only">Userverwaltung</span> | ||||
|         </a> | ||||
|         {% endif %}--> | ||||
|         {% endif %} | ||||
|         <a | ||||
|           href="/auth/logout" | ||||
|           class="inline-flex justify-center rounded-md bg-gray-200 ml-1 px-3 py-2 text-sm font-semibold text-primary-950 hover:bg-gray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" | ||||
|   | ||||
| @@ -66,6 +66,19 @@ | ||||
|         </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     {% if "Vorstand" in loggedin_user.roles %} | ||||
|         <div class="grid gap-3"> | ||||
|           <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||
|             <h2 class="h2">Vorstand</h2> | ||||
|              <div class="text-sm p-3"> | ||||
|               <ul class="list-disc ms-2"> | ||||
|                 <li class="py-1"><a href="/admin/user/fees" class="link-primary">Übersicht User Gebühren</a></li> | ||||
|               </ul> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     {% if "admin" in loggedin_user.roles %} | ||||
|         <div class="grid gap-3"> | ||||
|           <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user