feat: refactored accommodation booking status model

This commit is contained in:
Björn Fromme
2026-08-19 11:46:52 +02:00
parent e8accbb3ed
commit 5a3957e143
42 changed files with 1190 additions and 277 deletions
@@ -6,8 +6,8 @@ namespace App\Form\Admin\Filter;
use App\Entity\Groups\Accommodation;
use App\Entity\User;
use App\Enum\Groups\AccommodationBookingOrigin;
use App\Enum\Groups\AccommodationBookingStatus;
use App\Enum\Groups\AccommodationBookingType;
use App\Form\Model\Filter\AccommodationBookingFilterDto;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -30,10 +30,10 @@ class AccommodationBookingFilterType extends AbstractListFilterType
'expanded' => true,
'required' => false,
]],
'type' => [EnumType::class, [
'label' => 'Art',
'class' => AccommodationBookingType::class,
'choice_label' => static fn (AccommodationBookingType $type) => $type->label(),
'origin' => [EnumType::class, [
'label' => 'Herkunft',
'class' => AccommodationBookingOrigin::class,
'choice_label' => static fn (AccommodationBookingOrigin $origin) => $origin->label(),
'multiple' => true,
'expanded' => true,
'required' => false,
@@ -6,15 +6,12 @@ namespace App\Form\Admin\Groups;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\AccommodationBooking;
use App\Enum\Groups\AccommodationBookingStatus;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -50,17 +47,9 @@ class AccommodationBookingCreateType extends AbstractType
->add('childrenCount', IntegerType::class, [
'label' => 'davon Kinder',
])
->add('status', EnumType::class, [
'label' => 'Status',
'class' => AccommodationBookingStatus::class,
'choices' => [
AccommodationBookingStatus::Draft,
AccommodationBookingStatus::Open,
],
'choice_label' => fn (AccommodationBookingStatus $status) => $status->label(),
])
// Not marked required in HTML: a draft may be saved without it, and the
// validation group below enforces it for everything else.
// Not marked required in HTML: a new record is always an Entwurf, which is a
// scratch record nobody is pointed at yet. The address is enforced when the
// offer is actually sent — see SendOfferController.
->add('email', EmailType::class, [
'label' => 'E-Mail',
'required' => false,
@@ -73,15 +62,10 @@ class AccommodationBookingCreateType extends AbstractType
{
$resolver->setDefaults([
'data_class' => AccommodationBooking::class,
// A draft is a scratch record, but anything the customer can be pointed at
// needs a reachable address — see the `offer` group on the entity. The rest of
// the contact data is collected on the edit page, which validates `edit`.
'validation_groups' => static function (FormInterface $form): array {
/** @var AccommodationBooking $booking */
$booking = $form->getData();
return $booking->isDraft() ? ['Default'] : ['Default', 'offer'];
},
// Creation always yields an Entwurf, a scratch record the office fills in over
// time, so nothing beyond the basics is required here. The contact data is
// collected on the edit page, which validates `edit`.
'validation_groups' => ['Default'],
]);
}
}
@@ -8,13 +8,11 @@ use App\Entity\Groups\AccommodationBooking;
use App\Entity\Groups\AdditionalService;
use App\Entity\Groups\BoardService;
use App\Entity\User;
use App\Enum\Groups\AccommodationBookingStatus;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
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\EnumType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -137,11 +135,8 @@ class AccommodationBookingType extends AbstractType
}
$builder
->add('status', EnumType::class, [
'label' => 'Status',
'class' => AccommodationBookingStatus::class,
'choice_label' => fn (AccommodationBookingStatus $status) => $status->label(),
])
// No status field: the status is only ever moved by a named action, so that it
// can never end up set without the timestamp and email that belong to it.
->add('accommodationDiscount', IntegerType::class, [
'label' => 'Rabatt Unterkunft (%)',
'required' => false,
@@ -175,9 +170,9 @@ class AccommodationBookingType extends AbstractType
$resolver->setDefaults([
'data_class' => AccommodationBooking::class,
// Entwurf is a scratch record the office fills in over time, and an Absage is
// never going anywhere — neither needs complete contact data. The groups are
// resolved after binding, so promoting a draft to Anfrage or Buchung enforces
// the contact data in the very same save.
// never going anywhere — neither needs complete contact data. Everything else,
// Angefragt included, has already been in contact with the customer and is
// expected to carry the full set.
'validation_groups' => static function (FormInterface $form): array {
/** @var AccommodationBooking $booking */
$booking = $form->getData();
@@ -6,8 +6,8 @@ namespace App\Form\Model\Filter;
use App\Entity\Groups\Accommodation;
use App\Entity\User;
use App\Enum\Groups\AccommodationBookingOrigin;
use App\Enum\Groups\AccommodationBookingStatus;
use App\Enum\Groups\AccommodationBookingType;
use App\Model\ListFilterChip;
class AccommodationBookingFilterDto extends AbstractListFilterDto
@@ -15,8 +15,8 @@ class AccommodationBookingFilterDto extends AbstractListFilterDto
/** @var AccommodationBookingStatus[] */
public array $status = [];
/** @var AccommodationBookingType[] */
public array $type = [];
/** @var AccommodationBookingOrigin[] */
public array $origin = [];
public ?User $managedBy = null;
@@ -38,15 +38,17 @@ class AccommodationBookingFilterDto extends AbstractListFilterDto
/**
* The list as it presents itself to someone who has not filtered yet.
*
* The still-live statuses — drafts, open bookings and bookings awaiting validation, i.e.
* everything that may still need work — nothing whose stay is already over, and, for
* staff who are not group admins, only the bookings they are responsible for.
* The still-live statuses — drafts, inquiries awaiting an offer, offers out with the
* customer and bookings awaiting validation, i.e. everything that may still need work —
* nothing whose stay is already over, and, for staff who are not group admins, only the
* bookings they are responsible for.
*/
public static function defaults(bool $seesAllBookings, ?User $currentUser): self
{
$filter = new self();
$filter->status = [
AccommodationBookingStatus::Draft,
AccommodationBookingStatus::Requested,
AccommodationBookingStatus::Open,
AccommodationBookingStatus::Received,
];
@@ -72,11 +74,11 @@ class AccommodationBookingFilterDto extends AbstractListFilterDto
);
}
if ([] !== $this->type) {
if ([] !== $this->origin) {
$chips[] = new ListFilterChip(
'Art',
implode(', ', array_map(static fn (AccommodationBookingType $t) => $t->label(), $this->type)),
['type'],
'Herkunft',
implode(', ', array_map(static fn (AccommodationBookingOrigin $o) => $o->label(), $this->origin)),
['origin'],
);
}