notification-badge #409
@ -72,5 +72,6 @@ export default defineConfig({
|
|||||||
webServer: {
|
webServer: {
|
||||||
timeout: 15 * 60 * 1000,
|
timeout: 15 * 60 * 1000,
|
||||||
command: 'cd .. && ./test_db.sh && cargo r',
|
command: 'cd .. && ./test_db.sh && cargo r',
|
||||||
|
url: 'http://127.0.0.1:8000'
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -13,3 +13,4 @@
|
|||||||
@import 'components/search';
|
@import 'components/search';
|
||||||
@import 'components/important';
|
@import 'components/important';
|
||||||
@import 'components/searchable-table';
|
@import 'components/searchable-table';
|
||||||
|
@import 'components/notification';
|
||||||
|
5
frontend/scss/components/_notification.scss
Normal file
5
frontend/scss/components/_notification.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.notification {
|
||||||
|
right: -.2rem;
|
||||||
|
top: -.1rem;
|
||||||
|
font-size: .5rem;
|
||||||
|
}
|
@ -8,7 +8,7 @@ test("cox can create and delete trip", async ({ page }) => {
|
|||||||
await page.getByPlaceholder("Passwort").fill("cox");
|
await page.getByPlaceholder("Passwort").fill("cox");
|
||||||
await page.getByPlaceholder("Passwort").press("Enter");
|
await page.getByPlaceholder("Passwort").press("Enter");
|
||||||
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
||||||
await page.locator(".relative").first().click();
|
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
|
||||||
await page.locator("#sidebar #planned_starting_time").click();
|
await page.locator("#sidebar #planned_starting_time").click();
|
||||||
await page.locator("#sidebar #planned_starting_time").fill("18:00");
|
await page.locator("#sidebar #planned_starting_time").fill("18:00");
|
||||||
await page.locator("#sidebar #planned_starting_time").press("Tab");
|
await page.locator("#sidebar #planned_starting_time").press("Tab");
|
||||||
@ -38,8 +38,8 @@ test.describe("cox can edit trips", () => {
|
|||||||
await page.getByPlaceholder("Name").press("Tab");
|
await page.getByPlaceholder("Name").press("Tab");
|
||||||
await page.getByPlaceholder("Passwort").fill("cox");
|
await page.getByPlaceholder("Passwort").fill("cox");
|
||||||
await page.getByPlaceholder("Passwort").press("Enter");
|
await page.getByPlaceholder("Passwort").press("Enter");
|
||||||
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
||||||
await page.locator(".relative").first().click();
|
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
|
||||||
await page.locator("#sidebar #planned_starting_time").click();
|
await page.locator("#sidebar #planned_starting_time").click();
|
||||||
await page.locator("#sidebar #planned_starting_time").fill("18:00");
|
await page.locator("#sidebar #planned_starting_time").fill("18:00");
|
||||||
await page.locator("#sidebar #planned_starting_time").press("Tab");
|
await page.locator("#sidebar #planned_starting_time").press("Tab");
|
||||||
|
@ -591,7 +591,7 @@ ORDER BY departure DESC
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
|
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
|
||||||
Log::create(db, format!("{user:?} deleted trip: {self:?}")).await;
|
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
|
||||||
|
|
||||||
if user.has_role(db, "admin").await
|
if user.has_role(db, "admin").await
|
||||||
|| user.has_role(db, "Vorstand").await
|
|| user.has_role(db, "Vorstand").await
|
||||||
|
@ -47,16 +47,18 @@ pub struct User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct UserWithRoles {
|
pub struct UserWithRolesAndNotificationCount {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub user: User,
|
pub user: User,
|
||||||
|
pub amount_unread_notifications: i32,
|
||||||
pub roles: Vec<String>,
|
pub roles: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserWithRoles {
|
impl UserWithRolesAndNotificationCount {
|
||||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
roles: user.roles(db).await,
|
roles: user.roles(db).await,
|
||||||
|
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
||||||
user,
|
user,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,6 +239,17 @@ impl User {
|
|||||||
.count
|
.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i32 {
|
||||||
|
sqlx::query!(
|
||||||
|
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
|
||||||
|
self.id
|
||||||
|
)
|
||||||
|
.fetch_one(db)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.count
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool {
|
pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool {
|
||||||
if sqlx::query!(
|
if sqlx::query!(
|
||||||
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
||||||
|
@ -2,7 +2,7 @@ use crate::model::{
|
|||||||
boat::{Boat, BoatToAdd, BoatToUpdate},
|
boat::{Boat, BoatToAdd, BoatToUpdate},
|
||||||
location::Location,
|
location::Location,
|
||||||
log::Log,
|
log::Log,
|
||||||
user::{AdminUser, User, UserWithRoles},
|
user::{AdminUser, User, UserWithRolesAndNotificationCount},
|
||||||
};
|
};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::Form,
|
form::Form,
|
||||||
@ -33,7 +33,7 @@ async fn index(
|
|||||||
context.insert("users", &users);
|
context.insert("users", &users);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(admin.user, db).await,
|
&UserWithRolesAndNotificationCount::from_user(admin.user, db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("admin/boat/index", context.into_json())
|
Template::render("admin/boat/index", context.into_json())
|
||||||
|
@ -10,7 +10,7 @@ use crate::model::log::Log;
|
|||||||
use crate::model::mail::Mail;
|
use crate::model::mail::Mail;
|
||||||
use crate::model::role::Role;
|
use crate::model::role::Role;
|
||||||
use crate::model::user::AdminUser;
|
use crate::model::user::AdminUser;
|
||||||
use crate::model::user::UserWithRoles;
|
use crate::model::user::UserWithRolesAndNotificationCount;
|
||||||
use crate::tera::Config;
|
use crate::tera::Config;
|
||||||
|
|
||||||
#[get("/mail")]
|
#[get("/mail")]
|
||||||
@ -27,7 +27,7 @@ async fn index(
|
|||||||
|
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(admin.user, db).await,
|
&UserWithRolesAndNotificationCount::from_user(admin.user, db).await,
|
||||||
);
|
);
|
||||||
context.insert("roles", &roles);
|
context.insert("roles", &roles);
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ use crate::model::{
|
|||||||
log::Log,
|
log::Log,
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{AdminUser, User, UserWithRoles},
|
user::{AdminUser, User, UserWithRolesAndNotificationCount},
|
||||||
};
|
};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::Form,
|
form::Form,
|
||||||
@ -26,7 +26,7 @@ async fn index(
|
|||||||
}
|
}
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(user.user, db).await,
|
&UserWithRolesAndNotificationCount::from_user(user.user, db).await,
|
||||||
);
|
);
|
||||||
context.insert("roles", &Role::all(db).await);
|
context.insert("roles", &Role::all(db).await);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::model::{
|
use crate::model::{
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{SchnupperBetreuerUser, User, UserWithRoles},
|
user::{SchnupperBetreuerUser, User, UserWithRolesAndNotificationCount},
|
||||||
};
|
};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
@ -38,9 +38,9 @@ async fn index(
|
|||||||
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
|
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| async move { UserWithRoles::from_user(u, db).await })
|
.map(|u| async move { UserWithRolesAndNotificationCount::from_user(u, db).await })
|
||||||
.collect();
|
.collect();
|
||||||
let users: Vec<UserWithRoles> = join_all(user_futures).await;
|
let users: Vec<UserWithRolesAndNotificationCount> = join_all(user_futures).await;
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
if let Some(msg) = flash {
|
if let Some(msg) = flash {
|
||||||
@ -49,7 +49,7 @@ async fn index(
|
|||||||
context.insert("schnupperanten", &users);
|
context.insert("schnupperanten", &users);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(user.into(), db).await,
|
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("admin/schnupper/index", context.into_json())
|
Template::render("admin/schnupper/index", context.into_json())
|
||||||
|
@ -6,8 +6,8 @@ use crate::model::{
|
|||||||
logbook::Logbook,
|
logbook::Logbook,
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{
|
user::{
|
||||||
AdminUser, User, UserWithMembershipPdf, UserWithRoles, UserWithRolesAndMembershipPdf,
|
AdminUser, User, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
|
||||||
VorstandUser,
|
UserWithRolesAndNotificationCount, VorstandUser,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
@ -67,7 +67,10 @@ async fn index(
|
|||||||
context.insert("users", &users);
|
context.insert("users", &users);
|
||||||
context.insert("roles", &roles);
|
context.insert("roles", &roles);
|
||||||
context.insert("families", &families);
|
context.insert("families", &families);
|
||||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
context.insert(
|
||||||
|
"loggedin_user",
|
||||||
|
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
||||||
|
);
|
||||||
|
|
||||||
Template::render("admin/user/index", context.into_json())
|
Template::render("admin/user/index", context.into_json())
|
||||||
}
|
}
|
||||||
@ -99,7 +102,10 @@ async fn index_admin(
|
|||||||
context.insert("users", &users);
|
context.insert("users", &users);
|
||||||
context.insert("roles", &roles);
|
context.insert("roles", &roles);
|
||||||
context.insert("families", &families);
|
context.insert("families", &families);
|
||||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
context.insert(
|
||||||
|
"loggedin_user",
|
||||||
|
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
||||||
|
);
|
||||||
|
|
||||||
Template::render("admin/user/index", context.into_json())
|
Template::render("admin/user/index", context.into_json())
|
||||||
}
|
}
|
||||||
@ -127,7 +133,7 @@ async fn fees(
|
|||||||
}
|
}
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(admin.into(), db).await,
|
&UserWithRolesAndNotificationCount::from_user(admin.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("admin/user/fees", context.into_json())
|
Template::render("admin/user/fees", context.into_json())
|
||||||
@ -147,7 +153,7 @@ async fn scheckbuch(
|
|||||||
for s in scheckbooks {
|
for s in scheckbooks {
|
||||||
scheckbooks_with_roles.push((
|
scheckbooks_with_roles.push((
|
||||||
Logbook::completed_with_user(db, &s).await,
|
Logbook::completed_with_user(db, &s).await,
|
||||||
UserWithRoles::from_user(s, db).await,
|
UserWithRolesAndNotificationCount::from_user(s, db).await,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +164,7 @@ async fn scheckbuch(
|
|||||||
}
|
}
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(user.into(), db).await,
|
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("admin/user/scheckbuch", context.into_json())
|
Template::render("admin/user/scheckbuch", context.into_json())
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::model::{
|
use crate::model::{
|
||||||
boat::Boat,
|
boat::Boat,
|
||||||
boathouse::Boathouse,
|
boathouse::Boathouse,
|
||||||
user::{AdminUser, UserWithRoles, VorstandUser},
|
user::{AdminUser, UserWithRolesAndNotificationCount, VorstandUser},
|
||||||
};
|
};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::Form,
|
form::Form,
|
||||||
@ -39,7 +39,7 @@ async fn index(
|
|||||||
|
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(admin.into(), db).await,
|
&UserWithRolesAndNotificationCount::from_user(admin.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("board/boathouse", context.into_json())
|
Template::render("board/boathouse", context.into_json())
|
||||||
|
@ -13,7 +13,7 @@ use crate::{
|
|||||||
model::{
|
model::{
|
||||||
boat::Boat,
|
boat::Boat,
|
||||||
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
||||||
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles},
|
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRolesAndNotificationCount},
|
||||||
},
|
},
|
||||||
tera::log::KioskCookie,
|
tera::log::KioskCookie,
|
||||||
};
|
};
|
||||||
@ -59,7 +59,7 @@ async fn index(
|
|||||||
context.insert("boats", &boats);
|
context.insert("boats", &boats);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(user.into(), db).await,
|
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("boatdamages", context.into_json())
|
Template::render("boatdamages", context.into_json())
|
||||||
|
@ -14,7 +14,7 @@ use crate::{
|
|||||||
model::{
|
model::{
|
||||||
boat::Boat,
|
boat::Boat,
|
||||||
boatreservation::{BoatReservation, BoatReservationToAdd},
|
boatreservation::{BoatReservation, BoatReservationToAdd},
|
||||||
user::{DonauLinzUser, User, UserWithRoles},
|
user::{DonauLinzUser, User, UserWithRolesAndNotificationCount},
|
||||||
},
|
},
|
||||||
tera::log::KioskCookie,
|
tera::log::KioskCookie,
|
||||||
};
|
};
|
||||||
@ -74,7 +74,7 @@ async fn index(
|
|||||||
context.insert("user", &User::all(db).await);
|
context.insert("user", &User::all(db).await);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(user.into(), db).await,
|
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("boatreservations", context.into_json())
|
Template::render("boatreservations", context.into_json())
|
||||||
|
@ -105,7 +105,7 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
|
|||||||
"Du hast dich bereits als Ruderer angemeldet!",
|
"Du hast dich bereits als Ruderer angemeldet!",
|
||||||
),
|
),
|
||||||
Err(CoxHelpError::DetailsLocked) => {
|
Err(CoxHelpError::DetailsLocked) => {
|
||||||
Flash::error(Redirect::to("/planned"), "Boot ist bereits eingeteilt.")
|
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 {
|
} else {
|
||||||
@ -151,7 +151,7 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
|
|||||||
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
|
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
|
||||||
}
|
}
|
||||||
Err(TripHelpDeleteError::DetailsLocked) => {
|
Err(TripHelpDeleteError::DetailsLocked) => {
|
||||||
Flash::error(Redirect::to("/planned"), "Boot bereits eingeteilt")
|
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) => {
|
Err(TripHelpDeleteError::CoxNotHelping) => {
|
||||||
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
|
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
|
||||||
|
@ -18,7 +18,7 @@ use tera::Context;
|
|||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
log::Log,
|
log::Log,
|
||||||
user::{AdminUser, User, UserWithRoles},
|
user::{AdminUser, User, UserWithRolesAndNotificationCount},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -51,7 +51,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"ergo.final",
|
"ergo.final",
|
||||||
context!(loggedin_user: &UserWithRoles::from_user(_user.user, db).await, thirty, dozen),
|
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(_user.user, db).await, thirty, dozen),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +120,10 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
|||||||
if let Some(msg) = flash {
|
if let Some(msg) = flash {
|
||||||
context.insert("flash", &msg.into_inner());
|
context.insert("flash", &msg.into_inner());
|
||||||
}
|
}
|
||||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
context.insert(
|
||||||
|
"loggedin_user",
|
||||||
|
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
||||||
|
);
|
||||||
context.insert("users", &users);
|
context.insert("users", &users);
|
||||||
context.insert("thirty", &thirty);
|
context.insert("thirty", &thirty);
|
||||||
context.insert("dozen", &dozen);
|
context.insert("dozen", &dozen);
|
||||||
|
@ -24,7 +24,9 @@ use crate::model::{
|
|||||||
LogbookUpdateError,
|
LogbookUpdateError,
|
||||||
},
|
},
|
||||||
logtype::LogType,
|
logtype::LogType,
|
||||||
user::{AdminUser, DonauLinzUser, User, UserWithRoles, UserWithWaterStatus},
|
user::{
|
||||||
|
AdminUser, DonauLinzUser, User, UserWithRolesAndNotificationCount, UserWithWaterStatus,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct KioskCookie(String);
|
pub struct KioskCookie(String);
|
||||||
@ -84,7 +86,7 @@ async fn index(
|
|||||||
context.insert("logtypes", &logtypes);
|
context.insert("logtypes", &logtypes);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(user.into(), db).await,
|
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
context.insert("on_water", &on_water);
|
context.insert("on_water", &on_water);
|
||||||
context.insert("distances", &distances);
|
context.insert("distances", &distances);
|
||||||
@ -98,7 +100,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"log.completed",
|
"log.completed",
|
||||||
context!(logs, loggedin_user: &UserWithRoles::from_user(user.into(), db).await),
|
context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +110,7 @@ async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Te
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"log.completed",
|
"log.completed",
|
||||||
context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await),
|
context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.user, db).await),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ use tera::Context;
|
|||||||
use crate::model::{
|
use crate::model::{
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{User, UserWithRoles},
|
user::{User, UserWithRolesAndNotificationCount},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod admin;
|
pub(crate) mod admin;
|
||||||
@ -53,7 +53,10 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.insert("notifications", &Notification::for_user(db, &user).await);
|
context.insert("notifications", &Notification::for_user(db, &user).await);
|
||||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
context.insert(
|
||||||
|
"loggedin_user",
|
||||||
|
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
||||||
|
);
|
||||||
Template::render("index", context.into_json())
|
Template::render("index", context.into_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +78,10 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
|
|||||||
context.insert("coxes", &coxes);
|
context.insert("coxes", &coxes);
|
||||||
context.insert("bootskundige", &bootskundige);
|
context.insert("bootskundige", &bootskundige);
|
||||||
|
|
||||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
context.insert(
|
||||||
|
"loggedin_user",
|
||||||
|
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
||||||
|
);
|
||||||
Template::render("steering", context.into_json())
|
Template::render("steering", context.into_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ use crate::model::{
|
|||||||
logbook::Logbook,
|
logbook::Logbook,
|
||||||
tripdetails::TripDetails,
|
tripdetails::TripDetails,
|
||||||
triptype::TripType,
|
triptype::TripType,
|
||||||
user::{AllowedForPlannedTripsUser, User, UserWithRoles},
|
user::{AllowedForPlannedTripsUser, User, UserWithRolesAndNotificationCount},
|
||||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -44,7 +44,10 @@ async fn index(
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.insert("fee", &user.fee(db).await);
|
context.insert("fee", &user.fee(db).await);
|
||||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
context.insert(
|
||||||
|
"loggedin_user",
|
||||||
|
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
||||||
|
);
|
||||||
context.insert("days", &days);
|
context.insert("days", &days);
|
||||||
Template::render("planned", context.into_json())
|
Template::render("planned", context.into_json())
|
||||||
}
|
}
|
||||||
@ -107,7 +110,7 @@ async fn join(
|
|||||||
),
|
),
|
||||||
Err(UserTripError::DetailsLocked) => Flash::error(
|
Err(UserTripError::DetailsLocked) => Flash::error(
|
||||||
Redirect::to("/planned"),
|
Redirect::to("/planned"),
|
||||||
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
|
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,7 +151,7 @@ async fn remove_guest(
|
|||||||
)
|
)
|
||||||
.await;
|
.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"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
|
||||||
}
|
}
|
||||||
Err(UserTripDeleteError::GuestNotParticipating) => {
|
Err(UserTripDeleteError::GuestNotParticipating) => {
|
||||||
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
|
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
|
||||||
|
@ -4,7 +4,7 @@ use sqlx::SqlitePool;
|
|||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
stat::{self, BoatStat, Stat},
|
stat::{self, BoatStat, Stat},
|
||||||
user::{DonauLinzUser, UserWithRoles},
|
user::{DonauLinzUser, UserWithRolesAndNotificationCount},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::log::KioskCookie;
|
use super::log::KioskCookie;
|
||||||
@ -16,7 +16,7 @@ async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"stat.boats",
|
"stat.boats",
|
||||||
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, kiosk),
|
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, kiosk),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"stat.people",
|
"stat.people",
|
||||||
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,24 @@
|
|||||||
{{ loggedin_user.name }}
|
{{ loggedin_user.name }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex items-center">
|
||||||
|
{% if loggedin_user.amount_unread_notifications > 0 %}
|
||||||
|
<a href="/#notification"
|
||||||
|
class="relative inline-flex items-end ms-2 me-3">
|
||||||
|
<svg height="20"
|
||||||
|
width="24"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 24">
|
||||||
|
<path d="M1.5 8.67v8.58a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V8.67l-8.928 5.493a3 3 0 0 1-3.144 0L1.5 8.67Z" />
|
||||||
|
<path d="M22.5 6.908V6.75a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3v.158l9.714 5.978a1.5 1.5 0 0 0 1.572 0L22.5 6.908Z" />
|
||||||
|
</svg>
|
||||||
|
<small class="bg-red-500 rounded-full w-3 h-3 inline-flex justify-center items-center absolute p-1 notification">
|
||||||
|
{{ loggedin_user.amount_unread_notifications }}
|
||||||
|
</small>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 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="inline-flex justify-center rounded-md bg-primary-600 mx-1 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"
|
||||||
data-sidebar="true"
|
data-sidebar="true"
|
||||||
@ -163,18 +180,24 @@
|
|||||||
<div class="p-2 border border-t-0 border-{{ bg }} mb-4 rounded-b-md">
|
<div class="p-2 border border-t-0 border-{{ bg }} mb-4 rounded-b-md">
|
||||||
{% if participants | length > 0 %}
|
{% if participants | length > 0 %}
|
||||||
{% for rower in participants %}
|
{% for rower in participants %}
|
||||||
{{ rower.name }}
|
<div class="relative">
|
||||||
{% if rower.is_guest %}<small class="text-gray-600 dark:text-gray-100">(Scheckbuch)</small>{% endif %}
|
{{ rower.name }}
|
||||||
{% if rower.is_real_guest %}
|
{% if rower.is_guest %}<small class="text-gray-600 dark:text-gray-100">(Scheckbuch)</small>{% endif %}
|
||||||
<small class="text-gray-600 dark:text-gray-100">(Gast)</small>
|
{% if rower.is_real_guest %}
|
||||||
{% if allow_removing %}
|
<small class="text-gray-600 dark:text-gray-100">(Gast)</small>
|
||||||
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}"
|
{% if allow_removing %}
|
||||||
class="btn btn-attention btn-fw">Abmelden</a>
|
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}"
|
||||||
|
class="absolute r-0 bg-red-500 w-5 h-5 text-white rounded-full flex items-center justify-center transform rotate-45 top-0 right-0">
|
||||||
|
<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>
|
||||||
|
<span class="sr-only">Abmelden</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
<span class="hidden">(angemeldet seit
|
||||||
<span class="hidden">(angemeldet seit
|
{{ rower.registered_at }})</span>
|
||||||
{{ rower.registered_at }})</span>
|
</div>
|
||||||
<br />
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ text }}
|
{{ text }}
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
{% extends "base" %}
|
{% extends "base" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-screen-lg w-full">
|
<div class="max-w-screen-lg w-full">
|
||||||
<h1 class="h1">Ruderassistent</h1>
|
<h1 class="h1">
|
||||||
|
Ruder
|
||||||
|
<wbr />
|
||||||
|
assistent
|
||||||
|
</h1>
|
||||||
<div class="grid gap-3 my-5">
|
<div class="grid gap-3 my-5">
|
||||||
<div class="m-auto">
|
<div class="m-auto">
|
||||||
<a href="/planned"
|
<a href="/planned"
|
||||||
@ -11,9 +15,15 @@
|
|||||||
<span class="text-xl px-3">Geplante Ausfahrten</span>
|
<span class="text-xl px-3">Geplante Ausfahrten</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
<div id="notification"
|
||||||
|
class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
role="alert">
|
role="alert">
|
||||||
<h2 class="h2">Nachrichten</h2>
|
<h2 class="h2">Nachrichten</h2>
|
||||||
|
{% 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">
|
||||||
|
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 (✓).
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="divide-y">
|
<div class="divide-y">
|
||||||
{% for notification in notifications %}
|
{% for notification in notifications %}
|
||||||
{% if not notification.read_at %}
|
{% if not notification.read_at %}
|
||||||
@ -38,8 +48,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<details class="py-3 bg-gray-200 dark:bg-primary-950 rounded-b-md">
|
<details class="py-3 border-t rounded-b-md">
|
||||||
<summary class="px-3">Vergangene Nachrichten (14 Tage)</summary>
|
<summary class="px-3 cursor-pointer">Vergangene Nachrichten (14 Tage)</summary>
|
||||||
<div class="divide-y text-sm">
|
<div class="divide-y text-sm">
|
||||||
{% for notification in notifications %}
|
{% for notification in notifications %}
|
||||||
{% if notification.read_at %}
|
{% if notification.read_at %}
|
||||||
|
Loading…
Reference in New Issue
Block a user