From 957e20d7635f3de932b0b4e3b7bb3e444cfd6940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Mon, 19 Jan 2026 15:51:39 +0100 Subject: [PATCH] fix: preserve selected services from booking even when unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When editing a booking, services that were previously booked but are no longer available in the travel catalog were being dropped. This caused API error 650 ("Anzahl Leistung stimmt nicht mit Teilnehmerzuordnung überein") because the service participant counts no longer matched. The fix ensures that in edit mode, booked services are merged with travel data services in both: - Form rendering (ParticipantFieldOptionsProvider): so checkboxes appear - Handler validation (AbstractParticipantFieldHandler): so selections are accepted This allows users to keep their existing service selections or deliberately replace them with other available options. Create mode remains unchanged. --- .../PersonalDataSynchronizer.php | 2 +- src/BusProNet/Model/PersonalData.php | 2 +- .../Admin/BookingEditDraftCrudController.php | 6 +- src/Controller/Admin/DashboardController.php | 2 +- src/Controller/Admin/UserCrudController.php | 4 +- .../AbstractParticipantFieldHandler.php | 66 +++++++++++++++- ...ticipantAdditionalServicesFieldHandler.php | 8 +- .../Service/ParticipantBoardFieldHandler.php | 8 +- .../ParticipantCoursesFieldHandler.php | 8 +- .../ParticipantFieldOptionsProvider.php | 76 +++++++++++++++++-- .../ParticipantParkingFieldHandler.php | 14 +++- ...ParticipantRentalInsuranceFieldHandler.php | 15 +++- .../ParticipantRentalsFieldHandler.php | 9 ++- .../ParticipantSkiPassFieldHandler.php | 9 ++- .../Service/ParticipantVegFieldHandler.php | 8 +- src/Service/BookingExportService.php | 4 +- 16 files changed, 204 insertions(+), 37 deletions(-) diff --git a/src/BusProNet/DataProcessor/PersonalDataSynchronizer.php b/src/BusProNet/DataProcessor/PersonalDataSynchronizer.php index 633cc1f..fde530f 100644 --- a/src/BusProNet/DataProcessor/PersonalDataSynchronizer.php +++ b/src/BusProNet/DataProcessor/PersonalDataSynchronizer.php @@ -127,7 +127,7 @@ class PersonalDataSynchronizer * Ensures mandatory fields have sensible defaults before submission to the * BPN API. Currently handles nationality which defaults to 'D' (German). * - * @param \App\BusProNet\Model\PersonalData $participant The participant to fill defaults for + * @param PersonalData $participant The participant to fill defaults for */ private function fillMissingParticipantFields(PersonalData $participant): void { diff --git a/src/BusProNet/Model/PersonalData.php b/src/BusProNet/Model/PersonalData.php index 3e5a1bf..881fdcb 100644 --- a/src/BusProNet/Model/PersonalData.php +++ b/src/BusProNet/Model/PersonalData.php @@ -91,7 +91,7 @@ class PersonalData ]; if (true === $includeDateOfBirth) { - $dob = $this->dateOfBirth ?? new \DateTimeImmutable(self::DEFAULT_AGE_YEARS . ' years ago'); + $dob = $this->dateOfBirth ?? new \DateTimeImmutable(self::DEFAULT_AGE_YEARS.' years ago'); $payload['geburtsdatum'] = $dob->format('d.m.Y'); } diff --git a/src/Controller/Admin/BookingEditDraftCrudController.php b/src/Controller/Admin/BookingEditDraftCrudController.php index 1218c47..e1c5337 100644 --- a/src/Controller/Admin/BookingEditDraftCrudController.php +++ b/src/Controller/Admin/BookingEditDraftCrudController.php @@ -48,7 +48,7 @@ class BookingEditDraftCrudController extends AbstractCrudController ->remove(Crud::PAGE_INDEX, Action::NEW) ->remove(Crud::PAGE_INDEX, Action::EDIT) ->remove(Crud::PAGE_DETAIL, Action::EDIT) - ; + ; } #[Route('/admin/booking-draft/{id}/export', name: 'admin_booking_draft_export', requirements: ['id' => '\d+'])] @@ -68,7 +68,7 @@ class BookingEditDraftCrudController extends AbstractCrudController try { return $this->exportService->createExportResponse($draft); } catch (\RuntimeException $e) { - $this->addFlash('danger', 'Export fehlgeschlagen: ' . $e->getMessage()); + $this->addFlash('danger', 'Export fehlgeschlagen: '.$e->getMessage()); $url = $this->adminUrlGenerator ->setController(self::class) @@ -94,7 +94,7 @@ class BookingEditDraftCrudController extends AbstractCrudController AssociationField::new('user', 'Kundenaccount')->formatValue(fn (User $user) => $user->getEmail()), DateTimeField::new('createdAt', 'erstellt am'), DateTimeField::new('updatedAt', 'aktualisiert am'), - JsonDataField::new('formData', 'Daten')->onlyOnDetail() + JsonDataField::new('formData', 'Daten')->onlyOnDetail(), ]; } } diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php index b26d6f1..6e3319f 100644 --- a/src/Controller/Admin/DashboardController.php +++ b/src/Controller/Admin/DashboardController.php @@ -34,7 +34,7 @@ class DashboardController extends AbstractDashboardController { return parent::configureUserMenu($user) ->addMenuItems([ - MenuItem::linkToRoute('MyE&P', 'fa fa-user', 'app_account') + MenuItem::linkToRoute('MyE&P', 'fa fa-user', 'app_account'), ]); } diff --git a/src/Controller/Admin/UserCrudController.php b/src/Controller/Admin/UserCrudController.php index e5fae35..77067a5 100644 --- a/src/Controller/Admin/UserCrudController.php +++ b/src/Controller/Admin/UserCrudController.php @@ -8,8 +8,6 @@ use EasyCorp\Bundle\EasyAdminBundle\Config\Actions; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; -use EasyCorp\Bundle\EasyAdminBundle\Field\IdField; -use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; class UserCrudController extends AbstractCrudController @@ -25,7 +23,7 @@ class UserCrudController extends AbstractCrudController ->remove(Crud::PAGE_INDEX, Action::NEW) ->remove(Crud::PAGE_INDEX, Action::EDIT) ->remove(Crud::PAGE_INDEX, Action::DELETE) - ; + ; } public function configureCrud(Crud $crud): Crud diff --git a/src/Form/Service/Abstract/AbstractParticipantFieldHandler.php b/src/Form/Service/Abstract/AbstractParticipantFieldHandler.php index 8eef76b..14f8cdc 100644 --- a/src/Form/Service/Abstract/AbstractParticipantFieldHandler.php +++ b/src/Form/Service/Abstract/AbstractParticipantFieldHandler.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Form\Service\Abstract; +use App\BusProNet\Model\Booking; use App\BusProNet\Model\Service; use App\Form\Model\BookingDto; use App\Form\Service\Contract\ParticipantFieldHandlerInterface; @@ -83,7 +84,7 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle */ protected function isEditMode(BookingDto $bookingDto): bool { - return BookingDto::MODE_EDIT === $bookingDto->mode; + return BookingDto::MODE_EDIT === $bookingDto->getMode(); } /** @@ -95,7 +96,7 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle */ protected function isCreateMode(BookingDto $bookingDto): bool { - return BookingDto::MODE_CREATE === $bookingDto->mode; + return BookingDto::MODE_CREATE === $bookingDto->getMode(); } /** @@ -269,4 +270,65 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle return false; } + + /** + * Retrieves available services from travel data, merged with booked services in edit mode. + * + * In edit mode, services that were previously booked for the participant are included + * even if they're no longer available in travel data. This prevents booked services + * from being dropped during form validation when the travel catalog has changed. + * + * @param BookingDto $bookingDto The booking DTO containing travel and booking data + * @param int $participantIndex The participant index to retrieve booked services for + * @param mixed $serviceGroup The service group(s) to filter by (e.g., TOKEN_RENTALS) + * @param bool $filterByTravelDateRange Whether to filter travel services by date range + * + * @return Service[] Array of available services keyed by service ID + */ + protected function getAvailableServicesWithBooked( + BookingDto $bookingDto, + int $participantIndex, + mixed $serviceGroup, + bool $filterByTravelDateRange = false, + ): array { + $availableServices = $bookingDto->travel->getAdditionalServicesBySubTypes($serviceGroup, $filterByTravelDateRange); + + if ($this->isEditMode($bookingDto) && null !== $bookingDto->booking) { + $availableServices = $this->mergeBookedServices( + $availableServices, + $bookingDto->booking, + $participantIndex, + $serviceGroup + ); + } + + return $availableServices; + } + + /** + * Merges booked services into the available services array. + * + * @param Service[] $availableServices The services available from travel data + * @param Booking $booking The booking containing previously booked services + * @param int $participantIndex The participant index to retrieve booked services for + * @param mixed $serviceGroup The service group(s) to filter by + * + * @return Service[] Merged array of services keyed by service ID + */ + private function mergeBookedServices( + array $availableServices, + Booking $booking, + int $participantIndex, + mixed $serviceGroup, + ): array { + $bookedServices = $booking->getAdditionalServicesForParticipantByGroup($participantIndex, $serviceGroup); + + foreach ($bookedServices as $bookedService) { + if (false === isset($availableServices[$bookedService->id])) { + $availableServices[$bookedService->id] = $bookedService; + } + } + + return $availableServices; + } } diff --git a/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php b/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php index d6d4180..d94985c 100644 --- a/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php +++ b/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php @@ -99,8 +99,12 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField // Extract current service selections from submitted data $selectedServices = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; - // Get available additional services from travel data - $availableServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL); + // Get available additional services from travel data (includes booked services in edit mode) + $availableServices = $this->getAvailableServicesWithBooked( + $bookingDto, + $participantIndex, + Constants::TOKEN_ADDITIONAL + ); // Filter selections to keep only age-appropriate services $validSelections = $this->filterValidServiceSelections( diff --git a/src/Form/Service/ParticipantBoardFieldHandler.php b/src/Form/Service/ParticipantBoardFieldHandler.php index faac2f0..4bd88ab 100644 --- a/src/Form/Service/ParticipantBoardFieldHandler.php +++ b/src/Form/Service/ParticipantBoardFieldHandler.php @@ -58,7 +58,13 @@ class ParticipantBoardFieldHandler extends AbstractParticipantFieldHandler } $selectedBoard = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; - $availableBoard = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD); + + // Get available board options from travel data (includes booked options in edit mode) + $availableBoard = $this->getAvailableServicesWithBooked( + $bookingDto, + $participantIndex, + Constants::TOKEN_BOARD + ); $validSelections = $this->filterValidServiceSelections( $selectedBoard, diff --git a/src/Form/Service/ParticipantCoursesFieldHandler.php b/src/Form/Service/ParticipantCoursesFieldHandler.php index 7c521f9..d83ab82 100644 --- a/src/Form/Service/ParticipantCoursesFieldHandler.php +++ b/src/Form/Service/ParticipantCoursesFieldHandler.php @@ -92,8 +92,12 @@ class ParticipantCoursesFieldHandler extends AbstractParticipantFieldHandler // Extract current course selections from submitted data $selectedCourses = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; - // Get available courses from travel data - $availableCourses = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES); + // Get available courses from travel data (includes booked courses in edit mode) + $availableCourses = $this->getAvailableServicesWithBooked( + $bookingDto, + $participantIndex, + Constants::TOKEN_COURSES + ); // Filter selections to keep only age-appropriate courses $validSelections = $this->filterValidServiceSelections( diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index f88a485..18b4f12 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Form\Service; use App\BusProNet\Constants; +use App\BusProNet\Model\Booking; use App\BusProNet\Model\Insurance; use App\BusProNet\Model\Pickup; use App\BusProNet\Model\Room; @@ -122,7 +123,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider // Returns empty array when no courses available $this->fieldOptionProviders['courses'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array { $choices = $this->filterServicesByAgeConstraints( - $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES), + $this->getServicesWithBooked($bookingDto, $participantIndex, Constants::TOKEN_COURSES), $bookingDto, $participantIndex ); @@ -166,7 +167,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider // Returns empty array when no additional services available $this->fieldOptionProviders['additionalServices'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array { $choices = $this->filterServicesByAgeConstraints( - $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL), + $this->getServicesWithBooked($bookingDto, $participantIndex, Constants::TOKEN_ADDITIONAL), $bookingDto, $participantIndex ); @@ -217,7 +218,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider // Returns empty array when no board options available $this->fieldOptionProviders['board'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array { $choices = $this->filterServicesByAgeConstraints( - $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD), + $this->getServicesWithBooked($bookingDto, $participantIndex, Constants::TOKEN_BOARD), $bookingDto, $participantIndex ); @@ -261,7 +262,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider // Returns empty array when no dietary options available $this->fieldOptionProviders['veg'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array { $choices = $this->filterServicesByAgeConstraints( - $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_VEG, true), + $this->getServicesWithBooked($bookingDto, $participantIndex, Constants::TOKEN_VEG, true), $bookingDto, $participantIndex ); @@ -316,7 +317,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider $this->fieldOptionProviders['rentals'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array { $choices = $this->filterServicesByAgeConstraints( $this->filterRentalsBySkiPassDuration( - $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true), + $this->getServicesWithBooked($bookingDto, $participantIndex, Constants::TOKEN_RENTALS, true), $bookingDto, $participantIndex ), @@ -386,7 +387,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider // Returns empty array when no skipass options available $this->fieldOptionProviders['skiPass'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array { $choices = $this->filterSkiPassChoices( - $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true), + $this->getServicesWithBooked($bookingDto, $participantIndex, Constants::TOKEN_SKI_PASS, true), $bookingDto, $participantIndex ); @@ -557,7 +558,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider // Simple checkbox since there's only ever one parking type // Returns empty array when no parking services exist to prevent field from rendering $this->fieldOptionProviders['parking'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array { - $parkingServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING); + $parkingServices = $this->getServicesWithBooked($bookingDto, $participantIndex, Constants::TOKEN_PARKING); if (true === empty($parkingServices)) { return []; } @@ -1255,4 +1256,65 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider // Fallback: return all services if we don't have the expected discount/regular pair return [...$otherServices, ...$pkwServices]; } + + /** + * Retrieves available services from travel data, merged with booked services in edit mode. + * + * In edit mode, services that were previously booked for the participant are included + * even if they're no longer available in travel data. This ensures form checkboxes + * appear for booked services, allowing users to keep or deselect them. + * + * @param BookingDto $bookingDto The booking DTO containing travel and booking data + * @param int $participantIndex The participant index to retrieve booked services for + * @param mixed $serviceGroup The service group(s) to filter by (e.g., TOKEN_RENTALS) + * @param bool $filterByTravelDateRange Whether to filter travel services by date range + * + * @return Service[] Array of available services keyed by service ID + */ + private function getServicesWithBooked( + BookingDto $bookingDto, + int $participantIndex, + mixed $serviceGroup, + bool $filterByTravelDateRange = false, + ): array { + $availableServices = $bookingDto->travel->getAdditionalServicesBySubTypes($serviceGroup, $filterByTravelDateRange); + + if (BookingDto::MODE_EDIT === $bookingDto->getMode() && null !== $bookingDto->booking) { + $availableServices = $this->mergeBookedServices( + $availableServices, + $bookingDto->booking, + $participantIndex, + $serviceGroup + ); + } + + return $availableServices; + } + + /** + * Merges booked services into the available services array. + * + * @param Service[] $availableServices The services available from travel data + * @param Booking $booking The booking containing previously booked services + * @param int $participantIndex The participant index to retrieve booked services for + * @param mixed $serviceGroup The service group(s) to filter by + * + * @return Service[] Merged array of services keyed by service ID + */ + private function mergeBookedServices( + array $availableServices, + Booking $booking, + int $participantIndex, + mixed $serviceGroup, + ): array { + $bookedServices = $booking->getAdditionalServicesForParticipantByGroup($participantIndex, $serviceGroup); + + foreach ($bookedServices as $bookedService) { + if (false === isset($availableServices[$bookedService->id])) { + $availableServices[$bookedService->id] = $bookedService; + } + } + + return $availableServices; + } } diff --git a/src/Form/Service/ParticipantParkingFieldHandler.php b/src/Form/Service/ParticipantParkingFieldHandler.php index 7121cb8..8a46876 100644 --- a/src/Form/Service/ParticipantParkingFieldHandler.php +++ b/src/Form/Service/ParticipantParkingFieldHandler.php @@ -73,7 +73,7 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler // Store parking service object for pricing calculation if (true === $isParkingSelected) { - $participant->parkingService = $this->findParkingService($bookingDto); + $participant->parkingService = $this->findParkingService($bookingDto, $participantIndex); } else { $participant->parkingService = null; } @@ -112,13 +112,19 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler * Gets the first (and typically only) parking service for pricing calculation. * Returns null if no parking services are available. * - * @param BookingDto $bookingDto The booking DTO containing travel data + * @param BookingDto $bookingDto The booking DTO containing travel data + * @param int $participantIndex The participant index for booked services lookup * * @return Service|null The parking service object, or null if not found */ - private function findParkingService(BookingDto $bookingDto): ?Service + private function findParkingService(BookingDto $bookingDto, int $participantIndex): ?Service { - $parkingServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING); + // Get available parking services (includes booked parking in edit mode) + $parkingServices = $this->getAvailableServicesWithBooked( + $bookingDto, + $participantIndex, + Constants::TOKEN_PARKING + ); if (empty($parkingServices)) { return null; diff --git a/src/Form/Service/ParticipantRentalInsuranceFieldHandler.php b/src/Form/Service/ParticipantRentalInsuranceFieldHandler.php index 9f65bcf..79848a7 100644 --- a/src/Form/Service/ParticipantRentalInsuranceFieldHandler.php +++ b/src/Form/Service/ParticipantRentalInsuranceFieldHandler.php @@ -107,7 +107,7 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan // Store service object based on checkbox state for pricing calculations if (true === $isRentalInsuranceSelected) { - $participant->rentalInsurance = $this->findRentalInsuranceService($bookingDto); + $participant->rentalInsurance = $this->findRentalInsuranceService($bookingDto, $participantIndex); } else { $participant->rentalInsurance = null; } @@ -119,13 +119,20 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan * Gets the first (and typically only) rental insurance service. * Returns null if no rental insurance services are available. * - * @param BookingDto $bookingDto The booking DTO containing travel data + * @param BookingDto $bookingDto The booking DTO containing travel data + * @param int $participantIndex The participant index for booked services lookup * * @return Service|null The rental insurance service object, or null if not found */ - private function findRentalInsuranceService(BookingDto $bookingDto): ?Service + private function findRentalInsuranceService(BookingDto $bookingDto, int $participantIndex): ?Service { - $rentalInsuranceServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true); + // Get available rental insurance services (includes booked insurance in edit mode) + $rentalInsuranceServices = $this->getAvailableServicesWithBooked( + $bookingDto, + $participantIndex, + Constants::TOKEN_RENTAL_INSURANCE, + true + ); if (empty($rentalInsuranceServices)) { return null; diff --git a/src/Form/Service/ParticipantRentalsFieldHandler.php b/src/Form/Service/ParticipantRentalsFieldHandler.php index ff0c58f..b62b1fb 100644 --- a/src/Form/Service/ParticipantRentalsFieldHandler.php +++ b/src/Form/Service/ParticipantRentalsFieldHandler.php @@ -83,7 +83,14 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler } $selectedRentals = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; - $availableRentals = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true); + + // Get available rentals from travel data (includes booked rentals in edit mode) + $availableRentals = $this->getAvailableServicesWithBooked( + $bookingDto, + $participantIndex, + Constants::TOKEN_RENTALS, + true + ); // Filter rentals by skipass duration to ensure only matching rentals are available $durationFilteredRentals = $this->filterRentalsBySkiPassDuration($availableRentals, $participant); diff --git a/src/Form/Service/ParticipantSkiPassFieldHandler.php b/src/Form/Service/ParticipantSkiPassFieldHandler.php index 56ced3a..731ef57 100644 --- a/src/Form/Service/ParticipantSkiPassFieldHandler.php +++ b/src/Form/Service/ParticipantSkiPassFieldHandler.php @@ -94,8 +94,13 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler // Extract current skipass selection from submitted data $selectedSkiPass = $this->getFieldValue($submittedData, $this->getFieldName()); - // Get all skipasses from travel data (with date filtering) - $availableSkipasses = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true); + // Get all skipasses from travel data (includes booked skipasses in edit mode) + $availableSkipasses = $this->getAvailableServicesWithBooked( + $bookingDto, + $participantIndex, + Constants::TOKEN_SKI_PASS, + true + ); // For single selection, validate the selected skipass and convert ID to Service object $validSelection = null; diff --git a/src/Form/Service/ParticipantVegFieldHandler.php b/src/Form/Service/ParticipantVegFieldHandler.php index 2389545..ec30873 100644 --- a/src/Form/Service/ParticipantVegFieldHandler.php +++ b/src/Form/Service/ParticipantVegFieldHandler.php @@ -84,7 +84,13 @@ class ParticipantVegFieldHandler extends AbstractParticipantFieldHandler $selectedVeg = $this->getFieldValue($submittedData, $this->getFieldName()); - $availableVegOptions = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_VEG, true); + // Get available veg options from travel data (includes booked options in edit mode) + $availableVegOptions = $this->getAvailableServicesWithBooked( + $bookingDto, + $participantIndex, + Constants::TOKEN_VEG, + true + ); $validSelection = null; if (null !== $selectedVeg) { diff --git a/src/Service/BookingExportService.php b/src/Service/BookingExportService.php index 7425e42..97dc889 100644 --- a/src/Service/BookingExportService.php +++ b/src/Service/BookingExportService.php @@ -178,7 +178,7 @@ class BookingExportService ], ]; $lastColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex(count(self::COLUMN_HEADERS)); - $sheet->getStyle('A1:' . $lastColumn . '1')->applyFromArray($headerStyle); + $sheet->getStyle('A1:'.$lastColumn.'1')->applyFromArray($headerStyle); // Write participant data $formData = $draft->getFormData(); @@ -206,7 +206,7 @@ class BookingExportService \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, int $row, array $participant, - array $lookups + array $lookups, ): void { $personal = $participant['personalData'] ?? []; $address = $participant['address'] ?? [];