use chrono::{Local, NaiveDateTime, TimeZone}; use rocket::FromForm; use serde::Serialize; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use super::{boat::Boat, rower::Rower, user::User}; #[derive(FromRow, Serialize, Clone)] 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(FromForm)] pub struct LogToAdd { pub boat_id: i32, pub shipmaster: i64, pub shipmaster_only_steering: bool, pub departure: String, pub arrival: Option, pub destination: Option, pub distance_in_km: Option, pub comments: Option, pub logtype: Option, pub rower: Vec, } #[derive(FromForm)] pub struct LogToFinalize { pub destination: String, pub distance_in_km: i64, pub comments: Option, pub logtype: Option, pub rower: Vec, } #[derive(Serialize)] pub struct LogbookWithBoatAndRowers { #[serde(flatten)] pub logbook: Logbook, pub boat: Boat, pub shipmaster_user: User, pub rowers: Vec, pub departure_timestamp: i64, pub arrival_timestamp: Option, } pub enum LogbookUpdateError { NotYourEntry, TooManyRowers(usize, usize), } pub enum LogbookCreateError { BoatAlreadyOnWater, BoatLocked, BoatNotFound, TooManyRowers(usize, usize), ShipmasterAlreadyOnWater, RowerAlreadyOnWater(User), } impl Logbook { pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { sqlx::query_as!( Self, " SELECT id,boat_id,shipmaster,shipmaster_only_steering,departure,arrival,destination,distance_in_km,comments,logtype FROM logbook WHERE id like ? ", id ) .fetch_one(db) .await .ok() } pub async fn on_water(db: &SqlitePool) -> Vec { let logs = sqlx::query_as!( Logbook, " SELECT id, boat_id, shipmaster, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype FROM logbook WHERE arrival is null ORDER BY departure DESC " ) .fetch_all(db) .await .unwrap(); //TODO: fixme let mut ret = Vec::new(); for log in logs { let date_time_naive = NaiveDateTime::parse_from_str(&log.departure, "%Y-%m-%d %H:%M").unwrap(); let date_time = Local .from_local_datetime(&date_time_naive) .single() .unwrap(); ret.push(LogbookWithBoatAndRowers { rowers: Rower::for_log(db, &log).await, boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(), shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(), logbook: log, arrival_timestamp: None, //TODO: send arrival timestmap departure_timestamp: date_time.timestamp(), }); } ret } pub async fn completed(db: &SqlitePool) -> Vec { let logs = sqlx::query_as!( Logbook, " SELECT id, boat_id, shipmaster, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype FROM logbook WHERE arrival is not null ORDER BY departure DESC " ) .fetch_all(db) .await .unwrap(); //TODO: fixme let mut ret = Vec::new(); for log in logs { ret.push(LogbookWithBoatAndRowers { rowers: Rower::for_log(db, &log).await, boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(), shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(), logbook: log, arrival_timestamp: None, departure_timestamp: 0, }); } ret } pub async fn create(db: &SqlitePool, log: LogToAdd) -> Result<(), LogbookCreateError> { let Some(boat) = Boat::find_by_id(db, log.boat_id).await else { return Err(LogbookCreateError::BoatNotFound); }; if boat.is_locked(db).await { return Err(LogbookCreateError::BoatLocked); } if boat.on_water(db).await { return Err(LogbookCreateError::BoatAlreadyOnWater); } if (User::find_by_id(db, log.shipmaster as i32).await.unwrap()) .on_water(db) .await { return Err(LogbookCreateError::ShipmasterAlreadyOnWater); } if log.rower.len() > boat.amount_seats as usize - 1 { return Err(LogbookCreateError::TooManyRowers( boat.amount_seats as usize, log.rower.len() + 1, )); } for rower in &log.rower { let user = User::find_by_id(db, *rower as i32).await.unwrap(); if user.on_water(db).await { return Err(LogbookCreateError::RowerAlreadyOnWater(user)); } } let mut tx = db.begin().await.unwrap(); let departure = NaiveDateTime::parse_from_str(&log.departure, "%Y-%m-%dT%H:%M").unwrap(); let arrival = log .arrival .map(|a| NaiveDateTime::parse_from_str(&a, "%Y-%m-%dT%H:%M").unwrap()); let inserted_row = sqlx::query!( "INSERT INTO logbook(boat_id, shipmaster, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?) RETURNING id", log.boat_id, log.shipmaster, log.shipmaster_only_steering, departure, arrival, log.destination, log.distance_in_km, log.comments, log.logtype ) .fetch_one(&mut tx) .await.unwrap(); for rower in &log.rower { Rower::create(&mut tx, inserted_row.id, *rower).await; } tx.commit().await.unwrap(); Ok(()) } pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> { let result = sqlx::query!("SELECT destination, distance_in_km FROM logbook WHERE id IN (SELECT MIN(id) FROM logbook GROUP BY destination) AND destination IS NOT NULL AND distance_in_km IS NOT NULL;") .fetch_all(db) .await .unwrap(); result .into_iter() .filter_map(|r| { if let (Some(destination), Some(distance_in_km)) = (r.destination, r.distance_in_km) { Some((destination, distance_in_km)) } else { None } }) .collect() } async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) { sqlx::query!("DELETE FROM rower WHERE logbook_id=?", self.id) .execute(db) .await .unwrap(); } pub async fn home( &self, db: &SqlitePool, user: &User, log: LogToFinalize, ) -> Result<(), LogbookUpdateError> { if user.id != self.shipmaster { return Err(LogbookUpdateError::NotYourEntry); } let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap(); //ok if log.rower.len() > boat.amount_seats as usize - 1 { return Err(LogbookUpdateError::TooManyRowers( boat.amount_seats as usize, log.rower.len() + 1, )); } let arrival = format!("{}", chrono::offset::Local::now().format("%Y-%m-%d %H:%M")); let mut tx = db.begin().await.unwrap(); sqlx::query!( "UPDATE logbook SET destination=?, distance_in_km=?, comments=?, logtype=?, arrival=? WHERE id=?", log.destination, log.distance_in_km, log.comments, log.logtype, arrival, self.id ) .execute(&mut tx) .await.unwrap(); //TODO: fixme self.remove_rowers(&mut tx).await; for rower in &log.rower { Rower::create(&mut tx, self.id, *rower).await; } tx.commit().await.unwrap(); 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 // ); // } //}