Kurzanmeldungen
This commit is contained in:
+2
-1
@@ -12,7 +12,8 @@ class CertificateOfConductionCheckCommand {
|
||||
public function execute() : CertificateOfConductionCheckResponse {
|
||||
$response = new CertificateOfConductionCheckResponse();
|
||||
|
||||
$localGroup = str_replace('Stamm ', '', $this->request->participant->localGroup()->first()->name);
|
||||
// Der Stamm ist optional (die Kurzanmeldung erhebt ihn nicht, in der Verwaltung kann er entfernt werden).
|
||||
$localGroup = str_replace('Stamm ', '', $this->request->participant->localGroup()->first()?->name ?? '');
|
||||
|
||||
$apiResponse = Http::acceptJson()
|
||||
->asJson()
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\ShortSignUp;
|
||||
|
||||
use App\Domains\Event\Actions\SignUp\SignUpCommand;
|
||||
use App\Domains\Event\Actions\SignUp\SignUpRequest;
|
||||
use App\ValueObjects\Age;
|
||||
|
||||
/**
|
||||
* Verkürzte Anmeldung: ergänzt die nicht erhobenen Pflichtangaben um Platzhalter und übergibt an die reguläre
|
||||
* Anmelde-Action. Dadurch gelten für Kurzanmeldungen dieselben Regeln wie für jede andere Anmeldung
|
||||
* (Transaktion, laufende Rechnungsnummer, eFZ-Vorbelegung, Snapshot der Teilnahmeoptionen).
|
||||
*/
|
||||
class ShortSignUpCommand {
|
||||
/** Platzhalter für Angaben, die die Kurzanmeldung nicht erhebt. */
|
||||
private const string PLACEHOLDER = 'Nicht erforderlich';
|
||||
|
||||
/**
|
||||
* Die Postleitzahl bekommt bewusst keinen Text-Platzhalter: aus ihr wird in Teilnehmerliste und CSV-Export
|
||||
* das Bundesland abgeleitet.
|
||||
*/
|
||||
private const string PLACEHOLDER_POSTCODE = '00000';
|
||||
|
||||
public function __construct(public ShortSignUpRequest $request) {
|
||||
}
|
||||
|
||||
public function execute() : ShortSignUpResponse {
|
||||
$response = new ShortSignUpResponse();
|
||||
|
||||
// Ohne konfigurierte Teilnahmegruppe gäbe es keinen gültigen `participation_type` -- sauber abbrechen,
|
||||
// statt in einen Fremdschlüssel-Fehler zu laufen.
|
||||
$participationType = $this->request->event->firstParticipationTypeSlug();
|
||||
if ($participationType === null) {
|
||||
$response->message = 'Für diese Veranstaltung ist noch keine Teilnahmegruppe hinterlegt.';
|
||||
return $response;
|
||||
}
|
||||
|
||||
$participantAge = new Age($this->request->birthday);
|
||||
|
||||
// Bei Minderjährigen ist die Kontaktperson Pflicht; der Client prüft das ebenfalls, verlassen wird sich
|
||||
// darauf nicht.
|
||||
if (!$participantAge->isfullAged()
|
||||
&& ($this->request->contactPerson === null || $this->request->contactEmail === null)) {
|
||||
$response->message = 'Für Minderjährige werden Name und E-Mail-Adresse einer Kontaktperson benötigt.';
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Stamm: bei genau einem teilnehmenden Stamm wird dieser gesetzt, bei mehreren muss gewählt werden,
|
||||
// ohne teilnehmende Stämme bleibt die Zuordnung leer.
|
||||
$localGroups = $this->request->event->localGroups;
|
||||
$localGroup = null;
|
||||
|
||||
if ($localGroups->count() === 1) {
|
||||
$localGroup = $localGroups->first();
|
||||
} elseif ($localGroups->count() > 1) {
|
||||
$localGroup = $localGroups->firstWhere('id', $this->request->localGroupId);
|
||||
|
||||
if ($localGroup === null) {
|
||||
$response->message = 'Bitte einen Stamm auswählen.';
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
$eventResource = $this->request->event->toResource();
|
||||
|
||||
// Zeitraum, Gruppe und Beitragsstufe stehen bei der Kurzanmeldung fest -- es gibt nichts zu wählen.
|
||||
$amount = $eventResource->calculateAmount(
|
||||
$participationType,
|
||||
'standard',
|
||||
$this->request->event->start_date,
|
||||
$this->request->event->end_date,
|
||||
false
|
||||
);
|
||||
|
||||
$signUpRequest = new SignUpRequest(
|
||||
event: $this->request->event,
|
||||
user_id: $this->request->user_id,
|
||||
firstname: $this->request->firstname,
|
||||
lastname: $this->request->lastname,
|
||||
nickname: $this->request->nickname,
|
||||
participationType: $participationType,
|
||||
localGroup: $localGroup,
|
||||
birthday: $this->request->birthday,
|
||||
address_1: self::PLACEHOLDER,
|
||||
address_2: null,
|
||||
postcode: self::PLACEHOLDER_POSTCODE,
|
||||
city: self::PLACEHOLDER,
|
||||
email_1: $this->request->email,
|
||||
phone_1: $this->request->phone,
|
||||
email_2: $this->request->contactEmail,
|
||||
phone_2: null,
|
||||
contact_person: $this->request->contactPerson,
|
||||
allergies: $this->request->allergies,
|
||||
intolerances: $this->request->intolerances,
|
||||
medications: null,
|
||||
tetanus_vaccination: null,
|
||||
// Kein Wert der Zuordnung -- der SignUpCommand fällt damit auf "Omnivor" zurück.
|
||||
eating_habit: '',
|
||||
swimming_permission: $this->resolveSwimmingPermission($participantAge),
|
||||
first_aid_permission: $participantAge->isfullAged() ? '-1' : ($this->request->firstAidPermission ?? '-1'),
|
||||
foto_socialmedia: $this->request->foto_socialmedia,
|
||||
foto_print: $this->request->foto_print,
|
||||
foto_webseite: $this->request->foto_webseite,
|
||||
foto_partner: $this->request->foto_partner,
|
||||
foto_intern: $this->request->foto_intern,
|
||||
arrival: $this->request->event->start_date,
|
||||
departure: $this->request->event->end_date,
|
||||
arrival_eating: 1,
|
||||
departure_eating: 2,
|
||||
notes: null,
|
||||
amount: $amount,
|
||||
paymentMethod: $this->request->paymentMethod,
|
||||
paymentOptions: $this->request->paymentOptions,
|
||||
participationOptions: $this->request->participationOptions,
|
||||
addonItems: [],
|
||||
feeType: 'standard',
|
||||
siblingReduction: false,
|
||||
);
|
||||
|
||||
$signUpResponse = new SignUpCommand($signUpRequest)->execute();
|
||||
|
||||
$response->success = $signUpResponse->success;
|
||||
$response->participant = $signUpResponse->participant;
|
||||
$response->message = $signUpResponse->message;
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bei Volljährigen und wenn die Veranstaltung die Badeerlaubnis nicht abfragt, entscheidet der SignUpCommand
|
||||
* (volljährig => erteilt, nicht abgefragt => keine). Nur die getroffene Auswahl Minderjähriger wird
|
||||
* durchgereicht.
|
||||
*/
|
||||
private function resolveSwimmingPermission(Age $participantAge) : ?string
|
||||
{
|
||||
if ($participantAge->isfullAged() || !$this->request->event->swimming_permission_required) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->request->swimmingPermission;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\ShortSignUp;
|
||||
|
||||
use App\Models\Event;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Eingabedaten der verkürzten Anmeldung -- ausschließlich das, was der Kurz-Wizard tatsächlich erhebt.
|
||||
* Alle übrigen Pflichtangaben des Teilnehmers setzt der Command auf Platzhalter.
|
||||
*/
|
||||
class ShortSignUpRequest {
|
||||
function __construct(
|
||||
public Event $event,
|
||||
public ?int $user_id,
|
||||
public string $firstname,
|
||||
public string $lastname,
|
||||
public ?string $nickname,
|
||||
public DateTime $birthday,
|
||||
public string $email,
|
||||
public string $phone,
|
||||
/**
|
||||
* Tenant-ID des Stamms. Nur gefüllt, wenn die Veranstaltung mehrere teilnehmende Stämme hat --
|
||||
* bei genau einem wird dieser gesetzt, ohne zu fragen.
|
||||
*/
|
||||
public ?int $localGroupId,
|
||||
/** Kontaktperson: nur bei Minderjährigen erhoben. */
|
||||
public ?string $contactPerson,
|
||||
public ?string $contactEmail,
|
||||
public ?string $allergies,
|
||||
public ?string $intolerances,
|
||||
/** Slug aus `first_aid_permissions`; nur bei Minderjährigen erhoben. */
|
||||
public ?string $firstAidPermission,
|
||||
/** Slug aus `swimming_permissions`; nur bei Minderjährigen und nur wenn die Veranstaltung sie abfragt. */
|
||||
public ?string $swimmingPermission,
|
||||
public bool $foto_socialmedia,
|
||||
public bool $foto_print,
|
||||
public bool $foto_webseite,
|
||||
public bool $foto_partner,
|
||||
public bool $foto_intern,
|
||||
public ?string $paymentMethod,
|
||||
public array $paymentOptions = [],
|
||||
/** Antworten auf die Teilnahmeoptionen: [ question_key => value ]. */
|
||||
public array $participationOptions = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\ShortSignUp;
|
||||
|
||||
use App\Models\EventParticipant;
|
||||
|
||||
class ShortSignUpResponse {
|
||||
public bool $success;
|
||||
public ?EventParticipant $participant;
|
||||
public ?string $message = null;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
$this->participant = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Domains\Event\Actions\SignUp;
|
||||
|
||||
use App\Enumerations\EatingHabit;
|
||||
use App\Enumerations\EfzStatus;
|
||||
use App\Enumerations\SwimmingPermission;
|
||||
use App\EventPaymentModules\EventPaymentModuleRegistry;
|
||||
use App\ValueObjects\Age;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -59,7 +60,7 @@ class SignUpCommand {
|
||||
'participation_type' => $this->request->participationType,
|
||||
'fee_type' => $this->request->feeType,
|
||||
'sibling_reduction' => $this->request->siblingReduction,
|
||||
'local_group' => $this->request->localGroup->slug,
|
||||
'local_group' => $this->request->localGroup?->slug,
|
||||
'birthday' => $this->request->birthday,
|
||||
'address_1' => $this->request->address_1,
|
||||
'address_2' => $this->request->address_2,
|
||||
@@ -75,9 +76,7 @@ class SignUpCommand {
|
||||
'medications' => $this->request->medications,
|
||||
'tetanus_vaccination' => $this->request->tetanus_vaccination,
|
||||
'eating_habit' => $eatingHabit,
|
||||
'swimming_permission' => ($participantAge->isfullAged() || $this->request->swimming_permission === '-1')
|
||||
? 'SWIMMING_PERMISSION_ALLOWED'
|
||||
: $this->request->swimming_permission,
|
||||
'swimming_permission' => $this->resolveSwimmingPermission($participantAge),
|
||||
'first_aid_permission' => ($participantAge->isfullAged() || $this->request->first_aid_permission === '-1')
|
||||
? 'FIRST_AID_PERMISSION_ALLOWED'
|
||||
: $this->request->first_aid_permission,
|
||||
@@ -109,6 +108,26 @@ class SignUpCommand {
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Badeerlaubnis auflösen. Volljährige brauchen keine Erlaubnis. Für Minderjährige gilt die getroffene Auswahl;
|
||||
* fehlt sie -- weil die Veranstaltung die Badeerlaubnis gar nicht abfragt oder keine Wahl getroffen wurde --,
|
||||
* wird bewusst "Keine" hinterlegt und nicht etwa stillschweigend eine Erlaubnis angenommen.
|
||||
*/
|
||||
private function resolveSwimmingPermission(Age $participantAge): string
|
||||
{
|
||||
if ($participantAge->isfullAged()) {
|
||||
return SwimmingPermission::SWIMMING_PERMISSION_ALLOWED;
|
||||
}
|
||||
|
||||
if (!$this->request->event->swimming_permission_required
|
||||
|| $this->request->swimming_permission === null
|
||||
|| $this->request->swimming_permission === '-1') {
|
||||
return SwimmingPermission::SWIMMING_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
return $this->request->swimming_permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nächste laufende Nummer des Teilis innerhalb der Veranstaltung -- der letzte Block der
|
||||
* Rechnungsnummer. Die Event-Zeile wird gesperrt, damit gleichzeitige Anmeldungen derselben
|
||||
|
||||
@@ -15,7 +15,8 @@ class SignUpRequest {
|
||||
public string $lastname,
|
||||
public ?string $nickname,
|
||||
public string $participationType,
|
||||
public Tenant $localGroup,
|
||||
/** Stamm des Teilnehmers; in der Kurzanmeldung nicht erhoben und deshalb optional. */
|
||||
public ?Tenant $localGroup,
|
||||
public DateTime $birthday,
|
||||
public string $address_1,
|
||||
public ?string $address_2,
|
||||
|
||||
@@ -22,6 +22,8 @@ class UpdateEventCommand {
|
||||
$this->request->event->support_flat = $this->request->flatSupport;
|
||||
$this->request->event->send_weekly_report = $this->request->sendWeeklyReports;
|
||||
$this->request->event->registration_allowed = $this->request->registrationAllowed;
|
||||
$this->request->event->short_registration = $this->request->shortRegistration;
|
||||
$this->request->event->swimming_permission_required = $this->request->swimmingPermissionRequired;
|
||||
$this->request->event->save();
|
||||
|
||||
$this->request->event->resetAllowedEatingHabits();
|
||||
|
||||
@@ -17,12 +17,14 @@ class UpdateEventRequest {
|
||||
public int $alcoholicsAge;
|
||||
public bool $sendWeeklyReports;
|
||||
public bool $registrationAllowed;
|
||||
public bool $shortRegistration;
|
||||
public bool $swimmingPermissionRequired;
|
||||
public Amount $flatSupport;
|
||||
public Amount $supportPerPerson;
|
||||
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)
|
||||
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)
|
||||
{
|
||||
$this->event = $event;
|
||||
$this->eventName = $eventName;
|
||||
@@ -34,6 +36,8 @@ class UpdateEventRequest {
|
||||
$this->alcoholicsAge = $alcoholicsAge;
|
||||
$this->sendWeeklyReports = $sendWeeklyReports;
|
||||
$this->registrationAllowed = $registrationAllowed;
|
||||
$this->shortRegistration = $shortRegistration;
|
||||
$this->swimmingPermissionRequired = $swimmingPermissionRequired;
|
||||
$this->flatSupport = $flatSupport;
|
||||
$this->supportPerPerson = $supportPerPerson;
|
||||
$this->contributingLocalGroups = $contributingLocalGroups;
|
||||
|
||||
@@ -31,7 +31,8 @@ class UpdateParticipantCommand {
|
||||
$p->address_2 = $this->request->address_2;
|
||||
$p->postcode = $this->request->postcode;
|
||||
$p->city = $this->request->city;
|
||||
$p->local_group = $this->request->localgroup;
|
||||
// Leerer String würde als Fremdschlüssel scheitern -- kein Stamm heißt null.
|
||||
$p->local_group = $this->request->localgroup ?: null;
|
||||
$p->birthday = DateTime::createFromFormat('Y-m-d', $this->request->birthday);
|
||||
$p->email_1 = $this->request->email_1;
|
||||
$p->phone_1 = $this->request->phone_1;
|
||||
|
||||
@@ -14,7 +14,8 @@ class UpdateParticipantRequest {
|
||||
public ?string $address_2,
|
||||
public string $postcode,
|
||||
public string $city,
|
||||
public string $localgroup,
|
||||
/** Stamm-Slug; leer/null bei Anmeldungen, die keinen Stamm erfasst haben (Kurzanmeldung). */
|
||||
public ?string $localgroup,
|
||||
public string $birthday,
|
||||
public string $email_1,
|
||||
public string $phone_1,
|
||||
|
||||
@@ -60,7 +60,9 @@ class DetailsController extends CommonController {
|
||||
$flatSupport,
|
||||
$supportPerPerson,
|
||||
$contributinLocalGroups,
|
||||
$eatingHabits
|
||||
$eatingHabits,
|
||||
(bool)$request->input('shortRegistration', false),
|
||||
(bool)$request->input('swimmingPermissionRequired', true),
|
||||
);
|
||||
|
||||
$eventUpdateCommand = new UpdateEventCommand($eventUpdateRequest);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Controllers;
|
||||
|
||||
use App\Scopes\CommonController;
|
||||
use App\ValueObjects\Amount;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* Beitrag einer Kurzanmeldung. Teilnahmegruppe, Beitragsstufe und Zeitraum stehen fest, deshalb braucht der
|
||||
* Endpunkt keine Eingaben -- und der Betrag lässt sich vom Client nicht beeinflussen.
|
||||
*
|
||||
* `amountValue` steuert im Frontend, ob der Schritt "Zahlungsart" gezeigt wird.
|
||||
*/
|
||||
class ShortCalculateAmountController extends CommonController {
|
||||
public function __invoke(int $eventId) : JsonResponse {
|
||||
$event = $this->events->getById($eventId, false);
|
||||
|
||||
$participationType = $event->firstParticipationTypeSlug();
|
||||
|
||||
$amount = $participationType === null
|
||||
? new Amount(0, 'Euro')
|
||||
: $event->toResource()->calculateAmount(
|
||||
$participationType,
|
||||
'standard',
|
||||
$event->start_date,
|
||||
$event->end_date,
|
||||
false
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'amount' => $amount->toString(),
|
||||
'amountValue' => $amount->getAmount(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Controllers;
|
||||
|
||||
use App\Domains\Event\Actions\CertificateOfConductionCheck\CertificateOfConductionCheckCommand;
|
||||
use App\Domains\Event\Actions\CertificateOfConductionCheck\CertificateOfConductionCheckRequest;
|
||||
use App\Domains\Event\Actions\ShortSignUp\ShortSignUpCommand;
|
||||
use App\Domains\Event\Actions\ShortSignUp\ShortSignUpRequest;
|
||||
use App\Enumerations\FirstAidPermission;
|
||||
use App\Enumerations\SwimmingPermission;
|
||||
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
|
||||
use App\Providers\DoubleCheckEventRegistrationProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use DateTime;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
/**
|
||||
* Nimmt die verkürzte Anmeldung entgegen. Öffentlich erreichbar, deshalb werden die Eingaben hier -- anders als
|
||||
* im langen Prozess -- validiert, bevor sie die Action erreichen.
|
||||
*/
|
||||
class ShortSignupController extends CommonController {
|
||||
public function __invoke(int $eventId, Request $request) : JsonResponse {
|
||||
$event = $this->events->getById($eventId, false);
|
||||
|
||||
if (!$event->registration_allowed || new DateTime() > $event->start_date) {
|
||||
return response()->json(['status' => 'closed'], 403);
|
||||
}
|
||||
|
||||
// Die Kurzanmeldung darf nicht als Abkürzung um den langen Prozess herum benutzt werden.
|
||||
if (!$event->short_registration) {
|
||||
return response()->json(['status' => 'closed'], 403);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'firstname' => 'required|string|max:255',
|
||||
'lastname' => 'required|string|max:255',
|
||||
'nickname' => 'nullable|string|max:255',
|
||||
'birthday' => 'required|date_format:Y-m-d|before:today',
|
||||
'email' => 'required|email|max:255',
|
||||
'phone' => 'required|string|max:255',
|
||||
// Gegen die teilnehmenden Stämme prüft der Command -- hier reicht die Typprüfung.
|
||||
'localGroup' => 'nullable|integer',
|
||||
'contactPerson' => 'nullable|string|max:255',
|
||||
'contactEmail' => 'nullable|email|max:255',
|
||||
'allergies' => 'nullable|string|max:255',
|
||||
'intolerances' => 'nullable|string|max:255',
|
||||
'firstAidPermission' => ['nullable', 'string', 'in:' . implode(',', FirstAidPermission::query()->pluck('slug')->toArray())],
|
||||
'swimmingPermission' => ['nullable', 'string', 'in:' . implode(',', SwimmingPermission::query()->pluck('slug')->toArray())],
|
||||
'foto' => 'array',
|
||||
'participationOptions' => 'array',
|
||||
'paymentMethod' => 'nullable|string',
|
||||
'paymentOptions' => 'array',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['status' => 'error', 'message' => $validator->errors()->first()], 422);
|
||||
}
|
||||
|
||||
$birthday = DateTime::createFromFormat('Y-m-d', $request->input('birthday'));
|
||||
|
||||
$doubleCheck = new DoubleCheckEventRegistrationProvider(
|
||||
$event,
|
||||
$request->input('firstname'),
|
||||
$request->input('lastname'),
|
||||
$request->input('email'),
|
||||
$birthday
|
||||
);
|
||||
|
||||
if ($doubleCheck->isRegistered()) {
|
||||
return response()->json(['status' => 'exists']);
|
||||
}
|
||||
|
||||
$foto = (array)$request->input('foto', []);
|
||||
|
||||
$shortSignUpRequest = new ShortSignUpRequest(
|
||||
event: $event,
|
||||
user_id: currentUser()?->id,
|
||||
firstname: $request->input('firstname'),
|
||||
lastname: $request->input('lastname'),
|
||||
nickname: $request->input('nickname'),
|
||||
birthday: $birthday,
|
||||
email: $request->input('email'),
|
||||
phone: $request->input('phone'),
|
||||
localGroupId: $request->input('localGroup') !== null ? (int)$request->input('localGroup') : null,
|
||||
contactPerson: $request->input('contactPerson') ?: null,
|
||||
contactEmail: $request->input('contactEmail') ?: null,
|
||||
allergies: $request->input('allergies') ?: null,
|
||||
intolerances: $request->input('intolerances') ?: null,
|
||||
firstAidPermission: $request->input('firstAidPermission') ?: null,
|
||||
swimmingPermission: $request->input('swimmingPermission') ?: null,
|
||||
foto_socialmedia: (bool)($foto['socialmedia'] ?? false),
|
||||
foto_print: (bool)($foto['print'] ?? false),
|
||||
foto_webseite: (bool)($foto['webseite'] ?? false),
|
||||
foto_partner: (bool)($foto['partner'] ?? false),
|
||||
foto_intern: (bool)($foto['intern'] ?? false),
|
||||
paymentMethod: $request->input('paymentMethod') ?: null,
|
||||
paymentOptions: (array)$request->input('paymentOptions', []),
|
||||
participationOptions: (array)$request->input('participationOptions', []),
|
||||
);
|
||||
|
||||
$shortSignUpResponse = new ShortSignUpCommand($shortSignUpRequest)->execute();
|
||||
|
||||
if (!$shortSignUpResponse->success) {
|
||||
return response()->json(['status' => 'error', 'message' => $shortSignUpResponse->message], 422);
|
||||
}
|
||||
|
||||
$participant = $shortSignUpResponse->participant;
|
||||
|
||||
$cocResponse = new CertificateOfConductionCheckCommand(
|
||||
new CertificateOfConductionCheckRequest($participant)
|
||||
)->execute();
|
||||
|
||||
$participant->efz_status = $cocResponse->status;
|
||||
$participant->save();
|
||||
|
||||
Mail::to($participant->email_1)->send(new EventSignUpSuccessfullMail(participant: $participant));
|
||||
|
||||
if ($participant->email_2 !== null) {
|
||||
Mail::to($participant->email_2)->send(new EventSignUpSuccessfullMail(participant: $participant));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'participant' => $participant->toResource()->toArray($request),
|
||||
'status' => 'success',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ use App\Domains\Event\Controllers\ParticipantReSignOnController;
|
||||
use App\Domains\Event\Controllers\ParticipantSignOffController;
|
||||
use App\Domains\Event\Controllers\ParticipantUpdateController;
|
||||
use App\Domains\Event\Controllers\PaymentReminderController;
|
||||
use App\Domains\Event\Controllers\ShortCalculateAmountController;
|
||||
use App\Domains\Event\Controllers\ShortSignupController;
|
||||
use App\Domains\Event\Controllers\SignupController;
|
||||
use App\Middleware\IdentifyTenant;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -27,6 +29,9 @@ Route::prefix('api/v1')
|
||||
Route::post('{eventId}/calculate-amount', [SignupController::class, 'calculateAmount']);
|
||||
Route::post('{eventId}/signup', [SignupController::class, 'signUp']);
|
||||
|
||||
Route::post('{eventId}/short-calculate-amount', ShortCalculateAmountController::class);
|
||||
Route::post('{eventId}/short-signup', ShortSignupController::class);
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::post('/create', [CreateController::class, 'doCreate']);
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ const emit = defineEmits(['close'])
|
||||
eatingHabits: eatingHabits.value,
|
||||
sendWeeklyReports: props.event.sendWeeklyReports,
|
||||
registrationAllowed: props.event.registrationAllowed,
|
||||
shortRegistration: props.event.shortRegistration,
|
||||
swimmingPermissionRequired: props.event.swimmingPermissionRequired,
|
||||
flatSupport: props.event.flatSupportEdit,
|
||||
supportPerson: props.event.supportPersonIndex,
|
||||
})
|
||||
@@ -64,6 +66,8 @@ const emit = defineEmits(['close'])
|
||||
alcoholicsAge: formData.alcoholicsAge,
|
||||
sendWeeklyReports: formData.sendWeeklyReports,
|
||||
registrationAllowed: formData.registrationAllowed,
|
||||
shortRegistration: formData.shortRegistration,
|
||||
swimmingPermissionRequired: formData.swimmingPermissionRequired,
|
||||
flatSupport: formData.flatSupport,
|
||||
supportPerson: formData.supportPerson,
|
||||
contributingLocalGroups: contributingLocalGroups.value,
|
||||
@@ -172,6 +176,27 @@ const emit = defineEmits(['close'])
|
||||
<label for="registrationAllowed">Veranstaltung ist für Anmeldungen geöffnet</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="checkbox" v-model="formData.shortRegistration" id="shortRegistration" />
|
||||
<label for="shortRegistration">Verkürzte Anmeldung verwenden</label><br />
|
||||
<span style="font-size: 0.8rem; color: #6b7280;">
|
||||
Fragt nur die nötigsten Daten ab (Name, Kontakt, Allergien, Foto-Erlaubnis).
|
||||
Nicht erhobene Angaben wie Anschrift und An-/Abreise werden automatisch gefüllt.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="checkbox" v-model="formData.swimmingPermissionRequired" id="swimmingPermissionRequired" />
|
||||
<label for="swimmingPermissionRequired">Badeerlaubnis abfragen</label><br />
|
||||
<span style="font-size: 0.8rem; color: #6b7280;">
|
||||
Ist die Abfrage deaktiviert, wird für Minderjährige „Keine Badeerlaubnis" hinterlegt.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
@@ -233,6 +233,7 @@ function saveParticipant() {
|
||||
<td>
|
||||
<span v-if="!staticProps.editMode">{{ props.participant.localgroup }}</span>
|
||||
<select v-else v-model="form.localgroup">
|
||||
<option value="">Kein Stamm</option>
|
||||
<option v-for="group in staticProps.event.contributingLocalGroups" :key="group.id" :value="group.slug">{{ group.name }}</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup>
|
||||
import {computed} from 'vue'
|
||||
import {useShortSignupForm} from './composables/useShortSignupForm.js'
|
||||
import {
|
||||
SHORT_STEP_PERSON,
|
||||
SHORT_STEP_PARTICIPATION_OPTIONS,
|
||||
SHORT_STEP_PHOTO,
|
||||
SHORT_STEP_PAYMENT,
|
||||
SHORT_STEP_SUMMARY,
|
||||
stepVisible,
|
||||
} from './composables/shortStepFlow.js'
|
||||
import ShortStepPerson from './steps/ShortStepPerson.vue'
|
||||
import ShortStepSummary from './steps/ShortStepSummary.vue'
|
||||
import StepParticipationOptions from '../SignUpForm/steps/StepParticipationOptions.vue'
|
||||
import StepPhotoPermissions from '../SignUpForm/steps/StepPhotoPermissions.vue'
|
||||
import StepPaymentMethod from '../SignUpForm/steps/StepPaymentMethod.vue'
|
||||
import SubmitSuccess from '../SignUpForm/after-submit/SubmitSuccess.vue'
|
||||
import SubmitAlreadyExists from '../SignUpForm/after-submit/SubmitAlreadyExists.vue'
|
||||
|
||||
const props = defineProps({
|
||||
event: Object,
|
||||
participantData: Object,
|
||||
})
|
||||
|
||||
const {
|
||||
currentStep, goToStep, goNext, goBack, formData, isMinor, localGroups, needsLocalGroup,
|
||||
submit, submitting, submitResult, submitError,
|
||||
summaryLoading, summaryAmount, summaryAmountValue,
|
||||
} = useShortSignupForm(props.event, props.participantData)
|
||||
|
||||
// Die aus dem langen Prozess übernommenen Schritte emittieren ihre Zielnummer aus jenem Flow mit. Hier zählt
|
||||
// nur, dass "weiter" bzw. "zurück" gedrückt wurde -- die Reihenfolge kennt dieser Host.
|
||||
const onNext = () => goNext()
|
||||
const onBack = () => goBack()
|
||||
|
||||
const allSteps = [
|
||||
{step: SHORT_STEP_PERSON, label: 'Person'},
|
||||
{step: SHORT_STEP_PARTICIPATION_OPTIONS, label: 'Teilnahmeoptionen'},
|
||||
{step: SHORT_STEP_PHOTO, label: 'Fotoerlaubnis'},
|
||||
{step: SHORT_STEP_PAYMENT, label: 'Zahlungsart'},
|
||||
{step: SHORT_STEP_SUMMARY, label: 'Zusammenfassung'},
|
||||
]
|
||||
|
||||
// Nur die tatsächlich vorkommenden Schritte anzeigen, sonst zeigt die Leiste Schritte, die nie erscheinen.
|
||||
const steps = computed(() =>
|
||||
allSteps.filter(s => stepVisible(props.event, s.step, summaryAmountValue.value))
|
||||
)
|
||||
|
||||
const currentIndex = computed(() => steps.value.findIndex(s => s.step === currentStep.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Nach Submit -->
|
||||
<SubmitSuccess
|
||||
v-if="submitResult?.status === 'success'"
|
||||
:participant="submitResult?.participant"
|
||||
:event="event"
|
||||
/>
|
||||
<SubmitAlreadyExists v-else-if="submitResult?.status === 'exists'" :event="event" />
|
||||
|
||||
<template v-else>
|
||||
<!-- Fortschrittsleiste -->
|
||||
<div class="signup-progress">
|
||||
<div class="signup-progress-pills">
|
||||
<template v-for="(s, index) in steps" :key="s.step">
|
||||
<div v-if="index > 0" class="signup-progress-separator"></div>
|
||||
<div
|
||||
class="signup-pill"
|
||||
:class="{
|
||||
'signup-pill--active': currentStep === s.step,
|
||||
'signup-pill--done': currentStep > s.step,
|
||||
'signup-pill--upcoming': currentStep < s.step,
|
||||
}"
|
||||
@click="currentStep > s.step ? goToStep(s.step) : null"
|
||||
>
|
||||
<span v-if="currentStep > s.step" class="signup-pill__check">✓</span>
|
||||
{{ s.label }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="signup-progress-bar">
|
||||
<div
|
||||
class="signup-progress-bar__fill"
|
||||
:style="{ width: (steps.length > 1 ? (currentIndex / (steps.length - 1)) * 100 : 100) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="signup-progress-mobile">
|
||||
Schritt {{ currentIndex + 1 }} von {{ steps.length }}:
|
||||
<strong>{{ steps.find(s => s.step === currentStep)?.label }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Steps -->
|
||||
<form @submit.prevent="submit">
|
||||
<ShortStepPerson
|
||||
v-if="currentStep === SHORT_STEP_PERSON"
|
||||
:formData="formData" :event="event" :isMinor="isMinor"
|
||||
:localGroups="localGroups" :needsLocalGroup="needsLocalGroup"
|
||||
@next="onNext"
|
||||
/>
|
||||
<StepParticipationOptions
|
||||
v-if="currentStep === SHORT_STEP_PARTICIPATION_OPTIONS"
|
||||
:formData="formData" :event="event"
|
||||
@next="onNext" @back="onBack"
|
||||
/>
|
||||
<StepPhotoPermissions
|
||||
v-if="currentStep === SHORT_STEP_PHOTO"
|
||||
:formData="formData" :event="event"
|
||||
@next="onNext" @back="onBack"
|
||||
/>
|
||||
<StepPaymentMethod
|
||||
v-if="currentStep === SHORT_STEP_PAYMENT"
|
||||
:formData="formData" :event="event"
|
||||
@next="onNext" @back="onBack"
|
||||
/>
|
||||
<ShortStepSummary
|
||||
v-if="currentStep === SHORT_STEP_SUMMARY"
|
||||
:formData="formData"
|
||||
:event="event"
|
||||
:isMinor="isMinor"
|
||||
:summaryAmount="summaryAmount"
|
||||
:summaryAmountValue="summaryAmountValue"
|
||||
:summaryLoading="summaryLoading"
|
||||
:submitting="submitting"
|
||||
:submitError="submitError"
|
||||
@back="onBack"
|
||||
@submit="submit"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Unscoped und mit dem langen Anmeldeprozess geteilt. -->
|
||||
<style src="../SignUpForm/signupForm.css"></style>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup>
|
||||
import ErrorText from "../../../../../../Views/Components/ErrorText.vue";
|
||||
import {reactive} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: Object,
|
||||
event: Object,
|
||||
isMinor: Boolean,
|
||||
localGroups: {type: Array, default: () => []},
|
||||
needsLocalGroup: Boolean,
|
||||
})
|
||||
const emit = defineEmits(['next'])
|
||||
|
||||
const errors = reactive({
|
||||
vorname: '',
|
||||
nachname: '',
|
||||
geburtsdatum: '',
|
||||
localGroup: '',
|
||||
email_1: '',
|
||||
telefon_1: '',
|
||||
ansprechpartner: '',
|
||||
email_2: '',
|
||||
first_aid: '',
|
||||
badeerlaubnis: '',
|
||||
})
|
||||
|
||||
// Nur sichtbare Felder werden geprüft -- Kontaktperson und Erlaubnisse gibt es bei Volljährigen nicht.
|
||||
const next = () => {
|
||||
Object.keys(errors).forEach(key => errors[key] = '')
|
||||
|
||||
let hasError = false
|
||||
|
||||
if (!props.formData.vorname) {
|
||||
errors.vorname = 'Bitte einen Vornamen angeben.'
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (!props.formData.nachname) {
|
||||
errors.nachname = 'Bitte einen Nachnamen angeben.'
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (!props.formData.geburtsdatum) {
|
||||
errors.geburtsdatum = 'Bitte ein Geburtsdatum angeben.'
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (props.needsLocalGroup && (!props.formData.localGroup || props.formData.localGroup === '-1')) {
|
||||
errors.localGroup = 'Bitte einen Stamm auswählen.'
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (!props.formData.email_1) {
|
||||
errors.email_1 = 'Bitte eine E-Mail-Adresse angeben.'
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (!props.formData.telefon_1) {
|
||||
errors.telefon_1 = 'Bitte eine Telefonnummer angeben.'
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (props.isMinor) {
|
||||
if (!props.formData.ansprechpartner) {
|
||||
errors.ansprechpartner = 'Bitte eine Kontaktperson angeben.'
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (!props.formData.email_2) {
|
||||
errors.email_2 = 'Bitte eine E-Mail-Adresse der Kontaktperson angeben.'
|
||||
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 (props.event.swimmingPermissionRequired && props.formData.badeerlaubnis === '-1') {
|
||||
errors.badeerlaubnis = 'Bitte triff eine Entscheidung. Bist du dir unsicher, kontaktiere bitte die Aktionsleitung.'
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('next')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h3>Angaben zur Person</h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<td>Vorname:</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.vorname" />
|
||||
<ErrorText :message="errors.vorname" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pfadiname:</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.pfadiname" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nachname:</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.nachname" />
|
||||
<ErrorText :message="errors.nachname" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Geburtsdatum:</td>
|
||||
<td>
|
||||
<input type="date" v-model="formData.geburtsdatum" />
|
||||
<ErrorText :message="errors.geburtsdatum" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="needsLocalGroup">
|
||||
<td>Stamm:</td>
|
||||
<td>
|
||||
<select v-model="formData.localGroup">
|
||||
<option value="-1">Bitte wählen</option>
|
||||
<option v-for="group in localGroups" :key="group.id" :value="group.id">{{ group.name }}</option>
|
||||
</select>
|
||||
<ErrorText :message="errors.localGroup" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>E-Mail:</td>
|
||||
<td>
|
||||
<input type="email" v-model="formData.email_1" />
|
||||
<ErrorText :message="errors.email_1" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Telefon:</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.telefon_1" />
|
||||
<ErrorText :message="errors.telefon_1" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<template v-if="isMinor">
|
||||
<tr>
|
||||
<td>Kontaktperson (Nachname, Vorname):</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.ansprechpartner" />
|
||||
<ErrorText :message="errors.ansprechpartner" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>E-Mail der Kontaktperson:</td>
|
||||
<td>
|
||||
<input type="email" v-model="formData.email_2" />
|
||||
<ErrorText :message="errors.email_2" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<tr>
|
||||
<td>Allergien:</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.allergien" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Unverträglichkeiten:</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.intolerances" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<template v-if="isMinor">
|
||||
<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 event.firstAidPermissions"
|
||||
:key="firstAidPermission.slug"
|
||||
: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 v-if="event.swimmingPermissionRequired">
|
||||
<td>Badeerlaubnis:</td>
|
||||
<td>
|
||||
<select v-model="formData.badeerlaubnis">
|
||||
<option value="-1">Bitte wählen</option>
|
||||
<option
|
||||
v-for="swimmingPermission in event.swimmingPermissions"
|
||||
:key="swimmingPermission.slug"
|
||||
:value="swimmingPermission.slug">{{ swimmingPermission.name }}</option>
|
||||
</select>
|
||||
<ErrorText :message="errors.badeerlaubnis" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" class="btn-row">
|
||||
<button type="button" class="btn-primary" @click="next">Weiter →</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
|
||||
const PAYMENT_ACCOUNT_TRANSACTION = 'PAYMENT_ACCOUNT_TRANSACTION'
|
||||
|
||||
const props = defineProps({
|
||||
formData: Object,
|
||||
event: Object,
|
||||
isMinor: Boolean,
|
||||
summaryAmount: String,
|
||||
summaryAmountValue: {type: Number, default: 0},
|
||||
summaryLoading: Boolean,
|
||||
submitting: Boolean,
|
||||
submitError: {type: String, default: ''},
|
||||
})
|
||||
const emit = defineEmits(['back', 'submit'])
|
||||
|
||||
const selectedMethod = computed(() =>
|
||||
(props.event.paymentMethods ?? []).find(m => m.slug === props.formData.paymentMethod) ?? null
|
||||
)
|
||||
|
||||
// Die Überweisungs-Bestätigung wird nur bei Banküberweisung verlangt.
|
||||
const requiresPaymentConfirmation = computed(() =>
|
||||
props.formData.paymentMethod === PAYMENT_ACCOUNT_TRANSACTION
|
||||
)
|
||||
|
||||
// Bestätigungstext ist ein admin-konfigurierbarer Freitext ({amount}-Platzhalter), sonst Default.
|
||||
const paymentConfirmationText = computed(() => {
|
||||
const configured = selectedMethod.value?.configuration?.summary_confirmation_text
|
||||
const template = (configured && String(configured).trim() !== '')
|
||||
? configured
|
||||
: 'Ich bestätige, den Betrag von {amount} zu überweisen.'
|
||||
return template.replaceAll('{amount}', props.summaryAmount ?? '')
|
||||
})
|
||||
|
||||
// Gewählte Teilnahmeoptionen für die Zusammenfassung (Label statt Wert 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.title || question.label, value: option?.label ?? ''}
|
||||
}).filter(entry => entry.value !== '')
|
||||
)
|
||||
|
||||
// Der Stamm steht entweder fest (genau ein teilnehmender Stamm) oder wurde auf Seite 1 gewählt.
|
||||
const localGroupName = computed(() =>
|
||||
(props.event.contributingLocalGroups ?? []).find(g => g.id === props.formData.localGroup)?.name ?? null
|
||||
)
|
||||
|
||||
const permissionName = (list, slug) =>
|
||||
(list ?? []).find(entry => entry.slug === slug)?.name ?? '—'
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
props.formData.summary_information_correct
|
||||
&& props.formData.summary_accept_terms
|
||||
&& props.formData.legal_accepted
|
||||
&& (!requiresPaymentConfirmation.value || props.formData.payment)
|
||||
&& !props.submitting
|
||||
)
|
||||
</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>Dein Name:</td>
|
||||
<td>{{ formData.vorname }} {{ formData.nachname }}</td>
|
||||
</tr>
|
||||
<tr v-if="formData.pfadiname">
|
||||
<td>Pfadiname:</td>
|
||||
<td>{{ formData.pfadiname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deine E-Mail:</td>
|
||||
<td>{{ formData.email_1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deine Telefonnummer:</td>
|
||||
<td>{{ formData.telefon_1 }}</td>
|
||||
</tr>
|
||||
<tr v-if="localGroupName">
|
||||
<td>Stamm:</td>
|
||||
<td>{{ localGroupName }}</td>
|
||||
</tr>
|
||||
|
||||
<template v-if="isMinor">
|
||||
<tr>
|
||||
<td>Kontaktperson:</td>
|
||||
<td>{{ formData.ansprechpartner }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>E-Mail der Kontaktperson:</td>
|
||||
<td>{{ formData.email_2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Erweiterte Erste Hilfe:</td>
|
||||
<td>{{ permissionName(event.firstAidPermissions, formData.first_aid) }}</td>
|
||||
</tr>
|
||||
<tr v-if="event.swimmingPermissionRequired">
|
||||
<td>Badeerlaubnis:</td>
|
||||
<td>{{ permissionName(event.swimmingPermissions, formData.badeerlaubnis) }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<tr>
|
||||
<td>Allergien:</td>
|
||||
<td>{{ formData.allergien || 'Keine Angabe' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Unverträglichkeiten:</td>
|
||||
<td>{{ formData.intolerances || 'Keine Angabe' }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="option in selectedParticipationOptions" :key="option.label">
|
||||
<td>{{ option.label }}:</td>
|
||||
<td>{{ option.value }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Foto-Erlaubnis:</td>
|
||||
<td>
|
||||
<strong>Social Media:</strong> {{ formData.foto.socialmedia ? 'Ja' : 'Nein' }},
|
||||
<strong>Printmedien:</strong> {{ formData.foto.print ? 'Ja' : 'Nein' }},
|
||||
<strong>Webseite:</strong> {{ formData.foto.webseite ? 'Ja' : 'Nein' }},
|
||||
<strong>Partnerorganisationen:</strong> {{ formData.foto.partner ? 'Ja' : 'Nein' }},
|
||||
<strong>Interne Zwecke:</strong> {{ formData.foto.intern ? 'Ja' : 'Nein' }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr><td>Veranstaltung:</td><td><strong>{{ event.name }}</strong></td></tr>
|
||||
<tr><td>Zeitraum:</td><td>{{ event.eventBegin }} – {{ event.eventEnd }}</td></tr>
|
||||
</table>
|
||||
|
||||
<h4 style="margin: 0 0 8px 0;">Kostenübersicht</h4>
|
||||
<table class="form-table cost-table" style="margin-bottom: 20px;">
|
||||
<tr class="cost-total">
|
||||
<td><strong>Teilnahmebeitrag:</strong></td>
|
||||
<td><strong>{{ summaryAmount }}</strong></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 v-if="requiresPaymentConfirmation" style="display: flex; gap: 10px; cursor: pointer;">
|
||||
<input type="checkbox" v-model="formData.payment" />
|
||||
{{ paymentConfirmationText }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p v-if="submitError" style="color: #991b1b; font-weight: 600;">{{ submitError }}</p>
|
||||
|
||||
<div class="btn-row">
|
||||
<button type="button" class="btn-secondary" @click="emit('back')">← Zurück</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-primary"
|
||||
:disabled="!canSubmit"
|
||||
style="background: #059669;"
|
||||
>
|
||||
{{ submitting ? 'Wird gesendet…' : 'Jetzt anmelden ✓' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cost-table .cost-total td {
|
||||
border-top: 1px solid #d1d5db;
|
||||
padding-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -122,170 +122,5 @@ const steps = [
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* ─── 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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- Unscoped und mit der Kurzanmeldung geteilt (ShortSignupForm.vue bindet dieselbe Datei ein). -->
|
||||
<style src="./signupForm.css"></style>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 = () => {
|
||||
<ErrorText :message="errors.email_2" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr v-if="event.swimmingPermissionRequired">
|
||||
<td>Badeerlaubnis:</td>
|
||||
<td>
|
||||
<select v-model="formData.badeerlaubnis">
|
||||
|
||||
@@ -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() {
|
||||
<hr class="signup-divider" />
|
||||
|
||||
<div class="signup-body">
|
||||
<ShortSignupForm
|
||||
v-if="props.event.registrationAllowed && props.event.shortRegistration"
|
||||
:event="props.event"
|
||||
:participantData="props.participantData ?? {}"
|
||||
/>
|
||||
<SignupForm
|
||||
v-if="props.event.registrationAllowed"
|
||||
v-else-if="props.event.registrationAllowed"
|
||||
:event="props.event"
|
||||
:participantData="props.participantData ?? {}"
|
||||
:localGroups="props.localGroups ?? []"
|
||||
|
||||
@@ -45,6 +45,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property float $support_flat
|
||||
* @property int $alcoholics_age
|
||||
* @property boolean $archived
|
||||
* @property boolean $short_registration
|
||||
* @property boolean $swimming_permission_required
|
||||
* @property string|null $invoice_key
|
||||
*/
|
||||
class Event extends InstancedModel
|
||||
@@ -91,6 +93,9 @@ class Event extends InstancedModel
|
||||
'participation_options',
|
||||
'addons',
|
||||
|
||||
'short_registration',
|
||||
'swimming_permission_required',
|
||||
|
||||
'invoice_key',
|
||||
];
|
||||
|
||||
@@ -121,6 +126,9 @@ class Event extends InstancedModel
|
||||
|
||||
'participation_options' => '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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user