notification #292
| @@ -150,3 +150,12 @@ CREATE TABLE IF NOT EXISTS "boathouse" ( | |||||||
|     CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space  |     CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space  | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | CREATE TABLE IF NOT EXISTS "notification" ( | ||||||
|  | 	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||||
|  | 	"user_id" INTEGER NOT NULL REFERENCES user(id), | ||||||
|  | 	"message" TEXT NOT NULL, | ||||||
|  | 	"read_at" DATETIME, | ||||||
|  | 	"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, | ||||||
|  | 	"category" TEXT NOT NULL, | ||||||
|  | 	"link" TEXT | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -61,3 +61,4 @@ INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_ste | |||||||
| INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3); | INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3); | ||||||
| INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02'); | INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02'); | ||||||
| INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1); | INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1); | ||||||
|  | INSERT INTO "notification" (user_id, message, category) VALUES (1, 'This is a test notification', 'test-cat'); | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ use rocket::FromForm; | |||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||||
|  |  | ||||||
| use super::{boat::Boat, log::Log, rower::Rower, user::User}; | use super::{boat::Boat, log::Log, notification::Notification, rower::Rower, user::User}; | ||||||
|  |  | ||||||
| #[derive(FromRow, Serialize, Clone, Debug)] | #[derive(FromRow, Serialize, Clone, Debug)] | ||||||
| pub struct Logbook { | pub struct Logbook { | ||||||
| @@ -525,6 +525,23 @@ ORDER BY departure DESC | |||||||
|             Rower::create(db, self.id, *rower) |             Rower::create(db, self.id, *rower) | ||||||
|                 .await |                 .await | ||||||
|                 .map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?; |                 .map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?; | ||||||
|  |  | ||||||
|  |             let user = User::find_by_id_tx(db, *rower as i32).await.unwrap(); | ||||||
|  |             Notification::create_with_tx( | ||||||
|  |                 db, | ||||||
|  |                 &user, | ||||||
|  |                 &format!( | ||||||
|  |                     "Neuer Logbucheintrag: Ausfahrt am {}.{}.{} nach {} ({} km)", | ||||||
|  |                     dep.day(), | ||||||
|  |                     dep.month(), | ||||||
|  |                     dep.year(), | ||||||
|  |                     log.destination, | ||||||
|  |                     log.distance_in_km | ||||||
|  |                 ), | ||||||
|  |                 "Neuer Logbucheintrag", | ||||||
|  |                 None, | ||||||
|  |             ) | ||||||
|  |             .await; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         sqlx::query!( |         sqlx::query!( | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ pub mod log; | |||||||
| pub mod logbook; | pub mod logbook; | ||||||
| pub mod logtype; | pub mod logtype; | ||||||
| pub mod mail; | pub mod mail; | ||||||
|  | pub mod notification; | ||||||
| pub mod planned_event; | pub mod planned_event; | ||||||
| pub mod role; | pub mod role; | ||||||
| pub mod rower; | pub mod rower; | ||||||
|   | |||||||
| @@ -1,36 +1,78 @@ | |||||||
| use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; | use std::ops::DerefMut; | ||||||
|  |  | ||||||
|  | use chrono::NaiveDateTime; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use sqlx::{FromRow, SqlitePool}; | use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||||
|  |  | ||||||
|  | use super::user::User; | ||||||
|  |  | ||||||
| #[derive(FromRow, Debug, Serialize, Deserialize)] | #[derive(FromRow, Debug, Serialize, Deserialize)] | ||||||
| pub struct Notification { | pub struct Notification { | ||||||
|     pub id: i64, |     pub id: i64, | ||||||
|     pub user_id: i64, |     pub user_id: i64, | ||||||
|     pub message: String, |     pub message: String, | ||||||
|     pub read_at: NaiveDateTime, |     pub read_at: Option<NaiveDateTime>, | ||||||
|  |     pub created_at: NaiveDateTime, | ||||||
|     pub category: String, |     pub category: String, | ||||||
|  |     pub link: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Notification { | impl Notification { | ||||||
|     //pub async fn create(db: &SqlitePool, msg: String) -> bool { |     pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { | ||||||
|     //    sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,) |         sqlx::query_as!(Self, "SELECT  * FROM notification WHERE id like ?", id) | ||||||
|     //        .execute(db) |             .fetch_one(db) | ||||||
|     //        .await |             .await | ||||||
|     //        .is_ok() |             .ok() | ||||||
|     //} |     } | ||||||
|  |     pub async fn create_with_tx( | ||||||
|  |         db: &mut Transaction<'_, Sqlite>, | ||||||
|  |         user: &User, | ||||||
|  |         message: &str, | ||||||
|  |         category: &str, | ||||||
|  |         link: Option<&str>, | ||||||
|  |     ) { | ||||||
|  |         sqlx::query!( | ||||||
|  |             "INSERT INTO notification(user_id, message, category, link) VALUES (?, ?, ?, ?)", | ||||||
|  |             user.id, | ||||||
|  |             message, | ||||||
|  |             category, | ||||||
|  |             link | ||||||
|  |         ) | ||||||
|  |         .execute(db.deref_mut()) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> { |     pub async fn create( | ||||||
|  |         db: &SqlitePool, | ||||||
|  |         user: &User, | ||||||
|  |         message: &str, | ||||||
|  |         category: &str, | ||||||
|  |         link: Option<&str>, | ||||||
|  |     ) { | ||||||
|  |         let mut tx = db.begin().await.unwrap(); | ||||||
|  |         Self::create_with_tx(&mut tx, user, message, category, link).await; | ||||||
|  |         tx.commit().await.unwrap(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> { | ||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Log, |             Self, | ||||||
|             " |             "SELECT * FROM notification WHERE user_id = ?", | ||||||
| SELECT id, user_id, message, read_at, category |  | ||||||
| FROM notification |  | ||||||
| WHERE user_id = {} |  | ||||||
|     ", |  | ||||||
|             user.id |             user.id | ||||||
|         ) |         ) | ||||||
|         .fetch_all(db) |         .fetch_all(db) | ||||||
|         .await |         .await | ||||||
|         .unwrap() |         .unwrap() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub async fn mark_read(self, db: &SqlitePool) { | ||||||
|  |         sqlx::query!( | ||||||
|  |             "UPDATE notification SET read_at=CURRENT_TIMESTAMP WHERE id=?", | ||||||
|  |             self.id | ||||||
|  |         ) | ||||||
|  |         .execute(db) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,21 +3,22 @@ use serde::Serialize; | |||||||
| use sqlx::SqlitePool; | use sqlx::SqlitePool; | ||||||
|  |  | ||||||
| use super::{ | use super::{ | ||||||
|  |     notification::Notification, | ||||||
|     planned_event::{PlannedEvent, Registration}, |     planned_event::{PlannedEvent, Registration}, | ||||||
|     tripdetails::TripDetails, |     tripdetails::TripDetails, | ||||||
|     triptype::TripType, |     triptype::TripType, | ||||||
|     user::CoxUser, |     user::{CoxUser, User}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #[derive(Serialize, Clone, Debug)] | #[derive(Serialize, Clone, Debug)] | ||||||
| pub struct Trip { | pub struct Trip { | ||||||
|     id: i64, |     id: i64, | ||||||
|     cox_id: i64, |     pub cox_id: i64, | ||||||
|     cox_name: String, |     cox_name: String, | ||||||
|     trip_details_id: Option<i64>, |     trip_details_id: Option<i64>, | ||||||
|     planned_starting_time: String, |     planned_starting_time: String, | ||||||
|     pub max_people: i64, |     pub max_people: i64, | ||||||
|     day: String, |     pub day: String, | ||||||
|     pub notes: Option<String>, |     pub notes: Option<String>, | ||||||
|     pub allow_guests: bool, |     pub allow_guests: bool, | ||||||
|     trip_type_id: Option<i64>, |     trip_type_id: Option<i64>, | ||||||
| @@ -43,6 +44,51 @@ impl Trip { | |||||||
|         ) |         ) | ||||||
|         .execute(db) |         .execute(db) | ||||||
|         .await; |         .await; | ||||||
|  |  | ||||||
|  |         let same_starting_datetime = TripDetails::find_by_startingdatetime( | ||||||
|  |             db, | ||||||
|  |             trip_details.day, | ||||||
|  |             trip_details.planned_starting_time, | ||||||
|  |         ) | ||||||
|  |         .await; | ||||||
|  |         if same_starting_datetime.len() > 1 { | ||||||
|  |             for notify in same_starting_datetime { | ||||||
|  |                 if notify.id != trip_details.id { | ||||||
|  |                     // notify everyone except oneself | ||||||
|  |                     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(); | ||||||
|  |                         Notification::create( | ||||||
|  |                             db, | ||||||
|  |                             &user, | ||||||
|  |                             &format!( | ||||||
|  |                                 "{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt", | ||||||
|  |                                 user.name, trip.day, trip.planned_starting_time | ||||||
|  |                             ), | ||||||
|  |                             "Neue Ausfahrt zur selben Zeit".into(), | ||||||
|  |                             None, | ||||||
|  |                         ) | ||||||
|  |                         .await; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> { | ||||||
|  |         sqlx::query_as!( | ||||||
|  |             Self, | ||||||
|  |             " | ||||||
|  | SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked | ||||||
|  | FROM trip  | ||||||
|  | INNER JOIN trip_details ON trip.trip_details_id = trip_details.id | ||||||
|  | INNER JOIN user ON trip.cox_id = user.id | ||||||
|  | WHERE trip_details.id=? | ||||||
|  |         ", | ||||||
|  |             tripdetails_id | ||||||
|  |         ) | ||||||
|  |         .fetch_one(db) | ||||||
|  |         .await | ||||||
|  |         .ok() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { |     pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { | ||||||
|   | |||||||
| @@ -46,6 +46,24 @@ WHERE id like ? | |||||||
|         .ok() |         .ok() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub async fn find_by_startingdatetime( | ||||||
|  |         db: &SqlitePool, | ||||||
|  |         day: String, | ||||||
|  |         planned_starting_time: String, | ||||||
|  |     ) -> Vec<Self> { | ||||||
|  |         sqlx::query_as!( | ||||||
|  |             Self, | ||||||
|  |             " | ||||||
|  | SELECT id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked | ||||||
|  | FROM trip_details  | ||||||
|  | WHERE day = ? AND planned_starting_time = ? | ||||||
|  |         " | ||||||
|  |         , day, planned_starting_time | ||||||
|  |         ) | ||||||
|  |         .fetch_all(db) | ||||||
|  |         .await.unwrap() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /// Creates a new entry in `trip_details` and returns its id. |     /// Creates a new entry in `trip_details` and returns its id. | ||||||
|     pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 { |     pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 { | ||||||
|         let query = sqlx::query!( |         let query = sqlx::query!( | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| use sqlx::SqlitePool; | use sqlx::SqlitePool; | ||||||
|  |  | ||||||
| use super::{tripdetails::TripDetails, user::User}; | use super::{notification::Notification, trip::Trip, tripdetails::TripDetails, user::User}; | ||||||
| use crate::model::tripdetails::{Action, CoxAtTrip::Yes}; | use crate::model::tripdetails::{Action, CoxAtTrip::Yes}; | ||||||
|  |  | ||||||
| pub struct UserTrip {} | pub struct UserTrip {} | ||||||
| @@ -27,6 +27,7 @@ impl UserTrip { | |||||||
|         //TODO: Check if user sees the event (otherwise she could forge trip_details_id) |         //TODO: Check if user sees the event (otherwise she could forge trip_details_id) | ||||||
|  |  | ||||||
|         let is_cox = trip_details.user_is_cox(db, user).await; |         let is_cox = trip_details.user_is_cox(db, user).await; | ||||||
|  |         let mut name_newly_registered_person = String::new(); | ||||||
|         if user_note.is_none() { |         if user_note.is_none() { | ||||||
|             if let Yes(action) = is_cox { |             if let Yes(action) = is_cox { | ||||||
|                 match action { |                 match action { | ||||||
| @@ -47,6 +48,8 @@ impl UserTrip { | |||||||
|             .execute(db) |             .execute(db) | ||||||
|             .await |             .await | ||||||
|             .unwrap(); |             .unwrap(); | ||||||
|  |  | ||||||
|  |             name_newly_registered_person = user.name.clone(); | ||||||
|         } else { |         } else { | ||||||
|             if !trip_details.user_allowed_to_change(db, user).await { |             if !trip_details.user_allowed_to_change(db, user).await { | ||||||
|                 return Err(UserTripError::NotAllowedToAddGuest); |                 return Err(UserTripError::NotAllowedToAddGuest); | ||||||
| @@ -59,6 +62,23 @@ impl UserTrip { | |||||||
|             .execute(db) |             .execute(db) | ||||||
|             .await |             .await | ||||||
|             .unwrap(); |             .unwrap(); | ||||||
|  |  | ||||||
|  |             name_newly_registered_person = user_note.unwrap(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await { | ||||||
|  |             let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap(); | ||||||
|  |             Notification::create( | ||||||
|  |                 db, | ||||||
|  |                 &cox, | ||||||
|  |                 &format!( | ||||||
|  |                     "{} hat sich für deine Ausfahrt am {} registriert", | ||||||
|  |                     name_newly_registered_person, trip.day | ||||||
|  |                 ), | ||||||
|  |                 "Registrierung bei Ausfahrt", | ||||||
|  |                 None, | ||||||
|  |             ) | ||||||
|  |             .await; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Ok(()) |         Ok(()) | ||||||
|   | |||||||
| @@ -17,7 +17,10 @@ use serde::Deserialize; | |||||||
| use sqlx::SqlitePool; | use sqlx::SqlitePool; | ||||||
| use tera::Context; | use tera::Context; | ||||||
|  |  | ||||||
| use crate::model::user::{User, UserWithRoles}; | use crate::model::{ | ||||||
|  |     notification::Notification, | ||||||
|  |     user::{User, UserWithRoles}, | ||||||
|  | }; | ||||||
|  |  | ||||||
| pub(crate) mod admin; | pub(crate) mod admin; | ||||||
| mod auth; | mod auth; | ||||||
| @@ -27,6 +30,7 @@ mod cox; | |||||||
| mod ergo; | mod ergo; | ||||||
| mod log; | mod log; | ||||||
| mod misc; | mod misc; | ||||||
|  | mod notification; | ||||||
| mod planned; | mod planned; | ||||||
| mod stat; | mod stat; | ||||||
|  |  | ||||||
| @@ -43,6 +47,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_ | |||||||
|         context.insert("flash", &msg.into_inner()); |         context.insert("flash", &msg.into_inner()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     context.insert("notifications", &Notification::for_user(db, &user).await); | ||||||
|     context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); |     context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); | ||||||
|     Template::render("index", context.into_json()) |     Template::render("index", context.into_json()) | ||||||
| } | } | ||||||
| @@ -86,6 +91,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { | |||||||
|         .mount("/log", log::routes()) |         .mount("/log", log::routes()) | ||||||
|         .mount("/planned", planned::routes()) |         .mount("/planned", planned::routes()) | ||||||
|         .mount("/ergo", ergo::routes()) |         .mount("/ergo", ergo::routes()) | ||||||
|  |         .mount("/notification", notification::routes()) | ||||||
|         .mount("/stat", stat::routes()) |         .mount("/stat", stat::routes()) | ||||||
|         .mount("/boatdamage", boatdamage::routes()) |         .mount("/boatdamage", boatdamage::routes()) | ||||||
|         .mount("/cox", cox::routes()) |         .mount("/cox", cox::routes()) | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								src/tera/notification.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/tera/notification.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | use rocket::{ | ||||||
|  |     get, | ||||||
|  |     response::{Flash, Redirect}, | ||||||
|  |     routes, Route, State, | ||||||
|  | }; | ||||||
|  | use sqlx::SqlitePool; | ||||||
|  |  | ||||||
|  | use crate::model::{notification::Notification, user::User}; | ||||||
|  |  | ||||||
|  | #[get("/<notification_id>/read")] | ||||||
|  | async fn mark_read(db: &State<SqlitePool>, user: User, notification_id: i64) -> Flash<Redirect> { | ||||||
|  |     let Some(notification) = Notification::find_by_id(db, notification_id).await else { | ||||||
|  |         return Flash::error( | ||||||
|  |             Redirect::to("/"), | ||||||
|  |             format!("Nachricht mit ID {notification_id} nicht gefunden."), | ||||||
|  |         ); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if notification.user_id == user.id { | ||||||
|  |         notification.mark_read(db).await; | ||||||
|  |         Flash::success(Redirect::to("/"), "Nachricht als gelesen markiert") | ||||||
|  |     } else { | ||||||
|  |         Flash::success( | ||||||
|  |             Redirect::to("/"), | ||||||
|  |             "Du kannst fremde Nachrichten nicht als gelesen markieren.", | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn routes() -> Vec<Route> { | ||||||
|  |     routes![mark_read] | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | CREATE TABLE IF NOT EXISTS "notification" ( | ||||||
|  | 	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||||
|  | 	"user_id" INTEGER NOT NULL REFERENCES user(id), | ||||||
|  | 	"message" TEXT NOT NULL, | ||||||
|  | 	"read_at" DATETIME, | ||||||
|  | 	"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, | ||||||
|  | 	"category" TEXT NOT NULL, | ||||||
|  | 	"link" TEXT | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -3,6 +3,57 @@ | |||||||
| {% 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">Ruderassistent</h1> | ||||||
|  |         <div class="grid gap-3"> | ||||||
|  |             <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" | ||||||
|  |                  role="alert"> | ||||||
|  |                 <h2 class="h2">Nachrichten</h2> | ||||||
|  |                 <div class="text-sm p-3"> | ||||||
|  |                     {% for notification in notifications %} | ||||||
|  |                         {% if not notification.read_at %} | ||||||
|  |                             <div class="relative flex bg-clip-border rounded-xl bg-white text-gray-700 shadow-md w-full flex-row"> | ||||||
|  |                                 <div class="p-6"> | ||||||
|  |                                     <h6 class="block mb-4 font-sans text-base antialiased font-semibold leading-relaxed tracking-normal text-gray-700 uppercase"> | ||||||
|  |                                         {{ notification.category }} | ||||||
|  |                                     </h6> | ||||||
|  |                                     <h4 class="block mb-2 font-sans text-2xl antialiased font-semibold leading-snug tracking-normal text-blue-gray-900"> | ||||||
|  |                                         {{ notification.message }} | ||||||
|  |                                     </h4> | ||||||
|  |                                     <p class="block mb-8 font-sans text-base antialiased font-normal leading-relaxed text-gray-700"> | ||||||
|  |                                         {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }} | ||||||
|  |                                     </p> | ||||||
|  |                                     {% if not notification.read_at %} | ||||||
|  |                                         <a href="/notification/{{ notification.id }}/read" class="inline-block"> | ||||||
|  |                                             <button class="flex items-center gap-2 px-6 py-3 font-sans text-xs font-bold text-center text-gray-900 uppercase align-middle transition-all rounded-lg select-none disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none hover:bg-gray-900/10 active:bg-gray-900/20" | ||||||
|  |                                                     type="button">Als gelesen markieren</button> | ||||||
|  |                                         </a> | ||||||
|  |                                     {% endif %} | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                         {% endif %} | ||||||
|  |                     {% endfor %} | ||||||
|  |                     <details> | ||||||
|  |                         <summary>Vergangene Nachrichten</summary> | ||||||
|  |                         {% for notification in notifications %} | ||||||
|  |                             {% if notification.read_at %} | ||||||
|  |                                 <div class="relative flex bg-clip-border rounded-xl bg-white text-gray-700 shadow-md w-full flex-row"> | ||||||
|  |                                     <div class="p-6"> | ||||||
|  |                                         <h6 class="block mb-4 font-sans text-base antialiased font-semibold leading-relaxed tracking-normal text-gray-700 uppercase"> | ||||||
|  |                                             {{ notification.category }} | ||||||
|  |                                         </h6> | ||||||
|  |                                         <h4 class="block mb-2 font-sans text-2xl antialiased font-semibold leading-snug tracking-normal text-blue-gray-900"> | ||||||
|  |                                             {{ notification.message }} | ||||||
|  |                                         </h4> | ||||||
|  |                                         <p class="block mb-8 font-sans text-base antialiased font-normal leading-relaxed text-gray-700"> | ||||||
|  |                                             {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }} | ||||||
|  |                                         </p> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             {% endif %} | ||||||
|  |                         {% endfor %} | ||||||
|  |                     </details> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="grid gap-3"> |         <div class="grid gap-3"> | ||||||
|             <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" |             <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" | ||||||
|                  role="alert"> |                  role="alert"> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user