Compare commits

..

No commits in common. "3fb51201427841cf3dab38dcc16321755f3962e4" and "197d0d724a1afc37ad96358e52b0f5b8aa0aaebc" have entirely different histories.

269 changed files with 2538 additions and 6907 deletions

View File

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

View File

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

View File

@ -113,14 +113,7 @@ generate-version:
before_script: before_script:
- apk add -q git - apk add -q git
script: 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 "${VERSION}"
- echo -n "${VERSION}" > storage/app/VERSION - echo -n "${VERSION}" > storage/app/VERSION
@ -239,7 +232,6 @@ test:
--coverage-text --coverage-html "${HOMEDIR}/coverage/" --coverage-text --coverage-html "${HOMEDIR}/coverage/"
--log-junit "${HOMEDIR}/unittests.xml" --log-junit "${HOMEDIR}/unittests.xml"
after_script: after_script:
- sed -i 's~/var/www/~~' unittests.xml
- '"${DOCROOT}/bin/migrate" down' - '"${DOCROOT}/bin/migrate" down'
dump-database: dump-database:
@ -260,9 +252,6 @@ dump-database:
- cd "${DOCROOT}" - cd "${DOCROOT}"
- ./bin/migrate - ./bin/migrate
script: 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}" mysqldump -h "${MYSQL_HOST}" -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}"
> "${HOMEDIR}/initial-install.sql" > "${HOMEDIR}/initial-install.sql"
@ -452,8 +441,7 @@ deploy:
GIT_STRATEGY: none GIT_STRATEGY: none
when: manual when: manual
script: script:
- TARGETS=all,ingress,pvc,certificate - kubectl delete all,ingress,pvc -l app=$CI_PROJECT_PATH_SLUG -l environment=$CI_ENVIRONMENT_SLUG
- kubectl -n "${KUBE_NAMESPACE}" delete $TARGETS -l app=$CI_PROJECT_PATH_SLUG -l environment=$CI_ENVIRONMENT_SLUG
deploy-k8s-review: deploy-k8s-review:
<<: *deploy_k8s <<: *deploy_k8s

View File

@ -88,11 +88,6 @@ docker compose exec es_workspace yarn build:watch
docker compose exec -e THEMES=0,1 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 ## Localhost
You can find your local Engelsystem on [http://localhost:5080](http://localhost:5080). You can find your local Engelsystem on [http://localhost:5080](http://localhost:5080).

View File

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

View File

@ -35,14 +35,14 @@
"ext-pdo": "*", "ext-pdo": "*",
"ext-simplexml": "*", "ext-simplexml": "*",
"ext-xml": "*", "ext-xml": "*",
"doctrine/dbal": "^3.7", "doctrine/dbal": "^3.6",
"erusev/parsedown": "^1.7", "erusev/parsedown": "^1.7",
"gettext/gettext": "^5.7", "gettext/gettext": "^5.7",
"gettext/translator": "^1.2", "gettext/translator": "^1.1",
"guzzlehttp/guzzle": "^7.8", "guzzlehttp/guzzle": "^7.8",
"illuminate/container": "^10.38", "illuminate/container": "^10.23",
"illuminate/database": "^10.38", "illuminate/database": "^10.23",
"illuminate/support": "^10.38", "illuminate/support": "^10.23",
"league/oauth2-client": "^2.7", "league/oauth2-client": "^2.7",
"league/openapi-psr7-validator": "^0.21", "league/openapi-psr7-validator": "^0.21",
"nikic/fast-route": "^1.3", "nikic/fast-route": "^1.3",
@ -51,13 +51,13 @@
"psr/http-message": "^1.1", "psr/http-message": "^1.1",
"psr/http-server-middleware": "^1.0", "psr/http-server-middleware": "^1.0",
"psr/log": "^3.0", "psr/log": "^3.0",
"rcrowe/twigbridge": "^0.14.1", "rcrowe/twigbridge": "^0.14.0",
"respect/validation": "^1.1", "respect/validation": "^1.1",
"symfony/http-foundation": "^6.4", "symfony/http-foundation": "^6.3",
"symfony/mailer": "^6.4", "symfony/mailer": "^6.3",
"symfony/psr-http-message-bridge": "^2.3", "symfony/psr-http-message-bridge": "^2.3",
"twig/twig": "^3.8", "twig/twig": "^3.7",
"vlucas/phpdotenv": "^5.6" "vlucas/phpdotenv": "^5.5"
}, },
"require-dev": { "require-dev": {
"dms/phpunit-arraysubset-asserts": "^0.5", "dms/phpunit-arraysubset-asserts": "^0.5",
@ -66,9 +66,9 @@
"filp/whoops": "^2.15", "filp/whoops": "^2.15",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.6", "phpunit/phpunit": "^9.6",
"slevomat/coding-standard": "^8.14", "slevomat/coding-standard": "^8.13",
"squizlabs/php_codesniffer": "^3.8", "squizlabs/php_codesniffer": "^3.7",
"symfony/var-dumper": "^6.4" "symfony/var-dumper": "^6.3"
}, },
"autoload": { "autoload": {
"psr-4": { "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', 'oauth2.login' => \Engelsystem\Events\Listener\OAuth2::class . '@login',
'shift.deleting' => [ 'shift.entry.deleting' => [
\Engelsystem\Events\Listener\Shifts::class . '@deletingCreateWorklogs', \Engelsystem\Events\Listener\Shift::class . '@deletedEntryCreateWorklog',
\Engelsystem\Events\Listener\Shifts::class . '@deletingSendEmails', \Engelsystem\Events\Listener\Shift::class . '@deletedEntrySendEmail',
], ],
'shift.updating' => \Engelsystem\Events\Listener\Shifts::class . '@updatedSendEmail', 'shift.updating' => \Engelsystem\Events\Listener\Shift::class . '@updatedShiftSendEmail',
], ],
]; ];

View File

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

View File

@ -51,16 +51,6 @@ $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 // Password recovery
$route->addGroup( $route->addGroup(
'/password/reset', '/password/reset',
@ -197,11 +187,11 @@ $route->addGroup(
$route->addGroup( $route->addGroup(
'/schedule', '/schedule',
function (RouteCollector $route): void { function (RouteCollector $route): void {
$route->get('', 'Admin\\ScheduleController@index'); $route->get('', 'Admin\\Schedule\\ImportSchedule@index');
$route->get('/edit[/{schedule_id:\d+}]', 'Admin\\ScheduleController@edit'); $route->get('/edit[/{schedule_id:\d+}]', 'Admin\\Schedule\\ImportSchedule@edit');
$route->post('/edit[/{schedule_id:\d+}]', 'Admin\\ScheduleController@save'); $route->post('/edit[/{schedule_id:\d+}]', 'Admin\\Schedule\\ImportSchedule@save');
$route->get('/load/{schedule_id:\d+}', 'Admin\\ScheduleController@loadSchedule'); $route->get('/load/{schedule_id:\d+}', 'Admin\\Schedule\\ImportSchedule@loadSchedule');
$route->post('/import/{schedule_id:\d+}', 'Admin\\ScheduleController@importSchedule'); $route->post('/import/{schedule_id:\d+}', 'Admin\\Schedule\\ImportSchedule@importSchedule');
} }
); );
@ -252,12 +242,12 @@ $route->addGroup(
$route->addGroup( $route->addGroup(
'/user/{user_id:\d+}', '/user/{user_id:\d+}',
function (RouteCollector $route): void { function (RouteCollector $route): void {
// Goodies // Shirts
$route->addGroup( $route->addGroup(
'/goodie', '/goodie',
function (RouteCollector $route): void { function (RouteCollector $route): void {
$route->get('', 'Admin\\UserGoodieController@editGoodie'); $route->get('', 'Admin\\UserShirtController@editShirt');
$route->post('', 'Admin\\UserGoodieController@saveGoodie'); $route->post('', 'Admin\\UserShirtController@saveShirt');
} }
); );

View File

@ -21,17 +21,9 @@ class LicenseFactory extends Factory
$drive_12t = $drive_7_5t && $this->faker->boolean(.3); $drive_12t = $drive_7_5t && $this->faker->boolean(.3);
$drive_forklift = ($drive_car && $this->faker->boolean(.1)) $drive_forklift = ($drive_car && $this->faker->boolean(.1))
|| ($drive_12t && $this->faker->boolean(.7)); || ($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 = $this->faker->boolean(0.1);
$ifsg_certificate_light = $this->faker->boolean(0.5) && !$ifsg_certificate; $ifsg_certificate_light = $this->faker->boolean(0.5) && !$ifsg_certificate;
$ifsg_confirmed = $this->faker->boolean(0.5) && ($ifsg_certificate || $ifsg_certificate_light);
return [ return [
'user_id' => User::factory(), 'user_id' => User::factory(),
@ -41,10 +33,8 @@ class LicenseFactory extends Factory
'drive_3_5t' => $drive_3_5t, 'drive_3_5t' => $drive_3_5t,
'drive_7_5t' => $drive_7_5t, 'drive_7_5t' => $drive_7_5t,
'drive_12t' => $drive_12t, 'drive_12t' => $drive_12t,
'drive_confirmed' => $drive_confirmed,
'ifsg_certificate' => $ifsg_certificate, 'ifsg_certificate' => $ifsg_certificate,
'ifsg_certificate_light' => $ifsg_certificate_light, '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), 'theme' => $this->faker->numberBetween(1, 20),
'email_human' => $this->faker->boolean(), 'email_human' => $this->faker->boolean(),
'email_messages' => $this->faker->boolean(), 'email_messages' => $this->faker->boolean(),
'email_goodie' => $this->faker->boolean(), 'email_goody' => $this->faker->boolean(),
'email_shiftinfo' => $this->faker->boolean(), 'email_shiftinfo' => $this->faker->boolean(),
'email_news' => $this->faker->boolean(), 'email_news' => $this->faker->boolean(),
'mobile_show' => $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(), 'user_info' => $this->faker->optional(.1)->text(),
'active' => $this->faker->boolean(.3), 'active' => $this->faker->boolean(.3),
'force_active' => $this->faker->boolean(.1), 'force_active' => $this->faker->boolean(.1),
'got_goodie' => $this->faker->boolean(), 'got_shirt' => $this->faker->boolean(),
'got_voucher' => $this->faker->numberBetween(0, 10), 'got_voucher' => $this->faker->numberBetween(0, 10),
]; ];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,52 +0,0 @@
<?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

@ -1,77 +0,0 @@
<?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

@ -1,31 +0,0 @@
<?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

@ -1,46 +0,0 @@
<?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

@ -1,31 +0,0 @@
<?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

@ -1,46 +0,0 @@
<?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

@ -1,44 +0,0 @@
<?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

@ -1,31 +0,0 @@
<?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

@ -1,49 +0,0 @@
<?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

@ -1,205 +0,0 @@
<?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

@ -1,73 +0,0 @@
<?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 # Engelsystem development workspace
# Contains all tools required to build / manage the system # Contains all tools required to build / manage the system
FROM es_base AS es_workspace FROM es_base AS es_workspace
RUN echo 'memory_limit = 1024M' > /usr/local/etc/php/conf.d/docker-php.ini RUN echo 'memory_limit = 512M' > /usr/local/etc/php/conf.d/docker-php.ini
RUN apk add --no-cache gettext git nodejs npm yarn RUN apk add --no-cache gettext git nodejs npm yarn
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
ENTRYPOINT php -r 'sleep(PHP_INT_MAX);' ENTRYPOINT php -r 'sleep(PHP_INT_MAX);'

View File

@ -20,7 +20,10 @@ services:
APP_NAME: Engelsystem DEV APP_NAME: Engelsystem DEV
env_file: deployment.env env_file: deployment.env
ports: ports:
- "127.0.0.1:5080:80" - "5080:80"
networks:
- database
- internet
depends_on: depends_on:
- es_database - es_database
es_workspace: es_workspace:
@ -41,17 +44,11 @@ services:
ENVIRONMENT: development ENVIRONMENT: development
MAIL_DRIVER: log MAIL_DRIVER: log
APP_NAME: Engelsystem DEV APP_NAME: Engelsystem DEV
networks:
- database
- internet
depends_on: depends_on:
- es_database - 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: es_database:
image: mariadb:10.2 image: mariadb:10.2
environment: environment:
@ -62,5 +59,12 @@ services:
MYSQL_INITDB_SKIP_TZINFO: "yes" MYSQL_INITDB_SKIP_TZINFO: "yes"
volumes: volumes:
- db:/var/lib/mysql - db:/var/lib/mysql
networks:
- database
volumes: volumes:
db: {} db: {}
networks:
database:
internal: true
internet:

View File

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

View File

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

View File

@ -1,8 +1,5 @@
<?php <?php
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Http\Exceptions\HttpNotFound;
use Engelsystem\Http\Redirector;
use Engelsystem\Models\AngelType; use Engelsystem\Models\AngelType;
use Engelsystem\Models\Location; use Engelsystem\Models\Location;
use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\NeededAngelType;
@ -11,7 +8,6 @@ use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Models\Shifts\ShiftType;
use Engelsystem\Models\Shifts\ShiftSignupStatus; use Engelsystem\Models\Shifts\ShiftSignupStatus;
use Engelsystem\ShiftSignupState; use Engelsystem\ShiftSignupState;
use Illuminate\Support\Str;
/** /**
* @param array|Shift $shift * @param array|Shift $shift
@ -27,6 +23,15 @@ function shift_link($shift)
return url('/shifts', $parameters); 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 * @param Shift $shift
* @return string * @return string
@ -195,7 +200,7 @@ function shift_edit_controller()
htmlspecialchars($angeltype_name), htmlspecialchars($angeltype_name),
$needed_angel_types[$angeltype_id], $needed_angel_types[$angeltype_id],
[], [],
(bool) ScheduleShift::whereShiftId($shift->id)->first(), ScheduleShift::whereShiftId($shift->id)->first() ? true : false,
); );
} }
@ -226,24 +231,42 @@ function shift_edit_controller()
); );
} }
function shift_delete_controller(): void /**
* @return string
*/
function shift_delete_controller()
{ {
$request = request(); $request = request();
// Only accessible for admins / ShiCos with user_shifts_admin privileg
if (!auth()->can('user_shifts_admin')) { if (!auth()->can('user_shifts_admin')) {
throw new HttpForbidden(); throw_redirect(url('/user-shifts'));
} }
// Must contain shift id and confirmation // Schicht komplett löschen (nur für admins/user mit user_shifts_admin privileg)
if (!$request->has('delete_shift') || !$request->hasPostData('delete')) { if (!$request->has('delete_shift') || !preg_match('/^\d+$/', $request->input('delete_shift'))) {
throw new HttpNotFound(); throw_redirect(url('/user-shifts'));
} }
$shift_id = $request->input('delete_shift'); $shift_id = $request->input('delete_shift');
$shift = Shift::findOrFail($shift_id);
event('shift.deleting', ['shift' => $shift]); $shift = Shift($shift_id);
if (empty($shift)) {
throw_redirect(url('/user-shifts'));
}
// 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();
@ -253,15 +276,25 @@ function shift_delete_controller(): void
. ' to ' . $shift->end->format('Y-m-d H:i') . ' to ' . $shift->end->format('Y-m-d H:i')
); );
success(__('Shift deleted.')); 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(url('/user-shifts'));
} }
throw_redirect($old); $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'),
]),
]
);
} }
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
<?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,6 +5,7 @@ use Engelsystem\Database\Db;
use Engelsystem\Models\AngelType; use Engelsystem\Models\AngelType;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
use Engelsystem\Models\Worklog; use Engelsystem\Models\Worklog;
use Engelsystem\ValidationResult;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -13,17 +14,17 @@ use Illuminate\Support\Collection;
*/ */
/** /**
* Returns the goodie score (number of hours counted for tshirt). * Returns the tshirt score (number of hours counted for tshirt).
* Accounts only ended shifts. * Accounts only ended shifts.
* *
* @param int $userId * @param int $userId
* @return float * @return int
*/ */
function User_goodie_score(int $userId): float function User_tshirt_score($userId)
{ {
$shift_sum_formula = User_get_shifts_sum_query(); $shift_sum_formula = User_get_shifts_sum_query();
$result_shifts = Db::selectOne(sprintf(' $result_shifts = Db::selectOne(sprintf('
SELECT ROUND((%s) / 3600, 2) AS `goodie_score` SELECT ROUND((%s) / 3600, 2) AS `tshirt_score`
FROM `users` LEFT JOIN `shift_entries` ON `users`.`id` = `shift_entries`.`user_id` FROM `users` LEFT JOIN `shift_entries` ON `users`.`id` = `shift_entries`.`user_id`
LEFT JOIN `shifts` ON `shift_entries`.`shift_id` = `shifts`.`id` LEFT JOIN `shifts` ON `shift_entries`.`shift_id` = `shifts`.`id`
WHERE `users`.`id` = ? WHERE `users`.`id` = ?
@ -32,8 +33,8 @@ function User_goodie_score(int $userId): float
', $shift_sum_formula), [ ', $shift_sum_formula), [
$userId, $userId,
]); ]);
if (!isset($result_shifts['goodie_score'])) { if (!isset($result_shifts['tshirt_score'])) {
$result_shifts = ['goodie_score' => 0]; $result_shifts = ['tshirt_score' => 0];
} }
$worklogHours = Worklog::query() $worklogHours = Worklog::query()
@ -41,7 +42,7 @@ function User_goodie_score(int $userId): float
->where('worked_at', '<=', Carbon::Now()) ->where('worked_at', '<=', Carbon::Now())
->sum('hours'); ->sum('hours');
return $result_shifts['goodie_score'] + $worklogHours; return $result_shifts['tshirt_score'] + $worklogHours;
} }
/** /**
@ -66,6 +67,76 @@ function Users_by_angeltype_inverted(AngelType $angeltype)
->get(); ->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 * @param User $user
* @return float * @return float
@ -78,10 +149,7 @@ function User_get_eligable_voucher_count($user)
: null; : null;
$shiftEntries = ShiftEntries_finished_by_user($user, $start); $shiftEntries = ShiftEntries_finished_by_user($user, $start);
$worklog = $user->worklogs() $worklog = UserWorkLogsForUser($user->id, $start);
->whereDate('worked_at', '>=', $start ?: 0)
->with(['user', 'creator'])
->get();
$shifts_done = $shifts_done =
count($shiftEntries) count($shiftEntries)
+ $worklog->count(); + $worklog->count();
@ -123,20 +191,13 @@ function User_get_shifts_sum_query()
return 'COALESCE(SUM(UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start)), 0)'; 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( return sprintf(
' '
COALESCE(SUM( COALESCE(SUM(
(1 + ( (1 + (
/* Starts during night */ (HOUR(shifts.end) > %1$d AND HOUR(shifts.end) < %2$d)
HOUR(shifts.start) >= %1$d AND HOUR(shifts.start) < %2$d OR (HOUR(shifts.start) > %1$d AND HOUR(shifts.start) < %2$d)
/* Ends during night */ OR (HOUR(shifts.start) <= %1$d AND HOUR(shifts.end) >= %2$d)
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)) * (UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start))
* (1 - (%3$d + 1) * `shift_entries`.`freeloaded`) * (1 - (%3$d + 1) * `shift_entries`.`freeloaded`)

View File

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

View File

@ -8,7 +8,7 @@ use Engelsystem\Models\User\User;
*/ */
function admin_arrive_title() function admin_arrive_title()
{ {
return auth()->can('admin_arrive') ? __('Arrive angels') : __('Angels'); return __('Arrive angels');
} }
/** /**
@ -19,14 +19,12 @@ function admin_arrive()
$msg = ''; $msg = '';
$search = ''; $search = '';
$request = request(); $request = request();
$admin_arrive = auth()->can('admin_arrive');
if ($request->has('search')) { if ($request->has('search')) {
$search = strip_request_item('search'); $search = strip_request_item('search');
$search = trim($search); $search = trim($search);
} }
if ($admin_arrive) {
$action = $request->get('action'); $action = $request->get('action');
if ( if (
$action == 'reset' $action == 'reset'
@ -65,10 +63,9 @@ function admin_arrive()
$msg = error(__('Angel not found.'), true); $msg = error(__('Angel not found.'), true);
} }
} }
}
/** @var User[] $users */ /** @var User[] $users */
$users = User::with(['personalData', 'state'])->orderBy('name')->get(); $users = User::with('personalData')->orderBy('name')->get();
$arrival_count_at_day = []; $arrival_count_at_day = [];
$planned_arrival_count_at_day = []; $planned_arrival_count_at_day = [];
$planned_departure_count_at_day = []; $planned_departure_count_at_day = [];
@ -103,7 +100,9 @@ function admin_arrive()
$usr->name = User_Nick_render($usr) $usr->name = User_Nick_render($usr)
. User_Pronoun_render($usr) . User_Pronoun_render($usr)
. user_info_icon($usr); . ($usr->state->user_info
? ' <small><span class="bi bi-info-circle-fill text-info"></span></small>'
: '');
$plannedDepartureDate = $usr->personalData->planned_departure_date; $plannedDepartureDate = $usr->personalData->planned_departure_date;
$arrivalDate = $usr->state->arrival_date; $arrivalDate = $usr->state->arrival_date;
$plannedArrivalDate = $usr->personalData->planned_arrival_date; $plannedArrivalDate = $usr->personalData->planned_arrival_date;
@ -209,17 +208,15 @@ function admin_arrive()
form_text('search', __('form.search'), $search), form_text('search', __('form.search'), $search),
form_submit('submit', icon('search') . __('form.search')), form_submit('submit', icon('search') . __('form.search')),
], url('/admin-arrive')), ], url('/admin-arrive')),
table(array_merge( table([
['name' => __('general.name'),], 'name' => __('general.name'),
($admin_arrive ? ['rendered_planned_arrival_date' => __('Planned arrival')] : []), 'rendered_planned_arrival_date' => __('Planned arrival'),
['arrived' => __('Arrived?')], 'arrived' => __('Arrived?'),
($admin_arrive ? [
'rendered_arrival_date' => __('Arrival date'), 'rendered_arrival_date' => __('Arrival date'),
'rendered_planned_departure_date' => __('Planned departure'), 'rendered_planned_departure_date' => __('Planned departure'),
'actions' => __('general.actions'), 'actions' => __('general.actions'),
] : []) ], $users_matched),
), $users_matched), div('row', [
div('row', $admin_arrive ? [
div('col-md-4', [ div('col-md-4', [
heading(__('Planned arrival statistics'), 3), heading(__('Planned arrival statistics'), 3),
BarChart::render([ BarChart::render([
@ -265,6 +262,6 @@ function admin_arrive()
'sum' => __('Sum'), 'sum' => __('Sum'),
], $planned_departure_at_day), ], $planned_departure_at_day),
]), ]),
] : []), ]),
]); ]);
} }

View File

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

View File

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

View File

@ -41,13 +41,11 @@ function admin_shifts()
// Locations laden // Locations laden
$locations = Location::orderBy('name')->get(); $locations = Location::orderBy('name')->get();
$no_locations = $locations->isEmpty();
$location_array = $locations->pluck('name', 'id')->toArray(); $location_array = $locations->pluck('name', 'id')->toArray();
// Load angeltypes // Load angeltypes
/** @var AngelType[]|Collection $types */ /** @var AngelType[] $types */
$types = AngelType::all(); $types = AngelType::all();
$no_angeltypes = $types->isEmpty();
$needed_angel_types = []; $needed_angel_types = [];
foreach ($types as $type) { foreach ($types as $type) {
$needed_angel_types[$type->id] = 0; $needed_angel_types[$type->id] = 0;
@ -56,7 +54,6 @@ function admin_shifts()
// Load shift types // Load shift types
/** @var ShiftType[]|Collection $shifttypes_source */ /** @var ShiftType[]|Collection $shifttypes_source */
$shifttypes_source = ShiftType::all(); $shifttypes_source = ShiftType::all();
$no_shifttypes = $shifttypes_source->isEmpty();
$shifttypes = []; $shifttypes = [];
foreach ($shifttypes_source as $shifttype) { foreach ($shifttypes_source as $shifttype) {
$shifttypes[$shifttype->id] = $shifttype->name; $shifttypes[$shifttype->id] = $shifttype->name;
@ -187,23 +184,15 @@ function admin_shifts()
error(sprintf(__('Please check the needed angels for team %s.'), $type->name)); 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 { } else {
$valid = false; $valid = false;
error(__('Please select a mode for needed angels.')); 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 { } else {
$valid = false; $valid = false;
error(__('Please select needed angels.')); error(__('Please select needed angels.'));
@ -329,9 +318,6 @@ function admin_shifts()
$shifts_table = []; $shifts_table = [];
foreach ($shifts as $shift) { foreach ($shifts as $shift) {
$shiftType = $shifttypes_source->find($shift['shift_type_id']);
$location = $locations->find($shift['location_id']);
/** @var Carbon $start */ /** @var Carbon $start */
$start = $shift['start']; $start = $shift['start'];
/** @var Carbon $end */ /** @var Carbon $end */
@ -346,9 +332,9 @@ function admin_shifts()
. '</span>' . '</span>'
. ', ' . round($end->copy()->diffInMinutes($start) / 60, 2) . 'h' . ', ' . round($end->copy()->diffInMinutes($start) / 60, 2) . 'h'
. '<br>' . '<br>'
. location_name_render($location), . location_name_render(Location::find($shift['location_id'])),
'title' => 'title' =>
htmlspecialchars($shiftType->name) htmlspecialchars(ShiftType::find($shifttype_id)->name)
. ($shift['title'] ? '<br />' . htmlspecialchars($shift['title']) : ''), . ($shift['title'] ? '<br />' . htmlspecialchars($shift['title']) : ''),
'needed_angels' => '', 'needed_angels' => '',
]; ];
@ -428,6 +414,15 @@ function admin_shifts()
$shift->createdBy()->associate(auth()->user()); $shift->createdBy()->associate(auth()->user());
$shift->save(); $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 = []; $needed_angel_types_info = [];
foreach ($session->get('admin_shifts_types', []) as $type_id => $count) { foreach ($session->get('admin_shifts_types', []) as $type_id => $count) {
$angel_type_source = AngelType::find($type_id); $angel_type_source = AngelType::find($type_id);
@ -441,21 +436,10 @@ function admin_shifts()
$needed_angel_types_info[] = $angel_type_source->name . ': ' . $count; $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')); throw_redirect(url('/admin-shifts'));
} else { } else {
$session->remove('admin_shifts_shifts'); $session->remove('admin_shifts_shifts');
@ -504,9 +488,6 @@ function admin_shifts()
icon('clock-history') icon('clock-history')
) . form([$reset], '', 'display:inline'), ) . 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(), msg(),
form([ form([
div('row', [ div('row', [
@ -547,7 +528,7 @@ function admin_shifts()
form_radio('mode', __('Create multiple shifts'), $mode == 'multi', 'multi'), form_radio('mode', __('Create multiple shifts'), $mode == 'multi', 'multi'),
form_text( form_text(
'length', 'length',
__('Length (in minutes)'), __('Length'),
$request->has('length') $request->has('length')
? $request->input('length') ? $request->input('length')
: '120', : '120',

View File

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

View File

@ -2,13 +2,13 @@
declare(strict_types=1); declare(strict_types=1);
namespace Engelsystem\Controllers\Admin; namespace Engelsystem\Controllers\Admin\Schedule;
use Engelsystem\Controllers\NotificationType; use Engelsystem\Controllers\NotificationType;
use Engelsystem\Helpers\Carbon; use Engelsystem\Helpers\Carbon;
use DateTimeInterface;
use Engelsystem\Controllers\BaseController; use Engelsystem\Controllers\BaseController;
use Engelsystem\Controllers\HasUserNotifications; use Engelsystem\Controllers\HasUserNotifications;
use Engelsystem\Helpers\Schedule\ConferenceTrack;
use Engelsystem\Helpers\Schedule\Event; use Engelsystem\Helpers\Schedule\Event;
use Engelsystem\Helpers\Schedule\Room; use Engelsystem\Helpers\Schedule\Room;
use Engelsystem\Helpers\Schedule\Schedule; use Engelsystem\Helpers\Schedule\Schedule;
@ -17,45 +17,73 @@ use Engelsystem\Helpers\Uuid;
use Engelsystem\Http\Request; use Engelsystem\Http\Request;
use Engelsystem\Http\Response; use Engelsystem\Http\Response;
use Engelsystem\Models\Location; use Engelsystem\Models\Location;
use Engelsystem\Models\Shifts\Schedule as ScheduleModel; use Engelsystem\Models\Shifts\Schedule as ScheduleUrl;
use Engelsystem\Models\Shifts\ScheduleShift; use Engelsystem\Models\Shifts\ScheduleShift;
use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Models\Shifts\ShiftType;
use Engelsystem\Models\User\User;
use ErrorException; use ErrorException;
use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Database\Connection as DatabaseConnection; use Illuminate\Database\Connection as DatabaseConnection;
use Illuminate\Database\Eloquent\Builder as QueryBuilder;
use Illuminate\Database\Eloquent\Collection as DatabaseCollection; use Illuminate\Database\Eloquent\Collection as DatabaseCollection;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class ScheduleController extends BaseController class ImportSchedule extends BaseController
{ {
use HasUserNotifications; use HasUserNotifications;
/** @var DatabaseConnection */
protected $db;
/** @var LoggerInterface */
protected $log;
protected array $permissions = [ protected array $permissions = [
'schedule.import', 'schedule.import',
]; ];
protected string $url = '/admin/schedule'; /** @var XmlParser */
protected $parser;
/** @var Response */
protected $response;
/** @var SessionInterface */
protected $session;
/** @var string */
protected $url = '/admin/schedule';
/** @var GuzzleClient */
protected $guzzle;
public function __construct( public function __construct(
protected Response $response, Response $response,
protected GuzzleClient $guzzle, SessionInterface $session,
protected XmlParser $parser, GuzzleClient $guzzle,
protected DatabaseConnection $db, XmlParser $parser,
protected LoggerInterface $log DatabaseConnection $db,
LoggerInterface $log
) { ) {
$this->guzzle = $guzzle;
$this->parser = $parser;
$this->response = $response;
$this->session = $session;
$this->db = $db;
$this->log = $log;
} }
public function index(): Response public function index(): Response
{ {
return $this->response->withView( return $this->response->withView(
'admin/schedule/index', 'admin/schedule/index.twig',
[ [
'is_index' => true, 'is_index' => true,
'schedules' => ScheduleModel::all(), 'schedules' => ScheduleUrl::all(),
] ]
); );
} }
@ -63,14 +91,14 @@ class ScheduleController extends BaseController
public function edit(Request $request): Response public function edit(Request $request): Response
{ {
$scheduleId = $request->getAttribute('schedule_id'); // optional $scheduleId = $request->getAttribute('schedule_id'); // optional
$schedule = ScheduleModel::findOrNew($scheduleId);
$schedule = ScheduleUrl::find($scheduleId);
return $this->response->withView( return $this->response->withView(
'admin/schedule/edit', 'admin/schedule/edit.twig',
[ [
'schedule' => $schedule, 'schedule' => $schedule,
'shift_types' => ShiftType::all()->sortBy('name')->pluck('name', 'id'), 'shift_types' => ShiftType::all()->pluck('name', 'id'),
'locations' => Location::all()->sortBy('name')->pluck('name', 'id'),
] ]
); );
} }
@ -79,19 +107,13 @@ class ScheduleController extends BaseController
{ {
$scheduleId = $request->getAttribute('schedule_id'); // optional $scheduleId = $request->getAttribute('schedule_id'); // optional
/** @var ScheduleModel $schedule */ /** @var ScheduleUrl $schedule */
$schedule = ScheduleModel::findOrNew($scheduleId); $schedule = ScheduleUrl::findOrNew($scheduleId);
if ($request->request->has('delete')) { if ($request->request->has('delete')) {
return $this->delete($schedule); return $this->delete($schedule);
} }
$locationsList = Location::all()->pluck('id');
$locationsValidation = [];
foreach ($locationsList as $id) {
$locationsValidation['location_' . $id] = 'optional|checked';
}
$data = $this->validate($request, [ $data = $this->validate($request, [
'name' => 'required', 'name' => 'required',
'url' => 'required', 'url' => 'required',
@ -99,8 +121,11 @@ class ScheduleController extends BaseController
'needed_from_shift_type' => 'optional|checked', 'needed_from_shift_type' => 'optional|checked',
'minutes_before' => 'int', 'minutes_before' => 'int',
'minutes_after' => 'int', 'minutes_after' => 'int',
] + $locationsValidation); ]);
ShiftType::findOrFail($data['shift_type']);
if (!ShiftType::find($data['shift_type'])) {
throw new ErrorException('schedule.import.invalid-shift-type');
}
$schedule->name = $data['name']; $schedule->name = $data['name'];
$schedule->url = $data['url']; $schedule->url = $data['url'];
@ -110,31 +135,16 @@ class ScheduleController extends BaseController
$schedule->minutes_after = $data['minutes_after']; $schedule->minutes_after = $data['minutes_after'];
$schedule->save(); $schedule->save();
$schedule->activeLocations()->detach();
$for = new Collection();
foreach ($locationsList as $id) {
if (!$data['location_' . $id]) {
continue;
}
$location = Location::find($id);
$schedule->activeLocations()->attach($location);
$for[] = $location->name;
}
$this->log->info( $this->log->info(
'Schedule {name}: Url {url}, Shift Type {shift_type_name} ({shift_type_id}), ({need}), ' 'Schedule {name}: Url {url}, Shift Type {shift_type}, ({need}), minutes before/after {before}/{after}',
. 'minutes before/after {before}/{after}, for: {locations}',
[ [
'name' => $schedule->name, 'name' => $schedule->name,
'url' => $schedule->name, 'url' => $schedule->name,
'shift_type_name' => Shifttype::find($schedule->shift_type)->name, 'shift_type' => $schedule->shift_type,
'shift_type_id' => $schedule->shift_type,
'need' => $schedule->needed_from_shift_type ? 'from shift type' : 'from room', 'need' => $schedule->needed_from_shift_type ? 'from shift type' : 'from room',
'before' => $schedule->minutes_before, 'before' => $schedule->minutes_before,
'after' => $schedule->minutes_after, 'after' => $schedule->minutes_after,
'locations' => $for->implode(', '),
] ]
); );
@ -143,7 +153,7 @@ class ScheduleController extends BaseController
return redirect('/admin/schedule/load/' . $schedule->id); return redirect('/admin/schedule/load/' . $schedule->id);
} }
protected function delete(ScheduleModel $schedule): Response protected function delete(ScheduleUrl $schedule): Response
{ {
foreach ($schedule->scheduleShifts as $scheduleShift) { foreach ($schedule->scheduleShifts as $scheduleShift) {
// Only guid is needed here // Only guid is needed here
@ -159,14 +169,13 @@ class ScheduleController extends BaseController
'', '',
'', '',
'', '',
new ConferenceTrack('') ''
); );
$this->deleteEvent($event, $schedule); $this->deleteEvent($event, $schedule);
} }
$schedule->delete(); $schedule->delete();
$this->log->info('Schedule {name} deleted', ['name' => $schedule->name]);
$this->addNotification('schedule.delete.success'); $this->addNotification('schedule.delete.success');
return redirect('/admin/schedule'); return redirect('/admin/schedule');
} }
@ -180,7 +189,7 @@ class ScheduleController extends BaseController
* @var Event[] $deleteEvents * @var Event[] $deleteEvents
* @var Room[] $newRooms * @var Room[] $newRooms
* @var int $shiftType * @var int $shiftType
* @var ScheduleModel $scheduleModel * @var ScheduleUrl $scheduleUrl
* @var Schedule $schedule * @var Schedule $schedule
* @var int $minutesBefore * @var int $minutesBefore
* @var int $minutesAfter * @var int $minutesAfter
@ -191,7 +200,7 @@ class ScheduleController extends BaseController
$deleteEvents, $deleteEvents,
$newRooms, $newRooms,
, ,
$scheduleModel, $scheduleUrl,
$schedule $schedule
) = $this->getScheduleData($request); ) = $this->getScheduleData($request);
} catch (ErrorException $e) { } catch (ErrorException $e) {
@ -200,9 +209,9 @@ class ScheduleController extends BaseController
} }
return $this->response->withView( return $this->response->withView(
'admin/schedule/load', 'admin/schedule/load.twig',
[ [
'schedule_id' => $scheduleModel->id, 'schedule_id' => $scheduleUrl->id,
'schedule' => $schedule, 'schedule' => $schedule,
'locations' => [ 'locations' => [
'add' => $newRooms, 'add' => $newRooms,
@ -225,7 +234,7 @@ class ScheduleController extends BaseController
* @var Event[] $deleteEvents * @var Event[] $deleteEvents
* @var Room[] $newRooms * @var Room[] $newRooms
* @var int $shiftType * @var int $shiftType
* @var ScheduleModel $schedule * @var ScheduleUrl $scheduleUrl
*/ */
list( list(
$newEvents, $newEvents,
@ -233,14 +242,14 @@ class ScheduleController extends BaseController
$deleteEvents, $deleteEvents,
$newRooms, $newRooms,
$shiftType, $shiftType,
$schedule $scheduleUrl
) = $this->getScheduleData($request); ) = $this->getScheduleData($request);
} catch (ErrorException $e) { } catch (ErrorException $e) {
$this->addNotification($e->getMessage(), NotificationType::ERROR); $this->addNotification($e->getMessage(), NotificationType::ERROR);
return back(); return back();
} }
$this->log->info('Started schedule "{name}" import', ['name' => $schedule->name]); $this->log('Started schedule "{name}" import', ['name' => $scheduleUrl->name]);
foreach ($newRooms as $room) { foreach ($newRooms as $room) {
$this->createLocation($room); $this->createLocation($room);
@ -254,7 +263,7 @@ class ScheduleController extends BaseController
$locations $locations
->where('name', $event->getRoom()->getName()) ->where('name', $event->getRoom()->getName())
->first(), ->first(),
$schedule $scheduleUrl
); );
} }
@ -265,16 +274,16 @@ class ScheduleController extends BaseController
$locations $locations
->where('name', $event->getRoom()->getName()) ->where('name', $event->getRoom()->getName())
->first(), ->first(),
$schedule $scheduleUrl
); );
} }
foreach ($deleteEvents as $event) { foreach ($deleteEvents as $event) {
$this->deleteEvent($event, $schedule); $this->deleteEvent($event, $scheduleUrl);
} }
$schedule->touch(); $scheduleUrl->touch();
$this->log->info('Ended schedule "{name}" import', ['name' => $schedule->name]); $this->log('Ended schedule "{name}" import', ['name' => $scheduleUrl->name]);
$this->addNotification('schedule.import.success'); $this->addNotification('schedule.import.success');
return redirect($this->url, 303); return redirect($this->url, 303);
@ -286,22 +295,41 @@ class ScheduleController extends BaseController
$location->name = $room->getName(); $location->name = $room->getName();
$location->save(); $location->save();
$this->log->info('Created schedule location "{location}"', ['location' => $room->getName()]); $this->log('Created schedule location "{location}"', ['location' => $room->getName()]);
} }
protected function fireDeleteShiftEvents(Event $event, ScheduleModel $schedule): void protected function fireDeleteShiftEntryEvents(Event $event, ScheduleUrl $schedule): void
{ {
/** @var DatabaseCollection|ScheduleShift[] $scheduleShifts */ $shiftEntries = $this->db
$scheduleShifts = ScheduleShift::where('guid', $event->getGuid()) ->table('shift_entries')
->where('schedule_id', $schedule->id) ->select([
'shift_types.name', 'shifts.title', 'angel_types.name AS type', 'locations.id AS location_id',
'shifts.start', 'shifts.end', 'shift_entries.user_id', 'shift_entries.freeloaded',
])
->join('shifts', 'shifts.id', 'shift_entries.shift_id')
->join('schedule_shift', 'shifts.id', 'schedule_shift.shift_id')
->join('locations', 'locations.id', 'shifts.location_id')
->join('angel_types', 'angel_types.id', 'shift_entries.angel_type_id')
->join('shift_types', 'shift_types.id', 'shifts.shift_type_id')
->where('schedule_shift.guid', $event->getGuid())
->where('schedule_shift.schedule_id', $schedule->id)
->get(); ->get();
foreach ($scheduleShifts as $scheduleShift) { foreach ($shiftEntries as $shiftEntry) {
event('shift.deleting', ['shift' => $scheduleShift->shift]); event('shift.entry.deleting', [
'user' => User::find($shiftEntry->user_id),
'start' => Carbon::make($shiftEntry->start),
'end' => Carbon::make($shiftEntry->end),
'name' => $shiftEntry->name,
'title' => $shiftEntry->title,
'type' => $shiftEntry->type,
'location' => Location::find($shiftEntry->location_id),
'freeloaded' => $shiftEntry->freeloaded,
]);
} }
} }
protected function createEvent(Event $event, int $shiftTypeId, Location $location, ScheduleModel $schedule): void protected function createEvent(Event $event, int $shiftTypeId, Location $location, ScheduleUrl $scheduleUrl): void
{ {
$user = auth()->user(); $user = auth()->user();
$eventTimeZone = Carbon::now()->timezone; $eventTimeZone = Carbon::now()->timezone;
@ -313,31 +341,28 @@ class ScheduleController extends BaseController
$shift->end = $event->getEndDate()->copy()->timezone($eventTimeZone); $shift->end = $event->getEndDate()->copy()->timezone($eventTimeZone);
$shift->location()->associate($location); $shift->location()->associate($location);
$shift->url = $event->getUrl() ?? ''; $shift->url = $event->getUrl() ?? '';
$shift->transaction_id = Uuid::uuidBy($schedule->id, '5c4ed01e'); $shift->transaction_id = Uuid::uuidBy($scheduleUrl->id, '5c4ed01e');
$shift->createdBy()->associate($user); $shift->createdBy()->associate($user);
$shift->save(); $shift->save();
$scheduleShift = new ScheduleShift(['guid' => $event->getGuid()]); $scheduleShift = new ScheduleShift(['guid' => $event->getGuid()]);
$scheduleShift->schedule()->associate($schedule); $scheduleShift->schedule()->associate($scheduleUrl);
$scheduleShift->shift()->associate($shift); $scheduleShift->shift()->associate($shift);
$scheduleShift->save(); $scheduleShift->save();
$this->log->info( $this->log(
'Created schedule ({schedule}) shift: {shifttype} with title ' 'Created schedule shift "{shift}" in "{location}" ({from} {to}, {guid})',
. '"{shift}" in "{location}" ({from} - {to}, {guid})',
[ [
'schedule' => $scheduleShift->schedule->name,
'shifttype' => $shift->shiftType->name,
'shift' => $shift->title, 'shift' => $shift->title,
'location' => $shift->location->name, 'location' => $shift->location->name,
'from' => $shift->start->format('Y-m-d H:i'), 'from' => $shift->start->format(DateTimeInterface::RFC3339),
'to' => $shift->end->format('Y-m-d H:i'), 'to' => $shift->end->format(DateTimeInterface::RFC3339),
'guid' => $scheduleShift->guid, 'guid' => $scheduleShift->guid,
] ]
); );
} }
protected function updateEvent(Event $event, int $shiftTypeId, Location $location, ScheduleModel $schedule): void protected function updateEvent(Event $event, int $shiftTypeId, Location $location, ScheduleUrl $schedule): void
{ {
$user = auth()->user(); $user = auth()->user();
$eventTimeZone = Carbon::now()->timezone; $eventTimeZone = Carbon::now()->timezone;
@ -357,39 +382,34 @@ class ScheduleController extends BaseController
$this->fireUpdateShiftUpdateEvent($oldShift, $shift); $this->fireUpdateShiftUpdateEvent($oldShift, $shift);
$this->log->info( $this->log(
'Updated schedule ({schedule}) shift: {shifttype} with title ' 'Updated schedule shift "{shift}" in "{location}" ({from} {to}, {guid})',
. '"{shift}" in "{location}" ({from} - {to}, {guid})',
[ [
'schedule' => $scheduleShift->schedule->name,
'shifttype' => $shift->shiftType->name,
'shift' => $shift->title, 'shift' => $shift->title,
'location' => $shift->location->name, 'location' => $shift->location->name,
'from' => $shift->start->format('Y-m-d H:i'), 'from' => $shift->start->format(DateTimeInterface::RFC3339),
'to' => $shift->end->format('Y-m-d H:i'), 'to' => $shift->end->format(DateTimeInterface::RFC3339),
'guid' => $scheduleShift->guid, 'guid' => $scheduleShift->guid,
] ]
); );
} }
protected function deleteEvent(Event $event, ScheduleModel $schedule): void protected function deleteEvent(Event $event, ScheduleUrl $schedule): void
{ {
/** @var ScheduleShift $scheduleShift */ /** @var ScheduleShift $scheduleShift */
$scheduleShift = ScheduleShift::whereGuid($event->getGuid())->where('schedule_id', $schedule->id)->first(); $scheduleShift = ScheduleShift::whereGuid($event->getGuid())->where('schedule_id', $schedule->id)->first();
$shift = $scheduleShift->shift; $shift = $scheduleShift->shift;
$this->fireDeleteShiftEvents($event, $schedule);
$shift->delete(); $shift->delete();
$scheduleShift->delete();
$this->log->info( $this->fireDeleteShiftEntryEvents($event, $schedule);
'Deleted schedule ({schedule}) shift: "{shift}" in {location} ({from} - {to}, {guid})',
$this->log(
'Deleted schedule shift "{shift}" in {location} ({from} {to}, {guid})',
[ [
'schedule' => $scheduleShift->schedule->name,
'shift' => $shift->title, 'shift' => $shift->title,
'location' => $shift->location->name, 'location' => $shift->location->name,
'from' => $shift->start->format('Y-m-d H:i'), 'from' => $shift->start->format(DateTimeInterface::RFC3339),
'to' => $shift->end->format('Y-m-d H:i'), 'to' => $shift->end->format(DateTimeInterface::RFC3339),
'guid' => $scheduleShift->guid, 'guid' => $scheduleShift->guid,
] ]
); );
@ -404,45 +424,40 @@ class ScheduleController extends BaseController
} }
/** /**
* @param Request $request
* @return Event[]|Room[]|Location[] * @return Event[]|Room[]|Location[]
* @throws ErrorException * @throws ErrorException
*/ */
protected function getScheduleData(Request $request): array protected function getScheduleData(Request $request)
{ {
$scheduleId = (int) $request->getAttribute('schedule_id'); $scheduleId = (int) $request->getAttribute('schedule_id');
/** @var ScheduleModel $scheduleModel */ /** @var ScheduleUrl $scheduleUrl */
$scheduleModel = ScheduleModel::findOrFail($scheduleId); $scheduleUrl = ScheduleUrl::findOrFail($scheduleId);
try { try {
$scheduleResponse = $this->guzzle->get($scheduleModel->url); $scheduleResponse = $this->guzzle->get($scheduleUrl->url);
} catch (ConnectException | GuzzleException $e) { } catch (ConnectException $e) {
$this->log->error('Exception during schedule request', ['exception' => $e]);
throw new ErrorException('schedule.import.request-error'); throw new ErrorException('schedule.import.request-error');
} }
if ($scheduleResponse->getStatusCode() != 200) { if ($scheduleResponse->getStatusCode() != 200) {
$this->log->warning(
'Problem during schedule request, got code {code}',
['code' => $scheduleResponse->getStatusCode()]
);
throw new ErrorException('schedule.import.request-error'); throw new ErrorException('schedule.import.request-error');
} }
$scheduleData = (string) $scheduleResponse->getBody(); $scheduleData = (string) $scheduleResponse->getBody();
if (!$this->parser->load($scheduleData)) { if (!$this->parser->load($scheduleData)) {
$this->log->warning('Problem during schedule parsing');
throw new ErrorException('schedule.import.read-error'); throw new ErrorException('schedule.import.read-error');
} }
$shiftType = $scheduleModel->shift_type; $shiftType = $scheduleUrl->shift_type;
$schedule = $this->parser->getSchedule(); $schedule = $this->parser->getSchedule();
$minutesBefore = $scheduleModel->minutes_before; $minutesBefore = $scheduleUrl->minutes_before;
$minutesAfter = $scheduleModel->minutes_after; $minutesAfter = $scheduleUrl->minutes_after;
$newRooms = $this->newRooms($schedule->getRooms()); $newRooms = $this->newRooms($schedule->getRooms());
return array_merge( return array_merge(
$this->shiftsDiff($schedule, $scheduleModel, $shiftType, $minutesBefore, $minutesAfter), $this->shiftsDiff($schedule, $scheduleUrl, $shiftType, $minutesBefore, $minutesAfter),
[$newRooms, $shiftType, $scheduleModel, $schedule, $minutesBefore, $minutesAfter] [$newRooms, $shiftType, $scheduleUrl, $schedule, $minutesBefore, $minutesAfter]
); );
} }
@ -467,11 +482,16 @@ class ScheduleController extends BaseController
} }
/** /**
* @param Schedule $schedule
* @param ScheduleUrl $scheduleUrl
* @param int $shiftType
* @param int $minutesBefore
* @param int $minutesAfter
* @return Event[] * @return Event[]
*/ */
protected function shiftsDiff( protected function shiftsDiff(
Schedule $schedule, Schedule $schedule,
ScheduleModel $scheduleModel, ScheduleUrl $scheduleUrl,
int $shiftType, int $shiftType,
int $minutesBefore, int $minutesBefore,
int $minutesAfter int $minutesAfter
@ -487,13 +507,9 @@ class ScheduleController extends BaseController
$locations = $this->getAllLocations(); $locations = $this->getAllLocations();
$eventTimeZone = Carbon::now()->timezone; $eventTimeZone = Carbon::now()->timezone;
foreach ($schedule->getDays() as $day) { foreach ($schedule->getDay() as $day) {
foreach ($day->getRooms() as $room) { foreach ($day->getRoom() as $room) {
if (!$scheduleModel->activeLocations->where('name', $room->getName())->count()) { foreach ($room->getEvent() as $event) {
continue;
}
foreach ($room->getEvents() as $event) {
$scheduleEvents[$event->getGuid()] = $event; $scheduleEvents[$event->getGuid()] = $event;
$event->getDate()->timezone($eventTimeZone)->subMinutes($minutesBefore); $event->getDate()->timezone($eventTimeZone)->subMinutes($minutesBefore);
@ -508,12 +524,12 @@ class ScheduleController extends BaseController
} }
$scheduleEventsGuidList = array_keys($scheduleEvents); $scheduleEventsGuidList = array_keys($scheduleEvents);
$existingShifts = $this->getScheduleShiftsByGuid($scheduleModel, $scheduleEventsGuidList); $existingShifts = $this->getScheduleShiftsByGuid($scheduleUrl, $scheduleEventsGuidList);
foreach ($existingShifts as $scheduleShift) { foreach ($existingShifts as $scheduleShift) {
$guid = $scheduleShift->guid; $guid = $scheduleShift->guid;
$shift = $scheduleShift->shift; /** @var Shift $shift */
$shift = Shift::with('location')->find($scheduleShift->shift_id);
$event = $scheduleEvents[$guid]; $event = $scheduleEvents[$guid];
/** @var Location $location */
$location = $locations->where('name', $event->getRoom()->getName())->first(); $location = $locations->where('name', $event->getRoom()->getName())->first();
if ( if (
@ -534,7 +550,7 @@ class ScheduleController extends BaseController
$newEvents[$scheduleEvent->getGuid()] = $scheduleEvent; $newEvents[$scheduleEvent->getGuid()] = $scheduleEvent;
} }
$scheduleShifts = $this->getScheduleShiftsWhereNotGuid($scheduleModel, $scheduleEventsGuidList); $scheduleShifts = $this->getScheduleShiftsWhereNotGuid($scheduleUrl, $scheduleEventsGuidList);
foreach ($scheduleShifts as $scheduleShift) { foreach ($scheduleShifts as $scheduleShift) {
$event = $this->eventFromScheduleShift($scheduleShift); $event = $this->eventFromScheduleShift($scheduleShift);
$deleteEvents[$event->getGuid()] = $event; $deleteEvents[$event->getGuid()] = $event;
@ -560,40 +576,50 @@ class ScheduleController extends BaseController
$duration->format('%H:%I'), $duration->format('%H:%I'),
'', '',
'', '',
new ConferenceTrack('') ''
); );
} }
/** /**
* @return Location[]|Collection * @return Location[]|Collection
*/ */
protected function getAllLocations(): Collection | array protected function getAllLocations(): Collection
{ {
return Location::all(); return Location::all();
} }
/** /**
* @param ScheduleUrl $scheduleUrl
* @param string[] $events * @param string[] $events
* * @return QueryBuilder[]|DatabaseCollection|Collection|ScheduleShift[]
* @return Collection|ScheduleShift[]
*/ */
protected function getScheduleShiftsByGuid(ScheduleModel $schedule, array $events): Collection | array protected function getScheduleShiftsByGuid(ScheduleUrl $scheduleUrl, array $events)
{ {
return ScheduleShift::with('shift.location') return ScheduleShift::query()
->whereIn('guid', $events) ->whereIn('guid', $events)
->where('schedule_id', $schedule->id) ->where('schedule_id', $scheduleUrl->id)
->get(); ->get();
} }
/** /**
* @param ScheduleUrl $scheduleUrl
* @param string[] $events * @param string[] $events
* @return Collection|ScheduleShift[] * @return QueryBuilder[]|DatabaseCollection|Collection|ScheduleShift[]
*/ */
protected function getScheduleShiftsWhereNotGuid(ScheduleModel $schedule, array $events): Collection | array protected function getScheduleShiftsWhereNotGuid(ScheduleUrl $scheduleUrl, array $events)
{ {
return ScheduleShift::with('shift.location') return ScheduleShift::query()
->whereNotIn('guid', $events) ->whereNotIn('guid', $events)
->where('schedule_id', $schedule->id) ->where('schedule_id', $scheduleUrl->id)
->get(); ->get();
} }
/**
* @param string $message
* @param array $context
*/
protected function log(string $message, array $context = []): void
{
$this->log->info($message, $context);
}
} }

View File

@ -20,18 +20,10 @@ function user_myshifts()
{ {
$user = auth()->user(); $user = auth()->user();
$request = request(); $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 ( if (
$request->has('id') $request->has('id')
&& (auth()->can('user_shifts_admin') || $is_angeltype_supporter) && auth()->can('user_shifts_admin')
&& preg_match('/^\d+$/', $request->input('id')) && preg_match('/^\d+$/', $request->input('id'))
&& User::find($request->input('id')) && User::find($request->input('id'))
) { ) {
@ -41,7 +33,21 @@ function user_myshifts()
} }
$shifts_user = User::find($shift_entry_id); $shifts_user = User::find($shift_entry_id);
if ($request->has('edit') && preg_match('/^\d+$/', $request->input('edit'))) { 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'))) {
$shift_entry_id = $request->input('edit'); $shift_entry_id = $request->input('edit');
/** @var ShiftEntry $shiftEntry */ /** @var ShiftEntry $shiftEntry */
$shiftEntry = ShiftEntry::where('id', $shift_entry_id) $shiftEntry = ShiftEntry::where('id', $shift_entry_id)
@ -55,10 +61,7 @@ function user_myshifts()
if ($request->hasPostData('submit')) { if ($request->hasPostData('submit')) {
$valid = true; $valid = true;
if ( if (auth()->can('user_shifts_admin')) {
auth()->can('user_shifts_admin')
|| $is_angeltype_supporter
) {
$freeloaded = $request->has('freeloaded'); $freeloaded = $request->has('freeloaded');
$freeloaded_comment = strip_request_item_nl('freeloaded_comment'); $freeloaded_comment = strip_request_item_nl('freeloaded_comment');
if ($freeloaded && $freeloaded_comment == '') { if ($freeloaded && $freeloaded_comment == '') {
@ -88,9 +91,6 @@ function user_myshifts()
. '. Freeloaded: ' . ($freeloaded ? 'YES Comment: ' . $freeloaded_comment : 'NO') . '. Freeloaded: ' . ($freeloaded ? 'YES Comment: ' . $freeloaded_comment : 'NO')
); );
success(__('Shift saved.')); 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])); throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id]));
} }
} }
@ -104,13 +104,13 @@ function user_myshifts()
$shiftEntry->user_comment, $shiftEntry->user_comment,
$shiftEntry->freeloaded, $shiftEntry->freeloaded,
$shiftEntry->freeloaded_comment, $shiftEntry->freeloaded_comment,
auth()->can('user_shifts_admin'), auth()->can('user_shifts_admin')
$is_angeltype_supporter
); );
} else { } else {
throw_redirect(url('/user-myshifts')); throw_redirect(url('/user-myshifts'));
} }
} }
throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id])); throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id]));
return ''; return '';
} }

View File

@ -17,7 +17,7 @@ use Illuminate\Support\Collection;
*/ */
function shifts_title() function shifts_title()
{ {
return __('general.shifts'); return __('Shifts');
} }
/** /**
@ -297,6 +297,7 @@ function view_user_shifts()
return page([ return page([
div('col-md-12', [ div('col-md-12', [
msg(),
view(__DIR__ . '/../../resources/views/pages/user-shifts.html', [ view(__DIR__ . '/../../resources/views/pages/user-shifts.html', [
'title' => shifts_title(), 'title' => shifts_title(),
'add_link' => auth()->can('admin_shifts') ? $link : '', 'add_link' => auth()->can('admin_shifts') ? $link : '',
@ -374,11 +375,18 @@ function ical_hint()
return heading(__('iCal export and API') . ' ' . button_help('user/ical'), 2) return heading(__('iCal export and API') . ' ' . button_help('user/ical'), 2)
. '<p>' . sprintf( . '<p>' . sprintf(
__('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>).'), __('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>).'),
url('/ical', ['key' => $user->api_key]), url('/ical', ['key' => $user->api_key]),
url('/shifts-json-export', ['key' => $user->api_key]), url('/shifts-json-export', ['key' => $user->api_key]),
url('/settings/api') url('/user-myshifts', ['reset' => 1])
) . '</p>'; )
. ' <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>';
} }
/** /**

View File

@ -22,7 +22,6 @@ function form_hidden($name, $value)
* @param string $label * @param string $label
* @param int $value * @param int $value
* @param array $data_attributes * @param array $data_attributes
* @param bool $isDisabled
* @return string * @return string
*/ */
function form_spinner(string $name, string $label, int $value, array $data_attributes = [], bool $isDisabled = false) function form_spinner(string $name, string $label, int $value, array $data_attributes = [], bool $isDisabled = false)
@ -142,24 +141,11 @@ function form_info($label, $text = '')
* @param string $class * @param string $class
* @param bool $wrapForm * @param bool $wrapForm
* @param string $buttonType * @param string $buttonType
* @param string $title
* @param array $dataAttributes
* @return string * @return string
*/ */
function form_submit( function form_submit($name, $label, $class = '', $wrapForm = true, $buttonType = 'primary', $title = '')
$name, {
$label, $button = '<button class="btn btn-' . $buttonType . ($class ? ' ' . $class : '') . '" type="submit" name="' . $name . '" title="' . $title . '">'
$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 . $label
. '</button>'; . '</button>';

View File

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

View File

@ -116,7 +116,7 @@ function check_date($input, $error_message = null, $null_allowed = false, $time_
} else { } else {
$time = Carbon::createFromFormat('Y-m-d', $trimmed_input); $time = Carbon::createFromFormat('Y-m-d', $trimmed_input);
} }
} catch (InvalidArgumentException) { } catch (InvalidArgumentException $e) {
$time = null; $time = null;
} }
@ -197,3 +197,20 @@ function strip_item($item)
// Only allow letters, symbols, punctuation, separators and numbers without html tags // 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)); 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,6 +1,5 @@
<?php <?php
use Engelsystem\Models\User\User;
use Illuminate\Support\Str; use Illuminate\Support\Str;
/** /**
@ -352,7 +351,7 @@ function render_table($columns, $rows, $data = true)
* @param string $id * @param string $id
* @return string * @return string
*/ */
function button($href, $label, $class = '', $id = '', $title = '', $disabled = false) function button($href, $label, $class = '', $id = '', $title = '')
{ {
if (!Str::contains(str_replace(['btn-sm', 'btn-xl'], '', $class), 'btn-')) { if (!Str::contains(str_replace(['btn-sm', 'btn-xl'], '', $class), 'btn-')) {
$class = 'btn-secondary' . ($class ? ' ' . $class : ''); $class = 'btn-secondary' . ($class ? ' ' . $class : '');
@ -361,7 +360,7 @@ function button($href, $label, $class = '', $id = '', $title = '', $disabled = f
$idAttribute = $id ? 'id="' . $id . '"' : ''; $idAttribute = $id ? 'id="' . $id . '"' : '';
return '<a ' . $idAttribute . ' href="' . $href return '<a ' . $idAttribute . ' href="' . $href
. '" class="btn ' . $class . ($disabled ? ' disabled' : '') . '" title="' . $title . '">' . $label . '</a>'; . '" class="btn ' . $class . '" title="' . $title . '">' . $label . '</a>';
} }
/** /**
@ -387,9 +386,9 @@ function button_checkbox_selection($name, $label, $value)
* *
* @return string * @return string
*/ */
function button_icon($href, $icon, $class = '', $title = '', $disabled = false) function button_icon($href, $icon, $class = '', $title = '')
{ {
return button($href, icon($icon), $class, '', $title, $disabled); return button($href, icon($icon), $class, '', $title);
} }
/** /**
@ -422,16 +421,3 @@ function table_buttons($buttons = [], $additionalClass = '')
{ {
return '<div class="btn-group ' . $additionalClass . '" role="group">' . join('', $buttons) . '</div>'; 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,39 +83,9 @@ function AngelType_delete_view(AngelType $angeltype)
*/ */
function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode) 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 $link = button($angeltype->id
? url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id]) ? url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])
: url('/angeltypes'), icon('chevron-left'), 'btn-sm', '', __('general.back')); : url('/angeltypes'), icon('chevron-left'), 'btn-sm', '', __('general.back'));
return page_with_title( return page_with_title(
$link . ' ' . ( $link . ' ' . (
$angeltype->id ? $angeltype->id ?
@ -150,8 +120,30 @@ function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode)
__('angeltypes.shift.self_signup.info') . '"></span>', __('angeltypes.shift.self_signup.info') . '"></span>',
$angeltype->shift_self_signup $angeltype->shift_self_signup
), ),
$requires_driving_license, $supporter_mode ?
$requires_ifsg, 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
),
$supporter_mode $supporter_mode
? form_info(__('Show on dashboard'), $angeltype->show_on_dashboard ? __('Yes') : __('No')) ? form_info(__('Show on dashboard'), $angeltype->show_on_dashboard ? __('Yes') : __('No'))
: form_checkbox('show_on_dashboard', __('Show on dashboard'), $angeltype->show_on_dashboard), : form_checkbox('show_on_dashboard', __('Show on dashboard'), $angeltype->show_on_dashboard),
@ -190,7 +182,7 @@ function AngelType_edit_view(AngelType $angeltype, bool $supporter_mode)
* @param UserAngelType|null $user_angeltype * @param UserAngelType|null $user_angeltype
* @param bool $admin_angeltypes * @param bool $admin_angeltypes
* @param bool $supporter * @param bool $supporter
* @param License $user_license * @param License $user_driver_license
* @param User|null $user * @param User|null $user
* @return string * @return string
*/ */
@ -199,24 +191,16 @@ function AngelType_view_buttons(
?UserAngelType $user_angeltype, ?UserAngelType $user_angeltype,
$admin_angeltypes, $admin_angeltypes,
$supporter, $supporter,
$user_license, $user_driver_license,
$user $user
) { ) {
if ( if ($angeltype->requires_driver_license) {
config('driving_license_enabled')
&& $angeltype->requires_driver_license
&& $user_angeltype
) {
$buttons[] = button( $buttons[] = button(
url('/settings/certificates'), url('/settings/certificates'),
icon('person-vcard') . __('My driving license') icon('person-vcard') . __('my driving license')
); );
} }
if ( if (config('isfg_enabled') && $angeltype->requires_ifsg_certificate) {
config('ifsg_enabled')
&& $angeltype->requires_ifsg_certificate
&& $user_angeltype
) {
$buttons[] = button( $buttons[] = button(
url('/settings/certificates'), url('/settings/certificates'),
icon('card-checklist') . __('angeltype.ifsg.own') icon('card-checklist') . __('angeltype.ifsg.own')
@ -232,7 +216,7 @@ function AngelType_view_buttons(
($admin_angeltypes ? 'Join' : ''), ($admin_angeltypes ? 'Join' : ''),
); );
} else { } else {
if (config('driving_license_enabled') && $angeltype->requires_driver_license && !$user_license->wantsToDrive()) { if ($angeltype->requires_driver_license && !$user_driver_license->wantsToDrive()) {
error(__('This angeltype requires a driver license. Please enter your driver license information!')); error(__('This angeltype requires a driver license. Please enter your driver license information!'));
} }
@ -281,13 +265,6 @@ function AngelType_view_buttons(
return buttons($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. * Renders and sorts the members of an angeltype into supporters, members and unconfirmed members.
* *
@ -305,52 +282,26 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange
foreach ($members as $member) { foreach ($members as $member) {
$member->name = User_Nick_render($member) . User_Pronoun_render($member); $member->name = User_Nick_render($member) . User_Pronoun_render($member);
if (config('enable_dect')) { if (config('enable_dect')) {
$member['dect'] = $member['dect'] = htmlspecialchars((string) $member->contact->dect);
sprintf('<a href="tel:%s">%1$s</a>', htmlspecialchars((string) $member->contact->dect));
} }
if (config('driving_license_enabled') && $angeltype->requires_driver_license) { if ($angeltype->requires_driver_license) {
$drive_confirmed = $member->license->drive_confirmed; $member['wants_to_drive'] = icon_bool($member->license->wantsToDrive());
$member['wants_to_drive'] = certificateIcon($drive_confirmed, $member->license->wantsToDrive());
$member['has_car'] = icon_bool($member->license->has_car); $member['has_car'] = icon_bool($member->license->has_car);
$member['has_license_car'] = certificateIcon($drive_confirmed, $member->license->drive_car); $member['has_license_car'] = icon_bool($member->license->drive_car);
$member['has_license_3_5t_transporter'] = certificateIcon($drive_confirmed, $member->license->drive_3_5t); $member['has_license_3_5t_transporter'] = icon_bool($member->license->drive_3_5t);
$member['has_license_7_5t_truck'] = certificateIcon($drive_confirmed, $member->license->drive_7_5t); $member['has_license_7_5t_truck'] = icon_bool($member->license->drive_7_5t);
$member['has_license_12t_truck'] = certificateIcon($drive_confirmed, $member->license->drive_12t); $member['has_license_12t_truck'] = icon_bool($member->license->drive_12t);
$member['has_license_forklift'] = certificateIcon($drive_confirmed, $member->license->drive_forklift); $member['has_license_forklift'] = icon_bool($member->license->drive_forklift);
} }
if (config('ifsg_enabled') && $angeltype->requires_ifsg_certificate) { if ($angeltype->requires_ifsg_certificate && config('ifsg_enabled')) {
$ifsg_confirmed = $member->license->ifsg_confirmed; $member['ifsg_certificate'] = icon_bool($member->license->ifsg_certificate);
$member['ifsg_certificate'] = certificateIcon($ifsg_confirmed, $member->license->ifsg_certificate);
if (config('ifsg_light_enabled')) { if (config('ifsg_light_enabled')) {
$member['ifsg_certificate_light'] = certificateIcon($ifsg_confirmed, $member->license->ifsg_certificate_light); $member['ifsg_certificate_light'] = icon_bool($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)) { if ($angeltype->restricted && empty($member->pivot->confirm_user_id)) {
$member['actions'] = table_buttons([ $member['actions'] = table_buttons([
$edit_certificates,
button( button(
url( url(
'/user-angeltypes', '/user-angeltypes',
@ -370,9 +321,8 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange
]); ]);
$members_unconfirmed[] = $member; $members_unconfirmed[] = $member;
} elseif ($member->pivot->supporter) { } elseif ($member->pivot->supporter) {
if ($admin_angeltypes || ($admin_user_angeltypes && config('supporters_can_promote'))) { if ($admin_angeltypes) {
$member['actions'] = table_buttons([ $member['actions'] = table_buttons([
$edit_certificates,
button( button(
url('/user-angeltypes', [ url('/user-angeltypes', [
'action' => 'update', 'action' => 'update',
@ -386,16 +336,13 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange
), ),
]); ]);
} else { } else {
$member['actions'] = $edit_certificates $member['actions'] = '';
? table_buttons([$edit_certificates,])
: '';
} }
$supporters[] = $member; $supporters[] = $member;
} else { } else {
if ($admin_user_angeltypes) { if ($admin_user_angeltypes) {
$member['actions'] = table_buttons([ $member['actions'] = table_buttons([
$edit_certificates, $admin_angeltypes ?
($admin_angeltypes || config('supporters_can_promote')) ?
button( button(
url('/user-angeltypes', [ url('/user-angeltypes', [
'action' => 'update', 'action' => 'update',
@ -419,10 +366,6 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange
__('Remove'), __('Remove'),
), ),
]); ]);
} elseif ($edit_certificates) {
$member['actions'] = table_buttons([
$edit_certificates,
]);
} }
$members_confirmed[] = $member; $members_confirmed[] = $member;
} }
@ -453,10 +396,7 @@ function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_a
$headers['dect'] = __('general.dect'); $headers['dect'] = __('general.dect');
} }
if ( if ($angeltype->requires_driver_license && ($supporter || $admin_angeltypes)) {
config('driving_license_enabled') && $angeltype->requires_driver_license
&& ($supporter || $admin_angeltypes || auth()->can('user.drive.edit'))
) {
$headers = array_merge($headers, [ $headers = array_merge($headers, [
'wants_to_drive' => __('Driver'), 'wants_to_drive' => __('Driver'),
'has_car' => __('Has car'), 'has_car' => __('Has car'),
@ -468,10 +408,7 @@ function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_a
]); ]);
} }
if ( if (config('ifsg_enabled') && $angeltype->requires_ifsg_certificate && ($supporter || $admin_angeltypes)) {
config('ifsg_enabled') && $angeltype->requires_ifsg_certificate
&& ($supporter || $admin_angeltypes || auth()->can('user.ifsg.edit'))
) {
if (config('ifsg_light_enabled')) { if (config('ifsg_light_enabled')) {
$headers['ifsg_certificate_light'] = __('ifsg.certificate_light'); $headers['ifsg_certificate_light'] = __('ifsg.certificate_light');
} }
@ -492,7 +429,7 @@ function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_a
* @param bool $admin_user_angeltypes * @param bool $admin_user_angeltypes
* @param bool $admin_angeltypes * @param bool $admin_angeltypes
* @param bool $supporter * @param bool $supporter
* @param License $user_license * @param License $user_driver_license
* @param User $user * @param User $user
* @param ShiftsFilterRenderer $shiftsFilterRenderer * @param ShiftsFilterRenderer $shiftsFilterRenderer
* @param ShiftCalendarRenderer $shiftCalendarRenderer * @param ShiftCalendarRenderer $shiftCalendarRenderer
@ -506,7 +443,7 @@ function AngelType_view(
$admin_user_angeltypes, $admin_user_angeltypes,
$admin_angeltypes, $admin_angeltypes,
$supporter, $supporter,
$user_license, $user_driver_license,
$user, $user,
ShiftsFilterRenderer $shiftsFilterRenderer, ShiftsFilterRenderer $shiftsFilterRenderer,
ShiftCalendarRenderer $shiftCalendarRenderer, ShiftCalendarRenderer $shiftCalendarRenderer,
@ -516,7 +453,7 @@ function AngelType_view(
return page_with_title( return page_with_title(
$link . ' ' . sprintf(__('Team %s'), htmlspecialchars($angeltype->name)), $link . ' ' . sprintf(__('Team %s'), htmlspecialchars($angeltype->name)),
[ [
AngelType_view_buttons($angeltype, $user_angeltype, $admin_angeltypes, $supporter, $user_license, $user), AngelType_view_buttons($angeltype, $user_angeltype, $admin_angeltypes, $supporter, $user_driver_license, $user),
msg(), msg(),
tabs([ tabs([
__('Info') => AngelType_view_info( __('Info') => AngelType_view_info(
@ -526,7 +463,7 @@ function AngelType_view(
$admin_angeltypes, $admin_angeltypes,
$supporter $supporter
), ),
__('general.shifts') => AngelType_view_shifts( __('Shifts') => AngelType_view_shifts(
$angeltype, $angeltype,
$shiftsFilterRenderer, $shiftsFilterRenderer,
$shiftCalendarRenderer $shiftCalendarRenderer
@ -569,13 +506,6 @@ function AngelType_view_info(
$admin_angeltypes, $admin_angeltypes,
$supporter $supporter
) { ) {
$required_info_show = !auth()->user()
->userAngelTypes()
->where('angel_types.id', $angeltype->id)
->count()
&& !$admin_angeltypes
&& !$admin_user_angeltypes
&& !$supporter;
$info = []; $info = [];
if ($angeltype->hasContactInfo()) { if ($angeltype->hasContactInfo()) {
$info[] = AngelTypes_render_contact_info($angeltype); $info[] = AngelTypes_render_contact_info($angeltype);
@ -586,12 +516,6 @@ function AngelType_view_info(
if ($angeltype->description != '') { if ($angeltype->description != '') {
$info[] = $parsedown->parse(htmlspecialchars($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( list($supporters, $members_confirmed, $members_unconfirmed) = AngelType_view_members(
$angeltype, $angeltype,

View File

@ -27,22 +27,6 @@ function location_view(Location $location, ShiftsFilterRenderer $shiftsFilterRen
$description .= $parsedown->parse(htmlspecialchars($location->description)); $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 = ''; $dect = '';
if (config('enable_dect') && $location->dect) { if (config('enable_dect') && $location->dect) {
$dect = heading(__('Contact'), 3) $dect = heading(__('Contact'), 3)
@ -56,13 +40,13 @@ function location_view(Location $location, ShiftsFilterRenderer $shiftsFilterRen
if ($location->map_url) { if ($location->map_url) {
$tabs[__('location.map_url')] = sprintf( $tabs[__('location.map_url')] = sprintf(
'<div class="map">' '<div class="map">'
. '<iframe style="width: 100%%; min-height: 75vh; border: 0 none;" src="%s"></iframe>' . '<iframe style="width: 100%%; min-height: 400px; border: 0 none;" src="%s"></iframe>'
. '</div>', . '</div>',
htmlspecialchars($location->map_url) htmlspecialchars($location->map_url)
); );
} }
$tabs[__('general.shifts')] = div('first', [ $tabs[__('Shifts')] = div('first', [
$shiftsFilterRenderer->render(url('/locations', [ $shiftsFilterRenderer->render(url('/locations', [
'action' => 'view', 'action' => 'view',
'location_id' => $location->id, 'location_id' => $location->id,
@ -93,7 +77,6 @@ function location_view(Location $location, ShiftsFilterRenderer $shiftsFilterRen
]) : '', ]) : '',
$dect, $dect,
$description, $description,
$neededAngelTypes,
tabs($tabs, $selected_tab), tabs($tabs, $selected_tab),
], ],
true true

View File

@ -215,12 +215,6 @@ class ShiftCalendarRenderer
{ {
$time = Carbon::createFromTimestamp($time); $time = Carbon::createFromTimestamp($time);
$class = $label ? 'tick bg-' . theme_type() : 'tick '; $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 ($time->isStartOfDay()) {
if (!$label) { if (!$label) {
return div($class . ' day'); return div($class . ' day');
@ -302,7 +296,7 @@ class ShiftCalendarRenderer
*/ */
private function calcBlocksPerSlot() private function calcBlocksPerSlot()
{ {
return (int) ceil( return ceil(
($this->getLastBlockEndTime() - $this->getFirstBlockStartTime()) ($this->getLastBlockEndTime() - $this->getFirstBlockStartTime())
/ ShiftCalendarRenderer::SECONDS_PER_ROW / ShiftCalendarRenderer::SECONDS_PER_ROW
); );

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
<?php <?php
use Engelsystem\Config\GoodieType;
use Engelsystem\Helpers\Carbon;
use Engelsystem\Models\AngelType; use Engelsystem\Models\AngelType;
use Engelsystem\Models\Location; use Engelsystem\Models\Location;
use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\Shift;
@ -74,35 +72,6 @@ function Shift_editor_info_render(Shift $shift)
User_Nick_render($shift->updatedBy) 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); return join('<br />', $info);
} }
@ -155,11 +124,7 @@ function Shift_view(
$shift_admin = auth()->can('admin_shifts'); $shift_admin = auth()->can('admin_shifts');
$user_shift_admin = auth()->can('user_shifts_admin'); $user_shift_admin = auth()->can('user_shifts_admin');
$admin_locations = auth()->can('admin_locations'); $admin_locations = auth()->can('admin_locations');
$admin_shifttypes = auth()->can('shifttypes.view'); $admin_shifttypes = auth()->can('shifttypes');
$nightShiftsConfig = config('night_shifts');
$goodie = GoodieType::from(config('goodie_type'));
$goodie_enabled = $goodie !== GoodieType::None;
$goodie_tshirt = $goodie === GoodieType::Tshirt;
$parsedown = new Parsedown(); $parsedown = new Parsedown();
@ -195,10 +160,7 @@ function Shift_view(
} }
if ($shift_signup_state->getState() === ShiftSignupStatus::SIGNED_UP) { if ($shift_signup_state->getState() === ShiftSignupStatus::SIGNED_UP) {
$content[] = info(__('You are signed up for this shift.') $content[] = info(__('You are signed up for this shift.'), true);
. (($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) { if (config('signup_advance_hours') && $shift->start->timestamp > time() + config('signup_advance_hours') * 3600) {
@ -212,25 +174,7 @@ function Shift_view(
if ($shift_admin || $admin_shifttypes || $admin_locations) { if ($shift_admin || $admin_shifttypes || $admin_locations) {
$buttons = [ $buttons = [
$shift_admin ? button(shift_edit_link($shift), icon('pencil'), '', '', __('form.edit')) : '', $shift_admin ? button(shift_edit_link($shift), icon('pencil'), '', '', __('form.edit')) : '',
$shift_admin ? form([ $shift_admin ? button(shift_delete_link($shift), icon('trash'), 'btn-danger', '', __('form.delete')) : '',
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 $admin_shifttypes
? button(url('/admin/shifttypes/' . $shifttype->id), htmlspecialchars($shifttype->name)) ? button(url('/admin/shifttypes/' . $shifttype->id), htmlspecialchars($shifttype->name))
: '', : '',
@ -267,22 +211,11 @@ function Shift_view(
$start = $shift->start->format(__('general.datetime')); $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')); $link = button(url('/user-shifts'), icon('chevron-left'), 'btn-sm', '', __('general.back'));
return page_with_title( return page_with_title(
$link . ' ' $link . ' '
. htmlspecialchars($shift->shiftType->name) . 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 $content
); );
} }
@ -353,21 +286,17 @@ function Shift_view_render_shift_entry(ShiftEntry $shift_entry, $user_shift_admi
$isUser = $shift_entry->user_id == auth()->user()->id; $isUser = $shift_entry->user_id == auth()->user()->id;
if ($user_shift_admin || $angeltype_supporter || $isUser) { if ($user_shift_admin || $angeltype_supporter || $isUser) {
$entry .= ' <div class="btn-group m-1">'; $entry .= ' <div class="btn-group m-1">';
if ($user_shift_admin || $isUser) {
$entry .= button_icon( $entry .= button_icon(
url('/user-myshifts', ['edit' => $shift_entry->id, 'id' => $shift_entry->user_id]), url('/user-myshifts', ['edit' => $shift_entry->id, 'id' => $shift_entry->user_id]),
'pencil', 'pencil',
'btn-sm', 'btn-sm',
__('form.edit') __('form.edit')
); );
}
$angeltype = $shift_entry->angelType; $angeltype = $shift_entry->angelType;
$disabled = Shift_signout_allowed($shift, $angeltype, $shift_entry->user_id) ? '' : ' btn-disabled'; $disabled = Shift_signout_allowed($shift, $angeltype, $shift_entry->user_id) ? '' : ' btn-disabled';
$entry .= button_icon( $entry .= button_icon(shift_entry_delete_link($shift_entry), 'trash', 'btn-sm btn-danger' . $disabled, __('form.delete'));
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>'; $entry .= '</div>';
} }
return $entry; return $entry;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +0,0 @@
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,7 +318,6 @@ ready(() => {
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="${element.className}" <button type="button" class="${element.className}"
autofocus
title="${element.title}" data-submit=""> title="${element.title}" data-submit="">
${element.dataset.confirm_button_text ?? element.innerHTML} ${element.dataset.confirm_button_text ?? element.innerHTML}
</button> </button>
@ -329,25 +328,15 @@ ready(() => {
` `
); );
const modal = document.getElementById('confirmation-modal'); let modal = document.getElementById('confirmation-modal');
modal.addEventListener('hide.bs.modal', () => { modal.addEventListener('hide.bs.modal', () => {
modalOpen = false; modalOpen = false;
}); });
modal.querySelector('[data-submit]').addEventListener('click', (event) => {
const modalSubmitButton = modal.querySelector('[data-submit]');
modalSubmitButton.addEventListener('click', () => {
element.type = oldType; element.type = oldType;
element.click(); 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; modalOpen = true;
let bootstrapModal = new bootstrap.Modal(modal); let bootstrapModal = new bootstrap.Modal(modal);
bootstrapModal.show(); bootstrapModal.show();
@ -360,7 +349,7 @@ ready(() => {
*/ */
ready(() => { ready(() => {
[ [
['welcome-title', '.registration .d-none'], ['welcome-title', '.btn-group .btn.d-none'],
['settings-title', '.user-settings .nav-item'], ['settings-title', '.user-settings .nav-item'],
['oauth-settings-title', 'table tr.d-none'], ['oauth-settings-title', 'table tr.d-none'],
].forEach(([id, selector]) => { ].forEach(([id, selector]) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,200 +0,0 @@
// 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;
}

View File

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

View File

@ -32,9 +32,6 @@ msgstr ""
msgid "password.email.message" msgid "password.email.message"
msgstr "Um dein Passwort zurückzusetzen, besuche %s" msgstr "Um dein Passwort zurückzusetzen, besuche %s"
msgid "page.error.title"
msgstr "Fehler %s"
msgid "page.403.title" msgid "page.403.title"
msgstr "Nicht erlaubt" msgstr "Nicht erlaubt"
@ -90,7 +87,7 @@ msgid "form.submit"
msgstr "Absenden" msgstr "Absenden"
msgid "form.send_notification" msgid "form.send_notification"
msgstr "%d Benachrichtigungen versenden" msgstr "Benachrichtigungen versenden"
msgid "credits.source" msgid "credits.source"
msgstr "Quellcode" msgstr "Quellcode"
@ -177,9 +174,6 @@ msgstr "Lösche Engeltyp %s"
msgid "Please check the name. Maybe it already exists." msgid "Please check the name. Maybe it already exists."
msgstr "Bitte überprüfe den Namen. Vielleicht ist er bereits vergeben." msgstr "Bitte überprüfe den Namen. Vielleicht ist er bereits vergeben."
msgid "Angel type saved."
msgstr "Engeltyp wurde gespeichert."
msgid "Create angeltype" msgid "Create angeltype"
msgstr "Engeltyp erstellen" msgstr "Engeltyp erstellen"
@ -324,8 +318,8 @@ msgstr "Benötigte Engel"
msgid "Shift deleted." msgid "Shift deleted."
msgstr "Schicht gelöscht." msgstr "Schicht gelöscht."
msgid "Do you want to delete the shift \"%s\" from %s to %s?" 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?" msgstr "Möchtest Du die Schicht %s von %s bis %s löschen?"
msgid "Shift could not be found." msgid "Shift could not be found."
msgstr "Schicht konnte nicht gefunden werden." msgstr "Schicht konnte nicht gefunden werden."
@ -368,17 +362,11 @@ msgstr "Engeltyp für Benutzer bestätigen"
msgid "You are not allowed to delete this users angeltype." msgid "You are not allowed to delete this users angeltype."
msgstr "Du darfst diesen Benutzer nicht von diesem Engeltyp entfernen." msgstr "Du darfst diesen Benutzer nicht von diesem Engeltyp entfernen."
msgid "User \"%s\" removed from \"%s\"." msgid "User %s removed from %s."
msgstr "Benutzer \"%s\" von \"%s\" entfernt." msgstr "Benutzer %s von %s entfernt."
msgid "Edit certificates" msgid "Remove angeltype"
msgstr "Zertifikate bearbeiten" msgstr "Engeltyp löschen"
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." msgid "You are not allowed to set supporter rights."
msgstr "Du darfst keine Supporterrechte bearbeiten." msgstr "Du darfst keine Supporterrechte bearbeiten."
@ -527,7 +515,7 @@ msgstr "Suche Engel:"
msgid "Show all shifts" msgid "Show all shifts"
msgstr "Alle Schichten anzeigen" msgstr "Alle Schichten anzeigen"
msgid "How many angels should be active?" msgid "How much angels should be active?"
msgstr "Wie viele Engel sollten aktiv sein?" msgstr "Wie viele Engel sollten aktiv sein?"
msgid "Size" msgid "Size"
@ -536,23 +524,20 @@ msgstr "Größe"
msgid "No." msgid "No."
msgstr "Nr." msgstr "Nr."
msgid "general.shifts" msgid "Shifts"
msgstr "Schichten" msgstr "Schichten"
msgid "Length" msgid "Length"
msgstr "Länge" msgstr "Länge"
msgid "Length (in minutes)" msgid "Active?"
msgstr "Länge (in Minuten)" msgstr "Aktiv?"
msgid "Active"
msgstr "Aktiv"
msgid "Forced" msgid "Forced"
msgstr "Erzwungen" msgstr "Erzwungen"
msgid "T-shirt" msgid "T-shirt?"
msgstr "T-Shirt" msgstr "T-Shirt?"
msgid "T-shirt statistic" msgid "T-shirt statistic"
msgstr "T-Shirt Statistik" msgstr "T-Shirt Statistik"
@ -608,18 +593,6 @@ msgstr "Engeltyp"
msgid "shift.next" msgid "shift.next"
msgstr "Nächste Schicht" 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" msgid "Last shift"
msgstr "Letzte Schicht" msgstr "Letzte Schicht"
@ -761,8 +734,22 @@ msgstr "User bearbeiten"
msgid "general.datetime" msgid "general.datetime"
msgstr "d.m.Y H:i" msgstr "d.m.Y H:i"
msgid "API Settings" msgid "Key changed."
msgstr "API Einstellungen" 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 "Please enter a freeload comment!" msgid "Please enter a freeload comment!"
msgstr "Gib bitte einen Schwänz-Kommentar ein!" msgstr "Gib bitte einen Schwänz-Kommentar ein!"
@ -832,13 +819,16 @@ msgid "iCal export and API"
msgstr "iCal Export und API" msgstr "iCal Export und API"
msgid "" msgid ""
"Export your own shifts formatted as <a href=\"%s\" target=\"_blank\">iCal</a> or " "Export your own shifts. <a href=\"%s\">iCal format</a> or <a href=\"%s"
"<a href=\"%s\" target=\"_blank\">JSON</a> (please keep the link secret, otherwise you have to reset the api key " "\">JSON format</a> available (please keep secret, otherwise <a href=\"%s"
"<a href=\"%s\">in your settings</a>)." "\">reset the api key</a>)."
msgstr "" msgstr ""
"Exportiere Deine Schichten im <a href=\"%s\" target=\"_blank\">iCal</a> oder <a href=\"%s" "Exportiere Deine Schichten. <a href=\"%s\">iCal Format</a> oder <a href=\"%s"
"\" target=\"_blank\">JSON</a> Format (Link bitte geheimhalten, sonst musst du den API-Key in " "\">JSON Format</a> verfügbar (Link bitte geheimhalten, sonst <a href=\"%s"
"<a href=\"%s\">deinen Einstellungen</a> zurücksetzen)." "\">API-Key zurücksetzen</a>)."
msgid "Show API Key"
msgstr "API Key anzeigen"
msgid "All" msgid "All"
msgstr "Alle" msgstr "Alle"
@ -903,15 +893,9 @@ msgstr "Kontakt"
msgid "Primary contact person/desk for user questions." msgid "Primary contact person/desk for user questions."
msgstr "Ansprechpartner für Fragen." msgstr "Ansprechpartner für Fragen."
msgid "My driving license" msgid "my driving license"
msgstr "Meine Führerschein-Infos" 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 "" msgid ""
"This angeltype requires a driver license. Please enter your driver license " "This angeltype requires a driver license. Please enter your driver license "
"information!" "information!"
@ -1078,8 +1062,8 @@ msgstr "Schicht Anmeldung"
msgid "Freeloaded" msgid "Freeloaded"
msgstr "Geschwänzt" msgstr "Geschwänzt"
msgid "Freeload comment (Only for shift coordination and supporters):" msgid "Freeload comment (Only for shift coordination):"
msgstr "Schwänzer Kommentar (Nur für Supporter und die Schicht-Koordination):" msgstr "Schwänzer Kommentar (Nur für die Schicht-Koordination):"
msgid "Edit shift entry" msgid "Edit shift entry"
msgstr "Schichteintrag bearbeiten" msgstr "Schichteintrag bearbeiten"
@ -1102,9 +1086,6 @@ msgstr "erstellt am %s von %s"
msgid "edited at %s by %s" msgid "edited at %s by %s"
msgstr "bearbeitet am %s von %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." msgid "This shift is in the far future and becomes available for signup at %s."
msgstr "" msgstr ""
"Diese Schicht liegt in der fernen Zukunft und du kannst dich ab %s eintragen." "Diese Schicht liegt in der fernen Zukunft und du kannst dich ab %s eintragen."
@ -1124,18 +1105,12 @@ msgstr "Möchtest Du wirklich alle Benutzer als %s bestätigen?"
msgid "Do you really want to confirm %s for %s?" msgid "Do you really want to confirm %s for %s?"
msgstr "Möchtest Du wirklich %s für %s bestätigen?" msgstr "Möchtest Du wirklich %s für %s bestätigen?"
msgid "Do you really want to remove \"%s\" from \"%s\"?" msgid "Do you really want to delete %s from %s?"
msgstr "Möchtest Du wirklich \"%s\" von \"%s\" entfernen?" 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?" msgid "Do you really want to add %s to %s?"
msgstr "Möchtest Du wirklich %s zu %s hinzufügen?" 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" msgid "Confirm user"
msgstr "Benutzer bestätigen" msgstr "Benutzer bestätigen"
@ -1170,6 +1145,9 @@ msgstr "Gutschein"
msgid "Freeloads" msgid "Freeloads"
msgstr "Schwänzereien" msgstr "Schwänzereien"
msgid "T-shirt"
msgstr "T-Shirt"
msgid "Last login" msgid "Last login"
msgstr "Letzter Login" msgstr "Letzter Login"
@ -1191,8 +1169,8 @@ msgstr "Austragen"
msgid "Sum:" msgid "Sum:"
msgstr "Summe:" msgstr "Summe:"
msgid "T-shirt score" msgid "Your T-shirt score"
msgstr "T-Shirt Score" msgstr "Dein T-Shirt Score"
msgid "Work log entry" msgid "Work log entry"
msgstr "Arbeitseinsatz" msgstr "Arbeitseinsatz"
@ -1212,9 +1190,6 @@ msgstr "Name & Kollegen"
msgid "You have done enough." msgid "You have done enough."
msgstr "Du hast genug gemacht." msgstr "Du hast genug gemacht."
msgid "%s has done enough."
msgstr "%s hat genug gemacht."
msgid "Vouchers" msgid "Vouchers"
msgstr "Gutscheine" msgstr "Gutscheine"
@ -1224,8 +1199,8 @@ msgstr "iCal Export"
msgid "JSON Export" msgid "JSON Export"
msgstr "JSON Export" msgstr "JSON Export"
msgid "Night shifts between %d and %d am are multiplied by %d for the %s score." msgid "Your night shifts between %d and %d am count twice for the %s score."
msgstr "Nachtschichten zwischen %d und %d Uhr werden für den %4$s Score mit %3$d multipliziert." msgstr "Deine Nachtschichten zwischen %d und %d Uhr zählen für den %s Score doppelt."
msgid "" msgid ""
"Go to the <a href=\"%s\">shifts table</a> to sign yourself up for some " "Go to the <a href=\"%s\">shifts table</a> to sign yourself up for some "
@ -1275,6 +1250,13 @@ msgstr ""
"Bitte gib Dein geplantes Abreisedatum an, damit wir ein Gefühl für die Abbau-" "Bitte gib Dein geplantes Abreisedatum an, damit wir ein Gefühl für die Abbau-"
"Planung bekommen." "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" msgid "tshirt.required.hint"
msgstr "Bitte gib eine T-Shirt-Größe in deinen Einstellungen an." msgstr "Bitte gib eine T-Shirt-Größe in deinen Einstellungen an."
@ -1348,8 +1330,8 @@ msgstr "Goodie"
msgid "goodie" msgid "goodie"
msgstr "Goodie" msgstr "Goodie"
msgid "Goodie score" msgid "Your goodie score"
msgstr "Goodie Score" msgstr "Dein Goodie Score"
msgid "Given goodies" msgid "Given goodies"
msgstr "Ausgegebene Goodies" msgstr "Ausgegebene Goodies"
@ -1363,6 +1345,9 @@ msgstr "Goodie entfernen"
msgid "Got goodie" msgid "Got goodie"
msgstr "Goodie bekommen" msgstr "Goodie bekommen"
msgid "Goodie?"
msgstr "Goodie?"
msgid "Angel has got a goodie." msgid "Angel has got a goodie."
msgstr "Engel hat ein Goodie bekommen." msgstr "Engel hat ein Goodie bekommen."
@ -1456,9 +1441,6 @@ msgstr "Minuten vor Talk beginn hinzufügen"
msgid "schedule.minutes-after" msgid "schedule.minutes-after"
msgstr "Minuten nach Talk ende hinzufügen" msgstr "Minuten nach Talk ende hinzufügen"
msgid "schedule.for_locations"
msgstr "Für Orte"
msgid "schedule.import.locations.add" msgid "schedule.import.locations.add"
msgstr "Neue Orte" msgstr "Neue Orte"
@ -1538,7 +1520,7 @@ msgid "news.comments.delete.title"
msgstr "Kommentar \"%s\" löschen" msgstr "Kommentar \"%s\" löschen"
msgid "notification.news.updated.introduction" msgid "notification.news.updated.introduction"
msgstr "Die News \"%1$s\" wurde aktualisiert" msgstr "Die News %1$s wurde aktualisiert"
msgid "notification.news.updated.text" msgid "notification.news.updated.text"
msgstr "Du kannst sie dir unter %3$s anschauen." msgstr "Du kannst sie dir unter %3$s anschauen."
@ -1618,11 +1600,7 @@ msgstr "Benachrichtige mich bei neuen privaten Nachrichten."
msgid "settings.profile.email_by_human_allowed" msgid "settings.profile.email_by_human_allowed"
msgstr "Erlaube Himmel-Engeln mich per E-Mail zu kontaktieren." msgstr "Erlaube Himmel-Engeln mich per E-Mail zu kontaktieren."
msgid "settings.profile.email_goodie" msgid "settings.profile.email_goody"
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, " 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." "dass mein Nick, E-Mail-Adresse, geleistete Arbeit und T-Shirt-Größe solange gespeichert werden."
@ -1632,16 +1610,6 @@ msgstr "Dies kann jederzeit durch eine E-Mail an <a href=\"mailto:%s\">%1$s</a>
msgid "settings.profile.shirt_size" msgid "settings.profile.shirt_size"
msgstr "T-Shirt-Größe" 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" msgid "settings.profile.angeltypes.info"
msgstr "Du kannst deine Engeltypen <a href=\"%s\">auf der Engeltypen-Seite</a> verwalten." msgstr "Du kannst deine Engeltypen <a href=\"%s\">auf der Engeltypen-Seite</a> verwalten."
@ -1725,31 +1693,11 @@ msgstr "Gabelstapler"
msgid "settings.certificates.ifsg_light" msgid "settings.certificates.ifsg_light"
msgstr "Ich wurde vor Ort nach IfSG §43 (Frikadellendiplom light) belehrt." 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" msgid "settings.certificates.ifsg"
msgstr "Ich habe eine Belehrung nach §43 IfSG (Frikadellendiplom) bei meinem Gesundheitsamt " 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. " "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." "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" msgid "settings.certificates.success"
msgstr "Zertifikate wurden erfolgreich aktualisiert." msgstr "Zertifikate wurden erfolgreich aktualisiert."
@ -1797,12 +1745,9 @@ msgstr "API"
msgid "settings.api.about" msgid "settings.api.about"
msgstr "" msgstr ""
"Die API erlaubt es dir, über externe Programme, mit dem %s zu interagieren. " "Die API erlaubt es dir, über externe Programme, mit dem Engelsystem zu interagieren. "
"Sie ist noch nicht vollständig, wir arbeiten aber daran sie zu erweitern.\n" "Sie ist noch nicht vollständig, wir arbeiten aber daran sie zu erweitern.\n"
"Der Einstiegspunkt der API befindet sich unter `%s` und ist in der [OpenAPI Spezifikation](%s) beschrieben." "Der API Einstiegspunkt befindet sich unter `%s` und ist in der [OpenAPI Spezifikation](%s) beschrieben.\n"
msgid "settings.api.about.warning"
msgstr ""
"Teile deinen persönlichen API Key mit niemandem, er erlaubt es deine persönlichen Daten einzusehen " "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!" "und Änderungen in deinem Namen durch zu führen!"
@ -1866,9 +1811,6 @@ msgstr "FAQ \"%s\" löschen"
msgid "question.questions" msgid "question.questions"
msgstr "Fragen" msgstr "Fragen"
msgid "question.admin"
msgstr "Fragen beantworten"
msgid "question.faq_link" msgid "question.faq_link"
msgstr "Hast du eine generelle Frage? Vielleicht ist diese schon in den <a href=\"%s\">FAQ</a> beantwortet." msgstr "Hast du eine generelle Frage? Vielleicht ist diese schon in den <a href=\"%s\">FAQ</a> beantwortet."
@ -1887,9 +1829,6 @@ msgstr "Antwort"
msgid "question.delete.title" msgid "question.delete.title"
msgstr "Frage \"%s\" löschen" msgstr "Frage \"%s\" löschen"
msgid "question.contact_options"
msgstr "Weitere Kontaktmöglichkeiten: "
msgid "user.edit.shirt" msgid "user.edit.shirt"
msgstr "T-Shirt bearbeiten" msgstr "T-Shirt bearbeiten"
@ -1972,6 +1911,9 @@ msgstr "Dieser Engeltyp benötigt eine Einweisung bei einem Einführungstreffen.
msgid "angeltypes.can-change-later" msgid "angeltypes.can-change-later"
msgstr "Du kannst Deine Auswahl später in den Einstellungen ändern." msgstr "Du kannst Deine Auswahl später in den Einstellungen ändern."
msgid "angeltypes.email"
msgstr "E-Mail"
msgid "angeltypes.hide_on_shift_view" msgid "angeltypes.hide_on_shift_view"
msgstr "Auf Schicht-Ansicht ausblenden" msgstr "Auf Schicht-Ansicht ausblenden"
@ -1979,9 +1921,6 @@ msgid "angeltypes.hide_on_shift_view.info"
msgstr "Wenn ausgewählt, können nur Admins und Mitglieder des Engeltyps auf der " msgstr "Wenn ausgewählt, können nur Admins und Mitglieder des Engeltyps auf der "
"Schicht Seite die Filteroption für diesen Engeltyp sehen." "Schicht Seite die Filteroption für diesen Engeltyp sehen."
msgid "location.location"
msgstr "Ort"
msgid "location.locations" msgid "location.locations"
msgstr "Orte" msgstr "Orte"
@ -2107,44 +2046,3 @@ msgstr "Schicht Ort wurde von %s nach %s verschoben"
msgid "notification.shift.updated.shift" msgid "notification.shift.updated.shift"
msgstr "Die aktualisierte Schicht:" 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,6 +83,9 @@ msgstr "The schedule could not be requested."
msgid "schedule.import.read-error" msgid "schedule.import.read-error"
msgstr "Unable to parse schedule." msgstr "Unable to parse schedule."
msgid "schedule.import.invalid-shift-type"
msgstr "The shift type can't not be found."
msgid "schedule.import.success" msgid "schedule.import.success"
msgstr "Schedule import successful." msgstr "Schedule import successful."
@ -117,7 +120,7 @@ msgid "news.comment-delete.success"
msgstr "Comment successfully deleted." msgstr "Comment successfully deleted."
msgid "news.edit.duplicate" msgid "news.edit.duplicate"
msgstr "This news has already been created." msgstr "Diese News wurde bereits erstellt."
msgid "news.edit.success" msgid "news.edit.success"
msgstr "News successfully updated." msgstr "News successfully updated."

View File

@ -23,7 +23,7 @@ msgid "form.submit"
msgstr "Submit" msgstr "Submit"
msgid "form.send_notification" msgid "form.send_notification"
msgstr "Send %d notifications" msgstr "Send notifications"
msgid "general.login" msgid "general.login"
msgstr "Login" msgstr "Login"
@ -34,9 +34,6 @@ msgstr "DECT"
msgid "general.name" msgid "general.name"
msgstr "Name" msgstr "Name"
msgid "general.shifts"
msgstr "Shifts"
msgid "general.description" msgid "general.description"
msgstr "Description" msgstr "Description"
@ -49,9 +46,6 @@ msgstr "You are not allowed to access this page"
msgid "page.403.login" msgid "page.403.login"
msgstr "Please log in." msgstr "Please log in."
msgid "page.error.title"
msgstr "Error %s"
msgid "page.404.title" msgid "page.404.title"
msgstr "Page not found" msgstr "Page not found"
@ -169,9 +163,6 @@ msgstr "Add minutes before talk begins"
msgid "schedule.minutes-after" msgid "schedule.minutes-after"
msgstr "Add minutes after talk ends" msgstr "Add minutes after talk ends"
msgid "schedule.for_locations"
msgstr "For locations"
msgid "schedule.import.request_error" msgid "schedule.import.request_error"
msgstr "Unable to load schedule." msgstr "Unable to load schedule."
@ -257,7 +248,7 @@ msgid "news.comments.delete.title"
msgstr "Delete comment \"%s\"" msgstr "Delete comment \"%s\""
msgid "notification.news.updated.introduction" msgid "notification.news.updated.introduction"
msgstr "The news \"%1$s\" was updated" msgstr "The news %1$s was updated"
msgid "notification.news.updated.text" msgid "notification.news.updated.text"
msgstr "You can view it at %3$s" msgstr "You can view it at %3$s"
@ -337,11 +328,7 @@ msgstr "Notify me on new private messages."
msgid "settings.profile.email_by_human_allowed" msgid "settings.profile.email_by_human_allowed"
msgstr "Allow heaven angels to contact me by e-mail." msgstr "Allow heaven angels to contact me by e-mail."
msgid "settings.profile.email_goodie" msgid "settings.profile.email_goody"
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 " 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." "that my nick, e-mail address, worked hours and T-shirt size will be stored until then."
@ -351,16 +338,6 @@ msgstr "To withdraw your approval, send an e-mail to <a href=\"mailto:%s\">%1$s<
msgid "settings.profile.shirt_size" msgid "settings.profile.shirt_size"
msgstr "T-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" msgid "settings.profile.angeltypes.info"
msgstr "You can manage your Angeltypes <a href=\"%s\">on the Angeltypes page</a>." msgstr "You can manage your Angeltypes <a href=\"%s\">on the Angeltypes page</a>."
@ -444,31 +421,11 @@ msgstr "Forklift"
msgid "settings.certificates.ifsg_light" msgid "settings.certificates.ifsg_light"
msgstr "I was instructed about IfSG §43 (aka Frikadellendiplom light) on site." 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" msgid "settings.certificates.ifsg"
msgstr "I have gotten the instruction about §43 IfSG (aka Frikadellendiplom) from my Health Department " 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/association within 3 months. " "and a second instruction from us or my employer/chef/assosiation within 3 months. "
"Additionally my second instruction is not older than 2 years." "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" msgid "settings.certificates.success"
msgstr "Certificates were updated successfully." msgstr "Certificates were updated successfully."
@ -476,16 +433,13 @@ msgid "angeltype.ifsg.required"
msgstr "Requires health instruction" msgstr "Requires health instruction"
msgid "ifsg.certificate" msgid "ifsg.certificate"
msgstr "Health instruction" msgstr "health instruction"
msgid "ifsg.certificate_light" msgid "ifsg.certificate_light"
msgstr "Health instruction on site" msgstr "health instruction on site"
msgid "angeltype.ifsg.own" 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" msgid "angeltype.ifsg.required.info"
msgstr "This angeltype requires a health instruction. Please enter your health instruction information!" msgstr "This angeltype requires a health instruction. Please enter your health instruction information!"
@ -499,9 +453,6 @@ msgstr ""
"You joined an angeltype which requires a driving license. " "You joined an angeltype which requires a driving license. "
"Please edit your driving license information here: %s." "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" msgid "ifsg.info"
msgstr "Health instruction information" msgstr "Health instruction information"
@ -522,14 +473,11 @@ msgstr "API"
msgid "settings.api.about" msgid "settings.api.about"
msgstr "" msgstr ""
"The API allows you to interact with the %s by using external programs. " "The API allows you to interact with the Engelsystem by using external programs. "
"It's not complete but we are working on extending it.\n" "It's not complete but we are working on extending it.\n"
"The endpoint of the API is located at `%s` and described in the [OpenAPI specification](%s)." "The API endpoint is located at `%s` and described in the [OpenAPI specification](%s).\n"
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 " "Don't share your personal API key with anyone as it can be used to view your personal data "
"and do changes on your behalf!" "and do changes your behalf!"
msgid "settings.api.shifts_json_show" msgid "settings.api.shifts_json_show"
msgstr "Show JSON shifts export" msgstr "Show JSON shifts export"
@ -591,9 +539,6 @@ msgstr "Delete FAQ \"%s\""
msgid "question.questions" msgid "question.questions"
msgstr "Questions" msgstr "Questions"
msgid "question.admin"
msgstr "Answer questions"
msgid "question.faq_link" msgid "question.faq_link"
msgstr "For general questions, have a look at our <a href=\"%s\">FAQ</a>." msgstr "For general questions, have a look at our <a href=\"%s\">FAQ</a>."
@ -612,15 +557,15 @@ msgstr "Answer"
msgid "question.delete.title" msgid "question.delete.title"
msgstr "Delete question \"%s\"" msgstr "Delete question \"%s\""
msgid "question.contact_options"
msgstr "Other contact options: "
msgid "user.edit.shirt" msgid "user.edit.shirt"
msgstr "Edit T-shirt" msgstr "Edit T-shirt"
msgid "user.edit.goodie" msgid "user.edit.goodie"
msgstr "Edit goodie" msgstr "Edit goodie"
msgid "form.shirt"
msgstr "T-shirt"
msgid "user.shirt_size" msgid "user.shirt_size"
msgstr "T-shirt size" msgstr "T-shirt size"
@ -701,8 +646,11 @@ msgstr "This angeltype requires the attendance at an introduction meeting. "
msgid "angeltypes.can-change-later" msgid "angeltypes.can-change-later"
msgstr "You can change your selection later in the settings." msgstr "You can change your selection later in the settings."
msgid "angeltypes.email"
msgstr "E-mail"
msgid "angeltypes.shift.self_signup.info" msgid "angeltypes.shift.self_signup.info"
msgstr "Angel types which have shift self signup enabled allow angels to self sign up for their shifts, " msgstr "Angel types which have shift self signup enabled allow angels to self sign up for there shifts, "
"if shift self signup is disabled only supporters and admins can sign angels into shifts of these angel types." "if shift self signup is disabled only supporters and admins can sign angels into shifts of these angel types."
msgid "shift.self_signup" msgid "shift.self_signup"
@ -721,9 +669,6 @@ msgstr "If checked only admins and members of the angeltype "
msgid "registration.register" msgid "registration.register"
msgstr "Register" msgstr "Register"
msgid "location.location"
msgstr "Location"
msgid "location.locations" msgid "location.locations"
msgstr "Locations" msgstr "Locations"
@ -820,7 +765,7 @@ msgid "general.created_at"
msgstr "Created at" msgstr "Created at"
msgid "shifts.random" msgid "shifts.random"
msgstr "Random shift" msgstr "Zufällige Schicht"
msgid "shifts.history" msgid "shifts.history"
msgstr "Shifts history" msgstr "Shifts history"
@ -889,18 +834,6 @@ msgstr "Register"
msgid "shift.next" msgid "shift.next"
msgstr "Next shift" 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" msgid "general.logout"
msgstr "Logout" msgstr "Logout"
@ -1001,38 +934,3 @@ msgstr "Actions"
msgid "general.back" msgid "general.back"
msgstr "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"> <div class="container">
<h1> <h1>
{% if not is_index|default(false) %} {% if not is_index|default(false) %}
{{ m.back(location {{ m.button(m.icon('chevron-left'), location
? url('/locations', {'action': 'view', 'location_id': location.id}) ? url('/locations', {'action': 'view', 'location_id': location.id})
: url('/admin/locations')) }} : url('/admin/locations'), 'secondary', 'sm', __('general.back')) }}
{% endif %} {% endif %}
{{ block('title') }} {{ block('title') }}
{% if is_index|default(false) %} {% if is_index|default(false) %}
{{ m.button(m.icon('plus-lg'), url('/admin/locations/edit')) }} {{ m.button(m.icon('plus-lg'), url('/admin/locations/edit'), 'secondary') }}
{% endif %} {% endif %}
</h1> </h1>
@ -33,7 +33,6 @@
<th>{{ __('general.name') }}</th> <th>{{ __('general.name') }}</th>
<th>{{ __('general.dect') }}</th> <th>{{ __('general.dect') }}</th>
<th>{{ __('location.map_url') }}</th> <th>{{ __('location.map_url') }}</th>
<th>{{ __('general.shifts') }}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -52,12 +51,10 @@
<td>{{ m.iconBool(location.map_url) }}</td> <td>{{ m.iconBool(location.map_url) }}</td>
<td>{{ m.iconBool(location.shifts.count) }}</td>
<td> <td>
<div class="d-flex ms-auto"> <div class="d-flex ms-auto">
{{ m.edit(url('/admin/locations/edit/' ~ location.id)) }} {{ m.button(m.icon('pencil'), url('/admin/locations/edit/' ~ location.id), null, 'sm', __('form.edit')) }}
<form method="post" class="ps-1"> <form method="post" class="ps-1">
{{ csrf() }} {{ csrf() }}

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