diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49c0df0..8bebfe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,6 @@ jobs: - name: Twig Linter run: docker compose exec -T php ./vendor/bin/twigcs templates/ --exclude vendor - name: Install eslint - run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn add eslint + run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn add eslint@8.57.0 - name: Run eslint on javascript files run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn lint diff --git a/Dockerfile b/Dockerfile index becedd2..bc3bb4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact ARG PHP_VERSION=8.1 -ARG CADDY_VERSION=2 +ARG CADDY_VERSION=2.8.4 # yarn build FROM gmolaire/yarn AS yarn_build @@ -24,7 +24,7 @@ RUN yarn build FROM php:${PHP_VERSION}-fpm-alpine AS app_php # needed for security update until base image is updated -RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto3 libssl3 +#RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto3 libssl3 # Allow to use development versions of Symfony ARG STABILITY="stable" @@ -188,7 +188,7 @@ RUN rm -f .env.local.php # Temporary fix for https://github.com/dunglas/mercure/issues/770 # https://github.com/dunglas/symfony-docker/pull/407/files -FROM caddy:2.7-builder-alpine AS app_caddy_builder +FROM caddy:2.8.4-builder-alpine AS app_caddy_builder # RUN xcaddy build \ # --with github.com/dunglas/mercure \ @@ -204,7 +204,7 @@ RUN xcaddy build \ FROM caddy:${CADDY_VERSION} AS app_caddy # needed for security update until base image is updated -RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto1.1 libssl1.1 libcrypto3 libssl3 +#RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto1.1 libssl1.1 libcrypto3 libssl3 WORKDIR /srv/app diff --git a/assets/admin.js b/assets/admin.js new file mode 100644 index 0000000..b3e40e5 --- /dev/null +++ b/assets/admin.js @@ -0,0 +1,8 @@ +import { startStimulusApp } from '@symfony/stimulus-bridge' +import AdminParentgroup from './controllers/admin_parentgroup_controller' + +import './styles/admin.css' + +const app = startStimulusApp() + +app.register('admin-parentgroup', AdminParentgroup) diff --git a/assets/controllers/admin_parentgroup_controller.js b/assets/controllers/admin_parentgroup_controller.js new file mode 100644 index 0000000..df6af16 --- /dev/null +++ b/assets/controllers/admin_parentgroup_controller.js @@ -0,0 +1,77 @@ +import { Controller} from '@hotwired/stimulus' + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static targets = ['servicesEnabledField', 'parentField', 'idField'] + + parentFieldTargetConnected(element) { + const observer = new MutationObserver( ( ) => { + if (element.tomselect) { + observer.disconnect() + + const toggle = document.getElementById('Group_servicesEnabled') + toggle.addEventListener('change', () => { + this.updateParentOptions(toggle.checked, this.parentFieldTarget) + }) + } + }) + observer.observe(element, {attributes: true}) + } + + async updateParentOptions(servicesEnabled, parentField) { + const url = `/api/groups?services_enabled=${servicesEnabled}` + + const response = await fetch(url, { method: 'GET' }) + if (!response.ok) { + return + } + const data = await response.json() + const groups = data['hydra:member'] + + // Remove options + parentField.tomselect.clearOptions() + + // Populate with new options + groups.map(group => { + parentField.tomselect.addOption(new Option(group.name, group.id)) + }) + } + + servicesEnabledFieldTargetConnected() { + this.servicesEnabledFieldTarget.addEventListener('change', () => { + if(!this.servicesEnabledFieldTarget.checked) { + const params = new URLSearchParams(this.servicesEnabledFieldTarget.getAttribute('data-toggle-url')) + this.disableServicesForChildGroups(params.get('entityId')) + } + }) + } + + async disableServicesForChildGroups(groupId) { + const url = `/api/groups/${groupId}/disable_child_services` + + const response = await fetch(url, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/merge-patch+json', + }, + }) + if (!response.ok) { + return + } + const data = await response.json() + const groupChild = data.children + + const groupChildId = groupChild.map(group => { + return group.split('/')[3] + }) + + const allToggles = document.querySelectorAll('[data-admin-parentgroup-target="servicesEnabledField"]') + Array.from(allToggles).map(toggle => { + const params = new URLSearchParams(toggle.getAttribute('data-toggle-url')) + if(groupChildId.includes(params.get('entityId'))) { + toggle.checked = false + } + }) + + } +} diff --git a/assets/controllers/parentgroup_controller.js b/assets/controllers/parentgroup_controller.js new file mode 100644 index 0000000..b5577c1 --- /dev/null +++ b/assets/controllers/parentgroup_controller.js @@ -0,0 +1,36 @@ +import { Controller} from '@hotwired/stimulus' + +export default class extends Controller { + static targets = ['servicesEnabledField', 'parentField'] + + connect() { + } + + updateParentOptions() { + const userId = this.servicesEnabledFieldTarget.getAttribute('data-user-id') + const servicesEnabled = this.servicesEnabledFieldTarget.checked + + const url = `/api/groups?user=${userId}&services_enabled=${servicesEnabled}` + this.addGroupsWithEnabledServices(url) + } + + async addGroupsWithEnabledServices(url) { + const response = await fetch(url, { method: 'GET' }) + if (!response.ok) { + return + } + const data = await response.json() + const groups = data['hydra:member'] + + // Remove options and set a default value + Array.from(this.parentFieldTarget.options).map((group) => { + this.parentFieldTarget.remove(group) + }) + this.parentFieldTarget.add(new Option()) + + // Populate with new options + groups.map(group => { + this.parentFieldTarget.add(new Option(group.name, group.id)) + }) + } +} diff --git a/assets/stimulus.js b/assets/stimulus.js index c02d609..352bda5 100644 --- a/assets/stimulus.js +++ b/assets/stimulus.js @@ -1,5 +1,4 @@ import { startStimulusApp } from '@symfony/stimulus-bridge' -import { Application } from '@hotwired/stimulus' import PasswordVisibility from 'stimulus-password-visibility' import Carousel from 'stimulus-carousel' @@ -15,7 +14,5 @@ export const app = startStimulusApp(require.context( // register any custom, 3rd party controllers here // app.register('some_controller_name', SomeImportedController); -const application = Application.start() -application.register('carousel', Carousel) -application.register('password-visibility', PasswordVisibility) - +app.register('carousel', Carousel) +app.register('password-visibility', PasswordVisibility) diff --git a/assets/styles/admin.css b/assets/styles/admin.css new file mode 100644 index 0000000..5862dac --- /dev/null +++ b/assets/styles/admin.css @@ -0,0 +1,4 @@ +/* Remove the extra arrow added by Tom Select */ +.ts-wrapper.single .ts-control:after { + display: none; +} diff --git a/assets/styles/app.css b/assets/styles/app.css new file mode 100644 index 0000000..dd6181a --- /dev/null +++ b/assets/styles/app.css @@ -0,0 +1,3 @@ +body { + background-color: skyblue; +} diff --git a/composer.json b/composer.json index 8f02a41..7899c1e 100644 --- a/composer.json +++ b/composer.json @@ -78,6 +78,7 @@ "symfony/runtime": "6.2.*", "symfony/security-bundle": "6.2.*", "symfony/serializer": "6.2.*", + "symfony/stimulus-bundle": "^2.14", "symfony/translation-contracts": "^3.2", "symfony/twig-bridge": "6.2.*", "symfony/twig-bundle": "6.2.*", diff --git a/composer.lock b/composer.lock index 3c77d18..db0756a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3e1f8a3631e991b528d9f68758299e0e", + "content-hash": "2b97a3771217b9b8a9f07e3f5ec9aec2", "packages": [ { "name": "alcohol/iso4217", @@ -13247,6 +13247,75 @@ ], "time": "2023-03-01T10:32:47+00:00" }, + { + "name": "symfony/stimulus-bundle", + "version": "v2.14.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/stimulus-bundle.git", + "reference": "f775f6e811215156bfe41e6be234272d0c27e02b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/f775f6e811215156bfe41e6be234272d0c27e02b", + "reference": "f775f6e811215156bfe41e6be234272d0c27e02b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.0|^3.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "twig/twig": "^2.15.3|^3.4.3" + }, + "require-dev": { + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "zenstruck/browser": "^1.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\UX\\StimulusBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Stimulus!", + "keywords": [ + "symfony-ux" + ], + "support": { + "source": "https://github.com/symfony/stimulus-bundle/tree/v2.14.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-07T20:26:48+00:00" + }, { "name": "symfony/stopwatch", "version": "v6.2.7", @@ -18590,5 +18659,5 @@ "ext-zip": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/bundles.php b/config/bundles.php index efd16ea..9f15f1f 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -33,4 +33,5 @@ return [ Payum\Bundle\PayumBundle\PayumBundle::class => ['all' => true], FOS\CKEditorBundle\FOSCKEditorBundle::class => ['all' => true], Misd\PhoneNumberBundle\MisdPhoneNumberBundle::class => ['all' => true], + Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], ]; diff --git a/fixtures/prod-boot/configuration.yaml b/fixtures/prod-boot/configuration.yaml index 4866531..e2facd2 100644 --- a/fixtures/prod-boot/configuration.yaml +++ b/fixtures/prod-boot/configuration.yaml @@ -4,8 +4,9 @@ App\Entity\Configuration: features (extends configuration_template): configuration: - services: - servicesEnabled: true + global: + globalServicesEnabled: true + globalPaidMembership: false notificationsSender: notificationsSenderEmail: info@example.com notificationsSenderName: Contact diff --git a/fixtures/test/configuration.yaml b/fixtures/test/configuration.yaml index 74fe9ee..97062e6 100644 --- a/fixtures/test/configuration.yaml +++ b/fixtures/test/configuration.yaml @@ -4,8 +4,9 @@ App\Entity\Configuration: features (extends configuration_template): configuration: - services: - servicesEnabled: true + global: + globalServicesEnabled: true + globalPaidMembership: false notificationsSender: notificationsSenderEmail: info@example.com notificationsSenderName: Contact diff --git a/fixtures/test/group_offer.yaml b/fixtures/test/group_offer.yaml index 723a1f4..d2881b3 100644 --- a/fixtures/test/group_offer.yaml +++ b/fixtures/test/group_offer.yaml @@ -4,11 +4,11 @@ App\Entity\GroupOffer: # Templates group_offer_group_1_template (template, extends group_offer_template): group: '@group_1' - type: !php/enum App\Enum\Group\GroupOfferType::YEARLY + type: !php/enum App\Enum\OfferType::YEARLY group_offer_group_7_template (template, extends group_offer_template): group: '@group_7' - type: !php/enum App\Enum\Group\GroupOfferType::YEARLY + type: !php/enum App\Enum\OfferType::YEARLY # Group 1 group_offer_group_1_10 (extends group_offer_group_1_template): diff --git a/fixtures/test/platform_offer.yaml b/fixtures/test/platform_offer.yaml new file mode 100644 index 0000000..9a1d756 --- /dev/null +++ b/fixtures/test/platform_offer.yaml @@ -0,0 +1,11 @@ +App\Entity\PlatformOffer: + platform_offer_1: + id: + name: Lorem ipsum + price: 2000 + type: !php/enum App\Enum\OfferType::YEARLY + + platform_offer_2: + name: Aliquet risus + price: 200 + type: !php/enum App\Enum\OfferType::MONTHLY diff --git a/src/Controller/Admin/GroupCrudController.php b/src/Controller/Admin/GroupCrudController.php index fb42db0..04498d2 100755 --- a/src/Controller/Admin/GroupCrudController.php +++ b/src/Controller/Admin/GroupCrudController.php @@ -19,6 +19,7 @@ use App\Form\Type\Security\GroupInvitationFormType; use App\Helper\CsvExporter; use App\Message\Command\Group\CreateGroupInvitationMessage; use App\MessageBus\CommandBus; +use App\Repository\ConfigurationRepository; use App\Security\Checker\AuthorizationChecker; use Doctrine\ORM\QueryBuilder; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; @@ -66,6 +67,7 @@ final class GroupCrudController extends AbstractCrudController implements GroupA private readonly TranslatorInterface $translator, private readonly FilterFactory $filterFactory, private readonly SluggerInterface $slugger, + private readonly ConfigurationRepository $configurationRepository ) { } @@ -75,6 +77,7 @@ final class GroupCrudController extends AbstractCrudController implements GroupA ->setEntityLabelInPlural('groups') ->setSearchFields(['name', 'description']) ->setDefaultSort(['id' => 'ASC']) + ->overrideTemplate('crud/field/boolean', 'admin/field/services_enabled.html.twig') ; } @@ -230,8 +233,24 @@ final class GroupCrudController extends AbstractCrudController implements GroupA ->setFormTypeOption('class', GroupMembership::class) ->setChoices(GroupMembership::getAsArray()); + if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) { + $servicesEnabledField = BooleanField::new('servicesEnabled') + ->renderAsSwitch() + ->setFormTypeOption('attr', [ + 'data-controller' => 'admin-parentgroup', + 'data-admin-parentgroup-target' => 'servicesEnabledField', + ]) + ->addWebpackEncoreEntries('admin'); + } + $parentField = AssociationField::new('parent') - ->setRequired(false); + ->setRequired(false) + ->addWebpackEncoreEntries('admin') + ->setFormTypeOption('attr', [ + 'data-controller' => 'admin-parentgroup', + 'data-admin-parentgroup-target' => 'parentField', + ]) + ; $childrenField = AssociationField::new('children'); $usersField = AssociationField::new('userGroups') ->setTemplatePath('admin/group/user_groups_field.html.twig'); @@ -247,14 +266,20 @@ final class GroupCrudController extends AbstractCrudController implements GroupA $panels = $this->getPanels(); if ($pageName === Crud::PAGE_INDEX) { - return [$nameField, $typeField, $parentField, $membershipField, $usersField, $createdAt, $updatedAt]; + $fields = [$nameField, $typeField, $parentField, $membershipField, $usersField, $createdAt, $updatedAt]; + + if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) { + array_splice($fields, 3, 0, [$servicesEnabledField]); + } + + return $fields; } if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { $typeField->setChoices(GroupType::cases()); $membershipField->setChoices(GroupMembership::cases()); - return [ + $fields = [ $nameField, $typeField, $membershipField, @@ -264,11 +289,17 @@ final class GroupCrudController extends AbstractCrudController implements GroupA $invitationByAdminField, $membershipField, ]; + + if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) { + array_splice($fields, 3, 0, [$servicesEnabledField]); + } + + return $fields; } // show - return [ + $fields = [ $panels['information'], $nameField, $parentField, @@ -283,6 +314,12 @@ final class GroupCrudController extends AbstractCrudController implements GroupA $updatedAt, $createdAt, ]; + + if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) { + array_splice($fields, 2, 0, [$servicesEnabledField]); + } + + return $fields; } /** @@ -309,7 +346,7 @@ final class GroupCrudController extends AbstractCrudController implements GroupA } /** - * For now we export exactly what we see in the list to avoid seurity problems. + * For now we export exactly what we see in the list to avoid security problems. */ public function export(AdminContext $context): Response { diff --git a/src/Controller/Admin/GroupOfferCrudController.php b/src/Controller/Admin/GroupOfferCrudController.php index 1597cca..8b93f18 100755 --- a/src/Controller/Admin/GroupOfferCrudController.php +++ b/src/Controller/Admin/GroupOfferCrudController.php @@ -10,12 +10,12 @@ use App\EasyAdmin\Field\FieldTrait; use App\EasyAdmin\Filter\EnumFilter; use App\EasyAdmin\Filter\UserGroup\MyGroupFilter; use App\EasyAdmin\Filter\UuidFilter; -use App\EasyAdmin\Form\Type\GroupOfferTypeType; +use App\EasyAdmin\Form\Type\OfferTypeType; use App\Entity\GroupOffer; use App\Entity\User; use App\Enum\Group\GroupMembership; -use App\Enum\Group\GroupOfferType; use App\Enum\Group\UserMembership; +use App\Enum\OfferType; use App\Security\Checker\AuthorizationChecker; use Doctrine\ORM\QueryBuilder; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; @@ -65,7 +65,7 @@ final class GroupOfferCrudController extends AbstractCrudController implements G return $filters ->add(UuidFilter::new('id')) ->add(MyGroupFilter::new('group')) - ->add(EnumFilter::new('membership', GroupOfferTypeType::class)) + ->add(EnumFilter::new('membership', OfferTypeType::class)) ->add('name') ->add('active') ; @@ -129,8 +129,8 @@ final class GroupOfferCrudController extends AbstractCrudController implements G $nameField = TextField::new('name'); $typeField = ChoiceField::new('type') ->setFormType(EnumType::class) - ->setFormTypeOption('class', GroupOfferType::class) - ->setChoices(GroupOfferType::getAsArray()); + ->setFormTypeOption('class', OfferType::class) + ->setChoices(OfferType::getAsArray()); $priceField = MoneyField::new('price') ->setCurrencyPropertyPath('currency') @@ -149,7 +149,7 @@ final class GroupOfferCrudController extends AbstractCrudController implements G } if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { - $typeField->setChoices(GroupOfferType::cases()); + $typeField->setChoices(OfferType::cases()); return [ $groupField, diff --git a/src/Controller/Admin/PlatformOfferCrudController.php b/src/Controller/Admin/PlatformOfferCrudController.php new file mode 100644 index 0000000..0c49a27 --- /dev/null +++ b/src/Controller/Admin/PlatformOfferCrudController.php @@ -0,0 +1,121 @@ +setEntityLabelInPlural('platform_offers') + ->setSearchFields(['name']) + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add(UuidFilter::new('id')) + ->add(EnumFilter::new('type', OfferTypeType::class)) + ->add('name') + ->add('active') + ; + } + + public function configureActions(Actions $actions): Actions + { + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ; + } + + public static function getEntityFqcn(): string + { + return PlatformOffer::class; + } + + public function configureFields(string $pageName): iterable + { + $idFIeld = IdField::new('id') + ->setLabel('id') + ->hideOnForm(); + + $nameField = TextField::new('name'); + $typeField = ChoiceField::new('type') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', OfferType::class) + ->setChoices(OfferType::getAsArray()); + + $priceField = MoneyField::new('price') + ->setCurrencyPropertyPath('currency') + ->setStoredAsCents(); + $currencyField = CurrencyField::new('currency'); + + $activeField = BooleanField::new('active') + ->setTemplatePath('easy_admin/field/boolean.html.twig') + ; + $createdAtField = DateTimeField::new('createdAt'); + $updatedAtField = DateTimeField::new('updatedAt'); + + $panels = $this->getPanels(); + if ($pageName === Crud::PAGE_INDEX) { + return [$nameField, $typeField, $priceField, $activeField, $createdAtField, $updatedAtField]; + } + + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + $typeField->setChoices(OfferType::cases()); + + return [ + $nameField, + $typeField, + $priceField, + $currencyField, + $activeField, + ]; + } + + // show + return [ + $panels['information'], + $nameField, + $typeField, + $priceField, + $currencyField, + + $panels['tech_information'], + $idFIeld, + $updatedAtField, + $createdAtField, + ]; + } +} diff --git a/src/Doctrine/Behavior/AbstractOfferEntity.php b/src/Doctrine/Behavior/AbstractOfferEntity.php new file mode 100644 index 0000000..0355573 --- /dev/null +++ b/src/Doctrine/Behavior/AbstractOfferEntity.php @@ -0,0 +1,148 @@ +name.' ('.$this->type->value.')'; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getType(): OfferType + { + return $this->type; + } + + public function setType(OfferType $type): self + { + $this->type = $type; + + return $this; + } + + public function setPrice(int $price): self + { + $this->price = $price; + + return $this; + } + + public function getPrice(): int + { + return $this->price; + } + + public function getActualPrice(): int + { + return $this->price / 100; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function setCurrency(string $currency): self + { + $this->currency = $currency; + + return $this; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } +} diff --git a/src/EasyAdmin/Form/Type/GroupOfferTypeType.php b/src/EasyAdmin/Form/Type/OfferTypeType.php similarity index 75% rename from src/EasyAdmin/Form/Type/GroupOfferTypeType.php rename to src/EasyAdmin/Form/Type/OfferTypeType.php index 719e9fb..2efa4fd 100644 --- a/src/EasyAdmin/Form/Type/GroupOfferTypeType.php +++ b/src/EasyAdmin/Form/Type/OfferTypeType.php @@ -5,20 +5,20 @@ declare(strict_types=1); namespace App\EasyAdmin\Form\Type; use App\Controller\Admin\DashboardController; -use App\Enum\Group\GroupOfferType; +use App\Enum\OfferType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\OptionsResolver; /** - * Form type for the GroupOfferType enumeration. + * Form type for the OfferType enumeration. */ -class GroupOfferTypeType extends AbstractType +class OfferTypeType extends AbstractType { public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'choices' => GroupOfferType::getAsArray(), + 'choices' => OfferType::getAsArray(), 'translation_domain' => DashboardController::DOMAIN, ]); } diff --git a/src/Entity/Configuration.php b/src/Entity/Configuration.php index e60559c..fa5d509 100644 --- a/src/Entity/Configuration.php +++ b/src/Entity/Configuration.php @@ -78,19 +78,26 @@ class Configuration /** * @return bool[] */ - public function getServices(): array + public function getGlobals(): array { - /** @var array $services */ - $services = $this->configuration['services'] ?? []; + /** @var array $globals */ + $globals = $this->configuration['global'] ?? []; - return $services; + return $globals; } public function getServicesEnabled(): bool { - $services = $this->getServices(); + $globals = $this->getGlobals(); - return $services['servicesEnabled']; + return $globals['globalServicesEnabled']; + } + + public function getPaidMembership(): bool + { + $globals = $this->getGlobals(); + + return $globals['globalPaidMembership']; } /** diff --git a/src/Entity/Group.php b/src/Entity/Group.php index fa86764..34c5cc1 100755 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -7,10 +7,15 @@ namespace App\Entity; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; use App\Doctrine\Behavior\TimestampableEntity; use App\Enum\Group\GroupMembership; use App\Enum\Group\GroupType; use App\Repository\GroupRepository; +use App\State\GroupsProvider; +use App\State\Processor\GroupChildServicesEnabledProcessor; use App\Validator as AppAssert; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -26,6 +31,16 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Index(columns: ['type'])] #[ApiFilter(OrderFilter::class, properties: ['name'])] #[AppAssert\Constraints\Group\GroupParentNotSelf] +#[ApiResource( + operations: [ + new GetCollection(provider: GroupsProvider::class), + new Patch( + uriTemplate: '/groups/{id}/disable_child_services', + input: false, + processor: GroupChildServicesEnabledProcessor::class + ), + ] +)] class Group implements \Stringable { use TimestampableEntity; @@ -123,6 +138,9 @@ class Group implements \Stringable #[ORM\ManyToMany(targetEntity: Product::class, mappedBy: 'groups')] private Collection $products; + #[ORM\Column(type: 'boolean')] + private bool $servicesEnabled = false; + public function __construct() { $this->children = new ArrayCollection(); @@ -340,6 +358,16 @@ class Group implements \Stringable return $this; } + public function getServicesEnabled(): bool + { + return $this->servicesEnabled; + } + + public function setServicesEnabled(bool $servicesEnabled): void + { + $this->servicesEnabled = $servicesEnabled; + } + // End of basic 'etters ---------------------------------------------------- /** diff --git a/src/Entity/GroupOffer.php b/src/Entity/GroupOffer.php index 87263c1..752655e 100644 --- a/src/Entity/GroupOffer.php +++ b/src/Entity/GroupOffer.php @@ -4,33 +4,16 @@ declare(strict_types=1); namespace App\Entity; -use App\Doctrine\Behavior\TimestampableEntity; -use App\Enum\Group\GroupOfferType; +use App\Doctrine\Behavior\AbstractOfferEntity; use App\Repository\GroupOfferRepository; -use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; -use Symfony\Component\Uid\Uuid; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: GroupOfferRepository::class)] #[ORM\Table(name: 'group_offer')] #[ORM\Index(columns: ['type'])] -class GroupOffer implements \Stringable +class GroupOffer extends AbstractOfferEntity { - use TimestampableEntity; - - final public const DEFAULT_CURRENCY = 'EUR'; - - /** - * Generates a V6 uuid. - */ - #[ORM\Id] - #[ORM\Column(type: 'uuid', unique: true)] - #[ORM\GeneratedValue(strategy: 'CUSTOM')] - #[ORM\CustomIdGenerator(class: UuidGenerator::class)] - private Uuid $id; - /** * Related group. */ @@ -39,134 +22,15 @@ class GroupOffer implements \Stringable #[ORM\OrderBy(['createdAt' => 'ASC'])] private Group $group; - /** - * Short name of the offer. - */ - #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] - #[Assert\NotBlank] - #[Assert\Length(max: 255)] - private string $name; - - /** - * Type of offer. - */ - #[ORM\Column(name: 'type', type: 'string', nullable: false, enumType: GroupOfferType::class)] - #[Assert\NotBlank] - protected GroupOfferType $type; - - /** - * Price, we stored the amount multiplied by 100 so we can use an integer for - * this property. - */ - #[ORM\Column(type: Types::INTEGER, nullable: false)] - protected int $price; - - /** - * Associated currency for the price property. - * - * @see https://en.wikipedia.org/wiki/ISO_4217 - */ - #[ORM\Column(type: Types::STRING, nullable: false)] - protected string $currency = self::DEFAULT_CURRENCY; - - /** - * If the offer is visible on the front site. Can be use to deactivate offers - * for some time. - */ - #[ORM\Column(type: 'boolean', nullable: false)] - protected bool $active = true; - - public function __toString() - { - return $this->name.' ('.$this->type->value.')'; - } - - public function getId(): Uuid - { - return $this->id; - } - - public function setId(Uuid $uuid): self - { - $this->id = $uuid; - - return $this; - } - public function getGroup(): Group { return $this->group; } - public function setGroup(Group $group): GroupOffer + public function setGroup(Group $group): self { $this->group = $group; return $this; } - - public function getName(): string - { - return $this->name; - } - - public function setName(string $name): GroupOffer - { - $this->name = $name; - - return $this; - } - - public function getType(): GroupOfferType - { - return $this->type; - } - - public function setType(GroupOfferType $type): GroupOffer - { - $this->type = $type; - - return $this; - } - - public function setPrice(int $price): self - { - $this->price = $price; - - return $this; - } - - public function getPrice(): int - { - return $this->price; - } - - public function getActualPrice(): int - { - return $this->price / 100; - } - - public function getCurrency(): string - { - return $this->currency; - } - - public function setCurrency(string $currency): self - { - $this->currency = $currency; - - return $this; - } - - public function isActive(): bool - { - return $this->active; - } - - public function setActive(bool $active): GroupOffer - { - $this->active = $active; - - return $this; - } } diff --git a/src/Entity/PlatformOffer.php b/src/Entity/PlatformOffer.php new file mode 100644 index 0000000..7462085 --- /dev/null +++ b/src/Entity/PlatformOffer.php @@ -0,0 +1,16 @@ + 'Swicth the status of the product'], + openapiContext: ['summary' => 'Switch the status of the product'], normalizationContext: ['groups' => [ProductSwitchProcessor::class]], security: "is_granted('".ProductVoter::EDIT."', object)", input: false, diff --git a/src/Entity/User.php b/src/Entity/User.php index 132004d..50140db 100755 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -770,15 +770,23 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageIn * * @return Collection */ - public function getMyGroupsAsAdmin(): Collection + public function getMyGroupsAsAdmin(bool $enabledServices = false): Collection { $adminUserGroups = $this->userGroups->filter( static fn (UserGroup $userGroup) => $userGroup->getMembership()->isAdmin() || $userGroup->isMainAdminAccount() ); - return new ArrayCollection( + $groups = new ArrayCollection( array_map(static fn (UserGroup $userGroup) => $userGroup->getGroup(), $adminUserGroups->toArray()) ); + + if ($enabledServices) { + return $groups->filter( + static fn (Group $group) => $group->getServicesEnabled() + ); + } + + return $groups; } /** diff --git a/src/Entity/UserGroup.php b/src/Entity/UserGroup.php index 8400882..d4e38f5 100644 --- a/src/Entity/UserGroup.php +++ b/src/Entity/UserGroup.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Entity; +use ApiPlatform\Metadata\ApiResource; use App\Doctrine\Behavior\TimestampableEntity; use App\Enum\Group\UserMembership; use App\Repository\UserGroupRepository; @@ -16,6 +17,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: UserGroupRepository::class)] #[ORM\UniqueConstraint(columns: ['user', 'group'])] +#[ApiResource] class UserGroup { use TimestampableEntity; diff --git a/src/Enum/Group/GroupOfferType.php b/src/Enum/OfferType.php similarity index 82% rename from src/Enum/Group/GroupOfferType.php rename to src/Enum/OfferType.php index bbf761b..611b55f 100644 --- a/src/Enum/Group/GroupOfferType.php +++ b/src/Enum/OfferType.php @@ -2,22 +2,20 @@ declare(strict_types=1); -namespace App\Enum\Group; +namespace App\Enum; -use App\Enum\AsArrayTrait; - -enum GroupOfferType: string +enum OfferType: string { use AsArrayTrait; - // The user only to pay once to access the group. In his case the end date is + // The user only to pay once to access the group/platform. In his case the end date is // not set and the membership is valid until it is deleted or a end date is // set. The end date can always be set manually in case of a problem. case ONESHOT = 'oneshot'; // Monthly subscription. The membership is valid 1 month and the user has to // renew it once the end date is over. This can be useful when a user when to - // try a group on the short period before taking a longer subscription. + // try a group/platform on the short period before taking a longer subscription. case MONTHLY = 'monthly'; // Subscription valid for one year. An email will be send a few days before diff --git a/src/Form/Type/Admin/ParametersFormType.php b/src/Form/Type/Admin/ParametersFormType.php index c4e7735..034ae0c 100755 --- a/src/Form/Type/Admin/ParametersFormType.php +++ b/src/Form/Type/Admin/ParametersFormType.php @@ -25,13 +25,20 @@ final class ParametersFormType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('servicesEnabled', CheckboxType::class, [ + ->add('globalServicesEnabled', CheckboxType::class, [ 'label' => 'parameter.services', 'label_attr' => [ 'class' => 'checkbox-inline checkbox-switch', ], ]) + ->add('globalPaidMembership', CheckboxType::class, [ + 'label' => 'parameter.paid_membership', + 'label_attr' => [ + 'class' => 'checkbox-inline checkbox-switch', + ], + ]) + ->add('notificationsSenderEmail', EmailType::class, [ 'label' => 'parameter.mail', 'label_attr' => ['class' => 'col-sm-2 col-form-label'], diff --git a/src/Form/Type/Group/CreateGroupFormType.php b/src/Form/Type/Group/CreateGroupFormType.php index 565827a..125dbae 100644 --- a/src/Form/Type/Group/CreateGroupFormType.php +++ b/src/Form/Type/Group/CreateGroupFormType.php @@ -4,23 +4,30 @@ declare(strict_types=1); namespace App\Form\Type\Group; +use App\Entity\Configuration; use App\Entity\Group; use App\Entity\User; use App\Enum\Group\GroupMembership; use App\Enum\Group\GroupType; +use App\Repository\ConfigurationRepository; +use App\Repository\GroupRepository; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Webmozart\Assert\Assert; class CreateGroupFormType extends AbstractType { public function __construct( private readonly Security $security, + private readonly ConfigurationRepository $configurationRepository, + private readonly GroupRepository $groupRepository ) { } @@ -28,6 +35,8 @@ class CreateGroupFormType extends AbstractType { /** @var User $user */ $user = $this->security->getUser(); + $configuration = $this->configurationRepository->getInstanceConfiguration(); + $myGroupsWithDisabledServices = $this->groupRepository->getGroupsByEnabledServices(false, $user); $builder ->add('name', TextType::class, [ @@ -43,27 +52,44 @@ class CreateGroupFormType extends AbstractType 'label_attr' => ['class' => 'fs-6 text-black'], 'expanded' => true, 'choice_label' => 'transKey', - ]) - ->add('membership', EnumType::class, [ - 'class' => GroupMembership::class, - 'label' => 'templates.pages.group.create.form.membership', - 'label_attr' => ['class' => 'fs-6 text-black'], - 'expanded' => true, - ]) - ->add('submit', SubmitType::class, [ - 'label' => 'templates.pages.group.create.form.submit', - 'attr' => ['class' => 'btn btn-primary btn-sm d-grid col-12 my-5'], ]); + Assert::isInstanceOf($configuration, Configuration::class); + if ($configuration->getServicesEnabled()) { + $builder + ->add('servicesEnabled', CheckboxType::class, [ + 'label' => 'templates.pages.group.create.form.servicesEnabled', + 'label_attr' => ['class' => 'fs-6 text-black mb-3 switch-custom'], + 'required' => false, + 'attr' => [ + 'data-action' => 'click->parentgroup#updateParentOptions', + 'data-parentgroup-target' => 'servicesEnabledField', + 'data-user-id' => $user->getId(), + ], + ]); + } + $builder + ->add('membership', EnumType::class, [ + 'class' => GroupMembership::class, + 'label' => 'templates.pages.group.create.form.membership', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'expanded' => true, + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'templates.pages.group.create.form.submit', + 'attr' => ['class' => 'btn btn-primary btn-sm d-grid col-12 my-5'], + ]); - $myGroups = $user->getMyGroupsAsAdmin(); - if (!$myGroups->isEmpty()) { + if ([] === $myGroupsWithDisabledServices) { $builder ->add('parent', EntityType::class, [ 'class' => Group::class, - 'choices' => $myGroups, + 'choices' => $myGroupsWithDisabledServices, 'label' => 'templates.pages.group.create.form.subgroup', 'label_attr' => ['class' => 'fs-6 text-black'], 'required' => false, + 'attr' => [ + 'data-parentgroup-target' => 'parentField', + ], ]); } } diff --git a/src/Message/Command/Admin/ParametersFormCommand.php b/src/Message/Command/Admin/ParametersFormCommand.php index c719a45..73ba808 100755 --- a/src/Message/Command/Admin/ParametersFormCommand.php +++ b/src/Message/Command/Admin/ParametersFormCommand.php @@ -17,9 +17,12 @@ final class ParametersFormCommand extends AbstractFormCommand final public const ONLY_ADMIN = 'only_admin'; final public const ALL = 'all'; - // services section ————————————————————————————————————————————— + // global section ————————————————————————————————————————————— #[Assert\Type('bool')] - public bool $servicesEnabled = true; + public bool $globalServicesEnabled = true; + + #[Assert\Type('bool')] + public bool $globalPaidMembership = false; // notificationsSender section ————————————————————————————————————————————— #[Assert\Email()] @@ -59,7 +62,7 @@ final class ParametersFormCommand extends AbstractFormCommand protected function getSections(): array { return [ - 'services', + 'global', 'notificationsSender', 'contact', 'groups', diff --git a/src/MessageHandler/Command/Admin/ParametersFormCommandHandler.php b/src/MessageHandler/Command/Admin/ParametersFormCommandHandler.php index b9d42f0..dcd9c09 100644 --- a/src/MessageHandler/Command/Admin/ParametersFormCommandHandler.php +++ b/src/MessageHandler/Command/Admin/ParametersFormCommandHandler.php @@ -6,13 +6,15 @@ namespace App\MessageHandler\Command\Admin; use App\Message\Command\Admin\ParametersFormCommand; use App\Repository\ConfigurationRepository; +use App\Repository\GroupRepository; use Symfony\Component\Messenger\Attribute\AsMessageHandler; #[AsMessageHandler] final class ParametersFormCommandHandler { public function __construct( - private readonly ConfigurationRepository $configurationRepository + private readonly ConfigurationRepository $configurationRepository, + private readonly GroupRepository $groupRepository, ) { } @@ -24,5 +26,10 @@ final class ParametersFormCommandHandler $configuration = $this->configurationRepository->getInstanceConfigurationOrCreate(); $configuration->setConfiguration($message->toJsonArray()); $this->configurationRepository->save($configuration, true); + + if (!$configuration->getServicesEnabled()) { + $groups = $this->groupRepository->findAll(); + $this->groupRepository->disableServicesForAllGroups($groups); + } } } diff --git a/src/Repository/ConfigurationRepository.php b/src/Repository/ConfigurationRepository.php index 9601154..c2f392d 100644 --- a/src/Repository/ConfigurationRepository.php +++ b/src/Repository/ConfigurationRepository.php @@ -61,13 +61,13 @@ final class ConfigurationRepository extends ServiceEntityRepository public function getServicesParameter(): bool { - /** @var array{configuration: array{ services: array{ servicesEnabled: bool }}} $config */ + /** @var array{configuration: array{ global: array{ globalServicesEnabled: bool }}} $config */ $config = $this ->createQueryBuilder('c') ->select('c.configuration') ->setMaxResults(1) ->getQuery()->getOneOrNullResult(); - return $config['configuration']['services']['servicesEnabled']; + return $config['configuration']['global']['globalServicesEnabled']; } } diff --git a/src/Repository/GroupRepository.php b/src/Repository/GroupRepository.php index 65a01b6..a1c1a5f 100644 --- a/src/Repository/GroupRepository.php +++ b/src/Repository/GroupRepository.php @@ -8,6 +8,7 @@ use App\Entity\Group; use App\Entity\User; use App\Entity\UserGroup; use App\Enum\Group\GroupType; +use App\Enum\Group\UserMembership; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; @@ -87,4 +88,51 @@ final class GroupRepository extends ServiceEntityRepository ->andWhere('ug.user = :user') ->setParameter('user', $user); } + + /** + * @return Group[] + */ + public function getGroupsByEnabledServices(bool $servicesEnabled, ?User $user = null): array + { + $qb = $this->createQueryBuilder('g') + ->andWhere('g.servicesEnabled = :servicesEnabled') + ->setParameter('servicesEnabled', $servicesEnabled); + + if ($user instanceof User) { + $qb + ->leftJoin('g.userGroups', 'gu') + ->andWhere('gu.user = :user') + ->andWhere('gu.mainAdminAccount = :mainAdminAccount OR gu.membership = :membership') + ->setParameter('user', $user) + ->setParameter('mainAdminAccount', true) + ->setParameter('membership', UserMembership::ADMIN); + } + + /** @var Group[] */ + return $qb + ->getQuery() + ->getResult(); + } + + /** + * @param Group[] $groups + */ + public function disableServicesForAllGroups(array $groups): void + { + foreach ($groups as $group) { + $group->setServicesEnabled(false); + $this->getEntityManager()->persist($group); + } + $this->getEntityManager()->flush(); + } + + public function disableServicesForChildGroup(Group $group): void + { + /** @var Group $child */ + foreach ($group->getChildren() as $child) { + $child->setServicesEnabled(false); + $this->getEntityManager()->persist($child); + } + $this->getEntityManager()->flush(); + } } diff --git a/src/Repository/PlatformOfferRepository.php b/src/Repository/PlatformOfferRepository.php new file mode 100644 index 0000000..8ae77f5 --- /dev/null +++ b/src/Repository/PlatformOfferRepository.php @@ -0,0 +1,35 @@ + + * + * @method PlatformOffer|null find($id, $lockMode = null, $lockVersion = null) + * @method PlatformOffer|null findOneBy(array $criteria, array $orderBy = null) + * @method PlatformOffer[] findAll() + * @method PlatformOffer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PlatformOfferRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = PlatformOffer::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + /** + * Return an object or throws an exception if not found. + */ + public function get(mixed $id, int|null $lockMode = null, int|null $lockVersion = null): PlatformOffer + { + return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('Platform offer not found.'); + } +} diff --git a/src/State/GroupsProvider.php b/src/State/GroupsProvider.php new file mode 100644 index 0000000..31a1ad7 --- /dev/null +++ b/src/State/GroupsProvider.php @@ -0,0 +1,34 @@ + + */ +class GroupsProvider implements ProviderInterface +{ + public function __construct( + readonly private GroupRepository $groupRepository, + readonly private UserRepository $userRepository + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null // @phpstan-ignore-line + { + if (isset($context['filters']['user'])) { + $user = $this->userRepository->find($context['filters']['user']); + + return $this->groupRepository->getGroupsByEnabledServices($context['filters']['services_enabled'] === 'true', $user); + } + + return $this->groupRepository->getGroupsByEnabledServices($context['filters']['services_enabled'] === 'true'); + } +} diff --git a/src/State/Processor/GroupChildServicesEnabledProcessor.php b/src/State/Processor/GroupChildServicesEnabledProcessor.php new file mode 100644 index 0000000..fa2cd15 --- /dev/null +++ b/src/State/Processor/GroupChildServicesEnabledProcessor.php @@ -0,0 +1,31 @@ + $uriVariables + * @param array $context + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Group + { + Assert::isInstanceOf($data, Group::class); + $this->groupRepository->disableServicesForChildGroup($data); + + return $data; + } +} diff --git a/symfony.lock b/symfony.lock index 2073928..edffc86 100644 --- a/symfony.lock +++ b/symfony.lock @@ -185,6 +185,15 @@ "config/packages/http_discovery.yaml" ] }, + "phpstan/phpstan": { + "version": "1.11", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767" + } + }, "phpunit/phpunit": { "version": "9.5", "recipe": { @@ -444,6 +453,15 @@ "config/packages/security.yaml" ] }, + "symfony/stimulus-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.8", + "ref": "9e33a8a3794b603fb4be6c04ee5ecab901ce549e" + } + }, "symfony/translation": { "version": "6.2", "recipe": { diff --git a/templates/admin/field/services_enabled.html.twig b/templates/admin/field/services_enabled.html.twig new file mode 100644 index 0000000..f87d522 --- /dev/null +++ b/templates/admin/field/services_enabled.html.twig @@ -0,0 +1,17 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} +{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} +{% trans_default_domain 'EasyAdminBundle' %} + +{% if ea.crud.currentAction == 'detail' or not field.customOptions.get('renderAsSwitch') %} + + {{ (field.value == true ? 'label.true' : 'label.false')|trans }} + +{% else %} +
+ + +
+{% endif %} diff --git a/templates/admin/parameters.html.twig b/templates/admin/parameters.html.twig index 435fdda..d6354ca 100644 --- a/templates/admin/parameters.html.twig +++ b/templates/admin/parameters.html.twig @@ -14,12 +14,28 @@ {% block main %} {{ form_start(form) }}
-

{{ 'parameters.services.h2'|trans }}

+

{{ 'parameters.global.h2'|trans }}


- {{ form_widget(form.servicesEnabled) }} +

{{ 'parameters.services.h3'|trans }}

-

{{ 'parameters.senders.h2'|trans }}

+ {{ form_widget(form.globalServicesEnabled) }} + +

{{ 'parameters.paid_membership.h3'|trans }}

+ + {{ form_widget(form.globalPaidMembership) }} + + + +

{{ 'parameters.senders.h2'|trans }}


{{ form_row(form.notificationsSenderEmail) }} diff --git a/templates/pages/group/create.html.twig b/templates/pages/group/create.html.twig index 11ee476..a91f317 100644 --- a/templates/pages/group/create.html.twig +++ b/templates/pages/group/create.html.twig @@ -17,9 +17,20 @@ {{ form_row(form.name) }} {{ form_row(form.type) }} {{ form_row(form.membership) }} - {% if form.parent is defined and form.parent is not null %} - {{ form_row(form.parent) }} - {% endif %} + +
+ {% if form.servicesEnabled is defined %} +
+ {{ form_widget(form.servicesEnabled) }} +
+ {% endif %} + {% if form.parent is defined and form.parent is not null %} + {{ form_row(form.parent) }} + {% endif %} +
{{ form_widget(form.submit) }} {{ form_end(form) }}
diff --git a/tests/Functional/Controller/Admin/PlatformOfferCrudControllerTest.php b/tests/Functional/Controller/Admin/PlatformOfferCrudControllerTest.php new file mode 100644 index 0000000..e2a1d71 --- /dev/null +++ b/tests/Functional/Controller/Admin/PlatformOfferCrudControllerTest.php @@ -0,0 +1,57 @@ +loginAsAdmin($client); + + // list+custom filters + $filters = '&filters[type]='.(OfferType::MONTHLY->isMonthly() ? '1' : '0'); + + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', PlatformOfferCrudController::class.'&'.$filters)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', PlatformOfferCrudController::class, TestReference::PLATFORM_OFFER_1)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', PlatformOfferCrudController::class, TestReference::PLATFORM_OFFER_1)); + self::assertResponseIsSuccessful(); + + // new + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', PlatformOfferCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'New special offer', + $form->getName().'[type]' => 'yearly', + $form->getName().'[price]' => 490, + $form->getName().'[currency]' => 'EUR', + $form->getName().'[active]' => false, + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Group/CreateGroupActionTest.php b/tests/Functional/Controller/Group/CreateGroupActionTest.php index 046eb95..7cf1226 100644 --- a/tests/Functional/Controller/Group/CreateGroupActionTest.php +++ b/tests/Functional/Controller/Group/CreateGroupActionTest.php @@ -33,7 +33,7 @@ final class CreateGroupActionTest extends WebTestCase $client->submit($form, [ $form->getName().'[name]' => 'Groupe 1', $form->getName().'[type]' => 'public', - $form->getName().'[parent]' => TestReference::GROUP_1, + $form->getName().'[servicesEnabled]' => false, ]); self::assertResponseRedirects(); self::assertTrue(u($client->getResponse()->headers->get('Location'))->startsWith('http://localhost/admin')); diff --git a/tests/TestReference.php b/tests/TestReference.php index dc43b23..2bb1347 100644 --- a/tests/TestReference.php +++ b/tests/TestReference.php @@ -127,4 +127,7 @@ final class TestReference // payments final public const PAYMENT_USER_16_1 = '1edcefc9-45b3-6a3e-b4a6-db137f56da56'; + + // platform offer + final public const PLATFORM_OFFER_1 = '9040b3fb-8a01-4bbf-a228-ca9f90db5034'; } diff --git a/tests/Unit/Controller/Payment/DoneActionTest.php b/tests/Unit/Controller/Payment/DoneActionTest.php index bc4c817..1d18ce7 100644 --- a/tests/Unit/Controller/Payment/DoneActionTest.php +++ b/tests/Unit/Controller/Payment/DoneActionTest.php @@ -39,7 +39,7 @@ final class DoneActionTest extends TestCase ->setId($this->getUuid()) ->setSlug('group'); - return (new GroupOffer()) + return (new GroupOffer()) // @phpstan-ignore-line ->setId($this->getUuid()) ->setGroup($group); } diff --git a/tests/Unit/Enum/Group/GroupOfferTypeTest.php b/tests/Unit/Enum/Group/GroupOfferTypeTest.php index 392f199..c69b24a 100644 --- a/tests/Unit/Enum/Group/GroupOfferTypeTest.php +++ b/tests/Unit/Enum/Group/GroupOfferTypeTest.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace App\Tests\Unit\Enum\Group; -use App\Enum\Group\GroupOfferType; +use App\Enum\OfferType; use PHPUnit\Framework\TestCase; final class GroupOfferTypeTest extends TestCase { public function testGroupOfferType(): void { - self::assertTrue(GroupOfferType::MONTHLY->isMonthly()); - self::assertTrue(GroupOfferType::YEARLY->isYearly()); + self::assertTrue(OfferType::MONTHLY->isMonthly()); + self::assertTrue(OfferType::YEARLY->isYearly()); } } diff --git a/translations/admin.fr.xlf b/translations/admin.fr.xlf index cbd466e..db23fa1 100644 --- a/translations/admin.fr.xlf +++ b/translations/admin.fr.xlf @@ -260,11 +260,21 @@ Paramètres de l'instance - - parameters.services.h2 + + parameters.global.h2 + Paramètres généraux de la plateforme + + + + parameters.services.h3 Services + + parameters.paid_membership.h3 + Adhésion payante + + parameters.senders.h2 Expéditeur·rice des notifications diff --git a/translations/app/Controller/Admin/PlatformOfferCrudController/admin.fr.xlf b/translations/app/Controller/Admin/PlatformOfferCrudController/admin.fr.xlf new file mode 100644 index 0000000..8d81373 --- /dev/null +++ b/translations/app/Controller/Admin/PlatformOfferCrudController/admin.fr.xlf @@ -0,0 +1,20 @@ + + + +
+ +
+ + + platform_offers + Tarifs d'adhésion pour la plateforme + + + + PlatformOffer + Tarifs d'adhésion + + + +
+
diff --git a/translations/group/admin.fr.xlf b/translations/group/admin.fr.xlf index c2b00d8..fd333a5 100644 --- a/translations/group/admin.fr.xlf +++ b/translations/group/admin.fr.xlf @@ -25,6 +25,11 @@ Envoi d'invitations par les admins possible
+ + Services Enabled + Activer la disponibilité des services pour le groupe + + Invitation By Moderator Envoi d'invitations par les modérateur·rice·s possible diff --git a/translations/parameters/admin.fr.xlf b/translations/parameters/admin.fr.xlf index 87cfc04..d7e5243 100644 --- a/translations/parameters/admin.fr.xlf +++ b/translations/parameters/admin.fr.xlf @@ -6,12 +6,22 @@ - + parameter.services Services activés + + parameter.paid_membership + Adhésion à la plateforme payante + + + + parameter.set_price + Configurer le tarif + + parameter.mail diff --git a/translations/templates/pages/group/create/messages.fr.xlf b/translations/templates/pages/group/create/messages.fr.xlf index 5776265..331a35b 100644 --- a/translations/templates/pages/group/create/messages.fr.xlf +++ b/translations/templates/pages/group/create/messages.fr.xlf @@ -26,6 +26,11 @@ Type + + templates.pages.group.create.form.servicesEnabled + Activer la disponibilité des services pour le groupe + + templates.pages.group.create.form.membership Tarif diff --git a/webpack.config.js b/webpack.config.js index fef2287..a6620c4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,7 @@ Encore * and one CSS file (e.g. app.css) if your JavaScript imports CSS. */ .addEntry('app', './assets/app.js') + .addEntry('admin', './assets/admin.js') // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js) .enableStimulusBridge('./assets/controllers.json')