forked from Ruderverein-Donau-Linz/rowt
Merge remote-tracking branch 'upstream/main' into upd
This commit is contained in:
commit
ab7ec637ab
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]
|
[package]
|
||||||
name = "rot"
|
name = "rot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["rest", "rowing-tera" ]
|
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 }
|
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
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"
|
argon2 = "0.5"
|
||||||
serde = { version = "1.0", features = [ "derive" ]}
|
serde = { version = "1.0", features = [ "derive" ]}
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
chrono = { version = "0.4", features = ["serde"]}
|
chrono = { version = "0.4", features = ["serde"]}
|
||||||
chrono-tz = "0.9"
|
chrono-tz = "0.10"
|
||||||
tera = { version = "1.18", features = ["date-locale"], optional = true}
|
tera = { version = "1.18", features = ["date-locale"], optional = true}
|
||||||
ics = "0.5"
|
ics = "0.5"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
lettre = "0.11"
|
lettre = "0.11"
|
||||||
csv = "1.3"
|
csv = "1.3"
|
||||||
itertools = "0.13"
|
itertools = "0.14"
|
||||||
job_scheduler_ng = "2.0"
|
job_scheduler_ng = "2.0"
|
||||||
ureq = { version = "2.9", features = ["json"] }
|
ureq = { version = "3.0", features = ["json"] }
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
addRelationMagic(<HTMLElement>document.querySelector("body"));
|
addRelationMagic(<HTMLElement>document.querySelector("body"));
|
||||||
reloadPage();
|
reloadPage();
|
||||||
setCurrentdate(<HTMLInputElement>document.querySelector("#departure"));
|
setCurrentdate(<HTMLInputElement>document.querySelector("#departure"));
|
||||||
|
initDropdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
function changeTheme() {
|
function changeTheme() {
|
||||||
@ -795,3 +796,21 @@ function replaceStrings() {
|
|||||||
weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag");
|
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_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" (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 ('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 ('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?', '⛱');
|
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()
|
.unwrap()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| Registration {
|
.map(|r| Registration {
|
||||||
name: r.name,
|
name: r.name.unwrap(),
|
||||||
registered_at: r.registered_at,
|
registered_at: r.registered_at.unwrap(),
|
||||||
is_guest: false,
|
is_guest: false,
|
||||||
is_real_guest: false,
|
is_real_guest: false,
|
||||||
})
|
})
|
||||||
|
@ -11,6 +11,8 @@ use self::{
|
|||||||
waterlevel::Waterlevel,
|
waterlevel::Waterlevel,
|
||||||
weather::Weather,
|
weather::Weather,
|
||||||
};
|
};
|
||||||
|
use boatreservation::{BoatReservation, BoatReservationWithDetails};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
@ -34,6 +36,7 @@ pub struct Day {
|
|||||||
regular_sees_this_day: bool,
|
regular_sees_this_day: bool,
|
||||||
max_waterlevel: Option<WaterlevelDay>,
|
max_waterlevel: Option<WaterlevelDay>,
|
||||||
weather: Option<Weather>,
|
weather: Option<Weather>,
|
||||||
|
boat_reservations: HashMap<String, Vec<BoatReservationWithDetails>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Day {
|
impl Day {
|
||||||
@ -50,6 +53,9 @@ impl Day {
|
|||||||
regular_sees_this_day,
|
regular_sees_this_day,
|
||||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||||
weather: Weather::find_by_day(db, day).await,
|
weather: Weather::find_by_day(db, day).await,
|
||||||
|
boat_reservations: BoatReservation::with_groups(
|
||||||
|
BoatReservation::for_day(db, day).await,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Self {
|
Self {
|
||||||
@ -60,6 +66,9 @@ impl Day {
|
|||||||
regular_sees_this_day,
|
regular_sees_this_day,
|
||||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||||
weather: Weather::find_by_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) {
|
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"DELETE FROM notification WHERE action_after_reading=? and read_at is null",
|
"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.category, "Absage Ausfahrt");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rower_notification.action_after_reading.as_deref(),
|
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
|
// Cox received notification
|
||||||
|
@ -81,33 +81,31 @@ impl Trip {
|
|||||||
trip_details.planned_starting_time,
|
trip_details.planned_starting_time,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if same_starting_datetime.len() > 1 {
|
for notify in same_starting_datetime {
|
||||||
for notify in same_starting_datetime {
|
// don't notify oneself
|
||||||
// don't notify oneself
|
if notify.id == trip_details.id {
|
||||||
if notify.id == trip_details.id {
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// don't notify people who have cancelled their trip
|
// don't notify people who have cancelled their trip
|
||||||
if notify.cancelled() {
|
if notify.cancelled() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
|
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();
|
let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||||
Notification::create(
|
Notification::create(
|
||||||
db,
|
db,
|
||||||
&user,
|
&user_earlier_trip,
|
||||||
&format!(
|
&format!(
|
||||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||||
user.name, trip.day, trip.planned_starting_time
|
user.name, trip.day, trip.planned_starting_time
|
||||||
),
|
),
|
||||||
"Neue Ausfahrt zur selben Zeit",
|
"Neue Ausfahrt zur selben Zeit",
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -277,10 +275,8 @@ WHERE day=?
|
|||||||
return Err(TripUpdateError::NotYourTrip);
|
return Err(TripUpdateError::NotYourTrip);
|
||||||
}
|
}
|
||||||
|
|
||||||
if update.trip_type != Some(4) {
|
if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await {
|
||||||
if !update.cox.allowed_to_steer(db).await {
|
return Err(TripUpdateError::TripTypeNotAllowed);
|
||||||
return Err(TripUpdateError::TripTypeNotAllowed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(trip_details_id) = update.trip.trip_details_id else {
|
let Some(trip_details_id) = update.trip.trip_details_id else {
|
||||||
@ -478,6 +474,7 @@ mod test {
|
|||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
event::Event,
|
event::Event,
|
||||||
|
notification::Notification,
|
||||||
trip::{self, TripDeleteError},
|
trip::{self, TripDeleteError},
|
||||||
tripdetails::TripDetails,
|
tripdetails::TripDetails,
|
||||||
user::{SteeringUser, User},
|
user::{SteeringUser, User},
|
||||||
@ -509,6 +506,34 @@ mod test {
|
|||||||
assert!(Trip::find_by_id(&pool, 1).await.is_some());
|
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]
|
#[sqlx::test]
|
||||||
fn test_get_day_cox_trip() {
|
fn test_get_day_cox_trip() {
|
||||||
let pool = testdb!();
|
let pool = testdb!();
|
||||||
|
@ -339,7 +339,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.await,
|
.await,
|
||||||
3,
|
4,
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
TripDetails::create(
|
TripDetails::create(
|
||||||
@ -354,7 +354,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.await,
|
.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 {
|
pub struct UserWithDetails {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub user: User,
|
pub user: User,
|
||||||
pub amount_unread_notifications: i32,
|
pub amount_unread_notifications: i64,
|
||||||
pub allowed_to_steer: bool,
|
pub allowed_to_steer: bool,
|
||||||
pub roles: Vec<String>,
|
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
|
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!(
|
sqlx::query!(
|
||||||
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
|
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
|
||||||
self.id
|
self.id
|
||||||
@ -197,18 +197,27 @@ WHERE lower(name)=?
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||||
sqlx::query_as!(
|
Self::all_with_order(db, "last_access", false).await
|
||||||
Self,
|
}
|
||||||
|
|
||||||
|
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
|
SELECT id, name, pw, deleted, last_access, user_token
|
||||||
FROM user
|
FROM user
|
||||||
WHERE deleted = 0
|
WHERE deleted = 0
|
||||||
ORDER BY last_access DESC
|
ORDER BY {}
|
||||||
"
|
",
|
||||||
)
|
sort
|
||||||
.fetch_all(db)
|
);
|
||||||
.await
|
if !asc {
|
||||||
.unwrap()
|
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> {
|
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";
|
let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
|
||||||
|
|
||||||
match ureq::get(url).call() {
|
match ureq::get(url).call() {
|
||||||
Ok(response) => {
|
Ok(mut response) => {
|
||||||
let forecast: Result<Vec<Station>, _> = response.into_json();
|
let forecast: Result<Vec<Station>, _> = response.body_mut().read_json();
|
||||||
|
|
||||||
if let Ok(data) = forecast {
|
if let Ok(data) = forecast {
|
||||||
if data.len() == 1 {
|
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}");
|
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() {
|
match ureq::get(&url).call() {
|
||||||
Ok(response) => {
|
Ok(mut response) => {
|
||||||
let data: Result<Data, _> = response.into_json();
|
let data: Result<Data, _> = response.body_mut().read_json();
|
||||||
|
|
||||||
if let Ok(data) = data {
|
if let Ok(data) = data {
|
||||||
Ok(data)
|
Ok(data)
|
||||||
|
@ -33,13 +33,17 @@ impl<'r> FromRequest<'r> for Referer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/user")]
|
#[get("/user?<sort>&<asc>")]
|
||||||
async fn index(
|
async fn index(
|
||||||
db: &State<SqlitePool>,
|
db: &State<SqlitePool>,
|
||||||
user: ManageUserUser,
|
user: ManageUserUser,
|
||||||
flash: Option<FlashMessage<'_>>,
|
flash: Option<FlashMessage<'_>>,
|
||||||
|
sort: Option<String>,
|
||||||
|
asc: bool,
|
||||||
) -> Template {
|
) -> 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
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| async move { UserWithDetails::from_user(u, db).await })
|
.map(|u| async move { UserWithDetails::from_user(u, db).await })
|
||||||
|
@ -38,7 +38,7 @@ async fn cal_registered(
|
|||||||
return Err("Invalid".into());
|
return Err("Invalid".into());
|
||||||
};
|
};
|
||||||
|
|
||||||
if &user.user_token != uuid {
|
if user.user_token != uuid {
|
||||||
return Err("Invalid".into());
|
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> {
|
#[get("/read/all")]
|
||||||
routes![mark_read]
|
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">
|
<div class="max-w-screen-lg w-full">
|
||||||
<h1 class="h1">Mitglieder</h1>
|
<h1 class="h1">Mitglieder</h1>
|
||||||
{% if allowed_to_edit %}
|
{% 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"
|
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">
|
<div class="w-full">
|
||||||
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neues Mitglied hinzufügen</h2>
|
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neues Mitglied hinzufügen</h2>
|
||||||
<div class="grid md:grid-cols-3">
|
<div class="grid md:grid-cols-3">
|
||||||
@ -19,21 +22,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right ml-3">
|
||||||
<input value="Hinzufügen"
|
<input value="Hinzufügen"
|
||||||
type="submit"
|
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" />
|
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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</details>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- START filterBar -->
|
<!-- START filterBar -->
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper flex">
|
||||||
<label for="name" class="sr-only">Suche</label>
|
<label for="name" class="sr-only">Suche</label>
|
||||||
<input type="search"
|
<input type="search"
|
||||||
name="name"
|
name="name"
|
||||||
id="filter-js"
|
id="filter-js"
|
||||||
class="search-bar"
|
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>
|
</div>
|
||||||
<!-- END filterBar -->
|
<!-- END filterBar -->
|
||||||
<div id="filter-result-js" class="search-result"></div>
|
<div id="filter-result-js" class="search-result"></div>
|
||||||
@ -46,7 +72,7 @@
|
|||||||
<span class="text-black dark:text-white cursor-pointer">
|
<span class="text-black dark:text-white cursor-pointer">
|
||||||
<span class="font-bold">
|
<span class="font-bold">
|
||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
{% if not user.last_access and "admin" in loggedin_user.roles %}
|
{% if not user.last_access and allowed_to_edit and user.mail %}
|
||||||
<form action="/admin/user"
|
<form action="/admin/user"
|
||||||
method="post"
|
method="post"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
|
@ -4,13 +4,12 @@
|
|||||||
{% extends "base" %}
|
{% extends "base" %}
|
||||||
{% macro show_place(aisle_name, side_name, level) %}
|
{% macro show_place(aisle_name, side_name, level) %}
|
||||||
<li class="truncate p-2 flex relative w-full">
|
<li class="truncate p-2 flex relative w-full">
|
||||||
{% set aisle = aisle_name ~ "-aisle" %}
|
{% set place = boathouse[aisle_name][side_name].boats %}
|
||||||
{% set place = boathouse[aisle][side_name] %}
|
|
||||||
{% if place[level] %}
|
{% if place[level] %}
|
||||||
{{ place[level].1.name }}
|
{{ place[level].boat.name }}
|
||||||
{% if "admin" in loggedin_user.roles %}
|
{% if "admin" in loggedin_user.roles %}
|
||||||
<a class="btn btn-primary absolute end-0"
|
<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 %}
|
{% endif %}
|
||||||
{% elif boats | length > 0 %}
|
{% elif boats | length > 0 %}
|
||||||
{% if "admin" in loggedin_user.roles %}
|
{% if "admin" in loggedin_user.roles %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user