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

This commit is contained in:
Björn Fromme committed 2026-08-03 15:25:10 +02:00
1 parent eabed8295a
commit 9d2aa11fdb
258 files changed
+16231 -582

No files matched your search

@@ -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 (0–3 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']);
}
}