diff --git a/frontend/scss/components/_input.scss b/frontend/scss/components/_input.scss index 54e75c8..6cf702a 100644 --- a/frontend/scss/components/_input.scss +++ b/frontend/scss/components/_input.scss @@ -5,7 +5,8 @@ .input-group { @apply flex; - input[readonly] { + input[readonly], + select[disabled] { opacity: .7; } diff --git a/frontend/scss/components/_links.scss b/frontend/scss/components/_links.scss index 21d9ab7..ccf00a0 100644 --- a/frontend/scss/components/_links.scss +++ b/frontend/scss/components/_links.scss @@ -10,4 +10,8 @@ &-white { @apply text-white hover:text-primary-100 underline; } + + &-no-underline { + @apply no-underline; + } } diff --git a/migration.sql b/migration.sql index d774e7f..1d18740 100644 --- a/migration.sql +++ b/migration.sql @@ -225,6 +225,15 @@ CREATE TABLE IF NOT EXISTS "distance" ( ); +CREATE TABLE IF NOT EXISTS "activity" ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + text TEXT NOT NULL, + relevant_for TEXT NOT NULL, -- e.g. user_id=123;trip_id=456 + keep_until DATETIME +); + + CREATE TRIGGER IF NOT EXISTS prevent_multiple_roles_same_cluster BEFORE INSERT ON user_role BEGIN diff --git a/src/model/activity.rs b/src/model/activity.rs new file mode 100644 index 0000000..c19d744 --- /dev/null +++ b/src/model/activity.rs @@ -0,0 +1,54 @@ +use std::ops::DerefMut; + +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; + +#[derive(FromRow, Debug, Serialize, Deserialize, Clone)] +pub struct Activity { + pub id: i64, + pub created_at: NaiveDateTime, + pub text: String, + pub relevant_for: String, + pub keep_until: Option, +} + +impl Activity { + pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { + sqlx::query_as!( + Self, + "SELECT id, created_at, text, relevant_for, keep_until FROM activity WHERE id like ?", + id + ) + .fetch_one(db) + .await + .ok() + } + pub async fn create_with_tx( + db: &mut Transaction<'_, Sqlite>, + text: &str, + relevant_for: &str, + keep_until: Option, + ) { + sqlx::query!( + "INSERT INTO activity(text, relevant_for, keep_until) VALUES (?, ?, ?)", + text, + relevant_for, + keep_until + ) + .execute(db.deref_mut()) + .await + .unwrap(); + } + + pub async fn create( + db: &SqlitePool, + text: &str, + relevant_for: &str, + keep_until: Option, + ) { + let mut tx = db.begin().await.unwrap(); + Self::create_with_tx(&mut tx, text, relevant_for, keep_until).await; + tx.commit().await.unwrap(); + } +} diff --git a/src/model/mail.rs b/src/model/mail.rs index 675c8a1..746889f 100644 --- a/src/model/mail.rs +++ b/src/model/mail.rs @@ -79,7 +79,9 @@ impl Mail { .build(); // Send the email - mailer.send(&email).unwrap(); + if let Err(e) = mailer.send(&email) { + Log::create_with_tx(db, format!("Mail nicht versandt: {e:?}")).await; + } Ok(()) } diff --git a/src/model/mod.rs b/src/model/mod.rs index 8cc6613..6e531d1 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -14,6 +14,7 @@ use self::{ use boatreservation::{BoatReservation, BoatReservationWithDetails}; use std::collections::HashMap; +pub mod activity; pub mod boat; pub mod boatdamage; pub mod boathouse; diff --git a/src/model/user/basic.rs b/src/model/user/basic.rs index 82916b4..d61ac96 100644 --- a/src/model/user/basic.rs +++ b/src/model/user/basic.rs @@ -1,12 +1,25 @@ // TODO: put back in `src/model/user/mod.rs` once that is cleaned up use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User}; -use crate::model::{family::Family, log::Log, mail::valid_mails, role::Role}; +use crate::model::{activity::Activity, family::Family, log::Log, mail::valid_mails, role::Role}; use chrono::NaiveDate; use rocket::{fs::TempFile, tokio::io::AsyncReadExt}; use sqlx::SqlitePool; impl User { + pub(crate) async fn add_note( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + note: &str, + ) -> Result<(), String> { + let note = note.trim(); + + Activity::create(db, note, "relevant_for", None).await; + + Ok(()) + } + pub(crate) async fn update_mail( &self, db: &SqlitePool, diff --git a/src/model/user/clubmember.rs b/src/model/user/clubmember.rs new file mode 100644 index 0000000..4219754 --- /dev/null +++ b/src/model/user/clubmember.rs @@ -0,0 +1,162 @@ +use super::User; +use crate::{ + model::{log::Log, notification::Notification, role::Role, user::ManageUserUser}, + special_user, +}; +use rocket::async_trait; +use sqlx::SqlitePool; + +special_user!(ClubMemberUser, +"Donau Linz", +"Förderndes Mitglied", +"Unterstützend"); + +impl ClubMemberUser { + async fn add_membership_role(&self, db: &SqlitePool, role: &Role) { + sqlx::query!( + "INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", + self.id, + role.id + ) + .execute(db) + .await + .unwrap(); + } + + async fn remove_membership_role(&self, db: &SqlitePool) { + let role = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap(); + sqlx::query!( + "DELETE FROM user_role WHERE user_id = ? and role_id = ?", + self.id, + role.id + ) + .execute(db) + .await + .unwrap(); + + let role = Role::find_by_name(db, "Unterstützend").await.unwrap(); + sqlx::query!( + "DELETE FROM user_role WHERE user_id = ? and role_id = ?", + self.id, + role.id + ) + .execute(db) + .await + .unwrap(); + + let role = Role::find_by_name(db, "Donau Linz").await.unwrap(); + sqlx::query!( + "DELETE FROM user_role WHERE user_id = ? and role_id = ?", + self.id, + role.id + ) + .execute(db) + .await + .unwrap(); + } + async fn new_membership_role(&self, db: &SqlitePool, role: &str) -> Result<(), String> { + let role = Role::find_by_name(db, role).await.unwrap(); + self.remove_membership_role(db).await; + self.add_membership_role(db, &role).await; + Ok(()) + } + + pub(crate) async fn move_to_regular( + self, + db: &SqlitePool, + modified_by: &ManageUserUser, + ) -> Result<(), String> { + if self.has_role(db, "Donau Linz").await { + return Err(format!("User {self} ist bereits reguläres Mitglied.")); + } + + self.new_membership_role(db, "Donau Linz").await?; + + Notification::create_for_steering_people( + db, + &format!( + "Liebe Steuerberechtigte, {} hat upgegraded und ist nun ein neues reguläres Mitglied. 🎉", + self.name, + ), + "Neues Vereinsmitglied", + None, + None, + ) + .await; + + Log::create( + db, + format!("{modified_by} has moved user {self} to regular membership."), + ) + .await; + + Ok(()) + } + + pub(crate) async fn move_to_unterstuetzend( + self, + db: &SqlitePool, + modified_by: &ManageUserUser, + ) -> Result<(), String> { + if self.has_role(db, "Unterstützend").await { + return Err(format!("User {self} ist bereits unterstützendes Mitglied.")); + } + + self.new_membership_role(db, "Unterstützend").await?; + + if let Some(vorstand) = Role::find_by_name(db, "vorstand").await { + Notification::create_for_role( + db, + &vorstand, + &format!( + "Lieber Vorstand, der Mitgliedstatus von {} hat sich geändert auf 'Unterstützendes Mitglied'.", + self.name, + ), + "Neues unterstützendes Vereinsmitglied", + None, + None, + ) + .await; + } + + Log::create( + db, + format!("{modified_by} has moved user {self} to unterstützend membership."), + ) + .await; + + Ok(()) + } + + pub(crate) async fn move_to_foerdernd( + self, + db: &SqlitePool, + modified_by: &ManageUserUser, + ) -> Result<(), String> { + if self.has_role(db, "Förderndes Mitglied").await { + return Err(format!("User {self} ist bereits förderndes Mitglied.")); + } + + self.new_membership_role(db, "Förderndes Mitglied").await?; + + if let Some(vorstand) = Role::find_by_name(db, "vorstand").await { + Notification::create_for_role( + db, + &vorstand, + &format!( + "Lieber Vorstand, der Mitgliedstatus von {} hat sich geändert auf 'Förderndes Mitglied'.", + self.name, + ), + "Neues förderndes Vereinsmitglied", + None, + None, + ) + .await; + } + + Log::create( + db, + format!("{modified_by} has moved user {self} to fördernd membership."), + ) + .await; + + Ok(()) + } +} diff --git a/src/model/user/foerdernd.rs b/src/model/user/foerdernd.rs new file mode 100644 index 0000000..215fbf6 --- /dev/null +++ b/src/model/user/foerdernd.rs @@ -0,0 +1,40 @@ +use super::User; +use crate::{model::mail::Mail, special_user}; +use rocket::async_trait; +use sqlx::SqlitePool; + +special_user!(FoerderndUser, +"Förderndes Mitglied"); + +impl FoerderndUser { + pub(crate) async fn send_welcome_mail_to_user( + &self, + db: &SqlitePool, + smtp_pw: &str, + ) -> Result<(), String> { + let Some(mail) = &self.mail else { + return Err(format!( + "Couldn't send welcome mail, as the user {self} has no mail..." + )); + }; + + Mail::send_single( + db, + mail, + "Willkommen im ASKÖ Ruderverein Donau Linz!", + format!( +"Hallo {0}, + +herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen. + +Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung. + +Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter. + +Riemen- & Dollenbruch +ASKÖ Ruderverein Donau Linz", self.name), + smtp_pw, + ).await?; + + Ok(()) + } +} diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs index 156b3fc..2730039 100644 --- a/src/model/user/mod.rs +++ b/src/model/user/mod.rs @@ -8,14 +8,12 @@ use rocket::{ http::{Cookie, Status}, request::{FromRequest, Outcome}, time::{Duration, OffsetDateTime}, - tokio::io::AsyncReadExt, Request, }; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use super::{ - family::Family, log::Log, logbook::Logbook, mail::Mail, @@ -26,14 +24,19 @@ use super::{ tripdetails::TripDetails, Day, }; -use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD}; +use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; use scheckbuch::ScheckbuchUser; mod basic; +pub(crate) mod clubmember; mod fee; +pub(crate) mod foerdernd; pub(crate) mod member; pub(crate) mod regular; pub(crate) mod scheckbuch; +pub(crate) mod schnupperant; +pub(crate) mod schnupperinterest; +pub(crate) mod unterstuetzend; #[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] pub struct User { @@ -120,7 +123,7 @@ impl User { if self.has_role(db, "schnupperant").await { self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?; } else if let Some(scheckbuch) = ScheckbuchUser::new(db, self).await { - scheckbuch.notify(db, mail, smtp_pw).await?; + scheckbuch.notify(db, smtp_pw).await?; } else { return Err(format!( "Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group", @@ -478,68 +481,6 @@ ORDER BY last_access DESC .unwrap(); //Okay, because we can only create a User of a valid id } - pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) -> Result<(), String> { - let mut db = db.begin().await.map_err(|e| e.to_string())?; - - let mut family_id = data.family_id; - - if family_id.is_some_and(|x| x == -1) { - family_id = Some(Family::insert_tx(&mut db).await) - } - - if !self.has_membership_pdf_tx(&mut db).await { - if let Some(membership_pdf) = data.membership_pdf { - let mut stream = membership_pdf.open().await.unwrap(); - let mut buffer = Vec::new(); - stream.read_to_end(&mut buffer).await.unwrap(); - sqlx::query!( - "UPDATE user SET membership_pdf = ? where id = ?", - buffer, - self.id - ) - .execute(db.deref_mut()) - .await - .unwrap(); //Okay, because we can only create a User of a valid id - } - } - - sqlx::query!( - "UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?", - data.dob, - data.weight, - data.sex, - data.member_since_date, - data.birthdate, - data.mail, - data.nickname, - data.notes, - data.phone, - data.address, - family_id, - self.id - ) - .execute(db.deref_mut()) - .await - .unwrap(); //Okay, because we can only create a User of a valid id - - // handle roles - sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id) - .execute(db.deref_mut()) - .await - .unwrap(); - - for role_id in data.roles.into_keys() { - let role = Role::find_by_id_tx(&mut db, role_id.parse::().unwrap()) - .await - .unwrap(); - self.add_role_tx(&mut db, &role).await?; - } - - db.commit().await.map_err(|e| e.to_string())?; - - Ok(()) - } - async fn send_end_mail_scheckbuch( &self, db: &mut Transaction<'_, Sqlite>, @@ -1010,9 +951,7 @@ impl UserWithMembershipPdf { #[cfg(test)] mod test { - use std::collections::HashMap; - - use crate::{tera::admin::user::UserEditForm, testdb}; + use crate::testdb; use super::User; use sqlx::SqlitePool; @@ -1073,38 +1012,6 @@ mod test { assert_eq!(User::create(&pool, "admin".into()).await, false); } - #[sqlx::test] - fn test_update() { - let pool = testdb!(); - - let user = User::find_by_id(&pool, 1).await.unwrap(); - user.update( - &pool, - UserEditForm { - id: 1, - dob: None, - weight: None, - sex: Some("m".into()), - roles: HashMap::new(), - member_since_date: None, - birthdate: None, - mail: None, - nickname: None, - notes: None, - phone: None, - address: None, - family_id: None, - membership_pdf: None, - }, - ) - .await - .unwrap(); - - let user = User::find_by_id(&pool, 1).await.unwrap(); - - assert_eq!(user.sex, Some("m".into())); - } - #[sqlx::test] fn succ_login_with_test_db() { let pool = testdb!(); diff --git a/src/model/user/regular.rs b/src/model/user/regular.rs index de557b8..cd7fdce 100644 --- a/src/model/user/regular.rs +++ b/src/model/user/regular.rs @@ -1,22 +1,12 @@ use super::User; -use crate::{ - model::{mail::Mail, notification::Notification}, - special_user, -}; +use crate::{model::mail::Mail, special_user}; use rocket::async_trait; use sqlx::SqlitePool; -special_user!(RegularUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied"); +special_user!(RegularUser, +"Donau Linz"); impl RegularUser { - pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { - self.notify_coxes_about_new_regular(db).await; - self.send_welcome_mail_to_user(db, smtp_pw).await?; - - Ok(()) - } - - async fn send_welcome_mail_to_user( + pub(crate) async fn send_welcome_mail_to_user( &self, db: &SqlitePool, smtp_pw: &str, @@ -55,19 +45,4 @@ ASKÖ Ruderverein Donau Linz", self.name), Ok(()) } - - async fn notify_coxes_about_new_regular(&self, db: &SqlitePool) { - Notification::create_for_steering_people( - db, - &format!( - "Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}", - self.member_since_date.clone().unwrap(), - self.name - ), - "Neues Vereinsmitglied", - None, - None, - ) - .await; - } } diff --git a/src/model/user/scheckbuch.rs b/src/model/user/scheckbuch.rs index 9273eb5..cc038ec 100644 --- a/src/model/user/scheckbuch.rs +++ b/src/model/user/scheckbuch.rs @@ -1,4 +1,6 @@ +use super::foerdernd::FoerderndUser; use super::regular::RegularUser; +use super::unterstuetzend::UnterstuetzendUser; use super::{ManageUserUser, User}; use crate::model::role::Role; use crate::NonEmptyString; @@ -14,10 +16,9 @@ use sqlx::SqlitePool; special_user!(ScheckbuchUser, +"scheckbuch"); impl ScheckbuchUser { - pub(crate) async fn convert_to_regular_user( - self, + async fn set_data_for_clubmember( + &self, db: &SqlitePool, - smtp_pw: &str, changed_by: &ManageUserUser, member_since: &NaiveDate, birthdate: &NaiveDate, @@ -25,7 +26,6 @@ impl ScheckbuchUser { address: NonEmptyString, membership_pdf: &TempFile<'_>, ) -> Result<(), String> { - // Set data self.user.update_birthdate(db, changed_by, birthdate).await; self.user .update_member_since(db, changed_by, member_since) @@ -37,6 +37,30 @@ impl ScheckbuchUser { .add_membership_pdf(db, changed_by, membership_pdf) .await?; + Ok(()) + } + pub(crate) async fn convert_to_regular_user( + self, + db: &SqlitePool, + smtp_pw: &str, + changed_by: &ManageUserUser, + member_since: &NaiveDate, + birthdate: &NaiveDate, + phone: NonEmptyString, + address: NonEmptyString, + membership_pdf: &TempFile<'_>, + ) -> Result<(), String> { + self.set_data_for_clubmember( + db, + changed_by, + member_since, + birthdate, + phone, + address, + membership_pdf, + ) + .await?; + // Change roles let regular = Role::find_by_name(db, "Donau Linz").await.unwrap(); let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap(); @@ -45,33 +69,146 @@ impl ScheckbuchUser { // Notify let regular = RegularUser::new(db, &self.user).await.unwrap(); - regular.notify(db, smtp_pw).await?; + regular.send_welcome_mail_to_user(db, smtp_pw).await?; + Notification::create_for_steering_people( + db, + &format!( + "Liebe Steuerberechtigte, {} hatte ein Scheckbuch und ist nun seit {} es ein neues reguläres Mitglied. 🎉", + self.name, + self.member_since_date.clone().unwrap() + ), + "Neues Vereinsmitglied", + None, + None, + ) + .await; + + Ok(()) + } + + pub(crate) async fn convert_to_unterstuetzend_user( + self, + db: &SqlitePool, + smtp_pw: &str, + changed_by: &ManageUserUser, + member_since: &NaiveDate, + birthdate: &NaiveDate, + phone: NonEmptyString, + address: NonEmptyString, + membership_pdf: &TempFile<'_>, + ) -> Result<(), String> { + // Set data + self.set_data_for_clubmember( + db, + changed_by, + member_since, + birthdate, + phone, + address, + membership_pdf, + ) + .await?; + + // Change roles + let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap(); + let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap(); + self.user.remove_role(db, changed_by, &scheckbook).await?; + self.user.add_role(db, changed_by, &unterstuetzend).await?; + + let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap(); + unterstuetzend + .send_welcome_mail_to_user(db, smtp_pw) + .await?; + if let Some(vorstand) = Role::find_by_name(db, "vorstand").await { + Notification::create_for_role( + db, + &vorstand, + &format!( + "Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues unterstützendes Mitglied.", + self.name, + self.member_since_date.clone().unwrap() + ), + "Neues unterstützendes Vereinsmitglied", + None, + None, + ) + .await; + } + + Ok(()) + } + + pub(crate) async fn convert_to_foerdernd_user( + self, + db: &SqlitePool, + smtp_pw: &str, + changed_by: &ManageUserUser, + member_since: &NaiveDate, + birthdate: &NaiveDate, + phone: NonEmptyString, + address: NonEmptyString, + membership_pdf: &TempFile<'_>, + ) -> Result<(), String> { + // Set data + self.set_data_for_clubmember( + db, + changed_by, + member_since, + birthdate, + phone, + address, + membership_pdf, + ) + .await?; + + // Change roles + let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap(); + let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap(); + self.user.remove_role(db, changed_by, &scheckbook).await?; + self.user.add_role(db, changed_by, &unterstuetzend).await?; + + let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap(); + foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?; + if let Some(vorstand) = Role::find_by_name(db, "vorstand").await { + Notification::create_for_role( + db, + &vorstand, + &format!( + "Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues förderndes Mitglied.", + self.name, + self.member_since_date.clone().unwrap() + ), + "Neues förderndes Vereinsmitglied", + None, + None, + ) + .await; + } Ok(()) } // TODO: make private - pub(crate) async fn notify( - &self, - db: &SqlitePool, - mail: &str, - smtp_pw: &str, - ) -> Result<(), String> { - self.send_welcome_mail_to_user(db, mail, smtp_pw).await?; + pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { self.notify_coxes_about_new_scheckbuch(db).await; + self.send_welcome_mail_to_user(db, smtp_pw).await?; Ok(()) } - async fn send_welcome_mail_to_user( + pub(crate) async fn send_welcome_mail_to_user( &self, db: &SqlitePool, - mail: &str, smtp_pw: &str, ) -> Result<(), String> { + let Some(mail) = &self.mail else { + return Err( + "Kann Mail nicht versenden, weil der User keine Mailadresse hinterlegt hat.".into(), + ); + }; Mail::send_single( db, - mail, + &mail, "ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich", format!( "Hallo {0}, diff --git a/src/model/user/schnupperant.rs b/src/model/user/schnupperant.rs new file mode 100644 index 0000000..6ff87b1 --- /dev/null +++ b/src/model/user/schnupperant.rs @@ -0,0 +1,319 @@ +use super::foerdernd::FoerderndUser; +use super::regular::RegularUser; +use super::scheckbuch::ScheckbuchUser; +use super::unterstuetzend::UnterstuetzendUser; +use super::{ManageUserUser, User}; +use crate::model::role::Role; +use crate::NonEmptyString; +use crate::{ + model::{mail::Mail, notification::Notification}, + special_user, +}; +use chrono::NaiveDate; +use rocket::async_trait; +use rocket::fs::TempFile; +use sqlx::SqlitePool; + +special_user!(SchnupperantUser, +"schnupperant"); + +impl SchnupperantUser { + async fn set_data_for_clubmember( + &self, + db: &SqlitePool, + changed_by: &ManageUserUser, + member_since: &NaiveDate, + birthdate: &NaiveDate, + phone: NonEmptyString, + address: NonEmptyString, + membership_pdf: &TempFile<'_>, + ) -> Result<(), String> { + self.user.update_birthdate(db, changed_by, birthdate).await; + self.user + .update_member_since(db, changed_by, member_since) + .await; + + self.user.update_phone(db, changed_by, &phone).await; + self.user.update_address(db, changed_by, &address).await; + self.user + .add_membership_pdf(db, changed_by, membership_pdf) + .await?; + + Ok(()) + } + pub(crate) async fn convert_to_regular_user( + self, + db: &SqlitePool, + smtp_pw: &str, + changed_by: &ManageUserUser, + member_since: &NaiveDate, + birthdate: &NaiveDate, + phone: NonEmptyString, + address: NonEmptyString, + membership_pdf: &TempFile<'_>, + ) -> Result<(), String> { + self.set_data_for_clubmember( + db, + changed_by, + member_since, + birthdate, + phone, + address, + membership_pdf, + ) + .await?; + + // Change roles + let regular = Role::find_by_name(db, "Donau Linz").await.unwrap(); + let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); + self.user.remove_role(db, changed_by, &scheckbook).await?; + self.user.add_role(db, changed_by, ®ular).await?; + + // Notify + let regular = RegularUser::new(db, &self.user).await.unwrap(); + regular.send_welcome_mail_to_user(db, smtp_pw).await?; + Notification::create_for_steering_people( + db, + &format!( + "Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {} ein neues reguläres Mitglied. 🎉", + self.name, + self.member_since_date.clone().unwrap() + ), + "Neues Vereinsmitglied", + None, + None, + ) + .await; + + Ok(()) + } + + pub(crate) async fn move_to_scheckbook( + self, + db: &SqlitePool, + changed_by: &ManageUserUser, + smtp_pw: &str, + ) -> Result<(), String> { + let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap(); + let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap(); + self.user.remove_role(db, changed_by, &schnupperant).await?; + self.user.add_role(db, changed_by, &scheckbook).await?; + + if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await { + self.add_role(db, &changed_by, &no_einschreibgebuehr) + .await + .expect("role doesn't have a group"); + } + + let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap(); + scheckbook.send_welcome_mail_to_user(db, smtp_pw).await?; + + Notification::create_for_steering_people( + db, + &format!( + "Liebe Steuerberechtigte, {} hat unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.", + self.name + ), + "Neues Scheckbuch", + None,None + ) + .await; + + Ok(()) + } + + pub(crate) async fn move_to_schnupperinterest( + self, + db: &SqlitePool, + changed_by: &ManageUserUser, + ) -> Result<(), String> { + let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte") + .await + .unwrap(); + let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap(); + self.user.remove_role(db, changed_by, &schnupperant).await?; + self.user + .add_role(db, changed_by, &schnupperinterest) + .await?; + + if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await { + Notification::create_for_role( + db, + &role, + &format!( + "Lieber Schnupperbetreuer, {} hat sich vom Schnupperkurs abgemeldet.", + self.name + ), + "Schnupperkurs Abmeldung", + None, + None, + ) + .await; + } + + Ok(()) + } + + pub(crate) async fn convert_to_unterstuetzend_user( + self, + db: &SqlitePool, + smtp_pw: &str, + changed_by: &ManageUserUser, + member_since: &NaiveDate, + birthdate: &NaiveDate, + phone: NonEmptyString, + address: NonEmptyString, + membership_pdf: &TempFile<'_>, + ) -> Result<(), String> { + // Set data + self.set_data_for_clubmember( + db, + changed_by, + member_since, + birthdate, + phone, + address, + membership_pdf, + ) + .await?; + + // Change roles + let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap(); + let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); + self.user.remove_role(db, changed_by, &scheckbook).await?; + self.user.add_role(db, changed_by, &unterstuetzend).await?; + if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await { + self.add_role(db, &changed_by, &no_einschreibgebuehr) + .await + .expect("role doesn't have a group"); + } + + let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap(); + unterstuetzend + .send_welcome_mail_to_user(db, smtp_pw) + .await?; + if let Some(vorstand) = Role::find_by_name(db, "vorstand").await { + Notification::create_for_role( + db, + &vorstand, + &format!( + "Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues unterstützendes Mitglied.", + self.name, + self.member_since_date.clone().unwrap() + ), + "Neues unterstützendes Vereinsmitglied", + None, + None, + ) + .await; + } + + Ok(()) + } + + pub(crate) async fn convert_to_foerdernd_user( + self, + db: &SqlitePool, + smtp_pw: &str, + changed_by: &ManageUserUser, + member_since: &NaiveDate, + birthdate: &NaiveDate, + phone: NonEmptyString, + address: NonEmptyString, + membership_pdf: &TempFile<'_>, + ) -> Result<(), String> { + // Set data + self.set_data_for_clubmember( + db, + changed_by, + member_since, + birthdate, + phone, + address, + membership_pdf, + ) + .await?; + + // Change roles + let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap(); + let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); + self.user.remove_role(db, changed_by, &scheckbook).await?; + self.user.add_role(db, changed_by, &unterstuetzend).await?; + if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await { + self.add_role(db, &changed_by, &no_einschreibgebuehr) + .await + .expect("role doesn't have a group"); + } + + let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap(); + foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?; + if let Some(vorstand) = Role::find_by_name(db, "vorstand").await { + Notification::create_for_role( + db, + &vorstand, + &format!( + "Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues förderndes Mitglied.", + self.name, + self.member_since_date.clone().unwrap() + ), + "Neues förderndes Vereinsmitglied", + None, + None, + ) + .await; + } + + Ok(()) + } + + // TODO: make private + pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { + self.notify_coxes_about_new_scheckbuch(db).await; + self.send_welcome_mail_to_user(db, smtp_pw).await?; + + Ok(()) + } + + async fn send_welcome_mail_to_user( + &self, + db: &SqlitePool, + smtp_pw: &str, + ) -> Result<(), String> { + let Some(mail) = &self.mail else { + return Err(format!( + "Couldn't send mail, because user {self} has no mail" + )); + }; + Mail::send_single( + db, + &mail, + "ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs", + format!( +"Hallo {0}, + +es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden. + +Riemen- & Dollenbruch, +ASKÖ Ruderverein Donau Linz", self.name), + smtp_pw, + ).await?; + + Ok(()) + } + + async fn notify_coxes_about_new_scheckbuch(&self, db: &SqlitePool) { + if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await { + Notification::create_for_role( + db, + &role, + &format!( + "Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.", + self.name + ), + "Neuer Schnupperant", + None, + None, + ) + .await; + } + } +} diff --git a/src/model/user/schnupperinterest.rs b/src/model/user/schnupperinterest.rs new file mode 100644 index 0000000..3fb07d1 --- /dev/null +++ b/src/model/user/schnupperinterest.rs @@ -0,0 +1,103 @@ +use super::scheckbuch::ScheckbuchUser; +use super::schnupperant::SchnupperantUser; +use super::{ManageUserUser, User}; +use crate::model::role::Role; +use crate::{model::notification::Notification, special_user}; +use rocket::async_trait; +use sqlx::SqlitePool; + +special_user!(SchnupperInterestUser, +"schnupper-interessierte"); + +impl SchnupperInterestUser { + pub(crate) async fn move_to_scheckbook( + self, + db: &SqlitePool, + changed_by: &ManageUserUser, + smtp_pw: &str, + ) -> Result<(), String> { + let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte") + .await + .unwrap(); + let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap(); + self.user + .remove_role(db, changed_by, &schnupperinterest) + .await?; + self.user.add_role(db, changed_by, &scheckbook).await?; + + let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap(); + scheckbook.send_welcome_mail_to_user(db, smtp_pw).await?; + + Notification::create_for_steering_people( + db, + &format!( + "Liebe Steuerberechtigte, {} wollte unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.", + self.name + ), + "Neues Scheckbuch", + None, + None + ) + .await; + + Ok(()) + } + + pub(crate) async fn move_to_schnupperant( + self, + db: &SqlitePool, + changed_by: &ManageUserUser, + smtp_pw: &str, + ) -> Result<(), String> { + let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte") + .await + .unwrap(); + let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap(); + self.user + .remove_role(db, changed_by, &schnupperinterest) + .await?; + self.user.add_role(db, changed_by, &schnupperant).await?; + + let schnupperant = SchnupperantUser::new(db, &self.user).await.unwrap(); + schnupperant.notify(db, smtp_pw).await?; + + if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await { + Notification::create_for_role( + db, + &role, + &format!( + "Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.", + self.name + ), + "Neuer Schnupper-Interessierte:r", + None, + None, + ) + .await; + } + + Ok(()) + } + + pub(crate) async fn notify(&self, db: &SqlitePool) -> Result<(), String> { + self.notify_schnupperbetreuer_about_new_interest(db).await; + + Ok(()) + } + + async fn notify_schnupperbetreuer_about_new_interest(&self, db: &SqlitePool) { + if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await { + Notification::create_for_role( + db, + &role, + &format!( + "Lieber Schnupperbetreuer, {} hat Interesse zum Schnupperkurs bekundet.", + self.name + ), + "Neuer Schnupper-Interessierte:r", + None, + None, + ) + .await; + } + } +} diff --git a/src/model/user/unterstuetzend.rs b/src/model/user/unterstuetzend.rs new file mode 100644 index 0000000..b417371 --- /dev/null +++ b/src/model/user/unterstuetzend.rs @@ -0,0 +1,40 @@ +use super::User; +use crate::{model::mail::Mail, special_user}; +use rocket::async_trait; +use sqlx::SqlitePool; + +special_user!(UnterstuetzendUser, +"Unterstützend"); + +impl UnterstuetzendUser { + pub(crate) async fn send_welcome_mail_to_user( + &self, + db: &SqlitePool, + smtp_pw: &str, + ) -> Result<(), String> { + let Some(mail) = &self.mail else { + return Err(format!( + "Couldn't send welcome mail, as the user {self} has no mail..." + )); + }; + + Mail::send_single( + db, + mail, + "Willkommen im ASKÖ Ruderverein Donau Linz!", + format!( +"Hallo {0}, + +herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen. + +Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung. + +Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter. + +Riemen- & Dollenbruch +ASKÖ Ruderverein Donau Linz", self.name), + smtp_pw, + ).await?; + + Ok(()) + } +} diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 6b853ba..42056cd 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use crate::{ model::{ family::Family, @@ -7,9 +5,10 @@ use crate::{ logbook::Logbook, role::Role, user::{ - member::Member, scheckbuch::ScheckbuchUser, AdminUser, AllowedToEditPaymentStatusUser, - ManageUserUser, User, UserWithDetails, UserWithMembershipPdf, - UserWithRolesAndMembershipPdf, VorstandUser, + clubmember::ClubMemberUser, member::Member, scheckbuch::ScheckbuchUser, + schnupperant::SchnupperantUser, schnupperinterest::SchnupperInterestUser, AdminUser, + AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails, + UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser, }, }, tera::Config, @@ -300,49 +299,6 @@ async fn delete(db: &State, admin: ManageUserUser, user: i32) -> Fla } } -#[derive(FromForm, Debug)] -pub struct UserEditForm<'a> { - pub(crate) id: i32, - pub(crate) dob: Option, - pub(crate) weight: Option, - pub(crate) sex: Option, - pub(crate) roles: HashMap, - pub(crate) member_since_date: Option, - pub(crate) birthdate: Option, - pub(crate) mail: Option, - pub(crate) nickname: Option, - pub(crate) notes: Option, - pub(crate) phone: Option, - pub(crate) address: Option, - pub(crate) family_id: Option, - pub(crate) membership_pdf: Option>, -} - -#[post("/user", data = "", format = "multipart/form-data")] -async fn update( - db: &State, - data: Form>, - admin: ManageUserUser, -) -> Flash { - let user = User::find_by_id(db, data.id).await; - Log::create( - db, - format!("{} updated user from {user:?} to {data:?}", admin.user.name), - ) - .await; - let Some(user) = user else { - return Flash::error( - Redirect::to("/admin/user"), - format!("User with ID {} does not exist!", data.id), - ); - }; - - match user.update(db, data.into_inner()).await { - Ok(_) => Flash::success(Redirect::to("/admin/user"), "Successfully updated user"), - Err(e) => Flash::error(Redirect::to("/admin/user"), e), - } -} - #[derive(FromForm, Debug)] pub struct MailUpdateForm { mail: String, @@ -371,6 +327,34 @@ async fn update_mail( } } +#[derive(FromForm, Debug)] +pub struct AddNoteForm { + note: String, +} + +#[post("/user//add-note", data = "")] +async fn add_note( + 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), + ); + }; + + match user.add_note(db, &admin, &data.note).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Notiz hinzugefügt", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), + } +} + #[derive(FromForm, Debug)] pub struct PhoneUpdateForm { phone: String, @@ -711,11 +695,11 @@ async fn create( } } -#[derive(FromForm, Debug)] -struct UserAddScheckbuchForm<'r> { - name: &'r str, - mail: &'r str, -} +//#[derive(FromForm, Debug)] +//struct UserAddScheckbuchForm<'r> { +// name: &'r str, +// mail: &'r str, +//} //#[post("/user/new/scheckbuch", data = "")] //async fn create_scheckbuch( @@ -764,58 +748,128 @@ struct UserAddScheckbuchForm<'r> { // Flash::success(Redirect::to("/admin/user/scheckbuch"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt.")) //} -//#[get("/user/move/schnupperant//to/scheckbuch")] -//async fn schnupper_to_scheckbuch( -// db: &State, -// id: i32, -// admin: SchnupperBetreuerUser, -// config: &State, -//) -> Flash { -// let Some(user) = User::find_by_id(db, id).await else { -// return Flash::error( -// Redirect::to("/admin/schnupper"), -// "user id not found".to_string(), -// ); -// }; -// -// if !user.has_role(db, "schnupperant").await { -// return Flash::error( -// Redirect::to("/admin/schnupper"), -// "kein schnupperant...".to_string(), -// ); -// } -// -// let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap(); -// let paid = Role::find_by_name(db, "paid").await.unwrap(); -// user.remove_role(db, &schnupperant).await; -// user.remove_role(db, &paid).await; -// -// let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap(); -// user.add_role(db, &scheckbuch) -// .await -// .expect("just removed 'schnupperant' thus can't have a role with that group"); -// -// if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await { -// user.add_role(db, &no_einschreibgebuehr) -// .await -// .expect("role doesn't have a group"); -// } -// -// user.send_welcome_email(db, &config.smtp_pw).await.unwrap(); -// -// Log::create( -// db, -// format!( -// "{} created new scheckbuch (from schnupperant): {}", -// admin.name, user.name -// ), -// ) -// .await; -// Flash::success(Redirect::to("/admin/schnupper"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap())) -//} +#[derive(FromForm, Debug)] +pub struct SchnupperantToRegularForm<'a> { + membertype: String, + member_since: String, + birthdate: String, + phone: String, + address: String, + membership_pdf: TempFile<'a>, +} + +#[post("/user//schnupperant-to-regular", data = "")] +async fn schnupperant_to_regular( + db: &State, + data: Form>, + admin: ManageUserUser, + config: &State, + 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 Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + format!( + "Geburtsdatum {} ist nicht im YYYY-MM-DD Format", + &data.birthdate + ), + ); + }; + let Ok(member_since) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + format!( + "Beitrittsdatum {} ist nicht im YYYY-MM-DD Format", + &data.birthdate + ), + ); + }; + + let Some(user) = SchnupperantUser::new(db, &user).await else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + "User ist kein Schnupperant", + ); + }; + + let Ok(phone) = data.phone.clone().try_into() else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + "Vereinsmitglied braucht eine Telefonnummer", + ); + }; + let Ok(address) = data.address.clone().try_into() else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + "Vereinsmitglied braucht eine Adresse", + ); + }; + let response = match &*data.membertype { + "regular" => { + user.convert_to_regular_user( + db, + &config.smtp_pw, + &admin, + &member_since, + &birthdate, + phone, + address, + &data.membership_pdf, + ) + .await + } + "unterstuetzend" => { + user.convert_to_unterstuetzend_user( + db, + &config.smtp_pw, + &admin, + &member_since, + &birthdate, + phone, + address, + &data.membership_pdf, + ) + .await + } + "foerdernd" => { + user.convert_to_foerdernd_user( + db, + &config.smtp_pw, + &admin, + &member_since, + &birthdate, + phone, + address, + &data.membership_pdf, + ) + .await + } + _ => { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + "Membertype gibts ned", + ) + } + }; + + match response { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", id)), + "Mitgliedstyp umgewandelt und Infos versendet", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e), + } +} #[derive(FromForm, Debug)] pub struct ScheckToRegularForm<'a> { + membertype: String, member_since: String, birthdate: String, phone: String, @@ -875,20 +929,220 @@ async fn scheckbook_to_regular( "Vereinsmitglied braucht eine Adresse", ); }; + let response = match &*data.membertype { + "regular" => { + user.convert_to_regular_user( + db, + &config.smtp_pw, + &admin, + &member_since, + &birthdate, + phone, + address, + &data.membership_pdf, + ) + .await + } + "unterstuetzend" => { + user.convert_to_unterstuetzend_user( + db, + &config.smtp_pw, + &admin, + &member_since, + &birthdate, + phone, + address, + &data.membership_pdf, + ) + .await + } + "foerdernd" => { + user.convert_to_foerdernd_user( + db, + &config.smtp_pw, + &admin, + &member_since, + &birthdate, + phone, + address, + &data.membership_pdf, + ) + .await + } + _ => { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + "Membertype gibts ned", + ) + } + }; - match user - .convert_to_regular_user( - db, - &config.smtp_pw, - &admin, - &member_since, - &birthdate, - phone, - address, - &data.membership_pdf, - ) - .await - { + match response { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", id)), + "Mitgliedstyp umgewandelt und Infos versendet", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e), + } +} + +#[derive(FromForm, Debug)] +pub struct ChangeMembertypeForm { + membertype: String, +} + +#[post("/user//change-membertype", data = "")] +async fn change_membertype( + db: &State, + admin: ManageUserUser, + data: Form, + 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 Some(user) = ClubMemberUser::new(&db, &user).await else { + return Flash::error( + Redirect::to("/admin/user"), + format!("User {user} ist kein Vereinsmitglied"), + ); + }; + + let response = match &*data.membertype { + "regular" => user.move_to_regular(db, &admin).await, + "unterstuetzend" => user.move_to_unterstuetzend(db, &admin).await, + "foerdernd" => user.move_to_foerdernd(db, &admin).await, + _ => { + return Flash::error( + Redirect::to(format!("/admin/user/{{ id }}")), + format!("Membertype gibt's ned"), + ) + } + }; + + match response { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", id)), + "Mitgliedstyp umgewandelt und Infos versendet", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e), + } +} + +#[get("/user//schnupperant-to-scheckbuch")] +async fn schnupperant_to_scheckbook( + db: &State, + admin: ManageUserUser, + config: &State, + 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 Some(user) = SchnupperantUser::new(&db, &user).await else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + format!("User {user} ist kein Schnupperant"), + ); + }; + + match user.move_to_scheckbook(db, &admin, &config.smtp_pw).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", id)), + "Mitgliedstyp umgewandelt und Infos versendet", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e), + } +} + +#[get("/user//schnupperinterest-to-schnupperant")] +async fn schnupperinterest_to_schnupperant( + db: &State, + admin: ManageUserUser, + config: &State, + 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 Some(user) = SchnupperInterestUser::new(&db, &user).await else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + format!("User {user} ist kein Schnupperinteressierter"), + ); + }; + + match user.move_to_schnupperant(db, &admin, &config.smtp_pw).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", id)), + "Mitgliedstyp umgewandelt und Infos versendet", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e), + } +} + +#[get("/user//schnupperant-to-schnupperinterest")] +async fn schnupperant_to_schnupperinterest( + db: &State, + 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 Some(user) = SchnupperantUser::new(&db, &user).await else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + format!("User {user} ist kein Schnupperant"), + ); + }; + + match user.move_to_schnupperinterest(db, &admin).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", id)), + "Mitgliedstyp umgewandelt.", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e), + } +} +#[get("/user//schnupperinterest-to-scheckbuch")] +async fn schnupperinterest_to_scheckbuch( + db: &State, + admin: ManageUserUser, + config: &State, + 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 Some(user) = SchnupperInterestUser::new(&db, &user).await else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + format!("User {user} ist kein Schnupperinteressierter"), + ); + }; + + match user.move_to_scheckbook(db, &admin, &config.smtp_pw).await { Ok(_) => Flash::success( Redirect::to(format!("/admin/user/{}", id)), "Mitgliedstyp umgewandelt und Infos versendet", @@ -903,10 +1157,8 @@ pub fn routes() -> Vec { index_admin, view, resetpw, - update, create, //create_scheckbuch, - //schnupper_to_scheckbuch, delete, fees, fees_paid, @@ -923,8 +1175,15 @@ pub fn routes() -> Vec { update_family, add_membership_pdf, add_role, + add_note, remove_role, // scheckbook_to_regular, + schnupperant_to_regular, + schnupperant_to_scheckbook, + schnupperinterest_to_schnupperant, + schnupperant_to_schnupperinterest, + schnupperinterest_to_scheckbuch, + change_membertype, ] } diff --git a/staging-diff.sql b/staging-diff.sql index 199172c..85e39e1 100644 --- a/staging-diff.sql +++ b/staging-diff.sql @@ -25,3 +25,11 @@ UPDATE role SET desc='Es können Logbucheinträge im Nachhinein hinzugefügt wer UPDATE role SET desc='Erlaubt den Login auf der Wordpress-Website um zB Artikel zu schreiben.' WHERE name='allow_website_login'; UPDATE role SET desc='Muss nur den halben Rennruderbeitrag bezahlen (da zB erst in der 2. Jahreshälfte dazugestoßen wurde)' WHERE name='half-rennrudern'; UPDATE role SET desc='Muss keinen Rennruderbeitrag bezahlen, obwohl man in Rennruder-Gruppe ist.' WHERE name='renntrainer'; + +CREATE TABLE activity ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + text TEXT NOT NULL, + relevant_for TEXT NOT NULL, -- e.g. user_id=123;trip_id=456 + keep_until DATETIME -- OPTIONAL field +); diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 3cf66aa..7d66067 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -67,115 +67,21 @@ {% for user in users %}
-
- - - - {{ user.name }} - {% if not user.last_access and allowed_to_edit and user.mail %} -
- • Willkommensmail verschicken -
- {% endif %} - {% if user.last_access %}• ⏳ {{ user.last_access | date }}{% endif %} -
- - {% for role in user.roles -%} - {{ role }} - {%- if not loop.last %}, - {% endif -%} - {% endfor %} - -
-
- ✏️ -
- {% if user.pw %} - Passwort zurücksetzen - {% 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 %} - {% for role in roles %} - {% if not role.cluster %} - {{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }} - {% endif %} - {% endfor %} -
- {% if user.membership_pdf %} - Beitrittserklärung herunterladen - {% else %} - {{ macros::input(label='Beitrittserklärung', name='membership_pdf', id=loop.index, type="file", readonly=allowed_to_edit == false, accept='application/pdf') }} - {% endif %} - {{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }} - {{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }} - {{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }} - {{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date, readonly=allowed_to_edit == false) }} - {{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate, readonly=allowed_to_edit == false) }} - {{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail, readonly=allowed_to_edit == false) }} - {{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname, readonly=allowed_to_edit == false) }} - {{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes, readonly=allowed_to_edit == false) }} - {{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone, readonly=allowed_to_edit == false) }} - {{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address, readonly=allowed_to_edit == false) }} - {% if allowed_to_edit %} - {{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }} - {% endif %} -
-
- {% if allowed_to_edit %} - - {% endif %} -
-
+ class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative flex justify-between items-center"> + + + {{ user.name }} + {% if user.last_access %}• ⏳ {{ user.last_access | date }}{% endif %} + + + {% for role in user.roles -%} + {{ role }} + {%- if not loop.last %}, + {% endif -%} + {% endfor %} + + + {% include "includes/pencil" %}
{% endfor %} diff --git a/templates/admin/user/view.html.tera b/templates/admin/user/view.html.tera index a99dc21..bd08efb 100644 --- a/templates/admin/user/view.html.tera +++ b/templates/admin/user/view.html.tera @@ -3,10 +3,12 @@ {% extends "base" %} {% block content %}
+ {% if "admin" in loggedin_user.roles or "Vorstand" in loggedin_user.roles %} + ← Userverwaltung + {% endif %}

{{ user.name }}

-