feat: consistent display of included services and prices

This commit is contained in:
Björn Fromme
2026-03-16 12:00:56 +01:00
parent b111a615a9
commit 3dd92ccdb4
11 changed files with 54 additions and 27 deletions
+5 -1
View File
@@ -76,10 +76,14 @@ class Pickup
{ {
$label = $this->getLabel(); $label = $this->getLabel();
if (null === $this->price || 0.0 === $this->price) { if (null === $this->price) {
return $label; return $label;
} }
if (0.0 === $this->price) {
return sprintf('%s (inkl.)', $label);
}
if ($this->price < 0) { if ($this->price < 0) {
return sprintf('%s (-%s€ Rabatt)', $label, number_format(abs($this->price), 2, ',', '.')); return sprintf('%s (-%s€ Rabatt)', $label, number_format(abs($this->price), 2, ',', '.'));
} }
+10 -2
View File
@@ -102,10 +102,14 @@ class BookingEditParticipantType extends AbstractType
'choice_value' => 'id', 'choice_value' => 'id',
'choice_label' => function (?Service $service) use ($participantIndex) { 'choice_label' => function (?Service $service) use ($participantIndex) {
$price = $service->individualPrice[$participantIndex] ?? $service->price; $price = $service->individualPrice[$participantIndex] ?? $service->price;
if (null === $price || 0.0 === $price) { if (null === $price) {
return $service->label; return $service->label;
} }
if (0.0 === $price) {
return sprintf('%s (inkl.)', $service->label);
}
return sprintf( return sprintf(
'%s (%s&nbsp;€)', '%s (%s&nbsp;€)',
$service->label, $service->label,
@@ -256,10 +260,14 @@ class BookingEditParticipantType extends AbstractType
$price = $pickup->price; $price = $pickup->price;
if (null === $price || 0.0 === $price) { if (null === $price) {
return $pickupLabel; return $pickupLabel;
} }
if (0.0 === $price) {
return sprintf('%s (inkl.)', $pickupLabel);
}
return sprintf( return sprintf(
'%s %s€', '%s %s€',
$pickupLabel, $pickupLabel,
@@ -605,10 +605,14 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
return ''; return '';
} }
if (null === $service->price || 0.0 === $service->price) { if (null === $service->price) {
return $service->label; return $service->label;
} }
if (0.0 === $service->price) {
return sprintf('%s (inkl.)', $service->label);
}
if ($service->price < 0) { if ($service->price < 0) {
// Negative prices are discounts // Negative prices are discounts
return sprintf('%s (-%s€ Rabatt)', $service->label, number_format(abs($service->price), 2, ',', '.')); return sprintf('%s (-%s€ Rabatt)', $service->label, number_format(abs($service->price), 2, ',', '.'));
+1 -1
View File
@@ -86,7 +86,7 @@ class ParticipantCardDataService
$name = trim($firstName.' '.$lastName); $name = trim($firstName.' '.$lastName);
if ('' === $name) { if ('' === $name) {
return 0 === $index ? 'Anmelder:in' : sprintf('Teilnehmer:in %d', $index); return 0 === $index ? 'Anmelder:in' : 'Teilnehmer:in';
} }
return $name; return $name;
-4
View File
@@ -217,10 +217,6 @@ class ServicePricingCalculator
$servicesBySubtypeAndSign = []; $servicesBySubtypeAndSign = [];
foreach ($serviceAggregation as $serviceData) { foreach ($serviceAggregation as $serviceData) {
if (0.0 === $serviceData['totalPrice']) {
continue; // Skip zero-price services
}
$subType = $serviceData['subType'] ?? 'other'; $subType = $serviceData['subType'] ?? 'other';
// Normalize rental subtypes to avoid duplicate sections // Normalize rental subtypes to avoid duplicate sections
+1
View File
@@ -14,6 +14,7 @@ class AppExtension extends AbstractExtension
new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']), new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']),
new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]), new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
new TwigFilter('format_money', [AppRuntime::class, 'formatMoney']), new TwigFilter('format_money', [AppRuntime::class, 'formatMoney']),
new TwigFilter('format_service_price', [AppRuntime::class, 'formatServicePrice']),
new TwigFilter('map_gender', [AppRuntime::class, 'mapGender']), new TwigFilter('map_gender', [AppRuntime::class, 'mapGender']),
new TwigFilter('map_status', [AppRuntime::class, 'mapStatus']), new TwigFilter('map_status', [AppRuntime::class, 'mapStatus']),
new TwigFilter('map_country', [AppRuntime::class, 'mapCountry']), new TwigFilter('map_country', [AppRuntime::class, 'mapCountry']),
+20
View File
@@ -52,6 +52,26 @@ class AppRuntime implements RuntimeExtensionInterface
return $this->intlExtension->formatCurrency($amount, 'EUR'); return $this->intlExtension->formatCurrency($amount, 'EUR');
} }
/**
* Formats a service price, showing "inkl." for zero-priced (included) services.
*
* @param float|int|null $price The price to format
*
* @return string The formatted price or "inkl." for zero/null prices
*/
public function formatServicePrice(float|int|null $price): string
{
if (null === $price) {
return '';
}
if (0 === $price || 0.0 === $price) {
return 'inkl.';
}
return $this->intlExtension->formatCurrency((float) $price, 'EUR');
}
public function mapStatus(string $status): string public function mapStatus(string $status): string
{ {
$status = strtoupper($status); $status = strtoupper($status);
+2 -2
View File
@@ -67,8 +67,8 @@
</td> </td>
{% if choiceData %} {% if choiceData %}
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap"> <td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if choiceData.price is defined and choiceData.price is not null %} {% if choiceData.price is defined %}
{{ choiceData.price | format_currency('EUR') }} {{ choiceData.price | format_service_price }}
{% endif %} {% endif %}
</td> </td>
{% endif %} {% endif %}
+6 -12
View File
@@ -51,9 +51,7 @@
{%- endif -%} {%- endif -%}
</td> </td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap"> <td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if price is not null %} {{ price | format_service_price }}
{{ price | format_currency('EUR') }}
{% endif %}
</td> </td>
<td class="border border-primary-bg p-2 align-top w-12 text-center"> <td class="border border-primary-bg p-2 align-top w-12 text-center">
{% set childTooltip = child.vars.attr['data-tooltip']|default(null) %} {% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
@@ -84,9 +82,7 @@
{%- endif -%} {%- endif -%}
</td> </td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap"> <td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if price is not null %} {{ price | format_service_price }}
{{ price | format_currency('EUR') }}
{% endif %}
</td> </td>
<td class="border border-primary-bg p-2 align-top w-12 text-center"> <td class="border border-primary-bg p-2 align-top w-12 text-center">
{% set fieldTooltip = checkboxField.vars.attr['data-tooltip']|default(null) %} {% set fieldTooltip = checkboxField.vars.attr['data-tooltip']|default(null) %}
@@ -124,9 +120,7 @@
{%- endif -%} {%- endif -%}
</td> </td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap"> <td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if price is not null %} {{ price | format_service_price }}
{{ price | format_currency('EUR') }}
{% endif %}
</td> </td>
<td class="border border-primary-bg p-2 align-top w-12 text-center"> <td class="border border-primary-bg p-2 align-top w-12 text-center">
{% set childTooltip = child.vars.attr['data-tooltip']|default(null) %} {% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
@@ -364,11 +358,11 @@
) }} ) }}
{% endif %} {% endif %}
</table> </table>
{% elseif form.skiPass is defined %} {% elseif form.skiPass is defined and not form.vars.data.participant.skiPass %}
{# Skipass field exists but no rentals yet - user needs to select skipass first #} {# Skipass field exists but no skipass selected yet #}
<div class="p-2 text-sm text-gray-500">Bitte zuerst den Skipass auswählen</div> <div class="p-2 text-sm text-gray-500">Bitte zuerst den Skipass auswählen</div>
{% else %} {% else %}
{# No skipass field (e.g., baby participant) - rentals not available #} {# No skipass field or skipass selected but no matching rentals #}
<div class="p-2 text-sm text-gray-500">Nicht wählbar</div> <div class="p-2 text-sm text-gray-500">Nicht wählbar</div>
{% endif %} {% endif %}
</fieldset> </fieldset>
+1 -1
View File
@@ -146,7 +146,7 @@
{{ servicePricing.participantCount }}x {{ servicePricing.label }} {{ servicePricing.participantCount }}x {{ servicePricing.label }}
</td> </td>
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap"> <td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
{{ servicePricing.totalPrice | format_currency('EUR') }} {{ servicePricing.totalPrice | format_service_price }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -289,7 +289,7 @@ class ParticipantCardDataServiceTest extends TestCase
$this->assertEquals('500,00 €', $result[1]['price']); $this->assertEquals('500,00 €', $result[1]['price']);
// Third participant (no name) // Third participant (no name)
$this->assertEquals('Teilnehmer:in 2', $result[2]['name']); $this->assertEquals('Teilnehmer:in', $result[2]['name']);
$this->assertEquals('Doppelzimmer', $result[2]['roomName']); $this->assertEquals('Doppelzimmer', $result[2]['roomName']);
$this->assertEquals('480,00 €', $result[2]['price']); $this->assertEquals('480,00 €', $result[2]['price']);
} }
@@ -347,8 +347,8 @@ class ParticipantCardDataServiceTest extends TestCase
$result3 = $this->service->getCardData($bookingDto, 2); $result3 = $this->service->getCardData($bookingDto, 2);
$this->assertEquals('Anmelder:in', $result1['name']); $this->assertEquals('Anmelder:in', $result1['name']);
$this->assertEquals('Teilnehmer:in 1', $result2['name']); $this->assertEquals('Teilnehmer:in', $result2['name']);
$this->assertEquals('Teilnehmer:in 2', $result3['name']); $this->assertEquals('Teilnehmer:in', $result3['name']);
} }
public function testGetCardDataWithValidationReturnsValidCard(): void public function testGetCardDataWithValidationReturnsValidCard(): void