23 Commits

Author SHA1 Message Date
2159696112 fix migration
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m19s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-16 20:00:44 +02:00
39bde35864 Merge pull request 'add wifi pw in welcome mail' (#593) from add-wifi-pw-new-members into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m45s
Reviewed-on: #593
2024-06-10 22:08:18 +02:00
5f301324ee add wifi pw in welcome mail
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 22:07:14 +02:00
9558965e8f Merge pull request 'require necessary fields' (#591) from require-necessary-fields into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m30s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m48s
Reviewed-on: #591
2024-06-10 20:59:43 +02:00
5f4d8982a8 require necessary fields
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-10 20:58:42 +02:00
0e2ef9e256 Merge pull request 'add trailer reservation funcitonality; Fixes #443' (#588) from trailer-reservation into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m3s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m7s
Reviewed-on: #588
2024-06-10 20:00:25 +02:00
1dc91f4f28 merged :-)
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-10 19:59:39 +02:00
f56da43723 fix copied stuff
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m41s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-10 19:48:16 +02:00
9dc1ec6fa0 add trailer reservation funcitonality; Fixes #443
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 19:43:59 +02:00
c9b67f5790 Merge pull request 'add delete trip button in edit window; Fixes #449' (#586) from delte-trip-btn into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m53s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m5s
Reviewed-on: #586
2024-06-10 19:08:22 +02:00
16687e39ab remove unused attribute
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-10 19:07:55 +02:00
957c474389 add delete trip button in edit window; Fixes #449
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 19:07:05 +02:00
f7aed68423 Merge pull request 'show if a user has < 30 km to thousand km trip, Fixes #551' (#582) from thousand-km-trips into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m59s
Reviewed-on: #582
2024-06-10 15:50:31 +02:00
01637d0800 Merge pull request 'use proper hydro license; add fluctuation' (#584) from proper-hydro into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #584
2024-06-10 15:49:51 +02:00
0a77011170 use proper hydro license; add fluctuation
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 15:48:49 +02:00
dea0c65da3 fix ci + add test
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 15:12:23 +02:00
31bf38f112 show if a user has < 30 km to thousand km trip, Fixes #551
Some checks failed
CI/CD Pipeline / test (push) Failing after 7m14s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-10 14:55:01 +02:00
6b29907596 Merge pull request 'fix boat select' (#580) from fix-boat-select into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m2s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m34s
Reviewed-on: #580
2024-06-10 11:01:47 +02:00
7b17c30ce2 fix boat select
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-10 11:00:54 +02:00
ec4068e499 Merge pull request 'cleaner cal' (#578) from cleaner-cal into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m47s
Reviewed-on: #578
2024-06-06 17:22:14 +02:00
fca19745f8 even nicer cal entries
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-06 17:21:55 +02:00
bb48ddb3de fix ci
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-06 17:17:40 +02:00
34b098fa2a cleaner cal
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-06 17:16:47 +02:00
21 changed files with 775 additions and 35 deletions

View File

@ -195,3 +195,21 @@ CREATE TABLE IF NOT EXISTS "weather" (
"wind_gust" FLOAT NOT NULL,
"rain_mm" FLOAT NOT NULL
);
CREATE TABLE IF NOT EXISTS "trailer" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS "trailer_reservation" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"trailer_id" INTEGER NOT NULL REFERENCES trailer(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
);

View File

@ -64,3 +64,5 @@ 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, 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');
INSERT INTO "trailer" (name) VALUES('Großer Hänger');
INSERT INTO "trailer" (name) VALUES('Kleiner Hänger');

View File

@ -81,20 +81,20 @@ pub struct BoatToUpdate<'r> {
impl Boat {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM boat WHERE name like ?", name)
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE name like ?", name)
.fetch_one(db)
.await
.ok()

View File

@ -360,21 +360,23 @@ WHERE trip_details.id=?
event.day.replace('-', ""),
event.planned_starting_time.replace(':', "")
)));
let tripdetails = event.trip_details(db).await;
let mut name = String::new();
if event.is_cancelled() {
name.push_str("ABGESAGT! :-( ");
name.push_str("ABGESAGT");
if let Some(notes) = &tripdetails.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
name.push_str(&format!("{} ", event.name));
let tripdetails = event.trip_details(db).await;
if let Some(triptype) = tripdetails.triptype(db).await {
name.push_str(&format!("{} ", triptype.name))
}
if let Some(notes) = tripdetails.notes {
if !notes.is_empty() {
name.push_str(&format!("({notes}) "))
}
}
vevent.push(Summary::new(name));
calendar.add_event(vevent);
}
@ -434,6 +436,6 @@ mod test {
let pool = testdb!();
let actual = Event::get_ics_feed(&pool).await;
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event (trip_details for a planned event) \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
}
}

View File

@ -301,7 +301,7 @@ ORDER BY departure DESC
db: &SqlitePool,
mut log: LogToAdd,
created_by_user: &User,
) -> Result<(), LogbookCreateError> {
) -> Result<String, LogbookCreateError> {
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
return Err(LogbookCreateError::BoatNotFound);
};
@ -354,7 +354,7 @@ ORDER BY departure DESC
{
Ok(_) => {
tx.commit().await.unwrap();
Ok(())
Ok(String::new())
}
Err(a) => Err(a.into()),
};
@ -426,7 +426,15 @@ ORDER BY departure DESC
tx.commit().await.unwrap();
Ok(())
let mut ret = String::new();
for rower in &log.rowers {
let user = User::find_by_id(db, *rower as i32).await.unwrap();
if let Some(msg) = user.close_thousands_trip(db).await {
ret.push_str(&format!("{msg}"));
}
}
Ok(ret)
}
pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
@ -635,6 +643,7 @@ mod test {
use crate::model::user::User;
use crate::testdb;
use chrono::Duration;
use sqlx::SqlitePool;
#[sqlx::test]
@ -686,7 +695,7 @@ mod test {
fn test_succ_create() {
let pool = testdb!();
Logbook::create(
let msg = Logbook::create(
&pool,
LogToAdd {
boat_id: 3,
@ -704,7 +713,62 @@ mod test {
&User::find_by_id(&pool, 4).await.unwrap(),
)
.await
.unwrap()
.unwrap();
assert_eq!(msg, String::from(""));
}
#[sqlx::test]
fn test_succ_create_with_thousands_msg() {
let pool = testdb!();
let logbook = Logbook::find_by_id(&pool, 1).await.unwrap();
let user = User::find_by_id(&pool, 2).await.unwrap();
let current_date = chrono::Local::now().format("%Y-%m-%d").to_string();
let start_date = chrono::Local::now() - Duration::days(3);
let start_date = start_date.format("%Y-%m-%d").to_string();
logbook
.home(
&pool,
&user,
super::LogToFinalize {
destination: "new-destination".into(),
distance_in_km: 995,
comments: Some("Perfect water".into()),
logtype: None,
rowers: vec![2],
shipmaster: Some(2),
steering_person: Some(2),
shipmaster_only_steering: false,
departure: format!("{}T10:00", start_date),
arrival: format!("{}T12:00", current_date),
},
)
.await
.unwrap();
let msg = Logbook::create(
&pool,
LogToAdd {
boat_id: 3,
shipmaster: Some(2),
steering_person: Some(2),
shipmaster_only_steering: false,
departure: "2128-05-20T12:00".into(),
arrival: None,
destination: None,
distance_in_km: None,
comments: None,
logtype: None,
rowers: vec![2],
},
&User::find_by_id(&pool, 1).await.unwrap(),
)
.await
.unwrap();
assert_eq!(
msg,
String::from(" • rower braucht nur mehr 5 km bis die 1000 km voll sind 🤑")
);
}
#[sqlx::test]

View File

@ -1,6 +1,7 @@
use chrono::NaiveDate;
use serde::Serialize;
use sqlx::SqlitePool;
use waterlevel::WaterlevelDay;
use self::{
event::{Event, EventWithUserAndTriptype},
@ -24,6 +25,8 @@ pub mod notification;
pub mod role;
pub mod rower;
pub mod stat;
pub mod trailer;
pub mod trailerreservation;
pub mod trip;
pub mod tripdetails;
pub mod triptype;
@ -38,7 +41,7 @@ pub struct Day {
events: Vec<EventWithUserAndTriptype>,
trips: Vec<TripWithUserAndType>,
is_pinned: bool,
max_waterlevel: Option<i64>,
max_waterlevel: Option<WaterlevelDay>,
weather: Option<Weather>,
}

View File

@ -95,7 +95,7 @@ ORDER BY
#[derive(FromRow, Serialize, Clone)]
pub struct Stat {
name: String,
rowed_km: i32,
pub(crate) rowed_km: i32,
}
impl Stat {
@ -195,6 +195,34 @@ ORDER BY rowed_km DESC, u.name;
})
.collect()
}
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
let year = match year {
Some(year) => year,
None => chrono::Local::now().year(),
};
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let row = sqlx::query(&format!(
"
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM (
SELECT * FROM user
WHERE id={}
) u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
",
user.id
))
.fetch_one(db)
.await
.unwrap();
Stat {
name: row.get("name"),
rowed_km: row.get("rowed_km"),
}
}
}
#[derive(Debug, Serialize)]
@ -218,7 +246,7 @@ FROM (
LEFT JOIN
rower r ON l.id = r.logbook_id
WHERE
l.shipmaster = {0} OR r.rower_id = {0}
r.rower_id = {}
GROUP BY
departure_date
) as subquery

31
src/model/trailer.rs Normal file
View File

@ -0,0 +1,31 @@
use std::ops::DerefMut;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
pub struct Trailer {
pub id: i64,
pub name: String,
}
impl Trailer {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM trailer")
.fetch_all(db)
.await
.unwrap()
}
}

View File

@ -0,0 +1,233 @@
use std::collections::HashMap;
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;
use super::trailer::Trailer;
use super::user::User;
use crate::tera::trailerreservation::ReservationEditForm;
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct TrailerReservation {
pub id: i64,
pub trailer_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<i64>,
pub created_at: NaiveDateTime,
}
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct TrailerReservationWithDetails {
#[serde(flatten)]
reservation: TrailerReservation,
trailer: Trailer,
user_applicant: User,
user_confirmation: Option<User>,
}
#[derive(Debug)]
pub struct TrailerReservationToAdd<'r> {
pub trailer: &'r Trailer,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub time_desc: &'r str,
pub usage: &'r str,
pub user_applicant: &'r User,
}
impl TrailerReservation {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM trailer_reservation
WHERE id like ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn all_future(db: &SqlitePool) -> Vec<TrailerReservationWithDetails> {
let trailerreservations = sqlx::query_as!(
Self,
"
SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM trailer_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 trailerreservations {
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 trailer = Trailer::find_by_id(db, reservation.trailer_id as i32)
.await
.unwrap();
res.push(TrailerReservationWithDetails {
reservation,
trailer,
user_applicant,
user_confirmation,
});
}
res
}
pub async fn all_future_with_groups(
db: &SqlitePool,
) -> HashMap<String, Vec<TrailerReservationWithDetails>> {
let mut grouped_reservations: HashMap<String, Vec<TrailerReservationWithDetails>> =
HashMap::new();
let reservations = Self::all_future(db).await;
for reservation in reservations {
let key = format!(
"{}-{}-{}-{}-{}",
reservation.reservation.start_date,
reservation.reservation.end_date,
reservation.reservation.time_desc,
reservation.reservation.usage,
reservation.user_applicant.name
);
grouped_reservations
.entry(key)
.or_default()
.push(reservation);
}
grouped_reservations
}
pub async fn create(
db: &SqlitePool,
trailerreservation: TrailerReservationToAdd<'_>,
) -> Result<(), String> {
if Self::trailer_reserved_between_dates(
db,
trailerreservation.trailer,
&trailerreservation.start_date,
&trailerreservation.end_date,
)
.await
{
return Err("Hänger in diesem Zeitraum bereits reserviert.".into());
}
Log::create(
db,
format!("New trailer reservation: {trailerreservation:?}"),
)
.await;
sqlx::query!(
"INSERT INTO trailer_reservation(trailer_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
trailerreservation.trailer.id,
trailerreservation.start_date,
trailerreservation.end_date,
trailerreservation.time_desc,
trailerreservation.usage,
trailerreservation.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 {
let date = if trailerreservation.start_date == trailerreservation.end_date {
format!("am {}", trailerreservation.start_date)
} else {
format!(
"von {} bis {}",
trailerreservation.start_date, trailerreservation.end_date
)
};
Notification::create(
db,
&user,
&format!(
"{} hat eine neue Hängerreservierung für Hänger '{}' {} angelegt. Zeit: {}; Zweck: {}",
trailerreservation.user_applicant.name,
trailerreservation.trailer.name,
date,
trailerreservation.time_desc,
trailerreservation.usage
),
"Neue Hängerreservierung",
None,None
)
.await;
}
Ok(())
}
pub async fn trailer_reserved_between_dates(
db: &SqlitePool,
trailer: &Trailer,
start_date: &NaiveDate,
end_date: &NaiveDate,
) -> bool {
sqlx::query!(
"SELECT COUNT(*) AS reservation_count
FROM trailer_reservation
WHERE trailer_id = ?
AND start_date <= ? AND end_date >= ?;",
trailer.id,
end_date,
start_date
)
.fetch_one(db)
.await
.unwrap()
.reservation_count
> 0
}
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
let time_desc = data.time_desc.trim();
let usage = data.usage.trim();
sqlx::query!(
"UPDATE trailer_reservation SET time_desc = ?, usage = ? where id = ?",
time_desc,
usage,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("DELETE FROM trailer_reservation WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Boat of a valid id
}
}

View File

@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{
family::Family, log::Log, mail::Mail, notification::Notification, role::Role,
family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat,
tripdetails::TripDetails, Day,
};
use crate::tera::admin::user::UserEditForm;
@ -281,6 +281,8 @@ Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dic
Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst.
Außerdem haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz'. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch
@ -849,6 +851,19 @@ ORDER BY last_access DESC
6
}
}
pub(crate) async fn close_thousands_trip(&self, db: &SqlitePool) -> Option<String> {
let rowed_km = Stat::person(db, None, self).await.rowed_km;
if rowed_km % 1000 > 970 {
return Some(format!(
"{} braucht nur mehr {} km bis die {} km voll sind 🤑",
self.name,
1000 - rowed_km % 1000,
rowed_km + 1000 - (rowed_km % 1000)
));
}
None
}
}
#[async_trait]

View File

@ -17,6 +17,13 @@ pub struct Waterlevel {
pub tumittel: i64,
}
#[derive(Debug, Serialize)]
pub struct WaterlevelDay {
pub day: NaiveDate,
pub avg: i64,
pub fluctuation: i64,
}
pub struct Create {
pub day: NaiveDate,
pub time: String,
@ -53,15 +60,28 @@ impl Waterlevel {
Ok(())
}
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<i64> {
sqlx::query!(
"SELECT MAX(mittel) as max FROM waterlevel WHERE day = ?",
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<WaterlevelDay> {
let waterlevel = sqlx::query_as!(
Waterlevel,
"SELECT id, day, time, max, min, mittel, tumax, tumin, tumittel FROM waterlevel WHERE day = ? ORDER BY mittel DESC LIMIT 1",
day
)
.fetch_one(db)
.await
.unwrap()
.max
.fetch_optional(db)
.await.unwrap();
if let Some(waterlevel) = waterlevel {
let max_diff = (waterlevel.mittel - waterlevel.max).abs();
let min_diff = (waterlevel.mittel - waterlevel.min).abs();
let fluctuation = max_diff.max(min_diff);
return Some(WaterlevelDay {
day: waterlevel.day,
avg: waterlevel.mittel,
fluctuation,
});
}
None
}
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {

View File

@ -212,7 +212,7 @@ async fn create_logbook(
)
.await
{
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt erfolgreich hinzugefügt"),
Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),

View File

@ -39,6 +39,7 @@ mod misc;
mod notification;
mod planned;
mod stat;
pub(crate) mod trailerreservation;
#[derive(FromForm, Debug)]
struct LoginForm<'r> {
@ -200,6 +201,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
.mount("/stat", stat::routes())
.mount("/boatdamage", boatdamage::routes())
.mount("/boatreservation", boatreservation::routes())
.mount("/trailerreservation", trailerreservation::routes())
.mount("/cox", cox::routes())
.mount("/admin", admin::routes())
.mount("/board", board::routes())

View File

@ -0,0 +1,211 @@
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::{
log::Log,
trailer::Trailer,
trailerreservation::{TrailerReservation, TrailerReservationToAdd},
user::{DonauLinzUser, User, UserWithDetails},
},
tera::log::KioskCookie,
};
#[get("/")]
async fn index_kiosk(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
_kiosk: KioskCookie,
) -> Template {
let trailerreservations = TrailerReservation::all_future(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("trailerreservations", &trailerreservations);
context.insert("trailers", &Trailer::all(db).await);
context.insert("user", &User::all(db).await);
context.insert("show_kiosk_header", &true);
Template::render("trailerreservations", context.into_json())
}
#[get("/", rank = 2)]
async fn index(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: DonauLinzUser,
) -> Template {
let trailerreservations = TrailerReservation::all_future(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("trailerreservations", &trailerreservations);
context.insert("trailers", &Trailer::all(db).await);
context.insert("user", &User::all(db).await);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("trailerreservations", context.into_json())
}
#[derive(Debug, FromForm)]
pub struct FormTrailerReservationToAdd<'r> {
pub trailer_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<i64>,
}
#[post("/new", data = "<data>", rank = 2)]
async fn create<'r>(
db: &State<SqlitePool>,
data: Form<FormTrailerReservationToAdd<'r>>,
user: DonauLinzUser,
) -> Flash<Redirect> {
let user_applicant: User = user.into();
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
.await
.unwrap();
let trailerreservation_to_add = TrailerReservationToAdd {
trailer: &trailer,
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 TrailerReservation::create(db, trailerreservation_to_add).await {
Ok(_) => Flash::success(
Redirect::to("/trailerreservation"),
"Reservierung erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to("/trailerreservation"), format!("Fehler: {e}")),
}
}
#[post("/new", data = "<data>")]
async fn create_from_kiosk<'r>(
db: &State<SqlitePool>,
data: Form<FormTrailerReservationToAdd<'r>>,
_kiosk: KioskCookie,
) -> Flash<Redirect> {
let user_applicant: User = User::find_by_id(db, data.user_id_applicant.unwrap() as i32)
.await
.unwrap();
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
.await
.unwrap();
let trailerreservation_to_add = TrailerReservationToAdd {
trailer: &trailer,
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 TrailerReservation::create(db, trailerreservation_to_add).await {
Ok(_) => Flash::success(
Redirect::to("/trailerreservation"),
"Reservierung erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to("/trailerreservation"), format!("Fehler: {e}")),
}
}
#[derive(FromForm, Debug)]
pub struct ReservationEditForm {
pub(crate) id: i32,
pub(crate) time_desc: String,
pub(crate) usage: String,
}
#[post("/", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<ReservationEditForm>,
user: User,
) -> Flash<Redirect> {
let Some(reservation) = TrailerReservation::find_by_id(db, data.id).await else {
return Flash::error(
Redirect::to("/trailerreservation"),
format!("Reservation with ID {} does not exist!", data.id),
);
};
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
return Flash::error(
Redirect::to("/trailerreservation"),
"Not allowed to update reservation (only admins + creator do so).".to_string(),
);
}
Log::create(
db,
format!(
"{} updated reservation from {reservation:?} to {data:?}",
user.name
),
)
.await;
reservation.update(db, data.into_inner()).await;
Flash::success(
Redirect::to("/trailerreservation"),
"Reservierung erfolgreich bearbeitet",
)
}
#[get("/<reservation_id>/delete")]
async fn delete<'r>(
db: &State<SqlitePool>,
reservation_id: i32,
user: DonauLinzUser,
) -> Flash<Redirect> {
let reservation = TrailerReservation::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("/trailerreservation"),
"Reservierung erfolgreich gelöscht",
)
} else {
Flash::error(
Redirect::to("/trailerreservation"),
"Nur der Reservierer darf die Reservierung löschen.".to_string(),
)
}
}
pub fn routes() -> Vec<Route> {
routes![
index,
index_kiosk,
create,
create_from_kiosk,
delete,
update
]
}

View File

@ -36,6 +36,7 @@
<a href="/stat/boats" class="px-2">Bootsauswertung</a>
<a href="/boatdamage" class="px-2">Bootsschaden</a>
<a href="/boatreservation" class="px-2">Bootsreservierung</a>
<a href="/trailerreservation" class="px-2">Hängerreservierung</a>
</div>
</header>
{% endif %}

View File

@ -100,7 +100,10 @@
<div class="p-3">
<ul>
<li>
Die Wetterdaten werden von <a class="underline" href="https://openweathermap.org">OpenWeather</a> bereitgestellt.
Die <strong>Wetterdaten</strong> werden von <a class="underline" href="https://openweathermap.org">OpenWeather</a> bereitgestellt.
</li>
<li>
<strong>Wasserstandsvorhersagen:</strong> Die Vorhersagen werden stündlich vom <a class="underline" href="https://hydro.ooe.gv.at">Hydrographischen Dienstes Oberösterreich</a> geladen und zwischengespeichert, der höchste Tages-Mittelwert wird gemeinsam mit der Schwankungsbreite bei den geplanten Ausfahrten angezeigt. Es handelt sich hierbei um ungeprüfte Rohdaten. Rohdatenfehler können durch betriebliche Störungen an den Messgeräten, Fernübertragungseinrichtungen u. dgl. entstehen. Die Vorhersagen sind daher mit Unsicherheiten behaftet! Mit der Länge des Vorhersagezeitraumeszeitraumes werden diese Unsicherheiten größer! Es wird keine Gewähr für die Vollständigkeit, Richtigkeit und Genauigkeit der dargestellten Daten übernommen. Gewährleistungs- und Haftungsansprüche werden ausdrücklich ausgeschlossen (sowohl vom Hydrographischen Dienstes Oberösterreich als auch vom ASKÖ Ruderverein Donau Linz).
</li>
</ul>
</div>

View File

@ -70,7 +70,7 @@
</form>
{% endmacro new %}
{% macro boat_select(id="boat_id") %}
{{ macros::select(label="Boot", data=boats, name="boat_id", id=id, display=["name", " (","cat",")"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true, nonSelectableDefault=" -- Wähle ein Boot aus ---") }}
{{ macros::select(label="Boot", data=boats, name="boat_id", required=true, id=id, display=["name", " (","cat",")"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true, nonSelectableDefault=" -- Wähle ein Boot aus ---") }}
{% endmacro boat_select %}
{% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %}
{#{% if not amount_seats or amount_seats > 1 %}#}
@ -279,4 +279,8 @@
</details>
<input class="btn btn-primary" type="submit" value="Ausfahrt beenden" />
</form>
<a href="/log/{{ log.id }}/delete"
class="btn btn-alert w-full absolute bottom-0 left-0"
style="border-radius: 0"
onclick="return confirm('Willst du diesen Eintrag wirklich löschen? Die Daten gehen verloren');">Löschen</a>
{% endmacro home %}

View File

@ -78,6 +78,8 @@
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsschaden</a>
<a href="/boatreservation"
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsreservierung</a>
<a href="/trailerreservation"
class="block w-100 py-2 hover:text-primary-600 border-t">Hängerreservierung</a>
{% endif %}
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600 border-t">Ergo</a>

View File

@ -90,6 +90,10 @@
<a href="/boatreservation"
class="block w-100 py-2 hover:text-primary-600">Bootsreservierung</a>
</li>
<li class="py-1">
<a href="/trailerreservation"
class="block w-100 py-2 hover:text-primary-600">Hängerreservierung</a>
</li>
<li class="py-1">
<a href="/steering" class="block w-100 py-2 hover:text-primary-600">Steuerleute & Co</a>
</li>

View File

@ -79,7 +79,7 @@
{% if day.max_waterlevel %}
&bullet; <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand"
target="_blank"
title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel }} cm">🌊{{ day.max_waterlevel }} cm</a>
title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel.avg }} ± {{ day.max_waterlevel.fluctuation }} cm (ungeprüfte Rohdaten, für Details siehe die Infos dazu im Impressum)">🌊{{ day.max_waterlevel.avg }} ± {{ day.max_waterlevel.fluctuation }} cm</a>
{% endif %}
</small>
{% if day.weather %}
@ -197,8 +197,7 @@
{% endif %}
{# --- END List Rowers --- #}
{% if "manage_events" in loggedin_user.roles %}
<form action="/planned/join/{{ event.trip_details_id }}"
method="get" />
<form action="/planned/join/{{ event.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen"
class="btn btn-primary w-full rounded-t-none-important"
@ -220,7 +219,7 @@
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=event.planned_amount_cox, required=true, min='0') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=event.id,checked=event.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=event.id,checked=event.is_locked) }}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=event.trip_type_id) }}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=event.trip_type_id) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=event.notes) }}
<input value="Speichern" class="btn btn-primary" type="submit" />
</form>
@ -244,7 +243,7 @@
<form action="/admin/planned-event" method="post" class="grid">
<input type="hidden" name="_method" value="put" />
<input type="hidden" name="id" value="{{ event.id }}" />
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
{{ macros::input(label='', name='name', type='hidden', value=event.name) }}
{{ macros::input(label='', name='max_people', type='hidden', value=event.max_people) }}

View File

@ -0,0 +1,98 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/log" as log %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">Hängerreservierungen</h1>
<h2 class="text-md font-bold tracking-wide bg-primary-900 mt-3 p-3 text-white flex justify-between items-center rounded-md">
Neue Reservierung
<a href="#"
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
data-sidebar="true"
data-trigger="sidebar"
data-header="Neue Reservierung anlegen"
data-body="#new-reservation">
{% include "includes/plus-icon" %}
<span class="sr-only">Neue Reservierung eintragen</span>
</a>
</h2>
<div class="hidden">
<div id="new-reservation">
<form action="/trailerreservation/new" method="post" class="grid gap-3">
{{ macros::select(label="Anhänger", data=trailers, name="trailer_id", id="trailer_id", display=["name"], wrapper_class="col-span-4", nonSelectableDefault=" -- Wähle einen Hänger aus ---", required=true) }}
{% if not loggedin_user %}{{ macros::select(label='Reserviert von', data=user, name='user_id_applicant') }}{% endif %}
{{ macros::input(label='Beginn', name='start_date', type='date', required=true, wrapper_class='col-span-4') }}
{{ macros::input(label='Ende', name='end_date', type='date', required=true, wrapper_class='col-span-4') }}
{{ macros::input(label='Uhrzeit (zB ab 14:00 Uhr, ganztägig, ...)', name='time_desc', type='text', required=true, wrapper_class='col-span-4') }}
{{ macros::input(label='Zweck (Wanderfahrt, ...)', name='usage', type='text', required=true, wrapper_class='col-span-4') }}
<input type="submit"
class="btn btn-primary w-full col-span-4"
value="Reservierung eintragen" />
</form>
</div>
</div>
<div class="search-wrapper">
<label for="name" class="sr-only">Suche</label>
<input type="search"
name="name"
id="filter-js"
class="search-bar"
placeholder="Suchen nach Namen...">
</div>
<div id="filter-result-js" class="search-result"></div>
{% for reservation in trailerreservations %}
{% set allowed_to_edit = false %}
{% if loggedin_user %}
{% if loggedin_user.id == reservation.user_applicant.id or "admin" in loggedin_user.roles %}
{% set allowed_to_edit = true %}
{% endif %}
{% endif %}
<div data-filterable="true"
data-filter="{{ reservation.user_applicant.name }} {{ reservation.trailer.name }}"
class="w-full border-t bg-white dark:bg-primary-900 text-black dark:text-white p-3">
<div class="w-full">
<strong>Boot:</strong>
{{ reservation.trailer.name }}
<br />
<strong>Reservierung:</strong>
{{ reservation.user_applicant.name }}
<br />
<strong>Datum:</strong>
{{ reservation.start_date }}
{% if reservation.end_date != reservation.start_date %}
-
{{ reservation.end_date }}
{% endif %}
<br />
{% if not allowed_to_edit %}
<strong>Uhrzeit:</strong>
{{ reservation.time_desc }}
<br />
<strong>Zweck:</strong>
{{ reservation.usage }}
{% endif %}
{% if allowed_to_edit %}
<form action="/trailerreservation"
method="post"
class="bg-white dark:bg-primary-900 pt-3 rounded-md w-full">
<div class="w-full grid gap-3">
<input type="hidden" name="id" value="{{ reservation.id }}" />
{{ macros::input(label='Uhrzeit', name='time_desc', id=loop.index, type="text", value=reservation.time_desc, readonly=false) }}
{{ macros::input(label='Zweck', name='usage', id=loop.index, type="text", value=reservation.usage, readonly=false) }}
</div>
<div class="mt-3 text-right">
<a href="/trailerreservation/{{ reservation.id }}/delete"
class="w-28 btn btn-alert"
onclick="return confirm('Willst du diese Reservierung wirklich löschen?');">
{% include "includes/delete-icon" %}
Löschen
</a>
<input value="Ändern" type="submit" class="w-28 btn btn-primary ml-1" />
</div>
</form>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endblock content %}