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
[*.js]
indent_size = 2
max_line_length = unset
quote_type = single
[{LICENSE,db/*.sql}]

View File

@ -1,10 +1,17 @@
{
"parser": "@babel/eslint-parser",
"extends": ["plugin:editorconfig/all", "prettier"],
"plugins": ["editorconfig"],
"extends": [ "plugin:editorconfig/all" ],
"plugins": [ "editorconfig" ],
"rules": {
"prefer-arrow-callback": "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:
- apk add -q git
script:
- >
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}"\
)\
)"
- VERSION="$(git describe --abbrev=0 --tags)-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}"
- echo "${VERSION}"
- echo -n "${VERSION}" > storage/app/VERSION
@ -239,7 +232,6 @@ test:
--coverage-text --coverage-html "${HOMEDIR}/coverage/"
--log-junit "${HOMEDIR}/unittests.xml"
after_script:
- sed -i 's~/var/www/~~' unittests.xml
- '"${DOCROOT}/bin/migrate" down'
dump-database:
@ -260,9 +252,6 @@ dump-database:
- cd "${DOCROOT}"
- ./bin/migrate
script:
- >-
mysql -h "${MYSQL_HOST}" -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}"
-e 'UPDATE users SET api_key="" WHERE name="admin"'
- >-
mysqldump -h "${MYSQL_HOST}" -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}"
> "${HOMEDIR}/initial-install.sql"
@ -452,8 +441,7 @@ deploy:
GIT_STRATEGY: none
when: manual
script:
- TARGETS=all,ingress,pvc,certificate
- kubectl -n "${KUBE_NAMESPACE}" delete $TARGETS -l app=$CI_PROJECT_PATH_SLUG -l environment=$CI_ENVIRONMENT_SLUG
- kubectl delete all,ingress,pvc -l app=$CI_PROJECT_PATH_SLUG -l environment=$CI_ENVIRONMENT_SLUG
deploy-k8s-review:
<<: *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
```
It might also be useful to have an interactive database interface for which a phpMyAdmin instance can be startet at [http://localhost:8888](http://localhost:8888).
```bash
docker compose --profile dev up
```
## Localhost
You can find your local Engelsystem on [http://localhost:5080](http://localhost:5080).

View File

@ -28,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
* Webserver, i.e. lighttpd, nginx, or Apache
From previous experience, 2 cores and 2GB ram are roughly enough for up to 1000 Angels (~700 arrived + 500 arrived but not working) during an event.
### Download
* Go to the [Releases](https://github.com/engelsystem/engelsystem/releases) page and download the latest stable release file.
* Extract the files to your webroot and continue with the directions for configurations and setup.
@ -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.
* There must be a MySQL database set up with a user who has full rights to that database.
* If necessary, create a `config/config.php` to override values from `config/config.default.php`.
* To disable/remove values from the following lists, set the value of the entry to `null`:
* `themes`
* `tshirt_sizes`
* `headers`
* `header_items`
* `footer_items`
* `locales`
* `contact_options`
* To disable/remove values from the `themes`, `tshirt_sizes`, `headers`, `header_items`, `footer_items`, or `locales` lists, set the value of the entry to `null`.
* To 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.
@ -79,8 +70,8 @@ cd docker
docker compose up -d
```
#### Set Up / Migrate Database
Create the Database Schema (on a fresh install) or import database changes to migrate it to the newest version
#### Migrate
Import database changes to migrate it to the newest version
```bash
cd docker
docker compose exec es_server bin/migrate

View File

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

697
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -80,11 +80,11 @@ return [
'oauth2.login' => \Engelsystem\Events\Listener\OAuth2::class . '@login',
'shift.deleting' => [
\Engelsystem\Events\Listener\Shifts::class . '@deletingCreateWorklogs',
\Engelsystem\Events\Listener\Shifts::class . '@deletingSendEmails',
'shift.entry.deleting' => [
\Engelsystem\Events\Listener\Shift::class . '@deletedEntryCreateWorklog',
\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'),
// Application URL and base path to use instead of the auto-detected one
'url' => env('APP_URL'),
'url' => env('APP_URL', null),
// Header links
// Available link placeholders: %lang%
@ -53,15 +53,8 @@ return [
'Contact' => env('CONTACT_EMAIL', 'mailto:ticket@c3heaven.de'),
],
// Other ways to ask the heaven
// Multiple contact options / links are possible, analogue to footer_items
'contact_options' => [
// E-mail address
'general.email' => env('CONTACT_EMAIL', 'mailto:ticket@c3heaven.de'),
],
// Text displayed on the FAQ page, rendered as markdown
'faq_text' => env('FAQ_TEXT'),
'faq_text' => env('FAQ_TEXT', null),
// Link to documentation/help
'documentation_url' => env('DOCUMENTATION_URL', 'https://engelsystem.de/doc/'),
@ -79,20 +72,17 @@ return [
'host' => env('MAIL_HOST', 'localhost'),
'port' => env('MAIL_PORT', 587),
// If tls transport encryption should be used
'tls' => env('MAIL_TLS'),
'tls' => env('MAIL_TLS', null),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'sendmail' => env('MAIL_SENDMAIL', '/usr/sbin/sendmail -bs'),
],
// Your privacy@ contact address
'privacy_email' => env('PRIVACY_EMAIL'),
// 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),
# Your privacy@ contact address
'privacy_email' => env('PRIVACY_EMAIL', null),
// Initial admin password
'setup_admin_password' => env('SETUP_ADMIN_PASSWORD'),
'setup_admin_password' => env('SETUP_ADMIN_PASSWORD', null),
'oauth' => [
// '[name]' => [config]
@ -154,11 +144,6 @@ return [
// Supported themes
// To disable a theme in the config.php, you can set its value to null
'themes' => [
17 => [
'name' => 'Engelsystem 37c3 (2023)',
'type' => 'dark',
'navbar_classes' => 'navbar-dark',
],
16 => [
'name' => 'Engelsystem cccamp23 (2023)',
'type' => 'dark',
@ -256,9 +241,6 @@ return [
// Users are able to sign up
'registration_enabled' => (bool) env('REGISTRATION_ENABLED', true),
// URL to external registration page, used on login page
'external_registration_url' => env('EXTERNAL_REGISTRATION_URL'),
// Required user fields
'required_user_fields' => [
'pronoun' => (bool) env('PRONOUN_REQUIRED', false),
@ -275,9 +257,6 @@ return [
// Whether newly-registered user should automatically be marked as arrived
'autoarrive' => (bool) env('ANGEL_AUTOARRIVE', false),
// Supporters of an angeltype can promote other angels of the angeltype to supporter
'supporters_can_promote' => (bool) env('SUPPORTERS_CAN_PROMOTE', false),
// Only allow shift signup this number of hours in advance
// Setting this to 0 disables the feature
'signup_advance_hours' => env('SIGNUP_ADVANCE_HOURS', 0),
@ -303,8 +282,9 @@ return [
// The minimum length for passwords
'min_password_length' => env('PASSWORD_MINIMUM_LENGTH', 8),
// Whether the login and registration via password should be enabled (login will be hidden)
// This is useful when using oauth, disabling it also disables normal registration without oauth
// Whether the Password field should be enabled on registration.
// This is useful when using oauth, disabling it also disables normal
// registration without oauth.
'enable_password' => (bool) env('ENABLE_PASSWORD', true),
// Whether the DECT field should be enabled
@ -330,9 +310,6 @@ return [
// Enables the planned arrival/leave date
'enable_planned_arrival' => (bool) env('ENABLE_PLANNED_ARRIVAL', true),
// Whether force active should be enabled
'enable_force_active' => (bool) env('ENABLE_FORCE_ACTIVE', true),
// Resembles the Goodie Type. There are three options:
// 'none' => no goodie at all
// 'goodie' => a goodie which has no sizing options
@ -351,12 +328,11 @@ return [
// Local timezone
'timezone' => env('TIMEZONE', 'Europe/Berlin'),
// Multiply 'night shifts' and freeloaded shifts (start or end between 2 and 8 exclusive) by 2 in goodie score
// Goodies must be enabled to use this feature
// Multiply 'night shifts' and freeloaded shifts (start or end between 2 and 6 exclusive) by 2
'night_shifts' => [
'enabled' => (bool) env('NIGHT_SHIFTS', true), // Disable to weigh every shift the same
'start' => env('NIGHT_SHIFTS_START', 2), // Starting from hour
'end' => env('NIGHT_SHIFTS_END', 8), // Ends at (without including) hour
'start' => env('NIGHT_SHIFTS_START', 2),
'end' => env('NIGHT_SHIFTS_END', 6),
'multiplier' => env('NIGHT_SHIFTS_MULTIPLIER', 2),
],
@ -366,17 +342,15 @@ return [
'shifts_per_voucher' => env('SHIFTS_PER_VOUCHER', 0),
'hours_per_voucher' => env('HOURS_PER_VOUCHER', 2),
// 'Y-m-d' formatted
'voucher_start' => env('VOUCHER_START') ?: null,
'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)
'ifsg_enabled' => (bool) env('IFSG_ENABLED', false),
# Instruction only onsite in accordance with § 43 Para. 1 of the German Infection Protection Act (IfSG)
'ifsg_light_enabled' => 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/
// To disable a locale in the config.php, you can set its value to null
@ -404,14 +378,11 @@ return [
'4XL' => '4XLarge Straight-Cut',
],
// T-shirt Size-Guide link
'tshirt_link' => env('TSHIRT_LINK'),
// Whether to show the current day of the event (-2, -1, 0, 1, 2…) in footer and on the dashboard.
// The event start date has to be set for it to appear.
'enable_show_day_of_event' => (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…)
'event_has_day0' => (bool) env('EVENT_HAS_DAY0', true),
'event_has_day0' => true,
'metrics' => [
// User work buckets in seconds
@ -447,10 +418,7 @@ return [
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'sameorigin',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Content-Security-Policy' =>
'default-src \'self\'; '
. ' style-src \'self\' \'unsafe-inline\'; '
. 'img-src \'self\' data:;',
'Content-Security-Policy' => 'default-src \'self\' \'unsafe-inline\' \'unsafe-eval\'; img-src \'self\' data:;',
'X-XSS-Protection' => '1; mode=block',
'Feature-Policy' => 'autoplay \'none\'',
//'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
$route->addGroup(
'/password/reset',
@ -197,11 +187,11 @@ $route->addGroup(
$route->addGroup(
'/schedule',
function (RouteCollector $route): void {
$route->get('', 'Admin\\ScheduleController@index');
$route->get('/edit[/{schedule_id:\d+}]', 'Admin\\ScheduleController@edit');
$route->post('/edit[/{schedule_id:\d+}]', 'Admin\\ScheduleController@save');
$route->get('/load/{schedule_id:\d+}', 'Admin\\ScheduleController@loadSchedule');
$route->post('/import/{schedule_id:\d+}', 'Admin\\ScheduleController@importSchedule');
$route->get('', 'Admin\\Schedule\\ImportSchedule@index');
$route->get('/edit[/{schedule_id:\d+}]', 'Admin\\Schedule\\ImportSchedule@edit');
$route->post('/edit[/{schedule_id:\d+}]', 'Admin\\Schedule\\ImportSchedule@save');
$route->get('/load/{schedule_id:\d+}', 'Admin\\Schedule\\ImportSchedule@loadSchedule');
$route->post('/import/{schedule_id:\d+}', 'Admin\\Schedule\\ImportSchedule@importSchedule');
}
);
@ -252,12 +242,12 @@ $route->addGroup(
$route->addGroup(
'/user/{user_id:\d+}',
function (RouteCollector $route): void {
// Goodies
// Shirts
$route->addGroup(
'/goodie',
function (RouteCollector $route): void {
$route->get('', 'Admin\\UserGoodieController@editGoodie');
$route->post('', 'Admin\\UserGoodieController@saveGoodie');
$route->get('', 'Admin\\UserShirtController@editShirt');
$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_forklift = ($drive_car && $this->faker->boolean(.1))
|| ($drive_12t && $this->faker->boolean(.7));
$drive_confirmed = $this->faker->boolean(0.5) && (
$drive_car
|| $drive_3_5t
|| $drive_7_5t
|| $drive_12t
|| $drive_forklift
);
$ifsg_certificate = $this->faker->boolean(0.1);
$ifsg_certificate_light = $this->faker->boolean(0.5) && !$ifsg_certificate;
$ifsg_confirmed = $this->faker->boolean(0.5) && ($ifsg_certificate || $ifsg_certificate_light);
return [
'user_id' => User::factory(),
@ -41,10 +33,8 @@ class LicenseFactory extends Factory
'drive_3_5t' => $drive_3_5t,
'drive_7_5t' => $drive_7_5t,
'drive_12t' => $drive_12t,
'drive_confirmed' => $drive_confirmed,
'ifsg_certificate' => $ifsg_certificate,
'ifsg_certificate_light' => $ifsg_certificate_light,
'ifsg_confirmed' => $ifsg_confirmed,
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,8 @@ use Illuminate\Database\Schema\Blueprint;
class AddUserInfoToUsersState extends Migration
{
use Reference;
/**
* 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
# Contains all tools required to build / manage the system
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
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
ENTRYPOINT php -r 'sleep(PHP_INT_MAX);'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,9 @@ function theme_id(): int
return $globals['themeId'];
}
/**
* @return array
*/
function theme(): array
{
$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/Stats.php',
__DIR__ . '/../includes/model/User_model.php',
__DIR__ . '/../includes/model/UserWorkLog_model.php',
__DIR__ . '/../includes/model/ValidationResult.php',
__DIR__ . '/../includes/view/AngelTypes_view.php',
@ -46,6 +47,7 @@ $includeFiles = [
__DIR__ . '/../includes/helper/legacy_helper.php',
__DIR__ . '/../includes/helper/message_helper.php',
__DIR__ . '/../includes/helper/email_helper.php',
__DIR__ . '/../includes/helper/shift_helper.php',
__DIR__ . '/../includes/mailer/shifts_mailer.php',
__DIR__ . '/../includes/mailer/users_mailer.php',
@ -58,6 +60,8 @@ $includeFiles = [
__DIR__ . '/../includes/pages/admin_user.php',
__DIR__ . '/../includes/pages/user_myshifts.php',
__DIR__ . '/../includes/pages/user_shifts.php',
__DIR__ . '/../includes/pages/schedule/ImportSchedule.php',
];
foreach ($includeFiles as $file) {

View File

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

View File

@ -116,7 +116,7 @@ function Shifts_free($start, $end, ShiftsFilter $filter = null)
$shifts = collect($shifts);
return Shift::with(['location', 'shiftType'])
return Shift::query()
->whereIn('id', $shifts->pluck('id')->toArray())
->orderBy('shifts.start')
->get();
@ -189,14 +189,12 @@ function Shifts_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
]
);
$shifts = new Collection();
$shifts = [];
foreach ($shiftsData as $shift) {
$shifts[] = (new Shift())->forceFill($shift);
}
$shifts->load(['location', 'shiftType']);
return $shifts;
return collect($shifts);
}
/**
@ -356,7 +354,7 @@ function NeededAngeltype_by_Shift_and_Angeltype(Shift $shift, AngelType $angelty
*/
function ShiftEntries_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
{
return ShiftEntry::with('user', 'user.state')
return ShiftEntry::with('user')
->join('shifts', 'shifts.id', 'shift_entries.shift_id')
->whereIn('shifts.location_id', $shiftsFilter->getLocations())
->whereBetween('start', [$shiftsFilter->getStart(), $shiftsFilter->getEnd()])
@ -430,6 +428,14 @@ function Shift_signup_allowed_angel(
) {
$free_entries = Shift_free_entries($needed_angeltype, $shift_entries);
if (config('signup_requires_arrival') && !$user->state->arrived) {
return new ShiftSignupState(ShiftSignupStatus::NOT_ARRIVED, $free_entries);
}
if (config('signup_advance_hours') && $shift->start->timestamp > time() + config('signup_advance_hours') * 3600) {
return new ShiftSignupState(ShiftSignupStatus::NOT_YET, $free_entries);
}
if (is_null($user_shifts) || $user_shifts->isEmpty()) {
$user_shifts = Shifts_by_user($user->id);
}
@ -481,14 +487,6 @@ function Shift_signup_allowed_angel(
return new ShiftSignupState(ShiftSignupStatus::COLLIDES, $free_entries);
}
if (config('signup_advance_hours') && $shift->start->timestamp > time() + config('signup_advance_hours') * 3600) {
return new ShiftSignupState(ShiftSignupStatus::NOT_YET, $free_entries);
}
if (config('signup_requires_arrival') && !$user->state->arrived) {
return new ShiftSignupState(ShiftSignupStatus::NOT_ARRIVED, $free_entries);
}
// Hooray, shift is free for you!
return new ShiftSignupState(ShiftSignupStatus::FREE, $free_entries);
}
@ -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
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;
}
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;
}
@ -586,7 +585,8 @@ function Shift_signup_allowed(
}
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);
}
@ -638,13 +638,12 @@ function Shifts_by_user($userId, $include_freeloaded_comments = false)
]
);
$shifts = new Collection();
$shifts = [];
foreach ($shiftsData as $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\User\User;
use Engelsystem\Models\Worklog;
use Engelsystem\ValidationResult;
use Illuminate\Database\Query\JoinClause;
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.
*
* @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();
$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`
LEFT JOIN `shifts` ON `shift_entries`.`shift_id` = `shifts`.`id`
WHERE `users`.`id` = ?
@ -32,8 +33,8 @@ function User_goodie_score(int $userId): float
', $shift_sum_formula), [
$userId,
]);
if (!isset($result_shifts['goodie_score'])) {
$result_shifts = ['goodie_score' => 0];
if (!isset($result_shifts['tshirt_score'])) {
$result_shifts = ['tshirt_score' => 0];
}
$worklogHours = Worklog::query()
@ -41,7 +42,7 @@ function User_goodie_score(int $userId): float
->where('worked_at', '<=', Carbon::Now())
->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();
}
/**
* Validate the planned arrival date
*
* @param int $planned_arrival_date Unix timestamp
* @return ValidationResult
*/
function User_validate_planned_arrival_date($planned_arrival_date)
{
if (is_null($planned_arrival_date)) {
// null is not okay
return new ValidationResult(false, time());
}
$config = config();
$buildup = $config->get('buildup_start');
$teardown = $config->get('teardown_end');
/** @var Carbon $buildup */
if (!empty($buildup) && Carbon::createFromTimestamp($planned_arrival_date)->lessThan($buildup->setTime(0, 0))) {
// Planned arrival can not be before buildup start date
return new ValidationResult(false, $buildup->getTimestamp());
}
/** @var Carbon $teardown */
if (!empty($teardown) && Carbon::createFromTimestamp($planned_arrival_date)->greaterThanOrEqualTo($teardown->addDay()->setTime(0, 0))) {
// Planned arrival can not be after teardown end date
return new ValidationResult(false, $teardown->getTimestamp());
}
return new ValidationResult(true, $planned_arrival_date);
}
/**
* Validate the planned departure date
*
* @param int $planned_arrival_date Unix timestamp
* @param int $planned_departure_date Unix timestamp
* @return ValidationResult
*/
function User_validate_planned_departure_date($planned_arrival_date, $planned_departure_date)
{
if (is_null($planned_departure_date)) {
// null is okay
return new ValidationResult(true, null);
}
if ($planned_arrival_date > $planned_departure_date) {
// departure cannot be before arrival
return new ValidationResult(false, $planned_arrival_date);
}
$config = config();
$buildup = $config->get('buildup_start');
$teardown = $config->get('teardown_end');
/** @var Carbon $buildup */
if (!empty($buildup) && Carbon::createFromTimestamp($planned_departure_date)->lessThan($buildup->setTime(0, 0))) {
// Planned departure can not be before buildup start date
return new ValidationResult(false, $buildup->getTimestamp());
}
/** @var Carbon $teardown */
if (!empty($teardown) && Carbon::createFromTimestamp($planned_departure_date)->greaterThanOrEqualTo($teardown->addDay()->setTime(0, 0))) {
// Planned departure can not be after teardown end date
return new ValidationResult(false, $teardown->getTimestamp());
}
return new ValidationResult(true, $planned_departure_date);
}
/**
* @param User $user
* @return float
@ -78,10 +149,7 @@ function User_get_eligable_voucher_count($user)
: null;
$shiftEntries = ShiftEntries_finished_by_user($user, $start);
$worklog = $user->worklogs()
->whereDate('worked_at', '>=', $start ?: 0)
->with(['user', 'creator'])
->get();
$worklog = UserWorkLogsForUser($user->id, $start);
$shifts_done =
count($shiftEntries)
+ $worklog->count();
@ -123,20 +191,13 @@ function User_get_shifts_sum_query()
return 'COALESCE(SUM(UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start)), 0)';
}
/* @see \Engelsystem\Models\Shifts\Shift::isNightShift to keep it in sync */
return sprintf(
'
COALESCE(SUM(
(1 + (
/* Starts during night */
HOUR(shifts.start) >= %1$d AND HOUR(shifts.start) < %2$d
/* Ends during night */
OR (
HOUR(shifts.end) > %1$d
|| HOUR(shifts.end) = %1$d AND MINUTE(shifts.end) > 0
) AND HOUR(shifts.end) <= %2$d
/* Starts before and ends after night */
OR HOUR(shifts.start) <= %1$d AND HOUR(shifts.end) >= %2$d
(HOUR(shifts.end) > %1$d AND HOUR(shifts.end) < %2$d)
OR (HOUR(shifts.start) > %1$d AND HOUR(shifts.start) < %2$d)
OR (HOUR(shifts.start) <= %1$d AND HOUR(shifts.end) >= %2$d)
))
* (UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start))
* (1 - (%3$d + 1) * `shift_entries`.`freeloaded`)

View File

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

View File

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

View File

@ -40,7 +40,7 @@ function admin_free()
/** @var User[] $users */
$users = [];
if ($request->has('submit')) {
$query = User::with(['personalData', 'contact', 'state'])
$query = User::with('personalData')
->select('users.*')
->leftJoin('shift_entries', 'users.id', 'shift_entries.user_id')
->leftJoin('users_state', 'users.id', 'users_state.user_id')
@ -99,7 +99,9 @@ function admin_free()
$free_users_table[] = [
'name' => User_Nick_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),
'last_shift' => User_last_shift_render($usr),
'dect' => sprintf('<a href="tel:%s">%1$s</a>', htmlspecialchars((string) $usr->contact->dect)),

View File

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

View File

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

View File

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

View File

@ -2,13 +2,13 @@
declare(strict_types=1);
namespace Engelsystem\Controllers\Admin;
namespace Engelsystem\Controllers\Admin\Schedule;
use Engelsystem\Controllers\NotificationType;
use Engelsystem\Helpers\Carbon;
use DateTimeInterface;
use Engelsystem\Controllers\BaseController;
use Engelsystem\Controllers\HasUserNotifications;
use Engelsystem\Helpers\Schedule\ConferenceTrack;
use Engelsystem\Helpers\Schedule\Event;
use Engelsystem\Helpers\Schedule\Room;
use Engelsystem\Helpers\Schedule\Schedule;
@ -17,45 +17,73 @@ use Engelsystem\Helpers\Uuid;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
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\Shift;
use Engelsystem\Models\Shifts\ShiftType;
use Engelsystem\Models\User\User;
use ErrorException;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Database\Connection as DatabaseConnection;
use Illuminate\Database\Eloquent\Builder as QueryBuilder;
use Illuminate\Database\Eloquent\Collection as DatabaseCollection;
use Illuminate\Support\Collection;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class ScheduleController extends BaseController
class ImportSchedule extends BaseController
{
use HasUserNotifications;
/** @var DatabaseConnection */
protected $db;
/** @var LoggerInterface */
protected $log;
protected array $permissions = [
'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(
protected Response $response,
protected GuzzleClient $guzzle,
protected XmlParser $parser,
protected DatabaseConnection $db,
protected LoggerInterface $log
Response $response,
SessionInterface $session,
GuzzleClient $guzzle,
XmlParser $parser,
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
{
return $this->response->withView(
'admin/schedule/index',
'admin/schedule/index.twig',
[
'is_index' => true,
'schedules' => ScheduleModel::all(),
'is_index' => true,
'schedules' => ScheduleUrl::all(),
]
);
}
@ -63,14 +91,14 @@ class ScheduleController extends BaseController
public function edit(Request $request): Response
{
$scheduleId = $request->getAttribute('schedule_id'); // optional
$schedule = ScheduleModel::findOrNew($scheduleId);
$schedule = ScheduleUrl::find($scheduleId);
return $this->response->withView(
'admin/schedule/edit',
'admin/schedule/edit.twig',
[
'schedule' => $schedule,
'shift_types' => ShiftType::all()->sortBy('name')->pluck('name', 'id'),
'locations' => Location::all()->sortBy('name')->pluck('name', 'id'),
'schedule' => $schedule,
'shift_types' => ShiftType::all()->pluck('name', 'id'),
]
);
}
@ -79,28 +107,25 @@ class ScheduleController extends BaseController
{
$scheduleId = $request->getAttribute('schedule_id'); // optional
/** @var ScheduleModel $schedule */
$schedule = ScheduleModel::findOrNew($scheduleId);
/** @var ScheduleUrl $schedule */
$schedule = ScheduleUrl::findOrNew($scheduleId);
if ($request->request->has('delete')) {
return $this->delete($schedule);
}
$locationsList = Location::all()->pluck('id');
$locationsValidation = [];
foreach ($locationsList as $id) {
$locationsValidation['location_' . $id] = 'optional|checked';
}
$data = $this->validate($request, [
'name' => 'required',
'url' => 'required',
'shift_type' => 'required|int',
'name' => 'required',
'url' => 'required',
'shift_type' => 'required|int',
'needed_from_shift_type' => 'optional|checked',
'minutes_before' => 'int',
'minutes_after' => 'int',
] + $locationsValidation);
ShiftType::findOrFail($data['shift_type']);
'minutes_after' => 'int',
]);
if (!ShiftType::find($data['shift_type'])) {
throw new ErrorException('schedule.import.invalid-shift-type');
}
$schedule->name = $data['name'];
$schedule->url = $data['url'];
@ -110,31 +135,16 @@ class ScheduleController extends BaseController
$schedule->minutes_after = $data['minutes_after'];
$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(
'Schedule {name}: Url {url}, Shift Type {shift_type_name} ({shift_type_id}), ({need}), '
. 'minutes before/after {before}/{after}, for: {locations}',
'Schedule {name}: Url {url}, Shift Type {shift_type}, ({need}), minutes before/after {before}/{after}',
[
'name' => $schedule->name,
'url' => $schedule->name,
'shift_type_name' => Shifttype::find($schedule->shift_type)->name,
'shift_type_id' => $schedule->shift_type,
'name' => $schedule->name,
'url' => $schedule->name,
'shift_type' => $schedule->shift_type,
'need' => $schedule->needed_from_shift_type ? 'from shift type' : 'from room',
'before' => $schedule->minutes_before,
'after' => $schedule->minutes_after,
'locations' => $for->implode(', '),
'before' => $schedule->minutes_before,
'after' => $schedule->minutes_after,
]
);
@ -143,7 +153,7 @@ class ScheduleController extends BaseController
return redirect('/admin/schedule/load/' . $schedule->id);
}
protected function delete(ScheduleModel $schedule): Response
protected function delete(ScheduleUrl $schedule): Response
{
foreach ($schedule->scheduleShifts as $scheduleShift) {
// Only guid is needed here
@ -159,14 +169,13 @@ class ScheduleController extends BaseController
'',
'',
'',
new ConferenceTrack('')
''
);
$this->deleteEvent($event, $schedule);
}
$schedule->delete();
$this->log->info('Schedule {name} deleted', ['name' => $schedule->name]);
$this->addNotification('schedule.delete.success');
return redirect('/admin/schedule');
}
@ -175,15 +184,15 @@ class ScheduleController extends BaseController
{
try {
/**
* @var Event[] $newEvents
* @var Event[] $changeEvents
* @var Event[] $deleteEvents
* @var Room[] $newRooms
* @var int $shiftType
* @var ScheduleModel $scheduleModel
* @var Schedule $schedule
* @var int $minutesBefore
* @var int $minutesAfter
* @var Event[] $newEvents
* @var Event[] $changeEvents
* @var Event[] $deleteEvents
* @var Room[] $newRooms
* @var int $shiftType
* @var ScheduleUrl $scheduleUrl
* @var Schedule $schedule
* @var int $minutesBefore
* @var int $minutesAfter
*/
list(
$newEvents,
@ -191,7 +200,7 @@ class ScheduleController extends BaseController
$deleteEvents,
$newRooms,
,
$scheduleModel,
$scheduleUrl,
$schedule
) = $this->getScheduleData($request);
} catch (ErrorException $e) {
@ -200,15 +209,15 @@ class ScheduleController extends BaseController
}
return $this->response->withView(
'admin/schedule/load',
'admin/schedule/load.twig',
[
'schedule_id' => $scheduleModel->id,
'schedule' => $schedule,
'locations' => [
'schedule_id' => $scheduleUrl->id,
'schedule' => $schedule,
'locations' => [
'add' => $newRooms,
],
'shifts' => [
'add' => $newEvents,
'shifts' => [
'add' => $newEvents,
'update' => $changeEvents,
'delete' => $deleteEvents,
],
@ -220,12 +229,12 @@ class ScheduleController extends BaseController
{
try {
/**
* @var Event[] $newEvents
* @var Event[] $changeEvents
* @var Event[] $deleteEvents
* @var Room[] $newRooms
* @var int $shiftType
* @var ScheduleModel $schedule
* @var Event[] $newEvents
* @var Event[] $changeEvents
* @var Event[] $deleteEvents
* @var Room[] $newRooms
* @var int $shiftType
* @var ScheduleUrl $scheduleUrl
*/
list(
$newEvents,
@ -233,14 +242,14 @@ class ScheduleController extends BaseController
$deleteEvents,
$newRooms,
$shiftType,
$schedule
$scheduleUrl
) = $this->getScheduleData($request);
} catch (ErrorException $e) {
$this->addNotification($e->getMessage(), NotificationType::ERROR);
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) {
$this->createLocation($room);
@ -254,7 +263,7 @@ class ScheduleController extends BaseController
$locations
->where('name', $event->getRoom()->getName())
->first(),
$schedule
$scheduleUrl
);
}
@ -265,16 +274,16 @@ class ScheduleController extends BaseController
$locations
->where('name', $event->getRoom()->getName())
->first(),
$schedule
$scheduleUrl
);
}
foreach ($deleteEvents as $event) {
$this->deleteEvent($event, $schedule);
$this->deleteEvent($event, $scheduleUrl);
}
$schedule->touch();
$this->log->info('Ended schedule "{name}" import', ['name' => $schedule->name]);
$scheduleUrl->touch();
$this->log('Ended schedule "{name}" import', ['name' => $scheduleUrl->name]);
$this->addNotification('schedule.import.success');
return redirect($this->url, 303);
@ -286,22 +295,41 @@ class ScheduleController extends BaseController
$location->name = $room->getName();
$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 */
$scheduleShifts = ScheduleShift::where('guid', $event->getGuid())
->where('schedule_id', $schedule->id)
$shiftEntries = $this->db
->table('shift_entries')
->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();
foreach ($scheduleShifts as $scheduleShift) {
event('shift.deleting', ['shift' => $scheduleShift->shift]);
foreach ($shiftEntries as $shiftEntry) {
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();
$eventTimeZone = Carbon::now()->timezone;
@ -313,31 +341,28 @@ class ScheduleController extends BaseController
$shift->end = $event->getEndDate()->copy()->timezone($eventTimeZone);
$shift->location()->associate($location);
$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->save();
$scheduleShift = new ScheduleShift(['guid' => $event->getGuid()]);
$scheduleShift->schedule()->associate($schedule);
$scheduleShift->schedule()->associate($scheduleUrl);
$scheduleShift->shift()->associate($shift);
$scheduleShift->save();
$this->log->info(
'Created schedule ({schedule}) shift: {shifttype} with title '
. '"{shift}" in "{location}" ({from} - {to}, {guid})',
$this->log(
'Created schedule shift "{shift}" in "{location}" ({from} {to}, {guid})',
[
'schedule' => $scheduleShift->schedule->name,
'shifttype' => $shift->shiftType->name,
'shift' => $shift->title,
'shift' => $shift->title,
'location' => $shift->location->name,
'from' => $shift->start->format('Y-m-d H:i'),
'to' => $shift->end->format('Y-m-d H:i'),
'guid' => $scheduleShift->guid,
'from' => $shift->start->format(DateTimeInterface::RFC3339),
'to' => $shift->end->format(DateTimeInterface::RFC3339),
'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();
$eventTimeZone = Carbon::now()->timezone;
@ -357,40 +382,35 @@ class ScheduleController extends BaseController
$this->fireUpdateShiftUpdateEvent($oldShift, $shift);
$this->log->info(
'Updated schedule ({schedule}) shift: {shifttype} with title '
. '"{shift}" in "{location}" ({from} - {to}, {guid})',
$this->log(
'Updated schedule shift "{shift}" in "{location}" ({from} {to}, {guid})',
[
'schedule' => $scheduleShift->schedule->name,
'shifttype' => $shift->shiftType->name,
'shift' => $shift->title,
'shift' => $shift->title,
'location' => $shift->location->name,
'from' => $shift->start->format('Y-m-d H:i'),
'to' => $shift->end->format('Y-m-d H:i'),
'guid' => $scheduleShift->guid,
'from' => $shift->start->format(DateTimeInterface::RFC3339),
'to' => $shift->end->format(DateTimeInterface::RFC3339),
'guid' => $scheduleShift->guid,
]
);
}
protected function deleteEvent(Event $event, ScheduleModel $schedule): void
protected function deleteEvent(Event $event, ScheduleUrl $schedule): void
{
/** @var ScheduleShift $scheduleShift */
$scheduleShift = ScheduleShift::whereGuid($event->getGuid())->where('schedule_id', $schedule->id)->first();
$shift = $scheduleShift->shift;
$this->fireDeleteShiftEvents($event, $schedule);
$shift->delete();
$scheduleShift->delete();
$this->log->info(
'Deleted schedule ({schedule}) shift: "{shift}" in {location} ({from} - {to}, {guid})',
$this->fireDeleteShiftEntryEvents($event, $schedule);
$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,
'from' => $shift->start->format('Y-m-d H:i'),
'to' => $shift->end->format('Y-m-d H:i'),
'guid' => $scheduleShift->guid,
'from' => $shift->start->format(DateTimeInterface::RFC3339),
'to' => $shift->end->format(DateTimeInterface::RFC3339),
'guid' => $scheduleShift->guid,
]
);
}
@ -404,45 +424,40 @@ class ScheduleController extends BaseController
}
/**
* @param Request $request
* @return Event[]|Room[]|Location[]
* @throws ErrorException
*/
protected function getScheduleData(Request $request): array
protected function getScheduleData(Request $request)
{
$scheduleId = (int) $request->getAttribute('schedule_id');
/** @var ScheduleModel $scheduleModel */
$scheduleModel = ScheduleModel::findOrFail($scheduleId);
/** @var ScheduleUrl $scheduleUrl */
$scheduleUrl = ScheduleUrl::findOrFail($scheduleId);
try {
$scheduleResponse = $this->guzzle->get($scheduleModel->url);
} catch (ConnectException | GuzzleException $e) {
$this->log->error('Exception during schedule request', ['exception' => $e]);
$scheduleResponse = $this->guzzle->get($scheduleUrl->url);
} catch (ConnectException $e) {
throw new ErrorException('schedule.import.request-error');
}
if ($scheduleResponse->getStatusCode() != 200) {
$this->log->warning(
'Problem during schedule request, got code {code}',
['code' => $scheduleResponse->getStatusCode()]
);
throw new ErrorException('schedule.import.request-error');
}
$scheduleData = (string) $scheduleResponse->getBody();
if (!$this->parser->load($scheduleData)) {
$this->log->warning('Problem during schedule parsing');
throw new ErrorException('schedule.import.read-error');
}
$shiftType = $scheduleModel->shift_type;
$shiftType = $scheduleUrl->shift_type;
$schedule = $this->parser->getSchedule();
$minutesBefore = $scheduleModel->minutes_before;
$minutesAfter = $scheduleModel->minutes_after;
$minutesBefore = $scheduleUrl->minutes_before;
$minutesAfter = $scheduleUrl->minutes_after;
$newRooms = $this->newRooms($schedule->getRooms());
return array_merge(
$this->shiftsDiff($schedule, $scheduleModel, $shiftType, $minutesBefore, $minutesAfter),
[$newRooms, $shiftType, $scheduleModel, $schedule, $minutesBefore, $minutesAfter]
$this->shiftsDiff($schedule, $scheduleUrl, $shiftType, $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[]
*/
protected function shiftsDiff(
Schedule $schedule,
ScheduleModel $scheduleModel,
ScheduleUrl $scheduleUrl,
int $shiftType,
int $minutesBefore,
int $minutesAfter
@ -487,13 +507,9 @@ class ScheduleController extends BaseController
$locations = $this->getAllLocations();
$eventTimeZone = Carbon::now()->timezone;
foreach ($schedule->getDays() as $day) {
foreach ($day->getRooms() as $room) {
if (!$scheduleModel->activeLocations->where('name', $room->getName())->count()) {
continue;
}
foreach ($room->getEvents() as $event) {
foreach ($schedule->getDay() as $day) {
foreach ($day->getRoom() as $room) {
foreach ($room->getEvent() as $event) {
$scheduleEvents[$event->getGuid()] = $event;
$event->getDate()->timezone($eventTimeZone)->subMinutes($minutesBefore);
@ -508,12 +524,12 @@ class ScheduleController extends BaseController
}
$scheduleEventsGuidList = array_keys($scheduleEvents);
$existingShifts = $this->getScheduleShiftsByGuid($scheduleModel, $scheduleEventsGuidList);
$existingShifts = $this->getScheduleShiftsByGuid($scheduleUrl, $scheduleEventsGuidList);
foreach ($existingShifts as $scheduleShift) {
$guid = $scheduleShift->guid;
$shift = $scheduleShift->shift;
/** @var Shift $shift */
$shift = Shift::with('location')->find($scheduleShift->shift_id);
$event = $scheduleEvents[$guid];
/** @var Location $location */
$location = $locations->where('name', $event->getRoom()->getName())->first();
if (
@ -534,7 +550,7 @@ class ScheduleController extends BaseController
$newEvents[$scheduleEvent->getGuid()] = $scheduleEvent;
}
$scheduleShifts = $this->getScheduleShiftsWhereNotGuid($scheduleModel, $scheduleEventsGuidList);
$scheduleShifts = $this->getScheduleShiftsWhereNotGuid($scheduleUrl, $scheduleEventsGuidList);
foreach ($scheduleShifts as $scheduleShift) {
$event = $this->eventFromScheduleShift($scheduleShift);
$deleteEvents[$event->getGuid()] = $event;
@ -560,40 +576,50 @@ class ScheduleController extends BaseController
$duration->format('%H:%I'),
'',
'',
new ConferenceTrack('')
''
);
}
/**
* @return Location[]|Collection
*/
protected function getAllLocations(): Collection | array
protected function getAllLocations(): Collection
{
return Location::all();
}
/**
* @param string[] $events
*
* @return Collection|ScheduleShift[]
* @param ScheduleUrl $scheduleUrl
* @param string[] $events
* @return QueryBuilder[]|DatabaseCollection|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)
->where('schedule_id', $schedule->id)
->where('schedule_id', $scheduleUrl->id)
->get();
}
/**
* @param string[] $events
* @return Collection|ScheduleShift[]
* @param ScheduleUrl $scheduleUrl
* @param string[] $events
* @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)
->where('schedule_id', $schedule->id)
->where('schedule_id', $scheduleUrl->id)
->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();
$request = request();
$is_angeltype_supporter = false;
if ($request->has('edit')) {
$id = $request->input('edit');
$shiftEntry = ShiftEntry::where('id', $id)
->where('user_id', User::find($request->input('id'))->id)
->first();
$is_angeltype_supporter = $shiftEntry && auth()->user()->isAngelTypeSupporter($shiftEntry->angelType);
}
if (
$request->has('id')
&& (auth()->can('user_shifts_admin') || $is_angeltype_supporter)
&& auth()->can('user_shifts_admin')
&& preg_match('/^\d+$/', $request->input('id'))
&& User::find($request->input('id'))
) {
@ -41,7 +33,21 @@ function user_myshifts()
}
$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');
/** @var ShiftEntry $shiftEntry */
$shiftEntry = ShiftEntry::where('id', $shift_entry_id)
@ -55,10 +61,7 @@ function user_myshifts()
if ($request->hasPostData('submit')) {
$valid = true;
if (
auth()->can('user_shifts_admin')
|| $is_angeltype_supporter
) {
if (auth()->can('user_shifts_admin')) {
$freeloaded = $request->has('freeloaded');
$freeloaded_comment = strip_request_item_nl('freeloaded_comment');
if ($freeloaded && $freeloaded_comment == '') {
@ -88,9 +91,6 @@ function user_myshifts()
. '. Freeloaded: ' . ($freeloaded ? 'YES Comment: ' . $freeloaded_comment : 'NO')
);
success(__('Shift saved.'));
if ($is_angeltype_supporter) {
throw_redirect(url('/shifts', ['action' => 'view', 'shift_id' => $shiftEntry->shift_id]));
}
throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id]));
}
}
@ -104,13 +104,13 @@ function user_myshifts()
$shiftEntry->user_comment,
$shiftEntry->freeloaded,
$shiftEntry->freeloaded_comment,
auth()->can('user_shifts_admin'),
$is_angeltype_supporter
auth()->can('user_shifts_admin')
);
} else {
throw_redirect(url('/user-myshifts'));
}
}
throw_redirect(url('/users', ['action' => 'view', 'user_id' => $shifts_user->id]));
return '';
}

View File

@ -17,7 +17,7 @@ use Illuminate\Support\Collection;
*/
function shifts_title()
{
return __('general.shifts');
return __('Shifts');
}
/**
@ -297,6 +297,7 @@ function view_user_shifts()
return page([
div('col-md-12', [
msg(),
view(__DIR__ . '/../../resources/views/pages/user-shifts.html', [
'title' => shifts_title(),
'add_link' => auth()->can('admin_shifts') ? $link : '',
@ -374,11 +375,18 @@ function ical_hint()
return heading(__('iCal export and API') . ' ' . button_help('user/ical'), 2)
. '<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('/shifts-json-export', ['key' => $user->api_key]),
url('/settings/api')
) . '</p>';
url('/user-myshifts', ['reset' => 1])
)
. ' <button class="btn btn-sm btn-danger" type="button"
data-bs-toggle="collapse" data-bs-target="#collapseApiKey"
aria-expanded="false" aria-controls="collapseApiKey">
' . __('Show API Key') . '
</button>'
. '</p>'
. '<p id="collapseApiKey" class="collapse"><code>' . $user->api_key . '</code></p>';
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -108,15 +108,14 @@ function UserAngelType_confirm_view(UserAngelType $user_angeltype, User $user, A
* @param UserAngelType $user_angeltype
* @param User $user
* @param AngelType $angeltype
* @param bool $isOwnAngelType
* @return string
*/
function UserAngelType_delete_view(UserAngelType $user_angeltype, User $user, AngelType $angeltype, 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(),
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,
$angeltype->name
), true),
@ -152,9 +151,7 @@ function UserAngelType_add_view(AngelType $angeltype, $users_source, $user_id)
msg(),
form([
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_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)
{
$isOther = $user->id != auth()->user()->id;
return page_with_title(sprintf(__('Become a %s'), htmlspecialchars($angeltype->name)), [
msg(),
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,
$angeltype->name
), true),

View File

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

View File

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

View File

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

View File

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

View File

@ -36,20 +36,12 @@ tags:
- name: user
description: User information
security:
- bearer-auth: [ ]
- api-key-header: [ ]
components:
securitySchemes:
bearer-auth:
bearerAuth:
type: http
scheme: bearer
bearerFormat: API key from settings
api-key-header:
type: apiKey
name: x-api-key
in: header
responses:
UnauthorizedError: # 401
@ -399,9 +391,12 @@ components:
properties:
start:
$ref: '#/components/schemas/DateTimeOptional'
end:
$ref: '#/components/schemas/DateTimeOptional'
required:
- start
event:
- end
teardown:
type: object
properties:
start:
@ -411,13 +406,6 @@ components:
required:
- start
- end
teardown:
type: object
properties:
end:
$ref: '#/components/schemas/DateTimeOptional'
required:
- end
required:
- api
- spec
@ -426,9 +414,11 @@ components:
- url
- timezone
- buildup
- event
- teardown
security:
- bearerAuth: [ ]
paths:
/angeltypes:
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 class="modal-footer">
<button type="button" class="${element.className}"
autofocus
title="${element.title}" data-submit="">
${element.dataset.confirm_button_text ?? element.innerHTML}
</button>
@ -329,25 +328,15 @@ ready(() => {
`
);
const modal = document.getElementById('confirmation-modal');
let modal = document.getElementById('confirmation-modal');
modal.addEventListener('hide.bs.modal', () => {
modalOpen = false;
});
const modalSubmitButton = modal.querySelector('[data-submit]');
modalSubmitButton.addEventListener('click', () => {
modal.querySelector('[data-submit]').addEventListener('click', (event) => {
element.type = oldType;
element.click();
});
/**
* After the modal has been shown, focus on the "Submit" button in the modal
* so that it can be confirmed with "Enter".
*/
modal.addEventListener('shown.bs.modal', () => {
modalSubmitButton.focus();
});
modalOpen = true;
let bootstrapModal = new bootstrap.Modal(modal);
bootstrapModal.show();
@ -360,7 +349,7 @@ ready(() => {
*/
ready(() => {
[
['welcome-title', '.registration .d-none'],
['welcome-title', '.btn-group .btn.d-none'],
['settings-title', '.user-settings .nav-item'],
['oauth-settings-title', 'table tr.d-none'],
].forEach(([id, selector]) => {

View File

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

View File

@ -18,6 +18,7 @@
&-40 {
// >= 40 bars
--barchart-bar-margin: 1px;
--barchart-bar-margin: 0.5px;
--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;
$choices-guttering: auto;
@import '~bootstrap/scss/utilities';
@import '~bootstrap/scss/root';
@ -102,10 +100,6 @@ a .icon-icon_angel {
background-color: $link-color;
}
a .disabled {
pointer-events: none;
}
.navbar .icon-icon_angel {
background-color: $nav-link-disabled-color;
}
@ -251,10 +245,6 @@ table .border-bottom {
font-size: 0.9em;
padding-left: 5px;
}
.tick.now {
border-top: 2px solid $info;
}
}
.lane.time {

View File

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

View File

@ -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"
msgstr "Das Programm konnte nicht gelesen werden."
msgid "schedule.import.invalid-shift-type"
msgstr "Der Schichttyp konnte nicht gefunden werden."
msgid "schedule.import.success"
msgstr "Das Programm wurde erfolgreich importiert."

View File

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

View File

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

View File

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

View File

@ -8,15 +8,15 @@
<div class="container">
<h1>
{% if not is_index|default(false) %}
{{ m.back(location
{{ m.button(m.icon('chevron-left'), location
? url('/locations', {'action': 'view', 'location_id': location.id})
: url('/admin/locations')) }}
: url('/admin/locations'), 'secondary', 'sm', __('general.back')) }}
{% endif %}
{{ block('title') }}
{% 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 %}
</h1>
@ -33,7 +33,6 @@
<th>{{ __('general.name') }}</th>
<th>{{ __('general.dect') }}</th>
<th>{{ __('location.map_url') }}</th>
<th>{{ __('general.shifts') }}</th>
<th></th>
</tr>
</thead>
@ -52,12 +51,10 @@
<td>{{ m.iconBool(location.map_url) }}</td>
<td>{{ m.iconBool(location.shifts.count) }}</td>
<td>
<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">
{{ csrf() }}

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