Merge pull request 'final' (#168) from final into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m13s
CI/CD Pipeline / deploy-staging (push) Successful in 4m24s
CI/CD Pipeline / deploy-main (push) Has been skipped

Reviewed-on: #168
This commit is contained in:
philipp 2024-01-19 01:00:07 +01:00
commit 519cd1985d
6 changed files with 245 additions and 33 deletions

View File

@ -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()
}
}

View File

@ -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;

View File

@ -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())

View 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%}

View File

@ -4,13 +4,17 @@
>
<div class="max-w-screen-xl w-full flex justify-between items-center">
<div class="w-1/3 truncate">
{% if "Donau Linz" in loggedin_user.roles %}
<a href="/planned">
{% else %}
<a href="/">
{% endif %}
{{ 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"

View File

@ -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">