Be able to update financial and skill; Fixes #974
This commit is contained in:
parent
905178e60d
commit
6362fed909
@ -1,4 +1,4 @@
|
||||
use std::{fmt::Display, ops::DerefMut};
|
||||
use std::{cmp::Ordering, fmt::Display, ops::DerefMut};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
@ -13,6 +13,30 @@ pub struct Role {
|
||||
pub(crate) cluster: Option<String>,
|
||||
}
|
||||
|
||||
// Implement PartialEq to compare roles based only on id
|
||||
impl PartialEq for Role {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Eq to indicate that equality is reflexive
|
||||
impl Eq for Role {}
|
||||
|
||||
// Implement PartialOrd if you need to sort or compare roles
|
||||
impl PartialOrd for Role {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.id.cmp(&other.id))
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Ord if you need total ordering (for sorting)
|
||||
impl Ord for Role {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.id.cmp(&other.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
@ -30,6 +54,27 @@ impl Role {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn all_cluster(db: &SqlitePool, cluster: &str) -> Vec<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
r#"SELECT id,
|
||||
CASE WHEN formatted_name IS NOT NULL AND formatted_name != ''
|
||||
THEN formatted_name
|
||||
ELSE name
|
||||
END AS "name!: String",
|
||||
'' as formatted_name,
|
||||
desc,
|
||||
hide_in_lists,
|
||||
cluster
|
||||
FROM role
|
||||
WHERE cluster = ?"#,
|
||||
cluster
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
@ -59,21 +104,6 @@ WHERE id like ?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_cluster_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
|
||||
FROM role
|
||||
WHERE cluster = ?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
|
@ -1,7 +1,10 @@
|
||||
// TODO: put back in `src/model/user/mod.rs` once that is cleaned up
|
||||
|
||||
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
|
||||
use crate::model::{activity::Activity, family::Family, log::Log, mail::valid_mails, role::Role};
|
||||
use crate::model::{
|
||||
activity::Activity, family::Family, log::Log, mail::valid_mails, notification::Notification,
|
||||
role::Role,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
|
||||
use sqlx::SqlitePool;
|
||||
@ -228,6 +231,103 @@ impl User {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn change_skill(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
skill: Option<Role>,
|
||||
) -> Result<(), String> {
|
||||
let old_skill = self.skill(db).await;
|
||||
|
||||
let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
let bootsfuehrer = Role::find_by_name(db, "Bootsführer").await.unwrap();
|
||||
|
||||
match (old_skill, skill) {
|
||||
(old, new) if old == None && new == Some(cox.clone()) => {
|
||||
self.add_role(db, updated_by, &cox).await?;
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&member,
|
||||
&format!(
|
||||
"Liebes Vereinsmitglied, {self} ist ab sofort Steuerperson 🎉 Hip hip ...!"
|
||||
),
|
||||
"Neue Steuerperson",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
(old, new) if old == Some(cox.clone()) && new == Some(bootsfuehrer.clone()) => {
|
||||
self.remove_role(db, updated_by, &cox).await?;
|
||||
self.add_role(db, updated_by, &bootsfuehrer).await?;
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&member,
|
||||
&format!(
|
||||
"Liebes Vereinsmitglied, {self} ist ab sofort Bootsführer:in 🎉 Hip hip ...!"
|
||||
),
|
||||
"Neue:r Bootsführer:in",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
(old, new) if new == None => {
|
||||
if let Some(old) = old {
|
||||
self.remove_role(db, updated_by, &old).await?;
|
||||
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("Lieber Vorstand, {self} ist ab kein {old} mehr."),
|
||||
"Steuerperson --",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
(old, new) => return Err(format!("Not allowed to change from {old:?} to {new:?}")),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn change_financial(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
financial: Option<Role>,
|
||||
) -> Result<(), String> {
|
||||
let mut new = String::new();
|
||||
let mut old = String::new();
|
||||
|
||||
if let Some(old_financial) = self.financial(db).await {
|
||||
self.remove_role(db, updated_by, &old_financial).await?;
|
||||
old.push_str(&old_financial.name);
|
||||
} else {
|
||||
old.push_str("Keine Ermäßigung");
|
||||
}
|
||||
|
||||
if let Some(new_financial) = financial {
|
||||
self.add_role(db, updated_by, &new_financial).await?;
|
||||
new.push_str(&new_financial.name);
|
||||
} else {
|
||||
new.push_str("Keine Ermäßigung");
|
||||
}
|
||||
|
||||
Activity::create(
|
||||
db,
|
||||
&format!("({updated_by}) Ermäßigung von {self} von {old} auf {new} geändert"),
|
||||
&format!("user-{};", self.id),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_role(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
|
@ -259,6 +259,39 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
.into_iter().map(|r| r.name).collect()
|
||||
}
|
||||
|
||||
pub async fn financial(&self, db: &SqlitePool) -> Option<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
"
|
||||
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
|
||||
FROM role r
|
||||
JOIN user_role ur ON r.id = ur.role_id
|
||||
WHERE ur.user_id = ?
|
||||
AND r.cluster = 'financial';
|
||||
",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
pub async fn skill(&self, db: &SqlitePool) -> Option<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
"
|
||||
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
|
||||
FROM role r
|
||||
JOIN user_role ur ON r.id = ur.role_id
|
||||
WHERE ur.user_id = ?
|
||||
AND r.cluster = 'skill';
|
||||
",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
|
@ -130,6 +130,10 @@ async fn view(
|
||||
let member = Member::from(db, user.clone()).await;
|
||||
let fee = user.fee(db).await;
|
||||
let activities = Activity::for_user(db, &user).await;
|
||||
let financial = Role::all_cluster(db, "financial").await;
|
||||
let user_financial = user.financial(db).await;
|
||||
let skill = Role::all_cluster(db, "skill").await;
|
||||
let user_skill = user.skill(db).await;
|
||||
|
||||
let user = UserWithRolesAndMembershipPdf::from_user(db, user).await;
|
||||
|
||||
@ -148,6 +152,10 @@ async fn view(
|
||||
context.insert("is_clubmember", &member.is_club_member());
|
||||
context.insert("supposed_to_pay", &member.supposed_to_pay());
|
||||
context.insert("fee", &fee);
|
||||
context.insert("skill", &skill);
|
||||
context.insert("user_skill", &user_skill);
|
||||
context.insert("financial", &financial);
|
||||
context.insert("user_financial", &user_financial);
|
||||
context.insert("member", &member);
|
||||
context.insert("activities", &activities);
|
||||
context.insert("roles", &roles);
|
||||
@ -456,6 +464,86 @@ async fn update_family(
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ChangeSkillForm {
|
||||
skill_id: String,
|
||||
}
|
||||
|
||||
#[post("/user/<id>/change-skill", data = "<data>")]
|
||||
async fn change_skill(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ChangeSkillForm>,
|
||||
admin: ManageUserUser,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
|
||||
let skill = if &data.skill_id == "" {
|
||||
None
|
||||
} else {
|
||||
let Ok(skill_id) = data.skill_id.parse() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!("Skill_id is not a number"),
|
||||
);
|
||||
};
|
||||
Role::find_by_id(db, skill_id).await
|
||||
};
|
||||
|
||||
match user.change_skill(db, &admin, skill).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", user.id)),
|
||||
"Skill erfolgreich geändert",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ChangeFinancialForm {
|
||||
financial_id: String,
|
||||
}
|
||||
|
||||
#[post("/user/<id>/change-financial", data = "<data>")]
|
||||
async fn change_financial(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ChangeFinancialForm>,
|
||||
admin: ManageUserUser,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
|
||||
let financial = if &data.financial_id == "" {
|
||||
None
|
||||
} else {
|
||||
let Ok(financial_id) = data.financial_id.parse() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!("Finacial_id is not a number"),
|
||||
);
|
||||
};
|
||||
Role::find_by_id(db, financial_id).await
|
||||
};
|
||||
|
||||
match user.change_financial(db, &admin, financial).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", user.id)),
|
||||
"Ermäßigung erfolgreich geändert",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct AddMembershipPDFForm<'a> {
|
||||
membership_pdf: TempFile<'a>,
|
||||
@ -1176,6 +1264,8 @@ pub fn routes() -> Vec<Route> {
|
||||
update_birthdate,
|
||||
update_address,
|
||||
update_family,
|
||||
change_skill,
|
||||
change_financial,
|
||||
add_membership_pdf,
|
||||
add_role,
|
||||
add_note,
|
||||
|
@ -33,3 +33,4 @@ CREATE TABLE activity (
|
||||
relevant_for TEXT NOT NULL, -- e.g. user_id=123;trip_id=456
|
||||
keep_until DATETIME -- OPTIONAL field
|
||||
);
|
||||
delete from role where name='Anwärter';
|
||||
|
@ -31,6 +31,13 @@
|
||||
<form action="/admin/user/{{ user.id }}/change-nickname" method="post">
|
||||
{{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-financial" method="post">
|
||||
{% if user_financial %}
|
||||
{{ macros::selectgroup(label="Finanzielles", data=financial, selected_id=user_financial.id, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
|
||||
{% else %}
|
||||
{{ macros::selectgroup(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% if allowed_to_edit %}
|
||||
<form action="/admin/user/{{ user.id }}/new-note" method="post">
|
||||
{{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }}
|
||||
@ -79,6 +86,14 @@
|
||||
<form action="/admin/user/{{ user.id }}/change-address" method="post">
|
||||
{{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-skill" method="post">
|
||||
{% if user_skill %}
|
||||
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, selected_id=user_skill.id, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
|
||||
{% else %}
|
||||
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
|
||||
|
||||
{% endif %}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-family" method="post">
|
||||
{{ macros::selectgroup(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen', readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
@ -398,64 +413,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<h2 class="h2">TODO</h2>
|
||||
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
|
||||
<span class="text-black dark:text-white cursor-pointer">
|
||||
<span class="font-bold">
|
||||
{{ user.name }}
|
||||
{% if not user.last_access and allowed_to_edit and user.mail %}
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="inline">
|
||||
• <a class="font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/send-welcome-mail"
|
||||
onclick="return confirm('Willst du wirklich das Willkommensmail an {{ user.name }} ausschicken?');">Willkommensmail verschicken</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
</span>
|
||||
</span>
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="w-full mt-2">
|
||||
<div class="w-full grid gap-3 mt-3">
|
||||
<input type="hidden" name="id" value="{{ user.id }}" />
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{% for cluster, cluster_roles in roles | group_by(attribute="cluster") %}
|
||||
<label for="cluster_{{ loop.index }}">{{ cluster }}</label>
|
||||
{# Determine the initially selected role within the cluster #}
|
||||
{% set_global selected_role_id = "none" %}
|
||||
{% for role in cluster_roles %}
|
||||
{% if selected_role_id == "none" and role.name in user.roles %}
|
||||
{% set_global selected_role_id = role.id %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{# Set default name to the selected role ID or first role if none selected #}
|
||||
<select id="cluster_{{ loop.index }}"
|
||||
{% if selected_role_id == 'none' %} {% else %} name="roles[{{ selected_role_id }}]" {% endif %}
|
||||
{% if allowed_to_edit == false %}disabled{% endif %}
|
||||
onchange=" if (this.value === '') { this.removeAttribute('name'); } else { this.name = 'roles[' + this.options[this.selectedIndex].getAttribute('data-role-id') + ']'; }">
|
||||
<option value=""
|
||||
data-role-id="none"
|
||||
{% if selected_role_id == 'none' %}selected="selected"{% endif %}>
|
||||
None
|
||||
</option>
|
||||
{% for role in cluster_roles %}
|
||||
<option value="on"
|
||||
data-role-id="{{ role.id }}"
|
||||
{% if role.id == selected_role_id %}selected="selected"{% endif %}>
|
||||
{{ role.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<h2 class="h2">Ergo-Challenge</h2>
|
||||
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
|
||||
|
Loading…
x
Reference in New Issue
Block a user