Compare commits

..

34 Commits

Author SHA1 Message Date
94b1d7b2ac Bugfixes 2026-04-26 17:41:39 +02:00
1f5f6bc32e Bugfixes 2026-04-26 17:08:12 +02:00
9e39435818 Bugfixes 2026-04-26 15:58:24 +02:00
096ba07b4c Bugfixes 2026-04-26 01:56:28 +02:00
5bcdc2fb5d Personal data and password change 2026-04-26 01:15:58 +02:00
f4ea07d82c Reduction of amount for siblings implemented 2026-04-25 22:33:48 +02:00
21be212129 Reduction of amount for siblings implemented 2026-04-25 22:31:38 +02:00
8348f677a5 Overview of upcoming events 2026-04-25 21:23:32 +02:00
f813056bf7 Widget for own participications 2026-04-25 20:47:34 +02:00
1ee6b9968f Handling ICAL import 2026-04-25 16:50:32 +02:00
6f8be58943 Notification after invoice state 2026-04-25 13:28:23 +02:00
2e8daf78e1 Cronjobs implemented 2026-04-25 00:32:15 +02:00
4878f750bd Improved handling for new invoices 2026-04-18 23:42:39 +02:00
33a9271013 Payment reminder mails 2026-04-18 22:09:57 +02:00
ff98f0860c Manual mails can be sent 2026-04-18 20:52:13 +02:00
ed7f887e3a Participant mangement 2026-04-11 22:17:38 +02:00
e6bd8c684d GUI for Participant management 2026-04-07 22:27:47 +02:00
653e85b781 Show participation details 2026-04-06 20:30:52 +02:00
43f8621053 All Lists finalized 2026-03-29 00:37:03 +01:00
2d17e61cc8 First Aid list, amount list and kitchen list PDF download implemented 2026-03-28 22:28:55 +01:00
7bea223ded First Aid list, amount list and kitchen list PDF download implemented 2026-03-28 22:22:06 +01:00
df7c14442e Fixed bug in real income 2026-03-25 20:55:35 +01:00
33b4a249cc WiP 2026-03-25 20:28:04 +01:00
37039f082c Event identifiers for anonymizations 2026-03-22 00:14:00 +01:00
405591d6dd Signup for events implemented 2026-03-22 00:06:03 +01:00
b8341890d3 Basic signup for events 2026-03-21 21:02:15 +01:00
23af267896 Running 2026-03-20 22:50:22 +01:00
b1c333648a Bugfixes & Model for participants 2026-02-17 21:58:55 +01:00
fcf41c5d13 Creation and editing of events 2026-02-16 21:59:21 +01:00
2b458eccd7 Invoice upload by robot 2026-02-14 00:04:00 +01:00
4f4dff2edd Invoice PAIN & CSV can be uploaded 2026-02-13 22:37:27 +01:00
cd526231ed Bugfix 2026-02-13 13:55:20 +01:00
fa886aad4d Design improvements 2026-02-13 13:51:07 +01:00
f468814a2f Managing own invoices 2026-02-13 12:38:48 +01:00
500 changed files with 193304 additions and 1026 deletions

81
.ai/conventions.md Normal file
View File

@@ -0,0 +1,81 @@
# Projektkonventionen
## Architektur: Actions (Request-Command-Response)
Jede fachliche Operation wird in eine eigene Action ausgelagert, die aus drei Klassen besteht.
Pfad: `app/Domains/{Domain}/Actions/{ActionName}/`
### Struktur
{ActionName}Request.php → Eingabedaten (Konstruktor oder Factory-Methoden) {ActionName}Command.php → Logik, ruft execute(): {ActionName}Response auf {ActionName}Response.php → Rückgabedaten (public Properties)
### Regeln
- Der Controller enthält **keine** fachliche Logik nur Absicherung, Action-Aufruf und HTTP-Response
- Commands sind nicht statisch und werden immer instanziiert
- Hat ein Request mehrere Varianten, werden **Factory-Methoden** (`forX()`) statt mehrerer Konstruktoren verwendet
- Aufrufreihenfolge im Controller: `new Request → new Command(request) → command->execute() → Response verwenden`
---
## Controller
- Alle Controller erben von `App\Scopes\CommonController`
- `CommonController` stellt folgende Repositories bereit (keine eigene Instanziierung nötig):
- `$this->eventParticipants``EventParticipantRepository`
- `$this->events``EventRepository`
- `$this->invoices``InvoiceRepository`
- `$this->costUnits``CostUnitRepository`
- `$this->users``UserRepository`
- `$this->tenant` → aktueller `Tenant`
- Die Controller besitzen ausschließlich eine __invoke() - Funktion
- Für die Speichern-Actions werden separate Controller-Klassen erstellt (z. B. `StoreEventParticipantController`)
---
## Repositories
- Datenbankzugriffe gehören **immer** ins Repository, nie direkt in Controller oder Actions
- Sicherheitschecks (z. B. „gehört diese Teilnahme dem eingeloggten User?") werden als eigene Repository-Methoden gekapselt
- Tenant-Filter: `app('tenant')->slug`
- Eingeloggter User: `auth()->user()`
---
## Models / Ressourcen
- Models erben von `App\Scopes\InstancedModel` (mit globalem `SiteScope`)
- `$model->toResource()->toArray($request)` liefert das aufbereitete Array über die zugehörige Resource-Klasse
- Resource-Klassen liegen in `app/Resources/{ModelName}Resource.php`
---
## Tenant
- Der aktuelle Tenant ist per `app('tenant')` verfügbar (gesetzt durch `IdentifyTenant`-Middleware)
- Tenant-Slug: `app('tenant')->slug`
- Jede tenant-spezifische DB-Abfrage filtert auf `['tenant' => app('tenant')->slug]`
---
## Routing
- API-Routen liegen in `app/Domains/{Domain}/Routes/api.php`
- Alle Routen sind in `IdentifyTenant::class`-Middleware gewrappt
- Authentifizierte Routen zusätzlich in `['auth']`-Middleware
---
## Mails
- Mails erben von `Illuminate\Mail\Mailable`
- Attachments werden über `Attachment::fromData(fn () => $content, $filename)->withMime(...)` angehängt
- Werden Daten sowohl in `content()` als auch in `attachments()` benötigt, wird eine **private Hilfsmethode mit Lazy-Caching** verwendet (einmaliges Berechnen, Ergebnis in private Property speichern)
- Blade-Templates referenzieren Mail-Attachments per `cid:`-Link: `<a href="cid:{{ $filename }}">...</a>`
## Actions
- Die Actions sind in `app/Domains/{Domain}/Actions/{ActionName}/`-Verzeichnissen organisiert
- Jede Action besitzt einen Request, einen Command und einen Response
- Der Request besitzt auschließlich einen Konstruktor, der die notwendigen Parameter annimmt
- Die Response-Klasse enthält ausschließlich die notwendigen Daten für die Antwort
- Die Action-Klasse enthält die Logik für die Verarbeitung der Anfrage und die Generierung der Antwort
- Die Logik wird in einer execute() - Funktion innerhalb des Commands impplementiert. Private Funktionen, für ausgelagerte Prozesse sind zulässig, wenn der Code damit lesbarer wird.

0
.ai/mcp/mcp.json Normal file
View File

12
.junie/AGENTS.md Normal file
View File

@@ -0,0 +1,12 @@
# Project Guidelines
This is a placeholder of the project guidelines for Junie.
Replace this text with any project-level instructions for Junie, e.g.:
* What is the project structure
* Whether Junie should run tests to check the correctness of the proposed solution
* How does Junie run tests (once it requires any non-standard approach)
* Whether Junie should build the project before submitting the result
* Any code-style related instructions
As an option you can ask Junie to create these guidelines for you.

34
app/Casts/AmountCast.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Casts;
use App\ValueObjects\Amount;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class AmountCast implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): ?Amount
{
if ($value === null) {
return null;
}
return new Amount((float) $value, 'Euro');
}
public function set(Model $model, string $key, mixed $value, array $attributes): ?float
{
if ($value === null) {
return null;
}
if ($value instanceof Amount) {
return $value->getAmount();
}
return (float) $value;
}
}

View File

@@ -22,8 +22,13 @@ class CreateCostUnitCommand {
'mail_on_new' => $this->request->mailOnNew,
'allow_new' => true,
'archived' => false,
]);
if (null !== $costUnit) {
$response->costUnit = $costUnit;
$response->success = true;
}
return $response;
}
}

View File

@@ -2,6 +2,14 @@
namespace App\Domains\CostUnit\Actions\CreateCostUnit;
class CreateCostUnitResponse {
use App\Models\CostUnit;
class CreateCostUnitResponse {
public bool $success;
public ?CostUnit $costUnit;
public function __construct() {
$this->success = false;
$this->costUnit = null;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest;
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceRequest;
use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptCommand;
use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptRequest;
use App\Enumerations\InvoiceStatus;
use App\Models\Tenant;
use App\Providers\FileWriteProvider;
use App\Providers\InvoiceCsvFileProvider;
use App\Providers\PainFileProvider;
use App\Providers\WebDavProvider;
use App\Providers\ZipArchiveFileProvider;
use App\Scopes\CommonController;
use Illuminate\Support\Facades\Storage;
class ExportController extends CommonController {
public function __invoke(int $costUnitId) {
$costUnit = $this->costUnits->getById($costUnitId);
$invoicesForExport = $this->invoices->getByStatus($costUnit, InvoiceStatus::INVOICE_STATUS_APPROVED, false);
$webdavProvider = new WebDavProvider(WebDavProvider::INVOICE_PREFIX . $this->tenant->url . '/' . $costUnit->name);
$painFileData = $this->painData($invoicesForExport);
$csvData = $this->csvData($invoicesForExport);
$filePrefix = Tenant::getTempDirectory();
$painFileWriteProvider = new FileWriteProvider($filePrefix . 'abrechnungen-' . date('Y-m-d_H-i') . '-sepa.xml', $painFileData);
$painFileWriteProvider->writeToFile();
$csvFileWriteProvider = new FileWriteProvider($filePrefix . 'abrechnungen-' . date('Y-m-d_H-i') . '.csv', $csvData);
$csvFileWriteProvider->writeToFile();
if ($this->tenant->upload_exports) {
$webdavProvider->uploadFile($painFileWriteProvider->fileName);
$webdavProvider->uploadFile($csvFileWriteProvider->fileName);
}
$downloadZipArchiveFiles = [
$painFileWriteProvider->fileName,
$csvFileWriteProvider->fileName
];
foreach ($invoicesForExport as $invoice) {
$changeStatusRequest = new ChangeStatusRequest($invoice, InvoiceStatus::INVOICE_STATUS_EXPORTED);
$changeStatusCommand = new ChangeStatusCommand($changeStatusRequest);
$changeStatusCommand->execute();
if ($this->tenant->download_exports) {
$createInvoiceReceiptRequest = new CreateInvoiceReceiptRequest($invoice);
$createInvoiceReceiptCommand = new CreateInvoiceReceiptCommand($createInvoiceReceiptRequest);
$response = $createInvoiceReceiptCommand->execute();
$downloadZipArchiveFiles[] = $response->fileName;
}
}
if ($this->tenant->download_exports) {
$zipFile = Tenant::getTempDirectory() . 'Abrechnungen-' . $costUnit->name . '.zip';
$zipFileProvider = new ZipArchiveFileProvider($zipFile);
foreach ($downloadZipArchiveFiles as $file) {
$zipFileProvider->addFile($file);
}
$zipFileProvider->create();
foreach ($downloadZipArchiveFiles as $file) {
Storage::delete($file);
}
Storage::delete($painFileWriteProvider->fileName);
Storage::delete($csvFileWriteProvider->fileName);
return response()->download(
storage_path('app/private/' . $zipFile),
basename($zipFile),
['Content-Type' => 'application/zip']
);
}
Storage::delete($painFileWriteProvider->fileName);
Storage::delete($csvFileWriteProvider->fileName);
return response()->json([
'message' => 'Die Abrechnungen wurden exportiert.' . PHP_EOL .'Die Belege werden asynchron auf dem Webdav-Server hinterlegt.' . PHP_EOL . PHP_EOL . 'Sollten diese in 15 Minuten nicht vollständig sein, kontaktiere den Adminbistrator.'
]);
}
private function painData(array $invoices) : string {
$invoicesForPainFile = [];
foreach ($invoices as $invoice) {
if ($invoice->contact_bank_owner !== null && $invoice->contact_bank_iban !== '' && !$invoice->donation) {
$invoicesForPainFile[] = $invoice;
}
}
$painFileProvider = new PainFileProvider($this->tenant->account_iban, $this->tenant->account_name, $this->tenant->account_bic, $invoicesForPainFile);
return $painFileProvider->createPainFileContent();
}
public function csvData(array $invoices) : string {
$csvDateProvider = new InvoiceCsvFileProvider($invoices);
return $csvDateProvider->createCsvFileContent();
}
}

View File

@@ -8,8 +8,11 @@ use Illuminate\Http\JsonResponse;
class OpenController extends CommonController {
public function __invoke(int $costUnitId) {
$costUnit = $this->costUnits->getById($costUnitId);
$inertiaProvider = new InertiaProvider('CostUnit/Open', [
'costUnitId' => $costUnitId
'costUnit' => $costUnit
]);
return $inertiaProvider->render();
}

View File

@@ -3,6 +3,7 @@ use App\Domains\CostUnit\Controllers\ChangeStateController;
use App\Domains\CostUnit\Controllers\CreateController;
use App\Domains\CostUnit\Controllers\DistanceAllowanceController;
use App\Domains\CostUnit\Controllers\EditController;
use App\Domains\CostUnit\Controllers\ExportController;
use App\Domains\CostUnit\Controllers\ListController;
use App\Domains\CostUnit\Controllers\OpenController;
use App\Domains\CostUnit\Controllers\TreasurersEditController;
@@ -37,6 +38,7 @@ Route::prefix('api/v1')
Route::get('/treasurers', TreasurersEditController::class);
Route::post('/treasurers', [TreasurersEditController::class, 'update']);
Route::get('/export-payouts', ExportController::class);
});

View File

@@ -8,11 +8,11 @@
import ListInvoices from "./Partials/ListInvoices.vue";
const props = defineProps({
costUnitId: Number
costUnit: Object
})
const urlParams = new URLSearchParams(window.location.search)
const initialCostUnitId = props.cost_unit_id
const initialCostUnitId = props.costUnit.id
const initialInvoiceId = props.invoice_id
@@ -22,7 +22,7 @@
{
title: 'Neue Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/new",
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/new",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
@@ -30,21 +30,21 @@
{
title: 'Nichtexportierte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/approved",
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/approved",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Exportierte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/exported",
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/exported",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Abgelehnte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/denied",
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/denied",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
@@ -58,7 +58,7 @@
</script>
<template>
<AppLayout title="Abrechnungen">
<AppLayout :title="'Abrechnungen ' + props.costUnit.name">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<tabbed-page :tabs="tabs" :initial-tab-id="initialCostUnitId" :initial-sub-tab-id="initialInvoiceId" />

View File

@@ -1,19 +1,11 @@
<script setup>
import {createApp, ref} from 'vue'
/*import {
_mareike_download_as_zip,
_mareike_use_webdav
} from "../../../assets/javascripts/library";*/
//import LoadingModal from "../../../assets/components/LoadingModal.vue";
//import Invoices from '../invoices/index.vue'
import LoadingModal from "../../../../Views/Components/LoadingModal.vue";
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import CostUnitDetails from "./CostUnitDetails.vue";
import {toast} from "vue3-toastify";
import Treasurers from "./Treasurers.vue";
//import CostUnitDetails from "./CostUnitDetails.vue";
const props = defineProps({
data: {
type: [Array, Object],
@@ -42,13 +34,8 @@ const show_cost_unit = ref(false)
const showTreasurers = ref(false)
const costUnit = ref(null)
const { data, loading, error, request, download } = useAjax()
if (props.deep_jump_id > 0) {
// open_invoice_list(props.deep_jump_id, 'new', props.deep_jump_id_sub)
}
async function costUnitDetails(costUnitId) {
const data = await request('/api/v1/cost-unit/' + costUnitId + '/details', {
method: "GET",
@@ -114,41 +101,45 @@ async function changeCostUnitState(costUnitId, endPoint) {
}
async function export_payouts(cost_unit_id) {
async function exportPayouts(costUnitId) {
showLoading.value = true;
const response = await fetch('/api/v1/core/retrieve-global-data');
const data = await response.json();
const exportUrl = '/api/v1/cost-unit/' + costUnitId + '/export-payouts';
try {
if (_mareike_download_as_zip()) {
const response = await fetch("/wp-json/mareike/costunits/export-payouts", {
method: "POST",
if (data.tenant.download_exports) {
const response = await fetch(exportUrl, {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
mareike_nonce: _mareike_nonce(),
costunit: cost_unit_id,
}),
});
if (!response.ok) throw new Error('Fehler beim Export (ZIP)');
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = downloadUrl;
a.download = `payouts-${cost_unit_id}.zip`;
a.download = "Abrechnungen-Sippenstunden.zip";
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => {
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
}, 100);
} else {
await request("/wp-json/mareike/costunits/export-payouts", {
method: "POST",
body: {
mareike_nonce: _mareike_nonce(),
costunit: cost_unit_id,
}
const response = await request(exportUrl, {
method: "GET",
});
toast.success(response.message);
}
showLoading.value = false;
toast.success('Die Abrechnungen wurden exportiert.');
} catch (err) {
showLoading.value = false;
toast.error('Beim Export der Abrechnungen ist ein Fehler aufgetreten.');
@@ -176,7 +167,7 @@ async function export_payouts(cost_unit_id) {
<input v-if="!costUnit.archived" type="button" value="Abrechnungen bearbeiten" @click="loadInvoices(costUnit.id)" />
<input v-else type="button" value="Abrechnungen einsehen" />
<br />
<input v-if="!costUnit.archived" type="button" value="Genehmigte Abrechnungen exportieren" style="margin-top: 10px;" />
<input v-if="!costUnit.archived" type="button" @click="exportPayouts(costUnit.id)" value="Genehmigte Abrechnungen exportieren" style="margin-top: 10px;" />
</td>
</tr>
@@ -217,17 +208,10 @@ async function export_payouts(cost_unit_id) {
<Treasurers :data="costUnit" :showTreasurers="showTreasurers" v-if="showTreasurers" @closeTreasurers="showTreasurers = false" />
</div>
<div v-else-if="showInvoiceList">
<invoices :data="invoices" :load_invoice_id="props.deep_jump_id_sub" :cost_unit_id="current_cost_unit" />
<LoadingModal :show="showLoading" v-if="_mareike_use_webdav()" message="Die PDF-Dateien werden asynchron erzeugt, diese sollten in 10 Minuten auf dem Webdav-Server liegen', 'mareike')" />
<LoadingModal :show="showLoading" v-else message='Die Abrechnungen werden exportiert, bitte warten.' />
</div>
<div v-else>
@@ -236,7 +220,7 @@ async function export_payouts(cost_unit_id) {
</strong>
</div>
<LoadingModal :show="showLoading" />
</template>
<style scoped>

View File

@@ -23,7 +23,7 @@ const commonProps = reactive({
onMounted(async () => {
const response = await fetch('/api/v1/retreive-global-data');
const response = await fetch('/api/v1/core/retrieve-global-data');
const data = await response.json();
Object.assign(commonProps, data);
@@ -63,8 +63,6 @@ async function updateCostUnit() {
toast.error(data.message);
}
}
console.log(props.data.treasurers)
</script>
<template>
@@ -74,8 +72,7 @@ console.log(props.data.treasurers)
title="Schatzis zuweisen"
@close="emit('closeTreasurers')"
>
Zuständige Schatzis:
<h3>Zuständige Schatzis:</h3>
<p v-for="user in commonProps.activeUsers">
<input
type="checkbox"

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Domains\Dashboard\Actions\UpdatePersonalData;
use App\Repositories\UserRepository;
class UpdatePersonalDataCommand
{
public function __construct(
private readonly UpdatePersonalDataRequest $request,
private readonly UserRepository $users
) {}
public function execute(): UpdatePersonalDataResponse
{
$this->users->updatePersonalData($this->request);
$response = new UpdatePersonalDataResponse();
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Domains\Dashboard\Actions\UpdatePersonalData;
use App\Models\User;
class UpdatePersonalDataRequest
{
public function __construct(
public readonly User $user,
public readonly ?string $nickname,
public readonly ?string $email,
public readonly ?string $phone,
public readonly ?string $address1,
public readonly ?string $address2,
public readonly ?string $postcode,
public readonly ?string $city,
public readonly ?string $birthday,
public readonly ?string $tetanusVaccination,
public readonly ?string $medications,
public readonly ?string $allergies,
public readonly ?string $intolerances,
public readonly ?string $eatingHabits,
public readonly ?string $swimmingPermission,
public readonly ?string $firstAidPermission,
public readonly ?string $bankAccountOwner,
public readonly ?string $bankAccountIban,
) {}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Domains\Dashboard\Actions\UpdatePersonalData;
class UpdatePersonalDataResponse
{
public bool $success;
}

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) {
@@ -21,7 +19,11 @@ class DashboardController extends CommonController {
private function renderForLoggedInUser(Request $request) {
$authCheckProvider = new AuthCheckProvider;
$inertiaProvider = new InertiaProvider('Dashboard/Dashboard', ['appName' => app('tenant')->name]);
$inertiaProvider = new InertiaProvider('Dashboard/Dashboard', [
'myInvoices' => $this->invoices->getMyInvoicesWidget(),
'myParticipations' => $this->eventParticipants->getMyParticipations(),
]);
return $inertiaProvider->render();
}
@@ -37,4 +39,12 @@ class DashboardController extends CommonController {
$costUnits = $this->costUnits->listForSummary(5);
return response()->json(['openCostUnits' => $costUnits]);
}
public function getMyParticipations() : JsonResponse {
return response()->json(['myParticipations' => $this->eventParticipants->getMyParticipations()]);
}
public function getUpcomingEvents() : JsonResponse {
return response()->json(['upcomingEvents' => $this->events->getUpcoming()]);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Domains\Dashboard\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
class MessagesController extends CommonController {
public function __invoke() {
$inertiaProvider = new InertiaProvider('Dashboard/Messages', []);
return $inertiaProvider->render();
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Domains\Dashboard\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
class PersonalDataController extends CommonController
{
public function __invoke()
{
if (!$this->checkAuth()) {
return redirect()->intended('/login');
}
$user = auth()->user();
$data = $this->users->getPersonalData($user);
$inertiaProvider = new InertiaProvider('Dashboard/PersonalData', [
'personalData' => [
'firstname' => $data['firstname'],
'lastname' => $data['lastname'],
'birthday' => $data['birthday'],
'nickname' => $data['nickname'],
'email' => $data['email'],
'phone' => $data['phone'],
'address1' => $data['address_1'],
'address2' => $data['address_2'],
'postcode' => $data['postcode'],
'city' => $data['city'],
'medications' => $data['medications'],
'allergies' => $data['allergies'],
'intolerances' => $data['intolerances'],
'eatingHabits' => $data['eating_habits'],
'swimmingPermission' => $data['swimming_permission'],
'firstAidPermission' => $data['first_aid_permission'],
'bankAccountOwner' => $data['bank_account_owner'],
'bankAccountIban' => $data['bank_account_iban'],
'tetanusVaccination' => $data['tetanus_vaccination'],
],
]);
return $inertiaProvider->render();
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Domains\Dashboard\Controllers;
use App\Domains\Dashboard\Actions\UpdatePersonalData\UpdatePersonalDataCommand;
use App\Domains\Dashboard\Actions\UpdatePersonalData\UpdatePersonalDataRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class StorePersonalDataController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$user = auth()->user();
$actionRequest = new UpdatePersonalDataRequest(
user: $user,
nickname: $request->input('nickname'),
email: $request->input('email'),
phone: $request->input('phone'),
address1: $request->input('address1'),
address2: $request->input('address2'),
postcode: $request->input('postcode'),
city: $request->input('city'),
medications: $request->input('medications'),
allergies: $request->input('allergies'),
intolerances: $request->input('intolerances'),
eatingHabits: $request->input('eatingHabits'),
swimmingPermission: $request->input('swimmingPermission'),
firstAidPermission: $request->input('firstAidPermission'),
bankAccountOwner: $request->input('bankAccountOwner'),
bankAccountIban: $request->input('bankAccountIban'),
birthday: $request->input('birthday'),
tetanusVaccination: $request->input('tetanusVaccination'),
);
$command = new UpdatePersonalDataCommand($actionRequest, $this->users);
$command->execute();
return response()->json(['success' => true, 'message' => 'Deine Daten wurden erfolgreich gespeichert.']);
}
}

View File

@@ -1,6 +1,7 @@
<?php
use App\Domains\Dashboard\Controllers\DashboardController;
use App\Domains\Dashboard\Controllers\StorePersonalDataController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
@@ -9,7 +10,12 @@ Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('api/v1/dashboard')->group(function () {
Route::get('/my-invoices', [DashboardController::class, 'getMyInvoices']);
Route::get('/open-cost-units', [DashboardController::class, 'getOpenCostUnits']);
Route::get('/upcoming-events', [DashboardController::class, 'getUpcomingEvents']);
Route::get('/my-participations', [DashboardController::class, 'getMyParticipations']);
Route::post('/personal-data', StorePersonalDataController::class);
});
});
});

View File

@@ -1 +1,16 @@
<?php
use App\Domains\Dashboard\Controllers\DashboardController;
use App\Domains\Dashboard\Controllers\MessagesController;
use App\Domains\Dashboard\Controllers\PersonalDataController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::middleware(['auth'])->group(function () {
Route::get('/personal-data', PersonalDataController::class);
Route::get('/messages', MessagesController::class);
});
});

View File

@@ -1,19 +1,33 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import {onMounted} from "vue";
import {toast} from "vue3-toastify";
import MyInvoices from "./Partials/Widgets/MyInvoices.vue";
import MyParticipations from "./Partials/Widgets/MyParticipations.vue";
const props = defineProps({
myInvoices: Object,
myParticipations: Object,
})
function newInvoice() {
window.location.href = '/invoice/new';
}
</script>
<template>
<AppLayout title='Dashboard'>
<diV class="dashboard-widget-container">
<shadowed-box class="dashboard-widget-box" style="width: 60%;">
Meine Anmeldungen
<MyParticipations />
</shadowed-box>
<shadowed-box class="dashboard-widget-box">
Meine Abrechnungen
<shadowed-box class="dashboard-widget-box" style="height: 275px;">
<MyInvoices />
<input type="button" value="Neue Abrechnung" @click="newInvoice" style="margin-top: 20px;">
</shadowed-box>
</diV>
</AppLayout>
@@ -31,7 +45,17 @@ import {toast} from "vue3-toastify";
.dashboard-widget-box {
flex-grow: 1; display: inline-block;
height: 150px;
margin: 0 10px;
}
.dashboard-widget-box h2 {
border-color: #c0c0c0;
border-left-width: 40px;
border-left-style: solid;
border-bottom-style: solid;
border-bottom-width: 1px;
padding: 5px 10px;
font-size: 13pt;
}
</style>

View File

@@ -0,0 +1,25 @@
<script setup>
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
</script>
<template>
<AppLayout title='Meine Nachrichten'>
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
Diese Funktion steht aktuell nicht zur Verfügung.<br />
Bitte versuche es später noch einmal.
</shadowed-box>
</AppLayout>
</template>
<style scoped>
textarea {
width: 100%;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.95rem;
box-sizing: border-box;
resize: vertical;
}
</style>

View File

@@ -16,8 +16,9 @@ onMounted(async () => {
</script>
<template>
<h2>Meine Abrechnungen</h2>
<p v-for="invoice in myInvoices.myInvoices" class="widget-content-item">
<a :href="'/invoices/my-invoices/' + invoice.slug" class="link">{{invoice.title}} ({{invoice.count}})</a>
<a :href="'/invoice/my-invoices/' + invoice.slug" class="link">{{invoice.title}} ({{invoice.count}})</a>
<label>
{{invoice.amount}}
</label>

View File

@@ -0,0 +1,71 @@
<script setup>
import {onMounted, reactive} from "vue";
import Icon from "../../../../../Views/Components/Icon.vue";
const myParticipations = reactive({
'myParticipations': '',
})
onMounted(async () => {
const response = await fetch('/api/v1/dashboard/my-participations');
const data = await response.json();
Object.assign(myParticipations, data);
});
function navigateTo(url) {
window.location.href = url;
}
</script>
<template>
<h2>Meine Anmeldungen</h2>
<p v-for="participation in myParticipations.myParticipations" class="widget-content-item">
<table>
<tr>
<td style="width: 40%; font-weight: bold;">{{participation.eventName}}</td>
<td style="width: 30%; font-weight: bold;">{{participation.arrivalDateReadable}} - {{participation.departureDateReadable}}</td>
<td style="width: 30%;">
<Icon name="euro-sign" style="padding: 5px; font-size: 11pt; color: #ffffff; margin-right: 5px;" :class="participation.needs_payment ? 'bg-red' : 'bg-green'" />
<Icon name="award" style="padding: 5px; font-size: 11pt; color: #ffffff; margin-right: 5px;" :class="participation.cocColor" />
</td>
</tr>
<tr>
<td>
{{participation.event.postal_code}} {{participation.event.location}}<br />
</td>
<td>
<a class="link" :href="`/api/v1/event/participant/${participation.identifier}/ical`">In Kalender importieren</a>
</td>
<td>
{{participation.amountPaid.readable}} / {{participation.amountExpected.readable}}
</td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
<td>eFZ-Status: {{participation.efzStatusReadable}}</td>
</tr>
</table>
</p>
<p v-if="myParticipations.myParticipations.length === 0">Du bist aktuelle für keine Veranstaltung angemeldet.</p>
<p>
<input type="button" value="Jetzt anmelden" class="button" @click="navigateTo('/event/available-events')" />
</p>
</template>
<style scoped>
.bg-red {
background-color: red;
}
.bg-green {
background-color: green;
}
.bg-yellow {
background-color: #e4e44c;
}
</style>

View File

@@ -0,0 +1,54 @@
<script setup>
import {onMounted, reactive} from "vue";
import Icon from "../../../../../Views/Components/Icon.vue";
const myParticipations = reactive({
'myParticipations': '',
})
onMounted(async () => {
const response = await fetch('/api/v1/dashboard/my-participations');
const data = await response.json();
Object.assign(myParticipations, data);
});
function navigateTo(url) {
window.location.href = url;
}
</script>
<template>
<table v-if="myParticipations.myParticipations.length > 0">
<tr v-for="participation in myParticipations.myParticipations.slice(0, 3)" class="widget-content-item">
<td>
{{participation.eventName}}<br />
{{participation.event.location}},
{{participation.arrivalDateReadable}} - {{participation.departureDateReadable}}
</td>
<td>
<Icon name="euro-sign" style="padding: 2px; font-size: 10pt; color: #ffffff; margin-right: 5px;" :class="participation.needs_payment ? 'bg-red' : 'bg-green'" />
<Icon name="award" style="padding: 2px; font-size: 10pt; color: #ffffff; margin-right: 5px;" :class="participation.cocColor" />
</td>
</tr>
</table>
<p v-else>
Du bist aktuelle für keine Veranstaltung angemeldet.<br /><br />
<input type="button" value="Jetzt anmelden" class="button" @click="navigateTo('/event/available-events')" />
</p>
</template>
<style scoped>
.bg-red {
background-color: red;
}
.bg-green {
background-color: green;
}
.bg-yellow {
background-color: #e4e44c;
}
</style>

View File

@@ -0,0 +1,40 @@
<script setup>
import {onMounted, reactive} from "vue";
const events = reactive({
'upcomingEvents': '',
})
onMounted(async () => {
const response = await fetch('/api/v1/dashboard/upcoming-events');
const data = await response.json();
Object.assign(events, data);
});
</script>
<template>
<table class="widget-content-item" v-if="events.upcomingEvents.length > 0">
<tr>
<td style="font-weight: bold">Veranstaltung</td>
<td style="font-weight: bold">Teilis</td>
<td style="font-weight: bold">Team</td>
<td style="font-weight: bold">GruFüs</td>
</tr>
<tr v-for="event in events.upcomingEvents">
<td><a :href="'/event/details/' + event.identifier" class="link">{{event.nameShort}}</a></td>
<td style="text-align: center;">{{event.countParticipant}}</td>
<td style="text-align: center;">{{event.countTeam}}</td>
<td style="text-align: center;">{{event.countVolunteer}}</td>
</tr>
</table>
<p v-else style="padding: 10px; font-weight: bold">Es existieren im Moment keine Veranstaltungen, für die du verantwortlich bist.</p>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,187 @@
<script setup>
import { reactive, ref } from 'vue'
import { request } from '../../../../resources/js/components/HttpClient.js'
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import {toast} from "vue3-toastify";
import IbanInput from "../../../Views/Components/IbanInput.vue";
const props = defineProps({
personalData: Object,
})
const form = reactive({
nickname: props.personalData.nickname ?? '',
email: props.personalData.email ?? '',
phone: props.personalData.phone ?? '',
address1: props.personalData.address1 ?? '',
address2: props.personalData.address2 ?? '',
postcode: props.personalData.postcode ?? '',
city: props.personalData.city ?? '',
birthday: props.personalData.birthday ?? '',
tetanusVaccination: props.personalData.tetanusVaccination ?? '',
medications: props.personalData.medications ?? '',
allergies: props.personalData.allergies ?? '',
intolerances: props.personalData.intolerances ?? '',
eatingHabits: props.personalData.eatingHabits ?? '',
swimmingPermission: props.personalData.swimmingPermission ?? '',
firstAidPermission: props.personalData.firstAidPermission ?? '',
bankAccountOwner: props.personalData.bankAccountOwner ?? '',
bankAccountIban: props.personalData.bankAccountIban ?? '',
})
const saving = ref(false)
const successMessage = ref('')
const errorMessage = ref('')
const submit = async () => {
saving.value = true
successMessage.value = ''
errorMessage.value = ''
const result = await request('/api/v1/dashboard/personal-data', {
method: 'POST',
body: { ...form },
})
saving.value = false
if (result?.success) {
toast.success(result.message)
} else {
toast.error(result.message)
}
}
</script>
<template>
<AppLayout title='Persönliche Daten'>
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<div class="max-w-2xl mx-auto p-6">
<form @submit.prevent="submit">
<table class="form-table" style="width: 90%; margin: 10px;">
<!-- Nicht veränderbare Felder -->
<tr>
<td style="width: 200px; padding: 5px;">Vorname:</td>
<td><span class="text-gray-700">{{ personalData.firstname }}</span></td>
</tr>
<tr>
<td style="width: 200px; padding: 5px;">Nachname:</td>
<td><span class="text-gray-700">{{ personalData.lastname }}</span></td>
</tr>
<!-- Veränderbare Felder -->
<tr>
<td>Pfadiname:</td>
<td><input type="text" v-model="form.nickname" /></td>
</tr>
<tr>
<td>E-Mail:</td>
<td><input type="email" v-model="form.email" /></td>
</tr>
<tr>
<td>Telefon:</td>
<td><input type="text" v-model="form.phone" /></td>
</tr>
<tr>
<td>Straße / Hausnummer:</td>
<td><input type="text" v-model="form.address1" /></td>
</tr>
<tr>
<td>Adresszusatz:</td>
<td><input type="text" v-model="form.address2" /></td>
</tr>
<tr>
<td>PLZ:</td>
<td><input type="text" v-model="form.postcode" /></td>
</tr>
<tr>
<td>Ort:</td>
<td><input type="text" v-model="form.city" /></td>
</tr>
<tr>
<td style="width: 200px; padding: 5px;">Geburtsdatum:</td>
<td><input type="date" v-model="form.birthday" /></td>
</tr>
<tr>
<td>Medikamente:</td>
<td><input type="text" v-model="form.medications" /></td>
</tr>
<tr>
<td>Allergien:</td>
<td><input type="text" v-model="form.allergies" /></td>
</tr>
<tr>
<td>Unverträglichkeiten:</td>
<td><input type="text" v-model="form.intolerances" /></td>
</tr>
<tr>
<td>Letzte Tetanus-Impfung:</td>
<td><input type="date" v-model="form.tetanusVaccination" /></td>
</tr>
<tr>
<td>Ernährungsgewohnheiten:</td>
<td>
<select v-model="form.eatingHabits">
<option value="EATING_HABIT_VEGAN">Vegan</option>
<option value="EATING_HABIT_VEGETARIAN">Vegetarisch</option>
<option value="EATING_HABIT_OMNIVOR">Omnivor</option>
</select>
</td>
</tr>
<tr>
<td>Badeerlaubnis:</td>
<td>
<select v-model="form.swimmingPermission">
<option value="SWIMMING_PERMISSION_ALLOWED">Erteilt, kann schwimmen</option>
<option value="SWIMMING_PERMISSION_LIMITED">Erteilt, kann nicht schwimmen</option>
<option value="SWIMMING_PERMISSION_DENIED">Nicht erteilt</option>
</select>
</td>
</tr>
<tr>
<td>Erste-Hilfe-Erlaubnis:</td>
<td>
<select v-model="form.firstAidPermission">
<option value="FIRST_AID_PERMISSION_ALLOWED">Erweiterte Erste Hilfe erlaubt</option>
<option value="FIRST_AID_PERMISSION_DENIED">Erweiterte Erste Hilfe verweigert</option>
</select>
</td>
</tr>
<tr>
<td>Kontoinhaber*in:</td>
<td><input type="text" v-model="form.bankAccountOwner" /></td>
</tr>
<tr>
<td>IBAN:</td>
<td><IbanInput v-model="form.bankAccountIban" /></td>
</tr>
<tr>
<td colspan="2" class="btn-row" style="padding-top: 20px;">
<button type="submit" class="button" :disabled="saving">
{{ saving ? 'Wird gespeichert…' : 'Speichern' }}
</button>
</td>
</tr>
</table>
</form>
</div>
</shadowed-box>
</AppLayout>
</template>
<style scoped>
textarea {
width: 100%;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.95rem;
box-sizing: border-box;
resize: vertical;
}
</style>

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Domains\Event\Actions\CertificateOfConductionCheck;
use App\Enumerations\EfzStatus;
use Illuminate\Support\Facades\Http;
class CertificateOfConductionCheckCommand {
function __construct(public CertificateOfConductionCheckRequest $request)
{}
public function execute() : CertificateOfConductionCheckResponse {
$response = new CertificateOfConductionCheckResponse();
$localGroup = str_replace('Stamm ', '', $this->request->participant->localGroup()->first()->name);
$apiResponse = Http::acceptJson()
->asJson()
->withoutVerifying()
->post(env('COC_CHECK_URL'), [
'firstName' => trim($this->request->participant->firstname),
'lastName' => $this->request->participant->lastname,
'nickname' => $this->request->participant->nickname,
'address' => trim($this->request->participant->address_1 . ' ' . $this->request->participant->address_2),
'zip' => $this->request->participant->zip,
'city' => $this->request->participant->city,
'birthday' => $this->request->participant->birthday->format('Y-m-d'),
'email' => $this->request->participant->email_1,
'localGroup' => $localGroup,
'checkForDate' => $this->request->participant->departure_date->format('Y-m-d'),
]);
if (! $apiResponse->successful()) {
return $response;
}
$responseParsed = $apiResponse->json();
if (($responseParsed['status'] ?? null) !== '200/ok') {
return $response;
}
$response->status = match ($responseParsed['efzStatus']) {
'NOT_REQUIRED' => EfzStatus::EFZ_STATUS_NOT_REQUIRED,
'CHECKED_VALID' => EfzStatus::EFZ_STATUS_CHECKED_VALID,
'CHECKED_INVALID' => EfzStatus::EFZ_STATUS_CHECKED_INVALID,
default => EfzStatus::EFZ_STATUS_NOT_CHECKED
};
return $response;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Event\Actions\CertificateOfConductionCheck;
use App\Models\EventParticipant;
class CertificateOfConductionCheckRequest {
function __construct(public EventParticipant $participant)
{
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Domains\Event\Actions\CertificateOfConductionCheck;
use App\Enumerations\EfzStatus;
class CertificateOfConductionCheckResponse {
public string $status;
public function __construct()
{
$this->status = EfzStatus::EFZ_STATUS_NOT_CHECKED;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Domains\Event\Actions\CreateEvent;
use App\Enumerations\EatingHabit;
use App\Models\Event;
use App\Models\Tenant;
use App\RelationModels\EventEatingHabits;
use App\RelationModels\EventLocalGroups;
use Illuminate\Support\Str;
class CreateEventCommand {
private CreateEventRequest $request;
public function __construct(CreateEventRequest $request) {
$this->request = $request;
}
public function execute(): CreateEventResponse {
$response = new CreateEventResponse();
$prefix = $this->request->begin->format('Y-m_');
if (!str_starts_with($this->request->name, $prefix)) {
$this->request->name = $prefix . $this->request->name;
}
$event = Event::create([
'tenant' => app('tenant')->slug,
'name' => $this->request->name,
'identifier' => Str::random(10),
'location' => $this->request->location,
'postal_code' => $this->request->postalCode,
'email' => $this->request->email,
'start_date' => $this->request->begin,
'end_date' => $this->request->end,
'early_bird_end' => $this->request->earlyBirdEnd,
'registration_final_end' => $this->request->registrationFinalEnd,
'early_bird_end_amount_increase' => $this->request->earlyBirdEndAmountIncrease,
'account_owner' => $this->request->accountOwner,
'account_iban' => $this->request->accountIban,
'participation_fee_type' => $this->request->participationFeeType->slug,
'pay_per_day' => $this->request->payPerDay,
'pay_direct' => $this->request->payDirect,
'total_max_amount' => 0,
'support_per_person' => 0,
'support_flat' => 0,
]);
if ($event !== null) {
EventEatingHabits::create([
'event_id' => $event->id,
'eating_habit_id' => EatingHabit::where('slug', EatingHabit::EATING_HABIT_VEGAN)->first()->id,
]);
EventEatingHabits::create([
'event_id' => $event->id,
'eating_habit_id' => EatingHabit::where('slug', EatingHabit::EATING_HABIT_VEGETARIAN)->first()->id,
]);
if (app('tenant')->slug === 'lv') {
foreach(Tenant::where(['is_active_local_group' => true])->get() as $tenant) {
EventLocalGroups::create(['event_id' => $event->id, 'local_group_id' => $tenant->id]);
}
} else {
EventLocalGroups::create(['event_id' => $event->id, 'local_group_id' => app('tenant')->id]);
}
$response->success = true;
$response->event = $event;
}
return $response;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Domains\Event\Actions\CreateEvent;
use App\Enumerations\ParticipationFeeType;
use DateTime;
class CreateEventRequest {
public string $name;
public string $location;
public string $postalCode;
public string $email;
public DateTime $begin;
public DateTime $end;
public DateTime $earlyBirdEnd;
public DateTime $registrationFinalEnd;
public int $earlyBirdEndAmountIncrease;
public ParticipationFeeType $participationFeeType;
public string $accountOwner;
public string $accountIban;
public bool $payPerDay;
public bool $payDirect;
public function __construct(string $name, string $location, string $postalCode, string $email, DateTime $begin, DateTime $end, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $earlyBirdEndAmountIncrease, ParticipationFeeType $participationFeeType, string $accountOwner, string $accountIban, bool $payPerDay, bool $payDirect) {
$this->name = $name;
$this->location = $location;
$this->postalCode = $postalCode;
$this->email = $email;
$this->begin = $begin;
$this->end = $end;
$this->earlyBirdEnd = $earlyBirdEnd;
$this->registrationFinalEnd = $registrationFinalEnd;
$this->earlyBirdEndAmountIncrease = $earlyBirdEndAmountIncrease;
$this->participationFeeType = $participationFeeType;
$this->accountOwner = $accountOwner;
$this->accountIban = $accountIban;
$this->payPerDay = $payPerDay;
$this->payDirect = $payDirect;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Domains\Event\Actions\CreateEvent;
use App\Models\Event;
class CreateEventResponse {
public bool $success;
public ?Event $event;
public function __construct() {
$this->success = false;
$this->event = null;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcal;
class GenerateIcalCommand
{
public function __construct(public GenerateIcalRequest $request)
{
}
public function execute(): GenerateIcalResponse
{
$participant = $this->request->participant;
$event = $participant->event;
$uid = $participant->identifier . '@' . app('tenant')->slug;
$dtStart = $event->start_date->format('Ymd');
$dtEnd = $event->end_date->copy()->addDay()->format('Ymd');
$now = now()->format('Ymd\THis\Z');
$summary = $this->escapeIcal($event->name);
$location = $this->escapeIcal(trim($event->postal_code . ' ' . $event->location));
$description = $this->escapeIcal('Teilnahme als: ' . $participant->getOfficialName());
$icalContent = implode("\r\n", [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//' . app('tenant')->name . '//Veranstaltungskalender//DE',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'BEGIN:VEVENT',
'UID:' . $uid,
'DTSTAMP:' . $now,
'DTSTART;VALUE=DATE:' . $dtStart,
'DTEND;VALUE=DATE:' . $dtEnd,
'SUMMARY:' . $summary,
'LOCATION:' . $location,
'DESCRIPTION:' . $description,
'END:VEVENT',
'END:VCALENDAR',
]) . "\r\n";
$filename = 'veranstaltung-' . $participant->event()->first()->name . '.ics';
return new GenerateIcalResponse($icalContent, $filename);
}
private function escapeIcal(string $value): string
{
return str_replace(
['\\', ';', ',', "\n"],
['\\\\', '\\;', '\\,', '\\n'],
$value
);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcal;
use App\Models\EventParticipant;
class GenerateIcalRequest
{
public function __construct(public EventParticipant $participant)
{
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcal;
class GenerateIcalResponse
{
public function __construct(
public string $icalContent,
public string $filename,
) {
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcalForDeadline;
class GenerateIcalForDeadlineCommand {
public function __construct(public GenerateIcalForDeadlineRequest $request)
{
}
public function execute(): GenerateIcalForDeadlineResponse
{
$event = $this->request->event;
$deadlineDate = $event->registration_final_end->copy()->subDays(2);
$dtDate = $deadlineDate->format('Ymd');
$now = now()->format('Ymd\THis\Z');
$summary = $this->escapeIcal('Zahlungsfrist: ' . $event->name);
$description = $this->escapeIcal(
'Bitte überweise den Teilnahmebeitrag für "' . $event->name . '" bis zu diesem Datum.'
);
$icalContent = implode("\r\n", [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//' . app('tenant')->name . '//Veranstaltungskalender//DE',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'BEGIN:VEVENT',
'UID:payment-deadline-' . $event->identifier . '@' . app('tenant')->slug,
'DTSTAMP:' . $now,
'DTSTART;VALUE=DATE:' . $dtDate,
'DTEND;VALUE=DATE:' . $dtDate,
'SUMMARY:' . $summary,
'DESCRIPTION:' . $description,
'END:VEVENT',
'END:VCALENDAR',
]) . "\r\n";
$filename = 'zahlungsziel-' . $event->name . '.ics';
return new GenerateIcalForDeadlineResponse($icalContent, $filename);
}
private function escapeIcal(string $value): string
{
return str_replace(
['\\', ';', ',', "\n"],
['\\\\', '\\;', '\\,', '\\n'],
$value
);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcalForDeadline;
use App\Models\Event;
class GenerateIcalForDeadlineRequest {
public function __construct(public Event $event) {
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcalForDeadline;
class GenerateIcalForDeadlineResponse
{
public function __construct(
public string $icalContent,
public string $filename,
) {
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Domains\Event\Actions\ManualCertificateOfConductionCheck;
use App\Enumerations\EfzStatus;
use App\Mail\ParticipantCocMails\ParticipantCocCompleteMail;
use App\Mail\ParticipantCocMails\ParticipantCocInvalidMail;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
class ManualCertificateOfConductionCheckCommand {
function __construct(public ManualCertificateOfConductionCheckRequest $request)
{}
public function execute() : ManualCertificateOfConductionCheckResponse {
$response = new ManualCertificateOfConductionCheckResponse();
$this->request->participant->efz_status = EfzStatus::EFZ_STATUS_CHECKED_VALID;
$this->request->participant->save();
$response->success = true;
Mail::to($this->request->participant->email_1)->send(new ParticipantCocCompleteMail(
participant: $this->request->participant,
));
return $response;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Event\Actions\ManualCertificateOfConductionCheck;
use App\Models\EventParticipant;
class ManualCertificateOfConductionCheckRequest {
function __construct(public EventParticipant $participant)
{
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Domains\Event\Actions\ManualCertificateOfConductionCheck;
use App\Enumerations\EfzStatus;
class ManualCertificateOfConductionCheckResponse {
public bool $success;
public function __construct()
{
$this->success = false;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Domains\Event\Actions\ParticipantPayment;
use App\Mail\ParticipantPaymentMails\ParticipantPaymentMissingPaymentMail;
use App\Mail\ParticipantPaymentMails\ParticipantPaymentOverpaidMail;
use App\Mail\ParticipantPaymentMails\ParticipantPaymentPaidMail;
use App\Providers\MissingPaymentProvider;
use Illuminate\Support\Facades\Mail;
class ParticipantPaymentCommand {
public function __construct(public ParticipantPaymentRequest $request) {
}
public function execute() : ParticipantPaymentResponse {
$response = new ParticipantPaymentResponse();
$this->request->participant->amount_paid = $this->request->amountPaid;
$this->request->participant->save();
$response->amountPaid = $this->request->participant->amount_paid;
$response->amountExpected = $this->request->participant->amount;
$amountToPay = MissingPaymentProvider::calculateMissingPayment(
amountPaid: $this->request->participant->amount_paid,
amountToPay: $this->request->participant->amount,
);
switch (true) {
case $amountToPay->getAmount() > 0:
Mail::to($this->request->participant->email_1)->send(new ParticipantPaymentMissingPaymentMail(
participant: $this->request->participant,
));
if ($this->request->participant->email_2 !== null) {
Mail::to($this->request->participant->email_2)->send(new ParticipantPaymentMissingPaymentMail(
participant: $this->request->participant,
));
}
break;
case $amountToPay->getAmount() < 0:
Mail::to($this->request->participant->email_1)->send(new ParticipantPaymentOverpaidMail(
participant: $this->request->participant,
));
if ($this->request->participant->email_2 !== null) {
Mail::to($this->request->participant->email_2)->send(new ParticipantPaymentOverpaidMail(
participant: $this->request->participant,
));
}
break;
default:
Mail::to($this->request->participant->email_1)->send(new ParticipantPaymentPaidMail(
participant: $this->request->participant,
));
if ($this->request->participant->email_2 !== null) {
Mail::to($this->request->participant->email_2)->send(new ParticipantPaymentPaidMail(
participant: $this->request->participant,
));
}
}
$response->participant = $this->request->participant;
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Event\Actions\ParticipantPayment;
use App\Models\EventParticipant;
use App\ValueObjects\Amount;
class ParticipantPaymentRequest {
public EventParticipant $participant;
public Amount $amountPaid;
public function __construct(EventParticipant $participant, Amount $amountPaid) {
$this->participant = $participant;
$this->amountPaid = $amountPaid;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Domains\Event\Actions\ParticipantPayment;
use App\Models\EventParticipant;
use App\ValueObjects\Amount;
class ParticipantPaymentResponse {
public bool $success;
public ?Amount $amountPaid;
public ?Amount $amountExpected;
public ?EventParticipant $participant;
public function __construct() {
$this->amountPaid = null;
$this->amountExpected = null;
$this->success = false;
$this->participant = null;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Domains\Event\Actions\SendMissingPaymentMails;
use App\Mail\ParticipantPaymentMails\ParticipantPaymentMissingPaymentMail;
use Illuminate\Support\Facades\Mail;
class SendMissingPaymentMailsCommand {
function __construct(public SendMissingPaymentMailsRequest $request) {}
public function execute() : SendMissingPaymentMailsResponse {
$response = new SendMissingPaymentMailsResponse();
foreach ($this->request->eventParticipants->getParticipantsWithMissingPayments($this->request->event, $this->request->httpRequest) as $participant) {
$participantResource = $participant->toResource()->toArray($this->request->httpRequest);
if (!$participantResource['needs_payment']) {
continue;
}
Mail::to($participant->email_1)->send(new ParticipantPaymentMissingPaymentMail(
participant: $participant,
));
if ($participant->email_2 !== null && $participant->email_2 !== $participant->email_1) {
Mail::to($participant->email_2)->send(new ParticipantPaymentMissingPaymentMail(
participant: $participant,
));
}
$response->remindedParticipants++;
}
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Event\Actions\SendMissingPaymentMails;
use App\Models\Event;
use App\Repositories\EventParticipantRepository;
use Illuminate\Http\Request;
class SendMissingPaymentMailsRequest {
function __construct(
public Event $event,
public EventParticipantRepository $eventParticipants,
public Request $httpRequest
) {
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Domains\Event\Actions\SendMissingPaymentMails;
class SendMissingPaymentMailsResponse {
function __construct(
public bool $success = false,
public int $remindedParticipants = 0
) {}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Domains\Event\Actions\SetCostUnit;
class SetCostUnitCommand {
private SetCostUnitRequest $request;
public function __construct(SetCostUnitRequest $request) {
$this->request = $request;
}
public function execute() : SetCostUnitResponse {
$response = new SetCostUnitResponse();
$this->request->event->cost_unit_id = $this->request->costUnit->id;
$response->success = $this->request->event->save();
return $response;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Event\Actions\SetCostUnit;
use App\Models\CostUnit;
use App\Models\Event;
class SetCostUnitRequest {
public Event $event;
public CostUnit $costUnit;
public function __construct(Event $event, CostUnit $costUnit) {
$this->event = $event;
$this->costUnit = $costUnit;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Event\Actions\SetCostUnit;
class SetCostUnitResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Domains\Event\Actions\SetParticipationFees;
use App\RelationModels\EventParticipationFee;
class SetParticipationFeesCommand {
private SetParticipationFeesRequest $request;
public function __construct(SetParticipationFeesRequest $request) {
$this->request = $request;
}
public function execute() : SetParticipationFeesResponse {
$response = new SetParticipationFeesResponse();
$this->request->event->sibling_reduction = $this->request->siblingReduction;
$this->request->event->save();
$this->cleanBefore();
$this->request->event->participationFee1()->associate(EventParticipationFee::create([
'tenant' => app('tenant')->slug,
'type' => $this->request->participationFeeFirst['type'],
'name' => $this->request->participationFeeFirst['name'],
'description' => $this->request->participationFeeFirst['description'],
'amount_standard' => $this->request->participationFeeFirst['amount_standard']->getAmount(),
'amount_reduced' => null !== $this->request->participationFeeFirst['amount_reduced'] ? $this->request->participationFeeFirst['amount_reduced']->getAmount() : null,
'amount_solidarity' => null !== $this->request->participationFeeFirst['amount_solidarity'] ? $this->request->participationFeeFirst['amount_solidarity']->getAmount() : null,
]))->save();
if ($this->request->participationFeeSecond !== null) {
$this->request->event->participationFee2()->associate(EventParticipationFee::create([
'tenant' => app('tenant')->slug,
'type' => $this->request->participationFeeSecond['type'],
'name' => $this->request->participationFeeSecond['name'],
'description' => $this->request->participationFeeSecond['description'],
'amount_standard' => $this->request->participationFeeSecond['amount_standard']->getAmount(),
'amount_reduced' => null !== $this->request->participationFeeSecond['amount_reduced'] ? $this->request->participationFeeSecond['amount_reduced']->getAmount() : null,
'amount_solidarity' => null !== $this->request->participationFeeSecond['amount_solidarity'] ? $this->request->participationFeeSecond['amount_solidarity']->getAmount() : null,
]))->save();
}
if ($this->request->participationFeeThird !== null) {
$this->request->event->participationFee3()->associate(EventParticipationFee::create([
'tenant' => app('tenant')->slug,
'type' => $this->request->participationFeeThird['type'],
'name' => $this->request->participationFeeThird['name'],
'description' => $this->request->participationFeeThird['description'],
'amount_standard' => $this->request->participationFeeThird['amount_standard']->getAmount(),
'amount_reduced' => null !== $this->request->participationFeeThird['amount_reduced'] ? $this->request->participationFeeThird['amount_reduced']->getAmount() : null,
'amount_solidarity' => null !== $this->request->participationFeeThird['amount_solidarity'] ? $this->request->participationFeeThird['amount_solidarity']->getAmount() : null,
]))->save();
}
if ($this->request->participationFeeFourth !== null) {
$this->request->event->participationFee4()->associate(EventParticipationFee::create([
'tenant' => app('tenant')->slug,
'type' => $this->request->participationFeeFourth['type'],
'name' => $this->request->participationFeeFourth['name'],
'description' => $this->request->participationFeeFourth['description'],
'amount_standard' => $this->request->participationFeeFourth['amount_standard']->getAmount(),
'amount_reduced' => null !== $this->request->participationFeeFourth['amount_reduced'] ? $this->request->participationFeeFourth['amount_reduced']->getAmount() : null,
'amount_solidarity' => null !== $this->request->participationFeeFourth['amount_solidarity'] ? $this->request->participationFeeFourth['amount_solidarity']->getAmount() : null,
]))->save();
}
$this->request->event->save();
$response->success = true;
return $response;
}
private function cleanBefore() {
if ($this->request->event->participationFee1()->first() !== null) {
$this->request->event->participationFee1()->first()->delete();
}
if ($this->request->event->participationFee2()->first() !== null) {
$this->request->event->participationFee2()->first()->delete();
}
if ($this->request->event->participationFee3()->first() !== null) {
$this->request->event->participationFee3()->first()->delete();
}
if ($this->request->event->participationFee4()->first() !== null) {
$this->request->event->participationFee4()->first()->delete();
}
$this->request->event->save();
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Domains\Event\Actions\SetParticipationFees;
use App\Models\Event;
use App\RelationModels\EventParticipationFee;
class SetParticipationFeesRequest {
public Event $event;
public array $participationFeeFirst;
public ?array $participationFeeSecond;
public ?array $participationFeeThird;
public ?array $participationFeeFourth;
public bool $siblingReduction;
public function __construct(Event $event, array $participationFeeFirst, bool $siblingReduction) {
$this->event = $event;
$this->participationFeeFirst = $participationFeeFirst;
$this->participationFeeSecond = null;
$this->participationFeeThird = null;
$this->participationFeeFourth = null;
$this->siblingReduction = $siblingReduction;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Event\Actions\SetParticipationFees;
class SetParticipationFeesResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Domains\Event\Actions\SetParticipationState;
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
use App\Mail\ParticipantParticipationMails\ParticipantSignOffMail;
use Illuminate\Support\Facades\Mail;
class SetParticipationStateCommand {
function __construct(private SetParticipationStateSignoffRequest|SetParticipationStateReSignonRequest $request) {}
public function execute() : SetParticipationStateResponse {
$response = new SetParticipationStateResponse();
switch (true) {
case $this->request instanceof SetParticipationStateSignoffRequest:
$this->request->participant->unregistered_at = $this->request->date;
$this->request->participant->save();
$response->success = true;
Mail::to($this->request->participant->email_1)->send(new ParticipantSignOffMail(
participant: $this->request->participant,
));
if ($this->request->participant->email_2 !== null) {
Mail::to($this->request->participant->email_2)->send(new ParticipantSignOffMail(
participant: $this->request->participant,
));
}
break;
case $this->request instanceof SetParticipationStateReSignonRequest:
$this->request->participant->unregistered_at = null;
$this->request->participant->save();
Mail::to($this->request->participant->email_1)->send(new EventSignUpSuccessfullMail(
participant: $this->request->participant,
));
if ($this->request->participant->email_2 !== null) {
Mail::to($this->request->participant->email_2)->send(new EventSignUpSuccessfullMail(
participant: $this->request->participant,
));
}
break;
}
return $response;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Domains\Event\Actions\SetParticipationState;
use App\Models\EventParticipant;
class SetParticipationStateReSignonRequest {
function __construct(public EventParticipant $participant) {}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Domains\Event\Actions\SetParticipationState;
class SetParticipationStateResponse {
function __construct(public bool $success = false) {}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Domains\Event\Actions\SetParticipationState;
use App\Models\EventParticipant;
class SetParticipationStateSignoffRequest {
function __construct(public EventParticipant $participant, public \DateTime $date) {
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Domains\Event\Actions\SignUp;
use App\Enumerations\EatingHabit;
use App\Enumerations\EfzStatus;
use App\ValueObjects\Age;
use Illuminate\Support\Str;
class SignUpCommand {
public function __construct(public SignUpRequest $request) {
}
public function execute() : SignUpResponse {
$response = new SignUpResponse();
$eatingHabit = match ($this->request->eating_habit) {
'vegan' => EatingHabit::EATING_HABIT_VEGAN,
'vegetarian' => EatingHabit::EATING_HABIT_VEGETARIAN,
default => EatingHabit::EATING_HABIT_OMNIVOR,
};
$participantAge = new Age($this->request->birthday);
$response->participant = $this->request->event->participants()->create(
[
'tenant' => $this->request->event->tenant,
'user_id' => $this->request->user_id,
'identifier' => Str::random(10),
'firstname' => $this->request->firstname,
'lastname' => $this->request->lastname,
'nickname' => $this->request->nickname,
'participation_type' => $this->request->participationType,
'local_group' => $this->request->localGroup->slug,
'birthday' => $this->request->birthday,
'address_1' => $this->request->address_1,
'address_2' => $this->request->address_2,
'postcode' => $this->request->postcode,
'city' => $this->request->city,
'email_1' => $this->request->email_1,
'email_2' => $this->request->email_2,
'phone_1' => $this->request->phone_1,
'phone_2' => $this->request->phone_2,
'contact_person' => $this->request->contact_person,
'allergies' => $this->request->allergies,
'intolerances' => $this->request->intolerances,
'medications' => $this->request->medications,
'tetanus_vaccination' => $this->request->tetanus_vaccination,
'eating_habit' => $eatingHabit,
'swimming_permission' => $participantAge->isfullAged() ? 'SWIMMING_PERMISSION_ALLOWED' : $this->request->swimming_permission,
'first_aid_permission' => $participantAge->isfullAged() ? 'FIRST_AID_PERMISSION_ALLOWED' : $this->request->first_aid_permission,
'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_date' => $this->request->arrival,
'departure_date' => $this->request->departure,
'arrival_eating' => $this->request->arrival_eating,
'departure_eating' => $this->request->departure_eating,
'notes' => $this->request->notes,
'amount' => $this->request->amount,
'payment_purpose' => $this->request->event->name . ' - Beitrag ' . $this->request->firstname . ' ' . $this->request->lastname,
'efz_status' => $participantAge->isfullAged() ? EfzStatus::EFZ_STATUS_NOT_CHECKED : EfzStatus::EFZ_STATUS_NOT_REQUIRED,
]
);
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Domains\Event\Actions\SignUp;
use App\Enumerations\ParticipationType;
use App\Models\Event;
use App\Models\Tenant;
use App\ValueObjects\Amount;
use DateTime;
class SignUpRequest {
function __construct(
public Event $event,
public ?int $user_id,
public string $firstname,
public string $lastname,
public ?string $nickname,
public string $participationType,
public Tenant $localGroup,
public DateTime $birthday,
public string $address_1,
public string $address_2,
public string $postcode,
public string $city,
public string $email_1,
public ?string $phone_1,
public ?string $email_2,
public ?string $phone_2,
public ?string $contact_person,
public ?string $allergies,
public ?string $intolerances,
public ?string $medications,
public ?DateTime $tetanus_vaccination,
public string $eating_habit,
public ?string $swimming_permission,
public ?string $first_aid_permission,
public bool $foto_socialmedia,
public bool $foto_print,
public bool $foto_webseite,
public bool $foto_partner,
public bool $foto_intern,
public DateTime $arrival,
public DateTime $departure,
public int $arrival_eating,
public int $departure_eating,
public ?string $notes,
public Amount $amount
) {
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Event\Actions\SignUp;
use App\Models\EventParticipant;
class SignUpResponse {
public bool $success;
public ?EventParticipant $participant;
public function __construct() {
$this->success = false;
$this->participant = null;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Domains\Event\Actions\UpdateEvent;
class UpdateEventCommand {
public UpdateEventRequest $request;
public function __construct(UpdateEventRequest $request) {
$this->request = $request;
}
public function execute() : UpdateEventResponse {
$response = new UpdateEventResponse();
$this->request->event->name = $this->request->eventName;
$this->request->event->location = $this->request->eventLocation;
$this->request->event->postal_code = $this->request->postalCode;
$this->request->event->email = $this->request->email;
$this->request->event->early_bird_end = $this->request->earlyBirdEnd;
$this->request->event->registration_final_end = $this->request->registrationFinalEnd;
$this->request->event->alcoholics_age = $this->request->alcoholicsAge;
$this->request->event->support_per_person = $this->request->supportPerPerson;
$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->save();
$this->request->event->resetAllowedEatingHabits();
$this->request->event->resetContributingLocalGroups();
foreach($this->request->eatingHabits as $eatingHabit) {
$this->request->event->eatingHabits()->attach($eatingHabit);
}
foreach($this->request->contributingLocalGroups as $contributingLocalGroup) {
$this->request->event->localGroups()->attach($contributingLocalGroup);
}
$this->request->event->save();
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Domains\Event\Actions\UpdateEvent;
use App\Models\Event;
use App\ValueObjects\Amount;
use DateTime;
class UpdateEventRequest {
public Event $event;
public string $eventName;
public string $eventLocation;
public string $postalCode;
public string $email;
public DateTime $earlyBirdEnd;
public DateTime $registrationFinalEnd;
public int $alcoholicsAge;
public bool $sendWeeklyReports;
public bool $registrationAllowed;
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) {
$this->event = $event;
$this->eventName = $eventName;
$this->eventLocation = $eventLocation;
$this->postalCode = $postalCode;
$this->email = $email;
$this->earlyBirdEnd = $earlyBirdEnd;
$this->registrationFinalEnd = $registrationFinalEnd;
$this->alcoholicsAge = $alcoholicsAge;
$this->sendWeeklyReports = $sendWeeklyReports;
$this->registrationAllowed = $registrationAllowed;
$this->flatSupport = $flatSupport;
$this->supportPerPerson = $supportPerPerson;
$this->contributingLocalGroups = $contributingLocalGroups;
$this->eatingHabits = $eatingHabits;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Event\Actions\UpdateEvent;
class UpdateEventResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Domains\Event\Actions\UpdateManagers;
class UpdateManagersCommand {
private UpdateManagersRequest $request;
public function __construct(UpdateManagersRequest $request) {
$this->request = $request;
}
public function execute() : UpdateManagersResponse {
$response = new UpdateManagersResponse();
$this->request->event->resetMangers();
foreach ($this->request->managers as $manager) {
$this->request->event->eventManagers()->attach($manager);
}
$this->request->event->save();
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Domains\Event\Actions\UpdateManagers;
use App\Models\Event;
class UpdateManagersRequest {
public Event $event;
public array $managers;
public function __construct(Event $event, array $managers) {
$this->managers = $managers;
$this->event = $event;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Event\Actions\UpdateManagers;
class UpdateManagersResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace App\Domains\Event\Actions\UpdateParticipant;
use App\Enumerations\EfzStatus;
use App\Mail\ParticipantCocMails\ParticipantCocCompleteMail;
use App\Mail\ParticipantCocMails\ParticipantCocInvalidMail;
use App\Mail\ParticipantPaymentMails\ParticipantPaymentMissingPaymentMail;
use App\Mail\ParticipantPaymentMails\ParticipantPaymentOverpaidMail;
use App\Mail\ParticipantPaymentMails\ParticipantPaymentPaidMail;
use App\Models\EventParticipant;
use App\Providers\MissingPaymentProvider;
use App\ValueObjects\Amount;
use DateTime;
use Illuminate\Support\Facades\Mail;
class UpdateParticipantCommand {
private UpdateParticipantResponse $response;
function __construct(public UpdateParticipantRequest $request) {
}
public function execute() {
$this->response = new UpdateParticipantResponse();
$p = clone($this->request->participant);
$p->firstname = $this->request->firstname;
$p->lastname = $this->request->lastname;
$p->nickname = $this->request->nickname;
$p->address_1 = $this->request->address_1;
$p->address_2 = $this->request->address_2;
$p->postcode = $this->request->postcode;
$p->city = $this->request->city;
$p->local_group = $this->request->localgroup;
$p->birthday = DateTime::createFromFormat('Y-m-d', $this->request->birthday);
$p->email_1 = $this->request->email_1;
$p->phone_1 = $this->request->phone_1;
$p->contact_person = $this->request->contact_person;
$p->email_2 = $this->request->email_2;
$p->phone_2 = $this->request->phone_2;
$p->arrival_date = DateTime::createFromFormat('Y-m-d', $this->request->arrival);
$p->departure_date = DateTime::createFromFormat('Y-m-d', $this->request->departure);
$p->participation_type = $this->request->participationType;
$p->eating_habit = $this->request->eatingHabit;
$p->allergies = $this->request->allergies;
$p->intolerances = $this->request->intolerances;
$p->medications = $this->request->medications;
$p->first_aid_permission = $this->request->extendedFirstAid;
$p->swimming_permission = $this->request->swimmingPermission;
$p->tetanus_vaccination = $this->request->tetanusVaccination !== null
? DateTime::createFromFormat('Y-m-d', $this->request->tetanusVaccination)
: null;
$p->notes = $this->request->notes;
$p->amount_paid = Amount::fromString($this->request->amountPaid);
$p->amount = Amount::fromString($this->request->amountExpected);
$p->efz_status = $this->request->cocStatus;
if (
MissingPaymentProvider::calculateMissingPayment(amountPaid: $p->amount_paid, amountToPay: $p->amount)->getAmount()
!==
MissingPaymentProvider::calculateMissingPayment(amountPaid: $this->request->participant->amount_paid, amountToPay: $this->request->participant->amount)->getAmount()
) {
$this->handleAmountChanges($p);
}
if (
$p->efz_status !== $this->request->participant->efz_status
) {
$this->handleCocStatusChange($p);
}
$p->save();
$this->response->success = true;
$this->response->participant = $p;
return $this->response;
}
private function handleAmountChanges(EventParticipant $participant) {
$this->response->amountPaid = $participant->amount_paid;
$this->response->amountExpected = $participant->amount;
$amountToPay = MissingPaymentProvider::calculateMissingPayment(
amountPaid: $participant->amount_paid,
amountToPay: $participant->amount,
);
switch (true) {
case $amountToPay->getAmount() > 0:
Mail::to($participant->email_1)->send(new ParticipantPaymentMissingPaymentMail(
participant: $participant,
));
if ($participant->email_2 !== null) {
Mail::to($participant->email_2)->send(new ParticipantPaymentMissingPaymentMail(
participant: $participant,
));
}
break;
case $amountToPay->getAmount() < 0:
Mail::to($participant->email_1)->send(new ParticipantPaymentOverpaidMail(
participant: $participant,
));
if ($participant->email_2 !== null) {
Mail::to($participant->email_2)->send(new ParticipantPaymentOverpaidMail(
participant: $participant,
));
}
break;
default:
Mail::to($participant->email_1)->send(new ParticipantPaymentPaidMail(
participant: $participant,
));
if ($participant->email_2 !== null) {
Mail::to($participant->email_2)->send(new ParticipantPaymentPaidMail(
participant: $participant,
));
}
}
}
private function handleCocStatusChange(EventParticipant $participant) {
$this->response->cocStatus = $participant->efzStatus()->first();
switch ($participant->efzStatus()->first()->slug) {
case EfzStatus::EFZ_STATUS_CHECKED_VALID:
case EfzStatus::EFZ_STATUS_NOT_REQUIRED:
Mail::to($participant->email_1)->send(new ParticipantCocCompleteMail(
participant: $participant,
));
break;
case EfzStatus::EFZ_STATUS_CHECKED_INVALID:
Mail::to($participant->email_1)->send(new ParticipantCocInvalidMail(
participant: $participant,
));
break;
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Domains\Event\Actions\UpdateParticipant;
use App\Models\EventParticipant;
class UpdateParticipantRequest {
function __construct(
public EventParticipant $participant,
public string $firstname,
public string $lastname,
public ?string $nickname,
public string $address_1,
public ?string $address_2,
public string $postcode,
public string $city,
public string $localgroup,
public string $birthday,
public string $email_1,
public string $phone_1,
public string $contact_person,
public ?string $email_2,
public ?string $phone_2,
public string $arrival,
public string $departure,
public string $participationType,
public string $eatingHabit,
public ?string $allergies,
public ?string $intolerances,
public ?string $medications,
public string $extendedFirstAid,
public string $swimmingPermission,
public ?string $tetanusVaccination,
public ?string $notes,
public string $amountPaid,
public string $amountExpected,
public string $cocStatus,
) {}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Domains\Event\Actions\UpdateParticipant;
use App\Enumerations\EfzStatus;
use App\Models\EventParticipant;
use App\ValueObjects\Amount;
class UpdateParticipantResponse {
public bool $success;
public ?EfzStatus $cocStatus;
public ?Amount $amountPaid;
public ?Amount $amountExpected;
public ?EventParticipant $participant;
public function __construct() {
$this->success = false;
$this->cocStatus =null;
$this->amountPaid = null;
$this->amountExpected = null;
$this->participant = null;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Inertia\Response;
class AvailableEventsController extends CommonController
{
public function __invoke(Request $request) : Response {
$events = [];
foreach ($this->events->getAvailable(false) as $event) {
$events[] = $event->toResource()->toArray($request);
};
$inertiaProvider = new InertiaProvider('Event/ListAvailable', ['events' => $events]);
return $inertiaProvider->render();
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitCommand;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitRequest;
use App\Domains\Event\Actions\CreateEvent\CreateEventCommand;
use App\Domains\Event\Actions\CreateEvent\CreateEventRequest;
use App\Domains\Event\Actions\SetCostUnit\SetCostUnitCommand;
use App\Domains\Event\Actions\SetCostUnit\SetCostUnitRequest;
use App\Enumerations\CostUnitType;
use App\Enumerations\ParticipationFeeType;
use App\Providers\InertiaProvider;
use App\Resources\EventResource;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use DateTime;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CreateController extends CommonController {
public function __invoke() {
return new InertiaProvider('Event/Create', [
'emailAddress' => auth()->user()->email,
'eventAccount' => $this->tenant->account_name,
'eventIban' => $this->tenant->account_iban,
'eventPayPerDay' => $this->tenant->slug === 'lv' ? true : false,
'participationFeeType' => $this->tenant->slug === 'lv' ?
ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED :
ParticipationFeeType::PARTICIPATION_FEE_TYPE_SOLIDARITY,
])->render();
}
public function doCreate(Request $request) : JsonResponse {
$eventBegin = DateTime::createFromFormat('Y-m-d', $request->input('eventBegin'));
$eventEnd = DateTime::createFromFormat('Y-m-d', $request->input('eventEnd'));
$eventEarlyBirdEnd = DateTime::createFromFormat('Y-m-d', $request->input('eventEarlyBirdEnd'));
$registrationFinalEnd = DateTime::createFromFormat('Y-m-d', $request->input('eventRegistrationFinalEnd'));
$participationFeeType = ParticipationFeeType::where('slug', $request->input('eventParticipationFeeType'))->first();
$payPerDay = $request->input('eventPayPerDay');
$payDirect = $request->input('eventPayDirectly');
$billingDeadline = $eventEnd->modify('+1 month');
$createRequest = new CreateEventRequest(
$request->input('eventName'),
$request->input('eventLocation'),
$request->input('eventPostalCode'),
$request->input('eventEmail'),
$eventBegin,
$eventEnd,
$eventEarlyBirdEnd,
$registrationFinalEnd,
$request->input('eventEarlyBirdEndAmountIncrease'),
$participationFeeType,
$request->input('eventAccount'),
$request->input('eventIban'),
$payPerDay,
$payDirect
);
$wasSuccessful = false;
$createCommand = new CreateEventCommand($createRequest);
$result = $createCommand->execute();
if ($result->success) {
$createCostUnitRequest = new CreateCostUnitRequest(
$result->event->name,
CostUnitType::COST_UNIT_TYPE_EVENT,
Amount::fromString('0,25'),
true,
$billingDeadline
);
$createCostUnitCommand = new CreateCostUnitCommand($createCostUnitRequest);
$costUnitResponse = $createCostUnitCommand->execute();
if ($costUnitResponse->success) {
$costUnitUpdateRequest = new SetCostUnitRequest($result->event, $costUnitResponse->costUnit);
$costUnitUpdateCommand = new SetCostUnitCommand($costUnitUpdateRequest);
$costUnitSetResponse = $costUnitUpdateCommand->execute();
$wasSuccessful = $costUnitSetResponse->success;
}
}
if ($wasSuccessful) {
return response()->json([
'status' => 'success',
'event' => new EventResource($costUnitUpdateRequest->event)->toArray($request)
]);
} else {
return response()->json([
'status' => 'error',
'message' => 'Die Veranstaltung konnte nicht angelegt werden.'
]);
}
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesCommand;
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesRequest;
use App\Domains\Event\Actions\UpdateEvent\UpdateEventCommand;
use App\Domains\Event\Actions\UpdateEvent\UpdateEventRequest;
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersCommand;
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersRequest;
use App\Enumerations\ParticipationFeeType;
use App\Enumerations\ParticipationType;
use App\Models\EventParticipant;
use App\Providers\InertiaProvider;
use App\Providers\PdfGenerateAndDownloadProvider;
use App\Resources\EventResource;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use function Symfony\Component\String\b;
class DetailsController extends CommonController {
public function __invoke(string $eventId) {
$event = $this->events->getByIdentifier($eventId);
return new InertiaProvider('Event/Details', ['event' => $event])->render();
}
public function summary(int $eventId, Request $request) : JsonResponse {
$event = $this->events->getById($eventId);
return response()->json(['event' => $event->toResource()->toArray($request)]);
}
public function updateCommonSettings(int $eventId, Request $request) : JsonResponse {
$event = $this->events->getById($eventId);
$earlyBirdEnd = \DateTime::createFromFormat('Y-m-d', $request->input('earlyBirdEnd'));
$registrationFinalEnd = \DateTime::createFromFormat('Y-m-d', $request->input('registrationFinalEnd'));
$flatSupport = Amount::fromString($request->input('flatSupport'));
$supportPerPerson = Amount::fromString($request->input('supportPerson'));
$contributinLocalGroups = $request->input('contributingLocalGroups');
$eatingHabits = $request->input('eatingHabits');
$eventUpdateRequest = new UpdateEventRequest(
$event,
$request->input('eventName'),
$request->input('eventLocation'),
$request->input('postalCode'),
$request->input('email'),
$earlyBirdEnd,
$registrationFinalEnd,
$request->input('alcoholicsAge'),
$request->input('sendWeeklyReports'),
$request->input('registrationAllowed'),
$flatSupport,
$supportPerPerson,
$contributinLocalGroups,
$eatingHabits
);
$eventUpdateCommand = new UpdateEventCommand($eventUpdateRequest);
$response = $eventUpdateCommand->execute();
return response()->json(['status' => $response->success ? 'success' : 'error']);
}
public function updateEventManagers(int $eventId, Request $request) : JsonResponse {
$event = $this->events->getById($eventId);
$updateEventManagersRequest = new UpdateManagersRequest($event, $request->input('selectedManagers'));
$updateEventManagersCommand = new UpdateManagersCommand($updateEventManagersRequest);
$response = $updateEventManagersCommand->execute();
return response()->json(['status' => $response->success ? 'success' : 'error']);
}
public function updateParticipationFees(int $eventId, Request $request) : JsonResponse {
$event = $this->events->getById($eventId);
$participationFeeFirst = [
'type' => ParticipationType::PARTICIPATION_TYPE_PARTICIPANT,
'name' => 'Teilnehmer',
'description' => $request->input('pft_1_description'),
'amount_standard' => Amount::fromString($request->input('pft_1_amount_standard')),
'amount_reduced' => null !== $request->input('pft_1_amount_reduced') ? Amount::fromString($request->input('pft_1_amount_reduced')) : null,
'amount_solidarity' => null !== $request->input('pft_1_amount_solidarity') ? Amount::fromString($request->input('pft_1_amount_solidarity')) : null
];
$siblingReduction = $request->input('sibling_reduction') ?? false;
$participationFeeRequest = new SetParticipationFeesRequest($event, $participationFeeFirst, $siblingReduction);
if ($request->input('pft_2_active')) {
$participationFeeRequest->participationFeeSecond = [
'type' => ParticipationType::PARTICIPATION_TYPE_TEAM,
'name' => $event->participation_fee_type === ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED ? 'Kernteam' : 'Solidaritätsbeitrag',
'description' => $request->input('pft_2_description'),
'amount_standard' => Amount::fromString($request->input('pft_2_amount_standard')),
'amount_reduced' => null !== $request->input('pft_2_amount_reduced') ? Amount::fromString($request->input('pft_2_amount_reduced')) : null,
'amount_solidarity' => null !== $request->input('pft_2_amount_solidarity') ? Amount::fromString($request->input('pft_2_amount_solidarity')) : null
];
}
if ($request->input('pft_3_active')) {
$participationFeeRequest->participationFeeThird = [
'type' => ParticipationType::PARTICIPATION_TYPE_VOLUNTEER,
'name' => $event->participation_fee_type === ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED ? 'Unterstützende' : 'Reduzierter Beitrag',
'description' => $event->participation_fee_type !== ParticipationFeeType::PARTICIPATION_FEE_TYPE_SOLIDARITY ? $request->input('pft_3_description') : 'Nach Verfügbarkeit',
'amount_standard' => Amount::fromString($request->input('pft_3_amount_standard')),
'amount_reduced' => null !== $request->input('pft_3_amount_reduced') ? Amount::fromString($request->input('pft_3_amount_reduced')) : null,
'amount_solidarity' => null !== $request->input('pft_3_amount_solidarity') ? Amount::fromString($request->input('pft_3_amount_solidarity')) : null
];
}
if ($request->input('pft_4_active') && $event->participation_fee_type === ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED) {
$participationFeeRequest->participationFeeFourth = [
'type' => ParticipationType::PARTICIPATION_TYPE_OTHER,
'name' => 'Sonstige',
'description' => $request->input('pft_4_description'),
'amount_standard' => Amount::fromString($request->input('pft_4_amount_standard')),
'amount_reduced' => null !== $request->input('pft_4_amount_reduced') ? Amount::fromString($request->input('pft_4_amount_reduced')) : null,
'amount_solidarity' => null !== $request->input('pft_4_amount_solidarity') ? Amount::fromString($request->input('pft_4_amount_solidarity')) : null
];
}
$participationFeeCommand = new SetParticipationFeesCommand($participationFeeRequest);
$response = $participationFeeCommand->execute();
return response()->json(['status' => $response->success ? 'success' : 'error']);
}
public function downloadPdfList(string $eventId, string $listType, Request $request): Response
{
$event = $this->events->getByIdentifier($eventId);
$participants = $this->eventParticipants->getForList($event, $request);
$kitchenOverview = $this->eventParticipants->getKitchenOverview($event);
$html = view('pdfs.' . $listType, [
'event' => $event->name,
'eventStart' => $event->start_date,
'eventEnd' => $event->end_date,
'rows' => $participants,
'kitchenRequirements' => $kitchenOverview,
'participantsForKitchenList' => $this->eventParticipants->getParticipantsWithIntolerances($event, $request),
])->render();
$pdf = PdfGenerateAndDownloadProvider::fromHtml(
$html,
'landscape'
);
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="' . $listType .'.pdf"',
]);
}
public function downloadCsvList(string $eventId, string $listType, Request $request): Response
{
$event = $this->events->getByIdentifier($eventId);
$participants = $this->eventParticipants->getForList($event, $request);
$kitchenOverview = $this->eventParticipants->getKitchenOverview($event);
$csv = view('csvs.' . $listType, [
'event' => $event->name,
'rows' => $participants,
])->render();
return response($csv, 200, [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="' . $listType . '.csv"',
]);
}
public function listParticipants(string $eventId, string $listType, Request $request) : JsonResponse {
$event = $this->events->getByIdentifier($eventId);
switch ($listType) {
case 'by-local-group':
$participants = $this->eventParticipants->groupByLocalGroup($event, $request);
break;
case 'by-participation-group':
$participants = $this->eventParticipants->groupByParticipationType($event, $request);
break;
case 'signed-off':
$participants = $this->eventParticipants->getSignedOffParticipants($event, $request);
break;
default:
$participants = ['Alle Teilnehmenden' => $this->eventParticipants->getForList($event, $request)];
}
return response()->json([
'participants' => $participants,
'listType' => $listType,
'event' => $event->toResource()->toArray($request)
]);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Domains\Event\Controllers\MailCompose;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
class ByGroupController extends CommonController
{
public function __invoke(string $eventIdentifier, string $groupType, Request $request) {
$event = $this->events->getByIdentifier($eventIdentifier, true);
$recipients = [];
switch ($groupType) {
case 'by-local-group':
$participants = $this->eventParticipants->groupByLocalGroup($event, $request, $request->input('groupName'));
$recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]);
break;
case 'by-participation-group':
$participants = $this->eventParticipants->groupByParticipationType($event, $request, $request->input('groupName'));
$recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]);
break;
case 'signed-off':
$participants = $this->eventParticipants->getSignedOffParticipants($event, $request, $request->input('groupName'));
$recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]);
break;
default:
$participants = $this->eventParticipants->getForList($event, $request);
$recipients = $this->eventParticipants->getMailAddresses($participants);
}
return response()->json(['recipients' => $recipients]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Domains\Event\Controllers\MailCompose;
use App\Mail\ManualMails\ManualMailsCommonMail;
use App\Mail\ManualMails\ManualMailsReportMail;
use App\Mail\ParticipantCocMails\ParticipantCocCompleteMail;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
class SendController extends CommonController
{
public function __invoke(string $eventIdentifier, Request $request) {
$recipients = $request->input('recipients')
|> function (string $value) : string { return str_replace(';', ',', $value); }
|> function (string $value) : array { return explode( ',', $value); };
$event = $this->events->getByIdentifier($eventIdentifier, true)->toResource()->toArray($request);
$sentRecipients = [];
$allOkay = true;
$subject = $request->input('subject') ?? 'Neue Nachricht zu Veranstaltung "' . $event['name'] . '"';
foreach ($recipients as $recipient) {
if (in_array($recipient, $sentRecipients)) {
continue;
}
$sentRecipients[] = $recipient;
try {
Mail::to(trim($recipient))->send(new ManualMailsCommonMail(
mailSubject: $subject,
message: $request->input('message'),
event: $event,
));
} catch (\Exception $e) {
$allOkay = false;
}
}
$user = auth()->user();
$reportSubject = sprintf('Sendebericht für Nachricht mit Betreff "%s"', $subject);
Mail::to($user->email)->send(new ManualMailsReportMail(
mailSubject: $reportSubject,
message: $request->input('message'),
event: $event,
originalRecipients: $sentRecipients
));
if ($allOkay) {
return response()->json([
'success' => true,
'message' => sprintf(
'E-Mail wurde erfolgreich an %1$s Personen versendet. Du hast eine Kopie an deine Mail-Adresse erhalten.',
count($sentRecipients)
),
]);
} else {
return response()->json([
'success' => false,
'message' => 'Es gab einen Fehler beim Versenden der Nachrichten.'
]);
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\ManualCertificateOfConductionCheck\ManualCertificateOfConductionCheckCommand;
use App\Domains\Event\Actions\ManualCertificateOfConductionCheck\ManualCertificateOfConductionCheckRequest;
use App\Domains\Event\Actions\ParticipantPayment\ParticipantPaymentCommand;
use App\Domains\Event\Actions\ParticipantPayment\ParticipantPaymentRequest;
use App\Models\EventParticipant;
use App\Scopes\CommonController;
use Illuminate\Support\Facades\Request;
class ParticipantController extends CommonController {
public function markCocExisting(string $participantIdentifier, Request $request) {
$participant = $this->eventParticipants->getByIdentifier($participantIdentifier, $this->events);
$cocRequest = new ManualCertificateOfConductionCheckRequest($participant);
$cocCommand = new ManualCertificateOfConductionCheckCommand($cocRequest);
$cocResponse = $cocCommand->execute();
return response()->json([
'status' => $cocResponse->success ? 'success' : 'error',
'message' => $cocResponse->success ? 'Das eFZ wurde als gültig hinterlegt' : 'Beim Aktualisieren des eFZ-Status ist ein Fehler aufgetreten.'
]);
}
public function __invoke(string $participantIdentifier, Request $request) {
$participant = $this->eventParticipants->getByIdentifier($participantIdentifier, $this->events)->toResource()->toArray($request);
return response()->json([
'participant' => $participant,
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\GenerateIcal\GenerateIcalCommand;
use App\Domains\Event\Actions\GenerateIcal\GenerateIcalRequest;
use App\Models\EventParticipant;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class ParticipantIcalController extends CommonController
{
public function __invoke(string $participantIdentifier, Request $request): Response
{
$participant = $this->eventParticipants->getMyParticipationByIdentifier($participantIdentifier);
if ($participant === null) {
abort(403, 'Zugriff verweigert.');
}
$icalRequest = new GenerateIcalRequest($participant);
$icalCommand = new GenerateIcalCommand($icalRequest);
$icalResponse = $icalCommand->execute();
return response($icalResponse->icalContent, 200, [
'Content-Type' => 'text/calendar; charset=utf-8',
'Content-Disposition' => 'attachment; filename="' . $icalResponse->filename . '"',
]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\GenerateIcal\GenerateIcalCommand;
use App\Domains\Event\Actions\GenerateIcal\GenerateIcalRequest;
use App\Domains\Event\Actions\GenerateIcalForDeadline\GenerateIcalForDeadlineCommand;
use App\Domains\Event\Actions\GenerateIcalForDeadline\GenerateIcalForDeadlineRequest;
use App\Models\EventParticipant;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class ParticipantIcalForPaymentController extends CommonController
{
public function __invoke(string $participantIdentifier, Request $request): Response
{
$participant = $this->eventParticipants->getMyParticipationByIdentifier($participantIdentifier);
if ($participant === null) {
abort(403, 'Zugriff verweigert.');
}
$icalRequest = new GenerateIcalForDeadlineRequest($participant->event()->first());
$icalCommand = new GenerateIcalForDeadlineCommand($icalRequest);
$icalResponse = $icalCommand->execute();
return response($icalResponse->icalContent, 200, [
'Content-Type' => 'text/calendar; charset=utf-8',
'Content-Disposition' => 'attachment; filename="' . $icalResponse->filename . '"',
]);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\ParticipantPayment\ParticipantPaymentCommand;
use App\Domains\Event\Actions\ParticipantPayment\ParticipantPaymentRequest;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\Request;
class ParticipantPaymentController extends CommonController
{
public function paymentComplete(string $participantIdentifier, Request $request) {
$participant = $this->eventParticipants->getByIdentifier($participantIdentifier, $this->events);
$paymentRequest = new ParticipantPaymentRequest($participant, $participant->amount);
$paymentCommand = new ParticipantPaymentCommand($paymentRequest);
$paymentResponse = $paymentCommand->execute();
return response()->json([
'status' => $paymentResponse->success ? 'success' : 'error',
'message' => $paymentResponse->success ? 'Die Zahlung wurde erfolgreich gebucht.' : 'Beim Buchen der Zahlung ist ein Fehler aufgetreten.'
]);
}
public function partialPaymentComplete(string $participantIdentifier, Request $request) {
$participant = $this->eventParticipants->getByIdentifier($participantIdentifier, $this->events);
$paymentRequest = new ParticipantPaymentRequest($participant, Amount::fromString($request->input('amount')));
$paymentCommand = new ParticipantPaymentCommand($paymentRequest);
$paymentResponse = $paymentCommand->execute();
$amountLeft = clone($paymentResponse->amountExpected);
$amountLeft->subtractAmount($paymentResponse->amountPaid);
return response()->json([
'status' => $paymentResponse->success ? 'success' : 'error',
'message' => $paymentResponse->success ? 'Die Zahlung wurde erfolgreich gebucht.' : 'Beim Buchen der Zahlung ist ein Fehler aufgetreten.',
'identifier' => $participant->identifier,
'amount' => [
'paid' => $paymentResponse->amountPaid->toString(),
'expected' => $paymentResponse->amountExpected->toString(),
'actions' => $amountLeft->getAmount() != 0 ? 'inline' : 'none',
'class' => $amountLeft->getAmount() != 0 ? 'not-paid' : 'paid',
]
]);
dd($participant);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\SetParticipationState\SetParticipationStateCommand;
use App\Domains\Event\Actions\SetParticipationState\SetParticipationStateReSignonRequest;
use App\Domains\Event\Actions\SetParticipationState\SetParticipationStateSignoffRequest;
use App\Scopes\CommonController;
use DateTime;
use Illuminate\Http\Request;
class ParticipantReSignOnController extends CommonController
{
public function __invoke(string $participantIdentifier, Request $request) {
$participant = $this->eventParticipants->getByIdentifier($participantIdentifier, $this->events);
$request = new SetParticipationStateReSignonRequest($participant);
$command = new SetParticipationStateCommand($request);
$command->execute();
return response()->json([
'status' => 'success',
'identifier' => $participant->identifier,
'message' => 'Die Wiederanmeldung wurde erfolgreich durchgeführt.'
]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\SetParticipationState\SetParticipationStateCommand;
use App\Domains\Event\Actions\SetParticipationState\SetParticipationStateSignoffRequest;
use App\Scopes\CommonController;
use DateTime;
use Illuminate\Http\Request;
class ParticipantSignOffController extends CommonController
{
public function __invoke(string $participantIdentifier, Request $request) {
$participant = $this->eventParticipants->getByIdentifier($participantIdentifier, $this->events);
$signOffDate = DateTime::createFromFormat('Y-m-d', $request->input('cancel_date'));
$signOffRequest = new SetParticipationStateSignoffRequest($participant, $signOffDate);
$signOffCommand = new SetParticipationStateCommand($signOffRequest);
$signOffCommand->execute();
return response()->json([
'status' => 'success',
'identifier' => $participant->identifier,
'message' => 'Die Abmeldung wurde erfolgreich durchgeführt.'
]);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\UpdateParticipant\UpdateParticipantCommand;
use App\Domains\Event\Actions\UpdateParticipant\UpdateParticipantRequest;
use App\Enumerations\EfzStatus;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ParticipantUpdateController extends CommonController {
public function __invoke(string $participantIdentifier, Request $request): JsonResponse {
$participant = $this->eventParticipants->getByIdentifier($participantIdentifier, $this->events);
$updateRequest = new UpdateParticipantRequest(
participant: $participant,
firstname: $request->input('firstname'),
lastname: $request->input('lastname'),
nickname: $request->input('nickname'),
address_1: $request->input('address_1'),
address_2: $request->input('address_2'),
postcode: $request->input('postcode'),
city: $request->input('city'),
localgroup: $request->input('localgroup'),
birthday: $request->input('birthday'),
email_1: $request->input('email_1'),
phone_1: $request->input('phone_1'),
contact_person: $request->input('contact_person'),
email_2: $request->input('email_2'),
phone_2: $request->input('phone_2'),
arrival: $request->input('arrival'),
departure: $request->input('departure'),
participationType: $request->input('participationType'),
eatingHabit: $request->input('eatingHabit'),
allergies: $request->input('allergies'),
intolerances: $request->input('intolerances'),
medications: $request->input('medications'),
extendedFirstAid: $request->input('extendedFirstAid'),
swimmingPermission: $request->input('swimmingPermission'),
tetanusVaccination: $request->input('tetanusVaccination'),
notes: $request->input('notes'),
amountPaid: $request->input('amountPaid'),
amountExpected: $request->input('amountExpected'),
cocStatus: $request->input('cocStatus'),
);
$command = new UpdateParticipantCommand($updateRequest);
$response = $command->execute();
$data = [
'status' => $response->success ? 'success' : 'error',
'identifier' => $participant->identifier,
'participant' => $response->participant->toResource()->toArray($request),
];
if ($response->cocStatus !== null) {
$data['cocChanged'] = true;
$data['coc']['action'] = in_array($response->cocStatus->slug, [
EfzStatus::EFZ_STATUS_CHECKED_INVALID,
EfzStatus::EFZ_STATUS_NOT_CHECKED]) ? 'inline' : 'none';
$data['coc']['statusText'] = $response->cocStatus->name;
$data['coc']['class'] = match($response->cocStatus->slug) {
EfzStatus::EFZ_STATUS_CHECKED_INVALID => 'efz-invalid',
EfzStatus::EFZ_STATUS_NOT_CHECKED => 'efz-not-checked',
default => 'efz-valid',
};
}
if ($response->amountPaid !== null) {
$amountLeft = clone($response->amountExpected);
$amountLeft->subtractAmount($response->amountPaid);
$data['amountChanged'] = true;
$data['amount'] = [
'paid' => $response->amountPaid->toString(),
'expected' => $response->amountExpected->toString(),
'actions' => $amountLeft->getAmount() != 0 ? 'inline' : 'none',
'class' => $amountLeft->getAmount() != 0 ? 'not-paid' : 'paid',
];
}
return response()->json($data);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\SendMissingPaymentMails\SendMissingPaymentMailsCommand;
use App\Domains\Event\Actions\SendMissingPaymentMails\SendMissingPaymentMailsRequest;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
class PaymentReminderController extends CommonController
{
public function __invoke(string $eventIdentifier, Request $request)
{
$event = $this->events->getByIdentifier($eventIdentifier, true);
$sendPaymentReminderMailsRequest = new SendMissingPaymentMailsRequest(
event: $event,
eventParticipants: $this->eventParticipants,
httpRequest: $request
);
$sendPaymentReminderMailsCommand = new SendMissingPaymentMailsCommand(request: $sendPaymentReminderMailsRequest);
$sendPaymentReminderResponse = $sendPaymentReminderMailsCommand->execute();
return response()->json([
'success' => $sendPaymentReminderResponse->success,
'message' => $sendPaymentReminderResponse->success ?
sprintf('Es wurden %1$s Personen über fehlende Teilnahmebeiträge informiert', $sendPaymentReminderResponse->remindedParticipants) :
'Beim Senden der Benachrichtigungen ist ein Fehler aufgetreten.',
]);
}
}

View File

@@ -0,0 +1,182 @@
<?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\SignUp\SignUpCommand;
use App\Domains\Event\Actions\SignUp\SignUpRequest;
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
use App\Models\Tenant;
use App\Providers\DoubleCheckEventRegistrationProvider;
use App\Providers\InertiaProvider;
use App\Resources\UserResource;
use App\Scopes\CommonController;
use DateTime;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
class SignupController extends CommonController {
public function __invoke(string $eventId, Request $request) {
$availableEvents = [];
foreach ($this->events->getAvailable(false) as $event) {
$availableEvents[] = $event->toResource()->toArray($request);
};
$event = $this->events->getByIdentifier($eventId, false)?->toResource()->toArray($request);
$participantData = [
'firstname' => '',
'lastname' => '',
];
if (auth()->check()) {
$user = new UserResource(auth()->user())->toArray($request);
$participantData = [
'id' => $user['id'],
'firstname' => $user['firstname'],
'lastname' => $user['lastname'],
'nickname' => $user['nickname'],
'email' => $user['email'],
'phone' => $user['phone'],
'postcode' => $user['postcode'],
'city' => $user['city'],
'address_1' => $user['address_1'],
'address_2' => $user['address_2'],
'birthday' => $user['birthday'],
'localGroup' => $user['localGroup'],
'allergies' => $user['allergies'],
'intolerances' => $user['intolerances'],
'eating_habit' => $user['eating_habits'],
'medications' => $user['medications'],
'tetanusVaccination' => $user['tetanus_vaccination'],
];
}
$inertiaProvider = new InertiaProvider('Event/Signup', [
'event' => $event,
'availableEvents' => $availableEvents,
'localGroups' => $event['contributingLocalGroups'],
'participantData' => $participantData,
]);
return $inertiaProvider->render();
}
public function signUp(int $eventId, Request $request) {
$event = $this->events->getById($eventId, false);
$eventResource = $event->toResource();
$registrationData = $request->input('registration_data');
$siblingReduction = $registrationData['sibling'] === 'true';
$arrival = \DateTime::createFromFormat('Y-m-d', $registrationData['arrival']);
$departure = \DateTime::createFromFormat('Y-m-d', $registrationData['departure']);
$tetanusVaccination = $registrationData['tetanusVaccination'] ? \DateTime::createFromFormat('Y-m-d', $registrationData['tetanusVaccination']) : null;
$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'],
$arrival,
$departure,
$siblingReduction
);
$signupRequest = new SignUpRequest(
$event,$registrationData['userId'],
$registrationData['vorname'],
$registrationData['nachname'],
$registrationData['pfadiname'],
$registrationData['participationType'],
Tenant::findOrFail($registrationData['localGroup']),
\DateTime::createFromFormat('Y-m-d', $registrationData['geburtsdatum']),
$registrationData['address1'],
$registrationData['address2'],
$registrationData['plz'],
$registrationData['ort'],
$registrationData['email_1'],
$registrationData['telefon_1'],
$registrationData['email_2'],
$registrationData['telefon_2'],
$registrationData['ansprechpartner'],
$registrationData['allergien'],
$registrationData['intolerances'],
$registrationData['medikamente'],
$tetanusVaccination,
$registrationData['essgewohnheit'],
$registrationData['badeerlaubnis'],
$registrationData['first_aid'],
$registrationData['foto']['socialmedia'],
$registrationData['foto']['print'],
$registrationData['foto']['webseite'],
$registrationData['foto']['partner'],
$registrationData['foto']['intern'],
$arrival,
$departure,
$registrationData['anreise_essen'],
$registrationData['abreise_essen'],
$registrationData['anmerkungen'],
$amount
);
$signupCommand = new SignUpCommand($signupRequest);
$signupResponse = $signupCommand->execute();
// 4. Addons registrieren
$certificateOfConductionCheckRequest = new CertificateOfConductionCheckRequest($signupResponse->participant);
$certificateOfConductionCheckCommand = new CertificateOfConductionCheckCommand($certificateOfConductionCheckRequest);
$certificateOfConductionCheckResponse = $certificateOfConductionCheckCommand->execute();
$signupResponse->participant->efz_status = $certificateOfConductionCheckResponse->status;
$signupResponse->participant->save();
Mail::to($signupResponse->participant->email_1)->send(new EventSignUpSuccessfullMail(
participant: $signupResponse->participant,
));
if ($signupResponse->participant->email_2 !== null) {
Mail::to($signupResponse->participant->email_2)->send(new EventSignUpSuccessfullMail(
participant: $signupResponse->participant,
));
}
return response()->json(
[
'participant' => $signupResponse->participant->toResource()->toArray($request),
'status' => 'success',
]
);
}
public function calculateAmount(int $eventId, Request $request, bool $forDisplay = true) : JsonResponse | float {
$event = $this->events->getById($eventId, false)->toResource();
$siblingReduction = $request->input('sibling') === 'true';
return response()->json(['amount' =>
$event->calculateAmount(
$request->input('participationType'),
$request->input('beitrag'),
\DateTime::createFromFormat('Y-m-d', $request->input('arrival')),
\DateTime::createFromFormat('Y-m-d', $request->input('departure')),
$siblingReduction
)->toString()
]);
}
}

View File

@@ -0,0 +1,64 @@
<?php
use App\Domains\Event\Controllers\CreateController;
use App\Domains\Event\Controllers\DetailsController;
use App\Domains\Event\Controllers\MailCompose\ByGroupController;
use App\Domains\Event\Controllers\MailCompose\SendController;
use App\Domains\Event\Controllers\ParticipantController;
use App\Domains\Event\Controllers\ParticipantIcalController;
use App\Domains\Event\Controllers\ParticipantIcalForPaymentController;
use App\Domains\Event\Controllers\ParticipantPaymentController;
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\SignupController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::prefix('api/v1')
->group(function () {
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('event')->group(function () {
Route::post('{eventId}/calculate-amount', [SignupController::class, 'calculateAmount']);
Route::post('{eventId}/signup', [SignupController::class, 'signUp']);
Route::middleware(['auth'])->group(function () {
Route::post('/create', [CreateController::class, 'doCreate']);
Route::prefix('{eventIdentifier}/mailing')->group(function () {
Route::post('/compose/to-group/{groupType}', ByGroupController::class);
Route::post('/send', SendController::class);
});
Route::get('{eventIdentifier}/send-payment-reminder', PaymentReminderController::class);
Route::prefix('/details/{eventId}') ->group(function () {
Route::get('/summary', [DetailsController::class, 'summary']);
Route::get('/participants/{listType}', [DetailsController::class, 'listParticipants']);
Route::post('/event-managers', [DetailsController::class, 'updateEventManagers']);
Route::post('/participation-fees', [DetailsController::class, 'updateParticipationFees']);
Route::post('/common-settings', [DetailsController::class, 'updateCommonSettings']);
});
Route::prefix('/participant/{participantIdentifier}')->group(function () {
Route::get('/', ParticipantController::class);
Route::get('/ical', ParticipantIcalController::class);
Route::get('/ical-payment', ParticipantIcalForPaymentController::class);
Route::post('/payment-complete', [ParticipantPaymentController::class, 'paymentComplete']);
Route::post('/partial-payment', [ParticipantPaymentController::class, 'partialPaymentComplete']);
Route::post('/mark-coc-existing', [ParticipantController::class, 'markCocExisting']);
Route::post('/signoff', ParticipantSignOffController::class);
Route::post('/re-signon', ParticipantReSignOnController::class);
Route::post('/update', ParticipantUpdateController::class);
});
});
});
});
});

View File

@@ -0,0 +1,25 @@
<?php
use App\Domains\Event\Controllers\AvailableEventsController;
use App\Domains\Event\Controllers\CreateController;
use App\Domains\Event\Controllers\DetailsController;
use App\Domains\Event\Controllers\SignupController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('event')->group(function () {
Route::get('/available-events', AvailableEventsController::class);
Route::get('/{eventId}', SignupController::class);
Route::get('/{eventId}/signup', SignupController::class);
Route::middleware(['auth'])->group(function () {
Route::get('/new', CreateController::class);
Route::get('/details/{eventId}', DetailsController::class);
Route::get('/details/{eventId}/pdf/{listType}', [DetailsController::class, 'downloadPdfList']);
Route::get('/details/{eventId}/csv/{listType}', [DetailsController::class, 'downloadCsvList']);
});
});
});

View File

@@ -0,0 +1,293 @@
<script setup>
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
import {reactive, watch, ref, computed} from 'vue'
import { subWeeks, format, parseISO, isValid, addDays } from 'date-fns'
import ErrorText from "../../../Views/Components/ErrorText.vue";
import {useAjax} from "../../../../resources/js/components/ajaxHandler.js";
import ParticipationFees from "./Partials/ParticipationFees.vue";
const { request } = useAjax();
const props = defineProps({
'emailAddress': String,
'eventAccount': String,
'eventIban': String,
"eventPayPerDay": Boolean,
"participationFeeType": String,
})
const errors = reactive({})
const formData = reactive({
eventName: '',
eventPostalCode: '',
eventLocation: '',
eventEmail: props.emailAddress ? props.emailAddress : '',
eventBegin: '',
eventEnd: '',
eventEarlyBirdEnd: '',
eventEarlyBirdEndAmountIncrease: 50,
eventRegistrationFinalEnd: '',
eventAccount: props.eventAccount ? props.eventAccount : '',
eventIban: props.eventIban ? props.eventIban : '',
eventPayDirectly: true,
eventPayPerDay: props.eventPayPerDay ? props.eventPayPerDay : false,
eventParticipationFeeType: props.participationFeeType ? props.participationFeeType : 'fixed',
});
watch(
() => formData.eventBegin,
(newValue) => {
if (!newValue) return
const beginDate = parseISO(newValue)
if (!isValid(beginDate)) return
const fourWeeksBefore = subWeeks(beginDate, 4)
const twoWeeksBefore = subWeeks(beginDate, 2)
const threeDaysAfter = addDays(beginDate, 2)
formData.eventEarlyBirdEnd = format(
fourWeeksBefore,
'yyyy-MM-dd'
)
formData.eventRegistrationFinalEnd = format(
twoWeeksBefore,
'yyyy-MM-dd'
)
formData.eventEnd = format(
threeDaysAfter,
'yyyy-MM-dd'
)
}
)
const formIsValid = computed(() => {
errors.eventEmail = '';
errors.eventName = '';
errors.eventLocation = '';
errors.eventPostalCode = '';
errors.eventBegin = '';
errors.eventEnd = '';
errors.eventEarlyBirdEnd = '';
errors.eventRegistrationFinalEnd = '';
errors.eventAccount = '';
errors.eventIban = '';
var returnValue = true;
if (!formData.eventName) {
errors.eventName = 'Bitte gib den Veranstaltungsnamen ein'
returnValue = false
}
if (!formData.eventEmail) {
errors.eventEmail = 'Bitte gib die E-Mail-Adresse der Veranstaltungsleitung für Rückfragen der Teilnehmenden ein'
returnValue = false
}
if (!formData.eventLocation) {
errors.eventLocation = 'Bitte gib den Veranstaltungsort ein'
returnValue = false
}
if (!formData.eventPostalCode) {
errors.eventPostalCode = 'Bitte gib die Postleitzahl des Veranstaltungsorts ein'
returnValue = false
}
if (!formData.eventBegin) {
errors.eventBegin = 'Bitte gib das Anfangsdatum der Veranstaltung ein'
returnValue = false
}
if (!formData.eventEnd ||formData.eventEnd < formData.eventBegin ) {
errors.eventEnd = 'Das Enddatum darf nicht vor dem Anfangsdatum liegen'
returnValue = false
}
if (!formData.eventEarlyBirdEnd ||formData.eventEarlyBirdEnd > formData.eventBegin ) {
errors.eventEarlyBirdEnd = 'Das Enddatum der Early-Bird-Phase muss vor dem Veranstaltungsbeginn liegen'
returnValue = false
}
if (!formData.eventRegistrationFinalEnd ||formData.eventRegistrationFinalEnd > formData.eventBegin ) {
errors.eventRegistrationFinalEnd = 'Der Anmeldeschluss darf nicht nach dem Veranstaltungsbeginn liegen'
returnValue = false
}
if (!formData.eventAccount) {
errors.eventAccount = 'Bitte gib an, auf wen das Veranstaltungskonto für eingehende Beiträge läuft'
returnValue = false
}
if (!formData.eventIban) {
errors.eventIban = 'Bitte gib die IBAN des Kontos für Teilnahmebeiträge ein'
returnValue = false
}
return returnValue;
})
const showParticipationFees = ref(false)
const newEvent = ref(null)
async function createEvent() {
if (!formIsValid.value) return false
const data = await request("/api/v1/event/create", {
method: "POST",
body: {
eventName: formData.eventName,
eventPostalCode: formData.eventPostalCode,
eventLocation: formData.eventLocation,
eventEmail: formData.eventEmail,
eventBegin: formData.eventBegin,
eventEnd: formData.eventEnd,
eventEarlyBirdEnd: formData.eventEarlyBirdEnd,
eventEarlyBirdEndAmountIncrease: formData.eventEarlyBirdEndAmountIncrease,
eventRegistrationFinalEnd: formData.eventRegistrationFinalEnd,
eventAccount: formData.eventAccount,
eventIban: formData.eventIban,
eventPayDirectly: formData.eventPayDirectly,
eventPayPerDay: formData.eventPayPerDay,
eventParticipationFeeType: formData.eventParticipationFeeType,
}
});
if (data.status !== 'success') {
toast.error(data.message);
return false;
} else {
newEvent.value = data.event;
showParticipationFees.value = true;
}
}
async function finishCreation() {
window.location.href = '/event/details/' + newEvent.value.identifier;
}
</script>
<template>
<AppLayout title="Neue Veranstaltung">
<fieldset>
<legend>
<span style="font-weight: bolder;">Grundlegende Veranstaltungsdaten</span>
</legend>
<ParticipationFees v-if="showParticipationFees" :event="newEvent" @close="finishCreation" />
<table style="margin-top: 40px; width: 100%" v-else>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Veranstaltungsname</th>
<td class="height-50"><input type="text" v-model="formData.eventName" class="width-half-full" />
<ErrorText :message="errors.eventName" /></td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Veranstaltungsort</th>
<td class="height-50"><input type="text" v-model="formData.eventLocation" class="width-half-full" />
<ErrorText :message="errors.eventLocation" /></td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Postleitzahl des Veranstaltungsorts</th>
<td class="height-50"><input type="text" v-model="formData.eventPostalCode" class="width-half-full" />
<ErrorText :message="errors.eventPostalCode" /></td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">E-Mail-Adresse der Veranstaltungsleitung</th>
<td class="height-50"><input type="email" v-model="formData.eventEmail" class="width-half-full" />
<ErrorText :message="errors.eventEmail" /></td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Beginn</th>
<td class="height-50"><input type="date" v-model="formData.eventBegin" class="width-half-full" />
<ErrorText :message="errors.eventBegin" /></td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Ende</th>
<td class="height-50"><input type="date" v-model="formData.eventEnd" class="width-half-full" />
<ErrorText :message="errors.eventEnd" /></td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Ende Early-Bird-Phase</th>
<td class="height-50"><input type="date" v-model="formData.eventEarlyBirdEnd" class="width-half-full" />
<ErrorText :message="errors.eventEarlyBirdEnd" /></td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Finaler Anmeldeschluss</th>
<td class="height-50"><input type="date" v-model="formData.eventRegistrationFinalEnd" class="width-half-full" />
<ErrorText :message="errors.eventRegistrationFinalEnd" />
</td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Beitragsart</th>
<td class="height-50">
<select v-model="formData.eventParticipationFeeType" class="width-half-full">
<option value="fixed">Festpreis</option>
<option value="solidarity">Solidaritätsprinzip</option>
</select>
</td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Preiserhöhung nach Early-Bird-Phase</th>
<td class="height-50"><input type="number" v-model="formData.eventEarlyBirdEndAmountIncrease" class="width-tiny" />%</td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Veranstsaltungs-Konto</th>
<td class="height-50"><input type="text" v-model="formData.eventAccount" class="width-full" />
<ErrorText :message="errors.eventAccount" /></td>
</tr>
<tr style="vertical-align: top;">
<th class="width-medium pr-20 height-50">Veranstaltungs-IBAN</th>
<td class="height-50"><input type="text" v-model="formData.eventIban" class="width-full" />
<ErrorText :message="errors.eventIban" /></td>
</tr>
<tr style="vertical-align: top;">
<td colspan="2" style="font-weight: bold;">
<input type="checkbox" v-model="formData.eventPayDirectly">
Teilnehmende zahlen direkt aufs Veranstaltungskonto
</td>
</tr>
<tr style="vertical-align: top;">
<td colspan="2" style="font-weight: bold;">
<input type="checkbox" v-model="formData.eventPayPerDay">
Beitrag abhängig von Anwesenheitstagen
</td>
</tr>
<tr>
<td colspan="2" class="pt-20">
<input type="button" value="Veranstaltung erstellen" @click="createEvent" />
</td>
</tr>
</table>
</fieldset>
</AppLayout>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,60 @@
<script setup>
import {reactive, inject, onMounted} from 'vue';
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import ParticipantsList from "./Partials/ParticipantsList.vue";
import Overview from "./Partials/Overview.vue";
const props = defineProps({
event: Object,
})
const tabs = [
{
title: 'Übersicht',
component: Overview,
endpoint: "/api/v1/event/details/" + props.event.id + '/summary',
},
{
title: 'Alle Teilnehmendenden',
component: ParticipantsList,
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/all',
},
{
title: 'Teilis nach Stamm',
component: ParticipantsList,
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/by-local-group',
},
{
title: 'Teilis nach Teili-Gruppe',
component: ParticipantsList,
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/by-participation-group',
},
{
title: 'Abgemeldete Teilis',
component: ParticipantsList,
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/signed-off',
},
{
title: 'Zusätze',
component: ParticipantsList,
endpoint: "/api/v1/cost-unit/open/archived-cost-units",
},
]
onMounted(() => {
if (undefined !== props.message) {
toast.success(props.message)
}
})
</script>
<template>
<AppLayout :title="'Veranstaltungsdetails ' + props.event.name">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<tabbed-page :tabs="tabs" />
</shadowed-box>
</AppLayout>
</template>

View File

@@ -0,0 +1,19 @@
<script setup>
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
import AvailableEvents from "./Partials/AvailableEvents.vue";
const props = defineProps({
events: Array,
})
</script>
<template>
<AppLayout title="Verfügbare Veranstaltungen">
<AvailableEvents :events="props.events" />
</AppLayout>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,100 @@
<script setup>
import ShadowedBox from "../../../../Views/Components/ShadowedBox.vue";
const props = defineProps({
events: Array,
})
</script>
<template>
<div style="width: 95%; margin: 20px auto;">
<div v-if="props.events.length === 0" style="text-align: center; color: #6b7280; padding: 40px 0;">
Aktuell sind keine Veranstaltungen verfügbar.
</div>
<shadowed-box
v-for="event in props.events"
:key="event.id"
style="padding: 24px; margin-bottom: 20px;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px;">
<div>
<h2 style="margin: 0 0 4px 0; font-size: 1.25rem;">{{ event.name }}</h2>
<span style="color: #6b7280; font-size: 0.9rem;">{{ event.postalCode }} {{ event.location }}</span>
</div>
<span
v-if="event.registrationAllowed"
style="background: #d1fae5; color: #065f46; padding: 4px 12px; border-radius: 999px; font-size: 0.8rem; font-weight: 600; white-space: nowrap;"
>
Anmeldung offen
</span>
<span
v-else
style="background: #fee2e2; color: #991b1b; padding: 4px 12px; border-radius: 999px; font-size: 0.8rem; font-weight: 600; white-space: nowrap;"
>
Anmeldung geschlossen
</span>
</div>
<hr style="margin: 16px 0; border: none; border-top: 1px solid #e5e7eb;" />
<table style="width: 100%; border-collapse: collapse;">
<tr>
<th style="text-align: left; padding: 6px 12px 6px 0; width: 220px; color: #374151; font-weight: 600;">Zeitraum</th>
<td style="padding: 6px 0; color: #111827;">{{ event.eventBegin }} {{ event.eventEnd }} ({{ event.duration }} Tage)</td>
</tr>
<tr>
<th style="text-align: left; padding: 6px 12px 6px 0; width: 220px; color: #374151; font-weight: 600;">Veranstaltungsort</th>
<td style="padding: 6px 0; color: #111827;">{{ event.postalCode }} {{ event.location }}</td>
</tr>
<tr>
<th style="text-align: left; padding: 6px 12px 6px 0; color: #374151; font-weight: 600;">Frühbuchen bis</th>
<td style="padding: 6px 0; color: #111827;">{{ event.earlyBirdEnd.formatted }}</td>
</tr>
<tr>
<th style="text-align: left; padding: 6px 12px 6px 0; color: #374151; font-weight: 600;">Anmeldeschluss</th>
<td style="padding: 6px 0; color: #111827;">{{ event.registrationFinalEnd.formatted }}</td>
</tr>
<tr v-if="event.email">
<th style="text-align: left; padding: 6px 12px 6px 0; color: #374151; font-weight: 600;">Kontakt</th>
<td style="padding: 6px 0;">
<a :href="'mailto:' + event.email" style="color: #2563eb;">{{ event.email }}</a>
</td>
</tr>
</table>
<div style="margin-top: 20px; display: flex; justify-content: flex-end;">
<a
:href="'/event/' + event.identifier + '/signup'"
style="
display: inline-block;
padding: 10px 24px;
background-color: #2563eb;
color: white;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
font-size: 0.95rem;
opacity: 1;
transition: background-color 0.2s;
"
:style="{ opacity: event.registrationAllowed ? '1' : '0.5', pointerEvents: event.registrationAllowed ? 'auto' : 'none' }"
>
Zur Anmeldung
</a>
</div>
</shadowed-box>
</div><div style="width: 95%; margin: 20px auto;">
<div v-if="props.events.length === 0" style="text-align: center; color: #6b7280; padding: 40px 0;">
Aktuell sind keine Veranstaltungen verfügbar.
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,258 @@
<script setup>
import {onMounted, reactive, ref} from "vue";
import ErrorText from "../../../../Views/Components/ErrorText.vue";
import AmountInput from "../../../../Views/Components/AmountInput.vue";
import {request} from "../../../../../resources/js/components/HttpClient.js";
import {toast} from "vue3-toastify";
const emit = defineEmits(['close'])
const props = defineProps({
event: Object,
})
const dynmicProps = reactive({
localGroups: [],
eatingHabits:[]
});
const contributingLocalGroups = ref([])
const eatingHabits = ref([]);
const errors = reactive({})
const formData = reactive({
contributingLocalGroups: contributingLocalGroups.value,
eventName: props.event.name,
eventLocation: props.event.location,
postalCode: props.event.postalCode,
email: props.event.email,
earlyBirdEnd: props.event.earlyBirdEnd.internal,
registrationFinalEnd: props.event.registrationFinalEnd.internal,
alcoholicsAge: props.event.alcoholicsAge,
eatingHabits: eatingHabits.value,
sendWeeklyReports: props.event.sendWeeklyReports,
registrationAllowed: props.event.registrationAllowed,
flatSupport: props.event.flatSupportEdit,
supportPerson: props.event.supportPersonIndex,
})
onMounted(async () => {
const response = await fetch('/api/v1/core/retrieve-event-setting-data');
const data = await response.json();
Object.assign(dynmicProps, data);
contributingLocalGroups.value = props.event.contributingLocalGroups?.map(t => t.id) ?? []
eatingHabits.value = props.event.eatingHabits?.map(t => t.id) ?? []
});
async function save() {
const response = await request('/api/v1/event/details/' + props.event.id + '/common-settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: {
eventName: formData.eventName,
eventLocation: formData.eventLocation,
postalCode: formData.postalCode,
email: formData.email,
earlyBirdEnd: formData.earlyBirdEnd,
registrationFinalEnd: formData.registrationFinalEnd,
alcoholicsAge: formData.alcoholicsAge,
sendWeeklyReports: formData.sendWeeklyReports,
registrationAllowed: formData.registrationAllowed,
flatSupport: formData.flatSupport,
supportPerson: formData.supportPerson,
contributingLocalGroups: contributingLocalGroups.value,
eatingHabits: eatingHabits.value,
}
})
if (response.status === 'success') {
toast.success('Einstellungen wurden erfolgreich gespeichert.')
emit('close')
} else {
toast.error('Beim Speichern ist ein Fehler aufgetreten.')
}
}
</script>
<template>
<h2>Einstellungen</h2>
<div class="container">
<div class="row top">
<div class="left">
<table class="event-settings-table" style="width: 80%;">
<tr>
<th>Veranstaltungsname</th>
<td>
<input type="text" v-model="formData.eventName" class="width-full" /><br />
<ErrorText :message="errors.eventName" />
</td>
</tr>
<tr>
<th>Veranstaltungsort</th>
<td>
<input type="text" v-model="formData.eventLocation" class="width-full" /><br />
<ErrorText :message="errors.eventLocation" />
</td>
</tr>
<tr>
<th>Postleitzahl des Veranstaltungsorts</th>
<td>
<input type="text" v-model="formData.postalCode" class="width-full" /><br />
<ErrorText :message="errors.eventPostalCode" />
</td>
</tr>
<tr>
<th>E-Mail-Adresse der Veranstaltungsleitung</th>
<td>
<input type="text" v-model="formData.email" class="width-full" /><br />
<ErrorText :message="errors.eventEmail" />
</td>
</tr>
<tr>
<th>Ende der EarlyBird-Phase</th>
<td>
<input type="date" v-model="formData.earlyBirdEnd" class="width-full" /><br />
<ErrorText :message="errors.earlyBirdEnd" />
</td>
</tr>
<tr>
<th>Finaler Anmeldeschluss</th>
<td>
<input type="date" v-model="formData.registrationFinalEnd" class="width-full" /><br />
<ErrorText :message="errors.registrationFinalEnd" />
</td>
</tr>
<tr>
<th>Fördermittel</th>
<td>
<amountInput v-model="formData.supportPerson" clasS="width-small" /> Euro p.P. / Tag
</td>
</tr>
<tr>
<th>Zuschüsse</th>
<td>
<amountInput v-model="formData.flatSupport" clasS="width-small" /> Euro pauschal
</td>
</tr>
<tr>
<th>Mindestalter für Alkoholkonsum</th>
<td>
<input type="number" v-model="formData.alcoholicsAge" class="width-tiny" /><br />
<ErrorText :message="errors.alcoholicsAge" />
</td>
</tr>
<tr>
<td colspan="2" style="height: 25px !important;">
<input type="checkbox" v-model="formData.sendWeeklyReports" id="sendWeeklyReports" />
<label for="sendWeeklyReports">Wöchentliche Zusammenfassung per E-Mail an Stämme schicken</label>
</td>
</tr>
<tr>
<td colspan="2">
<input type="checkbox" v-model="formData.registrationAllowed" id="registrationAllowed" />
<label for="registrationAllowed">Veranstaltung ist für Anmeldungen geöffnet</label>
</td>
</tr>
</table>
</div>
<div class="right">
<table>
<tr>
<th>Teilnehmende Stämme</th>
</tr>
<tr v-for="localGroup in dynmicProps.localGroups">
<td>
<input type="checkbox" :id="'localgroup_' + localGroup.id" :value="localGroup.id" v-model="contributingLocalGroups" />
<label style="padding-left: 5px;" :for="'localgroup_' + localGroup.id">{{localGroup.name}}</label>
</td>
</tr>
<tr>
<th style="padding-top: 40px !important;">Angebotene Ernährung</th>
</tr>
<tr v-for="eatingHabit in dynmicProps.eatingHabits">
<td>
<input type="checkbox" :id="'eatinghabit' + eatingHabit.id" :value="eatingHabit.id" v-model="eatingHabits" />
<label style="padding-left: 5px;" :for="'eatinghabit' + eatingHabit.id">{{eatingHabit.name}}</label>
</td>
</tr>
</table>
</div>
</div>
<div class="row bott">
<input type="button" value="Speichern" @click="save" />
</div>
</div>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
gap: 10px; /* Abstand zwischen den Zeilen */
width: 95%;
margin: auto;
}
.row {
display: flex;
gap: 10px; /* Abstand zwischen den Spalten */
}
.row.top .left {
flex: 0 0 70%; /* feste Breite von 80% */
padding: 10px;
}
.row.top .right {
flex: 0 0 30%; /* feste Breite von 20% */
padding: 10px;
}
.row.bottom {
padding: 10px;
}
.event-settings-table {
}
.event-settings-table tr {
vertical-align: top;
}
.event-settings-table td {
height: 50px;
}
.event-settings-table th {
vertical-align: top;
width: 250px;
}
</style>

View File

@@ -0,0 +1,63 @@
<script setup>
import {onMounted, reactive, ref} from "vue";
import {toast} from "vue3-toastify";
import {request} from "../../../../../resources/js/components/HttpClient.js";
const selectedManagers = ref([])
const emit = defineEmits(['close'])
const props = defineProps({
event: Object
})
const commonProps = reactive({
activeUsers: [],
});
onMounted(async () => {
const response = await fetch('/api/v1/core/retrieve-global-data');
const data = await response.json();
Object.assign(commonProps, data);
selectedManagers.value = props.event.managers?.map(t => t.id) ?? []
});
async function updateManagers() {
const response = await request('/api/v1/event/details/' + props.event.id + '/event-managers', {
method: "POST",
body: {
selectedManagers: selectedManagers.value,
}
});
if (response.status === 'success') {
toast.success('Einstellungen wurden erfolgreich gespeichert.')
emit('close')
} else {
toast.error('Beim Speichern ist ein Fehler aufgetreten.')
}
}
</script>
<template>
<h3>Aktionsleitung:</h3>
<p v-for="user in commonProps.activeUsers">
<input
type="checkbox"
:id="'user_' + user.id"
:value="user.id"
v-model="selectedManagers"
/>
<label :for="'user_' + user.id">{{user.fullname}}</label>
</p>
<input type="button" value="Speichern" @click="updateManagers" />
</template>

View File

@@ -0,0 +1,114 @@
<script setup>
import {onMounted, reactive, ref} from "vue";
import {useAjax} from "../../../../../resources/js/components/ajaxHandler.js";
import TextEditor from "../../../../Views/Components/TextEditor.vue";
import ErrorText from "../../../../Views/Components/ErrorText.vue";
import {toast} from "vue3-toastify";
import InfoText from "../../../../Views/Components/InfoText.vue";
const { request } = useAjax();
const props = defineProps({
event: Object,
mailToType: String,
recipientIdentifier: String,
})
const data = reactive({
recipients: null,
});
onMounted(async () => {
const groupTypes = ['all', 'signed-off', 'by-local-group', 'by-participation-group'];
if (!groupTypes.includes(props.mailToType)) {
console.error('Unknown recipient identifier:', props.recipientIdentifier);
return;
}
const response = await request('/api/v1/event/' + props.event.identifier + '/mailing/compose/to-group/' + props.mailToType, {
method: "POST",
body: {
groupName: props.recipientIdentifier
}
});
data.recipients = response.recipients;
form.recipients = data.recipients.join(', ');
});
const errorMessage = ref(null)
const infoMessage = ref(null)
const emit = defineEmits([
'closeComposer',
]);
function close() {
emit('closeComposer');
}
async function sendMail() {
document.getElementById('sendMessageButton').style.display = 'none';
infoMessage.value = 'Die Rundmail wird nun gesendet. Dies kann einen Moment dauern. Bitte verlasse diese Seite nicht.'
toast.info('Die Rundmail wird nun gesendet. Dies kann einen Moment dauern. Bitte verlasse diese Seite nicht.')
const response = await request('/api/v1/event/' + props.event.identifier + '/mailing/send', {
method: "POST",
body: {
recipients: form.recipients,
subject: form.subject,
message: form.message,
}
});
if (response.success) {
infoMessage.value = null
document.getElementById('sendMessageButton').style.display = 'block';
close();
toast.success(response.message)
} else {
infoMessage.value = null
document.getElementById('sendMessageButton').style.display = 'block';
errorMessage.value = response.message
toast.error(response.message)
}
}
const form = reactive({
recipients: '',
sendCopy: true,
subject: '',
message: '',
});
</script>
<template>
<h2>E-Mail senden</h2>
<div style="display: flex; flex-direction: column; gap: 12px;">
<div>
<label style="font-weight: bold">Empfänger*innen</label>
<textarea v-model="form.recipients" placeholder="Senden an" style="width: 100%;" rows="3"></textarea>
</div>
<div>
<label style="font-weight: bold">Betreff</label>
<input type="text" v-model="form.subject" placeholder="Betreff" style="width: 100%;" />
</div><br /><br />
<div>
<label style="font-weight: bold">Nachricht</label>
<TextEditor v-model="form.message" />
</div>
<strong><ErrorText :message="errorMessage" /></strong>
</div>
<info-text :message="infoMessage" />
<input type="button" id="sendMessageButton" @click="sendMail" value="Senden" class="" />
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,233 @@
<script setup>
import {onMounted, reactive, ref} from "vue";
import ParticipationFees from "./ParticipationFees.vue";
import ParticipationSummary from "./ParticipationSummary.vue";
import CommonSettings from "./CommonSettings.vue";
import EventManagement from "./EventManagement.vue";
import Modal from "../../../../Views/Components/Modal.vue";
import MailCompose from "./MailCompose.vue";
import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue";
import {toast} from "vue3-toastify";
const props = defineProps({
data: Object,
})
const dynamicProps = reactive({
event : null,
});
const displayData = ref('main');
const showEventData = ref(false);
async function showMain() {
const response = await fetch("/api/v1/event/details/" + props.data.event.id + '/summary');
const data = await response.json();
Object.assign(dynamicProps, data);
displayData.value = 'main';
}
async function showCommonSettings() {
displayData.value = 'commonSettings';
}
async function showParticipationFees() {
displayData.value = 'participationFees';
}
async function showEventManagement() {
displayData.value = 'eventManagement';
}
async function eventData() {
showEventData.value = true;
}
onMounted(async () => {
const response = await fetch("/api/v1/event/details/" + props.data.event.id + '/summary');
const data = await response.json();
Object.assign(dynamicProps, data);
});
const mailCompose = ref(false);
function mailToGroup() {
mailCompose.value = true
}
async function sendPaymentReminder() {
toast.info("Die Nachrichten werden gesendet. Bitte verlasse diese Seite nicht.");
const response = await fetch("/api/v1/event/" + props.data.event.identifier + "/send-payment-reminder/" );
const data = await response.json();
if (data.success) {
toast.success(data.message)
} else {
toast.error(data.message)
}
}
</script>
<template>
<ParticipationFees v-if="displayData === 'participationFees'" :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" />
<div class="event-flexbox" v-else>
<div class="event-flexbox-row top">
<div class="left"><ParticipationSummary v-if="dynamicProps.event" :event="dynamicProps.event" /></div>
<div class="right">
<a :href="'/event/details/' + props.data.event.identifier + '/pdf/first-aid-list'">
<input type="button" value="Erste-Hilfe-Liste (PDF)" />
</a><br/>
<a :href="'/event/details/' + props.data.event.identifier + '/csv/participant-list'">
<input type="button" value="Teili-Liste (CSV)" />
</a><br/>
<a :href="'/event/details/' + props.data.event.identifier + '/pdf/kitchen-list'">
<input type="button" value="Küchenübersicht (PDF)" />
</a><br/>
<a :href="'/event/details/' + props.data.event.identifier + '/pdf/amount-list'">
<input type="button" value="Beitragsliste (PDF)" />
</a><br/>
<a :href="'/event/details/' + props.data.event.identifier + '/pdf/drinking-list'">
<input type="button" value="Getränkeliste (PDF)" />
</a><br/>
<a :href="'/event/details/' + props.data.event.identifier + '/pdf/photo-permission-list'">
<input type="button" value="Foto-Erlaubnis (PDF)" />
</a><br/>
<input type="button" @click="sendPaymentReminder" class="fix-button" value="Zahlungserinnerung senden" /><br/>
<input type="button" class="deny-button" value="Letzte Mahnung senden" style="display: none" /><br/>
<input type="button" value="Rundmail senden" @click="mailToGroup" /><br/>
</div>
</div>
<div>
<table>
<tr>
<th style="width: 200px">Veranstaltung</th>
<td>{{ dynamicProps.event.name }}</td>
</tr>
<tr>
<th style="width: 200px">Beginn</th>
<td>{{ dynamicProps.event.eventBegin }}</td>
</tr>
<tr>
<th>Ende</th>
<td>{{ dynamicProps.event.eventEnd }}</td>
</tr>
<tr>
<th>Anmeldeschluss</th>
<td>{{dynamicProps.event.earlyBirdEnd.formatted}}</td>
</tr>
<tr>
<th>Nachmeldeschluss</th>
<td>{{dynamicProps.event.registrationFinalEnd.formatted}}</td>
</tr>
<tr>
<th>Anmelde-URL</th>
<td>
{{dynamicProps.event.url}}<br />
<img :src="'/print-event-code/' + dynamicProps.event.identifier" alt="Event Code" style="width: 150px; height: 150px; margin-top: 20px;" />
</td>
</tr>
</table>
</div>
<div class="event-flexbox-row bottom">
<label style="font-size: 9pt;" class="link" @click="showCommonSettings">Allgemeine Einstellungen</label> &nbsp;
<label style="font-size: 9pt;" class="link" @click="showEventManagement">Veranstaltungsleitung</label> &nbsp;
<label style="font-size: 9pt;" class="link" @click="showParticipationFees">Teilnahmegebühren</label>
<a style="font-size: 9pt;" class="link" :href="'/cost-unit/' + props.data.event.costUnit.id">Ausgabenübersicht</a>
</div>
</div>
<Modal title="Veranstaltungsdetails" v-if="showEventData" :show="showEventData" @close="showEventData = false">
<table>
<tr>
<th>Beginn</th>
<td>{{ dynamicProps.event.eventBegin }}</td>
</tr>
<tr>
<th>Ende</th>
<td>{{ dynamicProps.event.eventEnd }}</td>
</tr>
<tr>
<th>Anmeldeschluss</th>
<td>{{dynamicProps.event.earlyBirdEnd.formatted}}</td>
</tr>
<tr>
<th>Nachmeldeschluss</th>
<td>{{dynamicProps.event.registrationFinalEnd.formatted}}</td>
</tr>
<tr>
<th>Anmelde-URL</th>
<td>
{{dynamicProps.event.url}}<br />
<img :src="'/print-event-code/' + dynamicProps.event.identifier" alt="Event Code" style="width: 150px; height: 150px; margin-top: 20px;" />
</td>
</tr>
</table>
</Modal>
<FullScreenModal
:show="mailCompose"
title="E-Mail senden"
@close="mailCompose = false"
>
<MailCompose @closeComposer="mailCompose = false" :event="dynamicProps.event" mailToType="all" recipientIdentifier="Alle Teilnehmenden" />
</FullScreenModal>
</template>
<style>
.event-flexbox {
display: flex;
flex-direction: column;
gap: 10px;
width: 95%;
margin: 20px auto 0;
}
.event-flexbox-row {
display: flex;
gap: 10px; /* Abstand zwischen den Spalten */
}
.event-flexbox-row.top .left {
flex: 0 0 calc(100% - 300px);
padding: 10px;
}
.event-flexbox-row.top .right {
flex: 0 0 250px;
padding: 10px;
}
.event-flexbox-row.bottom {
padding: 10px;
}
.event-flexbox-row.top .right input[type="button"] {
width: 100% !important;
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,413 @@
<script setup>
import {onMounted, reactive, watch} from "vue";
import AmountInput from "../../../../Views/Components/AmountInput.vue";
const staticProps = defineProps({
editMode: Boolean,
participant: Object,
event: Object,
});
const props = reactive({
participant: staticProps.participant,
});
onMounted(async () => {
const response = await fetch('/api/v1/event/participant/' + staticProps.participant.identifier + '/');
const data = await response.json();
Object.assign(props, data);
});
const emit = defineEmits([
'closeParticipantDetails',
'markCocExisting',
'paymentComplete',
'cancelParticipation',
'editParticipant',
'saveParticipant',
]);
const form = reactive({
firstname: '',
lastname: '',
nickname: '',
address_1: '',
address_2: '',
postcode: '',
city: '',
localgroup: '',
birthday: '',
email_1: '',
phone_1: '',
contact_person: '',
email_2: '',
phone_2: '',
arrival: '',
departure: '',
participationType: '',
eatingHabit: '',
allergies: '',
intolerances: '',
medications: '',
extendedFirstAid: '',
swimmingPermission: '',
tetanusVaccination: '',
notes: '',
amountPaid: '',
amountExpected: '',
cocStatus: '',
});
watch(
() => props.participant,
(participant) => {
form.firstname = participant?.firstname ?? '';
form.lastname = participant?.lastname ?? '';
form.nickname = participant?.nickname ?? '';
form.address_1 = participant?.address_1 ?? '';
form.address_2 = participant?.address_2 ?? '';
form.postcode = participant?.postcode ?? '';
form.city = participant?.city ?? '';
form.localgroup = participant?.local_group ?? '';
form.birthday = participant?.birthdayDate ?? '';
form.email_1 = participant?.email_1 ?? '';
form.phone_1 = participant?.phone_1 ?? '';
form.contact_person = participant?.contact_person ?? '';
form.email_2 = participant?.email_2 ?? '';
form.phone_2 = participant?.phone_2 ?? '';
form.arrival = participant?.arrivalDate ?? '';
form.departure = participant?.departureDate ?? '';
form.participationType = participant?.participation_type ?? '';
form.eatingHabit = participant?.eating_habit ?? '';
form.allergies = participant?.allergies ?? '';
form.intolerances = participant?.intolerances ?? '';
form.medications = participant?.medications ?? '';
form.extendedFirstAid = participant?.first_aid_permission ?? '';
form.swimmingPermission = participant?.swimming_permission ?? '';
form.tetanusVaccination = participant?.tetanus_vaccination ?? '';
form.notes = participant?.notes ?? '';
form.amountPaid = participant?.amountPaid.short ?? '';
form.amountExpected = participant?.amountExpected.short ?? '';
form.cocStatus = participant?.efz_status ?? '';
},
{ immediate: true }
);
function markCocExisting(participant) {
emit('markCocExisting', participant);
close();
}
function paymentComplete(participant) {
emit('paymentComplete', participant);
close();
}
function close() {
emit('closeParticipantDetails');
}
function cancelParticipation(participant) {
emit('cancelParticipation', participant);
close();
}
function enableEditMode() {
emit('editParticipant');
}
function saveParticipant() {
emit('saveParticipant', { ...form });
close();
}
</script>
<template>
<div>
<h2>Anmeldedetails</h2>
<div class="participationData">
<div>
<h3>Persönliche Daten</h3>
<table>
<tr>
<th>Name</th>
<td>
<span v-if="!staticProps.editMode">
{{ props.participant.firstname }} {{ props.participant.lastname }}
</span>
<span v-else>
<input v-model="form.firstname" type="text" placeholder="Vorname" />
<input v-model="form.lastname" type="text" placeholder="Nachname" />
</span>
</td>
</tr>
<tr>
<th>Pfadiname</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.nickname }}</span>
<input v-else v-model="form.nickname" type="text" />
</td>
</tr>
<tr>
<th>Anschrift</th>
<td>
<span v-if="!staticProps.editMode">
{{ props.participant.address_1 }}<br />
{{ props.participant.address_2 }}<br />
{{ props.participant.postcode }}
{{ props.participant.city }}
</span>
<span v-else>
<input v-model="form.address_1" type="text" placeholder="Adresse 1" />
<input v-model="form.address_2" type="text" placeholder="Adresse 2" />
<input v-model="form.postcode" type="text" placeholder="PLZ" />
<input v-model="form.city" type="text" placeholder="Ort" />
</span>
</td>
</tr>
<tr>
<th>Stamm</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.localgroup }}</span>
<select v-else v-model="form.localgroup">
<option v-for="group in staticProps.event.contributingLocalGroups" :key="group.id" :value="group.slug">{{ group.name }}</option>
</select>
</td>
</tr>
<tr>
<th>Geburtsdatum</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.birthday }}</span>
<input v-else v-model="form.birthday" type="date" />
</td>
</tr>
</table>
</div>
<div>
<h3>Kontaktdaten</h3>
<table>
<tr>
<th>E-Mail</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.email_1 }}</span>
<input v-else v-model="form.email_1" type="email" />
</td>
</tr>
<tr>
<th>Telefon</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.phone_1 }}</span>
<input v-else v-model="form.phone_1" type="text" />
</td>
</tr>
<tr>
<th>Ansprechperson</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.contact_person }}</span>
<input v-else v-model="form.contact_person" type="text" />
</td>
</tr>
<tr>
<th>Ansprechperson E-Mail</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.email_2 }}</span>
<input v-else v-model="form.email_2" type="email" />
</td>
</tr>
<tr>
<th>Ansprechperson Telefon</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.phone_2 }}</span>
<input v-else v-model="form.phone_2" type="text" />
</td>
</tr>
</table>
</div>
</div>
<div class="participationData">
<div>
<h3>Teilnahmedetails</h3>
<table>
<tr>
<th>Anreise</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.arrival }}</span>
<input v-else v-model="form.arrival" type="date" />
</td>
</tr>
<tr>
<th>Abreise</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.departure }}</span>
<input v-else v-model="form.departure" type="date" />
</td>
</tr>
<tr>
<th>Teilnahmegruppe</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.participationType }}</span>
<select v-else v-model="form.participationType">
<option
v-for="participationType in staticProps.event.participationTypes"
:value="participationType.type.slug"
>
{{ participationType.type.name }}
</option>
</select>
</td>
</tr>
<tr>
<th>Ernährung</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.eatingHabit }}</span>
<select v-else v-model="form.eatingHabit">
<option
v-for="eatingHabit in staticProps.event.eatingHabits"
:value="eatingHabit.slug"
>
{{ eatingHabit.name }}
</option>
</select>
</td>
</tr>
<tr>
<th>eFZ-Status</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.efzStatusReadable }}</span>
<select v-else v-model="form.cocStatus">
<option value="not_checked">Nicht geprüft</option>
<option value="not_required">Nicht erforderlich</option>
<option value="checked_valid">Geprüft und Vorhanden</option>
<option value="checked_invalid">Nicht vorhanden</option>
</select>
</td>
</tr>
<tr>
<th>Beitrag</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.amountPaid.readable }} / {{ props.participant.amountExpected.readable }}</span>
<span v-else>
<AmountInput v-model="form.amountPaid" style="width:74px" /> Euro
/
<AmountInput v-model="form.amountExpected" style="width: 74px" /> Euro
</span>
</td>
</tr>
</table>
</div>
<div>
<h3>Medizinische Informationen</h3>
<table>
<tr>
<th>Allergien</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.allergies }}</span>
<input type="text" v-else v-model="form.allergies" />
</td>
</tr>
<tr>
<th>Unverträglichkeiten</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.intolerances }}</span>
<input type="text" v-else v-model="form.intolerances" />
</td>
</tr>
<tr>
<th>Medikamente</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.medications }}</span>
<input type="text" v-else v-model="form.medications" />
</td>
</tr>
<tr>
<th>Erweiterte Erste Hilfe</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.extendedFirstAid }}</span>
<select v-else v-model="form.extendedFirstAid">
<option value="FIRST_AID_PERMISSION_ALLOWED">Erlaubt</option>
<option value="FIRST_AID_PERMISSION_DENIED">Verweigert</option>
</select>
</td>
</tr>
<tr>
<th>Badeerlaubnis</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.swimmingPermission }}</span>
<select v-else v-model="form.swimmingPermission">
<option value="SWIMMING_PERMISSION_ALLOWED">Vorhanden, kann schwimmen</option>
<option value="SWIMMING_PERMISSION_LIMITED">Vorhanden, kann nicht schwimmen</option>
<option value="SWIMMING_PERMISSION_DENIED">Verweigert</option>
</select>
</td>
</tr>
<tr>
<th>Letzte Tetanus-Impfung</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.tetanusVaccination }}</span>
<input v-else v-model="form.tetanusVaccination" type="date" />
</td>
</tr>
<tr>
<th>Anmerkungen</th>
<td>
<span v-if="!staticProps.editMode">{{ props.participant.notes }}</span>
<textarea v-else v-model="form.notes"></textarea>
</td>
</tr>
</table>
</div>
</div>
</div>
<button v-if="!staticProps.editMode" class="button" @click="enableEditMode">Bearbeiten</button>
<button v-else class="button" @click="saveParticipant">Speichern</button>
<button v-if="!props.participant.unregistered" class="button" @click="paymentComplete(props.participant)">Zahlung vollständig</button>
<button v-if="!props.participant.unregistered" class="button" @click="markCocExisting(props.participant)">eFZ liegt vor</button>
<button v-if="!props.participant.unregistered" class="button" @click="cancelParticipation(props.participant)">Abmelden</button>
<button class="button" @click="close">Schließen</button>
</template>
<style scoped>
.participationData {
display: flex;
margin-bottom: 20px;
gap: 20px;
}
.participationData div {
flex: 1;
vertical-align: top;
}
input[type="text"],
input[type="email"],
input[type="date"],
textarea
{
width: 250px
}
textarea {
height: 100px;
}
select {
width: 262px;
}
</style>

View File

@@ -0,0 +1,538 @@
<script setup>
import { computed, reactive, ref } from "vue";
import Modal from "../../../../Views/Components/Modal.vue";
import ParticipantData from "./ParticipantData.vue";
import MailCompose from "./MailCompose.vue";
import {toast} from "vue3-toastify";
import {useAjax} from "../../../../../resources/js/components/ajaxHandler.js";
import {format, getDay, getMonth, getYear} from "date-fns";
import AmountInput from "../../../../Views/Components/AmountInput.vue";
import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue";
const props = defineProps({
data: {
type: Object,
default: () => ({
localGroups: {},
participants: {},
event: {},
listType: '',
}),
},
});
const today = format(new Date(), "yyyy-MM-dd");
const { request } = useAjax();
const searchTerms = reactive({});
const selectedStatuses = reactive({});
const event = computed(() => props.data?.event ?? {});
const participantGroups = computed(() => props.data?.participants ?? {});
const showParticipantDetails = ref(false);
const showParticipant = ref(null);
const editMode = ref(false);
const mailCompose = ref(false);
const openCancelDialog = ref(false);
const openPartialPaymentDialogSwitch = ref(false);
defineEmits(['showParticipantDetails', 'markCocExisting', 'paymentComplete'])
function openParticipantDetails(input) {
showParticipantDetails.value = true;
showParticipant.value = input;
editMode.value = false;
}
async function saveParticipant(formData) {
if (!showParticipant.value?.identifier) {
return;
}
const data = await request('/api/v1/event/participant/' + showParticipant.value.identifier + '/update', {
method: "POST",
body: formData,
});
if (data.status === 'success') {
toast.success(data.message ?? 'Die Änderungen wurden gespichert. Die Liste wird beim nächsten Neuladen neu generiert.');
document.getElementById('participant-' + data.participant.identifier + '-fullname').innerHTML = data.participant.fullname;
document.getElementById('participant-' + data.participant.identifier + '-birthday').innerText = data.participant.birthday;
document.getElementById('participant-' + data.participant.identifier + '-age').innerText = data.participant.age;
document.getElementById('participant-' + data.participant.identifier + '-localgroup').innerText = data.participant.localgroup;
document.getElementById('participant-' + data.participant.identifier + '-arrival').innerText = data.participant.arrival;
document.getElementById('participant-' + data.participant.identifier + '-departure').innerText = data.participant.departure;
document.getElementById('participant-' + data.participant.identifier + '-email_1').innerText = data.participant.email_1;
document.getElementById('participant-' + data.participant.identifier + '-email_2').innerText = data.participant.email_2 ?? '--';
document.getElementById('participant-' + data.participant.identifier + '-phone_1').innerText = data.participant.phone_1;
document.getElementById('participant-' + data.participant.identifier + '-phone_2').innerText = data.participant.phone_2 ?? '--';
if (data.cocChanged) {
document.getElementById('participant-' + data.identifier + '-coc-status').innerText = data.coc.statusText;
document.getElementById('participant-' + data.identifier + '-coc-action').style.display=data.coc.action;
document.getElementById('participant-' + data.identifier + '-name').className = data.coc.class;
}
if (data.amountChanged) {
document.getElementById('participant-' + data.identifier + '-payment').removeAttribute('class');
document.getElementById('participant-' + data.identifier + '-paid').innerText = data.amount.paid;
document.getElementById('participant-' + data.identifier + '-expected').innerText = data.amount.expected;
document.getElementById('participant-' + data.identifier + '-actions').style.display=data.amount.actions;
document.getElementById('participant-' + data.identifier + '-payment').className = data.amount.class;
}
editMode.value = false;
} else {
toast.error(data.message ?? 'Speichern fehlgeschlagen');
}
}
const getGroupEntries = computed(() => {
return Object.entries(participantGroups.value ?? {});
});
const getAgeCounts = (participants) => {
const buckets = {
'0-5': 0,
'6-11': 0,
'12-15': 0,
'16-17': 0,
'18-27': 0,
'27+': 0,
};
(participants ?? []).forEach((participant) => {
const age = Number(participant?.age);
if (!Number.isFinite(age)) {
return;
}
if (age >= 0 && age <= 5) {
buckets['0-5']++;
} else if (age <= 11) {
buckets['6-11']++;
} else if (age <= 15) {
buckets['12-15']++;
} else if (age <= 17) {
buckets['16-17']++;
} else if (age <= 27) {
buckets['18-27']++;
} else {
buckets['27+']++;
}
});
return buckets;
};
const getSearchText = (participant) => {
return [
participant?.firstname,
participant?.lastname,
participant?.nickname,
participant?.email_1,
participant?.email_2,
participant?.phone_1,
participant?.phone_2,
participant?.local_group_string,
participant?.participation_type_string,
participant?.efz_status_string,
participant?.amount_string,
]
.filter(Boolean)
.join(" ")
.toLowerCase();
};
const getFilteredParticipants = (groupKey, participants) => {
const searchTerm = (searchTerms[groupKey] ?? "").trim().toLowerCase();
return (participants ?? []).filter((participant) => {
const matchesSearch =
!searchTerm ||
getSearchText(participant).includes(searchTerm);
return matchesSearch;
});
};
const getRowClass = (participant) => {
if (participant?.unregistered_at) {
return "bg-gray-50 text-gray-500";
}
if (
Number(participant?.amount?.amount ?? participant?.amount ?? 0) !==
Number(participant?.amount_paid?.amount ?? participant?.amount_paid ?? 0)
) {
return "bg-red-50";
}
return "";
};
async function paymentComplete(participant) {
const data = await request('/api/v1/event/participant/' + participant.identifier + '/payment-complete', {
method: "POST",
});
if (data.status === 'success') {
toast.success(data.message);
document.getElementById('participant-' + participant.identifier + '-payment').removeAttribute('class');
document.getElementById('participant-' + participant.identifier + '-paid').innerText = participant.amountExpected.readable;
document.getElementById('participant-' + participant.identifier + '-actions').style.display='none';
} else {
toast.error(data.message);
}
}
async function markCocExisting(participant) {
const data = await request('/api/v1/event/participant/' + participant.identifier + '/mark-coc-existing', {
method: "POST",
});
if (data.status === 'success') {
toast.success(data.message);
document.getElementById('participant-' + participant.identifier + '-coc-status').innerText = 'Gültig';
document.getElementById('participant-' + participant.identifier + '-coc-action').style.display='none';
document.getElementById('participant-' + participant.identifier + '-name').removeAttribute('class');
} else {
toast.error(data.message);
}
}
function openCancelParticipationDialog(participant) {
showParticipant.value = participant;
openCancelDialog.value = true;
}
async function execCancelParticipation() {
const data = await request('/api/v1/event/participant/' + showParticipant.value.identifier + '/signoff', {
method: "POST",
body: {
cancel_date: document.getElementById('cancel_date').value,
},
});
if (data.status === 'success') {
toast.success(data.message);
document.getElementById('participant-' + data.identifier + '-common').style.display = 'none';
document.getElementById('participant-' + data.identifier + '-meta').style.display = 'none';
} else {
toast.error(data.message);
}
openCancelDialog.value = false;
}
async function execResignonParticipant(participant) {
const data = await request('/api/v1/event/participant/' + participant.identifier + '/re-signon', {
method: "POST",
});
if (data.status === 'success') {
toast.success(data.message);
document.getElementById('participant-' + data.identifier + '-common').style.display = 'none';
document.getElementById('participant-' + data.identifier + '-meta').style.display = 'none';
} else {
toast.error(data.message);
}
openCancelDialog.value = false;
}
function openPartialPaymentDialog(participant) {
showParticipant.value = participant;
openPartialPaymentDialogSwitch.value = true;
}
async function execPartialPayment() {
const data = await request('/api/v1/event/participant/' + showParticipant.value.identifier + '/partial-payment', {
method: "POST",
body: {
amount: document.getElementById('partial_payment_amount').value,
},
});
if (data.status === 'success') {
toast.success(data.message);
document.getElementById('participant-' + data.identifier + '-payment').removeAttribute('class');
document.getElementById('participant-' + data.identifier + '-paid').innerText = data.amount.paid;
document.getElementById('participant-' + data.identifier + '-expected').innerText = data.amount.expected;
document.getElementById('participant-' + data.identifier + '-actions').style.display=data.amount.actions;
document.getElementById('participant-' + data.identifier + '-payment').className = data.amount.class;
} else {
toast.error(data.message);
}
openPartialPaymentDialogSwitch.value = false;
}
const mailToType = ref('')
const recipientIdentifier = ref('')
function mailToGroup(groupKey) {
recipientIdentifier.value = groupKey;
mailToType.value = props.data.listType;
mailCompose.value = true
}
</script>
<template>
<h2>{{ event?.name ?? "Veranstaltung" }}</h2>
<div :key="groupKey">
<div>
<table style="width: 95%; margin: 20px auto; border-collapse: collapse;" v-for="[groupKey, participants] in getGroupEntries">
<thead>
<tr>
<th colspan="4" style="background: linear-gradient(to bottom, #fff, #f6f7f7); font-weight: bold">
{{ groupKey }} ({{ participants.length }} Personen)
</th>
</tr>
<tr style="background: linear-gradient(to bottom, #fff, #f6f7f7);">
<th>Name</th>
<th>Beitrag</th>
<th>E-Mail-Adresse</th>
<th>Telefon</th>
</tr>
</thead>
<tbody>
<template
v-for="participant in getFilteredParticipants(groupKey, participants)"
:key="participant.id"
>
<tr :class="getRowClass(participant)" :id="'participant-' + participant.identifier + '-common'">
<td :id="'participant-' + participant.identifier +'-name'"
style="width: 300px;"
:class="participant.efz_status === 'checked_invalid' ? 'efz-invalid' :
participant.efz_status === 'not_checked' ? 'efz-not-checked' : ''">
<div :id="'participant-' + participant.identifier +'-fullname'" v-html="participant.fullname" /><br />
Geburtsdatum: <label :id="'participant-' + participant.identifier +'-birthday'">{{ participant.birthday }}</label><br />
Alter: <label :id="'participant-' + participant.identifier +'-age'">{{ participant.age }}</label> Jahre<br />
<span>eFZ-Status: <label :id="'participant-' + participant.identifier +'-coc-status'">{{ participant.efzStatusReadable }}</label></span> &nbsp;
<span :id="'participant-' + participant.identifier +'-coc-action'" v-if="participant.efz_status !== 'checked_valid' && participant.efz_status !== 'not_required'" class="link" style="color: #3cb62e; font-size: 11pt;" @click="markCocExisting(participant)">Vorhanden?</span>
</td>
<td :id="'participant-' + participant.identifier +'-payment'" :class="participant.amount_left_value != 0 && !participant.unregistered ? 'not-paid' : ''" style="width: 275px; '">
Gezahlt: <label :id="'participant-' + participant.identifier + '-paid'">{{ participant?.amountPaid.readable }}</label> /<br />
Gesamt: <label :id="'participant-' + participant.identifier + '-expected'">{{ participant?.amountExpected.readable }}</label>
<br /><br />
<span v-if="participant.amount_left_value != 0 && !participant.unregistered" :id="'participant-' + participant.identifier + '-actions'">
<span class="link" style="font-size:10pt;" @click="paymentComplete(participant)">Zahlung buchen</span> &nbsp;
<span class="link" style="font-size:10pt;" @click="openPartialPaymentDialog(participant)">Teilzahlung buchen</span>
</span>
</td>
<td>
<label :id="'participant-' + participant.identifier +'-email_1'" class="block-label">{{ participant?.email_1 ?? "-" }}</label>
<label :id="'participant-' + participant.identifier +'-email_2'" class="block-label">{{ participant.email_2 }}</label>
</td>
<td>
<label :id="'participant-' + participant.identifier +'-phone_1'" class="block-label">{{ participant?.phone_1 }}</label>
<label :id="'participant-' + participant.identifier +'-phone_2'" class="block-label">{{ participant?.phone_2 }}</label>
</td>
</tr>
<tr class="participant-meta-row" :id="'participant-' + participant.identifier + '-meta'">
<td colspan="5" style="height: 15px !important; font-size: 9pt; background-color: #ffffff; border-top-style: none;">
<label :id="'participant-' + participant.identifier +'-localgroup'">
{{ participant?.localgroup ?? "-" }}
</label> |
<strong> Anreise: </strong>
<label :id="'participant-' + participant.identifier +'-arrival'">
{{ participant?.arrival ?? "-" }}
</label>|
<strong> Abreise: </strong>
<label :id="'participant-' + participant.identifier +'-departure'">
{{ participant?.departure ?? "-" }}
</label> |
<label v-if="!participant.unregistered">
<strong> Angemeldet am: </strong>{{ participant?.registerDate ?? "-" }}
</label>
<label v-else>
<strong> Abgemeldet am: </strong>{{ participant?.unregisteredAt ?? "-" }}
</label> |
<span class="link" @click="openParticipantDetails(participant)">Details</span> |
<span class="link">E-Mail senden</span> |
<span @click="openCancelParticipationDialog(participant)" v-if="!participant.unregistered" class="link" style="color: #da7070;">Abmelden</span>
<span v-else class="link" @click="execResignonParticipant(participant)" style="color: #3cb62e;">Wieder anmelden</span>
</td>
</tr>
</template>
<tr>
<td colspan="3" style="font-weight: normal;">
0 - 5 Jahre: <strong>{{ getAgeCounts(participants)['0-5'] ?? 0 }}</strong> |
6-11 Jahre: <strong>{{ getAgeCounts(participants)['6-11'] ?? 0 }}</strong> |
12-15 Jahre: <strong>{{ getAgeCounts(participants)['12-15'] ?? 0 }}</strong> |
16 - 17 Jahre: <strong>{{ getAgeCounts(participants)['16-17'] ?? 0 }}</strong> |
18 - 27 Jahre: <strong>{{ getAgeCounts(participants)['18-27'] ?? 0 }}</strong> |
27 Jahre und älter: <strong>{{ getAgeCounts(participants)['27+'] ?? 0 }}</strong>
</td>
<td>
<input type="button" class="button" @click="mailToGroup(groupKey)" value="E-Mail an Gruppe senden" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
<Modal
:show="showParticipantDetails"
title="Anmeldedetails ansehen"
@close="showParticipantDetails = false;"
>
<ParticipantData
@cancelParticipation="openCancelParticipationDialog"
:participant="showParticipant"
:editMode="editMode"
:event="event"
@editParticipant="editMode = true"
@saveParticipant="saveParticipant"
@paymentComplete="paymentComplete"
@markCocExisting="markCocExisting"
@closeParticipantDetails="showParticipantDetails = false" />
</Modal>
<Modal
:show="openCancelDialog"
title="Anmeldung stornieren"
width="350px"
@close="openCancelDialog = false"
>
Datum der Abmeldung:
<input type="date" style="margin-top: 10px;" id="cancel_date" :value="today" />
<br /><br />
<button class="button" @click="execCancelParticipation()">Abmeldung durchführen</button>
</Modal>
<Modal
:show="openPartialPaymentDialogSwitch"
title="Teilbetragszahlung"
width="350px"
@close="openPartialPaymentDialogSwitch = false"
>
Gesamtbetrag der Zahlung:
<AmountInput type="text" v-model="showParticipant.amountExpected.short" style="margin-top: 10px; width: 100px !important;" id="partial_payment_amount" /> Euro /
{{showParticipant.amountExpected.readable}} Euro
<br /><br />
<button class="button" @click="execPartialPayment()">Teilbetrag buchen</button>
</Modal>
<FullScreenModal
:show="mailCompose"
title="E-Mail senden"
@close="mailCompose = false"
>
<MailCompose @closeComposer="mailCompose = false" :event="event" :mailToType="mailToType" :recipientIdentifier="recipientIdentifier" />
</FullScreenModal>
</template>
<style scoped>
table {
margin-bottom: 60px !important;
}
tr {
vertical-align: top;
}
tr td {
height: 80px;
padding: 10px;
padding-top: 20px;
font-size: 11pt;
line-height: 1.5;
}
tr th {
height: 40px;
padding-left: 10px;
vertical-align: middle;
}
tr th:after {
content: "";
}
tr:nth-child(even) {
background-color: #f9fafb;
border-style: solid;
border-width: 0 1px;
border-color: #e5e7eb;
}
tr:nth-child(odd) {
background-color: #ffffff;
border-style: solid;
border-width: 5px 1px 0 1px;
border-color: #e5e7eb;
}
tr:first-child {
border-width: 1px 1px 0 1px;
}
tr:last-child {
border-width: 0 1px 1px 1px;
}
tr:last-child td {
background: linear-gradient(to bottom, #fff, #f6f7f7); font-weight: bold;
height: 30px;
}
.button {
display: block;
font-size: 10pt;
text-decoration: none;
}
.not-paid {
color: #ff0000; background-color: #ffe6e6;
}
.efz-invalid {
color: #ff0000; background-color: #ffe6e6;
}
.efz-not-checked {
color: #8D914BFF; background-color: #F4E99EFF;
}
.block-label {
display: block;
}
</style>

View File

@@ -0,0 +1,295 @@
<script setup>
import {reactive, watch} from "vue";
import AmountInput from "../../../../Views/Components/AmountInput.vue";
import ErrorText from "../../../../Views/Components/ErrorText.vue";
import {toast} from "vue3-toastify";
import {request} from "../../../../../resources/js/components/HttpClient.js";
const emit = defineEmits(['close'])
const props = defineProps({
event: Object,
})
const errors = reactive({})
const formData = reactive({
"pft_1_active": true,
"pft_1_amount_standard": props.event.participationFee_1.amount_standard_edit,
"pft_1_amount_reduced": props.event.participationFee_1.amount_reduced_edit,
"pft_1_amount_solidarity": props.event.participationFee_1.amount_solidarity_edit,
"pft_1_description": props.event.participationFee_1.description,
"pft_2_active": props.event.participationFee_2.active,
"pft_2_amount_standard": props.event.participationFee_2.amount_standard_edit,
"pft_2_amount_reduced": props.event.participationFee_2.amount_reduced_edit,
"pft_2_amount_solidarity": props.event.participationFee_2.amount_solidarity_edit,
"pft_2_description": props.event.participationFee_2.description,
"pft_3_active": props.event.participationFee_3.active,
"pft_3_amount_standard": props.event.participationFee_3.amount_standard_edit,
"pft_3_amount_reduced": props.event.participationFee_3.amount_reduced_edit,
"pft_3_amount_solidarity": props.event.participationFee_3.amount_solidarity_edit,
"pft_3_description": props.event.participationFee_3.description,
"pft_4_active": props.event.participationFee_4.active,
"pft_4_amount_standard": props.event.participationFee_4.amount_standard_edit,
"pft_4_amount_reduced": props.event.participationFee_4.amount_reduced_edit,
"pft_4_amount_solidarity": props.event.participationFee_4.amount_solidarity_edit,
"pft_4_description": props.event.participationFee_4.description,
'maxAmount': props.event.maxAmount,
'sibling_reduction': props.event.siblingReduction,
})
function validateInput() {
var noErrors = true;
if (formData.pft_1_description === '') {
errors.pft_1_description = 'Eine Beschreibung für diese Gruppe ist erforderlich';
noErrors = false;
}
if (formData.pft_2_description === '' && formData.pft_2_active) {
errors.pft_2_description = 'Eine Beschreibung für diese Gruppe ist erforderlich';
noErrors = false;
}
if (formData.pft_3_description === '' && formData.pft_3_active) {
errors.pft_3_description = 'Eine Beschreibung für diese Gruppe ist erforderlich';
noErrors = false;
}
if (formData.pft_4_description === '' && formData.pft_4_active) {
errors.pft_4_description = 'Eine Beschreibung für diese Gruppe ist erforderlich';
noErrors = false;
}
return noErrors;
}
async function saveParticipationFees() {
if (!validateInput()) {
toast.error('Bitte prüfe alle Eingaben auf Fehler')
return;
}
const data = await request('/api/v1/event/details/' + props.event.id + '/participation-fees', {
method: "POST",
body: {
event_id: props.event.id,
pft_1_active: formData.pft_1_active,
pft_1_amount_standard: formData.pft_1_amount_standard,
pft_1_amount_reduced: formData.pft_1_amount_reduced,
pft_1_amount_solidarity: formData.pft_1_amount_solidarity,
pft_1_description: formData.pft_1_description,
pft_2_active: formData.pft_2_active,
pft_2_amount_standard: formData.pft_2_amount_standard,
pft_2_amount_reduced: formData.pft_2_amount_reduced,
pft_2_amount_solidarity: formData.pft_2_amount_solidarity,
pft_2_description: formData.pft_2_description,
pft_3_active: formData.pft_3_active,
pft_3_amount_standard: formData.pft_3_amount_standard,
pft_3_amount_reduced: formData.pft_3_amount_reduced,
pft_3_amount_solidarity: formData.pft_3_amount_solidarity,
pft_3_description: formData.pft_3_description,
pft_4_active: formData.pft_4_active,
pft_4_amount_standard: formData.pft_4_amount_standard,
pft_4_amount_reduced: formData.pft_4_amount_reduced,
pft_4_amount_solidarity: formData.pft_4_amount_solidarity,
pft_4_description: formData.pft_4_description,
maxAmount: formData.maxAmount,
sibling_reduction: formData.sibling_reduction,
}
})
toast.success('Die Teilnahmekonditionen wurden gespeichert')
emit('close')
}
function recalculateMaxAmount(newValue) {
if (formData.maxAmount === 0) return;
var newAmount = parseFloat(newValue.replace(',', '.'));
if (props.event.payPerDay) {
newAmount = newAmount * props.event.duration;
}
var currentMaxAmount = formData.maxAmount.replace(',', '.');
if (newAmount > currentMaxAmount) {
formData.maxAmount = newAmount.toFixed(2).replace('.', ',');
}
}
</script>
<template>
<table style="width: 100%;">
<tr>
<td><h4>Aktiv</h4></td>
<td><h4>Preisgruppe</h4></td>
<td v-if="!props.event.solidarityPayment"><h4>Betrag</h4></td>
<td v-else><h4>Regulärer Beitrag</h4></td>
<td v-if="props.event.solidarityPayment"><h4>Reduzierter Beitrag</h4></td>
<td v-if="props.event.solidarityPayment"><h4>Solidaritätsbeitrag</h4></td>
<td><h4>Beschreibung</h4></td>
</tr>
<tr style="height: 65px; vertical-align: top">
<td>
<input type="checkbox" v-model="formData.participationFeeType_1" checked disabled/>
</td>
<td>
Teilnehmende
</td>
<td>
<AmountInput v-model="formData.pft_1_amount_standard" class="width-small" @blur="recalculateMaxAmount(formData.pft_1_amount_standard)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="props.event.solidarityPayment">
<AmountInput v-model="formData.pft_1_amount_reduced" class="width-small" @blur="recalculateMaxAmount(formData.pft_1_amount_reduced)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="props.event.solidarityPayment">
<AmountInput v-model="formData.pft_1_amount_solidarity" class="width-small" @blur="recalculateMaxAmount(formData.pft_1_amount_solidarity)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td>
<input type="text" v-model="formData.pft_1_description" style="width: 300px;" />
<ErrorText :message="errors.pft_1_description" />
</td>
</tr>
<tr style="height: 65px; vertical-align: top;">
<td>
<input id="use_pft_2" type="checkbox" v-model="formData.pft_2_active" :checked="formData.pft_2_active" />
</td>
<td>
<label for="use_pft_2" style="cursor: default">
Kernteam
</label>
</td>
<td v-if="formData.pft_2_active">
<AmountInput v-model="formData.pft_2_amount_standard" class="width-small" @blur="recalculateMaxAmount(formData.pft_2_amount_standard)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="props.event.solidarityPayment && formData.pft_2_active">
<AmountInput v-model="formData.pft_2_amount_reduced" class="width-small" @blur="recalculateMaxAmount(formData.pft_2_amount_reduced)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="props.event.solidarityPayment && formData.pft_2_active">
<AmountInput v-model="formData.pft_2_amount_solidarity" class="width-small" @blur="recalculateMaxAmount(formData.pft_2_amount_solidarity)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="formData.pft_2_active">
<input type="text" v-model="formData.pft_2_description" style="width: 300px;" />
<ErrorText :message="errors.pft_2_description" />
</td>
</tr>
<tr style="height: 65px; vertical-align: top;">
<td>
<input id="use_pft_3" type="checkbox" v-model="formData.pft_3_active" :checked="formData.pft_3_active" />
</td>
<td>
<label for="use_pft_3" style="cursor: default">
Unterstützende
</label>
</td>
<td v-if="formData.pft_3_active">
<AmountInput v-model="formData.pft_3_amount_standard" class="width-small" @blur="recalculateMaxAmount(formData.pft_3_amount_standard)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="props.event.solidarityPayment && formData.pft_3_active">
<AmountInput v-model="formData.pft_3_amount_reduced" class="width-small" @blur="recalculateMaxAmount(formData.pft_3_amount_reduced)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="props.event.solidarityPayment && formData.pft_3_active">
<AmountInput v-model="formData.pft_3_amount_solidarity" class="width-small" @blur="recalculateMaxAmount(formData.pft_3_amount_solidarity)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="formData.pft_3_active">
<input type="text" v-model="formData.pft_3_description" style="width: 300px;" />
<ErrorText :message="errors.pft_3_description" />
</td>
</tr>
<tr style="height: 65px; vertical-align: top;">
<td>
<input id="use_pft_4" type="checkbox" v-model="formData.pft_4_active" :checked="formData.pft_4_active" />
</td>
<td>
<label for="use_pft_4" style="cursor: default">
Sonstige
</label>
</td>
<td v-if="formData.pft_4_active">
<AmountInput v-model="formData.pft_4_amount_standard" class="width-small" @blur="recalculateMaxAmount(formData.pft_4_amount_standard)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="props.event.solidarityPayment && formData.pft_4_active">
<AmountInput v-model="formData.pft_4_amount_reduced" class="width-small" @blur="recalculateMaxAmount(formData.pft_4_amount_reduced)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="props.event.solidarityPayment && formData.pft_4_active">
<AmountInput v-model="formData.pft_4_amount_solidarity" class="width-small" @blur="recalculateMaxAmount(formData.pft_4_amount_solidarity)" />
<label style="font-size: 10pt;" v-if="props.event.payPerDay"> Euro / Tag</label>
<label style="font-size: 10pt;" v-else> Euro Gesamt</label>
</td>
<td v-if="formData.pft_4_active">
<input type="text" v-model="formData.pft_4_description" style="width: 300px;" />
<ErrorText :message="errors.pft_4_description" />
</td>
</tr>
<tr>
<td colspan="2">
Maximaler Beitrag für Veranstaltung:
</td>
<td colspan="2">
<AmountInput v-model="formData.maxAmount" class="width-small" /> Euro Gesamt
</td>
</tr>
<tr>
<td colspan="2">
<input type="checkbox" v-model="formData.sibling_reduction" id="sibling_reduction" :checked="formData.sibling_reduction" />
<label for="sibling_reduction">50% Preisnachlass für Geschwisterkinder gewähren</label>
</td>
</tr>
<tr>
<td colspan="4" style="padding-top: 20px;">
<input type="button" value="Speichern" @click="saveParticipationFees" />
</td>
</tr>
</table>
</template>

View File

@@ -0,0 +1,165 @@
<script setup>
const props = defineProps({
event: Object
})
</script>
<template>
<h2>Übersicht</h2>
<div class="participant-flexbox">
<div class="participant-flexbox-row top">
<div class="left">
<h3>Teilnehmende</h3>
<table class="participant-income-table" style="margin-bottom: 40px; font-size: 11pt;">
<tr>
<th>Teili</th>
<td><strong>{{props.event.participants.participant.count}} Personen:</strong></td>
<td>
{{props.event.participants.participant.amount.paid.readable}} /
</td>
<td>
{{props.event.participants.participant.amount.expected.readable}}
</td>
</tr>
<tr>
<th>Team</th>
<td><strong>{{props.event.participants.team.count}} Personen:</strong></td>
<td>
{{props.event.participants.team.amount.paid.readable}} /
</td>
<td>
{{props.event.participants.team.amount.expected.readable}}
</td>
</tr>
<tr>
<th>Unterstützende</th>
<td><strong>{{props.event.participants.volunteer.count}} Personen:</strong></td>
<td>
{{props.event.participants.volunteer.amount.paid.readable}} /
</td>
<td>
{{props.event.participants.volunteer.amount.expected.readable}}
</td>
</tr>
<tr>
<th>Sonstige</th>
<td><strong>{{props.event.participants.other.count}} Personen:</strong></td>
<td>
{{props.event.participants.other.amount.paid.readable}} /
</td>
<td>
{{props.event.participants.other.amount.expected.readable}}
</td>
</tr>
<tr>
<th colspan="2">Sonstige Einnahmen</th>
<td colspan="2">{{ props.event.flatSupport }}</td>
</tr>
<tr>
<th style="padding-bottom: 20px" colspan="2">Förderung</th>
<td style="padding-bottom: 20px" colspan="2">
{{ props.event.supportPerson.readable }}<br />
<label style="font-size: 9pt;">({{ props.event.supportPersonIndex }} / Tag p.P.)</label>
</td>
</tr>
<tr>
<th colspan="2" style="border-width: 1px; border-bottom-style: solid">Gesamt</th>
<td style="font-weight: bold; border-width: 1px; border-bottom-style: solid">
{{ props.event.income.real.readable }} /
</td>
<td style="font-weight: bold; border-width: 1px; border-bottom-style: solid">
{{ props.event.income.expected.readable }}
</td>
</tr>
<tr>
<th style="padding-top: 20px; font-size: 12pt !important;" colspan="2">Bilanz</th>
<td v-if="props.event.totalBalance.real.value >= 0" style="color: #4caf50;font-weight: bold; padding-top: 20px; font-size: 12pt !important;">
{{ props.event.totalBalance.real.readable }} /
</td>
<td v-else style="color: #f44336; font-weight: bold; padding-top: 20px; font-size: 12pt !important;">
{{props.event.totalBalance.real.readable}}
</td>
<td v-if="props.event.totalBalance.expected.value >= 0" style="color: #4caf50; font-weight: bold; padding-top: 20px; font-size: 12pt !important;">
{{ props.event.totalBalance.expected.readable }}
</td>
<td v-else style="color: #f44336; font-weight: bold; padding-top: 20px; font-size: 12pt !important;">
{{props.event.totalBalance.expected.readable}}
</td>
</tr>
</table>
</div>
<div class="right">
<h3>Ausgaben</h3>
<table class="event-payment-table" style="font-size: 10pt;">
<tr v-for="amount in props.event.costUnit.amounts">
<th>{{amount.name}}</th>
<td>{{amount.string}}</td>
</tr>
<tr>
<th style="color:#f44336; border-width: 1px; border-bottom-style: solid; padding-top: 58px">Gesamt</th>
<td style="color:#f44336; border-width: 1px; border-bottom-style: solid; padding-top: 58px; font-weight: bold">{{props.event.costUnit.overAllAmount.text}}</td>
</tr>
</table>
</div>
</div>
</div>
</template>
<style scoped>
.participant-flexbox {
display: flex;
flex-direction: column;
gap: 10px;
width: 95%;
margin: 20px auto 0;
}
.participant-flexbox-row {
display: flex;
gap: 10px; /* Abstand zwischen den Spalten */
}
.participant-flexbox-row.top .left {
flex: 0 0 50%;
padding: 10px;
}
.participant-flexbox.top .right {
flex: 0 0 50%;
padding: 10px;
}
.participant-income-table,
.event-payment-table {
width: 475px;
}
.participant-income-table th {
width: 20px;
font-size: 11pt !important;
}
.participant-income-table tr td:first-child {
width: 25px !important;
font-size: 11pt;
}
.event-payment-table {
width: 100%;
}
</style>

View File

@@ -0,0 +1,152 @@
<script setup>
import { useSignupForm } from './composables/useSignupForm.js'
import StepAge from './steps/StepAge.vue'
import StepContactPerson from './steps/StepContactPerson.vue'
import StepPersonalData from './steps/StepPersonalData.vue'
import StepRegistrationMode from './steps/StepRegistrationMode.vue'
import StepArrival from './steps/StepArrival.vue'
import StepAddons from './steps/StepAddons.vue'
import StepPhotoPermissions from './steps/StepPhotoPermissions.vue'
import StepAllergies from './steps/StepAllergies.vue'
import StepSummary from './steps/StepSummary.vue'
import SubmitSuccess from './after-submit/SubmitSuccess.vue'
import SubmitAlreadyExists from './after-submit/SubmitAlreadyExists.vue'
const props = defineProps({
event: Object,
participantData: Object,
localGroups: Array,
})
const emit = defineEmits(['registrationDone'])
const {
currentStep, goToStep, formData, selectedAddons,
submit, submitting, submitResult, summaryLoading, summaryAmount
} = useSignupForm(props.event, props.participantData)
const steps = [
{ step: 1, label: 'Alter' },
{ step: 2, label: 'Kontaktperson' },
{ step: 3, label: 'Persönliche Daten' },
{ step: 4, label: 'An-/Abreise' },
{ step: 5, label: 'Teilnahmegruppe' },
{ step: 6, label: 'Zusatzoptionen' },
{ step: 7, label: 'Fotoerlaubnis' },
{ step: 8, label: 'Allergien' },
{ step: 9, label: 'Zusammenfassung' },
]
</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 (ab Step 2) -->
<div v-if="currentStep > 1" style="margin-bottom: 28px;">
<div style="display: flex; gap: 6px; flex-wrap: wrap; align-items: center;">
<template v-for="(s, index) in steps.filter(s => s.step > 1)" :key="s.step">
<!-- Trennlinie zwischen Pills -->
<div v-if="index > 0" style="flex-shrink: 0; width: 16px; height: 2px; background: #e5e7eb; border-radius: 1px;"></div>
<div
:style="{
padding: '5px 14px',
borderRadius: '999px',
fontSize: '0.78rem',
fontWeight: '600',
whiteSpace: 'nowrap',
border: '2px solid',
borderColor: currentStep === s.step ? '#2563eb' : currentStep > s.step ? '#bbf7d0' : '#e5e7eb',
background: currentStep === s.step ? '#2563eb' : currentStep > s.step ? '#f0fdf4' : '#f9fafb',
color: currentStep === s.step ? 'white' : currentStep > s.step ? '#15803d' : '#9ca3af',
cursor: currentStep > s.step ? 'pointer' : 'default',
}"
@click="currentStep > s.step ? goToStep(s.step) : null"
>
<span v-if="currentStep > s.step" style="margin-right: 4px;"></span>
{{ s.label }}
</div>
</template>
</div>
<!-- Fortschrittsbalken -->
<div style="margin-top: 10px; height: 3px; background: #e5e7eb; border-radius: 2px; overflow: hidden;">
<div
:style="{
height: '100%',
background: 'linear-gradient(90deg, #2563eb, #3b82f6)',
borderRadius: '2px',
width: ((currentStep - 2) / (steps.length - 2) * 100) + '%',
transition: 'width 0.3s ease',
}"
></div>
</div>
</div>
<!-- Steps -->
<form @submit.prevent="submit">
<StepAge v-if="currentStep === 1" :event="event" @next="goToStep" />
<StepContactPerson v-if="currentStep === 2" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
<StepPersonalData v-if="currentStep === 3" :formData="formData" :localGroups="localGroups" @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" />
<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" />
<StepAllergies v-if="currentStep === 8" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
<StepSummary
v-if="currentStep === 9"
:formData="formData"
:event="event"
:summaryAmount="summaryAmount"
:summaryLoading="summaryLoading"
:submitting="submitting"
@back="goToStep"
@submit="submit"
/>
</form>
</template>
</div>
</template>
<style>
.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 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; }
.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;
}
</style>

Some files were not shown because too many files have changed in this diff Show More