Merge pull request 'single-user-edit-page' (#966) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m39s
CI/CD Pipeline / deploy-staging (push) Successful in 7m3s
CI/CD Pipeline / deploy-main (push) Has been skipped

Reviewed-on: #966
This commit is contained in:
philipp 2025-04-30 22:32:46 +02:00
commit 26038eabe4
24 changed files with 1521 additions and 401 deletions

View File

@ -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" (

View File

@ -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();
}
}

View File

@ -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<LogToAdd> for LogToFinalize {
}
}
#[derive(Serialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
pub struct LogbookWithBoatAndRowers {
#[serde(flatten)]
pub logbook: Logbook,

View File

@ -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::<Address>().is_err() {
return false;
}
}
true
}

View File

@ -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<String>,
pub(crate) desc: Option<String>,
pub(crate) hide_in_lists: bool,
pub(crate) cluster: Option<String>,
}
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<Role> {
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<Self> {
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 ?
",

View File

@ -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
}

337
src/model/user/basic.rs Normal file
View File

@ -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<Family>,
) {
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(())
}
}

44
src/model/user/member.rs Normal file
View File

@ -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<LogbookWithBoatAndRowers>),
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,
}
}
}

View File

@ -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<Role> {
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<Self, LoginError> {
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<String>,
pub roles: Vec<String>, // TODO: remove
pub proper_roles: Vec<Role>,
}
impl UserWithRolesAndMembershipPdf {
@ -1045,6 +1032,7 @@ impl UserWithRolesAndMembershipPdf {
Self {
roles: user.roles(db).await,
proper_roles: user.real_roles(db).await,
user,
membership_pdf,
}

View File

@ -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,

View File

@ -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/<user>")]
async fn view(
db: &State<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
user: i32,
) -> Result<Template, Flash<Redirect>> {
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<SqlitePool>,
@ -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/<id>/change-mail", data = "<data>")]
async fn update_mail(
db: &State<SqlitePool>,
data: Form<MailUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
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/<id>/change-phone", data = "<data>")]
async fn update_phone(
db: &State<SqlitePool>,
data: Form<PhoneUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
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/<id>/change-address", data = "<data>")]
async fn update_address(
db: &State<SqlitePool>,
data: Form<AddressUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
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<i64>,
}
#[post("/user/<id>/change-family", data = "<data>")]
async fn update_family(
db: &State<SqlitePool>,
data: Form<FamilyUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let 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/<id>/add-membership-pdf", data = "<data>")]
async fn add_membership_pdf(
db: &State<SqlitePool>,
data: Form<AddMembershipPDFForm<'_>>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
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/<id>/change-nickname", data = "<data>")]
async fn update_nickname(
db: &State<SqlitePool>,
data: Form<NicknameUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
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/<id>/change-member-since", data = "<data>")]
async fn update_member_since(
db: &State<SqlitePool>,
data: Form<MemberSinceUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let 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/<id>/change-birthdate", data = "<data>")]
async fn update_birthdate(
db: &State<SqlitePool>,
data: Form<BirthdateUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let 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/<id>/add-role", data = "<data>")]
async fn add_role(
db: &State<SqlitePool>,
data: Form<AddRoleForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let 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/<user_id>/remove-role/<role_id>")]
async fn remove_role(
db: &State<SqlitePool>,
admin: ManageUserUser,
user_id: i32,
role_id: i32,
) -> Flash<Redirect> {
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/<user>/membership")]
async fn download_membership_pdf(
db: &State<SqlitePool>,
@ -368,117 +713,129 @@ struct UserAddScheckbuchForm<'r> {
mail: &'r str,
}
#[post("/user/new/scheckbuch", data = "<data>")]
async fn create_scheckbuch(
db: &State<SqlitePool>,
data: Form<UserAddScheckbuchForm<'_>>,
admin: VorstandUser,
config: &State<Config>,
) -> Flash<Redirect> {
// 1. Check mail adress
let mail = data.mail.trim();
if mail.parse::<Address>().is_err() {
return Flash::error(
Redirect::to("/admin/user/scheckbuch"),
"Keine gültige Mailadresse".to_string(),
);
}
//#[post("/user/new/scheckbuch", data = "<data>")]
//async fn create_scheckbuch(
// db: &State<SqlitePool>,
// data: Form<UserAddScheckbuchForm<'_>>,
// admin: VorstandUser,
// config: &State<Config>,
//) -> Flash<Redirect> {
// // 1. Check mail adress
// let mail = data.mail.trim();
// if mail.parse::<Address>().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/<id>/to/scheckbuch")]
async fn schnupper_to_scheckbuch(
db: &State<SqlitePool>,
id: i32,
admin: SchnupperBetreuerUser,
config: &State<Config>,
) -> Flash<Redirect> {
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/<id>/to/scheckbuch")]
//async fn schnupper_to_scheckbuch(
// db: &State<SqlitePool>,
// id: i32,
// admin: SchnupperBetreuerUser,
// config: &State<Config>,
//) -> Flash<Redirect> {
// 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<Route> {
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,
]
}

View File

@ -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 = "<data>")]
async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> {
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 = "<data>")]
//async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> {
// 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<Route> {
routes![index, new_thirty, new_dozen, send, reset, update, new_user]
routes![
index, new_thirty, new_dozen, send, reset, update,
// new_user
]
}
#[cfg(test)]

View File

@ -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';

View File

@ -26,26 +26,24 @@
role="alert">
<h2 class="h2">Mitglieds-Beitrags-Info</h2>
<div class="p-3 grid gap-3">
<a class="btn btn-primary" href="/admin/mail/fee/test">
Test-Mail an mich versenden
</a>
<a class="btn btn-alert" href="/admin/mail/fee"
onclick="return confirm('Hast du die Gebührenauflistung geprüft und willst du die Mail an alle ausschicken?');">
An ALLE Mitglieder versenden
</a>
<a class="btn btn-primary" href="/admin/mail/fee/test">Test-Mail an mich versenden</a>
<a class="btn btn-alert"
href="/admin/mail/fee"
onclick="return confirm('Hast du die Gebührenauflistung geprüft und willst du die Mail an alle ausschicken?');">
An ALLE Mitglieder versenden
</a>
</div>
</div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Unfreundliche Zahlungsaufforderung</h2>
<div class="p-3 grid gap-3">
<a class="btn btn-primary" href="/admin/mail/fee-final/test">
Test-Mail an mich versenden
</a>
<a class="btn btn-alert" href="/admin/mail/fee-final"
onclick="return confirm('Hast du die Gebührenauflistung geprüft, gecheckt ob alle die bereits bezahlt haben auch eingetragen wurden und willst du die Mail an alle, die noch nicht bezahlt haben ausschicken?');">
An ALLE Mitglieder versenden, die noch nicht bezahlt haben
</a>
<a class="btn btn-primary" href="/admin/mail/fee-final/test">Test-Mail an mich versenden</a>
<a class="btn btn-alert"
href="/admin/mail/fee-final"
onclick="return confirm('Hast du die Gebührenauflistung geprüft, gecheckt ob alle die bereits bezahlt haben auch eingetragen wurden und willst du die Mail an alle, die noch nicht bezahlt haben ausschicken?');">
An ALLE Mitglieder versenden, die noch nicht bezahlt haben
</a>
</div>
</div>
</div>

View File

@ -5,28 +5,29 @@
<h1 class="h1">Users</h1>
{% if allowed_to_edit %}
<details class="mt-5 bg-gray-200 dark:bg-primary-600 p-3 rounded-md">
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">Neue Person hinzufügen</summary>
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">
Neue Person hinzufügen
</summary>
<form action="/admin/user/new"
onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
method="post"
class="flex mt-4 rounded-md sm:flex items-end justify-between">
<div class="w-full">
<div>
<label for="name" class="sr-only">Name</label>
<input type="text"
name="name"
class="input rounded-md w-100"
placeholder="Name" />
onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
method="post"
class="flex mt-4 rounded-md sm:flex items-end justify-between">
<div class="w-full">
<div>
<label for="name" class="sr-only">Name</label>
<input type="text"
name="name"
class="input rounded-md w-100"
placeholder="Name" />
</div>
</div>
</div>
<div class="text-right ml-3">
<input value="Hinzufügen"
type="submit"
class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
</div>
</form>
<div class="text-right ml-3">
<input value="Hinzufügen"
type="submit"
class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
</div>
</form>
</details>
{% endif %}
<!-- START filterBar -->
<div class="search-wrapper flex">
@ -36,26 +37,29 @@
id="filter-js"
class="search-bar"
placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
<div class="relative">
<button id="dropdownbtn" data-dropdown="dropdown" class="btn btn-dark ml-3" type="button">
Sortieren
</button>
<!-- Dropdown menu -->
<div id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-100 text-secondary-900 rounded-lg shadow-sm w-44 absolute right-0">
<ul class="py-2 text-sm" aria-labelledby="dropdownbtn">
<li>
<a href="./user" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Zuletzt eingeloggt</a>
</li>
<li>
<a href="?sort=name&asc" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name A-Z</a>
</li>
<li>
<a href="?sort=name" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
</li>
</ul>
</div>
<button id="dropdownbtn"
data-dropdown="dropdown"
class="btn btn-dark ml-3"
type="button">Sortieren</button>
<!-- Dropdown menu -->
<div id="dropdown"
class="z-10 hidden bg-white divide-y divide-gray-100 text-secondary-900 rounded-lg shadow-sm w-44 absolute right-0">
<ul class="py-2 text-sm" aria-labelledby="dropdownbtn">
<li>
<a href="./user"
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Zuletzt eingeloggt</a>
</li>
<li>
<a href="?sort=name&asc"
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name A-Z</a>
</li>
<li>
<a href="?sort=name"
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
</li>
</ul>
</div>
</div>
</div>
<!-- END filterBar -->
@ -90,6 +94,8 @@
</small>
</span>
</summary>
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}">✏️</a>
<form action="/admin/user"
method="post"
enctype="multipart/form-data"

View File

@ -56,9 +56,9 @@
<div style="width: 100%" class="col-span-2">
<b>{{ user.name }} - Ausfahrten: {{ trips | length }}</b>
<ul class="list-disc ms-4">
{% for trip in trips %}
<li>{{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}</li>
{% endfor %}
{% for trip in trips %}
<li>{{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}</li>
{% endfor %}
</ul>
</div>
{% if "admin" in loggedin_user.roles or "kassier" in loggedin_user.roles %}

View File

@ -0,0 +1,313 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/log" as log %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">{{ user.name }}</h1>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Grunddaten</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
{% if user.last_access %}
Zuletzt eingeloggt am {{ user.last_access | date(format="%d. %m. %Y") }}
{% else %}
{{ user.name }} hat sich noch nie eingeloggt.
{% endif %}
</div>
<div class="py-3">
<ul>
<li>
Mail: {{ user.mail }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-mail" method="post">
{{ macros::input(label='Neue Mailadresse', name='mail', type="text", value=user.mail) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
<li>Notizen: to be replaced with activity :-)</li>
<li>
Telefon: {{ user.phone }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-phone" method="post">
{{ macros::input(label='Neue Telefonnummer', name='phone', type="text", value=user.phone) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
<li>
Spitzname: {{ user.nickname }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-nickname" method="post">
{{ macros::input(label='Neuer Spitzname', name='nickname', type="text", value=user.nickname) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
</ul>
</div>
<div class="py-3">
Rollen:
<ul class="list-disc ms-4">
{% for role in user.proper_roles -%}
{% if not role.cluster and not role.hide_in_lists %}
<li>
<strong>
{% if role.formatted_name %}
{{ role.formatted_name }}
{% else %}
{{ role.name }}
{% endif %}
</strong> {{ role.desc }}
{% if allowed_to_edit %}
<a href="/admin/user/{{ user.id }}/remove-role/{{ role.id }}"
onclick="return confirm('Willst du die Rolle \'{{ role.name }}\' von {{ user.name }} wirklich entfernen?');">🗑️</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
{% if allowed_to_edit %}
<details>
<summary>+ Rolle</summary>
<form action="/admin/user/{{ user.id }}/add-role" method="post">
<fieldset>
<select name="role_id">
{% for role in roles %}
{% if not role.cluster and role not in user.proper_roles and not role.hide_in_lists %}
<option value="{{ role.id }}">{% if role.formatted_name %}
{{ role.formatted_name }}
{% else %}
{{ role.name }}
{% endif %} {% if role.desc %} ({{ role.desc }}) {% endif %}</option>
{% endif %}
{% endfor %}
</select>
<input value="Rolle hinzufügen" type="submit" class="btn btn-primary ml-1" />
</fieldset>
</form>
</details>
{% endif %}
</div>
</div>
</div>
{% if is_clubmember %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Vereinsmitglied</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
<ul class="list-disc ms-4">
<li>
Mitglied seit: {{ user.member_since_date }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-member-since" method="post">
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=user.member_since_date) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
<li>
Geburtsdatum: {{ user.birthdate }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-birthdate" method="post">
{{ macros::input(label='Geburtstag', name='birthdate', type="date", value=user.birthdate) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
<li>
Adresse: {{ user.address }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-address" method="post">
{{ macros::input(label='Neue Adresse', name='address', type="text", value=user.address) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
<li>
Familie:
{% for family in families %}
{% if user.family_id == family.id %}{{ family.names }}{% endif %}
{% endfor %}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-family" method="post">
{{ 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') }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
</ul>
</div>
<div class="py-3">
{% if user.membership_pdf %}
<a href="/admin/user/{{ user.id }}/membership"
class="text-black dark:text-white">Beitrittserklärung</a>
{% else %}
⚠️ Aktuell gibt's keine Beitrittserklärung 😢
{% if allowed_to_edit %}
Das kannst du hier ändern ⤵️
<form action="/admin/user/{{ user.id }}/add-membership-pdf" method="post">
<fieldset>
{{ macros::input(label='Neue Beitrittserklärung hochladen', name='membership_pdf', type="file", accept='application/pdf') }}
<input value="Hochladen" type="submit" class="btn btn-primary ml-1" />
</fieldset>
</form>
{% endif %}
{% endif %}
</div>
{% if allowed_to_edit %}
<div class="py-3">
<div class="mt-3 text-right">
<a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert"
onclick="return confirm('Ist {{ user.name }} wirklich aus dem Verein ausgetreten?');">
{% include "includes/delete-icon" %}
Mitglied ist ausgetreten
</a>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Mitgliedstyp</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
{{ user.name }}
{% if "SchnupperInterest" in member %}
ist interessiert am Schnupperkurs.
{% elif "Schnupperant" in member %}
ist beim nächsten Schnupperkurs angemeldet.
{% elif "Scheckbuch" in member %}
{% set logbook = member["Scheckbuch"] %}
hat ein Scheckbuch und {{ logbook | length }} Ausfahrten absolviert.
<details>
<summary>Ausfahrten</summary>
{% for log in logbook %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=false) }}
{% endfor %}
</details>
{% elif "Regular" in member %}
ist ein reguläres Vereinsmitglied.
{% elif "Foerdernd" in member %}
ist ein förderndes Vereinsmitglied.
{% elif "Unterstuetzend" in member %}
ist ein unterstützendes Vereinsmitglied.
{% endif %}
</div>
</div>
</div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Aktivität von und mit {{ user.name }}</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
<ul class="list-disc ms-4">
<li>Passwort zurückgesetzt am/um X</li>
<li>Am X beigetreten.</li>
</ul>
</div>
</div>
</div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">TODO</h2>
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
<span class="text-black dark:text-white cursor-pointer">
<span class="font-bold">
{{ user.name }}
{% if not user.last_access and allowed_to_edit and user.mail %}
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
class="inline">
&bullet; <a class="font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/send-welcome-mail"
onclick="return confirm('Willst du wirklich das Willkommensmail an {{ user.name }} ausschicken?');">Willkommensmail verschicken</a>
</form>
{% endif %}
</span>
</span>
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
class="w-full mt-2">
{% if user.pw %}
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/reset-pw"
onclick="return confirm('Willst du wirklich das Passwort zurücksetzen?');">Passwort zurücksetzen</a>
{% endif %}
<div class="w-full grid gap-3 mt-3">
<input type="hidden" name="id" value="{{ user.id }}" />
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{% for cluster, cluster_roles in roles | group_by(attribute="cluster") %}
<label for="cluster_{{ loop.index }}">{{ cluster }}</label>
{# Determine the initially selected role within the cluster #}
{% set_global selected_role_id = "none" %}
{% for role in cluster_roles %}
{% if selected_role_id == "none" and role.name in user.roles %}
{% set_global selected_role_id = role.id %}
{% endif %}
{% endfor %}
{# Set default name to the selected role ID or first role if none selected #}
<select id="cluster_{{ loop.index }}"
{% if selected_role_id == 'none' %} {% else %} name="roles[{{ selected_role_id }}]" {% endif %}
{% if allowed_to_edit == false %}disabled{% endif %}
onchange=" if (this.value === '') { this.removeAttribute('name'); } else { this.name = 'roles[' + this.options[this.selectedIndex].getAttribute('data-role-id') + ']'; }">
<option value=""
data-role-id="none"
{% if selected_role_id == 'none' %}selected="selected"{% endif %}>
None
</option>
{% for role in cluster_roles %}
<option value="on"
data-role-id="{{ role.id }}"
{% if role.id == selected_role_id %}selected="selected"{% endif %}>
{{ role.name }}
</option>
{% endfor %}
</select>
{% endfor %}
</div>
</div>
</form>
</div>
</div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Ergo-Challenge</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
{{ macros::input(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }}
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -7,7 +7,7 @@
<summary>Dirty Thirty</summary>
<p>
<div class="border-r border-l">
<textarea style="width: 100%; height: 300px;">
<textarea style="width: 100%; height: 300px;">
{%- for stat in thirty %}
{%- set names = stat.name | split(pat=" ") %}{% set lastname_index = names | length - 1 %}{% set lastname = names[lastname_index] %}{{ lastname }}&#9;
{%- for name in names -%}
@ -23,7 +23,7 @@
<summary>Dirty Dozen</summary>
<p>
<div class="border-r border-l">
<textarea style="width: 100%; height: 300px;">
<textarea style="width: 100%; height: 300px;">
{%- for stat in dozen -%}
{%- set names = stat.name | split(pat=" ") %}{% set lastname_index = names | length - 1 %}{% set lastname = names[lastname_index] %}{{ lastname }}&#9;
{%- for name in names -%}

View File

@ -21,8 +21,8 @@
</li>
<li class="py-1">
<a href="https://data.ergochallenge.at"
target="_blank"
style="text-decoration: underline">Offizielle Ergebnisse</a>, bei Fehlern direkt mit <a href="mailto:office@ergochallenge.at"
target="_blank"
style="text-decoration: underline">Offizielle Ergebnisse</a>, bei Fehlern direkt mit <a href="mailto:office@ergochallenge.at"
style="text-decoration: underline">Christian (Ister)</a> Kontakt aufnehmen
</li>
</ul>
@ -75,8 +75,8 @@
<div class="pt-3">
<p>
Folgende Daten hat der Ruderassistent von dir. Wenn diese nicht mehr aktuell sind, bitte gewünschte Änderungen an unseren Schriftführer melden (<a href="mailto:info@rudernlinz.at"
class="text-primary-600 dark:text-primary-200 hover:text-primary-950 hover:dark:text-primary-300 underline"
target="_blank">it@rudernlinz.at</a>).
class="text-primary-600 dark:text-primary-200 hover:text-primary-950 hover:dark:text-primary-300 underline"
target="_blank">it@rudernlinz.at</a>).
<br />
<br />
<ul>

View File

@ -6,11 +6,11 @@
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='allow_guests') }}
{{ macros::input(label='Anmerkungen', name='notes', type='input') }}
{% if loggedin_user.allowed_to_steer %}
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
{% else %}
{{ macros::select(label='Typ', data=trip_types, name='trip_type', only_ergo=true) }}
{% endif %}
{% if loggedin_user.allowed_to_steer %}
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
{% else %}
{{ macros::select(label='Typ', data=trip_types, name='trip_type', only_ergo=true) }}
{% endif %}
<input value="Erstellen" class="w-full btn btn-primary" type="submit" />
</form>
</div>

View File

@ -82,7 +82,13 @@
<label for="{{ id }}" class="text-sm text-gray-600 dark:text-gray-100">
Ruderer (inkl. Schiffsführer und Steuerperson)
</label>
<select style="width: 100%;" multiple name="rowers[]" id="{{ id }}" class="w-full" data-seats="{{ amount_seats }}" data-init={{ init }}>
<select style="width: 100%"
multiple
name="rowers[]"
id="{{ id }}"
class="w-full"
data-seats="{{ amount_seats }}"
data-init="{{ init }}">
{% for user in users %}
{% set_global sel = false %}
{% for rower in selected %}

View File

@ -203,7 +203,14 @@ function setChoiceByLabel(choicesInstance, label) {
{% if default %}<option selected value>{{ default }}</option>{% endif %}
{% if nonSelectableDefault %}<option disabled selected value>{{ nonSelectableDefault }}</option>{% endif %}
{% for d in data %}
<option value="{{ d.id }}" {% if only_ergo and d.id!=4 %} disabled {% endif %}{% if d.id == selected_id %}selected{% endif %} {% if extras != '' %} {% for extra in extras %} {% if extra != 'on_water' and d[extra] %} data- {{ extra }}={{ d[extra] }} {% else %} {% if d[extra] %}disabled{% endif %} {% endif %} {% endfor %} {% endif %} {% if show_seats %} data-custom-properties='{"amount_seats": {{ d["amount_seats"] }}, "owner": "{{ d["owner"] }}", "default_destination": "{{ d["default_destination"] }}", "boat_in_ottensheim": {{ d["location_id"] == 2 }}, "boat_reserved_today": {{ d["reserved_today"] }}, "convert_handoperated_possible": {{ d["convert_handoperated_possible"] }}, "default_handoperated": {{ d["default_shipmaster_only_steering"] }}}' {% endif %}>
<option value="{{ d.id }}"
{% if only_ergo and d.id!=4 %}disabled{% endif %}
{% if d.id == selected_id %}selected{% endif %}
{% if extras != '' %} {% for extra in extras %} {% if extra != 'on_water' and d[extra] %} data- {{ extra }}={{ d[extra] }} {% else %} {% if d[extra] %}disabled{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% if show_seats %} data-custom-properties='{"amount_seats": {{ d["amount_seats"] }}, "owner": "{{ d["owner"] }}", "default_destination": "{{ d["default_destination"] }}", "boat_in_ottensheim": {{ d["location_id"] == 2 }}, "boat_reserved_today": {{ d["reserved_today"] }}, "convert_handoperated_possible": {{ d["convert_handoperated_possible"] }}, "default_handoperated": {{ d["default_shipmaster_only_steering"] }}}' {% endif %}>
{% for displa in display -%}
{%- if d[displa] -%}
{{- d[displa] -}}

View File

@ -32,7 +32,9 @@
<h2 class="h2">Nachrichten</h2>
{% if loggedin_user.amount_unread_notifications > 10 %}
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte in Zukunft einen kurzen Moment Zeit sie zu überprüfen und als gelesen zu markieren (&#10003;).<br /><a href="/notification/read/all" class="underline">Du kannst hier ausnahmsweise alle als gelesen markieren.</a>
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte in Zukunft einen kurzen Moment Zeit sie zu überprüfen und als gelesen zu markieren (&#10003;).
<br />
<a href="/notification/read/all" class="underline">Du kannst hier ausnahmsweise alle als gelesen markieren.</a>
</div>
{% endif %}
<div class="divide-y">
@ -234,93 +236,88 @@
role="alert">
<h2 class="h2">Vereinsmitglied</h2>
<ul class="list-none ms-2 divide-y divide-gray-200 dark:divide-primary-600">
{% if "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
<li class="py-1">
<a href="/planned" class="block w-100 py-2 hover:text-primary-600">Geplante Ausfahrten</a>
</li>
<li class="py-1">
<a href="/log" class="block w-100 py-2 hover:text-primary-600">Ausfahrt eintragen</a>
</li>
<li class="py-1">
<a href="/log/show" class="block w-100 py-2 hover:text-primary-600">Logbuch</a>
</li>
<li class="py-1">
<a href="/stat" class="block w-100 py-2 hover:text-primary-600">Statistik</a>
</li>
<li class="py-1">
<a href="/stat/boats" class="block w-100 py-2 hover:text-primary-600">Bootsauswertung</a>
</li>
<li class="py-1">
<a href="/boatdamage" class="block w-100 py-2 hover:text-primary-600">Bootsschaden</a>
</li>
<li class="py-1">
<a href="/boatreservation"
class="block w-100 py-2 hover:text-primary-600">Bootsreservierung</a>
</li>
<li class="py-1">
<a href="/trailerreservation"
class="block w-100 py-2 hover:text-primary-600">Hängerreservierung</a>
</li>
<li class="py-1">
<a href="/steering" class="block w-100 py-2 hover:text-primary-600">Steuerleute & Co</a>
</li>
<div class="py-3">
<p>
<details>
<summary>
Kalender
</summary>
<p class="mt-3">
Du möchtest immer up-to-date mit den Events und Ausfahrten bleiben? Wir bieten 3 verschiedene Arten von Kalender an:
</p>
<ol class="list-decimal ml-5 my-3">
<li>
<strong>Alle Events und Ausfahrten</strong>, zu denen du dich angemeldet hast: <a class="underline break-all"
{% if "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
<li class="py-1">
<a href="/planned" class="block w-100 py-2 hover:text-primary-600">Geplante Ausfahrten</a>
</li>
<li class="py-1">
<a href="/log" class="block w-100 py-2 hover:text-primary-600">Ausfahrt eintragen</a>
</li>
<li class="py-1">
<a href="/log/show" class="block w-100 py-2 hover:text-primary-600">Logbuch</a>
</li>
<li class="py-1">
<a href="/stat" class="block w-100 py-2 hover:text-primary-600">Statistik</a>
</li>
<li class="py-1">
<a href="/stat/boats" class="block w-100 py-2 hover:text-primary-600">Bootsauswertung</a>
</li>
<li class="py-1">
<a href="/boatdamage" class="block w-100 py-2 hover:text-primary-600">Bootsschaden</a>
</li>
<li class="py-1">
<a href="/boatreservation"
class="block w-100 py-2 hover:text-primary-600">Bootsreservierung</a>
</li>
<li class="py-1">
<a href="/trailerreservation"
class="block w-100 py-2 hover:text-primary-600">Hängerreservierung</a>
</li>
<li class="py-1">
<a href="/steering" class="block w-100 py-2 hover:text-primary-600">Steuerleute & Co</a>
</li>
<div class="py-3">
<p>
<details>
<summary>Kalender</summary>
<p class="mt-3">
Du möchtest immer up-to-date mit den Events und Ausfahrten bleiben? Wir bieten 3 verschiedene Arten von Kalender an:
</p>
<ol class="list-decimal ml-5 my-3">
<li>
<strong>Alle Events und Ausfahrten</strong>, zu denen du dich angemeldet hast: <a class="underline break-all"
href="https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}">https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}</a>
<br />
<small>Dieser Link enthält einen zufällig generierten Teil, damit nur du (und jene, denen du diesen Link weitergibst) Zugang zu diesen Daten hast.</small>
</li>
<li>
<strong>Allgemeiner Kalender</strong>, zB save-the-dates (Wanderfahrten, ...): <a href="https://rudernlinz.at/cal" class="break-all underline">https://rudernlinz.at/cal</a>
</li>
<li>
<strong>Alle Events</strong>: <a class="break-all underline" href="https://app.rudernlinz.at/cal">https://app.rudernlinz.at/cal</a>
<br />
<small>Beachte, dass dieser Kalender keine Ausfahrten enthält, die von einzelnen Steuerpersonen augeschrieben werden. Dieser Kalender wird zB auf <a href="https://rudernlinz.at/termine" class="underline">https://rudernlinz.at/termine</a> verwendet und wir möchten keine persönlichen Daten (Namen etc.) leaken.</small>
</li>
</ol>
Du kannst die Kalender einfach in deinen Kalender als "externen Kalender" synchronisieren. Die genauen Schritte hängen von deiner verwendeten Software ab.
</details>
</p>
</div>
<br />
<small>Dieser Link enthält einen zufällig generierten Teil, damit nur du (und jene, denen du diesen Link weitergibst) Zugang zu diesen Daten hast.</small>
</li>
<li>
<strong>Allgemeiner Kalender</strong>, zB save-the-dates (Wanderfahrten, ...): <a href="https://rudernlinz.at/cal" class="break-all underline">https://rudernlinz.at/cal</a>
</li>
<li>
<strong>Alle Events</strong>: <a class="break-all underline" href="https://app.rudernlinz.at/cal">https://app.rudernlinz.at/cal</a>
<br />
<small>Beachte, dass dieser Kalender keine Ausfahrten enthält, die von einzelnen Steuerpersonen augeschrieben werden. Dieser Kalender wird zB auf <a href="https://rudernlinz.at/termine" class="underline">https://rudernlinz.at/termine</a> verwendet und wir möchten keine persönlichen Daten (Namen etc.) leaken.</small>
</li>
</ol>
Du kannst die Kalender einfach in deinen Kalender als "externen Kalender" synchronisieren. Die genauen Schritte hängen von deiner verwendeten Software ab.
</details>
</p>
</div>
<div class="py-3">
<p>
<details>
<summary>Signal-Gruppenchat Donau Linz</summary>
<p class="mt-3">
Mit diesem Link kannst du unserer Signal Gruppe beitreten: <a class="break-all underline"
href="https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH">https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH</a>
</p>
</details>
</p>
</div>
{% endif %}
<div class="py-3">
<p>
<details>
<summary>
Signal-Gruppenchat Donau Linz
</summary>
<summary>WLAN-Passwort</summary>
<p class="mt-3">
Mit diesem Link kannst du unserer Signal Gruppe beitreten: <a class="break-all underline" href="https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH">https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH</a>
</p>
</details>
</p>
</div>
{% endif %}
<div class="py-3">
<p>
<details>
<summary>
WLAN-Passwort
</summary>
<p class="mt-3">
Das Passwort für unser "ASKÖ Ruderverein Donau Linz" WLAN ist <q>donau1921</q> (ohne Anführungszeichen). Bitte an keine vereinsfremden Personen weitergeben.
Das Passwort für unser "ASKÖ Ruderverein Donau Linz" WLAN ist <q>donau1921</q> (ohne Anführungszeichen). Bitte an keine vereinsfremden Personen weitergeben.
</p>
</details>
</p>
</div>
</ul>
</div>
{% endif %}
{% endif %}
{% if "cox" in loggedin_user.roles or "Bootsführer" in loggedin_user.roles %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
@ -329,11 +326,10 @@
<div class="py-3">
<p>
<details>
<summary>
Signal-Gruppenchat Steuerpersonen Donau Linz
</summary>
<summary>Signal-Gruppenchat Steuerpersonen Donau Linz</summary>
<p class="mt-3">
Mit diesem Link kannst du unserer Signal Gruppe beitreten: <a class="break-all underline" href="https://signal.group/#CjQKIHJInNb3zSVW7ipLo7_ygIqVxhxUaaNYx4sy2jdklLsIEhBHJNM2KZM1UnBdQxWy_Gdp">https://signal.group/#CjQKIHJInNb3zSVW7ipLo7_ygIqVxhxUaaNYx4sy2jdklLsIEhBHJNM2KZM1UnBdQxWy_Gdp</a>
Mit diesem Link kannst du unserer Signal Gruppe beitreten: <a class="break-all underline"
href="https://signal.group/#CjQKIHJInNb3zSVW7ipLo7_ygIqVxhxUaaNYx4sy2jdklLsIEhBHJNM2KZM1UnBdQxWy_Gdp">https://signal.group/#CjQKIHJInNb3zSVW7ipLo7_ygIqVxhxUaaNYx4sy2jdklLsIEhBHJNM2KZM1UnBdQxWy_Gdp</a>
</p>
</details>
</p>

View File

@ -67,7 +67,7 @@
{% endif %}
{% endfor %}
{% endif %}
<div id="{{ day.day| date(format="%Y-%m-%d") }}"
<div id="{{ day.day| date(format='%Y-%m-%d') }}"
class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js"
style="min-height: 10rem"
data-trips="{{ amount_trips }}"
@ -93,20 +93,22 @@
<div class="grid grid-cols-1 gap-3 mb-3">
{# --- START Boatreservations--- #}
{% for _, reservations_for_event in day.boat_reservations %}
{% set reservation = reservations_for_event[0] %}
{% set reservation = reservations_for_event[0] %}
<div class="pt-2 px-3 border-gray-200">
<div class="flex justify-between items-center">
<div class="mr-1">
<span class="text-primary-900 dark:text-white">
⏳ {{ reservation.time_desc }} <small class="text-gray-600 dark:text-gray-100">({{ reservation.user_applicant.name }})</small><br/>
⏳ {{ reservation.time_desc }} <small class="text-gray-600 dark:text-gray-100">({{ reservation.user_applicant.name }})</small>
<br />
<strong>
{% for reservation in reservations_for_event -%}
{{ reservation.boat.name }}
{%- if not loop.last %} + {% endif -%}
{% endfor -%}
{% for reservation in reservations_for_event -%}
{{ reservation.boat.name }}
{%- if not loop.last %} +
{% endif -%}
{% endfor -%}
</strong>
</span>
<small class="text-gray-600 dark:text-gray-100">(Reservierung - {{ reservation.usage}})</small>
<small class="text-gray-600 dark:text-gray-100">(Reservierung - {{ reservation.usage }})</small>
</div>
</div>
</div>
@ -241,9 +243,9 @@
<input type="hidden" name="id" value="{{ event.id }}" />
{{ macros::input(label='Titel', name='name', type='input', value=event.name) }}
{% if event.cancelled %}
<input type="hidden" name="max_people" value="-1" />
<input type="hidden" name="max_people" value="-1" />
{% else %}
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='0') }}
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='0') }}
{% endif %}
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=event.planned_amount_cox, required=true, min='0') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=event.id,checked=event.always_show) }}
@ -378,11 +380,11 @@
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min=trip.rower | length) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
{% if loggedin_user.allowed_to_steer %}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id, only_ergo=not loggedin_user.allowed_to_steer) }}
{% else %}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, selected_id=trip.trip_type_id, only_ergo=not loggedin_user.allowed_to_steer, only_ergos=true) }}
{% endif %}
{% if loggedin_user.allowed_to_steer %}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id, only_ergo=not loggedin_user.allowed_to_steer) }}
{% else %}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, selected_id=trip.trip_type_id, only_ergo=not loggedin_user.allowed_to_steer, only_ergos=true) }}
{% endif %}
<input value="Speichern" class="btn btn-primary" type="submit" />
</form>
</div>
@ -472,9 +474,11 @@
{% endif %}
">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">{% include "includes/plus-icon" %}</span>
{% if not loggedin_user.allowed_to_steer %}Ergo-Session
{%- else -%}
Ausfahrt{%endif%}
{% if not loggedin_user.allowed_to_steer %}
Ergo-Session
{%- else -%}
Ausfahrt
{% endif %}
</a>
{% endif %}
</div>
@ -484,7 +488,7 @@
{% endfor %}
</div>
</div>
{% if loggedin_user.allowed_to_steer or "ergo" in loggedin_user.roles %}
{% if loggedin_user.allowed_to_steer or "ergo" in loggedin_user.roles %}
{% include "forms/trip" %}
{% endif %}
{% if "manage_events" in loggedin_user.roles %}