Compare commits
93 Commits
1f0de7abf4
...
test
Author | SHA1 | Date | |
---|---|---|---|
2159696112 | |||
39bde35864 | |||
5f301324ee | |||
9558965e8f | |||
5f4d8982a8 | |||
0e2ef9e256 | |||
1dc91f4f28 | |||
f56da43723 | |||
9dc1ec6fa0 | |||
c9b67f5790 | |||
16687e39ab | |||
957c474389 | |||
f7aed68423 | |||
01637d0800 | |||
0a77011170 | |||
dea0c65da3 | |||
31bf38f112 | |||
6b29907596 | |||
7b17c30ce2 | |||
ec4068e499 | |||
fca19745f8 | |||
bb48ddb3de | |||
34b098fa2a | |||
df1a06531f | |||
7b499fb457 | |||
d00570ff2f | |||
bd63f2c386 | |||
e1b78b2725 | |||
fa14cfbf83 | |||
5f6cb9a12b | |||
1cac70cabb | |||
09cb8ebfa9 | |||
ab88ce3230 | |||
30a6bc7109 | |||
99409f9407 | |||
7eff2a948a | |||
243838fd44 | |||
2de4c86c26 | |||
2889d40d55 | |||
e8d4672176 | |||
1bd643f6f4 | |||
562c32939d | |||
3c0b8e5114 | |||
6d5ff5404b | |||
a8c0282918 | |||
9973913af6 | |||
7055c999e8 | |||
c0d766832e | |||
f88c0be781 | |||
91fa2a7762 | |||
82aa94c024 | |||
aaf09208f3 | |||
86f7ca7065 | |||
e325e0478a | |||
0298617fc9 | |||
02ff89ba34 | |||
47a543fa64 | |||
4e8fd84134 | |||
544267a037 | |||
97b0ae83a9 | |||
f6d8c07c08 | |||
da56723909 | |||
603aed8394 | |||
f22d3b65be | |||
446e48020e | |||
93e3e0ef5c | |||
8f5cc70981 | |||
71c228f202 | |||
3ebde6afce | |||
a797180b0d | |||
9704893329 | |||
05c4c4f6a2 | |||
daf9460bf7 | |||
db5e0873a6 | |||
40f97f18a9 | |||
6b911f242a | |||
f4ce748a74 | |||
d4ffd8850e | |||
c93556a5ab | |||
96ce46d39c | |||
412ec27927 | |||
a1c7e4c690 | |||
64f3596132 | |||
10f2e3016a | |||
b6c9cb0b99 | |||
1b6aec8d89 | |||
387acdbd09 | |||
b8463122d6 | |||
76f08905ab | |||
b405cf9936 | |||
0318d1dfb2 | |||
073f5aed0c | |||
20cc085562 |
519
Cargo.lock
generated
519
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,7 @@ rest = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { version = "0.5.0", features = ["secrets"]}
|
rocket = { version = "0.5.0", features = ["secrets"]}
|
||||||
rocket_dyn_templates = {version = "0.1.0", features = [ "tera" ], optional = true }
|
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
|
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
|
||||||
@ -24,7 +24,7 @@ ics = "0.5"
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
lettre = "0.11"
|
lettre = "0.11"
|
||||||
csv = "1.3"
|
csv = "1.3"
|
||||||
itertools = "0.12"
|
itertools = "0.13"
|
||||||
job_scheduler_ng = "2.0"
|
job_scheduler_ng = "2.0"
|
||||||
ureq = { version = "2.9", features = ["json"] }
|
ureq = { version = "2.9", features = ["json"] }
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|

|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
## Frontend
|
## Frontend
|
||||||
1. `cd frontend`
|
1. `cd frontend`
|
||||||
|
@ -67,6 +67,16 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
|||||||
await expect(page.getByRole("listbox")).toContainText(
|
await expect(page.getByRole("listbox")).toContainText(
|
||||||
"Nur 2 Ruderer können hinzugefügt werden",
|
"Nur 2 Ruderer können hinzugefügt werden",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Trip starts 2 hours ago
|
||||||
|
const datetimeSelector = '#departure';
|
||||||
|
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||||
|
const currentDate = new Date(currentValue);
|
||||||
|
currentDate.setMinutes(currentDate.getMinutes());
|
||||||
|
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||||
|
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||||
|
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||||
|
|
||||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||||
"rower2 cox",
|
"rower2 cox",
|
||||||
@ -80,15 +90,6 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
|||||||
await page.goto("/log");
|
await page.goto("/log");
|
||||||
await page.locator("div:nth-child(2) > .border-0").click();
|
await page.locator("div:nth-child(2) > .border-0").click();
|
||||||
|
|
||||||
// Add a minute
|
|
||||||
const datetimeSelector = '#arrivaljs';
|
|
||||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
|
||||||
const currentDate = new Date(currentValue);
|
|
||||||
currentDate.setMinutes(currentDate.getMinutes() + 1);
|
|
||||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60);
|
|
||||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
|
||||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
|
||||||
|
|
||||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||||
@ -153,6 +154,16 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
|||||||
await expect(page.getByRole("listbox")).toContainText(
|
await expect(page.getByRole("listbox")).toContainText(
|
||||||
"Nur 2 Ruderer können hinzugefügt werden",
|
"Nur 2 Ruderer können hinzugefügt werden",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Trip starts 2 hours ago
|
||||||
|
const datetimeSelector = '#departure';
|
||||||
|
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||||
|
const currentDate = new Date(currentValue);
|
||||||
|
currentDate.setMinutes(currentDate.getMinutes());
|
||||||
|
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||||
|
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||||
|
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||||
|
|
||||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||||
"rower2 cox",
|
"rower2 cox",
|
||||||
@ -166,15 +177,6 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
|||||||
await page.goto("/log");
|
await page.goto("/log");
|
||||||
await page.locator('div:nth-child(2) > .pt-2 > div > div > div:nth-child(2) > .border-0').click(); // 2 trips currently running, try to close second one
|
await page.locator('div:nth-child(2) > .pt-2 > div > div > div:nth-child(2) > .border-0').click(); // 2 trips currently running, try to close second one
|
||||||
|
|
||||||
// Add a minute
|
|
||||||
const datetimeSelector = '#arrivaljs';
|
|
||||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
|
||||||
const currentDate = new Date(currentValue);
|
|
||||||
currentDate.setMinutes(currentDate.getMinutes() + 1);
|
|
||||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60);
|
|
||||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
|
||||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
|
||||||
|
|
||||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||||
|
@ -195,3 +195,21 @@ CREATE TABLE IF NOT EXISTS "weather" (
|
|||||||
"wind_gust" FLOAT NOT NULL,
|
"wind_gust" FLOAT NOT NULL,
|
||||||
"rain_mm" 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
|
||||||
|
);
|
||||||
|
|
||||||
|
@ -3,11 +3,12 @@ INSERT INTO "role" (name) VALUES ('cox');
|
|||||||
INSERT INTO "role" (name) VALUES ('scheckbuch');
|
INSERT INTO "role" (name) VALUES ('scheckbuch');
|
||||||
INSERT INTO "role" (name) VALUES ('tech');
|
INSERT INTO "role" (name) VALUES ('tech');
|
||||||
INSERT INTO "role" (name) VALUES ('Donau Linz');
|
INSERT INTO "role" (name) VALUES ('Donau Linz');
|
||||||
INSERT INTO "role" (name) VALUES ('planned_event');
|
INSERT INTO "role" (name) VALUES ('manage_events');
|
||||||
INSERT INTO "role" (name) VALUES ('Rennrudern');
|
INSERT INTO "role" (name) VALUES ('Rennrudern');
|
||||||
INSERT INTO "role" (name) VALUES ('paid');
|
INSERT INTO "role" (name) VALUES ('paid');
|
||||||
INSERT INTO "role" (name) VALUES ('Vorstand');
|
INSERT INTO "role" (name) VALUES ('Vorstand');
|
||||||
INSERT INTO "role" (name) VALUES ('Bootsführer');
|
INSERT INTO "role" (name) VALUES ('Bootsführer');
|
||||||
|
INSERT INTO "role" (name) VALUES ('schnupperant');
|
||||||
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
|
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
|
||||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
|
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
|
||||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
|
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
|
||||||
@ -63,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) 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 "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 "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');
|
||||||
|
@ -81,20 +81,20 @@ pub struct BoatToUpdate<'r> {
|
|||||||
|
|
||||||
impl Boat {
|
impl Boat {
|
||||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
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)
|
.fetch_one(db)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
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())
|
.fetch_one(db.deref_mut())
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
|
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)
|
.fetch_one(db)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
|
@ -3,7 +3,7 @@ use std::io::Write;
|
|||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use ics::{
|
use ics::{
|
||||||
properties::{DtStart, Summary},
|
properties::{DtStart, Summary},
|
||||||
Event, ICalendar,
|
ICalendar,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::{FromRow, Row, SqlitePool};
|
use sqlx::{FromRow, Row, SqlitePool};
|
||||||
@ -11,10 +11,10 @@ use sqlx::{FromRow, Row, SqlitePool};
|
|||||||
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
|
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
|
||||||
|
|
||||||
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
|
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
|
||||||
pub struct PlannedEvent {
|
pub struct Event {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
planned_amount_cox: i64,
|
pub(crate) planned_amount_cox: i64,
|
||||||
trip_details_id: i64,
|
trip_details_id: i64,
|
||||||
pub planned_starting_time: String,
|
pub planned_starting_time: String,
|
||||||
pub(crate) max_people: i64,
|
pub(crate) max_people: i64,
|
||||||
@ -22,14 +22,14 @@ pub struct PlannedEvent {
|
|||||||
pub notes: Option<String>,
|
pub notes: Option<String>,
|
||||||
pub allow_guests: bool,
|
pub allow_guests: bool,
|
||||||
trip_type_id: Option<i64>,
|
trip_type_id: Option<i64>,
|
||||||
always_show: bool,
|
pub(crate) always_show: bool,
|
||||||
is_locked: bool,
|
pub(crate) is_locked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct PlannedEventWithUserAndTriptype {
|
pub struct EventWithUserAndTriptype {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub planned_event: PlannedEvent,
|
pub event: Event,
|
||||||
trip_type: Option<TripType>,
|
trip_type: Option<TripType>,
|
||||||
cox_needed: bool,
|
cox_needed: bool,
|
||||||
cox: Vec<Registration>,
|
cox: Vec<Registration>,
|
||||||
@ -73,7 +73,7 @@ FROM user_trip WHERE trip_details_id = {}
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn all_cox(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
|
pub async fn all_cox(db: &SqlitePool, event_id: i64) -> Vec<Registration> {
|
||||||
//TODO: switch to join
|
//TODO: switch to join
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
@ -82,7 +82,7 @@ SELECT
|
|||||||
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
|
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
|
||||||
FROM trip WHERE planned_event_id = ?
|
FROM trip WHERE planned_event_id = ?
|
||||||
",
|
",
|
||||||
trip_details_id
|
event_id
|
||||||
)
|
)
|
||||||
.fetch_all(db)
|
.fetch_all(db)
|
||||||
.await
|
.await
|
||||||
@ -94,11 +94,22 @@ FROM trip WHERE planned_event_id = ?
|
|||||||
is_guest: false,
|
is_guest: false,
|
||||||
is_real_guest: false,
|
is_real_guest: false,
|
||||||
})
|
})
|
||||||
.collect() //Okay, as PlannedEvent can only be created with proper DB backing
|
.collect() //Okay, as Event can only be created with proper DB backing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlannedEvent {
|
#[derive(Debug)]
|
||||||
|
pub struct EventUpdate<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub planned_amount_cox: i32,
|
||||||
|
pub max_people: i32,
|
||||||
|
pub notes: Option<&'a str>,
|
||||||
|
pub always_show: bool,
|
||||||
|
pub is_locked: bool,
|
||||||
|
pub trip_type_id: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
@ -119,19 +130,16 @@ WHERE planned_event.id like ?
|
|||||||
pub async fn get_pinned_for_day(
|
pub async fn get_pinned_for_day(
|
||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
day: NaiveDate,
|
day: NaiveDate,
|
||||||
) -> Vec<PlannedEventWithUserAndTriptype> {
|
) -> Vec<EventWithUserAndTriptype> {
|
||||||
let mut events = Self::get_for_day(db, day).await;
|
let mut events = Self::get_for_day(db, day).await;
|
||||||
events.retain(|e| e.planned_event.always_show);
|
events.retain(|e| e.event.always_show);
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_for_day(
|
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithUserAndTriptype> {
|
||||||
db: &SqlitePool,
|
|
||||||
day: NaiveDate,
|
|
||||||
) -> Vec<PlannedEventWithUserAndTriptype> {
|
|
||||||
let day = format!("{day}");
|
let day = format!("{day}");
|
||||||
let events = sqlx::query_as!(
|
let events = sqlx::query_as!(
|
||||||
PlannedEvent,
|
Event,
|
||||||
"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
|
"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
|
FROM planned_event
|
||||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||||
@ -149,20 +157,20 @@ WHERE day=?",
|
|||||||
if let Some(trip_type_id) = event.trip_type_id {
|
if let Some(trip_type_id) = event.trip_type_id {
|
||||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||||
}
|
}
|
||||||
ret.push(PlannedEventWithUserAndTriptype {
|
ret.push(EventWithUserAndTriptype {
|
||||||
cox_needed: event.planned_amount_cox > cox.len() as i64,
|
cox_needed: event.planned_amount_cox > cox.len() as i64,
|
||||||
cox,
|
cox,
|
||||||
rower: Registration::all_rower(db, event.trip_details_id).await,
|
rower: Registration::all_rower(db, event.trip_details_id).await,
|
||||||
planned_event: event,
|
event,
|
||||||
trip_type,
|
trip_type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn all(db: &SqlitePool) -> Vec<PlannedEvent> {
|
pub async fn all(db: &SqlitePool) -> Vec<Event> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
PlannedEvent,
|
Event,
|
||||||
"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
|
"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
|
FROM planned_event
|
||||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||||
@ -189,11 +197,27 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
|||||||
is_rower.amount > 0
|
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(
|
pub async fn create(
|
||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
name: &str,
|
name: &str,
|
||||||
planned_amount_cox: i32,
|
planned_amount_cox: i32,
|
||||||
trip_details: TripDetails,
|
trip_details: &TripDetails,
|
||||||
) {
|
) {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
|
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
|
||||||
@ -207,20 +231,11 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO: create unit test
|
//TODO: create unit test
|
||||||
pub async fn update(
|
pub async fn update(&self, db: &SqlitePool, update: &EventUpdate<'_>) {
|
||||||
&self,
|
|
||||||
db: &SqlitePool,
|
|
||||||
name: &str,
|
|
||||||
planned_amount_cox: i32,
|
|
||||||
max_people: i32,
|
|
||||||
notes: Option<&str>,
|
|
||||||
always_show: bool,
|
|
||||||
is_locked: bool,
|
|
||||||
) {
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
|
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
|
||||||
name,
|
update.name,
|
||||||
planned_amount_cox,
|
update.planned_amount_cox,
|
||||||
self.id
|
self.id
|
||||||
)
|
)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
@ -231,36 +246,36 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
|||||||
let was_already_cancelled = tripdetails.max_people == 0;
|
let was_already_cancelled = tripdetails.max_people == 0;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?",
|
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
|
||||||
max_people,
|
update.max_people,
|
||||||
notes,
|
update.notes,
|
||||||
always_show,
|
update.always_show,
|
||||||
is_locked,
|
update.is_locked,
|
||||||
|
update.trip_type_id,
|
||||||
self.trip_details_id
|
self.trip_details_id
|
||||||
)
|
)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await
|
.await
|
||||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||||
|
|
||||||
if max_people == 0 && !was_already_cancelled {
|
if update.max_people == 0 && !was_already_cancelled {
|
||||||
let coxes = Registration::all_cox(db, self.id).await;
|
let coxes = Registration::all_cox(db, self.id).await;
|
||||||
for user in coxes {
|
for user in coxes {
|
||||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||||
let notes = match notes {
|
let notes = match update.notes {
|
||||||
Some(n) if !n.is_empty() => n,
|
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||||
_ => ".",
|
_ => String::from(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
Notification::create(
|
Notification::create(
|
||||||
db,
|
db,
|
||||||
&user,
|
&user,
|
||||||
&format!(
|
&format!(
|
||||||
"Die Ausfahrt {} am {} um {} wurde abgesagt{}",
|
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||||
self.name, self.day, self.planned_starting_time, notes
|
self.name, self.day, self.planned_starting_time, notes
|
||||||
),
|
),
|
||||||
"Absage Ausfahrt",
|
"Absage Ausfahrt",
|
||||||
None,
|
None,
|
||||||
Some(&format!("remove_trip_by_planned_event:{}", self.id)),
|
Some(&format!("remove_trip_by_event:{}", self.id)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -269,16 +284,16 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
|||||||
let rower = Registration::all_rower(db, self.trip_details_id).await;
|
let rower = Registration::all_rower(db, self.trip_details_id).await;
|
||||||
for user in rower {
|
for user in rower {
|
||||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||||
let notes = match notes {
|
let notes = match update.notes {
|
||||||
Some(n) if !n.is_empty() => n,
|
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||||
_ => ".",
|
_ => String::from(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
Notification::create(
|
Notification::create(
|
||||||
db,
|
db,
|
||||||
&user,
|
&user,
|
||||||
&format!(
|
&format!(
|
||||||
"Die Ausfahrt {} am {} um {} wurde abgesagt{}",
|
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||||
self.name, self.day, self.planned_starting_time, notes
|
self.name, self.day, self.planned_starting_time, notes
|
||||||
),
|
),
|
||||||
"Absage Ausfahrt",
|
"Absage Ausfahrt",
|
||||||
@ -292,31 +307,29 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if max_people > 0 && was_already_cancelled {
|
if update.max_people > 0 && was_already_cancelled {
|
||||||
Notification::delete_by_action(
|
Notification::delete_by_action(
|
||||||
db,
|
db,
|
||||||
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
|
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Notification::delete_by_action(
|
Notification::delete_by_action(db, &format!("remove_trip_by_event:{}", self.id)).await;
|
||||||
db,
|
|
||||||
&format!("remove_trip_by_planned_event:{}", self.id),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||||
if Registration::all_rower(db, self.trip_details_id)
|
if !Registration::all_rower(db, self.trip_details_id)
|
||||||
.await
|
.await
|
||||||
.len()
|
.is_empty()
|
||||||
> 0
|
|
||||||
{
|
{
|
||||||
return Err(
|
return Err(
|
||||||
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
|
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if Registration::all_cox(db, self.id).await.len() > 0 {
|
if !Registration::all_cox(db, self.trip_details_id)
|
||||||
|
.await
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
return Err(
|
return Err(
|
||||||
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
|
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
|
||||||
.into(),
|
.into(),
|
||||||
@ -326,23 +339,45 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
|||||||
sqlx::query!("DELETE FROM planned_event WHERE id = ?", self.id)
|
sqlx::query!("DELETE FROM planned_event WHERE id = ?", self.id)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await
|
.await
|
||||||
.unwrap(); //Okay, as PlannedEvent can only be created with proper DB backing
|
.unwrap(); //Okay, as Event can only be created with proper DB backing
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_cancelled(&self) -> bool {
|
||||||
|
self.max_people == 0
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_ics_feed(db: &SqlitePool) -> String {
|
pub async fn get_ics_feed(db: &SqlitePool) -> String {
|
||||||
let mut calendar = ICalendar::new("2.0", "ics-rs");
|
let mut calendar = ICalendar::new("2.0", "ics-rs");
|
||||||
|
|
||||||
let events = PlannedEvent::all(db).await;
|
let events = Event::all(db).await;
|
||||||
for event in events {
|
for event in events {
|
||||||
let mut vevent = Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
|
let mut vevent =
|
||||||
|
ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
|
||||||
vevent.push(DtStart::new(format!(
|
vevent.push(DtStart::new(format!(
|
||||||
"{}T{}00",
|
"{}T{}00",
|
||||||
event.day.replace('-', ""),
|
event.day.replace('-', ""),
|
||||||
event.planned_starting_time.replace(':', "")
|
event.planned_starting_time.replace(':', "")
|
||||||
)));
|
)));
|
||||||
vevent.push(Summary::new(event.name));
|
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));
|
||||||
calendar.add_event(vevent);
|
calendar.add_event(vevent);
|
||||||
}
|
}
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
@ -361,7 +396,7 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
|||||||
mod test {
|
mod test {
|
||||||
use crate::{model::tripdetails::TripDetails, testdb};
|
use crate::{model::tripdetails::TripDetails, testdb};
|
||||||
|
|
||||||
use super::PlannedEvent;
|
use super::Event;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
@ -369,8 +404,7 @@ mod test {
|
|||||||
fn test_get_day() {
|
fn test_get_day() {
|
||||||
let pool = testdb!();
|
let pool = testdb!();
|
||||||
|
|
||||||
let res =
|
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
|
||||||
assert_eq!(res.len(), 1);
|
assert_eq!(res.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,22 +414,20 @@ mod test {
|
|||||||
|
|
||||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||||
|
|
||||||
PlannedEvent::create(&pool, "new-event".into(), 2, trip_details).await;
|
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
|
||||||
|
|
||||||
let res =
|
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
|
||||||
assert_eq!(res.len(), 2);
|
assert_eq!(res.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[sqlx::test]
|
#[sqlx::test]
|
||||||
fn test_delete() {
|
fn test_delete() {
|
||||||
let pool = testdb!();
|
let pool = testdb!();
|
||||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||||
|
|
||||||
planned_event.delete(&pool).await.unwrap();
|
planned_event.delete(&pool).await.unwrap();
|
||||||
|
|
||||||
let res =
|
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
|
||||||
assert_eq!(res.len(), 0);
|
assert_eq!(res.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,7 +435,7 @@ mod test {
|
|||||||
fn test_ics() {
|
fn test_ics() {
|
||||||
let pool = testdb!();
|
let pool = testdb!();
|
||||||
|
|
||||||
let actual = PlannedEvent::get_ics_feed(&pool).await;
|
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);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -104,6 +104,7 @@ pub enum LogbookUpdateError {
|
|||||||
SteeringPersonNotInRowers,
|
SteeringPersonNotInRowers,
|
||||||
UserNotAllowedToUseBoat,
|
UserNotAllowedToUseBoat,
|
||||||
OnlyAllowedToEndTripsEndingToday,
|
OnlyAllowedToEndTripsEndingToday,
|
||||||
|
TooFast(i64, i64),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
@ -118,7 +119,7 @@ pub enum LogbookCreateError {
|
|||||||
BoatLocked,
|
BoatLocked,
|
||||||
BoatNotFound,
|
BoatNotFound,
|
||||||
TooManyRowers(usize, usize),
|
TooManyRowers(usize, usize),
|
||||||
RowerAlreadyOnWater(User),
|
RowerAlreadyOnWater(Box<User>),
|
||||||
RowerCreateError(i64, String),
|
RowerCreateError(i64, String),
|
||||||
ArrivalNotAfterDeparture,
|
ArrivalNotAfterDeparture,
|
||||||
SteeringPersonNotInRowers,
|
SteeringPersonNotInRowers,
|
||||||
@ -127,6 +128,7 @@ pub enum LogbookCreateError {
|
|||||||
ArrivalSetButNotRemainingTwo,
|
ArrivalSetButNotRemainingTwo,
|
||||||
OnlyAllowedToEndTripsEndingToday,
|
OnlyAllowedToEndTripsEndingToday,
|
||||||
CantChangeHandoperatableStatusForThisBoat,
|
CantChangeHandoperatableStatusForThisBoat,
|
||||||
|
TooFast(i64, i64),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<LogbookUpdateError> for LogbookCreateError {
|
impl From<LogbookUpdateError> for LogbookCreateError {
|
||||||
@ -150,6 +152,7 @@ impl From<LogbookUpdateError> for LogbookCreateError {
|
|||||||
LogbookUpdateError::OnlyAllowedToEndTripsEndingToday => {
|
LogbookUpdateError::OnlyAllowedToEndTripsEndingToday => {
|
||||||
LogbookCreateError::OnlyAllowedToEndTripsEndingToday
|
LogbookCreateError::OnlyAllowedToEndTripsEndingToday
|
||||||
}
|
}
|
||||||
|
LogbookUpdateError::TooFast(km, min) => LogbookCreateError::TooFast(km, min),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -298,7 +301,7 @@ ORDER BY departure DESC
|
|||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
mut log: LogToAdd,
|
mut log: LogToAdd,
|
||||||
created_by_user: &User,
|
created_by_user: &User,
|
||||||
) -> Result<(), LogbookCreateError> {
|
) -> Result<String, LogbookCreateError> {
|
||||||
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
|
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
|
||||||
return Err(LogbookCreateError::BoatNotFound);
|
return Err(LogbookCreateError::BoatNotFound);
|
||||||
};
|
};
|
||||||
@ -351,7 +354,7 @@ ORDER BY departure DESC
|
|||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tx.commit().await.unwrap();
|
tx.commit().await.unwrap();
|
||||||
Ok(())
|
Ok(String::new())
|
||||||
}
|
}
|
||||||
Err(a) => Err(a.into()),
|
Err(a) => Err(a.into()),
|
||||||
};
|
};
|
||||||
@ -386,7 +389,7 @@ ORDER BY departure DESC
|
|||||||
let user = User::find_by_id(db, *rower as i32).await.unwrap();
|
let user = User::find_by_id(db, *rower as i32).await.unwrap();
|
||||||
|
|
||||||
if user.on_water(db).await {
|
if user.on_water(db).await {
|
||||||
return Err(LogbookCreateError::RowerAlreadyOnWater(user));
|
return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,7 +426,15 @@ ORDER BY departure DESC
|
|||||||
|
|
||||||
tx.commit().await.unwrap();
|
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)> {
|
pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
|
||||||
@ -517,6 +528,17 @@ ORDER BY departure DESC
|
|||||||
if arr.and_utc().timestamp() < dep.and_utc().timestamp() {
|
if arr.and_utc().timestamp() < dep.and_utc().timestamp() {
|
||||||
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
|
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60;
|
||||||
|
// Not possible to row < 1 min / 500 m = < 2 min / km
|
||||||
|
let possible_distance_km = duration_in_mins / 2;
|
||||||
|
if log.distance_in_km > possible_distance_km {
|
||||||
|
return Err(LogbookUpdateError::TooFast(
|
||||||
|
log.distance_in_km,
|
||||||
|
duration_in_mins,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let today = Local::now().date_naive();
|
let today = Local::now().date_naive();
|
||||||
let day_diff = today - arr.date();
|
let day_diff = today - arr.date();
|
||||||
let day_diff = day_diff.num_days();
|
let day_diff = day_diff.num_days();
|
||||||
@ -621,6 +643,7 @@ mod test {
|
|||||||
use crate::model::user::User;
|
use crate::model::user::User;
|
||||||
use crate::testdb;
|
use crate::testdb;
|
||||||
|
|
||||||
|
use chrono::Duration;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
#[sqlx::test]
|
#[sqlx::test]
|
||||||
@ -672,7 +695,7 @@ mod test {
|
|||||||
fn test_succ_create() {
|
fn test_succ_create() {
|
||||||
let pool = testdb!();
|
let pool = testdb!();
|
||||||
|
|
||||||
Logbook::create(
|
let msg = Logbook::create(
|
||||||
&pool,
|
&pool,
|
||||||
LogToAdd {
|
LogToAdd {
|
||||||
boat_id: 3,
|
boat_id: 3,
|
||||||
@ -690,7 +713,62 @@ mod test {
|
|||||||
&User::find_by_id(&pool, 4).await.unwrap(),
|
&User::find_by_id(&pool, 4).await.unwrap(),
|
||||||
)
|
)
|
||||||
.await
|
.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]
|
#[sqlx::test]
|
||||||
|
@ -20,7 +20,7 @@ impl Mail {
|
|||||||
subject: &str,
|
subject: &str,
|
||||||
body: String,
|
body: String,
|
||||||
smtp_pw: &str,
|
smtp_pw: &str,
|
||||||
) {
|
) -> Result<(), String> {
|
||||||
let mut email = Message::builder()
|
let mut email = Message::builder()
|
||||||
.from(
|
.from(
|
||||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||||
@ -45,6 +45,9 @@ impl Mail {
|
|||||||
format!("Mail not sent to {single_rec}, because it could not be parsed"),
|
format!("Mail not sent to {single_rec}, because it could not be parsed"),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
return Err(format!(
|
||||||
|
"Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist."
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,6 +67,8 @@ impl Mail {
|
|||||||
|
|
||||||
// Send the email
|
// Send the email
|
||||||
mailer.send(&email).unwrap();
|
mailer.send(&email).unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
|
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
use waterlevel::WaterlevelDay;
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
|
event::{Event, EventWithUserAndTriptype},
|
||||||
trip::{Trip, TripWithUserAndType},
|
trip::{Trip, TripWithUserAndType},
|
||||||
waterlevel::Waterlevel,
|
waterlevel::Waterlevel,
|
||||||
weather::Weather,
|
weather::Weather,
|
||||||
@ -13,6 +14,7 @@ pub mod boat;
|
|||||||
pub mod boatdamage;
|
pub mod boatdamage;
|
||||||
pub mod boathouse;
|
pub mod boathouse;
|
||||||
pub mod boatreservation;
|
pub mod boatreservation;
|
||||||
|
pub mod event;
|
||||||
pub mod family;
|
pub mod family;
|
||||||
pub mod location;
|
pub mod location;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
@ -20,10 +22,11 @@ pub mod logbook;
|
|||||||
pub mod logtype;
|
pub mod logtype;
|
||||||
pub mod mail;
|
pub mod mail;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
pub mod planned_event;
|
|
||||||
pub mod role;
|
pub mod role;
|
||||||
pub mod rower;
|
pub mod rower;
|
||||||
pub mod stat;
|
pub mod stat;
|
||||||
|
pub mod trailer;
|
||||||
|
pub mod trailerreservation;
|
||||||
pub mod trip;
|
pub mod trip;
|
||||||
pub mod tripdetails;
|
pub mod tripdetails;
|
||||||
pub mod triptype;
|
pub mod triptype;
|
||||||
@ -35,10 +38,10 @@ pub mod weather;
|
|||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct Day {
|
pub struct Day {
|
||||||
day: NaiveDate,
|
day: NaiveDate,
|
||||||
planned_events: Vec<PlannedEventWithUserAndTriptype>,
|
events: Vec<EventWithUserAndTriptype>,
|
||||||
trips: Vec<TripWithUserAndType>,
|
trips: Vec<TripWithUserAndType>,
|
||||||
is_pinned: bool,
|
is_pinned: bool,
|
||||||
max_waterlevel: Option<i64>,
|
max_waterlevel: Option<WaterlevelDay>,
|
||||||
weather: Option<Weather>,
|
weather: Option<Weather>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +50,7 @@ impl Day {
|
|||||||
if is_pinned {
|
if is_pinned {
|
||||||
Self {
|
Self {
|
||||||
day,
|
day,
|
||||||
planned_events: PlannedEvent::get_pinned_for_day(db, day).await,
|
events: Event::get_pinned_for_day(db, day).await,
|
||||||
trips: Trip::get_pinned_for_day(db, day).await,
|
trips: Trip::get_pinned_for_day(db, day).await,
|
||||||
is_pinned,
|
is_pinned,
|
||||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||||
@ -56,7 +59,7 @@ impl Day {
|
|||||||
} else {
|
} else {
|
||||||
Self {
|
Self {
|
||||||
day,
|
day,
|
||||||
planned_events: PlannedEvent::get_for_day(db, day).await,
|
events: Event::get_for_day(db, day).await,
|
||||||
trips: Trip::get_for_day(db, day).await,
|
trips: Trip::get_for_day(db, day).await,
|
||||||
is_pinned,
|
is_pinned,
|
||||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||||
@ -67,7 +70,7 @@ impl Day {
|
|||||||
pub async fn new_guest(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
|
pub async fn new_guest(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
|
||||||
let mut day = Self::new(db, day, is_pinned).await;
|
let mut day = Self::new(db, day, is_pinned).await;
|
||||||
|
|
||||||
day.planned_events.retain(|e| e.planned_event.allow_guests);
|
day.events.retain(|e| e.event.allow_guests);
|
||||||
day.trips.retain(|t| t.trip.allow_guests);
|
day.trips.retain(|t| t.trip.allow_guests);
|
||||||
|
|
||||||
day
|
day
|
||||||
|
@ -7,7 +7,7 @@ use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
|||||||
|
|
||||||
use super::{role::Role, user::User};
|
use super::{role::Role, user::User};
|
||||||
|
|
||||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Notification {
|
pub struct Notification {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub user_id: i64,
|
pub user_id: i64,
|
||||||
@ -21,7 +21,7 @@ pub struct Notification {
|
|||||||
|
|
||||||
impl Notification {
|
impl Notification {
|
||||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||||
sqlx::query_as!(Self, "SELECT * FROM notification WHERE id like ?", id)
|
sqlx::query_as!(Self, "SELECT id, user_id, message, read_at, created_at, category, link, action_after_reading FROM notification WHERE id like ?", id)
|
||||||
.fetch_one(db)
|
.fetch_one(db)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
@ -153,7 +153,7 @@ ORDER BY read_at DESC, created_at DESC;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cox read notification about cancelled event
|
// Cox read notification about cancelled event
|
||||||
let re = Regex::new(r"^remove_trip_by_planned_event:(\d+)$").unwrap();
|
let re = Regex::new(r"^remove_trip_by_event:(\d+)$").unwrap();
|
||||||
if let Some(caps) = re.captures(action) {
|
if let Some(caps) = re.captures(action) {
|
||||||
if let Some(matched) = caps.get(1) {
|
if let Some(matched) = caps.get(1) {
|
||||||
if let Ok(number) = matched.as_str().parse::<i32>() {
|
if let Ok(number) = matched.as_str().parse::<i32>() {
|
||||||
@ -180,3 +180,116 @@ ORDER BY read_at DESC, created_at DESC;
|
|||||||
.unwrap();
|
.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -95,7 +95,7 @@ ORDER BY
|
|||||||
#[derive(FromRow, Serialize, Clone)]
|
#[derive(FromRow, Serialize, Clone)]
|
||||||
pub struct Stat {
|
pub struct Stat {
|
||||||
name: String,
|
name: String,
|
||||||
rowed_km: i32,
|
pub(crate) rowed_km: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stat {
|
impl Stat {
|
||||||
@ -195,6 +195,34 @@ ORDER BY rowed_km DESC, u.name;
|
|||||||
})
|
})
|
||||||
.collect()
|
.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)]
|
#[derive(Debug, Serialize)]
|
||||||
@ -218,7 +246,7 @@ FROM (
|
|||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
rower r ON l.id = r.logbook_id
|
rower r ON l.id = r.logbook_id
|
||||||
WHERE
|
WHERE
|
||||||
l.shipmaster = {0} OR r.rower_id = {0}
|
r.rower_id = {}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
departure_date
|
departure_date
|
||||||
) as subquery
|
) as subquery
|
||||||
|
31
src/model/trailer.rs
Normal file
31
src/model/trailer.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
233
src/model/trailerreservation.rs
Normal file
233
src/model/trailerreservation.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,8 @@ use serde::Serialize;
|
|||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
event::{Event, Registration},
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
planned_event::{PlannedEvent, Registration},
|
|
||||||
tripdetails::TripDetails,
|
tripdetails::TripDetails,
|
||||||
triptype::TripType,
|
triptype::TripType,
|
||||||
user::{CoxUser, User},
|
user::{CoxUser, User},
|
||||||
@ -34,6 +34,16 @@ pub struct TripWithUserAndType {
|
|||||||
trip_type: Option<TripType>,
|
trip_type: Option<TripType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TripUpdate<'a> {
|
||||||
|
pub cox: &'a CoxUser,
|
||||||
|
pub trip: &'a Trip,
|
||||||
|
pub max_people: i32,
|
||||||
|
pub notes: Option<&'a str>,
|
||||||
|
pub trip_type: Option<i64>, //TODO: Move to `TripType`
|
||||||
|
pub always_show: bool,
|
||||||
|
pub is_locked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl TripWithUserAndType {
|
impl TripWithUserAndType {
|
||||||
pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
|
pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
|
||||||
let mut trip_type = None;
|
let mut trip_type = None;
|
||||||
@ -123,28 +133,28 @@ WHERE trip.id=?
|
|||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cox decides to help in a planned event.
|
/// Cox decides to help in a event.
|
||||||
pub async fn new_join(
|
pub async fn new_join(
|
||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
cox: &CoxUser,
|
cox: &CoxUser,
|
||||||
planned_event: &PlannedEvent,
|
event: &Event,
|
||||||
) -> Result<(), CoxHelpError> {
|
) -> Result<(), CoxHelpError> {
|
||||||
if planned_event.is_rower_registered(db, cox).await {
|
if event.is_rower_registered(db, cox).await {
|
||||||
return Err(CoxHelpError::AlreadyRegisteredAsRower);
|
return Err(CoxHelpError::AlreadyRegisteredAsRower);
|
||||||
}
|
}
|
||||||
|
|
||||||
if planned_event.trip_details(db).await.is_locked {
|
if event.trip_details(db).await.is_locked {
|
||||||
return Err(CoxHelpError::DetailsLocked);
|
return Err(CoxHelpError::DetailsLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
if planned_event.max_people == 0 {
|
if event.max_people == 0 {
|
||||||
return Err(CoxHelpError::CanceledEvent);
|
return Err(CoxHelpError::CanceledEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
match sqlx::query!(
|
match sqlx::query!(
|
||||||
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
|
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
|
||||||
cox.id,
|
cox.id,
|
||||||
planned_event.id
|
event.id
|
||||||
)
|
)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await
|
.await
|
||||||
@ -181,19 +191,13 @@ WHERE day=?
|
|||||||
/// Cox decides to update own trip.
|
/// Cox decides to update own trip.
|
||||||
pub async fn update_own(
|
pub async fn update_own(
|
||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
cox: &CoxUser,
|
update: &TripUpdate<'_>,
|
||||||
trip: &Trip,
|
|
||||||
max_people: i32,
|
|
||||||
notes: Option<&str>,
|
|
||||||
trip_type: Option<i64>, //TODO: Move to `TripType`
|
|
||||||
always_show: bool,
|
|
||||||
is_locked: bool,
|
|
||||||
) -> Result<(), TripUpdateError> {
|
) -> Result<(), TripUpdateError> {
|
||||||
if !trip.is_trip_from_user(cox.id) {
|
if !update.trip.is_trip_from_user(update.cox.id) {
|
||||||
return Err(TripUpdateError::NotYourTrip);
|
return Err(TripUpdateError::NotYourTrip);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(trip_details_id) = trip.trip_details_id else {
|
let Some(trip_details_id) = update.trip.trip_details_id else {
|
||||||
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
|
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -202,32 +206,37 @@ WHERE day=?
|
|||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
|
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
|
||||||
max_people,
|
update.max_people,
|
||||||
notes,
|
update.notes,
|
||||||
trip_type,
|
update.trip_type,
|
||||||
always_show,
|
update.always_show,
|
||||||
is_locked,
|
update.is_locked,
|
||||||
trip_details_id
|
trip_details_id
|
||||||
)
|
)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await
|
.await
|
||||||
.unwrap(); //Okay, as trip_details can only be created with proper DB backing
|
.unwrap(); //Okay, as trip_details can only be created with proper DB backing
|
||||||
|
|
||||||
if max_people == 0 && !was_already_cancelled {
|
if update.max_people == 0 && !was_already_cancelled {
|
||||||
let rowers = TripWithUserAndType::from(db, trip.clone()).await.rower;
|
let rowers = TripWithUserAndType::from(db, update.trip.clone())
|
||||||
|
.await
|
||||||
|
.rower;
|
||||||
for user in rowers {
|
for user in rowers {
|
||||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||||
let notes = match notes {
|
let notes = match update.notes {
|
||||||
Some(n) if !n.is_empty() => n,
|
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||||
_ => ".",
|
_ => String::from(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
Notification::create(
|
Notification::create(
|
||||||
db,
|
db,
|
||||||
&user,
|
&user,
|
||||||
&format!(
|
&format!(
|
||||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt{}",
|
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {}",
|
||||||
cox.user.name, trip.day, trip.planned_starting_time, notes
|
update.cox.user.name,
|
||||||
|
update.trip.day,
|
||||||
|
update.trip.planned_starting_time,
|
||||||
|
notes
|
||||||
),
|
),
|
||||||
"Absage Ausfahrt",
|
"Absage Ausfahrt",
|
||||||
None,
|
None,
|
||||||
@ -239,9 +248,15 @@ WHERE day=?
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Notification::delete_by_action(
|
||||||
|
db,
|
||||||
|
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if max_people > 0 && was_already_cancelled {
|
if update.max_people > 0 && was_already_cancelled {
|
||||||
Notification::delete_by_action(
|
Notification::delete_by_action(
|
||||||
db,
|
db,
|
||||||
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||||
@ -265,16 +280,16 @@ WHERE day=?
|
|||||||
pub async fn delete_by_planned_event(
|
pub async fn delete_by_planned_event(
|
||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
cox: &CoxUser,
|
cox: &CoxUser,
|
||||||
planned_event: &PlannedEvent,
|
event: &Event,
|
||||||
) -> Result<(), TripHelpDeleteError> {
|
) -> Result<(), TripHelpDeleteError> {
|
||||||
if planned_event.trip_details(db).await.is_locked {
|
if event.trip_details(db).await.is_locked {
|
||||||
return Err(TripHelpDeleteError::DetailsLocked);
|
return Err(TripHelpDeleteError::DetailsLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
let affected_rows = sqlx::query!(
|
let affected_rows = sqlx::query!(
|
||||||
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
|
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
|
||||||
cox.id,
|
cox.id,
|
||||||
planned_event.id
|
event.id
|
||||||
)
|
)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await
|
.await
|
||||||
@ -358,8 +373,8 @@ pub enum TripUpdateError {
|
|||||||
mod test {
|
mod test {
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
planned_event::PlannedEvent,
|
event::Event,
|
||||||
trip::TripDeleteError,
|
trip::{self, TripDeleteError},
|
||||||
tripdetails::TripDetails,
|
tripdetails::TripDetails,
|
||||||
user::{CoxUser, User},
|
user::{CoxUser, User},
|
||||||
usertrip::UserTrip,
|
usertrip::UserTrip,
|
||||||
@ -409,7 +424,7 @@ mod test {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||||
|
|
||||||
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_ok());
|
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_ok());
|
||||||
}
|
}
|
||||||
@ -425,7 +440,7 @@ mod test {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||||
|
|
||||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
||||||
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_err());
|
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_err());
|
||||||
@ -444,11 +459,17 @@ mod test {
|
|||||||
|
|
||||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||||
|
|
||||||
assert!(
|
let update = trip::TripUpdate {
|
||||||
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false)
|
cox: &cox,
|
||||||
.await
|
trip: &trip,
|
||||||
.is_ok()
|
max_people: 10,
|
||||||
);
|
notes: None,
|
||||||
|
trip_type: None,
|
||||||
|
always_show: false,
|
||||||
|
is_locked: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||||
|
|
||||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||||
assert_eq!(trip.max_people, 10);
|
assert_eq!(trip.max_people, 10);
|
||||||
@ -467,11 +488,16 @@ mod test {
|
|||||||
|
|
||||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||||
|
|
||||||
assert!(
|
let update = trip::TripUpdate {
|
||||||
Trip::update_own(&pool, &cox, &trip, 10, None, Some(1), false, false)
|
cox: &cox,
|
||||||
.await
|
trip: &trip,
|
||||||
.is_ok()
|
max_people: 10,
|
||||||
);
|
notes: None,
|
||||||
|
trip_type: Some(1),
|
||||||
|
always_show: false,
|
||||||
|
is_locked: false,
|
||||||
|
};
|
||||||
|
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||||
|
|
||||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||||
assert_eq!(trip.max_people, 10);
|
assert_eq!(trip.max_people, 10);
|
||||||
@ -491,11 +517,16 @@ mod test {
|
|||||||
|
|
||||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||||
|
|
||||||
assert!(
|
let update = trip::TripUpdate {
|
||||||
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false)
|
cox: &cox,
|
||||||
.await
|
trip: &trip,
|
||||||
.is_err()
|
max_people: 10,
|
||||||
);
|
notes: None,
|
||||||
|
trip_type: None,
|
||||||
|
always_show: false,
|
||||||
|
is_locked: false,
|
||||||
|
};
|
||||||
|
assert!(Trip::update_own(&pool, &update).await.is_err());
|
||||||
assert_eq!(trip.max_people, 1);
|
assert_eq!(trip.max_people, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,7 +541,7 @@ mod test {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||||
|
|
||||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ use sqlx::{FromRow, SqlitePool};
|
|||||||
use super::{
|
use super::{
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
trip::{Trip, TripWithUserAndType},
|
trip::{Trip, TripWithUserAndType},
|
||||||
|
triptype::TripType,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||||
@ -51,6 +52,13 @@ WHERE id like ?
|
|||||||
.ok()
|
.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(
|
pub async fn find_by_startingdatetime(
|
||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
day: String,
|
day: String,
|
||||||
@ -77,6 +85,12 @@ WHERE day = ? AND planned_starting_time = ?
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.max_people == 0 {
|
||||||
|
// Cox cancelled event, thus it's probably bad weather. Don't bother with sending
|
||||||
|
// notifications
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if Trip::find_by_trip_details(db, self.id).await.is_none() {
|
if Trip::find_by_trip_details(db, self.id).await.is_none() {
|
||||||
// This trip_details belongs to a planned_event, no need to do anything
|
// This trip_details belongs to a planned_event, no need to do anything
|
||||||
return;
|
return;
|
||||||
|
@ -4,7 +4,7 @@ use sqlx::{FromRow, SqlitePool};
|
|||||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct TripType {
|
pub struct TripType {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
name: String,
|
pub name: String,
|
||||||
desc: String,
|
desc: String,
|
||||||
question: String,
|
question: String,
|
||||||
icon: String,
|
icon: String,
|
||||||
|
@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||||
|
|
||||||
use super::{
|
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,
|
tripdetails::TripDetails, Day,
|
||||||
};
|
};
|
||||||
use crate::tera::admin::user::UserEditForm;
|
use crate::tera::admin::user::UserEditForm;
|
||||||
@ -80,7 +80,7 @@ pub enum LoginError {
|
|||||||
NotACox,
|
NotACox,
|
||||||
NotATech,
|
NotATech,
|
||||||
GuestNotAllowed,
|
GuestNotAllowed,
|
||||||
NoPasswordSet(User),
|
NoPasswordSet(Box<User>),
|
||||||
DeserializationError,
|
DeserializationError,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,12 +150,15 @@ impl User {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if self.has_role(db, "Donau Linz").await {
|
if self.has_role(db, "Donau Linz").await {
|
||||||
self.send_welcome_mail_full_member(db, mail, smtp_pw).await;
|
self.send_welcome_mail_full_member(db, mail, smtp_pw)
|
||||||
|
.await?;
|
||||||
} else if self.has_role(db, "scheckbuch").await {
|
} else if self.has_role(db, "scheckbuch").await {
|
||||||
self.send_welcome_mail_scheckbuch(db, mail, smtp_pw).await;
|
self.send_welcome_mail_scheckbuch(db, mail, smtp_pw).await?;
|
||||||
|
} else if self.has_role(db, "schnupperant").await {
|
||||||
|
self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?;
|
||||||
} else {
|
} else {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch group",
|
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group",
|
||||||
self.name
|
self.name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -169,7 +172,51 @@ impl User {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_welcome_mail_scheckbuch(&self, db: &SqlitePool, mail: &str, smtp_pw: &str) {
|
async fn send_welcome_mail_schnupper(
|
||||||
|
&self,
|
||||||
|
db: &SqlitePool,
|
||||||
|
mail: &str,
|
||||||
|
smtp_pw: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// 2 things to do:
|
||||||
|
// 1. Send mail to user
|
||||||
|
Mail::send_single(
|
||||||
|
db,
|
||||||
|
mail,
|
||||||
|
"Schnupperrudern beim ASKÖ Ruderverein Donau Linz",
|
||||||
|
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.
|
||||||
|
|
||||||
|
Liebe Grüße, Philipp", self.name),
|
||||||
|
smtp_pw,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// 2. Notify all coxes
|
||||||
|
let coxes = Role::find_by_name(db, "schnupper-betreuer").await.unwrap();
|
||||||
|
Notification::create_for_role(
|
||||||
|
db,
|
||||||
|
&coxes,
|
||||||
|
&format!(
|
||||||
|
"Liebe Schnupper-Betreuer, {} nimmt am Schnupperkurs teil.",
|
||||||
|
self.name
|
||||||
|
),
|
||||||
|
"Neue(r) Schnupperteilnehmer:in ",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_welcome_mail_scheckbuch(
|
||||||
|
&self,
|
||||||
|
db: &SqlitePool,
|
||||||
|
mail: &str,
|
||||||
|
smtp_pw: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
// 2 things to do:
|
// 2 things to do:
|
||||||
// 1. Send mail to user
|
// 1. Send mail to user
|
||||||
Mail::send_single(
|
Mail::send_single(
|
||||||
@ -190,7 +237,7 @@ Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrun
|
|||||||
Riemen- & Dollenbruch,
|
Riemen- & Dollenbruch,
|
||||||
ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||||
smtp_pw,
|
smtp_pw,
|
||||||
).await;
|
).await?;
|
||||||
|
|
||||||
// 2. Notify all coxes
|
// 2. Notify all coxes
|
||||||
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
||||||
@ -205,9 +252,16 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
|||||||
None,None
|
None,None
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_welcome_mail_full_member(&self, db: &SqlitePool, mail: &str, smtp_pw: &str) {
|
async fn send_welcome_mail_full_member(
|
||||||
|
&self,
|
||||||
|
db: &SqlitePool,
|
||||||
|
mail: &str,
|
||||||
|
smtp_pw: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
// 2 things to do:
|
// 2 things to do:
|
||||||
// 1. Send mail to user
|
// 1. Send mail to user
|
||||||
Mail::send_single(
|
Mail::send_single(
|
||||||
@ -227,12 +281,14 @@ 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.
|
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!
|
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||||
|
|
||||||
Riemen- & Dollenbruch
|
Riemen- & Dollenbruch
|
||||||
ASKÖ Ruderverein Donau Linz", self.name),
|
ASKÖ Ruderverein Donau Linz", self.name),
|
||||||
smtp_pw,
|
smtp_pw,
|
||||||
).await;
|
).await?;
|
||||||
|
|
||||||
// 2. Notify all coxes
|
// 2. Notify all coxes
|
||||||
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
||||||
@ -249,6 +305,8 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
|
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
|
||||||
@ -430,12 +488,14 @@ WHERE id like ?
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
||||||
|
let name = name.trim().to_lowercase();
|
||||||
|
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||||
FROM user
|
FROM user
|
||||||
WHERE name=?
|
WHERE lower(name)=?
|
||||||
",
|
",
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
@ -649,8 +709,8 @@ ORDER BY last_access DESC
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
|
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
|
||||||
let name = name.trim(); // just to make sure...
|
let name = name.trim().to_lowercase(); // just to make sure...
|
||||||
let Some(user) = User::find_by_name(db, name).await else {
|
let Some(user) = User::find_by_name(db, &name).await else {
|
||||||
if ![
|
if ![
|
||||||
"n-sageder",
|
"n-sageder",
|
||||||
"p-hofer",
|
"p-hofer",
|
||||||
@ -677,10 +737,11 @@ ORDER BY last_access DESC
|
|||||||
"n.sageder",
|
"n.sageder",
|
||||||
"a.almousa",
|
"a.almousa",
|
||||||
"p.hofer",
|
"p.hofer",
|
||||||
|
"philipp-hofer",
|
||||||
"d.kortschak",
|
"d.kortschak",
|
||||||
"[login]",
|
"[login]",
|
||||||
]
|
]
|
||||||
.contains(&name)
|
.contains(&name.as_str())
|
||||||
{
|
{
|
||||||
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
|
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
|
||||||
}
|
}
|
||||||
@ -706,7 +767,7 @@ ORDER BY last_access DESC
|
|||||||
Err(LoginError::InvalidAuthenticationCombo)
|
Err(LoginError::InvalidAuthenticationCombo)
|
||||||
} else {
|
} else {
|
||||||
info!("User {name} has no PW set");
|
info!("User {name} has no PW set");
|
||||||
Err(LoginError::NoPasswordSet(user))
|
Err(LoginError::NoPasswordSet(Box::new(user)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -766,7 +827,7 @@ ORDER BY last_access DESC
|
|||||||
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
|
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
|
||||||
if self.has_role(db, "scheckbuch").await {
|
if self.has_role(db, "scheckbuch").await {
|
||||||
let day = Day::new_guest(db, date, true).await;
|
let day = Day::new_guest(db, date, true).await;
|
||||||
if !day.planned_events.is_empty() {
|
if !day.events.is_empty() {
|
||||||
days.push(day);
|
days.push(day);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -790,6 +851,19 @@ ORDER BY last_access DESC
|
|||||||
6
|
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]
|
#[async_trait]
|
||||||
@ -1067,15 +1141,15 @@ impl<'r> FromRequest<'r> for VorstandUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct PlannedEventUser(pub(crate) User);
|
pub struct EventUser(pub(crate) User);
|
||||||
|
|
||||||
impl From<PlannedEventUser> for User {
|
impl From<EventUser> for User {
|
||||||
fn from(val: PlannedEventUser) -> Self {
|
fn from(val: EventUser) -> Self {
|
||||||
val.0
|
val.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for PlannedEventUser {
|
impl Deref for EventUser {
|
||||||
type Target = User;
|
type Target = User;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@ -1126,15 +1200,15 @@ impl UserWithMembershipPdf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<'r> FromRequest<'r> for PlannedEventUser {
|
impl<'r> FromRequest<'r> for EventUser {
|
||||||
type Error = LoginError;
|
type Error = LoginError;
|
||||||
|
|
||||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||||
match User::from_request(req).await {
|
match User::from_request(req).await {
|
||||||
Outcome::Success(user) => {
|
Outcome::Success(user) => {
|
||||||
if user.has_role(db, "planned_event").await {
|
if user.has_role(db, "manage_events").await {
|
||||||
Outcome::Success(PlannedEventUser(user))
|
Outcome::Success(EventUser(user))
|
||||||
} else {
|
} else {
|
||||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,7 @@ pub enum UserTripDeleteError {
|
|||||||
mod test {
|
mod test {
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
planned_event::PlannedEvent, trip::Trip, tripdetails::TripDetails, user::CoxUser,
|
event::Event, trip::Trip, tripdetails::TripDetails, user::CoxUser,
|
||||||
usertrip::UserTripError,
|
usertrip::UserTripError,
|
||||||
},
|
},
|
||||||
testdb,
|
testdb,
|
||||||
@ -240,8 +240,8 @@ mod test {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
let event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
Trip::new_join(&pool, &cox, &event).await.unwrap();
|
||||||
|
|
||||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||||
let result = UserTrip::create(&pool, &cox, &trip_details, None)
|
let result = UserTrip::create(&pool, &cox, &trip_details, None)
|
||||||
|
@ -17,6 +17,24 @@ pub struct Waterlevel {
|
|||||||
pub tumittel: i64,
|
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,
|
||||||
|
pub max: i64,
|
||||||
|
pub min: i64,
|
||||||
|
pub mittel: i64,
|
||||||
|
pub tumax: i64,
|
||||||
|
pub tumin: i64,
|
||||||
|
pub tumittel: i64,
|
||||||
|
}
|
||||||
|
|
||||||
impl Waterlevel {
|
impl Waterlevel {
|
||||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||||
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
|
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
|
||||||
@ -31,20 +49,10 @@ impl Waterlevel {
|
|||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(
|
pub async fn create(db: &mut Transaction<'_, Sqlite>, create: &Create) -> Result<(), String> {
|
||||||
db: &mut Transaction<'_, Sqlite>,
|
|
||||||
day: NaiveDate,
|
|
||||||
time: String,
|
|
||||||
max: i64,
|
|
||||||
min: i64,
|
|
||||||
mittel: i64,
|
|
||||||
tumax: i64,
|
|
||||||
tumin: i64,
|
|
||||||
tumittel: i64,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO waterlevel(day, time, max, min, mittel, tumax, tumin, tumittel) VALUES (?,?,?,?,?,?,?,?)",
|
"INSERT INTO waterlevel(day, time, max, min, mittel, tumax, tumin, tumittel) VALUES (?,?,?,?,?,?,?,?)",
|
||||||
day, time, max, min, mittel, tumax, tumin, tumittel
|
create.day, create.time, create.max, create.min, create.mittel, create.tumax, create.tumin, create.tumittel
|
||||||
)
|
)
|
||||||
.execute(db.deref_mut())
|
.execute(db.deref_mut())
|
||||||
.await
|
.await
|
||||||
@ -52,15 +60,28 @@ impl Waterlevel {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<i64> {
|
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<WaterlevelDay> {
|
||||||
sqlx::query!(
|
let waterlevel = sqlx::query_as!(
|
||||||
"SELECT MAX(mittel) as max FROM waterlevel WHERE day = ?",
|
Waterlevel,
|
||||||
|
"SELECT id, day, time, max, min, mittel, tumax, tumin, tumittel FROM waterlevel WHERE day = ? ORDER BY mittel DESC LIMIT 1",
|
||||||
day
|
day
|
||||||
)
|
)
|
||||||
.fetch_one(db)
|
.fetch_optional(db)
|
||||||
.await
|
.await.unwrap();
|
||||||
.unwrap()
|
|
||||||
.max
|
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>) {
|
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {
|
||||||
|
@ -2,7 +2,7 @@ use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::model::waterlevel::Waterlevel;
|
use crate::model::waterlevel::{self, Waterlevel};
|
||||||
|
|
||||||
pub async fn update(db: &SqlitePool) -> Result<(), String> {
|
pub async fn update(db: &SqlitePool) -> Result<(), String> {
|
||||||
let mut tx = db.begin().await.unwrap();
|
let mut tx = db.begin().await.unwrap();
|
||||||
@ -29,10 +29,18 @@ pub async fn update(db: &SqlitePool) -> Result<(), String> {
|
|||||||
let time: NaiveTime = datetime.naive_utc().time();
|
let time: NaiveTime = datetime.naive_utc().time();
|
||||||
let time_str = time.format("%H:%M").to_string();
|
let time_str = time.format("%H:%M").to_string();
|
||||||
|
|
||||||
Waterlevel::create(
|
let create = waterlevel::Create {
|
||||||
&mut tx, date, time_str, max, min, mittel, tumax, tumin, tumittel,
|
day: date,
|
||||||
)
|
time: time_str,
|
||||||
.await?
|
max,
|
||||||
|
min,
|
||||||
|
mittel,
|
||||||
|
tumax,
|
||||||
|
tumin,
|
||||||
|
tumittel,
|
||||||
|
};
|
||||||
|
|
||||||
|
Waterlevel::create(&mut tx, &create).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Save in DB
|
// 3. Save in DB
|
||||||
@ -77,25 +85,23 @@ fn fetch() -> Result<Station, String> {
|
|||||||
|
|
||||||
if let Ok(data) = forecast {
|
if let Ok(data) = forecast {
|
||||||
if data.len() == 1 {
|
if data.len() == 1 {
|
||||||
return Ok(data[0].clone());
|
Ok(data[0].clone())
|
||||||
} else {
|
} else {
|
||||||
return Err(format!(
|
Err(format!(
|
||||||
"Expected 1 station (Linz); got {} while fetching from {url}. Maybe the hydro data format changed?",
|
"Expected 1 station (Linz); got {} while fetching from {url}. Maybe the hydro data format changed?",
|
||||||
data.len()
|
data.len()
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(format!(
|
Err(format!(
|
||||||
"Failed to parse the json received by {url}: {}",
|
"Failed to parse the json received by {url}: {}",
|
||||||
forecast.err().unwrap()
|
forecast.err().unwrap()
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => Err(format!(
|
||||||
return Err(format!(
|
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
||||||
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
)),
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,18 +103,16 @@ fn fetch(api_key: &str) -> Result<Data, String> {
|
|||||||
let data: Result<Data, _> = response.into_json();
|
let data: Result<Data, _> = response.into_json();
|
||||||
|
|
||||||
if let Ok(data) = data {
|
if let Ok(data) = data {
|
||||||
return Ok(data);
|
Ok(data)
|
||||||
} else {
|
} else {
|
||||||
return Err(format!(
|
Err(format!(
|
||||||
"Failed to parse the json received by {url}: {}",
|
"Failed to parse the json received by {url}: {}",
|
||||||
data.err().unwrap()
|
data.err().unwrap()
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => Err(format!(
|
||||||
return Err(format!(
|
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
||||||
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
)),
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,14 @@ use serde::Serialize;
|
|||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
planned_event::PlannedEvent,
|
event::{self, Event},
|
||||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||||
user::PlannedEventUser,
|
user::EventUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
||||||
#[derive(FromForm, Serialize)]
|
#[derive(FromForm, Serialize)]
|
||||||
struct AddPlannedEventForm<'r> {
|
struct AddEventForm<'r> {
|
||||||
name: &'r str,
|
name: &'r str,
|
||||||
planned_amount_cox: i32,
|
planned_amount_cox: i32,
|
||||||
tripdetails: TripDetailsToAdd<'r>,
|
tripdetails: TripDetailsToAdd<'r>,
|
||||||
@ -24,8 +24,8 @@ struct AddPlannedEventForm<'r> {
|
|||||||
#[post("/planned-event", data = "<data>")]
|
#[post("/planned-event", data = "<data>")]
|
||||||
async fn create(
|
async fn create(
|
||||||
db: &State<SqlitePool>,
|
db: &State<SqlitePool>,
|
||||||
data: Form<AddPlannedEventForm<'_>>,
|
data: Form<AddEventForm<'_>>,
|
||||||
_admin: PlannedEventUser,
|
_admin: EventUser,
|
||||||
) -> Flash<Redirect> {
|
) -> Flash<Redirect> {
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
|
|
||||||
@ -34,14 +34,14 @@ async fn create(
|
|||||||
//just created
|
//just created
|
||||||
//the object
|
//the object
|
||||||
|
|
||||||
PlannedEvent::create(db, data.name, data.planned_amount_cox, trip_details).await;
|
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await;
|
||||||
|
|
||||||
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
|
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm, Debug)]
|
||||||
struct UpdatePlannedEventForm<'r> {
|
struct UpdateEventForm<'r> {
|
||||||
id: i64,
|
id: i64,
|
||||||
name: &'r str,
|
name: &'r str,
|
||||||
planned_amount_cox: i32,
|
planned_amount_cox: i32,
|
||||||
@ -49,27 +49,27 @@ struct UpdatePlannedEventForm<'r> {
|
|||||||
notes: Option<&'r str>,
|
notes: Option<&'r str>,
|
||||||
always_show: bool,
|
always_show: bool,
|
||||||
is_locked: bool,
|
is_locked: bool,
|
||||||
|
trip_type: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/planned-event", data = "<data>")]
|
#[put("/planned-event", data = "<data>")]
|
||||||
async fn update(
|
async fn update(
|
||||||
db: &State<SqlitePool>,
|
db: &State<SqlitePool>,
|
||||||
data: Form<UpdatePlannedEventForm<'_>>,
|
data: Form<UpdateEventForm<'_>>,
|
||||||
_admin: PlannedEventUser,
|
_admin: EventUser,
|
||||||
) -> Flash<Redirect> {
|
) -> Flash<Redirect> {
|
||||||
match PlannedEvent::find_by_id(db, data.id).await {
|
let update = 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 {
|
||||||
Some(planned_event) => {
|
Some(planned_event) => {
|
||||||
planned_event
|
planned_event.update(db, &update).await;
|
||||||
.update(
|
|
||||||
db,
|
|
||||||
data.name,
|
|
||||||
data.planned_amount_cox,
|
|
||||||
data.max_people,
|
|
||||||
data.notes,
|
|
||||||
data.always_show,
|
|
||||||
data.is_locked,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
|
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
|
||||||
}
|
}
|
||||||
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
|
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
|
||||||
@ -77,9 +77,9 @@ async fn update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/planned-event/<id>/delete")]
|
#[get("/planned-event/<id>/delete")]
|
||||||
async fn delete(db: &State<SqlitePool>, id: i64, _admin: PlannedEventUser) -> Flash<Redirect> {
|
async fn delete(db: &State<SqlitePool>, id: i64, _admin: EventUser) -> Flash<Redirect> {
|
||||||
let Some(event) = PlannedEvent::find_by_id(db, id).await else {
|
let Some(event) = Event::find_by_id(db, id).await else {
|
||||||
return Flash::error(Redirect::to("/planned"), "PlannedEvent does not exist");
|
return Flash::error(Redirect::to("/planned"), "Event does not exist");
|
||||||
};
|
};
|
||||||
|
|
||||||
match event.delete(db).await {
|
match event.delete(db).await {
|
||||||
@ -107,7 +107,7 @@ mod test {
|
|||||||
fn test_delete() {
|
fn test_delete() {
|
||||||
let db = testdb!();
|
let db = testdb!();
|
||||||
|
|
||||||
let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap();
|
let _ = Event::find_by_id(&db, 1).await.unwrap();
|
||||||
|
|
||||||
let rocket = rocket::build().manage(db.clone());
|
let rocket = rocket::build().manage(db.clone());
|
||||||
let rocket = crate::tera::config(rocket);
|
let rocket = crate::tera::config(rocket);
|
||||||
@ -132,7 +132,7 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(flash_cookie.value(), "7:successEvent gelöscht");
|
assert_eq!(flash_cookie.value(), "7:successEvent gelöscht");
|
||||||
|
|
||||||
let event = PlannedEvent::find_by_id(&db, 1).await;
|
let event = Event::find_by_id(&db, 1).await;
|
||||||
assert_eq!(event, None);
|
assert_eq!(event, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,16 +161,16 @@ mod test {
|
|||||||
.get("_flash")
|
.get("_flash")
|
||||||
.expect("Expected flash cookie");
|
.expect("Expected flash cookie");
|
||||||
|
|
||||||
assert_eq!(flash_cookie.value(), "5:errorPlannedEvent does not exist");
|
assert_eq!(flash_cookie.value(), "5:errorEvent does not exist");
|
||||||
|
|
||||||
let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap();
|
let _ = Event::find_by_id(&db, 1).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[sqlx::test]
|
#[sqlx::test]
|
||||||
fn test_update() {
|
fn test_update() {
|
||||||
let db = testdb!();
|
let db = testdb!();
|
||||||
|
|
||||||
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
|
let event = Event::find_by_id(&db, 1).await.unwrap();
|
||||||
assert_eq!(event.notes, Some("trip_details for a planned event".into()));
|
assert_eq!(event.notes, Some("trip_details for a planned event".into()));
|
||||||
|
|
||||||
let rocket = rocket::build().manage(db.clone());
|
let rocket = rocket::build().manage(db.clone());
|
||||||
@ -202,7 +202,7 @@ mod test {
|
|||||||
"7:successEvent erfolgreich bearbeitet"
|
"7:successEvent erfolgreich bearbeitet"
|
||||||
);
|
);
|
||||||
|
|
||||||
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
|
let event = Event::find_by_id(&db, 1).await.unwrap();
|
||||||
assert_eq!(event.notes, Some("new-planned-event-text".into()));
|
assert_eq!(event.notes, Some("new-planned-event-text".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +269,7 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(flash_cookie.value(), "7:successEvent hinzugefügt");
|
assert_eq!(flash_cookie.value(), "7:successEvent hinzugefügt");
|
||||||
|
|
||||||
let event = PlannedEvent::find_by_id(&db, 2).await.unwrap();
|
let event = Event::find_by_id(&db, 2).await.unwrap();
|
||||||
assert_eq!(event.name, "my-cool-new-event");
|
assert_eq!(event.name, "my-cool-new-event");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,9 +9,9 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub mod boat;
|
pub mod boat;
|
||||||
|
pub mod event;
|
||||||
pub mod mail;
|
pub mod mail;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
pub mod planned_event;
|
|
||||||
pub mod schnupper;
|
pub mod schnupper;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
ret.append(&mut boat::routes());
|
ret.append(&mut boat::routes());
|
||||||
ret.append(&mut notification::routes());
|
ret.append(&mut notification::routes());
|
||||||
ret.append(&mut mail::routes());
|
ret.append(&mut mail::routes());
|
||||||
ret.append(&mut planned_event::routes());
|
ret.append(&mut event::routes());
|
||||||
ret.append(&mut routes![rss, show_rss, show_list, list]);
|
ret.append(&mut routes![rss, show_rss, show_list, list]);
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,6 @@ struct LoginForm<'r> {
|
|||||||
password: &'r str,
|
password: &'r str,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct UserAgent(String);
|
pub struct UserAgent(String);
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
@ -83,8 +82,8 @@ async fn login(
|
|||||||
Log::create(
|
Log::create(
|
||||||
db,
|
db,
|
||||||
format!(
|
format!(
|
||||||
"Succ login of {} with this useragent: {:?}",
|
"Succ login of {} with this useragent: {}",
|
||||||
login.name, agent
|
login.name, agent.0
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -7,9 +7,9 @@ use rocket::{
|
|||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
|
event::Event,
|
||||||
log::Log,
|
log::Log,
|
||||||
planned_event::PlannedEvent,
|
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||||
trip::{CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
|
||||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||||
user::CoxUser,
|
user::CoxUser,
|
||||||
};
|
};
|
||||||
@ -54,18 +54,16 @@ async fn update(
|
|||||||
cox: CoxUser,
|
cox: CoxUser,
|
||||||
) -> Flash<Redirect> {
|
) -> Flash<Redirect> {
|
||||||
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
|
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
|
||||||
match Trip::update_own(
|
let update = trip::TripUpdate {
|
||||||
db,
|
cox: &cox,
|
||||||
&cox,
|
trip: &trip,
|
||||||
&trip,
|
max_people: data.max_people,
|
||||||
data.max_people,
|
notes: data.notes,
|
||||||
data.notes,
|
trip_type: data.trip_type,
|
||||||
data.trip_type,
|
always_show: data.always_show,
|
||||||
data.always_show,
|
is_locked: data.is_locked,
|
||||||
data.is_locked,
|
};
|
||||||
)
|
match Trip::update_own(db, &update).await {
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => Flash::success(
|
Ok(_) => Flash::success(
|
||||||
Redirect::to("/planned"),
|
Redirect::to("/planned"),
|
||||||
"Ausfahrt erfolgreich aktualisiert.",
|
"Ausfahrt erfolgreich aktualisiert.",
|
||||||
@ -84,7 +82,7 @@ async fn update(
|
|||||||
|
|
||||||
#[get("/join/<planned_event_id>")]
|
#[get("/join/<planned_event_id>")]
|
||||||
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||||
if let Some(planned_event) = PlannedEvent::find_by_id(db, planned_event_id).await {
|
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
|
||||||
match Trip::new_join(db, &cox, &planned_event).await {
|
match Trip::new_join(db, &cox, &planned_event).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
Log::create(
|
Log::create(
|
||||||
@ -139,7 +137,7 @@ async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flas
|
|||||||
|
|
||||||
#[get("/remove/<planned_event_id>")]
|
#[get("/remove/<planned_event_id>")]
|
||||||
async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||||
if let Some(planned_event) = PlannedEvent::find_by_id(db, planned_event_id).await {
|
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
|
||||||
match Trip::delete_by_planned_event(db, &cox, &planned_event).await {
|
match Trip::delete_by_planned_event(db, &cox, &planned_event).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
Log::create(
|
Log::create(
|
||||||
|
@ -27,7 +27,7 @@ use crate::model::{
|
|||||||
user::{AdminUser, DonauLinzUser, User, UserWithDetails},
|
user::{AdminUser, DonauLinzUser, User, UserWithDetails},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct KioskCookie(String);
|
pub struct KioskCookie(());
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl<'r> FromRequest<'r> for KioskCookie {
|
impl<'r> FromRequest<'r> for KioskCookie {
|
||||||
@ -35,7 +35,7 @@ impl<'r> FromRequest<'r> for KioskCookie {
|
|||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<KioskCookie, Self::Error> {
|
async fn from_request(request: &'r Request<'_>) -> request::Outcome<KioskCookie, Self::Error> {
|
||||||
match request.cookies().get_private("kiosk") {
|
match request.cookies().get_private("kiosk") {
|
||||||
Some(cookie) => request::Outcome::Success(KioskCookie(cookie.value().to_string())),
|
Some(_) => request::Outcome::Success(KioskCookie(())),
|
||||||
None => request::Outcome::Forward(rocket::http::Status::SeeOther),
|
None => request::Outcome::Forward(rocket::http::Status::SeeOther),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,7 +212,7 @@ async fn create_logbook(
|
|||||||
)
|
)
|
||||||
.await
|
.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::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::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
|
||||||
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
|
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
|
||||||
@ -227,7 +227,7 @@ async fn create_logbook(
|
|||||||
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
|
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
|
||||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
|
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
|
||||||
|
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,6 +300,7 @@ async fn home_logbook(
|
|||||||
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
|
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
|
||||||
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
||||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||||
|
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||||
Err(e) => Flash::error(
|
Err(e) => Flash::error(
|
||||||
Redirect::to("/log"),
|
Redirect::to("/log"),
|
||||||
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
|
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use rocket::{get, http::ContentType, routes, Route, State};
|
use rocket::{get, http::ContentType, routes, Route, State};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::model::planned_event::PlannedEvent;
|
use crate::model::event::Event;
|
||||||
|
|
||||||
#[get("/cal")]
|
#[get("/cal")]
|
||||||
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||||
//TODO: add unit test once proper functionality is there
|
//TODO: add unit test once proper functionality is there
|
||||||
(ContentType::Calendar, PlannedEvent::get_ics_feed(db).await)
|
(ContentType::Calendar, Event::get_ics_feed(db).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
|
@ -39,6 +39,7 @@ mod misc;
|
|||||||
mod notification;
|
mod notification;
|
||||||
mod planned;
|
mod planned;
|
||||||
mod stat;
|
mod stat;
|
||||||
|
pub(crate) mod trailerreservation;
|
||||||
|
|
||||||
#[derive(FromForm, Debug)]
|
#[derive(FromForm, Debug)]
|
||||||
struct LoginForm<'r> {
|
struct LoginForm<'r> {
|
||||||
@ -200,6 +201,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
|||||||
.mount("/stat", stat::routes())
|
.mount("/stat", stat::routes())
|
||||||
.mount("/boatdamage", boatdamage::routes())
|
.mount("/boatdamage", boatdamage::routes())
|
||||||
.mount("/boatreservation", boatreservation::routes())
|
.mount("/boatreservation", boatreservation::routes())
|
||||||
|
.mount("/trailerreservation", trailerreservation::routes())
|
||||||
.mount("/cox", cox::routes())
|
.mount("/cox", cox::routes())
|
||||||
.mount("/admin", admin::routes())
|
.mount("/admin", admin::routes())
|
||||||
.mount("/board", board::routes())
|
.mount("/board", board::routes())
|
||||||
|
@ -26,7 +26,7 @@ async fn index(
|
|||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
|
|
||||||
if user.has_role(db, "cox").await || user.has_role(db, "planned_event").await {
|
if user.has_role(db, "cox").await || user.has_role(db, "manage_events").await {
|
||||||
let triptypes = TripType::all(db).await;
|
let triptypes = TripType::all(db).await;
|
||||||
context.insert("trip_types", &triptypes);
|
context.insert("trip_types", &triptypes);
|
||||||
}
|
}
|
||||||
|
211
src/tera/trailerreservation.rs
Normal file
211
src/tera/trailerreservation.rs
Normal 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
|
||||||
|
]
|
||||||
|
}
|
@ -36,6 +36,7 @@
|
|||||||
<a href="/stat/boats" class="px-2">Bootsauswertung</a>
|
<a href="/stat/boats" class="px-2">Bootsauswertung</a>
|
||||||
<a href="/boatdamage" class="px-2">Bootsschaden</a>
|
<a href="/boatdamage" class="px-2">Bootsschaden</a>
|
||||||
<a href="/boatreservation" class="px-2">Bootsreservierung</a>
|
<a href="/boatreservation" class="px-2">Bootsreservierung</a>
|
||||||
|
<a href="/trailerreservation" class="px-2">Hängerreservierung</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -55,5 +56,3 @@
|
|||||||
<script src="/public/main.js"></script>
|
<script src="/public/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if boatdamage.verified_at %}
|
{% if boatdamage.verified_at %}
|
||||||
<small class="block text-gray-600 dark:text-gray-100">Verifziert von {{ boatdamage.user_verified.name }} am/um {{ boatdamage.verified_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
|
<small class="block text-gray-600 dark:text-gray-100">Verifiziert von {{ boatdamage.user_verified.name }} am/um {{ boatdamage.verified_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if loggedin_user and "tech" in loggedin_user.roles and boatdamage.fixed_at %}
|
{% if loggedin_user and "tech" in loggedin_user.roles and boatdamage.fixed_at %}
|
||||||
<form action="/boatdamage/{{ boatdamage.id }}/verified"
|
<form action="/boatdamage/{{ boatdamage.id }}/verified"
|
||||||
|
@ -100,7 +100,10 @@
|
|||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<footer class="bg-primary-950 dark:bg-primary-900 text-white w-full flex justify-center p-3">
|
<footer class="bg-primary-950 dark:bg-primary-900 text-white w-full flex justify-center p-3">
|
||||||
<div class="max-w-screen-xl">
|
<div class="max-w-screen-xl w-full">
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-[#ff0000]">♥</span>
|
<span class="text-[#ff0000]">♥</span>
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endmacro new %}
|
{% endmacro new %}
|
||||||
{% macro boat_select(id="boat_id") %}
|
{% 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 %}
|
{% endmacro boat_select %}
|
||||||
{% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %}
|
{% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %}
|
||||||
{#{% if not amount_seats or amount_seats > 1 %}#}
|
{#{% if not amount_seats or amount_seats > 1 %}#}
|
||||||
@ -279,4 +279,8 @@
|
|||||||
</details>
|
</details>
|
||||||
<input class="btn btn-primary" type="submit" value="Ausfahrt beenden" />
|
<input class="btn btn-primary" type="submit" value="Ausfahrt beenden" />
|
||||||
</form>
|
</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 %}
|
{% endmacro home %}
|
||||||
|
@ -78,6 +78,8 @@
|
|||||||
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsschaden</a>
|
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsschaden</a>
|
||||||
<a href="/boatreservation"
|
<a href="/boatreservation"
|
||||||
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsreservierung</a>
|
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 %}
|
{% endif %}
|
||||||
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
|
{% 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>
|
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600 border-t">Ergo</a>
|
||||||
|
@ -90,6 +90,10 @@
|
|||||||
<a href="/boatreservation"
|
<a href="/boatreservation"
|
||||||
class="block w-100 py-2 hover:text-primary-600">Bootsreservierung</a>
|
class="block w-100 py-2 hover:text-primary-600">Bootsreservierung</a>
|
||||||
</li>
|
</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">
|
<li class="py-1">
|
||||||
<a href="/steering" class="block w-100 py-2 hover:text-primary-600">Steuerleute & Co</a>
|
<a href="/steering" class="block w-100 py-2 hover:text-primary-600">Steuerleute & Co</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -58,11 +58,11 @@
|
|||||||
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
|
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
|
||||||
{% include "includes/buttons" %}
|
{% include "includes/buttons" %}
|
||||||
{% for day in days %}
|
{% for day in days %}
|
||||||
{% set amount_trips = day.planned_events | length + day.trips | length %}
|
{% set amount_trips = day.events | length + day.trips | length %}
|
||||||
{% set_global day_cox_needed = false %}
|
{% set_global day_cox_needed = false %}
|
||||||
{% if day.planned_events | length > 0 %}
|
{% if day.events | length > 0 %}
|
||||||
{% for planned_event in day.planned_events %}
|
{% for event in day.events %}
|
||||||
{% if planned_event.cox_needed %}
|
{% if event.cox_needed %}
|
||||||
{% set_global day_cox_needed = true %}
|
{% set_global day_cox_needed = true %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -79,7 +79,7 @@
|
|||||||
{% if day.max_waterlevel %}
|
{% if day.max_waterlevel %}
|
||||||
• <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand"
|
• <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand"
|
||||||
target="_blank"
|
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 %}
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
{% if day.weather %}
|
{% if day.weather %}
|
||||||
@ -88,82 +88,82 @@
|
|||||||
</small>
|
</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
{% if day.planned_events | length > 0 or day.trips | length > 0 %}
|
{% if day.events | length > 0 or day.trips | length > 0 %}
|
||||||
<div class="grid grid-cols-1 gap-3 mb-3">
|
<div class="grid grid-cols-1 gap-3 mb-3">
|
||||||
{# --- START Events --- #}
|
{# --- START Events --- #}
|
||||||
{% if day.planned_events | length > 0 %}
|
{% if day.events | length > 0 %}
|
||||||
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
|
{% for event in day.events | sort(attribute="planned_starting_time") %}
|
||||||
{% set amount_cur_cox = planned_event.cox | length %}
|
{% set amount_cur_cox = event.cox | length %}
|
||||||
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
|
{% set amount_cox_missing = event.planned_amount_cox - amount_cur_cox %}
|
||||||
<div class="pt-2 px-3 border-t border-gray-200"
|
<div class="pt-2 px-3 border-t border-gray-200"
|
||||||
style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}">
|
style="order: {{ event.planned_starting_time | replace(from=":", to="") }}">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="mr-1">
|
<div class="mr-1">
|
||||||
{% if planned_event.max_people == 0 %}
|
{% if event.max_people == 0 %}
|
||||||
<strong class="text-[#f43f5e]">⚠ Absage
|
<strong class="text-[#f43f5e]">⚠ Absage
|
||||||
{{ planned_event.planned_starting_time }}
|
{{ event.planned_starting_time }}
|
||||||
Uhr
|
Uhr
|
||||||
</strong>
|
</strong>
|
||||||
<small class="text-[#f43f5e]">({{ planned_event.name }}
|
<small class="text-[#f43f5e]">({{ event.name }}
|
||||||
{%- if planned_event.trip_type %}
|
{%- if event.trip_type %}
|
||||||
- {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}
|
- {{ event.trip_type.icon | safe }} {{ event.trip_type.name }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
)</small>
|
)</small>
|
||||||
{% else %}
|
{% else %}
|
||||||
<strong class="text-primary-900 dark:text-white">
|
<strong class="text-primary-900 dark:text-white">
|
||||||
{{ planned_event.planned_starting_time }}
|
{{ event.planned_starting_time }}
|
||||||
Uhr
|
Uhr
|
||||||
</strong>
|
</strong>
|
||||||
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}
|
<small class="text-gray-600 dark:text-gray-100">({{ event.name }}
|
||||||
{%- if planned_event.trip_type %}
|
{%- if event.trip_type %}
|
||||||
- {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}
|
- {{ event.trip_type.icon | safe }} {{ event.trip_type.name }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
)</small>
|
)</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br />
|
<br />
|
||||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }})
|
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ event.planned_starting_time }} Uhr</strong> ({{ event.name }})
|
||||||
{% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}
|
{% if event.trip_type %}<small class='block'>{{ event.trip_type.desc }}</small>{% endif %}
|
||||||
{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}
|
{% if event.notes %}<small class='block'>{{ event.notes }}</small>{% endif %}
|
||||||
" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3">
|
" data-body="#event{{ event.trip_details_id }}" class="inline-block link-primary mr-3">
|
||||||
Details
|
Details
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right grid gap-2">
|
<div class="text-right grid gap-2">
|
||||||
{# --- START Row Buttons --- #}
|
{# --- START Row Buttons --- #}
|
||||||
{% set_global cur_user_participates = false %}
|
{% set_global cur_user_participates = false %}
|
||||||
{% for rower in planned_event.rower %}
|
{% for rower in event.rower %}
|
||||||
{% if rower.name == loggedin_user.name %}
|
{% if rower.name == loggedin_user.name %}
|
||||||
{% set_global cur_user_participates = true %}
|
{% set_global cur_user_participates = true %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if cur_user_participates %}
|
{% if cur_user_participates %}
|
||||||
<a href="/planned/remove/{{ planned_event.trip_details_id }}"
|
<a href="/planned/remove/{{ event.trip_details_id }}"
|
||||||
class="btn btn-attention btn-fw">Abmelden</a>
|
class="btn btn-attention btn-fw">Abmelden</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if planned_event.max_people > planned_event.rower | length and cur_user_participates == false %}
|
{% if event.max_people > event.rower | length and cur_user_participates == false %}
|
||||||
<a href="/planned/join/{{ planned_event.trip_details_id }}"
|
<a href="/planned/join/{{ event.trip_details_id }}"
|
||||||
class="btn btn-primary btn-fw"
|
class="btn btn-primary btn-fw"
|
||||||
{% if planned_event.trip_type %}onclick="return confirm('{{ planned_event.trip_type.question }}');"{% endif %}>Mitrudern</a>
|
{% if event.trip_type %}onclick="return confirm('{{ event.trip_type.question }}');"{% endif %}>Mitrudern</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# --- END Row Buttons --- #}
|
{# --- END Row Buttons --- #}
|
||||||
{# --- START Cox Buttons --- #}
|
{# --- START Cox Buttons --- #}
|
||||||
{% if "cox" in loggedin_user.roles %}
|
{% if "cox" in loggedin_user.roles %}
|
||||||
{% set_global cur_user_participates = false %}
|
{% set_global cur_user_participates = false %}
|
||||||
{% for cox in planned_event.cox %}
|
{% for cox in event.cox %}
|
||||||
{% if cox.name == loggedin_user.name %}
|
{% if cox.name == loggedin_user.name %}
|
||||||
{% set_global cur_user_participates = true %}
|
{% set_global cur_user_participates = true %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if cur_user_participates %}
|
{% if cur_user_participates %}
|
||||||
<a href="/cox/remove/{{ planned_event.id }}"
|
<a href="/cox/remove/{{ event.id }}"
|
||||||
class="block btn btn-attention btn-fw">
|
class="block btn btn-attention btn-fw">
|
||||||
{% include "includes/cox-icon" %}
|
{% include "includes/cox-icon" %}
|
||||||
Abmelden
|
Abmelden
|
||||||
</a>
|
</a>
|
||||||
{% elif planned_event.planned_amount_cox > 0 %}
|
{% elif event.planned_amount_cox > 0 %}
|
||||||
<a href="/cox/join/{{ planned_event.id }}"
|
<a href="/cox/join/{{ event.id }}"
|
||||||
class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw"
|
class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw"
|
||||||
{% if planned_event.trip_type %}onclick="return confirm('{{ planned_event.trip_type.question }}');"{% endif %}>
|
{% if event.trip_type %}onclick="return confirm('{{ event.trip_type.question }}');"{% endif %}>
|
||||||
{% include "includes/cox-icon" %}
|
{% include "includes/cox-icon" %}
|
||||||
Steuern
|
Steuern
|
||||||
</a>
|
</a>
|
||||||
@ -174,82 +174,83 @@
|
|||||||
</div>
|
</div>
|
||||||
{# --- START Sidebar Content --- #}
|
{# --- START Sidebar Content --- #}
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
<div id="event{{ planned_event.trip_details_id }}">
|
<div id="event{{ event.trip_details_id }}">
|
||||||
{# --- START List Coxes --- #}
|
{# --- START List Coxes --- #}
|
||||||
{% if planned_event.planned_amount_cox > 0 %}
|
{% if event.planned_amount_cox > 0 %}
|
||||||
{% if planned_event.max_people == 0 %}
|
{% if event.max_people == 0 %}
|
||||||
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }}
|
{{ macros::box(participants=event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if amount_cox_missing > 0 %}
|
{% if amount_cox_missing > 0 %}
|
||||||
{{ 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') }}
|
{{ macros::box(participants=event.cox, empty_seats=event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
|
{{ macros::box(participants=event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# --- END List Coxes --- #}
|
{# --- END List Coxes --- #}
|
||||||
{# --- START List Rowers --- #}
|
{# --- START List Rowers --- #}
|
||||||
{% set amount_cur_rower = planned_event.rower | length %}
|
{% set amount_cur_rower = event.rower | length %}
|
||||||
{% if planned_event.max_people == 0 %}
|
{% if 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) }}
|
{{ macros::box(header='Absage', bg='[#f43f5e]', participants=event.rower, trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ 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) }}
|
{{ 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) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# --- END List Rowers --- #}
|
{# --- END List Rowers --- #}
|
||||||
{% if "planned_event" in loggedin_user.roles %}
|
{% if "manage_events" in loggedin_user.roles %}
|
||||||
<form action="/planned/join/{{ planned_event.trip_details_id }}"
|
<form action="/planned/join/{{ event.trip_details_id }}" method="get" />
|
||||||
method="get" />
|
|
||||||
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
|
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
|
||||||
<input value="Gast hinzufügen"
|
<input value="Gast hinzufügen"
|
||||||
class="btn btn-primary w-full rounded-t-none-important"
|
class="btn btn-primary w-full rounded-t-none-important"
|
||||||
type="submit" />
|
type="submit" />
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if planned_event.allow_guests %}
|
{% if event.allow_guests %}
|
||||||
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
|
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "planned_event" in loggedin_user.roles %}
|
{% if "manage_events" in loggedin_user.roles %}
|
||||||
{# --- START Edit Form --- #}
|
{# --- START Edit Form --- #}
|
||||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
<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>
|
<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">
|
<form action="/admin/planned-event" method="post" class="grid gap-3">
|
||||||
<input type="hidden" name="_method" value="put" />
|
<input type="hidden" name="_method" value="put" />
|
||||||
<input type="hidden" name="id" value="{{ planned_event.id }}" />
|
<input type="hidden" name="id" value="{{ event.id }}" />
|
||||||
{{ macros::input(label='Titel', name='name', type='input', value=planned_event.name) }}
|
{{ macros::input(label='Titel', name='name', type='input', value=event.name) }}
|
||||||
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }}
|
{{ 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=planned_event.planned_amount_cox, required=true, min='0') }}
|
{{ 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=planned_event.id,checked=planned_event.always_show) }}
|
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=event.id,checked=event.always_show) }}
|
||||||
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }}
|
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=event.id,checked=event.is_locked) }}
|
||||||
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }}
|
{{ 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" />
|
<input value="Speichern" class="btn btn-primary" type="submit" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{# --- END Edit Form --- #}
|
{# --- END Edit Form --- #}
|
||||||
{# --- START Delete Btn --- #}
|
{# --- START Delete Btn --- #}
|
||||||
{% if planned_event.rower | length == 0 and amount_cur_cox == 0 %}
|
{% if event.rower | length == 0 and amount_cur_cox == 0 %}
|
||||||
<div class="text-right mt-6">
|
<div class="text-right mt-6">
|
||||||
<a href="/admin/planned-event/{{ planned_event.id }}/delete"
|
<a href="/admin/planned-event/{{ event.id }}/delete"
|
||||||
class="inline-block btn btn-alert">
|
class="inline-block btn btn-alert">
|
||||||
{% include "includes/delete-icon" %}
|
{% include "includes/delete-icon" %}
|
||||||
Termin löschen
|
Termin löschen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if planned_event.max_people == 0 %}
|
{% if event.max_people == 0 %}
|
||||||
Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
|
Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
<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>
|
<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">
|
<form action="/admin/planned-event" method="post" class="grid">
|
||||||
<input type="hidden" name="_method" value="put" />
|
<input type="hidden" name="_method" value="put" />
|
||||||
<input type="hidden" name="id" value="{{ planned_event.id }}" />
|
<input type="hidden" name="id" value="{{ 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='max_people', type='hidden', value=0) }}
|
||||||
{{ macros::input(label='', name='name', type='hidden', value=planned_event.name) }}
|
{{ macros::input(label='', name='name', type='hidden', value=event.name) }}
|
||||||
{{ macros::input(label='', name='max_people', type='hidden', value=planned_event.max_people) }}
|
{{ macros::input(label='', name='max_people', type='hidden', value=event.max_people) }}
|
||||||
{{ macros::input(label='', name='planned_amount_cox', type='hidden', value=planned_event.planned_amount_cox) }}
|
{{ macros::input(label='', name='planned_amount_cox', type='hidden', value=event.planned_amount_cox) }}
|
||||||
{{ macros::input(label='', name='notes', type='hidden', value=planned_event.notes) }}
|
{{ macros::input(label='', name='always_show', type='hidden', value=event.always_show) }}
|
||||||
{{ macros::input(label='', name='always_show', type='hidden', value=planned_event.always_show) }}
|
{{ macros::input(label='', name='is_locked', type='hidden', value=event.is_locked) }}
|
||||||
{{ macros::input(label='', name='is_locked', type='hidden', value=planned_event.is_locked) }}
|
{{ macros::input(label='', name='trip_type', type='hidden', value=event.trip_type_id) }}
|
||||||
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
|
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -280,16 +281,16 @@
|
|||||||
{{ trip.cox_name -}}
|
{{ trip.cox_name -}}
|
||||||
{% if trip.trip_type %}
|
{% if trip.trip_type %}
|
||||||
-
|
-
|
||||||
{{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }}
|
{{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}
|
||||||
{% endif -%}
|
{%- endif -%}
|
||||||
)</small>
|
)</small>
|
||||||
{% else %}
|
{% else %}
|
||||||
<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }}
|
<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }}
|
||||||
Uhr</strong>
|
Uhr</strong>
|
||||||
<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name -}}
|
<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name -}}
|
||||||
{% if trip.trip_type %}
|
{% if trip.trip_type %}
|
||||||
- {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}
|
- {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}
|
||||||
{% endif -%}
|
{%- endif -%}
|
||||||
)</small>
|
)</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br />
|
<br />
|
||||||
@ -367,7 +368,7 @@
|
|||||||
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt absagen</h3>
|
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt absagen</h3>
|
||||||
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid">
|
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid">
|
||||||
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
|
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
|
||||||
{{ macros::input(label='', name='notes', type='hidden', value=trip.notes) }}
|
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
|
||||||
{{ macros::input(label='', name='always_show', type='hidden', value=trip.always_show) }}
|
{{ macros::input(label='', name='always_show', type='hidden', value=trip.always_show) }}
|
||||||
{{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }}
|
{{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }}
|
||||||
{{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }}
|
{{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }}
|
||||||
@ -389,9 +390,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{# --- START Add Buttons --- #}
|
{# --- START Add Buttons --- #}
|
||||||
{% if "planned_event" in loggedin_user.roles or "cox" in loggedin_user.roles %}
|
{% if "manage_events" 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">
|
<div class="grid {% if "manage_events" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
|
||||||
{% if "planned_event" in loggedin_user.roles %}
|
{% if "manage_events" in loggedin_user.roles %}
|
||||||
<a href="#"
|
<a href="#"
|
||||||
data-sidebar="true"
|
data-sidebar="true"
|
||||||
data-trigger="sidebar"
|
data-trigger="sidebar"
|
||||||
@ -405,7 +406,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "cox" in loggedin_user.roles %}
|
{% 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
|
<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 "planned_event" in loggedin_user.roles %}
|
{% if "manage_events" in loggedin_user.roles %}
|
||||||
rounded-br-md
|
rounded-br-md
|
||||||
{% else %}
|
{% else %}
|
||||||
rounded-b-md
|
rounded-b-md
|
||||||
@ -425,7 +426,7 @@
|
|||||||
{% if "cox" in loggedin_user.roles %}
|
{% if "cox" in loggedin_user.roles %}
|
||||||
{% include "forms/trip" %}
|
{% include "forms/trip" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "planned_event" in loggedin_user.roles %}
|
{% if "manage_events" in loggedin_user.roles %}
|
||||||
{% include "forms/event" %}
|
{% include "forms/event" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
98
templates/trailerreservations.html.tera
Normal file
98
templates/trailerreservations.html.tera
Normal 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 %}
|
Reference in New Issue
Block a user