diff --git a/src/Controller/Booking/Create/Step2Controller.php b/src/Controller/Booking/Create/Step2Controller.php index e43d262..a72fa01 100644 --- a/src/Controller/Booking/Create/Step2Controller.php +++ b/src/Controller/Booking/Create/Step2Controller.php @@ -94,8 +94,10 @@ class Step2Controller extends AbstractController return $this->hxRedirect($request, $this->generateUrl('app_booking_create_step_3')); } - // Generate cards data - $cardsData = $this->generateAllCardsData($bookingCreateDto); + // Generate cards data with validation state if form was submitted and failed + $cardsData = (true === $form->isSubmitted() && false === $form->isValid()) + ? $this->participantCardService->getAllCardsDataWithValidation($bookingCreateDto, ['booking_create']) + : $this->generateAllCardsData($bookingCreateDto); // Calculate summary data $summaryData = $this->calculateSummaryData($bookingCreateDto); diff --git a/src/Service/ParticipantCardDataService.php b/src/Service/ParticipantCardDataService.php index e848226..637f95d 100644 --- a/src/Service/ParticipantCardDataService.php +++ b/src/Service/ParticipantCardDataService.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace App\Service; use App\Form\Model\BookingDto; +use App\Form\Model\ParticipantEditDto; +use Symfony\Component\Validator\Validator\ValidatorInterface; /** * Extracts card display data for participants in card-based booking flows. @@ -16,13 +18,14 @@ class ParticipantCardDataService { public function __construct( private readonly BookingPriceCalculatorService $priceCalculator, + private readonly ValidatorInterface $validator, ) { } /** * Get card data for a single participant. * - * @return array{name: string, roomName: string, price: string} + * @return array{name: string, roomName: string, price: string, isCanceled: bool} */ public function getCardData(BookingDto $bookingDto, int $index): array { @@ -41,17 +44,21 @@ class ParticipantCardDataService // Calculate and format individual price $price = $this->getFormattedPrice($bookingDto, $index); + // Check if participant is canceled + $isCanceled = $participant->isCanceled(); + return [ 'name' => $name, 'roomName' => $roomName, 'price' => $price, + 'isCanceled' => $isCanceled, ]; } /** * Get card data for all participants. * - * @return array + * @return array */ public function getAllCardsData(BookingDto $bookingDto): array { @@ -121,4 +128,59 @@ class ParticipantCardDataService return number_format($price, 2, ',', '.').' €'; } + + /** + * Get card data for a single participant with validation state. + * + * @return array{name: string, roomName: string, price: string, isCanceled: bool, isValid: bool, errorMessages: array} + */ + public function getCardDataWithValidation(BookingDto $bookingDto, int $index, array $validationGroups): array + { + $participant = $bookingDto->participants[$index] ?? null; + + if (null === $participant) { + throw new \InvalidArgumentException(sprintf('Participant at index %d does not exist', $index)); + } + + // Get basic card data + $cardData = $this->getCardData($bookingDto, $index); + + // Wrap participant for validation + $wrapper = new ParticipantEditDto( + participant: $participant, + bookingContext: $bookingDto, + ); + + // Validate the wrapper DTO + $violations = $this->validator->validate($wrapper, null, $validationGroups); + + // Extract validation state + $isValid = 0 === count($violations); + $errorMessages = []; + + foreach ($violations as $violation) { + $errorMessages[] = $violation->getMessage(); + } + + return array_merge($cardData, [ + 'isValid' => $isValid, + 'errorMessages' => $errorMessages, + ]); + } + + /** + * Get card data for all participants with validation state. + * + * @return array}> + */ + public function getAllCardsDataWithValidation(BookingDto $bookingDto, array $validationGroups): array + { + $cardsData = []; + + foreach ($bookingDto->participants as $index => $participant) { + $cardsData[$index] = $this->getCardDataWithValidation($bookingDto, $index, $validationGroups); + } + + return $cardsData; + } } diff --git a/templates/booking/_participant_card.html.twig b/templates/booking/_participant_card.html.twig index b8413b3..fcaec11 100644 --- a/templates/booking/_participant_card.html.twig +++ b/templates/booking/_participant_card.html.twig @@ -1,8 +1,7 @@ {# Compact participant card with name, room, price, and edit button #} -{% set isCanceled = isCanceled|default(false) %} -{% set hasErrors = hasErrors|default(false) %} -{% set isValid = true %} -{% set errorMessages = errorMessages|default([]) %} +{% set isCanceled = cardData.isCanceled is defined ? cardData.isCanceled : false %} +{% set isValid = cardData.isValid is defined ? cardData.isValid : true %} +{% set errorMessages = cardData.errorMessages|default([]) %} {% set mode = mode|default('create') %}
- {{ form_row(form.email, { - 'attr': { - 'hx-trigger': 'change', - 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), - 'hx-target': '#main-content', - 'hx-swap': 'innerHTML' - } - }) }} + {{ form_row(form.email) }} {{ form_row(form.mobile) }}
diff --git a/templates/booking/create/step_2.html.twig b/templates/booking/create/step_2.html.twig index 4e4f27b..398ec38 100644 --- a/templates/booking/create/step_2.html.twig +++ b/templates/booking/create/step_2.html.twig @@ -9,6 +9,8 @@ {% block participant_cards %}
{{ form_start(form, { + 'method': 'POST', + 'action': path('app_booking_create_step_2'), 'attr': { 'hx-post': path('app_booking_create_step_2'), 'hx-target': '#main-content', diff --git a/tests/Service/ParticipantCardDataServiceTest.php b/tests/Service/ParticipantCardDataServiceTest.php index 7374ce1..6e72d67 100644 --- a/tests/Service/ParticipantCardDataServiceTest.php +++ b/tests/Service/ParticipantCardDataServiceTest.php @@ -11,16 +11,19 @@ use App\Form\Model\ParticipantDto; use App\Service\BookingPriceCalculatorService; use App\Service\ParticipantCardDataService; use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Validator\ValidatorInterface; class ParticipantCardDataServiceTest extends TestCase { private ParticipantCardDataService $service; private BookingPriceCalculatorService $priceCalculator; + private ValidatorInterface $validator; protected function setUp(): void { $this->priceCalculator = $this->createMock(BookingPriceCalculatorService::class); - $this->service = new ParticipantCardDataService($this->priceCalculator); + $this->validator = $this->createMock(ValidatorInterface::class); + $this->service = new ParticipantCardDataService($this->priceCalculator, $this->validator); } public function testGetCardDataWithFullParticipantData(): void @@ -347,4 +350,134 @@ class ParticipantCardDataServiceTest extends TestCase $this->assertEquals('Teilnehmer:in 1', $result2['name']); $this->assertEquals('Teilnehmer:in 2', $result3['name']); } + + public function testGetCardDataWithValidationReturnsValidCard(): void + { + $room = new Room(); + $room->id = 1; + $room->label = 'Doppelzimmer'; + + $travel = new Travel(); + $travel->rooms = [$room]; + + $participant = new ParticipantDto(); + $participant->firstName = 'Max'; + $participant->lastName = 'Mustermann'; + $participant->assignedRoomId = 1; + $participant->email = 'max@example.com'; + + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $this->priceCalculator + ->method('calculateAllParticipantIndividualPrices') + ->willReturn([450.50]); + + // Mock validator to return no violations (valid) + $violations = $this->createMock(\Symfony\Component\Validator\ConstraintViolationListInterface::class); + $violations->expects($this->once()) + ->method('count') + ->willReturn(0); + + $this->validator + ->expects($this->once()) + ->method('validate') + ->willReturn($violations); + + $result = $this->service->getCardDataWithValidation($bookingDto, 0, ['booking_create']); + + $this->assertEquals('Max Mustermann', $result['name']); + $this->assertEquals('Doppelzimmer', $result['roomName']); + $this->assertEquals('450,50 €', $result['price']); + $this->assertTrue($result['isValid']); + $this->assertEmpty($result['errorMessages']); + } + + public function testGetCardDataWithValidationReturnsInvalidCard(): void + { + $travel = new Travel(); + $travel->rooms = []; + + $participant = new ParticipantDto(); + $participant->firstName = 'Max'; + $participant->lastName = 'Mustermann'; + $participant->email = 'duplicate@example.com'; + + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $this->priceCalculator + ->method('calculateAllParticipantIndividualPrices') + ->willReturn([0.0]); + + // Mock validator to return violations (invalid) + $violation = $this->createMock(\Symfony\Component\Validator\ConstraintViolationInterface::class); + $violation->expects($this->once()) + ->method('getMessage') + ->willReturn('Diese E-Mail Adresse wird bereits von einem anderen Teilnehmer verwendet'); + + // Use ConstraintViolationList directly instead of mocking + $violations = new \Symfony\Component\Validator\ConstraintViolationList([$violation]); + + $this->validator + ->expects($this->once()) + ->method('validate') + ->willReturn($violations); + + $result = $this->service->getCardDataWithValidation($bookingDto, 0, ['booking_create']); + + $this->assertEquals('Max Mustermann', $result['name']); + $this->assertFalse($result['isValid']); + $this->assertCount(1, $result['errorMessages']); + $this->assertEquals('Diese E-Mail Adresse wird bereits von einem anderen Teilnehmer verwendet', $result['errorMessages'][0]); + } + + public function testGetAllCardsDataWithValidationReturnsAllCards(): void + { + $room = new Room(); + $room->id = 1; + $room->label = 'Doppelzimmer'; + + $travel = new Travel(); + $travel->rooms = [$room]; + + $participant1 = new ParticipantDto(); + $participant1->firstName = 'Max'; + $participant1->lastName = 'Mustermann'; + $participant1->assignedRoomId = 1; + $participant1->email = 'max@example.com'; + + $participant2 = new ParticipantDto(); + $participant2->firstName = 'Anna'; + $participant2->lastName = 'Schmidt'; + $participant2->assignedRoomId = 1; + $participant2->email = 'anna@example.com'; + + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant1, $participant2]; + + $this->priceCalculator + ->expects($this->exactly(2)) + ->method('calculateAllParticipantIndividualPrices') + ->willReturn([450.0, 500.0]); + + // Mock validator to return no violations for both participants + $violations = $this->createMock(\Symfony\Component\Validator\ConstraintViolationListInterface::class); + $violations->expects($this->exactly(2)) + ->method('count') + ->willReturn(0); + + $this->validator + ->expects($this->exactly(2)) + ->method('validate') + ->willReturn($violations); + + $result = $this->service->getAllCardsDataWithValidation($bookingDto, ['booking_create']); + + $this->assertCount(2, $result); + $this->assertTrue($result[0]['isValid']); + $this->assertTrue($result[1]['isValid']); + $this->assertEquals('Max Mustermann', $result[0]['name']); + $this->assertEquals('Anna Schmidt', $result[1]['name']); + } }