diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 99b6271..ac48044 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(php -l:*)" + "Bash(php -l:*)", + "Bash(php:*)" ], "deny": [] } diff --git a/src/BusProNet/DataProcessor/BookingDataProcessor.php b/src/BusProNet/DataProcessor/BookingDataProcessor.php index 8d3850e..643b238 100644 --- a/src/BusProNet/DataProcessor/BookingDataProcessor.php +++ b/src/BusProNet/DataProcessor/BookingDataProcessor.php @@ -336,7 +336,7 @@ class BookingDataProcessor { foreach ($bookingData->participants as $index => $participant) { $payload['teilnehmerliste']['teilnehmer'][] = [ - '@id' => $index, + '@id' => $index + 1, 'status' => $bookingData->participantsStatus[$index], ...$participant->toPayload(), ]; @@ -358,7 +358,7 @@ class BookingDataProcessor $payload['zusatzleistungen']['zusatzleistung'][] = [ '@idleistung' => $service->id, '@anzahl' => count($service->mapping), - '@zuordnung' => implode(',', $service->mapping), + '@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $service->mapping)), ]; } @@ -366,7 +366,7 @@ class BookingDataProcessor $payload['beförderungen']['beförderung'][] = [ '@idleistung' => $service->id, '@anzahl' => count($service->mapping), - '@zuordnung' => implode(',', $service->mapping), + '@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $service->mapping)), ]; } @@ -378,7 +378,7 @@ class BookingDataProcessor '@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null, '@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null, '@anzahl' => $room->totalCount, - '@zuordnung' => implode(',', $room->mapping), + '@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $room->mapping)), ]; } } @@ -399,7 +399,7 @@ class BookingDataProcessor $payload['zustiege']['zustieg'][] = [ '@idzustieg' => $pickup->id, '@anzahl' => count($pickup->mapping), - '@zuordnung' => implode(',', $pickup->mapping), + '@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $pickup->mapping)), ]; } } diff --git a/src/BusProNet/Model/Booking.php b/src/BusProNet/Model/Booking.php index 95b39a1..d80716d 100644 --- a/src/BusProNet/Model/Booking.php +++ b/src/BusProNet/Model/Booking.php @@ -187,20 +187,24 @@ class Booking // Sum up all services foreach ([...$this->transportationServices, ...$this->additionalServices] as $service) { - if (in_array($participantIndex, $service->mapping) || true === $service->mandatory) { + if ((in_array($participantIndex, $service->mapping) || true === $service->mandatory) + && isset($service->individualPrice[$participantIndex])) { $price += $service->individualPrice[$participantIndex]; } } // Sum up all surcharges foreach ($this->surcharges as $surcharge) { - if (in_array($participantIndex, $surcharge->mapping)) { + if (in_array($participantIndex, $surcharge->mapping) + && isset($surcharge->individualPrice[$participantIndex])) { $price += $surcharge->individualPrice[$participantIndex]; } } $room = $this->getRoomForParticipant($participantIndex); - $price += $room->individualPrice[$participantIndex]; + if (null !== $room && isset($room->individualPrice[$participantIndex])) { + $price += $room->individualPrice[$participantIndex]; + } return $price; } diff --git a/src/BusProNet/XmlParser/BookingParser.php b/src/BusProNet/XmlParser/BookingParser.php index 70e589f..91d189e 100644 --- a/src/BusProNet/XmlParser/BookingParser.php +++ b/src/BusProNet/XmlParser/BookingParser.php @@ -49,7 +49,7 @@ class BookingParser extends AbstractParser $booking->applicant = $this->parsePersonalData($node->filterXPath('//anmelder')); $participantsStatus = $this->getArrayValue($node->filterXPath('//status_teilnehmer'), '/'); - $booking->participantsStatus = array_combine(range(1, count($participantsStatus)), $participantsStatus); + $booking->participantsStatus = array_combine(range(0, count($participantsStatus) - 1), $participantsStatus); $booking->participants = $this->parseParticipants($node->filterXPath('//teilnehmerliste/teilnehmer')); @@ -110,7 +110,7 @@ class BookingParser extends AbstractParser $node->each(function (Crawler $node) use (&$participants) { $id = (int) $node->attr('id'); - $participants[$id] = $this->parsePersonalData($node); + $participants[$id - 1] = $this->parsePersonalData($node); }); return $participants; diff --git a/src/BusProNet/XmlParser/PickupsParser.php b/src/BusProNet/XmlParser/PickupsParser.php index e70cc0a..47cd0d8 100644 --- a/src/BusProNet/XmlParser/PickupsParser.php +++ b/src/BusProNet/XmlParser/PickupsParser.php @@ -26,7 +26,7 @@ class PickupsParser extends AbstractParser $pickup->time = $this->stringToDateTime($pickupDate.' '.$pickupTime); $pickup->price = $price; $mapping = $this->stringToArray($node->attr('zuordnung')); - $pickup->mapping = array_map('intval', $mapping); + $pickup->mapping = array_map(function ($index) { return (int) $index - 1; }, $mapping); $pickups[$pickupId] = $pickup; }); diff --git a/src/BusProNet/XmlParser/RoomsParser.php b/src/BusProNet/XmlParser/RoomsParser.php index 4ff2166..b56fb06 100644 --- a/src/BusProNet/XmlParser/RoomsParser.php +++ b/src/BusProNet/XmlParser/RoomsParser.php @@ -24,14 +24,14 @@ class RoomsParser extends AbstractParser $room->maxPax = (int) $node->attr('maxpax'); $room->board = $node->attr('verpflegung'); $mapping = $this->stringToArray($node->attr('zuordnung')); - $room->mapping = array_map('intval', $mapping); + $room->mapping = array_map(function ($index) { return (int) $index - 1; }, $mapping); $room->totalPrice = $this->stringToFloat($node->attr('gesamtpreis')); $individualPrices = array_map( function ($price) { return $this->stringToFloat($price); }, $this->stringToArray($node->attr('einzelpreis', ''), '/') ); - $room->individualPrice = array_combine($mapping, $individualPrices); + $room->individualPrice = array_combine($room->mapping, $individualPrices); $rooms[$room->id] = $room; }); diff --git a/src/BusProNet/XmlParser/ServicesParser.php b/src/BusProNet/XmlParser/ServicesParser.php index 5983c26..2634fe2 100644 --- a/src/BusProNet/XmlParser/ServicesParser.php +++ b/src/BusProNet/XmlParser/ServicesParser.php @@ -23,14 +23,14 @@ class ServicesParser extends AbstractParser $service->subType = $node->attr('unterart'); $service->totalCount = $node->attr('anzahl') ? (int) $node->attr('anzahl') : null; $mapping = $this->stringToArray($node->attr('zuordnung')); - $service->mapping = array_map('intval', $mapping); + $service->mapping = array_map(function ($index) { return (int) $index - 1; }, $mapping); $service->totalPrice = $this->stringToFloat($node->attr('gesamtpreis')); $individualPrices = array_map( function ($price) { return $this->stringToFloat($price); }, $this->stringToArray($node->attr('einzelpreis', ''), '/') ); - $service->individualPrice = array_combine($mapping, $individualPrices); + $service->individualPrice = array_combine($service->mapping, $individualPrices); if (Constants::CATEGORY_TRANSPORTATION === $category) { $service->direction = $node->attr('richtung'); } diff --git a/src/BusProNet/XmlParser/SurchargesParser.php b/src/BusProNet/XmlParser/SurchargesParser.php index 222a701..d2c25af 100644 --- a/src/BusProNet/XmlParser/SurchargesParser.php +++ b/src/BusProNet/XmlParser/SurchargesParser.php @@ -17,13 +17,13 @@ class SurchargesParser extends AbstractParser $surcharge->label = empty($surchargeLabel) ? 'unbekannt' : $surchargeLabel; $surcharge->totalPrice = $this->stringToFloat($node->attr('gesamtpreis')); $mapping = $this->stringToArray($node->attr('zuordnung')); - $surcharge->mapping = array_map('intval', $mapping); + $surcharge->mapping = array_map(function ($index) { return (int) $index - 1; }, $mapping); $individualPrices = array_map( function ($price) { return $this->stringToFloat($price); }, $this->stringToArray($node->attr('einzelpreis', ''), '/') ); - $surcharge->individualPrice = array_combine($mapping, $individualPrices); + $surcharge->individualPrice = array_combine($surcharge->mapping, $individualPrices); $surcharges[] = $surcharge; }); diff --git a/src/Form/BookingCreateParticipantType.php b/src/Form/BookingCreateParticipantType.php index cf0f643..827a3a2 100644 --- a/src/Form/BookingCreateParticipantType.php +++ b/src/Form/BookingCreateParticipantType.php @@ -4,6 +4,7 @@ namespace App\Form; use App\BusProNet\Form\CountryType; use App\Form\Model\ParticipantDto; +use App\Form\Service\ParticipantRoomChoiceLoaderFactory; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\BirthdayType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -16,6 +17,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class BookingCreateParticipantType extends AbstractType { + public function __construct( + private readonly ParticipantRoomChoiceLoaderFactory $choiceLoaderFactory, + ) { + } + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder @@ -58,10 +64,16 @@ class BookingCreateParticipantType extends AbstractType $data = $event->getData(); $form = $event->getForm(); + $choiceLoader = $this->choiceLoaderFactory->createChoiceLoader( + $options['participant_adapter'], + $options['selected_rooms'], + $data->index + ); + $form->add('assignedRoomId', ChoiceType::class, [ 'label' => 'Zimmer', 'placeholder' => 'Bitte wählen', - 'choices' => $options['room_choices'][$data->index], + 'choice_loader' => $choiceLoader, ]); }) ; @@ -71,7 +83,8 @@ class BookingCreateParticipantType extends AbstractType { $resolver->setDefaults([ 'data_class' => ParticipantDto::class, - 'room_choices' => [], + 'participant_adapter' => null, + 'selected_rooms' => [], ]); } } diff --git a/src/Form/BookingCreateStep2Type.php b/src/Form/BookingCreateStep2Type.php index c3c5d6b..b4c503c 100644 --- a/src/Form/BookingCreateStep2Type.php +++ b/src/Form/BookingCreateStep2Type.php @@ -27,8 +27,7 @@ class BookingCreateStep2Type extends AbstractType $form = $event->getForm(); $adapter = new ParticipantDataAdapter($data->participants); - $roomChoices = $this->generateRoomChoices($adapter, $data->getSelectedRooms()); - $this->addParticipantsField($form, $roomChoices); + $this->addParticipantsField($form, $adapter, $data->getSelectedRooms()); } public function onPreSubmit(FormEvent $event): void @@ -39,73 +38,14 @@ class BookingCreateStep2Type extends AbstractType $bookingCreateDto = $form->getData(); $adapter = new ParticipantDataAdapter($data['participants']); - $roomChoices = $this->generateRoomChoices($adapter, $bookingCreateDto->getSelectedRooms()); $form->remove('participants'); - $this->addParticipantsField($form, $roomChoices); + $this->addParticipantsField($form, $adapter, $bookingCreateDto->getSelectedRooms()); } /** - * Calculates room occupancy based on participant assignments. - * - * @return array + * Adds the participants collection field to the form. */ - private function calculateRoomOccupancy(ParticipantDataAdapter $adapter): array - { - $roomOccupancy = []; - - foreach ($adapter->getParticipants() as $participant) { - $assignedRoomId = $adapter->getAssignedRoomId($participant); - if (null !== $assignedRoomId) { - $roomOccupancy[$assignedRoomId] = ($roomOccupancy[$assignedRoomId] ?? 0) + 1; - } - } - - return $roomOccupancy; - } - - /** - * Generates room choices for each participant based on availability and current assignments. - * - * @return array> - */ - private function generateRoomChoices(ParticipantDataAdapter $adapter, array $selectedRooms): array - { - $roomOccupancy = $this->calculateRoomOccupancy($adapter); - $roomChoices = []; - - foreach ($adapter->getParticipants() as $index => $participant) { - $participantRoomChoices = []; - $assignedRoomId = $adapter->getAssignedRoomId($participant); - - foreach ($selectedRooms as $roomSelection) { - $currentOccupancy = $roomOccupancy[$roomSelection->roomId] ?? 0; - - // If this participant is already assigned to this room, exclude them from occupancy count - $adjustedOccupancy = $currentOccupancy; - if ($assignedRoomId === $roomSelection->roomId) { - --$adjustedOccupancy; - } - - $remainingCapacity = $roomSelection->minPax - $adjustedOccupancy; - - // Include room if it has capacity OR if it's the participant's current assignment - if ($remainingCapacity > 0 || $assignedRoomId === $roomSelection->roomId) { - $participantRoomChoices[$roomSelection->roomLabel] = $roomSelection->roomId; - } - } - - $roomChoices[$index] = $participantRoomChoices; - } - - return $roomChoices; - } - - /** - * Adds the participants collection field to the form with the given room choices. - * - * @param array> $roomChoices - */ - private function addParticipantsField(FormInterface $form, array $roomChoices): void + private function addParticipantsField(FormInterface $form, ParticipantDataAdapter $adapter, array $selectedRooms): void { $form->add('participants', CollectionType::class, [ 'entry_type' => BookingCreateParticipantType::class, @@ -113,7 +53,8 @@ class BookingCreateStep2Type extends AbstractType 'allow_delete' => false, 'by_reference' => false, 'entry_options' => [ - 'room_choices' => $roomChoices, + 'participant_adapter' => $adapter, + 'selected_rooms' => $selectedRooms, ], ]); } diff --git a/src/Form/BoolingEditParticipantType.php b/src/Form/BookingEditParticipantType.php similarity index 99% rename from src/Form/BoolingEditParticipantType.php rename to src/Form/BookingEditParticipantType.php index 560dbf0..a71f4c8 100644 --- a/src/Form/BoolingEditParticipantType.php +++ b/src/Form/BookingEditParticipantType.php @@ -17,7 +17,7 @@ use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; -class BoolingEditParticipantType extends AbstractType +class BookingEditParticipantType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { @@ -34,7 +34,7 @@ class BoolingEditParticipantType extends AbstractType $form = $event->getForm(); $personalDataMutable = $options['personal_data_mutable'] && $participantData->mutable; - $isApplicant = 1 === $participantIndex; + $isApplicant = 0 === $participantIndex; $form ->add('firstName', TextType::class, [ diff --git a/src/Form/BookingEditType.php b/src/Form/BookingEditType.php index 0788764..34df0be 100644 --- a/src/Form/BookingEditType.php +++ b/src/Form/BookingEditType.php @@ -23,7 +23,7 @@ final class BookingEditType extends AbstractType $travelData = $data->travel; $form->add('participants', CollectionType::class, [ - 'entry_type' => BoolingEditParticipantType::class, + 'entry_type' => BookingEditParticipantType::class, 'entry_options' => [ 'selectable_courses' => $this->mergeSelectableServices($data, Constants::TOKEN_COURSES), 'selectable_ski_passes' => $this->mergeSelectableServices($data, Constants::TOKEN_SKI_PASS), diff --git a/src/Form/ChoiceLoader/ParticipantRoomChoiceLoader.php b/src/Form/ChoiceLoader/ParticipantRoomChoiceLoader.php new file mode 100644 index 0000000..f4f0688 --- /dev/null +++ b/src/Form/ChoiceLoader/ParticipantRoomChoiceLoader.php @@ -0,0 +1,120 @@ +choiceList) { + $choices = $this->generateRoomChoicesForParticipant(); + $this->choiceList = $this->factory->createListFromChoices($choices, $value); + } + + return $this->choiceList; + } + + public function loadChoicesForValues(array $values, ?callable $value = null): array + { + if (empty($values)) { + return []; + } + + return $this->loadChoiceList($value)->getChoicesForValues($values); + } + + public function loadValuesForChoices(array $choices, ?callable $value = null): array + { + if (empty($choices)) { + return []; + } + + return $this->loadChoiceList($value)->getValuesForChoices($choices); + } + + /** + * Generates room choices for the specific participant. + * + * @return array + */ + private function generateRoomChoicesForParticipant(): array + { + $roomOccupancy = $this->calculateRoomOccupancy(); + $participantRoomChoices = []; + $assignedRoomId = $this->getParticipantAssignedRoomId(); + + foreach ($this->selectedRooms as $roomSelection) { + $currentOccupancy = $roomOccupancy[$roomSelection->roomId] ?? 0; + + // If this participant is already assigned to this room, exclude them from occupancy count + $adjustedOccupancy = $currentOccupancy; + if ($assignedRoomId === $roomSelection->roomId) { + --$adjustedOccupancy; + } + + $remainingCapacity = $roomSelection->minPax - $adjustedOccupancy; + + // Include room if it has capacity OR if it's the participant's current assignment + if ($remainingCapacity > 0 || $assignedRoomId === $roomSelection->roomId) { + $participantRoomChoices[$roomSelection->roomLabel] = $roomSelection->roomId; + } + } + + return $participantRoomChoices; + } + + /** + * Calculates room occupancy based on participant assignments. + * + * @return array + */ + private function calculateRoomOccupancy(): array + { + $roomOccupancy = []; + + foreach ($this->adapter->getParticipants() as $participant) { + $assignedRoomId = $this->adapter->getAssignedRoomId($participant); + if (null !== $assignedRoomId) { + $roomOccupancy[$assignedRoomId] = ($roomOccupancy[$assignedRoomId] ?? 0) + 1; + } + } + + return $roomOccupancy; + } + + /** + * Gets the assigned room ID for the current participant. + */ + private function getParticipantAssignedRoomId(): ?int + { + $participants = $this->adapter->getParticipants(); + $participant = $participants[$this->participantIndex] ?? null; + + return null !== $participant ? $this->adapter->getAssignedRoomId($participant) : null; + } +} \ No newline at end of file diff --git a/src/Form/Service/ParticipantRoomChoiceLoaderFactory.php b/src/Form/Service/ParticipantRoomChoiceLoaderFactory.php new file mode 100644 index 0000000..9b7d77b --- /dev/null +++ b/src/Form/Service/ParticipantRoomChoiceLoaderFactory.php @@ -0,0 +1,39 @@ +choiceListFactory, + $adapter, + $selectedRooms, + $participantIndex + ); + } +} \ No newline at end of file diff --git a/templates/booking/edit.html.twig b/templates/booking/edit.html.twig index 96308a4..273a998 100644 --- a/templates/booking/edit.html.twig +++ b/templates/booking/edit.html.twig @@ -14,7 +14,7 @@ {% set messages = messages|merge([field.vars.label ~ ': ' ~ error.message]) %} {% endfor %} {% endfor %} - {% include '_partials/_alert.html.twig' with { 'level': 'error', 'title': 'Teilnehmer:in ' ~ participant.index ~ ':' ~ participant.firstName ~ participant.lastName, 'messages': messages } %} + {% include '_partials/_alert.html.twig' with { 'level': 'error', 'title': 'Teilnehmer:in ' ~ (participant.index + 1) ~ ':' ~ participant.firstName ~ participant.lastName, 'messages': messages } %} {% endif %} {% endfor %} {% endif %} @@ -125,7 +125,7 @@
- Teilnehmer:in {{ participant.index }}: {{ participant.firstName }} {{ participant.lastName }} + Teilnehmer:in {{ participant.index + 1 }}: {{ participant.firstName }} {{ participant.lastName }} {% if participant.status == 'S' %} storniert{% endif %}
@@ -188,7 +188,11 @@ Unterkunft {% set room = bookingData.roomForParticipant(participant.index) %} - {{ room.label }} ({{ room.individualPrice[participant.index]|format_currency('EUR') }}) + {% if room %} + {{ room.label }} ({{ room.individualPrice[participant.index]|format_currency('EUR') }}) + {% else %} + Keine Unterkunft zugeordnet + {% endif %}

diff --git a/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php b/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php index 963a83a..37ad549 100644 --- a/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php +++ b/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php @@ -58,8 +58,8 @@ class BookingDataProcessorTest extends TestCase $result = $this->processor->createUpdateRequestPayload($formData); $this->assertCount(2, $result['teilnehmerliste']['teilnehmer']); - $this->assertEquals(0, $result['teilnehmerliste']['teilnehmer'][0]['@id']); - $this->assertEquals(1, $result['teilnehmerliste']['teilnehmer'][1]['@id']); + $this->assertEquals(1, $result['teilnehmerliste']['teilnehmer'][0]['@id']); + $this->assertEquals(2, $result['teilnehmerliste']['teilnehmer'][1]['@id']); } public function testAdditionalServicesProcessing(): void @@ -74,7 +74,7 @@ class BookingDataProcessorTest extends TestCase $service = $result['zusatzleistungen']['zusatzleistung'][0]; $this->assertEquals(1, $service['@idleistung']); $this->assertEquals(1, $service['@anzahl']); - $this->assertEquals('0', $service['@zuordnung']); + $this->assertEquals('1', $service['@zuordnung']); } public function testTransportationServicesProcessing(): void