Compare commits

..

131 Commits

Author SHA1 Message Date
Xu 3fb5120142 phpstan ignore config.php 2024-05-26 17:34:04 +02:00
Xu 833af4c62e add edit and back macros to twig 2024-05-26 17:34:04 +02:00
Xu b380d7e68e m.button with opt 2024-05-26 17:34:04 +02:00
Xu 6f8dad070c load day config from env, fixed multiple texts 2024-05-26 17:34:04 +02:00
Xu 40bd61fd32 Angeltype view: Add requires info for not members 2024-05-26 13:07:39 +02:00
Xu 300786e5d0 shift colors when signup_requires_arrival and not arrived 2024-05-26 12:55:10 +02:00
Xu 13ded8de49 supporters can view users_myshifts 2024-05-26 12:27:04 +02:00
Xu c82e3183d6 fix shifttypes permission 2024-05-21 19:22:00 +02:00
Xu f1f5cd7c01 Add users.arrive.list permission & refactored LegacyMiddleware test 2024-05-07 00:25:59 +02:00
Xu b2951b7337 refactor permissions and groups 2024-05-07 00:25:59 +02:00
Xu 4e2e929c7e fix schedule logs 2024-05-04 17:51:48 +02:00
Xu 4a0f5c2e78 throw error on goodie page if no goodie 2024-05-04 17:51:48 +02:00
Xu 0fb09280b3 angeltype view member dect to link 2024-05-04 17:51:48 +02:00
Xu 3972998ba0 rename has_permission_to(_any) to can(Any) 2024-05-04 13:43:27 +02:00
Igor Scheller e514685444 Use settings page for API key resets 2024-05-04 13:43:27 +02:00
Igor Scheller 87f7a74f27 Implement canAny handling for controllers 2024-05-04 13:43:27 +02:00
Igor Scheller a214e0ff8f Auth: Added canAny 2024-05-04 13:43:27 +02:00
Xu d18e203560 docker dev setup on localhost 2024-04-27 00:15:07 +02:00
Xu 0f2c7c5394 refactor shirt to goodie 2024-04-27 00:12:23 +02:00
Xu 3fd8267f12 rename goody into goodie 2024-04-23 00:21:08 +02:00
Xu d0388e8344 fix colors of shifts for admins 2024-04-17 23:09:14 +02:00
Igor Scheller 8f6bd547d3 Move shifts event handler to right location 2024-04-14 21:13:40 +02:00
Igor Scheller f397d809de Extract controller delete blocks to methods 2024-04-14 21:13:40 +02:00
Igor Scheller dd096b0f46 Move Shifts helper methods to Shift model 2024-04-14 21:13:40 +02:00
Igor Scheller cb82ad9c74 Updated config parser to use consistent formatting 2024-04-14 21:13:40 +02:00
Daniel Poelzleithner 0155a33beb Add external_register_url to link to external registration site, hide password form if enable_password is false
Our users have problems with the disabled register button and
keep creating tickets. Add option to link to external site instead
of a disabled button.

People get confused by the login form if they should only
use the oauth login.
Now the login form is hidden if enable_password is false.
It can be shown by clicking the welcome text, in case an
admin needs access
2024-04-10 21:49:55 +02:00
Igor Scheller ab967c6f9c docker dev: Add config for phpMyAdmin, cleanup 2024-04-10 21:24:50 +02:00
Igor Scheller 87b50507f3 Fixed linter warnings 2024-04-10 21:24:50 +02:00
Igor Scheller 905d91d6ed Added/updated comments 2024-04-10 21:24:50 +02:00
Xu 9bf9bd2823 add 'enable_email_goody' config and fix wording 2024-04-07 22:05:33 +02:00
Xu d7d99900f8 Use icons in user edit view 2024-04-07 21:52:14 +02:00
Xu 7e06923ed0 Add user.fa.edit permission 2024-04-07 21:52:14 +02:00
Xu e6251256b3 fix certificates button on AngelTypes_view, add id to location change log 2024-04-07 21:27:03 +02:00
Xu 6e76843db4 add ifsg and drive confirmed stats to metrics 2024-04-07 21:27:03 +02:00
Igor Scheller fcf23d3824 Active angels page: Fix fa angels sorting 2024-03-29 14:31:59 +01:00
Igor Scheller c82e902360 CI: Remove base path from unittest results 2024-03-24 18:01:12 +01:00
Igor Scheller fc9b4d6da4 Fix worklog edit user & rename user on profile page 2024-03-24 18:01:12 +01:00
Igor Scheller 63c70c0ec2 Fixed notification translations & show larger map in locations 2024-03-24 18:01:12 +01:00
Igor Scheller 087d1cf31e Show password reset count and fix query 2024-03-24 18:01:12 +01:00
Igor Scheller f9059161ec Fix oauth name autofill for registration 2024-03-24 18:01:12 +01:00
Xu 541789ae27 add shifts column to Location index view 2024-03-24 16:06:21 +01:00
Xu 865ffe5e5a rename "Shifts" translation 2024-03-24 16:06:21 +01:00
Xu 49cc935ceb driving license: can be confirmed and edited by admins and config options 2024-03-24 14:34:54 +01:00
Igor Scheller 9639a0fe3f Fix coverage warning 2024-03-19 20:33:14 +01:00
Igor Scheller 8c24b78333 Shift deletion: Simplify notification workflow 2024-03-19 20:28:02 +01:00
Igor Scheller 1217de096a Fix disabled choices select color on dark theme 2024-03-16 17:36:34 +01:00
Igor Scheller ba908bf849 Fix translations when removing users from angeltype 2024-03-16 17:36:34 +01:00
Igor Scheller 0642bff804 Fix group rights edit buttons 2024-03-16 17:36:34 +01:00
Igor Scheller aea88b3579 Move footer to page bottom 2024-03-16 17:36:34 +01:00
Igor Scheller 460b416ff1 Shifts view: Highlight current time 2024-03-16 17:36:34 +01:00
Igor Scheller 8dda9a0dc3 Fix night shift calculation & update times 2024-03-16 17:36:34 +01:00
Igor Scheller 4ad8385386 isNightShift: Sync with sql query 2024-03-16 17:36:34 +01:00
Igor Scheller f56e9c534c Add title to "Enough" goodie score 2024-03-16 17:36:34 +01:00
Xu c0088d6601 add hints to myshifts table 2024-03-16 16:31:27 +01:00
Xu f3a12ebda8 explain freeloads for users and shicos 2024-03-16 16:31:27 +01:00
Xu 100d62134f fix table on admin_active length and score 2024-03-10 19:29:58 +01:00
Xu 873803eb2d ifsg: can be confirmed and edited by admins 2024-03-10 19:08:05 +01:00
Jens Brandt 400edd9a19 Fix typo in english language file 2024-03-09 09:59:53 +01:00
Xu a94aa36fa4 Password reset in User state 2024-02-27 16:00:58 +01:00
Xu 9a07a7afb3 add other contact options to questions page 2024-02-27 16:00:58 +01:00
Xu ef3f58e999 differentiate Answer questions page 2024-02-27 16:00:58 +01:00
Xu e18cddae86 success join angeltype button 2024-02-27 16:00:58 +01:00
Xu 5b8b59008a number of mails send when news saved 2024-02-19 20:25:25 +01:00
Xu ec7fb0615c add target="_blank" to links 2024-02-19 20:25:25 +01:00
Xu 759a4f9a14 add night shift multiplier to translations
more night shift hints
2024-02-18 14:14:34 +01:00
Igor Scheller d8f8a4f67d Schedule: Update to parse newer specification 2024-02-17 19:34:03 +01:00
Igor Scheller b63eb44b39 Schedule: Simplify & cleanup data classes 2024-02-17 19:34:03 +01:00
Igor Scheller e3e0fb33a2 Added tests for schedule import 2024-02-17 19:34:03 +01:00
Igor Scheller 790a04dc14 Schedule Import: Moved file, initial cleanup, updated schedules 2024-02-17 19:34:03 +01:00
Igor Scheller aef53a306b Fix formatting 2024-02-17 17:20:43 +01:00
Igor Scheller 4fa5db8a42 Use confirmation dialog and redirect back when deleting shifts 2024-02-17 17:02:09 +01:00
Igor Scheller b10264d6ef Fix random shifts test 2024-02-17 16:45:30 +01:00
Igor Scheller ff1edaad10 Added rough system requirements to readme 2024-02-17 16:45:30 +01:00
Lotte Steenbrink 5e505cb8d2
note that shift length is in minutes 2024-02-17 16:32:18 +01:00
Xu cf570502f4 make force active a config option 2024-01-29 19:54:15 +01:00
Xu e7ff3b657a supporters can freeload 2024-01-29 19:54:15 +01:00
Xu 7e26e20608 User view pronoun after name 2024-01-29 19:54:15 +01:00
Xu 67b24032a0 add shift no signout hint and disable buttons 2024-01-29 19:54:15 +01:00
Xu 76c37a5f18 config options supporters can promote 2024-01-29 19:54:15 +01:00
weeman 496d75b9ef Autofocus confirm modal submit button
So that it can be confirmed with enter
2024-01-29 14:39:09 +01:00
weeman c03ccf94c7 Add console log to design submit modal 2024-01-29 14:39:09 +01:00
weeman e47da9b8dd
Fix linter setup 2024-01-29 11:20:39 +01:00
Xu 0476083e77 add needed angels from to shift view 2024-01-28 21:11:55 +01:00
xuwhite 89d8a070d9
add hint admin_shifts no location, shift type or angeltype (#1337) 2024-01-28 20:57:59 +01:00
Lotte Steenbrink f387bc655d active_angels.php, default.po: fix typo 2024-01-27 17:01:35 +01:00
Xu 276b1aa976 add shirt sizes information hint and optional link 2024-01-27 16:37:13 +01:00
Lotte Steenbrink 50b1abe0d1 README: make it clearer that you have to run `migrate` on fresh installs too 2024-01-16 14:33:33 +01:00
Xu b20124e57e error admin_user.php nick or email already exits 2024-01-13 17:20:58 +01:00
Igor Scheller 4d1502d092 CI: Remove api key on db dump 2024-01-10 15:04:49 +01:00
Igor Scheller 00f039dbaa CI: Use tag as release version, fix delete 2024-01-10 12:58:06 +01:00
Igor Scheller dbbf30e233 Replace empty or too short api keys instead of resetting them 2024-01-08 10:57:09 +01:00
Igor Scheller 3f03d6b1d8 API: Set access-control-allow-origin header on api responses, trim bearer api key 2024-01-08 10:57:09 +01:00
Igor Scheller 8c64447273 API: Show from schedule via shift type, added comments 2024-01-08 10:57:09 +01:00
Igor Scheller 3432829c91 API: Document x-api-key header auth 2024-01-08 10:57:09 +01:00
Igor Scheller 6e4c9b2405 API: Added event time 2024-01-08 10:57:09 +01:00
Xu 7637e0c66e refactor user info icon 2024-01-03 19:43:58 +01:00
Xu 8cff7aa205 add user_info to admin_active 2024-01-03 19:43:58 +01:00
Xu 0239bf1988 goodie score visible with admin_user permission 2023-12-29 23:31:21 +01:00
Xu 1798ccda83 add shifttypes and angeltypes count to metrics 2023-12-29 17:50:54 +01:00
Xu 7d5837c5f1 fix needed angeltypes in shifttypes and locations, search for more user related data in log 2023-12-29 17:44:36 +01:00
msquare 8603d47fe0 fix random shift button, again, again 2023-12-29 16:05:39 +01:00
Xu 4a0c0994f0 fix errors when tshirt-size required 2023-12-29 16:02:19 +01:00
Xu ffa531f311 disable shirt size edit for user in settings if got shirt 2023-12-29 13:52:00 +01:00
xuwhite 89d68a56e7
Add needed angel types to location & shift type and permission to edit shift types (#1316) 2023-12-29 13:40:01 +01:00
Xu 9dac2a53db set arrive in user edit view 2023-12-29 13:08:19 +01:00
Igor Scheller 33209ea70b Remove unused functions and methods 2023-12-29 13:06:53 +01:00
Igor Scheller 05725cd58c Eager load relations and optimize queries 2023-12-29 13:06:53 +01:00
Igor Scheller 5fccc7e421 Remove inline js 2023-12-29 13:06:53 +01:00
Igor Scheller b76144b23d Remove unused reference references 2023-12-29 13:06:53 +01:00
Xu 2883a66f49 add user info to metrics 2023-12-28 18:15:52 +01:00
weeman d6412605f2 Add 37c3 theme 2023-12-28 17:56:50 +01:00
Xu 57373c846a Metrics: Added not arrived users 2023-12-28 17:17:26 +01:00
Igor Scheller 1d3509ba3c Fix error messages 2023-12-28 12:48:00 +01:00
Igor Scheller 57862ba722 random shift: check for schedule load flag 2023-12-28 01:08:57 +01:00
Igor Scheller b229d697a3 Only send notifications on new news by default 2023-12-27 18:16:32 +01:00
Igor Scheller 3c0cbe55b6 Cleanup the Fix 2023-12-27 18:16:32 +01:00
Igor Scheller 8140ebd1cc Added edit link on shift type page 2023-12-27 18:16:32 +01:00
Igor Scheller b62d4b4dce Improve history by showing more information, fix users list 2023-12-27 18:16:32 +01:00
Igor Scheller d803591a91 Shifts creation: Simplify log message, fix "room angels" validation 2023-12-27 18:16:32 +01:00
Igor Scheller 6d4f059b3a Fix join angeltype message 2023-12-27 18:16:32 +01:00
Igor Scheller f3e1192695 Worklog: log deletion, User page: fix voucher calculation 2023-12-27 18:16:32 +01:00
Igor Scheller 8833506e04 Cleanup template styles 2023-12-27 18:16:32 +01:00
Igor Scheller cc160e3e20 Error page: Add translation 2023-12-27 18:16:32 +01:00
Igor Scheller bcfcb95786 Use type_bg_class() macro in templates 2023-12-27 18:16:32 +01:00
Igor Scheller f7b0ee9ebb Fix random test 2023-12-27 17:46:13 +01:00
Igor Scheller 0f794be25e Update composer packages 2023-12-27 16:57:58 +01:00
Igor Scheller c5ae5d4aa0 Formatting 2023-12-27 16:43:55 +01:00
Leandra Eberle 64fef42087
Merge pull request #1306 from xuwhite/fixes
small bug fixes
2023-12-27 13:26:10 +01:00
msquare ea9aa9ef40 fixes 2023-12-27 12:50:03 +01:00
Igor Scheller caa699ff05 Added room selection for schedules 2023-12-27 12:50:03 +01:00
Xu 99dd081651 small bug fixes 2023-12-27 12:42:47 +01:00
269 changed files with 6911 additions and 2542 deletions

View File

@ -27,6 +27,8 @@ max_line_length = unset
indent_size = 2
[*.js]
indent_size = 2
max_line_length = unset
quote_type = single
[{LICENSE,db/*.sql}]

View File

@ -1,17 +1,10 @@
{
"parser": "@babel/eslint-parser",
"extends": [ "plugin:editorconfig/all" ],
"plugins": [ "editorconfig" ],
"extends": ["plugin:editorconfig/all", "prettier"],
"plugins": ["editorconfig"],
"rules": {
"prefer-arrow-callback": "error",
"prefer-template": "error",
"no-var": "error",
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
]
"no-var": "error"
}
}

View File

@ -113,7 +113,14 @@ generate-version:
before_script:
- apk add -q git
script:
- VERSION="$(git describe --abbrev=0 --tags)-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}"
- >
VERSION="$(\
git describe --exact-match --tags HEAD 2> /dev/null\
|| (\
(git describe --abbrev=0 --tags | tr -d '\n')\
&& echo "-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}"\
)\
)"
- echo "${VERSION}"
- echo -n "${VERSION}" > storage/app/VERSION
@ -232,6 +239,7 @@ test:
--coverage-text --coverage-html "${HOMEDIR}/coverage/"
--log-junit "${HOMEDIR}/unittests.xml"
after_script:
- sed -i 's~/var/www/~~' unittests.xml
- '"${DOCROOT}/bin/migrate" down'
dump-database:
@ -252,6 +260,9 @@ dump-database:
- cd "${DOCROOT}"
- ./bin/migrate
script:
- >-
mysql -h "${MYSQL_HOST}" -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}"
-e 'UPDATE users SET api_key="" WHERE name="admin"'
- >-
mysqldump -h "${MYSQL_HOST}" -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}"
> "${HOMEDIR}/initial-install.sql"
@ -441,7 +452,8 @@ deploy:
GIT_STRATEGY: none
when: manual
script:
- kubectl delete all,ingress,pvc -l app=$CI_PROJECT_PATH_SLUG -l environment=$CI_ENVIRONMENT_SLUG
- TARGETS=all,ingress,pvc,certificate
- kubectl -n "${KUBE_NAMESPACE}" delete $TARGETS -l app=$CI_PROJECT_PATH_SLUG -l environment=$CI_ENVIRONMENT_SLUG
deploy-k8s-review:
<<: *deploy_k8s

View File

@ -88,6 +88,11 @@ docker compose exec es_workspace yarn build:watch
docker compose exec -e THEMES=0,1 es_workspace yarn build:watch
```
It might also be useful to have an interactive database interface for which a phpMyAdmin instance can be startet at [http://localhost:8888](http://localhost:8888).
```bash
docker compose --profile dev up
```
## Localhost
You can find your local Engelsystem on [http://localhost:5080](http://localhost:5080).

View File

@ -28,6 +28,8 @@ The Engelsystem may be installed manually or by using the provided [docker setup
* MySQL-Server >= 5.7.8 or MariaDB-Server >= 10.2.2
* Webserver, i.e. lighttpd, nginx, or Apache
From previous experience, 2 cores and 2GB ram are roughly enough for up to 1000 Angels (~700 arrived + 500 arrived but not working) during an event.
### Download
* Go to the [Releases](https://github.com/engelsystem/engelsystem/releases) page and download the latest stable release file.
* Extract the files to your webroot and continue with the directions for configurations and setup.
@ -40,7 +42,14 @@ The Engelsystem may be installed manually or by using the provided [docker setup
* Recommended: Directory Listing should be disabled.
* There must be a MySQL database set up with a user who has full rights to that database.
* If necessary, create a `config/config.php` to override values from `config/config.default.php`.
* To disable/remove values from the `themes`, `tshirt_sizes`, `headers`, `header_items`, `footer_items`, or `locales` lists, set the value of the entry to `null`.
* To disable/remove values from the following lists, set the value of the entry to `null`:
* `themes`
* `tshirt_sizes`
* `headers`
* `header_items`
* `footer_items`
* `locales`
* `contact_options`
* To import the database, the `bin/migrate` script has to be run. If you can't execute scripts, you can use the `initial-install.sql` file from the release zip.
* In the browser, login with credentials `admin` : `asdfasdf` and change the password.
@ -70,8 +79,8 @@ cd docker
docker compose up -d
```
#### Migrate
Import database changes to migrate it to the newest version
#### Set Up / Migrate Database
Create the Database Schema (on a fresh install) or import database changes to migrate it to the newest version
```bash
cd docker
docker compose exec es_server bin/migrate

View File

@ -35,14 +35,14 @@
"ext-pdo": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"doctrine/dbal": "^3.6",
"doctrine/dbal": "^3.7",
"erusev/parsedown": "^1.7",
"gettext/gettext": "^5.7",
"gettext/translator": "^1.1",
"gettext/translator": "^1.2",
"guzzlehttp/guzzle": "^7.8",
"illuminate/container": "^10.23",
"illuminate/database": "^10.23",
"illuminate/support": "^10.23",
"illuminate/container": "^10.38",
"illuminate/database": "^10.38",
"illuminate/support": "^10.38",
"league/oauth2-client": "^2.7",
"league/openapi-psr7-validator": "^0.21",
"nikic/fast-route": "^1.3",
@ -51,13 +51,13 @@
"psr/http-message": "^1.1",
"psr/http-server-middleware": "^1.0",
"psr/log": "^3.0",
"rcrowe/twigbridge": "^0.14.0",
"rcrowe/twigbridge": "^0.14.1",
"respect/validation": "^1.1",
"symfony/http-foundation": "^6.3",
"symfony/mailer": "^6.3",
"symfony/http-foundation": "^6.4",
"symfony/mailer": "^6.4",
"symfony/psr-http-message-bridge": "^2.3",
"twig/twig": "^3.7",
"vlucas/phpdotenv": "^5.5"
"twig/twig": "^3.8",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"dms/phpunit-arraysubset-asserts": "^0.5",
@ -66,9 +66,9 @@
"filp/whoops": "^2.15",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.6",
"slevomat/coding-standard": "^8.13",
"squizlabs/php_codesniffer": "^3.7",
"symfony/var-dumper": "^6.3"
"slevomat/coding-standard": "^8.14",
"squizlabs/php_codesniffer": "^3.8",
"symfony/var-dumper": "^6.4"
},
"autoload": {
"psr-4": {

697
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -80,11 +80,11 @@ return [
'oauth2.login' => \Engelsystem\Events\Listener\OAuth2::class . '@login',
'shift.entry.deleting' => [
\Engelsystem\Events\Listener\Shift::class . '@deletedEntryCreateWorklog',
\Engelsystem\Events\Listener\Shift::class . '@deletedEntrySendEmail',
'shift.deleting' => [
\Engelsystem\Events\Listener\Shifts::class . '@deletingCreateWorklogs',
\Engelsystem\Events\Listener\Shifts::class . '@deletingSendEmails',
],
'shift.updating' => \Engelsystem\Events\Listener\Shift::class . '@updatedShiftSendEmail',
'shift.updating' => \Engelsystem\Events\Listener\Shifts::class . '@updatedSendEmail',
],
];

View File

@ -26,7 +26,7 @@ return [
'environment' => env('ENVIRONMENT', 'production'),
// Application URL and base path to use instead of the auto-detected one
'url' => env('APP_URL', null),
'url' => env('APP_URL'),
// Header links
// Available link placeholders: %lang%
@ -53,8 +53,15 @@ return [
'Contact' => env('CONTACT_EMAIL', 'mailto:ticket@c3heaven.de'),
],
// Other ways to ask the heaven
// Multiple contact options / links are possible, analogue to footer_items
'contact_options' => [
// E-mail address
'general.email' => env('CONTACT_EMAIL', 'mailto:ticket@c3heaven.de'),
],
// Text displayed on the FAQ page, rendered as markdown
'faq_text' => env('FAQ_TEXT', null),
'faq_text' => env('FAQ_TEXT'),
// Link to documentation/help
'documentation_url' => env('DOCUMENTATION_URL', 'https://engelsystem.de/doc/'),
@ -72,17 +79,20 @@ return [
'host' => env('MAIL_HOST', 'localhost'),
'port' => env('MAIL_PORT', 587),
// If tls transport encryption should be used
'tls' => env('MAIL_TLS', null),
'tls' => env('MAIL_TLS'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'sendmail' => env('MAIL_SENDMAIL', '/usr/sbin/sendmail -bs'),
],
# Your privacy@ contact address
'privacy_email' => env('PRIVACY_EMAIL', null),
// Your privacy@ contact address
'privacy_email' => env('PRIVACY_EMAIL'),
// Show opt in to save some personal data after the event on user profile and registration pages
'enable_email_goodie' => (bool) env('ENABLE_EMAIL_GOODIE', true),
// Initial admin password
'setup_admin_password' => env('SETUP_ADMIN_PASSWORD', null),
'setup_admin_password' => env('SETUP_ADMIN_PASSWORD'),
'oauth' => [
// '[name]' => [config]
@ -144,6 +154,11 @@ return [
// Supported themes
// To disable a theme in the config.php, you can set its value to null
'themes' => [
17 => [
'name' => 'Engelsystem 37c3 (2023)',
'type' => 'dark',
'navbar_classes' => 'navbar-dark',
],
16 => [
'name' => 'Engelsystem cccamp23 (2023)',
'type' => 'dark',
@ -241,6 +256,9 @@ return [
// Users are able to sign up
'registration_enabled' => (bool) env('REGISTRATION_ENABLED', true),
// URL to external registration page, used on login page
'external_registration_url' => env('EXTERNAL_REGISTRATION_URL'),
// Required user fields
'required_user_fields' => [
'pronoun' => (bool) env('PRONOUN_REQUIRED', false),
@ -257,6 +275,9 @@ return [
// Whether newly-registered user should automatically be marked as arrived
'autoarrive' => (bool) env('ANGEL_AUTOARRIVE', false),
// Supporters of an angeltype can promote other angels of the angeltype to supporter
'supporters_can_promote' => (bool) env('SUPPORTERS_CAN_PROMOTE', false),
// Only allow shift signup this number of hours in advance
// Setting this to 0 disables the feature
'signup_advance_hours' => env('SIGNUP_ADVANCE_HOURS', 0),
@ -282,9 +303,8 @@ return [
// The minimum length for passwords
'min_password_length' => env('PASSWORD_MINIMUM_LENGTH', 8),
// Whether the Password field should be enabled on registration.
// This is useful when using oauth, disabling it also disables normal
// registration without oauth.
// Whether the login and registration via password should be enabled (login will be hidden)
// This is useful when using oauth, disabling it also disables normal registration without oauth
'enable_password' => (bool) env('ENABLE_PASSWORD', true),
// Whether the DECT field should be enabled
@ -310,6 +330,9 @@ return [
// Enables the planned arrival/leave date
'enable_planned_arrival' => (bool) env('ENABLE_PLANNED_ARRIVAL', true),
// Whether force active should be enabled
'enable_force_active' => (bool) env('ENABLE_FORCE_ACTIVE', true),
// Resembles the Goodie Type. There are three options:
// 'none' => no goodie at all
// 'goodie' => a goodie which has no sizing options
@ -328,11 +351,12 @@ return [
// Local timezone
'timezone' => env('TIMEZONE', 'Europe/Berlin'),
// Multiply 'night shifts' and freeloaded shifts (start or end between 2 and 6 exclusive) by 2
// Multiply 'night shifts' and freeloaded shifts (start or end between 2 and 8 exclusive) by 2 in goodie score
// Goodies must be enabled to use this feature
'night_shifts' => [
'enabled' => (bool) env('NIGHT_SHIFTS', true), // Disable to weigh every shift the same
'start' => env('NIGHT_SHIFTS_START', 2),
'end' => env('NIGHT_SHIFTS_END', 6),
'start' => env('NIGHT_SHIFTS_START', 2), // Starting from hour
'end' => env('NIGHT_SHIFTS_END', 8), // Ends at (without including) hour
'multiplier' => env('NIGHT_SHIFTS_MULTIPLIER', 2),
],
@ -342,15 +366,17 @@ return [
'shifts_per_voucher' => env('SHIFTS_PER_VOUCHER', 0),
'hours_per_voucher' => env('HOURS_PER_VOUCHER', 2),
// 'Y-m-d' formatted
'voucher_start' => env('VOUCHER_START', null) ?: null,
'voucher_start' => env('VOUCHER_START') ?: null,
],
// Enables Driving License
'driving_license_enabled' => (bool) env('DRIVING_LICENSE_ENABLED', true),
# Instruction in accordance with § 43 Para. 1 of the German Infection Protection Act (IfSG)
'ifsg_enabled' => (bool) env('IFSG_ENABLED', false),
# Instruction only onsite in accordance with § 43 Para. 1 of the German Infection Protection Act (IfSG)
'ifsg_light_enabled' => (bool) env('IFSG_LIGHT_ENABLED', false)
&& env('IFSG_ENABLED', false),
'ifsg_light_enabled' => env('IFSG_LIGHT_ENABLED', false) && env('IFSG_ENABLED', false),
// Available locales in /resources/lang/
// To disable a locale in the config.php, you can set its value to null
@ -378,11 +404,14 @@ return [
'4XL' => '4XLarge Straight-Cut',
],
// T-shirt Size-Guide link
'tshirt_link' => env('TSHIRT_LINK'),
// Whether to show the current day of the event (-2, -1, 0, 1, 2…) in footer and on the dashboard.
// The event start date has to be set for it to appear.
'enable_show_day_of_event' => false,
'enable_show_day_of_event' => (bool) env('ENABLE_SHOW_DAY_OF_EVENT', false),
// If true there will be a day 0 (-1, 0, 1…). If false there won't (-1, 1…)
'event_has_day0' => true,
'event_has_day0' => (bool) env('EVENT_HAS_DAY0', true),
'metrics' => [
// User work buckets in seconds
@ -418,7 +447,10 @@ return [
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'sameorigin',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Content-Security-Policy' => 'default-src \'self\' \'unsafe-inline\' \'unsafe-eval\'; img-src \'self\' data:;',
'Content-Security-Policy' =>
'default-src \'self\'; '
. ' style-src \'self\' \'unsafe-inline\'; '
. 'img-src \'self\' data:;',
'X-XSS-Protection' => '1; mode=block',
'Feature-Policy' => 'autoplay \'none\'',
//'Strict-Transport-Security' => 'max-age=7776000',

View File

@ -51,6 +51,16 @@ $route->addGroup(
}
);
// User admin settings
$route->addGroup(
'/users/{user_id:\d+}',
function (RouteCollector $route): void {
$route->get('/certificates', 'Admin\\UserSettingsController@certificate');
$route->post('/certificates/ifsg', 'Admin\\UserSettingsController@saveIfsgCertificate');
$route->post('/certificates/driving', 'Admin\\UserSettingsController@saveDrivingLicense');
}
);
// Password recovery
$route->addGroup(
'/password/reset',
@ -187,11 +197,11 @@ $route->addGroup(
$route->addGroup(
'/schedule',
function (RouteCollector $route): void {
$route->get('', 'Admin\\Schedule\\ImportSchedule@index');
$route->get('/edit[/{schedule_id:\d+}]', 'Admin\\Schedule\\ImportSchedule@edit');
$route->post('/edit[/{schedule_id:\d+}]', 'Admin\\Schedule\\ImportSchedule@save');
$route->get('/load/{schedule_id:\d+}', 'Admin\\Schedule\\ImportSchedule@loadSchedule');
$route->post('/import/{schedule_id:\d+}', 'Admin\\Schedule\\ImportSchedule@importSchedule');
$route->get('', 'Admin\\ScheduleController@index');
$route->get('/edit[/{schedule_id:\d+}]', 'Admin\\ScheduleController@edit');
$route->post('/edit[/{schedule_id:\d+}]', 'Admin\\ScheduleController@save');
$route->get('/load/{schedule_id:\d+}', 'Admin\\ScheduleController@loadSchedule');
$route->post('/import/{schedule_id:\d+}', 'Admin\\ScheduleController@importSchedule');
}
);
@ -242,12 +252,12 @@ $route->addGroup(
$route->addGroup(
'/user/{user_id:\d+}',
function (RouteCollector $route): void {
// Shirts
// Goodies
$route->addGroup(
'/goodie',
function (RouteCollector $route): void {
$route->get('', 'Admin\\UserShirtController@editShirt');
$route->post('', 'Admin\\UserShirtController@saveShirt');
$route->get('', 'Admin\\UserGoodieController@editGoodie');
$route->post('', 'Admin\\UserGoodieController@saveGoodie');
}
);

View File

@ -21,9 +21,17 @@ class LicenseFactory extends Factory
$drive_12t = $drive_7_5t && $this->faker->boolean(.3);
$drive_forklift = ($drive_car && $this->faker->boolean(.1))
|| ($drive_12t && $this->faker->boolean(.7));
$drive_confirmed = $this->faker->boolean(0.5) && (
$drive_car
|| $drive_3_5t
|| $drive_7_5t
|| $drive_12t
|| $drive_forklift
);
$ifsg_certificate = $this->faker->boolean(0.1);
$ifsg_certificate_light = $this->faker->boolean(0.5) && !$ifsg_certificate;
$ifsg_confirmed = $this->faker->boolean(0.5) && ($ifsg_certificate || $ifsg_certificate_light);
return [
'user_id' => User::factory(),
@ -33,8 +41,10 @@ class LicenseFactory extends Factory
'drive_3_5t' => $drive_3_5t,
'drive_7_5t' => $drive_7_5t,
'drive_12t' => $drive_12t,
'drive_confirmed' => $drive_confirmed,
'ifsg_certificate' => $ifsg_certificate,
'ifsg_certificate_light' => $ifsg_certificate_light,
'ifsg_confirmed' => $ifsg_confirmed,
];
}
}

View File

@ -21,7 +21,7 @@ class SettingsFactory extends Factory
'theme' => $this->faker->numberBetween(1, 20),
'email_human' => $this->faker->boolean(),
'email_messages' => $this->faker->boolean(),
'email_goody' => $this->faker->boolean(),
'email_goodie' => $this->faker->boolean(),
'email_shiftinfo' => $this->faker->boolean(),
'email_news' => $this->faker->boolean(),
'mobile_show' => $this->faker->boolean(),

View File

@ -25,7 +25,7 @@ class StateFactory extends Factory
'user_info' => $this->faker->optional(.1)->text(),
'active' => $this->faker->boolean(.3),
'force_active' => $this->faker->boolean(.1),
'got_shirt' => $this->faker->boolean(),
'got_goodie' => $this->faker->boolean(),
'got_voucher' => $this->faker->numberBetween(0, 10),
];
}

View File

@ -9,7 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class ChangeUsersContactDectFieldSize extends Migration
{
/** @var array */
protected array $tables = [
'AngelTypes' => 'contact_dect',
'users_contact' => 'dect',

View File

@ -11,7 +11,6 @@ use stdClass;
class CreateRoomsTable extends Migration
{
use ChangesReferences;
use Reference;
/**
* Run the migration

View File

@ -10,8 +10,6 @@ use Illuminate\Database\Schema\Blueprint;
class AddTimestampsToQuestions extends Migration
{
use ChangesReferences;
/**
* Run the migration
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class AddEmailNewsToUsersSettings extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class OauthAddTokens extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class NewsAddIsPinned extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class OauthChangeTokensToText extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -12,8 +12,6 @@ use stdClass;
class CreateFirstUser extends Migration
{
use Reference;
public function __construct(SchemaBuilder $schemaBuilder, protected Config $config)
{
parent::__construct($schemaBuilder);

View File

@ -11,8 +11,6 @@ use stdClass;
class SetAdminPassword extends Migration
{
use Reference;
public function __construct(SchemaBuilder $schemaBuilder, protected Config $config)
{
parent::__construct($schemaBuilder);

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class AddShiftsDescription extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class UsersSettingsAddEmailGoody extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -9,9 +9,6 @@ use stdClass;
class FillPrivilegesAndGroupsRelatedTables extends Migration
{
use ChangesReferences;
use Reference;
/**
* Inserts missing data into permissions & groups related tables
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class AddEmailMessagesToUsersSettings extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Support\Str;
class CleanupShortApiKeys extends Migration
{
@ -14,8 +15,14 @@ class CleanupShortApiKeys extends Migration
public function up(): void
{
$db = $this->schema->getConnection();
$db->table('users')
->where($db->raw('LENGTH(api_key)'), '<=', 42)
->update(['api_key' => '']);
foreach ($db->table('users')->get() as $user) {
if (Str::length($user->api_key) > 42) {
continue;
}
$db->table('users')
->where('id', $user->id)
->update(['api_key' => bin2hex(random_bytes(32))]);
}
}
}

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class AddIfsgCerificatesToUsersLicenses extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class AddRequiresIfsgCerificateToAngeltypes extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class AngeltypesRenameNoSelfSignupToShiftSelfSignup extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class AddHideOnShiftViewToAngeltypes extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -9,8 +9,6 @@ use Illuminate\Database\Schema\Blueprint;
class AddUserInfoToUsersState extends Migration
{
use Reference;
/**
* Run the migration
*/

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateScheduleLocationsTable extends Migration
{
use ChangesReferences;
use Reference;
/**
* Creates the new table
*/
public function up(): void
{
$connection = $this->schema->getConnection();
$this->schema->create('schedule_locations', function (Blueprint $table): void {
$table->increments('id');
$this->references($table, 'schedules');
$this->references($table, 'locations');
$table->index(['schedule_id', 'location_id']);
});
$scheduleLocations = $connection
->table('schedule_shift')
->select(['schedules.id AS schedule_id', 'locations.id AS location_id'])
->leftJoin('schedules', 'schedules.id', 'schedule_shift.schedule_id')
->leftJoin('shifts', 'shifts.id', 'schedule_shift.shift_id')
->leftJoin('locations', 'locations.id', 'shifts.location_id')
->groupBy(['schedules.id', 'locations.id'])
->get();
foreach ($scheduleLocations as $scheduleLocation) {
$connection->table('schedule_locations')
->insert((array) $scheduleLocation);
}
}
/**
* Drops the table
*/
public function down(): void
{
$this->schema->drop('schedule_locations');
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Connection;
use Illuminate\Database\Schema\Builder as SchemaBuilder;
class AddShifttypesEditPermissionAndShifttypesRequiresShico extends Migration
{
protected int $bureaucrat = 80;
protected int $shiCo = 60;
protected int $shifttypes;
protected Connection $db;
public function __construct(SchemaBuilder $schema)
{
parent::__construct($schema);
$this->db = $this->schema->getConnection();
$this->shifttypes = $this->db->table('privileges')
->where('name', 'shifttypes')
->get(['id'])
->first()->id;
}
/**
* Run the migration
*/
public function up(): void
{
$db = $this->schema->getConnection();
$db->table('privileges')
->insert([
'name' => 'shifttypes.edit', 'description' => 'Edit shift types',
]);
$editShifttypes = $db->table('privileges')
->where('name', 'shifttypes.edit')
->get(['id'])
->first();
$this->movePermission($this->shifttypes, $this->bureaucrat, $this->shiCo);
$db->table('group_privileges')
->insertOrIgnore([
'group_id' => $this->bureaucrat, 'privilege_id' => $editShifttypes->id,
]);
}
/**
* Reverse the migration
*/
public function down(): void
{
$db = $this->schema->getConnection();
$db->table('privileges')
->where('name', 'shifttypes.edit')
->delete();
$this->movePermission($this->shifttypes, $this->shiCo, $this->bureaucrat);
}
protected function movePermission(int $privilege, int $oldGroup, int $newGroup): void
{
$this->db->table('group_privileges')
->insertOrIgnore(['group_id' => $newGroup, 'privilege_id' => $privilege]);
$this->db->table('group_privileges')
->where(['group_id' => $oldGroup, 'privilege_id' => $privilege])
->delete();
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddIfsgConfirmedToUsersLicenses extends Migration
{
/**
* Run the migration
*/
public function up(): void
{
$this->schema->table('users_licenses', function (Blueprint $table): void {
$table->boolean('ifsg_confirmed')->default(false)->after('ifsg_certificate');
});
}
/**
* Reverse the migration
*/
public function down(): void
{
$this->schema->table('users_licenses', function (Blueprint $table): void {
$table->dropColumn('ifsg_confirmed');
});
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
class AddUserIfsgEditPermission extends Migration
{
/**
* Run the migration
*/
public function up(): void
{
$db = $this->schema->getConnection();
$db->table('privileges')
->insert([
'name' => 'user.ifsg.edit', 'description' => 'Edit IfSG Certificate',
]);
$editIfsg = $db->table('privileges')
->where('name', 'user.ifsg.edit')
->get(['id'])
->first();
$shico = 60;
$team_coordinator = 65;
$db->table('group_privileges')
->insertOrIgnore([
['group_id' => $shico, 'privilege_id' => $editIfsg->id],
['group_id' => $team_coordinator, 'privilege_id' => $editIfsg->id],
]);
}
/**
* Reverse the migration
*/
public function down(): void
{
$db = $this->schema->getConnection();
$db->table('privileges')
->where('name', 'user.ifsg.edit')
->delete();
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddDriveConfirmedToUsersLicenses extends Migration
{
/**
* Run the migration
*/
public function up(): void
{
$this->schema->table('users_licenses', function (Blueprint $table): void {
$table->boolean('drive_confirmed')->default(false)->after('drive_12t');
});
}
/**
* Reverse the migration
*/
public function down(): void
{
$this->schema->table('users_licenses', function (Blueprint $table): void {
$table->dropColumn('drive_confirmed');
});
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
class AddUserDriveEditPermission extends Migration
{
/**
* Run the migration
*/
public function up(): void
{
$db = $this->schema->getConnection();
$db->table('privileges')
->insert([
'name' => 'user.drive.edit', 'description' => 'Edit Driving License',
]);
$editDrive = $db->table('privileges')
->where('name', 'user.drive.edit')
->get(['id'])
->first();
$shico = 60;
$team_coordinator = 65;
$db->table('group_privileges')
->insertOrIgnore([
['group_id' => $shico, 'privilege_id' => $editDrive->id],
['group_id' => $team_coordinator, 'privilege_id' => $editDrive->id],
]);
}
/**
* Reverse the migration
*/
public function down(): void
{
$db = $this->schema->getConnection();
$db->table('privileges')
->where('name', 'user.drive.edit')
->delete();
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
class AddUserFaEditPermission extends Migration
{
/**
* Run the migration
*/
public function up(): void
{
$db = $this->schema->getConnection();
$db->table('privileges')
->insert([
'name' => 'user.fa.edit', 'description' => 'Edit User Force Active State',
]);
$editFa = $db->table('privileges')
->where('name', 'user.fa.edit')
->get(['id'])
->first();
$bureaucrat = 80;
$db->table('group_privileges')
->insertOrIgnore([
['group_id' => $bureaucrat, 'privilege_id' => $editFa->id],
]);
}
/**
* Reverse the migration
*/
public function down(): void
{
$db = $this->schema->getConnection();
$db->table('privileges')
->where('name', 'user.fa.edit')
->delete();
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Schema\Blueprint;
class RenameGoodyToGoodie extends Migration
{
/**
* Run the migration
*/
public function up(): void
{
$this->schema->table('users_settings', function (Blueprint $table): void {
$table->renameColumn('email_goody', 'email_goodie');
});
}
/**
* Reverse the migration
*/
public function down(): void
{
$this->schema->table('users_settings', function (Blueprint $table): void {
$table->renameColumn('email_goodie', 'email_goody');
});
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Schema\Blueprint;
class RenameShirtToGoodie extends Migration
{
/**
* Run the migration
*/
public function up(): void
{
$this->schema->table('users_state', function (Blueprint $table): void {
$table->renameColumn('got_shirt', 'got_goodie');
});
$db = $this->schema->getConnection();
$db->table('privileges')->where('name', 'admin_active')->update([
'description' => 'Mark angels as active and if they got a goodie.',
]);
$db->table('privileges')->where('name', 'user.edit.shirt')->update([
'name' => 'user.goodie.edit',
'description' => 'Edit user goodies',
]);
}
/**
* Reverse the migration
*/
public function down(): void
{
$this->schema->table('users_state', function (Blueprint $table): void {
$table->renameColumn('got_goodie', 'got_shirt');
});
$db = $this->schema->getConnection();
$db->table('privileges')->where('name', 'admin_active')->update([
'description' => 'Mark angels as active and if they got a t-shirt.',
]);
$db->table('privileges')->where('name', 'user.goodie.edit')->update([
'name' => 'user.edit.shirt',
'description' => 'Edit user shirts',
]);
}
}

View File

@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Connection;
use Illuminate\Database\Schema\Builder as SchemaBuilder;
class RefactorPermissionsAndGroups extends Migration
{
protected int $developer = 90;
protected int $bureaucrat = 80;
protected int $shiCo = 60;
protected int $newsAdmin = 85;
protected int $teamCoordinator = 65;
protected int $angel = 20;
protected int $active;
protected int $driveEdit;
protected int $eventConfig;
protected int $goodieEdit;
protected int $ifsgEdit;
protected int $log;
protected int $news;
protected int $register;
protected int $scheduleImport;
protected int $shifts;
protected int $user;
protected int $userAngeltypes;
protected int $userShifts;
protected string $shiftentry = 'shiftentry_edit_angeltype_supporter';
protected string $language = 'admin_language';
protected string $userEdit = 'user.edit';
protected string $userNickEdit = 'user.nick.edit';
protected string $shifttypes = 'shifttypes';
protected string $shifttypesView = 'shifttypes.view';
protected Connection $db;
public function __construct(SchemaBuilder $schema)
{
parent::__construct($schema);
$this->db = $this->schema->getConnection();
$this->active = $this->getPrivilegeId('admin_active');
$this->driveEdit = $this->getPrivilegeId('user.drive.edit');
$this->eventConfig = $this->getPrivilegeId('admin_event_config');
$this->goodieEdit = $this->getPrivilegeId('user.goodie.edit');
$this->ifsgEdit = $this->getPrivilegeId('user.ifsg.edit');
$this->log = $this->getPrivilegeId('admin_log');
$this->news = $this->getPrivilegeId('admin_news');
$this->register = $this->getPrivilegeId('register');
$this->scheduleImport = $this->getPrivilegeId('schedule.import');
$this->shifts = $this->getPrivilegeId('admin_shifts');
$this->user = $this->getPrivilegeId('admin_user');
$this->userAngeltypes = $this->getPrivilegeId('admin_user_angeltypes');
$this->userShifts = $this->getPrivilegeId('user_shifts_admin');
}
/**
* Run the migration
*/
public function up(): void
{
$this->deletePermission($this->shiftentry);
$this->deletePermission($this->language);
$this->movePermission($this->active, $this->bureaucrat, $this->shiCo);
$this->movePermission($this->userAngeltypes, $this->bureaucrat, $this->shiCo);
$this->movePermission($this->eventConfig, $this->shiCo, $this->developer);
$this->movePermission($this->goodieEdit, $this->bureaucrat, $this->shiCo);
$this->insertGroupPermission($this->log, $this->bureaucrat);
$this->deleteGroupPermission($this->news, $this->bureaucrat);
$this->deleteGroupPermission($this->shifts, $this->bureaucrat);
$this->deleteGroupPermission($this->user, $this->bureaucrat);
$this->deleteGroupPermission($this->register, $this->bureaucrat);
$this->deleteGroupPermission($this->scheduleImport, $this->developer);
$this->updatePermission($this->shifttypes, $this->shifttypesView, 'View shift types');
$this->updatePermission($this->userEdit, $this->userNickEdit, 'Edit user nick');
$this->deleteGroup($this->newsAdmin);
$this->deleteGroup($this->teamCoordinator);
}
/**
* Reverse the migration
*/
public function down(): void
{
$this->insertPermission(
$this->shiftentry,
'If user with this privilege is angeltype supporter, he can put users in shifts for their angeltype',
$this->angel
);
$this->insertPermission(
$this->language,
'Translate the system',
$this->developer
);
$this->movePermission($this->active, $this->shiCo, $this->bureaucrat);
$this->movePermission($this->userAngeltypes, $this->shiCo, $this->bureaucrat);
$this->movePermission($this->eventConfig, $this->developer, $this->shiCo);
$this->movePermission($this->goodieEdit, $this->shiCo, $this->bureaucrat);
$this->deleteGroupPermission($this->log, $this->bureaucrat);
$this->insertGroupPermission($this->news, $this->bureaucrat);
$this->insertGroupPermission($this->shifts, $this->bureaucrat);
$this->insertGroupPermission($this->user, $this->bureaucrat);
$this->insertGroupPermission($this->register, $this->bureaucrat);
$this->insertGroupPermission($this->scheduleImport, $this->developer);
$this->updatePermission($this->shifttypesView, $this->shifttypes, 'Administrate shift types');
$this->updatePermission($this->userNickEdit, $this->userEdit, 'Edit user');
$this->insertGroup($this->newsAdmin, 'News Admin', [$this->news]);
$this->insertGroup($this->teamCoordinator, 'Team Coordinator', [
$this->news,
$this->userAngeltypes,
$this->driveEdit,
$this->ifsgEdit,
$this->userShifts,
]);
}
protected function getPrivilegeId(string $privilege): int
{
return $this->db->table('privileges')
->where('name', $privilege)
->get(['id'])
->first()->id;
}
protected function deleteGroup(int $group): void
{
$this->db->table('groups')
->where(['id' => $group])
->delete();
}
protected function insertGroup(int $id, string $name, array $privileges): void
{
$this->db->table('groups')
->insertOrIgnore([
'name' => $name,
'id' => $id,
]);
foreach ($privileges as $privilege) {
$this->insertGroupPermission($privilege, $id);
}
}
protected function deleteGroupPermission(int $privilege, int $group): void
{
$this->db->table('group_privileges')
->where(['group_id' => $group, 'privilege_id' => $privilege])
->delete();
}
protected function insertGroupPermission(int $privilege, int $group): void
{
$this->db->table('group_privileges')
->insertOrIgnore([
['group_id' => $group, 'privilege_id' => $privilege],
]);
}
protected function movePermission(int $privilege, int $oldGroup, int $newGroup): void
{
$this->insertGroupPermission($privilege, $newGroup);
$this->deleteGroupPermission($privilege, $oldGroup);
}
protected function insertPermission(string $name, string $description, int $group): void
{
$this->db->table('privileges')
->insertOrIgnore([
'name' => $name, 'description' => $description,
]);
$permission = $this->getPrivilegeId($name);
$this->insertGroupPermission($permission, $group);
}
protected function deletePermission(string $privilege): void
{
$this->db->table('privileges')
->where(['name' => $privilege])
->delete();
}
protected function updatePermission(string $oldName, string $newName, string $description): void
{
$this->db->table('privileges')->where('name', $oldName)->update([
'name' => $newName,
'description' => $description,
]);
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Connection;
use Illuminate\Database\Schema\Builder as SchemaBuilder;
class AddUsersArriveListPermission extends Migration
{
protected int $voucher = 35;
protected int $arrive;
protected Connection $db;
public function __construct(SchemaBuilder $schema)
{
parent::__construct($schema);
$this->db = $this->schema->getConnection();
$this->arrive = $this->db->table('privileges')
->where('name', 'admin_arrive')
->get(['id'])
->first()->id;
}
/**
* Run the migration
*/
public function up(): void
{
$this->db->table('privileges')
->insert([
'name' => 'users.arrive.list', 'description' => 'View arrive angels list',
]);
$arriveList = $this->db->table('privileges')
->where('name', 'users.arrive.list')
->get(['id'])
->first()->id;
// Goodie Manager, Shift Coordinator, Voucher Angel, Welcome Angel
$groups = [50, 60, 35, 30];
foreach ($groups as $group) {
$this->db->table('group_privileges')
->insertOrIgnore([
['group_id' => $group, 'privilege_id' => $arriveList],
]);
}
$this->db->table('group_privileges')
->where(['group_id' => $this->voucher, 'privilege_id' => $this->arrive])
->delete();
}
/**
* Reverse the migration
*/
public function down(): void
{
$this->db->table('privileges')
->where('name', 'users.arrive.list')
->delete();
$this->db->table('group_privileges')
->insertOrIgnore([
['group_id' => $this->voucher, 'privilege_id' => $this->arrive],
]);
}
}

View File

@ -24,7 +24,7 @@ ENV TRUSTED_PROXIES 10.0.0.0/8,::ffff:10.0.0.0/8,\
# Engelsystem development workspace
# Contains all tools required to build / manage the system
FROM es_base AS es_workspace
RUN echo 'memory_limit = 512M' > /usr/local/etc/php/conf.d/docker-php.ini
RUN echo 'memory_limit = 1024M' > /usr/local/etc/php/conf.d/docker-php.ini
RUN apk add --no-cache gettext git nodejs npm yarn
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
ENTRYPOINT php -r 'sleep(PHP_INT_MAX);'

View File

@ -20,10 +20,7 @@ services:
APP_NAME: Engelsystem DEV
env_file: deployment.env
ports:
- "5080:80"
networks:
- database
- internet
- "127.0.0.1:5080:80"
depends_on:
- es_database
es_workspace:
@ -44,11 +41,17 @@ services:
ENVIRONMENT: development
MAIL_DRIVER: log
APP_NAME: Engelsystem DEV
networks:
- database
- internet
depends_on:
- es_database
es_phpmyadmin:
image: phpmyadmin
environment:
PMA_HOST: es_database
ports:
- "127.0.0.1:8888:80"
depends_on:
- es_database
profiles: [ dev ]
es_database:
image: mariadb:10.2
environment:
@ -59,12 +62,5 @@ services:
MYSQL_INITDB_SKIP_TZINFO: "yes"
volumes:
- db:/var/lib/mysql
networks:
- database
volumes:
db: {}
networks:
database:
internal: true
internet:

View File

@ -136,12 +136,16 @@ function angeltype_edit_controller()
if ($valid) {
$angeltype->save();
success('Angel type saved.');
success(__('Angel type saved.'));
engelsystem_log(
'Saved angeltype: ' . $angeltype->name . ($angeltype->restricted ? ', restricted' : '')
. ($angeltype->shift_self_signup ? ', shift_self_signup' : '')
. ($angeltype->requires_driver_license ? ', requires driver license' : '') . ', '
. ($angeltype->requires_ifsg_certificate ? ', requires ifsg certificate' : '') . ', '
. (config('driving_license_enabled')
? (($angeltype->requires_driver_license ? ', requires driver license' : '') . ', ')
: '')
. (config('ifsg_enabled')
? (($angeltype->requires_ifsg_certificate ? ', requires ifsg certificate' : '') . ', ')
: '')
. $angeltype->contact_name . ', '
. $angeltype->contact_dect . ', '
. $angeltype->contact_email . ', '
@ -175,7 +179,9 @@ function angeltype_controller()
$angeltype = AngelType::findOrFail(request()->input('angeltype_id'));
/** @var UserAngelType $user_angeltype */
$user_angeltype = UserAngelType::whereUserId($user->id)->where('angel_type_id', $angeltype->id)->first();
$members = $angeltype->userAngelTypes->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE);
$members = $angeltype->userAngelTypes
->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE)
->load(['state', 'personalData', 'contact']);
$days = angeltype_controller_shiftsFilterDays($angeltype);
$shiftsFilter = angeltype_controller_shiftsFilter($angeltype, $days);
if (request()->input('showFilledShifts')) {
@ -323,7 +329,7 @@ function angeltypes_list_controller()
$actions[] = button(
url('/user_angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id]),
icon('box-arrow-in-right') . ($admin_angeltypes ? '' : __('Join')),
'btn-sm',
'btn-sm' . ($admin_angeltypes ? ' btn-success' : ''),
'',
($admin_angeltypes ? __('Join') : '')
);

View File

@ -210,7 +210,7 @@ function shift_entry_create_controller_user(Shift $shift, AngelType $angeltype):
$request = request();
$signup_user = auth()->user();
$needed_angeltype = (new AngelType())->forceFill(NeededAngeltype_by_Shift_and_Angeltype($shift, $angeltype));
$needed_angeltype = (new AngelType())->forceFill(NeededAngeltype_by_Shift_and_Angeltype($shift, $angeltype) ?: []);
$shift_entries = $shift->shiftEntries()
->where('angel_type_id', $angeltype->id)
->get();
@ -307,9 +307,8 @@ function shift_entry_load()
if (!$request->has('shift_entry_id') || !test_request_int('shift_entry_id')) {
throw_redirect(url('/user-shifts'));
}
$shiftEntry = ShiftEntry::findOrFail($request->input('shift_entry_id'));
return $shiftEntry;
return ShiftEntry::findOrFail($request->input('shift_entry_id'));
}
/**

View File

@ -1,5 +1,8 @@
<?php
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Http\Exceptions\HttpNotFound;
use Engelsystem\Http\Redirector;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Location;
use Engelsystem\Models\Shifts\NeededAngelType;
@ -8,6 +11,7 @@ use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftType;
use Engelsystem\Models\Shifts\ShiftSignupStatus;
use Engelsystem\ShiftSignupState;
use Illuminate\Support\Str;
/**
* @param array|Shift $shift
@ -23,15 +27,6 @@ function shift_link($shift)
return url('/shifts', $parameters);
}
/**
* @param Shift $shift
* @return string
*/
function shift_delete_link(Shift $shift)
{
return url('/user-shifts', ['delete_shift' => $shift->id]);
}
/**
* @param Shift $shift
* @return string
@ -200,7 +195,7 @@ function shift_edit_controller()
htmlspecialchars($angeltype_name),
$needed_angel_types[$angeltype_id],
[],
ScheduleShift::whereShiftId($shift->id)->first() ? true : false,
(bool) ScheduleShift::whereShiftId($shift->id)->first(),
);
}
@ -231,70 +226,42 @@ function shift_edit_controller()
);
}
/**
* @return string
*/
function shift_delete_controller()
function shift_delete_controller(): void
{
$request = request();
// Only accessible for admins / ShiCos with user_shifts_admin privileg
if (!auth()->can('user_shifts_admin')) {
throw_redirect(url('/user-shifts'));
throw new HttpForbidden();
}
// Schicht komplett löschen (nur für admins/user mit user_shifts_admin privileg)
if (!$request->has('delete_shift') || !preg_match('/^\d+$/', $request->input('delete_shift'))) {
throw_redirect(url('/user-shifts'));
// Must contain shift id and confirmation
if (!$request->has('delete_shift') || !$request->hasPostData('delete')) {
throw new HttpNotFound();
}
$shift_id = $request->input('delete_shift');
$shift = Shift::findOrFail($shift_id);
$shift = Shift($shift_id);
if (empty($shift)) {
throw_redirect(url('/user-shifts'));
}
event('shift.deleting', ['shift' => $shift]);
// Schicht löschen bestätigt
if ($request->hasPostData('delete')) {
foreach ($shift->shiftEntries as $entry) {
event('shift.entry.deleting', [
'user' => $entry->user,
'start' => $shift->start,
'end' => $shift->end,
'name' => $shift->shiftType->name,
'title' => $shift->title,
'type' => $entry->angelType->name,
'location' => $shift->location,
'freeloaded' => $entry->freeloaded,
]);
}
$shift->delete();
$shift->delete();
engelsystem_log(
'Deleted shift ' . $shift->title . ': ' . $shift->shiftType->name
. ' from ' . $shift->start->format('Y-m-d H:i')
. ' to ' . $shift->end->format('Y-m-d H:i')
);
success(__('Shift deleted.'));
throw_redirect(url('/user-shifts'));
}
$link = button(url('/shifts', ['action' => 'view', 'shift_id' => $shift_id]), icon('chevron-left'), 'btn-sm', '', __('general.back'));
return page_with_title(
$link . ' ' . shifts_title(),
[
error(sprintf(
__('Do you want to delete the shift %s from %s to %s?'),
$shift->shiftType->name,
$shift->start->format(__('general.datetime')),
$shift->end->format(__('H:i'))
), true),
form([
form_hidden('delete_shift', $shift->id),
form_submit('delete', icon('trash') . __('form.delete'), '', true, 'danger'),
]),
]
engelsystem_log(
'Deleted shift ' . $shift->title . ': ' . $shift->shiftType->name
. ' from ' . $shift->start->format('Y-m-d H:i')
. ' to ' . $shift->end->format('Y-m-d H:i')
);
success(__('Shift deleted.'));
/** @var Redirector $redirect */
$redirect = app('redirect');
$old = $redirect->back()->getHeaderLine('location');
if (Str::contains($old, '/shifts') && Str::contains($old, 'action=view')) {
throw_redirect(url('/user-shifts'));
}
throw_redirect($old);
}
/**

View File

@ -231,8 +231,9 @@ function user_angeltype_delete_controller(): array
$user_angeltype = UserAngelType::findOrFail($request->input('user_angeltype_id'));
$angeltype = $user_angeltype->angelType;
$user_source = $user_angeltype->user;
$isOwnAngelType = $user->id == $user_source->id;
if (
$user->id != $user_angeltype->user_id
!$isOwnAngelType
&& !$user->isAngelTypeSupporter($angeltype)
&& !auth()->can('admin_user_angeltypes')
) {
@ -243,15 +244,15 @@ function user_angeltype_delete_controller(): array
if ($request->hasPostData('delete')) {
$user_angeltype->delete();
engelsystem_log(sprintf('User %s removed from %s.', User_Nick_render($user_source, true), $angeltype->name));
success(sprintf(__('User %s removed from %s.'), $user_source->displayName, $angeltype->name));
engelsystem_log(sprintf('User "%s" removed from "%s".', User_Nick_render($user_source, true), $angeltype->name));
success(sprintf($isOwnAngelType ? __('You successfully left "%2$s".') : __('User "%s" removed from "%s".'), $user_source->displayName, $angeltype->name));
throw_redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]));
}
return [
__('Remove angeltype'),
UserAngelType_delete_view($user_angeltype, $user_source, $angeltype),
__('Leave angeltype'),
UserAngelType_delete_view($user_angeltype, $user_source, $angeltype, $isOwnAngelType),
];
}
@ -265,7 +266,7 @@ function user_angeltype_update_controller(): array
$supporter = false;
$request = request();
if (!auth()->can('admin_angel_types')) {
if (!auth()->can('admin_angel_types') && !config('supporters_can_promote')) {
error(__('You are not allowed to set supporter rights.'));
throw_redirect(url('/angeltypes'));
}

View File

@ -1,12 +1,14 @@
<?php
use Engelsystem\Database\Db;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Shifts\ShiftEntry;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User;
use Engelsystem\ShiftCalendarRenderer;
use Engelsystem\ShiftsFilter;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
/**
@ -204,7 +206,7 @@ function user_controller()
}
}
$shifts = Shifts_by_user($user_source->id, auth()->can('user_shifts_admin'));
$shifts = Shifts_by_user($user_source->id, true);
foreach ($shifts as $shift) {
// TODO: Move queries to model
$shift->needed_angeltypes = Db::select(
@ -237,12 +239,27 @@ function user_controller()
auth()->resetApiKey($user_source);
}
if ($user_source->state->force_active) {
$tshirt_score = __('Enough');
} else {
$tshirt_score = sprintf('%.2f', User_tshirt_score($user_source->id)) . '&nbsp;h';
$goodie_score = sprintf('%.2f', User_goodie_score($user_source->id)) . '&nbsp;h';
if ($user_source->state->force_active && config('enable_force_active')) {
$goodie_score = '<span title="' . $goodie_score . '">' . __('Enough') . '</span>';
}
$worklogs = $user_source->worklogs()
->with(['user', 'creator'])
->get();
$is_ifsg_supporter = (bool) AngelType::whereRequiresIfsgCertificate(true)
->leftJoin('user_angel_type', 'user_angel_type.angel_type_id', 'angel_types.id')
->where('user_angel_type.user_id', $user->id)
->where('user_angel_type.supporter', true)
->count();
$is_drive_supporter = (bool) AngelType::whereRequiresDriverLicense(true)
->leftJoin('user_angel_type', 'user_angel_type.angel_type_id', 'angel_types.id')
->where('user_angel_type.user_id', $user->id)
->where('user_angel_type.supporter', true)
->count();
return [
htmlspecialchars($user_source->displayName),
User_view(
@ -253,10 +270,14 @@ function user_controller()
$user_source->groups,
$shifts,
$user->id == $user_source->id,
$tshirt_score,
auth()->can('admin_active'),
$goodie_score,
auth()->can('user.goodie.edit'),
auth()->can('admin_user_worklog'),
UserWorkLogsForUser($user_source->id)
$worklogs,
auth()->can('user.ifsg.edit')
|| $is_ifsg_supporter
|| auth()->can('user.drive.edit')
|| $is_drive_supporter,
),
];
}
@ -286,7 +307,7 @@ function users_list_controller()
'freeloads',
'active',
'force_active',
'got_shirt',
'got_goodie',
'shirt_size',
'planned_arrival_date',
'planned_departure_date',
@ -297,13 +318,15 @@ function users_list_controller()
}
/** @var User[]|Collection $users */
$users = User::with(['contact', 'personalData', 'state'])
$users = User::with(['contact', 'personalData', 'state', 'shiftEntries' => function (HasMany $query) {
$query->where('freeloaded', true);
}])
->orderBy('name')
->get();
foreach ($users as $user) {
$user->setAttribute(
'freeloads',
$user->shiftEntries()
$user->shiftEntries
->where('freeloaded', true)
->count()
);
@ -328,7 +351,7 @@ function users_list_controller()
State::whereActive(true)->count(),
State::whereForceActive(true)->count(),
ShiftEntry::whereFreeloaded(true)->count(),
State::whereGotShirt(true)->count(),
State::whereGotGoodie(true)->count(),
State::query()->sum('got_voucher')
),
];
@ -449,7 +472,7 @@ function user_driver_license_required_hint()
$user = auth()->user();
// User has already entered data, no hint needed.
if ($user->license->wantsToDrive()) {
if (!config('driving_license_enabled') || $user->license->wantsToDrive()) {
return null;
}

View File

@ -4,6 +4,7 @@
* Bootstrap application
*/
use Engelsystem\Application;
use Engelsystem\Http\UrlGeneratorInterface;
require __DIR__ . '/application.php';
@ -18,7 +19,7 @@ require __DIR__ . '/includes.php';
/**
* Check for maintenance
*/
/** @var \Engelsystem\Application $app */
/** @var Application $app */
if ($app->get('config')->get('maintenance')) {
http_response_code(503);
$url = $app->get(UrlGeneratorInterface::class);

View File

@ -14,9 +14,6 @@ function theme_id(): int
return $globals['themeId'];
}
/**
* @return array
*/
function theme(): array
{
$theme_id = theme_id();

View File

@ -1,135 +0,0 @@
<?php
namespace Engelsystem\Events\Listener;
use Carbon\Carbon;
use Engelsystem\Helpers\Shifts;
use Engelsystem\Mail\EngelsystemMailer;
use Engelsystem\Models\Location;
use Engelsystem\Models\Shifts\Shift as ShiftModel;
use Engelsystem\Models\Shifts\ShiftEntry;
use Engelsystem\Models\User\User;
use Engelsystem\Models\Worklog;
use Illuminate\Database\Eloquent\Collection;
use Psr\Log\LoggerInterface;
class Shift
{
public function __construct(
protected LoggerInterface $log,
protected EngelsystemMailer $mailer
) {
}
public function deletedEntryCreateWorklog(
User $user,
Carbon $start,
Carbon $end,
string $name,
string $title,
string $type,
Location $location,
bool $freeloaded
): void {
if ($freeloaded || $start > Carbon::now()) {
return;
}
$workLog = new Worklog();
$workLog->user()->associate($user);
$workLog->creator()->associate(auth()->user());
$workLog->worked_at = $start->copy()->startOfDay();
$workLog->hours =
(($end->timestamp - $start->timestamp) / 60 / 60)
* Shifts::getNightShiftMultiplier($start, $end);
$workLog->comment = sprintf(
__('%s (%s as %s) in %s, %s - %s'),
$name,
$title,
$type,
$location->name,
$start->format(__('general.datetime')),
$end->format(__('general.datetime'))
);
$workLog->save();
$this->log->info(
'Created worklog entry from shift for {user} ({uid}): {worklog})',
['user' => $workLog->user->name, 'uid' => $workLog->user->id, 'worklog' => $workLog->comment]
);
}
public function deletedEntrySendEmail(
User $user,
Carbon $start,
Carbon $end,
string $name,
string $title,
string $type,
Location $location,
bool $freeloaded
): void {
if (!$user->settings->email_shiftinfo) {
return;
}
$this->mailer->sendViewTranslated(
$user,
'notification.shift.deleted',
'emails/worklog-from-shift',
[
'name' => $name,
'title' => $title,
'start' => $start,
'end' => $end,
'location' => $location,
'freeloaded' => $freeloaded,
'username' => $user->displayName,
]
);
}
public function updatedShiftSendEmail(
ShiftModel $shift,
ShiftModel $oldShift
): void {
// Only send e-mail on relevant changes
if (
$oldShift->shift_type_id == $shift->shift_type_id
&& $oldShift->title == $shift->title
&& $oldShift->start == $shift->start
&& $oldShift->end == $shift->end
&& $oldShift->location_id == $shift->location_id
) {
return;
}
$shift->load(['shiftType', 'location']);
$oldShift->load(['shiftType', 'location']);
/** @var ShiftEntry[]|Collection $shiftEntries */
$shiftEntries = $shift->shiftEntries()
->with(['angelType', 'user.settings'])
->get();
foreach ($shiftEntries as $shiftEntry) {
$user = $shiftEntry->user;
$angelType = $shiftEntry->angelType;
if (!$user->settings->email_shiftinfo || $shift->end < Carbon::now()) {
continue;
}
$this->mailer->sendViewTranslated(
$user,
'notification.shift.updated',
'emails/updated-shift',
[
'shift' => $shift,
'oldShift' => $oldShift,
'angelType' => $angelType,
'username' => $user->displayName,
]
);
}
}
}

View File

@ -18,7 +18,6 @@ $includeFiles = [
__DIR__ . '/../includes/model/ShiftSignupState.php',
__DIR__ . '/../includes/model/Stats.php',
__DIR__ . '/../includes/model/User_model.php',
__DIR__ . '/../includes/model/UserWorkLog_model.php',
__DIR__ . '/../includes/model/ValidationResult.php',
__DIR__ . '/../includes/view/AngelTypes_view.php',
@ -47,7 +46,6 @@ $includeFiles = [
__DIR__ . '/../includes/helper/legacy_helper.php',
__DIR__ . '/../includes/helper/message_helper.php',
__DIR__ . '/../includes/helper/email_helper.php',
__DIR__ . '/../includes/helper/shift_helper.php',
__DIR__ . '/../includes/mailer/shifts_mailer.php',
__DIR__ . '/../includes/mailer/users_mailer.php',
@ -60,8 +58,6 @@ $includeFiles = [
__DIR__ . '/../includes/pages/admin_user.php',
__DIR__ . '/../includes/pages/user_myshifts.php',
__DIR__ . '/../includes/pages/user_shifts.php',
__DIR__ . '/../includes/pages/schedule/ImportSchedule.php',
];
foreach ($includeFiles as $file) {

View File

@ -179,14 +179,6 @@ class ShiftsFilter
$this->locations = $locations;
}
/**
* @return bool
*/
public function isUserShiftsAdmin()
{
return $this->userShiftsAdmin;
}
/**
* @param bool $userShiftsAdmin
*/

View File

@ -116,7 +116,7 @@ function Shifts_free($start, $end, ShiftsFilter $filter = null)
$shifts = collect($shifts);
return Shift::query()
return Shift::with(['location', 'shiftType'])
->whereIn('id', $shifts->pluck('id')->toArray())
->orderBy('shifts.start')
->get();
@ -189,12 +189,14 @@ function Shifts_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
]
);
$shifts = [];
$shifts = new Collection();
foreach ($shiftsData as $shift) {
$shifts[] = (new Shift())->forceFill($shift);
}
return collect($shifts);
$shifts->load(['location', 'shiftType']);
return $shifts;
}
/**
@ -354,7 +356,7 @@ function NeededAngeltype_by_Shift_and_Angeltype(Shift $shift, AngelType $angelty
*/
function ShiftEntries_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
{
return ShiftEntry::with('user')
return ShiftEntry::with('user', 'user.state')
->join('shifts', 'shifts.id', 'shift_entries.shift_id')
->whereIn('shifts.location_id', $shiftsFilter->getLocations())
->whereBetween('start', [$shiftsFilter->getStart(), $shiftsFilter->getEnd()])
@ -428,14 +430,6 @@ function Shift_signup_allowed_angel(
) {
$free_entries = Shift_free_entries($needed_angeltype, $shift_entries);
if (config('signup_requires_arrival') && !$user->state->arrived) {
return new ShiftSignupState(ShiftSignupStatus::NOT_ARRIVED, $free_entries);
}
if (config('signup_advance_hours') && $shift->start->timestamp > time() + config('signup_advance_hours') * 3600) {
return new ShiftSignupState(ShiftSignupStatus::NOT_YET, $free_entries);
}
if (is_null($user_shifts) || $user_shifts->isEmpty()) {
$user_shifts = Shifts_by_user($user->id);
}
@ -487,6 +481,14 @@ function Shift_signup_allowed_angel(
return new ShiftSignupState(ShiftSignupStatus::COLLIDES, $free_entries);
}
if (config('signup_advance_hours') && $shift->start->timestamp > time() + config('signup_advance_hours') * 3600) {
return new ShiftSignupState(ShiftSignupStatus::NOT_YET, $free_entries);
}
if (config('signup_requires_arrival') && !$user->state->arrived) {
return new ShiftSignupState(ShiftSignupStatus::NOT_ARRIVED, $free_entries);
}
// Hooray, shift is free for you!
return new ShiftSignupState(ShiftSignupStatus::FREE, $free_entries);
}
@ -546,13 +548,12 @@ function Shift_signout_allowed(Shift $shift, AngelType $angeltype, $signout_user
// angeltype supporter can sign out any user at any time from their supported angeltype
if (
auth()->can('shiftentry_edit_angeltype_supporter')
&& ($user->isAngelTypeSupporter($angeltype) || auth()->can('admin_user_angeltypes'))
$user->isAngelTypeSupporter($angeltype) || auth()->can('admin_user_angeltypes')
) {
return true;
}
if ($signout_user_id == $user->id && $shift->start->timestamp > time() + config('last_unsubscribe') * 3600) {
if ($signout_user_id == $user->id && $shift->start->subHours(config('last_unsubscribe')) > Carbon::now()) {
return true;
}
@ -585,8 +586,7 @@ function Shift_signup_allowed(
}
if (
auth()->can('shiftentry_edit_angeltype_supporter')
&& (auth()->user()->isAngelTypeSupporter($angeltype) || auth()->can('admin_user_angeltypes'))
auth()->user()->isAngelTypeSupporter($angeltype) || auth()->can('admin_user_angeltypes')
) {
return Shift_signup_allowed_angeltype_supporter($needed_angeltype, $shift_entries);
}
@ -638,12 +638,13 @@ function Shifts_by_user($userId, $include_freeloaded_comments = false)
]
);
$shifts = [];
$shifts = new Collection();
foreach ($shiftsData as $data) {
$shifts[] = (new Shift())->forceFill($data);
}
$shifts->load(['shiftType', 'location']);
return collect($shifts);
return $shifts;
}
/**

View File

@ -1,23 +0,0 @@
<?php
use Carbon\Carbon;
use Engelsystem\Models\Worklog;
use Illuminate\Support\Collection;
/**
* Returns all work log entries for a user.
*
* @param int $userId
* @param Carbon|null $sinceTime
*
* @return Worklog[]|Collection
*/
function UserWorkLogsForUser($userId, Carbon $sinceTime = null)
{
$worklogs = Worklog::whereUserId($userId);
if ($sinceTime) {
$worklogs = $worklogs->whereDate('worked_at', '>=', $sinceTime);
}
return $worklogs->get();
}

View File

@ -5,7 +5,6 @@ use Engelsystem\Database\Db;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\User\User;
use Engelsystem\Models\Worklog;
use Engelsystem\ValidationResult;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
@ -14,17 +13,17 @@ use Illuminate\Support\Collection;
*/
/**
* Returns the tshirt score (number of hours counted for tshirt).
* Returns the goodie score (number of hours counted for tshirt).
* Accounts only ended shifts.
*
* @param int $userId
* @return int
* @return float
*/
function User_tshirt_score($userId)
function User_goodie_score(int $userId): float
{
$shift_sum_formula = User_get_shifts_sum_query();
$result_shifts = Db::selectOne(sprintf('
SELECT ROUND((%s) / 3600, 2) AS `tshirt_score`
SELECT ROUND((%s) / 3600, 2) AS `goodie_score`
FROM `users` LEFT JOIN `shift_entries` ON `users`.`id` = `shift_entries`.`user_id`
LEFT JOIN `shifts` ON `shift_entries`.`shift_id` = `shifts`.`id`
WHERE `users`.`id` = ?
@ -33,8 +32,8 @@ function User_tshirt_score($userId)
', $shift_sum_formula), [
$userId,
]);
if (!isset($result_shifts['tshirt_score'])) {
$result_shifts = ['tshirt_score' => 0];
if (!isset($result_shifts['goodie_score'])) {
$result_shifts = ['goodie_score' => 0];
}
$worklogHours = Worklog::query()
@ -42,7 +41,7 @@ function User_tshirt_score($userId)
->where('worked_at', '<=', Carbon::Now())
->sum('hours');
return $result_shifts['tshirt_score'] + $worklogHours;
return $result_shifts['goodie_score'] + $worklogHours;
}
/**
@ -67,76 +66,6 @@ function Users_by_angeltype_inverted(AngelType $angeltype)
->get();
}
/**
* Validate the planned arrival date
*
* @param int $planned_arrival_date Unix timestamp
* @return ValidationResult
*/
function User_validate_planned_arrival_date($planned_arrival_date)
{
if (is_null($planned_arrival_date)) {
// null is not okay
return new ValidationResult(false, time());
}
$config = config();
$buildup = $config->get('buildup_start');
$teardown = $config->get('teardown_end');
/** @var Carbon $buildup */
if (!empty($buildup) && Carbon::createFromTimestamp($planned_arrival_date)->lessThan($buildup->setTime(0, 0))) {
// Planned arrival can not be before buildup start date
return new ValidationResult(false, $buildup->getTimestamp());
}
/** @var Carbon $teardown */
if (!empty($teardown) && Carbon::createFromTimestamp($planned_arrival_date)->greaterThanOrEqualTo($teardown->addDay()->setTime(0, 0))) {
// Planned arrival can not be after teardown end date
return new ValidationResult(false, $teardown->getTimestamp());
}
return new ValidationResult(true, $planned_arrival_date);
}
/**
* Validate the planned departure date
*
* @param int $planned_arrival_date Unix timestamp
* @param int $planned_departure_date Unix timestamp
* @return ValidationResult
*/
function User_validate_planned_departure_date($planned_arrival_date, $planned_departure_date)
{
if (is_null($planned_departure_date)) {
// null is okay
return new ValidationResult(true, null);
}
if ($planned_arrival_date > $planned_departure_date) {
// departure cannot be before arrival
return new ValidationResult(false, $planned_arrival_date);
}
$config = config();
$buildup = $config->get('buildup_start');
$teardown = $config->get('teardown_end');
/** @var Carbon $buildup */
if (!empty($buildup) && Carbon::createFromTimestamp($planned_departure_date)->lessThan($buildup->setTime(0, 0))) {
// Planned departure can not be before buildup start date
return new ValidationResult(false, $buildup->getTimestamp());
}
/** @var Carbon $teardown */
if (!empty($teardown) && Carbon::createFromTimestamp($planned_departure_date)->greaterThanOrEqualTo($teardown->addDay()->setTime(0, 0))) {
// Planned departure can not be after teardown end date
return new ValidationResult(false, $teardown->getTimestamp());
}
return new ValidationResult(true, $planned_departure_date);
}
/**
* @param User $user
* @return float
@ -149,7 +78,10 @@ function User_get_eligable_voucher_count($user)
: null;
$shiftEntries = ShiftEntries_finished_by_user($user, $start);
$worklog = UserWorkLogsForUser($user->id, $start);
$worklog = $user->worklogs()
->whereDate('worked_at', '>=', $start ?: 0)
->with(['user', 'creator'])
->get();
$shifts_done =
count($shiftEntries)
+ $worklog->count();
@ -191,13 +123,20 @@ function User_get_shifts_sum_query()
return 'COALESCE(SUM(UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start)), 0)';
}
/* @see \Engelsystem\Models\Shifts\Shift::isNightShift to keep it in sync */
return sprintf(
'
COALESCE(SUM(
(1 + (
(HOUR(shifts.end) > %1$d AND HOUR(shifts.end) < %2$d)
OR (HOUR(shifts.start) > %1$d AND HOUR(shifts.start) < %2$d)
OR (HOUR(shifts.start) <= %1$d AND HOUR(shifts.end) >= %2$d)
/* Starts during night */
HOUR(shifts.start) >= %1$d AND HOUR(shifts.start) < %2$d
/* Ends during night */
OR (
HOUR(shifts.end) > %1$d
|| HOUR(shifts.end) = %1$d AND MINUTE(shifts.end) > 0
) AND HOUR(shifts.end) <= %2$d
/* Starts before and ends after night */
OR HOUR(shifts.start) <= %1$d AND HOUR(shifts.end) >= %2$d
))
* (UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start))
* (1 - (%3$d + 1) * `shift_entries`.`freeloaded`)

View File

@ -1,8 +1,10 @@
<?php
use Engelsystem\Helpers\Carbon;
use Engelsystem\Models\Shifts\ShiftEntry;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Engelsystem\Config\GoodieType;
@ -42,7 +44,7 @@ function admin_active()
if ($request->has('set_active')) {
if ($request->has('count') && preg_match('/^\d+$/', $request->input('count'))) {
$count = strip_request_item('count');
if ($count < $forced_count) {
if ($count < $forced_count && config('enable_force_active')) {
error(sprintf(
__('At least %s angels are forced to be active. The number has to be greater.'),
$forced_count
@ -56,7 +58,7 @@ function admin_active()
if ($request->hasPostData('ack')) {
State::query()
->where('got_shirt', '=', false)
->where('got_goodie', '=', false)
->update(['active' => false]);
$query = User::query()
@ -78,8 +80,16 @@ function admin_active()
->leftJoin('shifts', 'shift_entries.shift_id', '=', 'shifts.id')
->leftJoin('users_state', 'users.id', '=', 'users_state.user_id')
->where('users_state.arrived', '=', true)
->groupBy('users.id')
->orderByDesc('force_active')
->orWhere(function (EloquentBuilder $userinfo) {
$userinfo->where('users_state.arrived', '=', false)
->whereNotNull('users_state.user_info')
->whereNot('users_state.user_info', '');
})
->groupBy('users.id');
if (config('enable_force_active')) {
$query->orderByDesc('force_active');
}
$query
->orderByDesc('shift_length')
->orderByDesc('name')
->limit($count);
@ -130,7 +140,7 @@ function admin_active()
$user_id = $request->input('tshirt');
$user_source = User::find($user_id);
if ($user_source) {
$user_source->state->got_shirt = true;
$user_source->state->got_goodie = true;
$user_source->state->save();
engelsystem_log('User ' . User_Nick_render($user_source, true) . ' has tshirt now.');
$msg = success(($goodie_tshirt ? __('Angel has got a T-shirt.') : __('Angel has got a goodie.')), true);
@ -141,7 +151,7 @@ function admin_active()
$user_id = $request->input('not_tshirt');
$user_source = User::find($user_id);
if ($user_source) {
$user_source->state->got_shirt = false;
$user_source->state->got_goodie = false;
$user_source->state->save();
engelsystem_log('User ' . User_Nick_render($user_source, true) . ' has NO tshirt.');
$msg = success(($goodie_tshirt ? __('Angel has got no T-shirt.') : __('Angel has got no goodie.')), true);
@ -151,7 +161,7 @@ function admin_active()
}
}
$query = User::with('personalData')
$query = User::with(['personalData', 'state'])
->selectRaw(
sprintf(
'
@ -180,8 +190,16 @@ function admin_active()
})
->leftJoin('users_state', 'users.id', '=', 'users_state.user_id')
->where('users_state.arrived', '=', true)
->groupBy('users.id')
->orderByDesc('force_active')
->orWhere(function (EloquentBuilder $userinfo) {
$userinfo->where('users_state.arrived', '=', false)
->whereNotNull('users_state.user_info')
->whereNot('users_state.user_info', '');
})
->groupBy('users.id');
if (config('enable_force_active')) {
$query->orderByDesc('force_active');
}
$query
->orderByDesc('shift_length')
->orderByDesc('name');
@ -212,18 +230,34 @@ function admin_active()
}
}
$timeSum = 0;
/** @var ShiftEntry[] $shiftEntries */
$shiftEntries = $usr->shiftEntries()
->with('shift')
->get();
foreach ($shiftEntries as $entry) {
if ($entry->freeloaded || $entry->shift->start > Carbon::now()) {
continue;
}
$timeSum += ($entry->shift->end->timestamp - $entry->shift->start->timestamp);
}
foreach ($usr->worklogs as $worklog) {
$timeSum += $worklog->hours * 3600;
}
$shirtSize = $usr->personalData->shirt_size;
$userData = [];
$userData['no'] = count($matched_users) + 1;
$userData['nick'] = User_Nick_render($usr) . User_Pronoun_render($usr);
$userData['nick'] = User_Nick_render($usr) . User_Pronoun_render($usr) . user_info_icon($usr);
if ($goodie_tshirt) {
$userData['shirt_size'] = (isset($tshirt_sizes[$shirtSize]) ? $tshirt_sizes[$shirtSize] : '');
}
$userData['work_time'] = round($usr['shift_length'] / 60)
$userData['work_time'] = sprintf('%.2f', round($timeSum / 3600, 2)) . '&nbsp;h';
$userData['score'] = round($usr['shift_length'] / 60)
. ' min (' . sprintf('%.2f', $usr['shift_length'] / 3600) . '&nbsp;h)';
$userData['active'] = icon_bool($usr->state->active == 1);
$userData['force_active'] = icon_bool($usr->state->force_active == 1);
$userData['tshirt'] = icon_bool($usr->state->got_shirt == 1);
$userData['active'] = icon_bool($usr->state->active);
$userData['force_active'] = icon_bool($usr->state->force_active);
$userData['tshirt'] = icon_bool($usr->state->got_goodie);
$userData['shift_count'] = $usr['shift_count'];
$actions = [];
@ -257,7 +291,7 @@ function admin_active()
true
);
}
if (!$usr->state->got_shirt) {
if (!$usr->state->got_goodie) {
$parametersShirt = [
'tshirt' => $usr->id,
'search' => $search,
@ -275,7 +309,7 @@ function admin_active()
);
}
}
if ($usr->state->got_shirt) {
if ($usr->state->got_goodie) {
$parameters = [
'not_tshirt' => $usr->id,
'search' => $search,
@ -309,7 +343,7 @@ function admin_active()
$gc = State::query()
->leftJoin('users_settings', 'users_state.user_id', '=', 'users_settings.user_id')
->leftJoin('users_personal_data', 'users_state.user_id', '=', 'users_personal_data.user_id')
->where('users_state.got_shirt', '=', true)
->where('users_state.got_goodie', '=', true)
->where('users_personal_data.shirt_size', '=', $size)
->count();
$goodie_statistics[] = [
@ -321,7 +355,7 @@ function admin_active()
$goodie_statistics[] = array_merge(
($goodie_tshirt ? ['size' => '<b>' . __('Sum') . '</b>'] : []),
['given' => '<b>' . State::whereGotShirt(true)->count() . '</b>']
['given' => '<b>' . State::whereGotGoodie(true)->count() . '</b>']
);
return page_with_title(admin_active_title(), [
@ -331,7 +365,7 @@ function admin_active()
form_submit('submit', icon('search') . __('form.search')),
], url('/admin-active')),
$set_active == '' ? form([
form_text('count', __('How much angels should be active?'), $count ?: $forced_count),
form_text('count', __('How many angels should be active?'), $count ?: $forced_count),
form_submit('set_active', icon('eye') . __('form.preview'), 'btn-info'),
]) : $set_active,
$msg . msg(),
@ -343,12 +377,18 @@ function admin_active()
],
($goodie_tshirt ? ['shirt_size' => __('Size')] : []),
[
'shift_count' => __('Shifts'),
'shift_count' => __('general.shifts'),
'work_time' => __('Length'),
'active' => __('Active?'),
'force_active' => __('Forced'),
],
($goodie_enabled ? ['tshirt' => ($goodie_tshirt ? __('T-shirt?') : __('Goodie?'))] : []),
($goodie_enabled ? ['score' => ($goodie_tshirt
? __('T-shirt score')
: __('Goodie score')
)] : []),
[
'active' => __('Active'),
],
(config('enable_force_active') ? ['force_active' => __('Forced'),] : []),
($goodie_enabled ? ['tshirt' => ($goodie_tshirt ? __('T-shirt') : __('Goodie'))] : []),
[
'actions' => __('general.actions'),
]

View File

@ -8,7 +8,7 @@ use Engelsystem\Models\User\User;
*/
function admin_arrive_title()
{
return __('Arrive angels');
return auth()->can('admin_arrive') ? __('Arrive angels') : __('Angels');
}
/**
@ -19,53 +19,56 @@ function admin_arrive()
$msg = '';
$search = '';
$request = request();
$admin_arrive = auth()->can('admin_arrive');
if ($request->has('search')) {
$search = strip_request_item('search');
$search = trim($search);
}
$action = $request->get('action');
if (
$action == 'reset'
&& preg_match('/^\d+$/', $request->input('user'))
&& $request->hasPostData('submit')
) {
$user_id = $request->input('user');
$user_source = User::find($user_id);
if ($user_source) {
$user_source->state->arrived = false;
$user_source->state->arrival_date = null;
$user_source->state->save();
if ($admin_arrive) {
$action = $request->get('action');
if (
$action == 'reset'
&& preg_match('/^\d+$/', $request->input('user'))
&& $request->hasPostData('submit')
) {
$user_id = $request->input('user');
$user_source = User::find($user_id);
if ($user_source) {
$user_source->state->arrived = false;
$user_source->state->arrival_date = null;
$user_source->state->save();
engelsystem_log('User set to not arrived: ' . User_Nick_render($user_source, true));
success(__('Reset done. Angel has not arrived.'));
throw_redirect(user_link($user_source->id));
} else {
$msg = error(__('Angel not found.'), true);
}
} elseif (
$action == 'arrived'
&& preg_match('/^\d+$/', $request->input('user'))
&& $request->hasPostData('submit')
) {
$user_id = $request->input('user');
$user_source = User::find($user_id);
if ($user_source) {
$user_source->state->arrived = true;
$user_source->state->arrival_date = new Carbon\Carbon();
$user_source->state->save();
engelsystem_log('User set to not arrived: ' . User_Nick_render($user_source, true));
success(__('Reset done. Angel has not arrived.'));
throw_redirect(user_link($user_source->id));
} else {
$msg = error(__('Angel not found.'), true);
}
} elseif (
$action == 'arrived'
&& preg_match('/^\d+$/', $request->input('user'))
&& $request->hasPostData('submit')
) {
$user_id = $request->input('user');
$user_source = User::find($user_id);
if ($user_source) {
$user_source->state->arrived = true;
$user_source->state->arrival_date = new Carbon\Carbon();
$user_source->state->save();
engelsystem_log('User set has arrived: ' . User_Nick_render($user_source, true));
success(__('Angel has been marked as arrived.'));
throw_redirect(user_link($user_source->id));
} else {
$msg = error(__('Angel not found.'), true);
engelsystem_log('User set has arrived: ' . User_Nick_render($user_source, true));
success(__('Angel has been marked as arrived.'));
throw_redirect(user_link($user_source->id));
} else {
$msg = error(__('Angel not found.'), true);
}
}
}
/** @var User[] $users */
$users = User::with('personalData')->orderBy('name')->get();
$users = User::with(['personalData', 'state'])->orderBy('name')->get();
$arrival_count_at_day = [];
$planned_arrival_count_at_day = [];
$planned_departure_count_at_day = [];
@ -100,9 +103,7 @@ function admin_arrive()
$usr->name = User_Nick_render($usr)
. User_Pronoun_render($usr)
. ($usr->state->user_info
? ' <small><span class="bi bi-info-circle-fill text-info"></span></small>'
: '');
. user_info_icon($usr);
$plannedDepartureDate = $usr->personalData->planned_departure_date;
$arrivalDate = $usr->state->arrival_date;
$plannedArrivalDate = $usr->personalData->planned_arrival_date;
@ -208,15 +209,17 @@ function admin_arrive()
form_text('search', __('form.search'), $search),
form_submit('submit', icon('search') . __('form.search')),
], url('/admin-arrive')),
table([
'name' => __('general.name'),
'rendered_planned_arrival_date' => __('Planned arrival'),
'arrived' => __('Arrived?'),
'rendered_arrival_date' => __('Arrival date'),
'rendered_planned_departure_date' => __('Planned departure'),
'actions' => __('general.actions'),
], $users_matched),
div('row', [
table(array_merge(
['name' => __('general.name'),],
($admin_arrive ? ['rendered_planned_arrival_date' => __('Planned arrival')] : []),
['arrived' => __('Arrived?')],
($admin_arrive ? [
'rendered_arrival_date' => __('Arrival date'),
'rendered_planned_departure_date' => __('Planned departure'),
'actions' => __('general.actions'),
] : [])
), $users_matched),
div('row', $admin_arrive ? [
div('col-md-4', [
heading(__('Planned arrival statistics'), 3),
BarChart::render([
@ -262,6 +265,6 @@ function admin_arrive()
'sum' => __('Sum'),
], $planned_departure_at_day),
]),
]),
] : []),
]);
}

View File

@ -40,7 +40,7 @@ function admin_free()
/** @var User[] $users */
$users = [];
if ($request->has('submit')) {
$query = User::with('personalData')
$query = User::with(['personalData', 'contact', 'state'])
->select('users.*')
->leftJoin('shift_entries', 'users.id', 'shift_entries.user_id')
->leftJoin('users_state', 'users.id', 'users_state.user_id')
@ -99,9 +99,7 @@ function admin_free()
$free_users_table[] = [
'name' => User_Nick_render($usr)
. User_Pronoun_render($usr)
. ($usr->state->user_info
? ' <small><span class="bi bi-info-circle-fill text-info"></span></small>'
: ''),
. user_info_icon($usr),
'shift_state' => User_shift_state_render($usr),
'last_shift' => User_last_shift_render($usr),
'dect' => sprintf('<a href="tel:%s">%1$s</a>', htmlspecialchars((string) $usr->contact->dect)),

View File

@ -21,13 +21,13 @@ function admin_groups()
$html = '';
$request = request();
/** @var Group[]|Collection $groups */
$groups = Group::query()->orderBy('name')->get();
$groups = Group::with('privileges')->orderBy('name')->get();
if (!$request->has('action')) {
$groups_table = [];
foreach ($groups as $group) {
/** @var Privilege[]|Collection $privileges */
$privileges = $group->privileges()->orderBy('name')->get();
$privileges = $group->privileges->sortBy('name');
$privileges_html = [];
foreach ($privileges as $privilege) {
@ -43,10 +43,9 @@ function admin_groups()
['action' => 'edit', 'id' => $group->id]
),
icon('pencil'),
'btn-sm',
'',
'',
__('form.edit'),
'btn-sm'
__('form.edit')
),
];
}
@ -122,10 +121,9 @@ function admin_groups()
. ' edited: ' . join(', ', $privilege_names)
);
throw_redirect(url('/admin-groups'));
} else {
return error('No Group found.', true);
}
break;
return error('No Group found.', true);
}
}
return $html;

View File

@ -41,11 +41,13 @@ function admin_shifts()
// Locations laden
$locations = Location::orderBy('name')->get();
$no_locations = $locations->isEmpty();
$location_array = $locations->pluck('name', 'id')->toArray();
// Load angeltypes
/** @var AngelType[] $types */
/** @var AngelType[]|Collection $types */
$types = AngelType::all();
$no_angeltypes = $types->isEmpty();
$needed_angel_types = [];
foreach ($types as $type) {
$needed_angel_types[$type->id] = 0;
@ -54,6 +56,7 @@ function admin_shifts()
// Load shift types
/** @var ShiftType[]|Collection $shifttypes_source */
$shifttypes_source = ShiftType::all();
$no_shifttypes = $shifttypes_source->isEmpty();
$shifttypes = [];
foreach ($shifttypes_source as $shifttype) {
$shifttypes[$shifttype->id] = $shifttype->name;
@ -184,15 +187,23 @@ function admin_shifts()
error(sprintf(__('Please check the needed angels for team %s.'), $type->name));
}
}
if (array_sum($needed_angel_types) == 0) {
$valid = false;
error(__('There are 0 angels needed. Please enter the amounts of needed angels.'));
}
} else {
$valid = false;
error(__('Please select a mode for needed angels.'));
}
if (
$angelmode == 'manually' && array_sum($needed_angel_types) == 0
|| $angelmode == 'location' && !NeededAngelType::whereLocationId($lid)
->where('count', '>', '0')
->count()
|| $angelmode == 'shift_type' && !NeededAngelType::whereShiftTypeId($shifttype_id)
->where('count', '>', '0')
->count()
) {
$valid = false;
error(__('There are 0 angels needed. Please enter the amounts of needed angels.'));
}
} else {
$valid = false;
error(__('Please select needed angels.'));
@ -318,6 +329,9 @@ function admin_shifts()
$shifts_table = [];
foreach ($shifts as $shift) {
$shiftType = $shifttypes_source->find($shift['shift_type_id']);
$location = $locations->find($shift['location_id']);
/** @var Carbon $start */
$start = $shift['start'];
/** @var Carbon $end */
@ -332,9 +346,9 @@ function admin_shifts()
. '</span>'
. ', ' . round($end->copy()->diffInMinutes($start) / 60, 2) . 'h'
. '<br>'
. location_name_render(Location::find($shift['location_id'])),
. location_name_render($location),
'title' =>
htmlspecialchars(ShiftType::find($shifttype_id)->name)
htmlspecialchars($shiftType->name)
. ($shift['title'] ? '<br />' . htmlspecialchars($shift['title']) : ''),
'needed_angels' => '',
];
@ -414,15 +428,6 @@ function admin_shifts()
$shift->createdBy()->associate(auth()->user());
$shift->save();
engelsystem_log(
'Shift created: ' . $shifttypes[$shift->shift_type_id]
. ' with title ' . $shift->title
. ' with description ' . $shift->description
. ' from ' . $shift->start->format('Y-m-d H:i')
. ' to ' . $shift->end->format('Y-m-d H:i')
. ', transaction: ' . $transactionId
);
$needed_angel_types_info = [];
foreach ($session->get('admin_shifts_types', []) as $type_id => $count) {
$angel_type_source = AngelType::find($type_id);
@ -436,10 +441,21 @@ function admin_shifts()
$needed_angel_types_info[] = $angel_type_source->name . ': ' . $count;
}
}
engelsystem_log('Shift needs following angel types: ' . join(', ', $needed_angel_types_info));
engelsystem_log(
'Shift created: ' . $shifttypes[$shift->shift_type_id]
. ' (' . $shift->id . ')'
. ' with title ' . $shift->title
. ' and description ' . $shift->description
. ' from ' . $shift->start->format('Y-m-d H:i')
. ' to ' . $shift->end->format('Y-m-d H:i')
. ' in ' . $shift->location->name
. ' with angel types: ' . join(', ', $needed_angel_types_info)
. ', transaction: ' . $transactionId
);
}
success('Shifts created.');
success(__('Shifts created.'));
throw_redirect(url('/admin-shifts'));
} else {
$session->remove('admin_shifts_shifts');
@ -488,6 +504,9 @@ function admin_shifts()
icon('clock-history')
) . form([$reset], '', 'display:inline'),
[
$no_locations ? warning(__('admin_shifts.no_locations')) : '',
$no_shifttypes ? warning(__('admin_shifts.no_shifttypes')) : '',
$no_angeltypes ? warning(__('admin_shifts.no_angeltypes')) : '',
msg(),
form([
div('row', [
@ -528,7 +547,7 @@ function admin_shifts()
form_radio('mode', __('Create multiple shifts'), $mode == 'multi', 'multi'),
form_text(
'length',
__('Length'),
__('Length (in minutes)'),
$request->has('length')
? $request->input('length')
: '120',

View File

@ -28,8 +28,9 @@ function admin_user()
$goodie_enabled = $goodie !== GoodieType::None;
$goodie_tshirt = $goodie === GoodieType::Tshirt;
$user_info_edit = auth()->can('user.info.edit');
$user_edit_shirt = auth()->can('user.edit.shirt');
$user_edit = auth()->can('user.edit');
$user_goodie_edit = auth()->can('user.goodie.edit');
$user_nick_edit = auth()->can('user.nick.edit');
$admin_arrive = auth()->can('admin_arrive');
if (!$request->has('id')) {
throw_redirect(users_link());
@ -44,7 +45,7 @@ function admin_user()
}
$html .= __('Here you can change the user entry. Under the item \'Arrived\' the angel is marked as present, a yes at Active means that the angel was active.');
if ($goodie_enabled && $user_edit_shirt) {
if ($goodie_enabled && $user_goodie_edit) {
if ($goodie_tshirt) {
$html .= ' ' . __('If the angel is active, it can claim a T-shirt. If T-shirt is set to \'Yes\', the angel already got their T-shirt.');
} else {
@ -62,7 +63,7 @@ function admin_user()
$html .= '<table>' . "\n";
$html .= ' <tr><td>' . __('general.nick') . '</td><td>'
. '<input size="40" name="eNick" value="' . htmlspecialchars($user_source->name)
. '" class="form-control" maxlength="24" ' . ($user_edit ? '' : 'disabled') . '>'
. '" class="form-control" maxlength="24" ' . ($user_nick_edit ? '' : 'disabled') . '>'
. '</td></tr>' . "\n";
$html .= ' <tr><td>' . __('Last login') . '</td><td><p class="help-block">'
. ($user_source->last_login_at ? $user_source->last_login_at->format(__('general.datetime')) : '-')
@ -88,7 +89,7 @@ function admin_user()
. '<input type="email" size="40" name="eemail" value="' . htmlspecialchars($user_source->email) . '" class="form-control" maxlength="254">'
. '</td></tr>' . "\n";
}
if ($goodie_tshirt && $user_edit_shirt) {
if ($goodie_tshirt && $user_goodie_edit) {
$html .= ' <tr><td>' . __('user.shirt_size') . '</td><td>'
. html_select_key(
'size',
@ -121,34 +122,38 @@ function admin_user()
// Arrived?
$html .= ' <tr><td>' . __('user.arrived') . '</td><td>' . "\n";
$html .= ($user_source->state->arrived ? __('Yes') : __('No'));
$html .= $admin_arrive
? html_options('arrive', $options, $user_source->state->arrived)
: icon_bool($user_source->state->arrived);
$html .= '</td></tr>' . "\n";
// Active?
if ($user_edit_shirt) {
$html .= ' <tr><td>' . __('user.active') . '</td><td>' . "\n";
$html .= html_options('eAktiv', $options, $user_source->state->active) . '</td></tr>' . "\n";
} else {
$html .= ' <tr><td>' . __('user.active') . '</td><td>' . "\n";
$html .= ($user_source->state->active ? __('Yes') : __('No'));
$html .= ' <tr><td>' . __('user.active') . '</td><td>' . "\n";
$html .= $user_goodie_edit
? html_options('eAktiv', $options, $user_source->state->active)
: icon_bool($user_source->state->active);
$html .= '</td></tr>' . "\n";
// Forced active?
if (config('enable_force_active')) {
$html .= ' <tr><td>' . __('Force active') . '</td><td>' . "\n";
$html .= auth()->can('user.fa.edit')
? html_options('force_active', $options, $user_source->state->force_active)
: icon_bool($user_source->state->force_active);
$html .= '</td></tr>' . "\n";
}
// Forced active?
if (auth()->can('admin_active')) {
$html .= ' <tr><td>' . __('Force active') . '</td><td>' . "\n";
$html .= html_options('force_active', $options, $user_source->state->force_active) . '</td></tr>' . "\n";
if ($goodie_enabled) {
// got goodie?
$html .= ' <tr><td>'
. ($goodie_tshirt ? __('T-shirt') : __('Goodie'))
. '</td><td>' . "\n";
$html .= $user_goodie_edit
? html_options('eTshirt', $options, $user_source->state->got_goodie)
: icon_bool($user_source->state->got_goodie);
$html .= '</td></tr>' . "\n";
}
if ($goodie_enabled && $user_edit_shirt) {
// T-Shirt bekommen?
if ($goodie_tshirt) {
$html .= ' <tr><td>' . __('T-shirt') . '</td><td>' . "\n";
} else {
$html .= ' <tr><td>' . __('Goodie') . '</td><td>' . "\n";
}
$html .= html_options('eTshirt', $options, $user_source->state->got_shirt) . '</td></tr>' . "\n";
}
$html .= '</table>' . "\n" . '</td><td></td></tr>';
$html .= '</td></tr>' . "\n";
@ -284,18 +289,26 @@ function admin_user()
$user_source = User::find($user_id);
$changed_email = false;
$email = $request->postData('eemail');
if (($user_source->email !== $email) && User::whereEmail($email)->exists()) {
$html .= error(__('settings.profile.email.already-taken') . "\n", true);
break;
}
if ($user_source->settings->email_human) {
$changed_email = $user_source->email !== $request->postData('eemail');
$user_source->email = $request->postData('eemail');
$changed_email = $user_source->email !== $email;
$user_source->email = $email;
}
$nick = trim($request->get('eNick'));
$nickValid = (new Username())->validate($nick);
$changed_nick = false;
$nick = trim((string) $request->get('eNick'));
$nickValid = (new Username())->validate($nick);
if (($user_source->name !== $nick) && User::whereName($nick)->exists()) {
$html .= error(__('settings.profile.nick.already-taken') . "\n", true);
break;
}
$old_nick = $user_source->name;
if ($nickValid && $user_edit) {
$changed_nick = $user_source->name !== $nick;
if ($nickValid && $user_nick_edit) {
$changed_nick = ($user_source->name !== $nick) || User::whereName($nick)->exists();
$user_source->name = $nick;
}
$user_source->save();
@ -304,7 +317,7 @@ function admin_user()
$user_source->personalData->first_name = $request->postData('eVorname');
$user_source->personalData->last_name = $request->postData('eName');
}
if ($goodie_tshirt && $user_edit_shirt) {
if ($goodie_tshirt && $user_goodie_edit) {
$user_source->personalData->shirt_size = $request->postData('eSize');
}
$user_source->personalData->save();
@ -315,17 +328,20 @@ function admin_user()
}
$user_source->contact->save();
if ($goodie_enabled && $user_edit_shirt) {
$user_source->state->got_shirt = $request->postData('eTshirt');
if ($goodie_enabled && $user_goodie_edit) {
$user_source->state->got_goodie = $request->postData('eTshirt');
}
if ($user_info_edit) {
$user_source->state->user_info = $request->postData('userInfo');
}
if ($admin_arrive) {
$user_source->state->arrived = $request->postData('arrive');
}
if ($user_edit_shirt) {
if ($user_goodie_edit) {
$user_source->state->active = $request->postData('eAktiv');
}
if (auth()->can('admin_active')) {
if (auth()->can('user.fa.edit') && config('enable_force_active')) {
$user_source->state->force_active = $request->input('force_active');
}
$user_source->state->save();
@ -337,9 +353,10 @@ function admin_user()
. ' (' . $user_source->id . ')'
. ($changed_email ? ', email modified' : '')
. ($goodie_tshirt ? ', t-shirt-size: ' . $user_source->personalData->shirt_size : '')
. ', arrived: ' . $user_source->state->arrived
. ', active: ' . $user_source->state->active
. ', force-active: ' . $user_source->state->force_active
. ($goodie_tshirt ? ', t-shirt: ' : ', goodie: ' . $user_source->state->got_shirt)
. ($goodie_tshirt ? ', t-shirt: ' : ', goodie: ' . $user_source->state->got_goodie)
. ($user_info_edit ? ', user-info: ' . $user_source->state->user_info : '')
);
$html .= success(__('Changes were saved.') . "\n", true);

View File

@ -20,10 +20,18 @@ function user_myshifts()
{
$user = auth()->user();
$request = request();
$is_angeltype_supporter = false;
if ($request->has('edit')) {
$id = $request->input('edit');
$shiftEntry = ShiftEntry::where('id', $id)
->where('user_id', User::find($request->input('id'))->id)
->first();
$is_angeltype_supporter = $shiftEntry && auth()->user()->isAngelTypeSupporter($shiftEntry->angelType);
}
if (
$request->has('id')
&& auth()->can('user_shifts_admin')
&& (auth()->can('user_shifts_admin') || $is_angeltype_supporter)
&& preg_match('/^\d+$/', $request->input('id'))
&& User::find($request->input('id'))
) {
@ -33,21 +41,7 @@ function user_myshifts()
}
$shifts_user = User::find($shift_entry_id);
if ($request->has('reset')) {
if ($request->input('reset') == 'ack') {
auth()->resetApiKey($user);
engelsystem_log(sprintf('API key resetted (%s).', User_Nick_render($user, true)));
success(__('Key changed.'));
throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id]));
}
return page_with_title(__('Reset API key'), [
error(
__('If you reset the key, the url to your iCal- and JSON-export and your atom/rss feed changes! You have to update it in every application using one of these exports.'),
true
),
button(url('/user-myshifts', ['reset' => 'ack']), __('Continue'), 'btn-danger'),
]);
} elseif ($request->has('edit') && preg_match('/^\d+$/', $request->input('edit'))) {
if ($request->has('edit') && preg_match('/^\d+$/', $request->input('edit'))) {
$shift_entry_id = $request->input('edit');
/** @var ShiftEntry $shiftEntry */
$shiftEntry = ShiftEntry::where('id', $shift_entry_id)
@ -61,7 +55,10 @@ function user_myshifts()
if ($request->hasPostData('submit')) {
$valid = true;
if (auth()->can('user_shifts_admin')) {
if (
auth()->can('user_shifts_admin')
|| $is_angeltype_supporter
) {
$freeloaded = $request->has('freeloaded');
$freeloaded_comment = strip_request_item_nl('freeloaded_comment');
if ($freeloaded && $freeloaded_comment == '') {
@ -91,6 +88,9 @@ function user_myshifts()
. '. Freeloaded: ' . ($freeloaded ? 'YES Comment: ' . $freeloaded_comment : 'NO')
);
success(__('Shift saved.'));
if ($is_angeltype_supporter) {
throw_redirect(url('/shifts', ['action' => 'view', 'shift_id' => $shiftEntry->shift_id]));
}
throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id]));
}
}
@ -104,13 +104,13 @@ function user_myshifts()
$shiftEntry->user_comment,
$shiftEntry->freeloaded,
$shiftEntry->freeloaded_comment,
auth()->can('user_shifts_admin')
auth()->can('user_shifts_admin'),
$is_angeltype_supporter
);
} else {
throw_redirect(url('/user-myshifts'));
}
}
throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id]));
return '';
}

View File

@ -17,7 +17,7 @@ use Illuminate\Support\Collection;
*/
function shifts_title()
{
return __('Shifts');
return __('general.shifts');
}
/**
@ -297,7 +297,6 @@ function view_user_shifts()
return page([
div('col-md-12', [
msg(),
view(__DIR__ . '/../../resources/views/pages/user-shifts.html', [
'title' => shifts_title(),
'add_link' => auth()->can('admin_shifts') ? $link : '',
@ -375,18 +374,11 @@ function ical_hint()
return heading(__('iCal export and API') . ' ' . button_help('user/ical'), 2)
. '<p>' . sprintf(
__('Export your own shifts. <a href="%s">iCal format</a> or <a href="%s">JSON format</a> available (please keep secret, otherwise <a href="%s">reset the api key</a>).'),
__('Export your own shifts formatted as <a href="%s" target="_blank">iCal</a> or <a href="%s" target="_blank">JSON</a> (please keep the link secret, otherwise you have to reset the api key <a href="%s">in your settings</a>).'),
url('/ical', ['key' => $user->api_key]),
url('/shifts-json-export', ['key' => $user->api_key]),
url('/user-myshifts', ['reset' => 1])
)
. ' <button class="btn btn-sm btn-danger" type="button"
data-bs-toggle="collapse" data-bs-target="#collapseApiKey"
aria-expanded="false" aria-controls="collapseApiKey">
' . __('Show API Key') . '
</button>'
. '</p>'
. '<p id="collapseApiKey" class="collapse"><code>' . $user->api_key . '</code></p>';
url('/settings/api')
) . '</p>';
}
/**

View File

@ -20,8 +20,9 @@ function form_hidden($name, $value)
*
* @param string $name
* @param string $label
* @param int $value
* @param array $data_attributes
* @param int $value
* @param array $data_attributes
* @param bool $isDisabled
* @return string
*/
function form_spinner(string $name, string $label, int $value, array $data_attributes = [], bool $isDisabled = false)
@ -139,13 +140,26 @@ function form_info($label, $text = '')
* @param string $name
* @param string $label
* @param string $class
* @param bool $wrapForm
* @param bool $wrapForm
* @param string $buttonType
* @param string $title
* @param array $dataAttributes
* @return string
*/
function form_submit($name, $label, $class = '', $wrapForm = true, $buttonType = 'primary', $title = '')
{
$button = '<button class="btn btn-' . $buttonType . ($class ? ' ' . $class : '') . '" type="submit" name="' . $name . '" title="' . $title . '">'
function form_submit(
$name,
$label,
$class = '',
$wrapForm = true,
$buttonType = 'primary',
$title = '',
array $dataAttributes = []
) {
$add = '';
foreach ($dataAttributes as $dataType => $dataValue) {
$add .= ' data-' . $dataType . '="' . htmlspecialchars($dataValue) . '"';
}
$button = '<button class="btn btn-' . $buttonType . ($class ? ' ' . $class : '') . '" type="submit" name="' . $name . '" title="' . $title . '"' . $add . '>'
. $label
. '</button>';

View File

@ -28,7 +28,7 @@ function header_render_hints()
$hints_renderer->addHint(render_user_pronoun_hint(), true);
$hints_renderer->addHint(render_user_firstname_hint(), true);
$hints_renderer->addHint(render_user_lastname_hint(), true);
$hints_renderer->addHint(render_user_tshirt_hint(), true);
$hints_renderer->addHint(render_user_goodie_hint(), true);
$hints_renderer->addHint(render_user_dect_hint(), true);
$hints_renderer->addHint(render_user_mobile_hint(), true);
@ -58,7 +58,7 @@ function make_navigation()
$pages = [
'news' => __('news.title'),
'meetings' => [__('news.title.meetings'), 'user_meetings'],
'user_shifts' => __('Shifts'),
'user_shifts' => __('general.shifts'),
'angeltypes' => __('angeltypes.angeltypes'),
'questions' => [__('Ask the Heaven'), 'question.add'],
];
@ -85,12 +85,12 @@ function make_navigation()
// path => name,
// path => [name, permission],
'admin_arrive' => 'Arrive angels',
'admin_arrive' => [admin_arrive_title(), 'users.arrive.list'],
'admin_active' => 'Active angels',
'users' => ['All Angels', 'admin_user'],
'admin_free' => 'Free angels',
'admin/questions' => ['Answer questions', 'question.edit'],
'admin/shifttypes' => ['shifttype.shifttypes', 'shifttypes'],
'admin/shifttypes' => ['shifttype.shifttypes', 'shifttypes.view'],
'admin_shifts' => 'Create shifts',
'admin/locations' => ['location.locations', 'admin_locations'],
'admin_groups' => 'Grouprights',

View File

@ -116,7 +116,7 @@ function check_date($input, $error_message = null, $null_allowed = false, $time_
} else {
$time = Carbon::createFromFormat('Y-m-d', $trimmed_input);
}
} catch (InvalidArgumentException $e) {
} catch (InvalidArgumentException) {
$time = null;
}
@ -197,20 +197,3 @@ function strip_item($item)
// Only allow letters, symbols, punctuation, separators and numbers without html tags
return preg_replace('/([^\p{L}\p{S}\p{P}\p{Z}\p{N}+]+)/ui', '', strip_tags($item));
}
/**
* Validates an email address with support for IDN domain names.
*
* @param string $email
* @return bool
*/
function check_email($email)
{
// Convert the domain part from idn to ascii
if (substr_count($email, '@') == 1) {
list($name, $domain) = explode('@', $email);
$domain = idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
$email = $name . '@' . $domain;
}
return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
}

View File

@ -1,5 +1,6 @@
<?php
use Engelsystem\Models\User\User;
use Illuminate\Support\Str;
/**
@ -351,7 +352,7 @@ function render_table($columns, $rows, $data = true)
* @param string $id
* @return string
*/
function button($href, $label, $class = '', $id = '', $title = '')
function button($href, $label, $class = '', $id = '', $title = '', $disabled = false)
{
if (!Str::contains(str_replace(['btn-sm', 'btn-xl'], '', $class), 'btn-')) {
$class = 'btn-secondary' . ($class ? ' ' . $class : '');
@ -360,7 +361,7 @@ function button($href, $label, $class = '', $id = '', $title = '')
$idAttribute = $id ? 'id="' . $id . '"' : '';
return '<a ' . $idAttribute . ' href="' . $href
. '" class="btn ' . $class . '" title="' . $title . '">' . $label . '</a>';
. '" class="btn ' . $class . ($disabled ? ' disabled' : '') . '" title="' . $title . '">' . $label . '</a>';
}
/**
@ -386,9 +387,9 @@ function button_checkbox_selection($name, $label, $value)
*
* @return string
*/
function button_icon($href, $icon, $class = '', $title = '')
function button_icon($href, $icon, $class = '', $title = '', $disabled = false)
{
return button($href, icon($icon), $class, '', $title);
return button($href, icon($icon), $class, '', $title, $disabled);
}
/**
@ -421,3 +422,16 @@ function table_buttons($buttons = [], $additionalClass = '')
{
return '<div class="btn-group ' . $additionalClass . '" role="group">' . join('', $buttons) . '</div>';
}
function user_info_icon(User $user): string
{
if (!auth()->can('admin_arrive') || !$user->state->user_info) {
return '';
}
$infoIcon = ' <small><span class="bi bi-info-circle-fill text-info" ';
if (auth()->can('user.info.show')) {
$infoIcon .= 'data-bs-toggle="tooltip" title="' . htmlspecialchars($user->state->user_info) . '"';
}
$infoIcon .= '></span></small>';
return $infoIcon;
}

View File

@ -83,9 +83,39 @@ function AngelType_delete_view(AngelType $angeltype)
*/
function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode)
{
$requires_ifsg = '';
$requires_driving_license = '';
if (config('ifsg_enabled')) {
$requires_ifsg = $supporter_mode ?
form_info(
__('angeltype.ifsg.required'),
$angeltype->requires_ifsg_certificate
? __('Yes')
: __('No')
) : form_checkbox(
'requires_ifsg_certificate',
__('angeltype.ifsg.required'),
$angeltype->requires_ifsg_certificate
);
}
if (config('driving_license_enabled')) {
$requires_driving_license = $supporter_mode ?
form_info(
__('Requires driver license'),
$angeltype->requires_driver_license
? __('Yes')
: __('No')
) : form_checkbox(
'requires_driver_license',
__('Requires driver license'),
$angeltype->requires_driver_license
);
}
$link = button($angeltype->id
? url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])
: url('/angeltypes'), icon('chevron-left'), 'btn-sm', '', __('general.back'));
return page_with_title(
$link . ' ' . (
$angeltype->id ?
@ -120,30 +150,8 @@ function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode)
__('angeltypes.shift.self_signup.info') . '"></span>',
$angeltype->shift_self_signup
),
$supporter_mode ?
form_info(
__('Requires driver license'),
$angeltype->requires_driver_license
? __('Yes')
: __('No')
) :
form_checkbox(
'requires_driver_license',
__('Requires driver license'),
$angeltype->requires_driver_license
),
$supporter_mode && config('ifsg_enabled') ?
form_info(
__('angeltype.ifsg.required'),
$angeltype->requires_ifsg_certificate
? __('Yes')
: __('No')
) :
form_checkbox(
'requires_ifsg_certificate',
__('angeltype.ifsg.required'),
$angeltype->requires_ifsg_certificate
),
$requires_driving_license,
$requires_ifsg,
$supporter_mode
? form_info(__('Show on dashboard'), $angeltype->show_on_dashboard ? __('Yes') : __('No'))
: form_checkbox('show_on_dashboard', __('Show on dashboard'), $angeltype->show_on_dashboard),
@ -182,7 +190,7 @@ function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode)
* @param UserAngelType|null $user_angeltype
* @param bool $admin_angeltypes
* @param bool $supporter
* @param License $user_driver_license
* @param License $user_license
* @param User|null $user
* @return string
*/
@ -191,16 +199,24 @@ function AngelType_view_buttons(
?UserAngelType $user_angeltype,
$admin_angeltypes,
$supporter,
$user_driver_license,
$user_license,
$user
) {
if ($angeltype->requires_driver_license) {
if (
config('driving_license_enabled')
&& $angeltype->requires_driver_license
&& $user_angeltype
) {
$buttons[] = button(
url('/settings/certificates'),
icon('person-vcard') . __('my driving license')
icon('person-vcard') . __('My driving license')
);
}
if (config('isfg_enabled') && $angeltype->requires_ifsg_certificate) {
if (
config('ifsg_enabled')
&& $angeltype->requires_ifsg_certificate
&& $user_angeltype
) {
$buttons[] = button(
url('/settings/certificates'),
icon('card-checklist') . __('angeltype.ifsg.own')
@ -216,7 +232,7 @@ function AngelType_view_buttons(
($admin_angeltypes ? 'Join' : ''),
);
} else {
if ($angeltype->requires_driver_license && !$user_driver_license->wantsToDrive()) {
if (config('driving_license_enabled') && $angeltype->requires_driver_license && !$user_license->wantsToDrive()) {
error(__('This angeltype requires a driver license. Please enter your driver license information!'));
}
@ -265,6 +281,13 @@ function AngelType_view_buttons(
return buttons($buttons);
}
function certificateIcon($confirmed, $certificate)
{
return ($confirmed && $certificate)
? icon('check2-all', 'text-success')
: icon_bool($certificate);
}
/**
* Renders and sorts the members of an angeltype into supporters, members and unconfirmed members.
*
@ -282,26 +305,52 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange
foreach ($members as $member) {
$member->name = User_Nick_render($member) . User_Pronoun_render($member);
if (config('enable_dect')) {
$member['dect'] = htmlspecialchars((string) $member->contact->dect);
$member['dect'] =
sprintf('<a href="tel:%s">%1$s</a>', htmlspecialchars((string) $member->contact->dect));
}
if ($angeltype->requires_driver_license) {
$member['wants_to_drive'] = icon_bool($member->license->wantsToDrive());
if (config('driving_license_enabled') && $angeltype->requires_driver_license) {
$drive_confirmed = $member->license->drive_confirmed;
$member['wants_to_drive'] = certificateIcon($drive_confirmed, $member->license->wantsToDrive());
$member['has_car'] = icon_bool($member->license->has_car);
$member['has_license_car'] = icon_bool($member->license->drive_car);
$member['has_license_3_5t_transporter'] = icon_bool($member->license->drive_3_5t);
$member['has_license_7_5t_truck'] = icon_bool($member->license->drive_7_5t);
$member['has_license_12t_truck'] = icon_bool($member->license->drive_12t);
$member['has_license_forklift'] = icon_bool($member->license->drive_forklift);
$member['has_license_car'] = certificateIcon($drive_confirmed, $member->license->drive_car);
$member['has_license_3_5t_transporter'] = certificateIcon($drive_confirmed, $member->license->drive_3_5t);
$member['has_license_7_5t_truck'] = certificateIcon($drive_confirmed, $member->license->drive_7_5t);
$member['has_license_12t_truck'] = certificateIcon($drive_confirmed, $member->license->drive_12t);
$member['has_license_forklift'] = certificateIcon($drive_confirmed, $member->license->drive_forklift);
}
if ($angeltype->requires_ifsg_certificate && config('ifsg_enabled')) {
$member['ifsg_certificate'] = icon_bool($member->license->ifsg_certificate);
if (config('ifsg_enabled') && $angeltype->requires_ifsg_certificate) {
$ifsg_confirmed = $member->license->ifsg_confirmed;
$member['ifsg_certificate'] = certificateIcon($ifsg_confirmed, $member->license->ifsg_certificate);
if (config('ifsg_light_enabled')) {
$member['ifsg_certificate_light'] = icon_bool($member->license->ifsg_certificate_light);
$member['ifsg_certificate_light'] = certificateIcon($ifsg_confirmed, $member->license->ifsg_certificate_light);
}
}
$edit_certificates = '';
if (
(
config('driving_license_enabled')
&& $angeltype->requires_driver_license
&& ($admin_user_angeltypes || auth()->can('user.drive.edit'))
)
|| (
config('ifsg_enabled')
&& $angeltype->requires_ifsg_certificate
&& ($admin_user_angeltypes || auth()->can('user.ifsg.edit'))
)
) {
$edit_certificates =
button(
url('/users/' . $member->id . '/certificates'),
icon('card-checklist'),
'btn-sm',
'',
__('Edit certificates'),
);
}
if ($angeltype->restricted && empty($member->pivot->confirm_user_id)) {
$member['actions'] = table_buttons([
$edit_certificates,
button(
url(
'/user-angeltypes',
@ -321,8 +370,9 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange
]);
$members_unconfirmed[] = $member;
} elseif ($member->pivot->supporter) {
if ($admin_angeltypes) {
if ($admin_angeltypes || ($admin_user_angeltypes && config('supporters_can_promote'))) {
$member['actions'] = table_buttons([
$edit_certificates,
button(
url('/user-angeltypes', [
'action' => 'update',
@ -336,13 +386,16 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange
),
]);
} else {
$member['actions'] = '';
$member['actions'] = $edit_certificates
? table_buttons([$edit_certificates,])
: '';
}
$supporters[] = $member;
} else {
if ($admin_user_angeltypes) {
$member['actions'] = table_buttons([
$admin_angeltypes ?
$edit_certificates,
($admin_angeltypes || config('supporters_can_promote')) ?
button(
url('/user-angeltypes', [
'action' => 'update',
@ -366,6 +419,10 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange
__('Remove'),
),
]);
} elseif ($edit_certificates) {
$member['actions'] = table_buttons([
$edit_certificates,
]);
}
$members_confirmed[] = $member;
}
@ -396,7 +453,10 @@ function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_a
$headers['dect'] = __('general.dect');
}
if ($angeltype->requires_driver_license && ($supporter || $admin_angeltypes)) {
if (
config('driving_license_enabled') && $angeltype->requires_driver_license
&& ($supporter || $admin_angeltypes || auth()->can('user.drive.edit'))
) {
$headers = array_merge($headers, [
'wants_to_drive' => __('Driver'),
'has_car' => __('Has car'),
@ -408,7 +468,10 @@ function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_a
]);
}
if (config('ifsg_enabled') && $angeltype->requires_ifsg_certificate && ($supporter || $admin_angeltypes)) {
if (
config('ifsg_enabled') && $angeltype->requires_ifsg_certificate
&& ($supporter || $admin_angeltypes || auth()->can('user.ifsg.edit'))
) {
if (config('ifsg_light_enabled')) {
$headers['ifsg_certificate_light'] = __('ifsg.certificate_light');
}
@ -429,7 +492,7 @@ function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_a
* @param bool $admin_user_angeltypes
* @param bool $admin_angeltypes
* @param bool $supporter
* @param License $user_driver_license
* @param License $user_license
* @param User $user
* @param ShiftsFilterRenderer $shiftsFilterRenderer
* @param ShiftCalendarRenderer $shiftCalendarRenderer
@ -443,7 +506,7 @@ function AngelType_view(
$admin_user_angeltypes,
$admin_angeltypes,
$supporter,
$user_driver_license,
$user_license,
$user,
ShiftsFilterRenderer $shiftsFilterRenderer,
ShiftCalendarRenderer $shiftCalendarRenderer,
@ -453,7 +516,7 @@ function AngelType_view(
return page_with_title(
$link . ' ' . sprintf(__('Team %s'), htmlspecialchars($angeltype->name)),
[
AngelType_view_buttons($angeltype, $user_angeltype, $admin_angeltypes, $supporter, $user_driver_license, $user),
AngelType_view_buttons($angeltype, $user_angeltype, $admin_angeltypes, $supporter, $user_license, $user),
msg(),
tabs([
__('Info') => AngelType_view_info(
@ -463,7 +526,7 @@ function AngelType_view(
$admin_angeltypes,
$supporter
),
__('Shifts') => AngelType_view_shifts(
__('general.shifts') => AngelType_view_shifts(
$angeltype,
$shiftsFilterRenderer,
$shiftCalendarRenderer
@ -506,6 +569,13 @@ function AngelType_view_info(
$admin_angeltypes,
$supporter
) {
$required_info_show = !auth()->user()
->userAngelTypes()
->where('angel_types.id', $angeltype->id)
->count()
&& !$admin_angeltypes
&& !$admin_user_angeltypes
&& !$supporter;
$info = [];
if ($angeltype->hasContactInfo()) {
$info[] = AngelTypes_render_contact_info($angeltype);
@ -516,6 +586,12 @@ function AngelType_view_info(
if ($angeltype->description != '') {
$info[] = $parsedown->parse(htmlspecialchars($angeltype->description));
}
if ($angeltype->requires_ifsg_certificate && $required_info_show) {
$info[] = info(__('angeltype.ifsg.required.info.preview'), true);
}
if ($angeltype->requires_driver_license && $required_info_show) {
$info[] = info(__('angeltype.driving_license.required.info.preview'), true);
}
list($supporters, $members_confirmed, $members_unconfirmed) = AngelType_view_members(
$angeltype,

View File

@ -27,6 +27,22 @@ function location_view(Location $location, ShiftsFilterRenderer $shiftsFilterRen
$description .= $parsedown->parse(htmlspecialchars($location->description));
}
$neededAngelTypes = '';
if (auth()->can('admin_shifts') && $location->neededAngelTypes->isNotEmpty()) {
$neededAngelTypes .= '<h3>' . __('location.required_angels') . '</h3><ul>';
foreach ($location->neededAngelTypes as $neededAngelType) {
if ($neededAngelType->count) {
$neededAngelTypes .= '<li><a href="'
. url('angeltypes', ['action' => 'view', 'angeltype_id' => $neededAngelType->angelType->id])
. '">' . $neededAngelType->angelType->name
. '</a>: '
. $neededAngelType->count
. '</li>';
}
}
$neededAngelTypes .= '</ul>';
}
$dect = '';
if (config('enable_dect') && $location->dect) {
$dect = heading(__('Contact'), 3)
@ -40,13 +56,13 @@ function location_view(Location $location, ShiftsFilterRenderer $shiftsFilterRen
if ($location->map_url) {
$tabs[__('location.map_url')] = sprintf(
'<div class="map">'
. '<iframe style="width: 100%%; min-height: 400px; border: 0 none;" src="%s"></iframe>'
. '<iframe style="width: 100%%; min-height: 75vh; border: 0 none;" src="%s"></iframe>'
. '</div>',
htmlspecialchars($location->map_url)
);
}
$tabs[__('Shifts')] = div('first', [
$tabs[__('general.shifts')] = div('first', [
$shiftsFilterRenderer->render(url('/locations', [
'action' => 'view',
'location_id' => $location->id,
@ -77,6 +93,7 @@ function location_view(Location $location, ShiftsFilterRenderer $shiftsFilterRen
]) : '',
$dect,
$description,
$neededAngelTypes,
tabs($tabs, $selected_tab),
],
true

View File

@ -215,6 +215,12 @@ class ShiftCalendarRenderer
{
$time = Carbon::createFromTimestamp($time);
$class = $label ? 'tick bg-' . theme_type() : 'tick ';
$diffNow = $time->diffInMinutes(null, false) * 60;
if ($diffNow >= 0 && $diffNow < self::SECONDS_PER_ROW) {
$class .= ' now';
}
if ($time->isStartOfDay()) {
if (!$label) {
return div($class . ' day');
@ -296,7 +302,7 @@ class ShiftCalendarRenderer
*/
private function calcBlocksPerSlot()
{
return ceil(
return (int) ceil(
($this->getLastBlockEndTime() - $this->getFirstBlockStartTime())
/ ShiftCalendarRenderer::SECONDS_PER_ROW
);

View File

@ -2,6 +2,7 @@
namespace Engelsystem;
use Engelsystem\Config\GoodieType;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftEntry;
@ -169,6 +170,15 @@ class ShiftCalendarShiftRenderer
$angeltype,
$shift_entries
);
$shift_can_signup = Shift_signup_allowed_angel(
$user,
$shift,
$angeltype,
null,
null,
$angeltype,
$shift_entries
);
$freeEntriesCount = $shift_signup_state->getFreeEntries();
$inner_text = _e('%d helper needed', '%d helpers needed', $freeEntriesCount, [$freeEntriesCount]);
@ -215,7 +225,7 @@ class ShiftCalendarShiftRenderer
$shifts_row .= join(', ', $entry_list);
$shifts_row .= '</li>';
return [
$shift_signup_state,
$shift_can_signup,
$shifts_row,
];
}
@ -243,9 +253,12 @@ class ShiftCalendarShiftRenderer
*/
private function renderShiftHead(Shift $shift, $class, $needed_angeltypes_count)
{
$goodie = GoodieType::from(config('goodie_type'));
$goodie_enabled = $goodie !== GoodieType::None;
$header_buttons = '';
if (auth()->can('admin_shifts')) {
$header_buttons = '<div class="ms-auto d-print-none">' . table_buttons([
$header_buttons = div('ms-auto d-print-none d-flex', [
button(
url('/user-shifts', ['edit_shift' => $shift->id]),
icon('pencil'),
@ -253,18 +266,38 @@ class ShiftCalendarShiftRenderer
'',
__('form.edit')
),
button(
url('/user-shifts', ['delete_shift' => $shift->id]),
icon('trash'),
'btn-' . $class . ' btn-sm border-light text-white',
'',
__('form.delete')
),
]) . '</div>';
form([
form_hidden('delete_shift', $shift->id),
form_submit(
'delete',
icon('trash'),
'btn-' . $class . ' btn-sm border-light text-white ms-1',
false,
'danger',
__('form.delete'),
[
'confirm_submit_title' => __('Do you want to delete the shift "%s" from %s to %s?', [
$shift->shiftType->name,
$shift->start->format(__('general.datetime')),
$shift->end->format(__('H:i')),
]),
'confirm_button_text' => icon('trash') . __('form.delete'),
]
),
], url('/user-shifts', ['delete_shift' => $shift->id])),
]);
}
$shift_heading = $shift->start->format('H:i') . ' &dash; '
$night_shift = '';
if ($shift->isNightShift() && $goodie_enabled) {
$night_shift = ' <i class="bi-moon-stars"></i>';
}
$shift_heading = '<span>'
. $shift->start->format('H:i') . ' &dash; '
. $shift->end->format('H:i') . ' &mdash; '
. htmlspecialchars($shift->shiftType->name);
. htmlspecialchars($shift->shiftType->name)
. $night_shift
. '</span>';
if ($needed_angeltypes_count > 0) {
$shift_heading = '<span class="badge bg-light text-danger me-1">' . $needed_angeltypes_count . '</span> ' . $shift_heading;

View File

@ -1,5 +1,6 @@
<?php
use Engelsystem\Config\GoodieType;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Location;
use Engelsystem\Models\Shifts\Shift;
@ -205,15 +206,29 @@ function ShiftEntry_edit_view(
$comment,
$freeloaded,
$freeloaded_comment,
$user_admin_shifts = false
$user_admin_shifts = false,
$angeltype_supporter = false
) {
$freeload_form = [];
if ($user_admin_shifts) {
$goodie = GoodieType::from(config('goodie_type'));
$goodie_enabled = $goodie !== GoodieType::None;
$goodie_tshirt = $goodie === GoodieType::Tshirt;
if ($user_admin_shifts || $angeltype_supporter) {
if (!$goodie_enabled) {
$freeload_info = __('freeload.freeloaded.info', [config('max_freeloadable_shifts')]);
} else {
$freeload_info = __('freeload.freeloaded.info.goodie', [($goodie_tshirt
? __('T-shirt score')
: __('Goodie score')),
config('max_freeloadable_shifts')]);
}
$freeload_form = [
form_checkbox('freeloaded', __('Freeloaded'), $freeloaded),
form_checkbox('freeloaded', __('Freeloaded') . ' <span class="bi bi-info-circle-fill text-info" data-bs-toggle="tooltip" title="' .
$freeload_info . '"></span>', $freeloaded),
form_textarea(
'freeloaded_comment',
__('Freeload comment (Only for shift coordination):'),
__('Freeload comment (Only for shift coordination and supporters):'),
$freeloaded_comment
),
];

View File

@ -87,14 +87,4 @@ class ShiftsFilterRenderer
$this->daySelectionEnabled = true;
$this->days = $days;
}
/**
* Should the filter display a day selection.
*
* @return bool
*/
public function isDaySelectionEnabled()
{
return $this->daySelectionEnabled;
}
}

View File

@ -1,5 +1,7 @@
<?php
use Engelsystem\Config\GoodieType;
use Engelsystem\Helpers\Carbon;
use Engelsystem\Models\AngelType;
use Engelsystem\Models\Location;
use Engelsystem\Models\Shifts\Shift;
@ -72,6 +74,35 @@ function Shift_editor_info_render(Shift $shift)
User_Nick_render($shift->updatedBy)
);
}
if ($shift->transaction_id) {
$info[] = sprintf(
icon('clock-history') . __('History ID: %s'),
$shift->transaction_id
);
}
if ($shift->schedule) {
$angeltypeSource = $shift->schedule->needed_from_shift_type
? __(
'shift.angeltype_source.shift_type',
[
'<a href="' . url('/admin/schedule/edit/' . $shift->schedule->id) . '">'
. htmlspecialchars($shift->schedule->name)
. '</a>',
'<a href="' . url('/admin/shifttypes/' . $shift->shift_type_id) . '">'
. htmlspecialchars($shift->shiftType->name)
. '</a>',
]
)
: __('shift.angeltype_source.location', [
'<a href="' . url('/admin/schedule/edit/' . $shift->schedule->id) . '">'
. htmlspecialchars($shift->schedule->name)
. '</a>',
location_name_render($shift->location),
]);
} else {
$angeltypeSource = __('Shift');
}
$info[] = sprintf(__('shift.angeltype_source'), $angeltypeSource);
return join('<br />', $info);
}
@ -124,7 +155,11 @@ function Shift_view(
$shift_admin = auth()->can('admin_shifts');
$user_shift_admin = auth()->can('user_shifts_admin');
$admin_locations = auth()->can('admin_locations');
$admin_shifttypes = auth()->can('shifttypes');
$admin_shifttypes = auth()->can('shifttypes.view');
$nightShiftsConfig = config('night_shifts');
$goodie = GoodieType::from(config('goodie_type'));
$goodie_enabled = $goodie !== GoodieType::None;
$goodie_tshirt = $goodie === GoodieType::Tshirt;
$parsedown = new Parsedown();
@ -160,7 +195,10 @@ function Shift_view(
}
if ($shift_signup_state->getState() === ShiftSignupStatus::SIGNED_UP) {
$content[] = info(__('You are signed up for this shift.'), true);
$content[] = info(__('You are signed up for this shift.')
. (($shift->start->subHours(config('last_unsubscribe')) < Carbon::now() && $shift->end > Carbon::now())
? ' ' . __('shift.sign_out.hint', [config('last_unsubscribe')])
: ''), true);
}
if (config('signup_advance_hours') && $shift->start->timestamp > time() + config('signup_advance_hours') * 3600) {
@ -174,7 +212,25 @@ function Shift_view(
if ($shift_admin || $admin_shifttypes || $admin_locations) {
$buttons = [
$shift_admin ? button(shift_edit_link($shift), icon('pencil'), '', '', __('form.edit')) : '',
$shift_admin ? button(shift_delete_link($shift), icon('trash'), 'btn-danger', '', __('form.delete')) : '',
$shift_admin ? form([
form_hidden('delete_shift', $shift->id),
form_submit(
'delete',
icon('trash'),
'',
false,
'danger',
__('form.delete'),
[
'confirm_submit_title' => __('Do you want to delete the shift "%s" from %s to %s?', [
$shift->shiftType->name,
$shift->start->format(__('general.datetime')),
$shift->end->format(__('H:i')),
]),
'confirm_button_text' => icon('trash') . __('form.delete'),
]
),
], url('/user-shifts', ['delete_shift' => $shift->id])) : '',
$admin_shifttypes
? button(url('/admin/shifttypes/' . $shifttype->id), htmlspecialchars($shifttype->name))
: '',
@ -211,11 +267,22 @@ function Shift_view(
$start = $shift->start->format(__('general.datetime'));
$night_shift_hint = '';
if ($shift->isNightShift() && $goodie_enabled) {
$night_shift_hint = ' <small><span class="bi bi-moon-stars text-info" data-bs-toggle="tooltip" title="'
. __('Night shifts between %d and %d am are multiplied by %d for the %s score.', [
$nightShiftsConfig['start'],
$nightShiftsConfig['end'],
$nightShiftsConfig['multiplier'],
($goodie_tshirt ? __('T-shirt') : __('goodie'))])
. '"></span></small>';
}
$link = button(url('/user-shifts'), icon('chevron-left'), 'btn-sm', '', __('general.back'));
return page_with_title(
$link . ' '
. htmlspecialchars($shift->shiftType->name)
. ' <small title="' . $start . '" data-countdown-ts="' . $shift->start->timestamp . '">%c</small>',
. ' <small title="' . $start . '" data-countdown-ts="' . $shift->start->timestamp . '">%c</small>'
. $night_shift_hint,
$content
);
}
@ -286,17 +353,21 @@ function Shift_view_render_shift_entry(ShiftEntry $shift_entry, $user_shift_admi
$isUser = $shift_entry->user_id == auth()->user()->id;
if ($user_shift_admin || $angeltype_supporter || $isUser) {
$entry .= ' <div class="btn-group m-1">';
if ($user_shift_admin || $isUser) {
$entry .= button_icon(
url('/user-myshifts', ['edit' => $shift_entry->id, 'id' => $shift_entry->user_id]),
'pencil',
'btn-sm',
__('form.edit')
);
}
$entry .= button_icon(
url('/user-myshifts', ['edit' => $shift_entry->id, 'id' => $shift_entry->user_id]),
'pencil',
'btn-sm',
__('form.edit')
);
$angeltype = $shift_entry->angelType;
$disabled = Shift_signout_allowed($shift, $angeltype, $shift_entry->user_id) ? '' : ' btn-disabled';
$entry .= button_icon(shift_entry_delete_link($shift_entry), 'trash', 'btn-sm btn-danger' . $disabled, __('form.delete'));
$entry .= button_icon(
shift_entry_delete_link($shift_entry),
'trash',
'btn-sm btn-danger' . $disabled,
__('form.delete'),
!Shift_signout_allowed($shift, $angeltype, $shift_entry->user_id)
);
$entry .= '</div>';
}
return $entry;

View File

@ -108,14 +108,15 @@ function UserAngelType_confirm_view(UserAngelType $user_angeltype, User $user, A
* @param UserAngelType $user_angeltype
* @param User $user
* @param AngelType $angeltype
* @param bool $isOwnAngelType
* @return string
*/
function UserAngelType_delete_view(UserAngelType $user_angeltype, User $user, AngelType $angeltype)
function UserAngelType_delete_view(UserAngelType $user_angeltype, User $user, AngelType $angeltype, bool $isOwnAngelType)
{
return page_with_title(__('Remove angeltype'), [
return page_with_title(__('Leave angeltype'), [
msg(),
info(sprintf(
__('Do you really want to delete %s from %s?'),
$isOwnAngelType ? __('Do you really want to leave "%2$s"?') : __('Do you really want to remove "%s" from "%s"?'),
$user->displayName,
$angeltype->name
), true),
@ -151,7 +152,9 @@ function UserAngelType_add_view(AngelType $angeltype, $users_source, $user_id)
msg(),
form([
form_info(__('Angeltype'), htmlspecialchars($angeltype->name)),
form_checkbox('auto_confirm_user', __('Confirm user'), true),
$angeltype->restricted
? form_checkbox('auto_confirm_user', __('Confirm user'), true)
: '',
form_select('user_id', __('general.user'), $users, $user_id),
form_submit('submit', icon('plus-lg') . __('Add')),
]),
@ -165,10 +168,11 @@ function UserAngelType_add_view(AngelType $angeltype, $users_source, $user_id)
*/
function UserAngelType_join_view($user, AngelType $angeltype)
{
$isOther = $user->id != auth()->user()->id;
return page_with_title(sprintf(__('Become a %s'), htmlspecialchars($angeltype->name)), [
msg(),
info(sprintf(
__('Do you really want to add %s to %s?'),
$isOther ? __('Do you really want to add %s to %s?') : __('Do you want to become a %2$s?'),
$user->displayName,
$angeltype->name
), true),

View File

@ -6,6 +6,7 @@ use Engelsystem\Models\AngelType;
use Engelsystem\Models\Group;
use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftEntry;
use Engelsystem\Models\User\PasswordReset;
use Engelsystem\Models\User\User;
use Engelsystem\Models\Worklog;
use Illuminate\Support\Collection;
@ -47,7 +48,7 @@ function User_edit_vouchers_view($user)
[
msg(),
info(sprintf(
$user->state->force_active
$user->state->force_active && config('enable_force_active')
? __('Angel can receive another %d vouchers and is FA.')
: __('Angel can receive another %d vouchers.'),
User_get_eligable_voucher_count($user)
@ -70,7 +71,7 @@ function User_edit_vouchers_view($user)
* @param int $active_count
* @param int $force_active_count
* @param int $freeloads_count
* @param int $tshirts_count
* @param int $goodies_count
* @param int $voucher_count
* @return string
*/
@ -81,7 +82,7 @@ function Users_view(
$active_count,
$force_active_count,
$freeloads_count,
$tshirts_count,
$goodies_count,
$voucher_count
) {
$goodie = GoodieType::from(config('goodie_type'));
@ -92,9 +93,7 @@ function Users_view(
$u = [];
$u['name'] = User_Nick_render($user)
. User_Pronoun_render($user)
. ($user->state->user_info
? ' <small><span class="bi bi-info-circle-fill text-info"></span></small>'
: '');
. user_info_icon($user);
$u['first_name'] = htmlspecialchars((string) $user->personalData->first_name);
$u['last_name'] = htmlspecialchars((string) $user->personalData->last_name);
$u['dect'] = sprintf('<a href="tel:%s">%1$s</a>', htmlspecialchars((string) $user->contact->dect));
@ -106,7 +105,7 @@ function Users_view(
$u['active'] = icon_bool($user->state->active);
$u['force_active'] = icon_bool($user->state->force_active);
if ($goodie_enabled) {
$u['got_shirt'] = icon_bool($user->state->got_shirt);
$u['got_goodie'] = icon_bool($user->state->got_goodie);
if ($goodie_tshirt) {
$u['shirt_size'] = $user->personalData->shirt_size;
}
@ -122,8 +121,9 @@ function Users_view(
'/admin-user',
['id' => $user->id]
),
'pencil',
icon('pencil'),
'btn-sm',
'',
__('form.edit')
),
]);
@ -136,7 +136,7 @@ function Users_view(
'active' => $active_count,
'force_active' => $force_active_count,
'freeloads' => $freeloads_count,
'got_shirt' => $tshirts_count,
'got_goodie' => $goodies_count,
'actions' => '<strong>' . count($usersList) . '</strong>',
];
@ -158,13 +158,15 @@ function Users_view(
}
$user_table_headers['freeloads'] = Users_table_header_link('freeloads', __('Freeloads'), $order_by);
$user_table_headers['active'] = Users_table_header_link('active', __('user.active'), $order_by);
$user_table_headers['force_active'] = Users_table_header_link('force_active', __('Forced'), $order_by);
if (config('enable_force_active')) {
$user_table_headers['force_active'] = Users_table_header_link('force_active', __('Forced'), $order_by);
}
if ($goodie_enabled) {
if ($goodie_tshirt) {
$user_table_headers['got_shirt'] = Users_table_header_link('got_shirt', __('T-Shirt'), $order_by);
$user_table_headers['got_goodie'] = Users_table_header_link('got_goodie', __('T-Shirt'), $order_by);
$user_table_headers['shirt_size'] = Users_table_header_link('shirt_size', __('Size'), $order_by);
} else {
$user_table_headers['got_shirt'] = Users_table_header_link('got_shirt', __('Goodie'), $order_by);
$user_table_headers['got_goodie'] = Users_table_header_link('got_goodie', __('Goodie'), $order_by);
}
}
$user_table_headers['arrival_date'] = Users_table_header_link(
@ -308,6 +310,12 @@ function User_view_shiftentries($needed_angel_type)
*/
function User_view_myshift(Shift $shift, $user_source, $its_me)
{
$nightShiftsConfig = config('night_shifts');
$goodie = GoodieType::from(config('goodie_type'));
$goodie_enabled = $goodie !== GoodieType::None;
$goodie_tshirt = $goodie === GoodieType::Tshirt;
$supporter = auth()->user()->isAngelTypeSupporter(AngelType::findOrFail($shift->angel_type_id));
$shift_info = '<a href="' . shift_link($shift) . '">' . htmlspecialchars($shift->shiftType->name) . '</a>';
if ($shift->title) {
$shift_info .= '<br /><a href="' . shift_link($shift) . '">' . htmlspecialchars($shift->title) . '</a>';
@ -316,6 +324,17 @@ function User_view_myshift(Shift $shift, $user_source, $its_me)
$shift_info .= User_view_shiftentries($needed_angel_type);
}
$night_shift = '';
if ($shift->isNightShift() && $goodie_enabled) {
$night_shift = ' <span class="bi bi-moon-stars text-info" data-bs-toggle="tooltip" title="'
. __('Night shifts between %d and %d am are multiplied by %d for the %s score.', [
$nightShiftsConfig['start'],
$nightShiftsConfig['end'],
$nightShiftsConfig['multiplier'],
($goodie_tshirt ? __('T-shirt') : __('goodie')),
])
. '"></span>';
}
$myshift = [
'date' => icon('calendar-event')
. $shift->start->format(__('general.date')) . '<br>'
@ -323,6 +342,7 @@ function User_view_myshift(Shift $shift, $user_source, $its_me)
. ' - '
. $shift->end->format(__('H:i')),
'duration' => sprintf('%.2f', ($shift->end->timestamp - $shift->start->timestamp) / 3600) . '&nbsp;h',
'hints' => $night_shift,
'location' => location_name_render($shift->location),
'shift_info' => $shift_info,
'comment' => '',
@ -336,23 +356,35 @@ function User_view_myshift(Shift $shift, $user_source, $its_me)
}
if ($shift->freeloaded) {
$myshift['duration'] = '<p class="text-danger">'
. sprintf('%.2f', -($shift->end->timestamp - $shift->start->timestamp) / 3600 * 2) . '&nbsp;h'
. '</p>';
if (auth()->can('user_shifts_admin')) {
$myshift['duration'] = '<p class="text-danger"><s>'
. sprintf('%.2f', ($shift->end->timestamp - $shift->start->timestamp) / 3600) . '&nbsp;h'
. '</s></p>';
if (auth()->can('user_shifts_admin') || $supporter) {
$myshift['comment'] .= '<br />'
. '<p class="text-danger">'
. __('Freeloaded') . ': ' . htmlspecialchars($shift->freeloaded_comment)
. '</p>';
} else {
$myshift['comment'] .= '<br /><p class="text-danger">' . __('Freeloaded') . '</p>';
$myshift['comment'] .= '<br /><p class="text-danger">'
. __('Freeloaded')
. '</p>';
}
if (!$goodie_enabled) {
$freeload_info = __('freeload.info');
} else {
$freeload_info = __('freeload.info.goodie', [($goodie_tshirt
? __('T-shirt score')
: __('Goodie score'))]);
}
$myshift['hints'] .= ' <span class="bi bi-info-circle-fill text-danger" data-bs-toggle="tooltip" title="'
. $freeload_info
. '"></span>';
}
$myshift['actions'] = [
button(shift_link($shift), icon('eye'), 'btn-sm btn-info', '', __('View')),
];
if ($its_me || auth()->can('user_shifts_admin')) {
if ($its_me || auth()->can('user_shifts_admin') || $supporter) {
$myshift['actions'][] = button(
url('/user-myshifts', ['edit' => $shift->shift_entry_id, 'id' => $user_source->id]),
icon('pencil'),
@ -382,8 +414,8 @@ function User_view_myshift(Shift $shift, $user_source, $its_me)
* @param Shift[]|Collection $shifts
* @param User $user_source
* @param bool $its_me
* @param int $tshirt_score
* @param bool $tshirt_admin
* @param string $goodie_score
* @param bool $goodie_admin
* @param Worklog[]|Collection $user_worklogs
* @param bool $admin_user_worklog_privilege
*
@ -393,18 +425,29 @@ function User_view_myshifts(
$shifts,
$user_source,
$its_me,
$tshirt_score,
$tshirt_admin,
$goodie_score,
$goodie_admin,
$user_worklogs,
$admin_user_worklog_privilege
) {
$goodie = GoodieType::from(config('goodie_type'));
$goodie_enabled = $goodie !== GoodieType::None;
$goodie_tshirt = $goodie === GoodieType::Tshirt;
$supported_angeltypes = auth()->user()
->userAngelTypes()
->where('supporter', true)
->pluck('angel_types.id');
$show_sum = true;
$myshifts_table = [];
$timeSum = 0;
foreach ($shifts as $shift) {
$key = $shift->start->timestamp . '-shift-' . $shift->shift_entry_id . $shift->id;
$supporter = $supported_angeltypes->contains($shift->angel_type_id);
if (!auth()->can('user_shifts_admin') && !$supporter && !$its_me) {
$show_sum = false;
continue;
}
$myshifts_table[$key] = User_view_myshift($shift, $user_source, $its_me);
if (!$shift->freeloaded) {
$timeSum += ($shift->end->timestamp - $shift->start->timestamp);
@ -435,18 +478,22 @@ function User_view_myshifts(
$shift['row-class'] = 'border-bottom border-info';
}
}
$myshifts_table[] = [
'date' => '<b>' . __('Sum:') . '</b>',
'duration' => '<b>' . sprintf('%.2f', round($timeSum / 3600, 2)) . '&nbsp;h</b>',
'location' => '',
'shift_info' => '',
'comment' => '',
'actions' => '',
];
if ($goodie_enabled && ($its_me || $tshirt_admin)) {
if ($show_sum) {
$myshifts_table[] = [
'date' => '<b>' . ($goodie_tshirt ? __('Your T-shirt score') : __('Your goodie score')) . '&trade;:</b>',
'duration' => '<b>' . $tshirt_score . '</b>',
'date' => '<b>' . __('Sum:') . '</b>',
'duration' => '<b>' . sprintf('%.2f', round($timeSum / 3600, 2)) . '&nbsp;h</b>',
'hints' => '',
'location' => '',
'shift_info' => '',
'comment' => '',
'actions' => '',
];
}
if ($goodie_enabled && ($its_me || $goodie_admin || auth()->can('admin_user'))) {
$myshifts_table[] = [
'date' => '<b>' . ($goodie_tshirt ? __('T-shirt score') : __('Goodie score')) . '&trade;:</b>',
'duration' => '<b>' . $goodie_score . '</b>',
'hints' => '',
'location' => '',
'shift_info' => '',
'comment' => '',
@ -489,6 +536,7 @@ function User_view_worklog(Worklog $worklog, $admin_user_worklog_privilege)
return [
'date' => icon('calendar-event') . date(__('general.date'), $worklog->worked_at->timestamp),
'duration' => sprintf('%.2f', $worklog->hours) . ' h',
'hints' => '',
'location' => '',
'shift_info' => __('Work log entry'),
'comment' => htmlspecialchars($worklog->comment) . '<br>'
@ -514,10 +562,11 @@ function User_view_worklog(Worklog $worklog, $admin_user_worklog_privilege)
* @param Group[] $user_groups
* @param Shift[]|Collection $shifts
* @param bool $its_me
* @param int $tshirt_score
* @param bool $tshirt_admin
* @param string $goodie_score
* @param bool $goodie_admin
* @param bool $admin_user_worklog_privilege
* @param Worklog[]|Collection $user_worklogs
* @param bool $admin_certificates
*
* @return string
*/
@ -529,10 +578,11 @@ function User_view(
$user_groups,
$shifts,
$its_me,
$tshirt_score,
$tshirt_admin,
$goodie_score,
$goodie_admin,
$admin_user_worklog_privilege,
$user_worklogs
$user_worklogs,
$admin_certificates
) {
$goodie = GoodieType::from(config('goodie_type'));
$goodie_enabled = $goodie !== GoodieType::None;
@ -541,15 +591,20 @@ function User_view(
$nightShiftsConfig = config('night_shifts');
$user_name = htmlspecialchars((string) $user_source->personalData->first_name) . ' '
. htmlspecialchars((string) $user_source->personalData->last_name);
$user_info_show = auth()->can('user.info.show');
$myshifts_table = '';
if ($its_me || $admin_user_privilege || $tshirt_admin) {
$user_angeltypes_supporter = false;
foreach ($user_source->userAngelTypes as $user_angeltype) {
$user_angeltypes_supporter = $user_angeltypes_supporter
|| $auth->user()->isAngelTypeSupporter($user_angeltype);
}
if ($its_me || $admin_user_privilege || $goodie_admin || $user_angeltypes_supporter) {
$my_shifts = User_view_myshifts(
$shifts,
$user_source,
$its_me,
$tshirt_score,
$tshirt_admin,
$goodie_score,
$goodie_admin,
$user_worklogs,
$admin_user_worklog_privilege
);
@ -557,13 +612,17 @@ function User_view(
$myshifts_table = div('table-responsive', table([
'date' => __('Day & Time'),
'duration' => __('Duration'),
'hints' => '',
'location' => __('Location'),
'shift_info' => __('Name & Workmates'),
'comment' => __('worklog.comment'),
'actions' => __('general.actions'),
], $my_shifts));
} elseif ($user_source->state->force_active) {
$myshifts_table = success(__('You have done enough.'), true);
} elseif ($user_source->state->force_active && config('enable_force_active')) {
$myshifts_table = success(
($its_me ? __('You have done enough.') : (__('%s has done enough.', [$user_source->name]))),
true
);
}
}
@ -579,32 +638,20 @@ function User_view(
return page_with_title(
'<span class="icon-icon_angel"></span> '
. (
(config('enable_pronoun') && $user_source->personalData->pronoun)
? '<small>' . htmlspecialchars($user_source->personalData->pronoun) . '</small> '
: ''
)
. htmlspecialchars($user_source->name)
. (config('enable_user_name') ? ' <small>' . $user_name . '</small>' : '')
. (
(($user_info_show || auth()->can('admin_arrive')) && $user_source->state->user_info)
? (
' <small><span class="bi bi-info-circle-fill text-info" '
. ($user_info_show
? 'data-bs-toggle="tooltip" title="' . htmlspecialchars($user_source->state->user_info)
: '')
. '"></span></small>'
)
: ''
),
. ((config('enable_pronoun') && $user_source->personalData->pronoun)
? ' <small>(' . htmlspecialchars($user_source->personalData->pronoun) . ')</small> '
: '')
. user_info_icon($user_source),
[
msg(),
div('row', [
div('col-md-12', [
table_buttons([
$auth->can('user.edit.shirt') && $goodie_enabled ? button(
$auth->can('user.goodie.edit') && $goodie_enabled ? button(
url('/admin/user/' . $user_source->id . '/goodie'),
icon('person') . ($goodie_tshirt ? __('Shirt') : __('Goodie'))
icon('person') . ($goodie_tshirt ? __('T-shirt') : __('Goodie'))
) : '',
$admin_user_privilege ? button(
url('/admin-user', ['id' => $user_source->id]),
@ -625,6 +672,13 @@ function User_view(
icon('valentine') . __('Vouchers')
)
: '',
(
$admin_certificates
&& (config('ifsg_enabled') || config('driving_license_enabled'))
) ? button(
url('/users/' . $user_source->id . '/certificates'),
icon('card-checklist') . __('settings.certificates')
) : '',
$admin_user_worklog_privilege ? button(
url('/admin/user/' . $user_source->id . '/worklog'),
icon('clock-history') . __('worklog.add')
@ -643,13 +697,9 @@ function User_view(
url('/shifts-json-export', ['key' => $user_source->api_key]),
icon('braces') . __('JSON Export')
) : '',
(
$auth->can('shifts_json_export')
|| $auth->can('ical')
|| $auth->can('atom')
) ? button(
url('/user-myshifts', ['reset' => 1]),
icon('arrow-repeat') . __('Reset API key')
$auth->canAny(['api', 'shifts_json_export', 'ical', 'atom']) ? button(
url('/settings/api'),
icon('arrow-repeat') . __('API Settings')
) : '',
], 'mb-2') : '',
]),
@ -687,14 +737,15 @@ function User_view(
User_groups_render($user_groups),
$admin_user_privilege ? User_oauth_render($user_source) : '',
]),
($its_me || $admin_user_privilege) ? '<h2>' . __('Shifts') . '</h2>' : '',
($its_me || $admin_user_privilege) ? '<h2>' . __('general.shifts') . '</h2>' : '',
$myshifts_table,
($its_me && $nightShiftsConfig['enabled'] && $goodie_enabled) ? info(
sprintf(
icon('info-circle') . __('Your night shifts between %d and %d am count twice for the %s score.'),
$nightShiftsConfig['start'],
$nightShiftsConfig['end'],
($goodie_tshirt ? __('T-shirt') : __('goodie'))
icon('moon-stars') . __('Night shifts between %d and %d am are multiplied by %d for the %s score.', [
$nightShiftsConfig['start'],
$nightShiftsConfig['end'],
$nightShiftsConfig['multiplier'],
($goodie_tshirt ? __('T-shirt') : __('goodie'))])
),
true,
true
@ -767,6 +818,9 @@ function User_view_state_admin($freeloader, $user_source)
$goodie = GoodieType::from(config('goodie_type'));
$goodie_enabled = $goodie !== GoodieType::None;
$goodie_tshirt = $goodie === GoodieType::Tshirt;
$password_reset = PasswordReset::whereUserId($user_source->id)
->where('created_at', '>', $user_source->last_login_at ?: '')
->count();
if ($freeloader) {
$state[] = '<span class="text-danger">' . icon('exclamation-circle') . __('Freeloader') . '</span>';
@ -782,12 +836,12 @@ function User_view_state_admin($freeloader, $user_source)
)
. '</span>';
if ($user_source->state->force_active) {
if ($user_source->state->force_active && config('enable_force_active')) {
$state[] = '<span class="text-success">' . __('user.force_active') . '</span>';
} elseif ($user_source->state->active) {
$state[] = '<span class="text-success">' . __('user.active') . '</span>';
}
if ($user_source->state->got_shirt && $goodie_enabled) {
if ($user_source->state->got_goodie && $goodie_enabled) {
$state[] = '<span class="text-success">' . ($goodie_tshirt ? __('T-shirt') : __('Goodie')) . '</span>';
}
} else {
@ -817,6 +871,10 @@ function User_view_state_admin($freeloader, $user_source)
}
}
if ($password_reset) {
$state[] = __('Password reset in progress');
}
return $state;
}
@ -968,7 +1026,7 @@ function render_user_freeloader_hint()
{
if (auth()->user()->isFreeloader()) {
return sprintf(
__('You freeloaded at least %s shifts. Shift signup is locked. Please go to heavens desk to be unlocked again.'),
__('freeload.freeloader.info'),
config('max_freeloadable_shifts')
);
}
@ -1002,7 +1060,7 @@ function render_user_arrived_hint(bool $is_sys_menu = false)
/**
* @return string|null
*/
function render_user_tshirt_hint()
function render_user_goodie_hint()
{
$goodie = GoodieType::from(config('goodie_type'));
$goodie_tshirt = $goodie === GoodieType::Tshirt;

View File

@ -34,6 +34,7 @@
"css-minimizer-webpack-plugin": "^4.2.2",
"editorconfig-checker": "^5.1.1",
"eslint": "^8.44.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-editorconfig": "^4.0.3",
"mini-css-extract-plugin": "^2.7.2",
"postcss": "^8.4.31",

View File

@ -8,3 +8,8 @@ parameters:
- tests
scanDirectories:
- includes
ignoreErrors:
-
message: '#.*#'
path: config/config.php
reportUnmatched: false

View File

@ -6,6 +6,7 @@ use Engelsystem\Application;
use Engelsystem\Middleware\Dispatcher;
use Psr\Http\Message\ServerRequestInterface;
// Include app bootstrapping
require_once realpath(__DIR__ . '/../includes/engelsystem.php');
/** @var Application $app */
@ -18,4 +19,5 @@ $middleware = $app->getMiddleware();
$dispatcher = new Dispatcher($middleware);
$dispatcher->setContainer($app);
// Handle the request
$dispatcher->handle($request);

View File

@ -36,12 +36,20 @@ tags:
- name: user
description: User information
security:
- bearer-auth: [ ]
- api-key-header: [ ]
components:
securitySchemes:
bearerAuth:
bearer-auth:
type: http
scheme: bearer
bearerFormat: API key from settings
api-key-header:
type: apiKey
name: x-api-key
in: header
responses:
UnauthorizedError: # 401
@ -391,12 +399,9 @@ components:
properties:
start:
$ref: '#/components/schemas/DateTimeOptional'
end:
$ref: '#/components/schemas/DateTimeOptional'
required:
- start
- end
teardown:
event:
type: object
properties:
start:
@ -406,6 +411,13 @@ components:
required:
- start
- end
teardown:
type: object
properties:
end:
$ref: '#/components/schemas/DateTimeOptional'
required:
- end
required:
- api
- spec
@ -414,11 +426,9 @@ components:
- url
- timezone
- buildup
- event
- teardown
security:
- bearerAuth: [ ]
paths:
/angeltypes:
get:

View File

@ -0,0 +1,18 @@
import { ready } from './ready';
ready(() => {
[...document.getElementsByClassName('prevent-default')].forEach((element) => {
let preventDefault = (e) => {
e.preventDefault();
return false;
};
element.addEventListener('submit', preventDefault);
element.addEventListener('click', preventDefault);
});
document.getElementById('delete-form')?.addEventListener('submit', (event) => {
event.preventDefault();
console.log('Delete confirmed');
});
});

View File

@ -318,6 +318,7 @@ ready(() => {
</div>
<div class="modal-footer">
<button type="button" class="${element.className}"
autofocus
title="${element.title}" data-submit="">
${element.dataset.confirm_button_text ?? element.innerHTML}
</button>
@ -328,15 +329,25 @@ ready(() => {
`
);
let modal = document.getElementById('confirmation-modal');
const modal = document.getElementById('confirmation-modal');
modal.addEventListener('hide.bs.modal', () => {
modalOpen = false;
});
modal.querySelector('[data-submit]').addEventListener('click', (event) => {
const modalSubmitButton = modal.querySelector('[data-submit]');
modalSubmitButton.addEventListener('click', () => {
element.type = oldType;
element.click();
});
/**
* After the modal has been shown, focus on the "Submit" button in the modal
* so that it can be confirmed with "Enter".
*/
modal.addEventListener('shown.bs.modal', () => {
modalSubmitButton.focus();
});
modalOpen = true;
let bootstrapModal = new bootstrap.Modal(modal);
bootstrapModal.show();
@ -349,7 +360,7 @@ ready(() => {
*/
ready(() => {
[
['welcome-title', '.btn-group .btn.d-none'],
['welcome-title', '.registration .d-none'],
['settings-title', '.user-settings .nav-item'],
['oauth-settings-title', 'table tr.d-none'],
].forEach(([id, selector]) => {

View File

@ -3,3 +3,4 @@ window.bootstrap = require('bootstrap');
import './forms';
import './countdown';
import './dashboard';
import './design';

View File

@ -18,7 +18,6 @@
&-40 {
// >= 40 bars
--barchart-bar-margin: 1px;
--barchart-bar-margin: 0.5px;
--barchart-group-margin: 0.5%;
}

View File

@ -23,6 +23,8 @@ $theme-colors: map-merge($theme-colors, $custom-colors);
$form-label-font-weight: $font-weight-bold;
$choices-guttering: auto;
@import '~bootstrap/scss/utilities';
@import '~bootstrap/scss/root';
@ -100,6 +102,10 @@ a .icon-icon_angel {
background-color: $link-color;
}
a .disabled {
pointer-events: none;
}
.navbar .icon-icon_angel {
background-color: $nav-link-disabled-color;
}
@ -245,6 +251,10 @@ table .border-bottom {
font-size: 0.9em;
padding-left: 5px;
}
.tick.now {
border-top: 2px solid $info;
}
}
.lane.time {

View File

@ -10,6 +10,7 @@ $choices-bg-color-dropdown: $input-bg;
$choices-text-color: $input-color;
$es-choices-highlight-color: $choices-text-color !default;
$choices-bg-color-disabled: $input-disabled-bg;
@import '~choices.js/src/styles/choices.scss';

View File

@ -0,0 +1,200 @@
// cccamp23
// Variables
// --------------------------------------------------
@import 'dark';
//== changed Colors
$gray-dark: #231f20;
$gray-darker: darken($gray-dark, 30%);
$gray: lighten($gray-dark, 30%);
$gray-light: lighten($gray, 30%);
$gray-lighter: lighten($gray-light, 30%);
$dark: $gray-dark;
$primary: #de37ff;
$secondary: #28ffff;
$success: #79ff5e;
$info: #3dbddf;
$warning: #f6b345;
$danger: #de4040;
$text-muted: $gray-light;
$btn-link-disabled-color: $gray-light;
$dropdown-bg: #212529;
$dropdown-link-hover-color: #000000;
//== changed Forms
$input-bg: $gray-darker;
$input-bg-disabled: lighten($gray-lighter, 15%);
$input-border-color: $secondary;
$input-group-addon-bg: $input-bg;
$form-check-input-border: 1px solid $gray;
//== changed Pagination
$pagination-hover-color: $gray-lighter;
$pagination-active-color: $gray-lighter;
//== changed Form states and alerts
$state-success-text: #fff;
$state-success-bg: $success;
$state-success-border: darken($state-success-bg, 5%);
$state-info-text: #fff;
$state-info-bg: $info;
$state-info-border: darken($state-info-bg, 7%);
$state-warning-text: #fff;
$state-warning-bg: $warning;
$state-warning-border: darken($state-warning-bg, 3%);
$state-danger-text: #fff;
$state-danger-bg: $danger;
$state-danger-border: darken($state-danger-bg, 3%);
$headings-small-color: $gray-light;
code {
background-color: $state-info-bg;
color: $state-info-text;
}
$alert-bg-scale: 0%;
$alert-border-scale: 0%;
$alert-color-scale: 0%;
// Navs =======================================================================
$nav-tabs-link-active-border-color: $gray-dark;
$nav-tabs-link-active-color: $gray-darker;
$nav-pills-link-active-color: $gray-darker;
//== Pagination
//
//##
$pagination-color: $gray-lighter;
$pagination-border-color: $gray-dark;
$pagination-hover-color: #000;
$pagination-hover-border-color: $gray-dark;
$pagination-active-color: #000;
$pagination-active-border-color: $gray-dark;
$pagination-disabled-color: $gray-light;
$pagination-disabled-border-color: $gray-dark;
// dark
@import 'cyborg_variables';
@import 'cyborg_styles';
//== Typography
@font-face {
font-family: 'VCR OCD Faux';
src: url('theme17/VCROCDFaux.ttf') format('truetype'), url('theme17/VCROCDFaux.woff') format('woff'),
url('theme17/VCROCDFaux.woff2') format('woff2');
font-weight: 400;
}
@font-face {
font-family: Gabriella;
src: url('theme17/GabriellaHeavy.otf') format('opentype');
font-weight: 400;
}
$headings-font-family: Gabriella, $font-family-sans-serif;
$font-family-monospace: 'VCR OCD Faux', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
h1,
.h1,
h2,
.h2,
h3,
.h3,
h4,
.h4,
h5,
.h5 {
color: $white;
}
h1,
.h1 {
font-family: $headings-font-family;
}
.btn-secondary {
background: transparent !important;
border-color: $secondary !important;
color: $secondary !important;
}
// Specials ===================================================================
.bg-success a,
.bg-primary a,
.bg-warning a,
.bg-info a,
.bg-light a {
color: $gray-darker !important;
}
.bg-body a,
.bg-danger a,
.bg-secondary a,
.bg-dark a {
color: $state-danger-text !important;
}
.bg-primary,
.bg-success,
.bg-warning,
.bg-info,
.bg-light {
color: $gray-darker;
}
.bg-body,
.bg-danger,
.bg-secondary,
.bg-dark {
color: $state-danger-text;
}
.navbar {
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
}
.navbar-brand {
color: $primary;
font-family: $font-family-monospace;
}
.nav-tabs,
.nav-pills,
.pager {
a {
color: $gray-lighter;
}
}
.alert a {
color: $gray-darker;
}
.alert.alert-danger,
.alert.alert-danger a {
color: $gray-lighter;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -85,9 +85,6 @@ msgstr "Das Programm konnte nicht abgerufen werden."
msgid "schedule.import.read-error"
msgstr "Das Programm konnte nicht gelesen werden."
msgid "schedule.import.invalid-shift-type"
msgstr "Der Schichttyp konnte nicht gefunden werden."
msgid "schedule.import.success"
msgstr "Das Programm wurde erfolgreich importiert."

View File

@ -32,6 +32,9 @@ msgstr ""
msgid "password.email.message"
msgstr "Um dein Passwort zurückzusetzen, besuche %s"
msgid "page.error.title"
msgstr "Fehler %s"
msgid "page.403.title"
msgstr "Nicht erlaubt"
@ -87,7 +90,7 @@ msgid "form.submit"
msgstr "Absenden"
msgid "form.send_notification"
msgstr "Benachrichtigungen versenden"
msgstr "%d Benachrichtigungen versenden"
msgid "credits.source"
msgstr "Quellcode"
@ -174,6 +177,9 @@ msgstr "Lösche Engeltyp %s"
msgid "Please check the name. Maybe it already exists."
msgstr "Bitte überprüfe den Namen. Vielleicht ist er bereits vergeben."
msgid "Angel type saved."
msgstr "Engeltyp wurde gespeichert."
msgid "Create angeltype"
msgstr "Engeltyp erstellen"
@ -318,8 +324,8 @@ msgstr "Benötigte Engel"
msgid "Shift deleted."
msgstr "Schicht gelöscht."
msgid "Do you want to delete the shift %s from %s to %s?"
msgstr "Möchtest Du die Schicht %s von %s bis %s löschen?"
msgid "Do you want to delete the shift \"%s\" from %s to %s?"
msgstr "Möchtest Du die Schicht \"%s\" von %s bis %s löschen?"
msgid "Shift could not be found."
msgstr "Schicht konnte nicht gefunden werden."
@ -362,11 +368,17 @@ msgstr "Engeltyp für Benutzer bestätigen"
msgid "You are not allowed to delete this users angeltype."
msgstr "Du darfst diesen Benutzer nicht von diesem Engeltyp entfernen."
msgid "User %s removed from %s."
msgstr "Benutzer %s von %s entfernt."
msgid "User \"%s\" removed from \"%s\"."
msgstr "Benutzer \"%s\" von \"%s\" entfernt."
msgid "Remove angeltype"
msgstr "Engeltyp löschen"
msgid "Edit certificates"
msgstr "Zertifikate bearbeiten"
msgid "You successfully left \"%2$s\"."
msgstr "Du hast erfolgreich \"%2$s\" verlassen."
msgid "Leave angeltype"
msgstr "Engeltyp verlassen"
msgid "You are not allowed to set supporter rights."
msgstr "Du darfst keine Supporterrechte bearbeiten."
@ -515,7 +527,7 @@ msgstr "Suche Engel:"
msgid "Show all shifts"
msgstr "Alle Schichten anzeigen"
msgid "How much angels should be active?"
msgid "How many angels should be active?"
msgstr "Wie viele Engel sollten aktiv sein?"
msgid "Size"
@ -524,20 +536,23 @@ msgstr "Größe"
msgid "No."
msgstr "Nr."
msgid "Shifts"
msgid "general.shifts"
msgstr "Schichten"
msgid "Length"
msgstr "Länge"
msgid "Active?"
msgstr "Aktiv?"
msgid "Length (in minutes)"
msgstr "Länge (in Minuten)"
msgid "Active"
msgstr "Aktiv"
msgid "Forced"
msgstr "Erzwungen"
msgid "T-shirt?"
msgstr "T-Shirt?"
msgid "T-shirt"
msgstr "T-Shirt"
msgid "T-shirt statistic"
msgstr "T-Shirt Statistik"
@ -593,6 +608,18 @@ msgstr "Engeltyp"
msgid "shift.next"
msgstr "Nächste Schicht"
msgid "general.shift"
msgstr "Schicht"
msgid "shift.angeltype_source"
msgstr "Benötigte Engel von: %s"
msgid "shift.angeltype_source.shift_type"
msgstr "Programm %s via Schichttyp %s"
msgid "shift.angeltype_source.location"
msgstr "Programm %s via Ort %s"
msgid "Last shift"
msgstr "Letzte Schicht"
@ -734,22 +761,8 @@ msgstr "User bearbeiten"
msgid "general.datetime"
msgstr "d.m.Y H:i"
msgid "Key changed."
msgstr "Key geändert."
msgid "Reset API key"
msgstr "API-Key zurücksetzen"
msgid ""
"If you reset the key, the url to your iCal- and JSON-export and your atom/rss "
"feed changes! You have to update it in every application using one of these "
"exports."
msgstr ""
"Wenn du den API-Key zurücksetzt, ändert sich die URL zu deinem iCal-, JSON-"
"Export und Atom/RSS Feed! Du musst diesen überall ändern, wo er in Benutzung ist."
msgid "Continue"
msgstr "Fortfahren"
msgid "API Settings"
msgstr "API Einstellungen"
msgid "Please enter a freeload comment!"
msgstr "Gib bitte einen Schwänz-Kommentar ein!"
@ -819,16 +832,13 @@ msgid "iCal export and API"
msgstr "iCal Export und API"
msgid ""
"Export your own shifts. <a href=\"%s\">iCal format</a> or <a href=\"%s"
"\">JSON format</a> available (please keep secret, otherwise <a href=\"%s"
"\">reset the api key</a>)."
"Export your own shifts formatted as <a href=\"%s\" target=\"_blank\">iCal</a> or "
"<a href=\"%s\" target=\"_blank\">JSON</a> (please keep the link secret, otherwise you have to reset the api key "
"<a href=\"%s\">in your settings</a>)."
msgstr ""
"Exportiere Deine Schichten. <a href=\"%s\">iCal Format</a> oder <a href=\"%s"
"\">JSON Format</a> verfügbar (Link bitte geheimhalten, sonst <a href=\"%s"
"\">API-Key zurücksetzen</a>)."
msgid "Show API Key"
msgstr "API Key anzeigen"
"Exportiere Deine Schichten im <a href=\"%s\" target=\"_blank\">iCal</a> oder <a href=\"%s"
"\" target=\"_blank\">JSON</a> Format (Link bitte geheimhalten, sonst musst du den API-Key in "
"<a href=\"%s\">deinen Einstellungen</a> zurücksetzen)."
msgid "All"
msgstr "Alle"
@ -893,9 +903,15 @@ msgstr "Kontakt"
msgid "Primary contact person/desk for user questions."
msgstr "Ansprechpartner für Fragen."
msgid "my driving license"
msgid "My driving license"
msgstr "Meine Führerschein-Infos"
msgid "angeltype.ifsg.required.info.preview"
msgstr "Dieser Engeltyp benötigt eine Gesundheitsbelehrung."
msgid "angeltype.driving_license.required.info.preview"
msgstr "Dieser Engeltyp benötigt Führerschein-Infos."
msgid ""
"This angeltype requires a driver license. Please enter your driver license "
"information!"
@ -1062,8 +1078,8 @@ msgstr "Schicht Anmeldung"
msgid "Freeloaded"
msgstr "Geschwänzt"
msgid "Freeload comment (Only for shift coordination):"
msgstr "Schwänzer Kommentar (Nur für die Schicht-Koordination):"
msgid "Freeload comment (Only for shift coordination and supporters):"
msgstr "Schwänzer Kommentar (Nur für Supporter und die Schicht-Koordination):"
msgid "Edit shift entry"
msgstr "Schichteintrag bearbeiten"
@ -1086,6 +1102,9 @@ msgstr "erstellt am %s von %s"
msgid "edited at %s by %s"
msgstr "bearbeitet am %s von %s"
msgid "History ID: %s"
msgstr "Historien-ID: %s"
msgid "This shift is in the far future and becomes available for signup at %s."
msgstr ""
"Diese Schicht liegt in der fernen Zukunft und du kannst dich ab %s eintragen."
@ -1105,12 +1124,18 @@ msgstr "Möchtest Du wirklich alle Benutzer als %s bestätigen?"
msgid "Do you really want to confirm %s for %s?"
msgstr "Möchtest Du wirklich %s für %s bestätigen?"
msgid "Do you really want to delete %s from %s?"
msgstr "Möchtest Du wirklich %s von %s entfernen?"
msgid "Do you really want to remove \"%s\" from \"%s\"?"
msgstr "Möchtest Du wirklich \"%s\" von \"%s\" entfernen?"
msgid "Do you really want to leave \"%2$s\"?"
msgstr "Möchtest Du \"%2$s\" wirklich verlassen?"
msgid "Do you really want to add %s to %s?"
msgstr "Möchtest Du wirklich %s zu %s hinzufügen?"
msgid "Do you want to become a %2$s?"
msgstr "Möchtest Du ein %2$s werden?"
msgid "Confirm user"
msgstr "Benutzer bestätigen"
@ -1145,9 +1170,6 @@ msgstr "Gutschein"
msgid "Freeloads"
msgstr "Schwänzereien"
msgid "T-shirt"
msgstr "T-Shirt"
msgid "Last login"
msgstr "Letzter Login"
@ -1169,8 +1191,8 @@ msgstr "Austragen"
msgid "Sum:"
msgstr "Summe:"
msgid "Your T-shirt score"
msgstr "Dein T-Shirt Score"
msgid "T-shirt score"
msgstr "T-Shirt Score"
msgid "Work log entry"
msgstr "Arbeitseinsatz"
@ -1190,6 +1212,9 @@ msgstr "Name & Kollegen"
msgid "You have done enough."
msgstr "Du hast genug gemacht."
msgid "%s has done enough."
msgstr "%s hat genug gemacht."
msgid "Vouchers"
msgstr "Gutscheine"
@ -1199,8 +1224,8 @@ msgstr "iCal Export"
msgid "JSON Export"
msgstr "JSON Export"
msgid "Your night shifts between %d and %d am count twice for the %s score."
msgstr "Deine Nachtschichten zwischen %d und %d Uhr zählen für den %s Score doppelt."
msgid "Night shifts between %d and %d am are multiplied by %d for the %s score."
msgstr "Nachtschichten zwischen %d und %d Uhr werden für den %4$s Score mit %3$d multipliziert."
msgid ""
"Go to the <a href=\"%s\">shifts table</a> to sign yourself up for some "
@ -1250,13 +1275,6 @@ msgstr ""
"Bitte gib Dein geplantes Abreisedatum an, damit wir ein Gefühl für die Abbau-"
"Planung bekommen."
msgid ""
"You freeloaded at least %s shifts. Shift signup is locked. Please go to "
"heavens desk to be unlocked again."
msgstr ""
"Du hast mindestens %s Schichten geschwänzt. Schicht-Registrierung ist "
"gesperrt. Bitte gehe zum Himmelsschreibtisch um wieder entsperrt zu werden."
msgid "tshirt.required.hint"
msgstr "Bitte gib eine T-Shirt-Größe in deinen Einstellungen an."
@ -1330,8 +1348,8 @@ msgstr "Goodie"
msgid "goodie"
msgstr "Goodie"
msgid "Your goodie score"
msgstr "Dein Goodie Score"
msgid "Goodie score"
msgstr "Goodie Score"
msgid "Given goodies"
msgstr "Ausgegebene Goodies"
@ -1345,9 +1363,6 @@ msgstr "Goodie entfernen"
msgid "Got goodie"
msgstr "Goodie bekommen"
msgid "Goodie?"
msgstr "Goodie?"
msgid "Angel has got a goodie."
msgstr "Engel hat ein Goodie bekommen."
@ -1441,6 +1456,9 @@ msgstr "Minuten vor Talk beginn hinzufügen"
msgid "schedule.minutes-after"
msgstr "Minuten nach Talk ende hinzufügen"
msgid "schedule.for_locations"
msgstr "Für Orte"
msgid "schedule.import.locations.add"
msgstr "Neue Orte"
@ -1520,7 +1538,7 @@ msgid "news.comments.delete.title"
msgstr "Kommentar \"%s\" löschen"
msgid "notification.news.updated.introduction"
msgstr "Die News %1$s wurde aktualisiert"
msgstr "Die News \"%1$s\" wurde aktualisiert"
msgid "notification.news.updated.text"
msgstr "Du kannst sie dir unter %3$s anschauen."
@ -1600,7 +1618,11 @@ msgstr "Benachrichtige mich bei neuen privaten Nachrichten."
msgid "settings.profile.email_by_human_allowed"
msgstr "Erlaube Himmel-Engeln mich per E-Mail zu kontaktieren."
msgid "settings.profile.email_goody"
msgid "settings.profile.email_goodie"
msgstr "Um gegebenenfalls Voucher für das nächste gleichartige Event zu erhalten stimme ich zu, "
"dass mein Nick, E-Mail-Adresse und geleistete Arbeit solange gespeichert werden."
msgid "settings.profile.email_tshirt"
msgstr "Um gegebenenfalls Voucher für das nächste gleichartige Event zu erhalten stimme ich zu, "
"dass mein Nick, E-Mail-Adresse, geleistete Arbeit und T-Shirt-Größe solange gespeichert werden."
@ -1610,6 +1632,16 @@ msgstr "Dies kann jederzeit durch eine E-Mail an <a href=\"mailto:%s\">%1$s</a>
msgid "settings.profile.shirt_size"
msgstr "T-Shirt-Größe"
msgid "settings.profile.shirt_size.hint"
msgstr "Ein straight-cut T-Shirt hat breite Schultern und einen fast quadratischen Körper. "
"Ein fitted-cut (tailliertes) T-Shirt hat eine geschwungene Seitennaht, die an der Taille schmaler "
"ist und einen größeren Brust- sowie Hüft-Umfang hat. "
"Normalerweise fallen fitted-cut (taillierte) T-Shirts kleiner aus als straight-cut T-Shirts in der gleichen Größe, "
"außerdem sind die Größen von Marke zu Marke unterscheiden."
msgid "settings.profile.shirt.link"
msgstr "Mehr Informationen"
msgid "settings.profile.angeltypes.info"
msgstr "Du kannst deine Engeltypen <a href=\"%s\">auf der Engeltypen-Seite</a> verwalten."
@ -1693,11 +1725,31 @@ msgstr "Gabelstapler"
msgid "settings.certificates.ifsg_light"
msgstr "Ich wurde vor Ort nach IfSG §43 (Frikadellendiplom light) belehrt."
msgid "settings.certificates.ifsg_light_admin"
msgstr "Wurde vor Ort nach IfSG §43 (Frikadellendiplom light) belehrt."
msgid "settings.certificates.ifsg"
msgstr "Ich habe eine Belehrung nach §43 IfSG (Frikadellendiplom) bei meinem Gesundheitsamt "
"erhalten und innerhalb von 3 Monaten die Zweitbelehrung durch uns oder meinen Arbeitgeber/Koch/Verein bekommen. "
"Zusätzlich ist die Zweitbelehrung nicht älter als zwei Jahre."
msgid "settings.certificates.ifsg_admin"
msgstr "Hat eine Belehrung nach §43 IfSG (Frikadellendiplom) vom Gesundheitsamt "
"erhalten und innerhalb von 3 Monaten die Zweitbelehrung durch uns oder einen Arbeitgeber/Koch/Verein bekommen. "
"Zusätzlich ist die Zweitbelehrung nicht älter als zwei Jahre."
msgid "settings.certificates.confirmed"
msgstr "Zertifikat bestätigt"
msgid "settings.certificates.ifsg_confirmed.hint"
msgstr "Deine Gesundheitsbelehrung wurde bestätigt, du kannst deine Angaben nicht mehr selber ändern."
msgid "settings.certificates.drive_confirmed.hint"
msgstr "Dein Führerschein wurde bestätigt, du kannst deine Angaben nicht mehr selber ändern."
msgid "settings.certificates.confirmation.info"
msgstr "Du hast persönlich überprüft, dass die Zertifizierung / Bescheinigung den Anforderungen genügt."
msgid "settings.certificates.success"
msgstr "Zertifikate wurden erfolgreich aktualisiert."
@ -1745,9 +1797,12 @@ msgstr "API"
msgid "settings.api.about"
msgstr ""
"Die API erlaubt es dir, über externe Programme, mit dem Engelsystem zu interagieren. "
"Die API erlaubt es dir, über externe Programme, mit dem %s zu interagieren. "
"Sie ist noch nicht vollständig, wir arbeiten aber daran sie zu erweitern.\n"
"Der API Einstiegspunkt befindet sich unter `%s` und ist in der [OpenAPI Spezifikation](%s) beschrieben.\n"
"Der Einstiegspunkt der API befindet sich unter `%s` und ist in der [OpenAPI Spezifikation](%s) beschrieben."
msgid "settings.api.about.warning"
msgstr ""
"Teile deinen persönlichen API Key mit niemandem, er erlaubt es deine persönlichen Daten einzusehen "
"und Änderungen in deinem Namen durch zu führen!"
@ -1811,6 +1866,9 @@ msgstr "FAQ \"%s\" löschen"
msgid "question.questions"
msgstr "Fragen"
msgid "question.admin"
msgstr "Fragen beantworten"
msgid "question.faq_link"
msgstr "Hast du eine generelle Frage? Vielleicht ist diese schon in den <a href=\"%s\">FAQ</a> beantwortet."
@ -1829,6 +1887,9 @@ msgstr "Antwort"
msgid "question.delete.title"
msgstr "Frage \"%s\" löschen"
msgid "question.contact_options"
msgstr "Weitere Kontaktmöglichkeiten: "
msgid "user.edit.shirt"
msgstr "T-Shirt bearbeiten"
@ -1911,9 +1972,6 @@ msgstr "Dieser Engeltyp benötigt eine Einweisung bei einem Einführungstreffen.
msgid "angeltypes.can-change-later"
msgstr "Du kannst Deine Auswahl später in den Einstellungen ändern."
msgid "angeltypes.email"
msgstr "E-Mail"
msgid "angeltypes.hide_on_shift_view"
msgstr "Auf Schicht-Ansicht ausblenden"
@ -1921,6 +1979,9 @@ msgid "angeltypes.hide_on_shift_view.info"
msgstr "Wenn ausgewählt, können nur Admins und Mitglieder des Engeltyps auf der "
"Schicht Seite die Filteroption für diesen Engeltyp sehen."
msgid "location.location"
msgstr "Ort"
msgid "location.locations"
msgstr "Orte"
@ -2046,3 +2107,44 @@ msgstr "Schicht Ort wurde von %s nach %s verschoben"
msgid "notification.shift.updated.shift"
msgstr "Die aktualisierte Schicht:"
msgid "admin_shifts.no_locations"
msgstr "Es wurde noch kein Ort erstellt. Ohne können keine Schichten erstellt werden."
msgid "admin_shifts.no_shifttypes"
msgstr "Es wurde noch kein Schichttyp erstellt. Ohne können keine Schichten erstellt werden."
msgid "admin_shifts.no_angeltypes"
msgstr "Es wurde noch kein Engeltyp erstellt. Ohne können keine Schichten erstellt werden."
msgid "shift.sign_out.hint"
msgstr "Du kannst dich bis %s Stunden vor dem Start der Schicht austragen. "
"Wenn du nicht zu deiner Schicht kommen kannst, lass dich vom Himmel austragen."
msgid "Password reset in progress"
msgstr "Passwort zurücksetzen aktiv"
msgid "freeload.info.goodie"
msgstr "Du warst bei dieser Schicht nicht anwesend. "
"Die doppelte Länge der Schicht wurde von deinem %s abgezogen. "
"Bitte wende dich bei Fragen an den Himmel."
msgid "freeload.info"
msgstr "Du warst bei dieser Schicht nicht anwesend. "
"Bitte wende dich bei Fragen an den Himmel."
msgid "freeload.freeloader.info"
msgstr ""
"Du warst bei mindestens %s Schichten nicht anwesend. Deshalb ist die Schicht-Registrierung "
"gesperrt. Bitte gehe zum Himmel um wieder entsperrt zu werden."
msgid "freeload.freeloaded.info.goodie"
msgstr ""
"War ein Engel bei einer Schicht nicht anwesend, "
"wird die doppelte Länge der Schicht von dem %s abgezogen. "
"Bei %s geschwänzten Schichten wird die Schicht-Registration für den Engel gesperrt."
msgid "freeload.freeloaded.info"
msgstr ""
"Der Engel war bei einer Schicht nicht anwesend. "
"Bei %s geschwänzten Schichten wird die Schicht-Registration für den Engel gesperrt."

View File

@ -83,9 +83,6 @@ msgstr "The schedule could not be requested."
msgid "schedule.import.read-error"
msgstr "Unable to parse schedule."
msgid "schedule.import.invalid-shift-type"
msgstr "The shift type can't not be found."
msgid "schedule.import.success"
msgstr "Schedule import successful."
@ -120,7 +117,7 @@ msgid "news.comment-delete.success"
msgstr "Comment successfully deleted."
msgid "news.edit.duplicate"
msgstr "Diese News wurde bereits erstellt."
msgstr "This news has already been created."
msgid "news.edit.success"
msgstr "News successfully updated."

View File

@ -23,7 +23,7 @@ msgid "form.submit"
msgstr "Submit"
msgid "form.send_notification"
msgstr "Send notifications"
msgstr "Send %d notifications"
msgid "general.login"
msgstr "Login"
@ -34,6 +34,9 @@ msgstr "DECT"
msgid "general.name"
msgstr "Name"
msgid "general.shifts"
msgstr "Shifts"
msgid "general.description"
msgstr "Description"
@ -46,6 +49,9 @@ msgstr "You are not allowed to access this page"
msgid "page.403.login"
msgstr "Please log in."
msgid "page.error.title"
msgstr "Error %s"
msgid "page.404.title"
msgstr "Page not found"
@ -163,6 +169,9 @@ msgstr "Add minutes before talk begins"
msgid "schedule.minutes-after"
msgstr "Add minutes after talk ends"
msgid "schedule.for_locations"
msgstr "For locations"
msgid "schedule.import.request_error"
msgstr "Unable to load schedule."
@ -248,7 +257,7 @@ msgid "news.comments.delete.title"
msgstr "Delete comment \"%s\""
msgid "notification.news.updated.introduction"
msgstr "The news %1$s was updated"
msgstr "The news \"%1$s\" was updated"
msgid "notification.news.updated.text"
msgstr "You can view it at %3$s"
@ -328,7 +337,11 @@ msgstr "Notify me on new private messages."
msgid "settings.profile.email_by_human_allowed"
msgstr "Allow heaven angels to contact me by e-mail."
msgid "settings.profile.email_goody"
msgid "settings.profile.email_goodie"
msgstr "To possibly receive vouchers for the next similar event, I consent "
"that my nick, e-mail address and worked hours will be stored until then."
msgid "settings.profile.email_tshirt"
msgstr "To possibly receive vouchers for the next similar event, I consent "
"that my nick, e-mail address, worked hours and T-shirt size will be stored until then."
@ -338,6 +351,16 @@ msgstr "To withdraw your approval, send an e-mail to <a href=\"mailto:%s\">%1$s<
msgid "settings.profile.shirt_size"
msgstr "T-shirt size"
msgid "settings.profile.shirt_size.hint"
msgstr "A straight-cut shirt has wide shoulders and a body which is almost square. "
"A fitted-cut t-shirt has a curved side seam which comes in at the waist "
"and goes out at the upper and lower ends. "
"Normally fitted-cut shirts are smaller then same size straight-cut shirts, "
"and sizes differ between brands."
msgid "settings.profile.shirt.link"
msgstr "More information"
msgid "settings.profile.angeltypes.info"
msgstr "You can manage your Angeltypes <a href=\"%s\">on the Angeltypes page</a>."
@ -421,11 +444,31 @@ msgstr "Forklift"
msgid "settings.certificates.ifsg_light"
msgstr "I was instructed about IfSG §43 (aka Frikadellendiplom light) on site."
msgid "settings.certificates.ifsg_light_admin"
msgstr "Was instructed about IfSG §43 (aka Frikadellendiplom light) on site."
msgid "settings.certificates.ifsg"
msgstr "I have gotten the instruction about §43 IfSG (aka Frikadellendiplom) from my Health Department "
"and a second instruction from us or my employer/chef/assosiation within 3 months. "
"and a second instruction from us or my employer/chef/association within 3 months. "
"Additionally my second instruction is not older than 2 years."
msgid "settings.certificates.ifsg_admin"
msgstr "Got the instruction about §43 IfSG (aka Frikadellendiplom) from a Health Department "
"and a second instruction from us or his employer/chef/association within 3 months. "
"Additionally the second instruction is not older than 2 years."
msgid "settings.certificates.confirmed"
msgstr "Certificate confirmed"
msgid "settings.certificates.ifsg_confirmed.hint"
msgstr "Your health instruction has been confirmed, you can no longer change it by yourself."
msgid "settings.certificates.drive_confirmed.hint"
msgstr "Your driving license has been confirmed, you can no longer change it by yourself."
msgid "settings.certificates.confirmation.info"
msgstr "You personally checked that the certificate / license meets the requirements."
msgid "settings.certificates.success"
msgstr "Certificates were updated successfully."
@ -433,13 +476,16 @@ msgid "angeltype.ifsg.required"
msgstr "Requires health instruction"
msgid "ifsg.certificate"
msgstr "health instruction"
msgstr "Health instruction"
msgid "ifsg.certificate_light"
msgstr "health instruction on site"
msgstr "Health instruction on site"
msgid "angeltype.ifsg.own"
msgstr "my health instruction"
msgstr "My health instruction"
msgid "angeltype.ifsg.required.info.preview"
msgstr "This angeltype requires a health instruction."
msgid "angeltype.ifsg.required.info"
msgstr "This angeltype requires a health instruction. Please enter your health instruction information!"
@ -453,6 +499,9 @@ msgstr ""
"You joined an angeltype which requires a driving license. "
"Please edit your driving license information here: %s."
msgid "angeltype.driving_license.required.info.preview"
msgstr "This angeltype requires a driver's license."
msgid "ifsg.info"
msgstr "Health instruction information"
@ -473,11 +522,14 @@ msgstr "API"
msgid "settings.api.about"
msgstr ""
"The API allows you to interact with the Engelsystem by using external programs. "
"The API allows you to interact with the %s by using external programs. "
"It's not complete but we are working on extending it.\n"
"The API endpoint is located at `%s` and described in the [OpenAPI specification](%s).\n"
"The endpoint of the API is located at `%s` and described in the [OpenAPI specification](%s)."
msgid "settings.api.about.warning"
msgstr ""
"Don't share your personal API key with anyone as it can be used to view your personal data "
"and do changes your behalf!"
"and do changes on your behalf!"
msgid "settings.api.shifts_json_show"
msgstr "Show JSON shifts export"
@ -539,6 +591,9 @@ msgstr "Delete FAQ \"%s\""
msgid "question.questions"
msgstr "Questions"
msgid "question.admin"
msgstr "Answer questions"
msgid "question.faq_link"
msgstr "For general questions, have a look at our <a href=\"%s\">FAQ</a>."
@ -557,15 +612,15 @@ msgstr "Answer"
msgid "question.delete.title"
msgstr "Delete question \"%s\""
msgid "question.contact_options"
msgstr "Other contact options: "
msgid "user.edit.shirt"
msgstr "Edit T-shirt"
msgid "user.edit.goodie"
msgstr "Edit goodie"
msgid "form.shirt"
msgstr "T-shirt"
msgid "user.shirt_size"
msgstr "T-shirt size"
@ -646,11 +701,8 @@ msgstr "This angeltype requires the attendance at an introduction meeting. "
msgid "angeltypes.can-change-later"
msgstr "You can change your selection later in the settings."
msgid "angeltypes.email"
msgstr "E-mail"
msgid "angeltypes.shift.self_signup.info"
msgstr "Angel types which have shift self signup enabled allow angels to self sign up for there shifts, "
msgstr "Angel types which have shift self signup enabled allow angels to self sign up for their shifts, "
"if shift self signup is disabled only supporters and admins can sign angels into shifts of these angel types."
msgid "shift.self_signup"
@ -669,6 +721,9 @@ msgstr "If checked only admins and members of the angeltype "
msgid "registration.register"
msgstr "Register"
msgid "location.location"
msgstr "Location"
msgid "location.locations"
msgstr "Locations"
@ -765,7 +820,7 @@ msgid "general.created_at"
msgstr "Created at"
msgid "shifts.random"
msgstr "Zufällige Schicht"
msgstr "Random shift"
msgid "shifts.history"
msgstr "Shifts history"
@ -834,6 +889,18 @@ msgstr "Register"
msgid "shift.next"
msgstr "Next shift"
msgid "general.shift"
msgstr "Shift"
msgid "shift.angeltype_source"
msgstr "Needed angels from: %s"
msgid "shift.angeltype_source.shift_type"
msgstr "Schedule %s via shift type %s"
msgid "shift.angeltype_source.location"
msgstr "Schedule %s via location %s"
msgid "general.logout"
msgstr "Logout"
@ -934,3 +1001,38 @@ msgstr "Actions"
msgid "general.back"
msgstr "Back"
msgid "admin_shifts.no_locations"
msgstr "No location has been created yet. Shifts can't be created without one."
msgid "admin_shifts.no_shifttypes"
msgstr "No shift type has been created yet. Shifts can't be created without one."
msgid "admin_shifts.no_angeltypes"
msgstr "No angeltype has been created yet. Shifts can't be created without one."
msgid "shift.sign_out.hint"
msgstr "You can self sign out up to %s hours before the start of the shift. "
"If you can't attend your shift, ask heaven to sign you out."
msgid "freeload.info.goodie"
msgstr "You were not present for this shift. "
"The double length of the shift was deducted from your %s. Please contact heaven if you have questions."
msgid "freeload.info"
msgstr "You were not present for this shift. "
"Please contact heaven if you have questions."
msgid "freeload.freeloader.info"
msgstr "You were not present for at least %s shifts. Therefore, shift registration is blocked. "
"Please go to heaven to be unlocked again."
msgid "freeload.freeloaded.info.goodie"
msgstr ""
"If an angel was not present for a shift, the double length of the shift is deducted from the %s. "
"If %s shifts are freeloaded, the shift registration is blocked for the angel."
msgid "freeload.freeloaded.info"
msgstr ""
"The angel was not present for a shift. "
"If %s shifts are freeloaded, the shift registration is blocked for the angel."

View File

@ -8,15 +8,15 @@
<div class="container">
<h1>
{% if not is_index|default(false) %}
{{ m.button(m.icon('chevron-left'), location
{{ m.back(location
? url('/locations', {'action': 'view', 'location_id': location.id})
: url('/admin/locations'), 'secondary', 'sm', __('general.back')) }}
: url('/admin/locations')) }}
{% endif %}
{{ block('title') }}
{% if is_index|default(false) %}
{{ m.button(m.icon('plus-lg'), url('/admin/locations/edit'), 'secondary') }}
{{ m.button(m.icon('plus-lg'), url('/admin/locations/edit')) }}
{% endif %}
</h1>
@ -33,6 +33,7 @@
<th>{{ __('general.name') }}</th>
<th>{{ __('general.dect') }}</th>
<th>{{ __('location.map_url') }}</th>
<th>{{ __('general.shifts') }}</th>
<th></th>
</tr>
</thead>
@ -51,10 +52,12 @@
<td>{{ m.iconBool(location.map_url) }}</td>
<td>{{ m.iconBool(location.shifts.count) }}</td>
<td>
<div class="d-flex ms-auto">
{{ m.button(m.icon('pencil'), url('/admin/locations/edit/' ~ location.id), null, 'sm', __('form.edit')) }}
{{ m.edit(url('/admin/locations/edit/' ~ location.id)) }}
<form method="post" class="ps-1">
{{ csrf() }}

View File

@ -22,7 +22,7 @@
}) }}
</div>
{% if has_permission_to('logs.all') %}
{% if can('logs.all') %}
<div class="col-md-4">
{{ f.select('search_user_id', __('general.user'), users, {
'default_option': __('form.user_select'),
@ -36,7 +36,7 @@
</form>
</div>
{% if not has_permission_to('logs.all') %}
{% if not can('logs.all') %}
<div class="mb-3">
{{ m.alert(__('log.only_own')) }}
</div>

Some files were not shown because too many files have changed in this diff Show More