15 Commits

Author SHA1 Message Date
5296b6a6c1 Merge pull request 'single-user-edit-page' (#970) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m54s
CI/CD Pipeline / deploy-staging (push) Successful in 7m8s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #970
2025-05-03 13:46:39 +02:00
49e657ab54 Merge pull request 'single-user-edit-page' (#968) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m27s
CI/CD Pipeline / deploy-staging (push) Successful in 6m34s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #968
2025-05-02 18:16:53 +02:00
25bbaca0d3 Merge pull request 'show payment status in user view; Fixes #965' (#967) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m54s
CI/CD Pipeline / deploy-staging (push) Successful in 8m6s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #967
2025-04-30 23:32:36 +02:00
26038eabe4 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
2025-04-30 22:32:46 +02:00
57acd92e7c Merge pull request 'fix-list-scheckbuch' (#960) from fix-list-scheckbuch into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m37s
CI/CD Pipeline / deploy-staging (push) Successful in 7m3s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #960
2025-04-29 23:01:50 +02:00
c136c60e62 Merge pull request 'separate-scheckbuch-user' (#948) from separate-scheckbuch-user into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m17s
CI/CD Pipeline / deploy-staging (push) Successful in 6m45s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #948
2025-04-29 21:06:00 +02:00
a5e90ea014 Merge pull request 'log-event-updates' (#946) from log-event-updates into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 10m29s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #946
2025-04-29 20:37:11 +02:00
f0f3909239 Merge pull request 'format-cal-according-to-standard' (#944) from format-cal-according-to-standard into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m35s
CI/CD Pipeline / deploy-staging (push) Successful in 21m47s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #944
2025-04-28 22:20:39 +02:00
1438bbe3a8 Merge pull request 'hide-box' (#935) from hide-box into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m53s
CI/CD Pipeline / deploy-staging (push) Successful in 7m59s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #935
2025-04-19 21:30:20 +02:00
a910cd745d Merge pull request 'document nextcloud integration, for future nextcloud setups' (#933) from doc-nextcloud-integration into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m47s
CI/CD Pipeline / deploy-staging (push) Successful in 7m25s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #933
2025-04-19 09:19:58 +02:00
6265440288 Merge pull request 'zero-rower-events' (#931) from zero-rower-events into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m10s
CI/CD Pipeline / deploy-staging (push) Successful in 7m42s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #931
2025-04-19 00:22:27 +02:00
3baed66ebc Merge pull request 'also be able to cancel trips (not only events)' (#929) from zero-rower-events into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 13m39s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #929
2025-04-18 23:32:34 +02:00
499ce06438 Merge pull request 'zero-rower-events; Fixes #913' (#927) from zero-rower-events into staging
Some checks failed
CI/CD Pipeline / test (push) Has started running
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #927
2025-04-18 23:12:41 +02:00
67e5277c62 Merge pull request 'remove unused dep; cargo clippy' (#925) from simple-nx-auth into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m20s
CI/CD Pipeline / deploy-staging (push) Successful in 7m38s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #925
2025-04-18 17:45:44 +02:00
ce154bf060 Merge pull request 'simple-nx-auth' (#923) from simple-nx-auth into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m3s
CI/CD Pipeline / deploy-staging (push) Successful in 7m41s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #923
2025-04-18 17:10:44 +02:00
58 changed files with 993 additions and 3372 deletions

View File

@@ -5,7 +5,3 @@
.h2 { .h2 {
@apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3; @apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3;
} }
.h3 {
@apply text-center text-xl uppercase tracking-wide font-bold text-primary-900 dark:text-white;
}

View File

@@ -5,8 +5,7 @@
.input-group { .input-group {
@apply flex; @apply flex;
input[readonly], input[readonly] {
select[disabled] {
opacity: .7; opacity: .7;
} }

View File

@@ -10,12 +10,4 @@
&-white { &-white {
@apply text-white hover:text-primary-100 underline; @apply text-white hover:text-primary-100 underline;
} }
&-black {
@apply text-black hover:text-primary-950 dark:text-white hover:dark:text-primary-300 underline;
}
&-no-underline {
@apply no-underline;
}
} }

View File

@@ -225,15 +225,6 @@ CREATE TABLE IF NOT EXISTS "distance" (
); );
CREATE TABLE IF NOT EXISTS "activity" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
text TEXT NOT NULL,
relevant_for TEXT NOT NULL, -- e.g. user_id=123;trip_id=456
keep_until DATETIME
);
CREATE TRIGGER IF NOT EXISTS prevent_multiple_roles_same_cluster CREATE TRIGGER IF NOT EXISTS prevent_multiple_roles_same_cluster
BEFORE INSERT ON user_role BEFORE INSERT ON user_role
BEGIN BEGIN

View File

@@ -8,7 +8,7 @@ use rot::rest;
use rot::tera; use rot::tera;
use rot::{scheduled, tera::Config}; use rot::{scheduled, tera::Config};
use sqlx::{ConnectOptions, pool::PoolOptions, sqlite::SqliteConnectOptions}; use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;

View File

@@ -1,113 +0,0 @@
use std::ops::DerefMut;
use super::{role::Role, user::User};
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Activity {
pub id: i64,
pub created_at: NaiveDateTime,
pub text: String,
pub relevant_for: String,
pub keep_until: Option<NaiveDateTime>,
}
pub struct ActivityBuilder {
text: String,
relevant_for: String,
keep_until: Option<NaiveDateTime>,
}
impl ActivityBuilder {
#[must_use]
pub fn new(text: &str) -> Self {
Self {
text: text.into(),
relevant_for: String::new(),
keep_until: None,
}
}
#[must_use]
pub fn relevant_for_user(self, user: &User) -> Self {
Self {
relevant_for: format!("{}user-{};", self.relevant_for, user.id),
..self
}
}
#[must_use]
pub fn relevant_for_role(self, role: &Role) -> Self {
Self {
relevant_for: format!("{}role-{};", self.relevant_for, role.id),
..self
}
}
pub async fn save(self, db: &SqlitePool) {
Activity::create(db, &self.text, &self.relevant_for, self.keep_until).await;
}
pub async fn save_tx(self, db: &mut Transaction<'_, Sqlite>) {
Activity::create_with_tx(db, &self.text, &self.relevant_for, self.keep_until).await;
}
}
impl Activity {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, created_at, text, relevant_for, keep_until FROM activity WHERE id like ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub(super) async fn create_with_tx(
db: &mut Transaction<'_, Sqlite>,
text: &str,
relevant_for: &str,
keep_until: Option<NaiveDateTime>,
) {
sqlx::query!(
"INSERT INTO activity(text, relevant_for, keep_until) VALUES (?, ?, ?)",
text,
relevant_for,
keep_until
)
.execute(db.deref_mut())
.await
.unwrap();
}
pub(super) async fn create(
db: &SqlitePool,
text: &str,
relevant_for: &str,
keep_until: Option<NaiveDateTime>,
) {
let mut tx = db.begin().await.unwrap();
Self::create_with_tx(&mut tx, text, relevant_for, keep_until).await;
tx.commit().await.unwrap();
}
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Activity> {
let user_str = format!("user-{};", user.id);
sqlx::query_as!(
Self,
"
SELECT id, created_at, text, relevant_for, keep_until FROM activity
WHERE
relevant_for like CONCAT('%', ?, '%')
ORDER BY created_at DESC;
",
user_str
)
.fetch_all(db)
.await
.unwrap()
}
}

View File

@@ -2,8 +2,8 @@ use std::ops::DerefMut;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use itertools::Itertools; use itertools::Itertools;
use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use crate::model::boathouse::Boathouse; use crate::model::boathouse::Boathouse;

View File

@@ -1,7 +1,7 @@
use crate::model::{boat::Boat, user::User}; use crate::model::{boat::Boat, user::User};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
use super::log::Log; use super::log::Log;

View File

@@ -2,8 +2,8 @@ use std::io::Write;
use chrono::{Duration, NaiveDate, NaiveTime}; use chrono::{Duration, NaiveDate, NaiveTime};
use ics::{ use ics::{
ICalendar,
properties::{DtEnd, DtStart, Summary}, properties::{DtEnd, DtStart, Summary},
ICalendar,
}; };
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool}; use sqlx::{FromRow, Row, SqlitePool};
@@ -578,11 +578,6 @@ mod test {
let today = Local::now().date_naive().format("%Y%m%d").to_string(); let today = Local::now().date_naive().format("%Y%m%d").to_string();
let actual = Event::get_ics_feed(&pool).await; let actual = Event::get_ics_feed(&pool).await;
assert_eq!( assert_eq!(format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
format!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
),
actual
);
} }
} }

View File

@@ -1,7 +1,7 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction, sqlite::SqliteQueryResult}; use sqlx::{sqlite::SqliteQueryResult, FromRow, Sqlite, SqlitePool, Transaction};
use super::user::User; use super::user::User;

View File

@@ -823,13 +823,7 @@ ORDER BY departure DESC
if difference > Duration::hours(1) { if difference > Duration::hours(1) {
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap(); let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await; let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await;
let mut msg = format!( let mut msg = format!("{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}", user.name, logbook.steering_user.name, logbook.steering_user.name, logbook.logbook.departure.format("%Y-%m-%d %H:%M"));
"{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}",
user.name,
logbook.steering_user.name,
logbook.steering_user.name,
logbook.logbook.departure.format("%Y-%m-%d %H:%M")
);
if let Some(destination) = logbook.logbook.destination { if let Some(destination) = logbook.logbook.destination {
msg.push_str(&format!(", Ziel: {}", destination)); msg.push_str(&format!(", Ziel: {}", destination));
} else { } else {

View File

@@ -1,15 +1,15 @@
use std::{error::Error, fs}; use std::{error::Error, fs};
use lettre::{ use lettre::{
Address, Message, SmtpTransport, Transport, message::{header::ContentType, Attachment, MultiPart, SinglePart},
message::{Attachment, MultiPart, SinglePart, header::ContentType},
transport::smtp::authentication::Credentials, transport::smtp::authentication::Credentials,
Address, Message, SmtpTransport, Transport,
}; };
use sqlx::{Sqlite, SqlitePool, Transaction}; use sqlx::{Sqlite, SqlitePool, Transaction};
use crate::tera::admin::mail::MailToSend; use crate::tera::admin::mail::MailToSend;
use super::{activity::ActivityBuilder, family::Family, log::Log, role::Role, user::User}; use super::{family::Family, log::Log, role::Role, user::User};
pub struct Mail {} pub struct Mail {}
@@ -79,9 +79,7 @@ impl Mail {
.build(); .build();
// Send the email // Send the email
if let Err(e) = mailer.send(&email) { mailer.send(&email).unwrap();
Log::create_with_tx(db, format!("Mail nicht versandt: {e:?}")).await;
}
Ok(()) Ok(())
} }
@@ -253,12 +251,6 @@ Der Vorstand");
// Send the email // Send the email
mailer.send(&email).unwrap(); mailer.send(&email).unwrap();
ActivityBuilder::new(&format!(
"{user} hat die Info-Mail bzgl. Gebühren gesendet bekommen."
))
.relevant_for_user(&user)
.save(db)
.await;
} }
} }
} }
@@ -375,12 +367,6 @@ Der Vorstand");
// Send the email // Send the email
mailer.send(&email).unwrap(); mailer.send(&email).unwrap();
ActivityBuilder::new(&format!(
"{user} hat die Mahn-Mail bzgl. Gebühren gesendet bekommen."
))
.relevant_for_user(&user)
.save(db)
.await;
} }
} }
} }

View File

@@ -14,7 +14,6 @@ use self::{
use boatreservation::{BoatReservation, BoatReservationWithDetails}; use boatreservation::{BoatReservation, BoatReservationWithDetails};
use std::collections::HashMap; use std::collections::HashMap;
pub mod activity;
pub mod boat; pub mod boat;
pub mod boatdamage; pub mod boatdamage;
pub mod boathouse; pub mod boathouse;

View File

@@ -1,6 +1,6 @@
use std::io::Write; use std::io::Write;
use ics::{ICalendar, components::Property}; use ics::{components::Property, ICalendar};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{event::Event, trip::Trip, user::User}; use crate::model::{event::Event, trip::Trip, user::User};

View File

@@ -1,6 +1,5 @@
use std::{cmp::Ordering, fmt::Display, ops::DerefMut}; use std::{fmt::Display, ops::DerefMut};
use super::{activity::ActivityBuilder, user::AdminUser};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@@ -14,30 +13,6 @@ pub struct Role {
pub(crate) cluster: Option<String>, pub(crate) cluster: Option<String>,
} }
// Implement PartialEq to compare roles based only on id
impl PartialEq for Role {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
// Implement Eq to indicate that equality is reflexive
impl Eq for Role {}
// Implement PartialOrd if you need to sort or compare roles
impl PartialOrd for Role {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.id.cmp(&other.id))
}
}
// Implement Ord if you need total ordering (for sorting)
impl Ord for Role {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
impl Display for Role { impl Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name) write!(f, "{}", self.name)
@@ -55,27 +30,6 @@ impl Role {
.unwrap() .unwrap()
} }
pub async fn all_cluster(db: &SqlitePool, cluster: &str) -> Vec<Role> {
sqlx::query_as!(
Role,
r#"SELECT id,
CASE WHEN formatted_name IS NOT NULL AND formatted_name != ''
THEN formatted_name
ELSE name
END AS "name!: String",
'' as formatted_name,
desc,
hide_in_lists,
cluster
FROM role
WHERE cluster = ?"#,
cluster
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@@ -105,6 +59,21 @@ WHERE id like ?
.ok() .ok()
} }
pub async fn find_by_cluster_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
FROM role
WHERE cluster = ?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> { pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@@ -135,30 +104,6 @@ WHERE name like ?
.ok() .ok()
} }
pub async fn update(
&self,
db: &SqlitePool,
updated_by: &AdminUser,
formatted_name: &str,
desc: &str,
) -> Result<(), String> {
sqlx::query!(
"UPDATE role SET formatted_name=?, desc=? WHERE id=?",
formatted_name,
desc,
self.id
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
ActivityBuilder::new(&format!(
"{updated_by} hat Rolle {self} von {self:#?} auf FORMATTED_NAME={formatted_name}, DESC={desc} aktualisiert."
)).relevant_for_role(self).save(db).await;
Ok(())
}
pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> { pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> {
let query = format!( let query = format!(
"SELECT u.name "SELECT u.name

View File

@@ -567,11 +567,9 @@ mod test {
let last_notification = &Notification::for_user(&pool, &cox).await[0]; let last_notification = &Notification::for_user(&pool, &cox).await[0];
assert!( assert!(last_notification
last_notification
.message .message
.starts_with("cox2 hat eine Ausfahrt zur selben Zeit") .starts_with("cox2 hat eine Ausfahrt zur selben Zeit"));
);
} }
#[sqlx::test] #[sqlx::test]

View File

@@ -1,32 +1,12 @@
// TODO: put back in `src/model/user/mod.rs` once that is cleaned up // TODO: put back in `src/model/user/mod.rs` once that is cleaned up
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User}; use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
use crate::model::{ use crate::model::{family::Family, log::Log, mail::valid_mails, role::Role};
activity::ActivityBuilder, family::Family, mail::valid_mails, notification::Notification,
role::Role,
};
use chrono::NaiveDate; use chrono::NaiveDate;
use rocket::{fs::TempFile, tokio::io::AsyncReadExt}; use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool; use sqlx::SqlitePool;
impl User { impl User {
pub(crate) async fn add_note(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
user: &User,
note: &str,
) -> Result<(), String> {
let note = note.trim();
ActivityBuilder::new(&format!("({updated_by}) {note}"))
.relevant_for_user(user)
.save(db)
.await;
Ok(())
}
pub(crate) async fn update_mail( pub(crate) async fn update_mail(
&self, &self,
db: &SqlitePool, db: &SqlitePool,
@@ -47,20 +27,12 @@ impl User {
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.mail { let msg = match &self.mail {
Some(old_mail) => { Some(old_mail) => format!(
format!( "{updated_by} has changed the mail address of {self} from {old_mail} to {new_mail}"
"{updated_by} hat die Mail-Adresse von {self} von {old_mail} auf {new_mail} geändert." ),
) None => format!("{updated_by} has added a mail address for {self}: {new_mail}"),
}
None => {
format!("{updated_by} eine neue Mail-Adresse für {self} hinzugefügt: {new_mail}")
}
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -89,21 +61,11 @@ impl User {
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.phone { let msg = match &self.phone {
Some(old_phone) if new_phone.is_empty() => format!( Some(old_phone) if new_phone.is_empty() => format!("{updated_by} has removed the phone number of {self} (old number: {old_phone})"),
"{updated_by} hat die Telefonnummer von {self} entfernt (alte Nummer: {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}")
Some(old_phone) => format!(
"{updated_by} hat die Telefonnummer von {self} von {old_phone} auf {new_phone} geändert."
),
None => format!(
"{updated_by} hat eine neue Telefonnummer für {self} hinzugefügt: {new_phone}"
),
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_address( pub(crate) async fn update_address(
@@ -115,7 +77,7 @@ impl User {
let new_address = new_address.trim(); let new_address = new_address.trim();
let query = if new_address.is_empty() { let query = if new_address.is_empty() {
if self.address.is_none() { if !self.address.is_none() {
return; // nothing to do return; // nothing to do
} }
sqlx::query!("UPDATE user SET address = NULL where id = ?", self.id) sqlx::query!("UPDATE user SET address = NULL where id = ?", self.id)
@@ -134,19 +96,11 @@ impl User {
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.address { let msg = match &self.address {
Some(old_address) if new_address.is_empty() => format!( Some(old_address) if new_address.is_empty() => format!("{updated_by} has removed the address of {self} (old address: {old_address})"),
"{updated_by} hat die Adresse von {self} entfernt (alte Adresse: {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}")
Some(old_address) => format!(
"{updated_by} hat die Adresse von {self} von {old_address} auf {new_address} geändert."
),
None => format!("{updated_by} hat eine Adresse für {self} hinzugefügt: {new_address}"),
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_nickname( pub(crate) async fn update_nickname(
@@ -169,20 +123,11 @@ impl User {
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.nickname { let msg = match &self.nickname {
Some(old_nickname) if new_nickname.is_empty() => format!( Some(old_nickname) if new_nickname.is_empty() => format!("{updated_by} has removed the nickname of {self} (old nickname: {old_nickname})"),
"{updated_by} hat den Sitznamen von {self} entfernt (alter Spitzname: {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}")
Some(old_nickname) => format!(
"{updated_by} hat den Spitznamen von {self} von {old_nickname} auf {new_nickname} geändert."
),
None => format!(
"{updated_by} hat einen neuen Spitznamen für {self} hinzugefügt: {new_nickname}"
),
}; };
ActivityBuilder::new(&msg) Log::create(db, msg).await;
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -203,18 +148,10 @@ impl User {
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.member_since_date { let msg = match &self.member_since_date {
Some(old_member_since_date) => format!( 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}"),
"{updated_by} hat das Beitrittsdatum von {self} von {old_member_since_date} auf {new_member_since_date} geändert." None => format!("{updated_by} has added a member_since_date for {self}: {new_member_since_date}")
),
None => format!(
"{updated_by} hat ein neues Beitrittsdatum für {self} hinzugefügt: {new_member_since_date}"
),
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_birthdate( pub(crate) async fn update_birthdate(
@@ -233,18 +170,10 @@ impl User {
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.birthdate{ let msg = match &self.birthdate{
Some(old_birthdate) => format!( Some(old_birthdate) => format!("{updated_by} has changed the birthdate of {self} from {old_birthdate} to {new_birthdate}"),
"{updated_by} hat das Geburtsdatum von {self} von {old_birthdate} auf {new_birthdate} geändert." None => format!("{updated_by} has added a birthdate for {self}: {new_birthdate}")
),
None => {
format!("{updated_by} hat ein Geburtsdatum für {self} hinzugefügt: {new_birthdate}")
}
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_family( pub(crate) async fn update_family(
@@ -263,134 +192,20 @@ impl User {
.execute(db) .execute(db)
.await .await
.unwrap(); .unwrap();
ActivityBuilder::new(&format!(
"{updated_by} hat {self} zu einer Familie hinzugefügt."
))
.relevant_for_user(self)
.save(db)
.await;
} else { } else {
sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id) sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id)
.execute(db) .execute(db)
.await .await
.unwrap(); .unwrap();
ActivityBuilder::new(&format!(
"{updated_by} hat die Familienzugehörigkeit von {self} gelöscht."
))
.relevant_for_user(self)
.save(db)
.await;
}; };
Family::clean_families_without_members(db).await; Family::clean_families_without_members(db).await;
}
pub(crate) async fn change_skill( Log::create(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
skill: Option<Role>,
) -> Result<(), String> {
let old_skill = self.skill(db).await;
let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
let cox = Role::find_by_name(db, "cox").await.unwrap();
let bootsfuehrer = Role::find_by_name(db, "Bootsführer").await.unwrap();
match (old_skill, skill) {
(None, new) if new == Some(cox.clone()) => {
self.add_role(db, updated_by, &cox).await?;
Notification::create_for_role(
db, db,
&member, format!("{updated_by} hat die Familie von {self} aktualisiert."),
&format!(
"Liebes Vereinsmitglied, {self} ist ab sofort Steuerperson 🎉 Hip hip ...!"
),
"Neue Steuerperson",
None,
None,
) )
.await; .await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zur Steuerperson gemacht"))
.relevant_for_user(self)
.save(db)
.await;
}
(old, new) if old == Some(cox.clone()) && new == Some(bootsfuehrer.clone()) => {
self.remove_role(db, updated_by, &cox).await?;
self.add_role(db, updated_by, &bootsfuehrer).await?;
Notification::create_for_role(
db,
&member,
&format!(
"Liebes Vereinsmitglied, {self} ist ab sofort Bootsführer:in 🎉 Hip hip ...!"
),
"Neue:r Bootsführer:in",
None,
None,
)
.await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zum Bootsführer gemacht"))
.relevant_for_user(self)
.save(db)
.await;
}
(old, None) => {
if let Some(old) = old {
self.remove_role(db, updated_by, &old).await?;
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
Notification::create_for_role(
db,
&vorstand,
&format!("Lieber Vorstand, {self} ist ab kein {old} mehr."),
"Steuerperson --",
None,
None,
)
.await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitlgied gemacht (keine Steuerperson/Schiffsführer mehr)"))
.relevant_for_user(self)
.save(db)
.await;
}
}
(old, new) => return Err(format!("Not allowed to change from {old:?} to {new:?}")),
};
Ok(())
}
pub(crate) async fn change_financial(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
financial: Option<Role>,
) -> Result<(), String> {
let mut new = String::new();
let mut old = String::new();
if let Some(old_financial) = self.financial(db).await {
self.remove_role(db, updated_by, &old_financial).await?;
old.push_str(&old_financial.name);
} else {
old.push_str("Keine Ermäßigung");
}
if let Some(new_financial) = financial {
self.add_role(db, updated_by, &new_financial).await?;
new.push_str(&new_financial.name);
} else {
new.push_str("Keine Ermäßigung");
}
ActivityBuilder::new(&format!(
"{updated_by} hat die Ermäßigung von {self} von {old} auf {new} geändert"
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
} }
pub(crate) async fn remove_role( pub(crate) async fn remove_role(
@@ -400,9 +215,7 @@ impl User {
role: &Role, role: &Role,
) -> Result<(), String> { ) -> Result<(), String> {
if !self.has_role(db, &role.name).await { if !self.has_role(db, &role.name).await {
return Err(format!( return Err(format!("Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"));
"Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"
));
} }
sqlx::query!( sqlx::query!(
@@ -414,14 +227,11 @@ impl User {
.await .await
.unwrap(); .unwrap();
if !role.hide_in_lists && role.cluster.is_none() { Log::create(
ActivityBuilder::new(&format!( db,
"{updated_by} hat die Rolle {role} von {self} entfernt." format!("{updated_by} has removed role {role} from user {self}"),
)) )
.relevant_for_user(self)
.save(db)
.await; .await;
}
Ok(()) Ok(())
} }
@@ -442,11 +252,10 @@ impl User {
.await .await
.unwrap(); .unwrap();
ActivityBuilder::new(&format!( Log::create(
"{updated_by} hat den Bezahlstatus von {self} auf 'nicht bezahlt' gesetzt." db,
)) format!("{updated_by} has set that user {self} has NOT paid the fee (yet)"),
.relevant_for_user(self) )
.save(db)
.await; .await;
} }
pub(crate) async fn has_paid( pub(crate) async fn has_paid(
@@ -465,11 +274,10 @@ impl User {
.await .await
.expect("paid role has no group"); .expect("paid role has no group");
ActivityBuilder::new(&format!( Log::create(
"{updated_by} hat den Bezahlstatus von {self} auf 'bezahlt' gesetzt." db,
)) format!("{updated_by} has set that user {self} has paid the fee (yet)"),
.relevant_for_user(self) )
.save(db)
.await; .await;
} }
@@ -480,9 +288,7 @@ impl User {
role: &Role, role: &Role,
) -> Result<(), String> { ) -> Result<(), String> {
if self.has_role(db, &role.name).await { if self.has_role(db, &role.name).await {
return Err(format!( return Err(format!("Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"));
"Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"
));
} }
sqlx::query!( sqlx::query!(
@@ -501,14 +307,11 @@ impl User {
) )
})?; })?;
if !role.hide_in_lists && role.cluster.is_none() { Log::create(
ActivityBuilder::new(&format!( db,
"{updated_by} hat die Rolle '{role}' dem Benutzer {self} hinzugefügt." format!("{updated_by} has added role {role} to user {self}"),
)) )
.relevant_for_user(self)
.save(db)
.await; .await;
}
Ok(()) Ok(())
} }
@@ -523,7 +326,7 @@ impl User {
return Err(format!("User {self} hat bereits eine Beitrittserklärung.")); return Err(format!("User {self} hat bereits eine Beitrittserklärung."));
} }
if membership_pdf.len() == 0 { if membership_pdf.len() == 0 {
return Err("Keine Beitrittserklärung mitgeschickt.".to_string()); return Err(format!("Keine Beitrittserklärung mitgeschickt."));
} }
let mut stream = membership_pdf.open().await.unwrap(); let mut stream = membership_pdf.open().await.unwrap();
@@ -538,11 +341,10 @@ impl User {
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
ActivityBuilder::new(&format!( Log::create(
"{updated_by} hat die Mitgliedserklärung (PDF) für user {self} hinzugefügt." db,
)) format!("{updated_by} has added the membership pdf for user {self}"),
.relevant_for_user(self) )
.save(db)
.await; .await;
Ok(()) Ok(())

View File

@@ -1,167 +0,0 @@
use super::User;
use crate::{
model::{
activity::ActivityBuilder, notification::Notification, role::Role, user::ManageUserUser,
},
special_user,
};
use rocket::async_trait;
use sqlx::SqlitePool;
special_user!(ClubMemberUser, +"Donau Linz", +"Förderndes Mitglied", +"Unterstützend");
impl ClubMemberUser {
async fn add_membership_role(&self, db: &SqlitePool, role: &Role) {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
}
async fn remove_membership_role(&self, db: &SqlitePool) {
let role = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
let role = Role::find_by_name(db, "Unterstützend").await.unwrap();
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
}
async fn new_membership_role(&self, db: &SqlitePool, role: &str) -> Result<(), String> {
let role = Role::find_by_name(db, role).await.unwrap();
self.remove_membership_role(db).await;
self.add_membership_role(db, &role).await;
Ok(())
}
pub(crate) async fn move_to_regular(
self,
db: &SqlitePool,
modified_by: &ManageUserUser,
) -> Result<(), String> {
if self.has_role(db, "Donau Linz").await {
return Err(format!("User {self} ist bereits reguläres Mitglied."));
}
self.new_membership_role(db, "Donau Linz").await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} hat upgegraded und ist nun ein neues reguläres Mitglied. 🎉",
self.name,
),
"Neues Vereinsmitglied",
None,
None,
)
.await;
ActivityBuilder::new(&format!(
"{modified_by} hat {self} zu einem regulären hochgestuft."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_unterstuetzend(
self,
db: &SqlitePool,
modified_by: &ManageUserUser,
) -> Result<(), String> {
if self.has_role(db, "Unterstützend").await {
return Err(format!("User {self} ist bereits unterstützendes Mitglied."));
}
self.new_membership_role(db, "Unterstützend").await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, der Mitgliedstatus von {} hat sich geändert auf 'Unterstützendes Mitglied'.",
self.name,
),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{modified_by} hat {self} zu einem unterstützenden Mitglied gemacht."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_foerdernd(
self,
db: &SqlitePool,
modified_by: &ManageUserUser,
) -> Result<(), String> {
if self.has_role(db, "Förderndes Mitglied").await {
return Err(format!("User {self} ist bereits förderndes Mitglied."));
}
self.new_membership_role(db, "Förderndes Mitglied").await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, der Mitgliedstatus von {} hat sich geändert auf 'Förderndes Mitglied'.",
self.name,
),
"Neues förderndes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{modified_by} hat {self} zu ein förderndes Mitglied gemacht."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
}

View File

@@ -1,7 +1,7 @@
use super::User; use super::User;
use crate::{ use crate::{
BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, model::family::Family, BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO,
RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND, model::family::Family, FOERDERND, REGULAR, RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND,
}; };
use chrono::{Datelike, Local, NaiveDate}; use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize; use serde::Serialize;

View File

@@ -1,101 +0,0 @@
use super::{ManageUserUser, User, regular::ClubMember};
use crate::{
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile};
use sqlx::SqlitePool;
special_user!(FoerderndUser, +"Förderndes Mitglied");
impl ClubMember for FoerderndUser {}
impl FoerderndUser {
pub(crate) async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
smtp_pw: &str,
) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Couldn't send welcome mail, as the user {self} has no mail..."
));
};
Mail::send_single(
db,
mail,
"Willkommen im ASKÖ Ruderverein Donau Linz!",
format!(
"Hallo {0},
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Riemen- & Dollenbruch
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
ActivityBuilder::new(&format!(
"User {self} hat die Info-Mail bzgl. neues förderndes Mitglied (Handbuch und WLAN Infos) an {mail} gesendet bekommen"
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
let role = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
let user = Self::create_member(
db,
created_by,
&role,
name,
mail,
financial,
birthdate,
member_since,
phone,
address,
membership_pdf,
)
.await?;
let user = Self::new(db, &user).await.unwrap();
user.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!("Lieber Vorstand, es gibt ein neues förderndes Mitglied: {user}"),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
Ok(())
}
}

View File

@@ -36,19 +36,19 @@ impl Member {
} }
pub(crate) fn is_club_member(&self) -> bool { pub(crate) fn is_club_member(&self) -> bool {
matches!( match self {
self, Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_) => true,
Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_) _ => false,
) }
} }
pub(crate) fn supposed_to_pay(&self) -> bool { pub(crate) fn supposed_to_pay(&self) -> bool {
matches!( match self {
self,
Member::Schnupperant(_) Member::Schnupperant(_)
| Member::Scheckbuch(_) | Member::Scheckbuch(_)
| Member::Regular(_) | Member::Regular(_)
| Member::Foerdernd(_) | Member::Foerdernd(_)
| Member::Unterstuetzend(_) | Member::Unterstuetzend(_) => true,
) _ => false,
}
} }
} }

View File

@@ -8,13 +8,14 @@ use rocket::{
http::{Cookie, Status}, http::{Cookie, Status},
request::{FromRequest, Outcome}, request::{FromRequest, Outcome},
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
tokio::io::AsyncReadExt,
Request, Request,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::activity::ActivityBuilder;
use super::{ use super::{
family::Family,
log::Log, log::Log,
logbook::Logbook, logbook::Logbook,
mail::Mail, mail::Mail,
@@ -25,19 +26,14 @@ use super::{
tripdetails::TripDetails, tripdetails::TripDetails,
Day, Day,
}; };
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD};
use scheckbuch::ScheckbuchUser; use scheckbuch::ScheckbuchUser;
mod basic; mod basic;
pub(crate) mod clubmember;
mod fee; mod fee;
pub(crate) mod foerdernd;
pub(crate) mod member; pub(crate) mod member;
pub(crate) mod regular; pub(crate) mod regular;
pub(crate) mod scheckbuch; pub(crate) mod scheckbuch;
pub(crate) mod schnupperant;
pub(crate) mod schnupperinterest;
pub(crate) mod unterstuetzend;
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] #[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct User { pub struct User {
@@ -113,6 +109,74 @@ impl User {
self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await
} }
pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Could not send welcome mail, because user {} has no email address",
self.name
));
};
if self.has_role(db, "schnupperant").await {
self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?;
} else if let Some(scheckbuch) = ScheckbuchUser::new(db, self).await {
scheckbuch.notify(db, mail, smtp_pw).await?;
} else {
return Err(format!(
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group",
self.name
));
}
Log::create(
db,
format!("Willkommensemail wurde an {} versandt", self.name),
)
.await;
Ok(())
}
async fn send_welcome_mail_schnupper(
&self,
db: &SqlitePool,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
// 2 things to do:
// 1. Send mail to user
Mail::send_single(
db,
mail,
"Schnupperrudern beim ASKÖ Ruderverein Donau Linz",
format!(
"Hallo {0},
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
// 2. Notify all coxes
let coxes = Role::find_by_name(db, "schnupper-betreuer").await.unwrap();
Notification::create_for_role(
db,
&coxes,
&format!(
"Liebe Schnupper-Betreuer, {} nimmt am Schnupperkurs teil.",
self.name
),
"Neue(r) Schnupperteilnehmer:in ",
None,
None,
)
.await;
Ok(())
}
pub async fn amount_boats(&self, db: &SqlitePool) -> i64 { pub async fn amount_boats(&self, db: &SqlitePool) -> i64 {
sqlx::query!( sqlx::query!(
"SELECT COUNT(*) as count FROM boat WHERE owner = ?", "SELECT COUNT(*) as count FROM boat WHERE owner = ?",
@@ -192,40 +256,6 @@ impl User {
.into_iter().map(|r| r.name).collect() .into_iter().map(|r| r.name).collect()
} }
pub async fn financial(&self, db: &SqlitePool) -> Option<Role> {
sqlx::query_as!(
Role,
"
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
FROM role r
JOIN user_role ur ON r.id = ur.role_id
WHERE ur.user_id = ?
AND r.cluster = 'financial';
",
self.id
)
.fetch_optional(db)
.await
.unwrap()
}
pub async fn skill(&self, db: &SqlitePool) -> Option<Role> {
sqlx::query_as!(
Role,
"
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
FROM role r
JOIN user_role ur ON r.id = ur.role_id
WHERE ur.user_id = ?
AND r.cluster = 'skill';
",
self.id
)
.fetch_optional(db)
.await
.unwrap()
}
pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> { pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!( sqlx::query_as!(
Role, Role,
@@ -419,6 +449,22 @@ ORDER BY last_access DESC
.unwrap() .unwrap()
} }
pub async fn create(db: &SqlitePool, name: &str) -> bool {
let name = name.trim();
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
.execute(db)
.await
.is_ok()
}
pub async fn create_with_mail(db: &SqlitePool, name: &str, mail: &str) -> bool {
let name = name.trim();
sqlx::query!("INSERT INTO USER(name, mail) VALUES (?, ?)", name, mail)
.execute(db)
.await
.is_ok()
}
pub async fn update_ergo(&self, db: &SqlitePool, dob: i32, weight: i64, sex: &str) { pub async fn update_ergo(&self, db: &SqlitePool, dob: i32, weight: i64, sex: &str) {
sqlx::query!( sqlx::query!(
"UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?", "UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?",
@@ -432,6 +478,68 @@ ORDER BY last_access DESC
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
} }
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) -> Result<(), String> {
let mut db = db.begin().await.map_err(|e| e.to_string())?;
let mut family_id = data.family_id;
if family_id.is_some_and(|x| x == -1) {
family_id = Some(Family::insert_tx(&mut db).await)
}
if !self.has_membership_pdf_tx(&mut db).await {
if let Some(membership_pdf) = data.membership_pdf {
let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new();
stream.read_to_end(&mut buffer).await.unwrap();
sqlx::query!(
"UPDATE user SET membership_pdf = ? where id = ?",
buffer,
self.id
)
.execute(db.deref_mut())
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
}
sqlx::query!(
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?",
data.dob,
data.weight,
data.sex,
data.member_since_date,
data.birthdate,
data.mail,
data.nickname,
data.notes,
data.phone,
data.address,
family_id,
self.id
)
.execute(db.deref_mut())
.await
.unwrap(); //Okay, because we can only create a User of a valid id
// handle roles
sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id)
.execute(db.deref_mut())
.await
.unwrap();
for role_id in data.roles.into_keys() {
let role = Role::find_by_id_tx(&mut db, role_id.parse::<i32>().unwrap())
.await
.unwrap();
self.add_role_tx(&mut db, &role).await?;
}
db.commit().await.map_err(|e| e.to_string())?;
Ok(())
}
async fn send_end_mail_scheckbuch( async fn send_end_mail_scheckbuch(
&self, &self,
db: &mut Transaction<'_, Sqlite>, db: &mut Transaction<'_, Sqlite>,
@@ -458,7 +566,29 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw, smtp_pw,
).await?; ).await?;
ActivityBuilder::new(&format!("User {self} hat eine Mail bekommen, dass seine 5 Ausfahrten mit der heutigen Ausfahrt aufgebraucht sind, und dass der nächste Schritt eine Vereinsmitgliedschaft wäre (inkl. Links zu Beitrittserklärung + Info, dass sie an info@ geschickt werden soll.")).relevant_for_user(self).save_tx(db).await; Ok(())
}
pub async fn add_role_tx(
&self,
db: &mut Transaction<'_, Sqlite>,
role: &Role,
) -> Result<(), String> {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db.deref_mut())
.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(()) Ok(())
} }
@@ -505,11 +635,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
}; };
if user.deleted { if user.deleted {
ActivityBuilder::new(&format!( Log::create(
"User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde." db,
)) format!("User ({name}) already deleted (tried to login)."),
.relevant_for_user(&user) )
.save(db)
.await; .await;
return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has
//been deleted //been deleted
@@ -520,12 +649,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
if password_hash == user_pw { if password_hash == user_pw {
return Ok(user); return Ok(user);
} }
ActivityBuilder::new(&format!( Log::create(db, format!("User {name} supplied the wrong PW")).await;
"User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben."
))
.relevant_for_user(&user)
.save(db)
.await;
Err(LoginError::InvalidAuthenticationCombo) Err(LoginError::InvalidAuthenticationCombo)
} else { } else {
info!("User {name} has no PW set"); info!("User {name} has no PW set");
@@ -538,12 +662,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
// TODO: add responsible person
ActivityBuilder::new(&format!("Passwort von User {self} wurde zurückgesetzt."))
.relevant_for_user(self)
.save(db)
.await;
} }
pub async fn update_pw(&self, db: &SqlitePool, pw: &str) { pub async fn update_pw(&self, db: &SqlitePool, pw: &str) {
@@ -552,12 +670,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
ActivityBuilder::new(&format!(
"Passwort von User {self} wurde erfolgreich geändert."
))
.relevant_for_user(self)
.save(db)
.await;
} }
fn get_hashed_pw(pw: &str) -> String { fn get_hashed_pw(pw: &str) -> String {
@@ -577,21 +689,13 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
ActivityBuilder::new(&format!("User {self} hat sich eingeloggt."))
.relevant_for_user(self)
.save(db)
.await;
} }
pub async fn delete(&self, db: &SqlitePool, deleted_by: &ManageUserUser) { pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id) sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id)
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
ActivityBuilder::new(&format!("User {self} wurde von {deleted_by} gelöscht."))
.relevant_for_user(self)
.save(db)
.await;
} }
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> { pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
@@ -683,10 +787,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None None,None
) )
.await; .await;
ActivityBuilder::new(&format!("5 Scheckbuchausfahrten von {self} wurden mit der heutigen Ausfahrt aufgebraucht. Info-Mail wurde an {self} geschickt + alle Steuerberechtigten informiert, dass wir pot. ein neues Mitglied haben"))
.relevant_for_user(self)
.save_tx(db)
.await;
} }
a if a > 5 => { a if a > 5 => {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap(); let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
@@ -701,10 +801,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None None,None
) )
.await; .await;
ActivityBuilder::new(&format!("{self} hat nun bereits die {amount_trips}. seiner 5 Scheckbuchausfahrten absolviert. Vorstand wurde via Notification informiert."))
.relevant_for_user(self)
.save_tx(db)
.await;
} }
_ => {} _ => {}
} }
@@ -723,12 +819,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
"Fahrtenabzeichen geschafft", "Fahrtenabzeichen geschafft",
None,None None,None
) )
.await;
ActivityBuilder::new(&format!(
"{self} hat das heurige Fahrtenabzeichen geschafft! Der Vorstand + {self} wurde via Notification informiert."
))
.relevant_for_user(self)
.save_tx(db)
.await; .await;
Notification::create_with_tx(db, self, "Mit deiner letzten Ausfahrt hast du nun alle Anforderungen für das heurige Fahrtenzeichen erfüllt. Gratuliere! 🎉", "Fahrtenabzeichen geschafft", None, None).await; Notification::create_with_tx(db, self, "Mit deiner letzten Ausfahrt hast du nun alle Anforderungen für das heurige Fahrtenzeichen erfüllt. Gratuliere! 🎉", "Fahrtenabzeichen geschafft", None, None).await;
@@ -748,10 +838,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None None,None
) )
.await; .await;
ActivityBuilder::new(&format!("{self} hat den Äquatorpreis in {level} geschafft! Der Vorstand + {self} wurde via Notification informiert."))
.relevant_for_user(self)
.save_tx(db)
.await;
Notification::create_with_tx(db, self, &format!("Mit deiner letzten Ausfahrt erfüllst du nun alle Anforderungen für den Äquatorpreis in {level}. Gratuliere! 🎉"), "Äquatorpreis", None, None).await; Notification::create_with_tx(db, self, &format!("Mit deiner letzten Ausfahrt erfüllst du nun alle Anforderungen für den Äquatorpreis in {level}. Gratuliere! 🎉"), "Äquatorpreis", None, None).await;
} }
@@ -812,7 +898,7 @@ macro_rules! special_user {
#[async_trait] #[async_trait]
impl<'r> rocket::request::FromRequest<'r> for $name { impl<'r> rocket::request::FromRequest<'r> for $name {
type Error = $crate::model::user::LoginError; type Error = crate::model::user::LoginError;
async fn from_request(req: &'r rocket::request::Request<'_>) -> rocket::request::Outcome<Self, Self::Error> { async fn from_request(req: &'r rocket::request::Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap(); let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await { match User::from_request(req).await {
@@ -924,7 +1010,9 @@ impl UserWithMembershipPdf {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::testdb; use std::collections::HashMap;
use crate::{tera::admin::user::UserEditForm, testdb};
use super::User; use super::User;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -971,6 +1059,52 @@ mod test {
assert_eq!(res.len(), 4); assert_eq!(res.len(), 4);
} }
#[sqlx::test]
fn test_succ_create() {
let pool = testdb!();
assert_eq!(User::create(&pool, "new-user-name".into()).await, true);
}
#[sqlx::test]
fn test_duplicate_name_create() {
let pool = testdb!();
assert_eq!(User::create(&pool, "admin".into()).await, false);
}
#[sqlx::test]
fn test_update() {
let pool = testdb!();
let user = User::find_by_id(&pool, 1).await.unwrap();
user.update(
&pool,
UserEditForm {
id: 1,
dob: None,
weight: None,
sex: Some("m".into()),
roles: HashMap::new(),
member_since_date: None,
birthdate: None,
mail: None,
nickname: None,
notes: None,
phone: None,
address: None,
family_id: None,
membership_pdf: None,
},
)
.await
.unwrap();
let user = User::find_by_id(&pool, 1).await.unwrap();
assert_eq!(user.sex, Some("m".into()));
}
#[sqlx::test] #[sqlx::test]
fn succ_login_with_test_db() { fn succ_login_with_test_db() {
let pool = testdb!(); let pool = testdb!();

View File

@@ -1,69 +1,22 @@
use super::{ManageUserUser, User}; use super::User;
use crate::{ use crate::{
NonEmptyString, model::{mail::Mail, notification::Notification},
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user, special_user,
}; };
use chrono::NaiveDate; use rocket::async_trait;
use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool; use sqlx::SqlitePool;
special_user!(RegularUser, +"Donau Linz"); special_user!(RegularUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied");
pub trait ClubMember {
async fn create_member(
db: &SqlitePool,
created_by: &ManageUserUser,
role: &Role,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<User, String> {
if membership_pdf.len() == 0 {
return Err("Keine Beitrittserklärung mitgeschickt.".to_string());
}
let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new();
stream.read_to_end(&mut buffer).await.unwrap();
let name = name.as_str();
let phone = phone.as_str();
let address = address.as_str();
sqlx::query!(
"INSERT INTO user(name, member_since_date, birthdate, mail, phone, address, membership_pdf)
VALUES (?,?,?,?,?,?,?)",
name, member_since, birthdate, mail, phone, address,buffer
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.change_financial(db, created_by, financial).await?;
user.add_role(db, created_by, role).await?;
ActivityBuilder::new(&format!(
"{created_by} hat Mitglied {user} mit der Rolle {role} angelegt."
))
.relevant_for_user(&user)
.save(db)
.await;
Ok(user)
}
}
impl ClubMember for RegularUser {}
impl RegularUser { impl RegularUser {
pub(crate) async fn send_welcome_mail_to_user( pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
self.notify_coxes_about_new_regular(db).await;
self.send_welcome_mail_to_user(db, smtp_pw).await?;
Ok(())
}
async fn send_welcome_mail_to_user(
&self, &self,
db: &SqlitePool, db: &SqlitePool,
smtp_pw: &str, smtp_pw: &str,
@@ -100,55 +53,21 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw, smtp_pw,
).await?; ).await?;
ActivityBuilder::new(&format!("Willkommensmail für {self} wurde an {mail} verschickt (Handbuch, Signal-Gruppe, App-Info, Fingerprint, WLAN)."))
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
pub(crate) async fn create( async fn notify_coxes_about_new_regular(&self, db: &SqlitePool) {
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
let user = Self::create_member(
db,
created_by,
&role,
name,
mail,
financial,
birthdate,
member_since,
phone,
address,
membership_pdf,
)
.await?;
let user = Self::new(db, &user).await.unwrap();
user.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people( Notification::create_for_steering_people(
db, db,
&format!("Liebe Steuerberechtigte, es gibt ein neues Mitglied: {user} 🎉"), &format!(
"Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}",
self.member_since_date.clone().unwrap(),
self.name
),
"Neues Vereinsmitglied", "Neues Vereinsmitglied",
None, None,
None, None,
) )
.await; .await;
Ok(())
} }
} }

View File

@@ -1,14 +1,10 @@
use super::foerdernd::FoerderndUser;
use super::regular::RegularUser; use super::regular::RegularUser;
use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User}; use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role; use crate::model::role::Role;
use crate::NonEmptyString;
use crate::{ use crate::{
SCHECKBUCH,
model::{mail::Mail, notification::Notification}, model::{mail::Mail, notification::Notification},
special_user, special_user, SCHECKBUCH,
}; };
use chrono::NaiveDate; use chrono::NaiveDate;
use rocket::async_trait; use rocket::async_trait;
@@ -18,29 +14,6 @@ use sqlx::SqlitePool;
special_user!(ScheckbuchUser, +"scheckbuch"); special_user!(ScheckbuchUser, +"scheckbuch");
impl ScheckbuchUser { impl ScheckbuchUser {
async fn set_data_for_clubmember(
&self,
db: &SqlitePool,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.user.update_birthdate(db, changed_by, birthdate).await;
self.user
.update_member_since(db, changed_by, member_since)
.await;
self.user.update_phone(db, changed_by, &phone).await;
self.user.update_address(db, changed_by, &address).await;
self.user
.add_membership_pdf(db, changed_by, membership_pdf)
.await?;
Ok(())
}
pub(crate) async fn convert_to_regular_user( pub(crate) async fn convert_to_regular_user(
self, self,
db: &SqlitePool, db: &SqlitePool,
@@ -52,15 +25,16 @@ impl ScheckbuchUser {
address: NonEmptyString, address: NonEmptyString,
membership_pdf: &TempFile<'_>, membership_pdf: &TempFile<'_>,
) -> Result<(), String> { ) -> Result<(), String> {
self.set_data_for_clubmember( // Set data
db, self.user.update_birthdate(db, changed_by, birthdate).await;
changed_by, self.user
member_since, .update_member_since(db, changed_by, member_since)
birthdate, .await;
phone,
address, self.user.update_phone(db, changed_by, &phone).await;
membership_pdf, self.user.update_address(db, changed_by, &address).await;
) self.user
.add_membership_pdf(db, changed_by, membership_pdf)
.await?; .await?;
// Change roles // Change roles
@@ -71,167 +45,30 @@ impl ScheckbuchUser {
// Notify // Notify
let regular = RegularUser::new(db, &self.user).await.unwrap(); let regular = RegularUser::new(db, &self.user).await.unwrap();
regular.send_welcome_mail_to_user(db, smtp_pw).await?; regular.notify(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} hatte ein Scheckbuch und ist nun seit {} es ein neues reguläres Mitglied. 🎉",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues Vereinsmitglied",
None,
None,
)
.await;
ActivityBuilder::new(&format!(
"{changed_by} hat den Scheckbuch-User {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_unterstuetzend_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
// Set data
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?;
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
unterstuetzend
.send_welcome_mail_to_user(db, smtp_pw)
.await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues unterstützendes Mitglied.",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!("{changed_by} hat den Scheckbuch-User {self} auf ein unterstützendes Mitglied upgegraded!"))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_foerdernd_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
// Set data
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?;
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues förderndes Mitglied.",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues förderndes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den Scheckbuch-User {self} auf ein förderndes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
// TODO: make private // TODO: make private
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { pub(crate) async fn notify(
&self,
db: &SqlitePool,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
self.send_welcome_mail_to_user(db, mail, smtp_pw).await?;
self.notify_coxes_about_new_scheckbuch(db).await; self.notify_coxes_about_new_scheckbuch(db).await;
self.send_welcome_mail_to_user(db, smtp_pw).await?;
ActivityBuilder::new(&format!(
"{self} hat eine Info-Mail bekommen (Erklärung Scheckbuch, Ruderapp) und alle Steuerberechtigten wurden informiert."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
pub(crate) async fn send_welcome_mail_to_user( async fn send_welcome_mail_to_user(
&self, &self,
db: &SqlitePool, db: &SqlitePool,
mail: &str,
smtp_pw: &str, smtp_pw: &str,
) -> Result<(), String> { ) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(
"Kann Mail nicht versenden, weil der User keine Mailadresse hinterlegt hat.".into(),
);
};
Mail::send_single( Mail::send_single(
db, db,
mail, mail,
@@ -267,38 +104,4 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
) )
.await; .await;
} }
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
) -> Result<(), String> {
let role = Role::find_by_name(db, "scheckbuch").await.unwrap();
let name = name.as_str();
sqlx::query!(
"INSERT INTO user(name, mail)
VALUES (?,?)",
name,
mail
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.add_role(db, created_by, &role).await?;
let user = Self::new(db, &user).await.unwrap();
user.notify(db, smtp_pw).await?;
ActivityBuilder::new(&format!("{created_by} hat Scheckbuch {user} angelegt."))
.relevant_for_user(&user)
.save(db)
.await;
Ok(())
}
} }

View File

@@ -1,402 +0,0 @@
use super::foerdernd::FoerderndUser;
use super::regular::RegularUser;
use super::scheckbuch::ScheckbuchUser;
use super::schnupperinterest::SchnupperInterestUser;
use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::{
model::{mail::Mail, notification::Notification},
special_user,
};
use chrono::NaiveDate;
use rocket::async_trait;
use rocket::fs::TempFile;
use sqlx::SqlitePool;
special_user!(SchnupperantUser, +"schnupperant");
impl SchnupperantUser {
async fn set_data_for_clubmember(
&self,
db: &SqlitePool,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.user.update_birthdate(db, changed_by, birthdate).await;
self.user
.update_member_since(db, changed_by, member_since)
.await;
self.user.update_phone(db, changed_by, &phone).await;
self.user.update_address(db, changed_by, &address).await;
self.user
.add_membership_pdf(db, changed_by, membership_pdf)
.await?;
Ok(())
}
pub(crate) async fn convert_to_regular_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &regular).await?;
// Notify
let regular = RegularUser::new(db, &self.user).await.unwrap();
regular.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {} ein neues reguläres Mitglied. 🎉",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues Vereinsmitglied",
None,
None,
)
.await;
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein reguläres Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_scheckbook(
self,
db: &SqlitePool,
changed_by: &ManageUserUser,
smtp_pw: &str,
) -> Result<(), String> {
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
self.user.remove_role(db, changed_by, &schnupperant).await?;
self.user.add_role(db, changed_by, &scheckbook).await?;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
self.add_role(db, changed_by, &no_einschreibgebuehr)
.await
.expect("role doesn't have a group");
}
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
scheckbook.notify(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} hat unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
self.name
),
"Neues Scheckbuch",
None,None
)
.await;
ActivityBuilder::new(&format!(
"{changed_by} hat dem ehemaligen Schnupperant {self} nun ein Scheckbuch gegeben"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_schnupperinterest(
self,
db: &SqlitePool,
changed_by: &ManageUserUser,
) -> Result<(), String> {
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
.await
.unwrap();
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &schnupperant).await?;
self.user
.add_role(db, changed_by, &schnupperinterest)
.await?;
let schnupperinterest = SchnupperInterestUser::new(db, &self.user).await.unwrap();
schnupperinterest.notify(db).await?;
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role(
db,
&role,
&format!(
"Lieber Schnupperbetreuer, {} hat sich vom Schnupperkurs abgemeldet.",
self.name
),
"Schnupperkurs Abmeldung",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat dem eigentlichen Schnupperanten {self} wieder auf die 'Interessierten'-Liste zurückgegeben."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_unterstuetzend_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
// Set data
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
self.add_role(db, changed_by, &no_einschreibgebuehr)
.await
.expect("role doesn't have a group");
}
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
unterstuetzend
.send_welcome_mail_to_user(db, smtp_pw)
.await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues unterstützendes Mitglied.",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein unterstützendes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_foerdernd_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
// Set data
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
self.add_role(db, changed_by, &no_einschreibgebuehr)
.await
.expect("role doesn't have a group");
}
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues förderndes Mitglied.",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues förderndes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein förderndes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
// TODO: make private
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
self.notify_coxes_about_new_scheckbuch(db).await;
self.send_welcome_mail_to_user(db, smtp_pw).await?;
ActivityBuilder::new(&format!(
"{self} hat eine Mail bekommen (Inhalt: wir freuen uns auf ihn + senden detailliertere Infos später zu) und die Schnupperbetreuer wurden via Notification informiert."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
smtp_pw: &str,
) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Couldn't send mail, because user {self} has no mail"
));
};
Mail::send_single(
db,
mail,
"ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs",
format!(
"Hallo {0},
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
Ok(())
}
async fn notify_coxes_about_new_scheckbuch(&self, db: &SqlitePool) {
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role(
db,
&role,
&format!(
"Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.",
self.name
),
"Neuer Schnupperant",
None,
None,
)
.await;
}
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
) -> Result<(), String> {
let role = Role::find_by_name(db, "schnupperant").await.unwrap();
let name = name.as_str();
sqlx::query!(
"INSERT INTO user(name, mail)
VALUES (?,?)",
name,
mail
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.add_role(db, created_by, &role).await?;
let user = Self::new(db, &user).await.unwrap();
user.notify(db, smtp_pw).await?;
ActivityBuilder::new(&format!(
"{created_by} hat {user} zur fixen Schnupperkurs-Anmeldung hinzugefügt."
))
.relevant_for_user(&user)
.save(db)
.await;
Ok(())
}
}

View File

@@ -1,162 +0,0 @@
use super::scheckbuch::ScheckbuchUser;
use super::schnupperant::SchnupperantUser;
use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::{model::notification::Notification, special_user};
use rocket::async_trait;
use sqlx::SqlitePool;
special_user!(SchnupperInterestUser, +"schnupper-interessierte");
impl SchnupperInterestUser {
pub(crate) async fn move_to_scheckbook(
self,
db: &SqlitePool,
changed_by: &ManageUserUser,
smtp_pw: &str,
) -> Result<(), String> {
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
.await
.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
self.user
.remove_role(db, changed_by, &schnupperinterest)
.await?;
self.user.add_role(db, changed_by, &scheckbook).await?;
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
scheckbook.notify(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} wollte unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
self.name
),
"Neues Scheckbuch",
None,
None
)
.await;
ActivityBuilder::new(&format!(
"Der Schnupperinteressierte {self} hat sich (ohne Schnupperkurs) doch gleich direkt für ein Scheckbuch entschieden. {changed_by} hat dieses eingerichtet."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_schnupperant(
self,
db: &SqlitePool,
changed_by: &ManageUserUser,
smtp_pw: &str,
) -> Result<(), String> {
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
.await
.unwrap();
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user
.remove_role(db, changed_by, &schnupperinterest)
.await?;
self.user.add_role(db, changed_by, &schnupperant).await?;
let schnupperant = SchnupperantUser::new(db, &self.user).await.unwrap();
schnupperant.notify(db, smtp_pw).await?;
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role(
db,
&role,
&format!(
"Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.",
self.name
),
"Neuer Schnupper-Interessierte:r",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"Der Schnupperinteressierte {self} hat sich zum Schnupperkurs angemeldet."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn notify(&self, db: &SqlitePool) -> Result<(), String> {
self.notify_schnupperbetreuer_about_new_interest(db).await;
ActivityBuilder::new(&format!(
"Der Schnupperbetreuer hat eine Info via Notification bekommen, dass {self} Interesse an einen Schnupperkurs hat."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
async fn notify_schnupperbetreuer_about_new_interest(&self, db: &SqlitePool) {
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role(
db,
&role,
&format!(
"Lieber Schnupperbetreuer, {} hat Interesse zum Schnupperkurs bekundet.",
self.name
),
"Neuer Schnupper-Interessierte:r",
None,
None,
)
.await;
}
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
name: NonEmptyString,
mail: &str,
) -> Result<(), String> {
let role = Role::find_by_name(db, "schnupper-interessierte")
.await
.unwrap();
let name = name.as_str();
sqlx::query!(
"INSERT INTO user(name, mail)
VALUES (?,?)",
name,
mail
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.add_role(db, created_by, &role).await?;
let user = Self::new(db, &user).await.unwrap();
user.notify(db).await?;
ActivityBuilder::new(&format!(
"{created_by} hat Schnupper-Interessierten {user} angelegt."
))
.relevant_for_user(&user)
.save(db)
.await;
Ok(())
}
}

View File

@@ -1,101 +0,0 @@
use super::{ManageUserUser, User, regular::ClubMember};
use crate::{
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile};
use sqlx::SqlitePool;
special_user!(UnterstuetzendUser, +"Unterstützend");
impl ClubMember for UnterstuetzendUser {}
impl UnterstuetzendUser {
pub(crate) async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
smtp_pw: &str,
) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Couldn't send welcome mail, as the user {self} has no mail..."
));
};
Mail::send_single(
db,
mail,
"Willkommen im ASKÖ Ruderverein Donau Linz!",
format!(
"Hallo {0},
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Riemen- & Dollenbruch
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
ActivityBuilder::new(&format!(
"{self} hat eine Mail an {mail} bekommen, mit Infos dass er/sie nun ein unterstützendes Mitglied ist (Handbuch, WLAN)."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
let role = Role::find_by_name(db, "Unterstützend").await.unwrap();
let user = Self::create_member(
db,
created_by,
&role,
name,
mail,
financial,
birthdate,
member_since,
phone,
address,
membership_pdf,
)
.await?;
let user = Self::new(db, &user).await.unwrap();
user.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!("Lieber Vorstand, es gibt ein neues unterstützendes Mitglied: {user}"),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
use rocket::{Build, FromForm, Rocket, State, form::Form, post, routes}; use rocket::{form::Form, post, routes, Build, FromForm, Rocket, State};
use serde_json::json; use serde_json::json;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -96,9 +96,7 @@ struct DailyWeather {
} }
fn fetch(api_key: &str) -> Result<Data, String> { fn fetch(api_key: &str) -> Result<Data, String> {
let url = format!( let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
"https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}"
);
match ureq::get(&url).call() { match ureq::get(&url).call() {
Ok(mut response) => { Ok(mut response) => {

View File

@@ -5,14 +5,13 @@ use crate::model::{
user::{User, UserWithDetails, VorstandUser}, user::{User, UserWithDetails, VorstandUser},
}; };
use rocket::{ use rocket::{
Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, Route, State,
}; };
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/boat")] #[get("/boat")]
@@ -246,11 +245,9 @@ mod test {
let rocket = rocket::build().manage(db.clone()); let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket); let rocket = crate::tera::config(rocket);
assert!( assert!(Boat::find_by_name(&db, "completely-new-boat".into())
Boat::find_by_name(&db, "completely-new-boat".into())
.await .await
.is_none() .is_none());
);
let client = Client::tracked(rocket).await.unwrap(); let client = Client::tracked(rocket).await.unwrap();
let login = client let login = client

View File

@@ -1,9 +1,8 @@
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, put, get, post, put,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -1,9 +1,9 @@
use rocket::form::Form; use rocket::form::Form;
use rocket::fs::TempFile; use rocket::fs::TempFile;
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket::{FromForm, post}; use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket::{Route, State, get, request::FlashMessage, routes}; use rocket::{post, FromForm};
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::log::Log; use crate::model::log::Log;

View File

@@ -1,6 +1,6 @@
use csv::ReaderBuilder; use csv::ReaderBuilder;
use rocket::{FromForm, Route, State, form::Form, get, post, routes}; use rocket::{form::Form, get, post, routes, FromForm, Route, State};
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{ use crate::{
@@ -12,7 +12,6 @@ pub mod boat;
pub mod event; pub mod event;
pub mod mail; pub mod mail;
pub mod notification; pub mod notification;
pub mod role;
pub mod schnupper; pub mod schnupper;
pub mod user; pub mod user;
@@ -82,7 +81,6 @@ pub fn routes() -> Vec<Route> {
ret.append(&mut notification::routes()); ret.append(&mut notification::routes());
ret.append(&mut mail::routes()); ret.append(&mut mail::routes());
ret.append(&mut event::routes()); ret.append(&mut event::routes());
ret.append(&mut role::routes());
ret.append(&mut routes![rss, show_rss, show_list, list]); ret.append(&mut routes![rss, show_rss, show_list, list]);
ret ret
} }

View File

@@ -6,14 +6,13 @@ use crate::model::{
}; };
use itertools::Itertools; use itertools::Itertools;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/notification")] #[get("/notification")]

View File

@@ -1,64 +0,0 @@
use crate::model::{
role::Role,
user::{AdminUser, UserWithDetails, VorstandUser},
};
use rocket::{
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes, FromForm, Route, State,
};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
#[get("/role")]
async fn index(
db: &State<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let roles = Role::all(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("roles", &roles);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.user, db).await,
);
Template::render("admin/role", context.into_json())
}
#[derive(FromForm)]
pub struct RoleToUpdate<'r> {
pub formatted_name: &'r str,
pub desc: &'r str,
}
#[post("/role/<role_id>", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<RoleToUpdate<'_>>,
role_id: i32,
admin: AdminUser,
) -> Flash<Redirect> {
let role = Role::find_by_id(db, role_id).await;
let Some(role) = role else {
return Flash::error(Redirect::to("/admin/role"), "Role does not exist!");
};
match role
.update(db, &admin, &data.formatted_name, &data.desc)
.await
{
Ok(_) => Flash::success(Redirect::to("/admin/role"), "Rolle bearbeitet"),
Err(e) => Flash::error(Redirect::to("/admin/role"), e),
}
}
pub fn routes() -> Vec<Route> {
routes![index, update]
}

View File

@@ -3,8 +3,8 @@ use crate::model::{
user::{SchnupperBetreuerUser, User, UserWithDetails}, user::{SchnupperBetreuerUser, User, UserWithDetails},
}; };
use futures::future::join_all; use futures::future::join_all;
use rocket::{Route, State, get, request::FlashMessage, routes}; use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/schnupper")] #[get("/schnupper")]

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
use rocket::{ use rocket::{
FromForm, Request, Route, State,
form::Form, form::Form,
get, get,
http::{Cookie, CookieJar}, http::{Cookie, CookieJar},
@@ -9,8 +8,9 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
FromForm, Request, Route, State,
}; };
use rocket_dyn_templates::{Template, context, tera}; use rocket_dyn_templates::{context, tera, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
@@ -73,10 +73,7 @@ async fn login(
); );
} }
Err(_) => { Err(_) => {
return Flash::error( return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere unseren Schriftführer oder schreibe eine Mail an info@rudernlinz.at!");
Redirect::to("/auth"),
"Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere unseren Schriftführer oder schreibe eine Mail an info@rudernlinz.at!",
);
} }
}; };

View File

@@ -3,8 +3,8 @@ use crate::model::{
role::Role, role::Role,
user::{User, UserWithDetails, VorstandUser}, user::{User, UserWithDetails, VorstandUser},
}; };
use rocket::{Route, State, get, request::FlashMessage, routes}; use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/achievement")] #[get("/achievement")]

View File

@@ -4,14 +4,13 @@ use crate::model::{
user::{AdminUser, UserWithDetails, VorstandUser}, user::{AdminUser, UserWithDetails, VorstandUser},
}; };
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/boathouse")] #[get("/boathouse")]

View File

@@ -1,10 +1,9 @@
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -1,11 +1,10 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -1,9 +1,8 @@
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -138,10 +137,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
.await; .await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!") Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
} }
Err(CoxHelpError::CanceledEvent) => Flash::error( Err(CoxHelpError::CanceledEvent) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
"Die Ausfahrt wurde leider abgesagt...", }
),
Err(CoxHelpError::AlreadyRegisteredAsCox) => { Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!") Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
} }
@@ -149,10 +147,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
Redirect::to("/planned"), Redirect::to("/planned"),
"Du hast dich bereits als Ruderer angemeldet!", "Du hast dich bereits als Ruderer angemeldet!",
), ),
Err(CoxHelpError::DetailsLocked) => Flash::error( Err(CoxHelpError::DetailsLocked) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.")
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.", }
),
} }
} else { } else {
Flash::error(Redirect::to("/planned"), "Event gibt's nicht") Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
@@ -200,10 +197,9 @@ async fn remove(
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!") Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
} }
Err(TripHelpDeleteError::DetailsLocked) => Flash::error( Err(TripHelpDeleteError::DetailsLocked) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!")
"Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!", }
),
Err(TripHelpDeleteError::CoxNotHelping) => { Err(TripHelpDeleteError::CoxNotHelping) => {
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...") Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
} }

View File

@@ -2,7 +2,6 @@ use std::env;
use chrono::Utc; use chrono::Utc;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
fs::TempFile, fs::TempFile,
get, get,
@@ -10,9 +9,9 @@ use rocket::{
post, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;

View File

@@ -110,13 +110,10 @@ async fn index(
#[get("/show", rank = 3)] #[get("/show", rank = 3)]
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template { async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
let logs = Logbook::completed(db).await; let logs = Logbook::completed(db).await;
let boats = Boat::all(db).await;
let users = User::all(db).await;
let logtypes = LogType::all(db).await;
Template::render( Template::render(
"log.completed", "log.completed",
context!(logs, boats, users, logtypes, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await), context!(logs, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
) )
} }
@@ -218,77 +215,31 @@ async fn create_logbook(
user: &DonauLinzUser, user: &DonauLinzUser,
smtp_pw: &str, smtp_pw: &str,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
match Logbook::create(db, data.into_inner(), user, smtp_pw).await { match Logbook::create(
Ok(msg) => Flash::success( db,
Redirect::to("/log"), data.into_inner(),
format!("Ausfahrt erfolgreich hinzugefügt{msg}"), user, smtp_pw
), )
Err(LogbookCreateError::BoatAlreadyOnWater) => { .await
Flash::error(Redirect::to("/log"), "Boot schon am Wasser") {
} Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error( Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
Redirect::to("/log"), Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
format!("Ruderer {} schon am Wasser", rower.name),
),
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"), Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
Err(LogbookCreateError::BoatNotFound) => { Err(LogbookCreateError::BoatNotFound) => Flash::error(Redirect::to("/log"), "Boot gibt's ned"),
Flash::error(Redirect::to("/log"), "Boot gibt's ned") Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
} Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(Redirect::to("/log"), format!("Fehler bei Ruderer {rower}: {e}")),
Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error( Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(Redirect::to("/log"), "Ankunftszeit kann nicht vor der Abfahrtszeit sein"),
Redirect::to("/log"), Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(Redirect::to("/log"), "Schiffsführer darf dieses Boot nicht verwenden"),
format!( Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(Redirect::to("/log"), "Steuerperson nicht in Liste der Ruderer!"),
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)" Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"),
), Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"),
), Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error( Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten an den Vorstand (info@rudernlinz.at)."),
Redirect::to("/log"), Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
format!("Fehler bei Ruderer {rower}: {e}"), Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
), Err(LogbookCreateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error( Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
Redirect::to("/log"),
"Ankunftszeit kann nicht vor der Abfahrtszeit sein",
),
Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(
Redirect::to("/log"),
"Schiffsführer darf dieses Boot nicht verwenden",
),
Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(
Redirect::to("/log"),
"Steuerperson nicht in Liste der Ruderer!",
),
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(
Redirect::to("/log"),
"Schiffsführer nicht in Liste der Ruderer!",
),
Err(LogbookCreateError::NotYourEntry) => {
Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!")
}
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(
Redirect::to("/log"),
"Ankunftszeit gesetzt aber nicht Distanz + Strecke",
),
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(
Redirect::to("/log"),
"Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten an den Vorstand (info@rudernlinz.at).",
),
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(
Redirect::to("/log"),
"Handsteuer-Status dieses Boots kann nicht verändert werden.",
),
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(
Redirect::to("/log"),
format!(
"KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut."
),
),
Err(LogbookCreateError::AlreadyFinalized) => Flash::error(
Redirect::to("/log"),
"Logbucheintrag wurde bereits abgeschlossen.",
),
Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
Redirect::to("/log"),
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
),
} }
} }
@@ -361,13 +312,7 @@ async fn update(
let data = data.into_inner(); let data = data.into_inner();
let Some(logbook) = Logbook::find_by_id(db, data.id).await else { let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
return Flash::error( return Flash::error(Redirect::to("/log"), format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
Redirect::to("/log"),
format!(
"Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt",
data.id
),
);
}; };
match logbook.update(db, data.clone(), &user.user).await { match logbook.update(db, data.clone(), &user.user).await {
@@ -410,34 +355,12 @@ async fn home_logbook(
match logbook.home(db,user, data.into_inner(), smtp_pw).await { match logbook.home(db,user, data.into_inner(), smtp_pw).await {
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error( Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
Redirect::to("/log"), Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@rudernlinz.at."),
format!( Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)" Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
), Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
), Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit."),
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(
Redirect::to("/log"),
"Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@rudernlinz.at.",
),
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(
Redirect::to("/log"),
format!(
"KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut."
),
),
Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(
Redirect::to("/log"),
"Logbucheintrag wurde bereits abgeschlossen.",
),
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
Redirect::to("/log"),
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
),
Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(
Redirect::to("/log"),
"Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit.",
),
Err(e) => Flash::error( Err(e) => Flash::error(
Redirect::to("/log"), Redirect::to("/log"),
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"), format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),

View File

@@ -1,4 +1,4 @@
use rocket::{Route, State, get, http::ContentType, routes}; use rocket::{get, http::ContentType, routes, Route, State};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{event::Event, personal::cal::get_personal_cal, user::User}; use crate::model::{event::Event, personal::cal::get_personal_cal, user::User};

View File

@@ -2,7 +2,7 @@ use std::{fs::OpenOptions, io::Write};
use chrono::{Datelike, Local}; use chrono::{Datelike, Local};
use rocket::{ use rocket::{
Build, Data, FromForm, Request, Rocket, State, catch, catchers, catch, catchers,
fairing::{AdHoc, Fairing, Info, Kind}, fairing::{AdHoc, Fairing, Info, Kind},
form::Form, form::Form,
fs::FileServer, fs::FileServer,
@@ -13,6 +13,7 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
Build, Data, FromForm, Request, Rocket, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serde::Deserialize; use serde::Deserialize;
@@ -20,7 +21,6 @@ use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::{ use crate::{
SCHECKBUCH,
model::{ model::{
logbook::Logbook, logbook::Logbook,
notification::Notification, notification::Notification,
@@ -28,6 +28,7 @@ use crate::{
role::Role, role::Role,
user::{User, UserWithDetails}, user::{User, UserWithDetails},
}, },
SCHECKBUCH,
}; };
pub(crate) mod admin; pub(crate) mod admin;
@@ -201,10 +202,7 @@ async fn blogpost_unpublished(
#[catch(403)] //forbidden #[catch(403)] //forbidden
fn forbidden_error() -> Flash<Redirect> { fn forbidden_error() -> Flash<Redirect> {
Flash::error( Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
Redirect::to("/"),
"Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.",
)
} }
struct Usage {} struct Usage {}
@@ -330,13 +328,11 @@ mod test {
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert!( assert!(response
response
.into_string() .into_string()
.await .await
.unwrap() .unwrap()
.contains("Ruderassistent") .contains("Ruderassistent"));
);
} }
#[sqlx::test] #[sqlx::test]

View File

@@ -1,7 +1,7 @@
use rocket::{ use rocket::{
Route, State, get, get,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, Route, State,
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -1,15 +1,14 @@
use rocket::{ use rocket::{
Route, State, get, get,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, Route, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::{ use crate::{
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
model::{ model::{
log::Log, log::Log,
tripdetails::TripDetails, tripdetails::TripDetails,
@@ -17,6 +16,7 @@ use crate::{
user::{AllowedForPlannedTripsUser, User, UserWithDetails}, user::{AllowedForPlannedTripsUser, User, UserWithDetails},
usertrip::{UserTrip, UserTripDeleteError, UserTripError}, usertrip::{UserTrip, UserTripDeleteError, UserTripError},
}, },
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
}; };
#[get("/")] #[get("/")]
@@ -88,8 +88,7 @@ async fn join(
"User {} registered the guest '{}' for trip_details.id={}", "User {} registered the guest '{}' for trip_details.id={}",
user.name, registered_user, trip_details_id user.name, registered_user, trip_details_id
), ),
) ).await;
.await;
} }
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!") Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!")
} }
@@ -99,10 +98,9 @@ async fn join(
Err(UserTripError::AlreadyRegistered) => { Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!") Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!")
} }
Err(UserTripError::AlreadyRegisteredAsCox) => Flash::error( Err(UserTripError::AlreadyRegisteredAsCox) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!")
"Du hilfst bereits als Steuerperson aus!", }
),
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error( Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/planned"), Redirect::to("/planned"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)", "Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
@@ -162,10 +160,7 @@ async fn remove_guest(
) )
.await; .await;
Flash::error( Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
Redirect::to("/planned"),
"Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!",
)
} }
Err(UserTripDeleteError::GuestNotParticipating) => { Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.") Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
@@ -216,10 +211,7 @@ async fn remove(
) )
.await; .await;
Flash::error( Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
Redirect::to("/planned"),
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
)
} }
Err(UserTripDeleteError::NotVisibleToUser) => { Err(UserTripDeleteError::NotVisibleToUser) => {
Log::create( Log::create(
@@ -231,10 +223,7 @@ async fn remove(
) )
.await; .await;
Flash::error( Flash::error(Redirect::to("/planned"), "Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...")
Redirect::to("/planned"),
"Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...",
)
} }
Err(_) => { Err(_) => {
panic!("Not possible to be here"); panic!("Not possible to be here");

View File

@@ -1,5 +1,5 @@
use rocket::{Route, State, get, routes}; use rocket::{get, routes, Route, State};
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{

View File

@@ -1,11 +1,10 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;

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_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(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')); 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

@@ -1,37 +0,0 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/boat" as boat %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full dark:text-white">
<h1 class="h1">Rolle</h1>
<div class="grid ">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Rolle</h2>
{% for role in roles %}
<div data-filterable="true"
data-filter="{{ role.name }}"
class="w-full border-t">
<form action="/admin/role/{{ role.id }}"
data-filterable="true"
method="post"
class="bg-white dark:bg-primary-900 p-4 w-full">
<div class="w-full">
<input type="hidden" name="id" value="{{ role.id }}" />
<div class="font-bold mb-1 text-black dark:text-white">
{{ role.name }}
<br />
</div>
<div class="grid md:grid-cols-3 gap-3">
{{ macros::input(label='Formatierter Name', name='formatted_name', type='text', value=role.formatted_name) }}
{{ macros::input(label='Beschreibung', name='desc', type='text', value=role.desc) }}
<input value="Ändern" type="submit" class="w-28 btn btn-primary" />
</div>
</div>
</form>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -8,136 +8,25 @@
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white"> <summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">
Neue Person hinzufügen Neue Person hinzufügen
</summary> </summary>
<form action="/admin/user/new"
<div class="grid sm:grid-cols-3 gap-3 mt-3"> onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
<button type="button"
onclick="document.getElementById('add-clubuser').showModal()"
class="btn btn-primary">Vereinsmitglied</button>
<button type="button"
onclick="document.getElementById('add-scheckbuch').showModal()"
class="btn btn-dark">Scheckbuch</button>
<button type="button"
onclick="document.getElementById('add-schnupperkurs').showModal()"
class="btn btn-dark">Schnupperkurs</button>
</div>
<dialog id="add-clubuser"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('add-clubuser').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('add-clubuser').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<h2 class="h3 mb-3">Neues Vereinsmitglied</h2>
<form action="/admin/user/new/clubmember"
method="post" method="post"
enctype="multipart/form-data" class="flex mt-4 rounded-md sm:flex items-end justify-between">
class="grid gap-3"> <div class="w-full">
<div> <div>
<label for="membertype" class="text-sm text-gray-600 dark:text-gray-100">Mitgliedstyp</label> <label for="name" class="sr-only">Name</label>
<select name="membertype" id="membertype" class="input rounded-md "> <input type="text"
<option selected="" value="regular">Reguläres Vereinsmitglied</option> name="name"
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option> class="input rounded-md w-100"
<option value="foerdernd">Förderndes Vereinsmitglied</option> placeholder="Name" />
</select>
</div> </div>
{{ macros::input(label='Name', name='name', type="text", required=true) }} </div>
{{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }} <div class="text-right ml-3">
{{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }} <input value="Hinzufügen"
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }}
{{ macros::input(label='Geburtsdatum', name='birthdate', type="date", required=true) }}
{{ macros::input(label='Telefonnummer', name='phone', type="text", required=true) }}
{{ macros::input(label='Adresse', name='address', type="text", required=true) }}
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf', required=true) }}
<input value="Neues Vereinsmitglied anlegen"
type="submit" type="submit"
class="btn btn-primary" /> 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> </form>
</div>
</div>
</dialog>
<dialog id="add-scheckbuch"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('add-scheckbuch').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('add-scheckbuch').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<h2 class="h3 mb-3">Neues Scheckbuch</h2>
<form action="/admin/user/new/scheckbuch"
method="post"
enctype="multipart/form-data"
class="grid gap-3">
{{ macros::input(label='Name', name='name', type="text", required=true) }}
{{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }}
<input value="Neues Scheckbuch anlegen"
type="submit"
class="btn btn-primary" />
</form>
</div>
</div>
</dialog>
<dialog id="add-schnupperkurs"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('add-schnupperkurs').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('add-schnupperkurs').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<form action="/admin/user/new/schnupper"
method="post"
enctype="multipart/form-data"
class="grid gap-3">
<h2 class="h3 mb-3">Neuer Schnupperant</h2>
<div>
<label for="schnupper_type" class="text-sm text-gray-600 dark:text-gray-100">Typ</label>
<select name="schnupper_type" id="schnupper_type" class="input rounded-md ">
<option value="schnupperInterested">Interessiert am Schnupperkurs</option>
<option value="schnupperant">Fixe Schnupperkurs-Anmeldung</option>
</select>
</div>
{{ macros::input(label='Name', name='name', type="text", required=true) }}
{{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }}
{{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }}
<input value="Hinzufügen" type="submit" class="btn btn-primary" />
</form>
</div>
</div>
</dialog>
</details> </details>
{% endif %} {% endif %}
<!-- START filterBar --> <!-- START filterBar -->
@@ -178,10 +67,22 @@
{% for user in users %} {% for user in users %}
<div data-filterable="true" <div data-filterable="true"
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %}" data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %}"
class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative flex justify-between items-center"> class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
<span class="text-black dark:text-white"> <details class="block dark:text-white w-full">
<summary>
<span class="text-black dark:text-white cursor-pointer">
<span class="font-bold"> <span class="font-bold">
{{ user.name }} {{ 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 %}
{% if user.last_access %}&bullet; ⏳&nbsp;{{ user.last_access | date }}{% endif %} {% if user.last_access %}&bullet; ⏳&nbsp;{{ user.last_access | date }}{% endif %}
</span> </span>
<small class="block text-gray-600 dark:text-gray-100"> <small class="block text-gray-600 dark:text-gray-100">
@@ -192,7 +93,89 @@
{% endfor %} {% endfor %}
</small> </small>
</span> </span>
<a href="/admin/user/{{ user.id }}" class="btn btn-dark ml-3">{% include "includes/pencil" %}</a> </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"
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 %}
{% for role in roles %}
{% if not role.cluster %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
{% endif %}
{% endfor %}
<hr class="sm:col-span-2 lg:col-span-4 my-3" />
{% if user.membership_pdf %}
<a href="/admin/user/{{ user.id }}/membership"
class="text-black dark:text-white">Beitrittserklärung herunterladen</a>
{% else %}
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', id=loop.index, type="file", readonly=allowed_to_edit == false, accept='application/pdf') }}
{% endif %}
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address, readonly=allowed_to_edit == false) }}
{% if allowed_to_edit %}
{{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }}
{% endif %}
</div>
</div>
{% if allowed_to_edit %}
<div class="mt-3 text-right">
<a href="/admin/user/{{ user.id }}/delete"
class="w-28 btn btn-alert"
onclick="return confirm('Wirklich löschen?');">
{% include "includes/delete-icon" %}
Löschen
</a>
<input value="Ändern" type="submit" class="w-28 btn btn-primary ml-1" />
</div>
{% endif %}
</form>
</details>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -3,12 +3,10 @@
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<div class="max-w-screen-lg w-full"> <div class="max-w-screen-lg w-full">
{% if "admin" in loggedin_user.roles or "Vorstand" in loggedin_user.roles %}
<a href="/admin/user" class="link link-primary link-no-underline">&larr; Userverwaltung</a>
{% endif %}
<h1 class="h1">{{ user.name }}</h1> <h1 class="h1">{{ user.name }}</h1>
<div class="grid sm:grid-cols-2 gap-8 my-8"> <div class="grid sm:grid-cols-2 gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow"> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2"> <h2 class="h2">
Grunddaten Grunddaten
<br /> <br />
@@ -16,11 +14,11 @@
{% if user.last_access %} {% if user.last_access %}
Zuletzt eingeloggt am {{ user.last_access | date(format="%d. %m. %Y") }} Zuletzt eingeloggt am {{ user.last_access | date(format="%d. %m. %Y") }}
{% else %} {% else %}
App-Boykott 😢 {{ user.name }} hat sich noch nie eingeloggt.
{% endif %} {% endif %}
</small> </small>
</h2> </h2>
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600"> <div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3 grid gap-3"> <div class="py-3 grid gap-3">
<form action="/admin/user/{{ user.id }}/change-mail" method="post"> <form action="/admin/user/{{ user.id }}/change-mail" method="post">
{{ macros::inputgroup(label='Mailadresse', name='mail', type="text", value=user.mail, readonly=not allowed_to_edit) }} {{ macros::inputgroup(label='Mailadresse', name='mail', type="text", value=user.mail, readonly=not allowed_to_edit) }}
@@ -31,29 +29,12 @@
<form action="/admin/user/{{ user.id }}/change-nickname" method="post"> <form action="/admin/user/{{ user.id }}/change-nickname" method="post">
{{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }} {{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }}
</form> </form>
<form action="/admin/user/{{ user.id }}/change-financial" method="post"> <span>Notizen: to be replaced with activity :-)</span>
{% if user_financial %}
{{ macros::selectgroup(label="Finanzielles", data=financial, selected_id=user_financial.id, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
{% else %}
{{ macros::selectgroup(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
{% endif %}
</form>
{% if allowed_to_edit %}
<form action="/admin/user/{{ user.id }}/new-note" method="post">
{{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }}
</form>
{% endif %}
{% if user.pw and allowed_to_edit %}
<div>
<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>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow"> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2"> <h2 class="h2">
Mitgliedschaft Mitgliedschaft
<br /> <br />
@@ -74,7 +55,7 @@
{% endif %} {% endif %}
</small> </small>
</h2> </h2>
<div class="mx-3"> <div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
{% if is_clubmember %} {% if is_clubmember %}
<div class="py-3 grid gap-3"> <div class="py-3 grid gap-3">
<form action="/admin/user/{{ user.id }}/change-member-since" method="post"> <form action="/admin/user/{{ user.id }}/change-member-since" method="post">
@@ -86,21 +67,13 @@
<form action="/admin/user/{{ user.id }}/change-address" method="post"> <form action="/admin/user/{{ user.id }}/change-address" method="post">
{{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }} {{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }}
</form> </form>
<form action="/admin/user/{{ user.id }}/change-skill" method="post">
{% if user_skill %}
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, selected_id=user_skill.id, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
{% else %}
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
{% endif %}
</form>
<form action="/admin/user/{{ user.id }}/change-family" method="post"> <form action="/admin/user/{{ user.id }}/change-family" method="post">
{{ macros::selectgroup(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen', readonly=not allowed_to_edit) }} {{ macros::selectgroup(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen', readonly=not allowed_to_edit) }}
</form> </form>
</div> </div>
<div class="py-3"> <div class="py-3">
{% if user.membership_pdf %} {% if user.membership_pdf %}
<a href="/admin/user/{{ user.id }}/membership" <a href="/admin/user/{{ user.id }}/membership" class="link link-primary">Beitrittserklärung herunterladen</a>
class="link link-primary link-no-underline">Beitrittserklärung herunterladen &darr;</a>
{% else %} {% else %}
⚠️ Aktuell gibt's keine Beitrittserklärung 😢 ⚠️ Aktuell gibt's keine Beitrittserklärung 😢
{% if allowed_to_edit %} {% if allowed_to_edit %}
@@ -120,9 +93,6 @@
{% if allowed_to_edit %} {% if allowed_to_edit %}
<div class="py-3"> <div class="py-3">
<div class="mt-3 text-right"> <div class="mt-3 text-right">
<button type="button"
onclick="document.getElementById('change-member-type').showModal()"
class="btn btn-dark">Mitgliedsstatus ändern</button>
<a href="/admin/user/{{ user.id }}/delete" <a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert" class="btn btn-alert"
onclick="return confirm('Ist {{ user.name }} wirklich aus dem Verein ausgetreten?');"> onclick="return confirm('Ist {{ user.name }} wirklich aus dem Verein ausgetreten?');">
@@ -131,111 +101,18 @@
</a> </a>
</div> </div>
</div> </div>
<dialog id="change-member-type"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('change-member-type').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('change-member-type').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<form action="/admin/user/{{ user.id }}/change-membertype"
method="post"
enctype="multipart/form-data"
class="grid gap-3">
<div>
<label for="membertype" class="text-sm text-gray-600 dark:text-gray-100">Mitgliedsstatus</label>
<select name="membertype" id="membertype" class="input rounded-md ">
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
<option value="foerdernd">Förderndes Vereinsmitglied</option>
</select>
</div>
<input value="Ändern" type="submit" class="btn btn-primary" />
</form>
</div>
</div>
</dialog>
{% endif %} {% endif %}
{% elif "Scheckbuch" in member %} {% elif "Scheckbuch" in member %}
{% if allowed_to_edit %}
<div class="grid gap-3 pb-3"> <div class="grid gap-3 pb-3">
<div class="max-h-60 overflow-y-scroll">
{% for log in logbook %} {% for log in logbook %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=false) }} {{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=false) }}
{% endfor %} {% endfor %}
</div>
</div>
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert"
onclick="return confirm('Willst du die Daten von {{ user.name }} wirklich? Seine restlichen Scheckbuch-Ausfahrten entfallen damit...');">
{% include "includes/delete-icon" %}
Daten löschen
</a>
</div>
{% endif %}
{% elif "SchnupperInterest" in member %}
{% if allowed_to_edit %}
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-schnupperant"
class="btn btn-dark"
onclick="return confirm('Hat sich \'{{ user.name }}\' wirklich zum Kurs angemeldet?');">Zum Schnupperkurs angemeldet</a>
</div>
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-scheckbuch"
class="btn btn-dark"
onclick="return confirm('Willst du \'{{ user.name }}\' wirklich auf ein Scheckbuch umwandeln?');">In Scheckbuch umwandeln</a>
</div>
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert"
onclick="return confirm('Ist {{ user.name }} wirklich nicht mehr am Schnupperkurs interessiert?');">
{% include "includes/delete-icon" %}
Daten löschen
</a>
</div>
{% endif %}
{% elif "Schnupperant" in member %}
{% if allowed_to_edit %}
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/schnupperant-to-schnupperinterest"
class="btn btn-dark"
onclick="return confirm('Hat sich \'{{ user.name }}\' wirklich vom Schnupperkurs abgemeldet?');">Vom Kurs abgemeldet</a>
</div>
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/schnupperant-to-scheckbuch"
class="btn btn-dark"
onclick="return confirm('Willst du \'{{ user.name }}\' wirklich auf ein Scheckbuch umwandeln?');">In Scheckbuch umwandeln</a>
</div>
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert"
onclick="return confirm('Ist {{ user.name }} wirklich nicht mehr am Schnupperkurs interessiert?');">
{% include "includes/delete-icon" %}
Daten löschen
</a>
</div>
{% endif %}
{% endif %}
{% if "Scheckbuch" in member or "Schnupperant" in member %}
{% if allowed_to_edit %}
<div class="grid gap-3 pb-3 mt-3">
<button type="button" <button type="button"
onclick="document.getElementById('call-for-action').showModal()" onclick="document.getElementById('call-for-action').showModal()"
class="btn btn-primary">Zu Vereinsmitglied umwandeln</button> class="btn btn-primary">Zu Vereinsmitglied umwandeln</button>
</div> </div>
<dialog id="call-for-action" <dialog id="call-for-action"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" class="max-w-screen-sm w-full dark:bg-primary-600 dark:text-white rounded-md"
onclick="document.getElementById('call-for-action').close()"> onclick="document.getElementById('call-for-action').close()">
<div onclick="event.stopPropagation();" class="p-3"> <div onclick="event.stopPropagation();" class="p-3">
<button type="button" <button type="button"
@@ -251,23 +128,11 @@
</svg> </svg>
</button> </button>
<div class="mt-8"> <div class="mt-8">
{% if "Scheckbuch" in member %} <form action="/admin/user/{{ user.id }}/scheckbook-to-regular"
{% set action = "scheckbook-to-regular" %}
{% elif "Schnupperant" in member %}
{% set action = "schnupperant-to-regular" %}
{% endif %}
<form action="/admin/user/{{ user.id }}/{{ action }}"
method="post" method="post"
enctype="multipart/form-data" enctype="multipart/form-data"
class="grid gap-3"> class="grid gap-3">
<div> Type: Select -> normales Mitglied, förderndes Mitglied, unterstützendes Mitglied
<label for="membertype" class="text-sm text-gray-600 dark:text-gray-100">Mitgliedstyp</label>
<select name="membertype" id="membertype" class="input rounded-md ">
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
<option value="foerdernd">Förderndes Vereinsmitglied</option>
</select>
</div>
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }} {{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }}
{{ macros::input(label='Geburtsdatum', name='birthdate', type="date", value=user.birthdate, required=true) }} {{ macros::input(label='Geburtsdatum', name='birthdate', type="date", value=user.birthdate, required=true) }}
{{ macros::input(label='Telefonnummer', name='phone', type="text", value=user.phone, required=true) }} {{ macros::input(label='Telefonnummer', name='phone', type="text", value=user.phone, required=true) }}
@@ -281,17 +146,18 @@
</div> </div>
</dialog> </dialog>
{% endif %} {% endif %}
{% endif %}
</div> </div>
</div> </div>
{% if is_clubmember %} {% if is_clubmember %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow"> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Rollen</h2> <h2 class="h2">Rollen</h2>
<div> <div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<ul class="divide-y divide-gray-200 dark:divide-primary-60 w-full"> <div class="py-3">
<ul>
{% for role in user.proper_roles -%} {% for role in user.proper_roles -%}
{% if not role.cluster and not role.hide_in_lists %} {% if not role.cluster and not role.hide_in_lists %}
<li class="flex w-full justify-between items-center p-3 {% if allowed_to_edit %}hover:bg-gray-100 dark:hover:bg-primary-950{% endif %}"> <li class="flex my-2 w-full justify-between items-center hover:bg-gray-100">
<span> <span>
<strong> <strong>
{% if role.formatted_name %} {% if role.formatted_name %}
@@ -312,34 +178,11 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% if allowed_to_edit %} {% if allowed_to_edit %}
<div class="m-3"> <details>
<button type="button" <summary>+ Rolle</summary>
onclick="document.getElementById('role-modal').showModal()" <form action="/admin/user/{{ user.id }}/add-role" method="post">
class="btn btn-primary w-full">Rolle hinzufügen</button> <fieldset>
</div> <select name="role_id">
<dialog id="role-modal"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('role-modal').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('role-modal').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<form action="/admin/user/{{ user.id }}/add-role"
method="post"
class="grid gap-3">
<div>
<label for="role_id" class="text-sm text-gray-600 dark:text-gray-100">Rollen</label>
<select name="role_id" id="role_id" class="input rounded-md ">
{% for role in roles %} {% for role in roles %}
{% if not role.cluster and role not in user.proper_roles and not role.hide_in_lists %} {% if not role.cluster and role not in user.proper_roles and not role.hide_in_lists %}
<option value="{{ role.id }}"> <option value="{{ role.id }}">
@@ -348,24 +191,25 @@
{% else %} {% else %}
{{ role.name }} {{ role.name }}
{% endif %} {% endif %}
{% if role.desc %}({{ role.desc }}){% endif %}
</option> </option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
</div> <input value="Rolle hinzufügen" type="submit" class="btn btn-primary ml-1" />
<input value="Rolle hinzufügen" type="submit" class="btn btn-primary" /> </fieldset>
</form> </form>
</div> </details>
</div>
</dialog>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
{% endif %} {% endif %}
{% if supposed_to_pay %} {% if supposed_to_pay %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow"> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">💸-Beitrag</h2> <h2 class="h2">💸-Beitrag</h2>
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600"> <div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3"> <div class="py-3">
{% if fee %} {% if fee %}
<div> <div>
@@ -385,9 +229,7 @@
{% endif %} {% endif %}
{% else %} {% else %}
{% if "paid" in user.roles %} {% if "paid" in user.roles %}
✅ {% for key, value in member %} ✅ {{ member | keys }} hat schon bezahlt
{% if loop.first %}{{ key }}{% endif %}
{% endfor %} hat schon bezahlt
{% else %} {% else %}
{% for key, value in member %} {% for key, value in member %}
@@ -400,27 +242,90 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow"> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
<h2 class="h2">Aktivitäten</h2> role="alert">
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600"> <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"> <div class="py-3">
<ul class="list-disc ms-4"> <ul class="list-disc ms-4">
{% for activity in activities %} <li>Passwort zurückgesetzt am/um X</li>
<li>{{ activity.created_at | date(format="%d. %m. %Y") }}: {{ activity.text }}</li> <li>Am X beigetreten.</li>
{% else %}
<li>Noch keine Aktivität... Stay tuned 😆</li>
{% endfor %}
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow"> <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> <h2 class="h2">Ergo-Challenge</h2>
<div class="mx-3"> <div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="grid gap-3 pb-3 mt-3"> <div class="py-3">
{{ macros::inputgroup(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }} {{ macros::input(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }}
{{ macros::inputgroup(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }} {{ macros::input(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }}
{{ macros::inputgroup(label='Sex', name='sex', type="text", value=user.sex, 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>
</div> </div>

View File

@@ -183,6 +183,8 @@
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative" <div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
data-filterable="true" data-filterable="true"
data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}"> data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}">
<details>
<summary style="list-style: none;">
{% if log.logtype and not hide_type %} {% if log.logtype and not hide_type %}
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold"> <div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
{% if log.logtype == 1 %} {% if log.logtype == 1 %}
@@ -197,15 +199,7 @@
</div> </div>
{% endif %} {% endif %}
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}> <div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
{% if allowed_to_edit %} <strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
<a href="#"
onclick="document.getElementById('change-{{ log.id }}').showModal()"
class="link link-black font-bold">{{ log.boat.name }}</a>
{% else %}
<strong class="text-black dark:text-white">
{{ log.boat.name }}
</strong>
{% endif %}
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}} <small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
{% if log.shipmaster_only_steering %} {% if log.shipmaster_only_steering %}
- handgesteuert - handgesteuert
@@ -258,65 +252,35 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
</summary>
{% if allowed_to_edit %} {% if allowed_to_edit %}
<dialog id="change-{{ log.id }}" <form action="/log/update" method="post">
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('change-{{ log.id }}').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('change-{{ log.id }}').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<h2 class="h3">Eintrag '{{ log.boat.name }}' ändern </h2>
<p class="text-center mb-3">ID: {{ log.id }}</p>
<form action="/log/update" method="post" class="grid gap-3">
<input type="hidden" name="id" value="{{ log.id }}" /> <input type="hidden" name="id" value="{{ log.id }}" />
<input type="hidden" name="boat_id" value="{{ log.boat_id }}" />
<input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" />
<input type="hidden" <input type="hidden"
name="steering_person" name="steering_person"
value="{{ log.steering_person }}" /> value="{{ log.steering_person }}" />
{{ macros::select(label="Boot", data=boats, name="boat_id", id="boat_id{{ log.id }}", selected_id=log.boat.id ,display=["name", " (","amount_seats", " x)"]) }} Handgesteuert:
{{ macros::select(label="Schiffsführer", data=log.rowers, name="shipmaster", id="shipmaster{{ log.id }}", selected_id=log.shipmaster_user.id) }} <input type="checkbox"
name="shipmaster_only_steering"
{% if log.shipmaster_only_steering %}checked="checked"{% endif %} />
{{ macros::checkbox(label='Handgesteuert', name='shipmaster_only_steering', id=log.shipmaster_only_steering,checked=log.shipmaster_only_steering) }} <input type="datetime-local" name="departure" value="{{ log.departure }}" />
<div> <input type="datetime-local" name="arrival" value="{{ log.arrival }}" />
<label for="departure" class=" text-sm text-gray-600 dark:text-white ">
Abfahrt
</label>
<input type="datetime-local" class="input rounded-md" name="departure" value="{{ log.departure }}" />
</div>
<div>
<label for="arrival" class=" text-sm text-gray-600 dark:text-white ">
Ankunft
</label>
<input type="datetime-local" class="input rounded-md" name="arrival" value="{{ log.arrival }}" />
</div>
<input type="hidden" name="destination" value="{{ log.destination }}" /> <input type="hidden" name="destination" value="{{ log.destination }}" />
<input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" /> <input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" />
<input type="hidden" name="comments" value="{{ log.comments }}" /> <input type="hidden" name="comments" value="{{ log.comments }}" />
<input type="hidden" name="logtype" value="{{ log.logtype }}" /> <input type="hidden" name="logtype" value="{{ log.logtype }}" />
<input type="submit" class="btn btn-primary" value="Updaten" /> <input type="submit" value="Updaten" />
</form> </form>
<a href="/log/{{ log.id }}/delete" <a href="/log/{{ log.id }}/delete"
class="w-28 btn btn-alert mt-3" class="w-28 btn btn-alert"
onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');"> onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');">
{% include "includes/delete-icon" %} {% include "includes/delete-icon" %}
Löschen Löschen
</a> </a>
</div>
</div>
</dialog>
{% endif %} {% endif %}
</details>
</div> </div>
{% endmacro show_old %} {% endmacro show_old %}
{% macro home(log) %} {% macro home(log) %}

View File

@@ -156,7 +156,7 @@ function setChoiceByLabel(choicesInstance, label) {
</header> </header>
<div class="h-8"></div> <div class="h-8"></div>
{% endmacro header %} {% endmacro header %}
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false, accept='', placeholder='') %} {% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false, accept='') %}
<div class="{{ wrapper_class }}"> <div class="{{ wrapper_class }}">
<label for="{{ name }}" <label for="{{ name }}"
class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}"> class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">
@@ -169,7 +169,7 @@ function setChoiceByLabel(choicesInstance, label) {
{% if required %}required{% endif %} {% if required %}required{% endif %}
value="{{ value }}" value="{{ value }}"
class="input {{ class }}" class="input {{ class }}"
placeholder="{% if hide_label %}{{ label }}{% endif %}{% if placeholder %}{{ placeholder }}{% endif %}" placeholder="{% if hide_label %}{{ label }}{% endif %}"
{% if min is defined %}min="{{ min }}"{% endif %} {% if min is defined %}min="{{ min }}"{% endif %}
{% if autofocus %}autofocus{% endif %} {% if autofocus %}autofocus{% endif %}
{% if accept %}accept="{{ accept }}"{% endif %} {% if accept %}accept="{{ accept }}"{% endif %}
@@ -198,10 +198,7 @@ function setChoiceByLabel(choicesInstance, label) {
{% if pattern %}pattern="{{ pattern }}"{% endif %} {% if pattern %}pattern="{{ pattern }}"{% endif %}
readonly /> readonly />
{% if allowed_to_edit %} {% if allowed_to_edit %}
<button type="button" class="btn btn-dark rounded-l-none-important edit-js"> <button type="button" class="btn btn-dark rounded-l-none-important edit-js">{% include "includes/pencil" %}</button>
{% include "includes/pencil" %}
<span class="sr-only">Bearbeiten</span>
</button>
<input value="x" <input value="x"
type="reset" type="reset"
class="edit-js btn btn-alert btn-hidden rounded-none-important" /> class="edit-js btn btn-alert btn-hidden rounded-none-important" />
@@ -247,10 +244,7 @@ function setChoiceByLabel(choicesInstance, label) {
{% if new_last_entry %}<option value="-1">{{ new_last_entry }}</option>{% endif %} {% if new_last_entry %}<option value="-1">{{ new_last_entry }}</option>{% endif %}
</select> </select>
{% if allowed_to_edit %} {% if allowed_to_edit %}
<button type="button" class="btn btn-dark rounded-l-none-important edit-js"> <button type="button" class="btn btn-dark rounded-l-none-important edit-js">{% include "includes/pencil" %}</button>
{% include "includes/pencil" %}
<span class="sr-only">Bearbeiten</span>
</button>
<input value="x" <input value="x"
type="reset" type="reset"
class="edit-js btn btn-alert btn-hidden rounded-none-important" /> class="edit-js btn btn-alert btn-hidden rounded-none-important" />

View File

@@ -431,9 +431,6 @@
<li class="py-1"> <li class="py-1">
<a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a> <a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a>
</li> </li>
<li class="py-1">
<a href="/admin/role" class="block w-100 py-2 hover:text-primary-600">Rollen</a>
</li>
<li class="py-1"> <li class="py-1">
<a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a> <a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a>
</li> </li>