diff --git a/src/model/role.rs b/src/model/role.rs index 6b64b2f..07e09a4 100644 --- a/src/model/role.rs +++ b/src/model/role.rs @@ -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, } +// 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 { + 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 { + 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 { 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 { - 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 { sqlx::query_as!( Self, diff --git a/src/model/user/basic.rs b/src/model/user/basic.rs index 66d0aba..ea3bf10 100644 --- a/src/model/user/basic.rs +++ b/src/model/user/basic.rs @@ -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, + ) -> 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, + ) -> 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, diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs index 2730039..80b1af2 100644 --- a/src/model/user/mod.rs +++ b/src/model/user/mod.rs @@ -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 { + 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 { + 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 { sqlx::query_as!( Role, diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 8fa6aad..5b298be 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -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//change-skill", data = "")] +async fn change_skill( + db: &State, + data: Form, + admin: ManageUserUser, + id: i32, +) -> Flash { + 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//change-financial", data = "")] +async fn change_financial( + db: &State, + data: Form, + admin: ManageUserUser, + id: i32, +) -> Flash { + 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 { update_birthdate, update_address, update_family, + change_skill, + change_financial, add_membership_pdf, add_role, add_note, diff --git a/staging-diff.sql b/staging-diff.sql index 85e39e1..77e978c 100644 --- a/staging-diff.sql +++ b/staging-diff.sql @@ -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'; diff --git a/templates/admin/user/view.html.tera b/templates/admin/user/view.html.tera index 65966ff..e0de19c 100644 --- a/templates/admin/user/view.html.tera +++ b/templates/admin/user/view.html.tera @@ -31,6 +31,13 @@
{{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }}
+
+ {% 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 %} +
{% if allowed_to_edit %}
{{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }} @@ -79,6 +86,14 @@ {{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }}
+
+ {% 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 %} +
{{ 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) }}
@@ -398,64 +413,6 @@ -
-

TODO

-
- - - {{ user.name }} - {% if not user.last_access and allowed_to_edit and user.mail %} -
- • Willkommensmail verschicken -
- {% endif %} -
-
-
-
- -
- {% for cluster, cluster_roles in roles | group_by(attribute="cluster") %} - - {# 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 #} - - {% endfor %} -
-
-
-
-

Ergo-Challenge