Implemented additional event information
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import {reactive, inject, onMounted} from 'vue';
|
||||
import {onMounted} from 'vue';
|
||||
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
|
||||
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
|
||||
import ParticipantsList from "./Partials/ParticipantsList.vue";
|
||||
import ParticipantsByOption from "./Partials/ParticipantsByOption.vue";
|
||||
import Overview from "./Partials/Overview.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -31,6 +32,11 @@ const tabs = [
|
||||
component: ParticipantsList,
|
||||
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/by-participation-group',
|
||||
},
|
||||
{
|
||||
title: 'Teilis nach Teilnahmeoption',
|
||||
component: ParticipantsByOption,
|
||||
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/by-participation-option',
|
||||
},
|
||||
{
|
||||
title: 'Abgemeldete Teilis',
|
||||
component: ParticipantsList,
|
||||
|
||||
@@ -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 !== '')
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user