63 Commits

Author SHA1 Message Date
2159696112 fix migration
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m19s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-16 20:00:44 +02:00
39bde35864 Merge pull request 'add wifi pw in welcome mail' (#593) from add-wifi-pw-new-members into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m45s
Reviewed-on: #593
2024-06-10 22:08:18 +02:00
5f301324ee add wifi pw in welcome mail
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 22:07:14 +02:00
9558965e8f Merge pull request 'require necessary fields' (#591) from require-necessary-fields into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m30s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m48s
Reviewed-on: #591
2024-06-10 20:59:43 +02:00
5f4d8982a8 require necessary fields
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-10 20:58:42 +02:00
0e2ef9e256 Merge pull request 'add trailer reservation funcitonality; Fixes #443' (#588) from trailer-reservation into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m3s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m7s
Reviewed-on: #588
2024-06-10 20:00:25 +02:00
1dc91f4f28 merged :-)
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-10 19:59:39 +02:00
f56da43723 fix copied stuff
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m41s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-10 19:48:16 +02:00
9dc1ec6fa0 add trailer reservation funcitonality; Fixes #443
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 19:43:59 +02:00
c9b67f5790 Merge pull request 'add delete trip button in edit window; Fixes #449' (#586) from delte-trip-btn into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m53s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m5s
Reviewed-on: #586
2024-06-10 19:08:22 +02:00
16687e39ab remove unused attribute
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-10 19:07:55 +02:00
957c474389 add delete trip button in edit window; Fixes #449
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 19:07:05 +02:00
f7aed68423 Merge pull request 'show if a user has < 30 km to thousand km trip, Fixes #551' (#582) from thousand-km-trips into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m59s
Reviewed-on: #582
2024-06-10 15:50:31 +02:00
01637d0800 Merge pull request 'use proper hydro license; add fluctuation' (#584) from proper-hydro into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #584
2024-06-10 15:49:51 +02:00
0a77011170 use proper hydro license; add fluctuation
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 15:48:49 +02:00
dea0c65da3 fix ci + add test
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-10 15:12:23 +02:00
31bf38f112 show if a user has < 30 km to thousand km trip, Fixes #551
Some checks failed
CI/CD Pipeline / test (push) Failing after 7m14s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-10 14:55:01 +02:00
6b29907596 Merge pull request 'fix boat select' (#580) from fix-boat-select into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m2s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m34s
Reviewed-on: #580
2024-06-10 11:01:47 +02:00
7b17c30ce2 fix boat select
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-10 11:00:54 +02:00
ec4068e499 Merge pull request 'cleaner cal' (#578) from cleaner-cal into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m47s
Reviewed-on: #578
2024-06-06 17:22:14 +02:00
fca19745f8 even nicer cal entries
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-06 17:21:55 +02:00
bb48ddb3de fix ci
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-06 17:17:40 +02:00
34b098fa2a cleaner cal
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-06 17:16:47 +02:00
df1a06531f Merge pull request 'try new badge :-)' (#576) from add-badge into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 21m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 18m18s
Reviewed-on: #576
2024-06-06 10:48:51 +02:00
7b499fb457 try new badge :-)
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m55s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-06 10:38:18 +02:00
d00570ff2f Merge pull request 'nicer-cal' (#574) from nicer-cal into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m0s
Reviewed-on: #574
2024-06-06 07:11:43 +02:00
bd63f2c386 use new additional information in test
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-06 06:59:41 +02:00
e1b78b2725 don't lose trip_type on event cancellation; don't add empty notes in cal
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-06 06:57:43 +02:00
fa14cfbf83 add cancellation, trip_type and notes to cal export
Some checks failed
CI/CD Pipeline / test (push) Failing after 9m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-06 06:47:41 +02:00
5f6cb9a12b Merge pull request 'update deps' (#572) from update-deps into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m26s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 17m43s
Reviewed-on: #572
2024-06-05 15:07:08 +02:00
1cac70cabb update deps
All checks were successful
CI/CD Pipeline / test (push) Successful in 19m14s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-05 14:57:28 +02:00
09cb8ebfa9 Merge pull request 'allow-event-triptype-update' (#570) from allow-event-triptype-update into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m20s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m12s
Reviewed-on: #570
2024-06-04 08:32:57 +02:00
ab88ce3230 fix tests
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-04 08:13:24 +02:00
30a6bc7109 allow event trip_type update
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-04 08:12:20 +02:00
99409f9407 Merge pull request 'remove space' (#568) from fix-spaces into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m42s
Reviewed-on: #568
2024-06-02 14:53:14 +02:00
7eff2a948a remove space
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-02 14:52:07 +02:00
243838fd44 Merge pull request 'remove debug println; better phrasing' (#566) from better-text into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #566
2024-05-30 18:53:42 +02:00
2de4c86c26 remove debug println; better phrasing
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m26s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-30 11:57:03 +02:00
2889d40d55 Merge pull request 'better-text' (#564) from better-text into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m40s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m55s
Reviewed-on: #564
2024-05-30 11:52:17 +02:00
e8d4672176 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-05-30 11:34:01 +02:00
1bd643f6f4 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-05-30 11:11:08 +02:00
562c32939d better phrasing of text
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m38s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-30 10:39:55 +02:00
3c0b8e5114 Merge pull request 'fix error' (#560) from fix-user-find-bug into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m56s
Reviewed-on: #560
2024-05-28 15:04:50 +02:00
6d5ff5404b fix error
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-05-28 15:04:06 +02:00
a8c0282918 Merge pull request 'migrated db' (#558) from migrated-db into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m56s
Reviewed-on: #558
2024-05-28 11:27:46 +02:00
9973913af6 migrated db
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-05-28 11:26:22 +02:00
7055c999e8 Merge pull request 'rename role to manage_events' (#556) from reanme-to-event into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #556
2024-05-28 11:18:50 +02:00
c0d766832e rename role to manage_events
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m44s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-28 10:46:21 +02:00
f88c0be781 Merge pull request 'reanme-to-event' (#554) from reanme-to-event into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #554
2024-05-28 10:06:50 +02:00
91fa2a7762 add test for cancel-event-notification
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m39s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-28 09:59:16 +02:00
82aa94c024 rename planned_event to event
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-28 09:08:48 +02:00
aaf09208f3 Merge pull request 'don't care about cases for username' (#550) from case-insensitive-auth into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m33s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m15s
Reviewed-on: #550
2024-05-27 08:33:07 +02:00
86f7ca7065 don't care about cases for username
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m38s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-27 08:32:00 +02:00
e325e0478a Merge pull request 'better spacing' (#548) from fix-spacing into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m34s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m57s
Reviewed-on: #548
2024-05-26 18:48:07 +02:00
0298617fc9 better spacing
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-05-26 18:47:09 +02:00
02ff89ba34 Merge pull request 'fix spacing' (#546) from fix-spacing into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m57s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m38s
Reviewed-on: #546
2024-05-26 14:18:13 +02:00
47a543fa64 fix spacing
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-05-26 14:17:26 +02:00
4e8fd84134 Merge pull request 'fix typo' (#544) from type into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m45s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m22s
Reviewed-on: #544
2024-05-25 18:53:00 +02:00
544267a037 fix typo
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-05-25 18:52:00 +02:00
97b0ae83a9 Merge pull request 'extend log filter' (#540) from filter-logs into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m20s
Reviewed-on: #540
2024-05-22 23:42:19 +02:00
f6d8c07c08 extend log filter
Some checks failed
CI/CD Pipeline / test (push) Has started running
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-05-22 23:41:24 +02:00
da56723909 Merge pull request 'fix footer' (#538) from fix-footer into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m27s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m0s
Reviewed-on: #538
2024-05-22 22:51:54 +02:00
603aed8394 fix footer
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-05-22 22:51:10 +02:00
36 changed files with 1392 additions and 480 deletions

519
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -195,3 +195,21 @@ CREATE TABLE IF NOT EXISTS "weather" (
"wind_gust" FLOAT NOT NULL, "wind_gust" FLOAT NOT NULL,
"rain_mm" FLOAT NOT NULL "rain_mm" FLOAT NOT NULL
); );
CREATE TABLE IF NOT EXISTS "trailer" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS "trailer_reservation" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"trailer_id" INTEGER NOT NULL REFERENCES trailer(id),
"start_date" DATE NOT NULL,
"end_date" DATE NOT NULL,
"time_desc" TEXT NOT NULL,
"usage" TEXT NOT NULL,
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
"user_id_confirmation" INTEGER REFERENCES user(id),
"created_at" datetime not null default CURRENT_TIMESTAMP
);

View File

@ -3,7 +3,7 @@ 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');
@ -64,3 +64,5 @@ INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3);
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02'); INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) 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');

View File

@ -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()

View File

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

View File

@ -301,7 +301,7 @@ ORDER BY departure DESC
db: &SqlitePool, db: &SqlitePool,
mut log: LogToAdd, mut log: LogToAdd,
created_by_user: &User, created_by_user: &User,
) -> Result<(), LogbookCreateError> { ) -> Result<String, LogbookCreateError> {
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else { let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
return Err(LogbookCreateError::BoatNotFound); return Err(LogbookCreateError::BoatNotFound);
}; };
@ -354,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()),
}; };
@ -426,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)> {
@ -635,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]
@ -686,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,
@ -704,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]

View File

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

View File

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

View File

@ -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
View File

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

View File

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

View File

@ -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},
@ -133,28 +133,28 @@ WHERE trip.id=?
.ok() .ok()
} }
/// Cox decides to help in a planned event. /// Cox decides to help in a event.
pub async fn new_join( pub async fn new_join(
db: &SqlitePool, db: &SqlitePool,
cox: &CoxUser, cox: &CoxUser,
planned_event: &PlannedEvent, event: &Event,
) -> Result<(), CoxHelpError> { ) -> Result<(), CoxHelpError> {
if planned_event.is_rower_registered(db, cox).await { if event.is_rower_registered(db, cox).await {
return Err(CoxHelpError::AlreadyRegisteredAsRower); return Err(CoxHelpError::AlreadyRegisteredAsRower);
} }
if planned_event.trip_details(db).await.is_locked { if event.trip_details(db).await.is_locked {
return Err(CoxHelpError::DetailsLocked); return Err(CoxHelpError::DetailsLocked);
} }
if planned_event.max_people == 0 { if event.max_people == 0 {
return Err(CoxHelpError::CanceledEvent); return Err(CoxHelpError::CanceledEvent);
} }
match sqlx::query!( match sqlx::query!(
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)", "INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
cox.id, cox.id,
planned_event.id event.id
) )
.execute(db) .execute(db)
.await .await
@ -223,10 +223,9 @@ WHERE day=?
.rower; .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) = update.notes { let notes = match update.notes {
format!("Grund der Absage: {notes}") Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
} else { _ => String::from(""),
String::from("")
}; };
Notification::create( Notification::create(
@ -281,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
@ -374,7 +373,7 @@ pub enum TripUpdateError {
mod test { mod test {
use crate::{ use crate::{
model::{ model::{
planned_event::PlannedEvent, event::Event,
trip::{self, TripDeleteError}, trip::{self, TripDeleteError},
tripdetails::TripDetails, tripdetails::TripDetails,
user::{CoxUser, User}, user::{CoxUser, User},
@ -425,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());
} }
@ -441,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());
@ -542,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();

View File

@ -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,

View File

@ -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,

View File

@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{ use super::{
family::Family, log::Log, mail::Mail, notification::Notification, role::Role, family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat,
tripdetails::TripDetails, Day, tripdetails::TripDetails, Day,
}; };
use crate::tera::admin::user::UserEditForm; use crate::tera::admin::user::UserEditForm;
@ -187,7 +187,7 @@ impl User {
format!( format!(
"Hallo {0}, "Hallo {0},
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden. es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
Liebe Grüße, Philipp", self.name), Liebe Grüße, Philipp", self.name),
smtp_pw, smtp_pw,
@ -281,6 +281,8 @@ Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dic
Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst. Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst.
Außerdem haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz'. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln! Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch Riemen- & Dollenbruch
@ -486,12 +488,14 @@ WHERE id like ?
} }
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> { pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
let name = name.trim().to_lowercase();
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE name=? WHERE lower(name)=?
", ",
name name
) )
@ -705,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",
@ -733,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;
} }
@ -822,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 {
@ -846,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]
@ -1123,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 {
@ -1182,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))
} }

View File

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

View File

@ -17,6 +17,13 @@ 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 struct Create {
pub day: NaiveDate, pub day: NaiveDate,
pub time: String, pub time: String,
@ -53,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>) {

View File

@ -8,14 +8,14 @@ use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
planned_event::{self, 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,23 +49,25 @@ 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> {
let update = planned_event::EventUpdate { let update = event::EventUpdate {
name: data.name, name: data.name,
planned_amount_cox: data.planned_amount_cox, planned_amount_cox: data.planned_amount_cox,
max_people: data.max_people, max_people: data.max_people,
notes: data.notes, notes: data.notes,
always_show: data.always_show, always_show: data.always_show,
is_locked: data.is_locked, is_locked: data.is_locked,
trip_type_id: data.trip_type,
}; };
match PlannedEvent::find_by_id(db, data.id).await { match Event::find_by_id(db, data.id).await {
Some(planned_event) => { Some(planned_event) => {
planned_event.update(db, &update).await; planned_event.update(db, &update).await;
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet") Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
@ -75,9 +77,9 @@ async fn update(
} }
#[get("/planned-event/<id>/delete")] #[get("/planned-event/<id>/delete")]
async fn delete(db: &State<SqlitePool>, id: i64, _admin: PlannedEventUser) -> Flash<Redirect> { async fn delete(db: &State<SqlitePool>, id: i64, _admin: EventUser) -> Flash<Redirect> {
let Some(event) = PlannedEvent::find_by_id(db, id).await else { let Some(event) = Event::find_by_id(db, id).await else {
return Flash::error(Redirect::to("/planned"), "PlannedEvent does not exist"); return Flash::error(Redirect::to("/planned"), "Event does not exist");
}; };
match event.delete(db).await { match event.delete(db).await {
@ -105,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);
@ -130,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);
} }
@ -159,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());
@ -200,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()));
} }
@ -267,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");
} }
} }

View File

@ -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
} }

View File

@ -7,8 +7,8 @@ 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::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
user::CoxUser, user::CoxUser,
@ -82,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(
@ -137,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(

View File

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

View File

@ -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> {

View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@
<a href="/stat/boats" class="px-2">Bootsauswertung</a> <a href="/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 %}

View File

@ -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"

View File

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

View File

@ -1,5 +1,5 @@
<footer class="bg-primary-950 dark:bg-primary-900 text-white w-full flex justify-center p-3"> <footer class="bg-primary-950 dark:bg-primary-900 text-white w-full flex justify-center p-3">
<div class="max-w-screen-xl"> <div class="max-w-screen-xl w-full">
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div> <div>
<span class="text-[#ff0000]">&hearts;</span> <span class="text-[#ff0000]">&hearts;</span>

View File

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

View File

@ -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>

View File

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

View File

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

View File

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