Compare commits
160 Commits
67c8431157
...
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 | |||
1f0de7abf4 | |||
64f3596132 | |||
3b75f38dca | |||
10f2e3016a | |||
1069e29cf0 | |||
b4967b54e9 | |||
1285c3bc28 | |||
b6c9cb0b99 | |||
29fabb04b0 | |||
830aa58e7b | |||
1b6aec8d89 | |||
1bf1cc9c68 | |||
02e1f77f65 | |||
d819462b0d | |||
387acdbd09 | |||
b36144832a | |||
fc49e6c977 | |||
b8463122d6 | |||
86db4cb2f4 | |||
82865799ce | |||
76f08905ab | |||
8dd878b492 | |||
b405cf9936 | |||
01c2f0c4a3 | |||
0318d1dfb2 | |||
261753c6b4 | |||
d0038677ca | |||
4bd91b2a7e | |||
17f4291af0 | |||
073f5aed0c | |||
3097d99e00 | |||
0eac1a66f9 | |||
f034f80794 | |||
57c9d532c8 | |||
b774acf9ae | |||
20cc085562 | |||
862ec5624a | |||
77a90a8086 | |||
ca5a932ae5 | |||
626be1c9fb | |||
7e2c185c03 | |||
65068e44a5 | |||
133a517a2e | |||
3d45310c73 | |||
e4ef1f1584 | |||
ebb4fe84bb | |||
2bf517ccd8 | |||
1908f61268 | |||
ac3301e97b | |||
18faf4a72d | |||
e3c30e010b | |||
d9aa7cafe1 | |||
97b0ce65f9 | |||
a465dfcce5 | |||
6371366a96 | |||
0952bf7878 | |||
fa0dc5b544 | |||
c3c7ecec98 | |||
a0d53366e0 | |||
b69eded21d | |||
bd68bfc668 | |||
7355d9d69b | |||
5602ad2681 | |||
6813d75db5 | |||
b4023c1ea8 | |||
45b51f4698 | |||
31fda6bee9 | |||
e728c4dbea | |||
1d9adf071f | |||
fcb4d65d32 | |||
96036b180b | |||
8c563a9c36 | |||
b70929c5ce | |||
c98f33e138 | |||
17d1ee3566 | |||
a75ba765df | |||
1503544a73 | |||
0b350d344d |
@ -25,7 +25,7 @@ jobs:
|
|||||||
cargo build
|
cargo build
|
||||||
cd frontend && npm install && npm run build
|
cd frontend && npm install && npm run build
|
||||||
- name: Frontend tests
|
- name: Frontend tests
|
||||||
run: cd frontend && npx playwright test --workers 1 --reporter line
|
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line
|
||||||
- name: Backend tests
|
- name: Backend tests
|
||||||
run: cargo test --verbose
|
run: cargo test --verbose
|
||||||
#- uses: actions/upload-artifact@v3
|
#- uses: actions/upload-artifact@v3
|
||||||
|
520
Cargo.lock
generated
520
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,9 +24,10 @@ 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"
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
openssl = { version = "0.10", features = [ "vendored" ] }
|
openssl = { version = "0.10", features = [ "vendored" ] }
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|

|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
## Frontend
|
## Frontend
|
||||||
1. `cd frontend`
|
1. `cd frontend`
|
||||||
|
@ -4,3 +4,4 @@ rss_key = "rss-key-for-ci"
|
|||||||
limits = { file = "10 MiB", data-form = "10 MiB"}
|
limits = { file = "10 MiB", data-form = "10 MiB"}
|
||||||
smtp_pw = "8kIjlLH79Ky6D3jQ"
|
smtp_pw = "8kIjlLH79Ky6D3jQ"
|
||||||
usage_log_path = "./usage.txt"
|
usage_log_path = "./usage.txt"
|
||||||
|
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"
|
||||||
|
@ -189,11 +189,6 @@ function selectBoatChange() {
|
|||||||
|
|
||||||
inputElement.value = formattedDateTime;
|
inputElement.value = formattedDateTime;
|
||||||
|
|
||||||
const distinput = <HTMLInputElement>(
|
|
||||||
document.querySelector("#distance_in_km")
|
|
||||||
);
|
|
||||||
distinput.value = "";
|
|
||||||
|
|
||||||
const destinput = <HTMLInputElement>(
|
const destinput = <HTMLInputElement>(
|
||||||
document.querySelector("#destination")
|
document.querySelector("#destination")
|
||||||
);
|
);
|
||||||
@ -759,9 +754,11 @@ function addRelationMagic(bodyElement: HTMLElement) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (option && option.value !== ""){
|
||||||
// Get distance
|
// Get distance
|
||||||
const distance = option.getAttribute("distance");
|
const distance = option.getAttribute("distance");
|
||||||
if (distance) relatedField.value = distance;
|
if (distance && relatedField.value === "") relatedField.value = distance;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -160,6 +160,7 @@ CREATE TABLE IF NOT EXISTS "notification" (
|
|||||||
"read_at" DATETIME,
|
"read_at" DATETIME,
|
||||||
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
"category" TEXT NOT NULL,
|
"category" TEXT NOT NULL,
|
||||||
|
"action_after_reading" TEXT,
|
||||||
"link" TEXT
|
"link" TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -186,3 +187,29 @@ CREATE TABLE IF NOT EXISTS "waterlevel" (
|
|||||||
"tumin" INTEGER NOT NULL,
|
"tumin" INTEGER NOT NULL,
|
||||||
"tumittel" INTEGER NOT NULL
|
"tumittel" INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "weather" (
|
||||||
|
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"day" DATE NOT NULL,
|
||||||
|
"max_temp" FLOAT NOT NULL,
|
||||||
|
"wind_gust" FLOAT NOT NULL,
|
||||||
|
"rain_mm" FLOAT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "trailer" (
|
||||||
|
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"name" text NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "trailer_reservation" (
|
||||||
|
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"trailer_id" INTEGER NOT NULL REFERENCES trailer(id),
|
||||||
|
"start_date" DATE NOT NULL,
|
||||||
|
"end_date" DATE NOT NULL,
|
||||||
|
"time_desc" TEXT NOT NULL,
|
||||||
|
"usage" TEXT NOT NULL,
|
||||||
|
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
|
||||||
|
"user_id_confirmation" INTEGER REFERENCES user(id),
|
||||||
|
"created_at" datetime not null default CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
12
src/main.rs
12
src/main.rs
@ -4,9 +4,9 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
#[cfg(feature = "rest")]
|
#[cfg(feature = "rest")]
|
||||||
use rot::rest;
|
use rot::rest;
|
||||||
use rot::scheduled;
|
|
||||||
#[cfg(feature = "rowing-tera")]
|
#[cfg(feature = "rowing-tera")]
|
||||||
use rot::tera;
|
use rot::tera;
|
||||||
|
use rot::{scheduled, tera::Config};
|
||||||
|
|
||||||
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
|
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
|
||||||
|
|
||||||
@ -27,9 +27,7 @@ async fn rocket() -> _ {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
scheduled::schedule(&db);
|
let rocket = rocket::build().manage(db.clone());
|
||||||
|
|
||||||
let rocket = rocket::build().manage(db);
|
|
||||||
|
|
||||||
#[cfg(feature = "rowing-tera")]
|
#[cfg(feature = "rowing-tera")]
|
||||||
let rocket = tera::config(rocket);
|
let rocket = tera::config(rocket);
|
||||||
@ -37,5 +35,11 @@ async fn rocket() -> _ {
|
|||||||
#[cfg(feature = "rest")]
|
#[cfg(feature = "rest")]
|
||||||
let rocket = rest::config(rocket);
|
let rocket = rest::config(rocket);
|
||||||
|
|
||||||
|
let config: Config = rocket
|
||||||
|
.figment()
|
||||||
|
.extract()
|
||||||
|
.expect("Config extraction failed");
|
||||||
|
scheduled::schedule(&db, &config);
|
||||||
|
|
||||||
rocket
|
rocket
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
@ -181,13 +181,11 @@ AND date('now') BETWEEN start_date AND end_date;",
|
|||||||
damage = BoatDamage::Locked;
|
damage = BoatDamage::Locked;
|
||||||
}
|
}
|
||||||
let cat = if boat.external {
|
let cat = if boat.external {
|
||||||
format!("Vereinsfremde Boote")
|
"Vereinsfremde Boote".to_string()
|
||||||
|
} else if boat.default_shipmaster_only_steering {
|
||||||
|
format!("{}+", boat.amount_seats - 1)
|
||||||
} else {
|
} else {
|
||||||
if boat.default_shipmaster_only_steering {
|
format!("{}x", boat.amount_seats)
|
||||||
format!("{}+", boat.amount_seats - 1)
|
|
||||||
} else {
|
|
||||||
format!("{}x", boat.amount_seats)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
res.push(BoatWithDetails {
|
res.push(BoatWithDetails {
|
||||||
|
@ -119,6 +119,10 @@ ORDER BY created_at DESC
|
|||||||
|
|
||||||
pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> {
|
pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> {
|
||||||
Log::create(db, format!("New boat damage: {boatdamage:?}")).await;
|
Log::create(db, format!("New boat damage: {boatdamage:?}")).await;
|
||||||
|
let Some(boat) = Boat::find_by_id(db, boatdamage.boat_id as i32).await else {
|
||||||
|
return Err("Boot gibt's ned".into());
|
||||||
|
};
|
||||||
|
let was_unusable_before = boat.is_locked(db).await;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)",
|
"INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)",
|
||||||
@ -131,6 +135,11 @@ ORDER BY created_at DESC
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if !was_unusable_before && boat.is_locked(db).await {
|
||||||
|
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||||
|
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
|
||||||
|
}
|
||||||
|
|
||||||
let technicals =
|
let technicals =
|
||||||
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
||||||
for technical in technicals {
|
for technical in technicals {
|
||||||
@ -144,14 +153,12 @@ ORDER BY created_at DESC
|
|||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.name,
|
.name,
|
||||||
Boat::find_by_id(db, boatdamage.boat_id as i32)
|
boat.name,
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.name,
|
|
||||||
boatdamage.desc
|
boatdamage.desc
|
||||||
),
|
),
|
||||||
"Neuer Bootsschaden angelegt",
|
"Neuer Bootsschaden angelegt",
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -172,6 +179,7 @@ ORDER BY created_at DESC
|
|||||||
),
|
),
|
||||||
"Neuer Bootsschaden angelegt",
|
"Neuer Bootsschaden angelegt",
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@ -229,7 +237,7 @@ ORDER BY created_at DESC
|
|||||||
boat.name,
|
boat.name,
|
||||||
),
|
),
|
||||||
"Bootsschaden repariert",
|
"Bootsschaden repariert",
|
||||||
None,
|
None,None
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -255,7 +263,7 @@ ORDER BY created_at DESC
|
|||||||
boat_damage.desc, boat.name,
|
boat_damage.desc, boat.name,
|
||||||
),
|
),
|
||||||
"Bootsschaden repariert",
|
"Bootsschaden repariert",
|
||||||
None,
|
None,None
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -267,35 +275,41 @@ ORDER BY created_at DESC
|
|||||||
pub async fn verified(
|
pub async fn verified(
|
||||||
&self,
|
&self,
|
||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
boat: BoatDamageVerified<'_>,
|
boat_form: BoatDamageVerified<'_>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(verifier) = User::find_by_id(db, boat.user_id_verified).await {
|
if let Some(verifier) = User::find_by_id(db, boat_form.user_id_verified).await {
|
||||||
if !verifier.has_role(db, "tech").await {
|
if !verifier.has_role(db, "tech").await {
|
||||||
Log::create(db, format!("User {verifier:?} tried to verify boat {boat:?}. The user is no tech. Manually craftted request?")).await;
|
Log::create(db, format!("User {verifier:?} tried to verify boat {boat_form:?}. The user is no tech. Manually craftted request?")).await;
|
||||||
return Err("You are not allowed to verify the boat!".into());
|
return Err("You are not allowed to verify the boat!".into());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log::create(db, format!("Someone tried to verify the boat {boat:?} with user_id={} which does not exist. Manually craftted request?", boat.user_id_verified)).await;
|
Log::create(db, format!("Someone tried to verify the boat {boat_form:?} with user_id={} which does not exist. Manually craftted request?", boat_form.user_id_verified)).await;
|
||||||
return Err("Could not find user".into());
|
return Err("Could not find user".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::create(db, format!("Verified boat damage: {boat:?}")).await;
|
let Some(boat) = Boat::find_by_id(db, self.boat_id as i32).await else {
|
||||||
|
return Err("Boot gibt's ned".into());
|
||||||
|
};
|
||||||
|
let was_unusable_before = boat.is_locked(db).await;
|
||||||
|
|
||||||
|
Log::create(db, format!("Verified boat damage: {boat_form:?}")).await;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?",
|
"UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||||
boat.desc,
|
boat_form.desc,
|
||||||
boat.user_id_verified,
|
boat_form.user_id_verified,
|
||||||
self.id
|
self.id
|
||||||
)
|
)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await.map_err(|e| e.to_string())?;
|
.await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if boat.user_id_verified != self.user_id_created as i32 {
|
if boat_form.user_id_verified != self.user_id_created as i32 {
|
||||||
let user_verified = User::find_by_id(db, boat.user_id_verified).await.unwrap();
|
let user_verified = User::find_by_id(db, boat_form.user_id_verified)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap();
|
|
||||||
|
|
||||||
if user_verified.id == self.user_id_fixed.unwrap() {
|
if user_verified.id == self.user_id_fixed.unwrap() {
|
||||||
Notification::create(
|
Notification::create(
|
||||||
@ -308,6 +322,7 @@ ORDER BY created_at DESC
|
|||||||
),
|
),
|
||||||
"Bootsschaden repariert & verifiziert",
|
"Bootsschaden repariert & verifiziert",
|
||||||
None,
|
None,
|
||||||
|
None
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
@ -321,10 +336,16 @@ ORDER BY created_at DESC
|
|||||||
),
|
),
|
||||||
"Bootsschaden verifiziert",
|
"Bootsschaden verifiziert",
|
||||||
None,
|
None,
|
||||||
|
None
|
||||||
).await;
|
).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if was_unusable_before && !boat.is_locked(db).await {
|
||||||
|
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||||
|
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, {} wurde repariert und freut sich ab sofort wieder gerudert zu werden :-)", boat.name), "Boot repariert", None, None).await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
|||||||
|
|
||||||
grouped_reservations
|
grouped_reservations
|
||||||
.entry(key)
|
.entry(key)
|
||||||
.or_insert_with(Vec::new)
|
.or_default()
|
||||||
.push(reservation);
|
.push(reservation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
|||||||
boatreservation.usage
|
boatreservation.usage
|
||||||
),
|
),
|
||||||
"Neue Bootsreservierung",
|
"Neue Bootsreservierung",
|
||||||
None,
|
None,None
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -3,33 +3,33 @@ 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};
|
||||||
|
|
||||||
use super::{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,
|
||||||
max_people: i64,
|
pub(crate) max_people: i64,
|
||||||
pub day: String,
|
pub day: String,
|
||||||
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,58 +231,153 @@ 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)
|
||||||
.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
|
||||||
|
|
||||||
|
let tripdetails = self.trip_details(db).await;
|
||||||
|
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 update.max_people == 0 && !was_already_cancelled {
|
||||||
|
let coxes = Registration::all_cox(db, self.id).await;
|
||||||
|
for user in coxes {
|
||||||
|
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||||
|
let notes = match update.notes {
|
||||||
|
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||||
|
_ => String::from(""),
|
||||||
|
};
|
||||||
|
Notification::create(
|
||||||
|
db,
|
||||||
|
&user,
|
||||||
|
&format!(
|
||||||
|
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||||
|
self.name, self.day, self.planned_starting_time, notes
|
||||||
|
),
|
||||||
|
"Absage Ausfahrt",
|
||||||
|
None,
|
||||||
|
Some(&format!("remove_trip_by_event:{}", self.id)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rower = Registration::all_rower(db, self.trip_details_id).await;
|
||||||
|
for user in rower {
|
||||||
|
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||||
|
let notes = match update.notes {
|
||||||
|
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||||
|
_ => String::from(""),
|
||||||
|
};
|
||||||
|
|
||||||
|
Notification::create(
|
||||||
|
db,
|
||||||
|
&user,
|
||||||
|
&format!(
|
||||||
|
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||||
|
self.name, self.day, self.planned_starting_time, notes
|
||||||
|
),
|
||||||
|
"Absage Ausfahrt",
|
||||||
|
None,
|
||||||
|
Some(&format!(
|
||||||
|
"remove_user_trip_with_trip_details_id:{}",
|
||||||
|
tripdetails.id
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if update.max_people > 0 && was_already_cancelled {
|
||||||
|
Notification::delete_by_action(
|
||||||
|
db,
|
||||||
|
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
Notification::delete_by_action(db, &format!("remove_trip_by_event:{}", self.id)).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(&self, db: &SqlitePool) {
|
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||||
|
if !Registration::all_rower(db, self.trip_details_id)
|
||||||
|
.await
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
return Err(
|
||||||
|
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !Registration::all_cox(db, self.trip_details_id)
|
||||||
|
.await
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
return Err(
|
||||||
|
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
@ -277,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;
|
||||||
|
|
||||||
@ -285,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,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;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,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,15 +301,15 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
if log.shipmaster_only_steering != boat.default_shipmaster_only_steering {
|
if log.shipmaster_only_steering != boat.default_shipmaster_only_steering
|
||||||
if !boat.convert_handoperated_possible {
|
&& !boat.convert_handoperated_possible
|
||||||
return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat);
|
{
|
||||||
}
|
return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat);
|
||||||
}
|
}
|
||||||
|
|
||||||
if boat.amount_seats == 1 && log.rowers.is_empty() {
|
if boat.amount_seats == 1 && log.rowers.is_empty() {
|
||||||
@ -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();
|
||||||
@ -549,6 +571,7 @@ ORDER BY departure DESC
|
|||||||
),
|
),
|
||||||
"Neuer Logbucheintrag",
|
"Neuer Logbucheintrag",
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -578,7 +601,7 @@ ORDER BY departure DESC
|
|||||||
&vorstand,
|
&vorstand,
|
||||||
&format!("'{}' hat eine mehrtägige Ausfahrt vom {} bis {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,log.departure, log.arrival, log.distance_in_km, log.destination, log.comments.clone().unwrap_or("".into())),
|
&format!("'{}' hat eine mehrtägige Ausfahrt vom {} bis {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,log.departure, log.arrival, log.distance_in_km, log.destination, log.comments.clone().unwrap_or("".into())),
|
||||||
"Mehrtägige Ausfahrt eingetragen",
|
"Mehrtägige Ausfahrt eingetragen",
|
||||||
None,
|
None,None
|
||||||
).await;
|
).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -590,7 +613,7 @@ ORDER BY departure DESC
|
|||||||
&vorstand,
|
&vorstand,
|
||||||
&format!("'{}' hat eine Ausfahrt mit externem Boot '{}' am {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,boat.name,log.departure,log.distance_in_km, log.destination, log.comments.unwrap_or("".into())),
|
&format!("'{}' hat eine Ausfahrt mit externem Boot '{}' am {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,boat.name,log.departure,log.distance_in_km, log.destination, log.comments.unwrap_or("".into())),
|
||||||
"Ausfahrt mit externem Boot eingetragen",
|
"Ausfahrt mit externem Boot eingetragen",
|
||||||
None,
|
None,None,
|
||||||
).await;
|
).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,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]
|
||||||
@ -671,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,
|
||||||
@ -689,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]
|
||||||
|
@ -14,6 +14,63 @@ use super::{family::Family, log::Log, role::Role, user::User};
|
|||||||
pub struct Mail {}
|
pub struct Mail {}
|
||||||
|
|
||||||
impl Mail {
|
impl Mail {
|
||||||
|
pub async fn send_single(
|
||||||
|
db: &SqlitePool,
|
||||||
|
to: &str,
|
||||||
|
subject: &str,
|
||||||
|
body: String,
|
||||||
|
smtp_pw: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut email = Message::builder()
|
||||||
|
.from(
|
||||||
|
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.reply_to(
|
||||||
|
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||||
|
.parse()
|
||||||
|
.unwrap());
|
||||||
|
let splitted = to.split(',');
|
||||||
|
for single_rec in splitted {
|
||||||
|
match single_rec.parse() {
|
||||||
|
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||||
|
Err(_) => {
|
||||||
|
Log::create(
|
||||||
|
db,
|
||||||
|
format!("Mail not sent to {single_rec}, because it could not be parsed"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Err(format!(
|
||||||
|
"Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let email = email
|
||||||
|
.subject(subject)
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
|
.body(body)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into());
|
||||||
|
|
||||||
|
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||||
|
.unwrap()
|
||||||
|
.credentials(creds)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
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 {
|
||||||
let mut email = Message::builder()
|
let mut email = Message::builder()
|
||||||
.from(
|
.from(
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod boat;
|
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;
|
||||||
@ -19,24 +22,27 @@ 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;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod usertrip;
|
pub mod usertrip;
|
||||||
pub mod waterlevel;
|
pub mod waterlevel;
|
||||||
|
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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Day {
|
impl Day {
|
||||||
@ -44,25 +50,27 @@ 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,
|
||||||
|
weather: Weather::find_by_day(db, day).await,
|
||||||
}
|
}
|
||||||
} 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,
|
||||||
|
weather: Weather::find_by_day(db, day).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
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,
|
||||||
@ -15,11 +16,12 @@ pub struct Notification {
|
|||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub category: String,
|
pub category: String,
|
||||||
pub link: Option<String>,
|
pub link: Option<String>,
|
||||||
|
pub action_after_reading: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
@ -30,13 +32,15 @@ impl Notification {
|
|||||||
message: &str,
|
message: &str,
|
||||||
category: &str,
|
category: &str,
|
||||||
link: Option<&str>,
|
link: Option<&str>,
|
||||||
|
action_after_reading: Option<&str>,
|
||||||
) {
|
) {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO notification(user_id, message, category, link) VALUES (?, ?, ?, ?)",
|
"INSERT INTO notification(user_id, message, category, link, action_after_reading) VALUES (?, ?, ?, ?, ?)",
|
||||||
user.id,
|
user.id,
|
||||||
message,
|
message,
|
||||||
category,
|
category,
|
||||||
link
|
link,
|
||||||
|
action_after_reading
|
||||||
)
|
)
|
||||||
.execute(db.deref_mut())
|
.execute(db.deref_mut())
|
||||||
.await
|
.await
|
||||||
@ -49,9 +53,10 @@ impl Notification {
|
|||||||
message: &str,
|
message: &str,
|
||||||
category: &str,
|
category: &str,
|
||||||
link: Option<&str>,
|
link: Option<&str>,
|
||||||
|
action_after_reading: Option<&str>,
|
||||||
) {
|
) {
|
||||||
let mut tx = db.begin().await.unwrap();
|
let mut tx = db.begin().await.unwrap();
|
||||||
Self::create_with_tx(&mut tx, user, message, category, link).await;
|
Self::create_with_tx(&mut tx, user, message, category, link, action_after_reading).await;
|
||||||
tx.commit().await.unwrap();
|
tx.commit().await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,18 +66,33 @@ impl Notification {
|
|||||||
message: &str,
|
message: &str,
|
||||||
category: &str,
|
category: &str,
|
||||||
link: Option<&str>,
|
link: Option<&str>,
|
||||||
|
action_after_reading: Option<&str>,
|
||||||
) {
|
) {
|
||||||
let users = User::all_with_role_tx(db, role).await;
|
let users = User::all_with_role_tx(db, role).await;
|
||||||
|
|
||||||
for user in users {
|
for user in users {
|
||||||
Self::create_with_tx(db, &user, message, category, link).await;
|
Self::create_with_tx(db, &user, message, category, link, action_after_reading).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_for_role(
|
||||||
|
db: &SqlitePool,
|
||||||
|
role: &Role,
|
||||||
|
message: &str,
|
||||||
|
category: &str,
|
||||||
|
link: Option<&str>,
|
||||||
|
action_after_reading: Option<&str>,
|
||||||
|
) {
|
||||||
|
let mut tx = db.begin().await.unwrap();
|
||||||
|
Self::create_for_role_tx(&mut tx, role, message, category, link, action_after_reading)
|
||||||
|
.await;
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
|
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
|
||||||
let rows = sqlx::query!(
|
let rows = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT id, user_id, message, read_at, datetime(created_at, 'localtime') as created_at, category, link FROM notification
|
SELECT id, user_id, message, read_at, datetime(created_at, 'localtime') as created_at, category, link, action_after_reading FROM notification
|
||||||
WHERE
|
WHERE
|
||||||
user_id = ?
|
user_id = ?
|
||||||
AND (
|
AND (
|
||||||
@ -101,6 +121,7 @@ ORDER BY read_at DESC, created_at DESC;
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
category: rec.category,
|
category: rec.category,
|
||||||
link: rec.link,
|
link: rec.link,
|
||||||
|
action_after_reading: rec.action_after_reading,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@ -113,5 +134,162 @@ ORDER BY read_at DESC, created_at DESC;
|
|||||||
.execute(db)
|
.execute(db)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
if let Some(action) = self.action_after_reading.as_ref() {
|
||||||
|
// User read notification about cancelled trip/event
|
||||||
|
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
|
||||||
|
if let Some(caps) = re.captures(action) {
|
||||||
|
if let Some(matched) = caps.get(1) {
|
||||||
|
if let Ok(number) = matched.as_str().parse::<i32>() {
|
||||||
|
let _ = sqlx::query!(
|
||||||
|
"DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?",
|
||||||
|
self.user_id,
|
||||||
|
number
|
||||||
|
)
|
||||||
|
.execute(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cox read notification about cancelled event
|
||||||
|
let re = Regex::new(r"^remove_trip_by_event:(\d+)$").unwrap();
|
||||||
|
if let Some(caps) = re.captures(action) {
|
||||||
|
if let Some(matched) = caps.get(1) {
|
||||||
|
if let Ok(number) = matched.as_str().parse::<i32>() {
|
||||||
|
let _ = sqlx::query!(
|
||||||
|
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
|
||||||
|
self.user_id,
|
||||||
|
number
|
||||||
|
)
|
||||||
|
.execute(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM notification WHERE action_after_reading=? and read_at is null",
|
||||||
|
action
|
||||||
|
)
|
||||||
|
.execute(db)
|
||||||
|
.await
|
||||||
|
.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;
|
||||||
@ -80,6 +90,7 @@ impl Trip {
|
|||||||
),
|
),
|
||||||
"Neue Ausfahrt zur selben Zeit",
|
"Neue Ausfahrt zur selben Zeit",
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -122,24 +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 event.max_people == 0 {
|
||||||
|
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
|
||||||
@ -176,58 +191,77 @@ 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?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||||
|
let was_already_cancelled = tripdetails.max_people == 0;
|
||||||
|
|
||||||
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 {
|
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 = if let Some(notes) = notes {
|
let notes = match update.notes {
|
||||||
format!(": {notes}")
|
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||||
} else {
|
_ => String::from(""),
|
||||||
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,
|
||||||
|
Some(&format!(
|
||||||
|
"remove_user_trip_with_trip_details_id:{}",
|
||||||
|
trip_details_id
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Notification::delete_by_action(
|
||||||
|
db,
|
||||||
|
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.max_people > 0 && was_already_cancelled {
|
||||||
|
Notification::delete_by_action(
|
||||||
|
db,
|
||||||
|
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||||
@ -246,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
|
||||||
@ -314,6 +348,7 @@ pub enum CoxHelpError {
|
|||||||
AlreadyRegisteredAsRower,
|
AlreadyRegisteredAsRower,
|
||||||
AlreadyRegisteredAsCox,
|
AlreadyRegisteredAsCox,
|
||||||
DetailsLocked,
|
DetailsLocked,
|
||||||
|
CanceledEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
@ -338,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,
|
||||||
@ -389,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());
|
||||||
}
|
}
|
||||||
@ -405,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());
|
||||||
@ -424,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);
|
||||||
@ -447,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);
|
||||||
@ -471,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,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;
|
||||||
@ -120,7 +134,7 @@ WHERE day = ? AND planned_starting_time = ?
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Notification::create(db, &user, &format!("Du hast dich als Ruderer bei der Ausfahrt von {} am {} um {} angemeldet. Bei allen Ausfahrten zu dieser Zeit sind nun alle Plätze ausgebucht. Damit noch mehr (Nicht-Steuerleute) mitfahren können, wäre es super, wenn du eine eigene Ausfahrt zur selben Zeit ausschreiben könntest.", cox.name, self.day, self.planned_starting_time), "Volle Ausfahrt", None).await;
|
Notification::create(db, &user, &format!("Du hast dich als Ruderer bei der Ausfahrt von {} am {} um {} angemeldet. Bei allen Ausfahrten zu dieser Zeit sind nun alle Plätze ausgebucht. Damit noch mehr (Nicht-Steuerleute) mitfahren können, wäre es super, wenn du eine eigene Ausfahrt zur selben Zeit ausschreiben könntest.", cox.name, self.day, self.planned_starting_time), "Volle Ausfahrt", None, None).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -14,7 +14,10 @@ use rocket::{
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||||
|
|
||||||
use super::{family::Family, log::Log, role::Role, tripdetails::TripDetails, Day};
|
use super::{
|
||||||
|
family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat,
|
||||||
|
tripdetails::TripDetails, Day,
|
||||||
|
};
|
||||||
use crate::tera::admin::user::UserEditForm;
|
use crate::tera::admin::user::UserEditForm;
|
||||||
|
|
||||||
const RENNRUDERBEITRAG: i32 = 11000;
|
const RENNRUDERBEITRAG: i32 = 11000;
|
||||||
@ -25,8 +28,9 @@ const STUDENT_OR_PUPIL: i32 = 8000;
|
|||||||
const REGULAR: i32 = 22000;
|
const REGULAR: i32 = 22000;
|
||||||
const UNTERSTUETZEND: i32 = 2500;
|
const UNTERSTUETZEND: i32 = 2500;
|
||||||
const FOERDERND: i32 = 8500;
|
const FOERDERND: i32 = 8500;
|
||||||
|
pub const SCHECKBUCH: i32 = 3000;
|
||||||
|
|
||||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash)]
|
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -47,47 +51,25 @@ pub struct User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct UserWithRolesAndNotificationCount {
|
pub struct UserWithDetails {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub user: User,
|
pub user: User,
|
||||||
pub amount_unread_notifications: i32,
|
pub amount_unread_notifications: i32,
|
||||||
pub roles: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserWithRolesAndNotificationCount {
|
|
||||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
|
||||||
Self {
|
|
||||||
roles: user.roles(db).await,
|
|
||||||
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
|
||||||
user,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct UserWithWaterStatus {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub user: User,
|
|
||||||
pub on_water: bool,
|
pub on_water: bool,
|
||||||
pub roles: Vec<String>,
|
pub roles: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserWithWaterStatus {
|
impl UserWithDetails {
|
||||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
on_water: user.on_water(db).await,
|
on_water: user.on_water(db).await,
|
||||||
roles: user.roles(db).await,
|
roles: user.roles(db).await,
|
||||||
|
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
||||||
user,
|
user,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for User {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.id == other.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginError {
|
pub enum LoginError {
|
||||||
InvalidAuthenticationCombo,
|
InvalidAuthenticationCombo,
|
||||||
@ -98,7 +80,7 @@ pub enum LoginError {
|
|||||||
NotACox,
|
NotACox,
|
||||||
NotATech,
|
NotATech,
|
||||||
GuestNotAllowed,
|
GuestNotAllowed,
|
||||||
NoPasswordSet(User),
|
NoPasswordSet(Box<User>),
|
||||||
DeserializationError,
|
DeserializationError,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +141,174 @@ impl Fee {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
|
pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
|
||||||
|
let Some(mail) = &self.mail else {
|
||||||
|
return Err(format!(
|
||||||
|
"Could not send welcome mail, because user {} has no email address",
|
||||||
|
self.name
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.has_role(db, "Donau Linz").await {
|
||||||
|
self.send_welcome_mail_full_member(db, mail, smtp_pw)
|
||||||
|
.await?;
|
||||||
|
} else if self.has_role(db, "scheckbuch").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 {
|
||||||
|
return Err(format!(
|
||||||
|
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group",
|
||||||
|
self.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::create(
|
||||||
|
db,
|
||||||
|
format!("Willkommensemail wurde an {} versandt", self.name),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
// 1. Send mail to user
|
||||||
|
Mail::send_single(
|
||||||
|
db,
|
||||||
|
mail,
|
||||||
|
"ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich",
|
||||||
|
format!(
|
||||||
|
"Hallo {0},
|
||||||
|
|
||||||
|
herzlich willkommen beim ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dass Du Dich entschieden hast, das Rudern bei uns auszuprobieren. Mit Deinem Scheckbuch kannst Du jetzt an fünf Ausfahrten teilnehmen und so diesen Sport in seiner vollen Vielfalt erleben. Falls du die {1} € noch nicht bezahlt hast, nimm diese bitte zur nächsten Ausfahrt mit (oder überweise sie auf unser Bankkonto [dieses findest du auf https://rudernlinz.at]).
|
||||||
|
|
||||||
|
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge Dich bitte mit Deinem Namen ('{0}', ohne Anführungszeichen) ein. Beim ersten Mal kannst Du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst Du Dich jederzeit für eine Ausfahrt anmelden. Wir bieten mindestens einmal pro Woche Ausfahrten an, sowohl für Anfänger als auch für Fortgeschrittene (A+F Rudern). Zusätzliche Ausfahrten werden von unseren Steuerleuten ausgeschrieben, öfters reinschauen kann sich also lohnen :-)
|
||||||
|
|
||||||
|
Nach deinen 5 Ausfahrten würden wir uns freuen, dich als Mitglied in unserem Verein begrüßen zu dürfen.
|
||||||
|
|
||||||
|
Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||||
|
|
||||||
|
Riemen- & Dollenbruch,
|
||||||
|
ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||||
|
smtp_pw,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// 2. Notify all coxes
|
||||||
|
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
||||||
|
Notification::create_for_role(
|
||||||
|
db,
|
||||||
|
&coxes,
|
||||||
|
&format!(
|
||||||
|
"Liebe Steuerberechtigte, {} hat nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
|
||||||
|
self.name
|
||||||
|
),
|
||||||
|
"Neues Scheckbuch",
|
||||||
|
None,None
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_welcome_mail_full_member(
|
||||||
|
&self,
|
||||||
|
db: &SqlitePool,
|
||||||
|
mail: &str,
|
||||||
|
smtp_pw: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// 2 things to do:
|
||||||
|
// 1. Send mail to user
|
||||||
|
Mail::send_single(
|
||||||
|
db,
|
||||||
|
mail,
|
||||||
|
"Willkommen im ASKÖ Ruderverein Donau Linz!",
|
||||||
|
format!(
|
||||||
|
"Hallo {0},
|
||||||
|
|
||||||
|
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
|
||||||
|
|
||||||
|
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at und it@rudernlinz.at jederzeit zur Verfügung.
|
||||||
|
|
||||||
|
Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH
|
||||||
|
|
||||||
|
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden.
|
||||||
|
|
||||||
|
Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst.
|
||||||
|
|
||||||
|
Außerdem haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz'. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
|
||||||
|
|
||||||
|
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||||
|
|
||||||
|
Riemen- & Dollenbruch
|
||||||
|
ASKÖ Ruderverein Donau Linz", self.name),
|
||||||
|
smtp_pw,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// 2. Notify all coxes
|
||||||
|
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
||||||
|
Notification::create_for_role(
|
||||||
|
db,
|
||||||
|
&coxes,
|
||||||
|
&format!(
|
||||||
|
"Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}",
|
||||||
|
self.member_since_date.clone().unwrap(),
|
||||||
|
self.name
|
||||||
|
),
|
||||||
|
"Neues Vereinsmitglied",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
|
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
|
||||||
if !self.has_role(db, "Donau Linz").await {
|
if !self.has_role(db, "Donau Linz").await {
|
||||||
return None;
|
return None;
|
||||||
@ -267,6 +417,18 @@ impl User {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool {
|
||||||
|
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
|
||||||
|
.fetch_one(db)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
Some(a) if a.is_empty() => false,
|
||||||
|
None => false,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
|
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;",
|
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;",
|
||||||
@ -326,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 like ?
|
WHERE lower(name)=?
|
||||||
",
|
",
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
@ -456,6 +620,7 @@ ORDER BY last_access DESC
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(db: &SqlitePool, name: &str) -> bool {
|
pub async fn create(db: &SqlitePool, name: &str) -> bool {
|
||||||
|
let name = name.trim();
|
||||||
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
|
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await
|
.await
|
||||||
@ -469,9 +634,7 @@ ORDER BY last_access DESC
|
|||||||
family_id = Some(Family::insert(db).await)
|
family_id = Some(Family::insert(db).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_with_membershippdf = UserWithMembershipPdf::from(db, self.clone()).await;
|
if !self.has_membership_pdf(db).await {
|
||||||
|
|
||||||
if user_with_membershippdf.membership_pdf.is_none() {
|
|
||||||
if let Some(membership_pdf) = data.membership_pdf {
|
if let Some(membership_pdf) = data.membership_pdf {
|
||||||
let mut stream = membership_pdf.open().await.unwrap();
|
let mut stream = membership_pdf.open().await.unwrap();
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
@ -546,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",
|
||||||
@ -574,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;
|
||||||
}
|
}
|
||||||
@ -603,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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -663,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 {
|
||||||
@ -687,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]
|
||||||
@ -964,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 {
|
||||||
@ -989,16 +1166,7 @@ pub struct UserWithRolesAndMembershipPdf {
|
|||||||
|
|
||||||
impl UserWithRolesAndMembershipPdf {
|
impl UserWithRolesAndMembershipPdf {
|
||||||
pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self {
|
pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self {
|
||||||
let membership_pdf =
|
let membership_pdf = user.has_membership_pdf(db).await;
|
||||||
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", user.id)
|
|
||||||
.fetch_one(db)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
Some(a) if a.is_empty() => false,
|
|
||||||
None => false,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
roles: user.roles(db).await,
|
roles: user.roles(db).await,
|
||||||
@ -1032,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))
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,7 @@ impl UserTrip {
|
|||||||
),
|
),
|
||||||
"Registrierung bei deiner Ausfahrt",
|
"Registrierung bei deiner Ausfahrt",
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@ -149,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,
|
||||||
@ -239,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>) {
|
||||||
|
56
src/model/weather.rs
Normal file
56
src/model/weather.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||||
|
|
||||||
|
#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||||
|
pub struct Weather {
|
||||||
|
pub id: i64,
|
||||||
|
pub day: NaiveDate,
|
||||||
|
pub max_temp: f64,
|
||||||
|
pub wind_gust: f64,
|
||||||
|
pub rain_mm: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Weather {
|
||||||
|
pub async fn find_by_day(db: &SqlitePool, day: NaiveDate) -> Option<Self> {
|
||||||
|
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
|
||||||
|
.fetch_one(db)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, day: NaiveDate) -> Option<Self> {
|
||||||
|
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
|
||||||
|
.fetch_one(db.deref_mut())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(
|
||||||
|
db: &mut Transaction<'_, Sqlite>,
|
||||||
|
day: NaiveDate,
|
||||||
|
max_temp: f64,
|
||||||
|
wind_gust: f64,
|
||||||
|
rain_mm: f64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO weather(day, max_temp, wind_gust, rain_mm) VALUES (?,?,?,?)",
|
||||||
|
day,
|
||||||
|
max_temp,
|
||||||
|
wind_gust,
|
||||||
|
rain_mm
|
||||||
|
)
|
||||||
|
.execute(db.deref_mut())
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {
|
||||||
|
sqlx::query!("DELETE FROM weather;")
|
||||||
|
.execute(db.deref_mut())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
mod waterlevel;
|
mod waterlevel;
|
||||||
|
mod weather;
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -6,11 +7,15 @@ use job_scheduler_ng::{Job, JobScheduler};
|
|||||||
use rocket::tokio::{self, task, time};
|
use rocket::tokio::{self, task, time};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
pub fn schedule(db: &SqlitePool) {
|
use crate::tera::Config;
|
||||||
|
|
||||||
|
pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||||
let db = db.clone();
|
let db = db.clone();
|
||||||
|
let openweathermap_key = config.openweathermap_key.clone();
|
||||||
|
|
||||||
tokio::task::spawn(async {
|
tokio::task::spawn(async {
|
||||||
waterlevel::update(&db).await.unwrap();
|
waterlevel::update(&db).await.unwrap();
|
||||||
|
weather::update(&db, &openweathermap_key).await.unwrap();
|
||||||
|
|
||||||
let mut sched = JobScheduler::new();
|
let mut sched = JobScheduler::new();
|
||||||
|
|
||||||
@ -22,6 +27,9 @@ pub fn schedule(db: &SqlitePool) {
|
|||||||
task::block_in_place(|| {
|
task::block_in_place(|| {
|
||||||
tokio::runtime::Handle::current().block_on(async {
|
tokio::runtime::Handle::current().block_on(async {
|
||||||
waterlevel::update(&db_clone).await.unwrap();
|
waterlevel::update(&db_clone).await.unwrap();
|
||||||
|
weather::update(&db_clone, &openweathermap_key)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
@ -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?"
|
)),
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
118
src/scheduled/weather.rs
Normal file
118
src/scheduled/weather.rs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
use chrono::DateTime;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
use crate::model::weather::Weather;
|
||||||
|
|
||||||
|
pub async fn update(db: &SqlitePool, api_key: &str) -> Result<(), String> {
|
||||||
|
let mut tx = db.begin().await.unwrap();
|
||||||
|
|
||||||
|
// 1. Delete weather data
|
||||||
|
Weather::delete_all(&mut tx).await;
|
||||||
|
|
||||||
|
// 2. Fetch
|
||||||
|
let data = fetch(api_key)?;
|
||||||
|
for d in data.daily {
|
||||||
|
let Some(date) = DateTime::from_timestamp(d.dt, 0) else {
|
||||||
|
println!("Skipping {} because convertion to datetime failed", d.dt);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let max_temp = d.temp.max;
|
||||||
|
let wind_gust = d.wind_gust;
|
||||||
|
let rain_mm = d.rain.unwrap_or(0.);
|
||||||
|
|
||||||
|
Weather::create(
|
||||||
|
&mut tx,
|
||||||
|
date.naive_utc().into(),
|
||||||
|
max_temp,
|
||||||
|
wind_gust,
|
||||||
|
rain_mm,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Save in DB
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct Data {
|
||||||
|
lat: f64,
|
||||||
|
lon: f64,
|
||||||
|
timezone: String,
|
||||||
|
timezone_offset: i64,
|
||||||
|
daily: Vec<Daily>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct Daily {
|
||||||
|
dt: i64,
|
||||||
|
sunrise: i64,
|
||||||
|
sunset: i64,
|
||||||
|
moonrise: i64,
|
||||||
|
moonset: i64,
|
||||||
|
moon_phase: f64,
|
||||||
|
summary: String,
|
||||||
|
temp: Temp,
|
||||||
|
feels_like: FeelsLike,
|
||||||
|
pressure: i64,
|
||||||
|
humidity: i64,
|
||||||
|
dew_point: f64,
|
||||||
|
wind_speed: f64,
|
||||||
|
wind_deg: i64,
|
||||||
|
wind_gust: f64,
|
||||||
|
weather: Vec<DailyWeather>,
|
||||||
|
clouds: i64,
|
||||||
|
pop: f64,
|
||||||
|
rain: Option<f64>,
|
||||||
|
uvi: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct Temp {
|
||||||
|
day: f64,
|
||||||
|
min: f64,
|
||||||
|
max: f64,
|
||||||
|
night: f64,
|
||||||
|
eve: f64,
|
||||||
|
morn: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct FeelsLike {
|
||||||
|
day: f64,
|
||||||
|
night: f64,
|
||||||
|
eve: f64,
|
||||||
|
morn: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct DailyWeather {
|
||||||
|
id: i64,
|
||||||
|
main: String,
|
||||||
|
description: String,
|
||||||
|
icon: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch(api_key: &str) -> Result<Data, String> {
|
||||||
|
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
|
||||||
|
|
||||||
|
match ureq::get(&url).call() {
|
||||||
|
Ok(response) => {
|
||||||
|
let data: Result<Data, _> = response.into_json();
|
||||||
|
|
||||||
|
if let Ok(data) = data {
|
||||||
|
Ok(data)
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Failed to parse the json received by {url}: {}",
|
||||||
|
data.err().unwrap()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err(format!(
|
||||||
|
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ use crate::model::{
|
|||||||
boat::{Boat, BoatToAdd, BoatToUpdate},
|
boat::{Boat, BoatToAdd, BoatToUpdate},
|
||||||
location::Location,
|
location::Location,
|
||||||
log::Log,
|
log::Log,
|
||||||
user::{AdminUser, User, UserWithRolesAndNotificationCount},
|
user::{AdminUser, User, UserWithDetails},
|
||||||
};
|
};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::Form,
|
form::Form,
|
||||||
@ -33,7 +33,7 @@ async fn index(
|
|||||||
context.insert("users", &users);
|
context.insert("users", &users);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(admin.user, db).await,
|
&UserWithDetails::from_user(admin.user, db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("admin/boat/index", context.into_json())
|
Template::render("admin/boat/index", context.into_json())
|
||||||
|
@ -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,13 +77,14 @@ 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> {
|
||||||
match PlannedEvent::find_by_id(db, id).await {
|
let Some(event) = Event::find_by_id(db, id).await else {
|
||||||
Some(planned_event) => {
|
return Flash::error(Redirect::to("/planned"), "Event does not exist");
|
||||||
planned_event.delete(db).await;
|
};
|
||||||
Flash::success(Redirect::to("/planned"), "Event gelöscht")
|
|
||||||
}
|
match event.delete(db).await {
|
||||||
None => Flash::error(Redirect::to("/planned"), "PlannedEvent does not exist"),
|
Ok(()) => Flash::success(Redirect::to("/planned"), "Event gelöscht"),
|
||||||
|
Err(e) => Flash::error(Redirect::to("/planned"), e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,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);
|
||||||
@ -131,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,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());
|
||||||
@ -201,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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,7 +10,7 @@ use crate::model::log::Log;
|
|||||||
use crate::model::mail::Mail;
|
use crate::model::mail::Mail;
|
||||||
use crate::model::role::Role;
|
use crate::model::role::Role;
|
||||||
use crate::model::user::AdminUser;
|
use crate::model::user::AdminUser;
|
||||||
use crate::model::user::UserWithRolesAndNotificationCount;
|
use crate::model::user::UserWithDetails;
|
||||||
use crate::tera::Config;
|
use crate::tera::Config;
|
||||||
|
|
||||||
#[get("/mail")]
|
#[get("/mail")]
|
||||||
@ -27,7 +27,7 @@ async fn index(
|
|||||||
|
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(admin.user, db).await,
|
&UserWithDetails::from_user(admin.user, db).await,
|
||||||
);
|
);
|
||||||
context.insert("roles", &roles);
|
context.insert("roles", &roles);
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,9 @@ use crate::model::{
|
|||||||
log::Log,
|
log::Log,
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{AdminUser, User, UserWithRolesAndNotificationCount},
|
user::{AdminUser, User, UserWithDetails},
|
||||||
};
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::Form,
|
form::Form,
|
||||||
get, post,
|
get, post,
|
||||||
@ -26,24 +27,41 @@ async fn index(
|
|||||||
}
|
}
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(user.user, db).await,
|
&UserWithDetails::from_user(user.user, db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let users: Vec<User> = User::all(db)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter(|u| u.last_access.is_some()) // Not useful to send notifications to people who are
|
||||||
|
// not logging in
|
||||||
|
.sorted_by_key(|u| u.name.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
context.insert("roles", &Role::all(db).await);
|
context.insert("roles", &Role::all(db).await);
|
||||||
|
context.insert("users", &users);
|
||||||
|
|
||||||
Template::render("admin/notification", context.into_json())
|
Template::render("admin/notification", context.into_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromForm, Debug)]
|
#[derive(FromForm, Debug)]
|
||||||
pub struct NotificationToSend {
|
pub struct NotificationToSendGroup {
|
||||||
pub(crate) role_id: i32,
|
pub(crate) role_id: i32,
|
||||||
pub(crate) category: String,
|
pub(crate) category: String,
|
||||||
pub(crate) message: String,
|
pub(crate) message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/notification", data = "<data>")]
|
#[derive(FromForm, Debug)]
|
||||||
async fn send(
|
pub struct NotificationToSendUser {
|
||||||
|
pub(crate) user_id: i32,
|
||||||
|
pub(crate) category: String,
|
||||||
|
pub(crate) message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/notification/group", data = "<data>")]
|
||||||
|
async fn send_group(
|
||||||
db: &State<SqlitePool>,
|
db: &State<SqlitePool>,
|
||||||
data: Form<NotificationToSend>,
|
data: Form<NotificationToSendGroup>,
|
||||||
admin: AdminUser,
|
admin: AdminUser,
|
||||||
) -> Flash<Redirect> {
|
) -> Flash<Redirect> {
|
||||||
let d = data.into_inner();
|
let d = data.into_inner();
|
||||||
@ -58,7 +76,7 @@ async fn send(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for user in User::all_with_role(db, &role).await {
|
for user in User::all_with_role(db, &role).await {
|
||||||
Notification::create(db, &user, &d.message, &d.category, None).await;
|
Notification::create(db, &user, &d.message, &d.category, None, None).await;
|
||||||
}
|
}
|
||||||
Log::create(db, "Notification successfully sent".into()).await;
|
Log::create(db, "Notification successfully sent".into()).await;
|
||||||
Flash::success(
|
Flash::success(
|
||||||
@ -67,6 +85,32 @@ async fn send(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
#[post("/notification/user", data = "<data>")]
|
||||||
routes![index, send]
|
async fn send_user(
|
||||||
|
db: &State<SqlitePool>,
|
||||||
|
data: Form<NotificationToSendUser>,
|
||||||
|
admin: AdminUser,
|
||||||
|
) -> Flash<Redirect> {
|
||||||
|
let d = data.into_inner();
|
||||||
|
Log::create(
|
||||||
|
db,
|
||||||
|
format!("{admin:?} trying to send this notification: {d:?}"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let Some(user) = User::find_by_id(db, d.user_id).await else {
|
||||||
|
return Flash::error(Redirect::to("/admin/notification"), "User gibt's ned");
|
||||||
|
};
|
||||||
|
|
||||||
|
Notification::create(db, &user, &d.message, &d.category, None, None).await;
|
||||||
|
|
||||||
|
Log::create(db, "Notification successfully sent".into()).await;
|
||||||
|
Flash::success(
|
||||||
|
Redirect::to("/admin/notification"),
|
||||||
|
"Nachricht ausgeschickt",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![index, send_user, send_group]
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,12 @@
|
|||||||
use crate::model::{
|
use crate::model::{
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{SchnupperBetreuerUser, User, UserWithRolesAndNotificationCount},
|
user::{SchnupperBetreuerUser, User, UserWithDetails},
|
||||||
};
|
};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use rocket::{
|
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||||
get,
|
|
||||||
http::Status,
|
|
||||||
request::{FlashMessage, FromRequest, Outcome},
|
|
||||||
routes, Request, Route, State,
|
|
||||||
};
|
|
||||||
use rocket_dyn_templates::{tera::Context, Template};
|
use rocket_dyn_templates::{tera::Context, Template};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
// Custom request guard to extract the Referer header
|
|
||||||
struct Referer(String);
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for Referer {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
|
||||||
match request.headers().get_one("Referer") {
|
|
||||||
Some(referer) => Outcome::Success(Referer(referer.to_string())),
|
|
||||||
None => Outcome::Error((Status::BadRequest, ())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/schnupper")]
|
#[get("/schnupper")]
|
||||||
async fn index(
|
async fn index(
|
||||||
db: &State<SqlitePool>,
|
db: &State<SqlitePool>,
|
||||||
@ -38,9 +18,9 @@ async fn index(
|
|||||||
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
|
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| async move { UserWithRolesAndNotificationCount::from_user(u, db).await })
|
.map(|u| async move { UserWithDetails::from_user(u, db).await })
|
||||||
.collect();
|
.collect();
|
||||||
let users: Vec<UserWithRolesAndNotificationCount> = join_all(user_futures).await;
|
let users: Vec<UserWithDetails> = join_all(user_futures).await;
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
if let Some(msg) = flash {
|
if let Some(msg) = flash {
|
||||||
@ -49,7 +29,7 @@ async fn index(
|
|||||||
context.insert("schnupperanten", &users);
|
context.insert("schnupperanten", &users);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
&UserWithDetails::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("admin/schnupper/index", context.into_json())
|
Template::render("admin/schnupper/index", context.into_json())
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::model::{
|
use crate::{
|
||||||
family::Family,
|
model::{
|
||||||
log::Log,
|
family::Family,
|
||||||
logbook::Logbook,
|
log::Log,
|
||||||
role::Role,
|
logbook::Logbook,
|
||||||
user::{
|
role::Role,
|
||||||
AdminUser, User, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
|
user::{
|
||||||
UserWithRolesAndNotificationCount, VorstandUser,
|
AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
|
||||||
|
VorstandUser,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
tera::Config,
|
||||||
};
|
};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
@ -67,10 +70,7 @@ async fn index(
|
|||||||
context.insert("users", &users);
|
context.insert("users", &users);
|
||||||
context.insert("roles", &roles);
|
context.insert("roles", &roles);
|
||||||
context.insert("families", &families);
|
context.insert("families", &families);
|
||||||
context.insert(
|
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||||
"loggedin_user",
|
|
||||||
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
|
||||||
);
|
|
||||||
|
|
||||||
Template::render("admin/user/index", context.into_json())
|
Template::render("admin/user/index", context.into_json())
|
||||||
}
|
}
|
||||||
@ -102,10 +102,7 @@ async fn index_admin(
|
|||||||
context.insert("users", &users);
|
context.insert("users", &users);
|
||||||
context.insert("roles", &roles);
|
context.insert("roles", &roles);
|
||||||
context.insert("families", &families);
|
context.insert("families", &families);
|
||||||
context.insert(
|
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||||
"loggedin_user",
|
|
||||||
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
|
||||||
);
|
|
||||||
|
|
||||||
Template::render("admin/user/index", context.into_json())
|
Template::render("admin/user/index", context.into_json())
|
||||||
}
|
}
|
||||||
@ -133,7 +130,7 @@ async fn fees(
|
|||||||
}
|
}
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(admin.into(), db).await,
|
&UserWithDetails::from_user(admin.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("admin/user/fees", context.into_json())
|
Template::render("admin/user/fees", context.into_json())
|
||||||
@ -153,7 +150,7 @@ async fn scheckbuch(
|
|||||||
for s in scheckbooks {
|
for s in scheckbooks {
|
||||||
scheckbooks_with_roles.push((
|
scheckbooks_with_roles.push((
|
||||||
Logbook::completed_with_user(db, &s).await,
|
Logbook::completed_with_user(db, &s).await,
|
||||||
UserWithRolesAndNotificationCount::from_user(s, db).await,
|
UserWithDetails::from_user(s, db).await,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +161,7 @@ async fn scheckbuch(
|
|||||||
}
|
}
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
&UserWithDetails::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("admin/user/scheckbuch", context.into_json())
|
Template::render("admin/user/scheckbuch", context.into_json())
|
||||||
@ -208,11 +205,36 @@ async fn fees_paid(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/user/<user>/send-welcome-mail")]
|
||||||
|
async fn send_welcome_mail(
|
||||||
|
db: &State<SqlitePool>,
|
||||||
|
_admin: AdminUser,
|
||||||
|
config: &State<Config>,
|
||||||
|
user: i32,
|
||||||
|
) -> Flash<Redirect> {
|
||||||
|
let Some(user) = User::find_by_id(db, user).await else {
|
||||||
|
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
|
||||||
|
};
|
||||||
|
|
||||||
|
match user.send_welcome_email(db, &config.smtp_pw).await {
|
||||||
|
Ok(()) => Flash::success(
|
||||||
|
Redirect::to("/admin/user"),
|
||||||
|
format!("Willkommens-Email wurde an {} versandt.", user.name),
|
||||||
|
),
|
||||||
|
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/user/<user>/reset-pw")]
|
#[get("/user/<user>/reset-pw")]
|
||||||
async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> {
|
async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||||
let user = User::find_by_id(db, user).await;
|
let user = User::find_by_id(db, user).await;
|
||||||
match user {
|
match user {
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
|
Log::create(
|
||||||
|
db,
|
||||||
|
format!("{} has resetted the pw for {}", admin.user.name, user.name),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
user.reset_pw(db).await;
|
user.reset_pw(db).await;
|
||||||
Flash::success(
|
Flash::success(
|
||||||
Redirect::to("/admin/user"),
|
Redirect::to("/admin/user"),
|
||||||
@ -338,6 +360,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
fees,
|
fees,
|
||||||
fees_paid,
|
fees_paid,
|
||||||
scheckbuch,
|
scheckbuch,
|
||||||
download_membership_pdf
|
download_membership_pdf,
|
||||||
|
send_welcome_mail
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::model::{
|
use crate::model::{
|
||||||
boat::Boat,
|
boat::Boat,
|
||||||
boathouse::Boathouse,
|
boathouse::Boathouse,
|
||||||
user::{AdminUser, UserWithRolesAndNotificationCount, VorstandUser},
|
user::{AdminUser, UserWithDetails, VorstandUser},
|
||||||
};
|
};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::Form,
|
form::Form,
|
||||||
@ -39,7 +39,7 @@ async fn index(
|
|||||||
|
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(admin.into(), db).await,
|
&UserWithDetails::from_user(admin.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("board/boathouse", context.into_json())
|
Template::render("board/boathouse", context.into_json())
|
||||||
|
@ -13,7 +13,7 @@ use crate::{
|
|||||||
model::{
|
model::{
|
||||||
boat::Boat,
|
boat::Boat,
|
||||||
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
||||||
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRolesAndNotificationCount},
|
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
|
||||||
},
|
},
|
||||||
tera::log::KioskCookie,
|
tera::log::KioskCookie,
|
||||||
};
|
};
|
||||||
@ -59,7 +59,7 @@ async fn index(
|
|||||||
context.insert("boats", &boats);
|
context.insert("boats", &boats);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
&UserWithDetails::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("boatdamages", context.into_json())
|
Template::render("boatdamages", context.into_json())
|
||||||
|
@ -15,7 +15,7 @@ use crate::{
|
|||||||
boat::Boat,
|
boat::Boat,
|
||||||
boatreservation::{BoatReservation, BoatReservationToAdd},
|
boatreservation::{BoatReservation, BoatReservationToAdd},
|
||||||
log::Log,
|
log::Log,
|
||||||
user::{DonauLinzUser, User, UserWithRolesAndNotificationCount},
|
user::{DonauLinzUser, User, UserWithDetails},
|
||||||
},
|
},
|
||||||
tera::log::KioskCookie,
|
tera::log::KioskCookie,
|
||||||
};
|
};
|
||||||
@ -75,7 +75,7 @@ async fn index(
|
|||||||
context.insert("user", &User::all(db).await);
|
context.insert("user", &User::all(db).await);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
&UserWithDetails::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("boatreservations", context.into_json())
|
Template::render("boatreservations", context.into_json())
|
||||||
@ -166,7 +166,7 @@ async fn update(
|
|||||||
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
|
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
|
||||||
return Flash::error(
|
return Flash::error(
|
||||||
Redirect::to("/boatreservation"),
|
Redirect::to("/boatreservation"),
|
||||||
format!("Not allowed to update reservation (only admins + creator do so)."),
|
"Not allowed to update reservation (only admins + creator do so).".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
@ -97,6 +95,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
|
|||||||
.await;
|
.await;
|
||||||
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
|
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
|
||||||
}
|
}
|
||||||
|
Err(CoxHelpError::CanceledEvent) => {
|
||||||
|
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
|
||||||
|
}
|
||||||
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
|
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
|
||||||
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
|
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
|
||||||
}
|
}
|
||||||
@ -136,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(
|
||||||
|
@ -18,7 +18,7 @@ use tera::Context;
|
|||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
log::Log,
|
log::Log,
|
||||||
user::{AdminUser, User, UserWithRolesAndNotificationCount},
|
user::{AdminUser, User, UserWithDetails},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -51,7 +51,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"ergo.final",
|
"ergo.final",
|
||||||
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(_user.user, db).await, thirty, dozen),
|
context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,10 +120,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
|||||||
if let Some(msg) = flash {
|
if let Some(msg) = flash {
|
||||||
context.insert("flash", &msg.into_inner());
|
context.insert("flash", &msg.into_inner());
|
||||||
}
|
}
|
||||||
context.insert(
|
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||||
"loggedin_user",
|
|
||||||
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
|
||||||
);
|
|
||||||
context.insert("users", &users);
|
context.insert("users", &users);
|
||||||
context.insert("thirty", &thirty);
|
context.insert("thirty", &thirty);
|
||||||
context.insert("dozen", &dozen);
|
context.insert("dozen", &dozen);
|
||||||
|
@ -24,12 +24,10 @@ use crate::model::{
|
|||||||
LogbookUpdateError,
|
LogbookUpdateError,
|
||||||
},
|
},
|
||||||
logtype::LogType,
|
logtype::LogType,
|
||||||
user::{
|
user::{AdminUser, DonauLinzUser, User, UserWithDetails},
|
||||||
AdminUser, DonauLinzUser, User, UserWithRolesAndNotificationCount, UserWithWaterStatus,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
||||||
@ -37,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,23 +49,27 @@ async fn index(
|
|||||||
) -> Template {
|
) -> Template {
|
||||||
let boats = Boat::for_user(db, &user).await;
|
let boats = Boat::for_user(db, &user).await;
|
||||||
|
|
||||||
let mut coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
|
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
|
||||||
User::cox(db)
|
User::cox(db)
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
.map(|user| UserWithDetails::from_user(user, db)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
coxes.retain(|u| u.roles.contains(&"Donau Linz".into()));
|
coxes.retain(|u| {
|
||||||
|
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||||
|
});
|
||||||
|
|
||||||
let mut users: Vec<UserWithWaterStatus> = futures::future::join_all(
|
let mut users: Vec<UserWithDetails> = futures::future::join_all(
|
||||||
User::all(db)
|
User::all(db)
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
.map(|user| UserWithDetails::from_user(user, db)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
users.retain(|u| u.roles.contains(&"Donau Linz".into()));
|
users.retain(|u| {
|
||||||
|
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||||
|
});
|
||||||
|
|
||||||
let logtypes = LogType::all(db).await;
|
let logtypes = LogType::all(db).await;
|
||||||
let distances = Logbook::distances(db).await;
|
let distances = Logbook::distances(db).await;
|
||||||
@ -89,7 +91,7 @@ async fn index(
|
|||||||
context.insert("logtypes", &logtypes);
|
context.insert("logtypes", &logtypes);
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
|
&UserWithDetails::from_user(user.into(), db).await,
|
||||||
);
|
);
|
||||||
context.insert("on_water", &on_water);
|
context.insert("on_water", &on_water);
|
||||||
context.insert("distances", &distances);
|
context.insert("distances", &distances);
|
||||||
@ -103,7 +105,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"log.completed",
|
"log.completed",
|
||||||
context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await),
|
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +115,7 @@ async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Te
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"log.completed",
|
"log.completed",
|
||||||
context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.user, db).await),
|
context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,20 +151,30 @@ async fn kiosk(
|
|||||||
_kiosk: KioskCookie,
|
_kiosk: KioskCookie,
|
||||||
) -> Template {
|
) -> Template {
|
||||||
let boats = Boat::all(db).await;
|
let boats = Boat::all(db).await;
|
||||||
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
|
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
|
||||||
User::cox(db)
|
User::cox(db)
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
.map(|user| UserWithDetails::from_user(user, db)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let users: Vec<UserWithWaterStatus> = futures::future::join_all(
|
|
||||||
|
coxes.retain(|u| {
|
||||||
|
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut users: Vec<UserWithDetails> = futures::future::join_all(
|
||||||
User::all(db)
|
User::all(db)
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
.map(|user| UserWithDetails::from_user(user, db)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
users.retain(|u| {
|
||||||
|
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||||
|
});
|
||||||
|
|
||||||
let logtypes = LogType::all(db).await;
|
let logtypes = LogType::all(db).await;
|
||||||
let distances = Logbook::distances(db).await;
|
let distances = Logbook::distances(db).await;
|
||||||
|
|
||||||
@ -200,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"),
|
||||||
@ -215,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.")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +262,7 @@ async fn create_kiosk(
|
|||||||
} else if let Some(shipmaster) = data.shipmaster {
|
} else if let Some(shipmaster) = data.shipmaster {
|
||||||
User::find_by_id(db, shipmaster as i32).await.unwrap()
|
User::find_by_id(db, shipmaster as i32).await.unwrap()
|
||||||
} else {
|
} else {
|
||||||
let Some(rower) = data.rowers.get(0) else {
|
let Some(rower) = data.rowers.first() else {
|
||||||
return Flash::error(
|
return Flash::error(
|
||||||
Redirect::to("/log"),
|
Redirect::to("/log"),
|
||||||
"Ausfahrt ohne Benutzer kann nicht angelegt werden.",
|
"Ausfahrt ohne Benutzer kann nicht angelegt werden.",
|
||||||
@ -288,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> {
|
||||||
|
@ -21,9 +21,10 @@ use sqlx::SqlitePool;
|
|||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
|
logbook::Logbook,
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{User, UserWithRolesAndNotificationCount},
|
user::{User, UserWithDetails, SCHECKBUCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod admin;
|
pub(crate) mod admin;
|
||||||
@ -38,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> {
|
||||||
@ -52,14 +54,29 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
|||||||
context.insert("flash", &msg.into_inner());
|
context.insert("flash", &msg.into_inner());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.has_role(db, "scheckbuch").await {
|
||||||
|
let last_trips = Logbook::completed_with_user(db, &user).await;
|
||||||
|
context.insert("last_trips", &last_trips);
|
||||||
|
}
|
||||||
|
|
||||||
context.insert("notifications", &Notification::for_user(db, &user).await);
|
context.insert("notifications", &Notification::for_user(db, &user).await);
|
||||||
context.insert(
|
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||||
"loggedin_user",
|
context.insert("costs_scheckbuch", &SCHECKBUCH);
|
||||||
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
|
||||||
);
|
|
||||||
Template::render("index", context.into_json())
|
Template::render("index", context.into_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/impressum")]
|
||||||
|
async fn impressum(db: &State<SqlitePool>, user: Option<User>) -> Template {
|
||||||
|
let mut context = Context::new();
|
||||||
|
|
||||||
|
if let Some(user) = user {
|
||||||
|
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
Template::render("impressum", context.into_json())
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/steering")]
|
#[get("/steering")]
|
||||||
async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
@ -78,10 +95,7 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
|
|||||||
context.insert("coxes", &coxes);
|
context.insert("coxes", &coxes);
|
||||||
context.insert("bootskundige", &bootskundige);
|
context.insert("bootskundige", &bootskundige);
|
||||||
|
|
||||||
context.insert(
|
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||||
"loggedin_user",
|
|
||||||
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
|
||||||
);
|
|
||||||
Template::render("steering", context.into_json())
|
Template::render("steering", context.into_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,9 +123,7 @@ fn forbidden_error() -> Flash<Redirect> {
|
|||||||
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
|
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Usage {
|
struct Usage {}
|
||||||
data: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl Fairing for Usage {
|
impl Fairing for Usage {
|
||||||
@ -174,11 +186,12 @@ pub struct Config {
|
|||||||
rss_key: String,
|
rss_key: String,
|
||||||
smtp_pw: String,
|
smtp_pw: String,
|
||||||
usage_log_path: String,
|
usage_log_path: String,
|
||||||
|
pub openweathermap_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||||
rocket
|
rocket
|
||||||
.mount("/", routes![index, steering])
|
.mount("/", routes![index, steering, impressum])
|
||||||
.mount("/auth", auth::routes())
|
.mount("/auth", auth::routes())
|
||||||
.mount("/wikiauth", routes![wikiauth])
|
.mount("/wikiauth", routes![wikiauth])
|
||||||
.mount("/log", log::routes())
|
.mount("/log", log::routes())
|
||||||
@ -188,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())
|
||||||
@ -196,7 +210,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
|||||||
.register("/", catchers![unauthorized_error, forbidden_error])
|
.register("/", catchers![unauthorized_error, forbidden_error])
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
.attach(AdHoc::config::<Config>())
|
.attach(AdHoc::config::<Config>())
|
||||||
.attach(Usage { data: Vec::new() })
|
.attach(Usage {})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -10,10 +10,9 @@ use tera::Context;
|
|||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
log::Log,
|
log::Log,
|
||||||
logbook::Logbook,
|
|
||||||
tripdetails::TripDetails,
|
tripdetails::TripDetails,
|
||||||
triptype::TripType,
|
triptype::TripType,
|
||||||
user::{AllowedForPlannedTripsUser, User, UserWithRolesAndNotificationCount},
|
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
|
||||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,16 +26,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.has_role(db, "scheckbuch").await {
|
|
||||||
let last_trips = Logbook::completed_with_user(db, &user).await;
|
|
||||||
context.insert("last_trips", &last_trips);
|
|
||||||
}
|
|
||||||
|
|
||||||
let days = user.get_days(db).await;
|
let days = user.get_days(db).await;
|
||||||
|
|
||||||
if let Some(msg) = flash {
|
if let Some(msg) = flash {
|
||||||
@ -44,10 +38,7 @@ async fn index(
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.insert("fee", &user.fee(db).await);
|
context.insert("fee", &user.fee(db).await);
|
||||||
context.insert(
|
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||||
"loggedin_user",
|
|
||||||
&UserWithRolesAndNotificationCount::from_user(user, db).await,
|
|
||||||
);
|
|
||||||
context.insert("days", &days);
|
context.insert("days", &days);
|
||||||
Template::render("planned", context.into_json())
|
Template::render("planned", context.into_json())
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use sqlx::SqlitePool;
|
|||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
stat::{self, BoatStat, Stat},
|
stat::{self, BoatStat, Stat},
|
||||||
user::{DonauLinzUser, UserWithRolesAndNotificationCount},
|
user::{DonauLinzUser, UserWithDetails},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::log::KioskCookie;
|
use super::log::KioskCookie;
|
||||||
@ -16,7 +16,7 @@ async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"stat.boats",
|
"stat.boats",
|
||||||
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, kiosk),
|
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
|
|||||||
|
|
||||||
Template::render(
|
Template::render(
|
||||||
"stat.people",
|
"stat.people",
|
||||||
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
]
|
||||||
|
}
|
@ -3,18 +3,32 @@
|
|||||||
{% extends "base" %}
|
{% extends "base" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-screen-lg w-full dark:text-white">
|
<div class="max-w-screen-lg w-full dark:text-white">
|
||||||
<h1 class="h1">Nachricht</h1>
|
<h1 class="h1">Nachricht senden</h1>
|
||||||
<div class="grid ">
|
<div class="grid ">
|
||||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
role="alert">
|
role="alert">
|
||||||
<h2 class="h2">Nachricht senden</h2>
|
<h2 class="h2">Gruppe</h2>
|
||||||
<form action="/admin/notification" method="post" class="grid gap-3 p-3">
|
<form action="/admin/notification/group"
|
||||||
|
method="post"
|
||||||
|
class="grid gap-3 p-3">
|
||||||
{{ macros::select(label="Gruppe", data=roles, name="role_id") }}
|
{{ macros::select(label="Gruppe", data=roles, name="role_id") }}
|
||||||
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
|
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
|
||||||
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }}
|
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }}
|
||||||
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
|
role="alert">
|
||||||
|
<h2 class="h2">Person</h2>
|
||||||
|
<form action="/admin/notification/user"
|
||||||
|
method="post"
|
||||||
|
class="grid gap-3 p-3">
|
||||||
|
{{ macros::select(label="Person", data=users, name="user_id") }}
|
||||||
|
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
|
||||||
|
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }}
|
||||||
|
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -58,6 +58,10 @@
|
|||||||
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||||
href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a>
|
href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not user.last_access and "admin" in loggedin_user.roles %}
|
||||||
|
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||||
|
href="/admin/user/{{ user.id }}/send-welcome-mail">Willkommensmail verschicken</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
{% for role in roles %}
|
{% for role in roles %}
|
||||||
|
@ -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"
|
||||||
|
113
templates/impressum.html.tera
Normal file
113
templates/impressum.html.tera
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-screen-lg w-full">
|
||||||
|
<h1 class="h1">Impressum</h1>
|
||||||
|
<div class="grid gap-3 my-5">
|
||||||
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
|
role="alert">
|
||||||
|
<h2 class="h2">Allgemein</h2>
|
||||||
|
<div class="p-3">
|
||||||
|
Die Website wird vom ASKÖ Ruderverein Donau Linz betrieben.
|
||||||
|
<br />
|
||||||
|
<strong>Postanschrift:</strong>
|
||||||
|
<br />
|
||||||
|
ASKÖ Ruderverein Donau Linz
|
||||||
|
<br />
|
||||||
|
Heilhamerweg 2
|
||||||
|
<br />
|
||||||
|
4040 Linz
|
||||||
|
<br />
|
||||||
|
ZVR: 363903285
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-3 my-5">
|
||||||
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
|
role="alert">
|
||||||
|
<h2 class="h2">Datenschutz</h2>
|
||||||
|
<div class="p-3">
|
||||||
|
Folgende Daten werden verarbeitet:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Server-Log Files: IP-Adresse, Adresse der besuchten Seite, Browseragent, Datum und Uhrzeit. Wir nutzen diese Daten nicht und geben Sie in der Regel nicht weiter, können jedoch nicht ausschließen, dass diese Daten beim Vorliegen von rechtswidrigem Verhalten eingesehen werden.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Cookie: Diese Website verwendet nur einen Cookie (loggedin_user), der verschlüsselte Informationen über den Login-Status speichert. Weitere Cookies werden nicht verwendet.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if loggedin_user %}
|
||||||
|
<div class="grid gap-3 my-5">
|
||||||
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
|
role="alert">
|
||||||
|
<h2 class="h2">Daten</h2>
|
||||||
|
<div class="p-3">
|
||||||
|
Folgende personenbezogenen haben wir von dir gespeichert:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Name:</strong> {{ loggedin_user.name }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Passwort:</strong> (verschlüsselt als argon Hash)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Letzter Zugriff:</strong> {{ loggedin_user.last_access }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Mitglied seit:</strong> {{ loggedin_user.member_since_date }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Geburtsdatum:</strong> {{ loggedin_user.birthdate }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Mail:</strong> {{ loggedin_user.mail }}
|
||||||
|
</li>
|
||||||
|
{% if loggedin_user.nickname %}
|
||||||
|
<li>
|
||||||
|
<strong>Spitzname:</strong> {{ loggedin_user.nickname }}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<strong>Telefonnummer:</strong> {{ loggedin_user.phone }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Adresse:</strong> {{ loggedin_user.address }}
|
||||||
|
</li>
|
||||||
|
<li>(Beitrittserklärung)</li>
|
||||||
|
{% if loggedin_user.family_id %}
|
||||||
|
<li>Verbindung zu Familienmitglied (gespeichert um Familientarif anstatt Vollmitglied zu haben)</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<strong>Rollen:</strong> {{ loggedin_user.roles }} (werden für verschiedene Funktionen im Ruderassistenten verwendet)
|
||||||
|
</li>
|
||||||
|
<li>Anmeldungen zu Ausfahrten</li>
|
||||||
|
<li>Anmeldungen zu Events (zB Fetzenfahrt, Anrudern, USI-Rudern, ...)</li>
|
||||||
|
<li>Logbucheinträge</li>
|
||||||
|
<li>Selber eingetragene Bootsschäden, solange sie nicht > 1 Monat verifiziert und repariert wurden</li>
|
||||||
|
<li>Selber eingetragene Bootsreservierung</li>
|
||||||
|
<li>Boote, sofern es welche im Privatbesitz gibt</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="grid gap-3 my-5">
|
||||||
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
|
role="alert">
|
||||||
|
<h2 class="h2">Lizenzen</h2>
|
||||||
|
<div class="p-3">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Die <strong>Wetterdaten</strong> werden von <a class="underline" href="https://openweathermap.org">OpenWeather</a> bereitgestellt.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Wasserstandsvorhersagen:</strong> Die Vorhersagen werden stündlich vom <a class="underline" href="https://hydro.ooe.gv.at">Hydrographischen Dienstes Oberösterreich</a> geladen und zwischengespeichert, der höchste Tages-Mittelwert wird gemeinsam mit der Schwankungsbreite bei den geplanten Ausfahrten angezeigt. Es handelt sich hierbei um ungeprüfte Rohdaten. Rohdatenfehler können durch betriebliche Störungen an den Messgeräten, Fernübertragungseinrichtungen u. dgl. entstehen. Die Vorhersagen sind daher mit Unsicherheiten behaftet! Mit der Länge des Vorhersagezeitraumeszeitraumes werden diese Unsicherheiten größer! Es wird keine Gewähr für die Vollständigkeit, Richtigkeit und Genauigkeit der dargestellten Daten übernommen. Gewährleistungs- und Haftungsansprüche werden ausdrücklich ausgeschlossen (sowohl vom Hydrographischen Dienstes Oberösterreich als auch vom ASKÖ Ruderverein Donau Linz).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
@ -1,34 +1,39 @@
|
|||||||
<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 w-full flex justify-between items-center">
|
<div class="max-w-screen-xl w-full">
|
||||||
<div>
|
<div class="w-full flex justify-between items-center">
|
||||||
<span class="text-[#ff0000]">♥</span>
|
<div>
|
||||||
Erstellt vom ASKÖ Ruderverein Donau Linz <a onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird, dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at – für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');"
|
<span class="text-[#ff0000]">♥</span>
|
||||||
style="text-decoration:underline">... und dir?</a>
|
Erstellt vom ASKÖ Ruderverein Donau Linz <a class="underline"
|
||||||
|
onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird, dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at – für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');">... und dir?</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="theme-toggle-js"
|
||||||
|
type="button"
|
||||||
|
data-theme="light"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<span class="hidden dark:inline">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 16 16">
|
||||||
|
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="inline dark:hidden">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 16 16">
|
||||||
|
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="mt-3">
|
||||||
<button id="theme-toggle-js"
|
<a class="underline" href="/impressum">Impressum</a>
|
||||||
type="button"
|
|
||||||
data-theme="light"
|
|
||||||
class="btn btn-primary">
|
|
||||||
<span class="hidden dark:inline">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 16 16">
|
|
||||||
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span class="inline dark:hidden">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 16 16">
|
|
||||||
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -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 %}#}
|
||||||
@ -152,7 +152,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% set amount_rowers = log.rowers | length %}
|
{% set amount_rowers = log.rowers | length %}
|
||||||
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
||||||
{% if amount_guests > 0 %}
|
{% if amount_guests > 0 and not log.boat.external %}
|
||||||
Gäste
|
Gäste
|
||||||
<small class="text-gray-600">(ohne Account)</small>:
|
<small class="text-gray-600">(ohne Account)</small>:
|
||||||
{{ amount_guests }}
|
{{ amount_guests }}
|
||||||
@ -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>
|
||||||
@ -191,7 +193,7 @@
|
|||||||
{% if rower.is_real_guest %}
|
{% if rower.is_real_guest %}
|
||||||
<small class="text-gray-600 dark:text-gray-100">(Gast)</small>
|
<small class="text-gray-600 dark:text-gray-100">(Gast)</small>
|
||||||
{% if allow_removing %}
|
{% if allow_removing %}
|
||||||
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}"
|
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name | urlencode }}"
|
||||||
class="absolute r-0 bg-red-500 w-5 h-5 text-white rounded-full flex items-center justify-center transform rotate-45 top-0 right-0">
|
class="absolute r-0 bg-red-500 w-5 h-5 text-white rounded-full flex items-center justify-center transform rotate-45 top-0 right-0">
|
||||||
<svg class="inline h-5 w-5"
|
<svg class="inline h-5 w-5"
|
||||||
width="16"
|
width="16"
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
{% import "includes/macros" as macros %}
|
{% import "includes/macros" as macros %}
|
||||||
|
{% import "includes/forms/log" as log %}
|
||||||
{% extends "base" %}
|
{% extends "base" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-screen-lg w-full">
|
<div class="max-w-screen-lg w-full">
|
||||||
<h1 class="h1">
|
<h1 class="h1">Ruder­assistent</h1>
|
||||||
Ruder
|
|
||||||
<wbr />
|
|
||||||
assistent
|
|
||||||
</h1>
|
|
||||||
<div class="grid gap-3 my-5">
|
<div class="grid gap-3 my-5">
|
||||||
<div class="m-auto">
|
<div class="m-auto">
|
||||||
<a href="/planned"
|
<a href="/planned"
|
||||||
@ -15,55 +12,57 @@
|
|||||||
<span class="text-xl px-3">Geplante Ausfahrten</span>
|
<span class="text-xl px-3">Geplante Ausfahrten</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="notification"
|
{% if notifications %}
|
||||||
class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
<div id="notification"
|
||||||
role="alert">
|
class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
<h2 class="h2">Nachrichten</h2>
|
role="alert">
|
||||||
{% if loggedin_user.amount_unread_notifications > 10 %}
|
<h2 class="h2">Nachrichten</h2>
|
||||||
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
|
{% if loggedin_user.amount_unread_notifications > 10 %}
|
||||||
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte einen Moment Zeit sie zu überprüfen und als gelesen zu markieren (✓).
|
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
|
||||||
</div>
|
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte einen Moment Zeit sie zu überprüfen und als gelesen zu markieren (✓).
|
||||||
{% endif %}
|
</div>
|
||||||
<div class="divide-y">
|
{% endif %}
|
||||||
{% for notification in notifications %}
|
<div class="divide-y">
|
||||||
{% if not notification.read_at %}
|
|
||||||
<div class="relative flex justify-between items-center p-3">
|
|
||||||
<div class="grow me-4">
|
|
||||||
<small class="uppercase text-gray-600 dark:text-gray-100">
|
|
||||||
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M",) }}
|
|
||||||
</small>
|
|
||||||
<div class="mt-1">{{ notification.message | safe }}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{% if not notification.read_at %}
|
|
||||||
<a href="/notification/{{ notification.id }}/read" class="inline-block">
|
|
||||||
<button class="btn btn-primary" type="button">
|
|
||||||
✓
|
|
||||||
<span class="sr-only">Notification gelesen</span>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<details class="py-3 border-t rounded-b-md">
|
|
||||||
<summary class="px-3 cursor-pointer">Vergangene Nachrichten (14 Tage)</summary>
|
|
||||||
<div class="divide-y text-sm">
|
|
||||||
{% for notification in notifications %}
|
{% for notification in notifications %}
|
||||||
{% if notification.read_at %}
|
{% if not notification.read_at %}
|
||||||
<div class="p-3 relative">
|
<div class="relative flex justify-between items-center p-3">
|
||||||
<small class="uppercase text-gray-600 dark:text-gray-100">
|
<div class="grow me-4">
|
||||||
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }}
|
<small class="uppercase text-gray-600 dark:text-gray-100">
|
||||||
</small>
|
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M",) }}
|
||||||
<div class="mt-1">{{ notification.message | safe }}</div>
|
</small>
|
||||||
|
<div class="mt-1">{{ notification.message | safe }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% if not notification.read_at %}
|
||||||
|
<a href="/notification/{{ notification.id }}/read" class="inline-block">
|
||||||
|
<button class="btn btn-primary" type="button">
|
||||||
|
✓
|
||||||
|
<span class="sr-only">Notification gelesen</span>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
<details class="py-3 border-t rounded-b-md">
|
||||||
</div>
|
<summary class="px-3 cursor-pointer">Vergangene Nachrichten (14 Tage)</summary>
|
||||||
|
<div class="divide-y text-sm">
|
||||||
|
{% for notification in notifications %}
|
||||||
|
{% if notification.read_at %}
|
||||||
|
<div class="p-3 relative">
|
||||||
|
<small class="uppercase text-gray-600 dark:text-gray-100">
|
||||||
|
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }}
|
||||||
|
</small>
|
||||||
|
<div class="mt-1">{{ notification.message | safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
|
{% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
|
||||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
role="alert">
|
role="alert">
|
||||||
@ -91,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>
|
||||||
@ -98,14 +101,26 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "scheckbuch" in loggedin_user.roles %}
|
{% if "scheckbuch" in loggedin_user.roles %}
|
||||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
<div class="grid gap-3 mb-4">
|
||||||
role="alert">
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
<h2 class="h2">Scheckbuch</h2>
|
role="alert">
|
||||||
<ul class="list-none ms-2 divide-y divide-gray-200 dark:divide-primary-600">
|
<h2 class="h2">Scheckbuch</h2>
|
||||||
<li class="py-1">
|
{% if "paid" not in loggedin_user.roles %}
|
||||||
<a href="/planned" class="block w-100 py-2 hover:text-primary-600">Geplante Ausfahrten</a>
|
<div class="p-3 dark:text-white bg-white dark:bg-primary-900">
|
||||||
</li>
|
Bitte nimm zur nächsten Ausfahrt die {{ costs_scheckbuch / 100 }} € für das Scheckbuch mit. Falls du das bereits gemacht hast, gibt uns bitte kurz Bescheid, dass dies noch nicht eingetragen wurde.
|
||||||
</ul>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="text-sm p-3 bg-gray-200 bg-opacity-80 dark:bg-primary-950 dark:text-white text-primary-950">
|
||||||
|
<h3>Du hast bisher an {{ last_trips | length }} deiner 5 Scheckbuch-Ausfahrten teilgenommen.</h3>
|
||||||
|
{% if last_trips %}
|
||||||
|
<ol class="mt-3">
|
||||||
|
{% for last_trip in last_trips %}
|
||||||
|
<li>{{ log::show_old(log=last_trip, state="completed", only_ones=false, index=loop.index) }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "schnupper-betreuer" in loggedin_user.roles %}
|
{% if "schnupper-betreuer" in loggedin_user.roles %}
|
||||||
|
@ -3,22 +3,6 @@
|
|||||||
{% extends "base" %}
|
{% extends "base" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{% if "scheckbuch" in loggedin_user.roles %}
|
|
||||||
<div class="grid gap-3 sm:col-span-2 lg:col-span-3">
|
|
||||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
|
||||||
role="alert">
|
|
||||||
<h2 class="h2">Scheckbuch</h2>
|
|
||||||
<div class="text-sm p-3">
|
|
||||||
<h3>Du hast bisher {{ last_trips | length }} deiner 5 Scheckbuch-Ausfahrten gemacht:</h3>
|
|
||||||
<ol>
|
|
||||||
{% for last_trip in last_trips %}
|
|
||||||
<li>{{ log::show_old(log=last_trip, state="completed", only_ones=false, index=loop.index) }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if "paid" not in loggedin_user.roles and "Donau Linz" in loggedin_user.roles %}
|
{% if "paid" not in loggedin_user.roles and "Donau Linz" in loggedin_user.roles %}
|
||||||
<div class="grid gap-3 sm:col-span-2 lg:col-span-3">
|
<div class="grid gap-3 sm:col-span-2 lg:col-span-3">
|
||||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||||
@ -74,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 %}
|
||||||
@ -92,73 +76,94 @@
|
|||||||
<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">
|
<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">
|
||||||
{{ day.day| date(format="%d.%m.%Y") }}
|
{{ day.day| date(format="%d.%m.%Y") }}
|
||||||
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}
|
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}
|
||||||
{% if day.max_waterlevel %}• <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand" target="_blank" title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel }} cm">🌊{{ day.max_waterlevel }} cm</a>{% endif %}
|
{% if day.max_waterlevel %}
|
||||||
|
• <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand"
|
||||||
|
target="_blank"
|
||||||
|
title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel.avg }} ± {{ day.max_waterlevel.fluctuation }} cm (ungeprüfte Rohdaten, für Details siehe die Infos dazu im Impressum)">🌊{{ day.max_waterlevel.avg }} ± {{ day.max_waterlevel.fluctuation }} cm</a>
|
||||||
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
|
{% if day.weather %}
|
||||||
|
<small class="inline-block text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">
|
||||||
|
Temp: {{ day.weather.max_temp | round }}° • Windböe: {{ day.weather.wind_gust | round }} km/h • Regen: {{ day.weather.rain_mm | round }} mm
|
||||||
|
</small>
|
||||||
|
{% 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">
|
||||||
<strong class="text-primary-900 dark:text-white">
|
{% if event.max_people == 0 %}
|
||||||
{{ planned_event.planned_starting_time }}
|
<strong class="text-[#f43f5e]">⚠ Absage
|
||||||
Uhr
|
{{ event.planned_starting_time }}
|
||||||
</strong>
|
Uhr
|
||||||
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}
|
</strong>
|
||||||
{%- if planned_event.trip_type %}
|
<small class="text-[#f43f5e]">({{ event.name }}
|
||||||
- {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}
|
{%- if event.trip_type %}
|
||||||
{%- endif -%}
|
- {{ event.trip_type.icon | safe }} {{ event.trip_type.name }}
|
||||||
)</small>
|
{%- endif -%}
|
||||||
|
)</small>
|
||||||
|
{% else %}
|
||||||
|
<strong class="text-primary-900 dark:text-white">
|
||||||
|
{{ event.planned_starting_time }}
|
||||||
|
Uhr
|
||||||
|
</strong>
|
||||||
|
<small class="text-gray-600 dark:text-gray-100">({{ event.name }}
|
||||||
|
{%- if event.trip_type %}
|
||||||
|
- {{ event.trip_type.icon | safe }} {{ event.trip_type.name }}
|
||||||
|
{%- endif -%}
|
||||||
|
)</small>
|
||||||
|
{% 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>
|
||||||
@ -169,59 +174,88 @@
|
|||||||
</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 amount_cox_missing > 0 %}
|
{% if event.max_people == 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="", header='Absage', bg='[#f43f5e]') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
|
{% if amount_cox_missing > 0 %}
|
||||||
|
{{ macros::box(participants=event.cox, empty_seats=event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
|
||||||
|
{% else %}
|
||||||
|
{{ macros::box(participants=event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# --- END List Coxes --- #}
|
{# --- END List Coxes --- #}
|
||||||
{# --- START List Rowers --- #}
|
{# --- START List Rowers --- #}
|
||||||
{% if planned_event.max_people > 0 %}
|
{% set amount_cur_rower = event.rower | length %}
|
||||||
{% set amount_cur_rower = planned_event.rower | length %}
|
{% if event.max_people == 0 %}
|
||||||
{{ 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(header='Absage', bg='[#f43f5e]', participants=event.rower, trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
|
||||||
|
{% else %}
|
||||||
|
{{ macros::box(participants=event.rower, empty_seats=event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
|
||||||
{% 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 --- #}
|
||||||
<div class="text-right">
|
{% if event.rower | length == 0 and amount_cur_cox == 0 %}
|
||||||
<a href="/admin/planned-event/{{ planned_event.id }}/delete"
|
<div class="text-right mt-6">
|
||||||
class="inline-block btn btn-alert">
|
<a href="/admin/planned-event/{{ event.id }}/delete"
|
||||||
{% include "includes/delete-icon" %}
|
class="inline-block btn btn-alert">
|
||||||
Termin löschen
|
{% include "includes/delete-icon" %}
|
||||||
</a>
|
Termin löschen
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% if event.max_people == 0 %}
|
||||||
|
Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||||
|
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Event absagen</h3>
|
||||||
|
<form action="/admin/planned-event" method="post" class="grid">
|
||||||
|
<input type="hidden" name="_method" value="put" />
|
||||||
|
<input type="hidden" name="id" value="{{ event.id }}" />
|
||||||
|
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
|
||||||
|
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
|
||||||
|
{{ macros::input(label='', name='name', type='hidden', value=event.name) }}
|
||||||
|
{{ macros::input(label='', name='max_people', type='hidden', value=event.max_people) }}
|
||||||
|
{{ macros::input(label='', name='planned_amount_cox', type='hidden', value=event.planned_amount_cox) }}
|
||||||
|
{{ macros::input(label='', name='always_show', type='hidden', value=event.always_show) }}
|
||||||
|
{{ macros::input(label='', name='is_locked', type='hidden', value=event.is_locked) }}
|
||||||
|
{{ macros::input(label='', name='trip_type', type='hidden', value=event.trip_type_id) }}
|
||||||
|
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# --- END Delete Btn --- #}
|
{# --- END Delete Btn --- #}
|
||||||
</div>
|
</div>
|
||||||
@ -247,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 />
|
||||||
@ -334,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) }}
|
||||||
@ -356,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"
|
||||||
@ -372,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
|
||||||
@ -392,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