feat: unique email addresses of participants unless child, cleanup

addresses #869axbn21
This commit is contained in:
Björn Fromme
2025-10-22 17:42:17 +02:00
parent be2fffad23
commit 9c716477aa
57 changed files with 1036 additions and 153 deletions
+61 -44
View File
@@ -1,60 +1,77 @@
{# Compact participant card with name, room, price, and edit button #}
{% set isCanceled = isCanceled|default(false) %}
{% set hasErrors = hasErrors|default(false) %}
{% set errorMessages = errorMessages|default([]) %}
{% set mode = mode|default('create') %}
<div id="participant-card-{{ index }}"
class="border rounded p-4 flex justify-between items-center
class="border rounded p-4
{{ isCanceled ? 'border-gray-400 bg-gray-50' : (hasErrors ? 'border-red-500 bg-red-50' : '') }}">
<div>
<div class="flex items-center gap-2">
<h3 class="font-semibold {{ hasErrors ? 'text-red-800' : (isCanceled ? 'text-gray-600' : '') }}" {{ qa_attribute('participant-name', index) }}>
{{ cardData.name }}
</h3>
{% if isCanceled %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-700 text-white" {{ qa_attribute('participant-canceled', index) }}>
storniert
</span>
{% elseif hasErrors %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800" {{ qa_attribute('participant-incomplete', index) }}>
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
Unvollständig
</span>
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-2">
<h3 class="font-semibold {{ hasErrors ? 'text-red-800' : (isCanceled ? 'text-gray-600' : '') }}" {{ qa_attribute('participant-name', index) }}>
{{ cardData.name }}
</h3>
{% if isCanceled %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-700 text-white" {{ qa_attribute('participant-canceled', index) }}>
storniert
</span>
{% elseif hasErrors %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800" {{ qa_attribute('participant-incomplete', index) }}>
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
Unvollständig
</span>
{% endif %}
</div>
<p class="text-sm text-gray-600" {{ qa_attribute('participant-room-name', index) }}>{{ cardData.roomName }}</p>
{# Display specific error messages #}
{% if hasErrors and errorMessages|length > 0 %}
<div class="mt-2 space-y-1" {{ qa_attribute('participant-errors', index) }}>
{% for errorMessage in errorMessages %}
<p class="text-sm text-red-700">
<svg class="w-4 h-4 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
{{ errorMessage }}
</p>
{% endfor %}
</div>
{% endif %}
</div>
<p class="text-sm text-gray-600" {{ qa_attribute('participant-room-name', index) }}>{{ cardData.roomName }}</p>
</div>
<div class="flex items-center gap-4">
<span class="font-medium {{ isCanceled ? 'text-gray-500' : '' }}" {{ qa_attribute('participant-price', index) }}>{{ cardData.price }}</span>
{% if isCanceled %}
<button type="button"
class="button bg-button bg-button--secondary opacity-50 cursor-not-allowed"
disabled
title="Stornierte Teilnehmer können nicht bearbeitet werden">
Bearbeiten
</button>
{% else %}
{% if mode == 'edit' %}
<div class="flex items-center gap-4">
<span class="font-medium {{ isCanceled ? 'text-gray-500' : '' }}" {{ qa_attribute('participant-price', index) }}>{{ cardData.price }}</span>
{% if isCanceled %}
<button type="button"
class="button bg-button {{ hasErrors ? 'bg-button--primary' : 'bg-button--secondary' }}"
hx-get="{{ path('app_booking_edit_participant', {id: bookingId, index: index}) }}"
hx-target="#main-content"
hx-swap="innerHTML"
{{ qa_attribute('btn-edit-participant', index) }}>
class="button bg-button bg-button--secondary opacity-50 cursor-not-allowed"
disabled
title="Stornierte Teilnehmer können nicht bearbeitet werden">
Bearbeiten
</button>
{% else %}
<button type="button"
class="button bg-button {{ hasErrors ? 'bg-button--primary' : 'bg-button--secondary' }}"
hx-get="{{ path('app_booking_create_step_2_participant', {index: index}) }}"
hx-target="#main-content"
hx-swap="innerHTML"
{{ qa_attribute('btn-edit-participant', index) }}>
Bearbeiten
</button>
{% if mode == 'edit' %}
<button type="button"
class="button bg-button {{ hasErrors ? 'bg-button--primary' : 'bg-button--secondary' }}"
hx-get="{{ path('app_booking_edit_participant', {id: bookingId, index: index}) }}"
hx-target="#main-content"
hx-swap="innerHTML"
{{ qa_attribute('btn-edit-participant', index) }}>
Bearbeiten
</button>
{% else %}
<button type="button"
class="button bg-button {{ hasErrors ? 'bg-button--primary' : 'bg-button--secondary' }}"
hx-get="{{ path('app_booking_create_step_2_participant', {index: index}) }}"
hx-target="#main-content"
hx-swap="innerHTML"
{{ qa_attribute('btn-edit-participant', index) }}>
Bearbeiten
</button>
{% endif %}
{% endif %}
{% endif %}
</div>
</div>
</div>
@@ -60,7 +60,14 @@
{# Contact information #}
<div class="grid grid-cols-2 gap-4">
{{ form_row(form.email) }}
{{ form_row(form.email, {
'attr': {
'hx-trigger': 'change',
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
'hx-target': '#main-content',
'hx-swap': 'innerHTML'
}
}) }}
{{ form_row(form.mobile) }}
</div>
+3 -2
View File
@@ -2,7 +2,6 @@
{% block content %}
{% include '_partials/_flashes.html.twig' %}
<div {{ stimulus_controller('toast') }}></div>
<h1>Neue Buchung</h1>
<div class="grid grid-cols-3 gap-8">
@@ -37,11 +36,13 @@
<div id="participant-cards-grid" class="space-y-4">
{% for cardData in cardsData %}
{% set hasErrors = loop.index0 in participantErrors|default([]) %}
{% set errorMessages = participantErrorMessages[loop.index0]|default([]) %}
{% include 'booking/_participant_card.html.twig' with {
'cardData': cardData,
'index': loop.index0,
'mode': 'create',
'hasErrors': hasErrors
'hasErrors': hasErrors,
'errorMessages': errorMessages
} %}
{% endfor %}
</div>
+2 -1
View File
@@ -2,7 +2,6 @@
{% block content %}
{% include '_partials/_flashes.html.twig' %}
<div {{ stimulus_controller('toast') }}></div>
{# Header with reload button #}
<div class="flex justify-between items-center pb-8">
@@ -62,12 +61,14 @@
{% for participant in bookingDto.participants %}
{% set isCanceled = (bookingData.participantsStatus[loop.index0] ?? null) == 'S' %}
{% set hasErrors = loop.index0 in participantErrors|default([]) %}
{% set errorMessages = participantErrorMessages[loop.index0]|default([]) %}
{% include 'booking/_participant_card.html.twig' with {
'cardData': cardsData[loop.index0],
'index': loop.index0,
'mode': 'edit',
'isCanceled': isCanceled,
'hasErrors': hasErrors,
'errorMessages': errorMessages,
'bookingId': bookingData.id
} %}
{% endfor %}
+3
View File
@@ -1,6 +1,9 @@
{% extends 'base.html.twig' %}
{% block body %}
{# Global toast controller - persists across all HTMX swaps and page navigations #}
<div {{ stimulus_controller('toast') }}></div>
<div class="{{ html_classes('pb-8', { 'pt-24': is_granted('ROLE_USER') }) }}" {{ stimulus_controller('loading', [], { 'hidden': 'invisible' }) }}>
<div class="fixed inset-x-0 top-0 z-20">
<div class="max-w-screen-xl mx-auto flex items-center justify-between bg-primary-dark text-white">