feat: split nights off an accommodation booking as separate periods

addresses #869f6z36v
This commit is contained in:
fromme committed 2026-09-30 18:06:20 +02:00
1 parent 972839286c
commit 7001bc0cf2
35 files changed
+2221 -25

No files matched your search

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Form\Model\AccommodationBookingPeriodLineDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/** @extends AbstractType<AccommodationBookingPeriodLineDto> */
class AccommodationBookingPeriodLineType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('label', TextType::class, [
'label' => 'Bezeichnung',
])
->add('amount', MoneyType::class, [
'label' => 'Betrag',
'currency' => $options['currency'],
'divisor' => 100,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AccommodationBookingPeriodLineDto::class,
'empty_data' => static fn () => new AccommodationBookingPeriodLineDto(),
'currency' => 'EUR',
]);
$resolver->setAllowedTypes('currency', 'string');
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Form\Model\AccommodationBookingPeriodDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
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<AccommodationBookingPeriodDto> */
class AccommodationBookingPeriodType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('label', TextType::class, [
'label' => 'Bezeichnung',
'required' => false,
'help' => 'Überschrift in der Preisaufstellung, z. B. „Vorausteam“. Ohne Bezeichnung steht dort nur der Zeitraum.',
])
->add('nights', IntegerType::class, [
'label' => 'Nächte',
'attr' => ['min' => 1],
])
->add('lines', CollectionType::class, [
'label' => 'Positionen',
'entry_type' => AccommodationBookingPeriodLineType::class,
'entry_options' => ['label' => false, 'currency' => $options['currency']],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'prototype' => true,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AccommodationBookingPeriodDto::class,
'currency' => 'EUR',
]);
$resolver->setAllowedTypes('currency', 'string');
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Form\Model;
use App\Entity\Groups\AccommodationBooking;
use App\Entity\Groups\AccommodationBookingPeriod;
use Symfony\Component\Validator\Constraints as Assert;
/**
* What the split and edit modals submit for a split-off period. Whether the group keeps a
* night for itself depends on the booking, so that is left to the booking's own validation.
*/
class AccommodationBookingPeriodDto
{
/**
* @param array<int, AccommodationBookingPeriodLineDto> $lines keyed as the form submits them, so not necessarily a list
*/
public function __construct(
#[Assert\NotNull(message: 'Bitte die Anzahl der Nächte angeben.')]
#[Assert\Range(min: 1, max: AccommodationBooking::MAX_NIGHTS)]
public ?int $nights = 1,
#[Assert\Valid]
public array $lines = [],
#[Assert\Length(max: 255)]
public ?string $label = null,
) {
}
public static function fromPeriod(AccommodationBookingPeriod $period): self
{
return new self(
$period->getNights(),
array_map(
fn (array $line) => new AccommodationBookingPeriodLineDto($line['label'], $line['amount']),
$period->getLines(),
),
$period->getLabel(),
);
}
/** @return list<array{label: string, amount: int}> */
public function linesSnapshot(): array
{
return array_values(array_map(
fn (AccommodationBookingPeriodLineDto $line) => ['label' => (string) $line->label, 'amount' => (int) $line->amount],
$this->lines,
));
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Form\Model;
use Symfony\Component\Validator\Constraints as Assert;
/** One free-form price line of a split-off period, as the office keys it in. */
class AccommodationBookingPeriodLineDto
{
public function __construct(
#[Assert\NotBlank(message: 'Bitte eine Bezeichnung angeben.')]
#[Assert\Length(max: 255)]
public ?string $label = null,
/** In minor units of the booking's pricing currency. */
#[Assert\NotNull(message: 'Bitte einen Betrag angeben.')]
#[Assert\PositiveOrZero]
public ?int $amount = null,
) {
}
}