feat: create new bookings or inquiries on behalf of customer

This commit is contained in:
Björn Fromme
2026-08-04 17:29:05 +02:00
parent 5f2252af71
commit dc86e715cb
8 changed files with 149 additions and 83 deletions
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260804152215 extends AbstractMigration
{
public function getDescription(): string
{
return 'Make accommodation booking contact fields nullable (filled on the edit page after creation)';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE accommodation_booking CHANGE first_name first_name VARCHAR(100) DEFAULT NULL, CHANGE last_name last_name VARCHAR(100) DEFAULT NULL, CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE group_name group_name VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE accommodation_booking CHANGE group_name group_name VARCHAR(255) NOT NULL, CHANGE first_name first_name VARCHAR(100) NOT NULL, CHANGE last_name last_name VARCHAR(100) NOT NULL, CHANGE email email VARCHAR(255) NOT NULL');
}
}
@@ -5,7 +5,8 @@ declare(strict_types=1);
namespace App\Controller\Admin\AccommodationBooking;
use App\Entity\Groups\AccommodationBooking;
use App\Form\Admin\Groups\AccommodationBookingType;
use App\Form\Admin\Groups\AccommodationBookingCreateType;
use App\Htmx\HxRedirectResponse;
use App\Service\AccommodationBookingService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@@ -30,8 +31,8 @@ class CreateController extends AbstractController
{
$booking = new AccommodationBooking();
$form = $this->createForm(AccommodationBookingType::class, $booking, [
'with_accommodation' => true,
$form = $this->createForm(AccommodationBookingCreateType::class, $booking, [
'hx_post' => $request->getRequestUri(),
]);
$form->handleRequest($request);
@@ -40,20 +41,18 @@ class CreateController extends AbstractController
$this->entityManager->persist($booking);
$this->entityManager->flush();
$this->bookingService->issueAccessLinkForDirectBooking($booking);
$this->bookingService->sendCustomerConfirmationEmail($booking);
$this->addFlash('success', 'Die Buchung wurde erstellt');
$this->logger->info('Created accommodation booking', [
'id' => $booking->getId(),
'groupName' => $booking->getGroupName(),
]);
return $this->redirectToRoute('app_admin_accommodationbooking_edit', ['id' => $booking->getId()]);
return new HxRedirectResponse(
$this->generateUrl('app_admin_accommodationbooking_edit', ['id' => $booking->getId()])
);
}
return $this->render('admin/accommodation_booking/create.html.twig', [
return $this->render('admin/accommodation_booking/modal_create.html.twig', [
'form' => $form,
]);
}
+13 -8
View File
@@ -84,19 +84,24 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $acceptedAt = null;
#[ORM\Column(length: 255)]
#[ORM\Column(length: 255, nullable: true)]
#[Assert\NotBlank(groups: ['edit'])]
private ?string $groupName = null;
#[ORM\Column(length: 10, nullable: true)]
private ?string $salutation = null;
#[ORM\Column(length: 100)]
#[ORM\Column(length: 100, nullable: true)]
#[Assert\NotBlank(groups: ['edit'])]
private ?string $firstName = null;
#[ORM\Column(length: 100)]
#[ORM\Column(length: 100, nullable: true)]
#[Assert\NotBlank(groups: ['edit'])]
private ?string $lastName = null;
#[ORM\Column(length: 255)]
#[ORM\Column(length: 255, nullable: true)]
#[Assert\NotBlank(groups: ['edit'])]
#[Assert\Email(groups: ['edit'])]
private ?string $email = null;
#[ORM\Column(length: 50, nullable: true)]
@@ -371,7 +376,7 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
return $this->groupName;
}
public function setGroupName(string $groupName): self
public function setGroupName(?string $groupName): self
{
$this->groupName = $groupName;
@@ -395,7 +400,7 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
return $this->firstName;
}
public function setFirstName(string $firstName): self
public function setFirstName(?string $firstName): self
{
$this->firstName = $firstName;
@@ -407,7 +412,7 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
return $this->lastName;
}
public function setLastName(string $lastName): self
public function setLastName(?string $lastName): self
{
$this->lastName = $lastName;
@@ -419,7 +424,7 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
return $this->email;
}
public function setEmail(string $email): self
public function setEmail(?string $email): self
{
$this->email = $email;
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\AccommodationBooking;
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\DateType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Lean form for creating a booking. The available board and additional services can only be
* resolved once accommodation and date range are known, so they are offered on the edit page.
*
* @extends AbstractType<AccommodationBooking>
*/
class AccommodationBookingCreateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('accommodation', EntityType::class, [
'class' => Accommodation::class,
'choice_label' => 'name',
'label' => 'Gruppenhaus',
])
->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' => 'davon Kinder',
])
->add('isInquiry', CheckboxType::class, [
'label' => 'Anfrage (nicht bindend)',
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AccommodationBooking::class,
]);
}
}
@@ -4,7 +4,6 @@ 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;
@@ -26,14 +25,6 @@ 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',
@@ -158,14 +149,13 @@ class AccommodationBookingType extends AbstractType
{
$resolver->setDefaults([
'data_class' => AccommodationBooking::class,
'with_accommodation' => false,
'validation_groups' => ['Default', 'edit'],
'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');
@@ -1,53 +0,0 @@
{% extends 'layout_admin.html.twig' %}
{% block content %}
<twig:page:heading>Neue Buchung</twig:page:heading>
{% form_theme form 'forms_admin.html.twig' %}
{{ form_start(form) }}
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
<div class="lg:col-span-2">
{{ form_row(form.accommodation) }}
</div>
<h2 class="lg:col-span-2 text-base font-bold pt-2">Kontaktdaten</h2>
{{ form_row(form.groupName) }}
{{ form_row(form.salutation) }}
{{ form_row(form.firstName) }}
{{ form_row(form.lastName) }}
{{ form_row(form.email) }}
{{ form_row(form.phone) }}
{{ form_row(form.street) }}
{{ form_row(form.zip) }}
{{ form_row(form.city) }}
<h2 class="lg:col-span-2 text-base font-bold pt-2">Reisedaten</h2>
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
{{ form_row(form.paxCount) }}
{{ form_row(form.minorsCount) }}
{{ form_row(form.childrenCount) }}
<h2 class="lg:col-span-2 text-base font-bold pt-2">Status & Rabatt</h2>
<div class="lg:col-span-2">
{{ form_row(form.isInquiry) }}
</div>
{{ form_row(form.accommodationDiscount) }}
{{ form_row(form.boardServiceDiscount) }}
{{ form_row(form.additionalServicesDiscount) }}
<div class="lg:col-span-2">
{{ form_row(form.remarks) }}
</div>
</div>
<div class="flex justify-between pt-8">
<a href="{{ path('app_admin_accommodationbooking') }}" class="button button--secondary button--small">
zurück
</a>
<button type="submit" class="button button--primary button--small">
speichern
</button>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
@@ -93,8 +93,12 @@
</div>
</div>
<div class="flex justify-end">
<a href="{{ path('app_admin_accommodationbooking_create') }}" class="button button--primary button--small">
<button type="button"
hx-get="{{ path('app_admin_accommodationbooking_create') }}"
hx-target="body"
hx-swap="beforeend"
class="button button--primary button--small">
neu
</a>
</button>
</div>
{% endblock %}
@@ -0,0 +1,29 @@
{% extends 'htmx_modal_admin.html.twig' %}
{% form_theme form 'forms_admin.html.twig' %}
{% block content %}
<div class="text-xl font-bold pb-4">
Neue Buchung
</div>
{{ form_start(form) }}
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
<div class="lg:col-span-2">
{{ form_row(form.accommodation) }}
</div>
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
{{ form_row(form.paxCount) }}
{{ form_row(form.minorsCount) }}
{{ form_row(form.childrenCount) }}
<div class="lg:col-span-2">
{{ form_row(form.isInquiry) }}
</div>
</div>
<div class="flex justify-end pt-8">
<button type="submit" class="button button--primary button--small">
speichern
</button>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}