forked from Ruderverein-Donau-Linz/rowt
		
	Merge remote-tracking branch 'upstream/main' into upd
This commit is contained in:
		
							
								
								
									
										1481
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1481
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										10
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Cargo.toml
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "rot"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
edition = "2024"
 | 
			
		||||
 | 
			
		||||
[features]
 | 
			
		||||
default = ["rest", "rowing-tera" ]
 | 
			
		||||
@@ -13,20 +13,20 @@ rocket = { version = "0.5.0", features = ["secrets"]}
 | 
			
		||||
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
 | 
			
		||||
log = "0.4"
 | 
			
		||||
env_logger = "0.11"
 | 
			
		||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
 | 
			
		||||
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] }
 | 
			
		||||
argon2 = "0.5"
 | 
			
		||||
serde = { version = "1.0", features = [ "derive" ]}
 | 
			
		||||
serde_json = "1.0"
 | 
			
		||||
chrono =  { version = "0.4", features = ["serde"]}
 | 
			
		||||
chrono-tz = "0.9"
 | 
			
		||||
chrono-tz = "0.10"
 | 
			
		||||
tera =  { version = "1.18", features = ["date-locale"], optional = true}
 | 
			
		||||
ics = "0.5"
 | 
			
		||||
futures = "0.3"
 | 
			
		||||
lettre = "0.11"
 | 
			
		||||
csv = "1.3"
 | 
			
		||||
itertools = "0.13"
 | 
			
		||||
itertools = "0.14"
 | 
			
		||||
job_scheduler_ng = "2.0"
 | 
			
		||||
ureq = { version = "2.9", features = ["json"] }
 | 
			
		||||
ureq = { version = "3.0", features = ["json"] }
 | 
			
		||||
regex = "1.10"
 | 
			
		||||
urlencoding = "2.1"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ document.addEventListener("DOMContentLoaded", function () {
 | 
			
		||||
  addRelationMagic(<HTMLElement>document.querySelector("body"));
 | 
			
		||||
  reloadPage();
 | 
			
		||||
  setCurrentdate(<HTMLInputElement>document.querySelector("#departure"));
 | 
			
		||||
  initDropdown();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function changeTheme() {
 | 
			
		||||
@@ -795,3 +796,21 @@ function replaceStrings() {
 | 
			
		||||
    weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag");
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initDropdown() {
 | 
			
		||||
  const popoverTriggerList = document.querySelectorAll('[data-dropdown]');    
 | 
			
		||||
 | 
			
		||||
  popoverTriggerList.forEach((popoverTriggerEl: Element) => {
 | 
			
		||||
    const id = popoverTriggerEl.getAttribute('data-dropdown');
 | 
			
		||||
    
 | 
			
		||||
    if (id) {
 | 
			
		||||
      const element = document.getElementById(id);
 | 
			
		||||
      if (element) {
 | 
			
		||||
        // Toggle visibility of the dropdown when clicked
 | 
			
		||||
        popoverTriggerEl.addEventListener('click', () => {
 | 
			
		||||
          element.classList.toggle('hidden');
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,7 @@ INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('
 | 
			
		||||
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, date('now', '+1 day'), 'trip_details for trip from cox');
 | 
			
		||||
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2);
 | 
			
		||||
 | 
			
		||||
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'same trip_details as id=1');
 | 
			
		||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '🏅');
 | 
			
		||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '💪');
 | 
			
		||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '⛱');
 | 
			
		||||
 
 | 
			
		||||
@@ -96,8 +96,8 @@ FROM trip WHERE planned_event_id = ?
 | 
			
		||||
        .unwrap()
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .map(|r| Registration {
 | 
			
		||||
            name: r.name,
 | 
			
		||||
            registered_at: r.registered_at,
 | 
			
		||||
            name: r.name.unwrap(),
 | 
			
		||||
            registered_at: r.registered_at.unwrap(),
 | 
			
		||||
            is_guest: false,
 | 
			
		||||
            is_real_guest: false,
 | 
			
		||||
        })
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@ use self::{
 | 
			
		||||
    waterlevel::Waterlevel,
 | 
			
		||||
    weather::Weather,
 | 
			
		||||
};
 | 
			
		||||
use boatreservation::{BoatReservation, BoatReservationWithDetails};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
pub mod event;
 | 
			
		||||
pub mod log;
 | 
			
		||||
@@ -34,6 +36,7 @@ pub struct Day {
 | 
			
		||||
    regular_sees_this_day: bool,
 | 
			
		||||
    max_waterlevel: Option<WaterlevelDay>,
 | 
			
		||||
    weather: Option<Weather>,
 | 
			
		||||
    boat_reservations: HashMap<String, Vec<BoatReservationWithDetails>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Day {
 | 
			
		||||
@@ -50,6 +53,9 @@ impl Day {
 | 
			
		||||
                regular_sees_this_day,
 | 
			
		||||
                max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
 | 
			
		||||
                weather: Weather::find_by_day(db, day).await,
 | 
			
		||||
                boat_reservations: BoatReservation::with_groups(
 | 
			
		||||
                    BoatReservation::for_day(db, day).await,
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Self {
 | 
			
		||||
@@ -60,6 +66,9 @@ impl Day {
 | 
			
		||||
                regular_sees_this_day,
 | 
			
		||||
                max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
 | 
			
		||||
                weather: Weather::find_by_day(db, day).await,
 | 
			
		||||
                boat_reservations: BoatReservation::with_groups(
 | 
			
		||||
                    BoatReservation::for_day(db, day).await,
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -208,6 +208,15 @@ ORDER BY read_at DESC, created_at DESC;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn mark_all_read(db: &SqlitePool, user: &User) {
 | 
			
		||||
        let notifications = Self::for_user(db, user).await;
 | 
			
		||||
 | 
			
		||||
        for notification in notifications {
 | 
			
		||||
            notification.mark_read(db).await;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
 | 
			
		||||
        sqlx::query!(
 | 
			
		||||
            "DELETE FROM notification WHERE action_after_reading=? and read_at is null",
 | 
			
		||||
@@ -289,7 +298,7 @@ mod test {
 | 
			
		||||
        assert_eq!(rower_notification.category, "Absage Ausfahrt");
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            rower_notification.action_after_reading.as_deref(),
 | 
			
		||||
            Some("remove_user_trip_with_trip_details_id:3")
 | 
			
		||||
            Some("remove_user_trip_with_trip_details_id:4")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Cox received notification
 | 
			
		||||
 
 | 
			
		||||
@@ -81,33 +81,31 @@ impl Trip {
 | 
			
		||||
            trip_details.planned_starting_time,
 | 
			
		||||
        )
 | 
			
		||||
        .await;
 | 
			
		||||
        if same_starting_datetime.len() > 1 {
 | 
			
		||||
            for notify in same_starting_datetime {
 | 
			
		||||
                // don't notify oneself
 | 
			
		||||
                if notify.id == trip_details.id {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
        for notify in same_starting_datetime {
 | 
			
		||||
            // don't notify oneself
 | 
			
		||||
            if notify.id == trip_details.id {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                // don't notify people who have cancelled their trip
 | 
			
		||||
                if notify.cancelled() {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
            // don't notify people who have cancelled their trip
 | 
			
		||||
            if notify.cancelled() {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
 | 
			
		||||
                    let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
 | 
			
		||||
                    Notification::create(
 | 
			
		||||
                        db,
 | 
			
		||||
                        &user,
 | 
			
		||||
                        &format!(
 | 
			
		||||
                            "{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
 | 
			
		||||
                            user.name, trip.day, trip.planned_starting_time
 | 
			
		||||
                        ),
 | 
			
		||||
                        "Neue Ausfahrt zur selben Zeit",
 | 
			
		||||
                        None,
 | 
			
		||||
                        None,
 | 
			
		||||
                    )
 | 
			
		||||
                    .await;
 | 
			
		||||
                }
 | 
			
		||||
            if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
 | 
			
		||||
                let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
 | 
			
		||||
                Notification::create(
 | 
			
		||||
                    db,
 | 
			
		||||
                    &user_earlier_trip,
 | 
			
		||||
                    &format!(
 | 
			
		||||
                        "{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
 | 
			
		||||
                        user.name, trip.day, trip.planned_starting_time
 | 
			
		||||
                    ),
 | 
			
		||||
                    "Neue Ausfahrt zur selben Zeit",
 | 
			
		||||
                    None,
 | 
			
		||||
                    None,
 | 
			
		||||
                )
 | 
			
		||||
                .await;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -277,10 +275,8 @@ WHERE day=?
 | 
			
		||||
            return Err(TripUpdateError::NotYourTrip);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if update.trip_type != Some(4) {
 | 
			
		||||
            if !update.cox.allowed_to_steer(db).await {
 | 
			
		||||
                return Err(TripUpdateError::TripTypeNotAllowed);
 | 
			
		||||
            }
 | 
			
		||||
        if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await {
 | 
			
		||||
            return Err(TripUpdateError::TripTypeNotAllowed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let Some(trip_details_id) = update.trip.trip_details_id else {
 | 
			
		||||
@@ -478,6 +474,7 @@ mod test {
 | 
			
		||||
    use crate::{
 | 
			
		||||
        model::{
 | 
			
		||||
            event::Event,
 | 
			
		||||
            notification::Notification,
 | 
			
		||||
            trip::{self, TripDeleteError},
 | 
			
		||||
            tripdetails::TripDetails,
 | 
			
		||||
            user::{SteeringUser, User},
 | 
			
		||||
@@ -509,6 +506,34 @@ mod test {
 | 
			
		||||
        assert!(Trip::find_by_id(&pool, 1).await.is_some());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[sqlx::test]
 | 
			
		||||
    fn test_notification_cox_if_same_datetime() {
 | 
			
		||||
        let pool = testdb!();
 | 
			
		||||
        let cox = SteeringUser::new(
 | 
			
		||||
            &pool,
 | 
			
		||||
            User::find_by_name(&pool, "cox".into()).await.unwrap(),
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
        let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
 | 
			
		||||
        Trip::new_own(&pool, &cox, trip_details).await;
 | 
			
		||||
 | 
			
		||||
        let cox2 = SteeringUser::new(
 | 
			
		||||
            &pool,
 | 
			
		||||
            User::find_by_name(&pool, "cox2".into()).await.unwrap(),
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
        let trip_details = TripDetails::find_by_id(&pool, 3).await.unwrap();
 | 
			
		||||
        Trip::new_own(&pool, &cox2, trip_details).await;
 | 
			
		||||
 | 
			
		||||
        let last_notification = &Notification::for_user(&pool, &cox).await[0];
 | 
			
		||||
 | 
			
		||||
        assert!(last_notification
 | 
			
		||||
            .message
 | 
			
		||||
            .starts_with("cox2 hat eine Ausfahrt zur selben Zeit"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[sqlx::test]
 | 
			
		||||
    fn test_get_day_cox_trip() {
 | 
			
		||||
        let pool = testdb!();
 | 
			
		||||
 
 | 
			
		||||
@@ -339,7 +339,7 @@ mod test {
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            .await,
 | 
			
		||||
            3,
 | 
			
		||||
            4,
 | 
			
		||||
        );
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            TripDetails::create(
 | 
			
		||||
@@ -354,7 +354,7 @@ mod test {
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            .await,
 | 
			
		||||
            4,
 | 
			
		||||
            5,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								src/model/user/fee.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/model/user/fee.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
use super::User;
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize)]
 | 
			
		||||
pub struct Fee {
 | 
			
		||||
    pub sum_in_cents: i64,
 | 
			
		||||
    pub parts: Vec<(String, i64)>,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub user_ids: String,
 | 
			
		||||
    pub paid: bool,
 | 
			
		||||
    pub users: Vec<User>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for Fee {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self::new()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Fee {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            sum_in_cents: 0,
 | 
			
		||||
            name: "".into(),
 | 
			
		||||
            parts: Vec::new(),
 | 
			
		||||
            user_ids: "".into(),
 | 
			
		||||
            users: Vec::new(),
 | 
			
		||||
            paid: false,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add(&mut self, desc: String, price_in_cents: i64) {
 | 
			
		||||
        self.sum_in_cents += price_in_cents;
 | 
			
		||||
 | 
			
		||||
        self.parts.push((desc, price_in_cents));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_person(&mut self, user: &User) {
 | 
			
		||||
        if !self.name.is_empty() {
 | 
			
		||||
            self.name.push_str(" + ");
 | 
			
		||||
            self.user_ids.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        self.name.push_str(&user.name);
 | 
			
		||||
 | 
			
		||||
        self.user_ids.push_str(&format!("user_ids[]={}", user.id));
 | 
			
		||||
        self.users.push(user.clone());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn paid(&mut self) {
 | 
			
		||||
        self.paid = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn merge(&mut self, fee: Fee) {
 | 
			
		||||
        for (desc, price_in_cents) in fee.parts {
 | 
			
		||||
            self.add(desc, price_in_cents);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -31,7 +31,7 @@ pub struct User {
 | 
			
		||||
pub struct UserWithDetails {
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    pub user: User,
 | 
			
		||||
    pub amount_unread_notifications: i32,
 | 
			
		||||
    pub amount_unread_notifications: i64,
 | 
			
		||||
    pub allowed_to_steer: bool,
 | 
			
		||||
    pub roles: Vec<String>,
 | 
			
		||||
}
 | 
			
		||||
@@ -72,7 +72,7 @@ impl User {
 | 
			
		||||
        self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i32 {
 | 
			
		||||
    pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i64 {
 | 
			
		||||
        sqlx::query!(
 | 
			
		||||
            "SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
 | 
			
		||||
            self.id
 | 
			
		||||
@@ -197,18 +197,27 @@ WHERE lower(name)=?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn all(db: &SqlitePool) -> Vec<Self> {
 | 
			
		||||
        sqlx::query_as!(
 | 
			
		||||
            Self,
 | 
			
		||||
        Self::all_with_order(db, "last_access", false).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> {
 | 
			
		||||
        let mut query = format!(
 | 
			
		||||
            "
 | 
			
		||||
SELECT id, name, pw, deleted, last_access, user_token
 | 
			
		||||
FROM user
 | 
			
		||||
WHERE deleted = 0
 | 
			
		||||
ORDER BY last_access DESC
 | 
			
		||||
        "
 | 
			
		||||
        )
 | 
			
		||||
        .fetch_all(db)
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap()
 | 
			
		||||
        SELECT id, name, pw, deleted, last_access, user_token
 | 
			
		||||
        FROM user
 | 
			
		||||
        WHERE deleted = 0
 | 
			
		||||
        ORDER BY {}
 | 
			
		||||
        ",
 | 
			
		||||
            sort
 | 
			
		||||
        );
 | 
			
		||||
        if !asc {
 | 
			
		||||
            query.push_str(" DESC");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sqlx::query_as::<_, User>(&query)
 | 
			
		||||
            .fetch_all(db)
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec<Self> {
 | 
			
		||||
@@ -80,8 +80,8 @@ fn fetch() -> Result<Station, String> {
 | 
			
		||||
    let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
 | 
			
		||||
 | 
			
		||||
    match ureq::get(url).call() {
 | 
			
		||||
        Ok(response) => {
 | 
			
		||||
            let forecast: Result<Vec<Station>, _> = response.into_json();
 | 
			
		||||
        Ok(mut response) => {
 | 
			
		||||
            let forecast: Result<Vec<Station>, _> = response.body_mut().read_json();
 | 
			
		||||
 | 
			
		||||
            if let Ok(data) = forecast {
 | 
			
		||||
                if data.len() == 1 {
 | 
			
		||||
 
 | 
			
		||||
@@ -99,8 +99,8 @@ fn fetch(api_key: &str) -> Result<Data, String> {
 | 
			
		||||
    let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=47.766249&lon=13.367683&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
 | 
			
		||||
 | 
			
		||||
    match ureq::get(&url).call() {
 | 
			
		||||
        Ok(response) => {
 | 
			
		||||
            let data: Result<Data, _> = response.into_json();
 | 
			
		||||
        Ok(mut response) => {
 | 
			
		||||
            let data: Result<Data, _> = response.body_mut().read_json();
 | 
			
		||||
 | 
			
		||||
            if let Ok(data) = data {
 | 
			
		||||
                Ok(data)
 | 
			
		||||
 
 | 
			
		||||
@@ -33,13 +33,17 @@ impl<'r> FromRequest<'r> for Referer {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/user")]
 | 
			
		||||
#[get("/user?<sort>&<asc>")]
 | 
			
		||||
async fn index(
 | 
			
		||||
    db: &State<SqlitePool>,
 | 
			
		||||
    user: ManageUserUser,
 | 
			
		||||
    flash: Option<FlashMessage<'_>>,
 | 
			
		||||
    sort: Option<String>,
 | 
			
		||||
    asc: bool,
 | 
			
		||||
) -> Template {
 | 
			
		||||
    let user_futures: Vec<_> = User::all(db)
 | 
			
		||||
    let sort_column = sort.unwrap_or_else(|| "last_access".to_string());
 | 
			
		||||
 | 
			
		||||
    let user_futures: Vec<_> = User::all_with_order(db, &sort_column, asc)
 | 
			
		||||
        .await
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .map(|u| async move { UserWithDetails::from_user(u, db).await })
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ async fn cal_registered(
 | 
			
		||||
        return Err("Invalid".into());
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if &user.user_token != uuid {
 | 
			
		||||
    if user.user_token != uuid {
 | 
			
		||||
        return Err("Invalid".into());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,12 @@ async fn mark_read(db: &State<SqlitePool>, user: User, notification_id: i64) ->
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn routes() -> Vec<Route> {
 | 
			
		||||
    routes![mark_read]
 | 
			
		||||
#[get("/read/all")]
 | 
			
		||||
async fn mark_all_read(db: &State<SqlitePool>, user: User) -> Flash<Redirect> {
 | 
			
		||||
    Notification::mark_all_read(db, &user).await;
 | 
			
		||||
    Flash::success(Redirect::to("/"), "Alle Nachrichten als gelesen markiert")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn routes() -> Vec<Route> {
 | 
			
		||||
    routes![mark_read, mark_all_read]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,12 @@
 | 
			
		||||
    <div class="max-w-screen-lg w-full">
 | 
			
		||||
        <h1 class="h1">Mitglieder</h1>
 | 
			
		||||
        {% if allowed_to_edit %}
 | 
			
		||||
            <form action="/admin/user/new"
 | 
			
		||||
            <details class="mt-5 bg-gray-200 dark:bg-primary-600 p-3 rounded-md">
 | 
			
		||||
                <summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">Neue Person hinzufügen</summary>
 | 
			
		||||
                <form action="/admin/user/new"
 | 
			
		||||
		  onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
 | 
			
		||||
                  method="post"
 | 
			
		||||
                  class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between">
 | 
			
		||||
                  class="flex mt-4 rounded-md sm:flex items-end justify-between">
 | 
			
		||||
                <div class="w-full">
 | 
			
		||||
                    <h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neues Mitglied hinzufügen</h2>
 | 
			
		||||
                    <div class="grid md:grid-cols-3">
 | 
			
		||||
@@ -19,21 +22,44 @@
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="text-right">
 | 
			
		||||
                <div class="text-right ml-3">
 | 
			
		||||
                    <input value="Hinzufügen"
 | 
			
		||||
                           type="submit"
 | 
			
		||||
                           class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
            </details>
 | 
			
		||||
            
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <!-- START filterBar -->
 | 
			
		||||
        <div class="search-wrapper">
 | 
			
		||||
        <div class="search-wrapper flex">
 | 
			
		||||
            <label for="name" class="sr-only">Suche</label>
 | 
			
		||||
            <input type="search"
 | 
			
		||||
                   name="name"
 | 
			
		||||
                   id="filter-js"
 | 
			
		||||
                   class="search-bar"
 | 
			
		||||
                   placeholder="Suchen nach (Name, [yes|no]-role:<name>" />
 | 
			
		||||
                   placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
 | 
			
		||||
 | 
			
		||||
            <div class="relative">
 | 
			
		||||
              <button id="dropdownbtn" data-dropdown="dropdown" class="btn btn-dark ml-3" type="button">
 | 
			
		||||
                Sortieren 
 | 
			
		||||
              </button>
 | 
			
		||||
 | 
			
		||||
              <!-- Dropdown menu -->
 | 
			
		||||
              <div id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-100 text-secondary-900 rounded-lg shadow-sm w-44 absolute right-0">
 | 
			
		||||
                  <ul class="py-2 text-sm" aria-labelledby="dropdownbtn">
 | 
			
		||||
                    <li>
 | 
			
		||||
                      <a href="./user" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Zuletzt eingeloggt</a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                      <a href="?sort=name&asc" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name A-Z</a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                      <a href="?sort=name" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                  </ul>
 | 
			
		||||
              </div> 
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- END filterBar -->
 | 
			
		||||
        <div id="filter-result-js" class="search-result"></div>
 | 
			
		||||
@@ -46,7 +72,7 @@
 | 
			
		||||
                        <span class="text-black dark:text-white cursor-pointer">
 | 
			
		||||
                            <span class="font-bold">
 | 
			
		||||
                                {{ user.name }}
 | 
			
		||||
                                {% if not user.last_access and "admin" in loggedin_user.roles %}
 | 
			
		||||
                                {% if not user.last_access and allowed_to_edit and user.mail %}
 | 
			
		||||
                                    <form action="/admin/user"
 | 
			
		||||
                                          method="post"
 | 
			
		||||
                                          enctype="multipart/form-data"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,12 @@
 | 
			
		||||
{% extends "base" %}
 | 
			
		||||
{% macro show_place(aisle_name, side_name, level) %}
 | 
			
		||||
    <li class="truncate p-2 flex relative w-full">
 | 
			
		||||
        {% set aisle = aisle_name ~ "-aisle" %}
 | 
			
		||||
        {% set place = boathouse[aisle][side_name] %}
 | 
			
		||||
        {% set place = boathouse[aisle_name][side_name].boats %}
 | 
			
		||||
        {% if place[level] %}
 | 
			
		||||
            {{ place[level].1.name }}
 | 
			
		||||
            {{ place[level].boat.name }}
 | 
			
		||||
            {% if "admin" in loggedin_user.roles %}
 | 
			
		||||
                <a class="btn btn-primary absolute end-0"
 | 
			
		||||
                   href="/board/boathouse/{{ place[level].0 }}/delete">X</a>
 | 
			
		||||
                   href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X</a>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        {% elif boats | length > 0 %}
 | 
			
		||||
            {% if "admin" in loggedin_user.roles %}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user