diff --git a/frontend/main.ts b/frontend/main.ts index 4ac89e2..876f650 100644 --- a/frontend/main.ts +++ b/frontend/main.ts @@ -7,6 +7,7 @@ export interface choiceMap { let choiceObjects: choiceMap = {}; let boat_in_ottensheim = true; +let boat_reserved_today= true; document.addEventListener("DOMContentLoaded", function () { changeTheme(); @@ -116,6 +117,7 @@ interface ChoiceBoatEvent extends Event { owner: number; default_destination: string; boat_in_ottensheim: boolean; + boat_reserved_today: boolean; }; }; } @@ -134,7 +136,12 @@ function selectBoatChange() { boatSelect.addEventListener( "addItem", function (e) { + const event = e as ChoiceBoatEvent; + boat_reserved_today = event.detail.customProperties.boat_reserved_today; + if (boat_reserved_today){ + alert(event.detail.label.trim()+' wurde heute reserviert. Bitte kontrolliere, dass du die Reservierung nicht störst.'); + } boat_in_ottensheim = event.detail.customProperties.boat_in_ottensheim; const amount_seats = event.detail.customProperties.amount_seats; diff --git a/migration.sql b/migration.sql index 4504f92..675c976 100644 --- a/migration.sql +++ b/migration.sql @@ -160,3 +160,16 @@ CREATE TABLE IF NOT EXISTS "notification" ( "category" TEXT NOT NULL, "link" TEXT ); + +CREATE TABLE IF NOT EXISTS "boat_reservation" ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "boat_id" INTEGER NOT NULL REFERENCES boat(id), + "start_date" DATE NOT NULL, + "end_date" DATE NOT NULL, + "time_desc" TEXT NOT NULL, + "usage" TEXT NOT NULL, + "user_id_applicant" INTEGER NOT NULL REFERENCES user(id), + "user_id_confirmation" INTEGER REFERENCES user(id), + "created_at" datetime not null default CURRENT_TIMESTAMP +); + diff --git a/src/model/boat.rs b/src/model/boat.rs index 4354de8..cb03e2c 100644 --- a/src/model/boat.rs +++ b/src/model/boat.rs @@ -36,9 +36,10 @@ pub enum BoatDamage { #[derive(Serialize, Deserialize, Debug)] pub struct BoatWithDetails { #[serde(flatten)] - boat: Boat, + pub(crate) boat: Boat, damage: BoatDamage, on_water: bool, + reserved_today: bool, } #[derive(FromForm)] @@ -135,6 +136,20 @@ impl Boat { sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some() } + pub async fn reserved_today(&self, db: &SqlitePool) -> bool { + sqlx::query!( + "SELECT * +FROM boat_reservation +WHERE boat_id =? +AND date('now') BETWEEN start_date AND end_date;", + self.id + ) + .fetch_optional(db) + .await + .unwrap() + .is_some() + } + pub async fn on_water(&self, db: &SqlitePool) -> bool { sqlx::query!( "SELECT * FROM logbook WHERE boat_id=? AND arrival is null", @@ -159,6 +174,7 @@ impl Boat { res.push(BoatWithDetails { damage, on_water: boat.on_water(db).await, + reserved_today: boat.reserved_today(db).await, boat, }); } diff --git a/src/model/boatreservation.rs b/src/model/boatreservation.rs new file mode 100644 index 0000000..c20e841 --- /dev/null +++ b/src/model/boatreservation.rs @@ -0,0 +1,178 @@ +use crate::model::{boat::Boat, user::User}; +use chrono::NaiveDate; +use chrono::NaiveDateTime; +use rocket::serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; + +use super::log::Log; +use super::notification::Notification; +use super::role::Role; + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct BoatReservation { + pub id: i64, + pub boat_id: i64, + pub start_date: NaiveDate, + pub end_date: NaiveDate, + pub time_desc: String, + pub usage: String, + pub user_id_applicant: i64, + pub user_id_confirmation: Option, + pub created_at: NaiveDateTime, +} + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct BoatReservationWithDetails { + #[serde(flatten)] + boat_reservation: BoatReservation, + boat: Boat, + user_applicant: User, + user_confirmation: Option, +} + +#[derive(Debug)] +pub struct BoatReservationToAdd<'r> { + pub boat: &'r Boat, + pub start_date: NaiveDate, + pub end_date: NaiveDate, + pub time_desc: &'r str, + pub usage: &'r str, + pub user_applicant: &'r User, +} + +impl BoatReservation { + pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { + sqlx::query_as!( + Self, + "SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at + FROM boat_reservation + WHERE id like ?", + id + ) + .fetch_one(db) + .await + .ok() + } + + pub async fn all_future(db: &SqlitePool) -> Vec { + let boatreservations = sqlx::query_as!( + Self, + " +SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at +FROM boat_reservation +WHERE end_date >= CURRENT_DATE ORDER BY end_date + " + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + + let mut res = Vec::new(); + for reservation in boatreservations { + let user_confirmation = match reservation.user_id_confirmation { + Some(id) => { + let user = User::find_by_id(db, id as i32).await; + Some(user.unwrap()) + } + None => None, + }; + let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32) + .await + .unwrap(); + let boat = Boat::find_by_id(db, reservation.boat_id as i32) + .await + .unwrap(); + + res.push(BoatReservationWithDetails { + boat_reservation: reservation, + boat, + user_applicant, + user_confirmation, + }); + } + res + } + + pub async fn create( + db: &SqlitePool, + boatreservation: BoatReservationToAdd<'_>, + ) -> Result<(), String> { + if Self::boat_reserved_between_dates( + db, + boatreservation.boat, + &boatreservation.start_date, + &boatreservation.end_date, + ) + .await + { + return Err("Boot in diesem Zeitraum bereits reserviert.".into()); + } + + Log::create(db, format!("New boat reservation: {boatreservation:?}")).await; + + sqlx::query!( + "INSERT INTO boat_reservation(boat_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)", + boatreservation.boat.id, + boatreservation.start_date, + boatreservation.end_date, + boatreservation.time_desc, + boatreservation.usage, + boatreservation.user_applicant.id, + ) + .execute(db) + .await + .map_err(|e| e.to_string())?; + + let board = + User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await; + for user in board { + Notification::create( + db, + &user, + &format!( + "{} hat eine neue Bootsreservierung für Boot '{}' angelegt: Von {} bis {} um {} wegen {}", + boatreservation.user_applicant.name, + boatreservation.boat.name, + boatreservation.start_date, + boatreservation.end_date, + boatreservation.time_desc, + boatreservation.usage + ), + "Neue Bootsreservierung", + None, + ) + .await; + } + + Ok(()) + } + + pub async fn boat_reserved_between_dates( + db: &SqlitePool, + boat: &Boat, + start_date: &NaiveDate, + end_date: &NaiveDate, + ) -> bool { + sqlx::query!( + "SELECT COUNT(*) AS reservation_count +FROM boat_reservation +WHERE boat_id = ? +AND start_date <= ? AND end_date >= ?;", + boat.id, + end_date, + start_date + ) + .fetch_one(db) + .await + .unwrap() + .reservation_count + > 0 + } + + pub async fn delete(&self, db: &SqlitePool) { + sqlx::query!("DELETE FROM boat_reservation WHERE id=?", self.id) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a Boat of a valid id + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index b1e0416..55c6bcc 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -10,6 +10,7 @@ use self::{ pub mod boat; pub mod boatdamage; pub mod boathouse; +pub mod boatreservation; pub mod family; pub mod location; pub mod log; diff --git a/src/model/planned_event.rs b/src/model/planned_event.rs index 885ec64..6ecef00 100644 --- a/src/model/planned_event.rs +++ b/src/model/planned_event.rs @@ -6,7 +6,7 @@ use ics::{ Event, ICalendar, }; use serde::Serialize; -use sqlx::{FromRow, SqlitePool, Row}; +use sqlx::{FromRow, Row, SqlitePool}; use super::{tripdetails::TripDetails, triptype::TripType, user::User}; @@ -63,7 +63,7 @@ FROM user_trip WHERE trip_details_id = {} .await .unwrap() .into_iter() - .map(|r| + .map(|r| Registration { name: r.get::, usize>(0).or(r.get::, usize>(1)).unwrap(), //Ok, either name or user_note needs to be set registered_at: r.get::(3), diff --git a/src/tera/boatreservation.rs b/src/tera/boatreservation.rs new file mode 100644 index 0000000..bbb67ce --- /dev/null +++ b/src/tera/boatreservation.rs @@ -0,0 +1,171 @@ +use chrono::NaiveDate; +use rocket::{ + form::Form, + get, post, + request::FlashMessage, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use rocket_dyn_templates::Template; +use sqlx::SqlitePool; +use tera::Context; + +use crate::{ + model::{ + boat::Boat, + boatreservation::{BoatReservation, BoatReservationToAdd}, + user::{DonauLinzUser, User, UserWithRoles}, + }, + tera::log::KioskCookie, +}; + +#[get("/")] +async fn index_kiosk( + db: &State, + flash: Option>, + _kiosk: KioskCookie, +) -> Template { + let boatreservations = BoatReservation::all_future(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + let linz_boats = Boat::all_for_boatshouse(db).await; + let mut boats = Vec::new(); + for boat in linz_boats { + if boat.boat.owner.is_none() { + boats.push(boat); + } + } + + context.insert("boatreservations", &boatreservations); + context.insert("boats", &boats); + context.insert("user", &User::all(db).await); + context.insert("show_kiosk_header", &true); + + Template::render("boatreservations", context.into_json()) +} + +#[get("/", rank = 2)] +async fn index( + db: &State, + flash: Option>, + user: DonauLinzUser, +) -> Template { + let boatreservations = BoatReservation::all_future(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + let linz_boats = Boat::all_for_boatshouse(db).await; + let mut boats = Vec::new(); + for boat in linz_boats { + if boat.boat.owner.is_none() { + boats.push(boat); + } + } + + context.insert("boatreservations", &boatreservations); + context.insert("boats", &boats); + context.insert("user", &User::all(db).await); + context.insert( + "loggedin_user", + &UserWithRoles::from_user(user.into(), db).await, + ); + + Template::render("boatreservations", context.into_json()) +} + +#[derive(Debug, FromForm)] +pub struct FormBoatReservationToAdd<'r> { + pub boat_id: i64, + pub start_date: &'r str, + pub end_date: &'r str, + pub time_desc: &'r str, + pub usage: &'r str, + pub user_id_applicant: Option, +} + +#[post("/", data = "", rank = 2)] +async fn create<'r>( + db: &State, + data: Form>, + user: DonauLinzUser, +) -> Flash { + let user_applicant: User = user.into(); + let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap(); + let boatreservation_to_add = BoatReservationToAdd { + boat: &boat, + start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(), + end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(), + time_desc: data.time_desc, + usage: data.usage, + user_applicant: &user_applicant, + }; + match BoatReservation::create(db, boatreservation_to_add).await { + Ok(_) => Flash::success( + Redirect::to("/boatreservation"), + "Reservierung erfolgreich hinzugefügt", + ), + Err(e) => Flash::error(Redirect::to("/boatreservation"), format!("Fehler: {e}")), + } +} + +#[post("/", data = "")] +async fn create_from_kiosk<'r>( + db: &State, + data: Form>, + _kiosk: KioskCookie, +) -> Flash { + let user_applicant: User = User::find_by_id(db, data.user_id_applicant.unwrap() as i32) + .await + .unwrap(); + let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap(); + let boatreservation_to_add = BoatReservationToAdd { + boat: &boat, + start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(), + end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(), + time_desc: data.time_desc, + usage: data.usage, + user_applicant: &user_applicant, + }; + match BoatReservation::create(db, boatreservation_to_add).await { + Ok(_) => Flash::success( + Redirect::to("/boatreservation"), + "Reservierung erfolgreich hinzugefügt", + ), + Err(e) => Flash::error(Redirect::to("/boatreservation"), format!("Fehler: {e}")), + } +} + +#[get("//delete")] +async fn delete<'r>( + db: &State, + reservation_id: i32, + user: DonauLinzUser, +) -> Flash { + let reservation = BoatReservation::find_by_id(db, reservation_id) + .await + .unwrap(); + + if user.id == reservation.user_id_applicant || user.has_role(db, "admin").await { + reservation.delete(db).await; + Flash::success( + Redirect::to("/boatreservation"), + "Reservierung erfolgreich gelöscht", + ) + } else { + Flash::error( + Redirect::to("/boatreservation"), + format!("Nur der Reservierer darf die Reservierung löschen."), + ) + } +} + +pub fn routes() -> Vec { + routes![index, index_kiosk, create, create_from_kiosk, delete] +} diff --git a/src/tera/log.rs b/src/tera/log.rs index 3ff330b..686867d 100644 --- a/src/tera/log.rs +++ b/src/tera/log.rs @@ -17,6 +17,7 @@ use tera::Context; use crate::model::{ boat::Boat, + boatreservation::BoatReservation, log::Log, logbook::{ LogToAdd, LogToFinalize, Logbook, LogbookCreateError, LogbookDeleteError, @@ -73,6 +74,7 @@ async fn index( } context.insert("boats", &boats); + context.insert("reservations", &BoatReservation::all_future(db).await); context.insert("coxes", &coxes); context.insert("users", &users); context.insert("logtypes", &logtypes); @@ -163,6 +165,7 @@ async fn kiosk( } context.insert("boats", &boats); + context.insert("reservations", &BoatReservation::all_future(db).await); context.insert("coxes", &coxes); context.insert("users", &users); context.insert("logtypes", &logtypes); diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 0f1faf0..b5ab37c 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -26,6 +26,7 @@ pub(crate) mod admin; mod auth; pub(crate) mod board; mod boatdamage; +mod boatreservation; mod cox; mod ergo; mod log; @@ -94,6 +95,7 @@ pub fn config(rocket: Rocket) -> Rocket { .mount("/notification", notification::routes()) .mount("/stat", stat::routes()) .mount("/boatdamage", boatdamage::routes()) + .mount("/boatreservation", boatreservation::routes()) .mount("/cox", cox::routes()) .mount("/admin", admin::routes()) .mount("/board", board::routes()) diff --git a/templates/base.html.tera b/templates/base.html.tera index dbb98d5..604d9cf 100644 --- a/templates/base.html.tera +++ b/templates/base.html.tera @@ -22,6 +22,7 @@ Statistik Bootsauswertung Bootsschaden + Bootsreservierung {% endif %} diff --git a/templates/boatreservations.html.tera b/templates/boatreservations.html.tera new file mode 100644 index 0000000..6ddb6cd --- /dev/null +++ b/templates/boatreservations.html.tera @@ -0,0 +1,82 @@ +{% import "includes/macros" as macros %} +{% import "includes/forms/log" as log %} +{% extends "base" %} +{% block content %} +
+

Bootsreservierungen

+

+ Neue Reservierung + + {% include "includes/plus-icon" %} + Neue Reservierung eintragen + +

+ +
+ + +
+
+ {% for reservation in boatreservations %} +
+
+ Boot: + {{ reservation.boat.name }} +
+ Reservierung: + {{ reservation.user_applicant.name }} +
+ Datum: + {{ reservation.start_date }} + {% if reservation.end_date != reservation.start_date %} + - + {{ reservation.end_date }} + {% endif %} +
+ Uhrzeit: + {{ reservation.time_desc }} +
+ Zweck: + {{ reservation.usage }} + {% if loggedin_user %} + {% if loggedin_user.id == reservation.user_applicant.id or "admin" in loggedin_user.roles %} + + {% endif %} + {% endif %} +
+
+ {% endfor %} +
+{% endblock content %} diff --git a/templates/includes/forms/log.html.tera b/templates/includes/forms/log.html.tera index 1f29201..8c04c08 100644 --- a/templates/includes/forms/log.html.tera +++ b/templates/includes/forms/log.html.tera @@ -104,6 +104,7 @@ {{ log.departure | date(format="%H:%M") }} Uhr + ({{ log.departure | date(format="%d.%m.%Y") }}) 0 %} - Gäste (ohne Account): + Gäste + (ohne Account): {{ amount_guests }} {% endif %} {% if allowed_to_close and state == "on_water" %} @@ -182,18 +184,23 @@ {% endif %}
- {{ log.boat.name }} ({{ log.shipmaster_user.name -}} + {{ log.boat.name }} + ({{ log.shipmaster_user.name -}} {% if log.shipmaster_only_steering %} - handgesteuert {% endif -%} ) {% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %} - {{ log.departure | date(format='%d.%m.%Y') }} ({{ log.departure | date(format='%H:%M') }} - {{ log.arrival | date(format='%H:%M') }}) + {{ log.departure | date(format='%d.%m.%Y') }} + ({{ log.departure | date(format='%H:%M') }} + - + {{ log.arrival | date(format='%H:%M') }}) {% else %} {{ log.departure | date(format='%d.%m.%Y (%H:%M)') }} {% if state == "completed" %} - - {{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }} + - + {{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }} {% endif %} {% endif %} @@ -205,7 +212,8 @@
{{ log.destination }} {% if state == "completed" %} - ({{ log.distance_in_km }} km) + ({{ log.distance_in_km }} + km) {% endif %} {% if log.comments %}- "{{ log.comments }}"{% endif %}
@@ -217,7 +225,9 @@ {% if not loop.last or amount_guests > 0 and log.boat.name != 'Externes Boot' %},{% endif %} {% endfor %} {% if amount_guests > 0 and log.boat.name != 'Externes Boot' %} - Gäste (ohne Account): {{ amount_guests }} + Gäste + (ohne Account): + {{ amount_guests }} {% endif %}
{% endif %} diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 96865a7..9caa809 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -1,14 +1,26 @@ {% macro boatreservation() %} -
{% endif %} @@ -141,7 +155,7 @@ {% if required %}required="required"{% endif %}> {% if default %}{% endif %} {% for d in data %} -