Basic signup for events

This commit is contained in:
2026-03-21 21:02:15 +01:00
parent 23af267896
commit b8341890d3
74 changed files with 4046 additions and 947 deletions

View File

@@ -0,0 +1,45 @@
<script setup>
const props = defineProps({ formData: Object, event: Object, selectedAddons: Object })
const emit = defineEmits(['next', 'back'])
</script>
<template>
<div>
<!-- Solidarbeitrag-Auswahl -->
<div v-if="event.solidarityPayment" style="margin-bottom: 20px;">
<h3>Beitrag</h3>
<label v-if="event.participationFee_1?.active" style="display: block; margin-bottom: 8px;">
<input type="radio" v-model="formData.beitrag" value="reduced" />
{{ event.participationFee_1.name }} ({{ event.participationFee_1.amount }} )
</label>
<label style="display: block; margin-bottom: 8px;">
<input type="radio" v-model="formData.beitrag" value="regular" />
{{ event.participationFee_2?.name ?? 'Regulärer Beitrag' }} ({{ event.participationFee_2?.amount }} )
</label>
<label v-if="event.participationFee_3?.active" style="display: block; margin-bottom: 8px;">
<input type="radio" v-model="formData.beitrag" value="social" />
{{ event.participationFee_3.name }} ({{ event.participationFee_3.amount }} )
</label>
</div>
<!-- Addons -->
<div v-if="event.addons?.length > 0">
<h3>Zusatzoptionen</h3>
<div v-for="addon in event.addons" :key="addon.id" style="margin-bottom: 16px; padding: 12px; background: #f8fafc; border-radius: 8px;">
<label style="display: flex; gap: 12px; cursor: pointer;">
<input type="checkbox" v-model="selectedAddons[addon.id]" style="margin-top: 4px;" />
<span>
<strong>{{ addon.name }}</strong>
<span style="display: block; color: #6b7280; font-size: 0.875rem;">Betrag: {{ addon.amount }}</span>
<span style="display: block; color: #374151; font-size: 0.875rem; margin-top: 4px;">{{ addon.description }}</span>
</span>
</label>
</div>
</div>
<div class="btn-row">
<button type="button" class="btn-secondary" @click="emit('back', 5)"> Zurück</button>
<button type="button" class="btn-primary" @click="emit('next', 7)">Weiter </button>
</div>
</div>
</template>

View File

@@ -0,0 +1,103 @@
<script setup>
defineProps({ event: Object })
const emit = defineEmits(['next'])
</script>
<template>
<div>
<h3 style="margin: 0 0 6px 0; color: #111827;">Wer nimmt teil?</h3>
<p style="margin: 0 0 24px 0; color: #6b7280; font-size: 0.95rem;">Bitte wähle deine Altersgruppe aus.</p>
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<!-- Kind / Jugendliche:r -->
<div class="age-card" @click="emit('next', 2)">
<div class="age-card__badge">
<img :src="'/images/children.png'" alt="Abzeichen Kind" class="age-card__img" onerror="this.style.display='none'" />
<div class="age-card__badge-fallback">👦</div>
</div>
<div class="age-card__body">
<h4 class="age-card__title">Mein Kind anmelden:</h4>
<p class="age-card__desc">Mein Kind ist <strong>jünger als {{ event.alcoholicsAge }} Jahre.</strong></p>
</div>
</div>
<!-- Erwachsene:r -->
<div class="age-card" @click="emit('next', 3)">
<div class="age-card__badge">
<img :src="'/images/adults.png'" alt="Abzeichen Erwachsene" class="age-card__img" onerror="this.style.display='none'" />
<div class="age-card__badge-fallback">🧑</div>
</div>
<div class="age-card__body">
<h4 class="age-card__title">Mich selbst anmelden</h4>
<p class="age-card__desc">Ich bin <strong>{{ event.alcoholicsAge }} Jahre oder älter</strong>.</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.age-card {
flex: 1;
min-width: 220px;
display: flex;
flex-direction: column;
align-items: center;
background: #f8fafc;
border: 2px solid #e5e7eb;
border-radius: 12px;
padding: 28px 20px;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
text-align: center;
}
.age-card:hover {
border-color: #2563eb;
box-shadow: 0 4px 16px rgba(37, 99, 235, 0.12);
transform: translateY(-2px);
}
.age-card__badge {
position: relative;
width: 350px;
height: 200px;
margin-bottom: 16px;
}
.age-card__img {
width: 350px;
height: 200px;
object-fit: contain;
}
.age-card__badge-fallback {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
background: #e0f2fe;
border-radius: 50%;
}
/* Fallback ausblenden wenn Bild geladen ist */
.age-card__img:not([style*="display:none"]) + .age-card__badge-fallback {
display: none;
}
.age-card__body { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.age-card__title { margin: 0; font-size: 1.1rem; font-weight: 700; color: #111827; }
.age-card__desc { margin: 0; font-size: 0.9rem; color: #374151; }
.age-card__hint { margin: 0; font-size: 0.8rem; color: #6b7280; }
.age-card__cta {
margin-top: 10px;
display: inline-block;
padding: 6px 18px;
background: #2563eb;
color: white;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,52 @@
<script setup>
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
</script>
<template>
<div>
<h3>Allergien & Ernährung</h3>
<table class="form-table">
<tr><td>Allergien:</td><td><input type="text" v-model="props.formData.allergien" /></td></tr>
<tr>
<td>
Letzte Teranus-Impfung:
<span style="display: block; font-size: 0.8rem; color: #6b7280; margin-top: 4px;">Lass das Feld frei, wenn die Information nicht vorliegt oder du diese nicht mitteilen willst</span>
</td><td><input type="date" v-model="props.formData.tetanusVaccination" /></td></tr>
<tr><td>Unverträglichkeiten:</td><td><input type="text" v-model="props.formData.intolerances" /></td></tr>
<tr>
<td>
Medikamente:<br />
<span style="display: block; font-size: 0.8rem; color: #6b7280; margin-top: 4px;">Bitte in ausreichender Menge mitbringen</span>
</td>
<td>
<input type="text" v-model="props.formData.medikamente" />
</td>
</tr>
<tr>
<td>Ernährungsweise:</td>
<td>
<select v-model="props.formData.eatingHabit">
<option
v-for="eatingHabit in props.event.eatingHabits"
:value="eatingHabit.data.slug">{{eatingHabit.data.name}}</option>
</select>
</td>
</tr>
<tr>
<td>Anmerkungen:</td>
<td><textarea rows="5" v-model="props.formData.anmerkungen" style="width: 100%;"></textarea></td>
</tr>
<tr>
<td colspan="2" class="btn-row">
<button type="button" class="btn-secondary" @click="emit('back', 7)"> Zurück</button>
<button type="button" class="btn-primary" @click="emit('next', 9)">Weiter </button>
</td>
</tr>
</table>
</div>
</template>

View File

@@ -0,0 +1,81 @@
<script setup>
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
const next = () => {
const arrival = new Date(props.formData.arrival)
arrival.setHours(0,0,0,0);
const departure = new Date(props.formData.departure)
const eventStart = new Date(props.event.eventBeginInternal)
const eventEnd = new Date(props.event.eventEndInternal)
arrival.setHours(0,0,0,0);
departure.setHours(0,0,0,0);
eventStart.setHours(0,0,0,0);
eventEnd.setHours(0,0,0,0);
if (arrival < eventStart) {
alert('Bitte gültige Anreise angeben innerhalb des Veranstaltungszeitraums wählen.')
return
}
if (arrival > eventEnd) {
alert('Bitte gültige Abreise angeben innerhalb des Veranstaltungszeitraums wählen.')
return
}
if (departure < arrival) {
alert('Abreise kann niht vor der Anreise liegen. Bitte korrigieren.')
return
}
const hasAddons = (props.event.addons?.length > 0) || props.event.solidarityPayment
emit('next', 5)
}
const back = () => emit('back', 3)
</script>
<template>
<div>
<h3>An- und Abreise</h3>
<table class="form-table">
<tr>
<td>Anreise:</td>
<td>
<input type="date" v-model="formData.arrival" /><br />
<select v-model="formData.anreise_essen" style="margin-top: 6px;">
<option value="1">Vor dem Abendessen</option>
<option value="2">Vor dem Mittagessen</option>
<option value="3">Vor dem Frühstück</option>
<option value="4">Keine Mahlzeit</option>
</select>
</td>
</tr>
<tr>
<td>Abreise:</td>
<td>
<input type="date" v-model="formData.departure" /><br />
<select v-model="formData.abreise_essen" style="margin-top: 6px;">
<option value="1">Nach dem Frühstück</option>
<option value="2">Nach dem Mittagessen</option>
<option value="3">Nach dem Abendessen</option>
<option value="4">Keine Mahlzeit</option>
</select>
</td>
</tr>
<tr>
<td colspan="2" class="btn-row">
<button type="button" class="btn-secondary" @click="back"> Zurück</button>
<button type="button" class="btn-primary" @click="next">Weiter </button>
</td>
</tr>
</table>
</div>
</template>

View File

@@ -0,0 +1,125 @@
<script setup>
import ErrorText from "../../../../../../Views/Components/ErrorText.vue";
import {reactive} from "vue";
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
const errors = reactive({
ansprechpartner: '',
telefon_2: '',
email_2: '',
badeerlaubnis: '',
first_aid: '',
})
const next = () => {
errors.ansprechpartner = ''
errors.telefon_2 = ''
errors.email_2 = ''
errors.badeerlaubnis = ''
errors.first_aid = ''
let hasError = false
if (!props.formData.ansprechpartner) {
errors.ansprechpartner = 'Bitte eine Kontaktperson angeben.'
hasError = true
}
if (!props.formData.telefon_2) {
errors.telefon_2 = 'Bitte eine Telefonnummer angeben.'
hasError = true
}
if (!props.formData.email_2) {
errors.email_2 = 'Bitte eine E-Mail-Adresse angeben.'
hasError = true
}
if (props.formData.badeerlaubnis === '-1') {
errors.badeerlaubnis = 'Bitte triff eine Entscheidung. Bist du dir unsicher, kontaktiere bitte die Aktionsleitung'
hasError = true
}
if (props.formData.first_aid === '-1') {
errors.first_aid = 'Bitte triff eine Entscheidung. Bist du dir unsicher, kontaktiere bitte die Aktionsleitung.'
hasError = true
}
if (hasError) {
return
}
emit('next', 3)
}
</script>
<template>
<div>
<h3>Kontaktperson</h3>
<table class="form-table">
<tr>
<td>Name (Nachname, Vorname):</td>
<td>
<input type="text" v-model="formData.ansprechpartner" />
<ErrorText :message="errors.ansprechpartner" />
</td>
</tr>
<tr>
<td>Telefon:</td>
<td>
<input type="text" v-model="formData.telefon_2" />
<ErrorText :message="errors.telefon_2" />
</td>
</tr>
<tr>
<td>E-Mail:</td>
<td>
<input type="text" v-model="formData.email_2" />
<ErrorText :message="errors.email_2" />
</td>
</tr>
<tr>
<td>Badeerlaubnis:</td>
<td>
<select v-model="formData.badeerlaubnis">
<option value="-1">Bitte wählen</option>
<option
v-for="swimmingPermission in props.event.swimmingPermissions"
:value="swimmingPermission.slug">{{swimmingPermission.name}}</option>
</select>
<ErrorText :message="errors.badeerlaubnis" />
</td>
</tr>
<tr>
<td>Erweiterte Erste Hilfe erlaubt:*</td>
<td>
<select v-model="formData.first_aid">
<option value="-1">Bitte wählen</option>
<option
v-for="firstAidPermission in props.event.firstAidPermissions"
:value="firstAidPermission.slug">{{firstAidPermission.name}}</option>
</select><br />
<span style="font-size: 0.8rem; color: #6b7280;">
Nicht dringend-notwendige Erste-Hilfe-Maßnahmen, beinhaltet das Entfernen von Zecken und Splittern sowie das Kleben von Pflastern.
</span>
<ErrorText :message="errors.first_aid" />
</td>
</tr>
<tr>
<td></td>
<td>
</td>
</tr>
<tr>
<td colspan="2" class="btn-row">
<button type="button" class="btn-primary" @click="next">Weiter </button>
</td>
</tr>
</table>
</div>
</template>

View File

@@ -0,0 +1,124 @@
<script setup>
import {reactive} from "vue";
const props = defineProps({ formData: Object, localGroups: Array })
const emit = defineEmits(['next', 'back'])
const errors = reactive({
vorname: '',
nachname: '',
geburtsdatum: '',
localGroup: '',
address1: '',
plz: '',
ort: '',
telefon_1: '',
email_1: '',
})
const next = () => {
errors.vorname = ''
errors.nachname = ''
errors.geburtsdatum = ''
errors.localGroup = ''
errors.address1 = ''
errors.plz = ''
errors.ort = ''
errors.telefon_1 = ''
errors.email_1 = ''
let hasError = false
if (!props.formData.vorname) {
errors.vorname = 'Bitte den Vornamen angeben.'
hasError = true
}
if (!props.formData.nachname) {
errors.nachname = 'Bitte den Nachnamen angeben.'
hasError = true
}
if (!props.formData.geburtsdatum) {
errors.geburtsdatum = 'Bitte das Geburtsdatum angeben.'
hasError = true
}
if (props.formData.localGroup === '-1') {
errors.localGroup = 'Bitte den Stamm auswählen.'
hasError = true
}
if (!props.formData.address1) {
errors.address1 = 'Bitte die Adresse angeben.'
hasError = true
}
if (!props.formData.plz) {
errors.plz = 'Bitte die Postleitzahl angeben.'
hasError = true
}
if (!props.formData.ort) {
errors.ort = 'Bitte den Ort angeben.'
hasError = true
}
if (!props.formData.email_1) {
errors.email_1 = 'Bitte eine E-Mail-Adresse angeben.'
hasError = true
}
if (hasError) {
return
}
emit('next', 4)
}
</script>
<template>
<div>
<h3>Persönliche Daten</h3>
<table class="form-table">
<tr><td>Vorname:</td><td><input type="text" v-model="props.formData.vorname" /></td></tr>
<tr><td>Nachname:</td><td><input type="text" v-model="props.formData.nachname" /></td></tr>
<tr><td>Pfadiname:</td><td><input type="text" v-model="props.formData.pfadiname" /></td></tr>
<tr>
<td>Stamm:</td>
<td>
<select v-model="props.formData.localGroup">
<option value="-1">Bitte wählen</option>
<option v-for="lg in localGroups" :key="lg.id" :value="lg.id">{{ lg.name }}</option>
</select>
</td>
</tr>
<tr><td>Geburtsdatum:</td><td><input type="date" v-model="props.formData.geburtsdatum" /></td></tr>
<tr>
<td>Adresse:</td>
<td>
<input type="text" v-model="props.formData.address1" />
</td>
</tr>
<tr>
<td></td>
<td>
<input type="text" v-model="props.formData.address2" />
</td>
</tr>
<tr>
<td>PLZ, Ort:</td>
<td>
<input maxlength="5" type="text" v-model="props.formData.plz" style="width: 100px; margin-right: 8px;" />
<input type="text" v-model="props.formData.ort" style="width: calc(100% - 110px);" />
</td>
</tr>
<tr><td>Telefon:</td><td><input type="text" v-model="props.formData.telefon_1" /></td></tr>
<tr><td>E-Mail:</td><td><input type="text" v-model="props.formData.email_1" /></td></tr>
<tr>
<td colspan="2" class="btn-row">
<button type="button" class="btn-primary" @click="next">Weiter </button>
</td>
</tr>
</table>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup>
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
const acceptAll = () => {
Object.keys(props.formData.foto).forEach(k => props.formData.foto[k] = true)
emit('next', 8)
}
const back = () => {
const hasAddons = (props.event.addons?.length > 0) || props.event.solidarityPayment
emit('back', hasAddons ? 6 : 5)
}
</script>
<template>
<div>
<h3>Fotoerlaubnis</h3>
<div v-for="[key, label] in [['socialmedia','Social Media'],['print','Printmedien'],['webseite','Webseite'],['partner','Partnerorganisationen'],['intern','Interne Zwecke']]"
:key="key"
style="margin-bottom: 10px;">
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.foto[key]" />
{{ label }}
</label>
</div>
<div class="btn-row">
<button type="button" class="btn-secondary" @click="back"> Zurück</button>
<button type="button" class="btn-primary" style="background: #059669;" @click="acceptAll">Alle akzeptieren & weiter</button>
<button type="button" class="btn-primary" @click="emit('next', 8)">Weiter </button>
</div>
</div>
</template>

View File

@@ -0,0 +1,130 @@
<script setup>
import { watch } from "vue";
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
watch(
() => props.formData.participationType,
(value) => {
if (!value) {
props.formData.beitrag = 'standard'
return
}
props.formData.beitrag = 'standard'
}
)
const nextStep = () => {
const hasAddons = (props.event.addons?.length ?? 0) > 0
emit('next', hasAddons ? 6 : 7)
}
</script>
<template>
<div>
<h3 v-if="event.solidarityPayment">Solidarbeitrag Teilnahmegruppe</h3>
<h3 v-else>Ich nehme teil als ...</h3>
<table style="width: 100%;">
<tr
v-for="participationType in props.event.participationTypes"
:key="participationType.type.slug"
style="vertical-align: top;"
>
<td style="width: 50px; padding-top: 6px;">
<input
:id="participationType.type.slug"
v-model="props.formData.participationType"
type="radio"
:value="participationType.type.slug"
/>
</td>
<td style="padding-bottom: 16px;">
<label :for="participationType.type.slug" style="line-height: 1.5; font-weight: 600; cursor: pointer;">
{{ participationType.type.name }}
</label><br />
<label
:for="participationType.type.slug"
style="line-height: 1.5; padding-left: 15px; font-style: italic; color: #606060; cursor: pointer;"
>
{{ participationType.description }}
</label>
<div
v-if="props.formData.participationType === participationType.type.slug"
style="margin-top: 10px; margin-left: 15px; padding: 12px 14px; background: #f8fafc; border-left: 3px solid #2563eb; border-radius: 6px;"
>
<template
v-if="participationType.amount_reduced !== null || participationType.amount_solidarity !== null"
>
<div style="margin-bottom: 8px; font-size: 0.95rem; font-weight: 600; color: #374151;">
Beitrag auswählen
</div>
<label style="display: block; margin-bottom: 8px; cursor: pointer;">
<input type="radio" v-model="props.formData.beitrag" value="standard" />
Standardbeitrag
<span style="color: #606060;">
({{ participationType.amount_standard.readable }}
<template v-if="props.event.payPerDay">/ Tag</template>
<template v-else>Gesamt</template>)
</span>
</label>
<label
v-if="participationType.amount_reduced !== null"
style="display: block; margin-bottom: 8px; cursor: pointer;"
>
<input type="radio" v-model="props.formData.beitrag" value="reduced" />
Reduzierter Beitrag
<span style="color: #606060;">
({{ participationType.amount_reduced.readable }}
<template v-if="props.event.payPerDay">/ Tag</template>
<template v-else>Gesamt</template>)
</span>
</label>
<label
v-if="participationType.amount_solidarity !== null"
style="display: block; margin-bottom: 0; cursor: pointer;"
>
<input type="radio" v-model="props.formData.beitrag" value="solidarity" />
Solidaritätsbeitrag
<span style="color: #606060;">
({{ participationType.amount_solidarity.readable }}
<template v-if="props.event.payPerDay">/ Tag</template>
<template v-else>Gesamt</template>)
</span>
</label>
</template>
<template v-else>
<div style="font-size: 0.9rem; color: #606060;">
Standardbeitrag:
<strong>{{ participationType.amount_standard.readable }}</strong>
<template v-if="props.event.payPerDay">/ Tag</template>
<template v-else>Gesamt</template>
</div>
</template>
</div>
</td>
</tr>
</table>
<div class="btn-row">
<button type="button" class="btn-secondary" @click="emit('back', 3)"> Zurück</button>
<button
type="button"
v-if="props.formData.participationType"
class="btn-primary"
@click="nextStep"
>
Weiter
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,63 @@
<script setup>
import {format, parseISO} from "date-fns";
const props = defineProps({
formData: Object,
event: Object,
summaryAmount: String,
summaryLoading: Boolean,
submitting: Boolean,
})
const emit = defineEmits(['back', 'submit'])
function formatDate(dateString) {
if (!dateString) return ''
return format(parseISO(dateString), 'dd.MM.yyyy')
}
</script>
<template>
<div>
<h3>Zusammenfassung</h3>
<div v-if="summaryLoading" style="color: #6b7280; padding: 20px 0;">Wird geladen</div>
<div v-else>
<table class="form-table" style="margin-bottom: 20px;">
<tr><td>Veranstaltung:</td><td><strong>{{ event.name }}</strong></td></tr>
<tr><td>Anreise:</td><td>{{ formatDate(formData.arrival) }}</td></tr>
<tr><td>Abreise:</td><td>{{ formatDate(formData.departure) }}</td></tr>
</table>
<div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px;">
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.summary_information_correct" />
Ich bestätige, dass alle Angaben korrekt sind.
</label>
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.summary_accept_terms" />
Ich akzeptiere die Teilnahmebedingungen.
</label>
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.legal_accepted" />
Ich stimme der Datenschutzerklärung zu.
</label>
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.payment" />
Ich bestätige, den Betrag von <strong>{{ summaryAmount }}</strong> zu überweisen.
</label>
</div>
<div class="btn-row">
<button type="button" class="btn-secondary" @click="emit('back', 8)"> Zurück</button>
<button
type="submit"
class="btn-primary"
:disabled="!formData.summary_information_correct || !formData.summary_accept_terms || !formData.legal_accepted || !formData.payment || submitting"
style="background: #059669;"
>
{{ submitting ? 'Wird gesendet…' : 'Jetzt anmelden ✓' }}
</button>
</div>
</div>
</div>
</template>