From e7cf172b9fe368a1b897c7a5df7330656d1b0e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 11 Nov 2025 14:14:44 +0100 Subject: [PATCH] feat: add subtypes PAR and LVS as bookable additional services --- .../controllers/checkbox_toggle_controller.js | 32 ++++++++++ .../DataProcessor/BookingDataProcessor.php | 2 + src/BusProNet/Model/Service.php | 2 + src/Form/BookingType.php | 2 + src/Form/Model/BookingData.php | 5 ++ src/Form/Model/ParticipantData.php | 2 + src/Form/ParticipantType.php | 59 ++++++++++++++++++- templates/booking/edit.html.twig | 14 ++++- 8 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 assets/controllers/checkbox_toggle_controller.js diff --git a/assets/controllers/checkbox_toggle_controller.js b/assets/controllers/checkbox_toggle_controller.js new file mode 100644 index 0000000..cfa5a72 --- /dev/null +++ b/assets/controllers/checkbox_toggle_controller.js @@ -0,0 +1,32 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + + static classes = [ 'hidden' ] + static targets = [ 'container', 'checkboxGroup' ] + static values = { visible: Boolean } + + connect() { + this.updateVisibility() + } + + check() { + this.updateVisibility() + } + + updateVisibility() { + if (!this.hasCheckboxGroupTarget) { + return + } + + const checkboxes = this.checkboxGroupTarget.querySelectorAll('input[type="checkbox"]') + const hasChecked = Array.from(checkboxes).some(checkbox => checkbox.checked) + this.visibleValue = hasChecked + } + + visibleValueChanged(visible) { + if (this.hasContainerTarget) { + this.containerTarget.classList.toggle(this.hiddenClass, false === visible) + } + } +} diff --git a/src/BusProNet/DataProcessor/BookingDataProcessor.php b/src/BusProNet/DataProcessor/BookingDataProcessor.php index 8f11a1f..13c2633 100644 --- a/src/BusProNet/DataProcessor/BookingDataProcessor.php +++ b/src/BusProNet/DataProcessor/BookingDataProcessor.php @@ -29,6 +29,8 @@ class BookingDataProcessor ...$participant->skiPass, ...$participant->board, ...$participant->rentals, + ...$participant->parking, + ...$participant->rentalInsurance, ]; foreach ($servicesToMap as $service) { if (false === isset($this->additionalServices[$service->id])) { diff --git a/src/BusProNet/Model/Service.php b/src/BusProNet/Model/Service.php index 5c8c011..e3a557f 100644 --- a/src/BusProNet/Model/Service.php +++ b/src/BusProNet/Model/Service.php @@ -19,6 +19,8 @@ class Service public const TOKEN_ADDITIONAL = 'SON'; public const TOKEN_BOARD = 'VPF'; public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8']; + public const TOKEN_PARKING = 'PAR'; + public const TOKEN_RENTAL_INSURANCE = 'LVS'; public const STATUS_AVAILABLE = 'Frei'; public const STATUS_BLOCKED = 'Buchungsstop'; diff --git a/src/Form/BookingType.php b/src/Form/BookingType.php index 603117f..f64ce55 100644 --- a/src/Form/BookingType.php +++ b/src/Form/BookingType.php @@ -29,6 +29,8 @@ final class BookingType extends AbstractType 'selectable_services' => $this->mergeSelectableServices($data, Service::TOKEN_ADDITIONAL), 'selectable_board' => $this->mergeSelectableServices($data, Service::TOKEN_BOARD), 'selectable_rentals' => $this->mergeSelectableServices($data, Service::TOKEN_RENTALS), + 'selectable_parking' => $this->mergeSelectableServices($data, Service::TOKEN_PARKING), + 'selectable_rental_insurance' => $this->mergeSelectableServices($data, Service::TOKEN_RENTAL_INSURANCE), 'selectable_transportation_services_to' => $travelData ->getTransportationServicesByDirection('HIN', false), 'selectable_transportation_services_fro' => $travelData diff --git a/src/Form/Model/BookingData.php b/src/Form/Model/BookingData.php index 511a1b7..294c2cb 100644 --- a/src/Form/Model/BookingData.php +++ b/src/Form/Model/BookingData.php @@ -38,6 +38,11 @@ class BookingData ->getAdditionalServicesForParticipantByGroup($index, Service::TOKEN_BOARD); $participantData->rentals = $booking ->getAdditionalServicesForParticipantByGroup($index, Service::TOKEN_RENTALS); + $participantData->parking = $booking + ->getAdditionalServicesForParticipantByGroup($index, Service::TOKEN_PARKING); + $participantData->rentalInsurance = $booking + ->getAdditionalServicesForParticipantByGroup($index, Service::TOKEN_RENTAL_INSURANCE); + // Different keys for direction used in booking data (H <=> HIN, R <=> RUECK)! $participantData->transportationServiceTo = $booking ->getTransportationServiceForParticipantAndDirection($index, 'H'); diff --git a/src/Form/Model/ParticipantData.php b/src/Form/Model/ParticipantData.php index 6e5f50e..e7c1f8a 100644 --- a/src/Form/Model/ParticipantData.php +++ b/src/Form/Model/ParticipantData.php @@ -42,6 +42,8 @@ class ParticipantData public array $skiPass = []; public array $board = []; public array $rentals = []; + public array $parking = []; + public array $rentalInsurance = []; public ?Service $transportationServiceTo = null; public ?Service $transportationServiceFro = null; public ?Pickup $pickup = null; diff --git a/src/Form/ParticipantType.php b/src/Form/ParticipantType.php index 580d6da..93b3cbb 100644 --- a/src/Form/ParticipantType.php +++ b/src/Form/ParticipantType.php @@ -233,13 +233,36 @@ class ParticipantType extends AbstractType ]); } if (0 < count($options['selectable_rentals'])) { + // Extend common options for rentals to add checkbox-toggle action + $rentalChoiceAttr = $commonChoiceFieldOptions['choice_attr']; $form->add('rentals', ChoiceType::class, [ ...$commonChoiceFieldOptions, 'label' => 'Verleih', 'choices' => $options['selectable_rentals'], + 'choice_attr' => function (?Service $service) use ($rentalChoiceAttr) { + $attributes = $rentalChoiceAttr($service); + // Add checkbox-toggle action to existing actions + $attributes['data-action'] = 'booking#toggle checkbox-toggle#check'; + + return $attributes; + }, ]); } + // Parking - single checkbox (always add, JS will show/hide for PKW travelers) + $form->add('parking', ChoiceType::class, [ + ...$commonChoiceFieldOptions, + 'label' => 'Parkplatz', + 'choices' => $options['selectable_parking'], + ]); + + // Rental Insurance - single checkbox (always add, JS will show/hide when rentals selected) + $form->add('rentalInsurance', ChoiceType::class, [ + ...$commonChoiceFieldOptions, + 'label' => 'Leihausrüstungsversicherung', + 'choices' => $options['selectable_rental_insurance'], + ]); + // Transportation $transportationChoiceAttributes = function (?Service $service) use ($options) { $attributes = [ @@ -330,12 +353,39 @@ class ParticipantType extends AbstractType return; } + // Get selected transportation service $transportationId = $data['transportationServiceTo'] ?? null; - $transportation = $options['travel']->pickups[$transportationId] ?? null; + $transportation = null; + foreach ($options['selectable_transportation_services_to'] as $service) { + if ($service->id == $transportationId) { + $transportation = $service; + break; + } + } + // Remove pickup field for PKW (existing logic, corrected) if (null !== $transportation && 'PKW' === $transportation->subType) { - $form->remove('pickup'); - unset($data['pickup']); + if ($form->has('pickup')) { + $form->remove('pickup'); + unset($data['pickup']); + } + } + + // Remove parking field for BUS travelers (only show for PKW) + if (null !== $transportation && 'BUS' === $transportation->subType) { + if ($form->has('parking')) { + $form->remove('parking'); + unset($data['parking']); + } + } + + // Remove rental insurance if no rentals selected + $hasRentals = isset($data['rentals']) && is_array($data['rentals']) && 0 < count($data['rentals']); + if (false === $hasRentals) { + if ($form->has('rentalInsurance')) { + $form->remove('rentalInsurance'); + unset($data['rentalInsurance']); + } } // filter selectable services by participant's age @@ -389,6 +439,7 @@ class ParticipantType extends AbstractType ) { return true; } + return false; }); } @@ -402,6 +453,8 @@ class ParticipantType extends AbstractType 'selectable_services' => [], 'selectable_board' => [], 'selectable_rentals' => [], + 'selectable_parking' => [], + 'selectable_rental_insurance' => [], 'selectable_transportation_services_to' => [], 'selectable_transportation_services_fro' => [], 'selectable_pickups' => [], diff --git a/templates/booking/edit.html.twig b/templates/booking/edit.html.twig index dab627d..d2fa295 100644 --- a/templates/booking/edit.html.twig +++ b/templates/booking/edit.html.twig @@ -194,7 +194,7 @@

Leistungen

-
+
0 }, { 'hidden': 'hidden' }) }}> {% if child.courses is defined %} {{ form_row(child.courses) }} {% endif %} @@ -208,8 +208,16 @@ {{ form_row(child.board) }} {% endif %} {% if child.rentals is defined %} - {{ form_row(child.rentals) }} +
+ {{ form_row(child.rentals) }} +
{% endif %} +
+ {{ form_row(child.parking) }} +
+
+ {{ form_row(child.rentalInsurance) }} +
{% if not travelData.additionalServicesMutable %}
@@ -220,7 +228,7 @@ Hin-/Rückreise
-
+
{{ form_row(child.transportationServiceTo) }} {% if child.pickup is defined %}