feat: groups price calculator admin crud, booking/offer flow and api

This commit is contained in:
Björn Fromme
2026-08-03 15:25:10 +02:00
parent eabed8295a
commit 9d2aa11fdb
258 changed files with 16231 additions and 582 deletions
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Form\Model\AccommodationBookingDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
/**
* @extends AbstractType<AccommodationBookingDto>
*/
class AccommodationBookingConfirmationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$termsLink = sprintf(
'<a href="%s" target="_blank" rel="noopener" class="underline">allgemeinen Geschäftsbedingungen (AGB)</a>',
htmlspecialchars($options['terms_url'], ENT_QUOTES, 'UTF-8')
);
$builder->add('termsAccepted', CheckboxType::class, [
'label' => sprintf('Ich habe die %s gelesen und bin damit einverstanden, dass eine kostenpflichtige Buchung zustande kommt.', $termsLink),
'label_html' => true,
'mapped' => false,
'required' => true,
'constraints' => [
new IsTrue(message: 'Bitte akzeptiere die AGB, um die Buchung abzuschließen.'),
],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AccommodationBookingDto::class,
]);
$resolver->setRequired('terms_url');
$resolver->setAllowedTypes('terms_url', 'string');
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<null>
*/
class AccommodationInquiryConfirmationType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'csrf_token_id' => 'groups_booking_step4_inquiry',
]);
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Form\Model\AccommodationBookingDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<AccommodationBookingDto>
*/
class AccommodationStep2Type extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('paxCount', IntegerType::class, [
'label' => 'Anzahl Personen',
'attr' => ['min' => 1],
])
->add('minorsCount', IntegerType::class, [
'label' => 'davon Kinder (03 Jahre)',
'required' => false,
'attr' => ['min' => 0],
])
->add('childrenCount', IntegerType::class, [
'label' => sprintf('davon Kinder (4%d Jahre)', $options['max_adolescent_age']),
'required' => false,
'attr' => ['min' => 0],
])
->add('selectedBoardServiceId', ChoiceType::class, [
'label' => false,
'choices' => $options['board_service_choices'],
'expanded' => true,
'multiple' => false,
'required' => false,
'placeholder' => 'Selbstversorgung',
])
->add('selectedAdditionalServiceIds', ChoiceType::class, [
'label' => false,
'choices' => $options['additional_service_choices'],
'multiple' => true,
'expanded' => false,
'required' => false,
])
;
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event): void {
$data = $event->getData();
if (!is_array($data)) {
return;
}
foreach (['minorsCount', 'childrenCount'] as $field) {
if (!isset($data[$field]) || '' === $data[$field]) {
$data[$field] = 0;
}
}
// Board service rendered manually: empty string from the "Selbstversorgung" radio → null
if (isset($data['selectedBoardServiceId']) && '' === $data['selectedBoardServiceId']) {
unset($data['selectedBoardServiceId']);
}
$event->setData($data);
});
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AccommodationBookingDto::class,
'validation_groups' => ['step_2'],
'board_service_choices' => [],
'additional_service_choices' => [],
'max_adolescent_age' => 0,
]);
$resolver->setAllowedTypes('max_adolescent_age', 'int');
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Form\Model\AccommodationBookingDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<AccommodationBookingDto>
*/
class AccommodationStep3Type extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('groupName', TextType::class, [
'label' => 'Name der Gruppe',
])
->add('salutation', ChoiceType::class, [
'label' => 'Anrede',
'choices' => [
'Herr' => 'Herr',
'Frau' => 'Frau',
'divers' => 'divers',
],
'expanded' => false,
'multiple' => false,
'placeholder' => 'Bitte wählen',
])
->add('firstName', TextType::class, [
'label' => 'Vorname',
])
->add('lastName', TextType::class, [
'label' => 'Nachname',
])
->add('email', EmailType::class, [
'label' => 'E-Mail-Adresse',
])
->add('phone', TextType::class, [
'label' => 'Telefon',
])
->add('street', TextType::class, [
'label' => 'Straße und Hausnummer',
])
->add('zip', TextType::class, [
'label' => 'Postleitzahl',
])
->add('city', TextType::class, [
'label' => 'Ort',
])
->add('remarks', TextareaType::class, [
'label' => false,
'required' => false,
'attr' => ['rows' => 4],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AccommodationBookingDto::class,
'validation_groups' => ['step_3'],
]);
}
}
@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\AccommodationBooking;
use App\Entity\Groups\AdditionalService;
use App\Entity\Groups\BoardService;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Range;
/** @extends AbstractType<AccommodationBooking> */
class AccommodationBookingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if ($options['with_accommodation']) {
$builder->add('accommodation', EntityType::class, [
'class' => Accommodation::class,
'choice_label' => 'name',
'label' => 'Gruppenhaus',
]);
}
$builder
->add('groupName', TextType::class, [
'label' => 'Gruppenname',
])
->add('salutation', ChoiceType::class, [
'label' => 'Anrede',
'choices' => [
'Herr' => 'Herr',
'Frau' => 'Frau',
'divers' => 'divers',
],
'required' => false,
'placeholder' => '',
])
->add('firstName', TextType::class, [
'label' => 'Vorname',
])
->add('lastName', TextType::class, [
'label' => 'Nachname',
])
->add('email', EmailType::class, [
'label' => 'E-Mail',
])
->add('phone', TextType::class, [
'label' => 'Telefon',
'required' => false,
])
->add('street', TextType::class, [
'label' => 'Straße',
'required' => false,
])
->add('zip', TextType::class, [
'label' => 'PLZ',
'required' => false,
])
->add('city', TextType::class, [
'label' => 'Ort',
'required' => false,
])
->add('dateFrom', DateType::class, [
'label' => 'Anreise',
'widget' => 'single_text',
])
->add('dateTo', DateType::class, [
'label' => 'Abreise',
'widget' => 'single_text',
])
->add('paxCount', IntegerType::class, [
'label' => 'Anzahl Personen',
])
->add('minorsCount', IntegerType::class, [
'label' => 'davon Kinder (03 Jahre)',
])
->add('childrenCount', IntegerType::class, [
'label' => $options['max_adolescent_age'] > 0
? sprintf('davon Kinder (4%d Jahre)', $options['max_adolescent_age'])
: 'davon Kinder',
])
;
if (!empty($options['board_services'])) {
$builder->add('boardService', EntityType::class, [
'class' => BoardService::class,
'mapped' => false,
'required' => false,
'choices' => $options['board_services'],
'choice_label' => 'label',
'placeholder' => 'keine',
'label' => 'Verpflegung',
'data' => $options['current_board_service'],
]);
}
if (!empty($options['additional_services'])) {
$builder->add('selectedAdditionalServices', EntityType::class, [
'class' => AdditionalService::class,
'mapped' => false,
'required' => false,
'multiple' => true,
'expanded' => true,
'choices' => $options['additional_services'],
'choice_label' => 'label',
'label' => 'Zusatzleistungen',
'data' => $options['current_additional_services'],
]);
}
$builder
->add('isInquiry', CheckboxType::class, [
'label' => 'Anfrage (nicht bindend)',
'required' => false,
])
->add('accommodationDiscount', IntegerType::class, [
'label' => 'Rabatt Unterkunft (%)',
'required' => false,
'constraints' => [
new Range(min: 1, max: 100),
],
])
->add('boardServiceDiscount', IntegerType::class, [
'label' => 'Rabatt Verpflegung (%)',
'required' => false,
'constraints' => [
new Range(min: 1, max: 100),
],
])
->add('additionalServicesDiscount', IntegerType::class, [
'label' => 'Rabatt Zusatzleistungen (%)',
'required' => false,
'constraints' => [
new Range(min: 1, max: 100),
],
])
->add('remarks', TextareaType::class, [
'label' => 'Bemerkungen',
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AccommodationBooking::class,
'with_accommodation' => false,
'max_adolescent_age' => 0,
'board_services' => [],
'additional_services' => [],
'current_board_service' => null,
'current_additional_services' => [],
]);
$resolver->setAllowedTypes('with_accommodation', 'bool');
$resolver->setAllowedTypes('max_adolescent_age', 'int');
$resolver->setAllowedTypes('board_services', 'array');
$resolver->setAllowedTypes('additional_services', 'array');
$resolver->setAllowedTypes('current_board_service', ['null', BoardService::class]);
$resolver->setAllowedTypes('current_additional_services', 'array');
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\AccommodationPrice;
use App\Enum\Groups\PriceType;
use App\Enum\Groups\Season;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/** @extends AbstractType<AccommodationPrice> */
class AccommodationPriceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('dateFrom', DateType::class, [
'label' => 'Datum von',
'html5' => true,
'widget' => 'single_text',
])
->add('dateTo', DateType::class, [
'label' => 'Datum bis',
'html5' => true,
'widget' => 'single_text',
])
->add('type', EnumType::class, [
'label' => 'Typ',
'class' => PriceType::class,
'required' => false,
'placeholder' => 'Standard',
'choice_label' => fn($item) => $item->label(),
])
->add('season', EnumType::class, [
'label' => 'Saison',
'class' => Season::class,
'choice_label' => fn($item) => $item->token(),
])
->add('includedPax', IntegerType::class, [
'label' => 'Inklusiv-Personen',
])
->add('minNights', IntegerType::class, [
'label' => 'Mindestbelegung (Nächte)',
])
->add('pricePerNight', MoneyType::class, [
'label' => 'Preis pro Nacht',
'currency' => $options['currency'],
'divisor' => 100,
])
->add('priceAdditionalPerson', MoneyType::class, [
'label' => 'Preis weitere Person',
'currency' => $options['currency'],
'divisor' => 100,
])
->add('acceptUndersubscription', CheckboxType::class, [
'label' => 'Unterbelegung akzeptieren',
'required' => false,
])
->add('acceptShortTerm', CheckboxType::class, [
'label' => 'Kurzzeit-Belegung akzeptieren',
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AccommodationPrice::class,
'currency' => 'EUR',
]);
$resolver->setAllowedValues('currency', ['EUR', 'CHF']);
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\Accommodation;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/** @extends AbstractType<Accommodation> */
class AccommodationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'Name',
])
->add('calendarCode', TextType::class, [
'label' => 'Code für Kalender',
])
->add('cmsCode', TextType::class, [
'label' => 'Code für CMS-Daten',
'required' => false,
])
->add('maxAdolescentAge', IntegerType::class, [
'label' => 'Altersgrenze Kinder',
])
->add('currency', ChoiceType::class, [
'label' => 'Währung',
'choices' => [
'EUR' => 'EUR',
'CHF' => 'CHF',
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Accommodation::class,
]);
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\AdditionalService;
use App\Enum\Groups\AdditionalServiceType as AdditionalServiceTypeEnum;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/** @extends AbstractType<AdditionalService> */
class AdditionalServiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('label', TextType::class, [
'label' => 'Bezeichnung',
])
->add('description', TextareaType::class, [
'label' => 'Beschreibung',
'required' => false,
'attr' => [
'rows' => 2,
],
])
->add('type', EnumType::class, [
'label' => 'Preistyp',
'class' => AdditionalServiceTypeEnum::class,
'choice_label' => fn($item) => $item->label(),
])
->add('price', MoneyType::class, [
'label' => 'Preis',
'currency' => $options['currency'],
'divisor' => 100,
])
->add('selectionGroup', TextType::class, [
'label' => 'Auswahlgruppe',
'required' => false,
'disabled' => true,
])
->add('dateFrom', DateType::class, [
'label' => 'Datum von',
'html5' => true,
'widget' => 'single_text',
])
->add('dateTo', DateType::class, [
'label' => 'Datum bis',
'html5' => true,
'widget' => 'single_text',
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AdditionalService::class,
'currency' => 'EUR',
]);
$resolver->setAllowedValues('currency', ['EUR', 'CHF']);
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\BoardService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/** @extends AbstractType<BoardService> */
class BoardServiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('label', TextType::class, [
'label' => 'Bezeichnung',
])
->add('description', TextareaType::class, [
'label' => 'Beschreibung',
'required' => false,
'attr' => [
'rows' => 2,
],
])
->add('price', MoneyType::class, [
'label' => 'Preis pro Person/Nacht',
'currency' => $options['currency'],
'divisor' => 100,
])
->add('dateFrom', DateType::class, [
'label' => 'Datum von',
'html5' => true,
'widget' => 'single_text',
])
->add('dateTo', DateType::class, [
'label' => 'Datum bis',
'html5' => true,
'widget' => 'single_text',
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => BoardService::class,
'currency' => 'EUR',
]);
$resolver->setAllowedValues('currency', ['EUR', 'CHF']);
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/** @extends AbstractType<mixed> */
class DatepickerType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'html5' => false,
'widget' => 'single_text',
'input' => 'datetime_immutable',
'min_date' => null,
'max_date' => null,
'disable_weekends' => false,
]);
$resolver->setAllowedTypes('min_date', ['null', \DateTimeImmutable::class]);
$resolver->setAllowedTypes('max_date', ['null', \DateTimeImmutable::class]);
$resolver->setAllowedTypes('disable_weekends', 'bool');
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['min_date'] = $options['min_date'];
$view->vars['max_date'] = $options['max_date'];
$view->vars['disable_weekends'] = $options['disable_weekends'];
}
public function getParent(): string
{
return DateType::class;
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ModalSubmitExtension extends AbstractTypeExtension
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'hx_post' => null,
'hx_target' => '#htmx-modal',
'hx_swap' => 'outerHTML',
]);
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
if (null !== $options['hx_post']) {
$attr = [
'hx-post' => $options['hx_post'],
'hx-target' => $options['hx_target'],
'hx-swap' => $options['hx_swap'],
'hx-indicator' => '#htmx-modal-indicator',
];
$view->vars['attr'] = array_merge($view->vars['attr'], $attr);
}
}
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Form\Model;
use Symfony\Component\Validator\Constraints as Assert;
class AccommodationBookingDto
{
public int $currentStep = 1;
public ?int $accommodationId = null;
public ?\DateTimeImmutable $dateFrom = null;
public ?\DateTimeImmutable $dateTo = null;
#[Assert\Positive(message: 'invalid', groups: ['step_3'])]
public int $paxCount = 1;
#[Assert\PositiveOrZero(message: 'invalid', groups: ['step_3'])]
public int $minorsCount = 0;
#[Assert\PositiveOrZero(message: 'invalid', groups: ['step_3'])]
public int $childrenCount = 0;
public ?int $selectedBoardServiceId = null;
/** @var list<int>*/
public array $selectedAdditionalServiceIds = [];
public bool $isInquiry = false;
/** @var string[] */
public array $inquiryReasons = [];
public bool $forceInquiry = false;
public ?int $totalPrice = null;
/** @var array<string, mixed> */
public array $priceBreakdown = [];
#[Assert\NotBlank(message: 'required', groups: ['step_3'])]
public ?string $groupName = null;
#[Assert\NotBlank(message: 'required', groups: ['step_3'])]
#[Assert\Choice(choices: ['Herr', 'Frau', 'divers'], groups: ['step_3'])]
public ?string $salutation = null;
#[Assert\NotBlank(message: 'required', groups: ['step_3'])]
public ?string $firstName = null;
#[Assert\NotBlank(message: 'required', groups: ['step_3'])]
public ?string $lastName = null;
#[Assert\NotBlank(message: 'required', groups: ['step_3'])]
#[Assert\Email(message: 'invalid', groups: ['step_3'])]
public ?string $email = null;
#[Assert\NotBlank(message: 'required', groups: ['step_3'])]
public ?string $phone = null;
#[Assert\NotBlank(message: 'required', groups: ['step_3'])]
public ?string $street = null;
#[Assert\NotBlank(message: 'required', groups: ['step_3'])]
public ?string $zip = null;
#[Assert\NotBlank(message: 'required', groups: ['step_3'])]
public ?string $city = null;
public ?string $remarks = null;
public function getNights(): int
{
if (null === $this->dateFrom || null === $this->dateTo) {
return 0;
}
return $this->dateFrom->diff($this->dateTo)->days;
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\Model;
use App\Model\BookingSummaryCmsHotelData;
use App\Model\CmsHotelData;
/**
* DTO containing all booking summary data for sidebar display.
@@ -19,7 +19,7 @@ class BookingSummaryDto
public readonly int $participantCount,
public readonly BookingSummaryPricingDto $pricing,
public readonly BookingSummaryVoucherDto $vouchers,
public readonly ?BookingSummaryCmsHotelData $cmsData,
public readonly ?CmsHotelData $cmsData,
) {
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
/**
* @extends AbstractType<null>
*/
class OfferAcceptConfirmationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$termsLink = sprintf(
'<a href="%s" target="_blank" rel="noopener" class="underline">allgemeinen Geschäftsbedingungen (AGB)</a>',
htmlspecialchars($options['terms_url'], ENT_QUOTES, 'UTF-8')
);
$builder->add('termsAccepted', CheckboxType::class, [
'label' => sprintf('Ich habe die %s gelesen und bin damit einverstanden, dass eine kostenpflichtige Buchung zustande kommt.', $termsLink),
'label_html' => true,
'mapped' => false,
'required' => true,
'constraints' => [
new IsTrue(message: 'Bitte akzeptiere die AGB, um die Buchung abzuschließen.'),
],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired('terms_url');
$resolver->setAllowedTypes('terms_url', 'string');
}
}