Additional event informations can be added
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Event\Actions\SetParticipationOptions;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class SetParticipationOptionsCommand
|
||||||
|
{
|
||||||
|
public function __construct(private SetParticipationOptionsRequest $request)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(): SetParticipationOptionsResponse
|
||||||
|
{
|
||||||
|
$response = new SetParticipationOptionsResponse();
|
||||||
|
|
||||||
|
$this->request->event->participation_options = $this->normalize($this->request->questions);
|
||||||
|
$this->request->event->save();
|
||||||
|
|
||||||
|
$response->success = true;
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisiert die rohe Fragen-Definition: trimmt Labels, generiert stabile Keys/Values und verwirft
|
||||||
|
* unvollständige Fragen (kein Label oder keine gültige Option). Keys/Values sind innerhalb ihres
|
||||||
|
* Geltungsbereichs eindeutig, damit Antworten robust referenzieren können.
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $questions
|
||||||
|
* @return array<int, array{key: string, label: string, options: array<int, array{value: string, label: string}>}>
|
||||||
|
*/
|
||||||
|
private function normalize(array $questions): array
|
||||||
|
{
|
||||||
|
$normalized = [];
|
||||||
|
$usedKeys = [];
|
||||||
|
|
||||||
|
foreach ($questions as $question) {
|
||||||
|
$label = trim((string)($question['label'] ?? ''));
|
||||||
|
if ($label === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = $this->normalizeOptions($question['options'] ?? []);
|
||||||
|
if ($options === []) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $this->uniqueSlug((string)($question['key'] ?? ''), $label, $usedKeys);
|
||||||
|
$usedKeys[] = $key;
|
||||||
|
|
||||||
|
$normalized[] = [
|
||||||
|
'key' => $key,
|
||||||
|
'label' => $label,
|
||||||
|
'options' => $options,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<string, mixed>> $options
|
||||||
|
* @return array<int, array{value: string, label: string}>
|
||||||
|
*/
|
||||||
|
private function normalizeOptions(mixed $options): array
|
||||||
|
{
|
||||||
|
if (!is_array($options)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = [];
|
||||||
|
$usedValues = [];
|
||||||
|
|
||||||
|
foreach ($options as $option) {
|
||||||
|
$label = trim((string)($option['label'] ?? ''));
|
||||||
|
if ($label === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->uniqueSlug((string)($option['value'] ?? ''), $label, $usedValues);
|
||||||
|
$usedValues[] = $value;
|
||||||
|
|
||||||
|
$normalized[] = ['value' => $value, 'label' => $label];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert einen eindeutigen Slug: bevorzugt den bestehenden Wert (Bestandsschutz für schon gespeicherte
|
||||||
|
* Fragen/Antworten), sonst aus dem Label abgeleitet; Kollisionen werden durchnummeriert.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $used
|
||||||
|
*/
|
||||||
|
private function uniqueSlug(string $existing, string $label, array $used): string
|
||||||
|
{
|
||||||
|
$base = $existing !== '' ? $existing : Str::slug($label);
|
||||||
|
if ($base === '') {
|
||||||
|
$base = 'option';
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate = $base;
|
||||||
|
$suffix = 2;
|
||||||
|
while (in_array($candidate, $used, true)) {
|
||||||
|
$candidate = $base . '-' . $suffix;
|
||||||
|
$suffix++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Event\Actions\SetParticipationOptions;
|
||||||
|
|
||||||
|
use App\Models\Event;
|
||||||
|
|
||||||
|
class SetParticipationOptionsRequest
|
||||||
|
{
|
||||||
|
public Event $event;
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> Rohe Fragen-Definition aus dem Admin-Panel. */
|
||||||
|
public array $questions;
|
||||||
|
|
||||||
|
public function __construct(Event $event, array $questions)
|
||||||
|
{
|
||||||
|
$this->event = $event;
|
||||||
|
$this->questions = $questions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Event\Actions\SetParticipationOptions;
|
||||||
|
|
||||||
|
class SetParticipationOptionsResponse
|
||||||
|
{
|
||||||
|
public bool $success;
|
||||||
|
public ?string $message;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->success = false;
|
||||||
|
$this->message = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,14 @@ class SignUpCommand {
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Teilnahmeoptionen (event-spezifische Pflicht-Auswahl) gegen die Event-Definition absichern.
|
||||||
|
$participationOptionRows = $this->buildParticipationOptionRows();
|
||||||
|
if ($participationOptionRows === null) {
|
||||||
|
$response->success = false;
|
||||||
|
$response->message = 'Bitte alle Auswahlfelder ausfüllen.';
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
$eatingHabit = match ($this->request->eating_habit) {
|
$eatingHabit = match ($this->request->eating_habit) {
|
||||||
'vegan' => EatingHabit::EATING_HABIT_VEGAN,
|
'vegan' => EatingHabit::EATING_HABIT_VEGAN,
|
||||||
'vegetarian' => EatingHabit::EATING_HABIT_VEGETARIAN,
|
'vegetarian' => EatingHabit::EATING_HABIT_VEGETARIAN,
|
||||||
@@ -80,8 +88,68 @@ class SignUpCommand {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->persistParticipationOptions($response->participant, $participationOptionRows);
|
||||||
|
|
||||||
$response->success = true;
|
$response->success = true;
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validiert die Antworten gegen die am Event definierten Teilnahmeoptionen und erzeugt die zu speichernden
|
||||||
|
* Zeilen mit Label-Snapshots. Gibt null zurück, wenn eine Pflicht-Frage unbeantwortet oder eine Antwort
|
||||||
|
* ungültig ist (Absicherung gegen manipulierte Payloads).
|
||||||
|
*
|
||||||
|
* @return array<int, array{question_key: string, question_label: string, value: string, value_label: string}>|null
|
||||||
|
*/
|
||||||
|
private function buildParticipationOptionRows(): ?array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
foreach ($this->request->event->participation_options ?? [] as $question) {
|
||||||
|
$key = $question['key'] ?? null;
|
||||||
|
if ($key === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->request->participationOptions[$key] ?? null;
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$option = null;
|
||||||
|
foreach ($question['options'] ?? [] as $candidate) {
|
||||||
|
if (($candidate['value'] ?? null) === $value) {
|
||||||
|
$option = $candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($option === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'question_key' => $key,
|
||||||
|
'question_label' => $question['label'] ?? $key,
|
||||||
|
'value' => $value,
|
||||||
|
'value_label' => $option['label'] ?? $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{question_key: string, question_label: string, value: string, value_label: string}> $rows
|
||||||
|
*/
|
||||||
|
private function persistParticipationOptions(\App\Models\EventParticipant $participant, array $rows): void
|
||||||
|
{
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$participant->selectedOptions()->create(array_merge($row, [
|
||||||
|
'tenant' => $this->request->event->tenant,
|
||||||
|
'event_id' => $this->request->event->id,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class SignUpRequest {
|
|||||||
public Amount $amount,
|
public Amount $amount,
|
||||||
public ?string $paymentMethod,
|
public ?string $paymentMethod,
|
||||||
public array $paymentOptions = [],
|
public array $paymentOptions = [],
|
||||||
|
/** Antworten auf die Teilnahmeoptionen: [ question_key => value ]. */
|
||||||
|
public array $participationOptions = [],
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Domains\Event\Controllers;
|
|||||||
|
|
||||||
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesCommand;
|
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesCommand;
|
||||||
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesRequest;
|
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesRequest;
|
||||||
|
use App\Domains\Event\Actions\SetParticipationOptions\SetParticipationOptionsCommand;
|
||||||
|
use App\Domains\Event\Actions\SetParticipationOptions\SetParticipationOptionsRequest;
|
||||||
use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsCommand;
|
use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsCommand;
|
||||||
use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsRequest;
|
use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsRequest;
|
||||||
use App\Domains\Event\Actions\UpdateEvent\UpdateEventCommand;
|
use App\Domains\Event\Actions\UpdateEvent\UpdateEventCommand;
|
||||||
@@ -142,6 +144,17 @@ class DetailsController extends CommonController {
|
|||||||
return response()->json(['status' => $response->success ? 'success' : 'error']);
|
return response()->json(['status' => $response->success ? 'success' : 'error']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateParticipationOptions(int $eventId, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$event = $this->events->getById($eventId);
|
||||||
|
|
||||||
|
$setRequest = new SetParticipationOptionsRequest($event, (array)$request->input('questions', []));
|
||||||
|
$setCommand = new SetParticipationOptionsCommand($setRequest);
|
||||||
|
$response = $setCommand->execute();
|
||||||
|
|
||||||
|
return response()->json(['status' => $response->success ? 'success' : 'error', 'message' => $response->message]);
|
||||||
|
}
|
||||||
|
|
||||||
public function downloadPdfList(string $eventId, string $listType, Request $request): Response
|
public function downloadPdfList(string $eventId, string $listType, Request $request): Response
|
||||||
{
|
{
|
||||||
$event = $this->events->getByIdentifier($eventId);
|
$event = $this->events->getByIdentifier($eventId);
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ class SignupController extends CommonController {
|
|||||||
$amount,
|
$amount,
|
||||||
$registrationData['paymentMethod'] ?? null,
|
$registrationData['paymentMethod'] ?? null,
|
||||||
(array)($registrationData['paymentOptions'] ?? []),
|
(array)($registrationData['paymentOptions'] ?? []),
|
||||||
|
(array)($registrationData['participationOptions'] ?? []),
|
||||||
);
|
);
|
||||||
|
|
||||||
$signupCommand = new SignUpCommand($signupRequest);
|
$signupCommand = new SignUpCommand($signupRequest);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ Route::prefix('api/v1')
|
|||||||
Route::post('/participation-fees', [DetailsController::class, 'updateParticipationFees']);
|
Route::post('/participation-fees', [DetailsController::class, 'updateParticipationFees']);
|
||||||
Route::post('/common-settings', [DetailsController::class, 'updateCommonSettings']);
|
Route::post('/common-settings', [DetailsController::class, 'updateCommonSettings']);
|
||||||
Route::post('/payment-methods', [DetailsController::class, 'updatePaymentMethods']);
|
Route::post('/payment-methods', [DetailsController::class, 'updatePaymentMethods']);
|
||||||
|
Route::post('/participation-options', [DetailsController::class, 'updateParticipationOptions']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, reactive, ref} from "vue";
|
import {onMounted, reactive, ref} from "vue";
|
||||||
import ParticipationFees from "./ParticipationFees.vue";
|
import ParticipationFees from "./ParticipationFees.vue";
|
||||||
|
import ParticipationOptions from "./ParticipationOptions.vue";
|
||||||
import ParticipationSummary from "./ParticipationSummary.vue";
|
import ParticipationSummary from "./ParticipationSummary.vue";
|
||||||
import CommonSettings from "./CommonSettings.vue";
|
import CommonSettings from "./CommonSettings.vue";
|
||||||
import EventManagement from "./EventManagement.vue";
|
import EventManagement from "./EventManagement.vue";
|
||||||
@@ -36,6 +37,10 @@
|
|||||||
displayData.value = 'participationFees';
|
displayData.value = 'participationFees';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function showParticipationOptions() {
|
||||||
|
displayData.value = 'participationOptions';
|
||||||
|
}
|
||||||
|
|
||||||
async function showEventManagement() {
|
async function showEventManagement() {
|
||||||
displayData.value = 'eventManagement';
|
displayData.value = 'eventManagement';
|
||||||
}
|
}
|
||||||
@@ -107,6 +112,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<ParticipationFees v-if="displayData === 'participationFees'" :event="dynamicProps.event" @close="showMain" />
|
<ParticipationFees v-if="displayData === 'participationFees'" :event="dynamicProps.event" @close="showMain" />
|
||||||
|
<ParticipationOptions v-else-if="displayData === 'participationOptions'" :event="dynamicProps.event"
|
||||||
|
@close="showMain"/>
|
||||||
<CommonSettings v-else-if="displayData === 'commonSettings'" :event="dynamicProps.event" @close="showMain" />
|
<CommonSettings v-else-if="displayData === 'commonSettings'" :event="dynamicProps.event" @close="showMain" />
|
||||||
<EventManagement v-else-if="displayData === 'eventManagement'" :event="dynamicProps.event" @close="showMain" />
|
<EventManagement v-else-if="displayData === 'eventManagement'" :event="dynamicProps.event" @close="showMain" />
|
||||||
|
|
||||||
@@ -185,7 +192,8 @@
|
|||||||
<div class="event-flexbox-row bottom">
|
<div class="event-flexbox-row bottom">
|
||||||
<label style="font-size: 9pt;" class="link" @click="showCommonSettings">Allgemeine Einstellungen</label>
|
<label style="font-size: 9pt;" class="link" @click="showCommonSettings">Allgemeine Einstellungen</label>
|
||||||
<label style="font-size: 9pt;" class="link" @click="showEventManagement">Veranstaltungsleitung</label>
|
<label style="font-size: 9pt;" class="link" @click="showEventManagement">Veranstaltungsleitung</label>
|
||||||
<label style="font-size: 9pt;" class="link" @click="showParticipationFees">Teilnahmegebühren</label>
|
<label style="font-size: 9pt;" class="link" @click="showParticipationFees">Teilnahmegebühren</label>
|
||||||
|
<label style="font-size: 9pt;" class="link" @click="showParticipationOptions">Teilnahmeoptionen</label>
|
||||||
<a style="font-size: 9pt;" class="link" :href="'/budget/' + props.data.event.costUnit.id">Budget bearbeiten</a>
|
<a style="font-size: 9pt;" class="link" :href="'/budget/' + props.data.event.costUnit.id">Budget bearbeiten</a>
|
||||||
<a style="font-size: 9pt;" class="link" :href="'/cost-unit/' + props.data.event.costUnit.id">Ausgabenübersicht</a>
|
<a style="font-size: 9pt;" class="link" :href="'/cost-unit/' + props.data.event.costUnit.id">Ausgabenübersicht</a>
|
||||||
<a v-if="!dynamicProps.event.registrationAllowed && !dynamicProps.event.archived" style="color: #ff0000; font-size: 9pt;" class="link" @click="archiveEvent">Archivieren</a>
|
<a v-if="!dynamicProps.event.registrationAllowed && !dynamicProps.event.archived" style="color: #ff0000; font-size: 9pt;" class="link" @click="archiveEvent">Archivieren</a>
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {toast} from "vue3-toastify";
|
||||||
|
import {request} from "../../../../../resources/js/components/HttpClient.js";
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
event: Object,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fragen-Definition dieses Events: [ { key, label, options: [ { value, label } ] } ].
|
||||||
|
// key/value bleiben für bestehende Einträge erhalten (Bestandsschutz für bereits gespeicherte Antworten);
|
||||||
|
// neue Einträge bekommen serverseitig beim Speichern einen stabilen Slug.
|
||||||
|
const questions = ref((props.event.participationOptions ?? []).map(q => ({
|
||||||
|
key: q.key ?? '',
|
||||||
|
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: ''}]})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeQuestion(index) {
|
||||||
|
questions.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOption(question) {
|
||||||
|
question.options.push({value: '', label: ''})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeOption(question, index) {
|
||||||
|
question.options.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
for (const question of questions.value) {
|
||||||
|
if (question.label.trim() === '') {
|
||||||
|
toast.error('Bitte für jede Frage eine Beschriftung angeben.')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const validOptions = question.options.filter(o => o.label.trim() !== '')
|
||||||
|
if (validOptions.length < 1) {
|
||||||
|
toast.error('Jede Frage braucht mindestens eine Auswahlmöglichkeit.')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!validate()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request('/api/v1/event/details/' + props.event.id + '/participation-options', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
questions: questions.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status !== 'success') {
|
||||||
|
toast.error(response.message ?? 'Die Teilnahmeoptionen konnten nicht gespeichert werden.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Die Teilnahmeoptionen wurden gespeichert')
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="participation-options">
|
||||||
|
<h3>Teilnahmeoptionen</h3>
|
||||||
|
<p style="color: #6b7280; font-size: 0.9rem;">
|
||||||
|
Pflicht-Auswahlfragen für die Anmeldung (z. B. Kurs oder Stufe). Je Frage ist genau eine Option wählbar.
|
||||||
|
Ohne Fragen wird dieser Schritt in der Anmeldung übersprungen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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?“"
|
||||||
|
/>
|
||||||
|
<label class="link remove-link" @click="removeQuestion(qIndex)">Frage entfernen</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options">
|
||||||
|
<div v-for="(option, oIndex) in question.options" :key="oIndex" class="option-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="option.label"
|
||||||
|
class="width-full"
|
||||||
|
placeholder="Auswahlmöglichkeit"
|
||||||
|
/>
|
||||||
|
<label class="link remove-link" @click="removeOption(question, oIndex)">✕</label>
|
||||||
|
</div>
|
||||||
|
<label class="link" @click="addOption(question)">+ Option hinzufügen</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<label class="link" @click="addQuestion">+ Frage hinzufügen</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 24px;">
|
||||||
|
<input type="button" value="Speichern" @click="save"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.question-card {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 14px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,7 @@ import StepPersonalData from './steps/StepPersonalData.vue'
|
|||||||
import StepRegistrationMode from './steps/StepRegistrationMode.vue'
|
import StepRegistrationMode from './steps/StepRegistrationMode.vue'
|
||||||
import StepArrival from './steps/StepArrival.vue'
|
import StepArrival from './steps/StepArrival.vue'
|
||||||
import StepAddons from './steps/StepAddons.vue'
|
import StepAddons from './steps/StepAddons.vue'
|
||||||
|
import StepParticipationOptions from './steps/StepParticipationOptions.vue'
|
||||||
import StepPhotoPermissions from './steps/StepPhotoPermissions.vue'
|
import StepPhotoPermissions from './steps/StepPhotoPermissions.vue'
|
||||||
import StepAllergies from './steps/StepAllergies.vue'
|
import StepAllergies from './steps/StepAllergies.vue'
|
||||||
import StepPaymentMethod from './steps/StepPaymentMethod.vue'
|
import StepPaymentMethod from './steps/StepPaymentMethod.vue'
|
||||||
@@ -33,10 +34,11 @@ const steps = [
|
|||||||
{ step: 4, label: 'An-/Abreise' },
|
{ step: 4, label: 'An-/Abreise' },
|
||||||
{ step: 5, label: 'Teilnahmegruppe' },
|
{ step: 5, label: 'Teilnahmegruppe' },
|
||||||
{ step: 6, label: 'Zusatzoptionen' },
|
{ step: 6, label: 'Zusatzoptionen' },
|
||||||
{ step: 7, label: 'Fotoerlaubnis' },
|
{step: 7, label: 'Teilnahmeoptionen'},
|
||||||
{ step: 8, label: 'Allergien' },
|
{step: 8, label: 'Fotoerlaubnis'},
|
||||||
{step: 9, label: 'Zahlungsart'},
|
{step: 9, label: 'Allergien'},
|
||||||
{step: 10, label: 'Zusammenfassung'},
|
{step: 10, label: 'Zahlungsart'},
|
||||||
|
{step: 11, label: 'Zusammenfassung'},
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -93,12 +95,16 @@ const steps = [
|
|||||||
<StepArrival v-if="currentStep === 4" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
|
<StepArrival v-if="currentStep === 4" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
|
||||||
<StepRegistrationMode v-if="currentStep === 5" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
|
<StepRegistrationMode v-if="currentStep === 5" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
|
||||||
<StepAddons v-if="currentStep === 6" :formData="formData" :event="event" :selectedAddons="selectedAddons" @next="goToStep" @back="goToStep" />
|
<StepAddons v-if="currentStep === 6" :formData="formData" :event="event" :selectedAddons="selectedAddons" @next="goToStep" @back="goToStep" />
|
||||||
<StepPhotoPermissions v-if="currentStep === 7" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
|
<StepParticipationOptions v-if="currentStep === 7" :formData="formData" :event="event" @next="goToStep"
|
||||||
<StepAllergies v-if="currentStep === 8" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
|
@back="goToStep"/>
|
||||||
<StepPaymentMethod v-if="currentStep === 9" :formData="formData" :event="event" @next="goToStep"
|
<StepPhotoPermissions v-if="currentStep === 8" :formData="formData" :event="event" @next="goToStep"
|
||||||
|
@back="goToStep"/>
|
||||||
|
<StepAllergies v-if="currentStep === 9" :formData="formData" :event="event" @next="goToStep"
|
||||||
|
@back="goToStep"/>
|
||||||
|
<StepPaymentMethod v-if="currentStep === 10" :formData="formData" :event="event" @next="goToStep"
|
||||||
@back="goToStep"/>
|
@back="goToStep"/>
|
||||||
<StepSummary
|
<StepSummary
|
||||||
v-if="currentStep === 10"
|
v-if="currentStep === 11"
|
||||||
:formData="formData"
|
:formData="formData"
|
||||||
:event="event"
|
:event="event"
|
||||||
:summaryAmount="summaryAmount"
|
:summaryAmount="summaryAmount"
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// Zentrale Schrittreihenfolge des Anmelde-Wizards. Einige Schritte sind optional und werden
|
||||||
|
// übersprungen, wenn sie für das Event nicht zutreffen (Zusatzoptionen, Teilnahmeoptionen).
|
||||||
|
// Der Zahlungsschritt wird separat in useSignupForm anhand des berechneten Betrags übersprungen.
|
||||||
|
export const STEP_AGE = 1
|
||||||
|
export const STEP_CONTACT = 2
|
||||||
|
export const STEP_PERSONAL = 3
|
||||||
|
export const STEP_ARRIVAL = 4
|
||||||
|
export const STEP_REGISTRATION = 5
|
||||||
|
export const STEP_ADDONS = 6
|
||||||
|
export const STEP_PARTICIPATION_OPTIONS = 7
|
||||||
|
export const STEP_PHOTO = 8
|
||||||
|
export const STEP_ALLERGIES = 9
|
||||||
|
export const STEP_PAYMENT = 10
|
||||||
|
export const STEP_SUMMARY = 11
|
||||||
|
|
||||||
|
// Ob ein (optionaler) Schritt für dieses Event angezeigt wird.
|
||||||
|
export function stepVisible(event, step) {
|
||||||
|
if (step === STEP_ADDONS) {
|
||||||
|
return (event.addons?.length ?? 0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === STEP_PARTICIPATION_OPTIONS) {
|
||||||
|
return (event.participationOptions?.length ?? 0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nächster sichtbarer Schritt nach `step` (überspringt ausgeblendete optionale Schritte).
|
||||||
|
export function nextVisibleFrom(event, step) {
|
||||||
|
let candidate = step + 1
|
||||||
|
while (candidate < STEP_SUMMARY && !stepVisible(event, candidate)) {
|
||||||
|
candidate++
|
||||||
|
}
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vorheriger sichtbarer Schritt vor `step`.
|
||||||
|
export function prevVisibleFrom(event, step) {
|
||||||
|
let candidate = step - 1
|
||||||
|
while (candidate > STEP_AGE && !stepVisible(event, candidate)) {
|
||||||
|
candidate--
|
||||||
|
}
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import {reactive, ref} from 'vue'
|
import {reactive, ref} from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import {STEP_PAYMENT, STEP_SUMMARY} from './stepFlow.js'
|
||||||
|
|
||||||
export function useSignupForm(event, participantData) {
|
export function useSignupForm(event, participantData) {
|
||||||
const currentStep = ref(1)
|
const currentStep = ref(1)
|
||||||
@@ -16,6 +17,8 @@ export function useSignupForm(event, participantData) {
|
|||||||
eatingHabit: 'EATING_HABIT_VEGAN',
|
eatingHabit: 'EATING_HABIT_VEGAN',
|
||||||
paymentMethod: activeMethods.length === 1 ? activeMethods[0].slug : null,
|
paymentMethod: activeMethods.length === 1 ? activeMethods[0].slug : null,
|
||||||
paymentOptions: {},
|
paymentOptions: {},
|
||||||
|
// Antworten auf die event-spezifischen Teilnahmeoptionen: { [question.key]: option.value }
|
||||||
|
participationOptions: {},
|
||||||
userId: participantData.id,
|
userId: participantData.id,
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
vorname: participantData.firstname ?? '',
|
vorname: participantData.firstname ?? '',
|
||||||
@@ -79,10 +82,6 @@ export function useSignupForm(event, participantData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steps: ... 8 Allergien, 9 Zahlungsart, 10 Zusammenfassung.
|
|
||||||
const STEP_PAYMENT = 9
|
|
||||||
const STEP_SUMMARY = 10
|
|
||||||
|
|
||||||
const goToStep = async (step) => {
|
const goToStep = async (step) => {
|
||||||
if (step === STEP_PAYMENT) {
|
if (step === STEP_PAYMENT) {
|
||||||
// Betrag ermitteln: Bei 0 € wird die Zahlungsart-Auswahl übersprungen.
|
// Betrag ermitteln: Bei 0 € wird die Zahlungsart-Auswahl übersprungen.
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import {nextVisibleFrom, STEP_ADDONS} from "../composables/stepFlow.js";
|
||||||
|
|
||||||
const props = defineProps({ formData: Object, event: Object, selectedAddons: Object })
|
const props = defineProps({ formData: Object, event: Object, selectedAddons: Object })
|
||||||
const emit = defineEmits(['next', 'back'])
|
const emit = defineEmits(['next', 'back'])
|
||||||
|
|
||||||
|
const next = () => emit('next', nextVisibleFrom(props.event, STEP_ADDONS))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -39,7 +43,7 @@ const emit = defineEmits(['next', 'back'])
|
|||||||
|
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button type="button" class="btn-secondary" @click="emit('back', 5)">← Zurück</button>
|
<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>
|
<button type="button" class="btn-primary" @click="next">Weiter →</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ const emit = defineEmits(['next', 'back'])
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="btn-row">
|
<td colspan="2" class="btn-row">
|
||||||
<button type="button" class="btn-secondary" @click="emit('back', 7)">← Zurück</button>
|
<button type="button" class="btn-secondary" @click="emit('back', 8)">← Zurück</button>
|
||||||
<button type="button" class="btn-primary" @click="emit('next', 9)">Weiter →</button>
|
<button type="button" class="btn-primary" @click="emit('next', 10)">Weiter →</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {nextVisibleFrom, prevVisibleFrom, STEP_PARTICIPATION_OPTIONS} from "../composables/stepFlow.js";
|
||||||
|
|
||||||
|
const props = defineProps({formData: Object, event: Object})
|
||||||
|
const emit = defineEmits(['next', 'back'])
|
||||||
|
|
||||||
|
const questions = computed(() => props.event.participationOptions ?? [])
|
||||||
|
|
||||||
|
// Pflichtfeld: "Weiter" erst möglich, wenn jede Frage beantwortet ist.
|
||||||
|
const canProceed = computed(() =>
|
||||||
|
questions.value.every(q => {
|
||||||
|
const value = props.formData.participationOptions[q.key]
|
||||||
|
return value !== undefined && value !== null && String(value).trim() !== ''
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const back = () => emit('back', prevVisibleFrom(props.event, STEP_PARTICIPATION_OPTIONS))
|
||||||
|
const next = () => emit('next', nextVisibleFrom(props.event, STEP_PARTICIPATION_OPTIONS))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3>Teilnahmeoptionen</h3>
|
||||||
|
<p style="color: #6b7280; font-size: 0.95rem; margin-bottom: 20px;">
|
||||||
|
Bitte triff für jede Auswahl eine Entscheidung.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr v-for="question in questions" :key="question.key">
|
||||||
|
<td>{{ question.label }}:</td>
|
||||||
|
<td>
|
||||||
|
<select v-model="formData.participationOptions[question.key]">
|
||||||
|
<option value="">Bitte wählen</option>
|
||||||
|
<option v-for="option in question.options" :key="option.value" :value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="btn-row">
|
||||||
|
<button type="button" class="btn-secondary" @click="back">← Zurück</button>
|
||||||
|
<button type="button" class="btn-primary" :disabled="!canProceed" @click="next">Weiter →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -78,8 +78,8 @@ const canProceed = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button type="button" class="btn-secondary" @click="emit('back', 8)">← Zurück</button>
|
<button type="button" class="btn-secondary" @click="emit('back', 9)">← Zurück</button>
|
||||||
<button type="button" class="btn-primary" :disabled="!canProceed" @click="emit('next', 10)">Weiter →
|
<button type="button" class="btn-primary" :disabled="!canProceed" @click="emit('next', 11)">Weiter →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import {prevVisibleFrom, STEP_ALLERGIES, STEP_PHOTO} from "../composables/stepFlow.js";
|
||||||
|
|
||||||
const props = defineProps({ formData: Object, event: Object })
|
const props = defineProps({ formData: Object, event: Object })
|
||||||
const emit = defineEmits(['next', 'back'])
|
const emit = defineEmits(['next', 'back'])
|
||||||
|
|
||||||
const acceptAll = () => {
|
const acceptAll = () => {
|
||||||
Object.keys(props.formData.foto).forEach(k => props.formData.foto[k] = true)
|
Object.keys(props.formData.foto).forEach(k => props.formData.foto[k] = true)
|
||||||
emit('next', 8)
|
emit('next', STEP_ALLERGIES)
|
||||||
}
|
}
|
||||||
|
|
||||||
const back = () => {
|
const back = () => emit('back', prevVisibleFrom(props.event, STEP_PHOTO))
|
||||||
const hasAddons = (props.event.addons?.length > 0) || props.event.solidarityPayment
|
|
||||||
emit('back', hasAddons ? 6 : 5)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -27,7 +26,7 @@ const back = () => {
|
|||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button type="button" class="btn-secondary" @click="back">← Zurück</button>
|
<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" style="background: #059669;" @click="acceptAll">Alle akzeptieren & weiter</button>
|
||||||
<button type="button" class="btn-primary" @click="emit('next', 8)">Weiter →</button>
|
<button type="button" class="btn-primary" @click="emit('next', 9)">Weiter →</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {watch} from "vue";
|
import {watch} from "vue";
|
||||||
|
import {nextVisibleFrom, STEP_REGISTRATION} from "../composables/stepFlow.js";
|
||||||
|
|
||||||
const props = defineProps({ formData: Object, event: Object })
|
const props = defineProps({ formData: Object, event: Object })
|
||||||
const emit = defineEmits(['next', 'back'])
|
const emit = defineEmits(['next', 'back'])
|
||||||
@@ -17,8 +18,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const nextStep = () => {
|
const nextStep = () => {
|
||||||
const hasAddons = (props.event.addons?.length ?? 0) > 0
|
emit('next', nextVisibleFrom(props.event, STEP_REGISTRATION))
|
||||||
emit('next', hasAddons ? 6 : 7)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,17 @@ const paymentConfirmationText = computed(() => {
|
|||||||
return template.replaceAll('{amount}', props.summaryAmount ?? '')
|
return template.replaceAll('{amount}', props.summaryAmount ?? '')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Zurück führt zur Zahlungsart (9), sofern dieser Schritt gezeigt wurde; bei 0 € direkt zu Allergien (8).
|
// Zurück führt zur Zahlungsart (10), sofern dieser Schritt gezeigt wurde; bei 0 € direkt zu Allergien (9).
|
||||||
const backTarget = computed(() => props.summaryAmountValue > 0 ? 9 : 8)
|
const backTarget = computed(() => props.summaryAmountValue > 0 ? 10 : 9)
|
||||||
|
|
||||||
|
// Gewählte Teilnahmeoptionen für die Zusammenfassung (label statt value anzeigen).
|
||||||
|
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 ?? ''}
|
||||||
|
}).filter(entry => entry.value !== '')
|
||||||
|
)
|
||||||
|
|
||||||
const canSubmit = computed(() =>
|
const canSubmit = computed(() =>
|
||||||
props.formData.summary_information_correct
|
props.formData.summary_information_correct
|
||||||
@@ -115,6 +124,11 @@ function eatingHabit() {
|
|||||||
<td>{{ participationGroup() }}</td>
|
<td>{{ participationGroup() }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr v-for="option in selectedParticipationOptions" :key="option.label">
|
||||||
|
<td>{{ option.label }}:</td>
|
||||||
|
<td>{{ option.value }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Foto-Erlaubnis:</td>
|
<td>Foto-Erlaubnis:</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class Event extends InstancedModel
|
|||||||
'vat_pricing_mode',
|
'vat_pricing_mode',
|
||||||
'tax_exemption_reason',
|
'tax_exemption_reason',
|
||||||
'tax_exemption_note',
|
'tax_exemption_note',
|
||||||
|
'participation_options',
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -114,6 +115,8 @@ class Event extends InstancedModel
|
|||||||
|
|
||||||
'tax_liable' => 'boolean',
|
'tax_liable' => 'boolean',
|
||||||
'vat_rate' => 'integer',
|
'vat_rate' => 'integer',
|
||||||
|
|
||||||
|
'participation_options' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function tenant(): BelongsTo
|
public function tenant(): BelongsTo
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\EventPaymentModules\DTO\RegistrationSummaryResponse;
|
|||||||
use App\EventPaymentModules\EventPaymentModule;
|
use App\EventPaymentModules\EventPaymentModule;
|
||||||
use App\EventPaymentModules\EventPaymentModuleRegistry;
|
use App\EventPaymentModules\EventPaymentModuleRegistry;
|
||||||
use App\Scopes\InstancedModel;
|
use App\Scopes\InstancedModel;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
|
||||||
class EventParticipant extends InstancedModel
|
class EventParticipant extends InstancedModel
|
||||||
@@ -103,6 +104,12 @@ class EventParticipant extends InstancedModel
|
|||||||
return $this->belongsTo(Event::class);
|
return $this->belongsTo(Event::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gewählte Teilnahmeoptionen (event-spezifische Pflicht-Auswahl) dieser Anmeldung. */
|
||||||
|
public function selectedOptions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(EventParticipantOption::class, 'event_participant_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function user()
|
public function user()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Scopes\InstancedModel;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class EventParticipantOption extends InstancedModel
|
||||||
|
{
|
||||||
|
protected $table = 'event_participant_options';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant',
|
||||||
|
'event_id',
|
||||||
|
'event_participant_id',
|
||||||
|
'question_key',
|
||||||
|
'question_label',
|
||||||
|
'value',
|
||||||
|
'value_label',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function event(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function participant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(EventParticipant::class, 'event_participant_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Resources;
|
||||||
|
|
||||||
|
use App\Models\EventParticipantOption;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class EventParticipantOptionResource extends JsonResource
|
||||||
|
{
|
||||||
|
function __construct(EventParticipantOption $option)
|
||||||
|
{
|
||||||
|
parent::__construct($option);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray($request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'questionKey' => $this->resource->question_key,
|
||||||
|
'questionLabel' => $this->resource->question_label,
|
||||||
|
'value' => $this->resource->value,
|
||||||
|
'valueLabel' => $this->resource->value_label,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,6 +96,8 @@ class EventParticipantResource extends JsonResource
|
|||||||
'eventName' => $this->resource->event()->first()->name,
|
'eventName' => $this->resource->event()->first()->name,
|
||||||
'arrivalDateReadable' => $this->resource->arrival_date->format('d.m.Y'),
|
'arrivalDateReadable' => $this->resource->arrival_date->format('d.m.Y'),
|
||||||
'departureDateReadable' => $this->resource->departure_date->format('d.m.Y'),
|
'departureDateReadable' => $this->resource->departure_date->format('d.m.Y'),
|
||||||
|
'participationOptions' => $this->resource->selectedOptions()->get()->map(
|
||||||
|
fn($option) => new EventParticipantOptionResource($option)->toArray($request))->toArray(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ class EventResource extends JsonResource{
|
|||||||
$returnArray['paymentMethods'] = $this->event->paymentMethods()->get()->map(
|
$returnArray['paymentMethods'] = $this->event->paymentMethods()->get()->map(
|
||||||
fn($paymentMethod) => new AvailablePaymentMethodResource($paymentMethod)->toArray($request))->toArray();
|
fn($paymentMethod) => new AvailablePaymentMethodResource($paymentMethod)->toArray($request))->toArray();
|
||||||
|
|
||||||
|
// Veranstaltungsspezifische Pflicht-Auswahlfragen (Teilnahmeoptionen); leeres Array => Schritt entfällt.
|
||||||
|
$returnArray['participationOptions'] = $this->event->participation_options ?? [];
|
||||||
|
|
||||||
|
|
||||||
$returnArray['participationTypes'] = [];
|
$returnArray['participationTypes'] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('events', function (Blueprint $table) {
|
||||||
|
// Veranstaltungsspezifische Pflicht-Auswahlfragen (Teilnahmeoptionen), je Frage genau eine Option.
|
||||||
|
// Struktur: [ { key, label, options: [ { value, label } ] } ]
|
||||||
|
$table->json('participation_options')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('events', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('participation_options');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Gewählte Teilnahmeoptionen je Anmeldung -- eine Zeile pro beantworteter Frage.
|
||||||
|
// Frage-/Options-Label werden als Snapshot mitgespeichert, damit spätere Änderungen der
|
||||||
|
// Fragen-Definition am Event bestehende Anmeldungen/Auswertungen nicht verfälschen.
|
||||||
|
Schema::create('event_participant_options', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant');
|
||||||
|
|
||||||
|
$table->foreignId('event_id')->constrained('events', 'id')->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
|
$table->foreignId('event_participant_id')->constrained('event_participants', 'id')->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
|
|
||||||
|
$table->string('question_key');
|
||||||
|
$table->string('question_label');
|
||||||
|
$table->string('value');
|
||||||
|
$table->string('value_label');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('tenant')->references('slug')->on('tenants')->restrictOnDelete()->cascadeOnUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('event_participant_options');
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user