final #168
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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<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));
|
||||
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<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">
|
||||
{% 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">
|
||||
|
Loading…
Reference in New Issue
Block a user