wip: modernized edit flow
This commit is contained in:
@@ -33,6 +33,17 @@ class BankAccountDto
|
||||
#[Assert\IsTrue(message: 'Bitte akzeptieren Sie das SEPA-Mandat.')]
|
||||
public bool $sepaMandateAccepted = false;
|
||||
|
||||
public static function fromBankAccount(\App\BusProNet\Model\BankAccount $bankAccount): static
|
||||
{
|
||||
$instance = new static();
|
||||
$instance->iban = $bankAccount->iban;
|
||||
$instance->accountHolder = $bankAccount->holder;
|
||||
$instance->bankName = $bankAccount->bankName;
|
||||
$instance->sepaMandateAccepted = true;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns IBAN formatted with spaces for display (e.g., DE12 3456 7890 1234 5678 90).
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,11 @@ class BookingCreateDto implements BookingDtoInterface
|
||||
{
|
||||
}
|
||||
|
||||
public function getMode(): string
|
||||
{
|
||||
return BookingDtoInterface::MODE_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RoomSelectionDto>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Model;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
/**
|
||||
* Unified DTO for both booking creation and editing workflows.
|
||||
*
|
||||
* This DTO consolidates the previously separate BookingCreateDto and BookingEditDto
|
||||
* into a single class that handles both modes. All service selections are stored
|
||||
* in participant DTOs regardless of mode, ensuring consistent data structure and
|
||||
* simplifying pricing calculations, field handlers, and template rendering.
|
||||
*/
|
||||
class BookingDto
|
||||
{
|
||||
public const MODE_CREATE = 'create';
|
||||
public const MODE_EDIT = 'edit';
|
||||
|
||||
public int $currentStep = 1;
|
||||
|
||||
/**
|
||||
* @var array<int, RoomSelectionDto>
|
||||
*/
|
||||
#[Assert\Valid]
|
||||
public array $roomSelections = [];
|
||||
|
||||
/**
|
||||
* @var array<int, ParticipantDto>
|
||||
*/
|
||||
#[Assert\Valid]
|
||||
public array $participants = [];
|
||||
|
||||
#[Assert\Choice(
|
||||
choices: [Constants::PAYMENT_METHOD_TRANSFER, Constants::PAYMENT_METHOD_DEBIT],
|
||||
message: 'Bitte wählen Sie eine gültige Zahlungsart.'
|
||||
)]
|
||||
public ?string $paymentMethod = Constants::PAYMENT_METHOD_TRANSFER;
|
||||
|
||||
public ?BankAccountDto $bankAccount = null;
|
||||
|
||||
public ?int $agencyId = null;
|
||||
|
||||
/**
|
||||
* Reference to booking entity (only populated in edit mode).
|
||||
*/
|
||||
public ?Booking $booking = null;
|
||||
|
||||
public function __construct(public Travel $travel, public int $hotelId)
|
||||
{
|
||||
}
|
||||
|
||||
public function getMode(): string
|
||||
{
|
||||
return null !== $this->booking ? self::MODE_EDIT : self::MODE_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RoomSelectionDto>
|
||||
*/
|
||||
public function getSelectedRooms(): array
|
||||
{
|
||||
// In edit mode, rooms are fixed - return empty array
|
||||
if (self::MODE_EDIT === $this->getMode()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter($this->roomSelections, function (RoomSelectionDto $roomSelection) {
|
||||
return 0 < $roomSelection->quantity;
|
||||
});
|
||||
}
|
||||
|
||||
public function getParticipants(): array
|
||||
{
|
||||
return $this->participants;
|
||||
}
|
||||
|
||||
public function hasParticipant(int $index): bool
|
||||
{
|
||||
return isset($this->participants[$index]);
|
||||
}
|
||||
|
||||
public function getParticipant(int $index): ?ParticipantDto
|
||||
{
|
||||
return $this->participants[$index] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this is a family booking based on participant age distribution.
|
||||
*
|
||||
* A family booking is defined as:
|
||||
* - 1 or 2 participants aged 18 or older (adults)
|
||||
* - At least 1 participant younger than 18 (children)
|
||||
*/
|
||||
public function isFamilyBooking(): bool
|
||||
{
|
||||
$adults = 0;
|
||||
$children = 0;
|
||||
|
||||
$travelStartDate = $this->travel->dateFrom;
|
||||
|
||||
foreach ($this->participants as $participant) {
|
||||
$age = $participant->getAge($travelStartDate);
|
||||
|
||||
if (null === $age) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($age >= 18) {
|
||||
++$adults;
|
||||
} else {
|
||||
++$children;
|
||||
}
|
||||
}
|
||||
|
||||
return ($adults >= 1 && $adults <= 2) && ($children >= 1);
|
||||
}
|
||||
|
||||
public function isCanceled(): bool
|
||||
{
|
||||
return null !== $this->booking && 'S' === $this->booking->status;
|
||||
}
|
||||
|
||||
public function isOption(): bool
|
||||
{
|
||||
return null !== $this->booking && 'O' === $this->booking->status;
|
||||
}
|
||||
|
||||
#[Assert\Callback(callback: 'validateRoomSelection', groups: ['booking_create_step_1'])]
|
||||
public function validateRoomSelection(ExecutionContextInterface $context): void
|
||||
{
|
||||
$selectedRooms = $this->getSelectedRooms();
|
||||
|
||||
if (0 === count($selectedRooms)) {
|
||||
$context->buildViolation('Bitte mindestens ein Zimmer/Bett auswählen')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function validateBankAccount(ExecutionContextInterface $context): void
|
||||
{
|
||||
if (Constants::PAYMENT_METHOD_DEBIT !== $this->paymentMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $this->bankAccount) {
|
||||
$context->buildViolation('Bitte geben Sie Ihre Bankverbindung an.')
|
||||
->atPath('bankAccount')
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $this->bankAccount->iban || '' === trim($this->bankAccount->iban)) {
|
||||
$context->buildViolation('Bitte geben Sie Ihre IBAN ein.')
|
||||
->atPath('bankAccount.iban')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $this->bankAccount->accountHolder || '' === trim($this->bankAccount->accountHolder)) {
|
||||
$context->buildViolation('Bitte geben Sie den Kontoinhaber ein.')
|
||||
->atPath('bankAccount.accountHolder')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (false === $this->bankAccount->sepaMandateAccepted) {
|
||||
$context->buildViolation('Bitte akzeptieren Sie das SEPA-Mandat.')
|
||||
->atPath('bankAccount.sepaMandateAccepted')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,16 @@ namespace App\Form\Model;
|
||||
*/
|
||||
interface BookingDtoInterface
|
||||
{
|
||||
public const MODE_CREATE = 'create';
|
||||
public const MODE_EDIT = 'edit';
|
||||
|
||||
/**
|
||||
* Gets the booking mode (create or edit).
|
||||
*
|
||||
* @return string One of MODE_CREATE or MODE_EDIT constants
|
||||
*/
|
||||
public function getMode(): string;
|
||||
|
||||
/**
|
||||
* Gets all participants in the booking.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,11 @@ class BookingEditDto implements BookingDtoInterface
|
||||
{
|
||||
}
|
||||
|
||||
public function getMode(): string
|
||||
{
|
||||
return BookingDtoInterface::MODE_EDIT;
|
||||
}
|
||||
|
||||
public static function fromBooking(Booking $booking, Travel $travel): static
|
||||
{
|
||||
$instance = new static($booking, $travel);
|
||||
@@ -30,7 +35,9 @@ class BookingEditDto implements BookingDtoInterface
|
||||
|
||||
foreach ($booking->participants as $index => $participant) {
|
||||
/** @var PersonalData $participant */
|
||||
$participantData = ParticipantDto::fromPersonalData($participant);
|
||||
// For the first participant (applicant), use applicant data instead of participant data
|
||||
$personalData = 0 === $index && null !== $booking->applicant ? $booking->applicant : $participant;
|
||||
$participantData = ParticipantDto::fromPersonalData($personalData);
|
||||
$participantData->index = $index;
|
||||
|
||||
$participantData->courses = $booking
|
||||
@@ -102,7 +109,10 @@ class BookingEditDto implements BookingDtoInterface
|
||||
/**
|
||||
* Gets all selected rooms for the booking (edit context).
|
||||
*
|
||||
* @return array<int, RoomSelectionDto> always returns an empty array for edit DTOs unless implemented
|
||||
* In edit mode, rooms are fixed and not selectable - returns empty array.
|
||||
* Participant count should be derived from actual participants, not room selections.
|
||||
*
|
||||
* @return array<int, RoomSelectionDto>
|
||||
*/
|
||||
public function getSelectedRooms(): array
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Validator\Constraints as AppAssert;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[AppAssert\Participant(groups: ['booking_edit', 'booking_create_step_2'])]
|
||||
#[AppAssert\ApplicantAddress(groups: ['booking_create_step_2'])]
|
||||
class ParticipantDto
|
||||
{
|
||||
/**
|
||||
@@ -132,6 +133,12 @@ class ParticipantDto
|
||||
$instance->weight = $personalData->weight;
|
||||
$instance->shoeSize = $personalData->shoeSize;
|
||||
|
||||
// Clone address to prevent shared object references that could cause mutations
|
||||
$instance->address = null !== $personalData->address ? clone $personalData->address : null;
|
||||
|
||||
$instance->remarksRoom = $personalData->remarksRoom;
|
||||
$instance->licensePlate = $personalData->licensePlate;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user