11 Commits

Author SHA1 Message Date
3d340bf803 Merge pull request 'case-insensitive-auth' (#549) from case-insensitive-auth into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m27s
CI/CD Pipeline / deploy-staging (push) Successful in 6m54s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #549
2024-05-27 08:33:03 +02:00
ae096ad602 Merge pull request 'better spacing' (#547) from fix-spacing into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m7s
CI/CD Pipeline / deploy-staging (push) Successful in 8m21s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #547
2024-05-26 18:47:51 +02:00
25d8b1ea7c Merge pull request 'fix-spacing' (#545) from fix-spacing into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m18s
CI/CD Pipeline / deploy-staging (push) Successful in 7m50s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #545
2024-05-26 14:18:07 +02:00
410cd05acc Merge pull request 'type' (#543) from type into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m25s
CI/CD Pipeline / deploy-staging (push) Successful in 7m52s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #543
2024-05-25 18:52:45 +02:00
972811c2cf Merge pull request 'filter-logs' (#539) from filter-logs into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m8s
CI/CD Pipeline / deploy-staging (push) Successful in 7m35s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #539
2024-05-22 23:42:06 +02:00
e22d2d718e Merge pull request 'fix-footer' (#537) from fix-footer into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m23s
CI/CD Pipeline / deploy-staging (push) Successful in 7m54s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #537
2024-05-22 22:51:39 +02:00
b55f122f1d Merge pull request 'sanity-check-timing; fixes #488' (#535) from sanity-check-timing into staging
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
Reviewed-on: #535
2024-05-22 22:41:31 +02:00
d27489d714 Merge pull request 'automate-schnupper-mails' (#533) from automate-schnupper-mails into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m39s
CI/CD Pipeline / deploy-staging (push) Successful in 5m2s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #533
2024-05-22 08:51:39 +02:00
c340d1a916 Merge pull request 'reason-for-canceled-event' (#531) from reason-for-canceled-event into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m29s
CI/CD Pipeline / deploy-staging (push) Successful in 5m29s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #531
2024-05-22 08:13:55 +02:00
e7732b9e96 Merge pull request 'fix-ci' (#528) from fix-ci into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m55s
CI/CD Pipeline / deploy-staging (push) Successful in 7m20s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #528
2024-05-22 00:24:22 +02:00
97dc9308bc Merge pull request 'clippy' (#526) from clippy into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m2s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #526
2024-05-22 00:18:35 +02:00
34 changed files with 469 additions and 1380 deletions

517
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ rest = []
[dependencies]
rocket = { version = "0.5.0", features = ["secrets"]}
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
rocket_dyn_templates = {version = "0.1.0", features = [ "tera" ], optional = true }
log = "0.4"
env_logger = "0.11"
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
@ -24,7 +24,7 @@ ics = "0.5"
futures = "0.3"
lettre = "0.11"
csv = "1.3"
itertools = "0.13"
itertools = "0.12"
job_scheduler_ng = "2.0"
ureq = { version = "2.9", features = ["json"] }
regex = "1.10"

View File

@ -1,5 +1,3 @@
![latest CI run on main](https://git.hofer.link/Ruderverein-Donau-Linz/rowt/actions/workflows/action.yml/badge.svg?branch=main)
# Build
## Frontend
1. `cd frontend`

View File

@ -195,21 +195,3 @@ 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

@ -3,7 +3,7 @@ INSERT INTO "role" (name) VALUES ('cox');
INSERT INTO "role" (name) VALUES ('scheckbuch');
INSERT INTO "role" (name) VALUES ('tech');
INSERT INTO "role" (name) VALUES ('Donau Linz');
INSERT INTO "role" (name) VALUES ('manage_events');
INSERT INTO "role" (name) VALUES ('planned_event');
INSERT INTO "role" (name) VALUES ('Rennrudern');
INSERT INTO "role" (name) VALUES ('paid');
INSERT INTO "role" (name) VALUES ('Vorstand');
@ -64,5 +64,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, 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 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)
sqlx::query_as!(Self, "SELECT * 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 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)
sqlx::query_as!(Self, "SELECT * 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 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)
sqlx::query_as!(Self, "SELECT * FROM boat WHERE name like ?", name)
.fetch_one(db)
.await
.ok()

View File

@ -301,7 +301,7 @@ ORDER BY departure DESC
db: &SqlitePool,
mut log: LogToAdd,
created_by_user: &User,
) -> Result<String, LogbookCreateError> {
) -> Result<(), 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(String::new())
Ok(())
}
Err(a) => Err(a.into()),
};
@ -426,15 +426,7 @@ ORDER BY departure DESC
tx.commit().await.unwrap();
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)
Ok(())
}
pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
@ -643,7 +635,6 @@ mod test {
use crate::model::user::User;
use crate::testdb;
use chrono::Duration;
use sqlx::SqlitePool;
#[sqlx::test]
@ -695,7 +686,7 @@ mod test {
fn test_succ_create() {
let pool = testdb!();
let msg = Logbook::create(
Logbook::create(
&pool,
LogToAdd {
boat_id: 3,
@ -713,62 +704,7 @@ mod test {
&User::find_by_id(&pool, 4).await.unwrap(),
)
.await
.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 🤑")
);
.unwrap()
}
#[sqlx::test]

View File

@ -1,10 +1,9 @@
use chrono::NaiveDate;
use serde::Serialize;
use sqlx::SqlitePool;
use waterlevel::WaterlevelDay;
use self::{
event::{Event, EventWithUserAndTriptype},
planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
trip::{Trip, TripWithUserAndType},
waterlevel::Waterlevel,
weather::Weather,
@ -14,7 +13,6 @@ pub mod boat;
pub mod boatdamage;
pub mod boathouse;
pub mod boatreservation;
pub mod event;
pub mod family;
pub mod location;
pub mod log;
@ -22,11 +20,10 @@ pub mod logbook;
pub mod logtype;
pub mod mail;
pub mod notification;
pub mod planned_event;
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,10 +35,10 @@ pub mod weather;
#[derive(Serialize, Debug)]
pub struct Day {
day: NaiveDate,
events: Vec<EventWithUserAndTriptype>,
planned_events: Vec<PlannedEventWithUserAndTriptype>,
trips: Vec<TripWithUserAndType>,
is_pinned: bool,
max_waterlevel: Option<WaterlevelDay>,
max_waterlevel: Option<i64>,
weather: Option<Weather>,
}
@ -50,7 +47,7 @@ impl Day {
if is_pinned {
Self {
day,
events: Event::get_pinned_for_day(db, day).await,
planned_events: PlannedEvent::get_pinned_for_day(db, day).await,
trips: Trip::get_pinned_for_day(db, day).await,
is_pinned,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
@ -59,7 +56,7 @@ impl Day {
} else {
Self {
day,
events: Event::get_for_day(db, day).await,
planned_events: PlannedEvent::get_for_day(db, day).await,
trips: Trip::get_for_day(db, day).await,
is_pinned,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
@ -70,7 +67,7 @@ impl Day {
pub async fn new_guest(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
let mut day = Self::new(db, day, is_pinned).await;
day.events.retain(|e| e.event.allow_guests);
day.planned_events.retain(|e| e.planned_event.allow_guests);
day.trips.retain(|t| t.trip.allow_guests);
day

View File

@ -7,7 +7,7 @@ use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{role::Role, user::User};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Notification {
pub id: i64,
pub user_id: i64,
@ -21,7 +21,7 @@ pub struct Notification {
impl Notification {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, user_id, message, read_at, created_at, category, link, action_after_reading FROM notification WHERE id like ?", id)
sqlx::query_as!(Self, "SELECT * FROM notification WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
@ -153,7 +153,7 @@ ORDER BY read_at DESC, created_at DESC;
}
}
// Cox read notification about cancelled event
let re = Regex::new(r"^remove_trip_by_event:(\d+)$").unwrap();
let re = Regex::new(r"^remove_trip_by_planned_event:(\d+)$").unwrap();
if let Some(caps) = re.captures(action) {
if let Some(matched) = caps.get(1) {
if let Ok(number) = matched.as_str().parse::<i32>() {
@ -180,116 +180,3 @@ ORDER BY read_at DESC, created_at DESC;
.unwrap();
}
}
#[cfg(test)]
mod test {
use crate::{
model::{
event::{Event, EventUpdate, Registration},
notification::Notification,
trip::Trip,
tripdetails::{TripDetails, TripDetailsToAdd},
user::{CoxUser, User},
usertrip::UserTrip,
},
testdb,
};
use sqlx::SqlitePool;
#[sqlx::test]
fn event_canceled() {
let pool = testdb!();
// Create event
let add_tripdetails = TripDetailsToAdd {
planned_starting_time: "10:00",
max_people: 4,
day: "1970-02-01".into(),
notes: None,
trip_type: None,
allow_guests: false,
always_show: false,
};
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
.await
.unwrap();
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
let event = Event::find_by_trip_details(&pool, trip_details.id)
.await
.unwrap();
// Rower + Cox joins
let rower = User::find_by_name(&pool, "rower").await.unwrap();
UserTrip::create(&pool, &rower, &trip_details, None)
.await
.unwrap();
let cox = CoxUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
.await
.unwrap();
Trip::new_join(&pool, &cox, &event).await.unwrap();
// Cancel Event
let cancel_update = EventUpdate {
name: &event.name,
planned_amount_cox: event.planned_amount_cox as i32,
max_people: 0,
notes: event.notes.as_deref(),
always_show: event.always_show,
is_locked: event.is_locked,
trip_type_id: None,
};
event.update(&pool, &cancel_update).await;
// Rower received notification
let notifications = Notification::for_user(&pool, &rower).await;
let rower_notification = notifications[0].clone();
assert_eq!(rower_notification.category, "Absage Ausfahrt");
assert_eq!(
rower_notification.action_after_reading.as_deref(),
Some("remove_user_trip_with_trip_details_id:3")
);
// Cox received notification
let notifications = Notification::for_user(&pool, &cox.user).await;
let cox_notification = notifications[0].clone();
assert_eq!(cox_notification.category, "Absage Ausfahrt");
assert_eq!(
cox_notification.action_after_reading.as_deref(),
Some("remove_trip_by_event:2")
);
// Notification removed if cancellation is cancelled
let update = EventUpdate {
name: &event.name,
planned_amount_cox: event.planned_amount_cox as i32,
max_people: 3,
notes: event.notes.as_deref(),
always_show: event.always_show,
is_locked: event.is_locked,
trip_type_id: None,
};
event.update(&pool, &update).await;
assert!(Notification::for_user(&pool, &rower).await.is_empty());
assert!(Notification::for_user(&pool, &cox.user).await.is_empty());
// Cancel event again
event.update(&pool, &cancel_update).await;
// Rower is removed if notification is accepted
assert!(event.is_rower_registered(&pool, &rower).await);
rower_notification.mark_read(&pool).await;
assert!(!event.is_rower_registered(&pool, &rower).await);
// Cox is removed if notification is accepted
let registration = Registration::all_cox(&pool, event.id).await;
assert_eq!(registration.len(), 1);
assert_eq!(registration[0].name, "cox");
cox_notification.mark_read(&pool).await;
let registration = Registration::all_cox(&pool, event.id).await;
assert!(registration.is_empty());
}
}

View File

@ -3,7 +3,7 @@ use std::io::Write;
use chrono::NaiveDate;
use ics::{
properties::{DtStart, Summary},
ICalendar,
Event, ICalendar,
};
use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool};
@ -11,10 +11,10 @@ use sqlx::{FromRow, Row, SqlitePool};
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct Event {
pub struct PlannedEvent {
pub id: i64,
pub name: String,
pub(crate) planned_amount_cox: i64,
planned_amount_cox: i64,
trip_details_id: i64,
pub planned_starting_time: String,
pub(crate) max_people: i64,
@ -22,14 +22,14 @@ pub struct Event {
pub notes: Option<String>,
pub allow_guests: bool,
trip_type_id: Option<i64>,
pub(crate) always_show: bool,
pub(crate) is_locked: bool,
always_show: bool,
is_locked: bool,
}
#[derive(Serialize, Debug)]
pub struct EventWithUserAndTriptype {
pub struct PlannedEventWithUserAndTriptype {
#[serde(flatten)]
pub event: Event,
pub planned_event: PlannedEvent,
trip_type: Option<TripType>,
cox_needed: bool,
cox: Vec<Registration>,
@ -73,7 +73,7 @@ FROM user_trip WHERE trip_details_id = {}
.collect()
}
pub async fn all_cox(db: &SqlitePool, event_id: i64) -> Vec<Registration> {
pub async fn all_cox(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
//TODO: switch to join
sqlx::query!(
"
@ -82,7 +82,7 @@ SELECT
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
FROM trip WHERE planned_event_id = ?
",
event_id
trip_details_id
)
.fetch_all(db)
.await
@ -94,11 +94,10 @@ FROM trip WHERE planned_event_id = ?
is_guest: false,
is_real_guest: false,
})
.collect() //Okay, as Event can only be created with proper DB backing
.collect() //Okay, as PlannedEvent can only be created with proper DB backing
}
}
#[derive(Debug)]
pub struct EventUpdate<'a> {
pub name: &'a str,
pub planned_amount_cox: i32,
@ -106,10 +105,9 @@ pub struct EventUpdate<'a> {
pub notes: Option<&'a str>,
pub always_show: bool,
pub is_locked: bool,
pub trip_type_id: Option<i64>,
}
impl Event {
impl PlannedEvent {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
@ -130,16 +128,19 @@ WHERE planned_event.id like ?
pub async fn get_pinned_for_day(
db: &SqlitePool,
day: NaiveDate,
) -> Vec<EventWithUserAndTriptype> {
) -> Vec<PlannedEventWithUserAndTriptype> {
let mut events = Self::get_for_day(db, day).await;
events.retain(|e| e.event.always_show);
events.retain(|e| e.planned_event.always_show);
events
}
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithUserAndTriptype> {
pub async fn get_for_day(
db: &SqlitePool,
day: NaiveDate,
) -> Vec<PlannedEventWithUserAndTriptype> {
let day = format!("{day}");
let events = sqlx::query_as!(
Event,
PlannedEvent,
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
@ -157,20 +158,20 @@ WHERE day=?",
if let Some(trip_type_id) = event.trip_type_id {
trip_type = TripType::find_by_id(db, trip_type_id).await;
}
ret.push(EventWithUserAndTriptype {
ret.push(PlannedEventWithUserAndTriptype {
cox_needed: event.planned_amount_cox > cox.len() as i64,
cox,
rower: Registration::all_rower(db, event.trip_details_id).await,
event,
planned_event: event,
trip_type,
});
}
ret
}
pub async fn all(db: &SqlitePool) -> Vec<Event> {
pub async fn all(db: &SqlitePool) -> Vec<PlannedEvent> {
sqlx::query_as!(
Event,
PlannedEvent,
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
@ -197,27 +198,11 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
is_rower.amount > 0
}
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
WHERE trip_details.id=?
",
tripdetails_id
)
.fetch_one(db)
.await
.ok()
}
pub async fn create(
db: &SqlitePool,
name: &str,
planned_amount_cox: i32,
trip_details: &TripDetails,
trip_details: TripDetails,
) {
sqlx::query!(
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
@ -246,12 +231,11 @@ WHERE trip_details.id=?
let was_already_cancelled = tripdetails.max_people == 0;
sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?",
update.max_people,
update.notes,
update.always_show,
update.is_locked,
update.trip_type_id,
self.trip_details_id
)
.execute(db)
@ -262,9 +246,10 @@ WHERE trip_details.id=?
let coxes = Registration::all_cox(db, self.id).await;
for user in coxes {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match update.notes {
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
_ => String::from(""),
let notes = if let Some(notes) = update.notes {
format!("Grund der Absage: {notes}")
} else {
String::from("")
};
Notification::create(
db,
@ -275,7 +260,7 @@ WHERE trip_details.id=?
),
"Absage Ausfahrt",
None,
Some(&format!("remove_trip_by_event:{}", self.id)),
Some(&format!("remove_trip_by_planned_event:{}", self.id)),
)
.await;
}
@ -285,15 +270,15 @@ WHERE trip_details.id=?
for user in rower {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match update.notes {
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
_ => String::from(""),
Some(n) if !n.is_empty() => n,
_ => ".",
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
"Die Ausfahrt {} am {} um {} wurde abgesagt{}",
self.name, self.day, self.planned_starting_time, notes
),
"Absage Ausfahrt",
@ -313,7 +298,11 @@ WHERE trip_details.id=?
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
)
.await;
Notification::delete_by_action(db, &format!("remove_trip_by_event:{}", self.id)).await;
Notification::delete_by_action(
db,
&format!("remove_trip_by_planned_event:{}", self.id),
)
.await;
}
}
@ -339,45 +328,23 @@ WHERE trip_details.id=?
sqlx::query!("DELETE FROM planned_event WHERE id = ?", self.id)
.execute(db)
.await
.unwrap(); //Okay, as Event can only be created with proper DB backing
.unwrap(); //Okay, as PlannedEvent can only be created with proper DB backing
Ok(())
}
pub fn is_cancelled(&self) -> bool {
self.max_people == 0
}
pub async fn get_ics_feed(db: &SqlitePool) -> String {
let mut calendar = ICalendar::new("2.0", "ics-rs");
let events = Event::all(db).await;
let events = PlannedEvent::all(db).await;
for event in events {
let mut vevent =
ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
let mut vevent = Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
vevent.push(DtStart::new(format!(
"{}T{}00",
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");
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));
if let Some(triptype) = tripdetails.triptype(db).await {
name.push_str(&format!("{} ", triptype.name))
}
vevent.push(Summary::new(name));
vevent.push(Summary::new(event.name));
calendar.add_event(vevent);
}
let mut buf = Vec::new();
@ -396,7 +363,7 @@ WHERE trip_details.id=?
mod test {
use crate::{model::tripdetails::TripDetails, testdb};
use super::Event;
use super::PlannedEvent;
use chrono::NaiveDate;
use sqlx::SqlitePool;
@ -404,7 +371,8 @@ mod test {
fn test_get_day() {
let pool = testdb!();
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 1);
}
@ -414,20 +382,22 @@ mod test {
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
PlannedEvent::create(&pool, "new-event".into(), 2, trip_details).await;
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 2);
}
#[sqlx::test]
fn test_delete() {
let pool = testdb!();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
planned_event.delete(&pool).await.unwrap();
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 0);
}
@ -435,7 +405,7 @@ mod test {
fn test_ics() {
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 \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
let actual = PlannedEvent::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\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
}
}

View File

@ -95,7 +95,7 @@ ORDER BY
#[derive(FromRow, Serialize, Clone)]
pub struct Stat {
name: String,
pub(crate) rowed_km: i32,
rowed_km: i32,
}
impl Stat {
@ -195,34 +195,6 @@ 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)]
@ -246,7 +218,7 @@ FROM (
LEFT JOIN
rower r ON l.id = r.logbook_id
WHERE
r.rower_id = {}
l.shipmaster = {0} OR r.rower_id = {0}
GROUP BY
departure_date
) as subquery

View File

@ -1,31 +0,0 @@
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

@ -1,233 +0,0 @@
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

@ -3,8 +3,8 @@ use serde::Serialize;
use sqlx::SqlitePool;
use super::{
event::{Event, Registration},
notification::Notification,
planned_event::{PlannedEvent, Registration},
tripdetails::TripDetails,
triptype::TripType,
user::{CoxUser, User},
@ -133,28 +133,28 @@ WHERE trip.id=?
.ok()
}
/// Cox decides to help in a event.
/// Cox decides to help in a planned event.
pub async fn new_join(
db: &SqlitePool,
cox: &CoxUser,
event: &Event,
planned_event: &PlannedEvent,
) -> Result<(), CoxHelpError> {
if event.is_rower_registered(db, cox).await {
if planned_event.is_rower_registered(db, cox).await {
return Err(CoxHelpError::AlreadyRegisteredAsRower);
}
if event.trip_details(db).await.is_locked {
if planned_event.trip_details(db).await.is_locked {
return Err(CoxHelpError::DetailsLocked);
}
if event.max_people == 0 {
if planned_event.max_people == 0 {
return Err(CoxHelpError::CanceledEvent);
}
match sqlx::query!(
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
cox.id,
event.id
planned_event.id
)
.execute(db)
.await
@ -223,9 +223,10 @@ WHERE day=?
.rower;
for user in rowers {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match update.notes {
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
_ => String::from(""),
let notes = if let Some(notes) = update.notes {
format!("Grund der Absage: {notes}")
} else {
String::from("")
};
Notification::create(
@ -280,16 +281,16 @@ WHERE day=?
pub async fn delete_by_planned_event(
db: &SqlitePool,
cox: &CoxUser,
event: &Event,
planned_event: &PlannedEvent,
) -> Result<(), TripHelpDeleteError> {
if event.trip_details(db).await.is_locked {
if planned_event.trip_details(db).await.is_locked {
return Err(TripHelpDeleteError::DetailsLocked);
}
let affected_rows = sqlx::query!(
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
cox.id,
event.id
planned_event.id
)
.execute(db)
.await
@ -373,7 +374,7 @@ pub enum TripUpdateError {
mod test {
use crate::{
model::{
event::Event,
planned_event::PlannedEvent,
trip::{self, TripDeleteError},
tripdetails::TripDetails,
user::{CoxUser, User},
@ -424,7 +425,7 @@ mod test {
.await
.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_ok());
}
@ -440,7 +441,7 @@ mod test {
.await
.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_err());
@ -541,7 +542,7 @@ mod test {
.await
.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();

View File

@ -7,7 +7,6 @@ use sqlx::{FromRow, SqlitePool};
use super::{
notification::Notification,
trip::{Trip, TripWithUserAndType},
triptype::TripType,
};
#[derive(FromRow, Debug, Serialize, Deserialize)]
@ -52,13 +51,6 @@ WHERE id like ?
.ok()
}
pub async fn triptype(&self, db: &SqlitePool) -> Option<TripType> {
match self.trip_type_id {
None => None,
Some(id) => TripType::find_by_id(db, id).await,
}
}
pub async fn find_by_startingdatetime(
db: &SqlitePool,
day: String,

View File

@ -4,7 +4,7 @@ use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct TripType {
pub id: i64,
pub name: String,
name: String,
desc: String,
question: String,
icon: String,

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, stat::Stat,
family::Family, log::Log, mail::Mail, notification::Notification, role::Role,
tripdetails::TripDetails, Day,
};
use crate::tera::admin::user::UserEditForm;
@ -187,7 +187,7 @@ impl User {
format!(
"Hallo {0},
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
Liebe Grüße, Philipp", self.name),
smtp_pw,
@ -281,8 +281,6 @@ 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
@ -488,8 +486,6 @@ WHERE id like ?
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
let name = name.trim().to_lowercase();
sqlx::query_as!(
Self,
"
@ -827,7 +823,7 @@ ORDER BY last_access DESC
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
if self.has_role(db, "scheckbuch").await {
let day = Day::new_guest(db, date, true).await;
if !day.events.is_empty() {
if !day.planned_events.is_empty() {
days.push(day);
}
} else {
@ -851,19 +847,6 @@ 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]
@ -1141,15 +1124,15 @@ impl<'r> FromRequest<'r> for VorstandUser {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EventUser(pub(crate) User);
pub struct PlannedEventUser(pub(crate) User);
impl From<EventUser> for User {
fn from(val: EventUser) -> Self {
impl From<PlannedEventUser> for User {
fn from(val: PlannedEventUser) -> Self {
val.0
}
}
impl Deref for EventUser {
impl Deref for PlannedEventUser {
type Target = User;
fn deref(&self) -> &Self::Target {
@ -1200,15 +1183,15 @@ impl UserWithMembershipPdf {
}
#[async_trait]
impl<'r> FromRequest<'r> for EventUser {
impl<'r> FromRequest<'r> for PlannedEventUser {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if user.has_role(db, "manage_events").await {
Outcome::Success(EventUser(user))
if user.has_role(db, "planned_event").await {
Outcome::Success(PlannedEventUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}

View File

@ -150,7 +150,7 @@ pub enum UserTripDeleteError {
mod test {
use crate::{
model::{
event::Event, trip::Trip, tripdetails::TripDetails, user::CoxUser,
planned_event::PlannedEvent, trip::Trip, tripdetails::TripDetails, user::CoxUser,
usertrip::UserTripError,
},
testdb,
@ -240,8 +240,8 @@ mod test {
.await
.unwrap();
let event = Event::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &event).await.unwrap();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
let result = UserTrip::create(&pool, &cox, &trip_details, None)

View File

@ -17,13 +17,6 @@ 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,
@ -60,28 +53,15 @@ impl Waterlevel {
Ok(())
}
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",
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<i64> {
sqlx::query!(
"SELECT MAX(mittel) as max FROM waterlevel WHERE day = ?",
day
)
.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
.fetch_one(db)
.await
.unwrap()
.max
}
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {

View File

@ -9,9 +9,9 @@ use crate::{
};
pub mod boat;
pub mod event;
pub mod mail;
pub mod notification;
pub mod planned_event;
pub mod schnupper;
pub mod user;
@ -80,7 +80,7 @@ pub fn routes() -> Vec<Route> {
ret.append(&mut boat::routes());
ret.append(&mut notification::routes());
ret.append(&mut mail::routes());
ret.append(&mut event::routes());
ret.append(&mut planned_event::routes());
ret.append(&mut routes![rss, show_rss, show_list, list]);
ret
}

View File

@ -8,14 +8,14 @@ use serde::Serialize;
use sqlx::SqlitePool;
use crate::model::{
event::{self, Event},
planned_event::{self, PlannedEvent},
tripdetails::{TripDetails, TripDetailsToAdd},
user::EventUser,
user::PlannedEventUser,
};
//TODO: add constraints (e.g. planned_amount_cox > 0)
#[derive(FromForm, Serialize)]
struct AddEventForm<'r> {
struct AddPlannedEventForm<'r> {
name: &'r str,
planned_amount_cox: i32,
tripdetails: TripDetailsToAdd<'r>,
@ -24,8 +24,8 @@ struct AddEventForm<'r> {
#[post("/planned-event", data = "<data>")]
async fn create(
db: &State<SqlitePool>,
data: Form<AddEventForm<'_>>,
_admin: EventUser,
data: Form<AddPlannedEventForm<'_>>,
_admin: PlannedEventUser,
) -> Flash<Redirect> {
let data = data.into_inner();
@ -34,14 +34,14 @@ async fn create(
//just created
//the object
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await;
PlannedEvent::create(db, data.name, data.planned_amount_cox, trip_details).await;
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
}
//TODO: add constraints (e.g. planned_amount_cox > 0)
#[derive(FromForm, Debug)]
struct UpdateEventForm<'r> {
#[derive(FromForm)]
struct UpdatePlannedEventForm<'r> {
id: i64,
name: &'r str,
planned_amount_cox: i32,
@ -49,25 +49,23 @@ struct UpdateEventForm<'r> {
notes: Option<&'r str>,
always_show: bool,
is_locked: bool,
trip_type: Option<i64>,
}
#[put("/planned-event", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<UpdateEventForm<'_>>,
_admin: EventUser,
data: Form<UpdatePlannedEventForm<'_>>,
_admin: PlannedEventUser,
) -> Flash<Redirect> {
let update = event::EventUpdate {
let update = planned_event::EventUpdate {
name: data.name,
planned_amount_cox: data.planned_amount_cox,
max_people: data.max_people,
notes: data.notes,
always_show: data.always_show,
is_locked: data.is_locked,
trip_type_id: data.trip_type,
};
match Event::find_by_id(db, data.id).await {
match PlannedEvent::find_by_id(db, data.id).await {
Some(planned_event) => {
planned_event.update(db, &update).await;
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
@ -77,9 +75,9 @@ async fn update(
}
#[get("/planned-event/<id>/delete")]
async fn delete(db: &State<SqlitePool>, id: i64, _admin: EventUser) -> Flash<Redirect> {
let Some(event) = Event::find_by_id(db, id).await else {
return Flash::error(Redirect::to("/planned"), "Event does not exist");
async fn delete(db: &State<SqlitePool>, id: i64, _admin: PlannedEventUser) -> Flash<Redirect> {
let Some(event) = PlannedEvent::find_by_id(db, id).await else {
return Flash::error(Redirect::to("/planned"), "PlannedEvent does not exist");
};
match event.delete(db).await {
@ -107,7 +105,7 @@ mod test {
fn test_delete() {
let db = testdb!();
let _ = Event::find_by_id(&db, 1).await.unwrap();
let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
@ -132,7 +130,7 @@ mod test {
assert_eq!(flash_cookie.value(), "7:successEvent gelöscht");
let event = Event::find_by_id(&db, 1).await;
let event = PlannedEvent::find_by_id(&db, 1).await;
assert_eq!(event, None);
}
@ -161,16 +159,16 @@ mod test {
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "5:errorEvent does not exist");
assert_eq!(flash_cookie.value(), "5:errorPlannedEvent does not exist");
let _ = Event::find_by_id(&db, 1).await.unwrap();
let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap();
}
#[sqlx::test]
fn test_update() {
let db = testdb!();
let event = Event::find_by_id(&db, 1).await.unwrap();
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
assert_eq!(event.notes, Some("trip_details for a planned event".into()));
let rocket = rocket::build().manage(db.clone());
@ -202,7 +200,7 @@ mod test {
"7:successEvent erfolgreich bearbeitet"
);
let event = Event::find_by_id(&db, 1).await.unwrap();
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
assert_eq!(event.notes, Some("new-planned-event-text".into()));
}
@ -269,7 +267,7 @@ mod test {
assert_eq!(flash_cookie.value(), "7:successEvent hinzugefügt");
let event = Event::find_by_id(&db, 2).await.unwrap();
let event = PlannedEvent::find_by_id(&db, 2).await.unwrap();
assert_eq!(event.name, "my-cool-new-event");
}
}

View File

@ -7,8 +7,8 @@ use rocket::{
use sqlx::SqlitePool;
use crate::model::{
event::Event,
log::Log,
planned_event::PlannedEvent,
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
tripdetails::{TripDetails, TripDetailsToAdd},
user::CoxUser,
@ -82,7 +82,7 @@ async fn update(
#[get("/join/<planned_event_id>")]
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
if let Some(planned_event) = PlannedEvent::find_by_id(db, planned_event_id).await {
match Trip::new_join(db, &cox, &planned_event).await {
Ok(_) => {
Log::create(
@ -137,7 +137,7 @@ async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flas
#[get("/remove/<planned_event_id>")]
async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
if let Some(planned_event) = PlannedEvent::find_by_id(db, planned_event_id).await {
match Trip::delete_by_planned_event(db, &cox, &planned_event).await {
Ok(_) => {
Log::create(

View File

@ -212,7 +212,7 @@ async fn create_logbook(
)
.await
{
Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt erfolgreich hinzugefügt"),
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

@ -1,12 +1,12 @@
use rocket::{get, http::ContentType, routes, Route, State};
use sqlx::SqlitePool;
use crate::model::event::Event;
use crate::model::planned_event::PlannedEvent;
#[get("/cal")]
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
//TODO: add unit test once proper functionality is there
(ContentType::Calendar, Event::get_ics_feed(db).await)
(ContentType::Calendar, PlannedEvent::get_ics_feed(db).await)
}
pub fn routes() -> Vec<Route> {

View File

@ -39,7 +39,6 @@ mod misc;
mod notification;
mod planned;
mod stat;
pub(crate) mod trailerreservation;
#[derive(FromForm, Debug)]
struct LoginForm<'r> {
@ -201,7 +200,6 @@ 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

@ -26,7 +26,7 @@ async fn index(
let mut context = Context::new();
if user.has_role(db, "cox").await || user.has_role(db, "manage_events").await {
if user.has_role(db, "cox").await || user.has_role(db, "planned_event").await {
let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes);
}

View File

@ -1,211 +0,0 @@
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,7 +36,6 @@
<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,10 +100,7 @@
<div class="p-3">
<ul>
<li>
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).
Die Wetterdaten werden von <a class="underline" href="https://openweathermap.org">OpenWeather</a> bereitgestellt.
</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", 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 ---") }}
{{ 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 ---") }}
{% 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,8 +279,4 @@
</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,8 +78,6 @@
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,10 +90,6 @@
<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

@ -58,11 +58,11 @@
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
{% include "includes/buttons" %}
{% for day in days %}
{% set amount_trips = day.events | length + day.trips | length %}
{% set amount_trips = day.planned_events | length + day.trips | length %}
{% set_global day_cox_needed = false %}
{% if day.events | length > 0 %}
{% for event in day.events %}
{% if event.cox_needed %}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events %}
{% if planned_event.cox_needed %}
{% set_global day_cox_needed = true %}
{% endif %}
{% endfor %}
@ -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.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>
title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel }} cm">🌊{{ day.max_waterlevel }} cm</a>
{% endif %}
</small>
{% if day.weather %}
@ -88,82 +88,82 @@
</small>
{% endif %}
</h2>
{% if day.events | length > 0 or day.trips | length > 0 %}
{% if day.planned_events | length > 0 or day.trips | length > 0 %}
<div class="grid grid-cols-1 gap-3 mb-3">
{# --- START Events --- #}
{% if day.events | length > 0 %}
{% for event in day.events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = event.cox | length %}
{% set amount_cox_missing = event.planned_amount_cox - amount_cur_cox %}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = planned_event.cox | length %}
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
<div class="pt-2 px-3 border-t border-gray-200"
style="order: {{ event.planned_starting_time | replace(from=":", to="") }}">
style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}">
<div class="flex justify-between items-center">
<div class="mr-1">
{% if event.max_people == 0 %}
{% if planned_event.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888; Absage
{{ event.planned_starting_time }}
{{ planned_event.planned_starting_time }}
Uhr
</strong>
<small class="text-[#f43f5e]">({{ event.name }}
{%- if event.trip_type %}
- {{ event.trip_type.icon | safe }}&nbsp;{{ event.trip_type.name }}
<small class="text-[#f43f5e]">({{ planned_event.name }}
{%- if planned_event.trip_type %}
- {{ planned_event.trip_type.icon | safe }}&nbsp;{{ planned_event.trip_type.name }}
{%- endif -%}
)</small>
{% else %}
<strong class="text-primary-900 dark:text-white">
{{ event.planned_starting_time }}
{{ planned_event.planned_starting_time }}
Uhr
</strong>
<small class="text-gray-600 dark:text-gray-100">({{ event.name }}
{%- if event.trip_type %}
- {{ event.trip_type.icon | safe }}&nbsp;{{ event.trip_type.name }}
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}
{%- if planned_event.trip_type %}
- {{ planned_event.trip_type.icon | safe }}&nbsp;{{ planned_event.trip_type.name }}
{%- endif -%}
)</small>
{% endif %}
<br />
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ event.planned_starting_time }} Uhr</strong> ({{ event.name }})
{% if event.trip_type %}<small class='block'>{{ event.trip_type.desc }}</small>{% endif %}
{% if event.notes %}<small class='block'>{{ event.notes }}</small>{% endif %}
" data-body="#event{{ event.trip_details_id }}" class="inline-block link-primary mr-3">
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }})
{% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}
{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}
" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div class="text-right grid gap-2">
{# --- START Row Buttons --- #}
{% set_global cur_user_participates = false %}
{% for rower in event.rower %}
{% for rower in planned_event.rower %}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/planned/remove/{{ event.trip_details_id }}"
<a href="/planned/remove/{{ planned_event.trip_details_id }}"
class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if event.max_people > event.rower | length and cur_user_participates == false %}
<a href="/planned/join/{{ event.trip_details_id }}"
{% if planned_event.max_people > planned_event.rower | length and cur_user_participates == false %}
<a href="/planned/join/{{ planned_event.trip_details_id }}"
class="btn btn-primary btn-fw"
{% if event.trip_type %}onclick="return confirm('{{ event.trip_type.question }}');"{% endif %}>Mitrudern</a>
{% if planned_event.trip_type %}onclick="return confirm('{{ planned_event.trip_type.question }}');"{% endif %}>Mitrudern</a>
{% endif %}
{# --- END Row Buttons --- #}
{# --- START Cox Buttons --- #}
{% if "cox" in loggedin_user.roles %}
{% set_global cur_user_participates = false %}
{% for cox in event.cox %}
{% for cox in planned_event.cox %}
{% if cox.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/cox/remove/{{ event.id }}"
<a href="/cox/remove/{{ planned_event.id }}"
class="block btn btn-attention btn-fw">
{% include "includes/cox-icon" %}
Abmelden
</a>
{% elif event.planned_amount_cox > 0 %}
<a href="/cox/join/{{ event.id }}"
{% elif planned_event.planned_amount_cox > 0 %}
<a href="/cox/join/{{ planned_event.id }}"
class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw"
{% if event.trip_type %}onclick="return confirm('{{ event.trip_type.question }}');"{% endif %}>
{% if planned_event.trip_type %}onclick="return confirm('{{ planned_event.trip_type.question }}');"{% endif %}>
{% include "includes/cox-icon" %}
Steuern
</a>
@ -174,83 +174,82 @@
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div id="event{{ event.trip_details_id }}">
<div id="event{{ planned_event.trip_details_id }}">
{# --- START List Coxes --- #}
{% if event.planned_amount_cox > 0 %}
{% if event.max_people == 0 %}
{{ macros::box(participants=event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }}
{% if planned_event.planned_amount_cox > 0 %}
{% if planned_event.max_people == 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }}
{% else %}
{% if amount_cox_missing > 0 %}
{{ macros::box(participants=event.cox, empty_seats=event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{% else %}
{{ macros::box(participants=event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% endif %}
{% endif %}
{% endif %}
{# --- END List Coxes --- #}
{# --- START List Rowers --- #}
{% set amount_cur_rower = event.rower | length %}
{% if event.max_people == 0 %}
{{ macros::box(header='Absage', bg='[#f43f5e]', participants=event.rower, trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
{% set amount_cur_rower = planned_event.rower | length %}
{% if planned_event.max_people == 0 %}
{{ macros::box(header='Absage', bg='[#f43f5e]', participants=planned_event.rower, trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }}
{% else %}
{{ macros::box(participants=event.rower, empty_seats=event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }}
{% endif %}
{# --- END List Rowers --- #}
{% if "manage_events" in loggedin_user.roles %}
<form action="/planned/join/{{ event.trip_details_id }}" method="get" />
{% if "planned_event" in loggedin_user.roles %}
<form action="/planned/join/{{ planned_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"
type="submit" />
</form>
{% endif %}
{% if event.allow_guests %}
{% if planned_event.allow_guests %}
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
{% endif %}
{% if "manage_events" in loggedin_user.roles %}
{% if "planned_event" in loggedin_user.roles %}
{# --- START Edit Form --- #}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3>
<form action="/admin/planned-event" method="post" class="grid gap-3">
<input type="hidden" name="_method" value="put" />
<input type="hidden" name="id" value="{{ event.id }}" />
{{ macros::input(label='Titel', name='name', type='input', value=event.name) }}
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='1') }}
{{ 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::input(label='Anmerkungen', name='notes', type='input', value=event.notes) }}
<input type="hidden" name="id" value="{{ planned_event.id }}" />
{{ macros::input(label='Titel', name='name', type='input', value=planned_event.name) }}
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='1') }}
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }}
<input value="Speichern" class="btn btn-primary" type="submit" />
</form>
</div>
{# --- END Edit Form --- #}
{# --- START Delete Btn --- #}
{% if event.rower | length == 0 and amount_cur_cox == 0 %}
{% if planned_event.rower | length == 0 and amount_cur_cox == 0 %}
<div class="text-right mt-6">
<a href="/admin/planned-event/{{ event.id }}/delete"
<a href="/admin/planned-event/{{ planned_event.id }}/delete"
class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% else %}
{% if event.max_people == 0 %}
{% if planned_event.max_people == 0 %}
Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
{% else %}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Event absagen</h3>
<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='') }}
<input type="hidden" name="id" value="{{ planned_event.id }}" />
{{ 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) }}
{{ macros::input(label='', name='planned_amount_cox', type='hidden', value=event.planned_amount_cox) }}
{{ macros::input(label='', name='always_show', type='hidden', value=event.always_show) }}
{{ macros::input(label='', name='is_locked', type='hidden', value=event.is_locked) }}
{{ macros::input(label='', name='trip_type', type='hidden', value=event.trip_type_id) }}
{{ macros::input(label='', name='name', type='hidden', value=planned_event.name) }}
{{ macros::input(label='', name='max_people', type='hidden', value=planned_event.max_people) }}
{{ macros::input(label='', name='planned_amount_cox', type='hidden', value=planned_event.planned_amount_cox) }}
{{ macros::input(label='', name='always_show', type='hidden', value=planned_event.always_show) }}
{{ macros::input(label='', name='is_locked', type='hidden', value=planned_event.is_locked) }}
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
</form>
</div>
@ -390,9 +389,9 @@
{% endif %}
</div>
{# --- START Add Buttons --- #}
{% if "manage_events" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "manage_events" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
{% if "manage_events" in loggedin_user.roles %}
{% if "planned_event" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "planned_event" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
{% if "planned_event" in loggedin_user.roles %}
<a href="#"
data-sidebar="true"
data-trigger="sidebar"
@ -406,7 +405,7 @@
{% endif %}
{% if "cox" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200
{% if "manage_events" in loggedin_user.roles %}
{% if "planned_event" in loggedin_user.roles %}
rounded-br-md
{% else %}
rounded-b-md
@ -426,7 +425,7 @@
{% if "cox" in loggedin_user.roles %}
{% include "forms/trip" %}
{% endif %}
{% if "manage_events" in loggedin_user.roles %}
{% if "planned_event" in loggedin_user.roles %}
{% include "forms/event" %}
{% endif %}
{% endblock content %}

View File

@ -1,98 +0,0 @@
{% 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 %}