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"); | ||||||
| @@ -39,7 +39,7 @@ test.describe("cox can edit trips", () => { | |||||||
|     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 %} | ||||||
|  |                 <div class="relative"> | ||||||
|                     {{ rower.name }} |                     {{ rower.name }} | ||||||
|                     {% if rower.is_guest %}<small class="text-gray-600 dark:text-gray-100">(Scheckbuch)</small>{% endif %} |                     {% if rower.is_guest %}<small class="text-gray-600 dark:text-gray-100">(Scheckbuch)</small>{% endif %} | ||||||
|                     {% if rower.is_real_guest %} |                     {% if rower.is_real_guest %} | ||||||
|                         <small class="text-gray-600 dark:text-gray-100">(Gast)</small> |                         <small class="text-gray-600 dark:text-gray-100">(Gast)</small> | ||||||
|                         {% if allow_removing %} |                         {% if allow_removing %} | ||||||
|                             <a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}" |                             <a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}" | ||||||
|                            class="btn btn-attention btn-fw">Abmelden</a> |                               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> | ||||||
|                 <br /> |                 </div> | ||||||
|             {% 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 %} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user