From a1751f699b36ae968e643c67f21afd2e2b3aae9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 17 Jul 2026 11:36:04 +0200 Subject: [PATCH] wip: finalize offer layout --- assets/images/icons.svg | 335 +++++------------- .../Admin/Accommodation/EditController.php | 2 +- src/Controller/Groups/OfferController.php | 8 +- src/Form/Model/BookingSummaryDto.php | 4 +- src/Model/AccommodationBookingContext.php | 3 +- src/Model/BookingSummaryCmsHotelData.php | 21 -- src/Model/CmsHotelData.php | 31 ++ src/Model/CmsRegionData.php | 32 ++ src/Service/AccommodationBookingService.php | 11 +- src/Service/BookingSummaryAssembler.php | 6 +- src/Service/CmsDataProvider.php | 69 +++- src/Twig/AppExtension.php | 1 - src/Twig/AppRuntime.php | 38 -- templates/admin/accommodation/_form.html.twig | 2 +- .../groups/booking/_offer_summary.html.twig | 34 +- .../booking/_step4_services_recap.html.twig | 3 + templates/groups/booking/_summary.html.twig | 4 +- templates/groups/booking/offer.html.twig | 308 ++++++++-------- .../Controller/Groups/OfferControllerTest.php | 42 +-- tests/Service/CmsDataProviderTest.php | 74 ++++ 20 files changed, 487 insertions(+), 541 deletions(-) delete mode 100644 src/Model/BookingSummaryCmsHotelData.php create mode 100644 src/Model/CmsHotelData.php create mode 100644 src/Model/CmsRegionData.php create mode 100644 tests/Service/CmsDataProviderTest.php diff --git a/assets/images/icons.svg b/assets/images/icons.svg index 223de89f..0dbe9314 100644 --- a/assets/images/icons.svg +++ b/assets/images/icons.svg @@ -1,217 +1,69 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -219,41 +71,24 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/Controller/Admin/Accommodation/EditController.php b/src/Controller/Admin/Accommodation/EditController.php index a5b4dba3..89898da3 100644 --- a/src/Controller/Admin/Accommodation/EditController.php +++ b/src/Controller/Admin/Accommodation/EditController.php @@ -27,7 +27,7 @@ class EditController extends AbstractController public function index(Accommodation $accommodation, Request $request): Response { // fetch additional data from cms if available - $cmsData = []; + $cmsData = null; if (null !== $accommodation->getCalendarCode()) { $cmsData = $this->cmsDataProvider->getHotelDetails($accommodation->getEffectiveCmsCode()); diff --git a/src/Controller/Groups/OfferController.php b/src/Controller/Groups/OfferController.php index 7cbb638e..9442a4ef 100644 --- a/src/Controller/Groups/OfferController.php +++ b/src/Controller/Groups/OfferController.php @@ -16,7 +16,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface; use Symfony\Component\Routing\Attribute\Route; class OfferController extends AbstractController @@ -68,15 +67,10 @@ class OfferController extends AbstractController priceBreakdown: $priceBreakdown, ); - $session = $request->getSession(); - $justAccepted = $session instanceof FlashBagAwareSessionInterface - && [] !== $session->getFlashBag()->get('accommodation_offer_accepted'); - return $this->render('groups/booking/offer.html.twig', [ 'booking' => $booking, 'priceBreakdown' => $priceBreakdown, 'ctx' => $ctx, - 'justAccepted' => $justAccepted, ]); } @@ -100,7 +94,7 @@ class OfferController extends AbstractController if ($confirmationForm->isSubmitted() && $confirmationForm->isValid()) { $this->bookingService->acceptBooking($booking); - $this->addFlash('accommodation_offer_accepted', true); + $this->addFlash('success', 'Deine Buchung ist bestätigt.'); return $this->redirectToOfferPage($request, $uuid); } diff --git a/src/Form/Model/BookingSummaryDto.php b/src/Form/Model/BookingSummaryDto.php index ed34d606..817b4345 100644 --- a/src/Form/Model/BookingSummaryDto.php +++ b/src/Form/Model/BookingSummaryDto.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Form\Model; -use App\Model\BookingSummaryCmsHotelData; +use App\Model\CmsHotelData; /** * DTO containing all booking summary data for sidebar display. @@ -19,7 +19,7 @@ class BookingSummaryDto public readonly int $participantCount, public readonly BookingSummaryPricingDto $pricing, public readonly BookingSummaryVoucherDto $vouchers, - public readonly ?BookingSummaryCmsHotelData $cmsData, + public readonly ?CmsHotelData $cmsData, ) { } } diff --git a/src/Model/AccommodationBookingContext.php b/src/Model/AccommodationBookingContext.php index 75882d60..28dfc444 100644 --- a/src/Model/AccommodationBookingContext.php +++ b/src/Model/AccommodationBookingContext.php @@ -16,11 +16,10 @@ final readonly class AccommodationBookingContext * @param array $groupedAdditionalServices * @param AdditionalService[] $ungroupedAdditionalServices * @param array|null $priceBreakdown - * @param array|null $hotelCmsData see {@see \App\Service\CmsDataProvider::getHotelDetails()} for the shape */ public function __construct( public Accommodation $accommodation, - public ?array $hotelCmsData = null, + public ?CmsHotelData $hotelCmsData = null, public array $boardServices = [], public array $additionalServices = [], public array $groupedAdditionalServices = [], diff --git a/src/Model/BookingSummaryCmsHotelData.php b/src/Model/BookingSummaryCmsHotelData.php deleted file mode 100644 index 54cf1069..00000000 --- a/src/Model/BookingSummaryCmsHotelData.php +++ /dev/null @@ -1,21 +0,0 @@ -|null $images - */ - public function __construct( - public ?string $name, - public ?string $address, - public ?array $images, - ) { - } -} diff --git a/src/Model/CmsHotelData.php b/src/Model/CmsHotelData.php new file mode 100644 index 00000000..745c1ce7 --- /dev/null +++ b/src/Model/CmsHotelData.php @@ -0,0 +1,31 @@ +|null $images + * @param array|null $icons + */ + public function __construct( + public ?string $name, + public ?string $address, + public ?array $images, + public ?string $description = null, + public ?string $features = null, + public ?string $roomTypes = null, + public ?string $additionalInformation = null, + public ?array $icons = null, + public ?CmsRegionData $region = null, + ) { + } +} diff --git a/src/Model/CmsRegionData.php b/src/Model/CmsRegionData.php new file mode 100644 index 00000000..5335e99d --- /dev/null +++ b/src/Model/CmsRegionData.php @@ -0,0 +1,32 @@ +|null $images + * @param list|null $regionMaps + */ + public function __construct( + public ?string $name, + public ?float $latitude, + public ?float $longitude, + public ?string $webcam, + public ?string $skiArea, + public ?string $skiAreaExtended, + public ?string $description, + public ?string $news, + public ?int $length, + public ?int $altitude, + public ?int $lifts, + public ?array $images, + public ?array $regionMaps, + ) { + } +} diff --git a/src/Service/AccommodationBookingService.php b/src/Service/AccommodationBookingService.php index 304cf074..e1a8dec3 100644 --- a/src/Service/AccommodationBookingService.php +++ b/src/Service/AccommodationBookingService.php @@ -12,6 +12,7 @@ use App\Entity\Groups\AdditionalService; use App\Entity\Groups\BoardService; use App\Form\Model\AccommodationBookingDto; use App\Model\AccommodationBookingQueryParams; +use App\Model\CmsHotelData; use App\Model\InquiryStatus; use App\Repository\Groups\AccommodationPriceRepository; use App\Repository\Groups\AccommodationRepository; @@ -104,16 +105,10 @@ class AccommodationBookingService * Fetches hotel details from the CMS for display in the booking flow — name, images, * address, descriptive text blocks, amenity icons, and the surrounding region's info * (name, coordinates, webcam, ski area text, stats, images). - * - * @return array|null null on CMS error or missing code, see - * {@see CmsDataProvider::getHotelDetails()} for the shape */ - public function loadHotelCmsData(Accommodation $accommodation): ?array + public function loadHotelCmsData(Accommodation $accommodation): ?CmsHotelData { - $code = $accommodation->getEffectiveCmsCode(); - $data = $this->cmsDataProvider->getHotelDetails($code); - - return isset($data['success']) && false === $data['success'] ? null : $data; + return $this->cmsDataProvider->getHotelDetails($accommodation->getEffectiveCmsCode()); } /** diff --git a/src/Service/BookingSummaryAssembler.php b/src/Service/BookingSummaryAssembler.php index 26845697..401f8f73 100644 --- a/src/Service/BookingSummaryAssembler.php +++ b/src/Service/BookingSummaryAssembler.php @@ -12,7 +12,7 @@ use App\Form\Model\BookingDto; use App\Form\Model\BookingSummaryDto; use App\Form\Model\BookingSummaryPricingDto; use App\Form\Model\BookingSummaryVoucherDto; -use App\Model\BookingSummaryCmsHotelData; +use App\Model\CmsHotelData; use Psr\Log\LoggerInterface; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; @@ -126,7 +126,7 @@ class BookingSummaryAssembler * Data is cached for 1 hour. This method can be called early in the booking * flow to warm the cache. */ - public function getCmsDataForProduct(?string $productCode, ?string $hotelCode): ?BookingSummaryCmsHotelData + public function getCmsDataForProduct(?string $productCode, ?string $hotelCode): ?CmsHotelData { if (null === $hotelCode) { return null; @@ -144,7 +144,7 @@ class BookingSummaryAssembler // Fetch CMS images (nice to have) $images = $this->cmsDataService->getProductImages($productCode, $hotelCode); - return new BookingSummaryCmsHotelData( + return new CmsHotelData( name: $baseHotel?->name, address: $this->formatHotelAddress($baseHotel), images: $images, diff --git a/src/Service/CmsDataProvider.php b/src/Service/CmsDataProvider.php index 6ae739db..ab0342d7 100644 --- a/src/Service/CmsDataProvider.php +++ b/src/Service/CmsDataProvider.php @@ -2,6 +2,8 @@ namespace App\Service; +use App\Model\CmsHotelData; +use App\Model\CmsRegionData; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; @@ -37,13 +39,18 @@ class CmsDataProvider */ public function getHotelImages(string $hotelCode): ?array { - $result = $this->getHotelDetails($hotelCode); + return $this->getHotelDetails($hotelCode)?->images; + } - if (true === isset($result['success']) && false === $result['success']) { + public function getHotelDetails(string $hotelCode): ?CmsHotelData + { + $data = $this->fetchHotelDetails($hotelCode); + + if (isset($data['success']) && false === $data['success']) { return null; } - return $result['images'] ?? null; + return $this->mapHotelData($data); } /** @@ -54,8 +61,8 @@ class CmsDataProvider * address?: string, * description?: string, * features?: string, - * roomTypes?: string, - * additionalInformation?: string, + * room_types?: string, + * additional_information?: string, * images?: array, * icons?: array, * region?: array{ @@ -75,7 +82,7 @@ class CmsDataProvider * }, * } */ - public function getHotelDetails(string $hotelCode): array + private function fetchHotelDetails(string $hotelCode): array { return $this->cache->get( sprintf('cms_hotel_%s', $hotelCode), @@ -114,6 +121,56 @@ class CmsDataProvider ); } + /** + * @param array $data see {@see self::fetchHotelDetails()} for the shape + */ + private function mapHotelData(array $data): CmsHotelData + { + return new CmsHotelData( + name: $data['name'] ?? null, + address: $data['address'] ?? null, + images: $data['images'] ?? null, + description: $data['description'] ?? null, + features: $data['features'] ?? null, + roomTypes: $data['room_types'] ?? null, + additionalInformation: $data['additional_information'] ?? null, + icons: $data['icons'] ?? null, + region: isset($data['region']) ? $this->mapRegionData($data['region']) : null, + ); + } + + /** + * @param array $region see {@see self::fetchHotelDetails()} for the shape + */ + private function mapRegionData(array $region): CmsRegionData + { + return new CmsRegionData( + name: $region['name'] ?? null, + latitude: self::toFloatOrNull($region['latitude'] ?? null), + longitude: self::toFloatOrNull($region['longitude'] ?? null), + webcam: $region['webcam'] ?? null, + skiArea: $region['ski_area'] ?? null, + skiAreaExtended: $region['ski_area_extended'] ?? null, + description: $region['description'] ?? null, + news: $region['news'] ?? null, + length: self::toIntOrNull($region['length'] ?? null), + altitude: self::toIntOrNull($region['altitude'] ?? null), + lifts: self::toIntOrNull($region['lifts'] ?? null), + images: $region['images'] ?? null, + regionMaps: $region['region_maps'] ?? null, + ); + } + + private static function toIntOrNull(mixed $value): ?int + { + return is_numeric($value) ? (int) $value : null; + } + + private static function toFloatOrNull(mixed $value): ?float + { + return is_numeric($value) ? (float) $value : null; + } + /** @return array */ public function getProductDetails(string $productCode, ?string $hotelCode = null): array { diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index 67e4102f..4b31a37c 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -27,7 +27,6 @@ class AppExtension extends AbstractExtension new TwigFilter('map_status', [AppRuntime::class, 'mapStatus']), new TwigFilter('map_country', [AppRuntime::class, 'mapCountry']), new TwigFilter('map_nationality', [AppRuntime::class, 'mapNationality']), - new TwigFilter('visible_cms_icons', [AppRuntime::class, 'visibleCmsIcons']), ]; } diff --git a/src/Twig/AppRuntime.php b/src/Twig/AppRuntime.php index 8a254c4f..c80ff921 100644 --- a/src/Twig/AppRuntime.php +++ b/src/Twig/AppRuntime.php @@ -157,44 +157,6 @@ class AppRuntime implements RuntimeExtensionInterface ]); } - /** - * Normalizes a CMS "icons" map (identifier => bool|string|int) into the visible entries only. - * - * A value either toggles visibility (bool), makes the icon visible and overrides its - * default (humanized) label (non-empty string), or makes the icon visible with the - * number itself as the label to display next to the icon (non-zero int, e.g. bed_count). - * - * @param array|null $icons - * - * @return list - */ - public function visibleCmsIcons(?array $icons): array - { - if (null === $icons) { - return []; - } - - $result = []; - foreach ($icons as $key => $value) { - if (false === $value || null === $value || '' === $value || 0 === $value) { - continue; - } - - $label = match (true) { - is_int($value) => (string) $value, - is_string($value) => $value, - default => ucfirst(str_replace(['_', '-'], ' ', $key)), - }; - - $result[] = [ - 'key' => $key, - 'label' => $label, - ]; - } - - return $result; - } - /** * Checks if a form field should be rendered as static text. * diff --git a/templates/admin/accommodation/_form.html.twig b/templates/admin/accommodation/_form.html.twig index 4c9e8553..c86978b6 100644 --- a/templates/admin/accommodation/_form.html.twig +++ b/templates/admin/accommodation/_form.html.twig @@ -11,7 +11,7 @@ {{ form_row(form.maxAdolescentAge) }} {{ form_row(form.currency) }} - {% if cmsData is defined and cmsData.name is defined %} + {% if cmsData is not null and cmsData.name is not null %}
{% if cmsData.images.resized.m | length > 0 %} {% set image = cmsData.images.resized.m[0] %} diff --git a/templates/groups/booking/_offer_summary.html.twig b/templates/groups/booking/_offer_summary.html.twig index e6f2ba98..aa9bdaf0 100644 --- a/templates/groups/booking/_offer_summary.html.twig +++ b/templates/groups/booking/_offer_summary.html.twig @@ -2,7 +2,7 @@
{# Header #} -
+
{% if ctx.priceBreakdown is not null %}
Gesamtpreis @@ -31,7 +31,7 @@ {# Accommodation info #} {% set hasImage = ctx.hotelCmsData is not null - and ctx.hotelCmsData.images is defined + and ctx.hotelCmsData.images is not null and ctx.hotelCmsData.images.resized is defined and ctx.hotelCmsData.images.resized.l is defined and ctx.hotelCmsData.images.resized.l[0] is defined %} @@ -45,7 +45,7 @@ {% endif %}
{{ ctx.accommodation.name }} - {% if ctx.hotelCmsData is not null and ctx.hotelCmsData.address is defined %} + {% if ctx.hotelCmsData is not null and ctx.hotelCmsData.address is not null %}
{{ ctx.hotelCmsData.address|nl2br }}
{% endif %}
@@ -180,25 +180,17 @@
{% endif %} + + {% if booking.remarks %} +
+
Anmerkungen
+
+ {{ booking.remarks | nl2br }} +
+
+ {% endif %} +
- {# Footer: booking action / accepted-at status #} - {% if booking.inquiry %} -
- -
- {% elseif booking.acceptedAt is not null %} -
-

- Angenommen am {{ booking.acceptedAt | date('d.m.Y') }} um {{ booking.acceptedAt | date('H:i') }} Uhr -

-
- {% endif %}
{% endblock %} diff --git a/templates/groups/booking/_step4_services_recap.html.twig b/templates/groups/booking/_step4_services_recap.html.twig index 836d162f..ca4c190b 100644 --- a/templates/groups/booking/_step4_services_recap.html.twig +++ b/templates/groups/booking/_step4_services_recap.html.twig @@ -14,6 +14,9 @@ Verpflegung: {% if selectedBoardService %} {{ selectedBoardService.label }} + {% if selectedBoardService.price > 0 %} + — {{ (selectedBoardService.price / 100)|format_currency(ctx.accommodation.currency) }} pro Person und Nacht + {% endif %} {% if selectedBoardService.description %}
{{ selectedBoardService.description|nl2br }}
{% endif %} diff --git a/templates/groups/booking/_summary.html.twig b/templates/groups/booking/_summary.html.twig index fb1ef3dd..7a341580 100644 --- a/templates/groups/booking/_summary.html.twig +++ b/templates/groups/booking/_summary.html.twig @@ -32,7 +32,7 @@ {# Accommodation info #} {% set hasImage = ctx.hotelCmsData is not null - and ctx.hotelCmsData.images is defined + and ctx.hotelCmsData.images is not null and ctx.hotelCmsData.images.resized is defined and ctx.hotelCmsData.images.resized.l is defined and ctx.hotelCmsData.images.resized.l[0] is defined %} @@ -46,7 +46,7 @@ {% endif %}
{{ ctx.accommodation.name }} - {% if ctx.hotelCmsData is not null and ctx.hotelCmsData.address is defined %} + {% if ctx.hotelCmsData is not null and ctx.hotelCmsData.address is not null %}
{{ ctx.hotelCmsData.address|nl2br }}
{% endif %}
diff --git a/templates/groups/booking/offer.html.twig b/templates/groups/booking/offer.html.twig index 90ecc1c6..fdd29229 100644 --- a/templates/groups/booking/offer.html.twig +++ b/templates/groups/booking/offer.html.twig @@ -1,5 +1,29 @@ {% extends 'layout_booking.html.twig' %} +{% macro imageGallery(images, thumbImages, defaultAlt) %} + {% if images|length > 0 %} +
+ {% for image in images %} +
+ {{ image.alt ?? defaultAlt }} + {% if loop.index0 == 1 and images|length > 2 %} +
+ +{{ images|length - 2 }} +
+ {% endif %} +
+ {% endfor %} +
+ {% endif %} +{% endmacro %} + +{% import _self as macros %} + {% block title %}{{ booking.inquiry ? 'Dein Angebot' : 'Deine Buchung' }}{% endblock %} {% block background %}bg-outer bg-outer--summer{% endblock %} @@ -23,18 +47,25 @@
{# Main area #} -
+
+ {% include '_partials/_flashes.html.twig' %} -

- {{ booking.inquiry ? 'Dein Angebot' : 'Deine Buchung' }} -

-

{{ ctx.accommodation.name }}

+
+

+ {{ booking.inquiry ? 'Dein Angebot' : 'Deine Buchung' }} +

+

+ {{ ctx.accommodation.name }} +

+
{# Booked services #} {% if booking.boardServiceLabel or booking.additionalServices | length > 0 %} -
-

Gebuchte Leistungen

-
    +
    +

    + {{ booking.inquiry ? 'Angefragte' : 'Gebuchte' }} Leistungen +

    +
      {% if booking.boardServiceLabel %}
    • {{ booking.boardServiceLabel }}
    • {% endif %} @@ -45,150 +76,141 @@
    {% endif %} + {% if booking.inquiry %} +
    + +
    + {% elseif booking.acceptedAt is not null %} +
    + {{ icon('info', 'w-5 h-5') }} +
    + Angebot angenommen am {{ booking.acceptedAt | date('d.m.Y') }} +
    +
    + {% endif %} + + {# hotel description #} {% if ctx.hotelCmsData is not null %} -
    -

    - Die Unterkunft -

    - {{ ctx.hotelCmsData.description | raw }} -

    - Details zur Unterkunft -

    - {{ ctx.hotelCmsData.features | raw }} -

    - Die Zimmer -

    - {{ ctx.hotelCmsData.roomTypes | raw }} -

    - Weitere Infos -

    - {{ ctx.hotelCmsData.additionalInformation | raw }} -

    - Bilder -

    -
    - {% set largeImages = ctx.hotelCmsData.images.resized.l %} - {% set mediumImages = ctx.hotelCmsData.images.resized.m %} - {% if largeImages | length > 0 %} -
    - {% for image in largeImages %} -
    - {{ image.alt ?? ctx.accommodation.name }} - {% if loop.index0 == 1 and largeImages | length > 2 %} -
    - +{{ largeImages | length - 2 }} -
    - {% endif %} -
    - {% endfor %} +
    + + +
    + {% set icons = ctx.hotelCmsData.icons %} + {% if icons is not empty %} +
      + {% for icon, label in icons %} +
    • +
      + {{ icon(icon, 'w-8 h-8 shrink-0 text-gray-900') }} +
      + {{ label }} +
    • + {% endfor %} +
    + {% endif %} + + {{ ctx.hotelCmsData.description | raw }} + +

    + Details zur Unterkunft +

    + {{ ctx.hotelCmsData.features | raw }} + +

    + Die Zimmer +

    + {{ ctx.hotelCmsData.roomTypes | raw }} + +

    + Weitere Infos +

    + {{ ctx.hotelCmsData.additionalInformation | raw }}
    - {% endif %} - {% set visibleIcons = (ctx.hotelCmsData.icons ?? null) | visible_cms_icons %} - {% if visibleIcons is not empty %} -
      - {% for item in visibleIcons %} -
    • - {{ icon(item.key, 'w-5 h-5 shrink-0') }} - {{ item.label }} -
    • - {% endfor %} -
    - {% endif %} +
    + + {{ macros.imageGallery(ctx.hotelCmsData.images.resized.l, ctx.hotelCmsData.images.resized.m, ctx.accommodation.name) }} {% endif %} - {% if ctx.hotelCmsData is not null and ctx.hotelCmsData.region is defined %} + {# region description #} + {% if ctx.hotelCmsData is not null and ctx.hotelCmsData.region is not null %} {% set region = ctx.hotelCmsData.region %} -
    -

    Die Region {{ region.name }}

    - {% if region.altitude or region.length or region.lifts %} -
      - {% if region.altitude %} -
    • {{ region.altitude }} m Höhe
    • - {% endif %} - {% if region.length %} -
    • {{ region.length }} km Pisten
    • - {% endif %} - {% if region.lifts %} -
    • {{ region.lifts }} Lifte
    • - {% endif %} -
    - {% endif %} - {% if region.webcam %} -

    - Webcam der Region -

    - {% endif %} - {{ region.description | default('') | raw }} -

    Skigebiet

    - {{ region.ski_area | default('') | raw }} - {{ region.ski_area_extended | default('') | raw }} - {% if region.news %} -

    Aktuelles aus der Region

    - {{ region.news | raw }} - {% endif %} -
    - {% if region.region_maps is defined and region.region_maps | length > 0 %} -
    - {% for mapUrl in region.region_maps %} -
    - {{ region.name }} - {% if loop.index0 == 1 and region.region_maps | length > 2 %} -
    - +{{ region.region_maps | length - 2 }} -
    - {% endif %} -
    - {% endfor %} -
    - {% endif %} - {% set regionLargeImages = region.images.resized.l ?? [] %} - {% set regionMediumImages = region.images.resized.m ?? [] %} - {% if regionLargeImages | length > 0 %} -
    - {% for image in regionLargeImages %} -
    - {{ image.alt ?? region.name }} - {% if loop.index0 == 1 and regionLargeImages | length > 2 %} -
    - +{{ regionLargeImages | length - 2 }} -
    - {% endif %} -
    - {% endfor %} -
    - {% endif %} - {% endif %} +
    + - {% if booking.remarks %} -
    -

    Anmerkungen

    -

    {{ booking.remarks | nl2br }}

    +
    + {% if region.altitude or region.length or region.lifts %} +
      + {% if region.altitude %} +
    • {{ region.altitude }} m Höhe
    • + {% endif %} + {% if region.length %} +
    • {{ region.length }} km Pisten
    • + {% endif %} + {% if region.lifts %} +
    • {{ region.lifts }} Lifte
    • + {% endif %} +
    + {% endif %} + {% if region.webcam %} +

    + Webcam der Region +

    + {% endif %} + {{ region.description | default('') | raw }} + +

    + Skigebiet +

    + {{ region.skiArea | default('') | raw }} + {{ region.skiAreaExtended | default('') | raw }} + + {% if region.news %} +

    Aktuelles aus der Region

    + {{ region.news | raw }} + {% endif %} +
    + + {% if region.regionMaps is not null and region.regionMaps | length > 0 %} + {% set regionMapImages = region.regionMaps | map(url => {url: url}) %} + {{ macros.imageGallery(regionMapImages, [], region.name) }} + {% endif %} + + {{ macros.imageGallery(region.images.resized.l ?? [], region.images.resized.m ?? [], region.name) }} {% endif %}
    - - {% if justAccepted %} -
    - {% include '_partials/_alert.html.twig' with { - level: 'success', - title: 'Bestätigt', - messages: ['Deine Buchung ist verbindlich bestätigt.'], - } %} -
    - {% endif %} {% endblock %} diff --git a/tests/Controller/Groups/OfferControllerTest.php b/tests/Controller/Groups/OfferControllerTest.php index 5bb301e5..4d7ad0d4 100644 --- a/tests/Controller/Groups/OfferControllerTest.php +++ b/tests/Controller/Groups/OfferControllerTest.php @@ -118,7 +118,6 @@ class OfferControllerTest extends TestCase self::assertSame(Response::HTTP_OK, $response->getStatusCode()); self::assertSame('offer', $response->getContent()); self::assertSame('groups/booking/offer.html.twig', $controller->renderedView); - self::assertFalse($controller->renderedParameters['justAccepted']); self::assertSame($booking, $controller->renderedParameters['booking']); self::assertSame(['total' => 1000], $controller->renderedParameters['priceBreakdown']); self::assertSame($booking->getAccommodation(), $controller->renderedParameters['ctx']->accommodation); @@ -147,39 +146,6 @@ class OfferControllerTest extends TestCase self::assertSame('groups/booking/offer_unavailable.html.twig', $controller->renderedView); } - public function testViewMarksJustAcceptedWhenFlashIsPresent(): void - { - $booking = new AccommodationBooking(); - $booking->setIsInquiry(false); - $booking->setAccommodation(new Accommodation()); - - $bookingRepository = $this->createMock(AccommodationBookingRepository::class); - $bookingRepository->method('findOneBy')->willReturn($booking); - - $linkSigner = $this->createMock(AccommodationBookingLinkSigner::class); - $linkSigner->method('isSessionAuthorized')->willReturn(true); - - $breakdownCalculator = $this->createMock(AccommodationBookingBreakdownCalculator::class); - $breakdownCalculator->method('compute')->willReturn(['total' => 1000]); - - $controller = new TestableOfferController( - $bookingRepository, - $linkSigner, - $breakdownCalculator, - $this->createMock(AccommodationBookingService::class), - ); - - $session = new Session(new MockArraySessionStorage()); - $session->getFlashBag()->add('accommodation_offer_accepted', true); - - $request = Request::create('/groups/booking/offer/'.$booking->getUuid().'/view'); - $request->setSession($session); - $response = $controller->view($booking->getUuid(), $request); - - self::assertSame(Response::HTTP_OK, $response->getStatusCode()); - self::assertTrue($controller->renderedParameters['justAccepted']); - } - public function testConfirmGetRendersModalWithFreshForm(): void { $booking = new AccommodationBooking(); @@ -243,6 +209,7 @@ class OfferControllerTest extends TestCase self::assertInstanceOf(RedirectResponse::class, $response); self::assertSame('/app_groups_booking_offer_view?uuid='.$booking->getUuid(), $response->getTargetUrl()); + self::assertSame([['type' => 'success', 'message' => 'Deine Buchung ist bestätigt.']], $controller->flashes); } public function testConfirmPostValidViaHtmxReturnsHxRedirect(): void @@ -407,9 +374,14 @@ final class TestableOfferController extends OfferController return 'https://example.test/agb/'; } + /** + * @var array + */ + public array $flashes = []; + protected function addFlash(string $type, mixed $message): void { - // no-op: avoids requiring a full service container in these unit tests + $this->flashes[] = ['type' => $type, 'message' => $message]; } /** diff --git a/tests/Service/CmsDataProviderTest.php b/tests/Service/CmsDataProviderTest.php new file mode 100644 index 00000000..690cfee5 --- /dev/null +++ b/tests/Service/CmsDataProviderTest.php @@ -0,0 +1,74 @@ + new MockResponse(json_encode([ + 'success' => true, + 'name' => 'Hotel Alpin', + 'address' => "Musterstraße 1\n1234 Musterort", + 'description' => '

    Beschreibung

    ', + 'features' => '

    Ausstattung

    ', + 'room_types' => '

    Zimmer

    ', + 'additional_information' => '

    Weitere Infos

    ', + 'images' => ['resized' => ['l' => [['url' => 'l.jpg', 'alt' => 'Alt']]]], + 'icons' => ['sauna' => ['label' => 'Sauna', 'value' => true]], + 'region' => [ + 'name' => 'Alpenregion', + 'latitude' => 47.1, + 'longitude' => 11.2, + 'webcam' => 'https://webcam.test', + 'ski_area' => '

    Skigebiet

    ', + 'ski_area_extended' => '

    Mehr Skigebiet

    ', + 'description' => '

    Region

    ', + 'news' => '

    News

    ', + 'length' => '42', + 'altitude' => '1800', + 'lifts' => '5', + 'images' => ['resized' => ['l' => []]], + 'region_maps' => ['map1.jpg', 'map2.jpg'], + ], + ], JSON_THROW_ON_ERROR))); + + $provider = new CmsDataProvider($client, new ArrayAdapter(), 'test-key'); + + $data = $provider->getHotelDetails('HOTEL1'); + + self::assertNotNull($data); + self::assertSame('Hotel Alpin', $data->name); + self::assertSame("Musterstraße 1\n1234 Musterort", $data->address); + self::assertSame('

    Zimmer

    ', $data->roomTypes); + self::assertSame('

    Weitere Infos

    ', $data->additionalInformation); + self::assertSame(['sauna' => ['label' => 'Sauna', 'value' => true]], $data->icons); + self::assertNotNull($data->region); + self::assertSame('Alpenregion', $data->region->name); + self::assertSame('

    Skigebiet

    ', $data->region->skiArea); + self::assertSame('

    Mehr Skigebiet

    ', $data->region->skiAreaExtended); + self::assertSame(['map1.jpg', 'map2.jpg'], $data->region->regionMaps); + self::assertSame(42, $data->region->length); + self::assertSame(1800, $data->region->altitude); + self::assertSame(5, $data->region->lifts); + self::assertSame(47.1, $data->region->latitude); + self::assertSame(11.2, $data->region->longitude); + } + + public function testGetHotelDetailsReturnsNullOnFailure(): void + { + $client = new MockHttpClient(fn () => new MockResponse('', ['http_code' => 500])); + + $provider = new CmsDataProvider($client, new ArrayAdapter(), 'test-key'); + + self::assertNull($provider->getHotelDetails('UNKNOWN')); + } +}