From e95de52768fdf30b110bdef42cb40b89e244e46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Wed, 12 Aug 2026 15:54:00 +0200 Subject: [PATCH] Implemented additional event information --- .../SetParticipationOptionsCommand.php | 12 ++- .../UpdateParticipantCommand.php | 52 +++++++++++++ .../UpdateParticipantRequest.php | 2 + .../Event/Controllers/DetailsController.php | 3 + .../MailCompose/ByGroupController.php | 4 + .../ParticipantUpdateController.php | 1 + app/Domains/Event/Views/Details.vue | 8 +- .../Event/Views/Partials/ParticipantData.vue | 40 +++++++++- .../Views/Partials/ParticipantsByOption.vue | 67 ++++++++++++++++ .../Event/Views/Partials/ParticipantsList.vue | 8 +- .../Views/Partials/ParticipationOptions.vue | 47 ++++++++--- .../steps/StepParticipationOptions.vue | 16 +++- .../Partials/SignUpForm/steps/StepSummary.vue | 2 +- .../EventParticipantRepository.php | 78 +++++++++++++++++++ app/Views/Components/Modal.vue | 4 +- app/Views/Components/TabbedPage.vue | 5 +- 16 files changed, 327 insertions(+), 22 deletions(-) create mode 100644 app/Domains/Event/Views/Partials/ParticipantsByOption.vue diff --git a/app/Domains/Event/Actions/SetParticipationOptions/SetParticipationOptionsCommand.php b/app/Domains/Event/Actions/SetParticipationOptions/SetParticipationOptionsCommand.php index 8e7696d..e3276cb 100644 --- a/app/Domains/Event/Actions/SetParticipationOptions/SetParticipationOptionsCommand.php +++ b/app/Domains/Event/Actions/SetParticipationOptions/SetParticipationOptionsCommand.php @@ -27,7 +27,7 @@ class SetParticipationOptionsCommand * Geltungsbereichs eindeutig, damit Antworten robust referenzieren können. * * @param array> $questions - * @return array}> + * @return array}> */ private function normalize(array $questions): array { @@ -35,21 +35,25 @@ class SetParticipationOptionsCommand $usedKeys = []; foreach ($questions as $question) { - $label = trim((string)($question['label'] ?? '')); - if ($label === '') { + // Titel ist Pflicht (Überschrift/Feldbeschriftung); die Frage ist optionaler Erklärtext. + $title = trim((string)($question['title'] ?? '')); + if ($title === '') { continue; } + $label = trim((string)($question['label'] ?? '')); + $options = $this->normalizeOptions($question['options'] ?? []); if ($options === []) { continue; } - $key = $this->uniqueSlug((string)($question['key'] ?? ''), $label, $usedKeys); + $key = $this->uniqueSlug((string)($question['key'] ?? ''), $title, $usedKeys); $usedKeys[] = $key; $normalized[] = [ 'key' => $key, + 'title' => $title, 'label' => $label, 'options' => $options, ]; diff --git a/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantCommand.php b/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantCommand.php index c333fc0..91883e8 100644 --- a/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantCommand.php +++ b/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantCommand.php @@ -72,11 +72,63 @@ class UpdateParticipantCommand { $p->save(); + + if ($this->request->participationOptions !== null) { + $this->syncParticipationOptions($p); + } + $this->response->success = true; $this->response->participant = $p; return $this->response; } + /** + * Ersetzt die zugeordneten Teilnahmeoptionen anhand der übergebenen Antworten. Nachträgliche Zuordnung im + * Back-Office ist lückenlos möglich: nur gegen die Event-Definition gültige Antworten werden gespeichert, + * leere/entfallene Antworten werden abgeräumt. Frage-/Options-Label werden als Snapshot mitgeschrieben. + */ + private function syncParticipationOptions(EventParticipant $participant): void + { + $participant->selectedOptions()->delete(); + + $answers = $this->request->participationOptions ?? []; + + foreach ($participant->event?->participation_options ?? [] as $question) { + $key = $question['key'] ?? null; + if ($key === null) { + continue; + } + + $value = $answers[$key] ?? null; + if ($value === null || $value === '') { + continue; + } + + $option = null; + foreach ($question['options'] ?? [] as $candidate) { + if (($candidate['value'] ?? null) === $value) { + $option = $candidate; + break; + } + } + + if ($option === null) { + continue; + } + + $label = trim((string)($question['label'] ?? '')); + + $participant->selectedOptions()->create([ + 'tenant' => $participant->tenant, + 'event_id' => $participant->event_id, + 'question_key' => $key, + 'question_label' => $label !== '' ? $label : ($question['title'] ?? $key), + 'value' => $value, + 'value_label' => $option['label'] ?? $value, + ]); + } + } + private function handleAmountChanges(EventParticipant $participant) { $this->response->amountPaid = $participant->amount_paid; $this->response->amountExpected = $participant->amount; diff --git a/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantRequest.php b/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantRequest.php index 443695a..8d6166a 100644 --- a/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantRequest.php +++ b/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantRequest.php @@ -36,5 +36,7 @@ class UpdateParticipantRequest { public string $amountPaid, public string $amountExpected, public string $cocStatus, + /** Antworten auf die Teilnahmeoptionen: [ question_key => value ]. null = unverändert lassen. */ + public ?array $participationOptions = null, ) {} } diff --git a/app/Domains/Event/Controllers/DetailsController.php b/app/Domains/Event/Controllers/DetailsController.php index 2bb71d2..bb204ca 100644 --- a/app/Domains/Event/Controllers/DetailsController.php +++ b/app/Domains/Event/Controllers/DetailsController.php @@ -208,6 +208,9 @@ class DetailsController extends CommonController { case 'by-participation-group': $participants = $this->eventParticipants->groupByParticipationType($event, $request); break; + case 'by-participation-option': + $participants = $this->eventParticipants->groupByParticipationOption($event, $request); + break; case 'signed-off': $participants = $this->eventParticipants->getSignedOffParticipants($event, $request); break; diff --git a/app/Domains/Event/Controllers/MailCompose/ByGroupController.php b/app/Domains/Event/Controllers/MailCompose/ByGroupController.php index f265b25..9865ce5 100644 --- a/app/Domains/Event/Controllers/MailCompose/ByGroupController.php +++ b/app/Domains/Event/Controllers/MailCompose/ByGroupController.php @@ -19,6 +19,10 @@ class ByGroupController extends CommonController $participants = $this->eventParticipants->groupByParticipationType($event, $request, $request->input('groupName')); $recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]); break; + case 'by-participation-option': + $participants = $this->eventParticipants->groupByParticipationOption($event, $request, $request->input('groupName')); + $recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')] ?? []); + break; case 'signed-off': $participants = $this->eventParticipants->getSignedOffParticipants($event, $request, $request->input('groupName')); $recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]); diff --git a/app/Domains/Event/Controllers/ParticipantUpdateController.php b/app/Domains/Event/Controllers/ParticipantUpdateController.php index f4d1dbc..4d07f40 100644 --- a/app/Domains/Event/Controllers/ParticipantUpdateController.php +++ b/app/Domains/Event/Controllers/ParticipantUpdateController.php @@ -44,6 +44,7 @@ class ParticipantUpdateController extends CommonController { amountPaid: $request->input('amountPaid'), amountExpected: $request->input('amountExpected'), cocStatus: $request->input('cocStatus'), + participationOptions: $request->has('participationOptions') ? (array)$request->input('participationOptions') : null, ); $command = new UpdateParticipantCommand($updateRequest); diff --git a/app/Domains/Event/Views/Details.vue b/app/Domains/Event/Views/Details.vue index 8fdf945..fc57a06 100644 --- a/app/Domains/Event/Views/Details.vue +++ b/app/Domains/Event/Views/Details.vue @@ -1,9 +1,10 @@ + + + + diff --git a/app/Domains/Event/Views/Partials/ParticipantsList.vue b/app/Domains/Event/Views/Partials/ParticipantsList.vue index 93ea9be..0479493 100644 --- a/app/Domains/Event/Views/Partials/ParticipantsList.vue +++ b/app/Domains/Event/Views/Partials/ParticipantsList.vue @@ -20,6 +20,11 @@ const props = defineProps({ listType: '', }), }, + // Steuert, ob je Gruppe der „E-Mail an Gruppe senden"-Button angezeigt wird. + showGroupMail: { + type: Boolean, + default: true, + }, }); const today = format(new Date(), "yyyy-MM-dd"); @@ -404,7 +409,8 @@ function mailToGroup(groupKey) { 27 Jahre und älter: {{ getAgeCounts(participants)['27+'] ?? 0 }} - + diff --git a/app/Domains/Event/Views/Partials/ParticipationOptions.vue b/app/Domains/Event/Views/Partials/ParticipationOptions.vue index ac742ef..f57333a 100644 --- a/app/Domains/Event/Views/Partials/ParticipationOptions.vue +++ b/app/Domains/Event/Views/Partials/ParticipationOptions.vue @@ -14,12 +14,13 @@ const props = defineProps({ // neue Einträge bekommen serverseitig beim Speichern einen stabilen Slug. const questions = ref((props.event.participationOptions ?? []).map(q => ({ key: q.key ?? '', + title: q.title ?? '', label: q.label ?? '', options: (q.options ?? []).map(o => ({value: o.value ?? '', label: o.label ?? ''})), }))) function addQuestion() { - questions.value.push({key: '', label: '', options: [{value: '', label: ''}, {value: '', label: ''}]}) + questions.value.push({key: '', title: '', label: '', options: [{value: '', label: ''}, {value: '', label: ''}]}) } function removeQuestion(index) { @@ -36,8 +37,8 @@ function removeOption(question, index) { function validate() { for (const question of questions.value) { - if (question.label.trim() === '') { - toast.error('Bitte für jede Frage eine Beschriftung angeben.') + if (question.title.trim() === '') { + toast.error('Bitte für jede Frage einen Titel angeben.') return false } @@ -83,12 +84,23 @@ async function save() {
- +
+ + + + + +
@@ -127,10 +139,25 @@ async function save() { .question-head { display: flex; - align-items: center; + align-items: flex-start; gap: 12px; } +.question-fields { + flex: 1; +} + +.field-label { + display: block; + font-size: 0.78rem; + color: #6b7280; + margin: 6px 0 2px; +} + +.question-fields .field-label:first-child { + margin-top: 0; +} + .options { margin-top: 12px; padding-left: 12px; diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepParticipationOptions.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepParticipationOptions.vue index b4d7499..094eecd 100644 --- a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepParticipationOptions.vue +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepParticipationOptions.vue @@ -28,7 +28,10 @@ const next = () => emit('next', nextVisibleFrom(props.event, STEP_PARTICIPATION_ - +
{{ question.label }}: + {{ question.title || question.label }}: + {{ question.label }} +