From 29f83f6395c90f4801a8dbb7a2345d2a96ba22d5 Mon Sep 17 00:00:00 2001 From: KimSpeer Date: Fri, 12 Jul 2024 10:44:42 +0200 Subject: [PATCH] added trainings package --- packages/trainings/.github/FUNDING.yml | 13 + packages/trainings/.gitignore | 49 ++++ packages/trainings/CHANGELOG.md | 1 + packages/trainings/FUNDING.yml | 13 + packages/trainings/LICENSE.md | 21 ++ packages/trainings/README.md | 53 ++++ packages/trainings/SECURITY.md | 13 + packages/trainings/composer.json | 36 +++ packages/trainings/config/trainings.php | 16 ++ .../create_training_dateables_table.php.stub | 30 ++ .../create_training_dates_table.php.stub | 36 +++ ...te_training_invitationables_table.php.stub | 30 ++ ...create_training_invitations_table.php.stub | 32 +++ .../create_training_types_table.php.stub | 30 ++ .../create_trainingables_table.php.stub | 30 ++ .../create_trainings_table.php.stub | 52 ++++ ...igns_for_training_dateables_table.php.stub | 32 +++ ...foreigns_for_training_dates_table.php.stub | 32 +++ ...or_training_invitationables_table.php.stub | 32 +++ ...ns_for_training_invitations_table.php.stub | 32 +++ .../foreigns_for_trainingables_table.php.stub | 32 +++ .../foreigns_for_trainings_table.php.stub | 32 +++ .../resources/lang/de/translations.php | 16 ++ .../resources/lang/en/translations.php | 16 ++ .../resources/lang/es/translations.php | 16 ++ .../resources/lang/hr/translations.php | 16 ++ .../resources/lang/nb_NO/translations.php | 16 ++ .../resources/lang/ru/translations.php | 16 ++ .../views/emails/invitation-request.blade.php | 11 + .../views/emails/invitation.blade.php | 14 + .../trainings/src/Commands/InstallCommand.php | 163 +++++++++++ .../trainings/src/Filters/DateRangeFilter.php | 30 ++ .../src/Jobs/SendInvitationRequests.php | 116 ++++++++ .../trainings/src/Jobs/SendInvitations.php | 64 +++++ packages/trainings/src/Mail/Invitation.php | 43 +++ .../trainings/src/Mail/InvitationRequest.php | 43 +++ packages/trainings/src/Models/Training.php | 47 +++ .../trainings/src/Models/TrainingDate.php | 43 +++ .../src/Models/TrainingInvitation.php | 43 +++ .../trainings/src/Models/TrainingType.php | 22 ++ .../src/Resources/TrainingDateResource.php | 204 +++++++++++++ .../Pages/CreateTrainingDate.php | 11 + .../Pages/EditTrainingDate.php | 17 ++ .../Pages/ListTrainingDates.php | 22 ++ .../Pages/ViewTrainingDate.php | 18 ++ .../Resources/TrainingInvitationResource.php | 162 +++++++++++ .../Pages/CreateTrainingInvitation.php | 11 + .../Pages/EditTrainingInvitation.php | 55 ++++ .../Pages/ListTrainingInvitations.php | 33 +++ .../Pages/PrepareTrainingInvitation.php | 31 ++ .../Pages/ViewTrainingInvitation.php | 18 ++ .../TrainingDatesRelationManager.php | 176 ++++++++++++ .../src/Resources/TrainingResource.php | 268 ++++++++++++++++++ .../TrainingResource/Pages/CreateTraining.php | 11 + .../TrainingResource/Pages/EditTraining.php | 40 +++ .../TrainingResource/Pages/ListTrainings.php | 24 ++ .../TrainingResource/Pages/ViewTraining.php | 43 +++ .../TrainingInvitationsRelationManager.php | 120 ++++++++ .../src/Resources/TrainingTypeResource.php | 133 +++++++++ .../Pages/CreateTrainingType.php | 11 + .../Pages/EditTrainingType.php | 40 +++ .../Pages/ListTrainingTypes.php | 20 ++ .../Pages/ViewTrainingType.php | 43 +++ .../TrainingsRelationManager.php | 198 +++++++++++++ packages/trainings/src/TrainingDatePlugin.php | 35 +++ .../src/TrainingInvitationPlugin.php | 35 +++ packages/trainings/src/TrainingPlugin.php | 35 +++ .../trainings/src/TrainingServiceProvider.php | 37 +++ packages/trainings/src/TrainingTypePlugin.php | 35 +++ .../src/Traits/HasDescendingOrder.php | 13 + 70 files changed, 3281 insertions(+) create mode 100644 packages/trainings/.github/FUNDING.yml create mode 100644 packages/trainings/.gitignore create mode 100644 packages/trainings/CHANGELOG.md create mode 100644 packages/trainings/FUNDING.yml create mode 100644 packages/trainings/LICENSE.md create mode 100644 packages/trainings/README.md create mode 100644 packages/trainings/SECURITY.md create mode 100644 packages/trainings/composer.json create mode 100644 packages/trainings/config/trainings.php create mode 100644 packages/trainings/database/migrations/create_training_dateables_table.php.stub create mode 100644 packages/trainings/database/migrations/create_training_dates_table.php.stub create mode 100644 packages/trainings/database/migrations/create_training_invitationables_table.php.stub create mode 100644 packages/trainings/database/migrations/create_training_invitations_table.php.stub create mode 100644 packages/trainings/database/migrations/create_training_types_table.php.stub create mode 100644 packages/trainings/database/migrations/create_trainingables_table.php.stub create mode 100644 packages/trainings/database/migrations/create_trainings_table.php.stub create mode 100644 packages/trainings/database/migrations/foreigns_for_training_dateables_table.php.stub create mode 100644 packages/trainings/database/migrations/foreigns_for_training_dates_table.php.stub create mode 100644 packages/trainings/database/migrations/foreigns_for_training_invitationables_table.php.stub create mode 100644 packages/trainings/database/migrations/foreigns_for_training_invitations_table.php.stub create mode 100644 packages/trainings/database/migrations/foreigns_for_trainingables_table.php.stub create mode 100644 packages/trainings/database/migrations/foreigns_for_trainings_table.php.stub create mode 100644 packages/trainings/resources/lang/de/translations.php create mode 100644 packages/trainings/resources/lang/en/translations.php create mode 100644 packages/trainings/resources/lang/es/translations.php create mode 100644 packages/trainings/resources/lang/hr/translations.php create mode 100644 packages/trainings/resources/lang/nb_NO/translations.php create mode 100644 packages/trainings/resources/lang/ru/translations.php create mode 100644 packages/trainings/resources/views/emails/invitation-request.blade.php create mode 100644 packages/trainings/resources/views/emails/invitation.blade.php create mode 100644 packages/trainings/src/Commands/InstallCommand.php create mode 100644 packages/trainings/src/Filters/DateRangeFilter.php create mode 100644 packages/trainings/src/Jobs/SendInvitationRequests.php create mode 100644 packages/trainings/src/Jobs/SendInvitations.php create mode 100644 packages/trainings/src/Mail/Invitation.php create mode 100644 packages/trainings/src/Mail/InvitationRequest.php create mode 100644 packages/trainings/src/Models/Training.php create mode 100644 packages/trainings/src/Models/TrainingDate.php create mode 100644 packages/trainings/src/Models/TrainingInvitation.php create mode 100644 packages/trainings/src/Models/TrainingType.php create mode 100644 packages/trainings/src/Resources/TrainingDateResource.php create mode 100644 packages/trainings/src/Resources/TrainingDateResource/Pages/CreateTrainingDate.php create mode 100644 packages/trainings/src/Resources/TrainingDateResource/Pages/EditTrainingDate.php create mode 100644 packages/trainings/src/Resources/TrainingDateResource/Pages/ListTrainingDates.php create mode 100644 packages/trainings/src/Resources/TrainingDateResource/Pages/ViewTrainingDate.php create mode 100644 packages/trainings/src/Resources/TrainingInvitationResource.php create mode 100644 packages/trainings/src/Resources/TrainingInvitationResource/Pages/CreateTrainingInvitation.php create mode 100644 packages/trainings/src/Resources/TrainingInvitationResource/Pages/EditTrainingInvitation.php create mode 100644 packages/trainings/src/Resources/TrainingInvitationResource/Pages/ListTrainingInvitations.php create mode 100644 packages/trainings/src/Resources/TrainingInvitationResource/Pages/PrepareTrainingInvitation.php create mode 100644 packages/trainings/src/Resources/TrainingInvitationResource/Pages/ViewTrainingInvitation.php create mode 100644 packages/trainings/src/Resources/TrainingInvitationResource/RelationManagers/TrainingDatesRelationManager.php create mode 100644 packages/trainings/src/Resources/TrainingResource.php create mode 100644 packages/trainings/src/Resources/TrainingResource/Pages/CreateTraining.php create mode 100644 packages/trainings/src/Resources/TrainingResource/Pages/EditTraining.php create mode 100644 packages/trainings/src/Resources/TrainingResource/Pages/ListTrainings.php create mode 100644 packages/trainings/src/Resources/TrainingResource/Pages/ViewTraining.php create mode 100644 packages/trainings/src/Resources/TrainingResource/RelationManagers/TrainingInvitationsRelationManager.php create mode 100644 packages/trainings/src/Resources/TrainingTypeResource.php create mode 100644 packages/trainings/src/Resources/TrainingTypeResource/Pages/CreateTrainingType.php create mode 100644 packages/trainings/src/Resources/TrainingTypeResource/Pages/EditTrainingType.php create mode 100644 packages/trainings/src/Resources/TrainingTypeResource/Pages/ListTrainingTypes.php create mode 100644 packages/trainings/src/Resources/TrainingTypeResource/Pages/ViewTrainingType.php create mode 100644 packages/trainings/src/Resources/TrainingTypeResource/RelationManagers/TrainingsRelationManager.php create mode 100644 packages/trainings/src/TrainingDatePlugin.php create mode 100644 packages/trainings/src/TrainingInvitationPlugin.php create mode 100644 packages/trainings/src/TrainingPlugin.php create mode 100644 packages/trainings/src/TrainingServiceProvider.php create mode 100644 packages/trainings/src/TrainingTypePlugin.php create mode 100644 packages/trainings/src/Traits/HasDescendingOrder.php diff --git a/packages/trainings/.github/FUNDING.yml b/packages/trainings/.github/FUNDING.yml new file mode 100644 index 000000000..0446fa42d --- /dev/null +++ b/packages/trainings/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [mooxphp] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/packages/trainings/.gitignore b/packages/trainings/.gitignore new file mode 100644 index 000000000..6a81cbb00 --- /dev/null +++ b/packages/trainings/.gitignore @@ -0,0 +1,49 @@ +# Environment +.env +.env.backup + +# Composer +/vendor +composer.lock +auth.json + +# NPM / Node +/node_modules +npm-debug.log +package-lock.json + +# Laravel +/public/hot +/public/storage +/storage/*.key + +# PHPUnit +.phpunit.result.cache +phpunit.xml + +# Yarn +yarn-error.log + +# PHPStan +/build +phpstan.neon + +# Testbench +testbench.yaml + +# PHP CS Fixer +.php-cs-fixer.cache + +# Homestead +Homestead.json +Homestead.yaml + +# IDEs +/.idea +/.vscode + +# MacOS +.DS_Store + +# Windows +Thumbs.db diff --git a/packages/trainings/CHANGELOG.md b/packages/trainings/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/packages/trainings/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/packages/trainings/FUNDING.yml b/packages/trainings/FUNDING.yml new file mode 100644 index 000000000..0446fa42d --- /dev/null +++ b/packages/trainings/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [mooxphp] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/packages/trainings/LICENSE.md b/packages/trainings/LICENSE.md new file mode 100644 index 000000000..7dfc5ad0b --- /dev/null +++ b/packages/trainings/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Moox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/trainings/README.md b/packages/trainings/README.md new file mode 100644 index 000000000..c9250a0bc --- /dev/null +++ b/packages/trainings/README.md @@ -0,0 +1,53 @@ +![Moox Training](https://github.com/mooxphp/moox/raw/main/art/banner/trainings.jpg) + +# Moox Training + +This is my package trainings + +## Quick Installation + +These two commmands are all you need to install the package: + +```bash +composer require moox/trainings +php artisan mooxtrainings:install +``` + +Curious what the install command does? See manual installation below. + +## What it does + + + +Here are some things missing, like an overview with screenshots about this package, or simply a link to the package's docs. + + + +## Manual Installation + +Instead of using the install-command `php artisan mooxtrainings:install` you are able to install this package manually step by step: + +```bash +// Publish and run the migrations: +php artisan vendor:publish --tag="trainings-migrations" +php artisan migrate + +// Publish the config file with: +php artisan vendor:publish --tag="trainings-config" +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Security Vulnerabilities + +Please review [our security policy](https://github.com/mooxphp/moox/security/policy) on how to report security vulnerabilities. + +## Credits + +- [All Contributors](../../contributors) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/packages/trainings/SECURITY.md b/packages/trainings/SECURITY.md new file mode 100644 index 000000000..5c412e9a2 --- /dev/null +++ b/packages/trainings/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +We maintain the current version of `Moox Training` actively. + +Do not expect security fixes for older versions. + +## Reporting a Vulnerability + +If you find any security-related bug, please report it to security@moox.org. + +Please do not use Github issues, to give us enough time to review and fix the issue, before others can use it, to do stupid things. diff --git a/packages/trainings/composer.json b/packages/trainings/composer.json new file mode 100644 index 000000000..682144276 --- /dev/null +++ b/packages/trainings/composer.json @@ -0,0 +1,36 @@ +{ + "name": "moox/trainings", + "description": "This is my package trainings", + "keywords": [ + "Laravel", + "Filament", + "Filament plugin", + "Laravel package" + ], + "homepage": "https://moox.org/", + "license": "MIT", + "authors": [ + { + "name": "Moox Developer", + "email": "dev@moox.org", + "role": "Developer" + } + ], + "require": { + "moox/core": "*" + }, + "autoload": { + "psr-4": { + "Moox\\Training\\": "src" + } + }, + "extra": { + "laravel": { + "providers": [ + "Moox\\Training\\TrainingServiceProvider" + ] + } + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/packages/trainings/config/trainings.php b/packages/trainings/config/trainings.php new file mode 100644 index 000000000..ec9d8fb45 --- /dev/null +++ b/packages/trainings/config/trainings.php @@ -0,0 +1,16 @@ + 2001, + + // Wire with one or more user models + 'user_models' => [ + 'App Users' => \App\Models\User::class, + ], + + // Disable manual action buttons in UI + // and queue the provided jobs instead + 'create_trainings_action' => true, + 'training_invitations_collect_action' => true, + 'training_invitations_send_action' => true, +]; diff --git a/packages/trainings/database/migrations/create_training_dateables_table.php.stub b/packages/trainings/database/migrations/create_training_dateables_table.php.stub new file mode 100644 index 000000000..106ccad0b --- /dev/null +++ b/packages/trainings/database/migrations/create_training_dateables_table.php.stub @@ -0,0 +1,30 @@ +unsignedBigInteger('training_date_id'); + $table->unsignedBigInteger('training_dateable_id'); + $table->string('training_dateable_type'); + + $table->index('training_dateable_id'); + $table->index('training_dateable_type'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('training_dateables'); + } +}; diff --git a/packages/trainings/database/migrations/create_training_dates_table.php.stub b/packages/trainings/database/migrations/create_training_dates_table.php.stub new file mode 100644 index 000000000..c9a2c14e9 --- /dev/null +++ b/packages/trainings/database/migrations/create_training_dates_table.php.stub @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->unsignedBigInteger('training_invitation_id'); + $table->dateTime('begin'); + $table->dateTime('end'); + $table->set('type', ['onsite', 'teams', 'webex', 'slack', 'zoom']); + $table->string('link')->nullable(); + $table->string('location')->nullable(); + $table->integer('min_participants')->nullable(); + $table->integer('max_participants')->nullable(); + $table->dateTime('sent_at')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('training_dates'); + } +}; diff --git a/packages/trainings/database/migrations/create_training_invitationables_table.php.stub b/packages/trainings/database/migrations/create_training_invitationables_table.php.stub new file mode 100644 index 000000000..3d6c61273 --- /dev/null +++ b/packages/trainings/database/migrations/create_training_invitationables_table.php.stub @@ -0,0 +1,30 @@ +unsignedBigInteger('training_invitation_id'); + $table->unsignedBigInteger('training_invitationable_id'); + $table->string('training_invitationable_type'); + + $table->index('training_invitationable_id'); + $table->index('training_invitationable_type'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('training_invitationables'); + } +}; diff --git a/packages/trainings/database/migrations/create_training_invitations_table.php.stub b/packages/trainings/database/migrations/create_training_invitations_table.php.stub new file mode 100644 index 000000000..b8253adfa --- /dev/null +++ b/packages/trainings/database/migrations/create_training_invitations_table.php.stub @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->unsignedBigInteger('training_id'); + $table->string('title'); + $table->string('slug'); + $table->text('content')->nullable(); + $table->string('status'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('training_invitations'); + } +}; diff --git a/packages/trainings/database/migrations/create_training_types_table.php.stub b/packages/trainings/database/migrations/create_training_types_table.php.stub new file mode 100644 index 000000000..7cf08bb39 --- /dev/null +++ b/packages/trainings/database/migrations/create_training_types_table.php.stub @@ -0,0 +1,30 @@ +bigIncrements('id'); + $table->string('title'); + $table->string('slug'); + $table->text('description')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('training_types'); + } +}; diff --git a/packages/trainings/database/migrations/create_trainingables_table.php.stub b/packages/trainings/database/migrations/create_trainingables_table.php.stub new file mode 100644 index 000000000..1e576d8ee --- /dev/null +++ b/packages/trainings/database/migrations/create_trainingables_table.php.stub @@ -0,0 +1,30 @@ +unsignedBigInteger('training_id'); + $table->unsignedBigInteger('trainingable_id'); + $table->string('trainingable_type'); + + $table->index('trainingable_id'); + $table->index('trainingable_type'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('trainingables'); + } +}; diff --git a/packages/trainings/database/migrations/create_trainings_table.php.stub b/packages/trainings/database/migrations/create_trainings_table.php.stub new file mode 100644 index 000000000..48f3ed854 --- /dev/null +++ b/packages/trainings/database/migrations/create_trainings_table.php.stub @@ -0,0 +1,52 @@ +bigIncrements('id'); + $table->string('title'); + $table->string('slug'); + $table->text('description')->nullable(); + $table->integer('duration'); + $table->string('link'); + $table->dateTime('due_at'); + $table + ->set('cycle', [ + 'annually', + 'half-yearly', + 'quarterly', + 'monthly', + 'every 2 years', + 'every 3 years', + 'every 4 years', + 'every 5 years', + ]) + ->default('annually'); + $table->foreignId('source_id'); + $table->unsignedBigInteger('training_type_id'); + $table->unsignedBigInteger('trainingable_id'); + $table->string('trainingable_type'); + + $table->index('trainingable_id'); + $table->index('trainingable_type'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('trainings'); + } +}; diff --git a/packages/trainings/database/migrations/foreigns_for_training_dateables_table.php.stub b/packages/trainings/database/migrations/foreigns_for_training_dateables_table.php.stub new file mode 100644 index 000000000..1eae02ff0 --- /dev/null +++ b/packages/trainings/database/migrations/foreigns_for_training_dateables_table.php.stub @@ -0,0 +1,32 @@ +foreign('training_date_id') + ->references('id') + ->on('training_dates') + ->onUpdate('CASCADE') + ->onDelete('CASCADE'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('training_dateables', function (Blueprint $table) { + $table->dropForeign(['training_date_id']); + }); + } +}; diff --git a/packages/trainings/database/migrations/foreigns_for_training_dates_table.php.stub b/packages/trainings/database/migrations/foreigns_for_training_dates_table.php.stub new file mode 100644 index 000000000..b7f30f14d --- /dev/null +++ b/packages/trainings/database/migrations/foreigns_for_training_dates_table.php.stub @@ -0,0 +1,32 @@ +foreign('training_invitation_id') + ->references('id') + ->on('training_invitations') + ->onUpdate('CASCADE') + ->onDelete('CASCADE'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('training_dates', function (Blueprint $table) { + $table->dropForeign(['training_invitation_id']); + }); + } +}; diff --git a/packages/trainings/database/migrations/foreigns_for_training_invitationables_table.php.stub b/packages/trainings/database/migrations/foreigns_for_training_invitationables_table.php.stub new file mode 100644 index 000000000..746caa4f5 --- /dev/null +++ b/packages/trainings/database/migrations/foreigns_for_training_invitationables_table.php.stub @@ -0,0 +1,32 @@ +foreign('training_invitation_id') + ->references('id') + ->on('training_invitations') + ->onUpdate('CASCADE') + ->onDelete('CASCADE'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('training_invitationables', function (Blueprint $table) { + $table->dropForeign(['training_invitation_id']); + }); + } +}; diff --git a/packages/trainings/database/migrations/foreigns_for_training_invitations_table.php.stub b/packages/trainings/database/migrations/foreigns_for_training_invitations_table.php.stub new file mode 100644 index 000000000..e48276ea3 --- /dev/null +++ b/packages/trainings/database/migrations/foreigns_for_training_invitations_table.php.stub @@ -0,0 +1,32 @@ +foreign('training_id') + ->references('id') + ->on('trainings') + ->onUpdate('CASCADE') + ->onDelete('CASCADE'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('training_invitations', function (Blueprint $table) { + $table->dropForeign(['training_id']); + }); + } +}; diff --git a/packages/trainings/database/migrations/foreigns_for_trainingables_table.php.stub b/packages/trainings/database/migrations/foreigns_for_trainingables_table.php.stub new file mode 100644 index 000000000..a3e07e2c6 --- /dev/null +++ b/packages/trainings/database/migrations/foreigns_for_trainingables_table.php.stub @@ -0,0 +1,32 @@ +foreign('training_id') + ->references('id') + ->on('trainings') + ->onUpdate('CASCADE') + ->onDelete('CASCADE'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('trainingables', function (Blueprint $table) { + $table->dropForeign(['training_id']); + }); + } +}; diff --git a/packages/trainings/database/migrations/foreigns_for_trainings_table.php.stub b/packages/trainings/database/migrations/foreigns_for_trainings_table.php.stub new file mode 100644 index 000000000..0a38d5bd5 --- /dev/null +++ b/packages/trainings/database/migrations/foreigns_for_trainings_table.php.stub @@ -0,0 +1,32 @@ +foreign('training_type_id') + ->references('id') + ->on('training_types') + ->onUpdate('CASCADE') + ->onDelete('RESTRICT'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('trainings', function (Blueprint $table) { + $table->dropForeign(['training_type_id']); + }); + } +}; diff --git a/packages/trainings/resources/lang/de/translations.php b/packages/trainings/resources/lang/de/translations.php new file mode 100644 index 000000000..338323f44 --- /dev/null +++ b/packages/trainings/resources/lang/de/translations.php @@ -0,0 +1,16 @@ + 'Training', + 'plural' => 'Trainings', + 'breadcrumb' => 'Training', + 'title' => 'Training', + 'navigation_label' => 'Training', + 'navigation_group' => 'Moox Training', + 'totalone' => 'Training Eins', + 'totaltwo' => 'Training Zwei', + 'totalthree' => 'Training Drei', + 'name' => 'Name', + 'started_at' => 'Gestartet am', + 'failed' => 'failed', +]; diff --git a/packages/trainings/resources/lang/en/translations.php b/packages/trainings/resources/lang/en/translations.php new file mode 100644 index 000000000..ca1e56ff1 --- /dev/null +++ b/packages/trainings/resources/lang/en/translations.php @@ -0,0 +1,16 @@ + 'Training', + 'plural' => 'Trainings', + 'breadcrumb' => 'Training', + 'title' => 'Training', + 'navigation_label' => 'Training', + 'navigation_group' => 'Moox Training', + 'totalone' => 'Training One', + 'totaltwo' => 'Training Two', + 'totalthree' => 'Training Three', + 'name' => 'Name', + 'started_at' => 'Started at', + 'failed' => 'failed', +]; diff --git a/packages/trainings/resources/lang/es/translations.php b/packages/trainings/resources/lang/es/translations.php new file mode 100644 index 000000000..e7e6673f7 --- /dev/null +++ b/packages/trainings/resources/lang/es/translations.php @@ -0,0 +1,16 @@ + 'Desarrollador', + 'plural' => 'Desarrolladores', + 'breadcrumb' => 'Desarrollador', + 'title' => 'Desarrollador', + 'navigation_label' => 'Desarrollador', + 'navigation_group' => 'Desarrollador de Moox', + 'totalone' => 'Desarrollador uno', + 'totaltwo' => 'Desarrollador dos', + 'totalthree' => 'Desarrollador tres', + 'name' => 'Nombre', + 'started_at' => 'Empezó a las', + 'failed' => 'failed', +]; diff --git a/packages/trainings/resources/lang/hr/translations.php b/packages/trainings/resources/lang/hr/translations.php new file mode 100644 index 000000000..a145e7504 --- /dev/null +++ b/packages/trainings/resources/lang/hr/translations.php @@ -0,0 +1,16 @@ + '', + 'plural' => '', + 'breadcrumb' => '', + 'title' => '', + 'navigation_label' => '', + 'navigation_group' => '', + 'totalone' => '', + 'totaltwo' => '', + 'totalthree' => '', + 'name' => 'Ime', + 'started_at' => 'Pokrenuto', + 'failed' => 'neuspjelo', +]; diff --git a/packages/trainings/resources/lang/nb_NO/translations.php b/packages/trainings/resources/lang/nb_NO/translations.php new file mode 100644 index 000000000..986562940 --- /dev/null +++ b/packages/trainings/resources/lang/nb_NO/translations.php @@ -0,0 +1,16 @@ + 'Bygger', + 'plural' => 'Byggers', + 'breadcrumb' => 'Bygger', + 'title' => 'Bygger', + 'navigation_label' => 'Bygger', + 'navigation_group' => 'Moox Training', + 'totalone' => 'Bygger én', + 'totaltwo' => 'Bygger to', + 'totalthree' => 'Bygger tre', + 'name' => 'Navn', + 'started_at' => 'Startet', + 'failed' => 'mislyktes', +]; diff --git a/packages/trainings/resources/lang/ru/translations.php b/packages/trainings/resources/lang/ru/translations.php new file mode 100644 index 000000000..38cdbd13b --- /dev/null +++ b/packages/trainings/resources/lang/ru/translations.php @@ -0,0 +1,16 @@ + 'Сборщик', + 'plural' => 'Сборщики', + 'breadcrumb' => 'Сборщик', + 'title' => 'Сборщик', + 'navigation_label' => 'Сборщик', + 'navigation_group' => 'Сборщик Moox', + 'totalone' => 'Первый сборщик', + 'totaltwo' => 'Второй Сборщик', + 'totalthree' => 'Третий Сборщик', + 'name' => 'Имя', + 'started_at' => 'Начато в', + 'failed' => 'Неудача', +]; diff --git a/packages/trainings/resources/views/emails/invitation-request.blade.php b/packages/trainings/resources/views/emails/invitation-request.blade.php new file mode 100644 index 000000000..3e2a1836e --- /dev/null +++ b/packages/trainings/resources/views/emails/invitation-request.blade.php @@ -0,0 +1,11 @@ + + + + Invitation Request + + +

Invitation Request for {{ $invitationRequest->title }}

+

Invitation request has been received for the training with ID: {{ $invitationRequest->id }}

+

Click here to prepare the training.

+ + diff --git a/packages/trainings/resources/views/emails/invitation.blade.php b/packages/trainings/resources/views/emails/invitation.blade.php new file mode 100644 index 000000000..17179b548 --- /dev/null +++ b/packages/trainings/resources/views/emails/invitation.blade.php @@ -0,0 +1,14 @@ + + + + Invitation + + +

Invitation for ...

+ + @foreach ($trainingDates as $trainingDate) +

Datum: {{ $trainingDate->date }}

+ @endforeach + + + diff --git a/packages/trainings/src/Commands/InstallCommand.php b/packages/trainings/src/Commands/InstallCommand.php new file mode 100644 index 000000000..d2a259efd --- /dev/null +++ b/packages/trainings/src/Commands/InstallCommand.php @@ -0,0 +1,163 @@ +art(); + $this->welcome(); + $this->publishConfiguration(); + $this->publishMigrations(); + $this->runMigrations(); + $this->registerPlugins(); + $this->finish(); + } + + public function art(): void + { + info(' + + ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ + ▓▓▒░░▒▓▓▒▒░░░░░░▒▒▓▓▓▒░░░░░░░▒▓▓ ▓▓▓▓▒░░░░░░░▒▓▓▓▓ ▓▓▓▓▓▒░░░░░░░▒▒▓▓▓▓▓▒▒▒▒▓▓ ▓▓▓▒▒▒▒▓▓ + ▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓ ▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓░░░░░▒▓▓ ▓▓▒░░░░░▓▓ + ▓▒░░░░░░▒▓▓▓▓▒░░░░░░░▒▓▓▓▓░░░░░▒▓▓▓░░░░░▒▓▓▓▓▒░░░░░░░▓▓▓▓░░░░░░▒▓▓▓▓▓░░░░░░▒▓▓░░░░░▒▓▓▓▓▓░░░░░▒▓▓ + ▓▒░░░░▓▓▓▓ ▓▓░░░░░▓▓▓ ▓▓▓░░░░▒▓▓░░░░▒▓▓▓ ▓▓▓▓░░░░░▓░░░░░░▓▓▓▓ ▓▓▓▒░░░░▓▓▓▒░░░░░▓▓▓░░░░░▓▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓▓ ▓▓▓░░▒░░░░░▓▓▓ ▓▓░░░░▒▓▓▓▓░░░░░░░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓ ▓▓▓░░░░░▒▓▓ ▓▓▒░░░░▓ ▓▓▓░░░░░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓▓ ▓▓▒░░░░░▒░░▒▓▓ ▓▓░░░░▒▓▓▓▒░░░░░▒░░░░░▒▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓░░░░▒▓▓▓ ▓▓▓▒░░░░░▒▒░░░░░▒▓▓▓ ▓▓▓░░░░░▓▓▓░░░░░▒▓▓▓░░░░░▒▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓▓░░░░░░▒▒▓▓▒░░░░░░▒▓▓▓▓░░░░░░░▒▒▓▓▒░░░░░░▓▓▓░░░░░▒▓▓▓▓▓▒░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓ ▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▒░░░░░▓▓▓ ▓▓▒░░░░░▒▓ + ▓▓░░░▒▓▓ ▓▓▒░░░▒▓▓ ▓▓░░░░▓▓ ▓▓▓▓▒░░░░░░▒▒▓▓▓▓ ▓▓▓▓▓▒▒░░░░░▒▒▓▓▓▓▓░░░░▒▓▓ ▓▓▓░░░░▒▓ + ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ + + '); + } + + public function welcome(): void + { + info('Welcome to Moox Training Installer'); + } + + public function publishConfiguration(): void + { + if (confirm('Do you wish to publish the configuration?', true)) { + if (! File::exists('config/trainings.php')) { + info('Publishing Training Configuration...'); + $this->callSilent('vendor:publish', ['--tag' => 'trainings-config']); + + return; + } + warning('The Training config already exist. The config will not be published.'); + } + } + + public function publishMigrations(): void + { + if (confirm('Do you wish to publish the migrations?', true)) { + if (Schema::hasTable('trainings')) { + warning('The trainings table already exists. The migrations will not be published.'); + + return; + } + info('Publishing Trainings Migrations...'); + $this->callSilent('vendor:publish', ['--tag' => 'trainings-migrations']); + } + } + + public function runMigrations(): void + { + if (confirm('Do you wish to run the migrations?', true)) { + info('Running Training Migrations...'); + $this->callSilent('migrate'); + } + } + + public function registerPlugins(): void + { + $providerPath = app_path('Providers/Filament/AdminPanelProvider.php'); + + if (File::exists($providerPath)) { + $content = File::get($providerPath); + + $intend = ' '; + + $namespace = "\Moox\Training"; + + $pluginsToAdd = multiselect( + label: 'These plugins will be installed:', + options: ['TrainingPlugin', 'TrainingInvitationPlugin', 'TrainingDatePlugin', 'TrainingTypePlugin'], + default: ['TrainingPlugin', 'TrainingInvitationPlugin', 'TrainingDatePlugin', 'TrainingTypePlugin'], + ); + + $function = '::make(),'; + + $pattern = '/->plugins\(\[([\s\S]*?)\]\);/'; + $newPlugins = ''; + + foreach ($pluginsToAdd as $plugin) { + $searchPlugin = '/'.$plugin.'/'; + if (preg_match($searchPlugin, $content)) { + warning("$plugin already registered."); + } else { + $newPlugins .= $intend.$namespace.'\\'.$plugin.$function."\n"; + } + } + + if ($newPlugins) { + if (preg_match($pattern, $content)) { + info('Plugins section found. Adding new plugins...'); + + $replacement = "->plugins([$1\n$newPlugins\n ]);"; + $newContent = preg_replace($pattern, $replacement, $content); + } else { + info('Plugins section created. Adding new plugins...'); + + $pluginsSection = " ->plugins([\n$newPlugins\n ]);"; + $placeholderPattern = '/(\->authMiddleware\(\[.*?\]\))\s*\;/s'; + $replacement = "$1\n".$pluginsSection; + $newContent = preg_replace($placeholderPattern, $replacement, $content, 1); + } + + File::put($providerPath, $newContent); + } + } else { + alert('AdminPanelProvider not found. You need to add the plugins manually.'); + } + } + + public function finish(): void + { + note('Moox Training installed successfully. Enjoy!'); + } +} diff --git a/packages/trainings/src/Filters/DateRangeFilter.php b/packages/trainings/src/Filters/DateRangeFilter.php new file mode 100644 index 000000000..e0d4f7334 --- /dev/null +++ b/packages/trainings/src/Filters/DateRangeFilter.php @@ -0,0 +1,30 @@ +form([ + DatePicker::make("{$name}_from"), + DatePicker::make("{$name}_until"), + ]) + ->query(function (Builder $query, array $data) use (&$name): Builder { + return $query + ->when( + $data["{$name}_from"], + fn (Builder $query, $date): Builder => $query->whereDate($name, '>=', $date), + ) + ->when( + $data["{$name}_until"], + fn (Builder $query, $date): Builder => $query->whereDate($name, '<=', $date), + ); + }); + } +} diff --git a/packages/trainings/src/Jobs/SendInvitationRequests.php b/packages/trainings/src/Jobs/SendInvitationRequests.php new file mode 100644 index 000000000..6eed135e4 --- /dev/null +++ b/packages/trainings/src/Jobs/SendInvitationRequests.php @@ -0,0 +1,116 @@ +tries = 3; + $this->timeout = 300; + $this->maxExceptions = 1; + $this->backoff = 350; + } + + public function handle() + { + $this->setProgress(1); + + $invitationRequests = Training::where('due_at', '<', now()) + ->get() + ->map(function ($training) { + return TrainingInvitation::create([ + 'training_id' => $training->id, + 'title' => $training->title, + 'slug' => Str::slug($training->title), + 'content' => $training->description, + 'status' => 'new', + // 'user_id' => $training->users->first()->id, + ]); + }); + + $this->setProgress(10); + + foreach ($invitationRequests as $invitationRequest) { + + $training = Training::find($invitationRequest->training_id); + + $cycle = $training->cycle; + + $dueAt = $training->due_at; + + switch ($cycle) { + case 'annually': + $dueAt->addYear(); + break; + case 'half-yearly': + $dueAt->addMonths(6); + break; + case 'quarterly': + $dueAt->addMonths(3); + break; + case 'monthly': + $dueAt->addMonth(); + break; + case 'every 2 years': + $dueAt->addYears(2); + break; + case 'every 3 years': + $dueAt->addYears(3); + break; + case 'every 4 years': + $dueAt->addYears(4); + break; + case 'every 5 years': + $dueAt->addYears(5); + break; + } + + $training->due_at = $dueAt; + $training->save(); + + $this->setProgress(30); + + // $userEmail = $this->getUserEmailById($invitationRequest->user_id); + + $userEmail = 'alf@drollinger.info'; + + Mail::to($userEmail)->send(new InvitationRequest($invitationRequest)); + } + + $this->setProgress(100); + } + + protected function getUserEmailById($userId) + { + $user = WpUser::find($userId); + if ($user) { + return $user->user_email; + } + + return null; + } +} diff --git a/packages/trainings/src/Jobs/SendInvitations.php b/packages/trainings/src/Jobs/SendInvitations.php new file mode 100644 index 000000000..6a8cef474 --- /dev/null +++ b/packages/trainings/src/Jobs/SendInvitations.php @@ -0,0 +1,64 @@ +tries = 3; + $this->timeout = 300; + $this->maxExceptions = 1; + $this->backoff = 350; + $this->invitationId = $invitationId; + } + + public function handle() + { + $this->setProgress(1); + + $trainingDates = []; + + // + $invitation = TrainingInvitation::find($this->invitationId); + + $invitation->trainingDates() + ->whereNull('sent_at') + ->get() + ->each(function ($trainingDate) { + $trainingDates[] = $trainingDate; + $trainingDate->update(['sent_at' => now()]); + }); + + $email = 'alf@drollinger.info'; + + Log::info('Sending invitation to '.$email.' for '.count($trainingDates).' training dates'); + + Mail::to($email)->send(new Invitation($trainingDates)); + + $this->setProgress(100); + } +} diff --git a/packages/trainings/src/Mail/Invitation.php b/packages/trainings/src/Mail/Invitation.php new file mode 100644 index 000000000..e0384259c --- /dev/null +++ b/packages/trainings/src/Mail/Invitation.php @@ -0,0 +1,43 @@ +trainingDates = $trainingDates; + } + + public function envelope(): Envelope + { + return new Envelope( + subject: 'Invitation', + ); + } + + public function content(): Content + { + return new Content( + view: 'trainings::emails.invitation', + with: [ + 'trainingDates' => $this->trainingDates, + ], + ); + } + + public function attachments(): array + { + return []; + } +} diff --git a/packages/trainings/src/Mail/InvitationRequest.php b/packages/trainings/src/Mail/InvitationRequest.php new file mode 100644 index 000000000..88407442e --- /dev/null +++ b/packages/trainings/src/Mail/InvitationRequest.php @@ -0,0 +1,43 @@ +invitationRequest = $invitationRequest; + } + + public function envelope(): Envelope + { + return new Envelope( + subject: 'Invitation Request', + ); + } + + public function content(): Content + { + return new Content( + view: 'trainings::emails.invitation-request', + with: [ + 'invitationId' => $this->invitationRequest, + ], + ); + } + + public function attachments(): array + { + return []; + } +} diff --git a/packages/trainings/src/Models/Training.php b/packages/trainings/src/Models/Training.php new file mode 100644 index 000000000..f6c22fd32 --- /dev/null +++ b/packages/trainings/src/Models/Training.php @@ -0,0 +1,47 @@ + 'datetime', + ]; + + public function trainingInvitations() + { + return $this->hasMany(TrainingInvitation::class); + } + + public function trainingType() + { + return $this->belongsTo(TrainingType::class); + } + + public function users() + { + return $this->morphedByMany(WpUser::class, 'trainingable'); + } +} diff --git a/packages/trainings/src/Models/TrainingDate.php b/packages/trainings/src/Models/TrainingDate.php new file mode 100644 index 000000000..cb0bb7a0b --- /dev/null +++ b/packages/trainings/src/Models/TrainingDate.php @@ -0,0 +1,43 @@ + 'datetime', + 'end' => 'datetime', + ]; + + public function trainingInvitation() + { + return $this->belongsTo(TrainingInvitation::class); + } + + public function users() + { + return $this->morphedByMany(WpUser::class, 'training_dateable'); + } +} diff --git a/packages/trainings/src/Models/TrainingInvitation.php b/packages/trainings/src/Models/TrainingInvitation.php new file mode 100644 index 000000000..eafd3c7c4 --- /dev/null +++ b/packages/trainings/src/Models/TrainingInvitation.php @@ -0,0 +1,43 @@ + 'datetime', + ]; + + public function training() + { + return $this->belongsTo(Training::class); + } + + public function trainingDates() + { + return $this->hasMany(TrainingDate::class); + } + + public function users() + { + return $this->morphedByMany(WpUser::class, 'training_invitationable'); + } +} diff --git a/packages/trainings/src/Models/TrainingType.php b/packages/trainings/src/Models/TrainingType.php new file mode 100644 index 000000000..f725e75fd --- /dev/null +++ b/packages/trainings/src/Models/TrainingType.php @@ -0,0 +1,22 @@ +hasMany(Training::class); + } +} diff --git a/packages/trainings/src/Resources/TrainingDateResource.php b/packages/trainings/src/Resources/TrainingDateResource.php new file mode 100644 index 000000000..6c516d18a --- /dev/null +++ b/packages/trainings/src/Resources/TrainingDateResource.php @@ -0,0 +1,204 @@ +schema([ + Section::make()->schema([ + Grid::make(['default' => 0])->schema([ + Select::make('training_invitation_id') + ->rules(['exists:training_invitations,id']) + ->required() + ->relationship('trainingInvitation', 'title') + ->searchable() + ->placeholder('Training Invitation') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + DateTimePicker::make('begin') + ->rules(['date']) + ->required() + ->placeholder('Begin') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + DateTimePicker::make('end') + ->rules(['date']) + ->required() + ->placeholder('End') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + Select::make('type') + ->rules(['in:onsite,teams,webex,slack,zoom']) + ->required() + ->searchable() + ->options([ + 'onsite' => 'Onsite', + 'teams' => 'Teams', + 'webex' => 'Webex', + 'slack' => 'Slack', + 'zoom' => 'Zoom', + ]) + ->placeholder('Type') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('link') + ->rules(['max:255', 'string']) + ->nullable() + ->placeholder('Link') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('location') + ->rules(['max:255', 'string']) + ->nullable() + ->placeholder('Location') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('min_participants') + ->rules(['numeric']) + ->nullable() + ->numeric() + ->placeholder('Min Participants') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('max_participants') + ->rules(['numeric']) + ->nullable() + ->numeric() + ->placeholder('Max Participants') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + DateTimePicker::make('sent_at') + ->rules(['date']) + ->nullable() + ->placeholder('Sent At') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + ]), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->poll('60s') + ->columns([ + Tables\Columns\TextColumn::make('trainingInvitation.title') + ->toggleable() + ->limit(50), + Tables\Columns\TextColumn::make('begin') + ->toggleable() + ->dateTime(), + Tables\Columns\TextColumn::make('end') + ->toggleable() + ->dateTime(), + Tables\Columns\TextColumn::make('type') + ->toggleable() + ->searchable(), + Tables\Columns\TextColumn::make('link') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + Tables\Columns\TextColumn::make('location') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + Tables\Columns\TextColumn::make('min_participants') + ->toggleable() + ->searchable(true, null, true), + Tables\Columns\TextColumn::make('max_participants') + ->toggleable() + ->searchable(true, null, true), + Tables\Columns\TextColumn::make('sent_at') + ->toggleable() + ->dateTime(), + ]) + ->filters([ + DateRangeFilter::make('created_at'), + + SelectFilter::make('training_invitation_id') + ->relationship('trainingInvitation', 'title') + ->indicator('TrainingInvitation') + ->multiple() + ->label('TrainingInvitation'), + ]) + ->actions([ViewAction::make(), EditAction::make()]) + ->bulkActions([DeleteBulkAction::make()]); + } + + public static function getRelations(): array + { + return []; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListTrainingDates::route('/'), + 'create' => Pages\CreateTrainingDate::route('/create'), + 'view' => Pages\ViewTrainingDate::route('/{record}'), + 'edit' => Pages\EditTrainingDate::route('/{record}/edit'), + ]; + } +} diff --git a/packages/trainings/src/Resources/TrainingDateResource/Pages/CreateTrainingDate.php b/packages/trainings/src/Resources/TrainingDateResource/Pages/CreateTrainingDate.php new file mode 100644 index 000000000..d79b14976 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingDateResource/Pages/CreateTrainingDate.php @@ -0,0 +1,11 @@ +schema([ + Section::make()->schema([ + Grid::make(['default' => 0])->schema([ + Select::make('training_id') + ->rules(['exists:trainings,id']) + ->required() + ->relationship('training', 'title') + ->searchable() + ->placeholder('Training') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('title') + ->rules(['max:255', 'string']) + ->required() + ->placeholder('Title') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('slug') + ->rules(['max:255', 'string']) + ->required() + ->placeholder('Slug') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + RichEditor::make('content') + ->rules(['max:255', 'string']) + ->nullable() + ->placeholder('Content') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + ]), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->poll('60s') + ->columns([ + Tables\Columns\TextColumn::make('training.title') + ->toggleable() + ->limit(50), + Tables\Columns\TextColumn::make('title') + ->toggleable() + ->searchable() + ->limit(50), + Tables\Columns\TextColumn::make('slug') + ->toggleable() + ->searchable() + ->limit(50), + Tables\Columns\TextColumn::make('content') + ->toggleable() + ->searchable() + ->limit(50), + Tables\Columns\TextColumn::make('status') + ->toggleable() + ->searchable() + ->limit(50), + ]) + ->filters([ + DateRangeFilter::make('created_at'), + + SelectFilter::make('training_id') + ->relationship('training', 'title') + ->indicator('Training') + ->multiple() + ->label('Training'), + ]) + ->actions([EditAction::make()]) + ->bulkActions([ + Tables\Actions\DeleteBulkAction::make() + ->action(function ($records, Tables\Actions\DeleteBulkAction $action) { + foreach ($records as $record) { + try { + $record->delete(); + Notification::make() + ->title('Training Invitations Deleted') + ->body('The invitations were deleted successfully.') + ->success() + ->send(); + } catch (QueryException $exception) { + if ($exception->getCode() === '23000') { + Notification::make() + ->title('Cannot Delete Training Invitations') + ->body('One or more invitations have associated training dates and cannot be deleted.') + ->danger() + ->send(); + } else { + throw $exception; + } + } + } + }), + ]); + } + + public static function getRelations(): array + { + return [ + TrainingInvitationResource\RelationManagers\TrainingDatesRelationManager::class, + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListTrainingInvitations::route('/'), + 'create' => Pages\CreateTrainingInvitation::route('/create'), + 'view' => Pages\ViewTrainingInvitation::route('/{record}'), + 'edit' => Pages\EditTrainingInvitation::route('/{record}/edit'), + 'prepare' => Pages\PrepareTrainingInvitation::route('/{record}/prepare'), + ]; + } +} diff --git a/packages/trainings/src/Resources/TrainingInvitationResource/Pages/CreateTrainingInvitation.php b/packages/trainings/src/Resources/TrainingInvitationResource/Pages/CreateTrainingInvitation.php new file mode 100644 index 000000000..77d0fbe7b --- /dev/null +++ b/packages/trainings/src/Resources/TrainingInvitationResource/Pages/CreateTrainingInvitation.php @@ -0,0 +1,11 @@ +label('Send Invitations') + ->action(function () { + SendInvitations::dispatch($this->record->getKey()); + }) + ->requiresConfirmation() + ->color('primary'); + } + + $actions[] = DeleteAction::make() + ->action(function ($record) { + try { + $record->delete(); + Notification::make() + ->title('Training Deleted') + ->body('The training was deleted successfully.') + ->success() + ->send(); + } catch (QueryException $exception) { + if ($exception->getCode() === '23000') { + Notification::make() + ->title('Cannot Delete Training') + ->body('The training has associated invitations and cannot be deleted.') + ->danger() + ->send(); + } else { + throw $exception; + } + } + }); + + return $actions; + } +} diff --git a/packages/trainings/src/Resources/TrainingInvitationResource/Pages/ListTrainingInvitations.php b/packages/trainings/src/Resources/TrainingInvitationResource/Pages/ListTrainingInvitations.php new file mode 100644 index 000000000..bb63ff03d --- /dev/null +++ b/packages/trainings/src/Resources/TrainingInvitationResource/Pages/ListTrainingInvitations.php @@ -0,0 +1,33 @@ +label('Collect Invitations') + ->action(function () { + SendInvitationRequests::dispatch(); + }) + ->requiresConfirmation() + ->color('primary'), + ]; + } +} diff --git a/packages/trainings/src/Resources/TrainingInvitationResource/Pages/PrepareTrainingInvitation.php b/packages/trainings/src/Resources/TrainingInvitationResource/Pages/PrepareTrainingInvitation.php new file mode 100644 index 000000000..70b070350 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingInvitationResource/Pages/PrepareTrainingInvitation.php @@ -0,0 +1,31 @@ +label('Send Invitations') + ->action(function () { + // Send invitation job + }), + DeleteAction::make(), + ]; + } + + public function form(Form $form): Form + { + return parent::form($form); + } +} diff --git a/packages/trainings/src/Resources/TrainingInvitationResource/Pages/ViewTrainingInvitation.php b/packages/trainings/src/Resources/TrainingInvitationResource/Pages/ViewTrainingInvitation.php new file mode 100644 index 000000000..a89a6bfb5 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingInvitationResource/Pages/ViewTrainingInvitation.php @@ -0,0 +1,18 @@ +schema([ + Grid::make(['default' => 0])->schema([ + DateTimePicker::make('begin') + ->rules(['date']) + ->placeholder('Begin') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + DateTimePicker::make('end') + ->rules(['date']) + ->placeholder('End') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + Select::make('type') + ->rules(['in:onsite,teams,webex,slack,zoom']) + ->searchable() + ->options([ + 'onsite' => 'Onsite', + 'teams' => 'Teams', + 'webex' => 'Webex', + 'slack' => 'Slack', + 'zoom' => 'Zoom', + ]) + ->placeholder('Type') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('link') + ->rules(['max:255', 'string']) + ->placeholder('Link') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('location') + ->rules(['max:255', 'string']) + ->placeholder('Location') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('min_participants') + ->rules(['numeric']) + ->numeric() + ->placeholder('Min Participants') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('max_participants') + ->rules(['numeric']) + ->numeric() + ->placeholder('Max Participants') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + DateTimePicker::make('sent_at') + ->rules(['date']) + ->nullable() + ->placeholder('Sent At') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + ]), + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make( + 'trainingInvitation.title' + )->limit(50), + TextColumn::make('begin')->dateTime(), + TextColumn::make('end')->dateTime(), + TextColumn::make('type'), + TextColumn::make('link')->limit(50), + TextColumn::make('location')->limit(50), + TextColumn::make('min_participants'), + TextColumn::make('max_participants'), + TextColumn::make('sent_at')->dateTime(), + ]) + ->filters([ + Filter::make('created_at') + ->form([ + DatePicker::make('created_from'), + DatePicker::make('created_until'), + ]) + ->query(function (Builder $query, array $data): Builder { + return $query + ->when( + $data['created_from'], + fn ( + Builder $query, + $date + ): Builder => $query->whereDate( + 'created_at', + '>=', + $date + ) + ) + ->when( + $data['created_until'], + fn ( + Builder $query, + $date + ): Builder => $query->whereDate( + 'created_at', + '<=', + $date + ) + ); + }), + + SelectFilter::make('training_invitation_id') + ->multiple() + ->relationship('trainingInvitation', 'title'), + ]) + ->headerActions([ + CreateAction::make(), + ]) + ->actions([EditAction::make(), DeleteAction::make()]) + ->bulkActions([DeleteBulkAction::make()]); + } +} diff --git a/packages/trainings/src/Resources/TrainingResource.php b/packages/trainings/src/Resources/TrainingResource.php new file mode 100644 index 000000000..7bb690038 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingResource.php @@ -0,0 +1,268 @@ +schema([ + Section::make()->schema([ + Grid::make(['default' => 0])->schema([ + TextInput::make('title') + ->rules(['max:255', 'string']) + ->required() + ->placeholder('Title') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('slug') + ->rules(['max:255', 'string']) + ->required() + ->placeholder('Slug') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + RichEditor::make('description') + ->rules(['max:255', 'string']) + ->nullable() + ->placeholder('Description') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('duration') + ->rules(['numeric']) + ->required() + ->numeric() + ->placeholder('Duration') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('link') + ->rules(['max:255', 'string']) + ->required() + ->placeholder('Link') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + DateTimePicker::make('due_at') + ->rules(['date']) + ->required() + ->placeholder('Due At') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + Select::make('cycle') + ->rules([ + 'in:annually,half-yearly,quarterly,monthly,every 2 years,every 3 years,every 4 years,every 5 years', + ]) + ->required() + ->searchable() + ->options([ + 'annually' => 'Annually', + 'half-yearly' => 'Half yearly', + 'quarterly' => 'Quarterly', + 'monthly' => 'Monthly', + 'every 2 years' => 'Every 2 years', + 'every 3 years' => 'Every 3 years', + 'every 4 years' => 'Every 4 years', + 'every 5 years' => 'Every 5 years', + ]) + ->placeholder('Cycle') + ->default('annually') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('source_id') + ->rules(['max:255']) + ->required() + ->placeholder('Source Id') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + Select::make('training_type_id') + ->rules(['exists:training_types,id']) + ->required() + ->relationship('trainingType', 'title') + ->placeholder('Training Type') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('trainingable_id') + ->rules(['max:255']) + ->required() + ->placeholder('Trainingable Id') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('trainingable_type') + ->rules(['max:255', 'string']) + ->required() + ->placeholder('Trainingable Type') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + ]), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->poll('60s') + ->columns([ + Tables\Columns\TextColumn::make('title') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + Tables\Columns\TextColumn::make('slug') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + Tables\Columns\TextColumn::make('description') + ->toggleable() + ->searchable() + ->limit(50), + Tables\Columns\TextColumn::make('duration') + ->toggleable() + ->searchable(true, null, true), + Tables\Columns\TextColumn::make('link') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + Tables\Columns\TextColumn::make('due_at') + ->toggleable() + ->dateTime(), + Tables\Columns\TextColumn::make('cycle') + ->toggleable() + ->searchable(), + Tables\Columns\TextColumn::make('source_id') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + Tables\Columns\TextColumn::make('trainingType.title') + ->toggleable() + ->limit(50), + Tables\Columns\TextColumn::make('trainingable_id') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + Tables\Columns\TextColumn::make('trainingable_type') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + ]) + ->filters([ + DateRangeFilter::make('created_at'), + + SelectFilter::make('training_type_id') + ->relationship('trainingType', 'title') + ->indicator('TrainingType') + ->multiple() + ->label('TrainingType'), + ]) + ->actions([ViewAction::make(), EditAction::make()]) + ->bulkActions([ + Tables\Actions\DeleteBulkAction::make() + ->action(function ($records, Tables\Actions\DeleteBulkAction $action) { + foreach ($records as $record) { + try { + $record->delete(); + Notification::make() + ->title('Trainings Deleted') + ->body('The trainings were deleted successfully.') + ->success() + ->send(); + } catch (QueryException $exception) { + if ($exception->getCode() === '23000') { + Notification::make() + ->title('Cannot Delete Trainings') + ->body('One or more trainings have associated training invitations and cannot be deleted.') + ->danger() + ->send(); + } else { + throw $exception; + } + } + } + }), + ]); + } + + public static function getRelations(): array + { + return [ + TrainingResource\RelationManagers\TrainingInvitationsRelationManager::class, + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListTrainings::route('/'), + 'create' => Pages\CreateTraining::route('/create'), + 'view' => Pages\ViewTraining::route('/{record}'), + 'edit' => Pages\EditTraining::route('/{record}/edit'), + ]; + } +} diff --git a/packages/trainings/src/Resources/TrainingResource/Pages/CreateTraining.php b/packages/trainings/src/Resources/TrainingResource/Pages/CreateTraining.php new file mode 100644 index 000000000..5cac916c0 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingResource/Pages/CreateTraining.php @@ -0,0 +1,11 @@ +action(function ($record, DeleteAction $action) { + try { + $record->delete(); + Notification::make() + ->title('Training Deleted') + ->body('The training was deleted successfully.') + ->success() + ->send(); + } catch (QueryException $exception) { + if ($exception->getCode() === '23000') { + Notification::make() + ->title('Cannot Delete Training') + ->body('The training has associated invitations and cannot be deleted.') + ->danger() + ->send(); + } else { + throw $exception; + } + } + }), + ]; + } +} diff --git a/packages/trainings/src/Resources/TrainingResource/Pages/ListTrainings.php b/packages/trainings/src/Resources/TrainingResource/Pages/ListTrainings.php new file mode 100644 index 000000000..70ab23718 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingResource/Pages/ListTrainings.php @@ -0,0 +1,24 @@ +action(function ($record, DeleteAction $action) { + try { + $record->delete(); + Notification::make() + ->title('Training Deleted') + ->body('The training was deleted successfully.') + ->success() + ->send(); + } catch (QueryException $exception) { + if ($exception->getCode() === '23000') { + Notification::make() + ->title('Cannot Delete Training') + ->body('The training has associated invitations and cannot be deleted.') + ->danger() + ->send(); + } else { + throw $exception; + } + } + }), + ]; + } +} diff --git a/packages/trainings/src/Resources/TrainingResource/RelationManagers/TrainingInvitationsRelationManager.php b/packages/trainings/src/Resources/TrainingResource/RelationManagers/TrainingInvitationsRelationManager.php new file mode 100644 index 000000000..ec7c9d2f0 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingResource/RelationManagers/TrainingInvitationsRelationManager.php @@ -0,0 +1,120 @@ +schema([ + Grid::make(['default' => 0])->schema([ + TextInput::make('title') + ->rules(['max:255', 'string']) + ->placeholder('Title') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('slug') + ->rules(['max:255', 'string']) + ->placeholder('Slug') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + RichEditor::make('content') + ->rules(['max:255', 'string']) + ->placeholder('Content') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + DateTimePicker::make('sent_at') + ->rules(['date']) + ->placeholder('Sent At') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + ]), + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('training.title')->limit(50), + Tables\Columns\TextColumn::make('title')->limit(50), + Tables\Columns\TextColumn::make('slug')->limit(50), + Tables\Columns\TextColumn::make('content')->limit(50), + Tables\Columns\TextColumn::make('sent_at')->dateTime(), + ]) + ->filters([ + Tables\Filters\Filter::make('created_at') + ->form([ + Forms\Components\DatePicker::make('created_from'), + Forms\Components\DatePicker::make('created_until'), + ]) + ->query(function (Builder $query, array $data): Builder { + return $query + ->when( + $data['created_from'], + fn ( + Builder $query, + $date + ): Builder => $query->whereDate( + 'created_at', + '>=', + $date + ) + ) + ->when( + $data['created_until'], + fn ( + Builder $query, + $date + ): Builder => $query->whereDate( + 'created_at', + '<=', + $date + ) + ); + }), + + SelectFilter::make('training_id') + ->multiple() + ->relationship('training', 'title'), + ]) + ->headerActions([CreateAction::make()]) + ->actions([EditAction::make(), DeleteAction::make()]) + ->bulkActions([DeleteBulkAction::make()]); + } +} diff --git a/packages/trainings/src/Resources/TrainingTypeResource.php b/packages/trainings/src/Resources/TrainingTypeResource.php new file mode 100644 index 000000000..db8954608 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingTypeResource.php @@ -0,0 +1,133 @@ +schema([ + Section::make()->schema([ + Grid::make(['default' => 0])->schema([ + TextInput::make('title') + ->rules(['max:255', 'string']) + ->required() + ->placeholder('Title') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('slug') + ->rules(['max:255', 'string']) + ->required() + ->placeholder('Slug') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + RichEditor::make('description') + ->rules(['max:255', 'string']) + ->nullable() + ->placeholder('Description') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + ]), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->poll('60s') + ->columns([ + Tables\Columns\TextColumn::make('title') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + Tables\Columns\TextColumn::make('slug') + ->toggleable() + ->searchable(true, null, true) + ->limit(50), + Tables\Columns\TextColumn::make('description') + ->toggleable() + ->searchable() + ->limit(50), + ]) + ->filters([DateRangeFilter::make('created_at')]) + ->actions([ViewAction::make(), EditAction::make()]) + ->bulkActions([ + Tables\Actions\DeleteBulkAction::make() + ->action(function ($records, Tables\Actions\DeleteBulkAction $action) { + foreach ($records as $record) { + try { + $record->delete(); + Notification::make() + ->title('Training Types Deleted') + ->body('The types were deleted successfully.') + ->success() + ->send(); + } catch (QueryException $exception) { + if ($exception->getCode() === '23000') { + Notification::make() + ->title('Cannot Delete Training Types') + ->body('One or more type have associated trainings and cannot be deleted.') + ->danger() + ->send(); + } else { + throw $exception; + } + } + } + }), + ]); + } + + public static function getRelations(): array + { + return [ + TrainingTypeResource\RelationManagers\TrainingsRelationManager::class, + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListTrainingTypes::route('/'), + 'create' => Pages\CreateTrainingType::route('/create'), + 'view' => Pages\ViewTrainingType::route('/{record}'), + 'edit' => Pages\EditTrainingType::route('/{record}/edit'), + ]; + } +} diff --git a/packages/trainings/src/Resources/TrainingTypeResource/Pages/CreateTrainingType.php b/packages/trainings/src/Resources/TrainingTypeResource/Pages/CreateTrainingType.php new file mode 100644 index 000000000..6e9ac3e47 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingTypeResource/Pages/CreateTrainingType.php @@ -0,0 +1,11 @@ +action(function ($record, DeleteAction $action) { + try { + $record->delete(); + Notification::make() + ->title('Training Type Deleted') + ->body('The type was deleted successfully.') + ->success() + ->send(); + } catch (QueryException $exception) { + if ($exception->getCode() === '23000') { + Notification::make() + ->title('Cannot Delete Training Type') + ->body('One or more type have associated trainings and cannot be deleted.') + ->danger() + ->send(); + } else { + throw $exception; + } + } + }), + ]; + } +} diff --git a/packages/trainings/src/Resources/TrainingTypeResource/Pages/ListTrainingTypes.php b/packages/trainings/src/Resources/TrainingTypeResource/Pages/ListTrainingTypes.php new file mode 100644 index 000000000..d5c85c9d9 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingTypeResource/Pages/ListTrainingTypes.php @@ -0,0 +1,20 @@ +action(function ($record, DeleteAction $action) { + try { + $record->delete(); + Notification::make() + ->title('Training Type Deleted') + ->body('The type was deleted successfully.') + ->success() + ->send(); + } catch (QueryException $exception) { + if ($exception->getCode() === '23000') { + Notification::make() + ->title('Cannot Delete Training Type') + ->body('The type has associated trainings and cannot be deleted.') + ->danger() + ->send(); + } else { + throw $exception; + } + } + }), + ]; + } +} diff --git a/packages/trainings/src/Resources/TrainingTypeResource/RelationManagers/TrainingsRelationManager.php b/packages/trainings/src/Resources/TrainingTypeResource/RelationManagers/TrainingsRelationManager.php new file mode 100644 index 000000000..42998c798 --- /dev/null +++ b/packages/trainings/src/Resources/TrainingTypeResource/RelationManagers/TrainingsRelationManager.php @@ -0,0 +1,198 @@ +schema([ + Grid::make(['default' => 0])->schema([ + TextInput::make('title') + ->rules(['max:255', 'string']) + ->placeholder('Title') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('slug') + ->rules(['max:255', 'string']) + ->placeholder('Slug') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + RichEditor::make('description') + ->rules(['max:255', 'string']) + ->placeholder('Description') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('duration') + ->rules(['numeric']) + ->numeric() + ->placeholder('Duration') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('link') + ->rules(['max:255', 'string']) + ->placeholder('Link') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + DateTimePicker::make('due_at') + ->rules(['date']) + ->placeholder('Due At') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + Select::make('cycle') + ->rules([ + 'in:annually,half-yearly,quarterly,monthly,every 2 years,every 3 years,every 4 years,every 5 years', + ]) + ->searchable() + ->options([ + 'annually' => 'Annually', + 'half-yearly' => 'Half yearly', + 'quarterly' => 'Quarterly', + 'monthly' => 'Monthly', + 'every 2 years' => 'Every 2 years', + 'every 3 years' => 'Every 3 years', + 'every 4 years' => 'Every 4 years', + 'every 5 years' => 'Every 5 years', + ]) + ->placeholder('Cycle') + ->default('annually') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('source_id') + ->rules(['max:255']) + ->placeholder('Source Id') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('trainingable_id') + ->rules(['max:255']) + ->placeholder('Trainingable Id') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + + TextInput::make('trainingable_type') + ->rules(['max:255', 'string']) + ->placeholder('Trainingable Type') + ->columnSpan([ + 'default' => 12, + 'md' => 12, + 'lg' => 12, + ]), + ]), + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('title')->limit(50), + Tables\Columns\TextColumn::make('slug')->limit(50), + Tables\Columns\TextColumn::make('description')->limit(50), + Tables\Columns\TextColumn::make('duration'), + Tables\Columns\TextColumn::make('link')->limit(50), + Tables\Columns\TextColumn::make('due_at')->dateTime(), + Tables\Columns\TextColumn::make('cycle'), + Tables\Columns\TextColumn::make('source_id')->limit(50), + Tables\Columns\TextColumn::make('trainingType.title')->limit( + 50 + ), + Tables\Columns\TextColumn::make('trainingable_id')->limit(50), + Tables\Columns\TextColumn::make('trainingable_type')->limit(50), + ]) + ->filters([ + Tables\Filters\Filter::make('created_at') + ->form([ + Forms\Components\DatePicker::make('created_from'), + Forms\Components\DatePicker::make('created_until'), + ]) + ->query(function (Builder $query, array $data): Builder { + return $query + ->when( + $data['created_from'], + fn ( + Builder $query, + $date + ): Builder => $query->whereDate( + 'created_at', + '>=', + $date + ) + ) + ->when( + $data['created_until'], + fn ( + Builder $query, + $date + ): Builder => $query->whereDate( + 'created_at', + '<=', + $date + ) + ); + }), + + SelectFilter::make('training_type_id') + ->multiple() + ->relationship('trainingType', 'title'), + ]) + ->headerActions([CreateAction::make()]) + ->actions([EditAction::make(), DeleteAction::make()]) + ->bulkActions([DeleteBulkAction::make()]); + } +} diff --git a/packages/trainings/src/TrainingDatePlugin.php b/packages/trainings/src/TrainingDatePlugin.php new file mode 100644 index 000000000..dbe6755ef --- /dev/null +++ b/packages/trainings/src/TrainingDatePlugin.php @@ -0,0 +1,35 @@ +resources([ + TrainingDateResource::class, + ]); + } + + public function boot(Panel $panel): void + { + // + } + + public static function make(): static + { + return app(static::class); + } +} diff --git a/packages/trainings/src/TrainingInvitationPlugin.php b/packages/trainings/src/TrainingInvitationPlugin.php new file mode 100644 index 000000000..15fffac41 --- /dev/null +++ b/packages/trainings/src/TrainingInvitationPlugin.php @@ -0,0 +1,35 @@ +resources([ + TrainingInvitationResource::class, + ]); + } + + public function boot(Panel $panel): void + { + // + } + + public static function make(): static + { + return app(static::class); + } +} diff --git a/packages/trainings/src/TrainingPlugin.php b/packages/trainings/src/TrainingPlugin.php new file mode 100644 index 000000000..07df395fc --- /dev/null +++ b/packages/trainings/src/TrainingPlugin.php @@ -0,0 +1,35 @@ +resources([ + TrainingResource::class, + ]); + } + + public function boot(Panel $panel): void + { + // + } + + public static function make(): static + { + return app(static::class); + } +} diff --git a/packages/trainings/src/TrainingServiceProvider.php b/packages/trainings/src/TrainingServiceProvider.php new file mode 100644 index 000000000..da234c3ac --- /dev/null +++ b/packages/trainings/src/TrainingServiceProvider.php @@ -0,0 +1,37 @@ +name('trainings') + ->hasConfigFile() + ->hasViews() + ->hasTranslations() + ->hasMigrations([ + 'create_training_dateables_table', + 'create_training_dates_table', + 'create_training_invitationables_table', + 'create_training_invitations_table', + 'create_training_types_table', + 'create_trainingables_table', + 'create_trainings_table', + 'foreigns_for_training_dateables_table', + 'foreigns_for_training_dates_table', + 'foreigns_for_training_invitationables_table', + 'foreigns_for_training_invitations_table', + 'foreigns_for_trainingables_table', + 'foreigns_for_trainings_table', + ]) + ->hasCommand(InstallCommand::class); + } +} diff --git a/packages/trainings/src/TrainingTypePlugin.php b/packages/trainings/src/TrainingTypePlugin.php new file mode 100644 index 000000000..67562d1bb --- /dev/null +++ b/packages/trainings/src/TrainingTypePlugin.php @@ -0,0 +1,35 @@ +resources([ + TrainingTypeResource::class, + ]); + } + + public function boot(Panel $panel): void + { + // + } + + public static function make(): static + { + return app(static::class); + } +} diff --git a/packages/trainings/src/Traits/HasDescendingOrder.php b/packages/trainings/src/Traits/HasDescendingOrder.php new file mode 100644 index 000000000..15f2b088c --- /dev/null +++ b/packages/trainings/src/Traits/HasDescendingOrder.php @@ -0,0 +1,13 @@ +latest(); + } +}