forked from Ruderverein-Donau-Linz/rowt
Merge pull request 'Rebase from rowt' (#6) from upd into main
Reviewed-on: #6
This commit is contained in:
commit
2520108fba
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,
|
||||||
})
|
})
|
||||||
|
@ -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,11 +4,13 @@
|
|||||||
<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>
|
|
||||||
<div class="grid md:grid-cols-3">
|
<div class="grid md:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<label for="name" class="sr-only">Name</label>
|
<label for="name" class="sr-only">Name</label>
|
||||||
@ -19,21 +21,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 +71,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 %}
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
{% if notifications %}
|
{% if notifications %}
|
||||||
{% if loggedin_user.amount_unread_notifications > 10 %}
|
{% if loggedin_user.amount_unread_notifications > 10 %}
|
||||||
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
|
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
|
||||||
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte einen Moment Zeit sie zu überprüfen und als gelesen zu markieren (✓).
|
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte in Zukunft einen kurzen Moment Zeit sie zu überprüfen und als gelesen zu markieren (✓).<br /><a href="/notification/read/all" class="underline">Du kannst hier ausnahmsweise alle als gelesen markieren.</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="divide-y">
|
<div class="divide-y">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user