Signup for events implemented

This commit is contained in:
2026-03-22 00:06:03 +01:00
parent b8341890d3
commit 405591d6dd
13 changed files with 428 additions and 24 deletions

View File

@@ -7,8 +7,6 @@ use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Js;
class DashboardController extends CommonController {
public function __invoke(Request $request) {

View File

@@ -6,17 +6,16 @@ use App\Domains\Event\Actions\CertificateOfConductionCheck\CertificateOfConducti
use App\Domains\Event\Actions\CertificateOfConductionCheck\CertificateOfConductionCheckRequest;
use App\Domains\Event\Actions\SignUp\SignUpCommand;
use App\Domains\Event\Actions\SignUp\SignUpRequest;
use App\Mail\EventSignUpSuccessfull;
use App\Models\Tenant;
use App\Models\User;
use App\Providers\DoubleCheckEventRegistrationProvider;
use App\Providers\InertiaProvider;
use App\Resources\EventResource;
use App\Resources\UserResource;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use http\Env\Response;
use DateTime;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Mail;
class SignupController extends CommonController {
public function __invoke(int $eventId, Request $request) {
@@ -76,11 +75,17 @@ class SignupController extends CommonController {
$departure = \DateTime::createFromFormat('Y-m-d', $registrationData['departure']);
$tetanusVaccination = $registrationData['tetanusVaccination'] ? \DateTime::createFromFormat('Y-m-d', $registrationData['tetanusVaccination']) : null;
// Steps:
// 1. Check, if bereits angemeldet
$doubleCheckEventRegistrationProvider = new DoubleCheckEventRegistrationProvider(
$event,
$registrationData['vorname'],
$registrationData['nachname'],
$registrationData['email_1'],
DateTime::createFromFormat('Y-m-d', $registrationData['geburtsdatum']));
if ($doubleCheckEventRegistrationProvider->isRegistered()) {
return response()->json(['status' => 'exists']);
}
//
$amount = $eventResource->calculateAmount(
$registrationData['participationType'],
$registrationData['beitrag'],
@@ -138,9 +143,15 @@ class SignupController extends CommonController {
$signupResponse->participant->efz_status = $certificateOfConductionCheckResponse->status;
$signupResponse->participant->save();
// 6. E-Mail senden & Bestätigung senden
Mail::to($signupResponse->participant->email_1)->send(new EventSignUpSuccessfull(
participant: $signupResponse->participant,
));
if ($signupResponse->participant->email_2 !== null) {
Mail::to($signupResponse->participant->email_2)->send(new EventSignUpSuccessfull(
participant: $signupResponse->participant,
));
}
return response()->json(
[

View File

@@ -18,8 +18,6 @@ const props = defineProps({
localGroups: Array,
})
console.log(props.participantData);
const emit = defineEmits(['registrationDone'])
const {
@@ -48,7 +46,7 @@ const steps = [
:participant="submitResult?.participant"
:event="event"
/>
<SubmitAlreadyExists v-else-if="submitResult?.type === 'exists'" :data="submitResult.data" />
<SubmitAlreadyExists v-else-if="submitResult?.status === 'exists'" :event="event" />
<template v-else>
<!-- Fortschrittsleiste (ab Step 2) -->

View File

@@ -1,12 +1,18 @@
<script setup>
defineProps({ data: Object })
const props = defineProps({
event: Object
})
</script>
<template>
<div style="padding: 20px 0;">
<h3>{{ data.nicename }}</h3>
<p>{{ data.text_1 }}</p>
<p>{{ data.text_2 }}</p>
<a :href="data.email_link" style="color: #2563eb;">{{ data.email_text }}</a>
<h3>Registrierung nicht möglich</h3>
<p>
Leider konnte deine Anmeldung nicht ausgeführt werden, da du bereits für die Veranstaltung {{props.event.name}} angemeldet bist.
</p>
<p>
Falls du bereits angemeldet warst und abgemeldet wurdest, oder andere Fragen hast, kontaktiere die Veranstaltungsleitung:
<a href="mailto:{{props.event.email}}">{{props.event.email}}</a>
</p>
</div>
</template>

View File

@@ -16,7 +16,7 @@ console.log(props.participant)
<table class="form-table" style="margin-bottom: 20px;">
<tr><td>Anreise:</td><td>{{ props.participant.arrival }}</td></tr>
<tr><td>Abreise:</td><td>{{ props.participant.departure }}</td></tr>
<tr><td>Teilnahmegruppe:</td><td>{{ props.participant.participation_group }}</td></tr>
<tr><td>Teilnahmegruppe:</td><td>{{ props.participant.participationType }}</td></tr>
</table>
<div v-if="props.participant.efz_status === 'NOT_CHECKED'" style="font-weight: bold; color: #b45309; margin-bottom: 20px;">

View File

@@ -14,6 +14,35 @@ function formatDate(dateString) {
if (!dateString) return ''
return format(parseISO(dateString), 'dd.MM.yyyy')
}
function participationGroup() {
if (props.formData.participationType === 'team') {
return 'Kernteam';
}
if (props.formData.participationType === 'participant') {
return 'Teilnehmende';
}
if (props.formData.participationType === 'volunteer') {
return 'Unterstützende';
}
return 'Sonstige';
}
function eatingHabit() {
if (props.formData.eatingHabit === 'EATING_HABIT_VEGAN') {
return 'Vegan';
}
if (props.formData.eatingHabit === 'EATING_HABIT_VEGETARIAN') {
return 'Vegetarisch';
}
return 'Omnivor';
}
</script>
<template>
@@ -23,6 +52,65 @@ function formatDate(dateString) {
<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>{{props.formData.vorname}} {{props.formData.vorname}}</td>
</tr>
<tr>
<td>Deine E-Mail:</td>
<td>{{props.formData.email_1}}</td>
</tr>
<tr v-if="props.formData.ansprechpartner !== ''">
<td>Name deiner Kontaktperson:</td>
<td>{{props.formData.ansprechpartner}}</td>
</tr>
<tr v-if="props.formData.email_2 !== ''">
<td>E-Mail-Adresse deiner Kontaktperson:</td>
<td>{{props.formData.email_2}}</td>
</tr>
<tr v-if="props.formData.telefon_2 !== ''">
<td>Telefonnummer deiner Kontaktperson:</td>
<td>{{props.formData.telefon_2}}</td>
</tr>
<tr>
<td>Teilnahmegruppe:</td>
<td>{{ participationGroup() }}</td>
</tr>
<tr>
<td>Foto-Erlaubnis:</td>
<td>
<strong>Social Media:</strong> {{props.formData.foto.socialmedia ? 'Ja' : 'Nein'}},
<strong>Printmedien:</strong> {{props.formData.foto.print ? 'Ja' : 'Nein'}},
<strong>Webseite:</strong> {{props.formData.foto.webseite ? 'Ja' : 'Nein'}},
<strong>Partnerorganisationen:</strong> {{props.formData.foto.partner ? 'Ja' : 'Nein'}},
<strong>Interne Zwecke:</strong> {{props.formData.foto.intern ? 'Ja' : 'Nein'}}
</td>
</tr>
<tr>
<td>Allergien:</td>
<td>{{props.formData.allergien}}</td>
</tr>
<tr>
<td>Lebensmittelunverträglichkeiten:</td>
<td>{{props.formData.intolerances}}</td>
</tr>
<tr>
<td>Ernährungsweise:</td>
<td>{{eatingHabit()}}</td>
</tr>
<tr><td>Veranstaltung:</td><td><strong>{{ event.name }}</strong></td></tr>
<tr><td>Anreise:</td><td>{{ formatDate(formData.arrival) }}</td></tr>
<tr><td>Abreise:</td><td>{{ formatDate(formData.departure) }}</td></tr>

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Mail;
use App\Enumerations\EfzStatus;
use App\Models\EventParticipant;
use App\Resources\EventParticipantResource;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Http\Request;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class EventSignUpSuccessfull extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(
private EventParticipant $participant,
)
{
//
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
$participant = $this->participant->toResource()->toArray(new Request());
$subject = sprintf(
$participant['needs_payment'] ? 'Teilnahme- & Zahlungsinformationen %1$s %2$s' : 'Anmeldebestätigung %1$s %2$s',
'für die Veranstaltung',
$this->participant->event()->first()->name
);
return new Envelope(
subject: $subject,
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
$event = $this->participant->event()->first()->toResource()->toArray(new Request());
$participant = $this->participant->toResource()->toArray(new Request());
$girocodeProvider = new \App\Providers\GiroCodeProvider(
$event['accountOwner'],
$event['accountIban'],
(float) $participant['amount_left_value'],
$participant['payment_purpose']
);
$girocodeBinary = (string)$girocodeProvider->create();
return new Content(
view: 'emails.events.signup_complete',
with: [
'participationType' => $participant['participationType'],
'name' => $participant['nicename'],
'eventTitle' => $event['name'],
'eventEmail' => $event['email'],
'arrival' => $participant['arrival'],
'departure' => $participant['departure'],
'amount' => $participant['amount_left_string'],
'paymentFinalDate' => $event['registrationFinalEnd']['formatted'],
'paymentRequired' => $participant['needs_payment'],
'accountOwner' => $event['accountOwner'],
'accountIban' => $event['accountIban'],
'paymentPurpose' => $participant['payment_purpose'],
'girocodeBinary' => $girocodeBinary,
'efzStatus' => $participant['efz_status']
],
);
}
/**
* Get the attachments for the message.
*
* @return array<int, Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Providers;
use App\Models\Event;
use DateTime;
class DoubleCheckEventRegistrationProvider {
function __construct(
private Event $event,
private string $firstname,
private string $lastname,
private string $email,
private DateTime $birthday,
) {}
public function isRegistered() : bool
{
$checkconditions = array(
'firstname' => $this->firstname,
'lastname' => $this->lastname,
'email_1' => $this->email,
'birthday' => $this->birthday->format('Y-m-d'),
);
return $this->event->participants()->where($checkconditions)->exists();
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Resources;
use App\Enumerations\ParticipationType;
use App\Models\EventParticipant;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -16,16 +17,23 @@ class EventParticipantResource extends JsonResource
{
$event = $this->resource->event;
$amountLeft = $this->resource->amount;
if ($this->resource->amount_paid !== null) {
$amountLeft->subtractAmount($this->resource->amount_paid);
}
return array_merge(
$this->resource->toArray(),
[
'participationType' => ParticipationType::where(['slug' => $this->resource->participation_type])->first()->name,
'needs_payment' => $this->resource->amount->getAmount() > 0 && $event->pay_direct,
'nicename' => $this->resource->getNicename(),
'arrival' => $this->resource->arrival_date->format('d.m.Y'),
'departure' => $this->resource->departure_date->format('d.m.Y'),
'amount_left_string' => $this->resource->amount->toString(),
'amount_left_string' => $amountLeft->toString(),
'amount_left_value' => $amountLeft->getAmount(),
'email_1' => $this->resource->email_1,
]
);

View File

@@ -28,7 +28,7 @@ return new class extends Migration {
$table->string('nickname')->nullable();
$table->string('participation_type');
$table->string('local_group');
$table->dateTime('birthday');
$table->date('birthday');
$table->string('address_1');
$table->string('address_2');
$table->string('postcode');

View File

@@ -0,0 +1,6 @@
Dies istt eine automatisch erzeugte E-Mail. Bitte antworte nicht auf diese E-Mail.<br />
Solltest du Fragen haben, kontaktiere bitte die Aktionsleitung direkt.<br /><br />
Du erhältst diese E-Mail. da du dich für die Veranstaltung {{$eventTitle}} angemeldet hast.<br />
Sollte dies nicht korrekt sein, oder wenn du weitere Fragen hast, wende dich bitte an die Veranstaltiungsleitung.<br /><br />
Du erreichst die Veranstaltungsleitung per E-Mail unter der Adresse: {{$eventEmail}}

View File

@@ -0,0 +1,11 @@
<h3>Häufige Fragen</h3>
<h4>Ich kann mir die Teilnahmegebühr momentan nicht leisten. Was kann ich tun?</h4>
Melde dich bei deiner Aktionsleitung, wir finden eine Lösung. Aber melde dich bitte auf jeden Fall trotzdem vor der o.g. Frist, sonst müssen wir leider davon ausgehen, dass du die Anmeldung absichtlich nicht vervollständigst und dich von der Liste der Teilnehmenden streichen.
<br /><br />
<h4>Warum streicht ihr Teilnehmende überhaupt, wenn sie nicht bis Ablauf der Zahlungsfrist zahlen?</h4>
All die Aufgaben, die an der Planung, Förderung und Verwaltung von Aktionen hängen, werden in unserem Verband ehrenamtlich geleistet. Einzelnen Säumigen hinterherzulaufen ist eine sehr anstrengende, undankbare und zeitaufwendige Angelegenheit. Wir möchten unsere Ressourcen lieber in die Unterstützung der Planungsteams, die Bearbeitung von finanziellen Unterstützungsanfragen seitens der Teilnehmenden oder in alle anderen Aufgaben stecken, die in unserer ehrenamtlichen Arbeit so anfallen. Die Streichung von Säumigen nach einer Zahlungserinnerung und einer Mahnung ist eine faire Lösung dafür.
<br /><br />
<h4>Warum gibt es diese Fristen überhaupt?</h4>
Diese Fristen gelten nur für Aktionen auf Landesverbandsebene. Aktionen, die auf Landesverbandsebene stattfinden, haben häufig einen größeren Teilnehmer:innenkreis. Dies erfordert Planungssicherheit für die Teams. Nur so können beispielsweise die Programmplanung, die finanzielle Kalkulation und die Teamressourcen passend aufgestellt werden. Oft müssen auch Verbindlichkeiten mit Externen (Vermieter:innen von Lagerplätzen oder Gruppenhäusern, Verkehrsverbünden, o.ä.) im Vorhinein beglichen werden. Auch wir sind hier an Stornierungsbedingungen gebunden und können nicht kurz vor einer Aktion von einem Vertrag zurücktreten, wenn sich herausstellt, dass wegen mangelnder Anmeldungen eine Aktion nicht stattfinden wird.
<h4>Warum benötige ich ein erweitertes Führungszeugnis?</h4>
Bei allen Veranstaltungen des BdP besteht ein erweitertes Führungszeugnis (eFZ) als Voraussetzung für die Teilnahme. Das eFZ ist ein behördlicher Nachweis der sicherstellt, dass du nicht wegen Delikten gemäß §72a SGB III auffällig geworden bist. Dies zu überprüfen stellt eine wichtige Säule unseres Kinder- und Jugenschutzkonzepts dar.

View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html>
<body>
<h1>Hallo {{$name}}!</h1>
<p>
Vielen Dank für deine Anmeldung zur Veranstaltung "{{$eventTitle}}".<br />
Wir haben folgende Daten zu deiner Teilnahme erfasst:
</p>
<table cellpadding="0" cellspacing="0" border="0" style="width: 100%; max-width: 640px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; color: #1f2937;">
<tr>
<td style="padding: 8px 12px; width: 180px; font-weight: 600; color: #4b5563; border-bottom: 1px solid #e5e7eb;">
Ankunft
</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb;">
{{ $arrival }}
</td>
</tr>
<tr>
<td style="padding: 8px 12px; font-weight: 600; color: #4b5563; border-bottom: 1px solid #e5e7eb;">
Abreise
</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb;">
{{ $departure }}
</td>
</tr>
<tr>
<td style="padding: 8px 12px; font-weight: 600; color: #4b5563;">
Teilnahmegruppe
</td>
<td style="padding: 8px 12px;">
{{ $participationType }}
</td>
</tr>
</table>
@if ($paymentRequired)
<p>
Deine Teilnahme ist <strong>noch nicht bestätigt,</strong> da dsies erst nach vollständigem Zahlungseingang der Fall ist.
Bitte zahle den Betrag in Höhe von <strong>{{$amount}}</strong> bis zum <strong>{{$paymentFinalDate}},</strong> da andernfalls die Stornierung deiner Anmeldung erfolgen kann.
Um deine Anmeldung zu vervollständigen, überweise den Betrag bitte zugunsten folgender Bankverbindung:
</p>
<table cellpadding="0" cellspacing="0" border="0" style="width: 100%; max-width: 640px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; color: #1f2937;">
<tr>
<td style="padding: 8px 12px; width: 180px; font-weight: 600; color: #4b5563; border-bottom: 1px solid #e5e7eb;">
Kontoinhaber*in
</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb;">
{{ $accountOwner }}
</td>
</tr>
<tr>
<td style="padding: 8px 12px; font-weight: 600; color: #4b5563; border-bottom: 1px solid #e5e7eb;">
IBAN
</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; font-family: monospace;">
{{ $accountIban }}
</td>
</tr>
<tr>
<td style="padding: 8px 12px; font-weight: 600; color: #4b5563; border-bottom: 1px solid #e5e7eb;">
Verwendungszweck
</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb;">
{{ $paymentPurpose }}
</td>
</tr>
<tr>
<td style="padding: 10px 12px; font-weight: 700; color: #111827; border-top: 2px solid #d1d5db;">
Betrag
</td>
<td style="padding: 10px 12px; font-weight: 700; color: #111827; border-top: 2px solid #d1d5db;">
{{ $amount }}
</td>
</tr>
</table>
@php
$girocodeSrc = $message->embedData($girocodeBinary, 'girocode.png', 'image/png');
@endphp
<table style="margin: 20px;">
<tr style="vertical-align: top; background-color: #eaeaea; border-spacing: 0">
<td style="padding: 20px; text-align: center; font-weight: bold;">
<strong>
Bequem überweisen mit QR-Code
</strong><br /><br />
<img
style="padding-left: 20px;width: 150px; height: 150px;"
src="{{ $girocodeSrc }}"
alt="Giro-Code">
</td>
<td>
<p style="padding-left:10px; padding-right: 10px; text-align: left;">
<strong>Und so funktioniert es</strong><br />
<ul style="text-align: left; list-style: decimal; font-weight: normal">
<li style="padding-right: 10px;">Mache einen Screenshot des QR-Codes</li>
<li style="padding-right: 10px;">Öffne deine Banking-App und wähle den Menüpunkt (Foto-)Überweisung“.</li>
<li style="padding-right: 10px;">Wähle das gespeicherte Bild aus</li>
</ul>
</p>
</td>
</tr>
</table>
<p>
Bitte zahle immer mit dem vorgegeben Betreff, damit deine Zahlung möglichst einfach zugeordnet werden kann.<br />
Sollte die Zahlung innerhalb dieser Frist nicht oder nur teilweise möglich sein, kontaktiere bitte die Aktionsleitung, sodass wir eine gemeinsame Lösung finden können.
</p>
@else
<p>
Deine Teilnahme wird entweder gefördert, sodass du nichts überweisen musst, oder die Beitragszahlung wird durch deinen Stamm abgewickelt.<br />
In diesem Fall wird sich dein Stamm bei dir melden, ob und in welcher Höhe ein Eigenanteil zu zahlen ist.
</p>
@endif
@switch ($efzStatus)
@case ('not_checked')
<p style="border-style: solid; border-width: 2px; border-color: #bdb053; padding: 10px; font-weight: bold; color: #501e1e; background-color: #f4e99e">
Dein erweitertes Führungszeugnis konnte nicht automatisch überprüft werden. Bitte kontaktiere die Aktionsleitung, da deine Teilnahme nur mit gültigem eFZ möglich ist.
</p>
@break
@case ('checked_invalid')
<p style="border-style: solid; border-width: 2px; border-color: #ff0000; padding: 10px; font-weight: bold; color: #501e1e; background-color: #da7070">
Du hast noch kein erweitertes Führungszeugnis bereitgestellt, sodass deine Teilnahme nicht möglich ist.
Bitte reiche dein erweitertes Führungszeugnis bis zum {{$paymentFinalDate}} ein, da deine Teilnahme andernfalls storniert wird.<br /><br />
Solltest du diese Frist nicht einhalten können, setze dich bitte mit der Aktionsleitung in Verbindung.',
</p>
@break
@default
@endswitch
<p>
@include('emails.events.faq')
</p><br /><br />
<p>
@include('emails.events.disclaimer')
</p>
</body>
</html>