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();
if (null === $this->price || 0.0 === $this->price) {
if (null === $this->price) {
return $label;
}
if (0.0 === $this->price) {
return sprintf('%s (inkl.)', $label);
}
if ($this->price < 0) {
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_label' => function (?Service $service) use ($participantIndex) {
$price = $service->individualPrice[$participantIndex] ?? $service->price;
if (null === $price || 0.0 === $price) {
if (null === $price) {
return $service->label;
}
if (0.0 === $price) {
return sprintf('%s (inkl.)', $service->label);
}
return sprintf(
'%s (%s&nbsp;€)',
$service->label,
@@ -256,10 +260,14 @@ class BookingEditParticipantType extends AbstractType
$price = $pickup->price;
if (null === $price || 0.0 === $price) {
if (null === $price) {
return $pickupLabel;
}
if (0.0 === $price) {
return sprintf('%s (inkl.)', $pickupLabel);
}
return sprintf(
'%s %s€',
$pickupLabel,
@@ -605,10 +605,14 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
return '';
}
if (null === $service->price || 0.0 === $service->price) {
if (null === $service->price) {
return $service->label;
}
if (0.0 === $service->price) {
return sprintf('%s (inkl.)', $service->label);
}
if ($service->price < 0) {
// Negative prices are discounts
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);
if ('' === $name) {
return 0 === $index ? 'Anmelder:in' : sprintf('Teilnehmer:in %d', $index);
return 0 === $index ? 'Anmelder:in' : 'Teilnehmer:in';
}
return $name;
-4
View File
@@ -217,10 +217,6 @@ class ServicePricingCalculator
$servicesBySubtypeAndSign = [];
foreach ($serviceAggregation as $serviceData) {
if (0.0 === $serviceData['totalPrice']) {
continue; // Skip zero-price services
}
$subType = $serviceData['subType'] ?? 'other';
// 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_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
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_status', [AppRuntime::class, 'mapStatus']),
new TwigFilter('map_country', [AppRuntime::class, 'mapCountry']),
+20
View File
@@ -52,6 +52,26 @@ class AppRuntime implements RuntimeExtensionInterface
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
{
$status = strtoupper($status);
+2 -2
View File
@@ -67,8 +67,8 @@
</td>
{% if choiceData %}
<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 %}
{{ choiceData.price | format_currency('EUR') }}
{% if choiceData.price is defined %}
{{ choiceData.price | format_service_price }}
{% endif %}
</td>
{% endif %}
+6 -12
View File
@@ -51,9 +51,7 @@
{%- endif -%}
</td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if price is not null %}
{{ price | format_currency('EUR') }}
{% endif %}
{{ price | format_service_price }}
</td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
{% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
@@ -84,9 +82,7 @@
{%- endif -%}
</td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if price is not null %}
{{ price | format_currency('EUR') }}
{% endif %}
{{ price | format_service_price }}
</td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
{% set fieldTooltip = checkboxField.vars.attr['data-tooltip']|default(null) %}
@@ -124,9 +120,7 @@
{%- endif -%}
</td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if price is not null %}
{{ price | format_currency('EUR') }}
{% endif %}
{{ price | format_service_price }}
</td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
{% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
@@ -364,11 +358,11 @@
) }}
{% endif %}
</table>
{% elseif form.skiPass is defined %}
{# Skipass field exists but no rentals yet - user needs to select skipass first #}
{% elseif form.skiPass is defined and not form.vars.data.participant.skiPass %}
{# Skipass field exists but no skipass selected yet #}
<div class="p-2 text-sm text-gray-500">Bitte zuerst den Skipass auswählen</div>
{% 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>
{% endif %}
</fieldset>
+1 -1
View File
@@ -146,7 +146,7 @@
{{ servicePricing.participantCount }}x {{ servicePricing.label }}
</td>
<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>
</tr>
{% endfor %}
@@ -289,7 +289,7 @@ class ParticipantCardDataServiceTest extends TestCase
$this->assertEquals('500,00 €', $result[1]['price']);
// 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('480,00 €', $result[2]['price']);
}
@@ -347,8 +347,8 @@ class ParticipantCardDataServiceTest extends TestCase
$result3 = $this->service->getCardData($bookingDto, 2);
$this->assertEquals('Anmelder:in', $result1['name']);
$this->assertEquals('Teilnehmer:in 1', $result2['name']);
$this->assertEquals('Teilnehmer:in 2', $result3['name']);
$this->assertEquals('Teilnehmer:in', $result2['name']);
$this->assertEquals('Teilnehmer:in', $result3['name']);
}
public function testGetCardDataWithValidationReturnsValidCard(): void