Implemented additional event information

This commit is contained in:
2026-08-12 15:54:00 +02:00
parent 9581176a0c
commit e95de52768
16 changed files with 327 additions and 22 deletions
@@ -1,5 +1,5 @@
<script setup>
import {onMounted, reactive, watch} from "vue";
import {computed, onMounted, reactive, watch} from "vue";
import AmountInput from "../../../../Views/Components/AmountInput.vue";
import DialableTelephoneNumber from "../../../../Views/Components/DialableTelephoneNumber.vue";
@@ -60,8 +60,18 @@ const form = reactive({
amountPaid: '',
amountExpected: '',
cocStatus: '',
participationOptions: {},
});
// Teilnahmeoptionen des Events (Definition mit Titel/Frage/Optionen).
const participationQuestions = computed(() => staticProps.event?.participationOptions ?? []);
// Gewählte Antwort (Options-Label) eines Teilnehmers zu einer Frage für die Ansicht „Titel: Antwort".
function answerLabelFor(questionKey) {
const answer = (props.participant?.participationOptions ?? []).find(o => o.questionKey === questionKey);
return answer?.valueLabel ?? '—';
}
watch(
() => props.participant,
(participant) => {
@@ -94,6 +104,9 @@ watch(
form.amountPaid = participant?.amountPaid.short ?? '';
form.amountExpected = participant?.amountExpected.short ?? '';
form.cocStatus = participant?.efz_status ?? '';
form.participationOptions = Object.fromEntries(
(participant?.participationOptions ?? []).map(o => [o.questionKey, o.value])
);
},
{ immediate: true }
);
@@ -388,6 +401,27 @@ function saveParticipant() {
</table>
</div>
</div>
<div class="participationData" v-if="participationQuestions.length > 0">
<div>
<h3>Teilnahmeoptionen</h3>
<table>
<tr v-for="question in participationQuestions" :key="question.key">
<th>{{ question.title || question.label }}</th>
<td>
<span v-if="!staticProps.editMode">{{ answerLabelFor(question.key) }}</span>
<select v-else v-model="form.participationOptions[question.key]">
<option value=""> keine </option>
<option v-for="option in question.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</td>
</tr>
</table>
</div>
<div></div>
</div>
</div>
<button v-if="!staticProps.editMode" class="button" @click="enableEditMode">Bearbeiten</button>
@@ -407,6 +441,10 @@ function saveParticipant() {
gap: 20px;
}
.participationData table tr th {
width: 200px;
}
.participationData div {
flex: 1;
vertical-align: top;
@@ -0,0 +1,67 @@
<script setup>
import {computed, inject} from "vue";
import ParticipantsList from "./ParticipantsList.vue";
const props = defineProps({
data: {
type: Object,
default: () => ({participants: {}, event: {}, listType: ''}),
},
});
// Von TabbedPage bereitgestellt: erlaubt den Sprung zum Übersicht-Tab (Index 0), wo die
// Teilnahmeoptionen konfiguriert werden.
const selectTab = inject('selectTab', null);
const hasOptions = computed(() => (props.data?.event?.participationOptions?.length ?? 0) > 0);
function goToOverview() {
if (selectTab) {
selectTab(0);
}
}
</script>
<template>
<ParticipantsList v-if="hasOptions" :data="data" :show-group-mail="false"/>
<div v-else class="no-options">
<h3>Keine Teilnahmeoptionen angelegt</h3>
<p>
Für diese Veranstaltung sind noch keine Teilnahmeoptionen angelegt. Sobald du unter
<strong>Übersicht Teilnahmeoptionen</strong> Fragen definierst, werden die Teilnehmenden hier nach
ihrer Auswahl gruppiert.
</p>
<button v-if="selectTab" type="button" class="button" @click="goToOverview">
Zu den Teilnahmeoptionen
</button>
</div>
</template>
<style scoped>
.no-options {
max-width: 640px;
margin: 20px auto;
padding: 24px;
background: #f8fafc;
border: 1px solid #e5e7eb;
border-radius: 10px;
text-align: center;
}
.no-options p {
color: #4b5563;
line-height: 1.6;
}
.button {
margin-top: 12px;
padding: 8px 18px;
background: #2563eb;
color: #fff;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
</style>
@@ -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: <strong>{{ getAgeCounts(participants)['27+'] ?? 0 }}</strong>
</td>
<td class="pl-summary-action">
<input type="button" class="button" @click="mailToGroup(groupKey)" value="E-Mail an Gruppe senden" />
<input v-if="showGroupMail" type="button" class="button" @click="mailToGroup(groupKey)"
value="E-Mail an Gruppe senden"/>
</td>
</tr>
</tbody>
@@ -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() {
<div v-for="(question, qIndex) in questions" :key="qIndex" class="question-card">
<div class="question-head">
<input
type="text"
v-model="question.label"
class="width-full"
placeholder="Frage, z. B. „Welchen Kurs möchtest du belegen?“"
/>
<div class="question-fields">
<label class="field-label">Titel (Überschrift / Feldbeschriftung)</label>
<input
type="text"
v-model="question.title"
class="width-full"
placeholder="Titel, z. B. „Kurs“"
/>
<label class="field-label">Frage / Erklärtext (optional)</label>
<input
type="text"
v-model="question.label"
class="width-full"
placeholder="z. B. „Welchen Kurs möchtest du belegen?“"
/>
</div>
<label class="link remove-link" @click="removeQuestion(qIndex)">Frage entfernen</label>
</div>
@@ -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;
@@ -28,7 +28,10 @@ const next = () => emit('next', nextVisibleFrom(props.event, STEP_PARTICIPATION_
<table class="form-table">
<tr v-for="question in questions" :key="question.key">
<td>{{ question.label }}:</td>
<td>
{{ question.title || question.label }}:
<span v-if="question.title && question.label" class="option-hint">{{ question.label }}</span>
</td>
<td>
<select v-model="formData.participationOptions[question.key]">
<option value="">Bitte wählen</option>
@@ -46,3 +49,14 @@ const next = () => emit('next', nextVisibleFrom(props.event, STEP_PARTICIPATION_
</div>
</div>
</template>
<style scoped>
.option-hint {
display: block;
margin-top: 2px;
font-size: 0.8rem;
font-style: italic;
font-weight: 400;
color: #6b7280;
}
</style>
@@ -40,7 +40,7 @@ const selectedParticipationOptions = computed(() =>
(props.event.participationOptions ?? []).map(question => {
const value = props.formData.participationOptions[question.key]
const option = (question.options ?? []).find(o => o.value === value)
return {label: question.label, value: option?.label ?? ''}
return {label: question.title || question.label, value: option?.label ?? ''}
}).filter(entry => entry.value !== '')
)