diff --git a/migration.sql b/migration.sql index 1103353..95ff9bb 100644 --- a/migration.sql +++ b/migration.sql @@ -98,7 +98,7 @@ CREATE TABLE IF NOT EXISTS "logbook" ( "destination" text, "distance_in_km" integer, "comments" text, - "type" INTEGER REFERENCES logbook_type(id) + "logtype" INTEGER REFERENCES logbook_type(id) ); CREATE TABLE IF NOT EXISTS "rower" ( diff --git a/src/model/logbook.rs b/src/model/logbook.rs new file mode 100644 index 0000000..2a5a207 --- /dev/null +++ b/src/model/logbook.rs @@ -0,0 +1,231 @@ +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct Logbook { + pub id: i64, + pub boat_id: i64, + pub shipmaster: i64, + #[serde(default = "bool::default")] + pub shipmaster_only_steering: bool, + pub departure: String, //TODO: Switch to chrono::nativedatetime + pub arrival: Option, //TODO: Switch to chrono::nativedatetime + pub destination: Option, + pub distance_in_km: Option, + pub comments: Option, + pub logtype: Option, +} + +#[derive(Serialize, FromRow)] +pub struct LogbookWithBoatAndUsers { + pub id: i64, + pub boat_id: i64, + pub shipmaster: i64, + #[serde(default = "bool::default")] + pub shipmaster_only_steering: bool, + pub departure: String, //TODO: Switch to chrono::nativedatetime + pub arrival: Option, //TODO: Switch to chrono::nativedatetime + pub destination: Option, + pub distance_in_km: Option, + pub comments: Option, + pub logtype: Option, + pub boat: String, + pub shipmaster_name: String, +} + +impl Logbook { + //pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { + // sqlx::query_as!( + // Self, + // " + //SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, skull, external + //FROM boat + //WHERE id like ? + // ", + // id + // ) + // .fetch_one(db) + // .await + // .ok() + //} + // + // pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option { + // sqlx::query_as!( + // User, + // " + //SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access + //FROM user + //WHERE name like ? + // ", + // name + // ) + // .fetch_one(db) + // .await + // .ok() + // } + // + pub async fn on_water(db: &SqlitePool) -> Vec { + sqlx::query_as!( + LogbookWithBoatAndUsers, + " + SELECT logbook.id, boat_id, shipmaster, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype, boat.name as boat, user.name as shipmaster_name + FROM logbook + INNER JOIN boat ON logbook.boat_id = boat.id + INNER JOIN user ON shipmaster = user.id + WHERE arrival is null + ORDER BY departure DESC + " + ) + .fetch_all(db) + .await + .unwrap() //TODO: fixme + } + + pub async fn completed(db: &SqlitePool) -> Vec { + sqlx::query_as!( + LogbookWithBoatAndUsers, + " + SELECT logbook.id, boat_id, shipmaster, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype, boat.name as boat, user.name as shipmaster_name + FROM logbook + INNER JOIN boat ON logbook.boat_id = boat.id + INNER JOIN user ON shipmaster = user.id + WHERE arrival is not null + ORDER BY arrival DESC + " + ) + .fetch_all(db) + .await + .unwrap() //TODO: fixme + } + + pub async fn create( + db: &SqlitePool, + boat_id: i64, + shipmaster: i64, + shipmaster_only_steering: bool, + departure: NaiveDateTime, + arrival: Option, + destination: Option, + distance_in_km: Option, + comments: Option, + logtype: Option, + ) -> bool { + sqlx::query!( + "INSERT INTO logbook(boat_id, shipmaster, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?)", + boat_id, shipmaster, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype + ) + .execute(db) + .await.is_ok() + } + // + // pub async fn update( + // &self, + // db: &SqlitePool, + // name: &str, + // amount_seats: i64, + // year_built: Option, + // boatbuilder: Option<&str>, + // default_shipmaster_only_steering: bool, + // skull: bool, + // external: bool, + // location_id: Option, + // owner: Option, + // ) -> bool { + // sqlx::query!( + // "UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, skull=?, external=?, location_id=?, owner=? WHERE id=?", + // name, + // amount_seats, + // year_built, + // boatbuilder, + // default_shipmaster_only_steering, + // skull, + // external, + // location_id, + // owner, + // self.id + // ) + // .execute(db) + // .await + // .is_ok() + // } + // + // pub async fn delete(&self, db: &SqlitePool) { + // sqlx::query!("DELETE FROM boat WHERE id=?", self.id) + // .execute(db) + // .await + // .unwrap(); //Okay, because we can only create a User of a valid id + // } +} +// +//#[cfg(test)] +//mod test { +// use crate::{model::boat::Boat, testdb}; +// +// use sqlx::SqlitePool; +// +// #[sqlx::test] +// fn test_find_correct_id() { +// let pool = testdb!(); +// let boat = Boat::find_by_id(&pool, 1).await.unwrap(); +// assert_eq!(boat.id, 1); +// } +// +// #[sqlx::test] +// fn test_find_wrong_id() { +// let pool = testdb!(); +// let boat = Boat::find_by_id(&pool, 1337).await; +// assert!(boat.is_none()); +// } +// +// #[sqlx::test] +// fn test_all() { +// let pool = testdb!(); +// let res = Boat::all(&pool).await; +// assert!(res.len() > 3); +// } +// +// #[sqlx::test] +// fn test_succ_create() { +// let pool = testdb!(); +// +// assert_eq!( +// Boat::create( +// &pool, +// "new-boat-name".into(), +// 42, +// None, +// "Best Boatbuilder".into(), +// true, +// true, +// false, +// Some(1), +// None +// ) +// .await, +// true +// ); +// } +// +// #[sqlx::test] +// fn test_duplicate_name_create() { +// let pool = testdb!(); +// +// assert_eq!( +// Boat::create( +// &pool, +// "Haichenbach".into(), +// 42, +// None, +// "Best Boatbuilder".into(), +// true, +// true, +// false, +// Some(1), +// None +// ) +// .await, +// false +// ); +// } +//} diff --git a/src/model/logtype.rs b/src/model/logtype.rs new file mode 100644 index 0000000..111d42d --- /dev/null +++ b/src/model/logtype.rs @@ -0,0 +1,52 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; + +#[derive(FromRow, Debug, Serialize, Deserialize, Clone)] +pub struct LogType{ + pub id: i64, + name: String, +} + +impl LogType{ + pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { + sqlx::query_as!( + Self, + " +SELECT id, name +FROM logbook_type +WHERE id like ? + ", + id + ) + .fetch_one(db) + .await + .ok() + } + + pub async fn all(db: &SqlitePool) -> Vec { + sqlx::query_as!( + Self, + " +SELECT id, name +FROM logbook_type + " + ) + .fetch_all(db) + .await + .unwrap() //TODO: fixme + } +} + +#[cfg(test)] +mod test { + use crate::testdb; + + use sqlx::SqlitePool; + + #[sqlx::test] + fn test_find_true() { + let pool = testdb!(); + } + + //TODO: write tests +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 9500984..df9ef27 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -10,6 +10,8 @@ use self::{ pub mod boat; pub mod location; pub mod log; +pub mod logbook; +pub mod logtype; pub mod planned_event; pub mod trip; pub mod tripdetails; diff --git a/src/model/user.rs b/src/model/user.rs index 7af6579..4172ff6 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -85,6 +85,21 @@ ORDER BY last_access DESC .unwrap() //TODO: fixme } + pub async fn cox(db: &SqlitePool) -> Vec { + sqlx::query_as!( + User, + " +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access +FROM user +WHERE deleted = 0 AND is_cox=true +ORDER BY last_access DESC + " + ) + .fetch_all(db) + .await + .unwrap() //TODO: fixme + } + pub async fn create(db: &SqlitePool, name: &str, is_guest: bool) -> bool { sqlx::query!( "INSERT INTO USER(name, is_guest) VALUES (?,?)", @@ -365,6 +380,13 @@ mod test { assert!(res.len() > 3); } + #[sqlx::test] + fn test_cox() { + let pool = testdb!(); + let res = User::cox(&pool).await; + assert_eq!(res.len(), 2); + } + #[sqlx::test] fn test_succ_create() { let pool = testdb!(); diff --git a/src/tera/log.rs b/src/tera/log.rs new file mode 100644 index 0000000..dc2bcd8 --- /dev/null +++ b/src/tera/log.rs @@ -0,0 +1,94 @@ +use chrono::NaiveDateTime; +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, + logbook::Logbook, + logtype::LogType, + user::{AdminUser, User}, +}; + +#[get("/")] +async fn index( + db: &State, + flash: Option>, + adminuser: AdminUser, +) -> Template { + let boats = Boat::all(db).await; + let users = User::cox(db).await; + let logtypes = LogType::all(db).await; + + let on_water = Logbook::on_water(db).await; + let completed = Logbook::completed(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + context.insert("boats", &boats); + context.insert("users", &users); + context.insert("logtypes", &logtypes); + context.insert("loggedin_user", &adminuser.user); + context.insert("on_water", &on_water); + context.insert("completed", &completed); + + Template::render("log", context.into_json()) +} + +#[derive(FromForm)] +struct LogAddForm { + boat_id: i64, + shipmaster: i64, + shipmaster_only_steering: bool, + departure: String, + arrival: Option, + destination: Option, + distance_in_km: Option, + comments: Option, + logtype: Option, +} + +#[post("/", data = "")] +async fn create( + db: &State, + data: Form, + _adminuser: AdminUser, +) -> Flash { + if Logbook::create( + db, + data.boat_id, + data.shipmaster, + data.shipmaster_only_steering, + NaiveDateTime::parse_from_str(&data.departure, "%Y-%m-%dT%H:%M").unwrap(), //TODO: fix + data.arrival + .clone() + .map(|a| NaiveDateTime::parse_from_str(&a, "%Y-%m-%dT%H:%M").unwrap()), //TODO: fix + data.destination.clone(), //TODO: fix + data.distance_in_km, + data.comments.clone(), //TODO: fix + data.logtype, + ) + .await + { + Flash::success(Redirect::to("/log"), "Ausfahrt erfolgreich hinzugefügt") + } else { + Flash::error(Redirect::to("/log"), format!("Fehler beim hinzufügen!")) + } +} + +pub fn routes() -> Vec { + routes![index, create] +} + +#[cfg(test)] +mod test {} diff --git a/src/tera/mod.rs b/src/tera/mod.rs index fff78e4..61367f2 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -22,6 +22,7 @@ use crate::model::{ mod admin; mod auth; mod cox; +mod log; mod misc; #[get("/")] @@ -114,6 +115,7 @@ pub fn config(rocket: Rocket) -> Rocket { rocket .mount("/", routes![index, join, remove]) .mount("/auth", auth::routes()) + .mount("/log", log::routes()) .mount("/cox", cox::routes()) .mount("/admin", admin::routes()) .mount("/", misc::routes()) diff --git a/templates/faq.html.tera b/templates/faq.html.tera index dfc87d2..efd28b1 100644 --- a/templates/faq.html.tera +++ b/templates/faq.html.tera @@ -4,7 +4,7 @@ {% block content %}
-

FAQs

+

Infrequently asked questions

{% if loggedin_user.is_cox %} diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 9189382..e1ab708 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -13,6 +13,10 @@ FAQs {% if loggedin_user.is_admin %} + + LOGBUCH + Logbuch + BOATS Bootsverwaltung @@ -48,7 +52,7 @@ {% endmacro checkbox %} {% macro select(data, select_name='trip_type', default='', selected_id='') %} - {% if default %} {% endif %} diff --git a/templates/log.html.tera b/templates/log.html.tera new file mode 100644 index 0000000..e2347a9 --- /dev/null +++ b/templates/log.html.tera @@ -0,0 +1,97 @@ +{% import "includes/macros" as macros %} + +{% extends "base" %} + +{% block content %} + + {% if flash %} + {{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} + {% endif %} + +
+

Logbuch

+

Neue Ausfahrt starten

+
+ {{ macros::select(data=boats, select_name='boat_id') }} + {{ macros::select(data=users, select_name='shipmaster', selected_id=loggedin_user.id) }} + {{ macros::checkbox(label='shipmaster_only_steering', name='shipmaster_only_steering') }} + Departure: + Arrival: + Destination: + + + {{ macros::input(label="Distanz", name="distance_in_km", type="number", min=0) }} + {{ macros::input(label="Kommentar", name="comments", type="text") }} + {{ macros::select(data=logtypes, select_name='logtype', default="Normal") }} + + + +
+ +

Am Wasser

+ {% for log in on_water %} + Bootsname: {{ log.boat }}
+ Schiffsführer: {{ log.shipmaster_name }}
+ {% if log.shipmaster_only_steering %} + Schiffsführer steuert nur + {% endif %} + Weggefahren: {{ log.departure }}
+ Ziel: {{ log.destination }}
+ Kommentare: {{ log.comments }}
+ Logtype: {{ log.logtype }}
+
+ {% endfor %} + +

Einträge

+ {% for log in completed %} + Bootsname: {{ log.boat }}
+ Schiffsführer: {{ log.shipmaster_name }}
+ {% if log.shipmaster_only_steering %} + Schiffsführer steuert nur + {% endif %} + Weggefahren: {{ log.departure }}
+ Angekommen: {{ log.arrival}}
+ Ziel: {{ log.destination }}
+ Kommentare: {{ log.comments }}
+ Logtype: {{ log.logtype }}
+
+ {% endfor %} +
+ + +{% endblock content%}