feat: extended insurance parsing, pass info urls to view
This commit is contained in:
@@ -37,6 +37,9 @@ class Insurance
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public bool $package = false;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public bool $complementary = false;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
public ?\DateTimeImmutable $travelDateFrom = null;
|
||||
@@ -110,4 +113,66 @@ class Insurance
|
||||
|
||||
return $this->subType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all info URLs from this insurance or its contained insurances (for packages).
|
||||
*
|
||||
* @return array<string> Array of info URLs
|
||||
*/
|
||||
public function getAllUrlsInfo(): array
|
||||
{
|
||||
if (true === $this->package) {
|
||||
return $this->collectUrlsFromContainedInsurances('urlInfo');
|
||||
}
|
||||
|
||||
return null !== $this->urlInfo ? [$this->urlInfo] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all product info URLs from this insurance or its contained insurances (for packages).
|
||||
*
|
||||
* @return array<string> Array of product info URLs
|
||||
*/
|
||||
public function getAllUrlsProductInfo(): array
|
||||
{
|
||||
if (true === $this->package) {
|
||||
return $this->collectUrlsFromContainedInsurances('urlProductInfo');
|
||||
}
|
||||
|
||||
return null !== $this->urlProductInfo ? [$this->urlProductInfo] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all terms URLs from this insurance or its contained insurances (for packages).
|
||||
*
|
||||
* @return array<string> Array of terms URLs
|
||||
*/
|
||||
public function getAllUrlsTerms(): array
|
||||
{
|
||||
if (true === $this->package) {
|
||||
return $this->collectUrlsFromContainedInsurances('urlTerms');
|
||||
}
|
||||
|
||||
return null !== $this->urlTerms ? [$this->urlTerms] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects URLs from contained insurances for a specific URL property.
|
||||
*
|
||||
* @param string $urlProperty The URL property name to collect (urlInfo, urlProductInfo, urlTerms)
|
||||
*
|
||||
* @return array<string> Array of non-null URLs from contained insurances
|
||||
*/
|
||||
private function collectUrlsFromContainedInsurances(string $urlProperty): array
|
||||
{
|
||||
$urls = [];
|
||||
|
||||
foreach ($this->containedInsurances as $containedInsurance) {
|
||||
if (null !== $containedInsurance->{$urlProperty}) {
|
||||
$urls[] = $containedInsurance->{$urlProperty};
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($urls));
|
||||
}
|
||||
}
|
||||
@@ -39,13 +39,14 @@ class InsuranceParser extends AbstractParser
|
||||
// Parse all individual insurances for reference lookup
|
||||
$insurance = $this->parseInsuranceNode($node, false);
|
||||
if (null !== $insurance && null !== $insurance->id) {
|
||||
// Set complementary flag from XML attribute
|
||||
$insurance->complementary = $isComplementary;
|
||||
|
||||
$individualInsurances[$insurance->id] = $insurance;
|
||||
|
||||
// Include only if it's NOT a complementary insurance (zusatzversicherung)
|
||||
// Complementary insurances are only available as part of packages, never standalone
|
||||
if (!$isComplementary) {
|
||||
$insurances[$insurance->id] = $insurance;
|
||||
}
|
||||
// Include ALL insurances in the result array
|
||||
// Complementary insurances will be filtered out at the form field level
|
||||
$insurances[$insurance->id] = $insurance;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -56,6 +57,15 @@ class InsuranceParser extends AbstractParser
|
||||
if (null !== $insurance && null !== $insurance->id) {
|
||||
// Parse contained insurance IDs
|
||||
$insurance->containedInsuranceIds = $this->parseContainedInsuranceIds($node);
|
||||
|
||||
// Populate containedInsurances with actual Insurance objects
|
||||
$insurance->containedInsurances = [];
|
||||
foreach ($insurance->containedInsuranceIds as $containedId) {
|
||||
if (isset($individualInsurances[$containedId])) {
|
||||
$insurance->containedInsurances[] = $individualInsurances[$containedId];
|
||||
}
|
||||
}
|
||||
|
||||
$insurances[$insurance->id] = $insurance;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -250,7 +250,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
'parking' => CheckboxType::class,
|
||||
'licensePlate' => TextType::class,
|
||||
'bulkInsuranceBooking' => CheckboxType::class,
|
||||
'insurance' => ChoiceType::class,
|
||||
'insurance' => InsuranceChoiceType::class,
|
||||
];
|
||||
|
||||
foreach ($dynamicFields as $fieldName => $fieldType) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form type for insurance selection that provides full Insurance objects to templates.
|
||||
*
|
||||
* This type extends ChoiceType to pass complete Insurance model data to the template layer,
|
||||
* enabling flexible rendering of insurance details including URLs, pricing, and coverage information.
|
||||
*/
|
||||
class InsuranceChoiceType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setRequired('insurances');
|
||||
$resolver->setAllowedTypes('insurances', 'array');
|
||||
|
||||
$resolver->setDefault('choices', function (Options $options) {
|
||||
// Prepend "no insurance" option to eligible insurances
|
||||
return array_merge(
|
||||
[0 => null],
|
||||
$options['insurances']
|
||||
);
|
||||
});
|
||||
|
||||
$resolver->setDefault('choice_value', function ($insurance) {
|
||||
// Handle "no insurance" option (null value at index 0)
|
||||
// instanceof check required because closures in configureOptions receive mixed types
|
||||
if ($insurance instanceof Insurance) {
|
||||
return (string) $insurance->id;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
$resolver->setDefault('choice_label', function ($insurance) {
|
||||
// Handle "no insurance" option (null value at index 0)
|
||||
// instanceof check required because closures in configureOptions receive mixed types
|
||||
if (!$insurance instanceof Insurance) {
|
||||
return 'Keine Versicherung';
|
||||
}
|
||||
|
||||
$label = $insurance->label;
|
||||
|
||||
if (null !== $insurance->price && $insurance->price > 0) {
|
||||
$label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.'));
|
||||
}
|
||||
|
||||
return $label;
|
||||
});
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
// Index insurances by ID for template lookup
|
||||
$insurances = [];
|
||||
foreach ($options['insurances'] as $insurance) {
|
||||
if ($insurance instanceof Insurance) {
|
||||
$insurances[(string) $insurance->id] = $insurance;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass Insurance objects to template indexed by choice value
|
||||
$view->vars['insurances'] = $insurances;
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'insurance_choice';
|
||||
}
|
||||
}
|
||||
@@ -422,12 +422,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'choices' => array_merge(
|
||||
[0 => null], // "no insurance" option
|
||||
$this->getEligibleInsurances($bookingDto, $participantIndex)
|
||||
),
|
||||
'choice_label' => fn (?Insurance $insurance) => $this->formatInsuranceLabel($insurance),
|
||||
'choice_value' => 'id',
|
||||
'insurances' => $this->getEligibleInsurances($bookingDto, $participantIndex),
|
||||
'attr' => [
|
||||
'hx-post' => $this->urlGenerator->generate('app_booking_create_step_2_refresh'),
|
||||
'hx-swap' => 'none',
|
||||
@@ -729,6 +724,10 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
|
||||
$availableInsurances = $bookingDto->travel->insurances ?? [];
|
||||
|
||||
// Exclude complementary insurances from standalone selection
|
||||
// They are only available as part of packages
|
||||
$availableInsurances = array_filter($availableInsurances, fn ($insurance) => !$insurance->complementary);
|
||||
|
||||
// Only apply insurance filtering for BookingCreateDto (creation workflow)
|
||||
if (!$bookingDto instanceof BookingCreateDto) {
|
||||
return $availableInsurances;
|
||||
|
||||
@@ -607,6 +607,10 @@ class TravelDataService
|
||||
try {
|
||||
$insurances = $this->insuranceLoader->loadAll();
|
||||
$travel->insurances = array_values($insurances);
|
||||
|
||||
// Hydrate package relationships after loading
|
||||
// Packages lose their containedInsurances during serialization, so rebuild them
|
||||
$this->hydrateInsurancePackageRelationships($travel->insurances);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Failed to load insurance data', [
|
||||
'travelId' => $travel->id,
|
||||
@@ -614,4 +618,36 @@ class TravelDataService
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs containedInsurances arrays for insurance packages.
|
||||
*
|
||||
* Insurance packages reference other insurances via containedInsuranceIds.
|
||||
* After deserialization, the containedInsurances array is empty due to
|
||||
* circular reference prevention. This method rebuilds those relationships.
|
||||
*
|
||||
* @param array<Insurance> $insurances All insurances including packages
|
||||
*/
|
||||
private function hydrateInsurancePackageRelationships(array $insurances): void
|
||||
{
|
||||
// Build lookup map of all insurances by ID (includes complementary insurances)
|
||||
$insuranceById = [];
|
||||
foreach ($insurances as $insurance) {
|
||||
$insuranceById[$insurance->id] = $insurance;
|
||||
}
|
||||
|
||||
// Reconstruct containedInsurances for each package
|
||||
foreach ($insurances as $insurance) {
|
||||
if (!$insurance->package || empty($insurance->containedInsuranceIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$insurance->containedInsurances = [];
|
||||
foreach ($insurance->containedInsuranceIds as $containedId) {
|
||||
if (isset($insuranceById[$containedId])) {
|
||||
$insurance->containedInsurances[] = $insuranceById[$containedId];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user