diff --git a/migration.sql b/migration.sql index 365aa72..d774e7f 100644 --- a/migration.sql +++ b/migration.sql @@ -28,7 +28,10 @@ CREATE TABLE IF NOT EXISTS "family" ( CREATE TABLE IF NOT EXISTS "role" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" text NOT NULL UNIQUE, - "cluster" text + "formatted_name" text, + "desc" text, + "cluster" text, + "hide_in_lists" BOOLEAN NOT NULL DEFAULT false ); CREATE TABLE IF NOT EXISTS "user_role" ( diff --git a/src/model/family.rs b/src/model/family.rs index 55aa5eb..46bc95e 100644 --- a/src/model/family.rs +++ b/src/model/family.rs @@ -7,7 +7,7 @@ use super::user::User; #[derive(FromRow, Serialize, Clone)] pub struct Family { - id: i64, + pub(crate) id: i64, } #[derive(Serialize, Clone)] @@ -91,4 +91,18 @@ GROUP BY family.id;" .await .unwrap() } + + pub async fn clean_families_without_members(db: &SqlitePool) { + sqlx::query( + "DELETE FROM family +WHERE id NOT IN ( + SELECT DISTINCT family_id + FROM user + WHERE family_id IS NOT NULL +);", + ) + .execute(db) + .await + .unwrap(); + } } diff --git a/src/model/logbook.rs b/src/model/logbook.rs index 4b104bf..8fd9f8d 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -2,14 +2,14 @@ use std::ops::DerefMut; use chrono::{Datelike, Duration, Local, NaiveDateTime}; use rocket::FromForm; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use super::{ boat::Boat, log::Log, notification::Notification, role::Role, rower::Rower, user::User, }; -#[derive(FromRow, Serialize, Clone, Debug)] +#[derive(FromRow, Serialize, Deserialize, Clone, Debug)] pub struct Logbook { pub id: i64, pub boat_id: i64, @@ -105,7 +105,7 @@ impl TryFrom for LogToFinalize { } } -#[derive(Serialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct LogbookWithBoatAndRowers { #[serde(flatten)] pub logbook: Logbook, diff --git a/src/model/mail.rs b/src/model/mail.rs index 9879630..675c8a1 100644 --- a/src/model/mail.rs +++ b/src/model/mail.rs @@ -3,7 +3,7 @@ use std::{error::Error, fs}; use lettre::{ message::{header::ContentType, Attachment, MultiPart, SinglePart}, transport::smtp::authentication::Credentials, - Message, SmtpTransport, Transport, + Address, Message, SmtpTransport, Transport, }; use sqlx::{Sqlite, SqlitePool, Transaction}; @@ -374,3 +374,13 @@ Der Vorstand"); } } } + +pub(crate) fn valid_mails(mails: &str) -> bool { + let splitted = mails.split(','); + for single_rec in splitted { + if single_rec.parse::
().is_err() { + return false; + } + } + true +} diff --git a/src/model/role.rs b/src/model/role.rs index c8accad..6b64b2f 100644 --- a/src/model/role.rs +++ b/src/model/role.rs @@ -1,4 +1,4 @@ -use std::ops::DerefMut; +use std::{fmt::Display, ops::DerefMut}; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; @@ -7,22 +7,34 @@ use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; pub struct Role { pub(crate) id: i64, pub(crate) name: String, + pub(crate) formatted_name: Option, + pub(crate) desc: Option, + pub(crate) hide_in_lists: bool, pub(crate) cluster: Option, } +impl Display for Role { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + impl Role { pub async fn all(db: &SqlitePool) -> Vec { - sqlx::query_as!(Role, "SELECT id, name, cluster FROM role") - .fetch_all(db) - .await - .unwrap() + sqlx::query_as!( + Role, + "SELECT id, name, formatted_name, desc, hide_in_lists, cluster FROM role" + ) + .fetch_all(db) + .await + .unwrap() } pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option { sqlx::query_as!( Self, " -SELECT id, name, cluster +SELECT id, name, formatted_name, desc, hide_in_lists, cluster FROM role WHERE id like ? ", @@ -36,7 +48,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, cluster +SELECT id, name, formatted_name, desc, hide_in_lists, cluster FROM role WHERE id like ? ", @@ -51,7 +63,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, cluster +SELECT id, name, formatted_name, desc, hide_in_lists, cluster FROM role WHERE cluster = ? ", @@ -66,7 +78,7 @@ WHERE cluster = ? sqlx::query_as!( Self, " -SELECT id, name, cluster +SELECT id, name, formatted_name, desc, hide_in_lists, cluster FROM role WHERE name like ? ", @@ -81,7 +93,7 @@ WHERE name like ? sqlx::query_as!( Self, " -SELECT id, name, cluster +SELECT id, name, formatted_name, desc, hide_in_lists, cluster FROM role WHERE name like ? ", diff --git a/src/model/trip.rs b/src/model/trip.rs index 36c0773..e94614a 100644 --- a/src/model/trip.rs +++ b/src/model/trip.rs @@ -47,7 +47,7 @@ pub struct TripUpdate<'a> { pub is_locked: bool, } -impl<'a> TripUpdate<'a> { +impl TripUpdate<'_> { fn cancelled(&self) -> bool { self.max_people == -1 } diff --git a/src/model/user/basic.rs b/src/model/user/basic.rs new file mode 100644 index 0000000..60a3cd7 --- /dev/null +++ b/src/model/user/basic.rs @@ -0,0 +1,337 @@ +// 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 chrono::NaiveDate; +use rocket::{fs::TempFile, tokio::io::AsyncReadExt}; +use sqlx::SqlitePool; + +impl User { + pub(crate) async fn update_mail( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + new_mail: &str, + ) -> Result<(), String> { + let new_mail = new_mail.trim(); + + if !valid_mails(new_mail) { + return Err(format!( + "{new_mail} ist kein gültiges Format für eine Mailadresse" + )); + } + + sqlx::query!("UPDATE user SET mail = ? where id = ?", new_mail, self.id) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + + let msg = match &self.mail { + Some(old_mail) => format!( + "{updated_by} has changed the mail address of {self} from {old_mail} to {new_mail}" + ), + None => format!("{updated_by} has added a mail address for {self}: {new_mail}"), + }; + Log::create(db, msg).await; + + Ok(()) + } + + pub(crate) async fn update_phone( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + new_phone: &str, + ) -> Result<(), String> { + let new_phone = new_phone.trim(); + + let query = if new_phone.is_empty() { + sqlx::query!("UPDATE user SET phone = NULL where id = ?", self.id) + } else { + sqlx::query!("UPDATE user SET phone = ? where id = ?", new_phone, self.id) + }; + query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id + + let msg = match &self.phone { + Some(old_phone) if new_phone.is_empty() => format!("{updated_by} has removed the phone number of {self} (old number: {old_phone})"), + Some(old_phone) => format!("{updated_by} has changed the phone number of {self} from {old_phone} to {new_phone}"), + None => format!("{updated_by} has added a phone number for {self}: {new_phone}") + }; + Log::create(db, msg).await; + + Ok(()) + } + + pub(crate) async fn update_address( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + new_address: &str, + ) -> Result<(), String> { + let new_address = new_address.trim(); + + let query = if new_address.is_empty() { + sqlx::query!("UPDATE user SET address = NULL where id = ?", self.id) + } else { + sqlx::query!( + "UPDATE user SET address = ? where id = ?", + new_address, + self.id + ) + }; + query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id + + let msg = match &self.address { + Some(old_address) if new_address.is_empty() => format!("{updated_by} has removed the address of {self} (old address: {old_address})"), + Some(old_address) => format!("{updated_by} has changed the address of {self} from {old_address} to {new_address}"), + None => format!("{updated_by} has added an address for {self}: {new_address}") + }; + Log::create(db, msg).await; + + Ok(()) + } + + pub(crate) async fn update_nickname( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + new_nickname: &str, + ) -> Result<(), String> { + let new_nickname = new_nickname.trim(); + + let query = if new_nickname.is_empty() { + sqlx::query!("UPDATE user SET nickname = NULL where id = ?", self.id) + } else { + sqlx::query!( + "UPDATE user SET nickname = ? where id = ?", + new_nickname, + self.id + ) + }; + query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id + + let msg = match &self.nickname { + Some(old_nickname) if new_nickname.is_empty() => format!("{updated_by} has removed the nickname of {self} (old nickname: {old_nickname})"), + Some(old_nickname) => format!("{updated_by} has changed the nickname of {self} from {old_nickname} to {new_nickname}"), + None => format!("{updated_by} has added a nickname for {self}: {new_nickname}") + }; + Log::create(db, msg).await; + + Ok(()) + } + + pub(crate) async fn update_member_since( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + new_member_since_date: &NaiveDate, + ) { + sqlx::query!( + "UPDATE user SET member_since_date = ? where id = ?", + new_member_since_date, + self.id + ) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + + let msg = match &self.member_since_date { + Some(old_member_since_date) => format!("{updated_by} has changed the member_since date of {self} from {old_member_since_date} to {new_member_since_date}"), + None => format!("{updated_by} has added a member_since_date for {self}: {new_member_since_date}") + }; + Log::create(db, msg).await; + } + + pub(crate) async fn update_birthdate( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + new_birthdate: &NaiveDate, + ) { + sqlx::query!( + "UPDATE user SET birthdate = ? where id = ?", + new_birthdate, + self.id + ) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + + let msg = match &self.birthdate{ + Some(old_birthdate) => format!("{updated_by} has changed the birthdate of {self} from {old_birthdate} to {new_birthdate}"), + None => format!("{updated_by} has added a birthdate for {self}: {new_birthdate}") + }; + Log::create(db, msg).await; + } + + pub(crate) async fn update_family( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + family: Option, + ) { + if let Some(family) = family { + let family_id = family.id; + sqlx::query!( + "UPDATE user SET family_id = ? where id = ?", + family_id, + self.id + ) + .execute(db) + .await + .unwrap(); + } else { + sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id) + .execute(db) + .await + .unwrap(); + }; + + Family::clean_families_without_members(db).await; + + Log::create( + db, + format!("{updated_by} hat die Familie von {self} aktualisiert."), + ) + .await; + } + + pub(crate) async fn remove_role( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + role: &Role, + ) -> Result<(), String> { + if !self.has_role(db, &role.name).await { + return Err(format!("Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat")); + } + + sqlx::query!( + "DELETE FROM user_role WHERE user_id = ? and role_id = ?", + self.id, + role.id + ) + .execute(db) + .await + .unwrap(); + + Log::create( + db, + format!("{updated_by} has removed role {role} from user {self}"), + ) + .await; + + Ok(()) + } + + pub(crate) async fn has_not_paid( + &self, + db: &SqlitePool, + updated_by: &AllowedToEditPaymentStatusUser, + ) { + let paid = Role::find_by_name(db, "paid").await.unwrap(); + + sqlx::query!( + "DELETE FROM user_role WHERE user_id = ? and role_id = ?", + self.id, + paid.id + ) + .execute(db) + .await + .unwrap(); + + Log::create( + db, + format!("{updated_by} has set that user {self} has NOT paid the fee (yet)"), + ) + .await; + } + pub(crate) async fn has_paid( + &self, + db: &SqlitePool, + updated_by: &AllowedToEditPaymentStatusUser, + ) { + let paid = Role::find_by_name(db, "paid").await.unwrap(); + + sqlx::query!( + "INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", + self.id, + paid.id + ) + .execute(db) + .await + .expect("paid role has no group"); + + Log::create( + db, + format!("{updated_by} has set that user {self} has paid the fee (yet)"), + ) + .await; + } + + pub(crate) async fn add_role( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + role: &Role, + ) -> Result<(), String> { + if self.has_role(db, &role.name).await { + return Err(format!("Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat")); + } + + sqlx::query!( + "INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", + self.id, + role.id + ) + .execute(db) + .await + .map_err(|_| { + format!( + "User already has a role in the cluster '{}'", + role.cluster + .clone() + .expect("db trigger can't activate on empty string") + ) + })?; + + Log::create( + db, + format!("{updated_by} has added role {role} to user {self}"), + ) + .await; + + Ok(()) + } + + pub(crate) async fn add_membership_pdf( + &self, + db: &SqlitePool, + updated_by: &ManageUserUser, + membership_pdf: &TempFile<'_>, + ) -> Result<(), String> { + if self.has_membership_pdf(db).await { + return Err(format!("User {self} hat bereits eine Beitrittserklärung.")); + } + + 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) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + + Log::create( + db, + format!("{updated_by} has added the membership pdf for user {self}"), + ) + .await; + + Ok(()) + } +} diff --git a/src/model/user/member.rs b/src/model/user/member.rs new file mode 100644 index 0000000..08e9840 --- /dev/null +++ b/src/model/user/member.rs @@ -0,0 +1,44 @@ +use super::ScheckbuchUser; +use crate::model::{ + logbook::{Logbook, LogbookWithBoatAndRowers}, + user::User, +}; +use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; + +#[derive(Serialize, Deserialize)] +pub(crate) enum Member { + SchnupperInterest(User), + Schnupperant(User), + Scheckbuch(Vec), + Regular(User), + Foerdernd(User), + Unterstuetzend(User), +} + +impl Member { + pub(crate) async fn from(db: &SqlitePool, user: User) -> Self { + if ScheckbuchUser::new(db, &user).await.is_some() { + Self::Scheckbuch(Logbook::completed_with_user(db, &user).await) + } else if user.has_role(db, "schnupper-interessierte").await { + Self::SchnupperInterest(user) + } else if user.has_role(db, "schnupperant").await { + Self::Schnupperant(user) + } else if user.has_role(db, "Donau Linz").await { + Self::Regular(user) + } else if user.has_role(db, "Förderndes Mitglied").await { + Self::Foerdernd(user) + } else if user.has_role(db, "Unterstützend").await { + Self::Unterstuetzend(user) + } else { + panic!("User {user} has no membership_type!!"); + } + } + + pub(crate) fn is_club_member(&self) -> bool { + match self { + Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_) => true, + _ => false, + } + } +} diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs index 707143f..8ca32b8 100644 --- a/src/model/user/mod.rs +++ b/src/model/user/mod.rs @@ -1,4 +1,7 @@ -use std::ops::{Deref, DerefMut}; +use std::{ + fmt::Display, + ops::{Deref, DerefMut}, +}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use chrono::{Datelike, Local, NaiveDate}; @@ -29,7 +32,9 @@ use super::{ use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD}; use scheckbuch::ScheckbuchUser; +mod basic; mod fee; +pub(crate) mod member; mod scheckbuch; #[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] @@ -53,6 +58,12 @@ pub struct User { pub user_token: String, } +impl Display for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct UserWithDetails { #[serde(flatten)] @@ -113,7 +124,7 @@ impl User { .await?; } else 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 { + } else if let Some(scheckbuch) = ScheckbuchUser::new(db, self).await { scheckbuch.notify(db, mail, smtp_pw).await?; } else { return Err(format!( @@ -262,7 +273,7 @@ ASKÖ Ruderverein Donau Linz", self.name), } pub async fn allowed_to_update_always_show_trip(&self, db: &SqlitePool) -> bool { - AllowedToUpdateTripToAlwaysBeShownUser::new(db, &self) + AllowedToUpdateTripToAlwaysBeShownUser::new(db, self) .await .is_some() } @@ -304,7 +315,7 @@ ASKÖ Ruderverein Donau Linz", self.name), pub async fn real_roles(&self, db: &SqlitePool) -> Vec { sqlx::query_as!( Role, - "SELECT r.id, r.name, r.cluster + "SELECT r.id, r.name, r.cluster, r.formatted_name, r.desc, r.hide_in_lists FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id @@ -585,26 +596,6 @@ ORDER BY last_access DESC Ok(()) } - pub async fn add_role(&self, db: &SqlitePool, role: &Role) -> Result<(), String> { - sqlx::query!( - "INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", - self.id, - role.id - ) - .execute(db) - .await - .map_err(|_| { - format!( - "User already has a role in the cluster '{}'", - role.cluster - .clone() - .expect("db trigger can't activate on empty string") - ) - })?; - - Ok(()) - } - async fn send_end_mail_scheckbuch( &self, db: &mut Transaction<'_, Sqlite>, @@ -658,17 +649,6 @@ ASKÖ Ruderverein Donau Linz", self.name), Ok(()) } - pub async fn remove_role(&self, db: &SqlitePool, role: &Role) { - sqlx::query!( - "DELETE FROM user_role WHERE user_id = ? and role_id = ?", - self.id, - role.id - ) - .execute(db) - .await - .unwrap(); - } - pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result { let name = name.trim().to_lowercase(); // just to make sure... let Some(user) = User::find_by_name(db, &name).await else { @@ -954,7 +934,7 @@ impl<'r> FromRequest<'r> for User { #[macro_export] macro_rules! special_user { ($name:ident, $($role:tt)*) => { - #[derive(Debug)] + #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct $name { pub(crate) user: User, } @@ -1000,6 +980,12 @@ macro_rules! special_user { } } } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } + } }; (@check_roles $user:ident, $db:ident, $(+$role:expr),* $(,-$neg_role:expr)*) => { { @@ -1036,7 +1022,8 @@ pub struct UserWithRolesAndMembershipPdf { #[serde(flatten)] pub user: User, pub membership_pdf: bool, - pub roles: Vec, + pub roles: Vec, // TODO: remove + pub proper_roles: Vec, } impl UserWithRolesAndMembershipPdf { @@ -1045,6 +1032,7 @@ impl UserWithRolesAndMembershipPdf { Self { roles: user.roles(db).await, + proper_roles: user.real_roles(db).await, user, membership_pdf, } diff --git a/src/model/user/scheckbuch.rs b/src/model/user/scheckbuch.rs index 3fc04fe..2521eef 100644 --- a/src/model/user/scheckbuch.rs +++ b/src/model/user/scheckbuch.rs @@ -1,7 +1,7 @@ use super::User; use crate::model::user::LoginError; use crate::{ - model::{mail::Mail, notification::Notification, role::Role}, + model::{mail::Mail, notification::Notification}, special_user, SCHECKBUCH, }; use rocket::async_trait; @@ -16,24 +16,24 @@ use std::ops::Deref; special_user!(ScheckbuchUser, +"scheckbuch"); impl ScheckbuchUser { - async fn from(user: User, db: &SqlitePool, mail: &str, smtp_pw: &str) -> Result<(), String> { - // TODO: see when/how to invoke this function (explicit `Neue Person hinzufügen` button? - // Button to transition existing users to scheckbuch? Automatically called when - // `scheckbuch` is newly selected as role? - if user.has_role(db, "scheckbuch").await { - return Err("User is already a scheckbuch".into()); - } + //async fn from(user: User, db: &SqlitePool, mail: &str, smtp_pw: &str) -> Result<(), String> { + // // TODO: see when/how to invoke this function (explicit `Neue Person hinzufügen` button? + // // Button to transition existing users to scheckbuch? Automatically called when + // // `scheckbuch` is newly selected as role? + // if user.has_role(db, "scheckbuch").await { + // return Err("User is already a scheckbuch".into()); + // } - // TODO: do we allow e.g. DonauLinz to scheckbuch? + // // TODO: do we allow e.g. DonauLinz to scheckbuch? - let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap(); - user.add_role(db, &scheckbuch).await.unwrap(); + // let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap(); + // user.add_role(db, &scheckbuch).await.unwrap(); - // TODO: remove all other `membership_type` roles - let new_user = Self::new(db, &user).await.unwrap(); + // // TODO: remove all other `membership_type` roles + // let new_user = Self::new(db, &user).await.unwrap(); - new_user.notify(db, mail, smtp_pw).await - } + // new_user.notify(db, mail, smtp_pw).await + //} pub(crate) async fn notify( &self, diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index fe4e607..2f4f3ce 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -7,14 +7,14 @@ use crate::{ logbook::Logbook, role::Role, user::{ - AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, SchnupperBetreuerUser, User, + member::Member, AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser, }, }, tera::Config, }; +use chrono::NaiveDate; use futures::future::join_all; -use lettre::Address; use rocket::{ form::Form, fs::TempFile, @@ -112,6 +112,48 @@ async fn index_admin( Template::render("admin/user/index", context.into_json()) } +#[get("/user/")] +async fn view( + db: &State, + admin: VorstandUser, + flash: Option>, + user: i32, +) -> Result> { + let Some(user) = User::find_by_id(db, user).await else { + return Err(Flash::error( + Redirect::to("/admin/user"), + format!("User mit ID {} gibts ned", user), + )); + }; + + let member = Member::from(db, user.clone()).await; + + let user = UserWithRolesAndMembershipPdf::from_user(db, user).await; + + let admin: User = admin.into_inner(); + let allowed_to_edit = ManageUserUser::new(db, &admin).await.is_some(); + + let roles = Role::all(db).await; + let families = Family::all_with_members(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("allowed_to_edit", &allowed_to_edit); + context.insert("user", &user); + context.insert("is_clubmember", &member.is_club_member()); + context.insert("member", &member); + context.insert("roles", &roles); + context.insert("families", &families); + context.insert( + "loggedin_user", + &UserWithDetails::from_user(admin, db).await, + ); + + Ok(Template::render("admin/user/view", context.into_json())) +} + #[get("/user/fees")] async fn fees( db: &State, @@ -184,28 +226,9 @@ async fn fees_paid( let user = User::find_by_id(db, user_id).await.unwrap(); res.push_str(&format!("{} + ", user.name)); if user.has_role(db, "paid").await { - Log::create( - db, - format!( - "{} set fees NOT paid for '{}'", - calling_user.user.name, user.name - ), - ) - .await; - user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap()) - .await; + user.has_not_paid(db, &calling_user).await; } else { - Log::create( - db, - format!( - "{} set fees paid for '{}'", - calling_user.user.name, user.name - ), - ) - .await; - user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap()) - .await - .expect("paid role has no group"); + user.has_paid(db, &calling_user).await; } } @@ -316,6 +339,328 @@ async fn update( } } +#[derive(FromForm, Debug)] +pub struct MailUpdateForm { + mail: String, +} + +#[post("/user//change-mail", data = "")] +async fn update_mail( + 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.update_mail(db, &admin, &data.mail).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Mailadresse erfolgreich geändert", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), + } +} + +#[derive(FromForm, Debug)] +pub struct PhoneUpdateForm { + phone: String, +} + +#[post("/user//change-phone", data = "")] +async fn update_phone( + 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.update_phone(db, &admin, &data.phone).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Telefonnummer erfolgreich geändert", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), + } +} + +#[derive(FromForm, Debug)] +pub struct AddressUpdateForm { + address: String, +} + +#[post("/user//change-address", data = "")] +async fn update_address( + 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.update_address(db, &admin, &data.address).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Adresse erfolgreich geändert", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), + } +} + +#[derive(FromForm, Debug)] +pub struct FamilyUpdateForm { + family_id: Option, +} + +#[post("/user//change-family", data = "")] +async fn update_family( + 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 family = match data.family_id { + Some(-1) => Some( + Family::find_by_id(db, Family::insert(db).await) + .await + .unwrap(), + ), + Some(id) => match Family::find_by_id(db, id).await { + Some(f) => Some(f), + None => { + return Flash::error( + Redirect::to("/admin/user/{id}"), + format!("Family with ID {} does not exist!", id), + ); + } + }, + None => None, + }; + + user.update_family(db, &admin, family).await; + + Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Familie erfolgreich geändert", + ) +} + +#[derive(FromForm, Debug)] +pub struct AddMembershipPDFForm<'a> { + membership_pdf: TempFile<'a>, +} + +#[post("/user//add-membership-pdf", data = "")] +async fn add_membership_pdf( + 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_membership_pdf(db, &admin, &data.membership_pdf) + .await + { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Beitrittserklärung erfolgreich hinzugefügt", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), + } +} + +#[derive(FromForm, Debug)] +pub struct NicknameUpdateForm { + nickname: String, +} + +#[post("/user//change-nickname", data = "")] +async fn update_nickname( + 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.update_nickname(db, &admin, &data.nickname).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Spitzname erfolgreich geändert", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), + } +} + +#[derive(FromForm, Debug)] +pub struct MemberSinceUpdateForm { + member_since: String, +} + +#[post("/user//change-member-since", data = "")] +async fn update_member_since( + 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 Ok(new_member_since_date) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") + else { + return Flash::error( + Redirect::to("/admin/user/{id}"), + format!( + "Datum {} ist nicht im YYYY-MM-DD Format", + &data.member_since + ), + ); + }; + + user.update_member_since(db, &admin, &new_member_since_date) + .await; + + Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Beitrittsdatum erfolgreich geändert", + ) +} + +#[derive(FromForm, Debug)] +pub struct BirthdateUpdateForm { + birthdate: String, +} + +#[post("/user//change-birthdate", data = "")] +async fn update_birthdate( + 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 Ok(new_birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else { + return Flash::error( + Redirect::to("/admin/user/{id}"), + format!("Datum {} ist nicht im YYYY-MM-DD Format", &data.birthdate), + ); + }; + + user.update_birthdate(db, &admin, &new_birthdate).await; + + Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Geburtstag erfolgreich geändert", + ) +} + +#[derive(FromForm, Debug)] +pub struct AddRoleForm { + role_id: i32, +} + +#[post("/user//add-role", data = "")] +async fn add_role( + 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 Some(role) = Role::find_by_id(db, data.role_id).await else { + return Flash::error( + Redirect::to("/admin/user/{user_id}"), + format!("Role with ID {} does not exist!", data.role_id), + ); + }; + + match user.add_role(db, &admin, &role).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Rolle erfolgreich hinzugefügt", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), + } +} + +#[get("/user//remove-role/")] +async fn remove_role( + db: &State, + admin: ManageUserUser, + user_id: i32, + role_id: i32, +) -> Flash { + let Some(user) = User::find_by_id(db, user_id).await else { + return Flash::error( + Redirect::to("/admin/user"), + format!("User with ID {} does not exist!", user_id), + ); + }; + let Some(role) = Role::find_by_id(db, role_id).await else { + return Flash::error( + Redirect::to("/admin/user/{user_id}"), + format!("Role with ID {} does not exist!", role_id), + ); + }; + + match user.remove_role(db, &admin, &role).await { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", user.id)), + "Rolle erfolgreich gelöscht", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), + } +} + #[get("/user//membership")] async fn download_membership_pdf( db: &State, @@ -368,117 +713,129 @@ struct UserAddScheckbuchForm<'r> { mail: &'r str, } -#[post("/user/new/scheckbuch", data = "")] -async fn create_scheckbuch( - db: &State, - data: Form>, - admin: VorstandUser, - config: &State, -) -> Flash { - // 1. Check mail adress - let mail = data.mail.trim(); - if mail.parse::
().is_err() { - return Flash::error( - Redirect::to("/admin/user/scheckbuch"), - "Keine gültige Mailadresse".to_string(), - ); - } +//#[post("/user/new/scheckbuch", data = "")] +//async fn create_scheckbuch( +// db: &State, +// data: Form>, +// admin: VorstandUser, +// config: &State, +//) -> Flash { +// // 1. Check mail adress +// let mail = data.mail.trim(); +// if mail.parse::
().is_err() { +// return Flash::error( +// Redirect::to("/admin/user/scheckbuch"), +// "Keine gültige Mailadresse".to_string(), +// ); +// } +// +// // 2. Check name +// let name = data.name.trim(); +// if User::find_by_name(db, name).await.is_some() { +// return Flash::error( +// Redirect::to("/admin/user/scheckbuch"), +// "Kann kein Scheckbuch erstellen, der Name wird bereits von einem User verwendet" +// .to_string(), +// ); +// } +// +// // 3. Create user +// User::create_with_mail(db, name, mail).await; +// let user = User::find_by_name(db, name).await.unwrap(); +// +// // 4. Add 'scheckbuch' role +// let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap(); +// user.add_role(db, &scheckbuch) +// .await +// .expect("new user has no roles yet"); +// +// // 4. Send welcome mail (+ notification) +// user.send_welcome_email(db, &config.smtp_pw).await.unwrap(); +// +// Log::create( +// db, +// format!("{} created new scheckbuch: {data:?}", admin.name), +// ) +// .await; +// Flash::success(Redirect::to("/admin/user/scheckbuch"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt.")) +//} - // 2. Check name - let name = data.name.trim(); - if User::find_by_name(db, name).await.is_some() { - return Flash::error( - Redirect::to("/admin/user/scheckbuch"), - "Kann kein Scheckbuch erstellen, der Name wird bereits von einem User verwendet" - .to_string(), - ); - } - - // 3. Create user - User::create_with_mail(db, name, mail).await; - let user = User::find_by_name(db, name).await.unwrap(); - - // 4. Add 'scheckbuch' role - let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap(); - user.add_role(db, &scheckbuch) - .await - .expect("new user has no roles yet"); - - // 4. Send welcome mail (+ notification) - user.send_welcome_email(db, &config.smtp_pw).await.unwrap(); - - Log::create( - db, - format!("{} created new scheckbuch: {data:?}", admin.name), - ) - .await; - 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())) -} +//#[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())) +//} pub fn routes() -> Vec { routes![ index, index_admin, + view, resetpw, update, create, - create_scheckbuch, - schnupper_to_scheckbuch, + //create_scheckbuch, + //schnupper_to_scheckbuch, delete, fees, fees_paid, scheckbuch, download_membership_pdf, - send_welcome_mail + send_welcome_mail, + // + update_mail, + update_phone, + update_nickname, + update_member_since, + update_birthdate, + update_address, + update_family, + add_membership_pdf, + add_role, + remove_role, ] } diff --git a/src/tera/ergo.rs b/src/tera/ergo.rs index 31b3d0d..68f7588 100644 --- a/src/tera/ergo.rs +++ b/src/tera/ergo.rs @@ -1,6 +1,6 @@ use std::env; -use chrono::{Datelike, Utc}; +use chrono::Utc; use rocket::{ form::Form, fs::TempFile, @@ -145,47 +145,47 @@ pub struct UserAdd { sex: String, } -#[post("/set-data", data = "")] -async fn new_user(db: &State, data: Form, user: User) -> Flash { - if user.has_role(db, "ergo").await { - return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei it@rudernlinz.at"); - } - - // check data - if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 { - return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr..."); - } - if data.weight < 20 || data.weight > 200 { - return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht..."); - } - if &data.sex != "f" && &data.sex != "m" { - return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht..."); - } - - // set data - user.update_ergo(db, data.birthyear, data.weight, &data.sex) - .await; - - // inform all other `ergo` users - let ergo = Role::find_by_name(db, "ergo").await.unwrap(); - Notification::create_for_role( - db, - &ergo, - &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name), - "Ergo Challenge", - None, - None, - ) - .await; - - // add to `ergo` group - user.add_role(db, &ergo).await.unwrap(); - - Flash::success( - Redirect::to("/ergo"), - "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)", - ) -} +//#[post("/set-data", data = "")] +//async fn new_user(db: &State, data: Form, user: User) -> Flash { +// if user.has_role(db, "ergo").await { +// return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei it@rudernlinz.at"); +// } +// +// // check data +// if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 { +// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr..."); +// } +// if data.weight < 20 || data.weight > 200 { +// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht..."); +// } +// if &data.sex != "f" && &data.sex != "m" { +// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht..."); +// } +// +// // set data +// user.update_ergo(db, data.birthyear, data.weight, &data.sex) +// .await; +// +// // inform all other `ergo` users +// let ergo = Role::find_by_name(db, "ergo").await.unwrap(); +// Notification::create_for_role( +// db, +// &ergo, +// &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name), +// "Ergo Challenge", +// None, +// None, +// ) +// .await; +// +// // add to `ergo` group +// user.add_role(db, &ergo).await.unwrap(); +// +// Flash::success( +// Redirect::to("/ergo"), +// "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)", +// ) +//} #[derive(FromForm, Debug)] pub struct ErgoToAdd<'a> { @@ -358,7 +358,10 @@ async fn new_dozen( } pub fn routes() -> Vec { - routes![index, new_thirty, new_dozen, send, reset, update, new_user] + routes![ + index, new_thirty, new_dozen, send, reset, update, + // new_user + ] } #[cfg(test)] diff --git a/staging-diff.sql b/staging-diff.sql index 6fb21fc..199172c 100644 --- a/staging-diff.sql +++ b/staging-diff.sql @@ -3,3 +3,25 @@ INSERT INTO user(name) VALUES('Marie'); INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz')); INSERT INTO user(name) VALUES('Philipp'); INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz')); + + +ALTER TABLE role ADD COLUMN formatted_name text; +ALTER TABLE role ADD COLUMN desc text; +ALTER TABLE role ADD COLUMN hide_in_lists BOOLEAN NOT NULL DEFAULT false; +UPDATE role SET hide_in_lists=true WHERE name='paid'; +UPDATE role SET hide_in_lists=true WHERE name='ergo'; +UPDATE role SET desc='Can do ANYTHING.' WHERE name='admin'; +UPDATE role SET desc='Kann Ausfahrten ausschreiben und kann alle Boote die in Linz lagern verwenden.', formatted_name='Steuerperson' WHERE name='cox'; +UPDATE role SET desc='Darf reparierte Bootschäden verifizieren und wird über Bootsschäden informiert.', formatted_name='Bootsreparateur' WHERE name='tech'; +UPDATE role SET desc = null WHERE name='Rechnungsprüfer'; +UPDATE role SET desc='Darf Boote die in Ottensheim lagern verwenden.' WHERE name='Rennrudern'; +UPDATE role SET desc='Haben zahlreiche Berechtigungen, siehe den Vorstand-Block im Menü.' WHERE name='Vorstand'; +UPDATE role SET desc='Können Events ausschreiben und bearbeiten.', formatted_name='Eventmanager' WHERE name='manage_events'; +UPDATE role SET desc='Sieht Details zum Schnupperkurs (Teilnehmer, Bezahlstatus, ...)' WHERE name='schnupper-betreuer'; +UPDATE role SET desc=null WHERE name='kassier'; +UPDATE role SET desc=null WHERE name='schriftfuehrer'; +UPDATE role SET desc='Entfernt bei der Gebührenberechnung die Einschreibgebühr.' WHERE name='no-einschreibgebuehr'; +UPDATE role SET desc='Es können Logbucheinträge im Nachhinein hinzugefügt werden. Idealerweise diese Rolle nur kurzfristig vergeben.' WHERE name='allow-retroactive-logbookentries'; +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'; diff --git a/templates/admin/mail.html.tera b/templates/admin/mail.html.tera index c80db2e..1e2ed46 100644 --- a/templates/admin/mail.html.tera +++ b/templates/admin/mail.html.tera @@ -26,26 +26,24 @@ role="alert">

Mitglieds-Beitrags-Info

diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 69e0121..3cf66aa 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -5,28 +5,29 @@

Users

{% if allowed_to_edit %}
- Neue Person hinzufügen + + Neue Person hinzufügen +
-
-
- - + onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');" + method="post" + class="flex mt-4 rounded-md sm:flex items-end justify-between"> +
+
+ + +
-
-
- -
- +
+ +
+
- {% endif %}
@@ -36,26 +37,29 @@ id="filter-js" class="search-bar" placeholder="Suchen nach (Name, [yes|no]-role:, has-[no-]membership-pdf)" /> -
- - - - + + +
@@ -90,6 +94,8 @@ + ✏️
{{ user.name }} - Ausfahrten: {{ trips | length }}
    - {% for trip in trips %} -
  • {{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}
  • - {% endfor %} + {% for trip in trips %} +
  • {{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}
  • + {% endfor %}
{% if "admin" in loggedin_user.roles or "kassier" in loggedin_user.roles %} diff --git a/templates/admin/user/view.html.tera b/templates/admin/user/view.html.tera new file mode 100644 index 0000000..515b095 --- /dev/null +++ b/templates/admin/user/view.html.tera @@ -0,0 +1,313 @@ +{% import "includes/macros" as macros %} +{% import "includes/forms/log" as log %} +{% extends "base" %} +{% block content %} +
+

{{ user.name }}

+ + {% if is_clubmember %} + + {% endif %} + + + + +
+{% endblock content %} diff --git a/templates/ergo/final.html.tera b/templates/ergo/final.html.tera index 8dc4731..c530904 100644 --- a/templates/ergo/final.html.tera +++ b/templates/ergo/final.html.tera @@ -7,7 +7,7 @@ Dirty Thirty

-