diff --git a/config/services.yaml b/config/services.yaml index 787d54c..2c09a95 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -266,6 +266,7 @@ services: - '@App\RequiredTeamerCheck\DriverLicenseRequiredCheck' - '@App\RequiredTeamerCheck\FinancialDataRequiredCheck' - '@App\RequiredTeamerCheck\PersonalDataVerificationRequiredCheck' + - '@App\RequiredTeamerCheck\MealPreferenceRequiredCheck' App\RequiredTeamerCheck\PersonalDataVerificationRequiredCheck: arguments: diff --git a/migrations/Version20260827101926.php b/migrations/Version20260827101926.php new file mode 100644 index 0000000..b91db46 --- /dev/null +++ b/migrations/Version20260827101926.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE teamer ADD meal_preference VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE teamer DROP meal_preference'); + } +} diff --git a/src/Controller/Teamer/Check/MealPreferenceController.php b/src/Controller/Teamer/Check/MealPreferenceController.php new file mode 100644 index 0000000..3a27142 --- /dev/null +++ b/src/Controller/Teamer/Check/MealPreferenceController.php @@ -0,0 +1,62 @@ +getUser(); + $teamer = $user->getTeamer(); + + if (null === $teamer) { + $teamer = new Teamer(); + $user->setTeamer($teamer); + $this->entityManager->persist($teamer); + $this->entityManager->flush(); + } + + $formData = MealPreferenceDto::fromTeamer($teamer); + $form = $this->createForm(MealPreferenceType::class, $formData); + $form->handleRequest($request); + + if (true === $form->isSubmitted() && true === $form->isValid()) { + $formData->applyTo($teamer); + $this->entityManager->flush(); + + $this->addFlash('success', 'Deine Angaben wurden gespeichert.'); + $this->logger->info('Submit teamer meal preference', [ + 'teamer_id' => $teamer->getId(), + 'teamer_name' => (string) $teamer, + ]); + + return $this->redirectToRoute('app_teamer_index'); + } + + return $this->render('teamer/check/meal_preference.html.twig', [ + 'form' => $form->createView(), + 'teamer' => $teamer, + ]); + } +} diff --git a/src/Entity/Teamer.php b/src/Entity/Teamer.php index 8f70c2f..849b94b 100644 --- a/src/Entity/Teamer.php +++ b/src/Entity/Teamer.php @@ -8,6 +8,7 @@ use App\Entity\Embeddable\BankAccount; use App\Entity\Embeddable\Communication; use App\Entity\Traits\SoftDeletableEntity; use App\Entity\Traits\TimestampableEntity; +use App\Enum\MealPreference; use App\Repository\TeamerRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -138,6 +139,10 @@ class Teamer implements TimestampableEntityInterface, SoftDeletableEntityInterfa #[ORM\Column(length: 3, nullable: true)] private ?string $size = null; + #[ORM\Column(nullable: true, enumType: MealPreference::class)] + #[Assert\NotNull(message: 'Bitte gib deine Ernährungsweise an', groups: ['profile', 'profile_preflight'])] + private ?MealPreference $mealPreference = null; + #[ORM\Column(nullable: true)] private ?array $crmSelections = null; @@ -923,6 +928,18 @@ class Teamer implements TimestampableEntityInterface, SoftDeletableEntityInterfa return $this; } + public function getMealPreference(): ?MealPreference + { + return $this->mealPreference; + } + + public function setMealPreference(?MealPreference $mealPreference): static + { + $this->mealPreference = $mealPreference; + + return $this; + } + public function getCrmSelections(): ?array { return $this->crmSelections; diff --git a/src/Enum/MealPreference.php b/src/Enum/MealPreference.php new file mode 100644 index 0000000..cbbe25c --- /dev/null +++ b/src/Enum/MealPreference.php @@ -0,0 +1,19 @@ + 'Vegetarisch', + self::VEGAN => 'Vegan', + self::NONE => 'Keine Präferenz', + }; + } +} diff --git a/src/Form/MealPreferenceType.php b/src/Form/MealPreferenceType.php new file mode 100644 index 0000000..e006a1c --- /dev/null +++ b/src/Form/MealPreferenceType.php @@ -0,0 +1,32 @@ +add('mealPreference', EnumType::class, [ + 'label' => 'Ernährungsweise', + 'class' => MealPreference::class, + 'choice_label' => 'label', + 'expanded' => true, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => MealPreferenceDto::class, + ]); + } +} diff --git a/src/Form/TeamerProfileType.php b/src/Form/TeamerProfileType.php index ec662f3..e48179f 100644 --- a/src/Form/TeamerProfileType.php +++ b/src/Form/TeamerProfileType.php @@ -3,12 +3,14 @@ namespace App\Form; use App\Entity\Teamer; +use App\Enum\MealPreference; use App\Model\UploadSessionDto; use App\Service\Upload\UploadHandler; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\BirthdayType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; @@ -93,6 +95,11 @@ class TeamerProfileType extends AbstractType 'XXL' => 'XXL', ], ]) + ->add('mealPreference', EnumType::class, [ + 'label' => 'Ernährungsweise', + 'class' => MealPreference::class, + 'choice_label' => 'label', + ]) ; } diff --git a/src/Model/MealPreferenceDto.php b/src/Model/MealPreferenceDto.php new file mode 100644 index 0000000..b903942 --- /dev/null +++ b/src/Model/MealPreferenceDto.php @@ -0,0 +1,35 @@ +setMealPreference($teamer->getMealPreference()); + } + + public function applyTo(Teamer $teamer): void + { + $teamer->setMealPreference($this->getMealPreference()); + } + + public function getMealPreference(): ?MealPreference + { + return $this->mealPreference; + } + + public function setMealPreference(?MealPreference $mealPreference): static + { + $this->mealPreference = $mealPreference; + + return $this; + } +} diff --git a/src/RequiredTeamerCheck/MealPreferenceRequiredCheck.php b/src/RequiredTeamerCheck/MealPreferenceRequiredCheck.php new file mode 100644 index 0000000..bac500e --- /dev/null +++ b/src/RequiredTeamerCheck/MealPreferenceRequiredCheck.php @@ -0,0 +1,29 @@ +getTeamer(); + + if (null === $teamer) { + return false; + } + + return null !== $teamer->getMealPreference(); + } + + public function getRouteName(): string + { + return 'app_teamer_check_meal_preference'; + } +} diff --git a/templates/administrative/teamer/modal_info.html.twig b/templates/administrative/teamer/modal_info.html.twig index d78eeb5..3c395ea 100644 --- a/templates/administrative/teamer/modal_info.html.twig +++ b/templates/administrative/teamer/modal_info.html.twig @@ -15,6 +15,16 @@
{% include '_partials/_teamer_training_info.html.twig' %} {% include '_partials/_teamer_license_info.html.twig' %} +

+ Ernährungsweise +

+

+ {% if teamer.mealPreference %} + {{ teamer.mealPreference.label }} + {% else %} + _ + {% endif %} +

Zustiege

diff --git a/templates/administrative/teamer/profile.html.twig b/templates/administrative/teamer/profile.html.twig index 7a2a0fb..06baa12 100644 --- a/templates/administrative/teamer/profile.html.twig +++ b/templates/administrative/teamer/profile.html.twig @@ -222,6 +222,18 @@ {{ teamer.size|default('-') }}
+
+
+ Ernährungsweise: +
+
+ {% if teamer.mealPreference %} + {{ teamer.mealPreference.label }} + {% else %} + _ + {% endif %} +
+
diff --git a/templates/teamer/check/meal_preference.html.twig b/templates/teamer/check/meal_preference.html.twig new file mode 100644 index 0000000..1df433f --- /dev/null +++ b/templates/teamer/check/meal_preference.html.twig @@ -0,0 +1,36 @@ +{% extends 'teamer/layout.html.twig' %} + +{% block title %}Ernährungsweise{% endblock %} + +{% block content %} + {{ form_start(form) }} + +
+

+ Ernährungsweise +

+

+ Damit wir bei deinen Einsätzen die Verpflegung passend planen können, benötigen wir einmalig deine + Ernährungsweise. Bitte wähle eine der folgenden Optionen: +

+ + {% if not form.vars.valid %} + {% embed '_partials/_alert.html.twig' with { 'type': 'warning' } %} + {% block message %} + {% include '_partials/_form_errors.html.twig' with { 'form': form } %} + {% endblock %} + {% endembed %} + {% endif %} + +
+ {{ form_row(form.mealPreference) }} +
+ + +
+ + {{ form_rest(form) }} + {{ form_end(form) }} +{% endblock %} diff --git a/templates/teamer/profile/index.html.twig b/templates/teamer/profile/index.html.twig index 4886296..e5ac68f 100644 --- a/templates/teamer/profile/index.html.twig +++ b/templates/teamer/profile/index.html.twig @@ -127,6 +127,7 @@ {{ form_row(form.healthInsuranceCompany) }} {{ form_row(form.language) }} {{ form_row(form.size) }} + {{ form_row(form.mealPreference) }}
Führerschein
diff --git a/tests/Form/MealPreferenceTypeTest.php b/tests/Form/MealPreferenceTypeTest.php new file mode 100644 index 0000000..48de8fa --- /dev/null +++ b/tests/Form/MealPreferenceTypeTest.php @@ -0,0 +1,62 @@ +setRoles(['ROLE_TEAMER']) + ->setTeamer($teamer) + ; + + $check = new MealPreferenceRequiredCheck(); + $this->assertFalse($check->isSatisfied($user)); + + $formData = MealPreferenceDto::fromTeamer($teamer); + $form = $this->createForm($formData); + $form->submit(['mealPreference' => MealPreference::VEGETARIAN->value]); + $formData->applyTo($teamer); + + $this->assertTrue($check->isSatisfied($user)); + } + + public function testEmptySubmitIsRejected(): void + { + $form = $this->createForm(new MealPreferenceDto()); + $form->submit(['mealPreference' => '']); + + $this->assertFalse($form->isValid()); + $this->assertCount(1, $form->get('mealPreference')->getErrors()); + } + + private function createForm(MealPreferenceDto $formData): FormInterface + { + self::bootKernel(); + + /** @var FormFactoryInterface $formFactory */ + $formFactory = self::getContainer()->get(FormFactoryInterface::class); + + return $formFactory->create(MealPreferenceType::class, $formData, [ + 'csrf_protection' => false, + ]); + } +} diff --git a/tests/Security/RequiredCheck/MealPreferenceRequiredCheckTest.php b/tests/Security/RequiredCheck/MealPreferenceRequiredCheckTest.php new file mode 100644 index 0000000..5f4f4ff --- /dev/null +++ b/tests/Security/RequiredCheck/MealPreferenceRequiredCheckTest.php @@ -0,0 +1,78 @@ +check = new MealPreferenceRequiredCheck(); + } + + /** + * @dataProvider mixedRoleProvider + */ + public function testDoesNotApplyToExcludedMixedRoleUsersWithTeamerRole(string $excludedRole): void + { + $user = $this->createTeamerUser(); + $user->setRoles([$excludedRole, 'ROLE_TEAMER']); + + $this->assertFalse($this->check->appliesTo($user)); + } + + public function mixedRoleProvider(): array + { + return [ + ['ROLE_ADMIN'], + ['ROLE_MANAGER'], + ['ROLE_HOUSE_MANAGER'], + ]; + } + + public function testRedirectsToItsOwnCheckPage(): void + { + $this->assertSame('app_teamer_check_meal_preference', $this->check->getRouteName()); + } + + public function testIsNotSatisfiedWithoutTeamer(): void + { + $user = (new User())->setRoles(['ROLE_TEAMER']); + + $this->assertFalse($this->check->isSatisfied($user)); + } + + public function testIsNotSatisfiedWhenMealPreferenceIsMissing(): void + { + $user = $this->createTeamerUser(); + + $this->assertFalse($this->check->isSatisfied($user)); + } + + public function testIsSatisfiedWhenMealPreferenceIsSet(): void + { + $user = $this->createTeamerUser(); + $user->getTeamer()?->setMealPreference(MealPreference::NONE); + + $this->assertTrue($this->check->isSatisfied($user)); + } + + private function createTeamerUser(): User + { + $teamer = new Teamer(); + + return (new User()) + ->setRoles(['ROLE_TEAMER']) + ->setTeamer($teamer) + ; + } +}