50 Commits

Author SHA1 Message Date
Marie Birner
8500ba826f [TASK] try to include boat and shipmaster in update popup
Some checks failed
CI/CD Pipeline / test (push) Failing after 22m47s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-05 21:41:52 +02:00
Marie Birner
af2e7cb557 Merge commit '17513bbc386e849962157517d9a4496870b1a064' into single-user-edit-page
Some checks failed
CI/CD Pipeline / test (push) Failing after 22m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-05 21:14:41 +02:00
Marie Birner
81b99ef414 [TASK] edit form on logbook fixes #635 2025-05-05 21:14:23 +02:00
17513bbc38 give frontend stuff to be able to update logbook entriese
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-05 21:11:41 +02:00
c1cecf3b20 don't panic on 'external cox'
Some checks failed
CI/CD Pipeline / test (push) Successful in 16m19s
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-05 20:46:41 +02:00
8e40e563c6 even less clutter!
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-05 20:44:00 +02:00
Marie Birner
bb78441cc4 Merge branch 'single-user-edit-page' of https://git.hofer.link/Ruderverein-Donau-Linz/rowt into single-user-edit-page
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-05 20:37:45 +02:00
Marie Birner
abcf46281b [TASK] style detail view user 2025-05-05 20:37:29 +02:00
c460494be8 Merge branch 'single-user-edit-page' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt into single-user-edit-page
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-05 20:37:20 +02:00
e5560ba536 don't clutter acitvities too much 2025-05-05 20:37:12 +02:00
Marie Birner
b7094bff06 Merge branch 'single-user-edit-page' of https://git.hofer.link/Ruderverein-Donau-Linz/rowt into single-user-edit-page
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-05 20:21:11 +02:00
Marie Birner
6b8b4ba1d2 [TASK] style new user action in list view 2025-05-05 20:20:52 +02:00
03f76b1ae5 Merge branch 'single-user-edit-page' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt into single-user-edit-page
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-05 20:01:48 +02:00
1864ea260c allow to edit roles 2025-05-05 20:01:32 +02:00
Marie Birner
35a5a55140 [TASK] change background color dark view
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m39s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-05 19:44:11 +02:00
9a4dcc0b9d create activity for 'user deleted'; Fixes #985
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m45s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-05 18:23:31 +02:00
43074b3bd7 fix err if scheckbuch has already paid
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-05 11:49:26 +02:00
933e407c64 update prod db :-)
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-05 11:41:32 +02:00
d9e86bf43b allow to create users
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-05 11:35:38 +02:00
ebbb4fe3da don't create activity, e.g. for paid-changes, as this is already logged
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-04 18:48:14 +02:00
9178476013 improve string
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
2025-05-04 18:41:50 +02:00
e853381bd7 Fill acitivites from various activities; Fixes #972
Some checks failed
CI/CD Pipeline / test (push) Failing after 11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-04 18:38:14 +02:00
8777ccb341 format
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-04 10:31:36 +02:00
6362fed909 Be able to update financial and skill; Fixes #974
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
2025-05-04 10:31:15 +02:00
905178e60d add explanation text + reformat
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m33s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-03 21:22:35 +02:00
cd52e76b61 remove already replaced part
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-03 21:17:17 +02:00
151c97aabc add notes
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-03 21:12:04 +02:00
e360c4f06b start with activity + fix tests
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m51s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-03 19:04:13 +02:00
d50501b362 delete large user-update-function 🎉 Fixes #958
Some checks failed
CI/CD Pipeline / test (push) Failing after 10m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-03 18:31:14 +02:00
e6895c8cf1 allow final usertype changes; Fixes #954
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
2025-05-03 18:26:19 +02:00
Marie Birner
7bd863ddf1 [TASK] add schnupper to scheckbuch button
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-03 17:44:27 +02:00
afc32cc41e allow to change from schnupperant to scheckbuch
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
2025-05-03 17:39:03 +02:00
Marie Birner
9dfcb4e2c4 [TASK] change text not logged in yet
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-05-03 17:23:40 +02:00
Marie Birner
149b6afbf5 [TASK] adapt action in user detail view schnupperant or scheckbuch
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-05-03 17:19:51 +02:00
f9a53a703b Merge branch 'single-user-edit-page' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt into single-user-edit-page
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
2025-05-03 17:14:10 +02:00
c8be0c2c22 allow changing from schnupperant 2025-05-03 17:14:03 +02:00
Marie Birner
6c8667973d [TASK] rm cursor pointer and rename mitgliedstyp to mitgliedsstatus
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m19s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-03 16:57:54 +02:00
Marie Birner
07f7dbca12 [BUGFIX] foerdend to foerdernd in html
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
2025-05-03 16:50:22 +02:00
3f29400831 Merge branch 'single-user-edit-page' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt into single-user-edit-page
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-05-03 16:46:45 +02:00
46981c3311 allow to change type between members 2025-05-03 16:46:40 +02:00
Marie Birner
b08fcdc05b [TASK] change list view user
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-05-03 16:46:30 +02:00
Marie Birner
a60606bbe4 [TASK] add icons to back button and download button
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m12s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-03 16:26:43 +02:00
Marie Birner
540031cab4 [TASK] add back button
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-05-03 16:20:24 +02:00
Marie Birner
a93c420630 [TASK] edit membership type button and dialog
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
2025-05-03 16:13:57 +02:00
8dc55a7aad format
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m37s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-03 15:54:52 +02:00
Marie Birner
5b78afff63 [TASK] refactor roles and move elements around
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
2025-05-03 15:54:10 +02:00
25c3a28c7d Merge branch 'single-user-edit-page' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt into single-user-edit-page
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-03 15:09:23 +02:00
2bb42c3f6a allow moving scheckbuch -> unterstützend + fördernd 2025-05-03 15:09:15 +02:00
Marie Birner
d7dec5da29 [TASK] only allowed users change role from scheckbuch to mitglied
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m13s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-03 14:45:57 +02:00
Marie Birner
ffe1745b65 [TASK] add opacity disabled select
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
2025-05-03 14:42:43 +02:00
58 changed files with 3366 additions and 987 deletions

View File

@@ -5,3 +5,7 @@
.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;
}
.h3 {
@apply text-center text-xl uppercase tracking-wide font-bold text-primary-900 dark:text-white;
}

View File

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

View File

@@ -10,4 +10,12 @@
&-white {
@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,6 +225,15 @@ CREATE TABLE IF NOT EXISTS "distance" (
);
CREATE TABLE IF NOT EXISTS "activity" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
text TEXT NOT NULL,
relevant_for TEXT NOT NULL, -- e.g. user_id=123;trip_id=456
keep_until DATETIME
);
CREATE TRIGGER IF NOT EXISTS prevent_multiple_roles_same_cluster
BEFORE INSERT ON user_role
BEGIN

View File

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

113
src/model/activity.rs Normal file
View File

@@ -0,0 +1,113 @@
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 itertools::Itertools;
use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use crate::model::boathouse::Boathouse;

View File

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

View File

@@ -2,8 +2,8 @@ use std::io::Write;
use chrono::{Duration, NaiveDate, NaiveTime};
use ics::{
properties::{DtEnd, DtStart, Summary},
ICalendar,
properties::{DtEnd, DtStart, Summary},
};
use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool};
@@ -578,6 +578,11 @@ mod test {
let today = Local::now().date_naive().format("%Y%m%d").to_string();
let actual = Event::get_ics_feed(&pool).await;
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);
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
);
}
}

View File

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

View File

@@ -823,7 +823,13 @@ ORDER BY departure DESC
if difference > Duration::hours(1) {
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await;
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"));
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")
);
if let Some(destination) = logbook.logbook.destination {
msg.push_str(&format!(", Ziel: {}", destination));
} else {

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
use std::{fmt::Display, ops::DerefMut};
use std::{cmp::Ordering, fmt::Display, ops::DerefMut};
use super::{activity::ActivityBuilder, user::AdminUser};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@@ -13,6 +14,30 @@ pub struct Role {
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
@@ -30,6 +55,27 @@ impl Role {
.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> {
sqlx::query_as!(
Self,
@@ -59,21 +105,6 @@ WHERE id like ?
.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> {
sqlx::query_as!(
Self,
@@ -104,6 +135,30 @@ WHERE name like ?
.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> {
let query = format!(
"SELECT u.name

View File

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

View File

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

View File

@@ -0,0 +1,167 @@
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 crate::{
model::family::Family, BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO,
FOERDERND, REGULAR, RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND,
BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR,
RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND, model::family::Family,
};
use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize;

101
src/model/user/foerdernd.rs Normal file
View File

@@ -0,0 +1,101 @@
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 {
match self {
Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_) => true,
_ => false,
}
matches!(
self,
Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_)
)
}
pub(crate) fn supposed_to_pay(&self) -> bool {
match self {
matches!(
self,
Member::Schnupperant(_)
| Member::Scheckbuch(_)
| Member::Regular(_)
| Member::Foerdernd(_)
| Member::Unterstuetzend(_) => true,
_ => false,
}
| Member::Unterstuetzend(_)
)
}
}

View File

@@ -8,14 +8,13 @@ use rocket::{
http::{Cookie, Status},
request::{FromRequest, Outcome},
time::{Duration, OffsetDateTime},
tokio::io::AsyncReadExt,
Request,
};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::activity::ActivityBuilder;
use super::{
family::Family,
log::Log,
logbook::Logbook,
mail::Mail,
@@ -26,14 +25,19 @@ use super::{
tripdetails::TripDetails,
Day,
};
use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD};
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
use scheckbuch::ScheckbuchUser;
mod basic;
pub(crate) mod clubmember;
mod fee;
pub(crate) mod foerdernd;
pub(crate) mod member;
pub(crate) mod regular;
pub(crate) mod scheckbuch;
pub(crate) mod schnupperant;
pub(crate) mod schnupperinterest;
pub(crate) mod unterstuetzend;
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct User {
@@ -109,74 +113,6 @@ impl User {
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 {
sqlx::query!(
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
@@ -256,6 +192,40 @@ ASKÖ Ruderverein Donau Linz", self.name),
.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> {
sqlx::query_as!(
Role,
@@ -449,22 +419,6 @@ ORDER BY last_access DESC
.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) {
sqlx::query!(
"UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?",
@@ -478,68 +432,6 @@ ORDER BY last_access DESC
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) -> Result<(), String> {
let mut db = db.begin().await.map_err(|e| e.to_string())?;
let mut family_id = data.family_id;
if family_id.is_some_and(|x| x == -1) {
family_id = Some(Family::insert_tx(&mut db).await)
}
if !self.has_membership_pdf_tx(&mut db).await {
if let Some(membership_pdf) = data.membership_pdf {
let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new();
stream.read_to_end(&mut buffer).await.unwrap();
sqlx::query!(
"UPDATE user SET membership_pdf = ? where id = ?",
buffer,
self.id
)
.execute(db.deref_mut())
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
}
sqlx::query!(
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?",
data.dob,
data.weight,
data.sex,
data.member_since_date,
data.birthdate,
data.mail,
data.nickname,
data.notes,
data.phone,
data.address,
family_id,
self.id
)
.execute(db.deref_mut())
.await
.unwrap(); //Okay, because we can only create a User of a valid id
// handle roles
sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id)
.execute(db.deref_mut())
.await
.unwrap();
for role_id in data.roles.into_keys() {
let role = Role::find_by_id_tx(&mut db, role_id.parse::<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(
&self,
db: &mut Transaction<'_, Sqlite>,
@@ -566,29 +458,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).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")
)
})?;
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(())
}
@@ -635,10 +505,11 @@ ASKÖ Ruderverein Donau Linz", self.name),
};
if user.deleted {
Log::create(
db,
format!("User ({name}) already deleted (tried to login)."),
)
ActivityBuilder::new(&format!(
"User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde."
))
.relevant_for_user(&user)
.save(db)
.await;
return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has
//been deleted
@@ -649,7 +520,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
if password_hash == user_pw {
return Ok(user);
}
Log::create(db, format!("User {name} supplied the wrong PW")).await;
ActivityBuilder::new(&format!(
"User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben."
))
.relevant_for_user(&user)
.save(db)
.await;
Err(LoginError::InvalidAuthenticationCombo)
} else {
info!("User {name} has no PW set");
@@ -662,6 +538,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db)
.await
.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) {
@@ -670,6 +552,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db)
.await
.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 {
@@ -689,13 +577,21 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db)
.await
.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) {
pub async fn delete(&self, db: &SqlitePool, deleted_by: &ManageUserUser) {
sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id)
.execute(db)
.await
.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> {
@@ -787,6 +683,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None
)
.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 => {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
@@ -801,6 +701,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None
)
.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;
}
_ => {}
}
@@ -819,6 +723,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
"Fahrtenabzeichen geschafft",
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;
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;
@@ -838,6 +748,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None
)
.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;
}
@@ -898,7 +812,7 @@ macro_rules! special_user {
#[async_trait]
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> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
@@ -1010,9 +924,7 @@ impl UserWithMembershipPdf {
#[cfg(test)]
mod test {
use std::collections::HashMap;
use crate::{tera::admin::user::UserEditForm, testdb};
use crate::testdb;
use super::User;
use sqlx::SqlitePool;
@@ -1059,52 +971,6 @@ mod test {
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]
fn succ_login_with_test_db() {
let pool = testdb!();

View File

@@ -1,22 +1,69 @@
use super::User;
use super::{ManageUserUser, User};
use crate::{
model::{mail::Mail, notification::Notification},
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use rocket::async_trait;
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool;
special_user!(RegularUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied");
special_user!(RegularUser, +"Donau Linz");
impl RegularUser {
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
self.notify_coxes_about_new_regular(db).await;
self.send_welcome_mail_to_user(db, smtp_pw).await?;
Ok(())
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());
}
async fn send_welcome_mail_to_user(
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 {
pub(crate) async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
smtp_pw: &str,
@@ -53,21 +100,55 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).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(())
}
async fn notify_coxes_about_new_regular(&self, db: &SqlitePool) {
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, "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(
db,
&format!(
"Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}",
self.member_since_date.clone().unwrap(),
self.name
),
&format!("Liebe Steuerberechtigte, es gibt ein neues Mitglied: {user} 🎉"),
"Neues Vereinsmitglied",
None,
None,
)
.await;
Ok(())
}
}

View File

@@ -1,10 +1,14 @@
use super::foerdernd::FoerderndUser;
use super::regular::RegularUser;
use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User};
use crate::model::role::Role;
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::{
SCHECKBUCH,
model::{mail::Mail, notification::Notification},
special_user, SCHECKBUCH,
special_user,
};
use chrono::NaiveDate;
use rocket::async_trait;
@@ -14,10 +18,9 @@ use sqlx::SqlitePool;
special_user!(ScheckbuchUser, +"scheckbuch");
impl ScheckbuchUser {
pub(crate) async fn convert_to_regular_user(
self,
async fn set_data_for_clubmember(
&self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
@@ -25,7 +28,6 @@ impl ScheckbuchUser {
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
// Set data
self.user.update_birthdate(db, changed_by, birthdate).await;
self.user
.update_member_since(db, changed_by, member_since)
@@ -37,6 +39,30 @@ impl ScheckbuchUser {
.add_membership_pdf(db, changed_by, membership_pdf)
.await?;
Ok(())
}
pub(crate) async fn convert_to_regular_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
@@ -45,30 +71,167 @@ impl ScheckbuchUser {
// Notify
let regular = RegularUser::new(db, &self.user).await.unwrap();
regular.notify(db, smtp_pw).await?;
regular.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} hatte ein Scheckbuch und ist nun seit {} es ein neues reguläres Mitglied. 🎉",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues Vereinsmitglied",
None,
None,
)
.await;
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(())
}
// TODO: make private
pub(crate) async fn notify(
&self,
db: &SqlitePool,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
self.send_welcome_mail_to_user(db, mail, smtp_pw).await?;
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
self.notify_coxes_about_new_scheckbuch(db).await;
self.send_welcome_mail_to_user(db, smtp_pw).await?;
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(())
}
async fn send_welcome_mail_to_user(
pub(crate) async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(
"Kann Mail nicht versenden, weil der User keine Mailadresse hinterlegt hat.".into(),
);
};
Mail::send_single(
db,
mail,
@@ -104,4 +267,38 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
)
.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

@@ -0,0 +1,402 @@
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

@@ -0,0 +1,162 @@
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

@@ -0,0 +1,101 @@
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::{form::Form, post, routes, Build, FromForm, Rocket, State};
use rocket::{Build, FromForm, Rocket, State, form::Form, post, routes};
use serde_json::json;
use sqlx::SqlitePool;

View File

@@ -96,7 +96,9 @@ struct DailyWeather {
}
fn fetch(api_key: &str) -> Result<Data, String> {
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}");
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}"
);
match ureq::get(&url).call() {
Ok(mut response) => {

View File

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

View File

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

View File

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

View File

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

View File

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

64
src/tera/admin/role.rs Normal file
View File

@@ -0,0 +1,64 @@
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},
};
use futures::future::join_all;
use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{tera::Context, Template};
use rocket::{Route, State, get, request::FlashMessage, routes};
use rocket_dyn_templates::{Template, tera::Context};
use sqlx::SqlitePool;
#[get("/schnupper")]

View File

@@ -1,15 +1,17 @@
use std::collections::HashMap;
use crate::{
model::{
activity::Activity,
family::Family,
log::Log,
logbook::Logbook,
mail::valid_mails,
role::Role,
user::{
member::Member, scheckbuch::ScheckbuchUser, AdminUser, AllowedToEditPaymentStatusUser,
ManageUserUser, User, UserWithDetails, UserWithMembershipPdf,
UserWithRolesAndMembershipPdf, VorstandUser,
clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member,
regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser,
schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser,
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
},
},
tera::Config,
@@ -64,6 +66,7 @@ async fn index(
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
let financial = Role::all_cluster(db, "financial").await;
let roles = Role::all(db).await;
let families = Family::all_with_members(db).await;
@@ -75,6 +78,7 @@ async fn index(
context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users);
context.insert("roles", &roles);
context.insert("financial", &financial);
context.insert("families", &families);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
@@ -95,6 +99,7 @@ async fn index_admin(
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
let user: User = user.user;
let financial = Role::all_cluster(db, "financial").await;
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
let roles = Role::all(db).await;
@@ -107,6 +112,7 @@ async fn index_admin(
context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users);
context.insert("roles", &roles);
context.insert("financial", &financial);
context.insert("families", &families);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
@@ -126,9 +132,20 @@ async fn view(
format!("User mit ID {} gibts ned", user),
));
};
if user.name == "Externe Steuerperson" {
return Err(Flash::error(
Redirect::to("/admin/user"),
"Diese besondere Person kannst du dir leider nicht anschauen, mein lieber neugieriger Ruderant!"
));
}
let member = Member::from(db, user.clone()).await;
let fee = user.fee(db).await;
let activities = Activity::for_user(db, &user).await;
let financial = Role::all_cluster(db, "financial").await;
let user_financial = user.financial(db).await;
let skill = Role::all_cluster(db, "skill").await;
let user_skill = user.skill(db).await;
let user = UserWithRolesAndMembershipPdf::from_user(db, user).await;
@@ -147,7 +164,12 @@ async fn view(
context.insert("is_clubmember", &member.is_club_member());
context.insert("supposed_to_pay", &member.supposed_to_pay());
context.insert("fee", &fee);
context.insert("skill", &skill);
context.insert("user_skill", &user_skill);
context.insert("financial", &financial);
context.insert("user_financial", &user_financial);
context.insert("member", &member);
context.insert("activities", &activities);
context.insert("roles", &roles);
context.insert("families", &families);
context.insert(
@@ -244,26 +266,6 @@ async fn fees_paid(
)
}
#[get("/user/<user>/send-welcome-mail")]
async fn send_welcome_mail(
db: &State<SqlitePool>,
_admin: ManageUserUser,
config: &State<Config>,
user: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, user).await else {
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
};
match user.send_welcome_email(db, &config.smtp_pw).await {
Ok(()) => Flash::success(
Redirect::to("/admin/user"),
format!("Willkommens-Email wurde an {} versandt.", user.name),
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await;
@@ -290,7 +292,7 @@ async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Fla
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
match user {
Some(user) => {
user.delete(db).await;
user.delete(db, &admin).await;
Flash::success(
Redirect::to("/admin/user"),
format!("Benutzer {} gelöscht", user.name),
@@ -300,49 +302,6 @@ async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Fla
}
}
#[derive(FromForm, Debug)]
pub struct UserEditForm<'a> {
pub(crate) id: i32,
pub(crate) dob: Option<String>,
pub(crate) weight: Option<String>,
pub(crate) sex: Option<String>,
pub(crate) roles: HashMap<String, String>,
pub(crate) member_since_date: Option<String>,
pub(crate) birthdate: Option<String>,
pub(crate) mail: Option<String>,
pub(crate) nickname: Option<String>,
pub(crate) notes: Option<String>,
pub(crate) phone: Option<String>,
pub(crate) address: Option<String>,
pub(crate) family_id: Option<i64>,
pub(crate) membership_pdf: Option<TempFile<'a>>,
}
#[post("/user", data = "<data>", format = "multipart/form-data")]
async fn update(
db: &State<SqlitePool>,
data: Form<UserEditForm<'_>>,
admin: ManageUserUser,
) -> Flash<Redirect> {
let user = User::find_by_id(db, data.id).await;
Log::create(
db,
format!("{} updated user from {user:?} to {data:?}", admin.user.name),
)
.await;
let Some(user) = user else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", data.id),
);
};
match user.update(db, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/admin/user"), "Successfully updated user"),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[derive(FromForm, Debug)]
pub struct MailUpdateForm {
mail: String,
@@ -371,6 +330,34 @@ async fn update_mail(
}
}
#[derive(FromForm, Debug)]
pub struct AddNoteForm {
note: String,
}
#[post("/user/<id>/new-note", data = "<data>")]
async fn add_note(
db: &State<SqlitePool>,
data: Form<AddNoteForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
match user.add_note(db, &admin, &user, &data.note).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Notiz hinzugefügt",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct PhoneUpdateForm {
phone: String,
@@ -469,6 +456,86 @@ async fn update_family(
)
}
#[derive(FromForm, Debug)]
pub struct ChangeSkillForm {
skill_id: String,
}
#[post("/user/<id>/change-skill", data = "<data>")]
async fn change_skill(
db: &State<SqlitePool>,
data: Form<ChangeSkillForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let skill = if data.skill_id.is_empty() {
None
} else {
let Ok(skill_id) = data.skill_id.parse() else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Skill_id is not a number",
);
};
Role::find_by_id(db, skill_id).await
};
match user.change_skill(db, &admin, skill).await {
Ok(()) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Skill erfolgreich geändert",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct ChangeFinancialForm {
financial_id: String,
}
#[post("/user/<id>/change-financial", data = "<data>")]
async fn change_financial(
db: &State<SqlitePool>,
data: Form<ChangeFinancialForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let financial = if data.financial_id.is_empty() {
None
} else {
let Ok(financial_id) = data.financial_id.parse() else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Finacial_id is not a number",
);
};
Role::find_by_id(db, financial_id).await
};
match user.change_financial(db, &admin, financial).await {
Ok(()) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Ermäßigung erfolgreich geändert",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct AddMembershipPDFForm<'a> {
membership_pdf: TempFile<'a>,
@@ -685,37 +752,11 @@ async fn download_membership_pdf(
(ContentType::PDF, user.membership_pdf.unwrap())
}
#[derive(FromForm, Debug)]
struct UserAddForm<'r> {
name: &'r str,
}
#[post("/user/new", data = "<data>")]
async fn create(
db: &State<SqlitePool>,
data: Form<UserAddForm<'_>>,
admin: ManageUserUser,
) -> Flash<Redirect> {
if User::create(db, data.name).await {
Log::create(
db,
format!("{} created new user: {data:?}", admin.user.name),
)
.await;
Flash::success(Redirect::to("/admin/user"), "Successfully created user")
} else {
Flash::error(
Redirect::to("/admin/user"),
format!("User {} already exists", data.name),
)
}
}
#[derive(FromForm, Debug)]
struct UserAddScheckbuchForm<'r> {
name: &'r str,
mail: &'r str,
}
//#[derive(FromForm, Debug)]
//struct UserAddScheckbuchForm<'r> {
// name: &'r str,
// mail: &'r str,
//}
//#[post("/user/new/scheckbuch", data = "<data>")]
//async fn create_scheckbuch(
@@ -764,58 +805,128 @@ struct UserAddScheckbuchForm<'r> {
// Flash::success(Redirect::to("/admin/user/scheckbuch"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt."))
//}
//#[get("/user/move/schnupperant/<id>/to/scheckbuch")]
//async fn schnupper_to_scheckbuch(
// db: &State<SqlitePool>,
// id: i32,
// admin: SchnupperBetreuerUser,
// config: &State<Config>,
//) -> Flash<Redirect> {
// let Some(user) = User::find_by_id(db, id).await else {
// return Flash::error(
// Redirect::to("/admin/schnupper"),
// "user id not found".to_string(),
// );
// };
//
// if !user.has_role(db, "schnupperant").await {
// return Flash::error(
// Redirect::to("/admin/schnupper"),
// "kein schnupperant...".to_string(),
// );
// }
//
// let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
// let paid = Role::find_by_name(db, "paid").await.unwrap();
// user.remove_role(db, &schnupperant).await;
// user.remove_role(db, &paid).await;
//
// let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
// user.add_role(db, &scheckbuch)
// .await
// .expect("just removed 'schnupperant' thus can't have a role with that group");
//
// if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
// user.add_role(db, &no_einschreibgebuehr)
// .await
// .expect("role doesn't have a group");
// }
//
// user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
//
// Log::create(
// db,
// format!(
// "{} created new scheckbuch (from schnupperant): {}",
// admin.name, user.name
// ),
// )
// .await;
// Flash::success(Redirect::to("/admin/schnupper"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap()))
//}
#[derive(FromForm, Debug)]
pub struct SchnupperantToRegularForm<'a> {
membertype: String,
member_since: String,
birthdate: String,
phone: String,
address: String,
membership_pdf: TempFile<'a>,
}
#[post("/user/<id>/schnupperant-to-regular", data = "<data>")]
async fn schnupperant_to_regular(
db: &State<SqlitePool>,
data: Form<SchnupperantToRegularForm<'_>>,
admin: ManageUserUser,
config: &State<Config>,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
format!(
"Geburtsdatum {} ist nicht im YYYY-MM-DD Format",
&data.birthdate
),
);
};
let Ok(member_since) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
format!(
"Beitrittsdatum {} ist nicht im YYYY-MM-DD Format",
&data.birthdate
),
);
};
let Some(user) = SchnupperantUser::new(db, &user).await else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"User ist kein Schnupperant",
);
};
let Ok(phone) = data.phone.clone().try_into() else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Vereinsmitglied braucht eine Telefonnummer",
);
};
let Ok(address) = data.address.clone().try_into() else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Vereinsmitglied braucht eine Adresse",
);
};
let response = match &*data.membertype {
"regular" => {
user.convert_to_regular_user(
db,
&config.smtp_pw,
&admin,
&member_since,
&birthdate,
phone,
address,
&data.membership_pdf,
)
.await
}
"unterstuetzend" => {
user.convert_to_unterstuetzend_user(
db,
&config.smtp_pw,
&admin,
&member_since,
&birthdate,
phone,
address,
&data.membership_pdf,
)
.await
}
"foerdernd" => {
user.convert_to_foerdernd_user(
db,
&config.smtp_pw,
&admin,
&member_since,
&birthdate,
phone,
address,
&data.membership_pdf,
)
.await
}
_ => {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Membertype gibts ned",
);
}
};
match response {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", id)),
"Mitgliedstyp umgewandelt und Infos versendet",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct ScheckToRegularForm<'a> {
membertype: String,
member_since: String,
birthdate: String,
phone: String,
@@ -875,9 +986,9 @@ async fn scheckbook_to_regular(
"Vereinsmitglied braucht eine Adresse",
);
};
match user
.convert_to_regular_user(
let response = match &*data.membertype {
"regular" => {
user.convert_to_regular_user(
db,
&config.smtp_pw,
&admin,
@@ -888,7 +999,42 @@ async fn scheckbook_to_regular(
&data.membership_pdf,
)
.await
{
}
"unterstuetzend" => {
user.convert_to_unterstuetzend_user(
db,
&config.smtp_pw,
&admin,
&member_since,
&birthdate,
phone,
address,
&data.membership_pdf,
)
.await
}
"foerdernd" => {
user.convert_to_foerdernd_user(
db,
&config.smtp_pw,
&admin,
&member_since,
&birthdate,
phone,
address,
&data.membership_pdf,
)
.await
}
_ => {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Membertype gibts ned",
);
}
};
match response {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", id)),
"Mitgliedstyp umgewandelt und Infos versendet",
@@ -897,23 +1043,401 @@ async fn scheckbook_to_regular(
}
}
#[derive(FromForm, Debug)]
pub struct ChangeMembertypeForm {
membertype: String,
}
#[post("/user/<id>/change-membertype", data = "<data>")]
async fn change_membertype(
db: &State<SqlitePool>,
admin: ManageUserUser,
data: Form<ChangeMembertypeForm>,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Some(user) = ClubMemberUser::new(db, &user).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User {user} ist kein Vereinsmitglied"),
);
};
let response = match &*data.membertype {
"regular" => user.move_to_regular(db, &admin).await,
"unterstuetzend" => user.move_to_unterstuetzend(db, &admin).await,
"foerdernd" => user.move_to_foerdernd(db, &admin).await,
_ => {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Membertype gibt's ned",
);
}
};
match response {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", id)),
"Mitgliedstyp umgewandelt und Infos versendet",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
}
}
#[get("/user/<id>/schnupperant-to-scheckbuch")]
async fn schnupperant_to_scheckbook(
db: &State<SqlitePool>,
admin: ManageUserUser,
config: &State<Config>,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Some(user) = SchnupperantUser::new(db, &user).await else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
format!("User {user} ist kein Schnupperant"),
);
};
match user.move_to_scheckbook(db, &admin, &config.smtp_pw).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", id)),
"Mitgliedstyp umgewandelt und Infos versendet",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
}
}
#[get("/user/<id>/schnupperinterest-to-schnupperant")]
async fn schnupperinterest_to_schnupperant(
db: &State<SqlitePool>,
admin: ManageUserUser,
config: &State<Config>,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Some(user) = SchnupperInterestUser::new(db, &user).await else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
format!("User {user} ist kein Schnupperinteressierter"),
);
};
match user.move_to_schnupperant(db, &admin, &config.smtp_pw).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", id)),
"Mitgliedstyp umgewandelt und Infos versendet",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
}
}
#[get("/user/<id>/schnupperant-to-schnupperinterest")]
async fn schnupperant_to_schnupperinterest(
db: &State<SqlitePool>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Some(user) = SchnupperantUser::new(db, &user).await else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
format!("User {user} ist kein Schnupperant"),
);
};
match user.move_to_schnupperinterest(db, &admin).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", id)),
"Mitgliedstyp umgewandelt.",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
}
}
#[get("/user/<id>/schnupperinterest-to-scheckbuch")]
async fn schnupperinterest_to_scheckbuch(
db: &State<SqlitePool>,
admin: ManageUserUser,
config: &State<Config>,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Some(user) = SchnupperInterestUser::new(db, &user).await else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
format!("User {user} ist kein Schnupperinteressierter"),
);
};
match user.move_to_scheckbook(db, &admin, &config.smtp_pw).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", id)),
"Mitgliedstyp umgewandelt und Infos versendet",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct AddClubMemberForm<'a> {
name: String,
mail: String,
financial_id: String,
membertype: String,
member_since: String,
birthdate: String,
phone: String,
address: String,
membership_pdf: TempFile<'a>,
}
#[post("/user/new/clubmember", data = "<data>")]
async fn add_club_member(
db: &State<SqlitePool>,
data: Form<AddClubMemberForm<'_>>,
admin: ManageUserUser,
config: &State<Config>,
) -> Flash<Redirect> {
if !valid_mails(&data.mail) {
return Flash::error(
Redirect::to("/admin/user"),
format!(
"{} ist kein gültiges Format für eine Mailadresse",
&data.mail
),
);
}
let financial = if data.financial_id.is_empty() {
None
} else {
let Ok(financial_id) = data.financial_id.parse() else {
return Flash::error(Redirect::to("/admin/user"), "Finacial_id is not a number");
};
Role::find_by_id(db, financial_id).await
};
let Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
return Flash::error(
Redirect::to("/admin/user/"),
format!(
"Geburtsdatum {} ist nicht im YYYY-MM-DD Format",
&data.birthdate
),
);
};
let Ok(member_since) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") else {
return Flash::error(
Redirect::to("/admin/user"),
format!(
"Beitrittsdatum {} ist nicht im YYYY-MM-DD Format",
&data.birthdate
),
);
};
let Ok(phone) = data.phone.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Vereinsmitglied braucht eine Telefonnummer",
);
};
let Ok(address) = data.address.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Vereinsmitglied braucht eine Adresse",
);
};
let Ok(name) = data.name.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Vereinsmitglied braucht einen Namen",
);
};
let response = match &*data.membertype {
"regular" => {
RegularUser::create(
db,
&admin,
&config.smtp_pw,
name,
&data.mail,
financial,
&birthdate,
&member_since,
phone,
address,
&data.membership_pdf,
)
.await
}
"unterstuetzend" => {
UnterstuetzendUser::create(
db,
&admin,
&config.smtp_pw,
name,
&data.mail,
financial,
&birthdate,
&member_since,
phone,
address,
&data.membership_pdf,
)
.await
}
"foerdernd" => {
FoerderndUser::create(
db,
&admin,
&config.smtp_pw,
name,
&data.mail,
financial,
&birthdate,
&member_since,
phone,
address,
&data.membership_pdf,
)
.await
}
_ => return Flash::error(Redirect::to("/admin/user"), "Membertype gibts ned"),
};
match response {
Ok(_) => Flash::success(Redirect::to("/admin/user"), "Mitglied erfolgreich erstellt"),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[derive(FromForm, Debug)]
pub struct AddScheckbuchForm {
name: String,
mail: String,
}
#[post("/user/new/scheckbuch", data = "<data>")]
async fn add_scheckbuch(
db: &State<SqlitePool>,
data: Form<AddScheckbuchForm>,
admin: ManageUserUser,
config: &State<Config>,
) -> Flash<Redirect> {
if !valid_mails(&data.mail) {
return Flash::error(
Redirect::to("/admin/user"),
format!(
"{} ist kein gültiges Format für eine Mailadresse",
&data.mail
),
);
}
let Ok(name) = data.name.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Scheckbuch braucht einen Namen",
);
};
match ScheckbuchUser::create(db, &admin, &config.smtp_pw, name, &data.mail).await {
Ok(_) => Flash::success(
Redirect::to("/admin/user"),
"Scheckbuch erfolgreich erstellt",
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[derive(FromForm, Debug)]
pub struct AddSchnupperForm {
name: String,
mail: String,
schnupper_type: String,
}
#[post("/user/new/schnupper", data = "<data>")]
async fn add_schnupper(
db: &State<SqlitePool>,
data: Form<AddSchnupperForm>,
admin: ManageUserUser,
config: &State<Config>,
) -> Flash<Redirect> {
if !valid_mails(&data.mail) {
return Flash::error(
Redirect::to("/admin/user"),
format!(
"{} ist kein gültiges Format für eine Mailadresse",
&data.mail
),
);
}
let Ok(name) = data.name.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Schnupperer braucht einen Namen",
);
};
let response = match &*data.schnupper_type {
"schnupperInterested" => SchnupperInterestUser::create(db, &admin, name, &data.mail).await,
"schnupperant" => {
SchnupperantUser::create(db, &admin, &config.smtp_pw, name, &data.mail).await
}
_ => return Flash::error(Redirect::to("/admin/user"), "Schnuppertyp gibts ned"),
};
match response {
Ok(_) => Flash::success(
Redirect::to("/admin/user"),
"Schnupperer erfolgreich erstellt",
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
pub fn routes() -> Vec<Route> {
routes![
index,
index_admin,
view,
resetpw,
update,
create,
//create_scheckbuch,
//schnupper_to_scheckbuch,
delete,
fees,
fees_paid,
scheckbuch,
download_membership_pdf,
send_welcome_mail,
//
// Updates
update_mail,
update_phone,
update_nickname,
@@ -921,10 +1445,23 @@ pub fn routes() -> Vec<Route> {
update_birthdate,
update_address,
update_family,
change_skill,
change_financial,
add_membership_pdf,
add_role,
add_note,
remove_role,
//
// Moves
scheckbook_to_regular,
schnupperant_to_regular,
schnupperant_to_scheckbook,
schnupperinterest_to_schnupperant,
schnupperant_to_schnupperinterest,
schnupperinterest_to_scheckbuch,
change_membertype,
// Add
add_club_member,
add_scheckbuch,
add_schnupper,
]
}

View File

@@ -1,4 +1,5 @@
use rocket::{
FromForm, Request, Route, State,
form::Form,
get,
http::{Cookie, CookieJar},
@@ -8,9 +9,8 @@ use rocket::{
response::{Flash, Redirect},
routes,
time::{Duration, OffsetDateTime},
FromForm, Request, Route, State,
};
use rocket_dyn_templates::{context, tera, Template};
use rocket_dyn_templates::{Template, context, tera};
use sqlx::SqlitePool;
use crate::model::{
@@ -73,7 +73,10 @@ async fn login(
);
}
Err(_) => {
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!");
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!",
);
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
use rocket::{
FromForm, Route, State,
form::Form,
get, post,
response::{Flash, Redirect},
routes, FromForm, Route, State,
routes,
};
use sqlx::SqlitePool;
@@ -137,9 +138,10 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
.await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
}
Err(CoxHelpError::CanceledEvent) => {
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
}
Err(CoxHelpError::CanceledEvent) => Flash::error(
Redirect::to("/planned"),
"Die Ausfahrt wurde leider abgesagt...",
),
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
}
@@ -147,9 +149,10 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
Redirect::to("/planned"),
"Du hast dich bereits als Ruderer angemeldet!",
),
Err(CoxHelpError::DetailsLocked) => {
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.")
}
Err(CoxHelpError::DetailsLocked) => 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.",
),
}
} else {
Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
@@ -197,9 +200,10 @@ async fn remove(
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
}
Err(TripHelpDeleteError::DetailsLocked) => {
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!")
}
Err(TripHelpDeleteError::DetailsLocked) => 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!",
),
Err(TripHelpDeleteError::CoxNotHelping) => {
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
}

View File

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

View File

@@ -110,10 +110,13 @@ async fn index(
#[get("/show", rank = 3)]
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
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(
"log.completed",
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
context!(logs, boats, users, logtypes, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
)
}
@@ -215,31 +218,77 @@ async fn create_logbook(
user: &DonauLinzUser,
smtp_pw: &str,
) -> Flash<Redirect> {
match Logbook::create(
db,
data.into_inner(),
user, smtp_pw
)
.await
{
Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
match Logbook::create(db, data.into_inner(), user, smtp_pw).await {
Ok(msg) => Flash::success(
Redirect::to("/log"),
format!("Ausfahrt erfolgreich hinzugefügt{msg}"),
),
Err(LogbookCreateError::BoatAlreadyOnWater) => {
Flash::error(Redirect::to("/log"), "Boot schon am Wasser")
}
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(
Redirect::to("/log"),
format!("Ruderer {} schon am Wasser", rower.name),
),
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"), "Boot gesperrt"),
Err(LogbookCreateError::BoatNotFound) => 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::ArrivalNotAfterDeparture) => Flash::error(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!"),
Err(LogbookCreateError::BoatNotFound) => {
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::ArrivalNotAfterDeparture) => Flash::error(
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!",
),
}
}
@@ -312,7 +361,13 @@ async fn update(
let data = data.into_inner();
let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
return Flash::error(Redirect::to("/log"), format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
return Flash::error(
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 {
@@ -355,12 +410,34 @@ async fn home_logbook(
match logbook.home(db, user, data.into_inner(), smtp_pw).await {
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
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)")),
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(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(
Redirect::to("/log"),
format!(
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)"
),
),
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(
Redirect::to("/log"),
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),

View File

@@ -1,4 +1,4 @@
use rocket::{get, http::ContentType, routes, Route, State};
use rocket::{Route, State, get, http::ContentType, routes};
use sqlx::SqlitePool;
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 rocket::{
catch, catchers,
Build, Data, FromForm, Request, Rocket, State, catch, catchers,
fairing::{AdHoc, Fairing, Info, Kind},
form::Form,
fs::FileServer,
@@ -13,7 +13,6 @@ use rocket::{
response::{Flash, Redirect},
routes,
time::{Duration, OffsetDateTime},
Build, Data, FromForm, Request, Rocket, State,
};
use rocket_dyn_templates::Template;
use serde::Deserialize;
@@ -21,6 +20,7 @@ use sqlx::SqlitePool;
use tera::Context;
use crate::{
SCHECKBUCH,
model::{
logbook::Logbook,
notification::Notification,
@@ -28,7 +28,6 @@ use crate::{
role::Role,
user::{User, UserWithDetails},
},
SCHECKBUCH,
};
pub(crate) mod admin;
@@ -202,7 +201,10 @@ async fn blogpost_unpublished(
#[catch(403)] //forbidden
fn forbidden_error() -> Flash<Redirect> {
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.")
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.",
)
}
struct Usage {}
@@ -328,11 +330,13 @@ mod test {
assert_eq!(response.status(), Status::Ok);
assert!(response
assert!(
response
.into_string()
.await
.unwrap()
.contains("Ruderassistent"));
.contains("Ruderassistent")
);
}
#[sqlx::test]

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,25 +3,3 @@ INSERT INTO user(name) VALUES('Marie');
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz'));
INSERT INTO user(name) VALUES('Philipp');
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz'));
ALTER TABLE role ADD COLUMN formatted_name text;
ALTER TABLE role ADD COLUMN desc text;
ALTER TABLE role ADD COLUMN hide_in_lists BOOLEAN NOT NULL DEFAULT false;
UPDATE role SET hide_in_lists=true WHERE name='paid';
UPDATE role SET hide_in_lists=true WHERE name='ergo';
UPDATE role SET desc='Can do ANYTHING.' WHERE name='admin';
UPDATE role SET desc='Kann Ausfahrten ausschreiben und kann alle Boote die in Linz lagern verwenden.', formatted_name='Steuerperson' WHERE name='cox';
UPDATE role SET desc='Darf reparierte Bootschäden verifizieren und wird über Bootsschäden informiert.', formatted_name='Bootsreparateur' WHERE name='tech';
UPDATE role SET desc = null WHERE name='Rechnungsprüfer';
UPDATE role SET desc='Darf Boote die in Ottensheim lagern verwenden.' WHERE name='Rennrudern';
UPDATE role SET desc='Haben zahlreiche Berechtigungen, siehe den Vorstand-Block im Menü.' WHERE name='Vorstand';
UPDATE role SET desc='Können Events ausschreiben und bearbeiten.', formatted_name='Eventmanager' WHERE name='manage_events';
UPDATE role SET desc='Sieht Details zum Schnupperkurs (Teilnehmer, Bezahlstatus, ...)' WHERE name='schnupper-betreuer';
UPDATE role SET desc=null WHERE name='kassier';
UPDATE role SET desc=null WHERE name='schriftfuehrer';
UPDATE role SET desc='Entfernt bei der Gebührenberechnung die Einschreibgebühr.' WHERE name='no-einschreibgebuehr';
UPDATE role SET desc='Es können Logbucheinträge im Nachhinein hinzugefügt werden. Idealerweise diese Rolle nur kurzfristig vergeben.' WHERE name='allow-retroactive-logbookentries';
UPDATE role SET desc='Erlaubt den Login auf der Wordpress-Website um zB Artikel zu schreiben.' WHERE name='allow_website_login';
UPDATE role SET desc='Muss nur den halben Rennruderbeitrag bezahlen (da zB erst in der 2. Jahreshälfte dazugestoßen wurde)' WHERE name='half-rennrudern';
UPDATE role SET desc='Muss keinen Rennruderbeitrag bezahlen, obwohl man in Rennruder-Gruppe ist.' WHERE name='renntrainer';

View File

@@ -0,0 +1,37 @@
{% 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,25 +8,136 @@
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">
Neue Person hinzufügen
</summary>
<form action="/admin/user/new"
onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
<div class="grid sm:grid-cols-3 gap-3 mt-3">
<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"
class="flex mt-4 rounded-md sm:flex items-end justify-between">
<div class="w-full">
enctype="multipart/form-data"
class="grid gap-3">
<div>
<label for="name" class="sr-only">Name</label>
<input type="text"
name="name"
class="input rounded-md w-100"
placeholder="Name" />
<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>
</div>
<div class="text-right ml-3">
<input value="Hinzufügen"
{{ 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") }}
{{ 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"
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>
class="btn btn-primary" />
</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>
{% endif %}
<!-- START filterBar -->
@@ -67,22 +178,10 @@
{% for user in users %}
<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 %}"
class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
<details class="block dark:text-white w-full">
<summary>
<span class="text-black dark:text-white cursor-pointer">
class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative flex justify-between items-center">
<span class="text-black dark:text-white">
<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 %}
{% if user.last_access %}&bullet; ⏳&nbsp;{{ user.last_access | date }}{% endif %}
</span>
<small class="block text-gray-600 dark:text-gray-100">
@@ -93,89 +192,7 @@
{% endfor %}
</small>
</span>
</summary>
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}">✏️</a>
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
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>
<a href="/admin/user/{{ user.id }}" class="btn btn-dark ml-3">{% include "includes/pencil" %}</a>
</div>
{% endfor %}
</div>

View File

@@ -3,10 +3,12 @@
{% extends "base" %}
{% block content %}
<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>
<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 mt-5"
role="alert">
<div class="grid sm:grid-cols-2 gap-8 my-8">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
<h2 class="h2">
Grunddaten
<br />
@@ -14,11 +16,11 @@
{% if user.last_access %}
Zuletzt eingeloggt am {{ user.last_access | date(format="%d. %m. %Y") }}
{% else %}
{{ user.name }} hat sich noch nie eingeloggt.
App-Boykott 😢
{% endif %}
</small>
</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3 grid gap-3">
<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) }}
@@ -29,12 +31,29 @@
<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) }}
</form>
<span>Notizen: to be replaced with activity :-)</span>
<form action="/admin/user/{{ user.id }}/change-financial" method="post">
{% if user_financial %}
{{ macros::selectgroup(label="Finanzielles", data=financial, selected_id=user_financial.id, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
{% else %}
{{ macros::selectgroup(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
{% endif %}
</form>
{% if allowed_to_edit %}
<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 class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
<h2 class="h2">
Mitgliedschaft
<br />
@@ -55,7 +74,7 @@
{% endif %}
</small>
</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="mx-3">
{% if is_clubmember %}
<div class="py-3 grid gap-3">
<form action="/admin/user/{{ user.id }}/change-member-since" method="post">
@@ -67,13 +86,21 @@
<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) }}
</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">
{{ 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>
</div>
<div class="py-3">
{% if user.membership_pdf %}
<a href="/admin/user/{{ user.id }}/membership" class="link link-primary">Beitrittserklärung herunterladen</a>
<a href="/admin/user/{{ user.id }}/membership"
class="link link-primary link-no-underline">Beitrittserklärung herunterladen &darr;</a>
{% else %}
⚠️ Aktuell gibt's keine Beitrittserklärung 😢
{% if allowed_to_edit %}
@@ -93,6 +120,9 @@
{% if allowed_to_edit %}
<div class="py-3">
<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"
class="btn btn-alert"
onclick="return confirm('Ist {{ user.name }} wirklich aus dem Verein ausgetreten?');">
@@ -101,18 +131,111 @@
</a>
</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 %}
{% elif "Scheckbuch" in member %}
{% if allowed_to_edit %}
<div class="grid gap-3 pb-3">
<div class="max-h-60 overflow-y-scroll">
{% for log in logbook %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=false) }}
{% 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"
onclick="document.getElementById('call-for-action').showModal()"
class="btn btn-primary">Zu Vereinsmitglied umwandeln</button>
</div>
<dialog id="call-for-action"
class="max-w-screen-sm w-full dark:bg-primary-600 dark:text-white rounded-md"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('call-for-action').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
@@ -128,11 +251,23 @@
</svg>
</button>
<div class="mt-8">
<form action="/admin/user/{{ user.id }}/scheckbook-to-regular"
{% if "Scheckbuch" in member %}
{% set action = "scheckbook-to-regular" %}
{% elif "Schnupperant" in member %}
{% set action = "schnupperant-to-regular" %}
{% endif %}
<form action="/admin/user/{{ user.id }}/{{ action }}"
method="post"
enctype="multipart/form-data"
class="grid gap-3">
Type: Select -> normales Mitglied, förderndes Mitglied, unterstützendes Mitglied
<div>
<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='Geburtsdatum', name='birthdate', type="date", value=user.birthdate, required=true) }}
{{ macros::input(label='Telefonnummer', name='phone', type="text", value=user.phone, required=true) }}
@@ -146,18 +281,17 @@
</div>
</dialog>
{% endif %}
{% endif %}
</div>
</div>
{% if is_clubmember %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
<h2 class="h2">Rollen</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
<ul>
<div>
<ul class="divide-y divide-gray-200 dark:divide-primary-60 w-full">
{% for role in user.proper_roles -%}
{% if not role.cluster and not role.hide_in_lists %}
<li class="flex my-2 w-full justify-between items-center hover:bg-gray-100">
<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 %}">
<span>
<strong>
{% if role.formatted_name %}
@@ -178,11 +312,34 @@
{% endfor %}
</ul>
{% if allowed_to_edit %}
<details>
<summary>+ Rolle</summary>
<form action="/admin/user/{{ user.id }}/add-role" method="post">
<fieldset>
<select name="role_id">
<div class="m-3">
<button type="button"
onclick="document.getElementById('role-modal').showModal()"
class="btn btn-primary w-full">Rolle hinzufügen</button>
</div>
<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 %}
{% if not role.cluster and role not in user.proper_roles and not role.hide_in_lists %}
<option value="{{ role.id }}">
@@ -191,25 +348,24 @@
{% else %}
{{ role.name }}
{% endif %}
{% if role.desc %}({{ role.desc }}){% endif %}
</option>
{% endif %}
{% endfor %}
</select>
<input value="Rolle hinzufügen" type="submit" class="btn btn-primary ml-1" />
</fieldset>
</form>
</details>
{% endif %}
</div>
<input value="Rolle hinzufügen" type="submit" class="btn btn-primary" />
</form>
</div>
</div>
</dialog>
{% endif %}
</div>
</div>
{% endif %}
{% if supposed_to_pay %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
<h2 class="h2">💸-Beitrag</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
{% if fee %}
<div>
@@ -229,7 +385,9 @@
{% endif %}
{% else %}
{% if "paid" in user.roles %}
✅ {{ member | keys }} hat schon bezahlt
✅ {% for key, value in member %}
{% if loop.first %}{{ key }}{% endif %}
{% endfor %} hat schon bezahlt
{% else %}
{% for key, value in member %}
@@ -242,90 +400,27 @@
</div>
</div>
{% endif %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Aktivität von und mit {{ user.name }}</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
<h2 class="h2">Aktivitäten</h2>
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
<ul class="list-disc ms-4">
<li>Passwort zurückgesetzt am/um X</li>
<li>Am X beigetreten.</li>
{% for activity in activities %}
<li>{{ activity.created_at | date(format="%d. %m. %Y") }}: {{ activity.text }}</li>
{% else %}
<li>Noch keine Aktivität... Stay tuned 😆</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">TODO</h2>
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
<span class="text-black dark:text-white cursor-pointer">
<span class="font-bold">
{{ user.name }}
{% if not user.last_access and allowed_to_edit and user.mail %}
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
class="inline">
&bullet; <a class="font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/send-welcome-mail"
onclick="return confirm('Willst du wirklich das Willkommensmail an {{ user.name }} ausschicken?');">Willkommensmail verschicken</a>
</form>
{% endif %}
</span>
</span>
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
class="w-full mt-2">
{% if user.pw %}
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/reset-pw"
onclick="return confirm('Willst du wirklich das Passwort zurücksetzen?');">Passwort zurücksetzen</a>
{% endif %}
<div class="w-full grid gap-3 mt-3">
<input type="hidden" name="id" value="{{ user.id }}" />
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{% for cluster, cluster_roles in roles | group_by(attribute="cluster") %}
<label for="cluster_{{ loop.index }}">{{ cluster }}</label>
{# Determine the initially selected role within the cluster #}
{% set_global selected_role_id = "none" %}
{% for role in cluster_roles %}
{% if selected_role_id == "none" and role.name in user.roles %}
{% set_global selected_role_id = role.id %}
{% endif %}
{% endfor %}
{# Set default name to the selected role ID or first role if none selected #}
<select id="cluster_{{ loop.index }}"
{% if selected_role_id == 'none' %} {% else %} name="roles[{{ selected_role_id }}]" {% endif %}
{% if allowed_to_edit == false %}disabled{% endif %}
onchange=" if (this.value === '') { this.removeAttribute('name'); } else { this.name = 'roles[' + this.options[this.selectedIndex].getAttribute('data-role-id') + ']'; }">
<option value=""
data-role-id="none"
{% if selected_role_id == 'none' %}selected="selected"{% endif %}>
None
</option>
{% for role in cluster_roles %}
<option value="on"
data-role-id="{{ role.id }}"
{% if role.id == selected_role_id %}selected="selected"{% endif %}>
{{ role.name }}
</option>
{% endfor %}
</select>
{% endfor %}
</div>
</div>
</form>
</div>
</div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
<h2 class="h2">Ergo-Challenge</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
{{ macros::input(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }}
<div class="mx-3">
<div class="grid gap-3 pb-3 mt-3">
{{ macros::inputgroup(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::inputgroup(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }}
</div>
</div>
</div>

View File

@@ -183,8 +183,6 @@
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
data-filterable="true"
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 %}
<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 %}
@@ -199,7 +197,15 @@
</div>
{% endif %}
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
{% if allowed_to_edit %}
<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 -}}
{% if log.shipmaster_only_steering %}
- handgesteuert
@@ -252,35 +258,65 @@
{% endif %}
{% endif %}
</div>
</summary>
{% if allowed_to_edit %}
<form action="/log/update" method="post">
<dialog id="change-{{ log.id }}"
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="boat_id" value="{{ log.boat_id }}" />
<input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" />
<input type="hidden"
name="steering_person"
value="{{ log.steering_person }}" />
Handgesteuert:
<input type="checkbox"
name="shipmaster_only_steering"
{% if log.shipmaster_only_steering %}checked="checked"{% endif %} />
<input type="datetime-local" name="departure" value="{{ log.departure }}" />
<input type="datetime-local" name="arrival" value="{{ log.arrival }}" />
{{ macros::select(label="Boot", data=boats, name="boat_id", id="boat_id{{ log.id }}", selected_id=log.boat.id ,display=["name", " (","amount_seats", " x)"]) }}
{{ macros::select(label="Schiffsführer", data=log.rowers, name="shipmaster", id="shipmaster{{ log.id }}", selected_id=log.shipmaster_user.id) }}
{{ macros::checkbox(label='Handgesteuert', name='shipmaster_only_steering', id=log.shipmaster_only_steering,checked=log.shipmaster_only_steering) }}
<div>
<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="distance_in_km" value="{{ log.distance_in_km }}" />
<input type="hidden" name="comments" value="{{ log.comments }}" />
<input type="hidden" name="logtype" value="{{ log.logtype }}" />
<input type="submit" value="Updaten" />
<input type="submit" class="btn btn-primary" value="Updaten" />
</form>
<a href="/log/{{ log.id }}/delete"
class="w-28 btn btn-alert"
class="w-28 btn btn-alert mt-3"
onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');">
{% include "includes/delete-icon" %}
Löschen
</a>
</div>
</div>
</dialog>
{% endif %}
</details>
</div>
{% endmacro show_old %}
{% macro home(log) %}

View File

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

View File

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