Compare commits
436 Commits
42a1579cd1
...
test
Author | SHA1 | Date | |
---|---|---|---|
2159696112 | |||
39bde35864 | |||
5f301324ee | |||
9558965e8f | |||
5f4d8982a8 | |||
0e2ef9e256 | |||
1dc91f4f28 | |||
f56da43723 | |||
9dc1ec6fa0 | |||
c9b67f5790 | |||
16687e39ab | |||
957c474389 | |||
f7aed68423 | |||
01637d0800 | |||
0a77011170 | |||
dea0c65da3 | |||
31bf38f112 | |||
6b29907596 | |||
7b17c30ce2 | |||
ec4068e499 | |||
fca19745f8 | |||
bb48ddb3de | |||
34b098fa2a | |||
df1a06531f | |||
7b499fb457 | |||
d00570ff2f | |||
bd63f2c386 | |||
e1b78b2725 | |||
fa14cfbf83 | |||
5f6cb9a12b | |||
1cac70cabb | |||
09cb8ebfa9 | |||
ab88ce3230 | |||
30a6bc7109 | |||
99409f9407 | |||
7eff2a948a | |||
243838fd44 | |||
2de4c86c26 | |||
2889d40d55 | |||
e8d4672176 | |||
1bd643f6f4 | |||
562c32939d | |||
3c0b8e5114 | |||
6d5ff5404b | |||
a8c0282918 | |||
9973913af6 | |||
7055c999e8 | |||
c0d766832e | |||
f88c0be781 | |||
91fa2a7762 | |||
82aa94c024 | |||
aaf09208f3 | |||
86f7ca7065 | |||
e325e0478a | |||
0298617fc9 | |||
02ff89ba34 | |||
47a543fa64 | |||
4e8fd84134 | |||
544267a037 | |||
97b0ae83a9 | |||
f6d8c07c08 | |||
da56723909 | |||
603aed8394 | |||
f22d3b65be | |||
446e48020e | |||
93e3e0ef5c | |||
8f5cc70981 | |||
71c228f202 | |||
3ebde6afce | |||
a797180b0d | |||
9704893329 | |||
05c4c4f6a2 | |||
daf9460bf7 | |||
db5e0873a6 | |||
40f97f18a9 | |||
6b911f242a | |||
f4ce748a74 | |||
d4ffd8850e | |||
c93556a5ab | |||
96ce46d39c | |||
412ec27927 | |||
a1c7e4c690 | |||
1f0de7abf4 | |||
64f3596132 | |||
3b75f38dca | |||
10f2e3016a | |||
1069e29cf0 | |||
b4967b54e9 | |||
1285c3bc28 | |||
b6c9cb0b99 | |||
29fabb04b0 | |||
830aa58e7b | |||
1b6aec8d89 | |||
1bf1cc9c68 | |||
02e1f77f65 | |||
d819462b0d | |||
387acdbd09 | |||
b36144832a | |||
fc49e6c977 | |||
b8463122d6 | |||
86db4cb2f4 | |||
82865799ce | |||
76f08905ab | |||
8dd878b492 | |||
b405cf9936 | |||
01c2f0c4a3 | |||
0318d1dfb2 | |||
261753c6b4 | |||
d0038677ca | |||
4bd91b2a7e | |||
17f4291af0 | |||
073f5aed0c | |||
3097d99e00 | |||
0eac1a66f9 | |||
f034f80794 | |||
57c9d532c8 | |||
b774acf9ae | |||
20cc085562 | |||
862ec5624a | |||
77a90a8086 | |||
ca5a932ae5 | |||
626be1c9fb | |||
7e2c185c03 | |||
65068e44a5 | |||
133a517a2e | |||
3d45310c73 | |||
e4ef1f1584 | |||
ebb4fe84bb | |||
2bf517ccd8 | |||
1908f61268 | |||
ac3301e97b | |||
18faf4a72d | |||
e3c30e010b | |||
d9aa7cafe1 | |||
97b0ce65f9 | |||
a465dfcce5 | |||
6371366a96 | |||
0952bf7878 | |||
fa0dc5b544 | |||
c3c7ecec98 | |||
a0d53366e0 | |||
b69eded21d | |||
bd68bfc668 | |||
7355d9d69b | |||
5602ad2681 | |||
6813d75db5 | |||
b4023c1ea8 | |||
45b51f4698 | |||
31fda6bee9 | |||
e728c4dbea | |||
1d9adf071f | |||
fcb4d65d32 | |||
96036b180b | |||
8c563a9c36 | |||
b70929c5ce | |||
c98f33e138 | |||
17d1ee3566 | |||
a75ba765df | |||
1503544a73 | |||
0b350d344d | |||
67c8431157 | |||
d6ecd87593 | |||
03073965a1 | |||
2189b082c0 | |||
6d4bc81720 | |||
25fe4c23ef | |||
2fdfddbd2e | |||
dea6520aa9 | |||
f8e0cd2d5b | |||
9fda9cbde2 | |||
74b24569dd | |||
3a39315a01 | |||
3323807e46 | |||
65d51c2cc2 | |||
8773bbb9d1 | |||
89ac4974db | |||
7dfdc55adb | |||
e4f1528b15 | |||
c449e878f0 | |||
311e611d5f | |||
8e5661b2f3 | |||
79976b751f | |||
139acb2ec5 | |||
5dcd7bc745 | |||
ebdfe37bec | |||
08fe779403 | |||
9ca510b892 | |||
d01895df90 | |||
f08d9728eb | |||
c27a2ad15e | |||
18047d16bf | |||
1866034431 | |||
e2306e890d | |||
688ce4c6fc | |||
46f8ca230a | |||
fcf86c7ff1 | |||
123b4bd39f | |||
7a8b79ccef | |||
5eadfd42bb | |||
87307378c6 | |||
8b42bdce0c | |||
5bb0cb4112 | |||
237377dc05 | |||
fc7ca28f56 | |||
6a18d7435a | |||
a42191715d | |||
43e073c54e | |||
14e7616b88 | |||
8e03c935a5 | |||
0560ed7a6a | |||
07b197cc63 | |||
a1ebd59f22 | |||
aac7444896 | |||
d646996c80 | |||
1412852087 | |||
5f5f215aad | |||
7a59c67763 | |||
a2b0146d6d | |||
7f813823f3 | |||
d91baeb7bc | |||
b5cdc8827a | |||
b6b88ead37 | |||
6b3851bfe4 | |||
a0dbd0f490 | |||
3f80cb498c | |||
7ba5070df3 | |||
cc1a7106cb | |||
d4218289f0 | |||
349a9f843c | |||
982bb3b5c8 | |||
b4f38089ee | |||
e22dd8eb51 | |||
ac9c4b256e | |||
cfec4ef8b3 | |||
a424e79cba | |||
be24001b4b | |||
d9885f9bba | |||
6c2e0669d5 | |||
e556b1375d | |||
23a623bdc9 | |||
b1d48a5154 | |||
61261c9816 | |||
f993eae27f | |||
f1ee266288 | |||
9aefc814a7 | |||
548c025fbc | |||
8f44fdadf2 | |||
3abff08f61 | |||
32dee2c320 | |||
3c67e0deeb | |||
ef21e719c8 | |||
4a7cd2f085 | |||
27b124cce5 | |||
254e8b7063 | |||
31d45d6ab4 | |||
c122dea6a9 | |||
2ad5f0883c | |||
8f2d1ef6f4 | |||
d6214d5369 | |||
74ededf913 | |||
8ffed75251 | |||
73a6f1d58c | |||
2540e49a31 | |||
d612cf01b6 | |||
44c1b1bb72 | |||
e3895f3c9c | |||
e00142926e | |||
4c6ef71a17 | |||
7b32b9bbcb | |||
9def90daa7 | |||
c528b8831a | |||
30756ad4aa | |||
8aa4f3577a | |||
6416356d89 | |||
bc2790fd4d | |||
b0ceb38e22 | |||
377be7c3b7 | |||
96c07c0eb3 | |||
010e600aa6 | |||
cf46032f24 | |||
ff27eb5eed | |||
aecfb27d6e | |||
686feaf66b | |||
c658ea133d | |||
cf56e8f6fe | |||
4bec5443a2 | |||
ee7c62c3b7 | |||
42a3addd9a | |||
7c71ce59bd | |||
858ae28eb3 | |||
55b259061b | |||
e2746d5105 | |||
1dd752f354 | |||
22d499d38b | |||
d0830e4631 | |||
ef85d30846 | |||
36d7c43bbd | |||
3ff8067c51 | |||
4d3af37d5f | |||
0f96f39f24 | |||
b32bdb5a70 | |||
6956e4c487 | |||
acb1c711e5 | |||
ec0bd91aa3 | |||
de786c2b51 | |||
558f600271 | |||
5c680a8bea | |||
74f4d4854a | |||
4b49b5517d | |||
0cd623482c | |||
0a01b95c85 | |||
6daf2495a8 | |||
8771a378da | |||
e84547c8ca | |||
872dcd0668 | |||
a9f74fcd3c | |||
95752703ff | |||
5e19a62c7e | |||
2694829b6e | |||
151e1b7864 | |||
5965b1d626 | |||
219b80377d | |||
8315a27ea8 | |||
d070c7731a | |||
c8f614e2d2 | |||
4b07c11bc3 | |||
0bc00472d7 | |||
37da4f2c3e | |||
74505c1554 | |||
1869b36e09 | |||
13808d0103 | |||
4a3803df51 | |||
d58d4642af | |||
c3fa5195d3 | |||
c4a9a541d3 | |||
8db9e020c8 | |||
42277699a7 | |||
a1b78db750 | |||
7ebbf5661a | |||
2ff08141ae | |||
297e0629a4 | |||
213a80bd28 | |||
b111c4a1b9 | |||
db43ef628f | |||
ccf9f41f4e | |||
fb9e694919 | |||
ea70170d2b | |||
36af52bf7e | |||
0b6461eeb5 | |||
b0c936cc34 | |||
d51174e8fd | |||
e2a30dad52 | |||
74d3957cf8 | |||
cfc35fbec6 | |||
b1d3f8ddc9 | |||
3b04b39d66 | |||
cdcb07a3f7 | |||
19523acc67 | |||
faa8e6a13b | |||
0fed206df6 | |||
7660953e6b | |||
4a5d9fa65b | |||
83c0285204 | |||
3823d959e8 | |||
6c302712d4 | |||
f93677b420 | |||
23cd62820c | |||
39d410b050 | |||
61c67d78da | |||
64d32e2688 | |||
1aba6948ee | |||
3b9103e9aa | |||
db3158d4e7 | |||
08a970853a | |||
7cbfafa5c5 | |||
c64f392fe7 | |||
4fef4ca2c6 | |||
268c2018ae | |||
b7e8e1fa37 | |||
fe6af27813 | |||
a2005c55aa | |||
da446c5073 | |||
d3bc2bea4f | |||
37fcdb81bc | |||
b3041d9ca7 | |||
7c8f20623c | |||
8b07ace876 | |||
073b2aca04 | |||
934795abd8 | |||
c3965c9528 | |||
5164ce1f02 | |||
18cdb20923 | |||
85a61dfdc0 | |||
c094097af7 | |||
39ba7d53dd | |||
98fc037f73 | |||
955f657298 | |||
853f9d901e | |||
d0c2b4d703 | |||
1af3838ebc | |||
d0bbf8f181 | |||
8c8a5c9762 | |||
b0ea0668c7 | |||
4e1de0c886 | |||
244eb1be07 | |||
06f9fcc427 | |||
784deaf9f4 | |||
3b63cafa79 | |||
0fe672c9da | |||
37ab6e9132 | |||
53afb4ee6f | |||
1783527f39 | |||
e7a679541b | |||
32b2185e94 | |||
c1d46a6e6b | |||
1c43d83bd4 | |||
6fb27d52d6 | |||
092dba3f4b | |||
6044aed46f | |||
ef4e6f57d9 | |||
974b4aeb48 | |||
4466c9f018 | |||
0ba2590bfd | |||
cccf62bb53 | |||
649169c192 | |||
1caced26d6 | |||
163c97b2f5 | |||
7a28f0360d | |||
a5ed2cb9e3 | |||
35333324ed | |||
2d36be07d2 | |||
183f26e5c3 | |||
2452d31b9a | |||
4549043a08 | |||
47986df47e | |||
abefd93be5 |
@ -11,30 +11,21 @@ env:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240215
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-debug-rowt-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-debug-rowt-
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build
|
||||
cd frontend && npm install && npm run build
|
||||
- name: Frontend tests
|
||||
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line
|
||||
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line
|
||||
- name: Backend tests
|
||||
run: cargo test --verbose
|
||||
#- uses: actions/upload-artifact@v3
|
||||
@ -46,7 +37,7 @@ jobs:
|
||||
|
||||
deploy-staging:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240215
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
steps:
|
||||
@ -56,17 +47,9 @@ jobs:
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-release-rowt-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-release-rowt-
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release --target $CARGO_TARGET
|
||||
@ -97,7 +80,7 @@ jobs:
|
||||
|
||||
deploy-main:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240215
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
@ -107,17 +90,8 @@ jobs:
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-release-rowt-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-release-rowt-
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
1
.gitignore
vendored
@ -5,3 +5,4 @@ Rocket.toml
|
||||
frontend/node_modules/*
|
||||
/static/
|
||||
/data-ergo/
|
||||
usage.txt
|
||||
|
707
Cargo.lock
generated
@ -10,7 +10,7 @@ rest = []
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
env_logger = "0.11"
|
||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
|
||||
@ -18,12 +18,16 @@ argon2 = "0.5"
|
||||
serde = { version = "1.0", features = [ "derive" ]}
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"]}
|
||||
chrono-tz = "0.8"
|
||||
chrono-tz = "0.9"
|
||||
tera = { version = "1.18", features = ["date-locale"], optional = true}
|
||||
ics = "0.5"
|
||||
futures = "0.3"
|
||||
lettre = "0.11"
|
||||
csv = "1.3"
|
||||
itertools = "0.13"
|
||||
job_scheduler_ng = "2.0"
|
||||
ureq = { version = "2.9", features = ["json"] }
|
||||
regex = "1.10"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
openssl = { version = "0.10", features = [ "vendored" ] }
|
||||
|
@ -5,7 +5,7 @@
|
||||
# 2. Tag the image: `docker tag <id> git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
|
||||
# 3. Push the image: `docker push git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
|
||||
|
||||
FROM rust:1.76
|
||||
FROM rust:1.77.2
|
||||
|
||||
RUN apt-get update && apt-get install -y sqlite3
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
# Build
|
||||
## Frontend
|
||||
1. `cd frontend`
|
||||
|
@ -3,3 +3,5 @@ secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk
|
||||
rss_key = "rss-key-for-ci"
|
||||
limits = { file = "10 MiB", data-form = "10 MiB"}
|
||||
smtp_pw = "8kIjlLH79Ky6D3jQ"
|
||||
usage_log_path = "./usage.txt"
|
||||
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"
|
||||
|
494
db.svg
Normal file
@ -0,0 +1,494 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 10.0.1 (0)
|
||||
-->
|
||||
<!-- Title: undefined Pages: 1 -->
|
||||
<svg width="2246pt" height="2402pt"
|
||||
viewBox="0.00 0.00 2245.60 2401.85" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(28.8 2350.8)">
|
||||
<title>undefined</title>
|
||||
<polygon fill="white" stroke="none" points="-28.8,51.05 -28.8,-2350.8 2216.8,-2350.8 2216.8,51.05 -28.8,51.05"/>
|
||||
<text text-anchor="start" x="1064.75" y="14.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">db.sqlite</text>
|
||||
<!-- user -->
|
||||
<g id="node1" class="node">
|
||||
<title>user</title>
|
||||
<path fill="none" stroke="black" d="M930.94,-936.33C930.94,-936.33 1074.02,-936.33 1074.02,-936.33 1080.02,-936.33 1086.02,-942.33 1086.02,-948.33 1086.02,-948.33 1086.02,-1306.6 1086.02,-1306.6 1086.02,-1312.6 1080.02,-1318.6 1074.02,-1318.6 1074.02,-1318.6 930.94,-1318.6 930.94,-1318.6 924.94,-1318.6 918.94,-1312.6 918.94,-1306.6 918.94,-1306.6 918.94,-948.33 918.94,-948.33 918.94,-942.33 924.94,-936.33 930.94,-936.33"/>
|
||||
<text text-anchor="start" x="986.35" y="-1298.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user</text>
|
||||
<polyline fill="none" stroke="black" points="918.94,-1288.84 1086.02,-1288.84"/>
|
||||
<text text-anchor="start" x="925.98" y="-1273.56" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="946.23" y="-1273.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="925.98" y="-1255.31" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="964.23" y="-1255.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1237.06" font-family="Helvetica,sans-Serif" font-size="12.00">pw </text>
|
||||
<text text-anchor="start" x="946.98" y="-1237.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1218.81" font-family="Helvetica,sans-Serif" font-size="12.00">deleted </text>
|
||||
<text text-anchor="start" x="974.73" y="-1218.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="925.98" y="-1200.56" font-family="Helvetica,sans-Serif" font-size="12.00">last_access </text>
|
||||
<text text-anchor="start" x="997.23" y="-1200.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="925.98" y="-1182.31" font-family="Helvetica,sans-Serif" font-size="12.00">dob </text>
|
||||
<text text-anchor="start" x="952.23" y="-1182.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1164.06" font-family="Helvetica,sans-Serif" font-size="12.00">weight </text>
|
||||
<text text-anchor="start" x="969.48" y="-1164.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1145.81" font-family="Helvetica,sans-Serif" font-size="12.00">sex </text>
|
||||
<text text-anchor="start" x="949.98" y="-1145.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1127.56" font-family="Helvetica,sans-Serif" font-size="12.00">dirty_thirty </text>
|
||||
<text text-anchor="start" x="994.23" y="-1127.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1109.31" font-family="Helvetica,sans-Serif" font-size="12.00">dirty_dozen </text>
|
||||
<text text-anchor="start" x="998.73" y="-1109.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1091.06" font-family="Helvetica,sans-Serif" font-size="12.00">member_since_date </text>
|
||||
<text text-anchor="start" x="1051.23" y="-1091.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1072.81" font-family="Helvetica,sans-Serif" font-size="12.00">birthdate </text>
|
||||
<text text-anchor="start" x="984.48" y="-1072.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1054.56" font-family="Helvetica,sans-Serif" font-size="12.00">mail </text>
|
||||
<text text-anchor="start" x="955.23" y="-1054.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1036.31" font-family="Helvetica,sans-Serif" font-size="12.00">nickname </text>
|
||||
<text text-anchor="start" x="988.23" y="-1036.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1018.06" font-family="Helvetica,sans-Serif" font-size="12.00">notes </text>
|
||||
<text text-anchor="start" x="962.73" y="-1018.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-999.81" font-family="Helvetica,sans-Serif" font-size="12.00">phone </text>
|
||||
<text text-anchor="start" x="967.23" y="-999.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-981.56" font-family="Helvetica,sans-Serif" font-size="12.00">address </text>
|
||||
<text text-anchor="start" x="976.23" y="-981.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-963.31" font-family="Helvetica,sans-Serif" font-size="12.00">family_id </text>
|
||||
<text text-anchor="start" x="982.98" y="-963.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="925.98" y="-945.06" font-family="Helvetica,sans-Serif" font-size="12.00">membership_pdf </text>
|
||||
<text text-anchor="start" x="1030.98" y="-945.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">blob</text>
|
||||
</g>
|
||||
<!-- family -->
|
||||
<g id="node16" class="node">
|
||||
<title>family</title>
|
||||
<path fill="none" stroke="black" d="M263.86,-1264.04C263.86,-1264.04 383.94,-1264.04 383.94,-1264.04 389.94,-1264.04 395.94,-1270.04 395.94,-1276.04 395.94,-1276.04 395.94,-1305.81 395.94,-1305.81 395.94,-1311.81 389.94,-1317.81 383.94,-1317.81 383.94,-1317.81 263.86,-1317.81 263.86,-1317.81 257.86,-1317.81 251.86,-1311.81 251.86,-1305.81 251.86,-1305.81 251.86,-1276.04 251.86,-1276.04 251.86,-1270.04 257.86,-1264.04 263.86,-1264.04"/>
|
||||
<text text-anchor="start" x="301.03" y="-1298.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">family</text>
|
||||
<polyline fill="none" stroke="black" points="251.86,-1288.05 395.94,-1288.05"/>
|
||||
<text text-anchor="start" x="258.9" y="-1272.77" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="279.15" y="-1272.77" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- user->family -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>user->family</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M918.59,-1147.68C787.22,-1179.32 534.59,-1240.17 404.97,-1271.4"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="404.68,-1268.59 397.56,-1273.18 405.99,-1274.03 404.68,-1268.59"/>
|
||||
</g>
|
||||
<!-- trip_details -->
|
||||
<g id="node2" class="node">
|
||||
<title>trip_details</title>
|
||||
<path fill="none" stroke="black" d="M1115.28,-1807.64C1115.28,-1807.64 1269.61,-1807.64 1269.61,-1807.64 1275.61,-1807.64 1281.61,-1813.64 1281.61,-1819.64 1281.61,-1819.64 1281.61,-1995.41 1281.61,-1995.41 1281.61,-2001.41 1275.61,-2007.41 1269.61,-2007.41 1269.61,-2007.41 1115.28,-2007.41 1115.28,-2007.41 1109.28,-2007.41 1103.28,-2001.41 1103.28,-1995.41 1103.28,-1995.41 1103.28,-1819.64 1103.28,-1819.64 1103.28,-1813.64 1109.28,-1807.64 1115.28,-1807.64"/>
|
||||
<text text-anchor="start" x="1151.19" y="-1987.68" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip_details</text>
|
||||
<polyline fill="none" stroke="black" points="1103.28,-1977.65 1281.61,-1977.65"/>
|
||||
<text text-anchor="start" x="1110.32" y="-1962.37" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1130.57" y="-1962.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1944.12" font-family="Helvetica,sans-Serif" font-size="12.00">planned_starting_time </text>
|
||||
<text text-anchor="start" x="1246.82" y="-1944.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1925.87" font-family="Helvetica,sans-Serif" font-size="12.00">max_people </text>
|
||||
<text text-anchor="start" x="1186.82" y="-1925.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1907.62" font-family="Helvetica,sans-Serif" font-size="12.00">day </text>
|
||||
<text text-anchor="start" x="1135.82" y="-1907.62" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1889.37" font-family="Helvetica,sans-Serif" font-size="12.00">notes </text>
|
||||
<text text-anchor="start" x="1147.07" y="-1889.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1871.12" font-family="Helvetica,sans-Serif" font-size="12.00">trip_type_id </text>
|
||||
<text text-anchor="start" x="1183.07" y="-1871.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1852.87" font-family="Helvetica,sans-Serif" font-size="12.00">allow_guests </text>
|
||||
<text text-anchor="start" x="1189.82" y="-1852.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1834.62" font-family="Helvetica,sans-Serif" font-size="12.00">always_show </text>
|
||||
<text text-anchor="start" x="1191.32" y="-1834.62" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1816.37" font-family="Helvetica,sans-Serif" font-size="12.00">is_locked </text>
|
||||
<text text-anchor="start" x="1168.07" y="-1816.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
</g>
|
||||
<!-- trip_type -->
|
||||
<g id="node5" class="node">
|
||||
<title>trip_type</title>
|
||||
<path fill="none" stroke="black" d="M1040.53,-2194.35C1040.53,-2194.35 1160.61,-2194.35 1160.61,-2194.35 1166.61,-2194.35 1172.61,-2200.35 1172.61,-2206.35 1172.61,-2206.35 1172.61,-2309.12 1172.61,-2309.12 1172.61,-2315.12 1166.61,-2321.12 1160.61,-2321.12 1160.61,-2321.12 1040.53,-2321.12 1040.53,-2321.12 1034.53,-2321.12 1028.53,-2315.12 1028.53,-2309.12 1028.53,-2309.12 1028.53,-2206.35 1028.53,-2206.35 1028.53,-2200.35 1034.53,-2194.35 1040.53,-2194.35"/>
|
||||
<text text-anchor="start" x="1067.95" y="-2301.39" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip_type</text>
|
||||
<polyline fill="none" stroke="black" points="1028.53,-2291.36 1172.61,-2291.36"/>
|
||||
<text text-anchor="start" x="1035.57" y="-2276.08" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1055.82" y="-2276.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2257.83" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="1073.82" y="-2257.83" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2239.58" font-family="Helvetica,sans-Serif" font-size="12.00">desc </text>
|
||||
<text text-anchor="start" x="1067.07" y="-2239.58" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2221.33" font-family="Helvetica,sans-Serif" font-size="12.00">question </text>
|
||||
<text text-anchor="start" x="1090.32" y="-2221.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2203.08" font-family="Helvetica,sans-Serif" font-size="12.00">icon </text>
|
||||
<text text-anchor="start" x="1064.07" y="-2203.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- trip_details->trip_type -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>trip_details->trip_type</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1166.12,-2007.88C1151.28,-2064.41 1133.1,-2133.75 1119.65,-2185.02"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1116.94,-2184.29 1117.62,-2192.74 1122.36,-2185.71 1116.94,-2184.29"/>
|
||||
</g>
|
||||
<!-- planned_event -->
|
||||
<g id="node3" class="node">
|
||||
<title>planned_event</title>
|
||||
<path fill="none" stroke="black" d="M853.81,-1400.29C853.81,-1400.29 1088.39,-1400.29 1088.39,-1400.29 1094.39,-1400.29 1100.39,-1406.29 1100.39,-1412.29 1100.39,-1412.29 1100.39,-1515.06 1100.39,-1515.06 1100.39,-1521.06 1094.39,-1527.06 1088.39,-1527.06 1088.39,-1527.06 853.81,-1527.06 853.81,-1527.06 847.81,-1527.06 841.81,-1521.06 841.81,-1515.06 841.81,-1515.06 841.81,-1412.29 841.81,-1412.29 841.81,-1406.29 847.81,-1400.29 853.81,-1400.29"/>
|
||||
<text text-anchor="start" x="917.85" y="-1507.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">planned_event</text>
|
||||
<polyline fill="none" stroke="black" points="841.81,-1497.3 1100.39,-1497.3"/>
|
||||
<text text-anchor="start" x="848.85" y="-1482.02" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="869.1" y="-1482.02" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="848.85" y="-1463.77" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="887.1" y="-1463.77" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="848.85" y="-1445.52" font-family="Helvetica,sans-Serif" font-size="12.00">planned_amount_cox </text>
|
||||
<text text-anchor="start" x="979.35" y="-1445.52" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer unsigned</text>
|
||||
<text text-anchor="start" x="848.85" y="-1427.27" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
|
||||
<text text-anchor="start" x="934.35" y="-1427.27" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="848.85" y="-1409.02" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="916.35" y="-1409.02" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- planned_event->trip_details -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>planned_event->trip_details</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1002.82,-1527.27C1038.14,-1598.09 1095.84,-1713.81 1138.33,-1799.02"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1135.71,-1800.04 1141.79,-1805.95 1140.72,-1797.54 1135.71,-1800.04"/>
|
||||
</g>
|
||||
<!-- trip -->
|
||||
<g id="node4" class="node">
|
||||
<title>trip</title>
|
||||
<path fill="none" stroke="black" d="M571.83,-1689.61C571.83,-1689.61 718.66,-1689.61 718.66,-1689.61 724.66,-1689.61 730.66,-1695.61 730.66,-1701.61 730.66,-1701.61 730.66,-1804.38 730.66,-1804.38 730.66,-1810.38 724.66,-1816.38 718.66,-1816.38 718.66,-1816.38 571.83,-1816.38 571.83,-1816.38 565.83,-1816.38 559.83,-1810.38 559.83,-1804.38 559.83,-1804.38 559.83,-1701.61 559.83,-1701.61 559.83,-1695.61 565.83,-1689.61 571.83,-1689.61"/>
|
||||
<text text-anchor="start" x="632.12" y="-1796.65" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip</text>
|
||||
<polyline fill="none" stroke="black" points="559.83,-1786.62 730.66,-1786.62"/>
|
||||
<text text-anchor="start" x="566.87" y="-1771.34" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="587.12" y="-1771.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1753.09" font-family="Helvetica,sans-Serif" font-size="12.00">cox_id </text>
|
||||
<text text-anchor="start" x="607.37" y="-1753.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1734.84" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
|
||||
<text text-anchor="start" x="652.37" y="-1734.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1716.59" font-family="Helvetica,sans-Serif" font-size="12.00">planned_event_id </text>
|
||||
<text text-anchor="start" x="674.87" y="-1716.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1698.34" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="634.37" y="-1698.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- trip->user -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>trip->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M678.73,-1689.16C716.58,-1617.53 780.51,-1498.13 838.61,-1397.09 862.41,-1355.71 888.99,-1311.27 913.7,-1270.64"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="915.98,-1272.28 917.75,-1263.99 911.2,-1269.37 915.98,-1272.28"/>
|
||||
</g>
|
||||
<!-- trip->trip_details -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>trip->trip_details</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M731.04,-1777.23C829.53,-1805.04 990.77,-1850.57 1094.21,-1879.78"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1093.33,-1882.45 1101.79,-1881.93 1094.86,-1877.06 1093.33,-1882.45"/>
|
||||
</g>
|
||||
<!-- trip->planned_event -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>trip->planned_event</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M717.16,-1689.15C769.1,-1643.03 839.22,-1580.77 892.62,-1533.36"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="894.23,-1535.67 898.35,-1528.27 890.51,-1531.48 894.23,-1535.67"/>
|
||||
</g>
|
||||
<!-- location -->
|
||||
<g id="node6" class="node">
|
||||
<title>location</title>
|
||||
<path fill="none" stroke="black" d="M11.68,-686.78C11.68,-686.78 131.76,-686.78 131.76,-686.78 137.76,-686.78 143.76,-692.78 143.76,-698.78 143.76,-698.78 143.76,-746.8 143.76,-746.8 143.76,-752.8 137.76,-758.8 131.76,-758.8 131.76,-758.8 11.68,-758.8 11.68,-758.8 5.68,-758.8 -0.32,-752.8 -0.32,-746.8 -0.32,-746.8 -0.32,-698.78 -0.32,-698.78 -0.32,-692.78 5.68,-686.78 11.68,-686.78"/>
|
||||
<text text-anchor="start" x="42.47" y="-739.07" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">location</text>
|
||||
<polyline fill="none" stroke="black" points="-0.32,-729.04 143.76,-729.04"/>
|
||||
<text text-anchor="start" x="6.72" y="-713.76" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="26.97" y="-713.76" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="6.72" y="-695.51" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="44.97" y="-695.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- boat -->
|
||||
<g id="node7" class="node">
|
||||
<title>boat</title>
|
||||
<path fill="none" stroke="black" d="M665.61,-560.97C665.61,-560.97 912.94,-560.97 912.94,-560.97 918.94,-560.97 924.94,-566.97 924.94,-572.97 924.94,-572.97 924.94,-785.24 924.94,-785.24 924.94,-791.24 918.94,-797.24 912.94,-797.24 912.94,-797.24 665.61,-797.24 665.61,-797.24 659.61,-797.24 653.61,-791.24 653.61,-785.24 653.61,-785.24 653.61,-572.97 653.61,-572.97 653.61,-566.97 659.61,-560.97 665.61,-560.97"/>
|
||||
<text text-anchor="start" x="772.77" y="-777.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boat</text>
|
||||
<polyline fill="none" stroke="black" points="653.61,-767.48 924.94,-767.48"/>
|
||||
<text text-anchor="start" x="660.65" y="-752.2" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="680.9" y="-752.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-733.95" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="698.9" y="-733.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="660.65" y="-715.7" font-family="Helvetica,sans-Serif" font-size="12.00">amount_seats </text>
|
||||
<text text-anchor="start" x="748.4" y="-715.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-697.45" font-family="Helvetica,sans-Serif" font-size="12.00">location_id </text>
|
||||
<text text-anchor="start" x="728.15" y="-697.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-679.2" font-family="Helvetica,sans-Serif" font-size="12.00">owner </text>
|
||||
<text text-anchor="start" x="701.9" y="-679.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-660.95" font-family="Helvetica,sans-Serif" font-size="12.00">year_built </text>
|
||||
<text text-anchor="start" x="722.9" y="-660.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-642.7" font-family="Helvetica,sans-Serif" font-size="12.00">boatbuilder </text>
|
||||
<text text-anchor="start" x="732.65" y="-642.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="660.65" y="-624.45" font-family="Helvetica,sans-Serif" font-size="12.00">default_shipmaster_only_steering </text>
|
||||
<text text-anchor="start" x="864.65" y="-624.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="660.65" y="-606.2" font-family="Helvetica,sans-Serif" font-size="12.00">skull </text>
|
||||
<text text-anchor="start" x="690.65" y="-606.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="660.65" y="-587.95" font-family="Helvetica,sans-Serif" font-size="12.00">external </text>
|
||||
<text text-anchor="start" x="713.15" y="-587.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="660.65" y="-569.7" font-family="Helvetica,sans-Serif" font-size="12.00">default_destination </text>
|
||||
<text text-anchor="start" x="778.4" y="-569.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- boat->user -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>boat->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M845.57,-797.5C866.72,-841.98 891.3,-893.67 914.65,-942.76"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="911.98,-943.68 917.95,-949.7 917.04,-941.27 911.98,-943.68"/>
|
||||
</g>
|
||||
<!-- boat->location -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>boat->location</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M653.46,-687.38C505.96,-696.35 275.15,-710.41 153.33,-717.82"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="153.21,-715.02 145.4,-718.3 153.55,-720.61 153.21,-715.02"/>
|
||||
</g>
|
||||
<!-- logbook_type -->
|
||||
<g id="node8" class="node">
|
||||
<title>logbook_type</title>
|
||||
<path fill="none" stroke="black" d="M1052.95,-0.71C1052.95,-0.71 1173.03,-0.71 1173.03,-0.71 1179.03,-0.71 1185.03,-6.71 1185.03,-12.71 1185.03,-12.71 1185.03,-60.73 1185.03,-60.73 1185.03,-66.73 1179.03,-72.73 1173.03,-72.73 1173.03,-72.73 1052.95,-72.73 1052.95,-72.73 1046.95,-72.73 1040.95,-66.73 1040.95,-60.73 1040.95,-60.73 1040.95,-12.71 1040.95,-12.71 1040.95,-6.71 1046.95,-0.71 1052.95,-0.71"/>
|
||||
<text text-anchor="start" x="1064.24" y="-53" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">logbook_type</text>
|
||||
<polyline fill="none" stroke="black" points="1040.95,-42.97 1185.03,-42.97"/>
|
||||
<text text-anchor="start" x="1047.99" y="-27.69" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1068.24" y="-27.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1047.99" y="-9.44" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="1086.24" y="-9.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- log -->
|
||||
<g id="node9" class="node">
|
||||
<title>log</title>
|
||||
<path fill="none" stroke="black" d="M1569.2,-692.51C1569.2,-692.51 1689.28,-692.51 1689.28,-692.51 1695.28,-692.51 1701.28,-698.51 1701.28,-704.51 1701.28,-704.51 1701.28,-770.78 1701.28,-770.78 1701.28,-776.78 1695.28,-782.78 1689.28,-782.78 1689.28,-782.78 1569.2,-782.78 1569.2,-782.78 1563.2,-782.78 1557.2,-776.78 1557.2,-770.78 1557.2,-770.78 1557.2,-704.51 1557.2,-704.51 1557.2,-698.51 1563.2,-692.51 1569.2,-692.51"/>
|
||||
<text text-anchor="start" x="1617.99" y="-763.05" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">log</text>
|
||||
<polyline fill="none" stroke="black" points="1557.2,-753.02 1701.28,-753.02"/>
|
||||
<text text-anchor="start" x="1564.24" y="-737.74" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1584.49" y="-737.74" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1564.24" y="-719.49" font-family="Helvetica,sans-Serif" font-size="12.00">msg </text>
|
||||
<text text-anchor="start" x="1593.49" y="-719.49" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1564.24" y="-701.24" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="1631.74" y="-701.24" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
</g>
|
||||
<!-- boat_damage -->
|
||||
<g id="node10" class="node">
|
||||
<title>boat_damage</title>
|
||||
<path fill="none" stroke="black" d="M476.91,-984.61C476.91,-984.61 613.99,-984.61 613.99,-984.61 619.99,-984.61 625.99,-990.61 625.99,-996.61 625.99,-996.61 625.99,-1190.63 625.99,-1190.63 625.99,-1196.63 619.99,-1202.63 613.99,-1202.63 613.99,-1202.63 476.91,-1202.63 476.91,-1202.63 470.91,-1202.63 464.91,-1196.63 464.91,-1190.63 464.91,-1190.63 464.91,-996.61 464.91,-996.61 464.91,-990.61 470.91,-984.61 476.91,-984.61"/>
|
||||
<text text-anchor="start" x="496.32" y="-1182.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boat_damage</text>
|
||||
<polyline fill="none" stroke="black" points="464.91,-1172.87 625.99,-1172.87"/>
|
||||
<text text-anchor="start" x="471.95" y="-1157.59" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="492.2" y="-1157.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1139.34" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
|
||||
<text text-anchor="start" x="519.2" y="-1139.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1121.09" font-family="Helvetica,sans-Serif" font-size="12.00">desc </text>
|
||||
<text text-anchor="start" x="503.45" y="-1121.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="471.95" y="-1102.84" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_created </text>
|
||||
<text text-anchor="start" x="570.2" y="-1102.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1084.59" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="539.45" y="-1084.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="471.95" y="-1066.34" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_fixed </text>
|
||||
<text text-anchor="start" x="553.7" y="-1066.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1048.09" font-family="Helvetica,sans-Serif" font-size="12.00">fixed_at </text>
|
||||
<text text-anchor="start" x="522.95" y="-1048.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="471.95" y="-1029.84" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_verified </text>
|
||||
<text text-anchor="start" x="569.45" y="-1029.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1011.59" font-family="Helvetica,sans-Serif" font-size="12.00">verified_at </text>
|
||||
<text text-anchor="start" x="538.7" y="-1011.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="471.95" y="-993.34" font-family="Helvetica,sans-Serif" font-size="12.00">lock_boat </text>
|
||||
<text text-anchor="start" x="532.7" y="-993.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
</g>
|
||||
<!-- boat_damage->user -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>boat_damage->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
|
||||
</g>
|
||||
<!-- boat_damage->user -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>boat_damage->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
|
||||
</g>
|
||||
<!-- boat_damage->user -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>boat_damage->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
|
||||
</g>
|
||||
<!-- boat_damage->boat -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>boat_damage->boat</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M609.83,-984.17C642.1,-929.3 681.35,-862.57 714.91,-805.53"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="717.27,-807.04 718.91,-798.72 712.44,-804.2 717.27,-807.04"/>
|
||||
</g>
|
||||
<!-- user_trip -->
|
||||
<g id="node11" class="node">
|
||||
<title>user_trip</title>
|
||||
<path fill="none" stroke="black" d="M1355.83,-1397.72C1355.83,-1397.72 1480.16,-1397.72 1480.16,-1397.72 1486.16,-1397.72 1492.16,-1403.72 1492.16,-1409.72 1492.16,-1409.72 1492.16,-1494.24 1492.16,-1494.24 1492.16,-1500.24 1486.16,-1506.24 1480.16,-1506.24 1480.16,-1506.24 1355.83,-1506.24 1355.83,-1506.24 1349.83,-1506.24 1343.83,-1500.24 1343.83,-1494.24 1343.83,-1494.24 1343.83,-1409.72 1343.83,-1409.72 1343.83,-1403.72 1349.83,-1397.72 1355.83,-1397.72"/>
|
||||
<text text-anchor="start" x="1385.37" y="-1486.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user_trip</text>
|
||||
<polyline fill="none" stroke="black" points="1343.83,-1476.48 1492.16,-1476.48"/>
|
||||
<text text-anchor="start" x="1350.87" y="-1461.2" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
|
||||
<text text-anchor="start" x="1397.37" y="-1461.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1350.87" y="-1442.95" font-family="Helvetica,sans-Serif" font-size="12.00">user_note </text>
|
||||
<text text-anchor="start" x="1413.87" y="-1442.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1350.87" y="-1424.7" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
|
||||
<text text-anchor="start" x="1436.37" y="-1424.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1350.87" y="-1406.45" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="1418.37" y="-1406.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- user_trip->user -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>user_trip->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1347.97,-1397.3C1278.07,-1342.7 1170.3,-1258.53 1093.73,-1198.73"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1095.52,-1196.58 1087.49,-1193.86 1092.07,-1200.99 1095.52,-1196.58"/>
|
||||
</g>
|
||||
<!-- user_trip->trip_details -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>user_trip->trip_details</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1391.01,-1506.48C1355.37,-1578.46 1291.79,-1706.87 1246.17,-1799.01"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1243.73,-1797.62 1242.69,-1806.03 1248.75,-1800.1 1243.73,-1797.62"/>
|
||||
</g>
|
||||
<!-- rower -->
|
||||
<g id="node12" class="node">
|
||||
<title>rower</title>
|
||||
<path fill="none" stroke="black" d="M1324.54,-940.65C1324.54,-940.65 1444.62,-940.65 1444.62,-940.65 1450.62,-940.65 1456.62,-946.65 1456.62,-952.65 1456.62,-952.65 1456.62,-1000.67 1456.62,-1000.67 1456.62,-1006.67 1450.62,-1012.67 1444.62,-1012.67 1444.62,-1012.67 1324.54,-1012.67 1324.54,-1012.67 1318.54,-1012.67 1312.54,-1006.67 1312.54,-1000.67 1312.54,-1000.67 1312.54,-952.65 1312.54,-952.65 1312.54,-946.65 1318.54,-940.65 1324.54,-940.65"/>
|
||||
<text text-anchor="start" x="1362.83" y="-992.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">rower</text>
|
||||
<polyline fill="none" stroke="black" points="1312.54,-982.91 1456.62,-982.91"/>
|
||||
<text text-anchor="start" x="1319.58" y="-967.63" font-family="Helvetica,sans-Serif" font-size="12.00">logbook_id </text>
|
||||
<text text-anchor="start" x="1387.08" y="-967.63" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1319.58" y="-949.38" font-family="Helvetica,sans-Serif" font-size="12.00">rower_id </text>
|
||||
<text text-anchor="start" x="1374.33" y="-949.38" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- rower->user -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>rower->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1312.26,-1005.2C1250.64,-1029.52 1161.71,-1064.62 1094.93,-1090.98"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1094.17,-1088.27 1087.75,-1093.81 1096.22,-1093.48 1094.17,-1088.27"/>
|
||||
</g>
|
||||
<!-- logbook -->
|
||||
<g id="node13" class="node">
|
||||
<title>logbook</title>
|
||||
<path fill="none" stroke="black" d="M968.96,-391.46C968.96,-391.46 1168.29,-391.46 1168.29,-391.46 1174.29,-391.46 1180.29,-397.46 1180.29,-403.46 1180.29,-403.46 1180.29,-615.73 1180.29,-615.73 1180.29,-621.73 1174.29,-627.73 1168.29,-627.73 1168.29,-627.73 968.96,-627.73 968.96,-627.73 962.96,-627.73 956.96,-621.73 956.96,-615.73 956.96,-615.73 956.96,-403.46 956.96,-403.46 956.96,-397.46 962.96,-391.46 968.96,-391.46"/>
|
||||
<text text-anchor="start" x="1039.38" y="-608" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">logbook</text>
|
||||
<polyline fill="none" stroke="black" points="956.96,-597.97 1180.29,-597.97"/>
|
||||
<text text-anchor="start" x="964" y="-582.69" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="984.25" y="-582.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-564.44" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
|
||||
<text text-anchor="start" x="1011.25" y="-564.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-546.19" font-family="Helvetica,sans-Serif" font-size="12.00">shipmaster </text>
|
||||
<text text-anchor="start" x="1034.5" y="-546.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-527.94" font-family="Helvetica,sans-Serif" font-size="12.00">steering_person </text>
|
||||
<text text-anchor="start" x="1063.75" y="-527.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-509.69" font-family="Helvetica,sans-Serif" font-size="12.00">shipmaster_only_steering </text>
|
||||
<text text-anchor="start" x="1120" y="-509.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="964" y="-491.44" font-family="Helvetica,sans-Serif" font-size="12.00">departure </text>
|
||||
<text text-anchor="start" x="1027" y="-491.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="964" y="-473.19" font-family="Helvetica,sans-Serif" font-size="12.00">arrival </text>
|
||||
<text text-anchor="start" x="1005.25" y="-473.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="964" y="-454.94" font-family="Helvetica,sans-Serif" font-size="12.00">destination </text>
|
||||
<text text-anchor="start" x="1033.75" y="-454.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="964" y="-436.69" font-family="Helvetica,sans-Serif" font-size="12.00">distance_in_km </text>
|
||||
<text text-anchor="start" x="1059.25" y="-436.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-418.44" font-family="Helvetica,sans-Serif" font-size="12.00">comments </text>
|
||||
<text text-anchor="start" x="1031.5" y="-418.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="964" y="-400.19" font-family="Helvetica,sans-Serif" font-size="12.00">logtype </text>
|
||||
<text text-anchor="start" x="1012" y="-400.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- rower->logbook -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>rower->logbook</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1360.05,-940.4C1316.38,-875.84 1223.23,-738.14 1153.93,-635.7"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1156.34,-634.26 1149.54,-629.2 1151.7,-637.4 1156.34,-634.26"/>
|
||||
</g>
|
||||
<!-- logbook->user -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>logbook->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1055.95,-628C1046.91,-712.44 1034.54,-828.03 1023.97,-926.76"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1021.2,-926.34 1023.13,-934.6 1026.76,-926.94 1021.2,-926.34"/>
|
||||
</g>
|
||||
<!-- logbook->user -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>logbook->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1055.95,-628C1046.91,-712.44 1034.54,-828.03 1023.97,-926.76"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1021.2,-926.34 1023.13,-934.6 1026.76,-926.94 1021.2,-926.34"/>
|
||||
</g>
|
||||
<!-- logbook->boat -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>logbook->boat</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M956.61,-577.57C948.87,-582.26 941.01,-587.03 933.13,-591.81"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="931.81,-589.34 926.42,-595.89 934.71,-594.13 931.81,-589.34"/>
|
||||
</g>
|
||||
<!-- logbook->logbook_type -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>logbook->logbook_type</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1079.74,-391.18C1089.13,-291.04 1102.09,-152.9 1108.72,-82.29"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1111.49,-82.78 1109.45,-74.56 1105.91,-82.26 1111.49,-82.78"/>
|
||||
</g>
|
||||
<!-- role -->
|
||||
<g id="node14" class="node">
|
||||
<title>role</title>
|
||||
<path fill="none" stroke="black" d="M2055.95,-1656.37C2055.95,-1656.37 2176.03,-1656.37 2176.03,-1656.37 2182.03,-1656.37 2188.03,-1662.37 2188.03,-1668.37 2188.03,-1668.37 2188.03,-1716.39 2188.03,-1716.39 2188.03,-1722.39 2182.03,-1728.39 2176.03,-1728.39 2176.03,-1728.39 2055.95,-1728.39 2055.95,-1728.39 2049.95,-1728.39 2043.95,-1722.39 2043.95,-1716.39 2043.95,-1716.39 2043.95,-1668.37 2043.95,-1668.37 2043.95,-1662.37 2049.95,-1656.37 2055.95,-1656.37"/>
|
||||
<text text-anchor="start" x="2101.36" y="-1708.66" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">role</text>
|
||||
<polyline fill="none" stroke="black" points="2043.95,-1698.63 2188.03,-1698.63"/>
|
||||
<text text-anchor="start" x="2050.99" y="-1683.35" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="2071.24" y="-1683.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="2050.99" y="-1665.1" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="2089.24" y="-1665.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- user_role -->
|
||||
<g id="node15" class="node">
|
||||
<title>user_role</title>
|
||||
<path fill="none" stroke="black" d="M1713.26,-1155.87C1713.26,-1155.87 1833.34,-1155.87 1833.34,-1155.87 1839.34,-1155.87 1845.34,-1161.87 1845.34,-1167.87 1845.34,-1167.87 1845.34,-1215.89 1845.34,-1215.89 1845.34,-1221.89 1839.34,-1227.89 1833.34,-1227.89 1833.34,-1227.89 1713.26,-1227.89 1713.26,-1227.89 1707.26,-1227.89 1701.26,-1221.89 1701.26,-1215.89 1701.26,-1215.89 1701.26,-1167.87 1701.26,-1167.87 1701.26,-1161.87 1707.26,-1155.87 1713.26,-1155.87"/>
|
||||
<text text-anchor="start" x="1739.18" y="-1208.16" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user_role</text>
|
||||
<polyline fill="none" stroke="black" points="1701.26,-1198.13 1845.34,-1198.13"/>
|
||||
<text text-anchor="start" x="1708.3" y="-1182.85" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
|
||||
<text text-anchor="start" x="1754.8" y="-1182.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1708.3" y="-1164.6" font-family="Helvetica,sans-Serif" font-size="12.00">role_id </text>
|
||||
<text text-anchor="start" x="1751.05" y="-1164.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- user_role->user -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>user_role->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1700.78,-1185.82C1560.23,-1174.08 1250.92,-1148.23 1095.49,-1135.24"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1095.95,-1132.47 1087.75,-1134.59 1095.48,-1138.05 1095.95,-1132.47"/>
|
||||
</g>
|
||||
<!-- user_role->role -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>user_role->role</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1798.22,-1228.28C1859.85,-1318.3 2019.25,-1551.1 2085.95,-1648.51"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="2083.56,-1649.98 2090.39,-1655 2088.18,-1646.81 2083.56,-1649.98"/>
|
||||
</g>
|
||||
<!-- boathouse -->
|
||||
<g id="node17" class="node">
|
||||
<title>boathouse</title>
|
||||
<path fill="none" stroke="black" d="M1461.39,-327.62C1461.39,-327.62 1581.47,-327.62 1581.47,-327.62 1587.47,-327.62 1593.47,-333.62 1593.47,-339.62 1593.47,-339.62 1593.47,-442.39 1593.47,-442.39 1593.47,-448.39 1587.47,-454.39 1581.47,-454.39 1581.47,-454.39 1461.39,-454.39 1461.39,-454.39 1455.39,-454.39 1449.39,-448.39 1449.39,-442.39 1449.39,-442.39 1449.39,-339.62 1449.39,-339.62 1449.39,-333.62 1455.39,-327.62 1461.39,-327.62"/>
|
||||
<text text-anchor="start" x="1483.18" y="-434.66" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boathouse</text>
|
||||
<polyline fill="none" stroke="black" points="1449.39,-424.63 1593.47,-424.63"/>
|
||||
<text text-anchor="start" x="1456.43" y="-409.35" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1476.68" y="-409.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1456.43" y="-391.1" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
|
||||
<text text-anchor="start" x="1503.68" y="-391.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1456.43" y="-372.85" font-family="Helvetica,sans-Serif" font-size="12.00">aisle </text>
|
||||
<text text-anchor="start" x="1487.18" y="-372.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1456.43" y="-354.6" font-family="Helvetica,sans-Serif" font-size="12.00">side </text>
|
||||
<text text-anchor="start" x="1484.18" y="-354.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1456.43" y="-336.35" font-family="Helvetica,sans-Serif" font-size="12.00">level </text>
|
||||
<text text-anchor="start" x="1487.93" y="-336.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- boathouse->boat -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>boathouse->boat</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1453.7,-454.77C1389.44,-511.19 1287.49,-590.65 1183.49,-630.93 1104.56,-661.5 1010.68,-673.83 934.33,-678.38"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="934.3,-675.58 926.47,-678.82 934.62,-681.17 934.3,-675.58"/>
|
||||
</g>
|
||||
<!-- notification -->
|
||||
<g id="node18" class="node">
|
||||
<title>notification</title>
|
||||
<path fill="none" stroke="black" d="M523.13,-247.96C523.13,-247.96 643.21,-247.96 643.21,-247.96 649.21,-247.96 655.21,-253.96 655.21,-259.96 655.21,-259.96 655.21,-399.23 655.21,-399.23 655.21,-405.23 649.21,-411.23 643.21,-411.23 643.21,-411.23 523.13,-411.23 523.13,-411.23 517.13,-411.23 511.13,-405.23 511.13,-399.23 511.13,-399.23 511.13,-259.96 511.13,-259.96 511.13,-253.96 517.13,-247.96 523.13,-247.96"/>
|
||||
<text text-anchor="start" x="541.55" y="-391.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">notification</text>
|
||||
<polyline fill="none" stroke="black" points="511.13,-381.47 655.21,-381.47"/>
|
||||
<text text-anchor="start" x="518.17" y="-366.19" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="538.42" y="-366.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="518.17" y="-347.94" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
|
||||
<text text-anchor="start" x="564.67" y="-347.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="518.17" y="-329.69" font-family="Helvetica,sans-Serif" font-size="12.00">message </text>
|
||||
<text text-anchor="start" x="575.92" y="-329.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="518.17" y="-311.44" font-family="Helvetica,sans-Serif" font-size="12.00">read_at </text>
|
||||
<text text-anchor="start" x="566.92" y="-311.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="518.17" y="-293.19" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="585.67" y="-293.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="518.17" y="-274.94" font-family="Helvetica,sans-Serif" font-size="12.00">category </text>
|
||||
<text text-anchor="start" x="575.17" y="-274.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="518.17" y="-256.69" font-family="Helvetica,sans-Serif" font-size="12.00">link </text>
|
||||
<text text-anchor="start" x="542.17" y="-256.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- notification->user -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>notification->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M578.8,-411.63C577.06,-509.73 585.42,-676.25 650.41,-800.44 710.85,-915.96 828.71,-1012.32 911.35,-1069.85"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="909.44,-1071.94 917.62,-1074.18 912.63,-1067.33 909.44,-1071.94"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 48 KiB |
5
fd
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
scp read@128.140.64.118:/home/rowing/db.sqlite db.sqlite
|
||||
#sqlite3 db.sqlite < seeds.sql
|
||||
|
@ -5,8 +5,10 @@ export interface choiceMap {
|
||||
[details: string]: Choices;
|
||||
}
|
||||
|
||||
declare var loggedin_user_id: string;
|
||||
let choiceObjects: choiceMap = {};
|
||||
let boat_in_ottensheim = true;
|
||||
let boat_reserved_today= true;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
changeTheme();
|
||||
@ -116,6 +118,9 @@ interface ChoiceBoatEvent extends Event {
|
||||
owner: number;
|
||||
default_destination: string;
|
||||
boat_in_ottensheim: boolean;
|
||||
boat_reserved_today: boolean;
|
||||
default_handoperated: boolean;
|
||||
convert_handoperated_possible: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -134,12 +139,30 @@ function selectBoatChange() {
|
||||
boatSelect.addEventListener(
|
||||
"addItem",
|
||||
function (e) {
|
||||
|
||||
const event = e as ChoiceBoatEvent;
|
||||
boat_reserved_today = event.detail.customProperties.boat_reserved_today;
|
||||
if (boat_reserved_today){
|
||||
alert(event.detail.label.trim()+' wurde heute reserviert. Bitte kontrolliere, dass du die Reservierung nicht störst.');
|
||||
}
|
||||
boat_in_ottensheim = event.detail.customProperties.boat_in_ottensheim;
|
||||
|
||||
const amount_seats = event.detail.customProperties.amount_seats;
|
||||
setMaxAmountRowers("newrower", amount_seats);
|
||||
|
||||
let only_steering = <HTMLSelectElement>document.querySelector('#shipmaster_only_steering');
|
||||
if (event.detail.customProperties.default_handoperated) {
|
||||
only_steering.setAttribute('checked', 'true');
|
||||
}else {
|
||||
only_steering.removeAttribute('checked');
|
||||
}
|
||||
|
||||
if (event.detail.customProperties.convert_handoperated_possible) {
|
||||
only_steering.removeAttribute('disabled');
|
||||
}else {
|
||||
only_steering.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
const destination = <HTMLSelectElement>(
|
||||
document.querySelector("#destination")
|
||||
);
|
||||
@ -147,9 +170,16 @@ function selectBoatChange() {
|
||||
|
||||
if (event.detail.customProperties.owner) {
|
||||
choiceObjects["newrower"].setChoiceByValue(
|
||||
event.detail.customProperties.owner + "",
|
||||
event.detail.customProperties.owner.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
if(event.detail.value === '36') {
|
||||
/** custom code for Etsch */
|
||||
choiceObjects["newrower"].setChoiceByValue("81");
|
||||
}
|
||||
}else if (typeof loggedin_user_id !== 'undefined'){
|
||||
choiceObjects["newrower"].setChoiceByValue(loggedin_user_id);
|
||||
}
|
||||
|
||||
const inputElement = document.getElementById(
|
||||
"departure",
|
||||
@ -159,11 +189,6 @@ function selectBoatChange() {
|
||||
|
||||
inputElement.value = formattedDateTime;
|
||||
|
||||
const distinput = <HTMLInputElement>(
|
||||
document.querySelector("#distance_in_km")
|
||||
);
|
||||
distinput.value = "";
|
||||
|
||||
const destinput = <HTMLInputElement>(
|
||||
document.querySelector("#destination")
|
||||
);
|
||||
@ -236,22 +261,6 @@ function setMaxAmountRowers(name: string, rowers: number) {
|
||||
}
|
||||
}
|
||||
|
||||
//let only_steering = <HTMLSelectElement>document.querySelector('#shipmaster_only_steering');
|
||||
//if(only_steering) {
|
||||
// if(isShipmasterSteering == 'true') {
|
||||
// only_steering.removeAttribute('disabled');
|
||||
// only_steering.setAttribute('checked', 'true');
|
||||
// only_steering.parentElement?.parentElement?.parentElement?.classList.remove('hidden');
|
||||
// only_steering.parentElement?.parentElement?.parentElement?.classList.remove('md:block');
|
||||
// only_steering.parentElement?.parentElement?.parentElement?.classList.remove('opacity-50');
|
||||
// } else {
|
||||
// only_steering.setAttribute('disabled', 'disabled');
|
||||
// only_steering.removeAttribute('checked');
|
||||
// only_steering.parentElement?.parentElement?.parentElement?.classList.add('hidden');
|
||||
// only_steering.parentElement?.parentElement?.parentElement?.classList.add('md:block');
|
||||
// only_steering.parentElement?.parentElement?.parentElement?.classList.add('opacity-50');
|
||||
// }
|
||||
//}
|
||||
let shipmaster = <HTMLElement>(
|
||||
document.querySelector("#shipmaster-" + name + "js")
|
||||
);
|
||||
@ -357,6 +366,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
steering_person.setAttribute("required", "required");
|
||||
}
|
||||
const choice = new Choices(select, {
|
||||
searchFields: ['label', 'value', 'customProperties.searchableText'],
|
||||
removeItemButton: true,
|
||||
loadingText: "Wird geladen...",
|
||||
noResultsText: "Keine Ergebnisse gefunden",
|
||||
@ -744,9 +754,11 @@ function addRelationMagic(bodyElement: HTMLElement) {
|
||||
},
|
||||
);
|
||||
|
||||
if (option && option.value !== ""){
|
||||
// Get distance
|
||||
const distance = option.getAttribute("distance");
|
||||
if (distance) relatedField.value = distance;
|
||||
if (distance && relatedField.value === "") relatedField.value = distance;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -72,5 +72,6 @@ export default defineConfig({
|
||||
webServer: {
|
||||
timeout: 15 * 60 * 1000,
|
||||
command: 'cd .. && ./test_db.sh && cargo r',
|
||||
url: 'http://127.0.0.1:8000'
|
||||
},
|
||||
});
|
||||
|
@ -12,3 +12,5 @@
|
||||
@import 'components/chart';
|
||||
@import 'components/search';
|
||||
@import 'components/important';
|
||||
@import 'components/searchable-table';
|
||||
@import 'components/notification';
|
||||
|
5
frontend/scss/components/_notification.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.notification {
|
||||
right: -.2rem;
|
||||
top: -.1rem;
|
||||
font-size: .5rem;
|
||||
}
|
178
frontend/scss/components/_searchable-table.scss
Normal file
@ -0,0 +1,178 @@
|
||||
/*!
|
||||
* JSTable v1.6.5
|
||||
*/
|
||||
|
||||
.dt-container{
|
||||
position:relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
overflow-y: hidden;
|
||||
|
||||
.dt-message {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dt-loading{
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
margin-left: -50%;
|
||||
margin-top: -20px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,0.9) 25%,rgba(255,255,255,0.9) 75%,rgba(255,255,255,0) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.dt-top,
|
||||
.dt-bottom {
|
||||
padding: 8px 10px;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
|
||||
.dt-info {
|
||||
margin: 7px 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* PAGER */
|
||||
.dt-pagination {
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
li {
|
||||
list-style: none;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
a, span{
|
||||
border: 1px solid transparent;
|
||||
float: left;
|
||||
margin-left: 2px;
|
||||
padding: 6px 12px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
.active a{
|
||||
&, &:focus, &:hover{
|
||||
background-color: #d9d9d9;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-ellipsis span{
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.disabled a{
|
||||
&, &:focus, &:hover{
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.pager a {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dt-table {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
|
||||
& > tbody, > tfoot, > thead{
|
||||
& > tr{
|
||||
& > td, & > th{
|
||||
vertical-align: top;
|
||||
padding: 8px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > thead > tr{
|
||||
& > th, & > td{
|
||||
vertical-align: bottom;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
& > tfoot > tr{
|
||||
& > th, & > td{
|
||||
vertical-align: bottom;
|
||||
text-align: left;
|
||||
border-top: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
vertical-align: bottom;
|
||||
text-align: left;
|
||||
|
||||
&.dt-sorter {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding-right:20px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-top: 4px solid #000;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-bottom: 4px solid #000;
|
||||
border-top: 4px solid transparent;
|
||||
bottom: 22px;
|
||||
}
|
||||
|
||||
&.asc::after,
|
||||
&.desc::before {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.dt-loading.hidden{
|
||||
display:none!important;
|
||||
opacity:0!important;
|
||||
}
|
||||
|
||||
.dt-input {
|
||||
@extend .input;
|
||||
}
|
||||
|
||||
.dt-selector {
|
||||
@extend .input;
|
||||
}
|
@ -10,6 +10,7 @@
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
height: 100dvh;
|
||||
height: 100vh;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
BIN
frontend/static/images/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
frontend/static/images/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
frontend/static/images/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
9
frontend/static/images/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
frontend/static/images/favicon copy.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
frontend/static/images/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/static/images/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
36
frontend/static/images/favicon.svg
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 372 372" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(17.8229,1.77636e-15,-1.77636e-15,17.8229,-6843.65,-3821.15)">
|
||||
<path d="M397.575,225.019C397.575,225.386 397.493,225.701 397.329,225.965C397.165,226.229 396.93,226.444 396.626,226.612C396.883,226.808 397.079,227.039 397.211,227.307C397.344,227.574 397.411,227.903 397.411,228.294C397.411,228.646 397.339,228.957 397.197,229.229C397.054,229.5 396.852,229.729 396.59,229.914C396.329,230.1 396.013,230.24 395.644,230.336C395.275,230.432 394.862,230.48 394.405,230.48C394.01,230.48 393.623,230.439 393.242,230.359C392.861,230.279 392.521,230.145 392.222,229.955C391.923,229.766 391.683,229.514 391.502,229.199C391.32,228.885 391.229,228.493 391.229,228.024L392.858,228.019C392.858,228.249 392.903,228.438 392.993,228.584C393.083,228.73 393.201,228.847 393.347,228.933C393.494,229.019 393.66,229.078 393.845,229.111C394.031,229.145 394.217,229.161 394.405,229.161C394.854,229.161 395.197,229.083 395.433,228.927C395.669,228.771 395.788,228.562 395.788,228.3C395.788,228.171 395.76,228.056 395.706,227.954C395.651,227.853 395.556,227.758 395.421,227.67C395.287,227.582 395.107,227.496 394.882,227.412C394.658,227.328 394.376,227.237 394.036,227.14C393.598,227.022 393.208,226.895 392.864,226.756C392.52,226.617 392.228,226.452 391.988,226.261C391.748,226.069 391.564,225.845 391.437,225.587C391.31,225.329 391.247,225.022 391.247,224.667C391.247,224.308 391.33,223.994 391.496,223.727C391.662,223.459 391.897,223.239 392.202,223.067C391.944,222.868 391.748,222.635 391.613,222.367C391.478,222.1 391.411,221.771 391.411,221.38C391.411,221.044 391.482,220.738 391.625,220.463C391.767,220.188 391.97,219.953 392.234,219.76C392.498,219.566 392.816,219.417 393.189,219.312C393.562,219.206 393.977,219.153 394.434,219.153C394.903,219.153 395.325,219.208 395.7,219.317C396.075,219.427 396.392,219.588 396.652,219.801C396.912,220.014 397.112,220.276 397.252,220.589C397.393,220.901 397.463,221.259 397.463,221.661L395.829,221.661C395.829,221.493 395.799,221.337 395.741,221.192C395.682,221.048 395.594,220.922 395.477,220.814C395.36,220.707 395.214,220.622 395.041,220.56C394.867,220.497 394.665,220.466 394.434,220.466C394.192,220.466 393.983,220.49 393.807,220.539C393.631,220.588 393.487,220.653 393.374,220.735C393.26,220.817 393.177,220.913 393.125,221.022C393.072,221.132 393.045,221.247 393.045,221.368C393.045,221.517 393.07,221.644 393.119,221.749C393.168,221.855 393.256,221.951 393.385,222.039C393.514,222.127 393.691,222.21 393.916,222.288C394.14,222.366 394.428,222.452 394.78,222.546C395.225,222.663 395.622,222.791 395.969,222.93C396.317,223.068 396.61,223.233 396.848,223.425C397.086,223.616 397.267,223.841 397.39,224.099C397.513,224.356 397.575,224.663 397.575,225.019ZM394.206,223.917C393.901,223.835 393.62,223.747 393.362,223.653C393.19,223.735 393.065,223.852 392.987,224.002C392.909,224.152 392.87,224.323 392.87,224.515C392.87,224.671 392.894,224.805 392.943,224.916C392.992,225.027 393.081,225.13 393.21,225.224C393.338,225.317 393.515,225.406 393.74,225.49C393.964,225.574 394.252,225.669 394.604,225.774C394.756,225.817 394.903,225.859 395.044,225.9C395.184,225.941 395.321,225.985 395.454,226.032C395.622,225.942 395.751,225.823 395.84,225.675C395.93,225.526 395.975,225.358 395.975,225.171C395.975,225.03 395.947,224.905 395.89,224.796C395.834,224.687 395.737,224.583 395.6,224.485C395.463,224.388 395.282,224.294 395.055,224.204C394.829,224.114 394.545,224.019 394.206,223.917Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.695637,0.718394,-0.718394,-0.695637,185.736,185.735)">
|
||||
<circle cx="0" cy="0" r="185.772" style="fill:url(#_Linear1);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.291595,0,0,0.291595,185.736,185.735)">
|
||||
<g transform="matrix(1,0,0,1,-291.5,-512)">
|
||||
<clipPath id="_clip2">
|
||||
<rect x="0" y="0" width="583" height="1024"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip2)">
|
||||
<g transform="matrix(1,0,0,1,-1574,-536.199)">
|
||||
<g transform="matrix(1,0,0,1,0,10.8235)">
|
||||
<g transform="matrix(0.948324,0.317305,0.307947,-0.920356,-304.665,1886.18)">
|
||||
<rect x="1838.29" y="1006.52" width="17.353" height="644.204" style="fill:white;"/>
|
||||
</g>
|
||||
<path d="M1944.68,934.772C1921.09,896.523 1932.18,782.181 1974.56,655.531C1990.23,608.676 2009.04,563.787 2029.1,525.376L2156.88,568.132C2149.78,610.876 2137.79,658.046 2122.11,704.901C2079.26,832.966 2018.43,931.728 1976.52,946.409L2085.08,568.504L1944.68,934.772Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-1,0,0,1,3730.88,10.8235)">
|
||||
<g transform="matrix(0.948324,0.317305,0.307947,-0.920356,-304.665,1886.18)">
|
||||
<rect x="1838.29" y="1006.52" width="17.353" height="644.204" style="fill:white;"/>
|
||||
</g>
|
||||
<path d="M1944.68,934.772C1921.09,896.523 1932.18,782.181 1974.56,655.531C1990.23,608.676 2009.04,563.787 2029.1,525.376L2156.88,568.132C2149.78,610.876 2137.79,658.046 2122.11,704.901C2079.26,832.966 2018.43,931.728 1976.52,946.409L2085.08,568.504L1944.68,934.772Z" style="fill:white;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(371.543,-2.84217e-14,2.84217e-14,371.543,-185.772,1.13687e-13)"><stop offset="0" style="stop-color:rgb(131,0,0);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(255,0,0);stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 6.0 KiB |
BIN
frontend/static/images/mstile-144x144.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
frontend/static/images/mstile-150x150.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
frontend/static/images/mstile-310x150.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
frontend/static/images/mstile-310x310.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
frontend/static/images/mstile-70x70.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
19
frontend/static/images/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
4
frontend/static/jstable.min.js
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/*!
|
||||
* JSTable v1.6.5
|
||||
*/
|
||||
const JSTableDefaultConfig={perPage:5,perPageSelect:[5,10,15,20,25],sortable:!0,searchable:!0,nextPrev:!0,firstLast:!1,prevText:"‹",nextText:"›",firstText:"«",lastText:"»",ellipsisText:"…",truncatePager:!0,pagerDelta:2,classes:{top:"dt-top",info:"dt-info",input:"dt-input",table:"dt-table",bottom:"dt-bottom",search:"dt-search",sorter:"dt-sorter",wrapper:"dt-wrapper",dropdown:"dt-dropdown",ellipsis:"dt-ellipsis",selector:"dt-selector",container:"dt-container",pagination:"dt-pagination",loading:"dt-loading",message:"dt-message"},labels:{placeholder:"Search...",perPage:"{select} entries per page",noRows:"No entries found",info:"Showing {start} to {end} of {rows} entries",loading:"Loading...",infoFiltered:"Showing {start} to {end} of {rows} entries (filtered from {rowsTotal} entries)"},layout:{top:"{select}{search}",bottom:"{info}{pager}"},serverSide:!1,deferLoading:null,ajax:null,ajaxParams:{},queryParams:{page:"page",search:"search",sortColumn:"sortColumn",sortDirection:"sortDirection",perPage:"perPage"},addQueryParams:!0,rowAttributesCreator:null,searchDelay:null,method:"GET"};class JSTable{constructor(e,t={}){let s=e;"string"==typeof e&&(s=document.querySelector(e)),null!==s&&(this.config=this._merge(JSTableDefaultConfig,t),this.table=new JSTableElement(s),this.currentPage=1,this.columnRenderers=[],this.columnsNotSearchable=[],this.searchQuery=null,this.sortColumn=null,this.sortDirection="asc",this.isSearching=!1,this.dataCount=null,this.filteredDataCount=null,this.searchTimeout=null,this.pager=new JSTablePager(this),this._build(),this._buildColumns(),this.update(null===this.config.deferLoading),this._bindEvents(),this._emit("init"),this._parseQueryParams())}_build(){let e=this.config;this.wrapper=document.createElement("div"),this.wrapper.className=e.classes.wrapper;var t=["<div class='",e.classes.top,"'>",e.layout.top,"</div>","<div class='",e.classes.container,"'>","<div class='",e.classes.loading," hidden'>",e.labels.loading,"</div>","</div>","<div class='",e.classes.bottom,"'>",e.layout.bottom,"</div>"].join("");if(t=t.replace("{info}","<div class='"+e.classes.info+"'></div>"),e.perPageSelect){var s=["<div class='",e.classes.dropdown,"'>","<label>",e.labels.perPage,"</label>","</div>"].join(""),a=document.createElement("select");a.className=e.classes.selector,e.perPageSelect.forEach((function(t){var s=t===e.perPage,r=new Option(t,t,s,s);a.add(r)})),s=s.replace("{select}",a.outerHTML),t=t.replace(/\{select\}/g,s)}else t=t.replace(/\{select\}/g,"");if(e.searchable){var r=["<div class='",e.classes.search,"'>","<input class='",e.classes.input,"' placeholder='",e.labels.placeholder,"' type='text'>","</div>"].join("");t=t.replace(/\{search\}/g,r)}else t=t.replace(/\{search\}/g,"");this.table.element.classList.add(e.classes.table),t=t.replace("{pager}","<div class='"+e.classes.pagination+"'></div>"),this.wrapper.innerHTML=t,this.table.element.parentNode.replaceChild(this.wrapper,this.table.element),this.wrapper.querySelector("."+e.classes.container).appendChild(this.table.element),this._updatePagination(),this._updateInfo()}async update(e=!0){var t=this;this.currentPage>this.pager.getPages()&&(this.currentPage=this.pager.getPages());let s=t.wrapper.querySelector(" ."+t.config.classes.loading);if(s.classList.remove("hidden"),this.table.header.getCells().forEach((function(e,s){let a=t.table.head.rows[0].cells[s];a.innerHTML=e.getInnerHTML(),e.classes.length>0&&(a.className=e.classes.join(" "));for(let t in e.attributes)a.setAttribute(t,e.attributes[t]);a.setAttribute("data-sortable",e.isSortable)})),e)return this.getPageData(this.currentPage).then((function(e){t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",e.forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")})).then((function(){t.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),t._emit("update")})).then((function(){t._updatePagination(),t._updateInfo()}));t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",this.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),this._getData().forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")}_updatePagination(){let e=this.wrapper.querySelector(" ."+this.config.classes.pagination);e.innerHTML="",e.appendChild(this.pager.render(this.currentPage))}_updateInfo(){let e=this.wrapper.querySelector(" ."+this.config.classes.info),t=this.isSearching?this.config.labels.infoFiltered:this.config.labels.info;if(e&&t.length){var s=t.replace("{start}",this.getDataCount()>0?this._getPageStartIndex()+1:0).replace("{end}",this._getPageEndIndex()+1).replace("{page}",this.currentPage).replace("{pages}",this.pager.getPages()).replace("{rows}",this.getDataCount()).replace("{rowsTotal}",this.getDataCountTotal());e.innerHTML=s}}_getPageStartIndex(){return(this.currentPage-1)*this.config.perPage}_getPageEndIndex(){let e=this.currentPage*this.config.perPage-1;return e>this.getDataCount()-1?this.getDataCount()-1:e}_getData(){return this._emit("getData",this.table.dataRows),this.table.dataRows.filter((function(e){return e.visible}))}_fetchData(){var e=this;let t={searchQuery:this.searchQuery,sortColumn:this.sortColumn,sortDirection:this.sortDirection,start:this._getPageStartIndex(),length:this.config.perPage,datatable:1};t=Object.assign({},this.config.ajaxParams,t);let s=this.config.ajax+"?"+this._queryParams(t);return fetch(s,{method:this.config.method,credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"}}).then((function(e){return e.json()})).then((function(t){return e._emit("fetchData",t),e.dataCount=t.recordsTotal,e.filteredDataCount=t.recordsFiltered,t.data})).then((function(e){let t=[];return e.forEach((function(e){t.push(JSTableRow.createFromData(e))})),t})).catch((function(e){console.error(e)}))}_queryParams(e){return Object.keys(e).map((t=>encodeURIComponent(t)+"="+encodeURIComponent(e[t]))).join("&")}getDataCount(){return this.isSearching?this.getDataCountFiltered():this.getDataCountTotal()}getDataCountFiltered(){return this.config.serverSide?this.filteredDataCount:this._getData().length}getDataCountTotal(){return this.config.serverSide?null!==this.config.deferLoading?this.config.deferLoading:this.dataCount:this.table.dataRows.length}getPageData(){if(this.config.serverSide)return this._fetchData();let e=this._getPageStartIndex();var t=this._getPageEndIndex();return Promise.resolve(this._getData()).then((function(s){return s.filter((function(s,a){return a>=e&&a<=t}))}))}async search(e){var t=this;if(this.searchQuery===e.toLowerCase())return!1;if(this.searchQuery=e.toLowerCase(),this.config.searchDelay){if(this.searchTimeout)return!1;this.searchTimeout=setTimeout((function(){t.searchTimeout=null}),this.config.searchDelay)}return this.currentPage=1,this.isSearching=!0,this.searchQuery.length?(this.config.serverSide||this.table.dataRows.forEach((function(e){e.visible=!1,t.searchQuery.split(" ").reduce((function(s,a){var r;let i=e.getCells();return i=i.filter((function(e,s){if(t.columnsNotSearchable.indexOf(s)<0)return!0})),r=i.some((function(e,t){if(e.getTextContent().toLowerCase().indexOf(a)>=0)return!0})),s&&r}),!0)&&(e.visible=!0)})),this.wrapper.classList.add("search-results"),this.update().then((function(){t._emit("search",e)}))):(this.table.dataRows.forEach((function(e){e.visible=!0})),this.isSearching=!1,t.wrapper.classList.remove("search-results"),t.update(),!1)}sort(e,t,s=!1){var a=this;if(this.sortColumn=e||0,this.sortDirection=t,this.sortColumn<0||this.sortColumn>this.table.getColumnCount()-1)return!1;var r=this.table.header.getCell(this.sortColumn),i=this.table.dataRows;this.table.header.getCells().forEach((function(e){e.removeClass("asc"),e.removeClass("desc")})),r.addClass(this.sortDirection),this.config.serverSide||(i=i.sort((function(e,t){var s=e.getCellTextContent(a.sortColumn).toLowerCase(),r=t.getCellTextContent(a.sortColumn).toLowerCase();return s=s.replace(/(\$|\,|\s|%)/g,""),r=r.replace(/(\$|\,|\s|%)/g,""),s=isNaN(s)||""===s?s:parseFloat(s),r=isNaN(r)||""===r?r:parseFloat(r),""===s&&""!==r||!isNaN(s)&&isNaN(r)?"asc"===a.sortDirection?1:-1:""!==s&&""===r||isNaN(s)&&!isNaN(r)?"asc"===a.sortDirection?-1:1:"asc"===a.sortDirection?s===r?0:s>r?1:-1:s===r?0:s<r?1:-1})),this.table.dataRows=i),this.config.serverSide&&s||this.update(),this._emit("sort",this.sortColumn,this.sortDirection)}async paginate(e){var t=this;return this.currentPage=e,this.update().then((function(){t._emit("paginate",t.currentPage,e)}))}_setQueryParam(e,t){if(!this.config.addQueryParams)return;const s=new URL(window.location.href);s.searchParams.set(this.config.queryParams[e],t),window.history.replaceState(null,null,s)}_bindEvents(){var e=this;this.wrapper.addEventListener("click",(function(t){var s=t.target;if(s.hasAttribute("data-page")){t.preventDefault();let a=parseInt(s.getAttribute("data-page"),10);e.paginate(a),e._setQueryParam("page",a)}if("TH"===s.nodeName&&s.hasAttribute("data-sortable")){if("false"===s.getAttribute("data-sortable"))return!1;t.preventDefault();let a=s.classList.contains("asc")?"desc":"asc";e.sort(s.cellIndex,a),e._setQueryParam("sortColumn",s.cellIndex),e._setQueryParam("sortDirection",a)}})),this.config.perPageSelect&&this.wrapper.addEventListener("change",(function(t){var s=t.target;if("SELECT"===s.nodeName&&s.classList.contains(e.config.classes.selector)){t.preventDefault();let a=parseInt(s.value,10);e._emit("perPageChange",e.config.perPage,a),e.config.perPage=a,e.update(),e._setQueryParam("perPage",a)}})),this.config.searchable&&this.wrapper.addEventListener("keyup",(function(t){"INPUT"===t.target.nodeName&&t.target.classList.contains(e.config.classes.input)&&(t.preventDefault(),e.search(t.target.value),e._setQueryParam("search",t.target.value))}))}on(e,t){this.events=this.events||{},this.events[e]=this.events[e]||[],this.events[e].push(t)}off(e,t){this.events=this.events||{},e in this.events!=!1&&this.events[e].splice(this.events[e].indexOf(t),1)}_emit(e){if(this.events=this.events||{},e in this.events!=!1)for(var t=0;t<this.events[e].length;t++)this.events[e][t].apply(this,Array.prototype.slice.call(arguments,1))}setMessage(e){var t=this.table.getColumnCount(),s=document.createElement("tr");s.innerHTML='<td class="'+this.config.classes.message+'" colspan="'+t+'">'+e+"</td>",this.table.body.innerHTML="",this.table.body.appendChild(s)}_buildColumns(){var e=this;let t=null,s=null;this.config.columns&&this.config.columns.forEach((function(a){isNaN(a.select)||(a.select=[a.select]),a.select.forEach((function(r){var i=e.table.header.getCell(r);if(void 0!==i){if(a.hasOwnProperty("render")&&"function"==typeof a.render&&(e.columnRenderers[r]=a.render),a.hasOwnProperty("sortable")){let r=!1;i.hasSortable?r=i.isSortable:(r=a.sortable,i.setSortable(r)),r&&(i.addClass(e.config.classes.sorter),a.hasOwnProperty("sort")&&1===a.select.length&&(t=a.select[0],s=a.sort))}a.hasOwnProperty("searchable")&&(i.addAttribute("data-searchable",a.searchable),!1===a.searchable&&e.columnsNotSearchable.push(r))}}))})),this.table.header.getCells().forEach((function(a,r){null===a.isSortable&&a.setSortable(e.config.sortable),a.isSortable&&(a.addClass(e.config.classes.sorter),a.hasSort&&(t=r,s=a.sortDirection))})),null!==t&&e.sort(t,s,!0)}_merge(e,t){var s=this;return Object.keys(e).forEach((function(a){!t.hasOwnProperty(a)||"object"!=typeof t[a]||t[a]instanceof Array||null===t[a]?t.hasOwnProperty(a)||(t[a]=e[a]):s._merge(e[a],t[a])})),t}async _parseQueryParams(){const e=new URLSearchParams(window.location.search);let t=e.get(this.config.queryParams.perPage);if(t){t=parseInt(t),this.config.perPage=t,this.wrapper.querySelectorAll("."+this.config.classes.selector).forEach((function(e){e.querySelectorAll("option").forEach((e=>e.removeAttribute("selected"))),e.value=t,e.querySelector(`option[value='${t}']`).setAttribute("selected","")})),this.update()}let s=e.get(this.config.queryParams.search);if(s){this.wrapper.querySelectorAll("."+this.config.classes.input).forEach((function(e){e.value=s})),await this.search(s)}let a=e.get(this.config.queryParams.page);a&&await this.paginate(parseInt(a));let r=e.get(this.config.queryParams.sortColumn);if(r){r=parseInt(r);let t=e.get(this.config.queryParams.sortDirection);t=null==t?"asc":t,this.sort(r,t)}}}class JSTableElement{constructor(e){this.element=e,this.body=this.element.tBodies[0],this.head=this.element.tHead,this.rows=Array.from(this.element.rows).map((function(e,t){return new JSTableRow(e,e.parentNode.nodeName,t)})),this.dataRows=this._getBodyRows(),this.header=this._getHeaderRow()}_getBodyRows(){return this.rows.filter((function(e){return!e.isHeader&&!e.isFooter}))}_getHeaderRow(){return this.rows.find((function(e){return e.isHeader}))}getColumnCount(){return this.header.getColumnCount()}getFooterRow(){return this.rows.find((function(e){return e.isFooter}))}}class JSTableRow{constructor(e,t="",s=null){this.cells=Array.from(e.cells).map((function(e){return new JSTableCell(e)})),this.d=this.cells.length,this.isHeader="THEAD"===t,this.isFooter="TFOOT"===t,this.visible=!0,this.rowID=s;var a=this;this.attributes={},[...e.attributes].forEach((function(e){a.attributes[e.name]=e.value}))}getCells(){return Array.from(this.cells)}getColumnCount(){return this.cells.length}getCell(e){return this.cells[e]}getCellTextContent(e){return this.getCell(e).getTextContent()}static createFromData(e){let t=document.createElement("tr");if(e.hasOwnProperty("data")){if(e.hasOwnProperty("attributes"))for(const s in e.attributes)t.setAttribute(s,e.attributes[s]);e=e.data}return e.forEach((function(e){let s=document.createElement("td");if(s.innerHTML=e&&e.hasOwnProperty("data")?e.data:e,e&&e.hasOwnProperty("attributes"))for(const t in e.attributes)s.setAttribute(t,e.attributes[t]);t.appendChild(s)})),new JSTableRow(t)}getFormatted(e,t=null){let s=document.createElement("tr");var a=this;for(let e in this.attributes)s.setAttribute(e,this.attributes[e]);let r=t?t.call(this,this.getCells()):{};for(const e in r)s.setAttribute(e,r[e]);return this.getCells().forEach((function(t,r){var i=document.createElement("td");i.innerHTML=t.getInnerHTML(),e.hasOwnProperty(r)&&(i.innerHTML=e[r].call(a,t.getElement(),r)),t.classes.length>0&&(i.className=t.classes.join(" "));for(let e in t.attributes)i.setAttribute(e,t.attributes[e]);s.appendChild(i)})),s}setCellClass(e,t){this.cells[e].addClass(t)}}class JSTableCell{constructor(e){this.textContent=e.textContent,this.innerHTML=e.innerHTML,this.className="",this.element=e,this.hasSortable=e.hasAttribute("data-sortable"),this.isSortable=this.hasSortable?"true"===e.getAttribute("data-sortable"):null,this.hasSort=e.hasAttribute("data-sort"),this.sortDirection=e.getAttribute("data-sort"),this.classes=[];var t=this;this.attributes={},[...e.attributes].forEach((function(e){t.attributes[e.name]=e.value}))}getElement(){return this.element}getTextContent(){return this.textContent}getInnerHTML(){return this.innerHTML}setClass(e){this.className=e}setSortable(e){this.isSortable=e}addClass(e){this.classes.push(e)}removeClass(e){this.classes.indexOf(e)>=0&&this.classes.splice(this.classes.indexOf(e),1)}addAttribute(e,t){this.attributes[e]=t}}class JSTablePager{constructor(e){this.instance=e}getPages(){let e=Math.ceil(this.instance.getDataCount()/this.instance.config.perPage);return 0===e?1:e}render(){var e=this.instance.config;let t=this.getPages(),s=document.createElement("ul");if(t>1){let a=1===this.instance.currentPage?1:this.instance.currentPage-1,r=this.instance.currentPage===t?t:this.instance.currentPage+1;e.firstLast&&s.appendChild(this.createItem("pager",1,e.firstText)),e.nextPrev&&s.appendChild(this.createItem("pager",a,e.prevText)),this.truncate().forEach((function(e){s.appendChild(e)})),e.nextPrev&&s.appendChild(this.createItem("pager",r,e.nextText)),e.firstLast&&s.appendChild(this.createItem("pager",t,e.lastText))}return s}createItem(e,t,s,a){let r=document.createElement("li");return r.className=e,r.innerHTML=a?"<span>"+s+"</span>":'<a href="#" data-page="'+t+'">'+s+"</a>",r}isValidPage(e){return e>0&&e<=this.getPages()}truncate(){var e,t=this,s=t.instance.config,a=2*s.pagerDelta,r=t.instance.currentPage,i=r-s.pagerDelta,n=r+s.pagerDelta,o=this.getPages(),l=[],c=[];if(this.instance.config.truncatePager){r<4-s.pagerDelta+a?n=3+a:r>this.getPages()-(3-s.pagerDelta+a)&&(i=this.getPages()-(2+a));for(var h=1;h<=o;h++)(1===h||h===o||h>=i&&h<=n)&&l.push(h);l.forEach((function(a){e&&(a-e==2?c.push(t.createItem("",e+1,e+1)):a-e!=1&&c.push(t.createItem(s.classes.ellipsis,0,s.ellipsisText,!0))),c.push(t.createItem(a==r?"active":"",a,a)),e=a}))}else for(let e=1;e<=this.getPages();e++)c.push(this.createItem(e===r?"active":"",e,e));return c}}window.JSTable=JSTable;
|
21
frontend/table.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// @ts-ignore
|
||||
new JSTable('#basic', {
|
||||
perPage: 100,
|
||||
perPageSelect: [10,100],
|
||||
|
||||
// Customise the display text
|
||||
labels: {
|
||||
placeholder: 'Suchen (z.B. "Linz")',
|
||||
perPage: '{select} per Seite',
|
||||
noRows: 'Keine Einträge gefunden',
|
||||
info: 'Zeigt {start} bis {end} von {rows} Einträgen',
|
||||
loading: 'Laden...',
|
||||
infoFiltered: 'Zeigt {start} bis {end} von {rows} Einträgen (gefiltert aus {rowsTotal} Einträgen)'
|
||||
},
|
||||
|
||||
// Customise the layout
|
||||
layout: {
|
||||
top: '{search}{select}',
|
||||
bottom: '{info}{pager}'
|
||||
},
|
||||
});
|
@ -8,7 +8,7 @@ test("cox can create and delete trip", async ({ page }) => {
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
||||
await page.locator(".relative").first().click();
|
||||
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
|
||||
await page.locator("#sidebar #planned_starting_time").click();
|
||||
await page.locator("#sidebar #planned_starting_time").fill("18:00");
|
||||
await page.locator("#sidebar #planned_starting_time").press("Tab");
|
||||
@ -38,8 +38,8 @@ test.describe("cox can edit trips", () => {
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
||||
await page.locator(".relative").first().click();
|
||||
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
||||
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
|
||||
await page.locator("#sidebar #planned_starting_time").click();
|
||||
await page.locator("#sidebar #planned_starting_time").fill("18:00");
|
||||
await page.locator("#sidebar #planned_starting_time").press("Tab");
|
||||
|
@ -12,11 +12,13 @@ test("Cox can start and cancel trip", async ({ page }, testInfo) => {
|
||||
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText("Kaputtes Boot :-( (7 x)").nth(1).click();
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
@ -52,17 +54,29 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
||||
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText("Kaputtes Boot :-( (7 x)").nth(1).click();
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
@ -76,15 +90,6 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/log");
|
||||
await page.locator("div:nth-child(2) > .border-0").click();
|
||||
|
||||
// Add a minute
|
||||
const datetimeSelector = '#arrivaljs';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes() + 1);
|
||||
currentDate.setHours(currentDate.getHours() + 1);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||
@ -103,9 +108,10 @@ test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/log/kiosk/ekrv2019/Linz");
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText("Kaputtes Boot :-( (7 x)").nth(1).click();
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
@ -136,9 +142,10 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText("Kaputtes Boot :-( (7 x)").nth(1).click();
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
@ -147,6 +154,16 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
@ -160,15 +177,6 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/log");
|
||||
await page.locator('div:nth-child(2) > .pt-2 > div > div > div:nth-child(2) > .border-0').click(); // 2 trips currently running, try to close second one
|
||||
|
||||
// Add a minute
|
||||
const datetimeSelector = '#arrivaljs';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes() + 1);
|
||||
currentDate.setHours(currentDate.getHours() + 1);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||
|
@ -24,6 +24,7 @@ export default defineConfig({
|
||||
input: {
|
||||
main: './main.ts',
|
||||
logbook: './logbook.ts',
|
||||
table: './table.ts',
|
||||
// Example for more entry points
|
||||
// test: './src/test.ts',
|
||||
},
|
||||
|
@ -99,9 +99,11 @@ CREATE TABLE IF NOT EXISTS "boat" (
|
||||
"year_built" INTEGER,
|
||||
"boatbuilder" TEXT,
|
||||
"default_shipmaster_only_steering" boolean default false not null,
|
||||
"convert_handoperated_possible" boolean default false not null,
|
||||
"default_destination" text,
|
||||
"skull" boolean default true NOT NULL, -- false => riemen
|
||||
"external" boolean default false NOT NULL -- false => owned by different club
|
||||
"external" boolean default false NOT NULL, -- false => owned by different club
|
||||
"deleted" boolean NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "logbook_type" (
|
||||
@ -158,5 +160,56 @@ CREATE TABLE IF NOT EXISTS "notification" (
|
||||
"read_at" DATETIME,
|
||||
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"action_after_reading" TEXT,
|
||||
"link" TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "boat_reservation" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"boat_id" INTEGER NOT NULL REFERENCES boat(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
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "waterlevel" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"day" DATE NOT NULL,
|
||||
"time" TEXT NOT NULL,
|
||||
"max" INTEGER NOT NULL,
|
||||
"min" INTEGER NOT NULL,
|
||||
"mittel" INTEGER NOT NULL,
|
||||
"tumax" INTEGER NOT NULL,
|
||||
"tumin" INTEGER NOT NULL,
|
||||
"tumittel" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "weather" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"day" DATE NOT NULL,
|
||||
"max_temp" FLOAT NOT NULL,
|
||||
"wind_gust" FLOAT NOT NULL,
|
||||
"rain_mm" FLOAT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "trailer" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "trailer_reservation" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"trailer_id" INTEGER NOT NULL REFERENCES trailer(id),
|
||||
"start_date" DATE NOT NULL,
|
||||
"end_date" DATE NOT NULL,
|
||||
"time_desc" TEXT NOT NULL,
|
||||
"usage" TEXT NOT NULL,
|
||||
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
|
||||
"user_id_confirmation" INTEGER REFERENCES user(id),
|
||||
"created_at" datetime not null default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
@ -3,10 +3,12 @@ INSERT INTO "role" (name) VALUES ('cox');
|
||||
INSERT INTO "role" (name) VALUES ('scheckbuch');
|
||||
INSERT INTO "role" (name) VALUES ('tech');
|
||||
INSERT INTO "role" (name) VALUES ('Donau Linz');
|
||||
INSERT INTO "role" (name) VALUES ('planned_event');
|
||||
INSERT INTO "role" (name) VALUES ('manage_events');
|
||||
INSERT INTO "role" (name) VALUES ('Rennrudern');
|
||||
INSERT INTO "role" (name) VALUES ('paid');
|
||||
INSERT INTO "role" (name) VALUES ('Vorstand');
|
||||
INSERT INTO "role" (name) VALUES ('Bootsführer');
|
||||
INSERT INTO "role" (name) VALUES ('schnupperant');
|
||||
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
|
||||
@ -62,3 +64,5 @@ INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3);
|
||||
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02');
|
||||
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1);
|
||||
INSERT INTO "notification" (user_id, message, category) VALUES (1, 'This is a test notification', 'test-cat');
|
||||
INSERT INTO "trailer" (name) VALUES('Großer Hänger');
|
||||
INSERT INTO "trailer" (name) VALUES('Kleiner Hänger');
|
||||
|
@ -8,6 +8,8 @@ pub mod tera;
|
||||
#[cfg(feature = "rest")]
|
||||
pub mod rest;
|
||||
|
||||
pub mod scheduled;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_export]
|
||||
macro_rules! testdb {
|
||||
|
@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||
use rot::rest;
|
||||
#[cfg(feature = "rowing-tera")]
|
||||
use rot::tera;
|
||||
use rot::{scheduled, tera::Config};
|
||||
|
||||
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
|
||||
|
||||
@ -26,7 +27,7 @@ async fn rocket() -> _ {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let rocket = rocket::build().manage(db);
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
|
||||
#[cfg(feature = "rowing-tera")]
|
||||
let rocket = tera::config(rocket);
|
||||
@ -34,5 +35,11 @@ async fn rocket() -> _ {
|
||||
#[cfg(feature = "rest")]
|
||||
let rocket = rest::config(rocket);
|
||||
|
||||
let config: Config = rocket
|
||||
.figment()
|
||||
.extract()
|
||||
.expect("Config extraction failed");
|
||||
scheduled::schedule(&db, &config);
|
||||
|
||||
rocket
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::model::boathouse::Boathouse;
|
||||
|
||||
use super::location::Location;
|
||||
use super::user::User;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Boat {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
@ -18,11 +21,14 @@ pub struct Boat {
|
||||
pub boatbuilder: Option<String>,
|
||||
pub default_destination: Option<String>,
|
||||
#[serde(default = "bool::default")]
|
||||
default_shipmaster_only_steering: bool,
|
||||
pub convert_handoperated_possible: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
skull: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
external: bool,
|
||||
pub external: bool,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@ -36,9 +42,11 @@ pub enum BoatDamage {
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct BoatWithDetails {
|
||||
#[serde(flatten)]
|
||||
boat: Boat,
|
||||
pub(crate) boat: Boat,
|
||||
damage: BoatDamage,
|
||||
on_water: bool,
|
||||
reserved_today: bool,
|
||||
cat: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
@ -48,6 +56,7 @@ pub struct BoatToAdd<'r> {
|
||||
pub year_built: Option<i64>,
|
||||
pub boatbuilder: Option<&'r str>,
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
pub convert_handoperated_possible: bool,
|
||||
pub default_destination: Option<&'r str>,
|
||||
pub skull: bool,
|
||||
pub external: bool,
|
||||
@ -64,6 +73,7 @@ pub struct BoatToUpdate<'r> {
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
pub default_destination: Option<&'r str>,
|
||||
pub skull: bool,
|
||||
pub convert_handoperated_possible: bool,
|
||||
pub external: bool,
|
||||
pub location_id: i64,
|
||||
pub owner: Option<i64>,
|
||||
@ -71,20 +81,20 @@ pub struct BoatToUpdate<'r> {
|
||||
|
||||
impl Boat {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM boat WHERE name like ?", name)
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE name like ?", name)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
@ -135,6 +145,20 @@ impl Boat {
|
||||
sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
|
||||
}
|
||||
|
||||
pub async fn reserved_today(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT *
|
||||
FROM boat_reservation
|
||||
WHERE boat_id =?
|
||||
AND date('now') BETWEEN start_date AND end_date;",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn on_water(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT * FROM logbook WHERE boat_id=? AND arrival is null",
|
||||
@ -156,10 +180,20 @@ impl Boat {
|
||||
if boat.is_locked(db).await {
|
||||
damage = BoatDamage::Locked;
|
||||
}
|
||||
let cat = if boat.external {
|
||||
"Vereinsfremde Boote".to_string()
|
||||
} else if boat.default_shipmaster_only_steering {
|
||||
format!("{}+", boat.amount_seats - 1)
|
||||
} else {
|
||||
format!("{}x", boat.amount_seats)
|
||||
};
|
||||
|
||||
res.push(BoatWithDetails {
|
||||
damage,
|
||||
on_water: boat.on_water(db).await,
|
||||
reserved_today: boat.reserved_today(db).await,
|
||||
boat,
|
||||
cat,
|
||||
});
|
||||
}
|
||||
res
|
||||
@ -169,8 +203,9 @@ impl Boat {
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE deleted=false
|
||||
ORDER BY amount_seats DESC
|
||||
"
|
||||
)
|
||||
@ -196,15 +231,15 @@ SELECT
|
||||
b.default_shipmaster_only_steering,
|
||||
b.default_destination,
|
||||
b.skull,
|
||||
b.external
|
||||
b.external,
|
||||
b.deleted,
|
||||
b.convert_handoperated_possible
|
||||
FROM
|
||||
boat AS b
|
||||
LEFT JOIN
|
||||
boathouse AS bh ON b.id = bh.boat_id
|
||||
WHERE
|
||||
b.external = false
|
||||
AND b.location_id = (SELECT id FROM location WHERE name = 'Linz')
|
||||
AND bh.id IS NULL -- This ensures the boat does not have an entry in the boathouse table
|
||||
AND b.deleted = false
|
||||
ORDER BY
|
||||
b.name DESC;
|
||||
"
|
||||
@ -224,9 +259,9 @@ ORDER BY
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE owner is null or owner = ?
|
||||
WHERE (owner is null or owner = ?) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
@ -238,9 +273,9 @@ ORDER BY amount_seats DESC
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE owner = ? OR (owner is null and amount_seats = 1)
|
||||
WHERE (owner = ? OR (owner is null and amount_seats = 1)) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
@ -256,9 +291,9 @@ ORDER BY amount_seats DESC
|
||||
.unwrap();
|
||||
let boats_in_ottensheim = sqlx::query_as!(
|
||||
Boat,
|
||||
"SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
|
||||
"SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE owner is null and location_id = ?
|
||||
WHERE (owner is null and location_id = ?) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",ottensheim.id)
|
||||
.fetch_all(db)
|
||||
@ -266,6 +301,7 @@ ORDER BY amount_seats DESC
|
||||
.unwrap(); //TODO: fixme
|
||||
boats.extend(boats_in_ottensheim.into_iter());
|
||||
}
|
||||
let boats = boats.into_iter().unique().collect();
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
@ -274,10 +310,10 @@ ORDER BY amount_seats DESC
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
|
||||
SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
INNER JOIN location ON boat.location_id = location.id
|
||||
WHERE location.name=?
|
||||
WHERE location.name=? AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
location
|
||||
@ -291,7 +327,7 @@ ORDER BY amount_seats DESC
|
||||
|
||||
pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
"INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner, convert_handoperated_possible) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
boat.name,
|
||||
boat.amount_seats,
|
||||
boat.year_built,
|
||||
@ -301,7 +337,8 @@ ORDER BY amount_seats DESC
|
||||
boat.skull,
|
||||
boat.external,
|
||||
boat.location_id,
|
||||
boat.owner
|
||||
boat.owner,
|
||||
boat.convert_handoperated_possible
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
@ -310,7 +347,7 @@ ORDER BY amount_seats DESC
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=? WHERE id=?",
|
||||
"UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=?, convert_handoperated_possible=? WHERE id=?",
|
||||
boat.name,
|
||||
boat.amount_seats,
|
||||
boat.year_built,
|
||||
@ -321,6 +358,7 @@ ORDER BY amount_seats DESC
|
||||
boat.external,
|
||||
boat.location_id,
|
||||
boat.owner,
|
||||
boat.convert_handoperated_possible,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
@ -328,12 +366,31 @@ ORDER BY amount_seats DESC
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn owner(&self, db: &SqlitePool) -> Option<User> {
|
||||
if let Some(owner_id) = self.owner {
|
||||
Some(User::find_by_id(db, owner_id as i32).await.unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM boat WHERE id=?", self.id)
|
||||
sqlx::query!("UPDATE boat SET deleted=1 WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
|
||||
pub async fn boathouse(&self, db: &SqlitePool) -> Option<Boathouse> {
|
||||
sqlx::query_as!(
|
||||
Boathouse,
|
||||
"SELECT * FROM boathouse WHERE boat_id like ?",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -381,6 +438,7 @@ mod test {
|
||||
year_built: None,
|
||||
boatbuilder: "Best Boatbuilder".into(),
|
||||
default_shipmaster_only_steering: true,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: Some(1),
|
||||
@ -406,6 +464,7 @@ mod test {
|
||||
year_built: None,
|
||||
boatbuilder: "Best Boatbuilder".into(),
|
||||
default_shipmaster_only_steering: true,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: Some(1),
|
||||
@ -508,6 +567,7 @@ mod test {
|
||||
year_built: None,
|
||||
boatbuilder: None,
|
||||
default_shipmaster_only_steering: false,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: 1,
|
||||
@ -531,6 +591,7 @@ mod test {
|
||||
year_built: None,
|
||||
boatbuilder: None,
|
||||
default_shipmaster_only_steering: false,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: 999,
|
||||
|
@ -73,6 +73,10 @@ impl BoatDamage {
|
||||
"
|
||||
SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat
|
||||
FROM boat_damage
|
||||
WHERE (
|
||||
verified_at IS NULL
|
||||
OR verified_at >= datetime('now', '-30 days')
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
"
|
||||
)
|
||||
@ -115,6 +119,10 @@ ORDER BY created_at DESC
|
||||
|
||||
pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> {
|
||||
Log::create(db, format!("New boat damage: {boatdamage:?}")).await;
|
||||
let Some(boat) = Boat::find_by_id(db, boatdamage.boat_id as i32).await else {
|
||||
return Err("Boot gibt's ned".into());
|
||||
};
|
||||
let was_unusable_before = boat.is_locked(db).await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)",
|
||||
@ -127,6 +135,11 @@ ORDER BY created_at DESC
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if !was_unusable_before && boat.is_locked(db).await {
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
|
||||
}
|
||||
|
||||
let technicals =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
||||
for technical in technicals {
|
||||
@ -140,14 +153,12 @@ ORDER BY created_at DESC
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
Boat::find_by_id(db, boatdamage.boat_id as i32)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boat.name,
|
||||
boatdamage.desc
|
||||
),
|
||||
"Neuer Bootsschaden angelegt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@ -168,6 +179,7 @@ ORDER BY created_at DESC
|
||||
),
|
||||
"Neuer Bootsschaden angelegt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -225,25 +237,37 @@ ORDER BY created_at DESC
|
||||
boat.name,
|
||||
),
|
||||
"Bootsschaden repariert",
|
||||
None,
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
if boat_damage.user_id_fixed != self.user_id_created as i32 {
|
||||
let user_fixed = User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap(),
|
||||
&format!(
|
||||
"Du hat den Bootschaden '{}' beim Boot '{}' repariert. Danke für deine Hilfe!",
|
||||
boat_damage.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden repariert",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
.unwrap();
|
||||
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Boatdamage is also directly verified, if a tech has repaired it. We don't want to
|
||||
// send 2 notifications.
|
||||
if !user_fixed.has_role(db, "tech").await {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert. Dieser muss nun noch von unseren Bootswarten bestätigt werden.",
|
||||
user_fixed.name,
|
||||
boat_damage.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden repariert",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -251,28 +275,77 @@ ORDER BY created_at DESC
|
||||
pub async fn verified(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
boat: BoatDamageVerified<'_>,
|
||||
boat_form: BoatDamageVerified<'_>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(verifier) = User::find_by_id(db, boat.user_id_verified).await {
|
||||
if let Some(verifier) = User::find_by_id(db, boat_form.user_id_verified).await {
|
||||
if !verifier.has_role(db, "tech").await {
|
||||
Log::create(db, format!("User {verifier:?} tried to verify boat {boat:?}. The user is no tech. Manually craftted request?")).await;
|
||||
Log::create(db, format!("User {verifier:?} tried to verify boat {boat_form:?}. The user is no tech. Manually craftted request?")).await;
|
||||
return Err("You are not allowed to verify the boat!".into());
|
||||
}
|
||||
} else {
|
||||
Log::create(db, format!("Someone tried to verify the boat {boat:?} with user_id={} which does not exist. Manually craftted request?", boat.user_id_verified)).await;
|
||||
Log::create(db, format!("Someone tried to verify the boat {boat_form:?} with user_id={} which does not exist. Manually craftted request?", boat_form.user_id_verified)).await;
|
||||
return Err("Could not find user".into());
|
||||
}
|
||||
|
||||
Log::create(db, format!("Verified boat damage: {boat:?}")).await;
|
||||
let Some(boat) = Boat::find_by_id(db, self.boat_id as i32).await else {
|
||||
return Err("Boot gibt's ned".into());
|
||||
};
|
||||
let was_unusable_before = boat.is_locked(db).await;
|
||||
|
||||
Log::create(db, format!("Verified boat damage: {boat_form:?}")).await;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
boat.desc,
|
||||
boat.user_id_verified,
|
||||
boat_form.desc,
|
||||
boat_form.user_id_verified,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
|
||||
if boat_form.user_id_verified != self.user_id_created as i32 {
|
||||
let user_verified = User::find_by_id(db, boat_form.user_id_verified)
|
||||
.await
|
||||
.unwrap();
|
||||
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if user_verified.id == self.user_id_fixed.unwrap() {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert und verifiziert.",
|
||||
user_verified.name,
|
||||
self.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden repariert & verifiziert",
|
||||
None,
|
||||
None
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat verifiziert, dass der von dir eingetragenen Bootschaden '{}' beim Boot '{}' korrekt repariert wurde.",
|
||||
user_verified.name,
|
||||
self.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden verifiziert",
|
||||
None,
|
||||
None
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
if was_unusable_before && !boat.is_locked(db).await {
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, {} wurde repariert und freut sich ab sofort wieder gerudert zu werden :-)", boat.name), "Boot repariert", None, None).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
228
src/model/boatreservation.rs
Normal file
@ -0,0 +1,228 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::{boat::Boat, user::User};
|
||||
use crate::tera::boatreservation::ReservationEditForm;
|
||||
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;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatReservation {
|
||||
pub id: i64,
|
||||
pub boat_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 BoatReservationWithDetails {
|
||||
#[serde(flatten)]
|
||||
reservation: BoatReservation,
|
||||
boat: Boat,
|
||||
user_applicant: User,
|
||||
user_confirmation: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoatReservationToAdd<'r> {
|
||||
pub boat: &'r Boat,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_applicant: &'r User,
|
||||
}
|
||||
|
||||
impl BoatReservation {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM boat_reservation
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
|
||||
let boatreservations = sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM boat_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 boatreservations {
|
||||
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 boat = Boat::find_by_id(db, reservation.boat_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
res.push(BoatReservationWithDetails {
|
||||
reservation,
|
||||
boat,
|
||||
user_applicant,
|
||||
user_confirmation,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
|
||||
let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> =
|
||||
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,
|
||||
boatreservation: BoatReservationToAdd<'_>,
|
||||
) -> Result<(), String> {
|
||||
if Self::boat_reserved_between_dates(
|
||||
db,
|
||||
boatreservation.boat,
|
||||
&boatreservation.start_date,
|
||||
&boatreservation.end_date,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err("Boot in diesem Zeitraum bereits reserviert.".into());
|
||||
}
|
||||
|
||||
Log::create(db, format!("New boat reservation: {boatreservation:?}")).await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat_reservation(boat_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
|
||||
boatreservation.boat.id,
|
||||
boatreservation.start_date,
|
||||
boatreservation.end_date,
|
||||
boatreservation.time_desc,
|
||||
boatreservation.usage,
|
||||
boatreservation.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 boatreservation.start_date == boatreservation.end_date {
|
||||
format!("am {}", boatreservation.start_date)
|
||||
} else {
|
||||
format!(
|
||||
"von {} bis {}",
|
||||
boatreservation.start_date, boatreservation.end_date
|
||||
)
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine neue Bootsreservierung für Boot '{}' {} angelegt. Zeit: {}; Zweck: {}",
|
||||
boatreservation.user_applicant.name,
|
||||
boatreservation.boat.name,
|
||||
date,
|
||||
boatreservation.time_desc,
|
||||
boatreservation.usage
|
||||
),
|
||||
"Neue Bootsreservierung",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn boat_reserved_between_dates(
|
||||
db: &SqlitePool,
|
||||
boat: &Boat,
|
||||
start_date: &NaiveDate,
|
||||
end_date: &NaiveDate,
|
||||
) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS reservation_count
|
||||
FROM boat_reservation
|
||||
WHERE boat_id = ?
|
||||
AND start_date <= ? AND end_date >= ?;",
|
||||
boat.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 boat_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 boat_reservation WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
@ -3,33 +3,33 @@ use std::io::Write;
|
||||
use chrono::NaiveDate;
|
||||
use ics::{
|
||||
properties::{DtStart, Summary},
|
||||
Event, ICalendar,
|
||||
ICalendar,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, SqlitePool, Row};
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
use super::{tripdetails::TripDetails, triptype::TripType, user::User};
|
||||
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
|
||||
|
||||
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
|
||||
pub struct PlannedEvent {
|
||||
pub struct Event {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
planned_amount_cox: i64,
|
||||
pub(crate) planned_amount_cox: i64,
|
||||
trip_details_id: i64,
|
||||
pub planned_starting_time: String,
|
||||
max_people: i64,
|
||||
pub(crate) max_people: i64,
|
||||
pub day: String,
|
||||
pub notes: Option<String>,
|
||||
pub allow_guests: bool,
|
||||
trip_type_id: Option<i64>,
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
pub(crate) always_show: bool,
|
||||
pub(crate) is_locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct PlannedEventWithUserAndTriptype {
|
||||
pub struct EventWithUserAndTriptype {
|
||||
#[serde(flatten)]
|
||||
pub planned_event: PlannedEvent,
|
||||
pub event: Event,
|
||||
trip_type: Option<TripType>,
|
||||
cox_needed: bool,
|
||||
cox: Vec<Registration>,
|
||||
@ -63,7 +63,7 @@ FROM user_trip WHERE trip_details_id = {}
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r|
|
||||
.map(|r|
|
||||
Registration {
|
||||
name: r.get::<Option<String>, usize>(0).or(r.get::<Option<String>, usize>(1)).unwrap(), //Ok, either name or user_note needs to be set
|
||||
registered_at: r.get::<String,usize>(3),
|
||||
@ -73,7 +73,7 @@ FROM user_trip WHERE trip_details_id = {}
|
||||
.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
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -82,7 +82,7 @@ SELECT
|
||||
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
|
||||
FROM trip WHERE planned_event_id = ?
|
||||
",
|
||||
trip_details_id
|
||||
event_id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
@ -94,11 +94,22 @@ FROM trip WHERE planned_event_id = ?
|
||||
is_guest: false,
|
||||
is_real_guest: false,
|
||||
})
|
||||
.collect() //Okay, as PlannedEvent can only be created with proper DB backing
|
||||
.collect() //Okay, as Event can only be created with proper DB backing
|
||||
}
|
||||
}
|
||||
|
||||
impl PlannedEvent {
|
||||
#[derive(Debug)]
|
||||
pub struct EventUpdate<'a> {
|
||||
pub name: &'a str,
|
||||
pub planned_amount_cox: i32,
|
||||
pub max_people: i32,
|
||||
pub notes: Option<&'a str>,
|
||||
pub always_show: bool,
|
||||
pub is_locked: bool,
|
||||
pub trip_type_id: Option<i64>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
@ -119,19 +130,16 @@ WHERE planned_event.id like ?
|
||||
pub async fn get_pinned_for_day(
|
||||
db: &SqlitePool,
|
||||
day: NaiveDate,
|
||||
) -> Vec<PlannedEventWithUserAndTriptype> {
|
||||
) -> Vec<EventWithUserAndTriptype> {
|
||||
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
|
||||
}
|
||||
|
||||
pub async fn get_for_day(
|
||||
db: &SqlitePool,
|
||||
day: NaiveDate,
|
||||
) -> Vec<PlannedEventWithUserAndTriptype> {
|
||||
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithUserAndTriptype> {
|
||||
let day = format!("{day}");
|
||||
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
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||
@ -149,20 +157,20 @@ WHERE day=?",
|
||||
if let Some(trip_type_id) = event.trip_type_id {
|
||||
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,
|
||||
rower: Registration::all_rower(db, event.trip_details_id).await,
|
||||
planned_event: event,
|
||||
event,
|
||||
trip_type,
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<PlannedEvent> {
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Event> {
|
||||
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
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
@ -189,11 +197,27 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
is_rower.amount > 0
|
||||
}
|
||||
|
||||
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||
WHERE trip_details.id=?
|
||||
",
|
||||
tripdetails_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
name: &str,
|
||||
planned_amount_cox: i32,
|
||||
trip_details: TripDetails,
|
||||
trip_details: &TripDetails,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
|
||||
@ -207,58 +231,153 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
}
|
||||
|
||||
//TODO: create unit test
|
||||
pub async fn update(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
name: &str,
|
||||
planned_amount_cox: i32,
|
||||
max_people: i32,
|
||||
notes: Option<&str>,
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
) {
|
||||
pub async fn update(&self, db: &SqlitePool, update: &EventUpdate<'_>) {
|
||||
sqlx::query!(
|
||||
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
|
||||
name,
|
||||
planned_amount_cox,
|
||||
update.name,
|
||||
update.planned_amount_cox,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
|
||||
let tripdetails = self.trip_details(db).await;
|
||||
let was_already_cancelled = tripdetails.max_people == 0;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?",
|
||||
max_people,
|
||||
notes,
|
||||
always_show,
|
||||
is_locked,
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.always_show,
|
||||
update.is_locked,
|
||||
update.trip_type_id,
|
||||
self.trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
|
||||
if update.max_people == 0 && !was_already_cancelled {
|
||||
let coxes = Registration::all_cox(db, self.id).await;
|
||||
for user in coxes {
|
||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||
let notes = match update.notes {
|
||||
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||
_ => String::from(""),
|
||||
};
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||
self.name, self.day, self.planned_starting_time, notes
|
||||
),
|
||||
"Absage Ausfahrt",
|
||||
None,
|
||||
Some(&format!("remove_trip_by_event:{}", self.id)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let rower = Registration::all_rower(db, self.trip_details_id).await;
|
||||
for user in rower {
|
||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||
let notes = match update.notes {
|
||||
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||
_ => String::from(""),
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||
self.name, self.day, self.planned_starting_time, notes
|
||||
),
|
||||
"Absage Ausfahrt",
|
||||
None,
|
||||
Some(&format!(
|
||||
"remove_user_trip_with_trip_details_id:{}",
|
||||
tripdetails.id
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
if update.max_people > 0 && was_already_cancelled {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
|
||||
)
|
||||
.await;
|
||||
Notification::delete_by_action(db, &format!("remove_trip_by_event:{}", self.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
if !Registration::all_rower(db, self.trip_details_id)
|
||||
.await
|
||||
.is_empty()
|
||||
{
|
||||
return Err(
|
||||
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
|
||||
);
|
||||
}
|
||||
if !Registration::all_cox(db, self.trip_details_id)
|
||||
.await
|
||||
.is_empty()
|
||||
{
|
||||
return Err(
|
||||
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
sqlx::query!("DELETE FROM planned_event WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as PlannedEvent can only be created with proper DB backing
|
||||
.unwrap(); //Okay, as Event can only be created with proper DB backing
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.max_people == 0
|
||||
}
|
||||
|
||||
pub async fn get_ics_feed(db: &SqlitePool) -> String {
|
||||
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 {
|
||||
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!(
|
||||
"{}T{}00",
|
||||
event.day.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);
|
||||
}
|
||||
let mut buf = Vec::new();
|
||||
@ -277,7 +396,7 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
mod test {
|
||||
use crate::{model::tripdetails::TripDetails, testdb};
|
||||
|
||||
use super::PlannedEvent;
|
||||
use super::Event;
|
||||
use chrono::NaiveDate;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
@ -285,8 +404,7 @@ mod test {
|
||||
fn test_get_day() {
|
||||
let pool = testdb!();
|
||||
|
||||
let res =
|
||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
@ -296,22 +414,20 @@ mod test {
|
||||
|
||||
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 =
|
||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
assert_eq!(res.len(), 2);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_delete() {
|
||||
let pool = testdb!();
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
planned_event.delete(&pool).await;
|
||||
planned_event.delete(&pool).await.unwrap();
|
||||
|
||||
let res =
|
||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
assert_eq!(res.len(), 0);
|
||||
}
|
||||
|
||||
@ -319,7 +435,7 @@ mod test {
|
||||
fn test_ics() {
|
||||
let pool = testdb!();
|
||||
|
||||
let actual = PlannedEvent::get_ics_feed(&pool).await;
|
||||
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
|
||||
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);
|
||||
}
|
||||
}
|
@ -5,7 +5,9 @@ use rocket::FromForm;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{boat::Boat, log::Log, notification::Notification, rower::Rower, user::User};
|
||||
use super::{
|
||||
boat::Boat, log::Log, notification::Notification, role::Role, rower::Rower, user::User,
|
||||
};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone, Debug)]
|
||||
pub struct Logbook {
|
||||
@ -102,6 +104,7 @@ pub enum LogbookUpdateError {
|
||||
SteeringPersonNotInRowers,
|
||||
UserNotAllowedToUseBoat,
|
||||
OnlyAllowedToEndTripsEndingToday,
|
||||
TooFast(i64, i64),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -116,7 +119,7 @@ pub enum LogbookCreateError {
|
||||
BoatLocked,
|
||||
BoatNotFound,
|
||||
TooManyRowers(usize, usize),
|
||||
RowerAlreadyOnWater(User),
|
||||
RowerAlreadyOnWater(Box<User>),
|
||||
RowerCreateError(i64, String),
|
||||
ArrivalNotAfterDeparture,
|
||||
SteeringPersonNotInRowers,
|
||||
@ -124,6 +127,8 @@ pub enum LogbookCreateError {
|
||||
NotYourEntry,
|
||||
ArrivalSetButNotRemainingTwo,
|
||||
OnlyAllowedToEndTripsEndingToday,
|
||||
CantChangeHandoperatableStatusForThisBoat,
|
||||
TooFast(i64, i64),
|
||||
}
|
||||
|
||||
impl From<LogbookUpdateError> for LogbookCreateError {
|
||||
@ -147,6 +152,7 @@ impl From<LogbookUpdateError> for LogbookCreateError {
|
||||
LogbookUpdateError::OnlyAllowedToEndTripsEndingToday => {
|
||||
LogbookCreateError::OnlyAllowedToEndTripsEndingToday
|
||||
}
|
||||
LogbookUpdateError::TooFast(km, min) => LogbookCreateError::TooFast(km, min),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -295,11 +301,17 @@ ORDER BY departure DESC
|
||||
db: &SqlitePool,
|
||||
mut log: LogToAdd,
|
||||
created_by_user: &User,
|
||||
) -> Result<(), LogbookCreateError> {
|
||||
) -> Result<String, LogbookCreateError> {
|
||||
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
|
||||
return Err(LogbookCreateError::BoatNotFound);
|
||||
};
|
||||
|
||||
if log.shipmaster_only_steering != boat.default_shipmaster_only_steering
|
||||
&& !boat.convert_handoperated_possible
|
||||
{
|
||||
return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat);
|
||||
}
|
||||
|
||||
if boat.amount_seats == 1 && log.rowers.is_empty() {
|
||||
log.rowers = vec![created_by_user.id];
|
||||
}
|
||||
@ -342,7 +354,7 @@ ORDER BY departure DESC
|
||||
{
|
||||
Ok(_) => {
|
||||
tx.commit().await.unwrap();
|
||||
Ok(())
|
||||
Ok(String::new())
|
||||
}
|
||||
Err(a) => Err(a.into()),
|
||||
};
|
||||
@ -377,7 +389,7 @@ ORDER BY departure DESC
|
||||
let user = User::find_by_id(db, *rower as i32).await.unwrap();
|
||||
|
||||
if user.on_water(db).await {
|
||||
return Err(LogbookCreateError::RowerAlreadyOnWater(user));
|
||||
return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -414,7 +426,15 @@ ORDER BY departure DESC
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
let mut ret = String::new();
|
||||
for rower in &log.rowers {
|
||||
let user = User::find_by_id(db, *rower as i32).await.unwrap();
|
||||
if let Some(msg) = user.close_thousands_trip(db).await {
|
||||
ret.push_str(&format!(" • {msg}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
|
||||
@ -505,9 +525,20 @@ ORDER BY departure DESC
|
||||
|
||||
let dep = NaiveDateTime::parse_from_str(&log.departure, "%Y-%m-%dT%H:%M").unwrap();
|
||||
let arr = NaiveDateTime::parse_from_str(&log.arrival, "%Y-%m-%dT%H:%M").unwrap();
|
||||
if arr.timestamp() <= dep.timestamp() {
|
||||
if arr.and_utc().timestamp() < dep.and_utc().timestamp() {
|
||||
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
|
||||
}
|
||||
|
||||
let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60;
|
||||
// Not possible to row < 1 min / 500 m = < 2 min / km
|
||||
let possible_distance_km = duration_in_mins / 2;
|
||||
if log.distance_in_km > possible_distance_km {
|
||||
return Err(LogbookUpdateError::TooFast(
|
||||
log.distance_in_km,
|
||||
duration_in_mins,
|
||||
));
|
||||
}
|
||||
|
||||
let today = Local::now().date_naive();
|
||||
let day_diff = today - arr.date();
|
||||
let day_diff = day_diff.num_days();
|
||||
@ -540,6 +571,7 @@ ORDER BY departure DESC
|
||||
),
|
||||
"Neuer Logbucheintrag",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@ -560,11 +592,36 @@ ORDER BY departure DESC
|
||||
.execute(db.deref_mut())
|
||||
.await.unwrap(); //TODO: fixme
|
||||
|
||||
let duration = arr - dep;
|
||||
if duration.num_days() > 0 {
|
||||
let vorstand = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
|
||||
|
||||
Notification::create_for_role_tx(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("'{}' hat eine mehrtägige Ausfahrt vom {} bis {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,log.departure, log.arrival, log.distance_in_km, log.destination, log.comments.clone().unwrap_or("".into())),
|
||||
"Mehrtägige Ausfahrt eingetragen",
|
||||
None,None
|
||||
).await;
|
||||
}
|
||||
|
||||
if boat.external {
|
||||
let vorstand = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
|
||||
|
||||
Notification::create_for_role_tx(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("'{}' hat eine Ausfahrt mit externem Boot '{}' am {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,boat.name,log.departure,log.distance_in_km, log.destination, log.comments.unwrap_or("".into())),
|
||||
"Ausfahrt mit externem Boot eingetragen",
|
||||
None,None,
|
||||
).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
|
||||
Log::create(db, format!("{user:?} deleted trip: {self:?}")).await;
|
||||
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
|
||||
|
||||
if user.has_role(db, "admin").await
|
||||
|| user.has_role(db, "Vorstand").await
|
||||
@ -586,6 +643,7 @@ mod test {
|
||||
use crate::model::user::User;
|
||||
use crate::testdb;
|
||||
|
||||
use chrono::Duration;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
@ -637,7 +695,7 @@ mod test {
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
Logbook::create(
|
||||
let msg = Logbook::create(
|
||||
&pool,
|
||||
LogToAdd {
|
||||
boat_id: 3,
|
||||
@ -655,7 +713,62 @@ mod test {
|
||||
&User::find_by_id(&pool, 4).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(msg, String::from(""));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create_with_thousands_msg() {
|
||||
let pool = testdb!();
|
||||
|
||||
let logbook = Logbook::find_by_id(&pool, 1).await.unwrap();
|
||||
let user = User::find_by_id(&pool, 2).await.unwrap();
|
||||
let current_date = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||
let start_date = chrono::Local::now() - Duration::days(3);
|
||||
let start_date = start_date.format("%Y-%m-%d").to_string();
|
||||
logbook
|
||||
.home(
|
||||
&pool,
|
||||
&user,
|
||||
super::LogToFinalize {
|
||||
destination: "new-destination".into(),
|
||||
distance_in_km: 995,
|
||||
comments: Some("Perfect water".into()),
|
||||
logtype: None,
|
||||
rowers: vec![2],
|
||||
shipmaster: Some(2),
|
||||
steering_person: Some(2),
|
||||
shipmaster_only_steering: false,
|
||||
departure: format!("{}T10:00", start_date),
|
||||
arrival: format!("{}T12:00", current_date),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msg = Logbook::create(
|
||||
&pool,
|
||||
LogToAdd {
|
||||
boat_id: 3,
|
||||
shipmaster: Some(2),
|
||||
steering_person: Some(2),
|
||||
shipmaster_only_steering: false,
|
||||
departure: "2128-05-20T12:00".into(),
|
||||
arrival: None,
|
||||
destination: None,
|
||||
distance_in_km: None,
|
||||
comments: None,
|
||||
logtype: None,
|
||||
rowers: vec![2],
|
||||
},
|
||||
&User::find_by_id(&pool, 1).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
msg,
|
||||
String::from(" • rower braucht nur mehr 5 km bis die 1000 km voll sind 🤑")
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
|
@ -14,6 +14,63 @@ use super::{family::Family, log::Log, role::Role, user::User};
|
||||
pub struct Mail {}
|
||||
|
||||
impl Mail {
|
||||
pub async fn send_single(
|
||||
db: &SqlitePool,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
body: String,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = to.split(',');
|
||||
for single_rec in splitted {
|
||||
match single_rec.parse() {
|
||||
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||
Err(_) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!("Mail not sent to {single_rec}, because it could not be parsed"),
|
||||
)
|
||||
.await;
|
||||
return Err(format!(
|
||||
"Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let email = email
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(body)
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into());
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
|
@ -1,15 +1,20 @@
|
||||
use chrono::NaiveDate;
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
use waterlevel::WaterlevelDay;
|
||||
|
||||
use self::{
|
||||
planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
|
||||
event::{Event, EventWithUserAndTriptype},
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
waterlevel::Waterlevel,
|
||||
weather::Weather,
|
||||
};
|
||||
|
||||
pub mod boat;
|
||||
pub mod boatdamage;
|
||||
pub mod boathouse;
|
||||
pub mod boatreservation;
|
||||
pub mod event;
|
||||
pub mod family;
|
||||
pub mod location;
|
||||
pub mod log;
|
||||
@ -17,22 +22,27 @@ pub mod logbook;
|
||||
pub mod logtype;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod planned_event;
|
||||
pub mod role;
|
||||
pub mod rower;
|
||||
pub mod stat;
|
||||
pub mod trailer;
|
||||
pub mod trailerreservation;
|
||||
pub mod trip;
|
||||
pub mod tripdetails;
|
||||
pub mod triptype;
|
||||
pub mod user;
|
||||
pub mod usertrip;
|
||||
pub mod waterlevel;
|
||||
pub mod weather;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Day {
|
||||
day: NaiveDate,
|
||||
planned_events: Vec<PlannedEventWithUserAndTriptype>,
|
||||
events: Vec<EventWithUserAndTriptype>,
|
||||
trips: Vec<TripWithUserAndType>,
|
||||
is_pinned: bool,
|
||||
max_waterlevel: Option<WaterlevelDay>,
|
||||
weather: Option<Weather>,
|
||||
}
|
||||
|
||||
impl Day {
|
||||
@ -40,23 +50,27 @@ impl Day {
|
||||
if is_pinned {
|
||||
Self {
|
||||
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,
|
||||
is_pinned,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
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,
|
||||
is_pinned,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn new_guest(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
|
||||
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
|
||||
|
@ -1,12 +1,13 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::user::User;
|
||||
use super::{role::Role, user::User};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Notification {
|
||||
pub id: i64,
|
||||
pub user_id: i64,
|
||||
@ -15,11 +16,12 @@ pub struct Notification {
|
||||
pub created_at: NaiveDateTime,
|
||||
pub category: String,
|
||||
pub link: Option<String>,
|
||||
pub action_after_reading: Option<String>,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
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)
|
||||
.await
|
||||
.ok()
|
||||
@ -30,13 +32,15 @@ impl Notification {
|
||||
message: &str,
|
||||
category: &str,
|
||||
link: Option<&str>,
|
||||
action_after_reading: Option<&str>,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"INSERT INTO notification(user_id, message, category, link) VALUES (?, ?, ?, ?)",
|
||||
"INSERT INTO notification(user_id, message, category, link, action_after_reading) VALUES (?, ?, ?, ?, ?)",
|
||||
user.id,
|
||||
message,
|
||||
category,
|
||||
link
|
||||
link,
|
||||
action_after_reading
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
@ -49,21 +53,77 @@ impl Notification {
|
||||
message: &str,
|
||||
category: &str,
|
||||
link: Option<&str>,
|
||||
action_after_reading: Option<&str>,
|
||||
) {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
Self::create_with_tx(&mut tx, user, message, category, link).await;
|
||||
Self::create_with_tx(&mut tx, user, message, category, link, action_after_reading).await;
|
||||
tx.commit().await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn create_for_role_tx(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
role: &Role,
|
||||
message: &str,
|
||||
category: &str,
|
||||
link: Option<&str>,
|
||||
action_after_reading: Option<&str>,
|
||||
) {
|
||||
let users = User::all_with_role_tx(db, role).await;
|
||||
|
||||
for user in users {
|
||||
Self::create_with_tx(db, &user, message, category, link, action_after_reading).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_for_role(
|
||||
db: &SqlitePool,
|
||||
role: &Role,
|
||||
message: &str,
|
||||
category: &str,
|
||||
link: Option<&str>,
|
||||
action_after_reading: Option<&str>,
|
||||
) {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
Self::create_for_role_tx(&mut tx, role, message, category, link, action_after_reading)
|
||||
.await;
|
||||
tx.commit().await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT * FROM notification WHERE user_id = ? ORDER BY created_at DESC",
|
||||
let rows = sqlx::query!(
|
||||
"
|
||||
SELECT id, user_id, message, read_at, datetime(created_at, 'localtime') as created_at, category, link, action_after_reading FROM notification
|
||||
WHERE
|
||||
user_id = ?
|
||||
AND (
|
||||
read_at IS NULL
|
||||
OR read_at >= datetime('now', '-14 days')
|
||||
)
|
||||
AND created_at is not NULL
|
||||
ORDER BY read_at DESC, created_at DESC;
|
||||
",
|
||||
user.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
rows.into_iter()
|
||||
.map(|rec| Notification {
|
||||
id: rec.id,
|
||||
user_id: rec.user_id,
|
||||
message: rec.message,
|
||||
read_at: rec.read_at,
|
||||
created_at: NaiveDateTime::parse_from_str(
|
||||
&rec.created_at.unwrap(),
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
.unwrap(),
|
||||
category: rec.category,
|
||||
link: rec.link,
|
||||
action_after_reading: rec.action_after_reading,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn mark_read(self, db: &SqlitePool) {
|
||||
@ -74,5 +134,162 @@ impl Notification {
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some(action) = self.action_after_reading.as_ref() {
|
||||
// User read notification about cancelled trip/event
|
||||
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
|
||||
if let Some(caps) = re.captures(action) {
|
||||
if let Some(matched) = caps.get(1) {
|
||||
if let Ok(number) = matched.as_str().parse::<i32>() {
|
||||
let _ = sqlx::query!(
|
||||
"DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?",
|
||||
self.user_id,
|
||||
number
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cox read notification about cancelled event
|
||||
let re = Regex::new(r"^remove_trip_by_event:(\d+)$").unwrap();
|
||||
if let Some(caps) = re.captures(action) {
|
||||
if let Some(matched) = caps.get(1) {
|
||||
if let Ok(number) = matched.as_str().parse::<i32>() {
|
||||
let _ = sqlx::query!(
|
||||
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
|
||||
self.user_id,
|
||||
number
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
|
||||
sqlx::query!(
|
||||
"DELETE FROM notification WHERE action_after_reading=? and read_at is null",
|
||||
action
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
event::{Event, EventUpdate, Registration},
|
||||
notification::Notification,
|
||||
trip::Trip,
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::{CoxUser, User},
|
||||
usertrip::UserTrip,
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn event_canceled() {
|
||||
let pool = testdb!();
|
||||
|
||||
// Create event
|
||||
let add_tripdetails = TripDetailsToAdd {
|
||||
planned_starting_time: "10:00",
|
||||
max_people: 4,
|
||||
day: "1970-02-01".into(),
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
allow_guests: false,
|
||||
always_show: false,
|
||||
};
|
||||
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
|
||||
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
|
||||
.await
|
||||
.unwrap();
|
||||
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
|
||||
let event = Event::find_by_trip_details(&pool, trip_details.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Rower + Cox joins
|
||||
let rower = User::find_by_name(&pool, "rower").await.unwrap();
|
||||
UserTrip::create(&pool, &rower, &trip_details, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
Trip::new_join(&pool, &cox, &event).await.unwrap();
|
||||
|
||||
// Cancel Event
|
||||
let cancel_update = EventUpdate {
|
||||
name: &event.name,
|
||||
planned_amount_cox: event.planned_amount_cox as i32,
|
||||
max_people: 0,
|
||||
notes: event.notes.as_deref(),
|
||||
always_show: event.always_show,
|
||||
is_locked: event.is_locked,
|
||||
trip_type_id: None,
|
||||
};
|
||||
event.update(&pool, &cancel_update).await;
|
||||
|
||||
// Rower received notification
|
||||
let notifications = Notification::for_user(&pool, &rower).await;
|
||||
let rower_notification = notifications[0].clone();
|
||||
assert_eq!(rower_notification.category, "Absage Ausfahrt");
|
||||
assert_eq!(
|
||||
rower_notification.action_after_reading.as_deref(),
|
||||
Some("remove_user_trip_with_trip_details_id:3")
|
||||
);
|
||||
|
||||
// Cox received notification
|
||||
let notifications = Notification::for_user(&pool, &cox.user).await;
|
||||
let cox_notification = notifications[0].clone();
|
||||
assert_eq!(cox_notification.category, "Absage Ausfahrt");
|
||||
assert_eq!(
|
||||
cox_notification.action_after_reading.as_deref(),
|
||||
Some("remove_trip_by_event:2")
|
||||
);
|
||||
|
||||
// Notification removed if cancellation is cancelled
|
||||
let update = EventUpdate {
|
||||
name: &event.name,
|
||||
planned_amount_cox: event.planned_amount_cox as i32,
|
||||
max_people: 3,
|
||||
notes: event.notes.as_deref(),
|
||||
always_show: event.always_show,
|
||||
is_locked: event.is_locked,
|
||||
trip_type_id: None,
|
||||
};
|
||||
event.update(&pool, &update).await;
|
||||
assert!(Notification::for_user(&pool, &rower).await.is_empty());
|
||||
assert!(Notification::for_user(&pool, &cox.user).await.is_empty());
|
||||
|
||||
// Cancel event again
|
||||
event.update(&pool, &cancel_update).await;
|
||||
|
||||
// Rower is removed if notification is accepted
|
||||
assert!(event.is_rower_registered(&pool, &rower).await);
|
||||
rower_notification.mark_read(&pool).await;
|
||||
assert!(!event.is_rower_registered(&pool, &rower).await);
|
||||
|
||||
// Cox is removed if notification is accepted
|
||||
let registration = Registration::all_cox(&pool, event.id).await;
|
||||
assert_eq!(registration.len(), 1);
|
||||
assert_eq!(registration[0].name, "cox");
|
||||
|
||||
cox_notification.mark_read(&pool).await;
|
||||
|
||||
let registration = Registration::all_cox(&pool, event.id).await;
|
||||
assert!(registration.is_empty());
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Role {
|
||||
@ -45,6 +47,21 @@ WHERE name like ?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name_tx(db: &mut Transaction<'_, Sqlite>, name: &str) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM role
|
||||
WHERE name like ?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> {
|
||||
let query = format!(
|
||||
"SELECT u.name
|
||||
|
@ -1,41 +1,104 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::user::User;
|
||||
use chrono::Datelike;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Stat {
|
||||
name: String,
|
||||
rowed_km: i32,
|
||||
use super::boat::Boat;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct BoatStat {
|
||||
pot_years: Vec<i32>,
|
||||
boats: Vec<SingleBoatStat>,
|
||||
}
|
||||
|
||||
impl Stat {
|
||||
pub async fn boats(db: &SqlitePool, year: Option<i32>) -> Vec<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)
|
||||
sqlx::query(&format!(
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct SingleBoatStat {
|
||||
name: String,
|
||||
location: String,
|
||||
owner: String,
|
||||
years: HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl BoatStat {
|
||||
pub async fn get(db: &SqlitePool) -> BoatStat {
|
||||
let mut years = Vec::new();
|
||||
let mut boat_stats_map: HashMap<String, SingleBoatStat> = HashMap::new();
|
||||
|
||||
let rows = sqlx::query(
|
||||
"
|
||||
SELECT (SELECT name FROM boat WHERE id=logbook.boat_id) as name, CAST(SUM(distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM logbook
|
||||
WHERE arrival LIKE '{year}-%' AND name != 'Externes Boot'
|
||||
GROUP BY boat_id
|
||||
ORDER BY rowed_km DESC;
|
||||
")
|
||||
SELECT
|
||||
boat.id,
|
||||
location.name AS location,
|
||||
CAST(strftime('%Y', COALESCE(arrival, 'now')) AS INTEGER) AS year,
|
||||
CAST(SUM(COALESCE(distance_in_km, 0)) AS INTEGER) AS rowed_km
|
||||
FROM
|
||||
boat
|
||||
LEFT JOIN
|
||||
logbook ON boat.id = logbook.boat_id AND logbook.arrival IS NOT NULL
|
||||
LEFT JOIN
|
||||
location ON boat.location_id = location.id
|
||||
WHERE
|
||||
not boat.external
|
||||
GROUP BY
|
||||
boat.id, year
|
||||
ORDER BY
|
||||
boat.name, year DESC;
|
||||
",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
.unwrap();
|
||||
|
||||
for row in rows {
|
||||
let id: i32 = row.get("id");
|
||||
let boat = Boat::find_by_id(db, id).await.unwrap();
|
||||
let owner = if let Some(owner) = boat.owner(db).await {
|
||||
owner.name
|
||||
} else {
|
||||
String::from("Verein")
|
||||
};
|
||||
let name = boat.name.clone();
|
||||
let location: String = row.get("location");
|
||||
let year: i32 = row.get("year");
|
||||
if year == 0 {
|
||||
continue; // Boat still on water
|
||||
}
|
||||
|
||||
if !years.contains(&year) {
|
||||
years.push(year);
|
||||
}
|
||||
|
||||
let year: String = format!("{year}");
|
||||
|
||||
let rowed_km: i32 = row.get("rowed_km");
|
||||
|
||||
let boat_stat = boat_stats_map
|
||||
.entry(name.clone())
|
||||
.or_insert(SingleBoatStat {
|
||||
name,
|
||||
location,
|
||||
owner,
|
||||
years: HashMap::new(),
|
||||
});
|
||||
boat_stat.years.insert(year, rowed_km);
|
||||
}
|
||||
|
||||
BoatStat {
|
||||
pot_years: years,
|
||||
boats: boat_stats_map.into_values().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Stat {
|
||||
name: String,
|
||||
pub(crate) rowed_km: i32,
|
||||
}
|
||||
|
||||
impl Stat {
|
||||
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
@ -52,7 +115,7 @@ LEFT JOIN (
|
||||
FROM rower
|
||||
GROUP BY logbook_id
|
||||
) m ON l.id = m.logbook_id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND b.name != 'Externes Boot';
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.external;
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
@ -132,6 +195,34 @@ ORDER BY rowed_km DESC, u.name;
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
let row = sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id={}
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@ -155,7 +246,7 @@ FROM (
|
||||
LEFT JOIN
|
||||
rower r ON l.id = r.logbook_id
|
||||
WHERE
|
||||
l.shipmaster = {0} OR r.rower_id = {0}
|
||||
r.rower_id = {}
|
||||
GROUP BY
|
||||
departure_date
|
||||
) as subquery
|
||||
|
31
src/model/trailer.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Trailer {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Trailer {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
233
src/model/trailerreservation.rs
Normal file
@ -0,0 +1,233 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
use super::trailer::Trailer;
|
||||
use super::user::User;
|
||||
use crate::tera::trailerreservation::ReservationEditForm;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TrailerReservation {
|
||||
pub id: i64,
|
||||
pub trailer_id: i64,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: String,
|
||||
pub usage: String,
|
||||
pub user_id_applicant: i64,
|
||||
pub user_id_confirmation: Option<i64>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TrailerReservationWithDetails {
|
||||
#[serde(flatten)]
|
||||
reservation: TrailerReservation,
|
||||
trailer: Trailer,
|
||||
user_applicant: User,
|
||||
user_confirmation: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TrailerReservationToAdd<'r> {
|
||||
pub trailer: &'r Trailer,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_applicant: &'r User,
|
||||
}
|
||||
|
||||
impl TrailerReservation {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM trailer_reservation
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all_future(db: &SqlitePool) -> Vec<TrailerReservationWithDetails> {
|
||||
let trailerreservations = sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM trailer_reservation
|
||||
WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut res = Vec::new();
|
||||
for reservation in trailerreservations {
|
||||
let user_confirmation = match reservation.user_id_confirmation {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailer = Trailer::find_by_id(db, reservation.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
res.push(TrailerReservationWithDetails {
|
||||
reservation,
|
||||
trailer,
|
||||
user_applicant,
|
||||
user_confirmation,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<TrailerReservationWithDetails>> {
|
||||
let mut grouped_reservations: HashMap<String, Vec<TrailerReservationWithDetails>> =
|
||||
HashMap::new();
|
||||
|
||||
let reservations = Self::all_future(db).await;
|
||||
for reservation in reservations {
|
||||
let key = format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
reservation.reservation.start_date,
|
||||
reservation.reservation.end_date,
|
||||
reservation.reservation.time_desc,
|
||||
reservation.reservation.usage,
|
||||
reservation.user_applicant.name
|
||||
);
|
||||
|
||||
grouped_reservations
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(reservation);
|
||||
}
|
||||
|
||||
grouped_reservations
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
trailerreservation: TrailerReservationToAdd<'_>,
|
||||
) -> Result<(), String> {
|
||||
if Self::trailer_reserved_between_dates(
|
||||
db,
|
||||
trailerreservation.trailer,
|
||||
&trailerreservation.start_date,
|
||||
&trailerreservation.end_date,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err("Hänger in diesem Zeitraum bereits reserviert.".into());
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("New trailer reservation: {trailerreservation:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO trailer_reservation(trailer_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
|
||||
trailerreservation.trailer.id,
|
||||
trailerreservation.start_date,
|
||||
trailerreservation.end_date,
|
||||
trailerreservation.time_desc,
|
||||
trailerreservation.usage,
|
||||
trailerreservation.user_applicant.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let board =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
|
||||
for user in board {
|
||||
let date = if trailerreservation.start_date == trailerreservation.end_date {
|
||||
format!("am {}", trailerreservation.start_date)
|
||||
} else {
|
||||
format!(
|
||||
"von {} bis {}",
|
||||
trailerreservation.start_date, trailerreservation.end_date
|
||||
)
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine neue Hängerreservierung für Hänger '{}' {} angelegt. Zeit: {}; Zweck: {}",
|
||||
trailerreservation.user_applicant.name,
|
||||
trailerreservation.trailer.name,
|
||||
date,
|
||||
trailerreservation.time_desc,
|
||||
trailerreservation.usage
|
||||
),
|
||||
"Neue Hängerreservierung",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn trailer_reserved_between_dates(
|
||||
db: &SqlitePool,
|
||||
trailer: &Trailer,
|
||||
start_date: &NaiveDate,
|
||||
end_date: &NaiveDate,
|
||||
) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS reservation_count
|
||||
FROM trailer_reservation
|
||||
WHERE trailer_id = ?
|
||||
AND start_date <= ? AND end_date >= ?;",
|
||||
trailer.id,
|
||||
end_date,
|
||||
start_date
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.reservation_count
|
||||
> 0
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
|
||||
let time_desc = data.time_desc.trim();
|
||||
let usage = data.usage.trim();
|
||||
sqlx::query!(
|
||||
"UPDATE trailer_reservation SET time_desc = ?, usage = ? where id = ?",
|
||||
time_desc,
|
||||
usage,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM trailer_reservation WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
@ -3,8 +3,8 @@ use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::{
|
||||
event::{Event, Registration},
|
||||
notification::Notification,
|
||||
planned_event::{PlannedEvent, Registration},
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{CoxUser, User},
|
||||
@ -30,10 +30,34 @@ pub struct Trip {
|
||||
pub struct TripWithUserAndType {
|
||||
#[serde(flatten)]
|
||||
pub trip: Trip,
|
||||
rower: Vec<Registration>,
|
||||
pub rower: Vec<Registration>,
|
||||
trip_type: Option<TripType>,
|
||||
}
|
||||
|
||||
pub struct TripUpdate<'a> {
|
||||
pub cox: &'a CoxUser,
|
||||
pub trip: &'a Trip,
|
||||
pub max_people: i32,
|
||||
pub notes: Option<&'a str>,
|
||||
pub trip_type: Option<i64>, //TODO: Move to `TripType`
|
||||
pub always_show: bool,
|
||||
pub is_locked: bool,
|
||||
}
|
||||
|
||||
impl TripWithUserAndType {
|
||||
pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
|
||||
let mut trip_type = None;
|
||||
if let Some(trip_type_id) = trip.trip_type_id {
|
||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||
}
|
||||
Self {
|
||||
rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
|
||||
trip,
|
||||
trip_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Trip {
|
||||
/// Cox decides to create own trip.
|
||||
pub async fn new_own(db: &SqlitePool, cox: &CoxUser, trip_details: TripDetails) {
|
||||
@ -62,9 +86,10 @@ impl Trip {
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
user.name, trip.day, trip.planned_starting_time
|
||||
cox.user.name, trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit".into(),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
@ -108,24 +133,28 @@ WHERE trip.id=?
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Cox decides to help in a planned event.
|
||||
/// Cox decides to help in a event.
|
||||
pub async fn new_join(
|
||||
db: &SqlitePool,
|
||||
cox: &CoxUser,
|
||||
planned_event: &PlannedEvent,
|
||||
event: &Event,
|
||||
) -> Result<(), CoxHelpError> {
|
||||
if planned_event.is_rower_registered(db, cox).await {
|
||||
if event.is_rower_registered(db, cox).await {
|
||||
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);
|
||||
}
|
||||
|
||||
if event.max_people == 0 {
|
||||
return Err(CoxHelpError::CanceledEvent);
|
||||
}
|
||||
|
||||
match sqlx::query!(
|
||||
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
|
||||
cox.id,
|
||||
planned_event.id
|
||||
event.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
@ -154,15 +183,7 @@ WHERE day=?
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for trip in trips {
|
||||
let mut trip_type = None;
|
||||
if let Some(trip_type_id) = trip.trip_type_id {
|
||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||
}
|
||||
ret.push(TripWithUserAndType {
|
||||
rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
|
||||
trip,
|
||||
trip_type,
|
||||
});
|
||||
ret.push(TripWithUserAndType::from(db, trip).await);
|
||||
}
|
||||
ret
|
||||
}
|
||||
@ -170,35 +191,82 @@ WHERE day=?
|
||||
/// Cox decides to update own trip.
|
||||
pub async fn update_own(
|
||||
db: &SqlitePool,
|
||||
cox: &CoxUser,
|
||||
trip: &Trip,
|
||||
max_people: i32,
|
||||
notes: Option<&str>,
|
||||
trip_type: Option<i64>, //TODO: Move to `TripType`
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
update: &TripUpdate<'_>,
|
||||
) -> Result<(), TripUpdateError> {
|
||||
if !trip.is_trip_from_user(cox.id) {
|
||||
if !update.trip.is_trip_from_user(update.cox.id) {
|
||||
return Err(TripUpdateError::NotYourTrip);
|
||||
}
|
||||
|
||||
let Some(trip_details_id) = trip.trip_details_id else {
|
||||
let Some(trip_details_id) = update.trip.trip_details_id else {
|
||||
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
|
||||
};
|
||||
|
||||
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||
let was_already_cancelled = tripdetails.max_people == 0;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
|
||||
max_people,
|
||||
notes,
|
||||
trip_type,
|
||||
always_show,
|
||||
is_locked,
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.trip_type,
|
||||
update.always_show,
|
||||
update.is_locked,
|
||||
trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as trip_details can only be created with proper DB backing
|
||||
|
||||
if update.max_people == 0 && !was_already_cancelled {
|
||||
let rowers = TripWithUserAndType::from(db, update.trip.clone())
|
||||
.await
|
||||
.rower;
|
||||
for user in rowers {
|
||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||
let notes = match update.notes {
|
||||
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||
_ => String::from(""),
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {}",
|
||||
update.cox.user.name,
|
||||
update.trip.day,
|
||||
update.trip.planned_starting_time,
|
||||
notes
|
||||
),
|
||||
"Absage Ausfahrt",
|
||||
None,
|
||||
Some(&format!(
|
||||
"remove_user_trip_with_trip_details_id:{}",
|
||||
trip_details_id
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if update.max_people > 0 && was_already_cancelled {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||
trip_details.check_free_spaces(db).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -212,16 +280,16 @@ WHERE day=?
|
||||
pub async fn delete_by_planned_event(
|
||||
db: &SqlitePool,
|
||||
cox: &CoxUser,
|
||||
planned_event: &PlannedEvent,
|
||||
event: &Event,
|
||||
) -> Result<(), TripHelpDeleteError> {
|
||||
if planned_event.trip_details(db).await.is_locked {
|
||||
if event.trip_details(db).await.is_locked {
|
||||
return Err(TripHelpDeleteError::DetailsLocked);
|
||||
}
|
||||
|
||||
let affected_rows = sqlx::query!(
|
||||
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
|
||||
cox.id,
|
||||
planned_event.id
|
||||
event.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
@ -280,6 +348,7 @@ pub enum CoxHelpError {
|
||||
AlreadyRegisteredAsRower,
|
||||
AlreadyRegisteredAsCox,
|
||||
DetailsLocked,
|
||||
CanceledEvent,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -304,8 +373,8 @@ pub enum TripUpdateError {
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
planned_event::PlannedEvent,
|
||||
trip::TripDeleteError,
|
||||
event::Event,
|
||||
trip::{self, TripDeleteError},
|
||||
tripdetails::TripDetails,
|
||||
user::{CoxUser, User},
|
||||
usertrip::UserTrip,
|
||||
@ -355,7 +424,7 @@ mod test {
|
||||
.await
|
||||
.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());
|
||||
}
|
||||
@ -371,7 +440,7 @@ mod test {
|
||||
.await
|
||||
.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();
|
||||
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_err());
|
||||
@ -390,11 +459,17 @@ mod test {
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
assert!(
|
||||
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(trip.max_people, 10);
|
||||
@ -413,11 +488,16 @@ mod test {
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
assert!(
|
||||
Trip::update_own(&pool, &cox, &trip, 10, None, Some(1), false, false)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: Some(1),
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(trip.max_people, 10);
|
||||
@ -437,11 +517,16 @@ mod test {
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
assert!(
|
||||
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_err());
|
||||
assert_eq!(trip.max_people, 1);
|
||||
}
|
||||
|
||||
@ -456,7 +541,7 @@ mod test {
|
||||
.await
|
||||
.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();
|
||||
|
||||
|
@ -4,6 +4,12 @@ use rocket::FromForm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::{
|
||||
notification::Notification,
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
triptype::TripType,
|
||||
};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TripDetails {
|
||||
pub id: i64,
|
||||
@ -46,6 +52,13 @@ WHERE id like ?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn triptype(&self, db: &SqlitePool) -> Option<TripType> {
|
||||
match self.trip_type_id {
|
||||
None => None,
|
||||
Some(id) => TripType::find_by_id(db, id).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_startingdatetime(
|
||||
db: &SqlitePool,
|
||||
day: String,
|
||||
@ -64,6 +77,68 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
.await.unwrap()
|
||||
}
|
||||
|
||||
/// This function is called when a person registers to a trip or when the cox changes the
|
||||
/// amount of free places.
|
||||
pub async fn check_free_spaces(&self, db: &SqlitePool) {
|
||||
if !self.is_full(db).await {
|
||||
// We still have space for new people, no need to do anything
|
||||
return;
|
||||
}
|
||||
|
||||
if self.max_people == 0 {
|
||||
// Cox cancelled event, thus it's probably bad weather. Don't bother with sending
|
||||
// notifications
|
||||
return;
|
||||
}
|
||||
|
||||
if Trip::find_by_trip_details(db, self.id).await.is_none() {
|
||||
// This trip_details belongs to a planned_event, no need to do anything
|
||||
return;
|
||||
};
|
||||
|
||||
let other_trips_same_time = Self::find_by_startingdatetime(
|
||||
db,
|
||||
self.day.clone(),
|
||||
self.planned_starting_time.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
for trip in &other_trips_same_time {
|
||||
if !trip.is_full(db).await {
|
||||
// There are trips on the same time, with open places
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We just got fully booked and there are no other trips with remaining rower places. Send
|
||||
// notification to all coxes which are registered as non-cox.
|
||||
for trip_details in other_trips_same_time {
|
||||
let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await else {
|
||||
// This trip_details belongs to a planned_event, no need to do anything
|
||||
continue;
|
||||
};
|
||||
let pot_coxes = TripWithUserAndType::from(db, trip.clone()).await;
|
||||
let pot_coxes = pot_coxes.rower;
|
||||
for user in pot_coxes {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
let Some(user) = User::find_by_name(db, &user.name).await else {
|
||||
// User is a guest, no need to bother.
|
||||
continue;
|
||||
};
|
||||
if !user.has_role(db, "cox").await {
|
||||
// User is no cox, no need to bother
|
||||
continue;
|
||||
}
|
||||
if user.id == cox.id {
|
||||
// User already offers a trip, no need to bother
|
||||
continue;
|
||||
}
|
||||
|
||||
Notification::create(db, &user, &format!("Du hast dich als Ruderer bei der Ausfahrt von {} am {} um {} angemeldet. Bei allen Ausfahrten zu dieser Zeit sind nun alle Plätze ausgebucht. Damit noch mehr (Nicht-Steuerleute) mitfahren können, wäre es super, wenn du eine eigene Ausfahrt zur selben Zeit ausschreiben könntest.", cox.name, self.day, self.planned_starting_time), "Volle Ausfahrt", None, None).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new entry in `trip_details` and returns its id.
|
||||
pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 {
|
||||
let query = sqlx::query!(
|
||||
@ -138,7 +213,7 @@ ORDER BY day;",
|
||||
|
||||
pub(crate) async fn user_allowed_to_change(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
if self.belongs_to_event(db).await {
|
||||
user.has_role(db, "admin").await
|
||||
user.has_role(db, "planned_event").await
|
||||
} else {
|
||||
self.user_is_cox(db, user).await != CoxAtTrip::No
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use sqlx::{FromRow, SqlitePool};
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TripType {
|
||||
pub id: i64,
|
||||
name: String,
|
||||
pub name: String,
|
||||
desc: String,
|
||||
question: String,
|
||||
icon: String,
|
||||
|
@ -14,7 +14,10 @@ use rocket::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{family::Family, log::Log, role::Role, tripdetails::TripDetails, Day};
|
||||
use super::{
|
||||
family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat,
|
||||
tripdetails::TripDetails, Day,
|
||||
};
|
||||
use crate::tera::admin::user::UserEditForm;
|
||||
|
||||
const RENNRUDERBEITRAG: i32 = 11000;
|
||||
@ -25,8 +28,9 @@ const STUDENT_OR_PUPIL: i32 = 8000;
|
||||
const REGULAR: i32 = 22000;
|
||||
const UNTERSTUETZEND: i32 = 2500;
|
||||
const FOERDERND: i32 = 8500;
|
||||
pub const SCHECKBUCH: i32 = 3000;
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
@ -47,45 +51,25 @@ pub struct User {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserWithRoles {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
impl UserWithRoles {
|
||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
||||
Self {
|
||||
roles: user.roles(db).await,
|
||||
user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserWithWaterStatus {
|
||||
pub struct UserWithDetails {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub amount_unread_notifications: i32,
|
||||
pub on_water: bool,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
impl UserWithWaterStatus {
|
||||
impl UserWithDetails {
|
||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
||||
Self {
|
||||
on_water: user.on_water(db).await,
|
||||
roles: user.roles(db).await,
|
||||
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
||||
user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for User {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginError {
|
||||
InvalidAuthenticationCombo,
|
||||
@ -96,7 +80,7 @@ pub enum LoginError {
|
||||
NotACox,
|
||||
NotATech,
|
||||
GuestNotAllowed,
|
||||
NoPasswordSet(User),
|
||||
NoPasswordSet(Box<User>),
|
||||
DeserializationError,
|
||||
}
|
||||
|
||||
@ -157,6 +141,174 @@ impl Fee {
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(format!(
|
||||
"Could not send welcome mail, because user {} has no email address",
|
||||
self.name
|
||||
));
|
||||
};
|
||||
|
||||
if self.has_role(db, "Donau Linz").await {
|
||||
self.send_welcome_mail_full_member(db, mail, smtp_pw)
|
||||
.await?;
|
||||
} else if self.has_role(db, "scheckbuch").await {
|
||||
self.send_welcome_mail_scheckbuch(db, mail, smtp_pw).await?;
|
||||
} else if self.has_role(db, "schnupperant").await {
|
||||
self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?;
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group",
|
||||
self.name
|
||||
));
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("Willkommensemail wurde an {} versandt", self.name),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_schnupper(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
// 2 things to do:
|
||||
// 1. Send mail to user
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"Schnupperrudern beim ASKÖ Ruderverein Donau Linz",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
|
||||
|
||||
Liebe Grüße, Philipp", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
// 2. Notify all coxes
|
||||
let coxes = Role::find_by_name(db, "schnupper-betreuer").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&coxes,
|
||||
&format!(
|
||||
"Liebe Schnupper-Betreuer, {} nimmt am Schnupperkurs teil.",
|
||||
self.name
|
||||
),
|
||||
"Neue(r) Schnupperteilnehmer:in ",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_scheckbuch(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
// 2 things to do:
|
||||
// 1. Send mail to user
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlich willkommen beim ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dass Du Dich entschieden hast, das Rudern bei uns auszuprobieren. Mit Deinem Scheckbuch kannst Du jetzt an fünf Ausfahrten teilnehmen und so diesen Sport in seiner vollen Vielfalt erleben. Falls du die {1} € noch nicht bezahlt hast, nimm diese bitte zur nächsten Ausfahrt mit (oder überweise sie auf unser Bankkonto [dieses findest du auf https://rudernlinz.at]).
|
||||
|
||||
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge Dich bitte mit Deinem Namen ('{0}', ohne Anführungszeichen) ein. Beim ersten Mal kannst Du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst Du Dich jederzeit für eine Ausfahrt anmelden. Wir bieten mindestens einmal pro Woche Ausfahrten an, sowohl für Anfänger als auch für Fortgeschrittene (A+F Rudern). Zusätzliche Ausfahrten werden von unseren Steuerleuten ausgeschrieben, öfters reinschauen kann sich also lohnen :-)
|
||||
|
||||
Nach deinen 5 Ausfahrten würden wir uns freuen, dich als Mitglied in unserem Verein begrüßen zu dürfen.
|
||||
|
||||
Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||
|
||||
Riemen- & Dollenbruch,
|
||||
ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
// 2. Notify all coxes
|
||||
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&coxes,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hat nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
|
||||
self.name
|
||||
),
|
||||
"Neues Scheckbuch",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_full_member(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
// 2 things to do:
|
||||
// 1. Send mail to user
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"Willkommen im ASKÖ Ruderverein Donau Linz!",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
|
||||
|
||||
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at und it@rudernlinz.at jederzeit zur Verfügung.
|
||||
|
||||
Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH
|
||||
|
||||
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden.
|
||||
|
||||
Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst.
|
||||
|
||||
Außerdem haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz'. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
|
||||
|
||||
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||
|
||||
Riemen- & Dollenbruch
|
||||
ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
// 2. Notify all coxes
|
||||
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&coxes,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}",
|
||||
self.member_since_date.clone().unwrap(),
|
||||
self.name
|
||||
),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
|
||||
if !self.has_role(db, "Donau Linz").await {
|
||||
return None;
|
||||
@ -237,6 +389,17 @@ impl User {
|
||||
.count
|
||||
}
|
||||
|
||||
pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i32 {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.count
|
||||
}
|
||||
|
||||
pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool {
|
||||
if sqlx::query!(
|
||||
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
||||
@ -254,6 +417,18 @@ impl User {
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool {
|
||||
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
Some(a) if a.is_empty() => false,
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
|
||||
sqlx::query!(
|
||||
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;",
|
||||
@ -313,12 +488,14 @@ WHERE id like ?
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
||||
let name = name.trim().to_lowercase();
|
||||
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
FROM user
|
||||
WHERE name like ?
|
||||
WHERE lower(name)=?
|
||||
",
|
||||
name
|
||||
)
|
||||
@ -370,6 +547,13 @@ ORDER BY last_access DESC
|
||||
}
|
||||
|
||||
pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec<Self> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
let ret = Self::all_with_role_tx(&mut tx, role).await;
|
||||
tx.commit().await.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn all_with_role_tx(db: &mut Transaction<'_, Sqlite>, role: &Role) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
@ -380,7 +564,7 @@ WHERE ur.role_id = ? AND deleted = 0
|
||||
ORDER BY name;
|
||||
", role.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.fetch_all(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
@ -436,6 +620,7 @@ ORDER BY last_access DESC
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, name: &str) -> bool {
|
||||
let name = name.trim();
|
||||
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
|
||||
.execute(db)
|
||||
.await
|
||||
@ -449,9 +634,7 @@ ORDER BY last_access DESC
|
||||
family_id = Some(Family::insert(db).await)
|
||||
}
|
||||
|
||||
let user_with_membershippdf = UserWithMembershipPdf::from(db, self.clone()).await;
|
||||
|
||||
if user_with_membershippdf.membership_pdf.is_none() {
|
||||
if !self.has_membership_pdf(db).await {
|
||||
if let Some(membership_pdf) = data.membership_pdf {
|
||||
let mut stream = membership_pdf.open().await.unwrap();
|
||||
let mut buffer = Vec::new();
|
||||
@ -526,8 +709,8 @@ ORDER BY last_access DESC
|
||||
}
|
||||
|
||||
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
|
||||
let name = name.trim(); // just to make sure...
|
||||
let Some(user) = User::find_by_name(db, name).await else {
|
||||
let name = name.trim().to_lowercase(); // just to make sure...
|
||||
let Some(user) = User::find_by_name(db, &name).await else {
|
||||
if ![
|
||||
"n-sageder",
|
||||
"p-hofer",
|
||||
@ -554,10 +737,11 @@ ORDER BY last_access DESC
|
||||
"n.sageder",
|
||||
"a.almousa",
|
||||
"p.hofer",
|
||||
"philipp-hofer",
|
||||
"d.kortschak",
|
||||
"[login]",
|
||||
]
|
||||
.contains(&name)
|
||||
.contains(&name.as_str())
|
||||
{
|
||||
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
|
||||
}
|
||||
@ -583,7 +767,7 @@ ORDER BY last_access DESC
|
||||
Err(LoginError::InvalidAuthenticationCombo)
|
||||
} else {
|
||||
info!("User {name} has no PW set");
|
||||
Err(LoginError::NoPasswordSet(user))
|
||||
Err(LoginError::NoPasswordSet(Box::new(user)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -643,7 +827,7 @@ ORDER BY last_access DESC
|
||||
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
|
||||
if self.has_role(db, "scheckbuch").await {
|
||||
let day = Day::new_guest(db, date, true).await;
|
||||
if !day.planned_events.is_empty() {
|
||||
if !day.events.is_empty() {
|
||||
days.push(day);
|
||||
}
|
||||
} else {
|
||||
@ -667,6 +851,19 @@ ORDER BY last_access DESC
|
||||
6
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn close_thousands_trip(&self, db: &SqlitePool) -> Option<String> {
|
||||
let rowed_km = Stat::person(db, None, self).await.rowed_km;
|
||||
if rowed_km % 1000 > 970 {
|
||||
return Some(format!(
|
||||
"{} braucht nur mehr {} km bis die {} km voll sind 🤑",
|
||||
self.name,
|
||||
1000 - rowed_km % 1000,
|
||||
rowed_km + 1000 - (rowed_km % 1000)
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -733,7 +930,7 @@ impl<'r> FromRequest<'r> for TechUser {
|
||||
}
|
||||
|
||||
pub struct CoxUser {
|
||||
user: User,
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
impl Deref for CoxUser {
|
||||
@ -944,21 +1141,40 @@ impl<'r> FromRequest<'r> for VorstandUser {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PlannedEventUser(pub(crate) User);
|
||||
pub struct EventUser(pub(crate) User);
|
||||
|
||||
impl From<PlannedEventUser> for User {
|
||||
fn from(val: PlannedEventUser) -> Self {
|
||||
impl From<EventUser> for User {
|
||||
fn from(val: EventUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PlannedEventUser {
|
||||
impl Deref for EventUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserWithRolesAndMembershipPdf {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub membership_pdf: bool,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
impl UserWithRolesAndMembershipPdf {
|
||||
pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self {
|
||||
let membership_pdf = user.has_membership_pdf(db).await;
|
||||
|
||||
Self {
|
||||
roles: user.roles(db).await,
|
||||
user,
|
||||
membership_pdf,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserWithMembershipPdf {
|
||||
@ -984,15 +1200,15 @@ impl UserWithMembershipPdf {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for PlannedEventUser {
|
||||
impl<'r> FromRequest<'r> for EventUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "planned_event").await {
|
||||
Outcome::Success(PlannedEventUser(user))
|
||||
if user.has_role(db, "manage_events").await {
|
||||
Outcome::Success(EventUser(user))
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ impl UserTrip {
|
||||
user: &User,
|
||||
trip_details: &TripDetails,
|
||||
user_note: Option<String>,
|
||||
) -> Result<(), UserTripError> {
|
||||
) -> Result<String, UserTripError> {
|
||||
if trip_details.is_full(db).await {
|
||||
return Err(UserTripError::EventAlreadyFull);
|
||||
}
|
||||
@ -27,8 +27,7 @@ impl UserTrip {
|
||||
//TODO: Check if user sees the event (otherwise she could forge trip_details_id)
|
||||
|
||||
let is_cox = trip_details.user_is_cox(db, user).await;
|
||||
let mut name_newly_registered_person = String::new();
|
||||
if user_note.is_none() {
|
||||
let name_newly_registered_person = if user_note.is_none() {
|
||||
if let Yes(action) = is_cox {
|
||||
match action {
|
||||
Action::Helping => return Err(UserTripError::AlreadyRegisteredAsCox),
|
||||
@ -49,7 +48,7 @@ impl UserTrip {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
name_newly_registered_person = user.name.clone();
|
||||
user.name.clone()
|
||||
} else {
|
||||
if !trip_details.user_allowed_to_change(db, user).await {
|
||||
return Err(UserTripError::NotAllowedToAddGuest);
|
||||
@ -63,8 +62,8 @@ impl UserTrip {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
name_newly_registered_person = user_note.unwrap();
|
||||
}
|
||||
user_note.unwrap()
|
||||
};
|
||||
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
@ -77,11 +76,14 @@ impl UserTrip {
|
||||
),
|
||||
"Registrierung bei deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
trip_details.check_free_spaces(db).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(name_newly_registered_person)
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
@ -148,7 +150,7 @@ pub enum UserTripDeleteError {
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
planned_event::PlannedEvent, trip::Trip, tripdetails::TripDetails, user::CoxUser,
|
||||
event::Event, trip::Trip, tripdetails::TripDetails, user::CoxUser,
|
||||
usertrip::UserTripError,
|
||||
},
|
||||
testdb,
|
||||
@ -238,8 +240,8 @@ mod test {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
||||
let event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
Trip::new_join(&pool, &cox, &event).await.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
let result = UserTrip::create(&pool, &cox, &trip_details, None)
|
||||
|
93
src/model/waterlevel.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Waterlevel {
|
||||
pub id: i64,
|
||||
pub day: NaiveDate,
|
||||
pub time: String,
|
||||
pub max: i64,
|
||||
pub min: i64,
|
||||
pub mittel: i64,
|
||||
pub tumax: i64,
|
||||
pub tumin: i64,
|
||||
pub tumittel: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct WaterlevelDay {
|
||||
pub day: NaiveDate,
|
||||
pub avg: i64,
|
||||
pub fluctuation: i64,
|
||||
}
|
||||
|
||||
pub struct Create {
|
||||
pub day: NaiveDate,
|
||||
pub time: String,
|
||||
pub max: i64,
|
||||
pub min: i64,
|
||||
pub mittel: i64,
|
||||
pub tumax: i64,
|
||||
pub tumin: i64,
|
||||
pub tumittel: i64,
|
||||
}
|
||||
|
||||
impl Waterlevel {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn create(db: &mut Transaction<'_, Sqlite>, create: &Create) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO waterlevel(day, time, max, min, mittel, tumax, tumin, tumittel) VALUES (?,?,?,?,?,?,?,?)",
|
||||
create.day, create.time, create.max, create.min, create.mittel, create.tumax, create.tumin, create.tumittel
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<WaterlevelDay> {
|
||||
let waterlevel = sqlx::query_as!(
|
||||
Waterlevel,
|
||||
"SELECT id, day, time, max, min, mittel, tumax, tumin, tumittel FROM waterlevel WHERE day = ? ORDER BY mittel DESC LIMIT 1",
|
||||
day
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await.unwrap();
|
||||
|
||||
if let Some(waterlevel) = waterlevel {
|
||||
let max_diff = (waterlevel.mittel - waterlevel.max).abs();
|
||||
let min_diff = (waterlevel.mittel - waterlevel.min).abs();
|
||||
let fluctuation = max_diff.max(min_diff);
|
||||
|
||||
return Some(WaterlevelDay {
|
||||
day: waterlevel.day,
|
||||
avg: waterlevel.mittel,
|
||||
fluctuation,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {
|
||||
sqlx::query!("DELETE FROM waterlevel;")
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
56
src/model/weather.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct Weather {
|
||||
pub id: i64,
|
||||
pub day: NaiveDate,
|
||||
pub max_temp: f64,
|
||||
pub wind_gust: f64,
|
||||
pub rain_mm: f64,
|
||||
}
|
||||
|
||||
impl Weather {
|
||||
pub async fn find_by_day(db: &SqlitePool, day: NaiveDate) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, day: NaiveDate) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
day: NaiveDate,
|
||||
max_temp: f64,
|
||||
wind_gust: f64,
|
||||
rain_mm: f64,
|
||||
) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO weather(day, max_temp, wind_gust, rain_mm) VALUES (?,?,?,?)",
|
||||
day,
|
||||
max_temp,
|
||||
wind_gust,
|
||||
rain_mm
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {
|
||||
sqlx::query!("DELETE FROM weather;")
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
43
src/scheduled/mod.rs
Normal file
@ -0,0 +1,43 @@
|
||||
mod waterlevel;
|
||||
mod weather;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use job_scheduler_ng::{Job, JobScheduler};
|
||||
use rocket::tokio::{self, task, time};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::tera::Config;
|
||||
|
||||
pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
let db = db.clone();
|
||||
let openweathermap_key = config.openweathermap_key.clone();
|
||||
|
||||
tokio::task::spawn(async {
|
||||
waterlevel::update(&db).await.unwrap();
|
||||
weather::update(&db, &openweathermap_key).await.unwrap();
|
||||
|
||||
let mut sched = JobScheduler::new();
|
||||
|
||||
// Every hour
|
||||
sched.add(Job::new("0 0 * * * * *".parse().unwrap(), move || {
|
||||
let db_clone = db.clone();
|
||||
// Use block_in_place to run async code in the synchronous function; TODO: Make it
|
||||
// nicer one's rust (stable) support async closures
|
||||
task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
waterlevel::update(&db_clone).await.unwrap();
|
||||
weather::update(&db_clone, &openweathermap_key)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
let mut interval = time::interval(Duration::from_secs(60));
|
||||
loop {
|
||||
sched.tick();
|
||||
interval.tick().await;
|
||||
}
|
||||
});
|
||||
}
|
118
src/scheduled/waterlevel.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::waterlevel::{self, Waterlevel};
|
||||
|
||||
pub async fn update(db: &SqlitePool) -> Result<(), String> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
// 1. Delete water levels starting from yesterday
|
||||
Waterlevel::delete_all(&mut tx).await;
|
||||
|
||||
// 2. Fetch
|
||||
let station = fetch()?;
|
||||
for d in station.data {
|
||||
let (Some(max), Some(min), Some(mittel), Some(tumax), Some(tumin), Some(tumittel)) =
|
||||
(d.max, d.min, d.mittel, d.tumax, d.tumin, d.tumittel)
|
||||
else {
|
||||
println!("Ignored invalid values: {d:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(datetime): Result<DateTime<FixedOffset>, _> = d.timestamp.parse() else {
|
||||
return Err("Failed to parse datetime from hydro json".into());
|
||||
};
|
||||
let date: NaiveDate = datetime.naive_utc().date();
|
||||
|
||||
// Extract time component and format as string
|
||||
let time: NaiveTime = datetime.naive_utc().time();
|
||||
let time_str = time.format("%H:%M").to_string();
|
||||
|
||||
let create = waterlevel::Create {
|
||||
day: date,
|
||||
time: time_str,
|
||||
max,
|
||||
min,
|
||||
mittel,
|
||||
tumax,
|
||||
tumin,
|
||||
tumittel,
|
||||
};
|
||||
|
||||
Waterlevel::create(&mut tx, &create).await?
|
||||
}
|
||||
|
||||
// 3. Save in DB
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Station {
|
||||
station_no: String,
|
||||
station_latitude: String,
|
||||
station_longitude: String,
|
||||
parametertype_name: String,
|
||||
ts_shortname: String,
|
||||
ts_name: String,
|
||||
ts_unitname: String,
|
||||
ts_unitsymbol: String,
|
||||
ts_precision: String,
|
||||
rows: String,
|
||||
columns: String,
|
||||
data: Vec<Data>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Data {
|
||||
timestamp: String,
|
||||
max: Option<i64>,
|
||||
min: Option<i64>,
|
||||
mittel: Option<i64>,
|
||||
tumax: Option<i64>,
|
||||
tumin: Option<i64>,
|
||||
tumittel: Option<i64>,
|
||||
}
|
||||
|
||||
fn fetch() -> Result<Station, String> {
|
||||
let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
|
||||
|
||||
match ureq::get(url).call() {
|
||||
Ok(response) => {
|
||||
let forecast: Result<Vec<Station>, _> = response.into_json();
|
||||
|
||||
if let Ok(data) = forecast {
|
||||
if data.len() == 1 {
|
||||
Ok(data[0].clone())
|
||||
} else {
|
||||
Err(format!(
|
||||
"Expected 1 station (Linz); got {} while fetching from {url}. Maybe the hydro data format changed?",
|
||||
data.len()
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(format!(
|
||||
"Failed to parse the json received by {url}: {}",
|
||||
forecast.err().unwrap()
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(format!(
|
||||
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
//#[cfg(test)]
|
||||
//mod test {
|
||||
// use crate::testdb;
|
||||
//
|
||||
// use super::*;
|
||||
// #[sqlx::test]
|
||||
// fn test_fetch_succ() {
|
||||
// let pool = testdb!();
|
||||
// fetch();
|
||||
// }
|
||||
//}
|
118
src/scheduled/weather.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use chrono::DateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::weather::Weather;
|
||||
|
||||
pub async fn update(db: &SqlitePool, api_key: &str) -> Result<(), String> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
// 1. Delete weather data
|
||||
Weather::delete_all(&mut tx).await;
|
||||
|
||||
// 2. Fetch
|
||||
let data = fetch(api_key)?;
|
||||
for d in data.daily {
|
||||
let Some(date) = DateTime::from_timestamp(d.dt, 0) else {
|
||||
println!("Skipping {} because convertion to datetime failed", d.dt);
|
||||
continue;
|
||||
};
|
||||
let max_temp = d.temp.max;
|
||||
let wind_gust = d.wind_gust;
|
||||
let rain_mm = d.rain.unwrap_or(0.);
|
||||
|
||||
Weather::create(
|
||||
&mut tx,
|
||||
date.naive_utc().into(),
|
||||
max_temp,
|
||||
wind_gust,
|
||||
rain_mm,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
// 3. Save in DB
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Data {
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
timezone: String,
|
||||
timezone_offset: i64,
|
||||
daily: Vec<Daily>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Daily {
|
||||
dt: i64,
|
||||
sunrise: i64,
|
||||
sunset: i64,
|
||||
moonrise: i64,
|
||||
moonset: i64,
|
||||
moon_phase: f64,
|
||||
summary: String,
|
||||
temp: Temp,
|
||||
feels_like: FeelsLike,
|
||||
pressure: i64,
|
||||
humidity: i64,
|
||||
dew_point: f64,
|
||||
wind_speed: f64,
|
||||
wind_deg: i64,
|
||||
wind_gust: f64,
|
||||
weather: Vec<DailyWeather>,
|
||||
clouds: i64,
|
||||
pop: f64,
|
||||
rain: Option<f64>,
|
||||
uvi: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Temp {
|
||||
day: f64,
|
||||
min: f64,
|
||||
max: f64,
|
||||
night: f64,
|
||||
eve: f64,
|
||||
morn: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct FeelsLike {
|
||||
day: f64,
|
||||
night: f64,
|
||||
eve: f64,
|
||||
morn: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct DailyWeather {
|
||||
id: i64,
|
||||
main: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
}
|
||||
|
||||
fn fetch(api_key: &str) -> Result<Data, String> {
|
||||
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(response) => {
|
||||
let data: Result<Data, _> = response.into_json();
|
||||
|
||||
if let Ok(data) = data {
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(format!(
|
||||
"Failed to parse the json received by {url}: {}",
|
||||
data.err().unwrap()
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(format!(
|
||||
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
||||
)),
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
use crate::model::{
|
||||
boat::{Boat, BoatToAdd, BoatToUpdate},
|
||||
location::Location,
|
||||
user::{AdminUser, User, UserWithRoles},
|
||||
log::Log,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
@ -32,15 +33,17 @@ async fn index(
|
||||
context.insert("users", &users);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(admin.user, db).await,
|
||||
&UserWithDetails::from_user(admin.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/boat/index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/boat/<boat>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, _admin: AdminUser, boat: i32) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, admin: AdminUser, boat: i32) -> Flash<Redirect> {
|
||||
let boat = Boat::find_by_id(db, boat).await;
|
||||
Log::create(db, format!("{} deleted boat: {boat:?}", admin.user.name)).await;
|
||||
|
||||
match boat {
|
||||
Some(boat) => {
|
||||
boat.delete(db).await;
|
||||
|
@ -8,14 +8,14 @@ use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
planned_event::PlannedEvent,
|
||||
event::{self, Event},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::PlannedEventUser,
|
||||
user::EventUser,
|
||||
};
|
||||
|
||||
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
||||
#[derive(FromForm, Serialize)]
|
||||
struct AddPlannedEventForm<'r> {
|
||||
struct AddEventForm<'r> {
|
||||
name: &'r str,
|
||||
planned_amount_cox: i32,
|
||||
tripdetails: TripDetailsToAdd<'r>,
|
||||
@ -24,8 +24,8 @@ struct AddPlannedEventForm<'r> {
|
||||
#[post("/planned-event", data = "<data>")]
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<AddPlannedEventForm<'_>>,
|
||||
_admin: PlannedEventUser,
|
||||
data: Form<AddEventForm<'_>>,
|
||||
_admin: EventUser,
|
||||
) -> Flash<Redirect> {
|
||||
let data = data.into_inner();
|
||||
|
||||
@ -34,14 +34,14 @@ async fn create(
|
||||
//just created
|
||||
//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")
|
||||
}
|
||||
|
||||
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
||||
#[derive(FromForm)]
|
||||
struct UpdatePlannedEventForm<'r> {
|
||||
#[derive(FromForm, Debug)]
|
||||
struct UpdateEventForm<'r> {
|
||||
id: i64,
|
||||
name: &'r str,
|
||||
planned_amount_cox: i32,
|
||||
@ -49,27 +49,27 @@ struct UpdatePlannedEventForm<'r> {
|
||||
notes: Option<&'r str>,
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
trip_type: Option<i64>,
|
||||
}
|
||||
|
||||
#[put("/planned-event", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UpdatePlannedEventForm<'_>>,
|
||||
_admin: PlannedEventUser,
|
||||
data: Form<UpdateEventForm<'_>>,
|
||||
_admin: EventUser,
|
||||
) -> Flash<Redirect> {
|
||||
match PlannedEvent::find_by_id(db, data.id).await {
|
||||
let update = event::EventUpdate {
|
||||
name: data.name,
|
||||
planned_amount_cox: data.planned_amount_cox,
|
||||
max_people: data.max_people,
|
||||
notes: data.notes,
|
||||
always_show: data.always_show,
|
||||
is_locked: data.is_locked,
|
||||
trip_type_id: data.trip_type,
|
||||
};
|
||||
match Event::find_by_id(db, data.id).await {
|
||||
Some(planned_event) => {
|
||||
planned_event
|
||||
.update(
|
||||
db,
|
||||
data.name,
|
||||
data.planned_amount_cox,
|
||||
data.max_people,
|
||||
data.notes,
|
||||
data.always_show,
|
||||
data.is_locked,
|
||||
)
|
||||
.await;
|
||||
planned_event.update(db, &update).await;
|
||||
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
|
||||
@ -77,13 +77,14 @@ async fn update(
|
||||
}
|
||||
|
||||
#[get("/planned-event/<id>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, id: i64, _admin: PlannedEventUser) -> Flash<Redirect> {
|
||||
match PlannedEvent::find_by_id(db, id).await {
|
||||
Some(planned_event) => {
|
||||
planned_event.delete(db).await;
|
||||
Flash::success(Redirect::to("/planned"), "Event gelöscht")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/planned"), "PlannedEvent does not exist"),
|
||||
async fn delete(db: &State<SqlitePool>, id: i64, _admin: EventUser) -> Flash<Redirect> {
|
||||
let Some(event) = Event::find_by_id(db, id).await else {
|
||||
return Flash::error(Redirect::to("/planned"), "Event does not exist");
|
||||
};
|
||||
|
||||
match event.delete(db).await {
|
||||
Ok(()) => Flash::success(Redirect::to("/planned"), "Event gelöscht"),
|
||||
Err(e) => Flash::error(Redirect::to("/planned"), e),
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,7 +107,7 @@ mod test {
|
||||
fn test_delete() {
|
||||
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 = crate::tera::config(rocket);
|
||||
@ -131,7 +132,7 @@ mod test {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -160,16 +161,16 @@ mod test {
|
||||
.get("_flash")
|
||||
.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]
|
||||
fn test_update() {
|
||||
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()));
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
@ -201,7 +202,7 @@ mod test {
|
||||
"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()));
|
||||
}
|
||||
|
||||
@ -268,7 +269,7 @@ mod test {
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ use crate::model::log::Log;
|
||||
use crate::model::mail::Mail;
|
||||
use crate::model::role::Role;
|
||||
use crate::model::user::AdminUser;
|
||||
use crate::model::user::UserWithRoles;
|
||||
use crate::model::user::UserWithDetails;
|
||||
use crate::tera::Config;
|
||||
|
||||
#[get("/mail")]
|
||||
@ -27,7 +27,7 @@ async fn index(
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(admin.user, db).await,
|
||||
&UserWithDetails::from_user(admin.user, db).await,
|
||||
);
|
||||
context.insert("roles", &roles);
|
||||
|
||||
|
@ -9,8 +9,9 @@ use crate::{
|
||||
};
|
||||
|
||||
pub mod boat;
|
||||
pub mod event;
|
||||
pub mod mail;
|
||||
pub mod planned_event;
|
||||
pub mod notification;
|
||||
pub mod schnupper;
|
||||
pub mod user;
|
||||
|
||||
@ -77,8 +78,9 @@ pub fn routes() -> Vec<Route> {
|
||||
ret.append(&mut user::routes());
|
||||
ret.append(&mut schnupper::routes());
|
||||
ret.append(&mut boat::routes());
|
||||
ret.append(&mut notification::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
|
||||
}
|
||||
|
116
src/tera/admin/notification.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/notification")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.user, db).await,
|
||||
);
|
||||
|
||||
let users: Vec<User> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|u| u.last_access.is_some()) // Not useful to send notifications to people who are
|
||||
// not logging in
|
||||
.sorted_by_key(|u| u.name.clone())
|
||||
.collect();
|
||||
|
||||
context.insert("roles", &Role::all(db).await);
|
||||
context.insert("users", &users);
|
||||
|
||||
Template::render("admin/notification", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct NotificationToSendGroup {
|
||||
pub(crate) role_id: i32,
|
||||
pub(crate) category: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct NotificationToSendUser {
|
||||
pub(crate) user_id: i32,
|
||||
pub(crate) category: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[post("/notification/group", data = "<data>")]
|
||||
async fn send_group(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendGroup>,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
db,
|
||||
format!("{admin:?} trying to send this notification: {d:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let Some(role) = Role::find_by_id(db, d.role_id).await else {
|
||||
return Flash::error(Redirect::to("/admin/notification"), "Rolle gibt's ned");
|
||||
};
|
||||
|
||||
for user in User::all_with_role(db, &role).await {
|
||||
Notification::create(db, &user, &d.message, &d.category, None, None).await;
|
||||
}
|
||||
Log::create(db, "Notification successfully sent".into()).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/notification"),
|
||||
"Nachricht ausgeschickt",
|
||||
)
|
||||
}
|
||||
|
||||
#[post("/notification/user", data = "<data>")]
|
||||
async fn send_user(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendUser>,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
db,
|
||||
format!("{admin:?} trying to send this notification: {d:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let Some(user) = User::find_by_id(db, d.user_id).await else {
|
||||
return Flash::error(Redirect::to("/admin/notification"), "User gibt's ned");
|
||||
};
|
||||
|
||||
Notification::create(db, &user, &d.message, &d.category, None, None).await;
|
||||
|
||||
Log::create(db, "Notification successfully sent".into()).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/notification"),
|
||||
"Nachricht ausgeschickt",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, send_user, send_group]
|
||||
}
|
@ -1,32 +1,12 @@
|
||||
use crate::model::{
|
||||
role::Role,
|
||||
user::{SchnupperBetreuerUser, User, UserWithRoles},
|
||||
user::{SchnupperBetreuerUser, User, UserWithDetails},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use rocket::{
|
||||
get,
|
||||
http::Status,
|
||||
request::{FlashMessage, FromRequest, Outcome},
|
||||
routes, Request, Route, State,
|
||||
};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
// Custom request guard to extract the Referer header
|
||||
struct Referer(String);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for Referer {
|
||||
type Error = ();
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
match request.headers().get_one("Referer") {
|
||||
Some(referer) => Outcome::Success(Referer(referer.to_string())),
|
||||
None => Outcome::Error((Status::BadRequest, ())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/schnupper")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
@ -38,9 +18,9 @@ async fn index(
|
||||
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRoles::from_user(u, db).await })
|
||||
.map(|u| async move { UserWithDetails::from_user(u, db).await })
|
||||
.collect();
|
||||
let users: Vec<UserWithRoles> = join_all(user_futures).await;
|
||||
let users: Vec<UserWithDetails> = join_all(user_futures).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
@ -49,7 +29,7 @@ async fn index(
|
||||
context.insert("schnupperanten", &users);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/schnupper/index", context.into_json())
|
||||
|
@ -1,11 +1,17 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::{
|
||||
family::Family,
|
||||
log::Log,
|
||||
logbook::Logbook,
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithMembershipPdf, UserWithRoles, VorstandUser},
|
||||
use crate::{
|
||||
model::{
|
||||
family::Family,
|
||||
log::Log,
|
||||
logbook::Logbook,
|
||||
role::Role,
|
||||
user::{
|
||||
AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
|
||||
VorstandUser,
|
||||
},
|
||||
},
|
||||
tera::Config,
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use rocket::{
|
||||
@ -45,13 +51,13 @@ async fn index(
|
||||
let user_futures: Vec<_> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRoles::from_user(u, db).await })
|
||||
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
|
||||
.collect();
|
||||
|
||||
let user: User = user.into();
|
||||
let allowed_to_edit = user.has_role(db, "admin").await;
|
||||
|
||||
let users: Vec<UserWithRoles> = join_all(user_futures).await;
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
|
||||
let roles = Role::all(db).await;
|
||||
let families = Family::all_with_members(db).await;
|
||||
@ -64,7 +70,7 @@ async fn index(
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("families", &families);
|
||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
|
||||
Template::render("admin/user/index", context.into_json())
|
||||
}
|
||||
@ -78,14 +84,13 @@ async fn index_admin(
|
||||
let user_futures: Vec<_> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRoles::from_user(u, db).await })
|
||||
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
|
||||
.collect();
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
|
||||
let user: User = user.user;
|
||||
let allowed_to_edit = user.has_role(db, "admin").await;
|
||||
|
||||
let users: Vec<UserWithRoles> = join_all(user_futures).await;
|
||||
|
||||
let roles = Role::all(db).await;
|
||||
let families = Family::all_with_members(db).await;
|
||||
|
||||
@ -97,7 +102,7 @@ async fn index_admin(
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("families", &families);
|
||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
|
||||
Template::render("admin/user/index", context.into_json())
|
||||
}
|
||||
@ -125,7 +130,7 @@ async fn fees(
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(admin.into(), db).await,
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/fees", context.into_json())
|
||||
@ -145,7 +150,7 @@ async fn scheckbuch(
|
||||
for s in scheckbooks {
|
||||
scheckbooks_with_roles.push((
|
||||
Logbook::completed_with_user(db, &s).await,
|
||||
UserWithRoles::from_user(s, db).await,
|
||||
UserWithDetails::from_user(s, db).await,
|
||||
))
|
||||
}
|
||||
|
||||
@ -156,7 +161,7 @@ async fn scheckbuch(
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/scheckbuch", context.into_json())
|
||||
@ -200,11 +205,36 @@ async fn fees_paid(
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/user/<user>/send-welcome-mail")]
|
||||
async fn send_welcome_mail(
|
||||
db: &State<SqlitePool>,
|
||||
_admin: AdminUser,
|
||||
config: &State<Config>,
|
||||
user: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, user).await else {
|
||||
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
|
||||
};
|
||||
|
||||
match user.send_welcome_email(db, &config.smtp_pw).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("Willkommens-Email wurde an {} versandt.", user.name),
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user/<user>/reset-pw")]
|
||||
async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, user).await;
|
||||
match user {
|
||||
Some(user) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} has resetted the pw for {}", admin.user.name, user.name),
|
||||
)
|
||||
.await;
|
||||
user.reset_pw(db).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
@ -284,8 +314,8 @@ async fn download_membership_pdf(
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} downloaded membership application for user: {user:?}",
|
||||
admin.user.name
|
||||
"{} downloaded membership application for user: {}",
|
||||
admin.user.name, user.user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@ -330,6 +360,7 @@ pub fn routes() -> Vec<Route> {
|
||||
fees,
|
||||
fees_paid,
|
||||
scheckbuch,
|
||||
download_membership_pdf
|
||||
download_membership_pdf,
|
||||
send_welcome_mail
|
||||
]
|
||||
}
|
||||
|
@ -39,7 +39,6 @@ struct LoginForm<'r> {
|
||||
password: &'r str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserAgent(String);
|
||||
|
||||
#[rocket::async_trait]
|
||||
@ -83,8 +82,8 @@ async fn login(
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"Succ login of {} with this useragent: {:?}",
|
||||
login.name, agent
|
||||
"Succ login of {} with this useragent: {}",
|
||||
login.name, agent.0
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::model::{
|
||||
boat::Boat,
|
||||
boathouse::Boathouse,
|
||||
user::{AdminUser, UserWithRoles, VorstandUser},
|
||||
user::{AdminUser, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
@ -25,14 +25,21 @@ async fn index(
|
||||
}
|
||||
|
||||
let boats = Boat::all_for_boatshouse(db).await;
|
||||
context.insert("boats", &boats);
|
||||
let mut final_boats = Vec::new();
|
||||
for boat in boats {
|
||||
if boat.boat.boathouse(db).await.is_none() && !boat.boat.external {
|
||||
final_boats.push(boat);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("boats", &final_boats);
|
||||
|
||||
let boathouse = Boathouse::get(db).await;
|
||||
context.insert("boathouse", &boathouse);
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(admin.into(), db).await,
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("board/boathouse", context.into_json())
|
||||
|
@ -13,7 +13,7 @@ use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
||||
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles},
|
||||
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
@ -59,7 +59,7 @@ async fn index(
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("boatdamages", context.into_json())
|
||||
|
223
src/tera/boatreservation.rs
Normal file
@ -0,0 +1,223 @@
|
||||
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::{
|
||||
boat::Boat,
|
||||
boatreservation::{BoatReservation, BoatReservationToAdd},
|
||||
log::Log,
|
||||
user::{DonauLinzUser, User, UserWithDetails},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
async fn index_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let boatreservations = BoatReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let linz_boats = Boat::all_for_boatshouse(db).await;
|
||||
let mut boats = Vec::new();
|
||||
for boat in linz_boats {
|
||||
if boat.boat.owner.is_none() {
|
||||
boats.push(boat);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("boatreservations", &boatreservations);
|
||||
context.insert("boats", &boats);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert("show_kiosk_header", &true);
|
||||
|
||||
Template::render("boatreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Template {
|
||||
let boatreservations = BoatReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let linz_boats = Boat::all_for_boatshouse(db).await;
|
||||
let mut boats = Vec::new();
|
||||
for boat in linz_boats {
|
||||
if boat.boat.owner.is_none() {
|
||||
boats.push(boat);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("boatreservations", &boatreservations);
|
||||
context.insert("boats", &boats);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("boatreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct FormBoatReservationToAdd<'r> {
|
||||
pub boat_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<FormBoatReservationToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = user.into();
|
||||
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
|
||||
let boatreservation_to_add = BoatReservationToAdd {
|
||||
boat: &boat,
|
||||
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 BoatReservation::create(db, boatreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/boatreservation"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/new", data = "<data>")]
|
||||
async fn create_from_kiosk<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatReservationToAdd<'r>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = User::find_by_id(db, data.user_id_applicant.unwrap() as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
|
||||
let boatreservation_to_add = BoatReservationToAdd {
|
||||
boat: &boat,
|
||||
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 BoatReservation::create(db, boatreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/boatreservation"), 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) = BoatReservation::find_by_id(db, data.id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/boatreservation"),
|
||||
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("/boatreservation"),
|
||||
"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("/boatreservation"),
|
||||
"Reservierung erfolgreich bearbeitet",
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/<reservation_id>/delete")]
|
||||
async fn delete<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
reservation_id: i32,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let reservation = BoatReservation::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("/boatreservation"),
|
||||
"Reservierung erfolgreich gelöscht",
|
||||
)
|
||||
} else {
|
||||
Flash::error(
|
||||
Redirect::to("/boatreservation"),
|
||||
"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
|
||||
]
|
||||
}
|
@ -7,9 +7,9 @@ use rocket::{
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
event::Event,
|
||||
log::Log,
|
||||
planned_event::PlannedEvent,
|
||||
trip::{CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::CoxUser,
|
||||
};
|
||||
@ -54,18 +54,16 @@ async fn update(
|
||||
cox: CoxUser,
|
||||
) -> Flash<Redirect> {
|
||||
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
|
||||
match Trip::update_own(
|
||||
db,
|
||||
&cox,
|
||||
&trip,
|
||||
data.max_people,
|
||||
data.notes,
|
||||
data.trip_type,
|
||||
data.always_show,
|
||||
data.is_locked,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: data.max_people,
|
||||
notes: data.notes,
|
||||
trip_type: data.trip_type,
|
||||
always_show: data.always_show,
|
||||
is_locked: data.is_locked,
|
||||
};
|
||||
match Trip::update_own(db, &update).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/planned"),
|
||||
"Ausfahrt erfolgreich aktualisiert.",
|
||||
@ -84,7 +82,7 @@ async fn update(
|
||||
|
||||
#[get("/join/<planned_event_id>")]
|
||||
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
if let Some(planned_event) = 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 {
|
||||
Ok(_) => {
|
||||
Log::create(
|
||||
@ -97,6 +95,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
|
||||
.await;
|
||||
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
|
||||
}
|
||||
Err(CoxHelpError::CanceledEvent) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
|
||||
}
|
||||
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
|
||||
}
|
||||
@ -105,7 +106,7 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
|
||||
"Du hast dich bereits als Ruderer angemeldet!",
|
||||
),
|
||||
Err(CoxHelpError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Boot ist bereits eingeteilt.")
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -136,7 +137,7 @@ async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flas
|
||||
|
||||
#[get("/remove/<planned_event_id>")]
|
||||
async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
if let Some(planned_event) = 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 {
|
||||
Ok(_) => {
|
||||
Log::create(
|
||||
@ -151,7 +152,7 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
|
||||
}
|
||||
Err(TripHelpDeleteError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Boot bereits eingeteilt")
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!")
|
||||
}
|
||||
Err(TripHelpDeleteError::CoxNotHelping) => {
|
||||
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
|
||||
|
@ -18,7 +18,7 @@ use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
user::{AdminUser, User, UserWithRoles},
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@ -51,7 +51,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
|
||||
|
||||
Template::render(
|
||||
"ergo.final",
|
||||
context!(loggedin_user: &UserWithRoles::from_user(_user.user, db).await, thirty, dozen),
|
||||
context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
|
||||
)
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("users", &users);
|
||||
context.insert("thirty", &thirty);
|
||||
context.insert("dozen", &dozen);
|
||||
|
@ -17,16 +17,17 @@ use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
boat::Boat,
|
||||
boatreservation::BoatReservation,
|
||||
log::Log,
|
||||
logbook::{
|
||||
LogToAdd, LogToFinalize, Logbook, LogbookCreateError, LogbookDeleteError,
|
||||
LogbookUpdateError,
|
||||
},
|
||||
logtype::LogType,
|
||||
user::{AdminUser, DonauLinzUser, User, UserWithRoles, UserWithWaterStatus},
|
||||
user::{AdminUser, DonauLinzUser, User, UserWithDetails},
|
||||
};
|
||||
|
||||
pub struct KioskCookie(String);
|
||||
pub struct KioskCookie(());
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for KioskCookie {
|
||||
@ -34,7 +35,7 @@ impl<'r> FromRequest<'r> for KioskCookie {
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<KioskCookie, Self::Error> {
|
||||
match request.cookies().get_private("kiosk") {
|
||||
Some(cookie) => request::Outcome::Success(KioskCookie(cookie.value().to_string())),
|
||||
Some(_) => request::Outcome::Success(KioskCookie(())),
|
||||
None => request::Outcome::Forward(rocket::http::Status::SeeOther),
|
||||
}
|
||||
}
|
||||
@ -48,20 +49,28 @@ async fn index(
|
||||
) -> Template {
|
||||
let boats = Boat::for_user(db, &user).await;
|
||||
|
||||
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
|
||||
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
|
||||
User::cox(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
let users: Vec<UserWithWaterStatus> = futures::future::join_all(
|
||||
coxes.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let mut users: Vec<UserWithDetails> = futures::future::join_all(
|
||||
User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
users.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Logbook::distances(db).await;
|
||||
|
||||
@ -73,12 +82,16 @@ async fn index(
|
||||
}
|
||||
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"reservations",
|
||||
&BoatReservation::all_future_with_groups(db).await,
|
||||
);
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("users", &users);
|
||||
context.insert("logtypes", &logtypes);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
context.insert("on_water", &on_water);
|
||||
context.insert("distances", &distances);
|
||||
@ -92,7 +105,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, loggedin_user: &UserWithRoles::from_user(user.into(), db).await),
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await),
|
||||
)
|
||||
}
|
||||
|
||||
@ -102,7 +115,7 @@ async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Te
|
||||
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await),
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await),
|
||||
)
|
||||
}
|
||||
|
||||
@ -138,20 +151,30 @@ async fn kiosk(
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let boats = Boat::all(db).await;
|
||||
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
|
||||
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
|
||||
User::cox(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
let users: Vec<UserWithWaterStatus> = futures::future::join_all(
|
||||
|
||||
coxes.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let mut users: Vec<UserWithDetails> = futures::future::join_all(
|
||||
User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
|
||||
users.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Logbook::distances(db).await;
|
||||
|
||||
@ -163,6 +186,10 @@ async fn kiosk(
|
||||
}
|
||||
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"reservations",
|
||||
&BoatReservation::all_future_with_groups(db).await,
|
||||
);
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("users", &users);
|
||||
context.insert("logtypes", &logtypes);
|
||||
@ -185,7 +212,7 @@ async fn create_logbook(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt erfolgreich hinzugefügt"),
|
||||
Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
|
||||
Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
|
||||
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
|
||||
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
|
||||
@ -199,7 +226,8 @@ async fn create_logbook(
|
||||
Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"),
|
||||
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
|
||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
|
||||
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,7 +262,13 @@ async fn create_kiosk(
|
||||
} else if let Some(shipmaster) = data.shipmaster {
|
||||
User::find_by_id(db, shipmaster as i32).await.unwrap()
|
||||
} else {
|
||||
User::find_by_id(db, data.rowers[0] as i32).await.unwrap()
|
||||
let Some(rower) = data.rowers.first() else {
|
||||
return Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Ausfahrt ohne Benutzer kann nicht angelegt werden.",
|
||||
);
|
||||
};
|
||||
User::find_by_id(db, *rower as i32).await.unwrap()
|
||||
};
|
||||
Log::create(
|
||||
db,
|
||||
@ -266,6 +300,7 @@ async fn home_logbook(
|
||||
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
|
||||
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(e) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
|
||||
|
@ -1,12 +1,12 @@
|
||||
use rocket::{get, http::ContentType, routes, Route, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::planned_event::PlannedEvent;
|
||||
use crate::model::event::Event;
|
||||
|
||||
#[get("/cal")]
|
||||
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||
//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> {
|
||||
|
119
src/tera/mod.rs
@ -1,6 +1,9 @@
|
||||
use std::{fs::OpenOptions, io::Write};
|
||||
|
||||
use chrono::Local;
|
||||
use rocket::{
|
||||
catch, catchers,
|
||||
fairing::AdHoc,
|
||||
fairing::{AdHoc, Fairing, Info, Kind},
|
||||
form::Form,
|
||||
fs::FileServer,
|
||||
get,
|
||||
@ -10,7 +13,7 @@ use rocket::{
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
time::{Duration, OffsetDateTime},
|
||||
Build, FromForm, Request, Rocket, State,
|
||||
Build, Data, FromForm, Request, Rocket, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Deserialize;
|
||||
@ -18,14 +21,17 @@ use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
user::{User, UserWithRoles},
|
||||
role::Role,
|
||||
user::{User, UserWithDetails, SCHECKBUCH},
|
||||
};
|
||||
|
||||
pub(crate) mod admin;
|
||||
mod auth;
|
||||
pub(crate) mod board;
|
||||
mod boatdamage;
|
||||
pub(crate) mod boatreservation;
|
||||
mod cox;
|
||||
mod ergo;
|
||||
mod log;
|
||||
@ -33,6 +39,7 @@ mod misc;
|
||||
mod notification;
|
||||
mod planned;
|
||||
mod stat;
|
||||
pub(crate) mod trailerreservation;
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct LoginForm<'r> {
|
||||
@ -47,11 +54,51 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
if user.has_role(db, "scheckbuch").await {
|
||||
let last_trips = Logbook::completed_with_user(db, &user).await;
|
||||
context.insert("last_trips", &last_trips);
|
||||
}
|
||||
|
||||
context.insert("notifications", &Notification::for_user(db, &user).await);
|
||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("costs_scheckbuch", &SCHECKBUCH);
|
||||
|
||||
Template::render("index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/impressum")]
|
||||
async fn impressum(db: &State<SqlitePool>, user: Option<User>) -> Template {
|
||||
let mut context = Context::new();
|
||||
|
||||
if let Some(user) = user {
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
}
|
||||
|
||||
Template::render("impressum", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/steering")]
|
||||
async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let bootskundige =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Bootsführer").await.unwrap()).await;
|
||||
|
||||
let mut coxes = User::all_with_role(db, &Role::find_by_name(db, "cox").await.unwrap()).await;
|
||||
|
||||
coxes.retain(|user| !bootskundige.contains(user)); // Remove bootskundige from coxes list
|
||||
coxes.retain(|user| user.name != "Externe Steuerperson");
|
||||
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("bootskundige", &bootskundige);
|
||||
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
Template::render("steering", context.into_json())
|
||||
}
|
||||
|
||||
#[post("/", data = "<login>")]
|
||||
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
|
||||
match User::login(db, login.name, login.password).await {
|
||||
@ -76,16 +123,75 @@ fn forbidden_error() -> Flash<Redirect> {
|
||||
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
|
||||
}
|
||||
|
||||
struct Usage {}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl Fairing for Usage {
|
||||
fn info(&self) -> Info {
|
||||
Info {
|
||||
name: "Usage stats of website",
|
||||
kind: Kind::Request,
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the counter for `GET` and `POST` requests.
|
||||
async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) {
|
||||
let timestamp = Local::now().format("%Y-%m-%dT%H:%M:%S");
|
||||
|
||||
let user = match req.cookies().get_private("loggedin_user") {
|
||||
Some(user_id) => match user_id.value().parse::<i32>() {
|
||||
Ok(user_id) => {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
if let Some(user) = User::find_by_id(db, user_id).await {
|
||||
format!("User: {}", user.name)
|
||||
} else {
|
||||
format!("USER ID {user_id} NOT EXISTS")
|
||||
}
|
||||
}
|
||||
Err(_) => format!("INVALID USER ID ({user_id})"),
|
||||
},
|
||||
None => "NOT LOGGED IN".to_string(),
|
||||
};
|
||||
|
||||
let uri = req.uri().to_string();
|
||||
|
||||
if !uri.ends_with(".css")
|
||||
&& !uri.ends_with(".js")
|
||||
&& !uri.ends_with(".ico")
|
||||
&& !uri.ends_with(".json")
|
||||
&& !uri.ends_with(".png")
|
||||
{
|
||||
let config = req.rocket().state::<Config>().unwrap();
|
||||
let Ok(mut file) = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(config.usage_log_path.clone())
|
||||
else {
|
||||
eprintln!(
|
||||
"File {} can't be found, not saving usage logs",
|
||||
config.usage_log_path.clone()
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = writeln!(file, "{timestamp};{user};{uri}") {
|
||||
eprintln!("Couldn't write to file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Config {
|
||||
rss_key: String,
|
||||
smtp_pw: String,
|
||||
usage_log_path: String,
|
||||
pub openweathermap_key: String,
|
||||
}
|
||||
|
||||
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
rocket
|
||||
.mount("/", routes![index])
|
||||
.mount("/", routes![index, steering, impressum])
|
||||
.mount("/auth", auth::routes())
|
||||
.mount("/wikiauth", routes![wikiauth])
|
||||
.mount("/log", log::routes())
|
||||
@ -94,6 +200,8 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
.mount("/notification", notification::routes())
|
||||
.mount("/stat", stat::routes())
|
||||
.mount("/boatdamage", boatdamage::routes())
|
||||
.mount("/boatreservation", boatreservation::routes())
|
||||
.mount("/trailerreservation", trailerreservation::routes())
|
||||
.mount("/cox", cox::routes())
|
||||
.mount("/admin", admin::routes())
|
||||
.mount("/board", board::routes())
|
||||
@ -102,6 +210,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
.register("/", catchers![unauthorized_error, forbidden_error])
|
||||
.attach(Template::fairing())
|
||||
.attach(AdHoc::config::<Config>())
|
||||
.attach(Usage {})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -10,10 +10,9 @@ use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
logbook::Logbook,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{AllowedForPlannedTripsUser, User, UserWithRoles},
|
||||
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
};
|
||||
|
||||
@ -27,16 +26,11 @@ async fn index(
|
||||
|
||||
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;
|
||||
context.insert("trip_types", &triptypes);
|
||||
}
|
||||
|
||||
if user.has_role(db, "scheckbuch").await {
|
||||
let last_trips = Logbook::completed_with_user(db, &user).await;
|
||||
context.insert("last_trips", &last_trips);
|
||||
}
|
||||
|
||||
let days = user.get_days(db).await;
|
||||
|
||||
if let Some(msg) = flash {
|
||||
@ -44,7 +38,7 @@ async fn index(
|
||||
}
|
||||
|
||||
context.insert("fee", &user.fee(db).await);
|
||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("days", &days);
|
||||
Template::render("planned", context.into_json())
|
||||
}
|
||||
@ -63,15 +57,25 @@ async fn join(
|
||||
};
|
||||
|
||||
match UserTrip::create(db, &user, &trip_details, user_note).await {
|
||||
Ok(_) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} registered for trip_details.id={}",
|
||||
user.name, trip_details_id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
Ok(registered_user) => {
|
||||
if registered_user == user.name {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} registered for trip_details.id={}",
|
||||
user.name, trip_details_id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}else{
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} registered the guest '{}' for trip_details.id={}",
|
||||
user.name, registered_user, trip_details_id
|
||||
),
|
||||
).await;
|
||||
}
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!")
|
||||
}
|
||||
Err(UserTripError::EventAlreadyFull) => {
|
||||
@ -97,7 +101,7 @@ async fn join(
|
||||
),
|
||||
Err(UserTripError::DetailsLocked) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.",
|
||||
),
|
||||
}
|
||||
}
|
||||
@ -138,7 +142,7 @@ async fn remove_guest(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
|
||||
}
|
||||
Err(UserTripDeleteError::GuestNotParticipating) => {
|
||||
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
|
||||
|
@ -3,30 +3,26 @@ use rocket_dyn_templates::{context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
stat::{self, Stat},
|
||||
user::{DonauLinzUser, UserWithRoles},
|
||||
stat::{self, BoatStat, Stat},
|
||||
user::{DonauLinzUser, UserWithDetails},
|
||||
};
|
||||
|
||||
use super::log::KioskCookie;
|
||||
|
||||
#[get("/boats?<year>", rank = 2)]
|
||||
async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
|
||||
let stat = Stat::boats(db, year).await;
|
||||
#[get("/boats", rank = 2)]
|
||||
async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
let stat = BoatStat::get(db).await;
|
||||
let kiosk = false;
|
||||
|
||||
Template::render(
|
||||
"stat.boats",
|
||||
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, kiosk),
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/boats?<year>")]
|
||||
async fn index_boat_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
_kiosk: KioskCookie,
|
||||
year: Option<i32>,
|
||||
) -> Template {
|
||||
let stat = Stat::boats(db, year).await;
|
||||
#[get("/boats")]
|
||||
async fn index_boat_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie) -> Template {
|
||||
let stat = BoatStat::get(db).await;
|
||||
let kiosk = true;
|
||||
|
||||
Template::render("stat.boats", context!(stat, kiosk, show_kiosk_header: true))
|
||||
@ -42,7 +38,7 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||
)
|
||||
}
|
||||
|
||||
|
211
src/tera/trailerreservation.rs
Normal file
@ -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
|
||||
]
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
-- test user
|
||||
INSERT INTO user(name) VALUES('Marie');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz'));
|
||||
INSERT INTO user(name) VALUES('Philipp');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz'));
|
||||
|
1
stats/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
tmp/
|
6
stats/s.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -rf tmp
|
||||
mkdir tmp
|
||||
scp root@128.140.64.118:"/var/log/nginx/access.log*" ./tmp/
|
||||
zcat -f ./tmp/access.log* | goaccess --log-format=COMBINED
|
@ -7,13 +7,19 @@
|
||||
<div class="grid ">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Mail senden</h2>
|
||||
<form action="/admin/mail" method="post" enctype="multipart/form-data">
|
||||
<h2 class="h2">Mail versenden</h2>
|
||||
<form action="/admin/mail"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="grid gap-3 p-3">
|
||||
{{ macros::select(label="Gruppe", data=roles, name="role_id") }}
|
||||
{{ macros::input(label="Betreff", name="subject", type="text", required=true) }}
|
||||
<textarea name="body" rows="4" cols="50" class="dark:text-white"></textarea>
|
||||
<div class="">
|
||||
<label for="content" class=" text-sm text-gray-600 dark:text-white ">Inhalt</label>
|
||||
<textarea id="content" name="body" rows="4" cols="50" class="input rounded-md"></textarea>
|
||||
</div>
|
||||
<input type="file" name="files" multiple />
|
||||
<input type="submit" value="Abschicken" />
|
||||
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
34
templates/admin/notification.html.tera
Normal file
@ -0,0 +1,34 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/boat" as boat %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full dark:text-white">
|
||||
<h1 class="h1">Nachricht senden</h1>
|
||||
<div class="grid ">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Gruppe</h2>
|
||||
<form action="/admin/notification/group"
|
||||
method="post"
|
||||
class="grid gap-3 p-3">
|
||||
{{ macros::select(label="Gruppe", data=roles, name="role_id") }}
|
||||
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
|
||||
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }}
|
||||
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Person</h2>
|
||||
<form action="/admin/notification/user"
|
||||
method="post"
|
||||
class="grid gap-3 p-3">
|
||||
{{ macros::select(label="Person", data=users, name="user_id") }}
|
||||
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
|
||||
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }}
|
||||
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -58,6 +58,10 @@
|
||||
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a>
|
||||
{% endif %}
|
||||
{% if not user.last_access and "admin" in loggedin_user.roles %}
|
||||
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/send-welcome-mail">Willkommensmail verschicken</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{% for role in roles %}
|
||||
|
@ -6,7 +6,20 @@
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="manifest" href="public/manifest.json" />
|
||||
<link rel="stylesheet" href="/public/main.css" />
|
||||
<link rel="icon" type="image/x-icon" href="/public/images/favicon.ico">
|
||||
<link rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/public/images/apple-touch-icon.png">
|
||||
<link rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/public/images/favicon-32x32.png">
|
||||
<link rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/public/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/public/images/site.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<title>Ruderassistent - ASKÖ Ruderverein Donau Linz</title>
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-black">
|
||||
@ -22,6 +35,8 @@
|
||||
<a href="/stat" class="px-2">Statistik</a>
|
||||
<a href="/stat/boats" class="px-2">Bootsauswertung</a>
|
||||
<a href="/boatdamage" class="px-2">Bootsschaden</a>
|
||||
<a href="/boatreservation" class="px-2">Bootsreservierung</a>
|
||||
<a href="/trailerreservation" class="px-2">Hängerreservierung</a>
|
||||
</div>
|
||||
</header>
|
||||
{% endif %}
|
||||
@ -41,5 +56,3 @@
|
||||
<script src="/public/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -7,8 +7,11 @@
|
||||
{% set aisle = aisle_name ~ "-aisle" %}
|
||||
{% set place = boathouse[aisle][side_name] %}
|
||||
{% if place[level] %}
|
||||
{{ place[level].1.name }} <a class="btn btn-primary absolute end-0"
|
||||
href="/board/boathouse/{{ place[level].0 }}/delete">X</a>
|
||||
{{ place[level].1.name }}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a class="btn btn-primary absolute end-0"
|
||||
href="/board/boathouse/{{ place[level].0 }}/delete">X</a>
|
||||
{% endif %}
|
||||
{% elif boats | length > 0 %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<details>
|
||||
|
@ -81,7 +81,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
{% if loggedin_user and "tech" in loggedin_user.roles and boatdamage.fixed_at %}
|
||||
<form action="/boatdamage/{{ boatdamage.id }}/verified"
|
||||
|
98
templates/boatreservations.html.tera
Normal file
@ -0,0 +1,98 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/log" as log %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Bootsreservierungen</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="/boatreservation/new" method="post" class="grid gap-3">
|
||||
{{ log::boat_select(only_ones=false, id='boat') }}
|
||||
{% 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 boatreservations %}
|
||||
{% 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.boat.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.boat.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="/boatreservation"
|
||||
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="/boatreservation/{{ 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 %}
|
@ -9,7 +9,7 @@
|
||||
{{ macros::input(label='Startzeit', name='tripdetails.planned_starting_time', type='time', required=true) }}
|
||||
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }}
|
||||
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='tripdetails.max_people', type='number', required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Gäste erlauben', name='tripdetails.allow_guests') }}
|
||||
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='tripdetails.allow_guests') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='tripdetails.always_show') }}
|
||||
{{ macros::input(label='Anmerkungen', name='tripdetails.notes', type='input') }}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='tripdetails.trip_type', default='Reguläre Ausfahrt') }}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<input class="day-js" type="hidden" name="day" value="" />
|
||||
{{ macros::input(label='Startzeit (zB "10:00")', name='planned_starting_time', type='time', required=true) }}
|
||||
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Gäste erlauben', name='allow_guests') }}
|
||||
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='allow_guests') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input') }}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
|
||||
|
113
templates/impressum.html.tera
Normal file
@ -0,0 +1,113 @@
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Impressum</h1>
|
||||
<div class="grid gap-3 my-5">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Allgemein</h2>
|
||||
<div class="p-3">
|
||||
Die Website wird vom ASKÖ Ruderverein Donau Linz betrieben.
|
||||
<br />
|
||||
<strong>Postanschrift:</strong>
|
||||
<br />
|
||||
ASKÖ Ruderverein Donau Linz
|
||||
<br />
|
||||
Heilhamerweg 2
|
||||
<br />
|
||||
4040 Linz
|
||||
<br />
|
||||
ZVR: 363903285
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-3 my-5">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Datenschutz</h2>
|
||||
<div class="p-3">
|
||||
Folgende Daten werden verarbeitet:
|
||||
<ul>
|
||||
<li>
|
||||
Server-Log Files: IP-Adresse, Adresse der besuchten Seite, Browseragent, Datum und Uhrzeit. Wir nutzen diese Daten nicht und geben Sie in der Regel nicht weiter, können jedoch nicht ausschließen, dass diese Daten beim Vorliegen von rechtswidrigem Verhalten eingesehen werden.
|
||||
</li>
|
||||
<li>
|
||||
Cookie: Diese Website verwendet nur einen Cookie (loggedin_user), der verschlüsselte Informationen über den Login-Status speichert. Weitere Cookies werden nicht verwendet.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if loggedin_user %}
|
||||
<div class="grid gap-3 my-5">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Daten</h2>
|
||||
<div class="p-3">
|
||||
Folgende personenbezogenen haben wir von dir gespeichert:
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Name:</strong> {{ loggedin_user.name }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Passwort:</strong> (verschlüsselt als argon Hash)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Letzter Zugriff:</strong> {{ loggedin_user.last_access }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Mitglied seit:</strong> {{ loggedin_user.member_since_date }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Geburtsdatum:</strong> {{ loggedin_user.birthdate }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Mail:</strong> {{ loggedin_user.mail }}
|
||||
</li>
|
||||
{% if loggedin_user.nickname %}
|
||||
<li>
|
||||
<strong>Spitzname:</strong> {{ loggedin_user.nickname }}
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<strong>Telefonnummer:</strong> {{ loggedin_user.phone }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Adresse:</strong> {{ loggedin_user.address }}
|
||||
</li>
|
||||
<li>(Beitrittserklärung)</li>
|
||||
{% if loggedin_user.family_id %}
|
||||
<li>Verbindung zu Familienmitglied (gespeichert um Familientarif anstatt Vollmitglied zu haben)</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<strong>Rollen:</strong> {{ loggedin_user.roles }} (werden für verschiedene Funktionen im Ruderassistenten verwendet)
|
||||
</li>
|
||||
<li>Anmeldungen zu Ausfahrten</li>
|
||||
<li>Anmeldungen zu Events (zB Fetzenfahrt, Anrudern, USI-Rudern, ...)</li>
|
||||
<li>Logbucheinträge</li>
|
||||
<li>Selber eingetragene Bootsschäden, solange sie nicht > 1 Monat verifiziert und repariert wurden</li>
|
||||
<li>Selber eingetragene Bootsreservierung</li>
|
||||
<li>Boote, sofern es welche im Privatbesitz gibt</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="grid gap-3 my-5">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Lizenzen</h2>
|
||||
<div class="p-3">
|
||||
<ul>
|
||||
<li>
|
||||
Die <strong>Wetterdaten</strong> werden von <a class="underline" href="https://openweathermap.org">OpenWeather</a> bereitgestellt.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Wasserstandsvorhersagen:</strong> Die Vorhersagen werden stündlich vom <a class="underline" href="https://hydro.ooe.gv.at">Hydrographischen Dienstes Oberösterreich</a> geladen und zwischengespeichert, der höchste Tages-Mittelwert wird gemeinsam mit der Schwankungsbreite bei den geplanten Ausfahrten angezeigt. Es handelt sich hierbei um ungeprüfte Rohdaten. Rohdatenfehler können durch betriebliche Störungen an den Messgeräten, Fernübertragungseinrichtungen u. dgl. entstehen. Die Vorhersagen sind daher mit Unsicherheiten behaftet! Mit der Länge des Vorhersagezeitraumeszeitraumes werden diese Unsicherheiten größer! Es wird keine Gewähr für die Vollständigkeit, Richtigkeit und Genauigkeit der dargestellten Daten übernommen. Gewährleistungs- und Haftungsansprüche werden ausdrücklich ausgeschlossen (sowohl vom Hydrographischen Dienstes Oberösterreich als auch vom ASKÖ Ruderverein Donau Linz).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -1,34 +1,39 @@
|
||||
<footer class="bg-primary-950 dark:bg-primary-900 text-white w-full flex justify-center p-3">
|
||||
<div class="max-w-screen-xl w-full flex justify-between items-center">
|
||||
<div>
|
||||
<span class="text-[#ff0000]">♥</span>
|
||||
Erstellt vom ASKÖ Ruderverein Donau Linz <a onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird, dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at – für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');"
|
||||
style="text-decoration:underline">... und dir?</a>
|
||||
<div class="max-w-screen-xl w-full">
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div>
|
||||
<span class="text-[#ff0000]">♥</span>
|
||||
Erstellt vom ASKÖ Ruderverein Donau Linz <a class="underline"
|
||||
onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird, dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at – für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');">... und dir?</a>
|
||||
</div>
|
||||
<div>
|
||||
<button id="theme-toggle-js"
|
||||
type="button"
|
||||
data-theme="light"
|
||||
class="btn btn-primary">
|
||||
<span class="hidden dark:inline">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="inline dark:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="theme-toggle-js"
|
||||
type="button"
|
||||
data-theme="light"
|
||||
class="btn btn-primary">
|
||||
<span class="hidden dark:inline">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="inline dark:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<div class="mt-3">
|
||||
<a class="underline" href="/impressum">Impressum</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -24,6 +24,7 @@
|
||||
{{ macros::input(label='Default destination', name='default_destination', type='text') }}
|
||||
<div>
|
||||
{{ macros::checkbox(label="handgesteuert", name="default_shipmaster_only_steering") }}
|
||||
{{ macros::checkbox(label="Umbau Hand-/Fußsteuerung möglich", name="convert_handoperated_possible") }}
|
||||
{{ macros::checkbox(label="Skull", name="skull", checked=true) }}
|
||||
{{ macros::checkbox(label="Externes Boot (anderer Verein)", name="external") }}
|
||||
</div>
|
||||
@ -66,6 +67,7 @@
|
||||
{{ macros::input(label='Baujahr', name='year_built', type='number', min=1950, value=boat.year_built) }}
|
||||
{{ macros::input(label='Bootsbauer', name='boatbuilder', type='text', value=boat.boatbuilder) }}
|
||||
{{ macros::checkbox(label='handgesteuert', name='default_shipmaster_only_steering', id=uuid , checked=boat.default_shipmaster_only_steering) }}
|
||||
{{ macros::checkbox(label='Umbau Hand-/Fußsteuerung möglich', name='convert_handoperated_possible', id=uuid , checked=boat.convert_handoperated_possible) }}
|
||||
{{ macros::input(label='Default destination', name='default_destination', type='text', value=boat.default_destination) }}
|
||||
{{ macros::checkbox(label='Skull', name='skull', id=uuid , checked=boat.skull) }}
|
||||
{{ macros::checkbox(label='Externes Boot', name='external', id=uuid , checked=boat.external) }}
|
||||
|
@ -3,23 +3,26 @@
|
||||
Inputs: boats
|
||||
#}
|
||||
{% macro show_boats() %}
|
||||
{% for amount_seats, grouped_boats in boats | group_by(attribute="amount_seats") %}
|
||||
<div class="pb-2">
|
||||
<div class="bg-gray-100 dark:bg-primary-600 text-primary-950 dark:text-white text-center text-sm mb-2">
|
||||
<strong>{{ amount_seats }}x</strong>
|
||||
{% for cat, grouped_boats in boats | group_by(attribute="cat") %}
|
||||
<details>
|
||||
<summary class="font-bold cursor-pointer text-primary-900 dark:text-white border-t p-3 hover:bg-gray-100 dark:hover:bg-primary-950">
|
||||
<span>{{ cat }}</span>
|
||||
<small class="text-gray-500 dark:text-gray-100">({{ grouped_boats | length }})</small>
|
||||
</summary>
|
||||
<div class="pb-3">
|
||||
{% for boat in grouped_boats | sort(attribute="name") %}
|
||||
<div id="boat-{{ boat.id }}"
|
||||
class="py-3 mx-3 boats-js text-black dark:text-white border-t {% if boat.damage != 'locked' and not boat.on_water %} cursor-pointer hover:text-primary-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-primary-950 {% endif %}"
|
||||
{% if boat.damage != 'locked' and not boat.on_water %} data-seats="{{ boat.amount_seats }}" data-default_shipmaster_only_steering="{{ boat.default_shipmaster_only_steering }}" data-default-destination="{{ boat.default_destination }}" data-onclick="true" {% endif %}
|
||||
data-id="{{ boat.id }}">
|
||||
<span class="status-damage status-damage-{{ boat.damage }}"></span>
|
||||
<span {% if boat.damage == 'locked' or boat.on_water %}class="opacity-50"{% endif %}>{{ boat.name }}
|
||||
{% if boat.owner %}<span class="opacity-50">(privat)</span>{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for boat in grouped_boats | sort(attribute="name") %}
|
||||
<div id="boat-{{ boat.id }}"
|
||||
class="px-3 boats-js text-black dark:text-white {% if boat.damage != 'locked' and not boat.on_water %} cursor-pointer hover:text-primary-900 dark:hover:text-gray-100 {% endif %}"
|
||||
{% if boat.damage != 'locked' and not boat.on_water %} data-seats="{{ boat.amount_seats }}" data-default_shipmaster_only_steering="{{ boat.default_shipmaster_only_steering }}" data-default-destination="{{ boat.default_destination }}" data-onclick="true" {% endif %}
|
||||
data-id="{{ boat.id }}">
|
||||
<span class="status-damage status-damage-{{ boat.damage }}"></span>
|
||||
<span {% if boat.damage == 'locked' or boat.on_water %}class="opacity-50"{% endif %}>{{ boat.name }}
|
||||
{% if boat.owner %}<span class="opacity-50">(privat)</span>{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{% endmacro show_boats %}
|
||||
{# Shows the form for creating a new logbook entry. #}
|
||||
@ -28,7 +31,7 @@
|
||||
method="post"
|
||||
id="form"
|
||||
class="grid grid-cols-4 gap-3"
|
||||
onsubmit="Array.from(this.elements).forEach(e=>!e.value.trim()&&(e.disabled=true));">
|
||||
onsubmit="if (!this.elements['boat_id'].value) { alert('Kein Boot ausgewählt'); return false; }Array.from(this.elements).forEach(e=>!e.value.trim()&&(e.disabled=true));">
|
||||
{{ log::boat_select() }}
|
||||
<div class="col-span-4 md:col-span-1">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">Bootssteuerung</div>
|
||||
@ -67,7 +70,7 @@
|
||||
</form>
|
||||
{% endmacro new %}
|
||||
{% macro boat_select(id="boat_id") %}
|
||||
{{ macros::select(label="Boot", data=boats, name="boat_id", id=id, display=["name", " (","amount_seats", " x)"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true) }}
|
||||
{{ macros::select(label="Boot", data=boats, name="boat_id", required=true, id=id, display=["name", " (","cat",")"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true, nonSelectableDefault=" -- Wähle ein Boot aus ---") }}
|
||||
{% endmacro boat_select %}
|
||||
{% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %}
|
||||
{#{% if not amount_seats or amount_seats > 1 %}#}
|
||||
@ -86,7 +89,7 @@
|
||||
<option value="{{ user.id }}"
|
||||
{% if sel %}selected{% endif %}
|
||||
{% if user.on_water %}disabled="disabled"{% endif %}
|
||||
data-custom-properties='{"is_cox": {{ "cox" in user.roles }}, "is_racing": {{ "Rennrudern" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat }}}'>
|
||||
data-custom-properties='{"is_cox": {{ "cox" in user.roles }}, "is_racing": {{ "Rennrudern" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat }}, "searchableText": "{{ user.nickname }}"}'>
|
||||
{{ user.name }}
|
||||
{% if user.on_water %}(am Wasser){% endif %}
|
||||
</option>
|
||||
@ -104,6 +107,7 @@
|
||||
<strong class="block text-primary-900 dark:text-white">
|
||||
{{ log.departure | date(format="%H:%M") }}
|
||||
Uhr
|
||||
<small>({{ log.departure | date(format="%d.%m.%Y") }})</small>
|
||||
</strong>
|
||||
<a href="#"
|
||||
data-sidebar="true"
|
||||
@ -138,7 +142,7 @@
|
||||
{% if rower.id == log.shipmaster or rower.id == log.steering_person %}
|
||||
<small class="text-gray-600 dark:text-primary-100">(
|
||||
{%- if rower.id == log.shipmaster %}Schiffsführer
|
||||
{% endif -%}
|
||||
{%- endif -%}
|
||||
{% if rower.id == log.shipmaster and rower.id == log.steering_person %}/{% endif %}
|
||||
{%- if rower.id == log.steering_person %}Steuerperson
|
||||
{%- endif -%}
|
||||
@ -148,8 +152,9 @@
|
||||
{% endfor %}
|
||||
{% set amount_rowers = log.rowers | length %}
|
||||
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
||||
{% if amount_guests > 0 %}
|
||||
Gäste <small class="text-gray-600">(ohne Account)</small>:
|
||||
{% if amount_guests > 0 and not log.boat.external %}
|
||||
Gäste
|
||||
<small class="text-gray-600">(ohne Account)</small>:
|
||||
{{ amount_guests }}
|
||||
{% endif %}
|
||||
{% if allowed_to_close and state == "on_water" %}
|
||||
@ -182,18 +187,23 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
|
||||
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong> <small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
|
||||
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
|
||||
{% if log.shipmaster_only_steering %}
|
||||
- handgesteuert
|
||||
{% endif -%}
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
|
||||
{{ log.departure | date(format='%d.%m.%Y') }} ({{ log.departure | date(format='%H:%M') }} - {{ log.arrival | date(format='%H:%M') }})
|
||||
{{ log.departure | date(format='%d.%m.%Y') }}
|
||||
({{ log.departure | date(format='%H:%M') }}
|
||||
-
|
||||
{{ log.arrival | date(format='%H:%M') }})
|
||||
{% else %}
|
||||
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% if state == "completed" %}
|
||||
- {{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
-
|
||||
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
@ -205,61 +215,72 @@
|
||||
<div class="text-black dark:text-white">
|
||||
{{ log.destination }}
|
||||
{% if state == "completed" %}
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }} km)</small>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }}
|
||||
km)</small>
|
||||
{% endif %}
|
||||
{% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %}
|
||||
</div>
|
||||
{% if amount_guests > 0 or log.rowers | length > 0 %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer:
|
||||
{% for rower in log.rowers -%}
|
||||
{{ rower.name -}}
|
||||
{% if not loop.last or amount_guests > 0 and log.boat.name != 'Externes Boot' %},{% endif %}
|
||||
{% endfor %}
|
||||
{% if amount_guests > 0 and log.boat.name != 'Externes Boot' %}
|
||||
Gäste <small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>: {{ amount_guests }}
|
||||
{% if not log.boat.amount_seats == 1 %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer:
|
||||
{% for rower in log.rowers -%}
|
||||
{{ rower.name }}
|
||||
{%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %}
|
||||
(Steuerperson){%- endif -%}
|
||||
{%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %}
|
||||
{% endfor -%}
|
||||
{% if amount_guests > 0 and not log.boat.external %}
|
||||
Gäste
|
||||
<small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>:
|
||||
{{ amount_guests }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro show_old %}
|
||||
{% macro home(log) %}
|
||||
<form class="grid grid-cols-1 gap-3"
|
||||
action="/log/{{ log.id }}"
|
||||
method="post">
|
||||
{{ macros::input(label='Ankunftszeit', name='arrival', type='datetime-local', required=true, class="change-id-js rounded-md current-date-time") }}
|
||||
<div>
|
||||
<label for="destination" class="text-sm text-gray-600 dark:text-gray-100">Ziel</label>
|
||||
<input class="input rounded-md set-distance-js change-id-js"
|
||||
type="search"
|
||||
list="destinations"
|
||||
placeholder="Destination"
|
||||
required="required"
|
||||
id="destination{{ log.id }}"
|
||||
name="destination"
|
||||
value="{{ log.destination }}"
|
||||
data-relation="distance_in_km{{ log.id }}" />
|
||||
</div>
|
||||
<div class="relative">
|
||||
{{ macros::input(label="Distanz", name="distance_in_km", id="distance_in_km" ~ log.id , type="number", min=0, value=log.distance_in_km, required=true, class="rounded-md change-id-js") }}
|
||||
<span class="absolute right-0 bottom-0 py-1.5 px-2 bg-white dark:bg-primary-950 border-0 text-gray-600 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-primary-950 rounded-br-md rounded-tr-md">km</span>
|
||||
</div>
|
||||
{{ macros::input(label="Kommentar", name="comments", id="comments" ~ log.id, type="text", value=log.comments, class="rounded-md change-id-js") }}
|
||||
<details class="bg-gray-200 bg-opacity-80 dark:bg-primary-900 rounded-md p-2">
|
||||
<summary class="cursor-pointer">Details ändern</summary>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
{{ macros::input(label='Abfahrtszeit', name='departure', type='datetime-local', required=true, class="change-id-js rounded-md", value=log.departure) }}
|
||||
{{ log::rower_select(id="rowers"~log.id, selected=log.rowers, amount_seats=log.boat.amount_seats, steering_person_id=log.steering_user.id, cox_on_boat=log.shipmaster_user.id) }}
|
||||
{{ macros::select(label="Schiffsführer", data=[], name='shipmaster', id="shipmaster-rowers"~log.id, class="change-id-js", selected_id=log.shipmaster_user.id, required=true) }}
|
||||
{{ macros::select(label="Steuerperson", data=[], name='steering_person', id="steering_person-rowers"~log.id, class="change-id-js", selected_id=log.steering_user.id, required=true) }}
|
||||
<div>
|
||||
{{ macros::checkbox(label="Handgesteuert", name="shipmaster_only_steering", id="shipmaster_only_steering" ~ log.id , checked=log.shipmaster_only_steering,class="rounded-md change-id-js") }}
|
||||
</div>
|
||||
{{ macros::select(label="Typ", data=logtypes, name="logtype", id="logtype" ~ log.id, default="Normal", selected_id=log.logtype, class="rounded-md change-id-js") }}
|
||||
</div>
|
||||
</details>
|
||||
<input class="btn btn-primary" type="submit" value="Ausfahrt beenden" />
|
||||
</form>
|
||||
{% endmacro home %}
|
||||
</div>
|
||||
{% endmacro show_old %}
|
||||
{% macro home(log) %}
|
||||
<form class="grid grid-cols-1 gap-3"
|
||||
action="/log/{{ log.id }}"
|
||||
method="post">
|
||||
{{ macros::input(label='Ankunftszeit', name='arrival', type='datetime-local', required=true, class="change-id-js rounded-md current-date-time") }}
|
||||
<div>
|
||||
<label for="destination" class="text-sm text-gray-600 dark:text-gray-100">Ziel</label>
|
||||
<input class="input rounded-md set-distance-js change-id-js"
|
||||
type="search"
|
||||
list="destinations"
|
||||
placeholder="Destination"
|
||||
required="required"
|
||||
id="destination{{ log.id }}"
|
||||
name="destination"
|
||||
value="{{ log.destination }}"
|
||||
data-relation="distance_in_km{{ log.id }}" />
|
||||
</div>
|
||||
<div class="relative">
|
||||
{{ macros::input(label="Distanz", name="distance_in_km", id="distance_in_km" ~ log.id , type="number", min=0, value=log.distance_in_km, required=true, class="rounded-md change-id-js") }}
|
||||
<span class="absolute right-0 bottom-0 py-1.5 px-2 bg-white dark:bg-primary-950 border-0 text-gray-600 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-primary-950 rounded-br-md rounded-tr-md">km</span>
|
||||
</div>
|
||||
{{ macros::input(label="Kommentar", name="comments", id="comments" ~ log.id, type="text", value=log.comments, class="rounded-md change-id-js") }}
|
||||
<details class="bg-gray-200 bg-opacity-80 dark:bg-primary-900 rounded-md p-2">
|
||||
<summary class="cursor-pointer">Details ändern</summary>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
{{ macros::input(label='Abfahrtszeit', name='departure', type='datetime-local', required=true, class="change-id-js rounded-md", value=log.departure) }}
|
||||
{{ log::rower_select(id="rowers"~log.id, selected=log.rowers, amount_seats=log.boat.amount_seats, steering_person_id=log.steering_user.id, cox_on_boat=log.shipmaster_user.id) }}
|
||||
{{ macros::select(label="Schiffsführer", data=[], name='shipmaster', id="shipmaster-rowers"~log.id, class="change-id-js", selected_id=log.shipmaster_user.id, required=true) }}
|
||||
{{ macros::select(label="Steuerperson", data=[], name='steering_person', id="steering_person-rowers"~log.id, class="change-id-js", selected_id=log.steering_user.id, required=true) }}
|
||||
<div>
|
||||
{{ macros::checkbox(label="Handgesteuert", name="shipmaster_only_steering", id="shipmaster_only_steering" ~ log.id , checked=log.shipmaster_only_steering,class="rounded-md change-id-js") }}
|
||||
</div>
|
||||
{{ macros::select(label="Typ", data=logtypes, name="logtype", id="logtype" ~ log.id, default="Normal", selected_id=log.logtype, class="rounded-md change-id-js") }}
|
||||
</div>
|
||||
</details>
|
||||
<input class="btn btn-primary" type="submit" value="Ausfahrt beenden" />
|
||||
</form>
|
||||
<a href="/log/{{ log.id }}/delete"
|
||||
class="btn btn-alert w-full absolute bottom-0 left-0"
|
||||
style="border-radius: 0"
|
||||
onclick="return confirm('Willst du diesen Eintrag wirklich löschen? Die Daten gehen verloren');">Löschen</a>
|
||||
{% endmacro home %}
|
||||
|
@ -1,16 +1,32 @@
|
||||
{% macro boatreservation() %}
|
||||
{#<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3"
|
||||
style="margin-top: 10px">
|
||||
<h2 class="h2">Bootsreservierungen</h2>
|
||||
<div class="p2" style="margin-bottom: 10px;">
|
||||
<ul style="display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0;
|
||||
list-style: none">
|
||||
<li style="display: inline-block;">22.04. | Christian Gusenbauer | Boot: Linz + kleiner Hänger</li>
|
||||
</ul>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2 mt-3">
|
||||
<h2 class="h2">Reservierungen ({{ reservations | length }})</h2>
|
||||
<div class="grid grid-cols-1 gap-3 mb-3 w-full">
|
||||
{% for _, reservations_for_event in reservations %}
|
||||
{% set reservation = reservations_for_event[0] %}
|
||||
<div class="pt-2 px-3 border-t text-primary-900 dark:text-white">
|
||||
<strong class="block">
|
||||
{{ reservation.start_date | date(format="%d.%m.%Y") }}
|
||||
{% if reservation.end_date != reservation.start_date %}
|
||||
-
|
||||
{{ reservation.end_date | date(format="%d.%m.%Y") }}
|
||||
{% endif %}
|
||||
<small>({{ reservation.time_desc }})</small>
|
||||
</strong>
|
||||
<span class="block">
|
||||
{% for reservation in reservations_for_event %}
|
||||
{{ reservation.boat.name }}
|
||||
{% if not loop.last %}+{% endif %}
|
||||
{% endfor %}
|
||||
<small>({{ reservation.user_applicant.name }})</small>
|
||||
</span>
|
||||
<span class="text-sm italic">{{ reservation.usage }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="p-3 text-center text-black dark:text-white">Keine Reservierung</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>#}
|
||||
</div>
|
||||
{% endmacro boatreservation %}
|
||||
{% macro header(loggedin_user) %}
|
||||
<header class="bg-primary-900 text-white flex justify-center p-3 fixed w-full z-10">
|
||||
@ -21,76 +37,80 @@
|
||||
{{ loggedin_user.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
{% if "scheckbuch" in loggedin_user.roles and loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
|
||||
<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="Ergo Challenge"
|
||||
data-body="#mobile-menu-guest">
|
||||
{% include "includes/book" %}
|
||||
<span class="sr-only">Ergo</span>
|
||||
<div class="flex items-center">
|
||||
{% if loggedin_user.amount_unread_notifications > 0 %}
|
||||
<a href="/#notification"
|
||||
class="relative inline-flex items-end ms-2 me-3">
|
||||
<svg height="20"
|
||||
width="24"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 24">
|
||||
<path d="M1.5 8.67v8.58a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V8.67l-8.928 5.493a3 3 0 0 1-3.144 0L1.5 8.67Z" />
|
||||
<path d="M22.5 6.908V6.75a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3v.158l9.714 5.978a1.5 1.5 0 0 0 1.572 0L22.5 6.908Z" />
|
||||
</svg>
|
||||
<small class="bg-red-500 rounded-full w-3 h-3 inline-flex justify-center items-center absolute p-1 notification">
|
||||
{{ loggedin_user.amount_unread_notifications }}
|
||||
</small>
|
||||
</a>
|
||||
<div class="hidden">
|
||||
<div id="mobile-menu-guest">
|
||||
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600 border-t">Ergo</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "scheckbuch" not in loggedin_user.roles %}
|
||||
<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="Menü"
|
||||
data-body="#mobile-menu">
|
||||
{% include "includes/book" %}
|
||||
<span class="sr-only">Logbuch</span>
|
||||
</a>
|
||||
<div class="hidden">
|
||||
<div id="mobile-menu">
|
||||
<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="Menü"
|
||||
data-body="#mobile-menu">
|
||||
{% include "includes/book" %}
|
||||
<span class="sr-only">Logbuch</span>
|
||||
</a>
|
||||
<div class="hidden">
|
||||
<div id="mobile-menu">
|
||||
{% if "Donau Linz" in loggedin_user.roles %}
|
||||
<a href="/planned" class="block w-100 py-2 hover:text-primary-600">Geplante Ausfahrten</a>
|
||||
<a href="/log" class="block w-100 py-2 hover:text-primary-600">Ausfahrt eintragen</a>
|
||||
<a href="/log" class="block w-100 py-2 hover:text-primary-600 border-t">Ausfahrt eintragen</a>
|
||||
<a href="/log/show"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Logbuch</a>
|
||||
{% if "admin" in loggedin_user.roles or "Vorstand" in loggedin_user.roles %}
|
||||
<a href="/admin/user"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Userverwaltung</a>
|
||||
{% endif %}
|
||||
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
|
||||
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600 border-t">Ergo</a>
|
||||
{% endif %}
|
||||
<a href="/stat" class="block w-100 py-2 hover:text-primary-600 border-t">Statistik</a>
|
||||
<a href="/stat/boats"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsauswertung</a>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a href="/admin/boat"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Boote</a>
|
||||
{% endif %}
|
||||
<a href="/boatdamage"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsschaden</a>
|
||||
</div>
|
||||
<a href="/boatreservation"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsreservierung</a>
|
||||
<a href="/trailerreservation"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Hängerreservierung</a>
|
||||
{% endif %}
|
||||
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
|
||||
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600 border-t">Ergo</a>
|
||||
{% endif %}
|
||||
{% if "admin" in loggedin_user.roles or "Vorstand" in loggedin_user.roles %}
|
||||
<a href="/admin/user"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Userverwaltung</a>
|
||||
{% endif %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a href="/admin/boat"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Boote</a>
|
||||
{% endif %}
|
||||
<a href="/auth/logout"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Ausloggen
|
||||
<svg class="inline h-4"
|
||||
width="24"
|
||||
height="24"
|
||||
viewbox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="feather feather-log-out">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="/auth/logout"
|
||||
class="inline-flex justify-center rounded-md bg-gray-200 ml-1 px-3 py-2 text-sm font-semibold text-primary-950 hover:bg-gray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer">
|
||||
<svg class="inline h-4"
|
||||
width="24"
|
||||
height="24"
|
||||
viewbox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="feather feather-log-out">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
<span class="sr-only">Ausloggen</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -129,7 +149,7 @@
|
||||
{{ label }}
|
||||
</label>
|
||||
{% endmacro checkbox %}
|
||||
{% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false, new_last_entry='') %}
|
||||
{% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false, new_last_entry='', nonSelectableDefault=false) %}
|
||||
<div class="{{ wrapper_class }}">
|
||||
<label for="{{ name }}" class="text-sm text-gray-600 dark:text-gray-100">{{ label }}</label>
|
||||
{% if display == '' %}
|
||||
@ -140,8 +160,9 @@
|
||||
class="input rounded-md {{ class }}"
|
||||
{% if required %}required="required"{% endif %}>
|
||||
{% if default %}<option selected value>{{ default }}</option>{% endif %}
|
||||
{% if nonSelectableDefault %}<option disabled selected value>{{ nonSelectableDefault }}</option>{% endif %}
|
||||
{% for d in data %}
|
||||
<option value="{{ d.id }}" {% if d.id == selected_id %}selected{% endif %} {% if extras != '' %} {% for extra in extras %} {% if extra != 'on_water' and d[extra] %} data-{{ extra }}={{ d[extra] }} {% else %} {% if d[extra] %}disabled{% endif %} {% endif %} {% endfor %} {% endif %} {% if show_seats %} data-custom-properties='{"amount_seats": {{ d["amount_seats"] }}, "owner": "{{ d["owner"] }}", "default_destination": "{{ d["default_destination"] }}", "boat_in_ottensheim": {{ d["location_id"] == 2 }}}'{% endif %}>
|
||||
<option value="{{ d.id }}" {% if d.id == selected_id %}selected{% endif %} {% if extras != '' %} {% for extra in extras %} {% if extra != 'on_water' and d[extra] %} data- {{ extra }}={{ d[extra] }} {% else %} {% if d[extra] %}disabled{% endif %} {% endif %} {% endfor %} {% endif %} {% if show_seats %} data-custom-properties='{"amount_seats": {{ d["amount_seats"] }}, "owner": "{{ d["owner"] }}", "default_destination": "{{ d["default_destination"] }}", "boat_in_ottensheim": {{ d["location_id"] == 2 }}, "boat_reserved_today": {{ d["reserved_today"] }}, "convert_handoperated_possible": {{ d["convert_handoperated_possible"] }}, "default_handoperated": {{ d["default_shipmaster_only_steering"] }}}' {% endif %}>
|
||||
{% for displa in display -%}
|
||||
{%- if d[displa] -%}
|
||||
{{- d[displa] -}}
|
||||
@ -166,18 +187,28 @@
|
||||
<div class="p-2 border border-t-0 border-{{ bg }} mb-4 rounded-b-md">
|
||||
{% if participants | length > 0 %}
|
||||
{% for rower in participants %}
|
||||
{{ rower.name }}
|
||||
{% if rower.is_guest %}<small class="text-gray-600 dark:text-gray-100">(Scheckbuch)</small>{% endif %}
|
||||
{% if rower.is_real_guest %}
|
||||
<small class="text-gray-600 dark:text-gray-100">(Gast)</small>
|
||||
{% if allow_removing %}
|
||||
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}"
|
||||
class="btn btn-attention btn-fw">Abmelden</a>
|
||||
<div class="relative">
|
||||
{{ rower.name }}
|
||||
{% if rower.is_guest %}<small class="text-gray-600 dark:text-gray-100">(Scheckbuch)</small>{% endif %}
|
||||
{% if rower.is_real_guest %}
|
||||
<small class="text-gray-600 dark:text-gray-100">(Gast)</small>
|
||||
{% if allow_removing %}
|
||||
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name | urlencode }}"
|
||||
class="absolute r-0 bg-red-500 w-5 h-5 text-white rounded-full flex items-center justify-center transform rotate-45 top-0 right-0">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
<span class="sr-only">Abmelden</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<span class="hidden">(angemeldet seit
|
||||
{{ rower.registered_at }})</span>
|
||||
<br />
|
||||
<span class="hidden">(angemeldet seit
|
||||
{{ rower.registered_at }})</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{{ text }}
|
||||
|
@ -1,8 +1,9 @@
|
||||
{% 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">Ruderassistent</h1>
|
||||
<h1 class="h1">Ruder­assistent</h1>
|
||||
<div class="grid gap-3 my-5">
|
||||
<div class="m-auto">
|
||||
<a href="/planned"
|
||||
@ -11,60 +12,55 @@
|
||||
<span class="text-xl px-3">Geplante Ausfahrten</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Nachrichten</h2>
|
||||
<div class="divide-y">
|
||||
{% for notification in notifications %}
|
||||
{% if not notification.read_at %}
|
||||
<div class="relative flex justify-between items-center p-3">
|
||||
<div class="grow me-4">
|
||||
<small class="uppercase text-gray-600 dark:text-gray-100">
|
||||
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }}
|
||||
</small>
|
||||
<div class="mt-1">{{ notification.message }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if not notification.read_at %}
|
||||
<a href="/notification/{{ notification.id }}/read" class="inline-block">
|
||||
<button class="btn btn-primary" type="button">
|
||||
✓
|
||||
<span class="sr-only">Notification gelesen</span>
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<details class="py-3 bg-gray-200 dark:bg-primary-950 rounded-b-md">
|
||||
<summary class="px-3">Vergangene Nachrichten</summary>
|
||||
<div class="divide-y text-sm">
|
||||
{% if notifications %}
|
||||
<div id="notification"
|
||||
class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Nachrichten</h2>
|
||||
{% if loggedin_user.amount_unread_notifications > 10 %}
|
||||
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
|
||||
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte einen Moment Zeit sie zu überprüfen und als gelesen zu markieren (✓).
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="divide-y">
|
||||
{% for notification in notifications %}
|
||||
{% if notification.read_at %}
|
||||
<div class="p-3 relative">
|
||||
<small class="uppercase text-gray-600 dark:text-gray-100">
|
||||
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }}
|
||||
</small>
|
||||
<div class="mt-1">{{ notification.message }}</div>
|
||||
{% if not notification.read_at %}
|
||||
<div class="relative flex justify-between items-center p-3">
|
||||
<div class="grow me-4">
|
||||
<small class="uppercase text-gray-600 dark:text-gray-100">
|
||||
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M",) }}
|
||||
</small>
|
||||
<div class="mt-1">{{ notification.message | safe }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if not notification.read_at %}
|
||||
<a href="/notification/{{ notification.id }}/read" class="inline-block">
|
||||
<button class="btn btn-primary" type="button">
|
||||
✓
|
||||
<span class="sr-only">Notification gelesen</span>
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Ergo</h2>
|
||||
<div class="p-3">
|
||||
<ul class="list-none ms-2">
|
||||
<li class="py-1">
|
||||
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600">Ergo</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<details class="py-3 border-t rounded-b-md">
|
||||
<summary class="px-3 cursor-pointer">Vergangene Nachrichten (14 Tage)</summary>
|
||||
<div class="divide-y text-sm">
|
||||
{% for notification in notifications %}
|
||||
{% if notification.read_at %}
|
||||
<div class="p-3 relative">
|
||||
<small class="uppercase text-gray-600 dark:text-gray-100">
|
||||
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }}
|
||||
</small>
|
||||
<div class="mt-1">{{ notification.message | safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
|
||||
@ -90,18 +86,41 @@
|
||||
<li class="py-1">
|
||||
<a href="/boatdamage" class="block w-100 py-2 hover:text-primary-600">Bootsschaden</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/boatreservation"
|
||||
class="block w-100 py-2 hover:text-primary-600">Bootsreservierung</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/trailerreservation"
|
||||
class="block w-100 py-2 hover:text-primary-600">Hängerreservierung</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/steering" class="block w-100 py-2 hover:text-primary-600">Steuerleute & Co</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "scheckbuch" in loggedin_user.roles %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Scheckbuch</h2>
|
||||
<ul class="list-none ms-2 divide-y divide-gray-200 dark:divide-primary-600">
|
||||
<li class="py-1">
|
||||
<a href="/planned" class="block w-100 py-2 hover:text-primary-600">Geplante Ausfahrten</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="grid gap-3 mb-4">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Scheckbuch</h2>
|
||||
{% if "paid" not in loggedin_user.roles %}
|
||||
<div class="p-3 dark:text-white bg-white dark:bg-primary-900">
|
||||
Bitte nimm zur nächsten Ausfahrt die {{ costs_scheckbuch / 100 }} € für das Scheckbuch mit. Falls du das bereits gemacht hast, gibt uns bitte kurz Bescheid, dass dies noch nicht eingetragen wurde.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="text-sm p-3 bg-gray-200 bg-opacity-80 dark:bg-primary-950 dark:text-white text-primary-950">
|
||||
<h3>Du hast bisher an {{ last_trips | length }} deiner 5 Scheckbuch-Ausfahrten teilgenommen.</h3>
|
||||
{% if last_trips %}
|
||||
<ol class="mt-3">
|
||||
{% for last_trip in last_trips %}
|
||||
<li>{{ log::show_old(log=last_trip, state="completed", only_ones=false, index=loop.index) }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "schnupper-betreuer" in loggedin_user.roles %}
|
||||
@ -151,7 +170,7 @@
|
||||
<a href="/admin/user" class="block w-100 py-2 hover:text-primary-600">User</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/mail" class="block w-100 py-2 hover:text-primary-600">Mail (beautifully layouted)</a>
|
||||
<a href="/admin/mail" class="block w-100 py-2 hover:text-primary-600">Mail</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a>
|
||||
@ -159,9 +178,26 @@
|
||||
<li class="py-1">
|
||||
<a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/notification"
|
||||
class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Ergo</h2>
|
||||
<div class="p-3">
|
||||
<ul class="list-none ms-2">
|
||||
<li class="py-1">
|
||||
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600">Ergo</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Allgemein</h2>
|
||||
|
@ -9,27 +9,50 @@
|
||||
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ macros::boatreservation() }}
|
||||
<div class="w-full grid md:grid-cols-5 gap-3 mt-5">
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md hidden md:block shadow">
|
||||
<h2 class="h2">Boote</h2>
|
||||
<div>{{ log::show_boats(only_ones=false) }}</div>
|
||||
</div>
|
||||
<div class="md:col-span-3 bg-white dark:bg-primary-900 rounded-md shadow">
|
||||
<h2 class="h2">Neue Ausfahrt</h2>
|
||||
<div class="p-3">{{ log::new(only_ones=false, shipmaster=-1) }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow">
|
||||
<h2 class="h2">Am Wasser</h2>
|
||||
<div>
|
||||
{% if on_water | length > 0 %}
|
||||
{% for log in on_water %}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones=false) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="p-3 text-center text-black dark:text-white">Kein Boot am Wasser</p>
|
||||
{% endif %}
|
||||
<div class="w-full grid lg:grid-cols-5 gap-3 mt-5">
|
||||
<div>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md hidden md:block shadow">
|
||||
<h2 class="h2">Boote</h2>
|
||||
<div>{{ log::show_boats(only_ones=false) }}</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md hidden lg:block shadow mt-3">
|
||||
<h2 class="h2">Schnellauswahl</h2>
|
||||
<div>
|
||||
{% for boat in boats | reverse %}
|
||||
{% if boat.id in [42, 36] %}
|
||||
<div class="p-3 boats-js text-black dark:text-white border-t {% if boat.damage != 'locked' and not boat.on_water %} cursor-pointer hover:text-primary-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-primary-950 {% endif %}"
|
||||
{% if boat.damage != 'locked' and not boat.on_water %} data-seats="{{ boat.amount_seats }}" data-default_shipmaster_only_steering="{{ boat.default_shipmaster_only_steering }}" data-default-destination="{{ boat.default_destination }}" data-onclick="true" {% endif %}
|
||||
data-id="{{ boat.id }}">
|
||||
<span class="status-damage status-damage-{{ boat.damage }}"></span>
|
||||
<span {% if boat.damage == 'locked' or boat.on_water %}class="opacity-50"{% endif %}>{{ boat.name }}
|
||||
{% if boat.owner %}<span class="opacity-50">(privat)</span>{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:col-span-3">
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow">
|
||||
<h2 class="h2">Neue Ausfahrt</h2>
|
||||
<div class="p-3">{{ log::new(only_ones=false, shipmaster=-1) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2">
|
||||
<h2 class="h2">Am Wasser</h2>
|
||||
<div>
|
||||
{% if on_water | length > 0 %}
|
||||
{% for log in on_water %}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones=false) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="p-3 text-center text-black dark:text-white">Kein Boot am Wasser</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{ macros::boatreservation() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,33 +4,40 @@
|
||||
{% block content %}
|
||||
<div class="w-full">
|
||||
<h1 class="h1">Logbuch</h1>
|
||||
{{ macros::boatreservation() }}
|
||||
<div class="w-full grid md:grid-cols-5 gap-3 mt-5">
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md hidden md:block shadow">
|
||||
<h2 class="h2">Boote</h2>
|
||||
<div>{{ log::show_boats(only_ones=false) }}</div>
|
||||
<div class="w-full grid lg:grid-cols-5 gap-3 mt-5">
|
||||
<div>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md hidden lg:block shadow">
|
||||
<h2 class="h2">Boote</h2>
|
||||
<div>{{ log::show_boats(only_ones=false) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-3 bg-white dark:bg-primary-900 rounded-md shadow">
|
||||
<h2 class="h2">Neue Ausfahrt</h2>
|
||||
<div class="p-3">{{ log::new(shipmaster=loggedin_user.id) }}</div>
|
||||
<div class="lg:col-span-3">
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow">
|
||||
<h2 class="h2">Neue Ausfahrt</h2>
|
||||
<div class="p-3">{{ log::new(shipmaster=loggedin_user.id) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow">
|
||||
<h2 class="h2">Am Wasser</h2>
|
||||
{% if on_water | length > 0 %}
|
||||
{% for log in on_water %}
|
||||
{% if log.shipmaster == loggedin_user.id %}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }}
|
||||
{% elif "Vorstand" in loggedin_user.roles %}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }}
|
||||
{% else %}
|
||||
{{ log::show(log=log, state="on_water", only_ones=true) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="p-3 text-center text-black dark:text-white">Kein Boot am Wasser</p>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2">
|
||||
<h2 class="h2">Am Wasser</h2>
|
||||
{% if on_water | length > 0 %}
|
||||
{% for log in on_water %}
|
||||
{% if log.shipmaster == loggedin_user.id %}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }}
|
||||
{% elif "Vorstand" in loggedin_user.roles %}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }}
|
||||
{% else %}
|
||||
{{ log::show(log=log, state="on_water", only_ones=true) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="p-3 text-center text-black dark:text-white">Kein Boot am Wasser</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ macros::boatreservation() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if loggedin_user %}<script>var loggedin_user_id = "{{ loggedin_user.id }}";</script>{% endif %}
|
||||
<script src="/public/logbook.js"></script>
|
||||
{% endblock content %}
|
||||
|
@ -3,22 +3,6 @@
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% if "scheckbuch" in loggedin_user.roles %}
|
||||
<div class="grid gap-3 sm:col-span-2 lg:col-span-3">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Scheckbuch</h2>
|
||||
<div class="text-sm p-3">
|
||||
<h3>Du hast bisher {{ last_trips | length }} deiner 5 Scheckbuch-Ausfahrten gemacht:</h3>
|
||||
<ol>
|
||||
{% for last_trip in last_trips %}
|
||||
<li>{{ log::show_old(log=last_trip, state="completed", only_ones=false, index=loop.index) }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "paid" not in loggedin_user.roles and "Donau Linz" in loggedin_user.roles %}
|
||||
<div class="grid gap-3 sm:col-span-2 lg:col-span-3">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
@ -74,11 +58,11 @@
|
||||
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
|
||||
{% include "includes/buttons" %}
|
||||
{% for day in days %}
|
||||
{% set amount_trips = day.planned_events | length + day.trips | length %}
|
||||
{% set amount_trips = day.events | length + day.trips | length %}
|
||||
{% set_global day_cox_needed = false %}
|
||||
{% if day.planned_events | length > 0 %}
|
||||
{% for planned_event in day.planned_events %}
|
||||
{% if planned_event.cox_needed %}
|
||||
{% if day.events | length > 0 %}
|
||||
{% for event in day.events %}
|
||||
{% if event.cox_needed %}
|
||||
{% set_global day_cox_needed = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@ -91,74 +75,95 @@
|
||||
<div>
|
||||
<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">
|
||||
{{ day.day| date(format="%d.%m.%Y") }}
|
||||
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}</small>
|
||||
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}
|
||||
{% if day.max_waterlevel %}
|
||||
• <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand"
|
||||
target="_blank"
|
||||
title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel.avg }} ± {{ day.max_waterlevel.fluctuation }} cm (ungeprüfte Rohdaten, für Details siehe die Infos dazu im Impressum)">🌊{{ day.max_waterlevel.avg }} ± {{ day.max_waterlevel.fluctuation }} cm</a>
|
||||
{% endif %}
|
||||
</small>
|
||||
{% if day.weather %}
|
||||
<small class="inline-block text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">
|
||||
Temp: {{ day.weather.max_temp | round }}° • Windböe: {{ day.weather.wind_gust | round }} km/h • Regen: {{ day.weather.rain_mm | round }} mm
|
||||
</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% 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">
|
||||
{# --- START Events --- #}
|
||||
{% if day.planned_events | length > 0 %}
|
||||
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
|
||||
{% set amount_cur_cox = planned_event.cox | length %}
|
||||
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
|
||||
{% if day.events | length > 0 %}
|
||||
{% for event in day.events | sort(attribute="planned_starting_time") %}
|
||||
{% set amount_cur_cox = event.cox | length %}
|
||||
{% set amount_cox_missing = event.planned_amount_cox - amount_cur_cox %}
|
||||
<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="mr-1">
|
||||
<strong class="text-primary-900 dark:text-white">
|
||||
{{ planned_event.planned_starting_time }}
|
||||
Uhr
|
||||
</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}
|
||||
{%- if planned_event.trip_type %}
|
||||
- {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
{% if event.max_people == 0 %}
|
||||
<strong class="text-[#f43f5e]">⚠ Absage
|
||||
{{ event.planned_starting_time }}
|
||||
Uhr
|
||||
</strong>
|
||||
<small class="text-[#f43f5e]">({{ event.name }}
|
||||
{%- if event.trip_type %}
|
||||
- {{ event.trip_type.icon | safe }} {{ event.trip_type.name }}
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
{% else %}
|
||||
<strong class="text-primary-900 dark:text-white">
|
||||
{{ event.planned_starting_time }}
|
||||
Uhr
|
||||
</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ event.name }}
|
||||
{%- if event.trip_type %}
|
||||
- {{ event.trip_type.icon | safe }} {{ event.trip_type.name }}
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
{% endif %}
|
||||
<br />
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }})
|
||||
{% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}
|
||||
{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}
|
||||
" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3">
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ event.planned_starting_time }} Uhr</strong> ({{ event.name }})
|
||||
{% if event.trip_type %}<small class='block'>{{ event.trip_type.desc }}</small>{% endif %}
|
||||
{% if event.notes %}<small class='block'>{{ event.notes }}</small>{% endif %}
|
||||
" data-body="#event{{ event.trip_details_id }}" class="inline-block link-primary mr-3">
|
||||
Details
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-right grid gap-2">
|
||||
{# --- START Row Buttons --- #}
|
||||
{% set_global cur_user_participates = false %}
|
||||
{% for rower in planned_event.rower %}
|
||||
{% for rower in event.rower %}
|
||||
{% if rower.name == loggedin_user.name %}
|
||||
{% set_global cur_user_participates = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% if planned_event.max_people > planned_event.rower | length %}
|
||||
{% if cur_user_participates == false %}
|
||||
<a href="/planned/join/{{ planned_event.trip_details_id }}"
|
||||
class="btn btn-primary btn-fw"
|
||||
{% if planned_event.trip_type %}onclick="return confirm('{{ planned_event.trip_type.question }}');"{% endif %}>Mitrudern</a>
|
||||
{% endif %}
|
||||
{% if event.max_people > event.rower | length and cur_user_participates == false %}
|
||||
<a href="/planned/join/{{ event.trip_details_id }}"
|
||||
class="btn btn-primary btn-fw"
|
||||
{% if event.trip_type %}onclick="return confirm('{{ event.trip_type.question }}');"{% endif %}>Mitrudern</a>
|
||||
{% endif %}
|
||||
{# --- END Row Buttons --- #}
|
||||
{# --- START Cox Buttons --- #}
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
{% set_global cur_user_participates = false %}
|
||||
{% for cox in planned_event.cox %}
|
||||
{% for cox in event.cox %}
|
||||
{% if cox.name == loggedin_user.name %}
|
||||
{% set_global cur_user_participates = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if cur_user_participates %}
|
||||
<a href="/cox/remove/{{ planned_event.id }}"
|
||||
<a href="/cox/remove/{{ event.id }}"
|
||||
class="block btn btn-attention btn-fw">
|
||||
{% include "includes/cox-icon" %}
|
||||
Abmelden
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/cox/join/{{ planned_event.id }}"
|
||||
{% elif event.planned_amount_cox > 0 %}
|
||||
<a href="/cox/join/{{ event.id }}"
|
||||
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" %}
|
||||
Steuern
|
||||
</a>
|
||||
@ -169,59 +174,88 @@
|
||||
</div>
|
||||
{# --- START Sidebar Content --- #}
|
||||
<div class="hidden">
|
||||
<div id="event{{ planned_event.trip_details_id }}">
|
||||
<div id="event{{ event.trip_details_id }}">
|
||||
{# --- START List Coxes --- #}
|
||||
{% if planned_event.planned_amount_cox > 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') }}
|
||||
{% if event.planned_amount_cox > 0 %}
|
||||
{% if event.max_people == 0 %}
|
||||
{{ macros::box(participants=event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }}
|
||||
{% else %}
|
||||
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
|
||||
{% if amount_cox_missing > 0 %}
|
||||
{{ macros::box(participants=event.cox, empty_seats=event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
|
||||
{% else %}
|
||||
{{ macros::box(participants=event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# --- END List Coxes --- #}
|
||||
{# --- START List Rowers --- #}
|
||||
{% if planned_event.max_people > 0 %}
|
||||
{% set amount_cur_rower = planned_event.rower | length %}
|
||||
{{ 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) }}
|
||||
{% set amount_cur_rower = event.rower | length %}
|
||||
{% if event.max_people == 0 %}
|
||||
{{ macros::box(header='Absage', bg='[#f43f5e]', participants=event.rower, trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
|
||||
{% else %}
|
||||
{{ macros::box(participants=event.rower, empty_seats=event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
|
||||
{% endif %}
|
||||
{# --- END List Rowers --- #}
|
||||
{% if "planned_event" in loggedin_user.roles %}
|
||||
<form action="/planned/join/{{ planned_event.trip_details_id }}"
|
||||
method="get" />
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
<form action="/planned/join/{{ event.trip_details_id }}" method="get" />
|
||||
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
|
||||
<input value="Gast hinzufügen"
|
||||
class="btn btn-primary w-full rounded-t-none-important"
|
||||
type="submit" />
|
||||
</form>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% if "planned_event" in loggedin_user.roles %}
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
{# --- START Edit Form --- #}
|
||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3>
|
||||
<form action="/admin/planned-event" method="post" class="grid gap-3">
|
||||
<input type="hidden" name="_method" value="put" />
|
||||
<input type="hidden" name="id" value="{{ planned_event.id }}" />
|
||||
{{ macros::input(label='Titel', name='name', type='input', value=planned_event.name) }}
|
||||
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }}
|
||||
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }}
|
||||
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }}
|
||||
<input type="hidden" name="id" value="{{ event.id }}" />
|
||||
{{ macros::input(label='Titel', name='name', type='input', value=event.name) }}
|
||||
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='1') }}
|
||||
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=event.planned_amount_cox, required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=event.id,checked=event.always_show) }}
|
||||
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=event.id,checked=event.is_locked) }}
|
||||
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=event.trip_type_id) }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=event.notes) }}
|
||||
<input value="Speichern" class="btn btn-primary" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
{# --- END Edit Form --- #}
|
||||
{# --- START Delete Btn --- #}
|
||||
<div class="text-right">
|
||||
<a href="/admin/planned-event/{{ planned_event.id }}/delete"
|
||||
class="inline-block btn btn-alert">
|
||||
{% include "includes/delete-icon" %}
|
||||
Termin löschen
|
||||
</a>
|
||||
</div>
|
||||
{% if event.rower | length == 0 and amount_cur_cox == 0 %}
|
||||
<div class="text-right mt-6">
|
||||
<a href="/admin/planned-event/{{ event.id }}/delete"
|
||||
class="inline-block btn btn-alert">
|
||||
{% include "includes/delete-icon" %}
|
||||
Termin löschen
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if event.max_people == 0 %}
|
||||
Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
|
||||
{% else %}
|
||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Event absagen</h3>
|
||||
<form action="/admin/planned-event" method="post" class="grid">
|
||||
<input type="hidden" name="_method" value="put" />
|
||||
<input type="hidden" name="id" value="{{ event.id }}" />
|
||||
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
|
||||
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
|
||||
{{ macros::input(label='', name='name', type='hidden', value=event.name) }}
|
||||
{{ macros::input(label='', name='max_people', type='hidden', value=event.max_people) }}
|
||||
{{ macros::input(label='', name='planned_amount_cox', type='hidden', value=event.planned_amount_cox) }}
|
||||
{{ macros::input(label='', name='always_show', type='hidden', value=event.always_show) }}
|
||||
{{ macros::input(label='', name='is_locked', type='hidden', value=event.is_locked) }}
|
||||
{{ macros::input(label='', name='trip_type', type='hidden', value=event.trip_type_id) }}
|
||||
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# --- END Delete Btn --- #}
|
||||
</div>
|
||||
@ -247,16 +281,16 @@
|
||||
{{ trip.cox_name -}}
|
||||
{% if trip.trip_type %}
|
||||
-
|
||||
{{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }}
|
||||
{% endif -%}
|
||||
{{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
{% else %}
|
||||
<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }}
|
||||
Uhr</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name -}}
|
||||
{% if trip.trip_type %}
|
||||
- {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}
|
||||
{% endif -%}
|
||||
- {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
{% endif %}
|
||||
<br />
|
||||
@ -310,7 +344,7 @@
|
||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3>
|
||||
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3">
|
||||
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min='0') }}
|
||||
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min=trip.rower | length) }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }}
|
||||
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
|
||||
@ -326,6 +360,22 @@
|
||||
Termin löschen
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if trip.max_people == 0 %}
|
||||
Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
|
||||
{% else %}
|
||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt absagen</h3>
|
||||
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid">
|
||||
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
|
||||
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
|
||||
{{ macros::input(label='', name='always_show', type='hidden', value=trip.always_show) }}
|
||||
{{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }}
|
||||
{{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }}
|
||||
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# --- END Edit Form --- #}
|
||||
@ -340,9 +390,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{# --- START Add Buttons --- #}
|
||||
{% if "planned_event" in loggedin_user.roles or "cox" in loggedin_user.roles %}
|
||||
<div class="grid {% if "planned_event" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
|
||||
{% if "planned_event" in loggedin_user.roles %}
|
||||
{% if "manage_events" in loggedin_user.roles or "cox" in loggedin_user.roles %}
|
||||
<div class="grid {% if "manage_events" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
<a href="#"
|
||||
data-sidebar="true"
|
||||
data-trigger="sidebar"
|
||||
@ -356,7 +406,7 @@
|
||||
{% endif %}
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200
|
||||
{% if "planned_event" in loggedin_user.roles %}
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
rounded-br-md
|
||||
{% else %}
|
||||
rounded-b-md
|
||||
@ -376,7 +426,7 @@
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
{% include "forms/trip" %}
|
||||
{% endif %}
|
||||
{% if "planned_event" in loggedin_user.roles %}
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
{% include "forms/event" %}
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|