Compare commits

...

151 Commits

Author SHA1 Message Date
ec5a69f3e6 Merge branch 'notification' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt into notification
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m26s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-04 10:03:59 +01:00
09fffa1830 add table 2024-03-04 10:03:53 +01:00
28acee3085 notifications 2024-03-04 10:03:53 +01:00
e338c78d04 Merge pull request 'allow vorstand to see member details, Fixes #199' (#225) from show-members-vorstand into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m22s
CI/CD Pipeline / deploy-staging (push) Successful in 4m4s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #225
2024-03-04 09:56:29 +01:00
0de21e9abb add tests and fix npm build, make frontend tests work @ mobile view
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #224
2024-03-04 09:55:39 +01:00
9c3ae7434e allow vorstand to see member details, Fixes #199
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-03-04 09:19:04 +01:00
996fcdc14f Merge pull request 'Allow 'Rennjugend' to use all boats in ottensheim, Fixes #200' (#222) from ottensheim-boats into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 3m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #222
2024-03-03 20:06:29 +01:00
5781937bee show ottensheim boats to rennjugend
Some checks failed
CI/CD Pipeline / test (push) Failing after 3m20s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-03 20:03:20 +01:00
d1296ec40a show ottensheim boats to rennjugend
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m34s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-03 18:08:15 +01:00
cc7ba59407 update deps and fix ci
All checks were successful
CI/CD Pipeline / test (push) Successful in 22m30s
CI/CD Pipeline / deploy-staging (push) Successful in 17m49s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-01 09:08:50 +01:00
770e321bed allow sending mails w/o attachments :-)
Some checks failed
CI/CD Pipeline / deploy-staging (push) Waiting to run
CI/CD Pipeline / deploy-main (push) Waiting to run
CI/CD Pipeline / test (push) Has been cancelled
2024-03-01 08:40:02 +01:00
f7a7ab8733 use old lock file to make ci work
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m24s
CI/CD Pipeline / deploy-staging (push) Successful in 3m29s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-21 15:35:12 +01:00
65e425fed7 increase timeout for frontend tests
Some checks failed
CI/CD Pipeline / deploy-staging (push) Waiting to run
CI/CD Pipeline / deploy-main (push) Waiting to run
CI/CD Pipeline / test (push) Has been cancelled
2024-02-21 15:06:35 +01:00
6ed28994c6 code cleanup
Some checks failed
CI/CD Pipeline / test (push) Failing after 15m23s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-21 14:46:17 +01:00
e9bee963fe update deps
Some checks failed
CI/CD Pipeline / test (push) Failing after 15m17s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-21 13:58:03 +01:00
75d7396b96 push
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m32s
CI/CD Pipeline / deploy-staging (push) Successful in 3m36s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-17 12:48:25 +01:00
9746a2114c push
All checks were successful
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-17 12:47:06 +01:00
6e0c594c84 dont expect money from deleted users :-)
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m14s
CI/CD Pipeline / deploy-staging (push) Successful in 3m35s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-16 13:13:57 +01:00
fe2ca0f33a also remove js function for (unnecessary) filtering of months
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m4s
CI/CD Pipeline / deploy-staging (push) Successful in 3m30s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-16 10:47:28 +01:00
043d042dcc remove useless button
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m15s
CI/CD Pipeline / deploy-staging (push) Successful in 3m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-16 08:05:07 +01:00
d0b7f9c76c dont sent deleted users a mail
All checks were successful
CI/CD Pipeline / test (push) Successful in 7m55s
CI/CD Pipeline / deploy-staging (push) Successful in 3m38s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-15 22:29:08 +01:00
58b498b9de use proper encoding
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-02-15 22:21:40 +01:00
23f5e3ca4a add functionality to send attachmets, Fixes #113
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m1s
CI/CD Pipeline / deploy-staging (push) Successful in 3m37s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-15 21:52:00 +01:00
7ff9978587 switch to new ci image
All checks were successful
CI/CD Pipeline / test (push) Successful in 21m11s
CI/CD Pipeline / deploy-staging (push) Successful in 18m55s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-15 12:01:28 +01:00
da9d2febf1 upgrade major deps
All checks were successful
CI/CD Pipeline / test (push) Successful in 17m59s
CI/CD Pipeline / deploy-staging (push) Successful in 11m55s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-13 23:25:08 +01:00
23cfc8aa1f update deps
All checks were successful
CI/CD Pipeline / test (push) Successful in 23m32s
CI/CD Pipeline / deploy-staging (push) Successful in 19m22s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-13 14:19:46 +01:00
0231d2bd21 only show donauLinz user in stats, otherwise count as guest
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m8s
CI/CD Pipeline / deploy-staging (push) Successful in 3m47s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-12 20:09:30 +01:00
5b5eb2e831 add remark about bank synchornistaion
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m12s
CI/CD Pipeline / deploy-staging (push) Successful in 3m45s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-12 19:07:45 +01:00
8c43ab3331 better wording
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m24s
CI/CD Pipeline / deploy-staging (push) Successful in 3m48s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-11 16:04:47 +01:00
9c8bb59ad7 update list of filtered log in attempts
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m59s
CI/CD Pipeline / deploy-staging (push) Successful in 3m41s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-07 12:36:27 +01:00
664bb62733 fix ci
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m48s
CI/CD Pipeline / deploy-staging (push) Successful in 3m52s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-06 16:12:10 +01:00
c1bed58fbb no sudo for deployment...
Some checks failed
CI/CD Pipeline / test (push) Successful in 8m53s
CI/CD Pipeline / deploy-staging (push) Failing after 3m49s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-06 16:03:11 +01:00
ccd9aad51f switch deplyment
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-02-06 16:01:34 +01:00
19afa34c13 only send mails to proper addresses
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m2s
CI/CD Pipeline / deploy-staging (push) Successful in 4m33s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-02-04 10:34:58 +01:00
59f0ee1429 increase whitelist of not logging certain (wp logins)
All checks were successful
CI/CD Pipeline / test (push) Successful in 23m44s
CI/CD Pipeline / deploy-staging (push) Successful in 20m26s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-31 10:23:33 +01:00
6f94bb1186 update deps
All checks were successful
CI/CD Pipeline / test (push) Successful in 25m59s
CI/CD Pipeline / deploy-staging (push) Successful in 19m53s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-26 13:26:26 +01:00
1913bf8b22 update tests to new name field in update
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m27s
CI/CD Pipeline / deploy-staging (push) Successful in 5m14s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-24 23:04:35 +01:00
3462ac5963 allow changing title of planned-event; fixes #52
Some checks failed
CI/CD Pipeline / test (push) Failing after 10m50s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-24 22:47:53 +01:00
9ae5963a0a supply all variables for planned_event roles
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m12s
CI/CD Pipeline / deploy-staging (push) Successful in 5m29s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-24 18:18:26 +01:00
99c615c400 fix edit/delete trip
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-24 18:12:22 +01:00
a6faa128ec don't spam logs with (unsuccessful) wordpress login attempts
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m8s
CI/CD Pipeline / deploy-staging (push) Successful in 5m7s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-24 13:09:12 +01:00
6ad3b8f741 add has-paid filter
All checks were successful
CI/CD Pipeline / test (push) Successful in 26m35s
CI/CD Pipeline / deploy-staging (push) Successful in 19m55s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-22 22:57:42 +01:00
083ddeadc1 update deps
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-22 22:34:54 +01:00
6d61d1f8bc Merge branch 'main' into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m37s
CI/CD Pipeline / deploy-staging (push) Successful in 4m41s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-22 22:08:39 +01:00
b7499bd6cb clean code; if logged out: save url where user tried to go to, go there once logged in, Fixes #179
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-22 22:08:05 +01:00
60b9a4dbba adapt unit tests to account for new redirect
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m16s
CI/CD Pipeline / deploy-staging (push) Successful in 4m24s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-22 20:55:05 +01:00
a1a5e2ad89 move managing events to own role
Some checks failed
CI/CD Pipeline / test (push) Failing after 38m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-22 19:27:22 +01:00
eda072c713 enable changing paid state
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m15s
CI/CD Pipeline / deploy-staging (push) Successful in 4m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-22 19:05:18 +01:00
4a06d519bf Merge pull request 'staging' (#177) from staging into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m48s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 4m15s
Reviewed-on: #177
2024-01-21 16:10:32 +01:00
ac5ecbafec Merge branch 'staging' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m54s
CI/CD Pipeline / deploy-staging (push) Successful in 4m33s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-21 16:09:53 +01:00
c54efdeec4 add spacing 2024-01-21 16:09:43 +01:00
491b2cac82 Merge pull request 'main' (#176) from main into staging
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #176
2024-01-21 16:08:47 +01:00
5f31c565a3 Merge pull request 'trim mails' (#175) from staging into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #175
2024-01-21 16:08:10 +01:00
6a22811ad9 trim mails
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-21 16:07:11 +01:00
7c7163f541 Merge pull request 'staging' (#174) from staging into main
Some checks failed
CI/CD Pipeline / test (push) Failing after 2m3s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #174
2024-01-21 16:01:22 +01:00
f4cb051b84 fix mail error
Some checks failed
CI/CD Pipeline / test (push) Has started running
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-01-21 16:00:41 +01:00
2032b7e0db Merge pull request 'main' (#173) from main into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m26s
CI/CD Pipeline / deploy-staging (push) Successful in 4m2s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #173
2024-01-19 11:58:41 +01:00
83c7b45139 Merge pull request 'staging' (#172) from staging into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 4m2s
Reviewed-on: #172
2024-01-19 11:58:05 +01:00
7f7259a3e1 improve texts
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-19 11:56:49 +01:00
99e3aa22a2 fix errors
Some checks failed
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 9m7s
CI/CD Pipeline / deploy-staging (push) Has been cancelled
2024-01-19 11:47:28 +01:00
0ed740ec47 create fee reminder mail
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m10s
CI/CD Pipeline / deploy-staging (push) Successful in 4m7s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-19 11:19:24 +01:00
578e3df9e9 use text instead of creditor id in qr code, more clear that fee is for full family
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m9s
CI/CD Pipeline / deploy-staging (push) Successful in 4m5s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-19 10:22:13 +01:00
3a8650028d add qr code for payment
Some checks failed
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 9m11s
CI/CD Pipeline / deploy-staging (push) Has been cancelled
2024-01-19 10:10:23 +01:00
c036cda593 Merge pull request 'remove wrong header' (#171) from staging into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m50s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 3m59s
Reviewed-on: #171
2024-01-19 08:48:29 +01:00
f1ba331fdf remove wrong header
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m2s
CI/CD Pipeline / deploy-staging (push) Successful in 4m3s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-19 08:47:49 +01:00
14a5952e14 Merge pull request 'group families in fee calc' (#170) from staging into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m7s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m22s
Reviewed-on: #170
2024-01-19 08:02:59 +01:00
e498b4be3b group families in fee calc
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m35s
CI/CD Pipeline / deploy-staging (push) Successful in 4m11s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-19 08:02:09 +01:00
ae8887c72d Merge pull request 'staging' (#169) from staging into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m20s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 4m19s
Reviewed-on: #169
2024-01-19 07:43:05 +01:00
519cd1985d Merge pull request 'final' (#168) from final into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m13s
CI/CD Pipeline / deploy-staging (push) Successful in 4m24s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #168
2024-01-19 01:00:07 +01:00
3df6791b6b fix ci
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-19 00:50:30 +01:00
cb892e1c0c remove unnecessary field
Some checks failed
CI/CD Pipeline / test (push) Failing after 2m7s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-19 00:46:51 +01:00
b893989dce handle supporing fees
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-19 00:44:53 +01:00
267becfbce progress @fees, next step, deply+enter families/student/pupil/...
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-19 00:39:15 +01:00
ff795ce66c start with fee calc
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m40s
CI/CD Pipeline / deploy-staging (push) Successful in 4m24s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-18 22:02:44 +01:00
474db1232d start working on calculating member fees
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m54s
CI/CD Pipeline / deploy-staging (push) Successful in 4m20s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-18 21:36:38 +01:00
dc794bde37 try
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m48s
CI/CD Pipeline / deploy-staging (push) Successful in 4m23s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-18 16:55:51 +01:00
85124cd699 Merge pull request 'add family' (#167) from family into staging
Some checks failed
CI/CD Pipeline / test (push) Successful in 8m44s
CI/CD Pipeline / deploy-staging (push) Failing after 4m17s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #167
2024-01-18 16:38:33 +01:00
07c76f4e64 add family
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-01-18 16:37:54 +01:00
357ee21533 add notes
All checks were successful
CI/CD Pipeline / test (push) Successful in 7m54s
CI/CD Pipeline / deploy-staging (push) Successful in 4m21s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-18 15:28:51 +01:00
66365c4a68 fix typo
All checks were successful
CI/CD Pipeline / test (push) Successful in 7m47s
CI/CD Pipeline / deploy-staging (push) Successful in 4m22s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-18 09:32:25 +01:00
3cd4807604 Delete package-lock.json
All checks were successful
CI/CD Pipeline / test (push) Successful in 7m47s
CI/CD Pipeline / deploy-staging (push) Successful in 4m27s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 23:49:28 +01:00
e9dfce5c95 Delete shame.txt
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
2024-01-17 23:48:43 +01:00
6de5d70b64 Delete update.sh
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
2024-01-17 23:48:26 +01:00
1c21d3ad65 Merge pull request 'own-dockerfile' (#166) from own-dockerfile into staging
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #166
2024-01-17 23:43:24 +01:00
fd99bc6f66 push
All checks were successful
CI/CD Pipeline / test (push) Successful in 22m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 23:42:07 +01:00
c5673559d0 try 2024-01-17 23:41:08 +01:00
54f9dc22e0 try 2024-01-17 23:40:49 +01:00
9267b4dbc8 try 2024-01-17 23:40:48 +01:00
6e8947a928 try 2024-01-17 23:39:57 +01:00
0ab121df8e try 2024-01-17 23:39:55 +01:00
7c7877d275 try 2024-01-17 23:39:35 +01:00
1e02b2f5bb try 2024-01-17 23:39:22 +01:00
ca11e72d00 try 2024-01-17 23:39:22 +01:00
5631b0551c try 2024-01-17 23:39:22 +01:00
97154ef4b9 try 2024-01-17 23:39:20 +01:00
e5311b4fab try 2024-01-17 23:38:06 +01:00
76a1dbccbf try 2024-01-17 23:36:18 +01:00
03947001d5 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-17 23:34:39 +01:00
95fb07f1e9 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-17 23:23:58 +01:00
22ee941ce0 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-17 23:08:28 +01:00
f1f17cdb44 optimize ci build: use own dockerfile for dependencies + cache for builds (#164)
Some checks failed
CI/CD Pipeline / test (push) Successful in 8m1s
CI/CD Pipeline / deploy-staging (push) Failing after 16m57s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #164
2024-01-17 22:56:33 +01:00
6bee538b55 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-01-17 22:52:31 +01:00
88d533c838 try
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 22:43:54 +01:00
57c1e13c14 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Waiting to run
CI/CD Pipeline / deploy-main (push) Waiting to run
CI/CD Pipeline / test (push) Has been cancelled
2024-01-17 21:22:57 +01:00
ba132a5735 try
All checks were successful
CI/CD Pipeline / test (push) Successful in 27m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 20:35:06 +01:00
0794671707 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-01-17 20:14:55 +01:00
d0adee74da try
All checks were successful
CI/CD Pipeline / test (push) Successful in 21m36s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 19:52:54 +01:00
5fec65e38f try 2024-01-17 19:50:32 +01:00
0b7711ed91 try 2024-01-17 19:49:46 +01:00
7be76f3ce8 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-17 19:45:21 +01:00
865c55cd18 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-01-17 19:28:13 +01:00
b258a5ac6e try
All checks were successful
CI/CD Pipeline / test (push) Successful in 19m43s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 19:07:37 +01:00
62171d72fe try 2024-01-17 19:06:12 +01:00
1f734b75a0 try
Some checks failed
CI/CD Pipeline / test-backend (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test-frontend (push) Has been cancelled
2024-01-17 18:35:29 +01:00
4af887c2b4 try
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 2s
CI/CD Pipeline / test-backend (push) Failing after 1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 18:32:18 +01:00
cf1cd0c126 explain dockerfile
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 1s
CI/CD Pipeline / test-backend (push) Failing after 1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 18:21:44 +01:00
c6da8b3db1 try
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 1s
CI/CD Pipeline / test-backend (push) Failing after 0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 18:09:29 +01:00
77d9e2ea9f try faster build
All checks were successful
CI/CD Pipeline / test-frontend (push) Successful in 18m47s
CI/CD Pipeline / test-backend (push) Successful in 14m53s
CI/CD Pipeline / deploy-staging (push) Successful in 18m32s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-17 08:23:46 +01:00
55b97eaf1f Merge pull request 'Include frontend tests, update readme, fix bugs' (#163) from test-playwright into staging
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 21m6s
CI/CD Pipeline / test-backend (push) Successful in 15m44s
CI/CD Pipeline / deploy-staging (push) Successful in 18m50s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #163
2024-01-16 23:44:03 +01:00
b7c2dff8eb try
Some checks failed
CI/CD Pipeline / test-backend (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test-frontend (push) Has been cancelled
2024-01-16 23:43:36 +01:00
80846e6986 try
Some checks failed
CI/CD Pipeline / test-backend (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Has been cancelled
2024-01-16 23:22:53 +01:00
beec5ba6c6 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 25m21s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 22:52:31 +01:00
c372051561 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 13m30s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 22:29:57 +01:00
7e1a0a2159 try
All checks were successful
CI/CD Pipeline / test-frontend (push) Successful in 17m9s
CI/CD Pipeline / test-backend (push) Successful in 15m25s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-16 20:24:02 +01:00
6a1bde55a2 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 17m22s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 20:04:34 +01:00
2a025d7519 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 2m21s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 20:00:41 +01:00
bc1916ceab try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 2m21s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 19:54:24 +01:00
7aef741c6f try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 2m22s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 19:48:41 +01:00
64bb15cc8b try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 2m23s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 19:40:20 +01:00
442453bef3 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 17m13s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 19:17:17 +01:00
b633a4bfee try
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 16m59s
CI/CD Pipeline / test-backend (push) Successful in 15m34s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-16 16:55:47 +01:00
875799c3d7 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 11m35s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 16:42:57 +01:00
98d8b512ee try
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 6m53s
CI/CD Pipeline / test-backend (push) Successful in 15m52s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-16 16:16:56 +01:00
ae673657b0 try
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test-frontend (push) Failing after 2m22s
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 16:13:41 +01:00
c1493ad914 try
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 2m22s
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 16:10:13 +01:00
4ab1a36fcd try
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 2s
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test-backend (push) Has been cancelled
2024-01-16 16:09:45 +01:00
bd650f738c try
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 2s
CI/CD Pipeline / test-backend (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-01-16 16:09:09 +01:00
537458b58e try 2024-01-16 16:07:40 +01:00
ce3562f079 add playwright tests
Some checks failed
CI/CD Pipeline / test-frontend (push) Failing after 4s
CI/CD Pipeline / test-backend (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-01-16 16:00:02 +01:00
2f26ce7e31 Merge pull request 'add mail' (#160) from add-rss into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m23s
CI/CD Pipeline / deploy-staging (push) Successful in 19m44s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #160
2024-01-10 15:52:54 +01:00
bbfc94d54b add mail
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-01-10 15:52:27 +01:00
0262f721e9 Merge pull request 'fix ci' (#159) from fix-redirects into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #159
2024-01-10 15:50:17 +01:00
5788d3d9f4 fix ci
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-01-10 15:49:57 +01:00
30105e7bbd Merge pull request 'fix ci' (#158) from fix-redirects into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 15m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #158
2024-01-10 15:11:56 +01:00
a95ff3657f fix ci
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-01-10 15:11:35 +01:00
f5bd470dac Merge pull request 'fix redirect' (#157) from fix-redirects into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #157
2024-01-10 15:10:48 +01:00
87e4b8fbb4 fix redirect
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-01-10 15:10:05 +01:00
5cc8f1ff48 Merge pull request 'limit-users-to-proper-roles' (#156) from limit-users-to-proper-roles into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #156
2024-01-10 14:55:41 +01:00
a4b8bf1e3f improve code w/ clippy
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m22s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-10 14:24:58 +01:00
3e2e058bcc limit users to proper role, Fixes #135
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 15m47s
2024-01-10 14:08:15 +01:00
041e83b425 Merge pull request 'update deps' (#154) from update-deps into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m6s
CI/CD Pipeline / deploy-staging (push) Successful in 19m38s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #154
2024-01-09 16:15:36 +01:00
53 changed files with 2461 additions and 1070 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
target/
db.sqlite
.history/
frontend/node_modules/*
/static/
/data-ergo/

View File

@ -11,59 +11,62 @@ env:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: rust:latest container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240215
steps: steps:
- name: Setup Environment - uses: actions/checkout@v3
run: | - name: Run Test DB Script
apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y run: ./test_db.sh
- name: Checkout - name: Set up cargo cache
uses: actions/checkout@v3 uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-debug-
- name: Run Test DB Script - name: Build
run: ./test_db.sh run: |
cargo build
- name: Build cd frontend && npm install && npm run build
run: | - name: Frontend tests
cargo build run: cd frontend && npx playwright install && npx playwright test --workers 1
cd frontend && npm install && npm run build - name: Backend tests
run: cargo test --verbose
- name: Run Tests #- uses: actions/upload-artifact@v3
run: cargo test --verbose # if: always()
# with:
# name: playwright-report
# path: frontend/playwright-report/
# retention-days: 30
deploy-staging: deploy-staging:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: rust:latest container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240215
needs: [test] needs: [test]
if: github.ref == 'refs/heads/staging' if: github.ref == 'refs/heads/staging'
steps: steps:
- name: Setup Environment
run: |
rustup target add $CARGO_TARGET
apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev
# Handling NodeSource GPG key
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o nodesource.gpg.key
if [ -f /etc/apt/keyrings/nodesource.gpg ]; then
rm /etc/apt/keyrings/nodesource.gpg
fi
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg nodesource.gpg.key
# Adding NodeSource repository
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
# Installing Node.js and npm
apt-get update
apt-get install nodejs -y
apt-get install npm -y
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Run Test DB Script - name: Run Test DB Script
run: ./test_db.sh 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-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-release-
- name: Build - name: Build
run: | run: |
cargo build --release --target $CARGO_TARGET cargo build --release --target $CARGO_TARGET
@ -72,20 +75,20 @@ jobs:
- name: Deploy to Staging - name: Deploy to Staging
run: | run: |
mkdir ~/.ssh mkdir -p ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/rot-updating scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/rot-updating
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/ scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/
scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/ scp -r static $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/
scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/ scp -r templates $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/
scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/ scp -r svelte $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging' ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
ssh $SSH_USER@$SSH_HOST 'rm /home/k004373/rowing-staging/db.sqlite && cp /home/k004373/rowing/db.sqlite /home/k004373/rowing-staging/db.sqlite && mkdir -p /home/k004373/rowing-staging/svelte/build && mkdir -p /home/k004373/rowing-staging/data-ergo/thirty && mkdir -p /home/k004373/rowing-staging/data-ergo/dozen && sqlite3 /home/k004373/rowing-staging/db.sqlite < /home/k004373/rowing-staging/staging-diff.sql' ssh $SSH_USER@$SSH_HOST 'rm /home/philipp/rowing-staging/db.sqlite && cp /home/philipp/rowing/db.sqlite /home/philipp/rowing-staging/db.sqlite && mkdir -p /home/philipp/rowing-staging/svelte/build && mkdir -p /home/philipp/rowing-staging/data-ergo/thirty && mkdir -p /home/philipp/rowing-staging/data-ergo/dozen && sqlite3 /home/philipp/rowing-staging/db.sqlite < /home/philipp/rowing-staging/staging-diff.sql'
ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing-staging/rot-updating /home/k004373/rowing-staging/rot' ssh $SSH_USER@$SSH_HOST 'mv /home/philipp/rowing-staging/rot-updating /home/philipp/rowing-staging/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging' ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
@ -94,20 +97,27 @@ jobs:
deploy-main: deploy-main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: rust:latest container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240215
needs: [test] needs: [test]
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
steps: steps:
- name: Setup Environment
run: |
rustup target add $CARGO_TARGET
apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Run Test DB Script - name: Run Test DB Script
run: ./test_db.sh 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-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-release-
- name: Build - name: Build
run: | run: |
@ -115,20 +125,20 @@ jobs:
strip target/$CARGO_TARGET/release/rot strip target/$CARGO_TARGET/release/rot
cd frontend && npm install && npm run build cd frontend && npm install && npm run build
- name: Deploy to Main - name: Deploy to production
run: | run: |
mkdir ~/.ssh mkdir -p ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing/rot-updating scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/philipp/rowing/rot-updating
scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing/ scp -r static $SSH_USER@$SSH_HOST:/home/philipp/rowing/
scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing/ scp -r templates $SSH_USER@$SSH_HOST:/home/philipp/rowing/
scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing/ scp -r svelte $SSH_USER@$SSH_HOST:/home/philipp/rowing/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/k004373/rowing/svelte/build && mkdir -p /home/k004373/rowing/data-ergo/thirty && mkdir -p /home/k004373/rowing/data-ergo/dozen' ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/philipp/rowing/svelte/build && mkdir -p /home/philipp/rowing/data-ergo/thirty && mkdir -p /home/philipp/rowing/data-ergo/dozen'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot' ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing/rot-updating /home/k004373/rowing/rot' ssh $SSH_USER@$SSH_HOST 'mv /home/philipp/rowing/rot-updating /home/philipp/rowing/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot' ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot'
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}

545
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ rest = []
rocket = { version = "0.5.0", features = ["secrets"]} rocket = { version = "0.5.0", features = ["secrets"]}
rocket_dyn_templates = {version = "0.1.0", features = [ "tera" ], optional = true } rocket_dyn_templates = {version = "0.1.0", features = [ "tera" ], optional = true }
log = "0.4" log = "0.4"
env_logger = "0.10" env_logger = "0.11"
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] } sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
argon2 = "0.5" argon2 = "0.5"
serde = { version = "1.0", features = [ "derive" ]} serde = { version = "1.0", features = [ "derive" ]}

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
# This dockerfile is used as basis for the CI jobs.
# Process to renew it:
# 0. Login to gitea docker registry: `docker login git.hofer.link`
# 1. Build the image `docker build .`
# 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
RUN apt-get update && apt-get install -y sqlite3
# nodejs
RUN apt-get install -y curl && \
curl -sL https://deb.nodesource.com/setup_21.x | bash - && \
apt-get install -y nodejs
# playwright
RUN npx playwright install --with-deps
# deployment
RUN rustup target add x86_64-unknown-linux-musl
RUN apt-get install -y -qq pkg-config sshpass musl musl-tools curl gnupg libssl-dev
# TEMPORARY act workaround (otherwise gitea cache is not working)
RUN apt-get install -y zstd

View File

@ -1,15 +1,18 @@
# Frontend Process # Build
´cd frontend´
´npm install´
´npm run (watch/build)´
# Notes / Bugfixes
## Frontend ## Frontend
- [] support esc to close sidebar 1. `cd frontend`
- [] reload page -> don't throw input away! 2. `npm install`
3. `npm run (watch/build)`
# Run
## Backend ## Backend
1. `cargo r`
# Nice to have # Test
## Frontend ## Frontend
- [] my trips for cox - `npx playwright test --workers 1 --project firefox`
- Nice UI: `--ui`
- Generate tests: `npx playwright codegen`
## Backend (Unit + Integration)
`cargo t`

5
frontend/.gitignore vendored
View File

@ -1 +1,6 @@
package-lock.json package-lock.json
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@ -6,6 +6,7 @@ export interface choiceMap {
} }
let choiceObjects: choiceMap = {}; let choiceObjects: choiceMap = {};
let boat_in_ottensheim = true;
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
changeTheme(); changeTheme();
@ -106,6 +107,7 @@ interface ChoiceBoatEvent extends Event{
amount_seats: number, amount_seats: number,
owner: number, owner: number,
default_destination: string, default_destination: string,
boat_in_ottensheim: boolean,
} }
}; };
} }
@ -123,6 +125,8 @@ function selectBoatChange() {
boatSelect.addEventListener('addItem', function(e) { boatSelect.addEventListener('addItem', function(e) {
const event = e as ChoiceBoatEvent; const event = e as ChoiceBoatEvent;
boat_in_ottensheim = event.detail.customProperties.boat_in_ottensheim;
const amount_seats = event.detail.customProperties.amount_seats; const amount_seats = event.detail.customProperties.amount_seats;
setMaxAmountRowers("newrower", amount_seats); setMaxAmountRowers("newrower", amount_seats);
@ -274,6 +278,7 @@ interface ChoiceEvent extends Event{
is_cox: boolean, is_cox: boolean,
steers: boolean, steers: boolean,
cox_on_boat: boolean, cox_on_boat: boolean,
is_racing: boolean,
} }
}; };
} }
@ -320,6 +325,16 @@ function initNewChoice(select: HTMLInputElement) {
}, },
callbackOnInit: function() { callbackOnInit: function() {
this._currentState.items.forEach(function(obj){ this._currentState.items.forEach(function(obj){
if (boat_in_ottensheim && obj.customProperties) {
if (obj.customProperties.is_racing) {
const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js');
var new_option = new Option(obj.label, obj.value);
if (obj.customProperties.cox_on_boat){
new_option.selected = true;
}
coxSelect.add(new_option);
}
}
if (obj.customProperties && obj.customProperties.is_cox){ if (obj.customProperties && obj.customProperties.is_cox){
const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js'); const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js');
var new_option = new Option(obj.label, obj.value); var new_option = new Option(obj.label, obj.value);
@ -346,6 +361,14 @@ function initNewChoice(select: HTMLInputElement) {
const user_id = event.detail.value; const user_id = event.detail.value;
const name = event.detail.label; const name = event.detail.label;
if (boat_in_ottensheim && event.detail.customProperties.is_racing) {
if (event.detail.customProperties.is_racing) {
const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js');
if (coxSelect){
coxSelect.add(new Option(name, user_id));
}
}
}
if (event.detail.customProperties.is_cox) { if (event.detail.customProperties.is_cox) {
const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js'); const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js');
if (coxSelect){ if (coxSelect){
@ -461,10 +484,6 @@ function filterAction(activeFilter: string) {
filterCoxs(); filterCoxs();
break; break;
} }
case 'filter-months': {
filterMonths(activeFilter)
break;
}
} }
} }
@ -482,28 +501,6 @@ function filterCoxs() {
}); });
} }
function filterMonths(action: string) {
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
const monthToggle = <HTMLButtonElement>document.querySelector('button[data-action="' + action + '"]');
if(monthToggle) {
const currentMonth = monthToggle.dataset.month;
if(currentMonth) {
const index = months.indexOf(currentMonth);
if (index > -1) { // only splice array when item is found
months.splice(index, 1); // 2nd parameter means remove one item only
}
Array.prototype.forEach.call(months, (month: HTMLElement) => {
const notThisMonth = document.querySelectorAll('div[data-month="' + month + '"]');
Array.prototype.forEach.call(notThisMonth, (notThisMonth: HTMLElement) => {
notThisMonth.classList.toggle('hidden');
});
});
}
}
}
function initSearch() { function initSearch() {
const input = <HTMLInputElement>document.querySelector('#filter-js'); const input = <HTMLInputElement>document.querySelector('#filter-js');

View File

@ -9,7 +9,9 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.40.1",
"@types/d3": "^7.4.1", "@types/d3": "^7.4.1",
"@types/node": "^20.11.4",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"sass": "^1.60.0", "sass": "^1.60.0",

View File

@ -0,0 +1,77 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
timeout: 180000,
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
//{
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
//},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
//{
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
//},
/* Test against branded browsers. */
//{
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
//},
//{
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
//},
],
/* Run your local dev server before starting the tests */
webServer: {
timeout: 15 * 60 * 1000,
command: 'cd .. && ./test_db.sh && cargo r',
},
});

116
frontend/tests/cox.spec.ts Normal file
View File

@ -0,0 +1,116 @@
import { test, expect, Page } from '@playwright/test';
test('cox can create and delete trip', async ({ page }) => {
await page.goto('http://localhost:8000/auth');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('cox');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Passwort').fill('cox');
await page.getByPlaceholder('Passwort').press('Enter');
await page.getByRole('link', { name: 'Geplante Ausfahrten' }).click();
await page.locator('.relative').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');
await page.locator('#sidebar #planned_starting_time').press('Tab');
await page.getByRole('spinbutton').fill('5');
await page.getByRole('button', { name: 'Erstellen', exact: true }).click();
await expect(page.locator('body')).toContainText('18:00 Uhr (cox) Details');
await page.goto('http://localhost:8000/planned');
await page.getByRole('link', { name: 'Details' }).click();
await page.getByRole('link', { name: 'Termin löschen' }).click();
await expect(page.locator('body')).toContainText('Erfolgreich gelöscht!');
});
// TODO: group -> cox can create trips
// TODO: cox can help/register at trips/events
test.describe('cox can edit trips', () => {
let sharedPage: Page;
test.beforeEach(async ({ browser }) => {
const page = await browser.newPage();
await page.goto('http://localhost:8000/auth');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('cox');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Passwort').fill('cox');
await page.getByPlaceholder('Passwort').press('Enter');
await page.getByRole('link', { name: 'Geplante Ausfahrten' }).click();
await page.locator('.relative').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');
await page.locator('#sidebar #planned_starting_time').press('Tab');
await page.getByRole('spinbutton').fill('5');
await page.getByRole('button', { name: 'Erstellen', exact: true }).click();
sharedPage = page;
});
test('edit remarks', async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await sharedPage.locator('#sidebar #notes').click();
await sharedPage.locator('#sidebar #notes').fill('Meine Anmerkung');
await sharedPage.getByRole('button', { name: 'Speichern' }).click();
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Meine Anmerkung');
await sharedPage.getByRole('button', { name: 'Ausfahrt erstellen schließen' }).click();
});
test('add and remove guest', async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await sharedPage.locator('#sidebar #user_note').click();
await sharedPage.locator('#sidebar #user_note').fill('Mein Gast');
await sharedPage.getByRole('button', { name: 'Gast hinzufügen' }).click();
await expect(sharedPage.locator('body')).toContainText('Erfolgreich angemeldet!');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 4');
await expect(sharedPage.locator('#sidebar')).toContainText('Mein Gast (Gast) Abmelden');
await expect(sharedPage.getByRole('link', { name: 'Termin löschen' })).not.toBeVisible();
await sharedPage.getByRole('link', { name: 'Abmelden' }).click();
await expect(sharedPage.locator('body')).toContainText('Erfolgreich abgemeldet!');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 5');
await expect(sharedPage.locator('#sidebar')).toContainText('Keine Ruderer angemeldet');
await expect(sharedPage.getByRole('link', { name: 'Termin löschen' })).toBeVisible();
await sharedPage.getByRole('button', { name: 'Ausfahrt erstellen schließen' }).click();
});
test('change amount rower', async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 5');
await sharedPage.getByRole('spinbutton').click();
await sharedPage.getByRole('spinbutton').fill('3');
await sharedPage.getByRole('button', { name: 'Speichern' }).click();
await expect(sharedPage.locator('body')).toContainText('Ausfahrt erfolgreich aktualisiert.');
});
test('call off trip', async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 5');
await sharedPage.getByRole('spinbutton').click();
await sharedPage.getByRole('spinbutton').fill('0');
await sharedPage.getByRole('button', { name: 'Speichern' }).click();
await expect(sharedPage.locator('body')).toContainText('Ausfahrt erfolgreich aktualisiert.');
await expect(sharedPage.locator('body')).toContainText('(Absage cox )');
});
test.afterEach(async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await sharedPage.getByRole('link', { name: 'Termin löschen' }).click();
await sharedPage.close();
});
// TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type
});

View File

@ -0,0 +1,69 @@
import { test, expect } from '@playwright/test';
test('Cox can start and cancel trip', async ({ page }, testInfo) => {
await page.goto('http://localhost:8000/auth');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('cox2');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Passwort').fill('cox');
await page.getByPlaceholder('Passwort').press('Enter');
await page.goto('http://localhost:8000/');
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.getByRole('option', { name: 'Joe' }).click();
} else{
await page.getByText('Joe', { exact: true }).click();
}
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');
await expect(page.locator('#shipmaster-newrowerjs')).toContainText('cox');
await expect(page.locator('#steering_person-newrowerjs')).toContainText('rower2 cox');
await page.getByRole('button', { name: 'Ausfahrt eintragen' }).click();
await expect(page.locator('body')).toContainText('Ausfahrt erfolgreich hinzugefügt');
await expect(page.locator('body')).toContainText('Joe');
await page.getByRole('link', { name: 'Joe' }).click();
page.once('dialog', dialog => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
});
test('Cox can start and finish trip', async ({ page }, testInfo) => {
await page.goto('http://localhost:8000/auth');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('cox2');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Passwort').fill('cox');
await page.getByPlaceholder('Passwort').press('Enter');
await page.goto('http://localhost:8000/');
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.getByRole('option', { name: 'Joe' }).click();
} else{
await page.getByText('Joe', { exact: true }).click();
}
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');
await expect(page.locator('#shipmaster-newrowerjs')).toContainText('cox');
await expect(page.locator('#steering_person-newrowerjs')).toContainText('rower2 cox');
await page.getByRole('button', { name: 'Ausfahrt eintragen' }).click();
await expect(page.locator('body')).toContainText('Ausfahrt erfolgreich hinzugefügt');
await expect(page.locator('body')).toContainText('Joe');
await page.goto('http://localhost:8000/log');
await page.waitForTimeout(60000);
await page.locator('div:nth-child(2) > .border-0').click();
await page.getByRole('combobox', { name: 'Destination' }).click();
await page.getByRole('combobox', { name: 'Destination' }).fill('Ottensheim');
await page.getByRole('button', { name: 'Ausfahrt beenden' }).click();
await expect(page.locator('body')).toContainText('Ausfahrt korrekt eingetragen');
});

View File

@ -15,7 +15,12 @@ CREATE TABLE IF NOT EXISTS "user" (
"nickname" text, "nickname" text,
"notes" text, "notes" text,
"phone" text, "phone" text,
"address" text "address" text,
"family_id" INTEGER REFERENCES family(id)
);
CREATE TABLE IF NOT EXISTS "family" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT
); );
CREATE TABLE IF NOT EXISTS "role" ( CREATE TABLE IF NOT EXISTS "role" (

73
notes.md Normal file
View File

@ -0,0 +1,73 @@
# Wordpress auth
Add the following code to `wp-content/themes/bravada/functions.php`:
```
function rot_auth( $user, $username, $password ){
// Make sure a username and password are present for us to work with
if($username == '' || $password == '') return;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Execute the cURL session and get the response
$response = curl_exec($ch);
// Check for cURL errors
if(curl_errno($ch)){
$user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) );
}
// Close the cURL session
curl_close($ch);
if (strpos($response, 'SUCC') !== false) {
$user = get_user_by('login', $username);
if (!$user) {
// User does not exist, create a new one
$userdata = array(
'user_email' => $username,
'user_login' => $username,
'first_name' => $username,
'last_name' => ''
);
$new_user_id = wp_insert_user($userdata);
if (!is_wp_error($new_user_id)) {
// Load the new user info
$user = new WP_User($new_user_id);
// Set role based on username
if ($username == 'Philipp Hofer' || $username == 'Marie Birner') {
$user->set_role('administrator');
} else {
$user->set_role('editor');
}
} else {
// Handle error in user creation
return $new_user_id;
}
} else {
}
} else {
$user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") );
}
return $user;
}
// Comment this line if you wish to fall back on WordPress authentication
// Useful for times when the external service is offline
remove_action('authenticate', 'wp_authenticate_username_password', 20);
add_filter( 'authenticate', 'rot_auth', 10, 3 );
```

10
package-lock.json generated
View File

@ -1,10 +0,0 @@
{
"name": "rot",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "rot"
}
}
}

View File

@ -4,7 +4,7 @@ Description=Rot
[Service] [Service]
User=root User=root
Group=root Group=root
WorkingDirectory=/home/k004373/rowing WorkingDirectory=/home/philipp/rowing
Environment="ROCKET_ENV=prod" Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=8001" Environment="ROCKET_PORT=8001"

View File

@ -4,12 +4,12 @@ Description=Rot Staging
[Service] [Service]
User=root User=root
Group=root Group=root
WorkingDirectory=/home/k004373/rowing-staging WorkingDirectory=/home/philipp/rowing-staging
Environment="ROCKET_ENV=prod" Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=7999" Environment="ROCKET_PORT=7999"
Environment="ROCKET_LOG=info" Environment="ROCKET_LOG=info"
ExecStart=/home/k004373/rowing-staging/rot ExecStart=/home/philipp/rowing-staging/rot
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -2,18 +2,32 @@ INSERT INTO "role" (name) VALUES ('admin');
INSERT INTO "role" (name) VALUES ('cox'); INSERT INTO "role" (name) VALUES ('cox');
INSERT INTO "role" (name) VALUES ('scheckbuch'); INSERT INTO "role" (name) VALUES ('scheckbuch');
INSERT INTO "role" (name) VALUES ('tech'); INSERT INTO "role" (name) VALUES ('tech');
INSERT INTO "role" (name) VALUES ('Donau Linz');
INSERT INTO "role" (name) VALUES ('planned_event');
INSERT INTO "role" (name) VALUES ('Rennrudern');
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1); INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2); INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,6);
INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(2,5);
INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ'); INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ');
INSERT INTO "user_role" (user_id, role_id) VALUES(3,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(3,3); INSERT INTO "user_role" (user_id, role_id) VALUES(3,3);
INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(4,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(4,2); INSERT INTO "user_role" (user_id, role_id) VALUES(4,2);
INSERT INTO "user" (name) VALUES('new'); INSERT INTO "user" (name) VALUES('new');
INSERT INTO "user_role" (user_id, role_id) VALUES(5,5);
INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(6,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(6,2); INSERT INTO "user_role" (user_id, role_id) VALUES(6,2);
INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(7,5);
INSERT INTO "user" (name, pw) VALUES('teen', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(8,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(8,7);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1);

View File

@ -1 +0,0 @@
2023-06-06: Phil Baillon um 19:10 für 18 Uhr Fahrt abgemeldet

View File

@ -1,3 +1,5 @@
#![allow(clippy::blocks_in_conditions)]
pub mod model; pub mod model;
#[cfg(feature = "rowing-tera")] #[cfg(feature = "rowing-tera")]

View File

@ -1,3 +1,5 @@
#![allow(clippy::blocks_in_conditions)]
use std::str::FromStr; use std::str::FromStr;
#[cfg(feature = "rest")] #[cfg(feature = "rest")]

View File

@ -185,7 +185,7 @@ ORDER BY amount_seats DESC
if user.has_role(db, "admin").await { if user.has_role(db, "admin").await {
return Self::all(db).await; return Self::all(db).await;
} }
let boats = if user.has_role(db, "cox").await { let mut boats = if user.has_role(db, "cox").await {
sqlx::query_as!( sqlx::query_as!(
Boat, Boat,
" "
@ -215,6 +215,23 @@ ORDER BY amount_seats DESC
.unwrap() //TODO: fixme .unwrap() //TODO: fixme
}; };
if user.has_role(db, "Rennrudern").await {
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
.await
.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
FROM boat
WHERE owner is null and location_id = ?
ORDER BY amount_seats DESC
",ottensheim.id)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
boats.extend(boats_in_ottensheim.into_iter());
}
Self::boats_to_details(db, boats).await Self::boats_to_details(db, boats).await
} }

83
src/model/family.rs Normal file
View File

@ -0,0 +1,83 @@
use serde::Serialize;
use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
use super::user::User;
#[derive(FromRow, Serialize, Clone)]
pub struct Family {
id: i64,
}
#[derive(Serialize, Clone)]
pub struct FamilyWithMembers {
id: i64,
names: Option<String>,
}
impl Family {
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id FROM role")
.fetch_all(db)
.await
.unwrap()
}
pub async fn insert(db: &SqlitePool) -> i64 {
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
.execute(db)
.await
.unwrap();
result.last_insert_rowid()
}
pub async fn all_with_members(db: &SqlitePool) -> Vec<FamilyWithMembers> {
sqlx::query_as!(
FamilyWithMembers,
"
SELECT
family.id as id,
GROUP_CONCAT(user.name, ', ') as names
FROM family
LEFT JOIN
user ON family.id = user.family_id
GROUP BY family.id;"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_opt_id(db: &SqlitePool, id: Option<i64>) -> Option<Self> {
if let Some(id) = id {
Self::find_by_id(db, id).await
} else {
None
}
}
pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 {
sqlx::query!(
"SELECT COUNT(*) as count FROM user WHERE family_id = ?",
self.id
)
.fetch_one(db)
.await
.unwrap()
.count
}
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id)
.fetch_all(db)
.await
.unwrap()
}
}

View File

@ -264,14 +264,16 @@ ORDER BY departure DESC
return Err(LogbookCreateError::BoatNotFound); return Err(LogbookCreateError::BoatNotFound);
}; };
if boat.amount_seats == 1 && log.rowers.is_empty() {
log.rowers = vec![created_by_user.id];
}
if boat.amount_seats == 1 { if boat.amount_seats == 1 {
log.shipmaster = Some(log.rowers[0]); log.shipmaster = Some(log.rowers[0]);
log.steering_person = Some(log.rowers[0]); log.steering_person = Some(log.rowers[0]);
} }
if let Ok(log_to_finalize) = TryInto::<LogToFinalize>::try_into(log.clone()) { if let Ok(log_to_finalize) = TryInto::<LogToFinalize>::try_into(log.clone()) {
//TODO: fix clone() above
if !boat.shipmaster_allowed(db, created_by_user).await { if !boat.shipmaster_allowed(db, created_by_user).await {
return Err(LogbookCreateError::UserNotAllowedToUseBoat); return Err(LogbookCreateError::UserNotAllowedToUseBoat);
} }

View File

@ -1,10 +1,7 @@
use std::error::Error; use std::{error::Error, fs};
use lettre::{ use lettre::{
message::{ message::{header::ContentType, Attachment, MultiPart, SinglePart},
header::{self, ContentType},
MultiPart, SinglePart,
},
transport::smtp::authentication::Credentials, transport::smtp::authentication::Credentials,
Message, SmtpTransport, Transport, Message, SmtpTransport, Transport,
}; };
@ -12,7 +9,7 @@ use sqlx::SqlitePool;
use crate::tera::admin::mail::MailToSend; use crate::tera::admin::mail::MailToSend;
use super::role::Role; use super::{family::Family, log::Log, role::Role, user::User};
pub struct Mail {} pub struct Mail {}
@ -36,17 +33,38 @@ impl Mail {
for rec in role.mails_from_role(db).await { for rec in role.mails_from_role(db).await {
let splitted = rec.split(','); let splitted = rec.split(',');
for single_rec in splitted { for single_rec in splitted {
email = email.bcc(single_rec.parse().unwrap()); match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => {
Log::create(
db,
format!("Mail not sent to {rec}, because it could not be parsed"),
)
.await;
}
}
} }
} }
// TODO: handle attachments let mut multipart = MultiPart::mixed().singlepart(SinglePart::plain(data.body));
let email = email for temp_file in &data.files {
.subject(data.subject) let content = fs::read(temp_file.path().unwrap()).unwrap();
.header(ContentType::TEXT_PLAIN) let media_type = format!("{}", temp_file.content_type().unwrap().media_type());
.body(String::from(data.body)) let content_type = ContentType::parse(&media_type).unwrap();
.unwrap(); if let Some(name) = temp_file.name() {
let attachment = Attachment::new(format!(
"{}.{}",
name,
temp_file.content_type().unwrap().extension().unwrap()
))
.body(content, content_type);
multipart = multipart.singlepart(attachment);
}
}
let email = email.subject(data.subject).multipart(multipart).unwrap();
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw); let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw);
@ -62,4 +80,106 @@ impl Mail {
}; };
false false
} }
pub async fn fees(db: &SqlitePool, smtp_pw: String) {
let users = User::all_payer_groups(db).await;
for user in users {
if !user.has_role(db, "paid").await {
let mut is_family = false;
let mut send_to = String::new();
match Family::find_by_opt_id(db, user.family_id).await {
Some(family) => {
is_family = true;
for member in family.members(db).await {
if let Some(mail) = member.mail {
send_to.push_str(&format!("{mail},"))
}
}
}
None => {
if let Some(mail) = &user.mail {
send_to.push_str(mail)
}
}
}
let fees = user.fee(db).await;
if let Some(fees) = fees {
let mut content = format!(
"Liebes Vereinsmitglied, \n\n\
dein Vereinsbeitrag für das aktuelle Jahr beträgt {}",
fees.sum_in_cents / 100,
);
if fees.parts.len() == 1 {
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
} else {
content.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
for (desc, fee_in_cents) in fees.parts {
content.push_str(&format!("- {}: {}\n", desc, fee_in_cents / 100))
}
}
if is_family {
content.push_str(&format!(
"Dieser gilt für die gesamte Familie ({}).\n",
fees.name
))
}
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT13 1200 0804 1300 1200. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
Beste Grüße\n\
Der Vorstand
");
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = send_to.split(',');
let mut send_mail = false;
for single_rec in splitted {
let single_rec = single_rec.trim();
match single_rec.parse() {
Ok(val) => {
email = email.bcc(val);
send_mail = true;
}
Err(_) => {
println!("Error in mail: {single_rec}");
}
}
}
if send_mail {
let email = email
.subject("ASKÖ Ruderverein Donau Linz | Vereinsgebühren")
.header(ContentType::TEXT_PLAIN)
.body(content)
.unwrap();
let creds =
Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.clone());
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
}
}
}
}
}
} }

View File

@ -9,6 +9,7 @@ use self::{
pub mod boat; pub mod boat;
pub mod boatdamage; pub mod boatdamage;
pub mod family;
pub mod location; pub mod location;
pub mod log; pub mod log;
pub mod logbook; pub mod logbook;

View File

@ -210,6 +210,7 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
pub async fn update( pub async fn update(
&self, &self,
db: &SqlitePool, db: &SqlitePool,
name: &str,
planned_amount_cox: i32, planned_amount_cox: i32,
max_people: i32, max_people: i32,
notes: Option<&str>, notes: Option<&str>,
@ -217,7 +218,8 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
is_locked: bool, is_locked: bool,
) { ) {
sqlx::query!( sqlx::query!(
"UPDATE planned_event SET planned_amount_cox = ? WHERE id = ?", "UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
name,
planned_amount_cox, planned_amount_cox,
self.id self.id
) )

View File

@ -3,8 +3,8 @@ use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Serialize, Clone)] #[derive(FromRow, Serialize, Clone)]
pub struct Role { pub struct Role {
id: i64, pub(crate) id: i64,
name: String, pub(crate) name: String,
} }
impl Role { impl Role {
@ -30,13 +30,28 @@ WHERE id like ?
.ok() .ok()
} }
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name
FROM role
WHERE name like ?
",
name
)
.fetch_one(db)
.await
.ok()
}
pub async fn mails_from_role(&self, db: &SqlitePool) -> Vec<String> { pub async fn mails_from_role(&self, db: &SqlitePool) -> Vec<String> {
let query = format!( let query = format!(
"SELECT u.mail "SELECT u.mail
FROM user u FROM user u
JOIN user_role ur ON u.id = ur.user_id JOIN user_role ur ON u.id = ur.user_id
JOIN role r ON ur.role_id = r.id JOIN role r ON ur.role_id = r.id
WHERE r.id = {}", WHERE r.id = {} AND deleted=0;",
self.id self.id
); );

View File

@ -16,7 +16,7 @@ impl Rower {
sqlx::query_as!( sqlx::query_as!(
User, User,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
", ",

View File

@ -62,13 +62,18 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND b.name != '
let rowed_km_guests = sqlx::query(&format!( let rowed_km_guests = sqlx::query(&format!(
" "
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM user u FROM user u
INNER JOIN rower r ON u.id = r.rower_id INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id INNER JOIN logbook l ON r.logbook_id = l.id
INNER JOIN user_role ur ON u.id = ur.user_id WHERE u.id NOT IN (
INNER JOIN role ro ON ur.role_id = ro.id SELECT ur.user_id
WHERE ro.name = 'scheckbuch' AND l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%'; FROM user_role ur
INNER JOIN role ro ON ur.role_id = ro.id
WHERE ro.name = 'Donau Linz'
)
AND l.distance_in_km IS NOT NULL
AND l.arrival LIKE '{year}-%';
" "
)) ))
.fetch_one(db) .fetch_one(db)
@ -93,10 +98,10 @@ WHERE ro.name = 'scheckbuch' AND l.distance_in_km IS NOT NULL AND l.arrival LIKE
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM ( FROM (
SELECT * FROM user SELECT * FROM user
WHERE id NOT IN ( WHERE id IN (
SELECT user_id FROM user_role SELECT user_id FROM user_role
JOIN role ON user_role.role_id = role.id JOIN role ON user_role.role_id = role.id
WHERE role.name = 'scheckbuch' WHERE role.name = 'Donau Linz'
) )
) u ) u
INNER JOIN rower r ON u.id = r.rower_id INNER JOIN rower r ON u.id = r.rower_id

View File

@ -13,9 +13,18 @@ use rocket::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{log::Log, tripdetails::TripDetails, Day}; use super::{family::Family, log::Log, role::Role, tripdetails::TripDetails, Day};
use crate::tera::admin::user::UserEditForm; use crate::tera::admin::user::UserEditForm;
const RENNRUDERBEITRAG: i32 = 11000;
const BOAT_STORAGE: i32 = 4500;
const FAMILY_TWO: i32 = 30000;
const FAMILY_THREE_OR_MORE: i32 = 35000;
const STUDENT_OR_PUPIL: i32 = 8000;
const REGULAR: i32 = 22000;
const UNTERSTUETZEND: i32 = 2500;
const FOERDERND: i32 = 8500;
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct User { pub struct User {
pub id: i64, pub id: i64,
@ -33,6 +42,7 @@ pub struct User {
pub notes: Option<String>, pub notes: Option<String>,
pub phone: Option<String>, pub phone: Option<String>,
pub address: Option<String>, pub address: Option<String>,
pub family_id: Option<i64>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -89,7 +99,132 @@ pub enum LoginError {
DeserializationError, DeserializationError,
} }
#[derive(Debug, Serialize)]
pub struct Fee {
pub sum_in_cents: i32,
pub parts: Vec<(String, i32)>,
pub name: String,
pub user_ids: String,
pub paid: bool,
}
impl Fee {
pub fn new() -> Self {
Self {
sum_in_cents: 0,
name: "".into(),
parts: Vec::new(),
user_ids: "".into(),
paid: false,
}
}
pub fn add(&mut self, desc: String, price_in_cents: i32) {
self.sum_in_cents += price_in_cents;
self.parts.push((desc, price_in_cents));
}
pub fn add_person(&mut self, user: &User) {
if !self.name.is_empty() {
self.name.push_str(" + ");
self.user_ids.push('&');
}
self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
}
pub fn paid(&mut self) {
self.paid = true;
}
pub fn merge(&mut self, fee: Fee) {
for (desc, price_in_cents) in fee.parts {
self.add(desc, price_in_cents);
}
}
}
impl User { impl User {
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
if !self.has_role(db, "Donau Linz").await {
return None;
}
if self.deleted {
return None;
}
let mut fee = Fee::new();
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
for member in family.members(db).await {
fee.add_person(&member);
if member.has_role(db, "paid").await {
fee.paid();
}
fee.merge(member.fee_without_families(db).await);
}
if family.amount_family_members(db).await > 2 {
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
} else {
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
}
} else {
fee.add_person(self);
if self.has_role(db, "paid").await {
fee.paid();
}
fee.merge(self.fee_without_families(db).await);
}
Some(fee)
}
async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
let mut fee = Fee::new();
if !self.has_role(db, "Donau Linz").await {
return fee;
}
if self.has_role(db, "Rennrudern").await {
fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG);
}
let amount_boats = self.amount_boats(db).await;
if amount_boats > 0 {
fee.add(
format!("{}x Bootsplatz", amount_boats),
amount_boats * BOAT_STORAGE,
);
}
if self.has_role(db, "Unterstützend").await {
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
} else if self.has_role(db, "Förderndes Mitglied").await {
fee.add("Förderndes Mitglied".into(), FOERDERND);
} else if Family::find_by_opt_id(db, self.family_id).await.is_none() {
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
} else {
fee.add("Mitgliedsbeitrag".into(), REGULAR);
}
}
fee
}
pub async fn amount_boats(&self, db: &SqlitePool) -> i32 {
sqlx::query!(
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
self.id
)
.fetch_one(db)
.await
.unwrap()
.count
}
pub async fn rowed_km(&self, db: &SqlitePool) -> i32 { pub async fn rowed_km(&self, db: &SqlitePool) -> i32 {
sqlx::query!( sqlx::query!(
"SELECT COALESCE(SUM(distance_in_km),0) as rowed_km "SELECT COALESCE(SUM(distance_in_km),0) as rowed_km
@ -161,7 +296,7 @@ impl User {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE id like ? WHERE id like ?
", ",
@ -176,7 +311,7 @@ WHERE id like ?
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE id like ? WHERE id like ?
", ",
@ -191,7 +326,7 @@ WHERE id like ?
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE name like ? WHERE name like ?
", ",
@ -233,7 +368,7 @@ WHERE name like ?
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE deleted = 0 WHERE deleted = 0
ORDER BY last_access DESC ORDER BY last_access DESC
@ -244,11 +379,31 @@ ORDER BY last_access DESC
.unwrap() .unwrap()
} }
pub async fn all_payer_groups(db: &SqlitePool) -> Vec<Self> {
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 family_id IS NOT NULL
GROUP BY family_id
UNION
-- Select users with a null family_id, without grouping
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
WHERE family_id IS NULL;
"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn ergo(db: &SqlitePool) -> Vec<Self> { pub async fn ergo(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
ORDER BY name ORDER BY name
@ -263,7 +418,7 @@ ORDER BY name
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0 WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
ORDER BY last_access DESC ORDER BY last_access DESC
@ -282,8 +437,14 @@ ORDER BY last_access DESC
} }
pub async fn update(&self, db: &SqlitePool, data: UserEditForm) { pub async fn update(&self, db: &SqlitePool, data: UserEditForm) {
let mut family_id = data.family_id;
if family_id.is_some_and(|x| x == -1) {
family_id = Some(Family::insert(db).await)
}
sqlx::query!( sqlx::query!(
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=? where id = ?", "UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?",
data.dob, data.dob,
data.weight, data.weight,
data.sex, data.sex,
@ -294,6 +455,7 @@ ORDER BY last_access DESC
data.notes, data.notes,
data.phone, data.phone,
data.address, data.address,
family_id,
self.id self.id
) )
.execute(db) .execute(db)
@ -307,21 +469,66 @@ ORDER BY last_access DESC
.unwrap(); .unwrap();
for role_id in data.roles.into_keys() { for role_id in data.roles.into_keys() {
sqlx::query!( self.add_role(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", db,
self.id, &Role::find_by_id(db, role_id.parse::<i32>().unwrap())
role_id .await
.unwrap(),
) )
.execute(db) .await;
.await
.unwrap();
} }
} }
pub async fn add_role(&self, db: &SqlitePool, role: &Role) {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
}
pub async fn remove_role(&self, db: &SqlitePool, role: &Role) {
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
}
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> { pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
let name = name.trim(); // just to make sure... let name = name.trim(); // just to make sure...
let Some(user) = User::find_by_name(db, name).await else { let Some(user) = User::find_by_name(db, name).await else {
Log::create(db, format!("Username ({name}) not found (tried to login)")).await; if ![
"n-sageder",
"p-hofer",
"m-birner",
"s-sollberger",
"d-kortschak",
"wwwadmin",
"wadminw",
"admin",
"m sageder",
"d kortschak",
"a almousa",
"p hofer",
"s sollberger",
"n sageder",
"wp-system",
"s.sollberger",
"m.birner",
"m-sageder",
"a-almousa",
]
.contains(&name)
{
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
}
return Err(LoginError::InvalidAuthenticationCombo); // Username not found return Err(LoginError::InvalidAuthenticationCombo); // Username not found
}; };
@ -440,23 +647,20 @@ impl<'r> FromRequest<'r> for User {
Ok(user_id) => { Ok(user_id) => {
let db = req.rocket().state::<SqlitePool>().unwrap(); let db = req.rocket().state::<SqlitePool>().unwrap();
let Some(user) = User::find_by_id(db, user_id).await else { let Some(user) = User::find_by_id(db, user_id).await else {
return Outcome::Error((Status::Unauthorized, LoginError::UserNotFound)); return Outcome::Error((Status::Forbidden, LoginError::UserNotFound));
}; };
if user.deleted { if user.deleted {
return Outcome::Error((Status::Unauthorized, LoginError::UserDeleted)); return Outcome::Error((Status::Forbidden, LoginError::UserDeleted));
} }
user.logged_in(db).await; user.logged_in(db).await;
let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id)); let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id));
cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(12)); cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2));
req.cookies().add_private(cookie); req.cookies().add_private(cookie);
Outcome::Success(user) Outcome::Success(user)
} }
Err(_) => { Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)),
println!("{:?}", user_id.value());
Outcome::Error((Status::Unauthorized, LoginError::DeserializationError))
}
}, },
None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)), None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)),
} }
@ -487,7 +691,7 @@ impl<'r> FromRequest<'r> for TechUser {
if user.has_role(db, "tech").await { if user.has_role(db, "tech").await {
Outcome::Success(TechUser { user }) Outcome::Success(TechUser { user })
} else { } else {
Outcome::Error((Status::Unauthorized, LoginError::NotACox)) Outcome::Error((Status::Forbidden, LoginError::NotACox))
} }
} }
Outcome::Error(f) => Outcome::Error(f), Outcome::Error(f) => Outcome::Error(f),
@ -530,7 +734,7 @@ impl<'r> FromRequest<'r> for CoxUser {
if user.has_role(db, "cox").await { if user.has_role(db, "cox").await {
Outcome::Success(CoxUser { user }) Outcome::Success(CoxUser { user })
} else { } else {
Outcome::Error((Status::Unauthorized, LoginError::NotACox)) Outcome::Error((Status::Forbidden, LoginError::NotACox))
} }
} }
Outcome::Error(f) => Outcome::Error(f), Outcome::Error(f) => Outcome::Error(f),
@ -555,7 +759,7 @@ impl<'r> FromRequest<'r> for AdminUser {
if user.has_role(db, "admin").await { if user.has_role(db, "admin").await {
Outcome::Success(AdminUser { user }) Outcome::Success(AdminUser { user })
} else { } else {
Outcome::Error((Status::Unauthorized, LoginError::NotACox)) Outcome::Error((Status::Forbidden, LoginError::NotACox))
} }
} }
Outcome::Error(f) => Outcome::Error(f), Outcome::Error(f) => Outcome::Error(f),
@ -565,22 +769,20 @@ impl<'r> FromRequest<'r> for AdminUser {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct NonGuestUser { pub struct AllowedForPlannedTripsUser(pub(crate) User);
pub(crate) user: User,
}
#[async_trait] #[async_trait]
impl<'r> FromRequest<'r> for NonGuestUser { impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser {
type Error = LoginError; type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap(); let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await { match User::from_request(req).await {
Outcome::Success(user) => { Outcome::Success(user) => {
if !user.has_role(db, "scheckbuch").await { if user.has_role(db, "Donau Linz").await | user.has_role(db, "scheckbuch").await {
Outcome::Success(NonGuestUser { user }) Outcome::Success(AllowedForPlannedTripsUser(user))
} else { } else {
Outcome::Error((Status::Unauthorized, LoginError::NotACox)) Outcome::Error((Status::Forbidden, LoginError::NotACox))
} }
} }
Outcome::Error(f) => Outcome::Error(f), Outcome::Error(f) => Outcome::Error(f),
@ -589,6 +791,125 @@ impl<'r> FromRequest<'r> for NonGuestUser {
} }
} }
impl Into<User> for AllowedForPlannedTripsUser {
fn into(self) -> User {
self.0
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DonauLinzUser(pub(crate) User);
impl Into<User> for DonauLinzUser {
fn into(self) -> User {
self.0
}
}
impl Deref for DonauLinzUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for DonauLinzUser {
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, "Donau Linz").await
&& !user.has_role(db, "Unterstützend").await
&& !user.has_role(db, "Förderndes Mitglied").await
{
Outcome::Success(DonauLinzUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VorstandUser(pub(crate) User);
impl Into<User> for VorstandUser {
fn into(self) -> User {
self.0
}
}
impl Deref for VorstandUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for VorstandUser {
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, "Vorstand").await {
Outcome::Success(VorstandUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PlannedEventUser(pub(crate) User);
impl Into<User> for PlannedEventUser {
fn into(self) -> User {
self.0
}
}
impl Deref for PlannedEventUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for PlannedEventUser {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if user.has_role(db, "planned_event").await {
Outcome::Success(PlannedEventUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::collections::HashMap; use std::collections::HashMap;
@ -674,6 +995,7 @@ mod test {
notes: None, notes: None,
phone: None, phone: None,
address: None, address: None,
family_id: None,
}, },
) )
.await; .await;

View File

@ -1,4 +1,4 @@
use rocket::{form::Form, fs::FileServer, post, routes, Build, FromForm, Rocket, State}; use rocket::{form::Form, post, routes, Build, FromForm, Rocket, State};
use serde_json::json; use serde_json::json;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@ -27,7 +27,7 @@ async fn login(login: Form<LoginForm<'_>>, db: &State<SqlitePool>) -> String {
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
rocket rocket
.mount("/", FileServer::from("svelte/build").rank(0)) //.mount("/", FileServer::from("svelte/build").rank(0))
.mount("/api/login", routes![login]) .mount("/api/login", routes![login])
} }

View File

@ -33,6 +33,12 @@ async fn index(
Template::render("admin/mail", context.into_json()) Template::render("admin/mail", context.into_json())
} }
#[get("/mail/fee")]
async fn fee(db: &State<SqlitePool>, _admin: AdminUser, config: &State<Config>) -> &'static str {
Mail::fees(db, config.smtp_pw.clone()).await;
"SUCC"
}
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
pub struct MailToSend<'a> { pub struct MailToSend<'a> {
pub(crate) role_id: i32, pub(crate) role_id: i32,
@ -50,14 +56,14 @@ async fn update(
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let d = data.into_inner(); let d = data.into_inner();
if Mail::send(db, d, config.smtp_pw.clone()).await { if Mail::send(db, d, config.smtp_pw.clone()).await {
return Flash::success(Redirect::to("/admin/mail"), "Mail versendet"); Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
} else { } else {
return Flash::error(Redirect::to("/admin/mail"), "Fehler"); Flash::error(Redirect::to("/admin/mail"), "Fehler")
} }
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![index, update] routes![index, update, fee]
} }
#[cfg(test)] #[cfg(test)]

View File

@ -10,7 +10,7 @@ use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
planned_event::PlannedEvent, planned_event::PlannedEvent,
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
user::AdminUser, user::PlannedEventUser,
}; };
//TODO: add constraints (e.g. planned_amount_cox > 0) //TODO: add constraints (e.g. planned_amount_cox > 0)
@ -25,7 +25,7 @@ struct AddPlannedEventForm<'r> {
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<AddPlannedEventForm<'_>>, data: Form<AddPlannedEventForm<'_>>,
_admin: AdminUser, _admin: PlannedEventUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let data = data.into_inner(); let data = data.into_inner();
@ -36,13 +36,14 @@ async fn create(
PlannedEvent::create(db, data.name, data.planned_amount_cox, trip_details).await; PlannedEvent::create(db, data.name, data.planned_amount_cox, trip_details).await;
Flash::success(Redirect::to("/"), "Event hinzugefügt") Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
} }
//TODO: add constraints (e.g. planned_amount_cox > 0) //TODO: add constraints (e.g. planned_amount_cox > 0)
#[derive(FromForm)] #[derive(FromForm)]
struct UpdatePlannedEventForm<'r> { struct UpdatePlannedEventForm<'r> {
id: i64, id: i64,
name: &'r str,
planned_amount_cox: i32, planned_amount_cox: i32,
max_people: i32, max_people: i32,
notes: Option<&'r str>, notes: Option<&'r str>,
@ -54,13 +55,14 @@ struct UpdatePlannedEventForm<'r> {
async fn update( async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<UpdatePlannedEventForm<'_>>, data: Form<UpdatePlannedEventForm<'_>>,
_admin: AdminUser, _admin: PlannedEventUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
match PlannedEvent::find_by_id(db, data.id).await { match PlannedEvent::find_by_id(db, data.id).await {
Some(planned_event) => { Some(planned_event) => {
planned_event planned_event
.update( .update(
db, db,
data.name,
data.planned_amount_cox, data.planned_amount_cox,
data.max_people, data.max_people,
data.notes, data.notes,
@ -68,20 +70,20 @@ async fn update(
data.is_locked, data.is_locked,
) )
.await; .await;
Flash::success(Redirect::to("/"), "Successfully edited the event") Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
} }
None => Flash::error(Redirect::to("/"), "Planned event id not found"), None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
} }
} }
#[get("/planned-event/<id>/delete")] #[get("/planned-event/<id>/delete")]
async fn delete(db: &State<SqlitePool>, id: i64, _admin: AdminUser) -> Flash<Redirect> { async fn delete(db: &State<SqlitePool>, id: i64, _admin: PlannedEventUser) -> Flash<Redirect> {
match PlannedEvent::find_by_id(db, id).await { match PlannedEvent::find_by_id(db, id).await {
Some(planned_event) => { Some(planned_event) => {
planned_event.delete(db).await; planned_event.delete(db).await;
Flash::success(Redirect::to("/"), "Event gelöscht") Flash::success(Redirect::to("/planned"), "Event gelöscht")
} }
None => Flash::error(Redirect::to("/"), "PlannedEvent does not exist"), None => Flash::error(Redirect::to("/planned"), "PlannedEvent does not exist"),
} }
} }
@ -120,7 +122,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -151,7 +153,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -183,11 +185,11 @@ mod test {
let req = client let req = client
.put("/admin/planned-event") .put("/admin/planned-event")
.header(ContentType::Form) // Set the content type to form .header(ContentType::Form) // Set the content type to form
.body("id=1&planned_amount_cox=2&max_people=3&notes=new-planned-event-text"); // Add the form data to the request body; .body("id=1&planned_amount_cox=2&max_people=3&notes=new-planned-event-text&name=test"); // Add the form data to the request body;
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -196,7 +198,7 @@ mod test {
assert_eq!( assert_eq!(
flash_cookie.value(), flash_cookie.value(),
"7:successSuccessfully edited the event" "7:successEvent erfolgreich bearbeitet"
); );
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap(); let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
@ -220,11 +222,13 @@ mod test {
let req = client let req = client
.put("/admin/planned-event") .put("/admin/planned-event")
.header(ContentType::Form) // Set the content type to form .header(ContentType::Form) // Set the content type to form
.body("id=1337&planned_amount_cox=2&max_people=3&notes=new-planned-event-text"); // Add the form data to the request body; .body(
"id=1337&planned_amount_cox=2&max_people=3&notes=new-planned-event-text&name=test",
); // Add the form data to the request body;
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -255,7 +259,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()

View File

@ -1,8 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::model::{ use crate::model::{
family::Family,
role::Role, role::Role,
user::{AdminUser, User, UserWithRoles}, user::{AdminUser, User, UserWithRoles, VorstandUser},
}; };
use futures::future::join_all; use futures::future::join_all;
use rocket::{ use rocket::{
@ -18,7 +19,7 @@ use sqlx::SqlitePool;
#[get("/user")] #[get("/user")]
async fn index( async fn index(
db: &State<SqlitePool>, db: &State<SqlitePool>,
admin: AdminUser, user: VorstandUser,
flash: Option<FlashMessage<'_>>, flash: Option<FlashMessage<'_>>,
) -> Template { ) -> Template {
let user_futures: Vec<_> = User::all(db) let user_futures: Vec<_> = User::all(db)
@ -27,24 +28,83 @@ async fn index(
.map(|u| async move { UserWithRoles::from_user(u, db).await }) .map(|u| async move { UserWithRoles::from_user(u, db).await })
.collect(); .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<UserWithRoles> = join_all(user_futures).await;
let roles = Role::all(db).await; let roles = Role::all(db).await;
let families = Family::all_with_members(db).await;
let mut context = Context::new(); let mut context = Context::new();
if let Some(msg) = flash { if let Some(msg) = flash {
context.insert("flash", &msg.into_inner()); context.insert("flash", &msg.into_inner());
} }
context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users); context.insert("users", &users);
context.insert("roles", &roles); context.insert("roles", &roles);
context.insert( context.insert("families", &families);
"loggedin_user", context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
&UserWithRoles::from_user(admin.user, db).await,
);
Template::render("admin/user/index", context.into_json()) Template::render("admin/user/index", context.into_json())
} }
#[get("/user/fees")]
async fn fees(
db: &State<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
let users = User::all_payer_groups(db).await;
let mut fees = Vec::new();
for user in users {
if let Some(fee) = user.fee(db).await {
fees.push(fee);
}
}
context.insert("fees", &fees);
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert(
"loggedin_user",
&UserWithRoles::from_user(admin.into(), db).await,
);
Template::render("admin/user/fees", context.into_json())
}
#[get("/user/fees/paid?<user_ids>")]
async fn fees_paid(
db: &State<SqlitePool>,
_admin: AdminUser,
user_ids: Vec<i32>,
) -> Flash<Redirect> {
let mut res = String::new();
for user_id in user_ids {
let user = User::find_by_id(db, user_id).await.unwrap();
res.push_str(&format!("{} + ", user.name));
if user.has_role(db, "paid").await {
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await;
} else {
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await;
}
}
res.truncate(res.len() - 3); // remove ' + ' from the end
Flash::success(
Redirect::to("/admin/user/fees"),
format!("Zahlungsstatus von {} erfolgreich geändert", res),
)
}
#[get("/user/<user>/reset-pw")] #[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> { async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await; let user = User::find_by_id(db, user).await;
@ -89,6 +149,7 @@ pub struct UserEditForm {
pub(crate) notes: Option<String>, pub(crate) notes: Option<String>,
pub(crate) phone: Option<String>, pub(crate) phone: Option<String>,
pub(crate) address: Option<String>, pub(crate) address: Option<String>,
pub(crate) family_id: Option<i64>,
} }
#[post("/user", data = "<data>")] #[post("/user", data = "<data>")]
@ -132,5 +193,5 @@ async fn create(
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![index, resetpw, update, create, delete] routes![index, resetpw, update, create, delete, fees, fees_paid]
} }

View File

@ -89,7 +89,15 @@ async fn login(
) )
.await; .await;
Flash::success(Redirect::to("/"), "Login erfolgreich") // Check for redirect_url cookie and redirect accordingly
match cookies.get_private("redirect_url") {
Some(redirect_cookie) => {
let redirect_url = redirect_cookie.value().to_string();
cookies.remove_private(redirect_cookie); // Remove the cookie after using it
Flash::success(Redirect::to(redirect_url), "Login erfolgreich")
}
None => Flash::success(Redirect::to("/"), "Login erfolgreich"),
}
} }
#[get("/set-pw/<userid>")] #[get("/set-pw/<userid>")]

View File

@ -13,7 +13,7 @@ use crate::{
model::{ model::{
boat::Boat, boat::Boat,
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles}, user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles},
}, },
tera::log::KioskCookie, tera::log::KioskCookie,
}; };
@ -45,7 +45,7 @@ async fn index_kiosk(
async fn index( async fn index(
db: &State<SqlitePool>, db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>, flash: Option<FlashMessage<'_>>,
user: NonGuestUser, user: DonauLinzUser,
) -> Template { ) -> Template {
let boatdamages = BoatDamage::all(db).await; let boatdamages = BoatDamage::all(db).await;
let boats = Boat::all(db).await; let boats = Boat::all(db).await;
@ -59,7 +59,7 @@ async fn index(
context.insert("boats", &boats); context.insert("boats", &boats);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(user.user, db).await, &UserWithRoles::from_user(user.into(), db).await,
); );
Template::render("boatdamages", context.into_json()) Template::render("boatdamages", context.into_json())
@ -76,13 +76,14 @@ pub struct FormBoatDamageToAdd<'r> {
async fn create<'r>( async fn create<'r>(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<FormBoatDamageToAdd<'r>>, data: Form<FormBoatDamageToAdd<'r>>,
user: NonGuestUser, user: DonauLinzUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let user: User = user.into();
let boatdamage_to_add = BoatDamageToAdd { let boatdamage_to_add = BoatDamageToAdd {
boat_id: data.boat_id, boat_id: data.boat_id,
desc: data.desc, desc: data.desc,
lock_boat: data.lock_boat, lock_boat: data.lock_boat,
user_id_created: user.user.id as i32, user_id_created: user.id as i32,
}; };
match BoatDamage::create(db, boatdamage_to_add).await { match BoatDamage::create(db, boatdamage_to_add).await {
Ok(_) => Flash::success( Ok(_) => Flash::success(

View File

@ -34,7 +34,7 @@ async fn create(
//) //)
//.await; //.await;
Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich erstellt.") Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
} }
#[derive(FromForm)] #[derive(FromForm)]
@ -66,16 +66,19 @@ async fn update(
) )
.await .await
{ {
Ok(_) => Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich aktualisiert."), Ok(_) => Flash::success(
Redirect::to("/planned"),
"Ausfahrt erfolgreich aktualisiert.",
),
Err(TripUpdateError::NotYourTrip) => { Err(TripUpdateError::NotYourTrip) => {
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!") Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
} }
Err(TripUpdateError::TripDetailsDoesNotExist) => { Err(TripUpdateError::TripDetailsDoesNotExist) => {
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht") Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
} }
} }
} else { } else {
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht") Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
} }
} }
@ -92,21 +95,21 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
), ),
) )
.await; .await;
Flash::success(Redirect::to("/"), "Danke für's helfen!") Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
} }
Err(CoxHelpError::AlreadyRegisteredAsCox) => { Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/"), "Du hilfst bereits aus!") Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
} }
Err(CoxHelpError::AlreadyRegisteredAsRower) => Flash::error( Err(CoxHelpError::AlreadyRegisteredAsRower) => Flash::error(
Redirect::to("/"), Redirect::to("/planned"),
"Du hast dich bereits als Ruderer angemeldet!", "Du hast dich bereits als Ruderer angemeldet!",
), ),
Err(CoxHelpError::DetailsLocked) => { Err(CoxHelpError::DetailsLocked) => {
Flash::error(Redirect::to("/"), "Boot ist bereits eingeteilt.") Flash::error(Redirect::to("/planned"), "Boot ist bereits eingeteilt.")
} }
} }
} else { } else {
Flash::error(Redirect::to("/"), "Event gibt's nicht") Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
} }
} }
@ -114,18 +117,18 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flash<Redirect> { async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flash<Redirect> {
let trip = Trip::find_by_id(db, trip_id).await; let trip = Trip::find_by_id(db, trip_id).await;
match trip { match trip {
None => Flash::error(Redirect::to("/"), "Trip gibt's nicht!"), None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"),
Some(trip) => match trip.delete(db, &cox).await { Some(trip) => match trip.delete(db, &cox).await {
Ok(_) => { Ok(_) => {
Log::create(db, format!("Cox {} deleted trip.id={}", cox.name, trip_id)).await; Log::create(db, format!("Cox {} deleted trip.id={}", cox.name, trip_id)).await;
Flash::success(Redirect::to("/"), "Erfolgreich gelöscht!") Flash::success(Redirect::to("/planned"), "Erfolgreich gelöscht!")
} }
Err(TripDeleteError::SomebodyAlreadyRegistered) => Flash::error( Err(TripDeleteError::SomebodyAlreadyRegistered) => Flash::error(
Redirect::to("/"), Redirect::to("/planned"),
"Ausfahrt kann nicht gelöscht werden, da bereits jemand registriert ist!", "Ausfahrt kann nicht gelöscht werden, da bereits jemand registriert ist!",
), ),
Err(TripDeleteError::NotYourTrip) => { Err(TripDeleteError::NotYourTrip) => {
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!") Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
} }
}, },
} }
@ -145,17 +148,17 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
) )
.await; .await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
} }
Err(TripHelpDeleteError::DetailsLocked) => { Err(TripHelpDeleteError::DetailsLocked) => {
Flash::error(Redirect::to("/"), "Boot bereits eingeteilt") Flash::error(Redirect::to("/planned"), "Boot bereits eingeteilt")
} }
Err(TripHelpDeleteError::CoxNotHelping) => { Err(TripHelpDeleteError::CoxNotHelping) => {
Flash::error(Redirect::to("/"), "Steuermann hilft nicht aus...") Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
} }
} }
} else { } else {
Flash::error(Redirect::to("/"), "Planned_event does not exist.") Flash::error(Redirect::to("/planned"), "Planned_event does not exist.")
} }
} }
@ -202,7 +205,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -250,7 +253,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -288,7 +291,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -326,7 +329,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -354,7 +357,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -367,7 +370,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -391,14 +394,14 @@ mod test {
.body("name=cox&password=cox"); // Add the form data to the request body; .body("name=cox&password=cox"); // Add the form data to the request body;
login.dispatch().await; login.dispatch().await;
let req = client.get("/join/1"); let req = client.get("/planned/join/1");
let _ = req.dispatch().await; let _ = req.dispatch().await;
let req = client.get("/cox/join/1"); let req = client.get("/cox/join/1");
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -429,7 +432,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -470,7 +473,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -498,7 +501,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@ -526,7 +529,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/")); assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()

View File

@ -23,7 +23,7 @@ use crate::model::{
LogbookUpdateError, LogbookUpdateError,
}, },
logtype::LogType, logtype::LogType,
user::{NonGuestUser, User, UserWithRoles, UserWithWaterStatus}, user::{DonauLinzUser, User, UserWithRoles, UserWithWaterStatus},
}; };
pub struct KioskCookie(String); pub struct KioskCookie(String);
@ -44,9 +44,9 @@ impl<'r> FromRequest<'r> for KioskCookie {
async fn index( async fn index(
db: &State<SqlitePool>, db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>, flash: Option<FlashMessage<'_>>,
user: NonGuestUser, user: DonauLinzUser,
) -> Template { ) -> Template {
let boats = Boat::for_user(db, &user.user).await; let boats = Boat::for_user(db, &user).await;
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all( let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
User::cox(db) User::cox(db)
@ -78,7 +78,7 @@ async fn index(
context.insert("logtypes", &logtypes); context.insert("logtypes", &logtypes);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(user.user, db).await, &UserWithRoles::from_user(user.into(), db).await,
); );
context.insert("on_water", &on_water); context.insert("on_water", &on_water);
context.insert("distances", &distances); context.insert("distances", &distances);
@ -87,12 +87,12 @@ async fn index(
} }
#[get("/show", rank = 2)] #[get("/show", rank = 2)]
async fn show(db: &State<SqlitePool>, user: NonGuestUser) -> Template { async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
let logs = Logbook::completed(db).await; let logs = Logbook::completed(db).await;
Template::render( Template::render(
"log.completed", "log.completed",
context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await), context!(logs, loggedin_user: &UserWithRoles::from_user(user.into(), db).await),
) )
} }
@ -125,7 +125,7 @@ async fn new_kiosk(
async fn kiosk( async fn kiosk(
db: &State<SqlitePool>, db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>, flash: Option<FlashMessage<'_>>,
kiosk: KioskCookie, _kiosk: KioskCookie,
) -> Template { ) -> Template {
let boats = Boat::all(db).await; let boats = Boat::all(db).await;
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all( let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
@ -166,12 +166,12 @@ async fn kiosk(
async fn create_logbook( async fn create_logbook(
db: &SqlitePool, db: &SqlitePool,
data: Form<LogToAdd>, data: Form<LogToAdd>,
user: &NonGuestUser, user: &DonauLinzUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
match Logbook::create( match Logbook::create(
db, db,
data.into_inner(), data.into_inner(),
&user.user user
) )
.await .await
{ {
@ -188,7 +188,7 @@ async fn create_logbook(
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"), Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"),
Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"), 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::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in den letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."), Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
} }
} }
@ -197,14 +197,11 @@ async fn create_logbook(
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<LogToAdd>, data: Form<LogToAdd>,
user: NonGuestUser, user: DonauLinzUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
Log::create( Log::create(
db, db,
format!( format!("User {} tries to create log entry={:?}", &user.name, data),
"User {} tries to create log entry={:?}",
user.user.name, data
),
) )
.await; .await;
@ -238,14 +235,14 @@ async fn create_kiosk(
) )
.await; .await;
create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme
} }
async fn home_logbook( async fn home_logbook(
db: &SqlitePool, db: &SqlitePool,
data: Form<LogToFinalize>, data: Form<LogToFinalize>,
logbook_id: i32, logbook_id: i32,
user: &NonGuestUser, user: &DonauLinzUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await; let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await;
let Some(logbook) = logbook else { let Some(logbook) = logbook else {
@ -255,7 +252,7 @@ async fn home_logbook(
); );
}; };
match logbook.home(db, &user.user, data.into_inner()).await { match logbook.home(db,user, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."), Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
@ -285,11 +282,11 @@ async fn home_kiosk(
db, db,
data, data,
logbook_id, logbook_id,
&NonGuestUser { &DonauLinzUser(
user: User::find_by_id(db, logbook.shipmaster as i32) User::find_by_id(db, logbook.shipmaster as i32)
.await .await
.unwrap(), //TODO: fixme .unwrap(),
}, ), //TODO: fixme
) )
.await .await
} }
@ -299,13 +296,13 @@ async fn home(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<LogToFinalize>, data: Form<LogToFinalize>,
logbook_id: i32, logbook_id: i32,
user: NonGuestUser, user: DonauLinzUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
Log::create( Log::create(
db, db,
format!( format!(
"User {} tries to finish log entry {logbook_id} {data:?}", "User {} tries to finish log entry {logbook_id} {data:?}",
user.user.name &user.name
), ),
) )
.await; .await;
@ -314,12 +311,12 @@ async fn home(
} }
#[get("/<logbook_id>/delete", rank = 2)] #[get("/<logbook_id>/delete", rank = 2)]
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: User) -> Flash<Redirect> { async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) -> Flash<Redirect> {
let logbook = Logbook::find_by_id(db, logbook_id).await; let logbook = Logbook::find_by_id(db, logbook_id).await;
if let Some(logbook) = logbook { if let Some(logbook) = logbook {
Log::create( Log::create(
db, db,
format!("User {} tries to delete log entry {logbook_id}", user.name), format!("User {} tries to delete log entry {logbook_id}", &user.name),
) )
.await; .await;
match logbook.delete(db, &user).await { match logbook.delete(db, &user).await {

View File

@ -3,22 +3,21 @@ use rocket::{
fairing::AdHoc, fairing::AdHoc,
form::Form, form::Form,
fs::FileServer, fs::FileServer,
get, post, get,
http::Cookie,
post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, Build, FromForm, Rocket, State, routes,
time::{Duration, OffsetDateTime},
Build, FromForm, Request, Rocket, State,
}; };
use rocket_dyn_templates::{tera::Context, Template}; use rocket_dyn_templates::Template;
use serde::Deserialize; use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context;
use crate::model::{ use crate::model::user::{User, UserWithRoles};
log::Log,
tripdetails::TripDetails,
triptype::TripType,
user::{User, UserWithRoles},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
};
pub(crate) mod admin; pub(crate) mod admin;
mod auth; mod auth;
@ -27,6 +26,7 @@ mod cox;
mod ergo; mod ergo;
mod log; mod log;
mod misc; mod misc;
mod planned;
mod stat; mod stat;
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
@ -35,6 +35,17 @@ struct LoginForm<'r> {
password: &'r str, password: &'r str,
} }
#[get("/")]
async fn index(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());
}
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
Template::render("index", context.into_json())
}
#[post("/", data = "<login>")] #[post("/", data = "<login>")]
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String { async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
match User::login(db, login.name, login.password).await { match User::login(db, login.name, login.password).await {
@ -43,164 +54,22 @@ async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String
} }
} }
#[get("/")] #[catch(401)] //Unauthorized
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template { fn unauthorized_error(req: &Request) -> Redirect {
let mut context = Context::new(); // Save the URL the user tried to access, to be able to go there once logged in
let mut redirect_cookie = Cookie::new("redirect_url", format!("{}", req.uri()));
println!("{}", req.uri());
redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1));
req.cookies().add_private(redirect_cookie);
if user.has_role(db, "cox").await || user.has_role(db, "admin").await {
let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes);
}
let days = user.get_days(db).await;
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
context.insert("days", &days);
Template::render("index", context.into_json())
}
#[get("/join/<trip_details_id>?<user_note>")]
async fn join(
db: &State<SqlitePool>,
trip_details_id: i64,
user: User,
user_note: Option<String>,
) -> Flash<Redirect> {
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
};
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;
Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!")
}
Err(UserTripError::EventAlreadyFull) => {
Flash::error(Redirect::to("/"), "Event bereits ausgebucht!")
}
Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/"), "Du nimmst bereits teil!")
}
Err(UserTripError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/"), "Du hilfst bereits als Steuerperson aus!")
}
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
),
Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error(
Redirect::to("/"),
"Bei dieser Ausfahrt können leider keine Gäste mitfahren.",
),
Err(UserTripError::NotAllowedToAddGuest) => Flash::error(
Redirect::to("/"),
"Du darfst keine Gäste hinzufügen.",
),
Err(UserTripError::DetailsLocked) => Flash::error(
Redirect::to("/"),
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
),
}
}
#[get("/remove/<trip_details_id>/<name>")]
async fn remove_guest(
db: &State<SqlitePool>,
trip_details_id: i64,
user: User,
name: String,
) -> Flash<Redirect> {
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, Some(name)).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/"), "Gast nicht angemeldet.")
}
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/"),
"Keine Berechtigung um den Gast zu entfernen.",
),
}
}
#[get("/remove/<trip_details_id>")]
async fn remove(db: &State<SqlitePool>, trip_details_id: i64, user: User) -> Flash<Redirect> {
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, None).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(_) => {
panic!("Not possible to be here");
}
}
}
#[catch(401)] //unauthorized
fn unauthorized_error() -> Redirect {
Redirect::to("/auth") Redirect::to("/auth")
} }
#[catch(403)] //forbidden
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.")
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct Config { pub struct Config {
@ -210,10 +79,11 @@ pub struct Config {
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
rocket rocket
.mount("/", routes![index, join, remove, remove_guest]) .mount("/", routes![index])
.mount("/auth", auth::routes()) .mount("/auth", auth::routes())
.mount("/wikiauth", routes![wikiauth]) .mount("/wikiauth", routes![wikiauth])
.mount("/log", log::routes()) .mount("/log", log::routes())
.mount("/planned", planned::routes())
.mount("/ergo", ergo::routes()) .mount("/ergo", ergo::routes())
.mount("/stat", stat::routes()) .mount("/stat", stat::routes())
.mount("/boatdamage", boatdamage::routes()) .mount("/boatdamage", boatdamage::routes())
@ -221,7 +91,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
.mount("/admin", admin::routes()) .mount("/admin", admin::routes())
.mount("/", misc::routes()) .mount("/", misc::routes())
.mount("/public", FileServer::from("static/")) .mount("/public", FileServer::from("static/"))
.register("/", catchers![unauthorized_error]) .register("/", catchers![unauthorized_error, forbidden_error])
.attach(Template::fairing()) .attach(Template::fairing())
.attach(AdHoc::config::<Config>()) .attach(AdHoc::config::<Config>())
} }
@ -255,7 +125,11 @@ mod test {
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert!(response.into_string().await.unwrap().contains("Ausfahrten")); assert!(response
.into_string()
.await
.unwrap()
.contains("Ruderassistent"));
} }
#[sqlx::test] #[sqlx::test]
@ -274,75 +148,6 @@ mod test {
assert_eq!(response.headers().get("Location").next(), Some("/auth")); assert_eq!(response.headers().get("Location").next(), Some("/auth"));
} }
#[sqlx::test]
fn test_join_and_remove() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/join/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!");
let req = client.get("/remove/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!");
}
#[sqlx::test]
fn test_join_invalid_event() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/join/9999");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist.");
}
#[sqlx::test] #[sqlx::test]
fn test_public() { fn test_public() {
let db = testdb!(); let db = testdb!();

272
src/tera/planned.rs Normal file
View File

@ -0,0 +1,272 @@
use rocket::{
get,
request::FlashMessage,
response::{Flash, Redirect},
routes, Route, State,
};
use rocket_dyn_templates::Template;
use sqlx::SqlitePool;
use tera::Context;
use crate::model::{
log::Log,
tripdetails::TripDetails,
triptype::TripType,
user::{AllowedForPlannedTripsUser, User, UserWithRoles},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
};
#[get("/")]
async fn index(
db: &State<SqlitePool>,
user: AllowedForPlannedTripsUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let user: User = user.into();
let mut context = Context::new();
if user.has_role(db, "cox").await || user.has_role(db, "planned_event").await {
let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes);
}
let days = user.get_days(db).await;
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("fee", &user.fee(db).await);
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
context.insert("days", &days);
Template::render("planned", context.into_json())
}
#[get("/join/<trip_details_id>?<user_note>")]
async fn join(
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
user_note: Option<String>,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
};
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;
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!")
}
Err(UserTripError::EventAlreadyFull) => {
Flash::error(Redirect::to("/planned"), "Event bereits ausgebucht!")
}
Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!")
}
Err(UserTripError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!")
}
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/planned"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
),
Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error(
Redirect::to("/planned"),
"Bei dieser Ausfahrt können leider keine Gäste mitfahren.",
),
Err(UserTripError::NotAllowedToAddGuest) => Flash::error(
Redirect::to("/planned"),
"Du darfst keine Gäste hinzufügen.",
),
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.",
),
}
}
#[get("/remove/<trip_details_id>/<name>")]
async fn remove_guest(
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
name: String,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, Some(name)).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.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.")
}
Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
}
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/planned"),
"Keine Berechtigung um den Gast zu entfernen.",
),
}
}
#[get("/remove/<trip_details_id>")]
async fn remove(
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, None).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.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.")
}
Err(_) => {
panic!("Not possible to be here");
}
}
}
pub fn routes() -> Vec<Route> {
routes![index, join, remove, remove_guest]
}
#[cfg(test)]
mod test {
use rocket::{
http::{ContentType, Status},
local::asynchronous::Client,
};
use sqlx::SqlitePool;
use crate::testdb;
#[sqlx::test]
fn test_join_and_remove() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/planned/join/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!");
let req = client.get("/planned/remove/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!");
}
#[sqlx::test]
fn test_join_invalid_event() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/planned/join/9999");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist.");
}
}

View File

@ -4,19 +4,19 @@ use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
stat::{self, Stat}, stat::{self, Stat},
user::{NonGuestUser, UserWithRoles}, user::{DonauLinzUser, UserWithRoles},
}; };
use super::log::KioskCookie; use super::log::KioskCookie;
#[get("/boats?<year>", rank = 2)] #[get("/boats?<year>", rank = 2)]
async fn index_boat(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template { async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
let stat = Stat::boats(db, year).await; let stat = Stat::boats(db, year).await;
let kiosk = false; let kiosk = false;
Template::render( Template::render(
"stat.boats", "stat.boats",
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, kiosk), context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, kiosk),
) )
} }
@ -33,15 +33,15 @@ async fn index_boat_kiosk(
} }
#[get("/?<year>", rank = 2)] #[get("/?<year>", rank = 2)]
async fn index(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template { async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await; let stat = Stat::people(db, year).await;
let guest_km = Stat::guest(db, year).await; let guest_km = Stat::guest(db, year).await;
let personal = stat::get_personal(db, &user.user).await; let personal = stat::get_personal(db, &user).await;
let kiosk = false; let kiosk = false;
Template::render( Template::render(
"stat.people", "stat.people",
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, personal, kiosk, guest_km), context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km),
) )
} }

1
svelte/.gitignore vendored
View File

@ -1,6 +1,5 @@
.DS_Store .DS_Store
node_modules node_modules
/build
/.svelte-kit /.svelte-kit
/package /package
.env .env

View File

@ -0,0 +1,47 @@
{% import "includes/macros" as macros %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
{% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
{% endif %}
<h1 class="h1">Gebühren</h1>
<!-- START filterBar -->
<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 Name"/>
</div>
<!-- END filterBar -->
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
<div id="filter-result-js" class="text-primary-950 dark:text-white text-right"></div>
{% for fee in fees | sort(attribute="name") %}
<div {% if fee.paid %} style="background-color: green;" {% endif %} data-filterable="true" data-filter="{{ fee.name }} {% if fee.paid %} has-already-paid {% else %} has-not-paid {% endif %}" class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
<div class="grid sm:grid-cols-1 gap-3">
<div style="width: 100%" class="col-span-2">
<b>{{ fee.name }}</b>
</div>
<div style="width: 100%">
{{ fee.sum_in_cents / 100 }}€:
</div>
<div style="width: 100%">
{% for p in fee.parts %}
{{ p.0 }} ({{ p.1 / 100 }}€) {% if not loop.last %} + {% endif %}
{% endfor %}
</div>
{% if "admin" in loggedin_user.roles %}
<a href="/admin/user/fees/paid?{{ fee.user_ids }}">Zahlungsstatus ändern</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock content %}

View File

@ -10,6 +10,7 @@
<h1 class="h1">Users</h1> <h1 class="h1">Users</h1>
{% if allowed_to_edit %}
<form action="/admin/user/new" method="post" class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between"> <form action="/admin/user/new" method="post" class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between">
<div class="w-full"> <div class="w-full">
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neuen User hinzufügen</h2> <h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neuen User hinzufügen</h2>
@ -24,6 +25,7 @@
<input value="Hinzufügen" type="submit" class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 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"/> <input value="Hinzufügen" type="submit" class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 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"/>
</div> </div>
</form> </form>
{% endif %}
<!-- START filterBar --> <!-- START filterBar -->
<div class="search-wrapper"> <div class="search-wrapper">
@ -60,20 +62,24 @@
</div> </div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3"> <div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{% for role in roles %} {% for role in roles %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles) }} {{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
{% endfor%} {% endfor%}
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob) }} {{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight) }} {{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex) }} {{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date) }} {{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate) }} {{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail) }} {{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname) }} {{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes) }} {{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone) }} {{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address) }} {{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address, readonly=allowed_to_edit == false) }}
{% if allowed_to_edit %}
{{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }}
{% endif %}
</div> </div>
</div> </div>
{% if allowed_to_edit %}
<div class="mt-3 text-right"> <div class="mt-3 text-right">
<a href="/admin/user/{{ user.id }}/delete" class="w-28 btn btn-alert" onclick="return confirm('Wirklich löschen?');"> <a href="/admin/user/{{ user.id }}/delete" class="w-28 btn btn-alert" onclick="return confirm('Wirklich löschen?');">
{% include "includes/delete-icon" %} {% include "includes/delete-icon" %}
@ -81,6 +87,7 @@
</a> </a>
<input value="Ändern" type="submit" class="w-28 btn btn-primary ml-1"/> <input value="Ändern" type="submit" class="w-28 btn btn-primary ml-1"/>
</div> </div>
{% endif %}
</form> </form>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -8,9 +8,5 @@
{% include "includes/funnel-icon" %} {% include "includes/funnel-icon" %}
Steuerleute gesucht Steuerleute gesucht
</button> </button>
<button type="button" title="Toggle View" class="group btn btn-primary filter-trips-js" data-action="filter-months" id="filtermonth-js" aria-pressed="false" data-month="{{ now() | date(format='%m') }}">
{% include "includes/funnel-icon" %}
Aktuellen Monat anzeigen
</button>
</div> </div>
{% endif %} {% endif %}

View File

@ -1,12 +1,8 @@
{# Shows a fancy, optional lists of boats. They are grouped by boat category. {# Shows a fancy, optional lists of boats. They are grouped by boat category.
Inputs: boats Inputs: boats
Parameters: only_ones: if set, only 1x boats are shown
#} #}
{% macro show_boats(only_ones) %} {% macro show_boats() %}
{% if only_ones %}
{% set_global boats = boats | filter(attribute="amount_seats", value=1) %}
{% endif %}
{% for amount_seats, grouped_boats in boats | group_by(attribute="amount_seats") %} {% for amount_seats, grouped_boats in boats | group_by(attribute="amount_seats") %}
<div class="pb-2"> <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"> <div class="bg-gray-100 dark:bg-primary-600 text-primary-950 dark:text-white text-center text-sm mb-2">
@ -27,20 +23,16 @@
{% endmacro show_boats %} {% endmacro show_boats %}
{# Shows the form for creating a new logbook entry. #} {# Shows the form for creating a new logbook entry. #}
{% macro new(only_ones, shipmaster) %} {% macro new(shipmaster) %}
<form action="/log" method="post" id="form" class="grid grid-cols-4 gap-3" onsubmit="Array.from(this.elements).forEach(e=>!e.value.trim()&&(e.disabled=true));"> <form action="/log" method="post" id="form" class="grid grid-cols-4 gap-3" onsubmit="Array.from(this.elements).forEach(e=>!e.value.trim()&&(e.disabled=true));">
{{ log::boat_select(only_ones=only_ones) }} {{ log::boat_select() }}
{% if not only_ones %}
<div class="col-span-4 md:col-span-1"> <div class="col-span-4 md:col-span-1">
<div class="text-sm text-gray-600 dark:text-gray-100">Bootssteuerung</div> <div class="text-sm text-gray-600 dark:text-gray-100">Bootssteuerung</div>
<div class="h-10 flex items-center"> <div class="h-10 flex items-center">
{{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', disabled=true) }} {{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', disabled=true) }}
</div> </div>
</div> </div>
{% endif %}
{% if not only_ones %}
{{ log::rower_select(id="newrower", selected=[], class="col-span-4", init=true) }} {{ log::rower_select(id="newrower", selected=[], class="col-span-4", init=true) }}
{% endif %}
{{ macros::select(label="Schiffsführer", data=[], name='shipmaster', id="shipmaster-newrowerjs", wrapper_class="col-span-2") }} {{ macros::select(label="Schiffsführer", data=[], name='shipmaster', id="shipmaster-newrowerjs", wrapper_class="col-span-2") }}
{{ macros::select(label="Steuerperson", data=[], name='steering_person', id="steering_person-newrowerjs", wrapper_class="col-span-2") }} {{ macros::select(label="Steuerperson", data=[], name='steering_person', id="steering_person-newrowerjs", wrapper_class="col-span-2") }}
{{ macros::input(label='Abfahrtszeit', name='departure', type='datetime-local', required=true, wrapper_class='col-span-2') }} {{ macros::input(label='Abfahrtszeit', name='departure', type='datetime-local', required=true, wrapper_class='col-span-2') }}
@ -64,13 +56,8 @@
{% endmacro new %} {% endmacro new %}
{% macro boat_select(only_ones, id="boat_id") %} {% macro boat_select(id="boat_id") %}
{% if not only_ones %}
{{ 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", 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) }}
{% else %}
{% set ones = boats | filter(attribute="amount_seats", value=1) %}
{{ macros::select(label="Boot", data=ones, 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) }}
{% endif %}
{% endmacro boat_select %} {% endmacro boat_select %}
{% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %} {% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %}
@ -85,7 +72,7 @@
{% set_global sel = true %} {% set_global sel = true %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<option value="{{ user.id }}" {% if sel %} selected {% endif %} {% if user.on_water %} disabled="disabled" {% endif %} data-custom-properties='{"is_cox": {{ "cox" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat}}}'> <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}}}'>
{{user.name}} {{user.name}}
{% if user.on_water %} {% if user.on_water %}
(am Wasser) (am Wasser)
@ -97,7 +84,7 @@
{#{% endif %}#} {#{% endif %}#}
{% endmacro rower_select %} {% endmacro rower_select %}
{% macro show(log, state, allowed_to_close=false, only_ones) %} {% macro show(log, state, allowed_to_close=false) %}
<div class="grid grid-cols-1 gap-3 mb-3 w-full"> <div class="grid grid-cols-1 gap-3 mb-3 w-full">
<div class="pt-2 px-3 {% if not loop.first %} border-t {% endif %}"> <div class="pt-2 px-3 {% if not loop.first %} border-t {% endif %}">
<div class="w-full"> <div class="w-full">
@ -123,7 +110,7 @@
<div class="hidden"> <div class="hidden">
{% if allowed_to_close and state == "on_water" %} {% if allowed_to_close and state == "on_water" %}
<div id="close{{ log.id }}"> <div id="close{{ log.id }}">
{{ log::home(log=log, only_ones=only_ones) }} {{ log::home(log=log) }}
</div> </div>
<div> <div>
LÖSCHEN LÖSCHEN
@ -155,7 +142,7 @@
</div> </div>
{% endmacro show %} {% endmacro show %}
{% macro show_old(log, state, allowed_to_close=false, only_ones, index) %} {% macro show_old(log, state, allowed_to_close=false, index) %}
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative" data-filterable="true" data-filter="{{ log.boat.name }} {% for rower in log.rowers %} {{ rower.name }} {% endfor %}"> <div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative" data-filterable="true" data-filter="{{ log.boat.name }} {% for rower in log.rowers %} {{ rower.name }} {% endfor %}">
{% if log.logtype %} {% if log.logtype %}
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold"> <div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
@ -183,7 +170,7 @@
{% set amount_rowers = log.rowers | length %} {% set amount_rowers = log.rowers | length %}
{% set amount_guests = log.boat.amount_seats - amount_rowers %} {% set amount_guests = log.boat.amount_seats - amount_rowers %}
{% if allowed_to_close and state == "on_water" %} {% if allowed_to_close and state == "on_water" %}
{{ log::home(log=log, only_ones=only_ones) }} {{ log::home(log=log) }}
{% else %} {% else %}
<div class="text-black dark:text-white"> <div class="text-black dark:text-white">
{{ log.destination }} {{ log.destination }}
@ -211,7 +198,7 @@
</div> </div>
{% endmacro show_old %} {% endmacro show_old %}
{% macro home(log, only_ones) %} {% macro home(log) %}
<form class="grid grid-cols-1 gap-3" action="/log/{{log.id}}" method="post"> <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") }} {{ macros::input(label='Ankunftszeit', name='arrival', type='datetime-local', required=true, class="change-id-js rounded-md current-date-time") }}

View File

@ -4,7 +4,11 @@
> >
<div class="max-w-screen-xl w-full flex justify-between items-center"> <div class="max-w-screen-xl w-full flex justify-between items-center">
<div class="w-1/3 truncate"> <div class="w-1/3 truncate">
<a href="/"> {% if "Donau Linz" in loggedin_user.roles %}
<a href="/planned">
{% else %}
<a href="/">
{% endif %}
{{ loggedin_user.name }} {{ loggedin_user.name }}
</a> </a>
@ -48,7 +52,7 @@
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" 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-sidebar="true"
data-trigger="sidebar" data-trigger="sidebar"
data-header="Logbuch" data-header="Menü"
data-body="#mobile-menu" data-body="#mobile-menu"
> >
{% include "includes/book" %} {% include "includes/book" %}
@ -150,10 +154,10 @@
<div class="h-8"></div> <div class="h-8"></div>
{% endmacro header %} {% endmacro header %}
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='') %} {% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false) %}
<div class="{{wrapper_class}}"> <div class="{{wrapper_class}}">
<label for="{{ name }}" class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">{{ label }}</label> <label for="{{ name }}" class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">{{ label }}</label>
<input {% if type=='datetime-local' %} onclick='if (!this.value) setCurrentdate(this)' {% endif %}{% if id %} id="{{ id }}" {% else %} id="{{ name }}" {% endif %} name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{% if hide_label %}{{ label }}{% endif %}" {% if min is defined %} min="{{ min }}" {% endif %} {% if autofocus %} autofocus {% endif %}{% if pattern %}pattern="{{ pattern }}"{% endif %}> <input {% if type=='datetime-local' %} onclick='if (!this.value) setCurrentdate(this)' {% endif %}{% if id %} id="{{ id }}" {% else %} id="{{ name }}" {% endif %} name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{% if hide_label %}{{ label }}{% endif %}" {% if min is defined %} min="{{ min }}" {% endif %} {% if autofocus %} autofocus {% endif %}{% if pattern %}pattern="{{ pattern }}"{% endif %}{% if readonly %}readonly{% endif %}>
</div> </div>
{% endmacro input %} {% endmacro input %}
@ -164,7 +168,7 @@
</label> </label>
{% endmacro checkbox %} {% endmacro checkbox %}
{% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false) %} {% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false, new_last_entry='') %}
<div class="{{wrapper_class}}"> <div class="{{wrapper_class}}">
<label for="{{ name }}" class="text-sm text-gray-600 dark:text-gray-100">{{ label }}</label> <label for="{{ name }}" class="text-sm text-gray-600 dark:text-gray-100">{{ label }}</label>
{% if display == '' %} {% if display == '' %}
@ -175,7 +179,7 @@
<option selected value>{{ default }}</option> <option selected value>{{ default }}</option>
{% endif %} {% endif %}
{% for d in data %} {% 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"] }}"}'{% 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 }}}'{% endif %}>
{% for displa in display -%} {% for displa in display -%}
{%- if d[displa] -%} {%- if d[displa] -%}
{{- d[displa] -}} {{- d[displa] -}}
@ -185,11 +189,13 @@
{%- endfor %} {%- endfor %}
</option> </option>
{% endfor %} {% endfor %}
{% if new_last_entry %}
<option value="-1">{{ new_last_entry }}</option>
{% endif %}
</select> </select>
</div> </div>
{% endmacro select %} {% endmacro select %}
{% macro alert(message, type, class='') %} {% macro alert(message, type, class='') %}
<div class="{{ class }} alert-{{ type }} text-white px-3 py-1 rounded-md text-center"> <div class="{{ class }} alert-{{ type }} text-white px-3 py-1 rounded-md text-center">
{{ message }} {{ message }}
@ -209,7 +215,7 @@
{% if rower.is_real_guest %} {% if rower.is_real_guest %}
<small class="text-gray-600 dark:text-gray-100">(Gast)</small> <small class="text-gray-600 dark:text-gray-100">(Gast)</small>
{% if allow_removing %} {% if allow_removing %}
<a href="/remove/{{ trip_details_id }}/{{ rower.name }}" class="btn btn-attention btn-fw">Abmelden</a> <a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<span class="hidden">(angemeldet seit <span class="hidden">(angemeldet seit

View File

@ -0,0 +1,4 @@
<script>var QRCodep;!function(){function a(a){this.mode=j.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=0,c=this.data.length;c>b;b++){var d=[],e=this.data.charCodeAt(b);e>65536?(d[0]=240|(1835008&e)>>>18,d[1]=128|(258048&e)>>>12,d[2]=128|(4032&e)>>>6,d[3]=128|63&e):e>2048?(d[0]=224|(61440&e)>>>12,d[1]=128|(4032&e)>>>6,d[2]=128|63&e):e>128?(d[0]=192|(1984&e)>>>6,d[1]=128|63&e):d[0]=e,this.parsedData.push(d)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function c(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function d(a,b){this.totalCount=a,this.dataCount=b}function e(){this.buffer=[],this.length=0}function f(){return"undefined"!=typeof CanvasRenderingContext2D}function g(){var a=!1,b=navigator.userAgent;if(/android/i.test(b)){a=!0;var c=b.toString().match(/android ([0-9]\.[0-9])/i);c&&c[1]&&(a=parseFloat(c[1]))}return a}function h(a,b){for(var c=1,d=i(a),e=0,f=p.length;f>=e;e++){var g=0;switch(b){case k.L:g=p[e][0];break;case k.M:g=p[e][1];break;case k.Q:g=p[e][2];break;case k.H:g=p[e][3]}if(g>=d)break;c++}if(c>p.length)throw new Error("Too long data");return c}function i(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(a){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?this.modules[a+c][b+d]=!0:this.modules[a+c][b+d]=!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=m.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=a%2==0);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=b%2==0)},setupPositionAdjustPattern:function(){for(var a=m.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var f=-2;2>=f;f++)for(var g=-2;2>=g;g++)-2==f||2==f||-2==g||2==g||0==f&&0==g?this.modules[d+f][e+g]=!0:this.modules[d+f][e+g]=!1}},setupTypeNumber:function(a){for(var b=m.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=m.getBCHTypeInfo(c),e=0;15>e;e++){var f=!a&&1==(d>>e&1);6>e?this.modules[e][8]=f:8>e?this.modules[e+1][8]=f:this.modules[this.moduleCount-15+e][8]=f}for(var e=0;15>e;e++){var f=!a&&1==(d>>e&1);8>e?this.modules[8][this.moduleCount-e-1]=f:9>e?this.modules[8][15-e-1+1]=f:this.modules[8][15-e-1]=f}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,f=0,g=this.moduleCount-1;g>0;g-=2)for(6==g&&g--;;){for(var h=0;2>h;h++)if(null==this.modules[d][g-h]){var i=!1;f<a.length&&(i=1==(a[f]>>>e&1));var j=m.getMask(b,d,g-h);j&&(i=!i),this.modules[d][g-h]=i,e--,-1==e&&(f++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,f){for(var g=d.getRSBlocks(a,c),h=new e,i=0;i<f.length;i++){var j=f[i];h.put(j.mode,4),h.put(j.getLength(),m.getLengthInBits(j.mode,a)),j.write(h)}for(var k=0,i=0;i<g.length;i++)k+=g[i].dataCount;if(h.getLengthInBits()>8*k)throw new Error("code length overflow. ("+h.getLengthInBits()+">"+8*k+")");for(h.getLengthInBits()+4<=8*k&&h.put(0,4);h.getLengthInBits()%8!=0;)h.putBit(!1);for(;;){if(h.getLengthInBits()>=8*k)break;if(h.put(b.PAD0,8),h.getLengthInBits()>=8*k)break;h.put(b.PAD1,8)}return b.createBytes(h,g)},b.createBytes=function(a,b){for(var d=0,e=0,f=0,g=new Array(b.length),h=new Array(b.length),i=0;i<b.length;i++){var j=b[i].dataCount,k=b[i].totalCount-j;e=Math.max(e,j),f=Math.max(f,k),g[i]=new Array(j);for(var l=0;l<g[i].length;l++)g[i][l]=255&a.buffer[l+d];d+=j;var n=m.getErrorCorrectPolynomial(k),o=new c(g[i],n.getLength()-1),p=o.mod(n);h[i]=new Array(n.getLength()-1);for(var l=0;l<h[i].length;l++){var q=l+p.getLength()-h[i].length;h[i][l]=q>=0?p.get(q):0}}for(var r=0,l=0;l<b.length;l++)r+=b[l].totalCount;for(var s=new Array(r),t=0,l=0;e>l;l++)for(var i=0;i<b.length;i++)l<g[i].length&&(s[t++]=g[i][l]);for(var l=0;f>l;l++)for(var i=0;i<b.length;i++)l<h[i].length&&(s[t++]=h[i][l]);return s};for(var j={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},k={L:1,M:0,Q:3,H:2},l={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},m={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;m.getBCHDigit(b)-m.getBCHDigit(m.G15)>=0;)b^=m.G15<<m.getBCHDigit(b)-m.getBCHDigit(m.G15);return(a<<10|b)^m.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;m.getBCHDigit(b)-m.getBCHDigit(m.G18)>=0;)b^=m.G18<<m.getBCHDigit(b)-m.getBCHDigit(m.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return m.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case l.PATTERN000:return(b+c)%2==0;case l.PATTERN001:return b%2==0;case l.PATTERN010:return c%3==0;case l.PATTERN011:return(b+c)%3==0;case l.PATTERN100:return(Math.floor(b/2)+Math.floor(c/3))%2==0;case l.PATTERN101:return b*c%2+b*c%3==0;case l.PATTERN110:return(b*c%2+b*c%3)%2==0;case l.PATTERN111:return(b*c%3+(b+c)%2)%2==0;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new c([1],0),d=0;a>d;d++)b=b.multiply(new c([1,n.gexp(d)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case j.MODE_NUMBER:return 10;case j.MODE_ALPHA_NUM:return 9;case j.MODE_8BIT_BYTE:return 8;case j.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case j.MODE_NUMBER:return 12;case j.MODE_ALPHA_NUM:return 11;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case j.MODE_NUMBER:return 14;case j.MODE_ALPHA_NUM:return 13;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||0==h&&0==i||g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,0!=j&&4!=j||(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},n={glog:function(a){if(1>a)throw new Error("glog("+a+")");return n.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return n.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},o=0;8>o;o++)n.EXP_TABLE[o]=1<<o;for(var o=8;256>o;o++)n.EXP_TABLE[o]=n.EXP_TABLE[o-4]^n.EXP_TABLE[o-5]^n.EXP_TABLE[o-6]^n.EXP_TABLE[o-8];for(var o=0;255>o;o++)n.LOG_TABLE[n.EXP_TABLE[o]]=o;c.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var e=0;e<a.getLength();e++)b[d+e]^=n.gexp(n.glog(this.get(d))+n.glog(a.get(e)));return new c(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=n.glog(this.get(0))-n.glog(a.get(0)),d=new Array(this.getLength()),e=0;e<this.getLength();e++)d[e]=this.get(e);for(var e=0;e<a.getLength();e++)d[e]^=n.gexp(n.glog(a.get(e))+b);return new c(d,0).mod(a)}},d.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],d.getRSBlocks=function(a,b){var c=d.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var e=c.length/3,f=[],g=0;e>g;g++)for(var h=c[3*g+0],i=c[3*g+1],j=c[3*g+2],k=0;h>k;k++)f.push(new d(i,j));return f},d.getRsBlockTable=function(a,b){switch(b){case k.L:return d.RS_BLOCK_TABLE[4*(a-1)+0];case k.M:return d.RS_BLOCK_TABLE[4*(a-1)+1];case k.Q:return d.RS_BLOCK_TABLE[4*(a-1)+2];case k.H:return d.RS_BLOCK_TABLE[4*(a-1)+3];default:return}},e.prototype={get:function(a){var b=Math.floor(a/8);return 1==(this.buffer[b]>>>7-a%8&1)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(a>>>b-c-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var p=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],q=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function b(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var c=this._htOption,d=this._el,e=a.getModuleCount();Math.floor(c.width/e),Math.floor(c.height/e);this.clear();var f=b("svg",{viewBox:"0 0 "+String(e)+" "+String(e),width:"100%",height:"100%",fill:c.colorLight});f.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),d.appendChild(f),f.appendChild(b("rect",{fill:c.colorLight,width:"100%",height:"100%"})),f.appendChild(b("rect",{fill:c.colorDark,width:"1",height:"1",id:"template"}));for(var g=0;e>g;g++)for(var h=0;e>h;h++)if(a.isDark(g,h)){var i=b("use",{x:String(h),y:String(g)});i.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),f.appendChild(i)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),r="svg"===document.documentElement.tagName.toLowerCase(),s=r?q:f()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function b(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&c._fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,void(d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var c=1/window.devicePixelRatio,d=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,b,e,f,g,h,i,j,k){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*c;else"undefined"==typeof j&&(arguments[1]*=c,arguments[2]*=c,arguments[3]*=c,arguments[4]*=c);d.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=g(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.alt="Scan me!",this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&b.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCodep=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:k.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._htOption.useSVG&&(s=q),this._android=g(),this._el=a,this._oQRCode=null,this._htOption.text&&this.makeCode(this._htOption.text)},QRCodep.prototype.makeCode=function(a){this._oQRCode=new b(h(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,-1!=this._htOption.dpi&&-1!=this._htOption.mmPerDot&&(this._htOption.width=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot,this._htOption.height=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot),this._oDrawing=new s(this._el,this._htOption),this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCodep.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCodep.prototype.clear=function(){this._oDrawing.clear()},QRCodep.CorrectLevel=k}();var sepaQR;!function(){"use strict";var a={UTF_8:1,ISO8859_1:2,ISO8859_2:3,ISO8859_4:4,ISO8859_5:5,ISO8859_7:6,ISO8859_10:7,ISO8859_15:8};sepaQR=function(b){if(this._sOpt={serviceTag:"BCD",version:"001",charset:a.UTF_8,identificationCode:"SCT",benefBIC:"",benefName:"",benefAccNr:"",amountEuro:"",purpose:"",creditorRef:"",remittanceInf:"",information:""},b)for(var c in b)this._sOpt[c]=b[c]},sepaQR.prototype.validServiceTag=function(){return"BCD"===this._sOpt.serviceTag},sepaQR.prototype.validVersion=function(){return"001"===this._sOpt.version||"002"===this._sOpt.version},sepaQR.prototype.validCharset=function(){return this._sOpt.charset>0&&this._sOpt.charset<=8},sepaQR.prototype.validIdentificationCode=function(){return"SCT"===this._sOpt.identificationCode},sepaQR.prototype.validBenefName=function(){var a="string"==typeof this._sOpt.benefName&&this._sOpt.benefName.length>=1&&this._sOpt.benefName.length<=70;if(!a)throw new Error("benefName not valid!");return a},sepaQR.prototype.validBenefAccNr=function(){var a="string"==typeof this._sOpt.benefAccNr&&this._sOpt.benefAccNr.length>=1&&this._sOpt.benefAccNr.length<=34;if(!a)throw new Error("benefAccNr not valid!");return a},sepaQR.prototype.validAmountEuro=function(){if("string"==typeof this._sOpt.amountEuro)return 0===this._sOpt.amountEuro.length;if("number"==typeof this._sOpt.amountEuro){this._sOpt.amountEuro=Math.round(100*this._sOpt.amountEuro)/100;var a=this._sOpt.amountEuro>.01&&this._sOpt.amountEuro<=999999999.99;if(!a)throw new Error("Amount not valid!");return a}},sepaQR.prototype.validBenefBic=function(){var a="002"==this._sOpt.version||"string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11;if(!a)throw new Error("BIC is mandatory in Version 001!");if(a="string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11,!a)throw new Error("benefBIC not valid!");return a},sepaQR.prototype.validPurpose=function(){var a="string"==typeof this._sOpt.purpose&&this._sOpt.purpose.length>=0&&this._sOpt.purpose.length<=4;if(!a)throw new Error("Purpose not valid!");return a},sepaQR.prototype.validInformation=function(){var a="string"==typeof this._sOpt.information&&this._sOpt.information.length>=0&&this._sOpt.information.length<=70;if(!a)throw new Error("Information not valid!");return a},sepaQR.prototype.validCreditorRefOrRemittance=function(){var a="string"==typeof this._sOpt.creditorRef&&0===this._sOpt.creditorRef.length,b="string"==typeof this._sOpt.remittanceInf&&0===this._sOpt.remittanceInf.length,c=a&&"string"==typeof this._sOpt.remittanceInf&&this._sOpt.remittanceInf.length<=140||b&&"string"==typeof this._sOpt.creditorRef&&this._sOpt.creditorRef.length<=35;if(!c)throw new Error("creditorRef or Remittance not valid!");return c},sepaQR.prototype.validQRTextLength=function(){for(var a=this.prepareQRText(),b=0,c=0,d=a.length;d>c;c++){var e=a.charCodeAt(c);b+=e>65536?4:e>2048?3:e>128?2:1}return 328>=b},sepaQR.prototype.valid=function(){var a=this.validServiceTag()&&this.validVersion()&&this.validCharset()&&this.validIdentificationCode()&&this.validBenefName()&&this.validBenefAccNr()&&this.validAmountEuro()&&this.validBenefBic()&&this.validPurpose()&&this.validInformation()&&this.validPurpose()&&this.validCreditorRefOrRemittance()&&this.validQRTextLength();return a},sepaQR.prototype.prepareQRText=function(){return(this._sOpt.serviceTag+"\n"+this._sOpt.version+"\n"+this._sOpt.charset+"\n"+this._sOpt.identificationCode+"\n"+this._sOpt.benefBIC+"\n"+this._sOpt.benefName+"\n"+this._sOpt.benefAccNr+"\nEUR"+this._sOpt.amountEuro+"\n"+this._sOpt.purpose+"\n"+this._sOpt.creditorRef+"\n"+this._sOpt.remittanceInf+"\n"+this._sOpt.information).trim()},sepaQR.prototype.toQRText=function(){return this.valid()?this.prepareQRText():""},sepaQR.prototype.makeCodeInto=function(a,b){var c={width:256,height:256,mmPerDot:.85,dpi:92,correctLevel:QRCodep.CorrectLevel.M,text:this.toQRText()};if(0!==c.text.length){if(b)for(var d in b)c[d]=b[d];return this.qrcode=new QRCodep(a,c),this.drawExplanatoryLink(document.getElementById(a).getElementsByTagName("canvas")[0],document.createElement("canvas")),this.qrcode}},sepaQR.prototype.drawExplanatoryLink=function(a,b){var c=3,d=12,e=8,f=6,g=a;b.width=g.width,b.height=g.height,b.getContext("2d").drawImage(g,0,0),g.width=b.width+2*(e+c+f),g.height=b.height+2*(e+c+f),CanvasRenderingContext2D.prototype.roundRect=function(a,b,c,d,e,f){return 2*e>c&&(e=c/2),2*e>d&&(e=d/2),this.beginPath(),this.moveTo(a+e,b),this.arcTo(a+c,b,a+c,b+d,e),this.arcTo(a+c,b+d,a,b+d,e),this.lineTo(a+c-f,b+d),this.moveTo(a+c-110,b+d),this.arcTo(a,b+d,a,b,e),this.arcTo(a,b,a+c,b,e),this};var h=g.getContext("2d");h.fillStyle="white",h.rect(0,0,g.width,g.height),h.fill(),h.drawImage(b,e+c+f,e+c+f),h.lineWidth=c,h.roundRect(f+c/2,f+c/2,g.width-c-2*f,g.height-c-2*f,d,3*e).stroke(),h.fillStyle="black",h.font=4.5*c+"px Arial",h.fillText("sepaQR.eu",g.width-110,g.height-f/2)},sepaQR.Charset=a}();</script>
<script>var QRCodep;!function(){function a(a){this.mode=j.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=0,c=this.data.length;c>b;b++){var d=[],e=this.data.charCodeAt(b);e>65536?(d[0]=240|(1835008&e)>>>18,d[1]=128|(258048&e)>>>12,d[2]=128|(4032&e)>>>6,d[3]=128|63&e):e>2048?(d[0]=224|(61440&e)>>>12,d[1]=128|(4032&e)>>>6,d[2]=128|63&e):e>128?(d[0]=192|(1984&e)>>>6,d[1]=128|63&e):d[0]=e,this.parsedData.push(d)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function c(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function d(a,b){this.totalCount=a,this.dataCount=b}function e(){this.buffer=[],this.length=0}function f(){return"undefined"!=typeof CanvasRenderingContext2D}function g(){var a=!1,b=navigator.userAgent;if(/android/i.test(b)){a=!0;var c=b.toString().match(/android ([0-9]\.[0-9])/i);c&&c[1]&&(a=parseFloat(c[1]))}return a}function h(a,b){for(var c=1,d=i(a),e=0,f=p.length;f>=e;e++){var g=0;switch(b){case k.L:g=p[e][0];break;case k.M:g=p[e][1];break;case k.Q:g=p[e][2];break;case k.H:g=p[e][3]}if(g>=d)break;c++}if(c>p.length)throw new Error("Too long data");return c}function i(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(a){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?this.modules[a+c][b+d]=!0:this.modules[a+c][b+d]=!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=m.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=a%2==0);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=b%2==0)},setupPositionAdjustPattern:function(){for(var a=m.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var f=-2;2>=f;f++)for(var g=-2;2>=g;g++)-2==f||2==f||-2==g||2==g||0==f&&0==g?this.modules[d+f][e+g]=!0:this.modules[d+f][e+g]=!1}},setupTypeNumber:function(a){for(var b=m.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=m.getBCHTypeInfo(c),e=0;15>e;e++){var f=!a&&1==(d>>e&1);6>e?this.modules[e][8]=f:8>e?this.modules[e+1][8]=f:this.modules[this.moduleCount-15+e][8]=f}for(var e=0;15>e;e++){var f=!a&&1==(d>>e&1);8>e?this.modules[8][this.moduleCount-e-1]=f:9>e?this.modules[8][15-e-1+1]=f:this.modules[8][15-e-1]=f}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,f=0,g=this.moduleCount-1;g>0;g-=2)for(6==g&&g--;;){for(var h=0;2>h;h++)if(null==this.modules[d][g-h]){var i=!1;f<a.length&&(i=1==(a[f]>>>e&1));var j=m.getMask(b,d,g-h);j&&(i=!i),this.modules[d][g-h]=i,e--,-1==e&&(f++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,f){for(var g=d.getRSBlocks(a,c),h=new e,i=0;i<f.length;i++){var j=f[i];h.put(j.mode,4),h.put(j.getLength(),m.getLengthInBits(j.mode,a)),j.write(h)}for(var k=0,i=0;i<g.length;i++)k+=g[i].dataCount;if(h.getLengthInBits()>8*k)throw new Error("code length overflow. ("+h.getLengthInBits()+">"+8*k+")");for(h.getLengthInBits()+4<=8*k&&h.put(0,4);h.getLengthInBits()%8!=0;)h.putBit(!1);for(;;){if(h.getLengthInBits()>=8*k)break;if(h.put(b.PAD0,8),h.getLengthInBits()>=8*k)break;h.put(b.PAD1,8)}return b.createBytes(h,g)},b.createBytes=function(a,b){for(var d=0,e=0,f=0,g=new Array(b.length),h=new Array(b.length),i=0;i<b.length;i++){var j=b[i].dataCount,k=b[i].totalCount-j;e=Math.max(e,j),f=Math.max(f,k),g[i]=new Array(j);for(var l=0;l<g[i].length;l++)g[i][l]=255&a.buffer[l+d];d+=j;var n=m.getErrorCorrectPolynomial(k),o=new c(g[i],n.getLength()-1),p=o.mod(n);h[i]=new Array(n.getLength()-1);for(var l=0;l<h[i].length;l++){var q=l+p.getLength()-h[i].length;h[i][l]=q>=0?p.get(q):0}}for(var r=0,l=0;l<b.length;l++)r+=b[l].totalCount;for(var s=new Array(r),t=0,l=0;e>l;l++)for(var i=0;i<b.length;i++)l<g[i].length&&(s[t++]=g[i][l]);for(var l=0;f>l;l++)for(var i=0;i<b.length;i++)l<h[i].length&&(s[t++]=h[i][l]);return s};for(var j={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},k={L:1,M:0,Q:3,H:2},l={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},m={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;m.getBCHDigit(b)-m.getBCHDigit(m.G15)>=0;)b^=m.G15<<m.getBCHDigit(b)-m.getBCHDigit(m.G15);return(a<<10|b)^m.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;m.getBCHDigit(b)-m.getBCHDigit(m.G18)>=0;)b^=m.G18<<m.getBCHDigit(b)-m.getBCHDigit(m.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return m.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case l.PATTERN000:return(b+c)%2==0;case l.PATTERN001:return b%2==0;case l.PATTERN010:return c%3==0;case l.PATTERN011:return(b+c)%3==0;case l.PATTERN100:return(Math.floor(b/2)+Math.floor(c/3))%2==0;case l.PATTERN101:return b*c%2+b*c%3==0;case l.PATTERN110:return(b*c%2+b*c%3)%2==0;case l.PATTERN111:return(b*c%3+(b+c)%2)%2==0;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new c([1],0),d=0;a>d;d++)b=b.multiply(new c([1,n.gexp(d)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case j.MODE_NUMBER:return 10;case j.MODE_ALPHA_NUM:return 9;case j.MODE_8BIT_BYTE:return 8;case j.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case j.MODE_NUMBER:return 12;case j.MODE_ALPHA_NUM:return 11;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case j.MODE_NUMBER:return 14;case j.MODE_ALPHA_NUM:return 13;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||0==h&&0==i||g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,0!=j&&4!=j||(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},n={glog:function(a){if(1>a)throw new Error("glog("+a+")");return n.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return n.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},o=0;8>o;o++)n.EXP_TABLE[o]=1<<o;for(var o=8;256>o;o++)n.EXP_TABLE[o]=n.EXP_TABLE[o-4]^n.EXP_TABLE[o-5]^n.EXP_TABLE[o-6]^n.EXP_TABLE[o-8];for(var o=0;255>o;o++)n.LOG_TABLE[n.EXP_TABLE[o]]=o;c.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var e=0;e<a.getLength();e++)b[d+e]^=n.gexp(n.glog(this.get(d))+n.glog(a.get(e)));return new c(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=n.glog(this.get(0))-n.glog(a.get(0)),d=new Array(this.getLength()),e=0;e<this.getLength();e++)d[e]=this.get(e);for(var e=0;e<a.getLength();e++)d[e]^=n.gexp(n.glog(a.get(e))+b);return new c(d,0).mod(a)}},d.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],d.getRSBlocks=function(a,b){var c=d.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var e=c.length/3,f=[],g=0;e>g;g++)for(var h=c[3*g+0],i=c[3*g+1],j=c[3*g+2],k=0;h>k;k++)f.push(new d(i,j));return f},d.getRsBlockTable=function(a,b){switch(b){case k.L:return d.RS_BLOCK_TABLE[4*(a-1)+0];case k.M:return d.RS_BLOCK_TABLE[4*(a-1)+1];case k.Q:return d.RS_BLOCK_TABLE[4*(a-1)+2];case k.H:return d.RS_BLOCK_TABLE[4*(a-1)+3];default:return}},e.prototype={get:function(a){var b=Math.floor(a/8);return 1==(this.buffer[b]>>>7-a%8&1)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(a>>>b-c-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var p=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],q=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function b(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var c=this._htOption,d=this._el,e=a.getModuleCount();Math.floor(c.width/e),Math.floor(c.height/e);this.clear();var f=b("svg",{viewBox:"0 0 "+String(e)+" "+String(e),width:"100%",height:"100%",fill:c.colorLight});f.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),d.appendChild(f),f.appendChild(b("rect",{fill:c.colorLight,width:"100%",height:"100%"})),f.appendChild(b("rect",{fill:c.colorDark,width:"1",height:"1",id:"template"}));for(var g=0;e>g;g++)for(var h=0;e>h;h++)if(a.isDark(g,h)){var i=b("use",{x:String(h),y:String(g)});i.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),f.appendChild(i)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),r="svg"===document.documentElement.tagName.toLowerCase(),s=r?q:f()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function b(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&c._fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,void(d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var c=1/window.devicePixelRatio,d=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,b,e,f,g,h,i,j,k){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*c;else"undefined"==typeof j&&(arguments[1]*=c,arguments[2]*=c,arguments[3]*=c,arguments[4]*=c);d.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=g(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.alt="Scan me!",this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&b.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCodep=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:k.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._htOption.useSVG&&(s=q),this._android=g(),this._el=a,this._oQRCode=null,this._htOption.text&&this.makeCode(this._htOption.text)},QRCodep.prototype.makeCode=function(a){this._oQRCode=new b(h(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,-1!=this._htOption.dpi&&-1!=this._htOption.mmPerDot&&(this._htOption.width=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot,this._htOption.height=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot),this._oDrawing=new s(this._el,this._htOption),this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCodep.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCodep.prototype.clear=function(){this._oDrawing.clear()},QRCodep.CorrectLevel=k}();var sepaQR;!function(){"use strict";var a={UTF_8:1,ISO8859_1:2,ISO8859_2:3,ISO8859_4:4,ISO8859_5:5,ISO8859_7:6,ISO8859_10:7,ISO8859_15:8};sepaQR=function(b){if(this._sOpt={serviceTag:"BCD",version:"001",charset:a.UTF_8,identificationCode:"SCT",benefBIC:"",benefName:"",benefAccNr:"",amountEuro:"",purpose:"",creditorRef:"",remittanceInf:"",information:""},b)for(var c in b)this._sOpt[c]=b[c]},sepaQR.prototype.validServiceTag=function(){return"BCD"===this._sOpt.serviceTag},sepaQR.prototype.validVersion=function(){return"001"===this._sOpt.version||"002"===this._sOpt.version},sepaQR.prototype.validCharset=function(){return this._sOpt.charset>0&&this._sOpt.charset<=8},sepaQR.prototype.validIdentificationCode=function(){return"SCT"===this._sOpt.identificationCode},sepaQR.prototype.validBenefName=function(){var a="string"==typeof this._sOpt.benefName&&this._sOpt.benefName.length>=1&&this._sOpt.benefName.length<=70;if(!a)throw new Error("benefName not valid!");return a},sepaQR.prototype.validBenefAccNr=function(){var a="string"==typeof this._sOpt.benefAccNr&&this._sOpt.benefAccNr.length>=1&&this._sOpt.benefAccNr.length<=34;if(!a)throw new Error("benefAccNr not valid!");return a},sepaQR.prototype.validAmountEuro=function(){if("string"==typeof this._sOpt.amountEuro)return 0===this._sOpt.amountEuro.length;if("number"==typeof this._sOpt.amountEuro){this._sOpt.amountEuro=Math.round(100*this._sOpt.amountEuro)/100;var a=this._sOpt.amountEuro>.01&&this._sOpt.amountEuro<=999999999.99;if(!a)throw new Error("Amount not valid!");return a}},sepaQR.prototype.validBenefBic=function(){var a="002"==this._sOpt.version||"string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11;if(!a)throw new Error("BIC is mandatory in Version 001!");if(a="string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11,!a)throw new Error("benefBIC not valid!");return a},sepaQR.prototype.validPurpose=function(){var a="string"==typeof this._sOpt.purpose&&this._sOpt.purpose.length>=0&&this._sOpt.purpose.length<=4;if(!a)throw new Error("Purpose not valid!");return a},sepaQR.prototype.validInformation=function(){var a="string"==typeof this._sOpt.information&&this._sOpt.information.length>=0&&this._sOpt.information.length<=70;if(!a)throw new Error("Information not valid!");return a},sepaQR.prototype.validCreditorRefOrRemittance=function(){var a="string"==typeof this._sOpt.creditorRef&&0===this._sOpt.creditorRef.length,b="string"==typeof this._sOpt.remittanceInf&&0===this._sOpt.remittanceInf.length,c=a&&"string"==typeof this._sOpt.remittanceInf&&this._sOpt.remittanceInf.length<=140||b&&"string"==typeof this._sOpt.creditorRef&&this._sOpt.creditorRef.length<=35;if(!c)throw new Error("creditorRef or Remittance not valid!");return c},sepaQR.prototype.validQRTextLength=function(){for(var a=this.prepareQRText(),b=0,c=0,d=a.length;d>c;c++){var e=a.charCodeAt(c);b+=e>65536?4:e>2048?3:e>128?2:1}return 328>=b},sepaQR.prototype.valid=function(){var a=this.validServiceTag()&&this.validVersion()&&this.validCharset()&&this.validIdentificationCode()&&this.validBenefName()&&this.validBenefAccNr()&&this.validAmountEuro()&&this.validBenefBic()&&this.validPurpose()&&this.validInformation()&&this.validPurpose()&&this.validCreditorRefOrRemittance()&&this.validQRTextLength();return a},sepaQR.prototype.prepareQRText=function(){return(this._sOpt.serviceTag+"\n"+this._sOpt.version+"\n"+this._sOpt.charset+"\n"+this._sOpt.identificationCode+"\n"+this._sOpt.benefBIC+"\n"+this._sOpt.benefName+"\n"+this._sOpt.benefAccNr+"\nEUR"+this._sOpt.amountEuro+"\n"+this._sOpt.purpose+"\n"+this._sOpt.creditorRef+"\n"+this._sOpt.remittanceInf+"\n"+this._sOpt.information).trim()},sepaQR.prototype.toQRText=function(){return this.valid()?this.prepareQRText():""},sepaQR.prototype.makeCodeInto=function(a,b){var c={width:256,height:256,mmPerDot:.85,dpi:92,correctLevel:QRCodep.CorrectLevel.M,text:this.toQRText()};if(0!==c.text.length){if(b)for(var d in b)c[d]=b[d];return this.qrcode=new QRCodep(a,c),this.drawExplanatoryLink(document.getElementById(a).getElementsByTagName("canvas")[0],document.createElement("canvas")),this.qrcode}},sepaQR.prototype.drawExplanatoryLink=function(a,b){var c=3,d=12,e=8,f=6,g=a;b.width=g.width,b.height=g.height,b.getContext("2d").drawImage(g,0,0),g.width=b.width+2*(e+c+f),g.height=b.height+2*(e+c+f),CanvasRenderingContext2D.prototype.roundRect=function(a,b,c,d,e,f){return 2*e>c&&(e=c/2),2*e>d&&(e=d/2),this.beginPath(),this.moveTo(a+e,b),this.arcTo(a+c,b,a+c,b+d,e),this.arcTo(a+c,b+d,a,b+d,e),this.lineTo(a+c-f,b+d),this.moveTo(a+c-110,b+d),this.arcTo(a,b+d,a,b,e),this.arcTo(a,b,a+c,b,e),this};var h=g.getContext("2d");h.fillStyle="white",h.rect(0,0,g.width,g.height),h.fill(),h.drawImage(b,e+c+f,e+c+f),h.lineWidth=c,h.roundRect(f+c/2,f+c/2,g.width-c-2*f,g.height-c-2*f,d,3*e).stroke(),h.fillStyle="black",h.font=4.5*c+"px Arial",h.fillText("sepaQR.eu",g.width-110,g.height-f/2)},sepaQR.Charset=a}();</script>
<script>var QRCodep;!function(){function a(a){this.mode=j.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=0,c=this.data.length;c>b;b++){var d=[],e=this.data.charCodeAt(b);e>65536?(d[0]=240|(1835008&e)>>>18,d[1]=128|(258048&e)>>>12,d[2]=128|(4032&e)>>>6,d[3]=128|63&e):e>2048?(d[0]=224|(61440&e)>>>12,d[1]=128|(4032&e)>>>6,d[2]=128|63&e):e>128?(d[0]=192|(1984&e)>>>6,d[1]=128|63&e):d[0]=e,this.parsedData.push(d)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function c(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function d(a,b){this.totalCount=a,this.dataCount=b}function e(){this.buffer=[],this.length=0}function f(){return"undefined"!=typeof CanvasRenderingContext2D}function g(){var a=!1,b=navigator.userAgent;if(/android/i.test(b)){a=!0;var c=b.toString().match(/android ([0-9]\.[0-9])/i);c&&c[1]&&(a=parseFloat(c[1]))}return a}function h(a,b){for(var c=1,d=i(a),e=0,f=p.length;f>=e;e++){var g=0;switch(b){case k.L:g=p[e][0];break;case k.M:g=p[e][1];break;case k.Q:g=p[e][2];break;case k.H:g=p[e][3]}if(g>=d)break;c++}if(c>p.length)throw new Error("Too long data");return c}function i(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(a){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?this.modules[a+c][b+d]=!0:this.modules[a+c][b+d]=!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=m.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=a%2==0);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=b%2==0)},setupPositionAdjustPattern:function(){for(var a=m.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var f=-2;2>=f;f++)for(var g=-2;2>=g;g++)-2==f||2==f||-2==g||2==g||0==f&&0==g?this.modules[d+f][e+g]=!0:this.modules[d+f][e+g]=!1}},setupTypeNumber:function(a){for(var b=m.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=m.getBCHTypeInfo(c),e=0;15>e;e++){var f=!a&&1==(d>>e&1);6>e?this.modules[e][8]=f:8>e?this.modules[e+1][8]=f:this.modules[this.moduleCount-15+e][8]=f}for(var e=0;15>e;e++){var f=!a&&1==(d>>e&1);8>e?this.modules[8][this.moduleCount-e-1]=f:9>e?this.modules[8][15-e-1+1]=f:this.modules[8][15-e-1]=f}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,f=0,g=this.moduleCount-1;g>0;g-=2)for(6==g&&g--;;){for(var h=0;2>h;h++)if(null==this.modules[d][g-h]){var i=!1;f<a.length&&(i=1==(a[f]>>>e&1));var j=m.getMask(b,d,g-h);j&&(i=!i),this.modules[d][g-h]=i,e--,-1==e&&(f++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,f){for(var g=d.getRSBlocks(a,c),h=new e,i=0;i<f.length;i++){var j=f[i];h.put(j.mode,4),h.put(j.getLength(),m.getLengthInBits(j.mode,a)),j.write(h)}for(var k=0,i=0;i<g.length;i++)k+=g[i].dataCount;if(h.getLengthInBits()>8*k)throw new Error("code length overflow. ("+h.getLengthInBits()+">"+8*k+")");for(h.getLengthInBits()+4<=8*k&&h.put(0,4);h.getLengthInBits()%8!=0;)h.putBit(!1);for(;;){if(h.getLengthInBits()>=8*k)break;if(h.put(b.PAD0,8),h.getLengthInBits()>=8*k)break;h.put(b.PAD1,8)}return b.createBytes(h,g)},b.createBytes=function(a,b){for(var d=0,e=0,f=0,g=new Array(b.length),h=new Array(b.length),i=0;i<b.length;i++){var j=b[i].dataCount,k=b[i].totalCount-j;e=Math.max(e,j),f=Math.max(f,k),g[i]=new Array(j);for(var l=0;l<g[i].length;l++)g[i][l]=255&a.buffer[l+d];d+=j;var n=m.getErrorCorrectPolynomial(k),o=new c(g[i],n.getLength()-1),p=o.mod(n);h[i]=new Array(n.getLength()-1);for(var l=0;l<h[i].length;l++){var q=l+p.getLength()-h[i].length;h[i][l]=q>=0?p.get(q):0}}for(var r=0,l=0;l<b.length;l++)r+=b[l].totalCount;for(var s=new Array(r),t=0,l=0;e>l;l++)for(var i=0;i<b.length;i++)l<g[i].length&&(s[t++]=g[i][l]);for(var l=0;f>l;l++)for(var i=0;i<b.length;i++)l<h[i].length&&(s[t++]=h[i][l]);return s};for(var j={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},k={L:1,M:0,Q:3,H:2},l={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},m={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;m.getBCHDigit(b)-m.getBCHDigit(m.G15)>=0;)b^=m.G15<<m.getBCHDigit(b)-m.getBCHDigit(m.G15);return(a<<10|b)^m.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;m.getBCHDigit(b)-m.getBCHDigit(m.G18)>=0;)b^=m.G18<<m.getBCHDigit(b)-m.getBCHDigit(m.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return m.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case l.PATTERN000:return(b+c)%2==0;case l.PATTERN001:return b%2==0;case l.PATTERN010:return c%3==0;case l.PATTERN011:return(b+c)%3==0;case l.PATTERN100:return(Math.floor(b/2)+Math.floor(c/3))%2==0;case l.PATTERN101:return b*c%2+b*c%3==0;case l.PATTERN110:return(b*c%2+b*c%3)%2==0;case l.PATTERN111:return(b*c%3+(b+c)%2)%2==0;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new c([1],0),d=0;a>d;d++)b=b.multiply(new c([1,n.gexp(d)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case j.MODE_NUMBER:return 10;case j.MODE_ALPHA_NUM:return 9;case j.MODE_8BIT_BYTE:return 8;case j.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case j.MODE_NUMBER:return 12;case j.MODE_ALPHA_NUM:return 11;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case j.MODE_NUMBER:return 14;case j.MODE_ALPHA_NUM:return 13;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||0==h&&0==i||g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,0!=j&&4!=j||(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},n={glog:function(a){if(1>a)throw new Error("glog("+a+")");return n.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return n.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},o=0;8>o;o++)n.EXP_TABLE[o]=1<<o;for(var o=8;256>o;o++)n.EXP_TABLE[o]=n.EXP_TABLE[o-4]^n.EXP_TABLE[o-5]^n.EXP_TABLE[o-6]^n.EXP_TABLE[o-8];for(var o=0;255>o;o++)n.LOG_TABLE[n.EXP_TABLE[o]]=o;c.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var e=0;e<a.getLength();e++)b[d+e]^=n.gexp(n.glog(this.get(d))+n.glog(a.get(e)));return new c(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=n.glog(this.get(0))-n.glog(a.get(0)),d=new Array(this.getLength()),e=0;e<this.getLength();e++)d[e]=this.get(e);for(var e=0;e<a.getLength();e++)d[e]^=n.gexp(n.glog(a.get(e))+b);return new c(d,0).mod(a)}},d.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],d.getRSBlocks=function(a,b){var c=d.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var e=c.length/3,f=[],g=0;e>g;g++)for(var h=c[3*g+0],i=c[3*g+1],j=c[3*g+2],k=0;h>k;k++)f.push(new d(i,j));return f},d.getRsBlockTable=function(a,b){switch(b){case k.L:return d.RS_BLOCK_TABLE[4*(a-1)+0];case k.M:return d.RS_BLOCK_TABLE[4*(a-1)+1];case k.Q:return d.RS_BLOCK_TABLE[4*(a-1)+2];case k.H:return d.RS_BLOCK_TABLE[4*(a-1)+3];default:return}},e.prototype={get:function(a){var b=Math.floor(a/8);return 1==(this.buffer[b]>>>7-a%8&1)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(a>>>b-c-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var p=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],q=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function b(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var c=this._htOption,d=this._el,e=a.getModuleCount();Math.floor(c.width/e),Math.floor(c.height/e);this.clear();var f=b("svg",{viewBox:"0 0 "+String(e)+" "+String(e),width:"100%",height:"100%",fill:c.colorLight});f.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),d.appendChild(f),f.appendChild(b("rect",{fill:c.colorLight,width:"100%",height:"100%"})),f.appendChild(b("rect",{fill:c.colorDark,width:"1",height:"1",id:"template"}));for(var g=0;e>g;g++)for(var h=0;e>h;h++)if(a.isDark(g,h)){var i=b("use",{x:String(h),y:String(g)});i.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),f.appendChild(i)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),r="svg"===document.documentElement.tagName.toLowerCase(),s=r?q:f()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function b(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&c._fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,void(d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var c=1/window.devicePixelRatio,d=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,b,e,f,g,h,i,j,k){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*c;else"undefined"==typeof j&&(arguments[1]*=c,arguments[2]*=c,arguments[3]*=c,arguments[4]*=c);d.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=g(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.alt="Scan me!",this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&b.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCodep=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:k.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._htOption.useSVG&&(s=q),this._android=g(),this._el=a,this._oQRCode=null,this._htOption.text&&this.makeCode(this._htOption.text)},QRCodep.prototype.makeCode=function(a){this._oQRCode=new b(h(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,-1!=this._htOption.dpi&&-1!=this._htOption.mmPerDot&&(this._htOption.width=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot,this._htOption.height=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot),this._oDrawing=new s(this._el,this._htOption),this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCodep.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCodep.prototype.clear=function(){this._oDrawing.clear()},QRCodep.CorrectLevel=k}();var sepaQR;!function(){"use strict";var a={UTF_8:1,ISO8859_1:2,ISO8859_2:3,ISO8859_4:4,ISO8859_5:5,ISO8859_7:6,ISO8859_10:7,ISO8859_15:8};sepaQR=function(b){if(this._sOpt={serviceTag:"BCD",version:"001",charset:a.UTF_8,identificationCode:"SCT",benefBIC:"",benefName:"",benefAccNr:"",amountEuro:"",purpose:"",creditorRef:"",remittanceInf:"",information:""},b)for(var c in b)this._sOpt[c]=b[c]},sepaQR.prototype.validServiceTag=function(){return"BCD"===this._sOpt.serviceTag},sepaQR.prototype.validVersion=function(){return"001"===this._sOpt.version||"002"===this._sOpt.version},sepaQR.prototype.validCharset=function(){return this._sOpt.charset>0&&this._sOpt.charset<=8},sepaQR.prototype.validIdentificationCode=function(){return"SCT"===this._sOpt.identificationCode},sepaQR.prototype.validBenefName=function(){var a="string"==typeof this._sOpt.benefName&&this._sOpt.benefName.length>=1&&this._sOpt.benefName.length<=70;if(!a)throw new Error("benefName not valid!");return a},sepaQR.prototype.validBenefAccNr=function(){var a="string"==typeof this._sOpt.benefAccNr&&this._sOpt.benefAccNr.length>=1&&this._sOpt.benefAccNr.length<=34;if(!a)throw new Error("benefAccNr not valid!");return a},sepaQR.prototype.validAmountEuro=function(){if("string"==typeof this._sOpt.amountEuro)return 0===this._sOpt.amountEuro.length;if("number"==typeof this._sOpt.amountEuro){this._sOpt.amountEuro=Math.round(100*this._sOpt.amountEuro)/100;var a=this._sOpt.amountEuro>.01&&this._sOpt.amountEuro<=999999999.99;if(!a)throw new Error("Amount not valid!");return a}},sepaQR.prototype.validBenefBic=function(){var a="002"==this._sOpt.version||"string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11;if(!a)throw new Error("BIC is mandatory in Version 001!");if(a="string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11,!a)throw new Error("benefBIC not valid!");return a},sepaQR.prototype.validPurpose=function(){var a="string"==typeof this._sOpt.purpose&&this._sOpt.purpose.length>=0&&this._sOpt.purpose.length<=4;if(!a)throw new Error("Purpose not valid!");return a},sepaQR.prototype.validInformation=function(){var a="string"==typeof this._sOpt.information&&this._sOpt.information.length>=0&&this._sOpt.information.length<=70;if(!a)throw new Error("Information not valid!");return a},sepaQR.prototype.validCreditorRefOrRemittance=function(){var a="string"==typeof this._sOpt.creditorRef&&0===this._sOpt.creditorRef.length,b="string"==typeof this._sOpt.remittanceInf&&0===this._sOpt.remittanceInf.length,c=a&&"string"==typeof this._sOpt.remittanceInf&&this._sOpt.remittanceInf.length<=140||b&&"string"==typeof this._sOpt.creditorRef&&this._sOpt.creditorRef.length<=35;if(!c)throw new Error("creditorRef or Remittance not valid!");return c},sepaQR.prototype.validQRTextLength=function(){for(var a=this.prepareQRText(),b=0,c=0,d=a.length;d>c;c++){var e=a.charCodeAt(c);b+=e>65536?4:e>2048?3:e>128?2:1}return 328>=b},sepaQR.prototype.valid=function(){var a=this.validServiceTag()&&this.validVersion()&&this.validCharset()&&this.validIdentificationCode()&&this.validBenefName()&&this.validBenefAccNr()&&this.validAmountEuro()&&this.validBenefBic()&&this.validPurpose()&&this.validInformation()&&this.validPurpose()&&this.validCreditorRefOrRemittance()&&this.validQRTextLength();return a},sepaQR.prototype.prepareQRText=function(){return(this._sOpt.serviceTag+"\n"+this._sOpt.version+"\n"+this._sOpt.charset+"\n"+this._sOpt.identificationCode+"\n"+this._sOpt.benefBIC+"\n"+this._sOpt.benefName+"\n"+this._sOpt.benefAccNr+"\nEUR"+this._sOpt.amountEuro+"\n"+this._sOpt.purpose+"\n"+this._sOpt.creditorRef+"\n"+this._sOpt.remittanceInf+"\n"+this._sOpt.information).trim()},sepaQR.prototype.toQRText=function(){return this.valid()?this.prepareQRText():""},sepaQR.prototype.makeCodeInto=function(a,b){var c={width:256,height:256,mmPerDot:.85,dpi:92,correctLevel:QRCodep.CorrectLevel.M,text:this.toQRText()};if(0!==c.text.length){if(b)for(var d in b)c[d]=b[d];return this.qrcode=new QRCodep(a,c),this.drawExplanatoryLink(document.getElementById(a).getElementsByTagName("canvas")[0],document.createElement("canvas")),this.qrcode}},sepaQR.prototype.drawExplanatoryLink=function(a,b){var c=3,d=12,e=8,f=6,g=a;b.width=g.width,b.height=g.height,b.getContext("2d").drawImage(g,0,0),g.width=b.width+2*(e+c+f),g.height=b.height+2*(e+c+f),CanvasRenderingContext2D.prototype.roundRect=function(a,b,c,d,e,f){return 2*e>c&&(e=c/2),2*e>d&&(e=d/2),this.beginPath(),this.moveTo(a+e,b),this.arcTo(a+c,b,a+c,b+d,e),this.arcTo(a+c,b+d,a,b+d,e),this.lineTo(a+c-f,b+d),this.moveTo(a+c-110,b+d),this.arcTo(a,b+d,a,b,e),this.arcTo(a,b,a+c,b,e),this};var h=g.getContext("2d");h.fillStyle="white",h.rect(0,0,g.width,g.height),h.fill(),h.drawImage(b,e+c+f,e+c+f),h.lineWidth=c,h.roundRect(f+c/2,f+c/2,g.width-c-2*f,g.height-c-2*f,d,3*e).stroke(),h.fillStyle="black",h.font=4.5*c+"px Arial",h.fillText("sepaQR.eu",g.width-110,g.height-f/2)},sepaQR.Charset=a}();</script>

View File

@ -3,292 +3,101 @@
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div class="max-w-screen-lg w-full">
{% if flash %} {% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} {{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
{% endif %} {% endif %}
<h1 class="h1">Ruderassistent</h1>
<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_global day_cox_needed = false %}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events %}
{% if planned_event.cox_needed %}
{% set_global day_cox_needed = true %}
{% endif %}
{% endfor %}
{% endif %}
<div class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js" style="min-height: 10rem;" data-trips="{{ amount_trips }}" data-month="{{ day.day| date(format='%m') }}" data-coxneeded="{{ day_cox_needed }}">
<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>
</h2>
{% if day.planned_events | length > 0 or day.trips | length > 0 %}
<div
class="grid grid-cols-1 gap-3 mb-3">
{# --- START Events --- #}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = planned_event.cox | length %}
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
<div class="pt-2 px-3 border-t border-gray-200" style="order: {{ planned_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><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">
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%}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/remove/{{ planned_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="/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 %}
{% 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 %}
{% 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 }}" class="block btn btn-attention btn-fw">
{% include "includes/cox-icon" %}
Abmelden
</a>
{% else %}
<a href="/cox/join/{{ planned_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 %}>
{% include "includes/cox-icon" %}
Steuern
</a>
{% endif %}
{% endif %}
{# --- END Cox Buttons --- #}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div
id="event{{ planned_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') }}
{% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% 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="admin" in loggedin_user.roles) }}
{% endif %}
{# --- END List Rowers --- #}
{% if "admin" in loggedin_user.roles %}
<form action="/join/{{ planned_event.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% endif %}
{% if planned_event.allow_guests %} <div class="grid gap-3">
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
{% endif %} <h2 class="h2">Allgemein</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="https://rudernlinz.at/termin" target="_blank" class="link-primary">FAQ (extern)</a></li>
</ul>
</div>
</div>
</div>
{% if "admin" in loggedin_user.roles %} {% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
<div class="grid gap-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">Ergo</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/ergo" class="link-primary">Ergo</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
{# --- START Edit Form --- #} {% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md"> <div class="grid gap-3">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<form action="/admin/planned-event" method="post" class="grid gap-3"> <h2 class="h2">Aktives Vereinsmitglied</h2>
<input type="hidden" name="_method" value="put"/> <div class="text-sm p-3">
<input type="hidden" name="id" value="{{ planned_event.id }}"/> <ul class="list-disc ms-2">
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }} <li class="py-1"><a href="/planned" class="link-primary">Geplante Ausfahrten</a></li>
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }} <li class="py-1"><a href="/log" class="link-primary">Ausfahrt eintragen</a></li>
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }} <li class="py-1"><a href="/log/show" class="link-primary">Logbuch</a></li>
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }} <li class="py-1"><a href="/stat" class="link-primary">Statistik</a></li>
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }} <li class="py-1"><a href="/stat/boats" class="link-primary">Bootsauswertung</a></li>
<li class="py-1"><a href="/boatdamage" class="link-primary">Bootsschaden</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
<input value="Speichern" class="btn btn-primary" type="submit"/>
</form>
</div>
{# --- END Edit Form --- #}
{# --- START Delete Btn --- #} {% if "scheckbuch" in loggedin_user.roles %}
<div class="text-right"> <div class="grid gap-3">
<a href="/admin/planned-event/{{ planned_event.id }}/delete" class="inline-block btn btn-alert"> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
{% include "includes/delete-icon" %} <h2 class="h2">Scheckbuch</h2>
Termin löschen <div class="text-sm p-3">
</a> <ul class="list-disc ms-2">
</div> <li class="py-1"><a href="/planned" class="link-primary">Geplante Ausfahrten</a></li>
{% endif %} </ul>
{# --- END Delete Btn --- #} </div>
</div> </div>
</div> </div>
{# --- END Sidebar Content --- #} {% endif %}
</div>
{% endfor %}
{% endif %}
{# --- END Events --- #}
{# --- START Trips --- #} {% if "Vorstand" in loggedin_user.roles %}
{% if day.trips | length > 0 %} <div class="grid gap-3">
{% for trip in day.trips | sort(attribute="planned_starting_time") %} <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<div class="pt-2 px-3 reset-js border-t border-gray-200" style="order: {{ trip.planned_starting_time | replace(from=":", to="") }}" data-coxneeded="false"> <h2 class="h2">Vorstand</h2>
<div class="flex justify-between items-center"> <div class="text-sm p-3">
<div class="mr-1"> <ul class="list-disc ms-2">
{% if trip.max_people == 0 %} <li class="py-1"><a href="/admin/user/fees" class="link-primary">Übersicht User Gebühren</a></li>
<strong class="text-[#f43f5e]">&#9888; <li class="py-1"><a href="/admin/user" class="link-primary">User</a></li>
{{ trip.planned_starting_time }} </ul>
Uhr</strong> </div>
<small class="text-[#f43f5e]">(Absage </div>
{{ trip.cox_name }} </div>
{% if trip.trip_type %} {% 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 %})</small>
{% endif %}
<br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{% if trip.max_people == 0 %}&#9888; {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.trip_type %}<small class='block'>{{ trip.trip_type.desc }}</small>{% endif %}{% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" data-body="#trip{{ trip.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div> {% if "admin" in loggedin_user.roles %}
{% set_global cur_user_participates = false %} <div class="grid gap-3">
{% for rower in trip.rower %} <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
{% if rower.name == loggedin_user.name %} <h2 class="h2">Admin</h2>
{% set_global cur_user_participates = true %} <div class="text-sm p-3">
{% endif %} <ul class="list-disc ms-2">
{% endfor %} <li class="py-1"><a href="/admin/boat" class="link-primary">Boote</a></li>
{% if cur_user_participates %} <li class="py-1"><a href="/admin/user" class="link-primary">User</a></li>
<a href="/remove/{{ trip.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a> <li class="py-1"><a href="/admin/mail" class="link-primary">Mail (beautifully layouted)</a></li>
{% endif %} <li class="py-1"><a href="/admin/rss" class="link-primary">Logs</a></li>
{% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%} </ul>
<a href="/join/{{ trip.trip_details_id }}" class="btn btn-primary btn-fw" {% if trip.trip_type %} onclick="return confirm('{{ trip.trip_type.question }}');" {% endif %}>Mitrudern</a> </div>
{% endif %} </div>
</div> </div>
</div> {% endif %}
{# --- START Sidebar Content --- #}
<div class="hidden">
<div id="trip{{ trip.trip_details_id }}">
{% if trip.max_people == 0 %}
{# --- border-[#f43f5e] bg-[#f43f5e] --- #}
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }}
{% else %}
{% set amount_cur_rower = trip.rower | length %}
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
{% if trip.cox_id == loggedin_user.id %}
<form action="/join/{{ trip.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 %}
{% endif %}
{# --- START Edit Form --- #}
{% if trip.cox_id == loggedin_user.id %}
<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='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) }}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }}
<input value="Speichern" class="btn btn-primary" type="submit"/> </div>
</form>
</div>
{% if trip.rower | length == 0 %}
<div class="text-right mt-6">
<a href="/cox/remove/trip/{{ trip.id }}" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{% endif %}
{# --- END Edit Form --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Trips --- #}
</div>
{% endif %}
</div>
{# --- START Add Buttons --- #} {% endblock content%}
{% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "admin" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center">
{% if "admin" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Event
</a>
{% 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 "admin" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Ausfahrt
</a>
{% endif %}
</div>
{% endif %}
{# --- END Add Buttons --- #}
</div>
{% endfor %}
</div>
</div>
{% if "cox" in loggedin_user.roles %}
{% include "forms/trip" %}
{% endif %}
{% if "admin" in loggedin_user.roles %}
{% include "forms/event" %}
{% endif %}{% endblock content %}

View File

@ -24,7 +24,7 @@
<div class="md:col-span-3 bg-white dark:bg-primary-900 rounded-md shadow"> <div class="md:col-span-3 bg-white dark:bg-primary-900 rounded-md shadow">
<h2 class="h2">Neue Ausfahrt</h2> <h2 class="h2">Neue Ausfahrt</h2>
<div class="p-3"> <div class="p-3">
{{ log::new(only_ones="cox" not in loggedin_user.roles, shipmaster=loggedin_user.id) }} {{ log::new(shipmaster=loggedin_user.id) }}
</div> </div>
</div> </div>
<div class="bg-white dark:bg-primary-900 rounded-md shadow"> <div class="bg-white dark:bg-primary-900 rounded-md shadow">

343
templates/planned.html.tera Normal file
View File

@ -0,0 +1,343 @@
{% import "includes/macros" as macros %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
{% endif %}
{% if "paid" not 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">Vereinsgebühren</h2>
<div class="text-sm p-3">
{% include "includes/qrcode" %}
<div id="qrcode" style="float: left; padding-top: 10 pt; padding-right: 10pt; padding-bottom: 10pt;"></div>
<script type="text/javascript">
var sepaqr = new sepaQR({
benefName: 'ASKÖ Ruderverein Donau Linz',
benefBIC: 'BKAUATWWXXX',
benefAccNr: 'AT131200080413001200',
amountEuro: {{ fee.sum_in_cents/100 }},
remittanceInf: 'Vereinsgebühren {{ fee.name }}',
});
var code = sepaqr.makeCodeInto("qrcode");
</script>
<b>Dein Vereinsbeitrag ({{ fee.name }}): {{ fee.sum_in_cents / 100 }}€ {% if fee.parts | length == 1 %} ({{ fee.parts[0].0 }}) {% endif %}</b><br />
{% if fee.parts | length > 1 %}
<small>
Setzt sich zusammen aus:
<ul style="list-style: circle; padding-left: 1em;">
{% for p in fee.parts %}
<li>{{ p.0 }} ({{ p.1 / 100 }}€) {% if not loop.last %} + {% endif %}</li>
{% endfor %}
</ul>
</small>
{% endif %}
Bitte auf folgendes Konto überweisen: IBAN AT13 1200 0804 1300 1200. Alternativ kannst du auch mit deiner Bankapp den QR Code scannen, damit sollten alle Daten vorausgefüllt sein.<br />
Falls die Berechnung nicht stimmt (korrekte Preise findest du <a href="https://rudernlinz.at/unser-verein/gebuhren/" target="_blank" rel="noopener noreferrer">hier</a>) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.<br />
<small>Wir aktualisieren den Ruderassistent unregelmäßig mit unserem Bankkonto. Falls du schon bezahlt hast, kannst du diese Nachricht getrost ignorieren :^)</small>
</div>
</div>
</div>
{% endif %}
<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_global day_cox_needed = false %}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events %}
{% if planned_event.cox_needed %}
{% set_global day_cox_needed = true %}
{% endif %}
{% endfor %}
{% endif %}
<div class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js" style="min-height: 10rem;" data-trips="{{ amount_trips }}" data-month="{{ day.day| date(format='%m') }}" data-coxneeded="{{ day_cox_needed }}">
<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>
</h2>
{% if day.planned_events | length > 0 or day.trips | length > 0 %}
<div
class="grid grid-cols-1 gap-3 mb-3">
{# --- START Events --- #}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = planned_event.cox | length %}
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
<div class="pt-2 px-3 border-t border-gray-200" style="order: {{ planned_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><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">
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%}
{% 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 }}" 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 %}
{% 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 %}
{% 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 }}" class="block btn btn-attention btn-fw">
{% include "includes/cox-icon" %}
Abmelden
</a>
{% else %}
<a href="/cox/join/{{ planned_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 %}>
{% include "includes/cox-icon" %}
Steuern
</a>
{% endif %}
{% endif %}
{# --- END Cox Buttons --- #}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div
id="event{{ planned_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') }}
{% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% 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) }}
{% endif %}
{# --- END List Rowers --- #}
{% if "planned_event" in loggedin_user.roles %}
<form action="/planned/join/{{ planned_event.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% endif %}
{% if planned_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 %}
{# --- 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 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>
{% endif %}
{# --- END Delete Btn --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Events --- #}
{# --- START Trips --- #}
{% if day.trips | length > 0 %}
{% for trip in day.trips | sort(attribute="planned_starting_time") %}
<div class="pt-2 px-3 reset-js border-t border-gray-200" style="order: {{ trip.planned_starting_time | replace(from=":", to="") }}" data-coxneeded="false">
<div class="flex justify-between items-center">
<div class="mr-1">
{% if trip.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888;
{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-[#f43f5e]">(Absage
{{ trip.cox_name }}
{% if trip.trip_type %}
-
{{ 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 %})</small>
{% endif %}
<br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{% if trip.max_people == 0 %}&#9888; {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.trip_type %}<small class='block'>{{ trip.trip_type.desc }}</small>{% endif %}{% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" data-body="#trip{{ trip.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div>
{% set_global cur_user_participates = false %}
{% for rower in trip.rower %}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/planned/remove/{{ trip.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%}
<a href="/planned/join/{{ trip.trip_details_id }}" class="btn btn-primary btn-fw" {% if trip.trip_type %} onclick="return confirm('{{ trip.trip_type.question }}');" {% endif %}>Mitrudern</a>
{% endif %}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div id="trip{{ trip.trip_details_id }}">
{% if trip.max_people == 0 %}
{# --- border-[#f43f5e] bg-[#f43f5e] --- #}
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }}
{% else %}
{% set amount_cur_rower = trip.rower | length %}
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
{% if trip.cox_id == loggedin_user.id %}
<form action="/planned/join/{{ trip.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 %}
{% endif %}
{# --- START Edit Form --- #}
{% if trip.cox_id == loggedin_user.id %}
<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='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) }}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }}
<input value="Speichern" class="btn btn-primary" type="submit"/>
</form>
</div>
{% if trip.rower | length == 0 %}
<div class="text-right mt-6">
<a href="/cox/remove/trip/{{ trip.id }}" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{% endif %}
{# --- END Edit Form --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Trips --- #}
</div>
{% 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 %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Event
</a>
{% 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 %} rounded-br-md {% else %} rounded-b-md {% endif %}">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Ausfahrt
</a>
{% endif %}
</div>
{% endif %}
{# --- END Add Buttons --- #}
</div>
{% endfor %}
</div>
</div>
{% if "cox" in loggedin_user.roles %}
{% include "forms/trip" %}
{% endif %}
{% if "planned_event" in loggedin_user.roles %}
{% include "forms/event" %}
{% endif %}{% endblock content %}

View File

@ -1,13 +0,0 @@
#!/bin/bash
git pull
cargo b -r
cd frontend
npm install
npm run build
cd ..
cd svelte
npm install
npm run build
sudo systemctl restart rot
sudo systemctl restart rot-svelte