Compare commits

..

61 Commits

Author SHA1 Message Date
7c1125e213 Small improvements 2026-04-30 20:39:56 +02:00
b91af2e889 Small improvements 2026-04-30 20:30:00 +02:00
4a97cf230b Bugfixes 2026-04-29 22:34:40 +02:00
0c7e3ab365 Bugfixes 2026-04-29 22:26:56 +02:00
222113b0d5 Bugfixes 2026-04-29 21:53:19 +02:00
895e44c984 Merge remote-tracking branch 'origin/main' 2026-04-29 21:51:02 +02:00
ef3d03200e Bugfixes 2026-04-29 21:50:05 +02:00
tux
6c891ff64a set prod user 2026-04-29 20:07:10 +02:00
d9ba5ab4f1 Merge remote-tracking branch 'origin/main' 2026-04-29 19:43:58 +02:00
1816ca5165 Bugfixes 2026-04-29 19:43:45 +02:00
tux
dff7d65476 production deployment stuff 2026-04-29 19:35:11 +02:00
ce57669263 Bugfixes 2026-04-29 18:23:28 +02:00
tux
2444aab204 prod docker stuff 2026-04-26 17:46:21 +02:00
7b2f2de1f0 New mareike version 2026-04-26 17:44:56 +02:00
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
ab711109a7 Pdf Viewer 2026-02-13 10:54:17 +01:00
72623df38f Pdf Viewer 2026-02-13 10:52:40 +01:00
9fd6839878 Small fixes 2026-02-13 10:41:20 +01:00
fd403f8520 Operation processes on invoices 2026-02-13 00:11:51 +01:00
882752472e Invoice Widgets completed 2026-02-11 19:38:06 +01:00
87531237c7 Show own invoices 2026-02-11 15:44:43 +01:00
ee7fc637f1 Invoices can be uploaded 2026-02-11 15:40:06 +01:00
bccfc11687 Cost units can be edited 2026-02-08 20:06:38 +01:00
6fc65e195c API-Route to new global variables 2026-02-05 09:18:24 +01:00
e9ae850002 Display nicename in dashboard 2026-02-05 07:40:00 +01:00
11108bdfcc Basic user management 2026-02-05 00:46:22 +01:00
e280fcfba8 Basic design created 2026-02-03 09:33:18 +01:00
3570f442f5 Basic tenant structure 2026-01-31 20:07:41 +01:00
616 changed files with 202133 additions and 894 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

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
.ai
.junie
.git
storage
tests

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@
Homestead.json
Homestead.yaml
Thumbs.db
/docker-compose.yaml

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.

View File

@@ -2,6 +2,11 @@
FRONTEND_DIR ?= .
setup:
rm -f docker-compose.yaml
cp docker-compose.dev docker-compose.yaml
docker-compose up -d
frontend:
@cd $(FRONTEND_DIR) && \
export QT_QPA_PLATFORM=offscreen && \

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

@@ -0,0 +1,22 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitDetails;
class ChangeCostUnitDetailsCommand {
private ChangeCostUnitDetailsRequest $request;
public function __construct(ChangeCostUnitDetailsRequest $request) {
$this->request = $request;
}
public function execute(): ChangeCostUnitDetailsResponse {
$response = new ChangeCostUnitDetailsResponse();
$this->request->costUnit->distance_allowance = $this->request->distanceAllowance->getAmount();
$this->request->costUnit->mail_on_new = $this->request->mailOnNew;
$this->request->costUnit->billing_deadline = $this->request->billingDeadline;
$response->success = $this->request->costUnit->save();
return $response;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitDetails;
use App\Models\CostUnit;
use App\ValueObjects\Amount;
use DateTime;
class ChangeCostUnitDetailsRequest {
public CostUnit $costUnit;
public Amount $distanceAllowance;
public bool $mailOnNew;
public ?DateTime $billingDeadline;
public function __construct(CostUnit $costUnit, Amount $distanceAllowance, bool $mailOnNew, ?DateTime $billingDeadline = null) {
$this->costUnit = $costUnit;
$this->distanceAllowance = $distanceAllowance;
$this->mailOnNew = $mailOnNew;
$this->billingDeadline = $billingDeadline;
}
}

View File

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

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitState;
class ChangeCostUnitStateCommand {
private ChangeCostUnitStateRequest $request;
public function __construct(ChangeCostUnitStateRequest $request) {
$this->request = $request;
}
public function execute() : ChangeCostUnitStateResponse {
$response = new ChangeCostUnitStateResponse();
$this->request->costUnit->allow_new = $this->request->allowNew;
$this->request->costUnit->archived = $this->request->isArchived;
$response->success = $this->request->costUnit->save();
return $response;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitState;
use App\Models\CostUnit;
class ChangeCostUnitStateRequest {
public bool $allowNew;
public bool $isArchived;
public CostUnit $costUnit;
public function __construct(CostUnit $costUnit, bool $allowNew, bool $isArchived) {
$this->costUnit = $costUnit;
$this->allowNew = $allowNew;
$this->isArchived = $isArchived;
}
}

View File

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

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
use App\Models\User;
class ChangeCostUnitTreasurersCommand {
private ChangeCostUnitTreasurersRequest $request;
public function __construct(ChangeCostUnitTreasurersRequest $request) {
$this->request = $request;
}
public function execute() : ChangeCostUnitTreasurersResponse {
$response = new ChangeCostUnitTreasurersResponse();
try {
$this->request->costUnit->resetTreasurers();
foreach ($this->request->treasurers as $treasurer) {
$this->request->costUnit->treasurers()->attach($treasurer);
}
$this->request->costUnit->save();
} catch (\Throwable $th) {
$response->success = false;
}
return $response;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
use App\Models\CostUnit;
class ChangeCostUnitTreasurersRequest {
public array $treasurers;
public CostUnit $costUnit;
public function __construct(CostUnit $costUnit, array $treasurers) {
$this->treasurers = $treasurers;
$this->costUnit = $costUnit;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
class ChangeCostUnitTreasurersResponse {
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Domains\CostUnit\Actions\CreateCostUnit;
use App\Models\CostUnit;
class CreateCostUnitCommand {
private CreateCostUnitRequest $request;
public function __construct(CreateCostUnitRequest $request) {
$this->request = $request;
}
public function execute() : CreateCostUnitResponse {
$response = new CreateCostUnitResponse();
$costUnit = CostUnit::create([
'name' => $this->request->name,
'tenant' => app('tenant')->slug,
'type' => $this->request->type,
'billing_deadline' => $this->request->billingDeadline,
'distance_allowance' => $this->request->distanceAllowance->getAmount(),
'mail_on_new' => $this->request->mailOnNew,
'allow_new' => true,
'archived' => false,
]);
if (null !== $costUnit) {
$response->costUnit = $costUnit;
$response->success = true;
}
return $response;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Domains\CostUnit\Actions\CreateCostUnit;
use App\Enumerations\CostUnitType;
use App\ValueObjects\Amount;
use DateTime;
class CreateCostUnitRequest {
public string $name;
public string $type;
public Amount $distanceAllowance;
public bool $mailOnNew;
public ?DateTime $billingDeadline;
public function __construct(string $name, string $type, Amount $distanceAllowance, bool $mailOnNew, ?DateTime $billingDeadline = null) {
$this->name = $name;
$this->type = $type;
$this->distanceAllowance = $distanceAllowance;
$this->mailOnNew = $mailOnNew;
$this->billingDeadline = $billingDeadline;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Domains\CostUnit\Actions\CreateCostUnit;
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,47 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Domains\CostUnit\Actions\ChangeCostUnitState\ChangeCostUnitStateCommand;
use App\Domains\CostUnit\Actions\ChangeCostUnitState\ChangeCostUnitStateRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
class ChangeStateController extends CommonController {
public function close(int $costUnitId) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
$changeStatRequest = new ChangeCostUnitStateRequest($costUnit, false, false);
return $this->changeCostUnitState($changeStatRequest, 'Der CostUnit wurde geschlossen.');
}
public function open(int $costUnitId) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
$changeStatRequest = new ChangeCostUnitStateRequest($costUnit, true, false);
return $this->changeCostUnitState($changeStatRequest, 'Der CostUnit wurde geöffnet.');
}
public function archive(int $costUnitId) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
$changeStatRequest = new ChangeCostUnitStateRequest($costUnit, false, true);
return $this->changeCostUnitState($changeStatRequest, 'Der CostUnit wurde archiviert.');
}
private function changeCostUnitState(ChangeCostUnitStateRequest $request, string $responseMessage) : JsonResponse {
$changeStatCommand = new ChangeCostUnitStateCommand($request);
if ($changeStatCommand->execute()) {
return response()->json([
'status' => 'success',
'message' => $responseMessage
]);
};
return response()->json([
'status' => 'error',
'message' => 'Ein Fehler ist aufgetreten.'
]);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitCommand;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitRequest;
use App\Enumerations\CostUnitType;
use App\Models\CostUnit;
use App\Providers\FlashMessageProvider;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CreateController extends CommonController{
public function showForm() {
$inertiaProvider = new InertiaProvider('CostUnit/Create', []);
return $inertiaProvider->render();
}
public function createCostUnitRunningJob(Request $request) : JsonResponse {
$createCostUnitRequest = new CreateCostUnitRequest(
$request->get('cost_unit_name'),
CostUnitType::COST_UNIT_TYPE_RUNNING_JOB,
Amount::fromString($request->get('distance_allowance')),
$request->get('mailOnNew')
);
$createCostUnitCommand = new CreateCostUnitCommand($createCostUnitRequest);
$result = $createCostUnitCommand->execute();
new FlashMessageProvider('Die laufende Tätigkeit wurde erfolgreich angelegt.', 'success');
return response()->json([]);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
class DistanceAllowanceController extends CommonController {
public function __invoke(int $costUnitId) : JsonResponse {
$distanceAllowance = 0.00;
$costUnit = $this->costUnits->getById($costUnitId, true);
if (null !== $costUnit) {
$distanceAllowance = $costUnit->distance_allowance;
}
return response()->json([
'distanceAllowance' => $distanceAllowance
]);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Domains\CostUnit\Actions\ChangeCostUnitDetails\ChangeCostUnitDetailsCommand;
use App\Domains\CostUnit\Actions\ChangeCostUnitDetails\ChangeCostUnitDetailsRequest;
use App\Resources\CostUnitResource;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EditController extends CommonController{
function __invoke(int $costUnitId) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
if (null === $costUnit) {
return response()->json([
'status' => 'error',
'message' => 'Die Kotenstelle konnte nicht geladen werden.'
]);
}
return response()->json([
'status' => 'success',
'costUnit' => new CostUnitResource($costUnit)->toArray(request())
]);
}
public function update(Request $request, int $costUnitId) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
if (null === $costUnit) {
return response()->json([
'status' => 'error',
'message' => 'Die Kotenstelle konnte nicht geladen werden.'
]);
}
$saveParams = $request->get('formData');
$distanceAllowance = Amount::fromString($saveParams['distanceAllowance']);
$billingDeadline = isset($saveParams['billingDeadline']) ? \DateTime::createFromFormat('Y-m-d', $saveParams['billingDeadline']) : null;
$request = new ChangeCostUnitDetailsRequest($costUnit, $distanceAllowance, $saveParams['mailOnNew'], $billingDeadline);
$command = new ChangeCostUnitDetailsCommand($request);
$result = $command->execute();
if (!$result->success) {
return response()->json([
'status' => 'error',
'message' => 'Bei der Verarbeitung ist ein Fehler aufgetreten.'
]);
}
return response()->json([
'status' => 'success',
'message' => 'Die Kostenstellendetails wurden erfolgreich gespeichert.',
]);
}
}

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

@@ -0,0 +1,48 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ListController extends CommonController {
public function __invoke() {
$inertiaProvider = new InertiaProvider('CostUnit/List', [
'cost_unit_id' => 1
]);
return $inertiaProvider->render();
}
public function listCurrentEvents(Request $request) : JsonResponse {
return response()->json([
'cost_unit_title' => 'Aktuelle Veranstaltungen',
'cost_units' => $this->costUnits->getCurrentEvents(),
]);
}
public function listCurrentRunningJobs(Request $request) : JsonResponse {
return response()->json([
'cost_unit_title' => 'Laufende Tätigkeiten',
'cost_units' => $this->costUnits->getRunningJobs(),
]);
}
public function listClosedCostUnits(Request $request) : JsonResponse {
return response()->json([
'cost_unit_title' => 'Geschlossene Kostenstellen',
'cost_units' => $this->costUnits->getClosedCostUnits(),
]);
}
public function listArchivedCostUnits(Request $request) : JsonResponse {
return response()->json([
'cost_unit_title' => 'Archivierte Kostenstellen',
'cost_units' => $this->costUnits->getArchivedCostUnits(),
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
class OpenController extends CommonController {
public function __invoke(int $costUnitId) {
$costUnit = $this->costUnits->getById($costUnitId);
$inertiaProvider = new InertiaProvider('CostUnit/Open', [
'costUnit' => $costUnit
]);
return $inertiaProvider->render();
}
public function listInvoices(int $costUnitId, string $invoiceStatus) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
$invoices = $this->invoices->getByStatus($costUnit, $invoiceStatus);
return response()->json([
'status' => 'success',
'costUnit' => $costUnit,
'invoices' => $invoices,
'endpoint' => $invoiceStatus,
]);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers\ChangeCostUnitTreasurersCommand;
use App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers\ChangeCostUnitTreasurersRequest;
use App\Providers\InertiaProvider;
use App\Resources\CostUnitResource;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TreasurersEditController extends CommonController {
public function __invoke(int $costUnitId) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
if (null === $costUnit) {
return response()->json([
'status' => 'error',
'message' => 'Die Kostenstelle konnte nicht geladen werden.'
]);
}
return response()->json([
'status' => 'success',
'costUnit' => new CostUnitResource($costUnit)->toArray(request())
]);
}
public function update(Request $request, int $costUnitId) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
if (null === $costUnit) {
return response()->json([
'status' => 'error',
'message' => 'Die Kostenstelle konnte nicht geladen werden.'
]);
}
$changeTreasurersRequest = new ChangeCostUnitTreasurersRequest($costUnit, $request->get('selectedTreasurers'));
$changeTreasurersCommand = new ChangeCostUnitTreasurersCommand($changeTreasurersRequest);
if ($changeTreasurersCommand->execute()) {
return response()->json([
'status' => 'success',
'message' => 'Die Schatzis wurden erfolgreich gespeichert.'
]);
}
return response()->json([
'status' => 'error',
'message' => 'Beim Bearbeiten der Kostenstelle ist ein Fehler aufgetreten.'
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
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;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::prefix('api/v1')
->group(function () {
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('cost-unit')->group(function () {
Route::get('/get-distance-allowance/{costUnitId}', DistanceAllowanceController::class);
Route::middleware(['auth'])->group(function () {
Route::post('/create-running-job', [CreateController::class, 'createCostUnitRunningJob']);
Route::prefix('/{costUnitId}') ->group(function () {
Route::get('/invoice-list/{invoiceStatus}', [OpenController::class, 'listInvoices']);
Route::post('/close', [ChangeStateController::class, 'close']);
Route::post('/open', [ChangeStateController::class, 'open']);
Route::post('/archive', [ChangeStateController::class, 'archive']);
Route::get('/details', EditController::class);
Route::post('/details', [EditController::class, 'update']);
Route::get('/treasurers', TreasurersEditController::class);
Route::post('/treasurers', [TreasurersEditController::class, 'update']);
Route::get('/export-payouts', ExportController::class);
});
Route::prefix('open')->group(function () {
Route::get('/current-events', [ListController::class, 'listCurrentEvents']);
Route::get('/current-running-jobs', [ListController::class, 'listCurrentRunningJobs']);
Route::get('/closed-cost-units', [ListController::class, 'listClosedCostUnits']);
Route::get('/archived-cost-units', [ListController::class, 'listArchivedCostUnits']);
});
});
});
});
});

View File

@@ -0,0 +1,39 @@
<?php
use App\Domains\CostUnit\Controllers\CreateController;
use App\Domains\CostUnit\Controllers\ListController;
use App\Domains\CostUnit\Controllers\OpenController;
use App\Domains\UserManagement\Controllers\EmailVerificationController;
use App\Domains\UserManagement\Controllers\LoginController;
use App\Domains\UserManagement\Controllers\LogOutController;
use App\Domains\UserManagement\Controllers\RegistrationController;
use App\Domains\UserManagement\Controllers\ResetPasswordController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('cost-unit')->group(function () {
Route::middleware(['auth'])->group(function () {
Route::get('/create', [CreateController::class, 'showForm']);
Route::get('/list', ListController::class);
Route::get('/{costUnitId}/', OpenController::class);
});
});
Route::get('/register', [RegistrationController::class, 'loginForm']);
Route::get('/register/verifyEmail', [EmailVerificationController::class, 'verifyEmailForm']);
Route::get('/reset-password', [ResetPasswordController::class, 'resetPasswordForm']);
route::get('/logout', LogOutController::class);
route::post('/login', [LoginController::class, 'doLogin']);
route::get('/login', [LoginController::class, 'loginForm']);
});

View File

@@ -0,0 +1,78 @@
<script setup>
import { reactive, inject } from 'vue';
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
import AmountInput from "../../../Views/Components/AmountInput.vue";
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import {toast} from "vue3-toastify";
const props = defineProps({
activeUsers: Object,
}
)
const { request } = useAjax();
const formData = reactive({
cost_unit_name: '',
distance_allowance: '0,25',
emailAddress: '',
mailOnNew: true
});
async function save() {
const data = await request("/api/v1/cost-unit/create-running-job", {
method: "POST",
body: {
cost_unit_name: formData.cost_unit_name,
distance_allowance: formData.distance_allowance,
mailOnNew: formData.mailOnNew
}
});
window.location.href = '/cost-unit/list';
}
</script>
<template>
<AppLayout title="Laufende Tätigkeit hinzufügen">
<form method="POST" action="/api/v1/cost-unit/create-running-job" @submit.prevent="save">
<input type="hidden" name="_token" :value="csrfToken" />
<shadowed-box style="width: 90%; margin: 20px auto; padding: 20px;">
<p>
Über dieses Formular können laufende Tätigkeiten angelegt werden.<br />
Eine Kostenstelle für eine Veranstaltung wird automatisch erstellt, sobald die Veranstaltung angelegt wurde.
</p>
<table style="margin-top: 40px; width: 100%">
<tr>
<th class="width-medium pr-20">Name der laufenden Tätigkeit</th>
<td><input type="text" v-model="formData.cost_unit_name" class="width-half-full" /></td>
</tr>
<tr>
<th class="pr-20">Kilometerpauschale</th>
<td>
<AmountInput v-model="formData.distance_allowance" class="width-small" /> Euro / Kilometer
</td>
</tr>
<tr>
<td colspan="2">
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" v-model="formData.mailOnNew" style="margin-right:8px;cursor:pointer;" />
<span>E-Mail-Benachrichtigung bei neuen Abrechnungen</span>
</label>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Speichern" />
</td>
</tr>
</table>
</shadowed-box>
</form>
</AppLayout>
</template>

View File

@@ -0,0 +1,83 @@
<script setup>
import { reactive, inject } from 'vue';
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
import AmountInput from "../../../Views/Components/AmountInput.vue";
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
const props = defineProps({
activeUsers: Object,
}
)
const { request } = useAjax();
const formData = reactive({
cost_unit_name: '',
distance_allowance: '0,25',
emailAddress: '',
mailOnNew: true
});
async function save() {
const data = await request("/wp-json/mareike/costunits/create-new-cost-unit", {
method: "POST",
body: {
mareike_nonce: _mareike_nonce(),
cost_unit_name: formData.cost_unit_name,
distance_allowance: formData.distance_allowance,
email_address: formData.emailAddress,
mailOnNew: formData.mailOnNew
}
});
toast('Die laufende Tätigkeit wurde erfolgreich angelegt.', { type: 'success' });
window.location.href = '/cost-units';
}
</script>
<template>
<AppLayout title="Laufende Tätigkeit hinzufügen">
<shadowed-box style="width: 90%; margin: 20px auto; padding: 20px;">
<p>
Über dieses Formular können laufende Tätigkeiten angelegt werden.<br />
Eine Kostenstelle für eine Veranstaltung wird automatisch erstellt, sobald die Veranstaltung angelegt wurde.
</p>
<table style="margin-top: 40px; width: 100%">
<tr>
<th class="width-medium pr-20">Name der laufenden Tätigkeit</th>
<td><input type="text" v-model="formData.cost_unit_name" class="width-half-full" /></td>
</tr>
<tr>
<th class="pr-20">Kilometerpauschale</th>
<td>
<AmountInput v-model="formData.distance_allowance" class="width-small" /> Euro / Kilometer
</td>
</tr>
<tr>
<td colspan="2">
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" v-model="formData.mailOnNew" style="margin-right:8px;cursor:pointer;" />
<span>E-Mail-Benachrichtigung bei neuen Abrechnungen</span>
</label>
<hr />
</td>
</tr>
<tr>
<td colspan="2">
<span v-for="user in props.activeUsers">
<input type="checkbox" :id="'user_' + user.id" />
<label :for="'user_' + user.id">{{user.fullname}} ({{user.localGroup}})</label>
</span>
</td>
</tr>
<tr>
<td colspan="2">
<input type="button" @click="save" value="Speichern" class="mareike-button button-small button-block" />
</td>
</tr>
</table>
</shadowed-box>
</AppLayout>
</template>

View File

@@ -0,0 +1,82 @@
<script setup>
import {reactive, inject, onMounted} from 'vue';
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import {toast} from "vue3-toastify";
import ListCostUnits from "./Partials/ListCostUnits.vue";
const props = defineProps({
message: String,
data: {
type: [Array, Object],
default: () => []
},
cost_unit_id: {
type: Number,
default: 0
},
invoice_id: {
type: Number,
default: 0
}
})
// Prüfen, ob ein ?id= Parameter in der URL übergeben wurde
const urlParams = new URLSearchParams(window.location.search)
const initialCostUnitId = props.cost_unit_id
const initialInvoiceId = props.invoice_id
const tabs = [
{
title: 'Aktuelle Veranstaltungen',
component: ListCostUnits,
endpoint: "/api/v1/cost-unit/open/current-events",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Laufende Tätigkeiten',
component: ListCostUnits,
endpoint: "/api/v1/cost-unit/open/current-running-jobs",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Geschlossene Kostenstellen',
component: ListCostUnits,
endpoint: "/api/v1/cost-unit/open/closed-cost-units",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Archivierte Kostenstellen',
component: ListCostUnits,
endpoint: "/api/v1/cost-unit/open/archived-cost-units",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
]
onMounted(() => {
if (undefined !== props.message) {
toast.success(props.message)
}
})
</script>
<template>
<AppLayout title="Kostenstellen">
<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" />
</shadowed-box>
</AppLayout>
</template>

View File

@@ -0,0 +1,67 @@
<script setup>
import {reactive, inject, onMounted} from 'vue';
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import {toast} from "vue3-toastify";
import ListInvoices from "./Partials/ListInvoices.vue";
const props = defineProps({
costUnit: Object
})
const urlParams = new URLSearchParams(window.location.search)
const initialCostUnitId = props.costUnit.id
const initialInvoiceId = props.invoice_id
const tabs = [
{
title: 'Neue Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/new",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Nichtexportierte Abrechnungen',
component: ListInvoices,
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.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.costUnit.id + "/invoice-list/denied",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
]
onMounted(() => {
if (undefined !== props.message) {
toast.success(props.message)
}
})
</script>
<template>
<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" />
</shadowed-box>
</AppLayout>
</template>

View File

@@ -0,0 +1,90 @@
<script setup>
import {reactive, ref} from "vue";
import Modal from "../../../../Views/Components/Modal.vue";
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import AmountInput from "../../../../Views/Components/AmountInput.vue";
import {toast} from "vue3-toastify";
const props = defineProps({
data: {
type: Object,
default: () => ({})
}, showCostUnit: Boolean
})
const mail_on_new = ref(Boolean(Number(props.data.mail_on_new)))
const emit = defineEmits(['close'])
const { request } = useAjax()
function close() {
emit('close')
}
const formData = reactive({
billingDeadline: props.data.billingDeadline,
mailOnNew: mail_on_new.value,
distanceAllowance: props.data.distanceAllowanceSmall,
});
async function updateCostUnit() {
const data = await request('/api/v1/cost-unit/' + props.data.id + '/details', {
method: "POST",
body: {
formData
}
});
close();
if (data.status === 'success') {
toast.success(data.message);
} else {
toast.error(data.message);
}
}
</script>
<template>
<Modal
:show="showCostUnit"
title="Details anpassen"
@close="emit('close')"
>
Kilometerpauschale:
<amount-input v-model="formData.distanceAllowance" class="width-small" /> Euro / km
<br /><br />
<span v-if="props.data.type !== 'running_job'">
Abrechnungsschluss am:
<input type="date" style="margin-top: 10px;" id="autoclose_date" v-model="formData.billingDeadline" />
<br /><br />
</span>
<div style="margin-top: 10px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input
type="checkbox"
id="mail_on_new"
v-model="formData.mailOnNew"
style="margin-right: 8px; cursor: pointer;"
/>
<span>E-Mail-Benachrichtigung bei neuen Abrechnungen</span>
</label>
</div>
<br />
<input type="button" value="Speichern" @click="updateCostUnit" />
</Modal>
</template>
<style>
.mareike-save-button {
background-color: #2271b1 !important;
color: #ffffff !important;
}
</style>

View File

@@ -0,0 +1,230 @@
<script setup>
import {createApp, ref} from '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";
const props = defineProps({
data: {
type: [Array, Object],
default: () => []
},
deep_jump_id: {
type: Number,
default: 0
},
deep_jump_id_sub: {
type: Number,
default: 0
}
})
const showInvoiceList = ref(false)
const invoices = ref(null)
const current_cost_unit = ref(null)
const showLoading = ref(false)
const show_invoice = ref(false)
const invoice = ref(null)
const show_cost_unit = ref(false)
const showTreasurers = ref(false)
const costUnit = ref(null)
const { data, loading, error, request, download } = useAjax()
async function costUnitDetails(costUnitId) {
const data = await request('/api/v1/cost-unit/' + costUnitId + '/details', {
method: "GET",
});
showLoading.value = false;
if (data.status === 'success') {
costUnit.value = data.costUnit
show_cost_unit.value = true
} else {
toast.error(data.message);
}
}
async function editTreasurers(costUnitId) {
const data = await request('/api/v1/cost-unit/' + costUnitId + '/treasurers', {
method: "GET",
});
showLoading.value = false;
if (data.status === 'success') {
costUnit.value = data.costUnit
showTreasurers.value = true
} else {
toast.error(data.message);
}
}
function loadInvoices(cost_unit_id) {
window.location.href = '/cost-unit/' + cost_unit_id;
}
async function denyNewRequests(costUnitId) {
changeCostUnitState(costUnitId, 'close');
}
async function archiveCostUnit(costUnitId) {
changeCostUnitState(costUnitId, 'archive');
}
async function allowNewRequests(costUnitId) {
changeCostUnitState(costUnitId, 'open');
}
async function changeCostUnitState(costUnitId, endPoint) {
showLoading.value = true;
const data = await request('/api/v1/cost-unit/' + costUnitId + '/' + endPoint, {
method: "POST",
});
showLoading.value = false;
if (data.status === 'success') {
toast.success(data.message);
document.getElementById('costUnitBox_' + costUnitId).style.display = 'none';
} else {
toast.error(data.message);
}
}
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 (data.tenant.download_exports) {
const response = await fetch(exportUrl, {
headers: { "Content-Type": "application/json" },
});
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 = "Abrechnungen-Sippenstunden.zip";
document.body.appendChild(a);
a.click();
setTimeout(() => {
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
}, 100);
} else {
const response = await request(exportUrl, {
method: "GET",
});
toast.success(response.message);
}
showLoading.value = false;
} catch (err) {
showLoading.value = false;
toast.error('Beim Export der Abrechnungen ist ein Fehler aufgetreten.');
}
}
</script>
<template>
<div v-if="props.data.cost_units && props.data.cost_units.length > 0 && !showInvoiceList">
<h2>{{ props.data.cost_unit_title }}</h2>
<span v-for="costUnit in props.data.cost_units" class="costunit-list" :id="'costUnitBox_' + costUnit.id">
<table style="width: 100%">
<thead>
<tr><td colspan="5">
{{ costUnit.name }}
</td></tr>
</thead>
<tr>
<th>Gesamtbeitrag</th>
<td>{{ costUnit.totalAmount }}</td>
<th>Unbearbeitet</th>
<td>{{ costUnit.countNewInvoices }}</td>
<td rowspan="4" style="vertical-align: top;">
<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" @click="exportPayouts(costUnit.id)" value="Genehmigte Abrechnungen exportieren" style="margin-top: 10px;" />
</td>
</tr>
<tr>
<th>Spenden</th>
<td>{{ costUnit.donatedAmount }}</td>
<th>Nicht exportiert</th>
<td>{{ costUnit.countApprovedInvoices }}</td>
</tr>
<tr>
<td colspan="2"></td>
<th>Ohne Auszahlung</th>
<td colspan="2">{{ costUnit.countDonatedInvoices }}</td>
</tr>
<tr>
<td colspan="2"></td>
<th>Abgelehnt</th>
<td colspan="2">{{ costUnit.countDeniedInvoices }}</td>
</tr>
<tr>
<td colspan="5" style="width: 100%; padding-top: 20px;">
<strong @click="costUnitDetails(costUnit.id)" v-if="costUnit.allow_new" class="link">Details anpassen</strong> &nbsp;
<strong @click="editTreasurers(costUnit.id)" v-if="!costUnit.archived" class="link">Schatzis zuweisen</strong> &nbsp;
<strong @click="denyNewRequests(costUnit.id)" v-if="costUnit.allow_new" class="link" style="color: #ff0000">Neue Abrechnungen verbieten</strong> &nbsp;
<strong @click="allowNewRequests(costUnit.id)" v-if="!costUnit.allow_new && !costUnit.archived" class="link" style="color: #529a30">Neue Abrechnungen erlauben</strong> &nbsp;
<strong @click="archiveCostUnit(costUnit.id)" v-if="!costUnit.allow_new && !costUnit.archived" class="link" style="color: #ff0000">Veranstaltung archivieren</strong> &nbsp;
</td>
</tr>
</table>
</span>
<CostUnitDetails :data="costUnit" :showCostUnit="show_cost_unit" v-if="show_cost_unit" @close="show_cost_unit = false" />
<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" />
</div>
<div v-else>
<strong style="width: 100%; text-align: center; display: block; margin-top: 20px;">
Es gibt keine Kostenstellen in dieser Kategorie, für die du verantwortlich bist.
</strong>
</div>
<LoadingModal :show="showLoading" />
</template>
<style scoped>
.costunit-list {
width: 96% !important;
}
</style>

View File

@@ -0,0 +1,80 @@
<script setup>
import Icon from "../../../../Views/Components/Icon.vue";
import InvoiceDetails from "../../../Invoice/Views/Partials/invoiceDetails/InvoiceDetails.vue";
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import {ref} from "vue";
const props = defineProps({
data: Object
})
const { request } = useAjax()
const invoice = ref(null)
const show_invoice = ref(false)
const localData = ref(props.data)
async function openInvoiceDetails(invoiceId) {
const url = '/api/v1/invoice/details/' + invoiceId
try {
const response = await fetch(url, { method: 'GET' })
const result = await response.json()
invoice.value = result.invoice
show_invoice.value = true
} catch (err) {
console.error('Error fetching invoices:', err)
}
}
async function reload() {
const url = "/api/v1/cost-unit/" + props.data.costUnit.id + "/invoice-list/" + props.data.endpoint
try {
const response = await fetch(url, { method: 'GET' })
if (!response.ok) throw new Error('Fehler beim Laden')
const result = await response.json()
localData.value = result
} catch (err) {
console.error('Error fetching invoices:', err)
}
}
</script>
<template>
<table v-if="localData.invoices.length > 0" class="invoice-list-table">
<tr>
<td colspan="6">{{props.data.costUnit.name}}</td>
</tr>
<tr v-for="invoice in localData.invoices" :id="'invoice_' + invoice.id">
<td>{{invoice.invoiceNumber}}</td>
<td>{{invoice.invoiceType}}</td>
<td>
{{invoice.amount}}
</td>
<td style="width: 150px;">
<Icon v-if="invoice.donation" name="hand-holding-dollar" style="color: #ffffff; background-color: green" />
<Icon v-if="invoice.alreadyPaid" name="comments-dollar" style="color: #ffffff; background-color: green" />
</td>
<td>
{{invoice.contactName}}<br />
<label v-if="invoice.contactEmail !== '--'">{{invoice.contactEmail}}<br /></label>
<label v-if="invoice.contactPhone !== '--'">{{invoice.contactPhone}}<br /></label>
</td>
<td>
<input type="button" value="Abrechnung Anzeigen" @click="openInvoiceDetails(invoice.id)" />
</td>
</tr>
</table>
<p v-else>Es sind keine Abrechnungen in dieser Kategorie vorhanden.</p>
<InvoiceDetails :data="invoice" :show-invoice="show_invoice" v-if="show_invoice" @close="show_invoice = false; reload()" />
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,96 @@
<script setup>
import {onMounted, reactive, ref} from "vue";
import Modal from "../../../../Views/Components/Modal.vue";
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import AmountInput from "../../../../Views/Components/AmountInput.vue";
import {toast} from "vue3-toastify";
const selectedTreasurers = ref([])
const props = defineProps({
data: {
type: Object,
default: () => ({})
}, showTreasurers: Boolean
})
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);
selectedTreasurers.value = props.data.treasurers?.map(t => t.id) ?? []
});
const mail_on_new = ref(Boolean(Number(props.data.mail_on_new)))
const emit = defineEmits(['closeTreasurers'])
const { request } = useAjax()
function closeTreasurers() {
emit('closeTreasurers')
}
const formData = reactive({
billingDeadline: props.data.billingDeadline,
mailOnNew: mail_on_new.value,
distanceAllowance: props.data.distanceAllowanceSmall,
});
async function updateCostUnit() {
const data = await request('/api/v1/cost-unit/' + props.data.id + '/treasurers', {
method: "POST",
body: {
selectedTreasurers: selectedTreasurers.value,
}
});
closeTreasurers();
if (data.status === 'success') {
toast.success(data.message);
} else {
toast.error(data.message);
}
}
</script>
<template>
<Modal
:show="showTreasurers"
title="Schatzis zuweisen"
@close="emit('closeTreasurers')"
>
<h3>Zuständige Schatzis:</h3>
<p v-for="user in commonProps.activeUsers">
<input
type="checkbox"
:id="'user_' + user.id"
:value="user.id"
v-model="selectedTreasurers"
/>
<label :for="'user_' + user.id">{{user.fullname}}</label>
</p>
<input type="button" value="Speichern" @click="updateCostUnit" />
</Modal>
</template>
<style>
.mareike-save-button {
background-color: #2271b1 !important;
color: #ffffff !important;
}
</style>

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

@@ -0,0 +1,50 @@
<?php
namespace App\Domains\Dashboard\Controllers;
use App\Providers\AuthCheckProvider;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DashboardController extends CommonController {
public function __invoke(Request $request) {
if ($this->checkAuth()) {
return $this->renderForLoggedInUser($request);
}
return redirect()->intended('/login');
}
private function renderForLoggedInUser(Request $request) {
$authCheckProvider = new AuthCheckProvider;
$inertiaProvider = new InertiaProvider('Dashboard/Dashboard', [
'myInvoices' => $this->invoices->getMyInvoicesWidget(),
'myParticipations' => $this->eventParticipants->getMyParticipations(),
]);
return $inertiaProvider->render();
}
private function renderForGuest(Request $request) {
}
public function getMyInvoices() : JsonResponse {
$invoices = $this->invoices->getMyInvoicesWidget();
return response()->json(['myInvoices' => $invoices]);
}
public function getOpenCostUnits() : JsonResponse {
$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

@@ -0,0 +1,21 @@
<?php
use App\Domains\Dashboard\Controllers\DashboardController;
use App\Domains\Dashboard\Controllers\StorePersonalDataController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::middleware(['auth'])->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

@@ -0,0 +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

@@ -0,0 +1,61 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
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%;">
<MyParticipations />
</shadowed-box>
<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>
</template>
<style>
.dashboard-widget-container {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
position: relative;
}
.dashboard-widget-box {
flex-grow: 1; display: inline-block;
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

@@ -0,0 +1,32 @@
<script setup>
import {onMounted, reactive} from "vue";
const myInvoices = reactive({
'myInvoices': '',
'approvedInvoices': '',
'deniedInvoices': '',
})
onMounted(async () => {
const response = await fetch('/api/v1/dashboard/my-invoices');
const data = await response.json();
Object.assign(myInvoices, data);
});
</script>
<template>
<h2>Meine Abrechnungen</h2>
<p v-for="invoice in myInvoices.myInvoices" class="widget-content-item">
<a :href="'/invoice/my-invoices/' + invoice.slug" class="link">{{invoice.title}} ({{invoice.count}})</a>
<label>
{{invoice.amount}}
</label>
</p>
</template>
<style scoped>
</style>

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 costUnits = reactive({
'openCostUnits': '',
})
onMounted(async () => {
const response = await fetch('/api/v1/dashboard/open-cost-units');
const data = await response.json();
Object.assign(costUnits, data);
});
</script>
<template>
<table class="widget-content-item" v-if="costUnits.openCostUnits.length > 0">
<tr>
<td style="font-weight: bold">Kostenstelle</td>
<td style="font-weight: bold">Neu</td>
<td style="font-weight: bold">Ang</td>
<td style="font-weight: bold">Betrag</td>
</tr>
<tr v-for="costUnit in costUnits.openCostUnits">
<td><a :href="'/cost-unit/' + costUnit.id" class="link">{{costUnit.name}}</a></td>
<td>{{costUnit.new_invoices_count}}</td>
<td>{{costUnit.approved_invoices_count}}</td>
<td>{{costUnit.totalAmount}}</td>
</tr>
</table>
<p v-else style="padding: 10px; font-weight: bold">Es existieren im Moment keine Abrechnugnen, um die du dich kümmern musst.</p>
</template>
<style scoped>
</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,101 @@
<?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 = clone $eventEnd;
$billingDeadline->modify('+6 weeks');
$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)
]);
}
}

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