Compare commits
	
		
			5 Commits
		
	
	
		
			ff795ce66c
			...
			519cd1985d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 519cd1985d | |||
| 3df6791b6b | |||
| cb892e1c0c | |||
| b893989dce | |||
| 267becfbce | 
| @@ -1,6 +1,8 @@ | |||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool}; | use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool}; | ||||||
|  |  | ||||||
|  | use super::user::User; | ||||||
|  |  | ||||||
| #[derive(FromRow, Serialize, Clone)] | #[derive(FromRow, Serialize, Clone)] | ||||||
| pub struct Family { | pub struct Family { | ||||||
|     id: i64, |     id: i64, | ||||||
| @@ -46,10 +48,36 @@ GROUP BY family.id;" | |||||||
|         .unwrap() |         .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) |         sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id) | ||||||
|             .fetch_one(db) |             .fetch_one(db) | ||||||
|             .await |             .await | ||||||
|             .ok() |             .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 argon2::{password_hash::SaltString, Argon2, PasswordHasher}; | ||||||
| use chrono::{Datelike, Local, NaiveDate}; | use chrono::{Datelike, Local, NaiveDate}; | ||||||
|  | use chrono_tz::Etc::UTC; | ||||||
| use log::info; | use log::info; | ||||||
| use rocket::{ | use rocket::{ | ||||||
|     async_trait, |     async_trait, | ||||||
| @@ -16,6 +17,15 @@ use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | |||||||
| use super::{family::Family, log::Log, tripdetails::TripDetails, Day}; | use super::{family::Family, log::Log, tripdetails::TripDetails, Day}; | ||||||
| use crate::tera::admin::user::UserEditForm; | 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)] | #[derive(FromRow, Debug, Serialize, Deserialize)] | ||||||
| pub struct User { | pub struct User { | ||||||
|     pub id: i64, |     pub id: i64, | ||||||
| @@ -90,7 +100,109 @@ pub enum LoginError { | |||||||
|     DeserializationError, |     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 { | 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 { |     pub async fn rowed_km(&self, db: &SqlitePool) -> i32 { | ||||||
|         sqlx::query!( |         sqlx::query!( | ||||||
|             "SELECT COALESCE(SUM(distance_in_km),0) as rowed_km |             "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)] | #[cfg(test)] | ||||||
| mod test { | mod test { | ||||||
|     use std::collections::HashMap; |     use std::collections::HashMap; | ||||||
|   | |||||||
| @@ -3,9 +3,9 @@ use std::collections::HashMap; | |||||||
| use crate::model::{ | use crate::model::{ | ||||||
|     family::Family, |     family::Family, | ||||||
|     role::Role, |     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::{ | use rocket::{ | ||||||
|     form::Form, |     form::Form, | ||||||
|     get, post, |     get, post, | ||||||
| @@ -51,40 +51,27 @@ async fn index( | |||||||
| #[get("/user/fees")] | #[get("/user/fees")] | ||||||
| async fn fees( | async fn fees( | ||||||
|     db: &State<SqlitePool>, |     db: &State<SqlitePool>, | ||||||
|     admin: AdminUser, |     admin: VorstandUser, | ||||||
|     flash: Option<FlashMessage<'_>>, |     flash: Option<FlashMessage<'_>>, | ||||||
| ) -> Template { | ) -> 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 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 { |     if let Some(msg) = flash { | ||||||
|         context.insert("flash", &msg.into_inner()); |         context.insert("flash", &msg.into_inner()); | ||||||
|     } |     } | ||||||
|     context.insert( |     context.insert( | ||||||
|         "loggedin_user", |         "loggedin_user", | ||||||
|         &UserWithRoles::from_user(admin.user, db).await, |         &UserWithRoles::from_user(admin.into(), db).await, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     Template::render("admin/user/fees", context.into_json()) |     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="max-w-screen-xl w-full flex justify-between items-center"> | ||||||
|       <div class="w-1/3 truncate"> |       <div class="w-1/3 truncate"> | ||||||
|         <a href="/"> |         {% if "Donau Linz" in loggedin_user.roles %} | ||||||
|  | 	        <a href="/planned"> | ||||||
|  |         {% else %} | ||||||
|  |         	<a href="/"> | ||||||
|  | 	{% endif %} | ||||||
|           Hü |           Hü | ||||||
|           {{ loggedin_user.name }} |           {{ loggedin_user.name }} | ||||||
|         </a> |         </a> | ||||||
|       </div> |       </div> | ||||||
|    |    | ||||||
|       <div><!-- |       <div> | ||||||
|         <a |         <a | ||||||
|           href="https://wiki.rudernlinz.at/ruderassistent#faq" |           href="https://wiki.rudernlinz.at/ruderassistent#faq" | ||||||
|           target="_blank" |           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" |           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-sidebar="true" | ||||||
|           data-trigger="sidebar" |           data-trigger="sidebar" | ||||||
|           data-header="Logbuch" |           data-header="Menü" | ||||||
|           data-body="#mobile-menu" |           data-body="#mobile-menu" | ||||||
|         > |         > | ||||||
|           {% include "includes/book" %} |           {% include "includes/book" %} | ||||||
| @@ -121,7 +125,7 @@ | |||||||
|           </svg> |           </svg> | ||||||
|           <span class="sr-only">Userverwaltung</span> |           <span class="sr-only">Userverwaltung</span> | ||||||
|         </a> |         </a> | ||||||
|         {% endif %}--> |         {% endif %} | ||||||
|         <a |         <a | ||||||
|           href="/auth/logout" |           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" |           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> |         </div> | ||||||
|     {% endif %} |     {% 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 %} |     {% if "admin" in loggedin_user.roles %} | ||||||
|         <div class="grid gap-3"> |         <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"> |           <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