diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index e5adfb0..520a154 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -72,5 +72,6 @@ export default defineConfig({ webServer: { timeout: 15 * 60 * 1000, command: 'cd .. && ./test_db.sh && cargo r', + url: 'http://127.0.0.1:8000' }, }); diff --git a/frontend/scss/app.scss b/frontend/scss/app.scss index 770dc11..02e94d7 100644 --- a/frontend/scss/app.scss +++ b/frontend/scss/app.scss @@ -13,3 +13,4 @@ @import 'components/search'; @import 'components/important'; @import 'components/searchable-table'; +@import 'components/notification'; diff --git a/frontend/scss/components/_notification.scss b/frontend/scss/components/_notification.scss new file mode 100644 index 0000000..0532299 --- /dev/null +++ b/frontend/scss/components/_notification.scss @@ -0,0 +1,5 @@ +.notification { + right: -.2rem; + top: -.1rem; + font-size: .5rem; +} diff --git a/frontend/tests/cox.spec.ts b/frontend/tests/cox.spec.ts index c19ae45..d38dc96 100644 --- a/frontend/tests/cox.spec.ts +++ b/frontend/tests/cox.spec.ts @@ -8,7 +8,7 @@ test("cox can create and delete trip", async ({ page }) => { await page.getByPlaceholder("Passwort").fill("cox"); await page.getByPlaceholder("Passwort").press("Enter"); 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").fill("18:00"); 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("Passwort").fill("cox"); await page.getByPlaceholder("Passwort").press("Enter"); - await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click(); - await page.locator(".relative").first().click(); + await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').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").fill("18:00"); await page.locator("#sidebar #planned_starting_time").press("Tab"); diff --git a/src/model/logbook.rs b/src/model/logbook.rs index 9ccae27..5d83f12 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -591,7 +591,7 @@ ORDER BY departure DESC } 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 || user.has_role(db, "Vorstand").await diff --git a/src/model/user.rs b/src/model/user.rs index 86e311a..83db24c 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -47,16 +47,18 @@ pub struct User { } #[derive(Debug, Serialize, Deserialize)] -pub struct UserWithRoles { +pub struct UserWithRolesAndNotificationCount { #[serde(flatten)] pub user: User, + pub amount_unread_notifications: i32, pub roles: Vec, } -impl UserWithRoles { +impl UserWithRolesAndNotificationCount { pub async fn from_user(user: User, db: &SqlitePool) -> Self { Self { roles: user.roles(db).await, + amount_unread_notifications: user.amount_unread_notifications(db).await, user, } } @@ -237,6 +239,17 @@ impl User { .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 { if sqlx::query!( "SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)", diff --git a/src/tera/admin/boat.rs b/src/tera/admin/boat.rs index e23d2e4..b034a38 100644 --- a/src/tera/admin/boat.rs +++ b/src/tera/admin/boat.rs @@ -2,7 +2,7 @@ use crate::model::{ boat::{Boat, BoatToAdd, BoatToUpdate}, location::Location, log::Log, - user::{AdminUser, User, UserWithRoles}, + user::{AdminUser, User, UserWithRolesAndNotificationCount}, }; use rocket::{ form::Form, @@ -33,7 +33,7 @@ async fn index( context.insert("users", &users); context.insert( "loggedin_user", - &UserWithRoles::from_user(admin.user, db).await, + &UserWithRolesAndNotificationCount::from_user(admin.user, db).await, ); Template::render("admin/boat/index", context.into_json()) diff --git a/src/tera/admin/mail.rs b/src/tera/admin/mail.rs index 2afcd3a..1f6f11c 100644 --- a/src/tera/admin/mail.rs +++ b/src/tera/admin/mail.rs @@ -10,7 +10,7 @@ use crate::model::log::Log; use crate::model::mail::Mail; use crate::model::role::Role; use crate::model::user::AdminUser; -use crate::model::user::UserWithRoles; +use crate::model::user::UserWithRolesAndNotificationCount; use crate::tera::Config; #[get("/mail")] @@ -27,7 +27,7 @@ async fn index( context.insert( "loggedin_user", - &UserWithRoles::from_user(admin.user, db).await, + &UserWithRolesAndNotificationCount::from_user(admin.user, db).await, ); context.insert("roles", &roles); diff --git a/src/tera/admin/notification.rs b/src/tera/admin/notification.rs index 2e36c51..0e7ab9a 100644 --- a/src/tera/admin/notification.rs +++ b/src/tera/admin/notification.rs @@ -2,7 +2,7 @@ use crate::model::{ log::Log, notification::Notification, role::Role, - user::{AdminUser, User, UserWithRoles}, + user::{AdminUser, User, UserWithRolesAndNotificationCount}, }; use rocket::{ form::Form, @@ -26,7 +26,7 @@ async fn index( } context.insert( "loggedin_user", - &UserWithRoles::from_user(user.user, db).await, + &UserWithRolesAndNotificationCount::from_user(user.user, db).await, ); context.insert("roles", &Role::all(db).await); diff --git a/src/tera/admin/schnupper.rs b/src/tera/admin/schnupper.rs index 41d536d..f08abb3 100644 --- a/src/tera/admin/schnupper.rs +++ b/src/tera/admin/schnupper.rs @@ -1,6 +1,6 @@ use crate::model::{ role::Role, - user::{SchnupperBetreuerUser, User, UserWithRoles}, + user::{SchnupperBetreuerUser, User, UserWithRolesAndNotificationCount}, }; use futures::future::join_all; use rocket::{ @@ -38,9 +38,9 @@ async fn index( let user_futures: Vec<_> = User::all_with_role(db, &schnupperant) .await .into_iter() - .map(|u| async move { UserWithRoles::from_user(u, db).await }) + .map(|u| async move { UserWithRolesAndNotificationCount::from_user(u, db).await }) .collect(); - let users: Vec = join_all(user_futures).await; + let users: Vec = join_all(user_futures).await; let mut context = Context::new(); if let Some(msg) = flash { @@ -49,7 +49,7 @@ async fn index( context.insert("schnupperanten", &users); context.insert( "loggedin_user", - &UserWithRoles::from_user(user.into(), db).await, + &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, ); Template::render("admin/schnupper/index", context.into_json()) diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 30c63b7..e5d9781 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -6,8 +6,8 @@ use crate::model::{ logbook::Logbook, role::Role, user::{ - AdminUser, User, UserWithMembershipPdf, UserWithRoles, UserWithRolesAndMembershipPdf, - VorstandUser, + AdminUser, User, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, + UserWithRolesAndNotificationCount, VorstandUser, }, }; use futures::future::join_all; @@ -67,7 +67,10 @@ async fn index( context.insert("users", &users); context.insert("roles", &roles); 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()) } @@ -99,7 +102,10 @@ async fn index_admin( context.insert("users", &users); context.insert("roles", &roles); 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()) } @@ -127,7 +133,7 @@ async fn fees( } context.insert( "loggedin_user", - &UserWithRoles::from_user(admin.into(), db).await, + &UserWithRolesAndNotificationCount::from_user(admin.into(), db).await, ); Template::render("admin/user/fees", context.into_json()) @@ -147,7 +153,7 @@ async fn scheckbuch( for s in scheckbooks { scheckbooks_with_roles.push(( 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( "loggedin_user", - &UserWithRoles::from_user(user.into(), db).await, + &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, ); Template::render("admin/user/scheckbuch", context.into_json()) diff --git a/src/tera/board/boathouse.rs b/src/tera/board/boathouse.rs index 266f81a..fbaa16c 100644 --- a/src/tera/board/boathouse.rs +++ b/src/tera/board/boathouse.rs @@ -1,7 +1,7 @@ use crate::model::{ boat::Boat, boathouse::Boathouse, - user::{AdminUser, UserWithRoles, VorstandUser}, + user::{AdminUser, UserWithRolesAndNotificationCount, VorstandUser}, }; use rocket::{ form::Form, @@ -39,7 +39,7 @@ async fn index( context.insert( "loggedin_user", - &UserWithRoles::from_user(admin.into(), db).await, + &UserWithRolesAndNotificationCount::from_user(admin.into(), db).await, ); Template::render("board/boathouse", context.into_json()) diff --git a/src/tera/boatdamage.rs b/src/tera/boatdamage.rs index 39088af..0fca62b 100644 --- a/src/tera/boatdamage.rs +++ b/src/tera/boatdamage.rs @@ -13,7 +13,7 @@ use crate::{ model::{ boat::Boat, boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, - user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles}, + user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRolesAndNotificationCount}, }, tera::log::KioskCookie, }; @@ -59,7 +59,7 @@ async fn index( context.insert("boats", &boats); context.insert( "loggedin_user", - &UserWithRoles::from_user(user.into(), db).await, + &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, ); Template::render("boatdamages", context.into_json()) diff --git a/src/tera/boatreservation.rs b/src/tera/boatreservation.rs index 0cc5574..06d42b3 100644 --- a/src/tera/boatreservation.rs +++ b/src/tera/boatreservation.rs @@ -14,7 +14,7 @@ use crate::{ model::{ boat::Boat, boatreservation::{BoatReservation, BoatReservationToAdd}, - user::{DonauLinzUser, User, UserWithRoles}, + user::{DonauLinzUser, User, UserWithRolesAndNotificationCount}, }, tera::log::KioskCookie, }; @@ -74,7 +74,7 @@ async fn index( context.insert("user", &User::all(db).await); context.insert( "loggedin_user", - &UserWithRoles::from_user(user.into(), db).await, + &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, ); Template::render("boatreservations", context.into_json()) diff --git a/src/tera/cox.rs b/src/tera/cox.rs index 0a04e8e..cc31580 100644 --- a/src/tera/cox.rs +++ b/src/tera/cox.rs @@ -105,7 +105,7 @@ async fn join(db: &State, planned_event_id: i64, cox: CoxUser) -> Fl "Du hast dich bereits als Ruderer angemeldet!", ), 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 { @@ -151,7 +151,7 @@ async fn remove(db: &State, planned_event_id: i64, cox: CoxUser) -> Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!") } 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) => { Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...") diff --git a/src/tera/ergo.rs b/src/tera/ergo.rs index 8818d6b..c419781 100644 --- a/src/tera/ergo.rs +++ b/src/tera/ergo.rs @@ -18,7 +18,7 @@ use tera::Context; use crate::model::{ log::Log, - user::{AdminUser, User, UserWithRoles}, + user::{AdminUser, User, UserWithRolesAndNotificationCount}, }; #[derive(Serialize)] @@ -51,7 +51,7 @@ async fn send(db: &State, _user: AdminUser) -> Template { Template::render( "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, user: User, flash: Option, user: DonauLinzUser) -> Template { Template::render( "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, user: AdminUser, year: i32) -> Te Template::render( "log.completed", - context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await), + context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.user, db).await), ) } diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 5f89c2b..fece01b 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -23,7 +23,7 @@ use tera::Context; use crate::model::{ notification::Notification, role::Role, - user::{User, UserWithRoles}, + user::{User, UserWithRolesAndNotificationCount}, }; pub(crate) mod admin; @@ -53,7 +53,10 @@ async fn index(db: &State, user: User, flash: Option, user: User, flash: Option Flash::error( 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; - 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) => { Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.") diff --git a/src/tera/stat.rs b/src/tera/stat.rs index 3280973..1a060c5 100644 --- a/src/tera/stat.rs +++ b/src/tera/stat.rs @@ -4,7 +4,7 @@ use sqlx::SqlitePool; use crate::model::{ stat::{self, BoatStat, Stat}, - user::{DonauLinzUser, UserWithRoles}, + user::{DonauLinzUser, UserWithRolesAndNotificationCount}, }; use super::log::KioskCookie; @@ -16,7 +16,7 @@ async fn index_boat(db: &State, user: DonauLinzUser) -> Template { Template::render( "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, user: DonauLinzUser, year: Option) - Template::render( "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), ) } diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index e591143..404ecf5 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -33,7 +33,24 @@ {{ loggedin_user.name }} -
+
+ {% if loggedin_user.amount_unread_notifications > 0 %} + + + + {{ loggedin_user.amount_unread_notifications }} + + + {% endif %} {% if participants | length > 0 %} {% for rower in participants %} - {{ rower.name }} - {% if rower.is_guest %}(Scheckbuch){% endif %} - {% if rower.is_real_guest %} - (Gast) - {% if allow_removing %} - Abmelden +
+ {{ rower.name }} + {% if rower.is_guest %}(Scheckbuch){% endif %} + {% if rower.is_real_guest %} + (Gast) + {% if allow_removing %} + + + + + Abmelden + + {% endif %} {% endif %} - {% endif %} - -
+ +
{% endfor %} {% else %} {{ text }} diff --git a/templates/index.html.tera b/templates/index.html.tera index 8899e66..b200554 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -2,7 +2,11 @@ {% extends "base" %} {% block content %}
-

Ruderassistent

+

+ Ruder + + assistent +

-