Merge pull request 'single-user-edit-page' (#975) from single-user-edit-page into staging
Reviewed-on: #975
This commit was merged in pull request #975.
	This commit is contained in:
		| @@ -1,4 +1,4 @@ | |||||||
| use std::{fmt::Display, ops::DerefMut}; | use std::{cmp::Ordering, fmt::Display, ops::DerefMut}; | ||||||
|  |  | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||||
| @@ -13,6 +13,30 @@ pub struct Role { | |||||||
|     pub(crate) cluster: Option<String>, |     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 { | impl Display for Role { | ||||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|         write!(f, "{}", self.name) |         write!(f, "{}", self.name) | ||||||
| @@ -30,6 +54,27 @@ impl Role { | |||||||
|         .unwrap() |         .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> { |     pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> { | ||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Self, |             Self, | ||||||
| @@ -59,21 +104,6 @@ WHERE id like ? | |||||||
|         .ok() |         .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> { |     pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> { | ||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Self, |             Self, | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
| // TODO: put back in `src/model/user/mod.rs` once that is cleaned up | // TODO: put back in `src/model/user/mod.rs` once that is cleaned up | ||||||
|  |  | ||||||
| use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User}; | 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 chrono::NaiveDate; | ||||||
| use rocket::{fs::TempFile, tokio::io::AsyncReadExt}; | use rocket::{fs::TempFile, tokio::io::AsyncReadExt}; | ||||||
| use sqlx::SqlitePool; | use sqlx::SqlitePool; | ||||||
| @@ -228,6 +231,103 @@ impl User { | |||||||
|         .await; |         .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( |     pub(crate) async fn remove_role( | ||||||
|         &self, |         &self, | ||||||
|         db: &SqlitePool, |         db: &SqlitePool, | ||||||
|   | |||||||
| @@ -259,6 +259,39 @@ ASKÖ Ruderverein Donau Linz", self.name), | |||||||
|         .into_iter().map(|r| r.name).collect() |         .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> { |     pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> { | ||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Role, |             Role, | ||||||
|   | |||||||
| @@ -130,6 +130,10 @@ async fn view( | |||||||
|     let member = Member::from(db, user.clone()).await; |     let member = Member::from(db, user.clone()).await; | ||||||
|     let fee = user.fee(db).await; |     let fee = user.fee(db).await; | ||||||
|     let activities = Activity::for_user(db, &user).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; |     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("is_clubmember", &member.is_club_member()); | ||||||
|     context.insert("supposed_to_pay", &member.supposed_to_pay()); |     context.insert("supposed_to_pay", &member.supposed_to_pay()); | ||||||
|     context.insert("fee", &fee); |     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("member", &member); | ||||||
|     context.insert("activities", &activities); |     context.insert("activities", &activities); | ||||||
|     context.insert("roles", &roles); |     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)] | #[derive(FromForm, Debug)] | ||||||
| pub struct AddMembershipPDFForm<'a> { | pub struct AddMembershipPDFForm<'a> { | ||||||
|     membership_pdf: TempFile<'a>, |     membership_pdf: TempFile<'a>, | ||||||
| @@ -1176,6 +1264,8 @@ pub fn routes() -> Vec<Route> { | |||||||
|         update_birthdate, |         update_birthdate, | ||||||
|         update_address, |         update_address, | ||||||
|         update_family, |         update_family, | ||||||
|  |         change_skill, | ||||||
|  |         change_financial, | ||||||
|         add_membership_pdf, |         add_membership_pdf, | ||||||
|         add_role, |         add_role, | ||||||
|         add_note, |         add_note, | ||||||
|   | |||||||
| @@ -33,3 +33,4 @@ CREATE TABLE activity ( | |||||||
|     relevant_for TEXT NOT NULL,  -- e.g. user_id=123;trip_id=456 |     relevant_for TEXT NOT NULL,  -- e.g. user_id=123;trip_id=456 | ||||||
|     keep_until DATETIME          -- OPTIONAL field |     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"> |                         <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) }} |                             {{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }} | ||||||
|                         </form> |                         </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 %} |                         {% if allowed_to_edit %} | ||||||
|                             <form action="/admin/user/{{ user.id }}/new-note" method="post"> |                             <form action="/admin/user/{{ user.id }}/new-note" method="post"> | ||||||
|                                 {{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }} |                                 {{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }} | ||||||
| @@ -79,6 +86,13 @@ | |||||||
|                             <form action="/admin/user/{{ user.id }}/change-address" method="post"> |                             <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) }} |                                 {{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }} | ||||||
|                             </form> |                             </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"> |                             <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) }} |                                 {{ 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> |                             </form> | ||||||
| @@ -398,64 +412,6 @@ | |||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </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"> |             <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> |                 <h2 class="h2">Ergo-Challenge</h2> | ||||||
|                 <div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600"> |                 <div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600"> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user