Date: Wed, 9 Sep 2026 17:32:50 +0200
Subject: [PATCH 2/6] User-Comfort im Zahlungsparser
---
.../Views/Partials/BankStatementImport.vue | 67 ++--
app/Views/Components/RichSelectBox.vue | 336 ++++++++++++++++--
2 files changed, 352 insertions(+), 51 deletions(-)
diff --git a/app/Domains/Event/Views/Partials/BankStatementImport.vue b/app/Domains/Event/Views/Partials/BankStatementImport.vue
index b5f391e..eb7d861 100644
--- a/app/Domains/Event/Views/Partials/BankStatementImport.vue
+++ b/app/Domains/Event/Views/Partials/BankStatementImport.vue
@@ -4,6 +4,7 @@ import {toast} from 'vue3-toastify'
import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue";
import LoadingModal from "../../../../Views/Components/LoadingModal.vue";
import Icon from "../../../../Views/Components/Icon.vue";
+import RichSelectBox from "../../../../Views/Components/RichSelectBox.vue";
import {useAjax} from "../../../../../resources/js/components/ajaxHandler.js";
/**
@@ -41,6 +42,39 @@ const participantsByIdentifier = computed(
() => Object.fromEntries(participants.value.map(participant => [participant.identifier, participant])),
)
+/**
+ * Die Auswahlliste für alle Zeilen -- einmal gebaut, nicht je Zeile.
+ *
+ * Getrennt nach an- und abgemeldet, weil die Entscheidung unterschiedlich weit trägt: Eine Buchung
+ * auf eine abgemeldete Anmeldung zieht eine Erstattung nach sich. Als Fließtext in einer Zeile ging
+ * das unter, als Gruppenüberschrift steht es da.
+ */
+const participantOptions = computed(() => {
+ const options = [{value: '', label: '— ignorieren —'}]
+
+ for (const participant of participants.value) {
+ const state = participant.isSettled ? 'vollständig bezahlt' : participant.amountOpen + ' offen'
+
+ options.push(participant.isSignedOff
+ ? {
+ value: participant.identifier,
+ label: participant.name,
+ description: 'abgemeldet am ' + participant.signedOffAt + ' · ' + state,
+ group: 'Abgemeldete Teilis',
+ icon: 'user-slash',
+ muted: true,
+ }
+ : {
+ value: participant.identifier,
+ label: participant.name,
+ description: state,
+ group: 'Angemeldete Teilis',
+ })
+ }
+
+ return options
+})
+
function isAssigned(row) {
return (assignment.value[row.rowNumber] ?? '') !== ''
}
@@ -140,16 +174,6 @@ function reset() {
if (fileInput.value) fileInput.value.value = ''
}
-function optionLabel(participant) {
- const state = participant.isSettled
- ? 'vollständig bezahlt'
- : participant.amountOpen + ' offen'
-
- return participant.isSignedOff
- ? participant.name + ' — abgemeldet, ' + state
- : participant.name + ' — ' + state
-}
-
function assignedParticipant(row) {
return participantsByIdentifier.value[assignment.value[row.rowNumber]] ?? null
}
@@ -257,13 +281,10 @@ function signedOffOf(row) {
{{ row.purpose }}
-
- — ignorieren —
-
- {{ optionLabel(participant) }}
-
-
+
{{ rowState(row).label }}
@@ -458,7 +479,7 @@ function signedOffOf(row) {
padding: 10px;
border-bottom: 1px solid #e5e7eb;
vertical-align: top;
- font-size: 0.9rem;
+ font-size: 0.95rem;
}
.statement-table .right {
@@ -476,7 +497,7 @@ function signedOffOf(row) {
}
.assignment-column {
- width: 320px;
+ width: 360px;
}
/* Zeilenfarbe sagt auf einen Blick, was passiert: grün wird gebucht, gelb will geprüft werden,
@@ -520,14 +541,6 @@ function signedOffOf(row) {
gap: 5px;
}
-.assignment-select {
- width: 100%;
- padding: 5px 6px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- background-color: #ffffff;
- font-size: 0.85rem;
-}
.pill {
align-self: flex-start;
diff --git a/app/Views/Components/RichSelectBox.vue b/app/Views/Components/RichSelectBox.vue
index c168065..477d452 100644
--- a/app/Views/Components/RichSelectBox.vue
+++ b/app/Views/Components/RichSelectBox.vue
@@ -1,57 +1,229 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ s.step ? goToStep(s.step) : null"
+ >
+ ✓
+ {{ s.label }}
+
+
+
+
+
+
+
+ Schritt {{ currentIndex + 1 }} von {{ steps.length }}:
+ {{ steps.find(s => s.step === currentStep)?.label }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/shortStepFlow.js b/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/shortStepFlow.js
new file mode 100644
index 0000000..465b472
--- /dev/null
+++ b/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/shortStepFlow.js
@@ -0,0 +1,37 @@
+// Schrittreihenfolge der Kurzanmeldung. Zwei Schritte sind optional: die Teilnahmeoptionen entfallen, wenn für
+// die Veranstaltung keine definiert sind, die Zahlungsart entfällt bei Betrag 0 oder wenn es ohnehin nur eine
+// Zahlungsart gibt (die wird dann vorausgewählt).
+export const SHORT_STEP_PERSON = 1
+export const SHORT_STEP_PARTICIPATION_OPTIONS = 2
+export const SHORT_STEP_PHOTO = 3
+export const SHORT_STEP_PAYMENT = 4
+export const SHORT_STEP_SUMMARY = 5
+
+export function stepVisible(event, step, amountValue) {
+ if (step === SHORT_STEP_PARTICIPATION_OPTIONS) {
+ return (event.participationOptions?.length ?? 0) > 0
+ }
+
+ if (step === SHORT_STEP_PAYMENT) {
+ const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
+ return amountValue > 0 && activeMethods.length > 1
+ }
+
+ return true
+}
+
+export function nextVisibleFrom(event, step, amountValue) {
+ let candidate = step + 1
+ while (candidate < SHORT_STEP_SUMMARY && !stepVisible(event, candidate, amountValue)) {
+ candidate++
+ }
+ return candidate
+}
+
+export function prevVisibleFrom(event, step, amountValue) {
+ let candidate = step - 1
+ while (candidate > SHORT_STEP_PERSON && !stepVisible(event, candidate, amountValue)) {
+ candidate--
+ }
+ return candidate
+}
diff --git a/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/useShortSignupForm.js b/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/useShortSignupForm.js
new file mode 100644
index 0000000..b591bd1
--- /dev/null
+++ b/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/useShortSignupForm.js
@@ -0,0 +1,155 @@
+import {computed, reactive, ref} from 'vue'
+import axios from 'axios'
+import {differenceInYears, parseISO} from 'date-fns'
+import {SHORT_STEP_PERSON, nextVisibleFrom, prevVisibleFrom} from './shortStepFlow.js'
+
+export function useShortSignupForm(event, participantData) {
+ const currentStep = ref(SHORT_STEP_PERSON)
+ const submitting = ref(false)
+ const summaryLoading = ref(false)
+ const submitResult = ref(null) // null | { status: 'success'|'exists', participant: {} }
+ const submitError = ref('')
+
+ // Gibt es nur eine Zahlungsart, wird sie vorausgewählt und der Zahlungsschritt entfällt.
+ const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
+
+ // Bei genau einem teilnehmenden Stamm wird dieser gesetzt, ohne danach zu fragen; erst ab zwei
+ // Stämmen erscheint das Auswahlfeld.
+ const localGroups = event.contributingLocalGroups ?? []
+ const needsLocalGroup = localGroups.length > 1
+
+ const formData = reactive({
+ userId: participantData.id,
+ vorname: participantData.firstname ?? '',
+ nachname: participantData.lastname ?? '',
+ pfadiname: participantData.nickname ?? '',
+ geburtsdatum: participantData.birthday ?? '',
+ email_1: participantData.email ?? '',
+ telefon_1: participantData.phone ?? '',
+ localGroup: localGroups.length === 1 ? localGroups[0].id : (participantData.localGroup ?? '-1'),
+ ansprechpartner: '',
+ email_2: '',
+ allergien: participantData.allergies ?? '',
+ intolerances: participantData.intolerances ?? '',
+ first_aid: '-1',
+ badeerlaubnis: '-1',
+ // Antworten auf die event-spezifischen Teilnahmeoptionen: { [question.key]: option.value }
+ participationOptions: {},
+ foto: {socialmedia: false, print: false, webseite: false, partner: false, intern: false},
+ paymentMethod: activeMethods.length === 1 ? activeMethods[0].slug : null,
+ paymentOptions: {},
+ summary_information_correct: false,
+ summary_accept_terms: false,
+ legal_accepted: false,
+ payment: false,
+ })
+
+ // Minderjährigkeit steuert Kontaktperson, Erweiterte Erste Hilfe und Badeerlaubnis. Die Grenze von 18 Jahren
+ // entspricht Age::isfullAged() im Backend.
+ const isMinor = computed(() => {
+ if (!formData.geburtsdatum) {
+ return false
+ }
+ return differenceInYears(new Date(), parseISO(formData.geburtsdatum)) < 18
+ })
+
+ const summaryAmount = ref('')
+ const summaryAmountValue = ref(0)
+ const amountLoaded = ref(false)
+
+ // Der Betrag steht für die ganze Anmeldung fest (feste Teilnahmegruppe, fester Zeitraum) und wird deshalb
+ // genau einmal geholt. Er entscheidet, ob der Zahlungsschritt überhaupt vorkommt.
+ const ensureAmount = async () => {
+ if (amountLoaded.value) {
+ return
+ }
+
+ summaryLoading.value = true
+ try {
+ const res = await axios.post('/api/v1/event/' + event.id + '/short-calculate-amount')
+ summaryAmount.value = res.data.amount
+ summaryAmountValue.value = Number(res.data.amountValue ?? 0)
+ amountLoaded.value = true
+
+ if (summaryAmountValue.value <= 0) {
+ formData.paymentMethod = null
+ formData.paymentOptions = {}
+ }
+ } finally {
+ summaryLoading.value = false
+ }
+ }
+
+ const goToStep = (step) => {
+ currentStep.value = step
+ }
+
+ const goNext = async () => {
+ await ensureAmount()
+ goToStep(nextVisibleFrom(event, currentStep.value, summaryAmountValue.value))
+ }
+
+ const goBack = async () => {
+ await ensureAmount()
+ goToStep(prevVisibleFrom(event, currentStep.value, summaryAmountValue.value))
+ }
+
+ const submit = async () => {
+ if (!formData.summary_information_correct || !formData.summary_accept_terms || !formData.legal_accepted) {
+ return
+ }
+
+ submitting.value = true
+ submitError.value = ''
+ try {
+ const res = await axios.post('/api/v1/event/' + event.id + '/short-signup', {
+ firstname: formData.vorname,
+ lastname: formData.nachname,
+ nickname: formData.pfadiname || null,
+ birthday: formData.geburtsdatum,
+ email: formData.email_1,
+ phone: formData.telefon_1,
+ localGroup: formData.localGroup !== '-1' ? formData.localGroup : null,
+ contactPerson: isMinor.value ? formData.ansprechpartner : null,
+ contactEmail: isMinor.value ? formData.email_2 : null,
+ allergies: formData.allergien || null,
+ intolerances: formData.intolerances || null,
+ firstAidPermission: isMinor.value && formData.first_aid !== '-1' ? formData.first_aid : null,
+ swimmingPermission: isMinor.value && formData.badeerlaubnis !== '-1' ? formData.badeerlaubnis : null,
+ foto: formData.foto,
+ participationOptions: formData.participationOptions,
+ paymentMethod: formData.paymentMethod,
+ paymentOptions: formData.paymentOptions,
+ })
+
+ submitResult.value = {
+ status: res.data.status,
+ participant: res.data.participant,
+ }
+ } catch (error) {
+ submitError.value = error.response?.data?.message
+ ?? 'Die Anmeldung konnte nicht gespeichert werden. Bitte versuche es später erneut.'
+ } finally {
+ submitting.value = false
+ }
+ }
+
+ return {
+ currentStep,
+ goToStep,
+ goNext,
+ goBack,
+ formData,
+ isMinor,
+ localGroups,
+ needsLocalGroup,
+ submit,
+ submitting,
+ submitResult,
+ submitError,
+ summaryLoading,
+ summaryAmount,
+ summaryAmountValue,
+ ensureAmount,
+ }
+}
diff --git a/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepPerson.vue b/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepPerson.vue
new file mode 100644
index 0000000..0b3e625
--- /dev/null
+++ b/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepPerson.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
diff --git a/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepSummary.vue b/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepSummary.vue
new file mode 100644
index 0000000..504eb19
--- /dev/null
+++ b/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepSummary.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
Zusammenfassung
+
+
Wird geladen…
+
+
+
+
+
diff --git a/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue b/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue
index 63f5f4c..e051dd2 100644
--- a/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue
+++ b/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue
@@ -122,170 +122,5 @@ const steps = [
-
+
+
diff --git a/app/Domains/Event/Views/Partials/SignUpForm/signupForm.css b/app/Domains/Event/Views/Partials/SignUpForm/signupForm.css
new file mode 100644
index 0000000..b00723c
--- /dev/null
+++ b/app/Domains/Event/Views/Partials/SignUpForm/signupForm.css
@@ -0,0 +1,167 @@
+/* Geteilte Styles der Anmelde-Wizards (langer Prozess und Kurzanmeldung). */
+
+/* ─── Progress (Step-Pills) ─── */
+.signup-progress {
+ margin-bottom: 28px;
+}
+
+.signup-progress-pills {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.signup-progress-separator {
+ flex-shrink: 0;
+ width: 16px;
+ height: 2px;
+ background: #e5e7eb;
+ border-radius: 1px;
+}
+
+.signup-pill {
+ padding: 5px 14px;
+ border-radius: 999px;
+ font-size: 0.78rem;
+ font-weight: 600;
+ white-space: nowrap;
+ border: 2px solid;
+ cursor: default;
+}
+
+.signup-pill__check { margin-right: 4px; }
+
+.signup-pill--active {
+ border-color: #2563eb;
+ background: #2563eb;
+ color: white;
+}
+
+.signup-pill--done {
+ border-color: #bbf7d0;
+ background: #f0fdf4;
+ color: #15803d;
+ cursor: pointer;
+}
+
+.signup-pill--upcoming {
+ border-color: #e5e7eb;
+ background: #f9fafb;
+ color: #9ca3af;
+}
+
+.signup-progress-bar {
+ margin-top: 10px;
+ height: 3px;
+ background: #e5e7eb;
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.signup-progress-bar__fill {
+ height: 100%;
+ background: linear-gradient(90deg, #2563eb, #3b82f6);
+ border-radius: 2px;
+ transition: width 0.3s ease;
+}
+
+.signup-progress-mobile {
+ display: none;
+ margin-top: 8px;
+ font-size: 0.9rem;
+ color: #374151;
+}
+
+/* ─── Form-Table ─── */
+.form-table { width: 100%; border-collapse: collapse; }
+.form-table td { padding: 8px 12px 8px 0; vertical-align: top; }
+.form-table td:first-child { width: 220px; color: #374151; font-weight: 500; }
+.form-table input[type="text"],
+.form-table input[type="date"],
+.form-table input[type="email"],
+.form-table input[type="number"],
+.form-table select,
+.form-table textarea {
+ width: 100%;
+ padding: 6px 10px;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ font-size: 0.95rem;
+ box-sizing: border-box;
+}
+
+.btn-row { display: flex; gap: 10px; padding-top: 16px; flex-wrap: wrap; }
+.btn-primary {
+ padding: 8px 20px;
+ background: #2563eb;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+}
+.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
+.btn-secondary {
+ padding: 8px 20px;
+ background: #f3f4f6;
+ color: #374151;
+ border: 1px solid #d1d5db;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+/* ─── Tablet ─── */
+@media (max-width: 1023px) {
+ .form-table td:first-child {
+ width: 160px;
+ }
+}
+
+/* ─── Smartphone ─── */
+@media (max-width: 639px) {
+ /* Pills auf Mobile: kompakter, Trennstriche ausblenden */
+ .signup-progress-pills {
+ display: none;
+ }
+ .signup-progress-mobile {
+ display: block;
+ }
+
+ /* Form-Table: Label oberhalb des Feldes */
+ .form-table,
+ .form-table tbody,
+ .form-table tr {
+ display: block;
+ width: 100%;
+ }
+
+ .form-table td {
+ display: block;
+ width: 100% !important;
+ padding: 4px 0;
+ }
+
+ .form-table td:first-child {
+ width: 100% !important;
+ font-weight: 600;
+ color: #374151;
+ padding-top: 10px;
+ }
+
+ .form-table td[colspan] {
+ width: 100% !important;
+ }
+
+ .btn-row {
+ flex-direction: column-reverse;
+ gap: 8px;
+ }
+
+ .btn-primary,
+ .btn-secondary {
+ width: 100%;
+ padding: 12px 20px;
+ }
+}
diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepContactPerson.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepContactPerson.vue
index 5b6367c..3e36d3b 100644
--- a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepContactPerson.vue
+++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepContactPerson.vue
@@ -42,7 +42,7 @@ const next = () => {
hasError = true
}
- if (props.formData.badeerlaubnis === '-1') {
+ if (props.event.swimmingPermissionRequired && props.formData.badeerlaubnis === '-1') {
errors.badeerlaubnis = 'Bitte triff eine Entscheidung. Bist du dir unsicher, kontaktiere bitte die Aktionsleitung'
hasError = true
}
@@ -86,7 +86,7 @@ const next = () => {
-
+
Badeerlaubnis:
diff --git a/app/Domains/Event/Views/Signup.vue b/app/Domains/Event/Views/Signup.vue
index a941154..b3af474 100644
--- a/app/Domains/Event/Views/Signup.vue
+++ b/app/Domains/Event/Views/Signup.vue
@@ -3,6 +3,7 @@
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import SignupForm from './Partials/SignUpForm/SignupForm.vue'
+import ShortSignupForm from './Partials/ShortSignUpForm/ShortSignupForm.vue'
import FullScreenModal from "../../../Views/Components/FullScreenModal.vue";
import AvailableEvents from "./Partials/AvailableEvents.vue";
import {ref} from "vue";
@@ -98,8 +99,13 @@ function close() {
+
'array',
'addons' => 'array',
+
+ 'short_registration' => 'boolean',
+ 'swimming_permission_required' => 'boolean',
];
public function tenant(): BelongsTo
@@ -205,4 +213,18 @@ class Event extends InstancedModel
return $this->hasMany(EventParticipant::class);
}
+ /**
+ * Slug der ersten konfigurierten Teilnahmegruppe. Die Kurzanmeldung fragt die Gruppe nicht ab und ordnet
+ * jede Anmeldung dieser Gruppe zu. Null, wenn für die Veranstaltung noch keine Gebühr hinterlegt ist --
+ * dann ist eine Kurzanmeldung nicht möglich, weil `participation_type` am Teilnehmer Pflicht ist.
+ */
+ public function firstParticipationTypeSlug() : ?string {
+ return collect([
+ $this->participationFee1,
+ $this->participationFee2,
+ $this->participationFee3,
+ $this->participationFee4,
+ ])->first(fn ($participationFee) => $participationFee !== null)?->type;
+ }
+
}
diff --git a/app/Resources/EventParticipantResource.php b/app/Resources/EventParticipantResource.php
index 42eaa5a..9a679bb 100644
--- a/app/Resources/EventParticipantResource.php
+++ b/app/Resources/EventParticipantResource.php
@@ -52,12 +52,15 @@ class EventParticipantResource extends JsonResource
'fullname' => $this->resource->getFullName(),
'age' => new Age($this->resource->birthday)->getAge(),
'localgroup' => $this->resource->localGroup()?->first()?->name ?? 'Nicht im LV',
- 'swimmingPermission' => $this->resource->swimmingPermission()->first()->short,
- 'extendedFirstAid' => $this->resource->firstAidPermission()->first()->name,
+ // Die Zuordnungen sind nullable: die Kurzanmeldung erhebt nicht alles, und in der Verwaltung
+ // lassen sich Werte leeren. Ohne Fallback fielen Teilnehmerliste und Detailansicht komplett aus.
+ 'swimmingPermission' => $this->resource->swimmingPermission()->first()?->short ?? 'Unbekannt',
+ 'extendedFirstAid' => $this->resource->firstAidPermission()->first()?->name ?? 'Unbekannt',
'tetanusVaccination' => $this->resource->tetanus_vaccination?->format('d.m.Y') ?? 'Unbekannt',
'tetanusVaccinationEdit' => $this->resource->tetanus_vaccination?->format('Y-m-d') ?? null,
'presenceDays' => ['real' => $presenceDays, 'support' => $presenceDaysSupport],
- 'participationType' => ParticipationType::where(['slug' => $this->resource->participation_type])->first()->name,
+ 'participationType' => ParticipationType::where(['slug' => $this->resource->participation_type])->first()?->name
+ ?? $this->resource->participation_type,
'needs_payment' => $this->resource->amount->getAmount() > 0
&& $this->resource->payment_method === PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION
&& $this->resource->amount_paid?->getAmount() < $this->resource->amount->getAmount(),
@@ -87,7 +90,7 @@ class EventParticipantResource extends JsonResource
'state' => config('postCode.map.' . $this->resource->postcode),
'localGroupState' => null !== $this->resource->localGroup()->first()?->postcode ? config('postCode.map.' . $this->resource->postcode) : '--',
'birthday' => $this->resource->birthday->format('d.m.Y'),
- 'eatingHabit' => EatingHabit::where('slug', $this->resource->eating_habit)->first()->name,
+ 'eatingHabit' => EatingHabit::where('slug', $this->resource->eating_habit)->first()?->name ?? 'Unbekannt',
'paymentMethod' => $this->resource->payment_method !== null
? AvailablePaymentMethod::withoutGlobalScopes()->where('tenant', $this->resource->tenant)->where('slug', $this->resource->payment_method)->first()?->name ?? $this->resource->payment_method
: null,
@@ -96,12 +99,14 @@ class EventParticipantResource extends JsonResource
EfzStatus::EFZ_STATUS_NOT_REQUIRED => 'bg-green',
EfzStatus::EFZ_STATUS_NOT_CHECKED => 'bg-yellow',
EfzStatus::EFZ_STATUS_CHECKED_INVALID => 'bg-red',
+ default => 'bg-yellow',
},
'efzStatusReadable' => match($this->resource->efz_status) {
EfzStatus::EFZ_STATUS_CHECKED_VALID => 'Gültig',
EfzStatus::EFZ_STATUS_CHECKED_INVALID => 'Nicht eingereicht',
EfzStatus::EFZ_STATUS_NOT_CHECKED => 'Nicht geprüft',
EfzStatus::EFZ_STATUS_NOT_REQUIRED => 'Nicht erforderlich',
+ default => 'Nicht geprüft',
},
'eventName' => $this->resource->event()->first()->name,
'arrivalDateReadable' => $this->resource->arrival_date->format('d.m.Y'),
diff --git a/app/Resources/EventResource.php b/app/Resources/EventResource.php
index 6077f88..ac06764 100644
--- a/app/Resources/EventResource.php
+++ b/app/Resources/EventResource.php
@@ -76,6 +76,10 @@ class EventResource extends JsonResource{
$returnArray['nameShort'] = substr($returnArray['nameShort'], 8, 13) . '...';
}
+ // Steuert im Frontend, welcher Anmelde-Wizard gerendert wird, und ob die Badeerlaubnis abgefragt wird.
+ $returnArray['shortRegistration'] = (bool)$this->event->short_registration;
+ $returnArray['swimmingPermissionRequired'] = (bool)$this->event->swimming_permission_required;
+
$returnArray['siblingReduction'] = $this->event->sibling_reduction ?? true;
$returnArray['costUnit'] = new CostUnitResource($this->event->costUnit()->first())->toArray(true);
$returnArray['solidarityPayment'] = $this->event->participation_fee_type === ParticipationFeeType::PARTICIPATION_FEE_TYPE_SOLIDARITY;
diff --git a/database/migrations/2026_09_15_140010_add_short_registration_to_events.php b/database/migrations/2026_09_15_140010_add_short_registration_to_events.php
new file mode 100644
index 0000000..d800be5
--- /dev/null
+++ b/database/migrations/2026_09_15_140010_add_short_registration_to_events.php
@@ -0,0 +1,27 @@
+boolean('short_registration')->default(false);
+
+ // Ob die Badeerlaubnis überhaupt abgefragt wird -- wirkt in beiden Anmeldeprozessen. Default true,
+ // damit bestehende Veranstaltungen sich unverändert verhalten.
+ $table->boolean('swimming_permission_required')->default(true);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('events', function (Blueprint $table) {
+ $table->dropColumn(['short_registration', 'swimming_permission_required']);
+ });
+ }
+};
diff --git a/phpunit.xml b/phpunit.xml
index d703241..f3eb8d1 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -18,6 +18,9 @@
+
+
diff --git a/tests/Feature/ShortSignUpTest.php b/tests/Feature/ShortSignUpTest.php
new file mode 100644
index 0000000..082b887
--- /dev/null
+++ b/tests/Feature/ShortSignUpTest.php
@@ -0,0 +1,331 @@
+tenant = Tenant::create([
+ 'slug' => 'tv', 'name' => 'Test', 'email' => 't@example.com', 'email_finance' => 'f@example.com',
+ 'url' => 'test.local', 'account_name' => 'Test e.V.', 'account_iban' => 'DE00', 'account_bic' => 'XY',
+ 'city' => 'Stadt', 'postcode' => '00000', 'is_active_local_group' => true, 'has_active_instance' => true,
+ ]);
+ app()->instance('tenant', $this->tenant);
+
+ PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION]);
+ DB::table('participation_fee_types')->insert(['slug' => 'FIXED', 'name' => 'Fixed', 'created_at' => now(), 'updated_at' => now()]);
+ ParticipationType::create(['slug' => ParticipationType::PARTICIPATION_TYPE_PARTICIPANT, 'name' => 'Teilnehmende']);
+ EfzStatus::create(['slug' => EfzStatus::EFZ_STATUS_NOT_CHECKED, 'name' => 'Nicht geprüft']);
+ EfzStatus::create(['slug' => EfzStatus::EFZ_STATUS_NOT_REQUIRED, 'name' => 'Nicht erforderlich']);
+ SwimmingPermission::create(['slug' => SwimmingPermission::SWIMMING_PERMISSION_ALLOWED, 'name' => 'Darf baden', 'short' => 'Erteilt']);
+ SwimmingPermission::create(['slug' => SwimmingPermission::SWIMMING_PERMISSION_DENIED, 'name' => 'Darf nicht baden', 'short' => 'Keine']);
+ FirstAidPermission::create(['slug' => FirstAidPermission::FIRST_AID_PERMISSION_ALLOWED, 'name' => 'Zugestimmt', 'description' => '']);
+ FirstAidPermission::create(['slug' => FirstAidPermission::FIRST_AID_PERMISSION_DENIED, 'name' => 'Abgelehnt', 'description' => '']);
+ EatingHabit::create(['slug' => EatingHabit::EATING_HABIT_OMNIVOR, 'name' => 'Omnivor']);
+ }
+
+ private function createEvent(bool $withFee = true, bool $swimmingPermissionRequired = true): Event
+ {
+ $event = Event::create([
+ 'tenant' => $this->tenant->slug, 'name' => 'Event', 'identifier' => 'evt-1', 'location' => 'Ort',
+ 'postal_code' => '00000', 'email' => 'e@example.com', 'start_date' => '2026-08-01', 'end_date' => '2026-08-05',
+ 'early_bird_end' => '2026-07-01', 'registration_final_end' => '2026-07-20', 'early_bird_end_amount_increase' => 0,
+ 'account_owner' => 'Owner', 'account_iban' => 'DE00', 'participation_fee_type' => 'FIXED',
+ 'pay_per_day' => false, 'pay_direct' => false,
+ 'short_registration' => true, 'swimming_permission_required' => $swimmingPermissionRequired,
+ ]);
+
+ if ($withFee) {
+ $fee = EventParticipationFee::create([
+ 'tenant' => $this->tenant->slug, 'type' => ParticipationType::PARTICIPATION_TYPE_PARTICIPANT,
+ 'name' => 'Teilnahme', 'description' => '', 'amount_standard' => 50.0,
+ 'amount_reduced' => 30.0, 'amount_solidarity' => 70.0,
+ ]);
+ $event->participation_fee_1 = $fee->id;
+ $event->save();
+ $event->refresh();
+ }
+
+ return $event;
+ }
+
+ private function shortRequest(Event $event, array $overrides = []): ShortSignUpRequest
+ {
+ return new ShortSignUpRequest(
+ event: $event,
+ user_id: null,
+ firstname: $overrides['firstname'] ?? 'Hans',
+ lastname: $overrides['lastname'] ?? 'Muster',
+ nickname: $overrides['nickname'] ?? null,
+ // Volljährig, sofern nicht anders angegeben.
+ birthday: $overrides['birthday'] ?? new \DateTime('2000-01-01'),
+ email: $overrides['email'] ?? 'h@example.com',
+ phone: $overrides['phone'] ?? '123',
+ localGroupId: $overrides['localGroupId'] ?? null,
+ contactPerson: $overrides['contactPerson'] ?? null,
+ contactEmail: $overrides['contactEmail'] ?? null,
+ allergies: $overrides['allergies'] ?? null,
+ intolerances: $overrides['intolerances'] ?? null,
+ firstAidPermission: $overrides['firstAidPermission'] ?? null,
+ swimmingPermission: $overrides['swimmingPermission'] ?? null,
+ foto_socialmedia: true,
+ foto_print: false,
+ foto_webseite: false,
+ foto_partner: false,
+ foto_intern: false,
+ paymentMethod: $overrides['paymentMethod'] ?? PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION,
+ paymentOptions: [],
+ participationOptions: $overrides['participationOptions'] ?? [],
+ );
+ }
+
+ public function test_kurzanmeldung_fuellt_nicht_erhobene_pflichtfelder(): void
+ {
+ $event = $this->createEvent();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event))->execute();
+
+ $this->assertTrue($response->success, $response->message ?? '');
+ $participant = $response->participant;
+
+ $this->assertSame('Nicht erforderlich', $participant->address_1);
+ $this->assertSame('Nicht erforderlich', $participant->city);
+ $this->assertSame('00000', $participant->postcode);
+ $this->assertNull($participant->local_group);
+ $this->assertNull($participant->address_2);
+ $this->assertNull($participant->medications);
+
+ // Zeitraum und Mahlzeiten kommen aus der Veranstaltung.
+ $this->assertSame('2026-08-01', $participant->arrival_date->format('Y-m-d'));
+ $this->assertSame('2026-08-05', $participant->departure_date->format('Y-m-d'));
+ $this->assertSame(1, $participant->arrival_eating);
+ $this->assertSame(2, $participant->departure_eating);
+
+ // Gruppe und Beitragsstufe stehen fest.
+ $this->assertSame(ParticipationType::PARTICIPATION_TYPE_PARTICIPANT, $participant->participation_type);
+ $this->assertSame('standard', $participant->fee_type);
+ $this->assertSame(50.0, $participant->amount->getAmount());
+ $this->assertSame(EatingHabit::EATING_HABIT_OMNIVOR, $participant->eating_habit);
+
+ // Volljährige brauchen keine Erlaubnisse.
+ $this->assertSame(SwimmingPermission::SWIMMING_PERMISSION_ALLOWED, $participant->swimming_permission);
+ $this->assertSame(FirstAidPermission::FIRST_AID_PERMISSION_ALLOWED, $participant->first_aid_permission);
+
+ $this->assertSame(1, $participant->invoice_sequence);
+ $this->assertTrue($participant->foto_socialmedia);
+ $this->assertFalse($participant->foto_print);
+ }
+
+ public function test_u18_ohne_abgefragte_badeerlaubnis_bekommt_keine(): void
+ {
+ $event = $this->createEvent(swimmingPermissionRequired: false);
+
+ $response = new ShortSignUpCommand($this->shortRequest($event, [
+ 'birthday' => new \DateTime('2015-01-01'),
+ 'contactPerson' => 'Muster, Maria',
+ 'contactEmail' => 'maria@example.com',
+ 'firstAidPermission' => FirstAidPermission::FIRST_AID_PERMISSION_ALLOWED,
+ ]))->execute();
+
+ $this->assertTrue($response->success, $response->message ?? '');
+ $this->assertSame(SwimmingPermission::SWIMMING_PERMISSION_DENIED, $response->participant->swimming_permission);
+ $this->assertSame(EfzStatus::EFZ_STATUS_NOT_REQUIRED, $response->participant->efz_status);
+ $this->assertSame('Muster, Maria', $response->participant->contact_person);
+ $this->assertSame('maria@example.com', $response->participant->email_2);
+ }
+
+ public function test_u18_uebernimmt_gewaehlte_badeerlaubnis(): void
+ {
+ $event = $this->createEvent();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event, [
+ 'birthday' => new \DateTime('2015-01-01'),
+ 'contactPerson' => 'Muster, Maria',
+ 'contactEmail' => 'maria@example.com',
+ 'firstAidPermission' => FirstAidPermission::FIRST_AID_PERMISSION_DENIED,
+ 'swimmingPermission' => SwimmingPermission::SWIMMING_PERMISSION_ALLOWED,
+ ]))->execute();
+
+ $this->assertTrue($response->success, $response->message ?? '');
+ $this->assertSame(SwimmingPermission::SWIMMING_PERMISSION_ALLOWED, $response->participant->swimming_permission);
+ $this->assertSame(FirstAidPermission::FIRST_AID_PERMISSION_DENIED, $response->participant->first_aid_permission);
+ }
+
+ public function test_u18_ohne_kontaktperson_wird_abgelehnt(): void
+ {
+ $event = $this->createEvent();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event, [
+ 'birthday' => new \DateTime('2015-01-01'),
+ ]))->execute();
+
+ $this->assertFalse($response->success);
+ $this->assertSame(0, EventParticipant::count());
+ }
+
+ public function test_ohne_teilnahmegruppe_wird_abgelehnt(): void
+ {
+ $event = $this->createEvent(withFee: false);
+
+ $response = new ShortSignUpCommand($this->shortRequest($event))->execute();
+
+ $this->assertFalse($response->success);
+ $this->assertNotNull($response->message);
+ $this->assertSame(0, EventParticipant::count());
+ }
+
+ public function test_teilnahmeoptionen_werden_gespeichert(): void
+ {
+ $event = $this->createEvent();
+ $event->participation_options = [[
+ 'key' => 'shirt',
+ 'title' => 'T-Shirt',
+ 'label' => 'T-Shirt-Größe',
+ 'options' => [['value' => 'm', 'label' => 'M'], ['value' => 'l', 'label' => 'L']],
+ ]];
+ $event->save();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event, [
+ 'participationOptions' => ['shirt' => 'l'],
+ ]))->execute();
+
+ $this->assertTrue($response->success, $response->message ?? '');
+ $options = $response->participant->selectedOptions()->get();
+ $this->assertCount(1, $options);
+ $this->assertSame('shirt', $options->first()->question_key);
+ $this->assertSame('L', $options->first()->value_label);
+ }
+
+ public function test_unbeantwortete_teilnahmeoption_wird_abgelehnt(): void
+ {
+ $event = $this->createEvent();
+ $event->participation_options = [[
+ 'key' => 'shirt',
+ 'title' => 'T-Shirt',
+ 'label' => 'T-Shirt-Größe',
+ 'options' => [['value' => 'm', 'label' => 'M']],
+ ]];
+ $event->save();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event))->execute();
+
+ $this->assertFalse($response->success);
+ $this->assertSame(0, EventParticipant::count());
+ }
+
+ private function createLocalGroup(string $slug, string $name): Tenant
+ {
+ return Tenant::create([
+ 'slug' => $slug, 'name' => $name, 'email' => $slug . '@example.com',
+ 'email_finance' => $slug . '-f@example.com', 'url' => $slug . '.local',
+ 'account_name' => $name, 'account_iban' => 'DE00', 'account_bic' => 'XY',
+ 'city' => 'Stadt', 'postcode' => '00000', 'is_active_local_group' => true,
+ 'has_active_instance' => false,
+ ]);
+ }
+
+ public function test_einziger_stamm_wird_ohne_auswahl_zugeordnet(): void
+ {
+ $event = $this->createEvent();
+ $group = $this->createLocalGroup('stamm-a', 'Stamm A');
+ $event->localGroups()->attach($group->id);
+ $event->refresh();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event))->execute();
+
+ $this->assertTrue($response->success, $response->message ?? '');
+ $this->assertSame('stamm-a', $response->participant->local_group);
+ }
+
+ public function test_bei_mehreren_staemmen_wird_die_auswahl_uebernommen(): void
+ {
+ $event = $this->createEvent();
+ $first = $this->createLocalGroup('stamm-a', 'Stamm A');
+ $second = $this->createLocalGroup('stamm-b', 'Stamm B');
+ $event->localGroups()->attach([$first->id, $second->id]);
+ $event->refresh();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event, [
+ 'localGroupId' => $second->id,
+ ]))->execute();
+
+ $this->assertTrue($response->success, $response->message ?? '');
+ $this->assertSame('stamm-b', $response->participant->local_group);
+ }
+
+ public function test_bei_mehreren_staemmen_ist_die_auswahl_pflicht(): void
+ {
+ $event = $this->createEvent();
+ $first = $this->createLocalGroup('stamm-a', 'Stamm A');
+ $second = $this->createLocalGroup('stamm-b', 'Stamm B');
+ $event->localGroups()->attach([$first->id, $second->id]);
+ $event->refresh();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event))->execute();
+
+ $this->assertFalse($response->success);
+ $this->assertSame(0, EventParticipant::count());
+ }
+
+ public function test_nicht_teilnehmender_stamm_wird_abgelehnt(): void
+ {
+ $event = $this->createEvent();
+ $first = $this->createLocalGroup('stamm-a', 'Stamm A');
+ $second = $this->createLocalGroup('stamm-b', 'Stamm B');
+ $event->localGroups()->attach([$first->id, $second->id]);
+ $event->refresh();
+
+ // Stamm existiert, nimmt aber nicht an der Veranstaltung teil.
+ $foreign = $this->createLocalGroup('stamm-c', 'Stamm C');
+
+ $response = new ShortSignUpCommand($this->shortRequest($event, [
+ 'localGroupId' => $foreign->id,
+ ]))->execute();
+
+ $this->assertFalse($response->success);
+ $this->assertSame(0, EventParticipant::count());
+ }
+
+ public function test_resource_ueberlebt_kurzanmeldung(): void
+ {
+ $event = $this->createEvent();
+
+ $response = new ShortSignUpCommand($this->shortRequest($event))->execute();
+ $this->assertTrue($response->success, $response->message ?? '');
+
+ // Regressionswächter für Teilnehmerliste, Detailansicht und CSV-Export.
+ $array = $response->participant->toResource()->toArray(request());
+
+ $this->assertSame('Nicht im LV', $array['localgroup']);
+ $this->assertSame('Erteilt', $array['swimmingPermission']);
+ $this->assertSame('Zugestimmt', $array['extendedFirstAid']);
+ $this->assertSame('Omnivor', $array['eatingHabit']);
+ $this->assertSame('Teilnehmende', $array['participationType']);
+ }
+}
From ddfaab15011099e63f781fb496b3c21833942e77 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20G=C3=BCnrher?=
Date: Thu, 10 Sep 2026 11:45:52 +0200
Subject: [PATCH 5/6] =?UTF-8?q?Anschrift=20f=C3=BCr=20Veranstaltungsort?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Partials/Widgets/MyParticipations.vue | 2 +-
.../CreateEvent/CreateEventCommand.php | 13 ++
.../CreateEvent/CreateEventRequest.php | 6 +-
.../GenerateIcal/GenerateIcalCommand.php | 2 +-
.../UpdateEvent/UpdateEventCommand.php | 13 ++
.../UpdateEvent/UpdateEventRequest.php | 6 +-
.../Controllers/ArchivedEventsController.php | 1 +
.../Event/Controllers/CreateController.php | 4 +-
.../Event/Controllers/DetailsController.php | 2 +
app/Domains/Event/Views/ArchivedEvents.vue | 2 +-
app/Domains/Event/Views/Create.vue | 14 ++
.../Event/Views/Partials/AvailableEvents.vue | 4 +-
.../Event/Views/Partials/CommonSettings.vue | 18 ++
app/Domains/Event/Views/Signup.vue | 4 +-
app/Models/Event.php | 16 ++
app/Resources/EventParticipantResource.php | 1 +
app/Resources/EventResource.php | 5 +
..._add_street_and_house_number_to_events.php | 26 +++
tests/Feature/EventAddressTest.php | 177 ++++++++++++++++++
19 files changed, 306 insertions(+), 10 deletions(-)
create mode 100644 database/migrations/2026_09_16_140010_add_street_and_house_number_to_events.php
create mode 100644 tests/Feature/EventAddressTest.php
diff --git a/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue b/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue
index 08254f2..c0dd4f9 100644
--- a/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue
+++ b/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue
@@ -33,7 +33,7 @@ function navigateTo(url) {
- {{participation.event.postal_code}} {{participation.event.location}}
+ {{participation.eventAddress}}
In Kalender importieren
diff --git a/app/Domains/Event/Actions/CreateEvent/CreateEventCommand.php b/app/Domains/Event/Actions/CreateEvent/CreateEventCommand.php
index 2203507..8b3fd18 100644
--- a/app/Domains/Event/Actions/CreateEvent/CreateEventCommand.php
+++ b/app/Domains/Event/Actions/CreateEvent/CreateEventCommand.php
@@ -38,6 +38,8 @@ class CreateEventCommand {
'name' => $this->request->name,
'identifier' => Str::random(10),
'location' => $this->request->location,
+ 'street' => $this->normalizeOptional($this->request->street),
+ 'house_number' => $this->normalizeOptional($this->request->houseNumber),
'postal_code' => $this->request->postalCode,
'email' => $this->request->email,
'start_date' => $this->request->begin,
@@ -107,6 +109,17 @@ class CreateEventCommand {
return $response;
}
+ /**
+ * Straße und Hausnummer sind optional. Ein leer gelassenes Eingabefeld liefert einen leeren String --
+ * gespeichert wird dafür `null`, damit „nicht angegeben" nur eine Darstellung hat.
+ */
+ private function normalizeOptional(?string $value): ?string
+ {
+ $value = trim((string) $value);
+
+ return '' === $value ? null : $value;
+ }
+
/**
* Event-Teil der Rechnungsnummer, z.B. `WM-V-20260701`: Tenant-Präfix, Dokumentart „V" für
* Veranstaltung, dann ohne Trenner Jahr, Monat des Beginns und die laufende Nummer der
diff --git a/app/Domains/Event/Actions/CreateEvent/CreateEventRequest.php b/app/Domains/Event/Actions/CreateEvent/CreateEventRequest.php
index dbf5ed5..8ff42f8 100644
--- a/app/Domains/Event/Actions/CreateEvent/CreateEventRequest.php
+++ b/app/Domains/Event/Actions/CreateEvent/CreateEventRequest.php
@@ -19,8 +19,10 @@ class CreateEventRequest {
public string $accountOwner;
public string $accountIban;
public bool $payPerDay;
+ public ?string $street;
+ public ?string $houseNumber;
- public function __construct(string $name, string $location, string $postalCode, string $email, DateTime $begin, DateTime $end, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $earlyBirdEndAmountIncrease, ParticipationFeeType $participationFeeType, string $accountOwner, string $accountIban, bool $payPerDay) {
+ public function __construct(string $name, string $location, string $postalCode, string $email, DateTime $begin, DateTime $end, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $earlyBirdEndAmountIncrease, ParticipationFeeType $participationFeeType, string $accountOwner, string $accountIban, bool $payPerDay, ?string $street = null, ?string $houseNumber = null) {
$this->name = $name;
$this->location = $location;
$this->postalCode = $postalCode;
@@ -34,5 +36,7 @@ class CreateEventRequest {
$this->accountOwner = $accountOwner;
$this->accountIban = $accountIban;
$this->payPerDay = $payPerDay;
+ $this->street = $street;
+ $this->houseNumber = $houseNumber;
}
}
diff --git a/app/Domains/Event/Actions/GenerateIcal/GenerateIcalCommand.php b/app/Domains/Event/Actions/GenerateIcal/GenerateIcalCommand.php
index b5ca0c9..425a554 100644
--- a/app/Domains/Event/Actions/GenerateIcal/GenerateIcalCommand.php
+++ b/app/Domains/Event/Actions/GenerateIcal/GenerateIcalCommand.php
@@ -18,7 +18,7 @@ class GenerateIcalCommand
$dtEnd = $event->end_date->copy()->addDay()->format('Ymd');
$now = now()->format('Ymd\THis\Z');
$summary = $this->escapeIcal($event->name);
- $location = $this->escapeIcal(trim($event->postal_code . ' ' . $event->location));
+ $location = $this->escapeIcal($event->getFullAddress());
$description = $this->escapeIcal('Teilnahme als: ' . $participant->getOfficialName());
$icalContent = implode("\r\n", [
diff --git a/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php b/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php
index 24bd1de..b6ce8d4 100644
--- a/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php
+++ b/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php
@@ -13,6 +13,8 @@ class UpdateEventCommand {
$this->request->event->name = $this->request->eventName;
$this->request->event->location = $this->request->eventLocation;
+ $this->request->event->street = $this->normalizeOptional($this->request->street);
+ $this->request->event->house_number = $this->normalizeOptional($this->request->houseNumber);
$this->request->event->postal_code = $this->request->postalCode;
$this->request->event->email = $this->request->email;
$this->request->event->early_bird_end = $this->request->earlyBirdEnd;
@@ -42,4 +44,15 @@ class UpdateEventCommand {
return $response;
}
+
+ /**
+ * Straße und Hausnummer sind optional. Ein geleertes Eingabefeld liefert einen leeren String --
+ * gespeichert wird dafür `null`, damit „nicht angegeben" nur eine Darstellung hat.
+ */
+ private function normalizeOptional(?string $value): ?string
+ {
+ $value = trim((string) $value);
+
+ return '' === $value ? null : $value;
+ }
}
diff --git a/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php b/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
index 4ce1ff2..2b34a50 100644
--- a/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
+++ b/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
@@ -10,6 +10,8 @@ class UpdateEventRequest {
public Event $event;
public string $eventName;
public string $eventLocation;
+ public ?string $street;
+ public ?string $houseNumber;
public string $postalCode;
public string $email;
public DateTime $earlyBirdEnd;
@@ -24,11 +26,13 @@ class UpdateEventRequest {
public array $contributingLocalGroups;
public array $eatingHabits;
- public function __construct(Event $event, string $eventName, string $eventLocation, string $postalCode, string $email, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $alcoholicsAge, bool $sendWeeklyReports, bool $registrationAllowed, Amount $flatSupport, Amount $supportPerPerson, array $contributingLocalGroups, array $eatingHabits, bool $shortRegistration = false, bool $swimmingPermissionRequired = true)
+ public function __construct(Event $event, string $eventName, string $eventLocation, string $postalCode, string $email, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $alcoholicsAge, bool $sendWeeklyReports, bool $registrationAllowed, Amount $flatSupport, Amount $supportPerPerson, array $contributingLocalGroups, array $eatingHabits, bool $shortRegistration = false, bool $swimmingPermissionRequired = true, ?string $street = null, ?string $houseNumber = null)
{
$this->event = $event;
$this->eventName = $eventName;
$this->eventLocation = $eventLocation;
+ $this->street = $street;
+ $this->houseNumber = $houseNumber;
$this->postalCode = $postalCode;
$this->email = $email;
$this->earlyBirdEnd = $earlyBirdEnd;
diff --git a/app/Domains/Event/Controllers/ArchivedEventsController.php b/app/Domains/Event/Controllers/ArchivedEventsController.php
index 24ef421..e20a9f6 100644
--- a/app/Domains/Event/Controllers/ArchivedEventsController.php
+++ b/app/Domains/Event/Controllers/ArchivedEventsController.php
@@ -17,6 +17,7 @@ class ArchivedEventsController extends CommonController
'name' => $event->name,
'location' => $event->location,
'postalCode' => $event->postal_code,
+ 'fullAddress' => $event->getFullAddress(),
'eventBegin' => $event->start_date->format('d.m.Y'),
'eventEnd' => $event->end_date->format('d.m.Y'),
];
diff --git a/app/Domains/Event/Controllers/CreateController.php b/app/Domains/Event/Controllers/CreateController.php
index ca7da14..8472a16 100644
--- a/app/Domains/Event/Controllers/CreateController.php
+++ b/app/Domains/Event/Controllers/CreateController.php
@@ -56,7 +56,9 @@ class CreateController extends CommonController {
$participationFeeType,
$request->input('eventAccount'),
$request->input('eventIban'),
- $payPerDay
+ $payPerDay,
+ $request->input('eventStreet'),
+ $request->input('eventHouseNumber')
);
$wasSuccessful = false;
diff --git a/app/Domains/Event/Controllers/DetailsController.php b/app/Domains/Event/Controllers/DetailsController.php
index da02064..dafa548 100644
--- a/app/Domains/Event/Controllers/DetailsController.php
+++ b/app/Domains/Event/Controllers/DetailsController.php
@@ -63,6 +63,8 @@ class DetailsController extends CommonController {
$eatingHabits,
(bool)$request->input('shortRegistration', false),
(bool)$request->input('swimmingPermissionRequired', true),
+ $request->input('street'),
+ $request->input('houseNumber'),
);
$eventUpdateCommand = new UpdateEventCommand($eventUpdateRequest);
diff --git a/app/Domains/Event/Views/ArchivedEvents.vue b/app/Domains/Event/Views/ArchivedEvents.vue
index 3a167fc..caa4967 100644
--- a/app/Domains/Event/Views/ArchivedEvents.vue
+++ b/app/Domains/Event/Views/ArchivedEvents.vue
@@ -47,7 +47,7 @@ async function unarchiveEvent(eventId) {
{{ event.name }}
- {{ event.postalCode }} {{ event.location }}
+ {{ event.fullAddress }}
·
{{ event.eventBegin }} – {{ event.eventEnd }}
diff --git a/app/Domains/Event/Views/Create.vue b/app/Domains/Event/Views/Create.vue
index 310cd5d..d6f7aad 100644
--- a/app/Domains/Event/Views/Create.vue
+++ b/app/Domains/Event/Views/Create.vue
@@ -23,6 +23,8 @@
eventName: '',
eventPostalCode: '',
eventLocation: '',
+ eventStreet: '',
+ eventHouseNumber: '',
eventEmail: props.emailAddress ? props.emailAddress : '',
eventBegin: '',
eventEnd: '',
@@ -145,6 +147,8 @@
eventName: formData.eventName,
eventPostalCode: formData.eventPostalCode,
eventLocation: formData.eventLocation,
+ eventStreet: formData.eventStreet,
+ eventHouseNumber: formData.eventHouseNumber,
eventEmail: formData.eventEmail,
eventBegin: formData.eventBegin,
eventEnd: formData.eventEnd,
@@ -195,6 +199,16 @@
+
+ Straße (optional)
+
+
+
+
+ Hausnummer (optional)
+
+
+
Postleitzahl des Veranstaltungsorts
diff --git a/app/Domains/Event/Views/Partials/AvailableEvents.vue b/app/Domains/Event/Views/Partials/AvailableEvents.vue
index bedd127..ab00175 100644
--- a/app/Domains/Event/Views/Partials/AvailableEvents.vue
+++ b/app/Domains/Event/Views/Partials/AvailableEvents.vue
@@ -22,7 +22,7 @@ const props = defineProps({