diff --git a/.ai/conventions.md b/.ai/conventions.md new file mode 100644 index 0000000..f703cdc --- /dev/null +++ b/.ai/conventions.md @@ -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: `...` + +## 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. diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index b71b1ea..656cf1b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ Homestead.json Homestead.yaml Thumbs.db +/docker-compose.yaml diff --git a/.junie/AGENTS.md b/.junie/AGENTS.md new file mode 100644 index 0000000..e8e0891 --- /dev/null +++ b/.junie/AGENTS.md @@ -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. diff --git a/Makefile b/Makefile index fbaa382..8baf640 100644 --- a/Makefile +++ b/Makefile @@ -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 && \ diff --git a/app/Casts/AmountCast.php b/app/Casts/AmountCast.php new file mode 100644 index 0000000..cda19ca --- /dev/null +++ b/app/Casts/AmountCast.php @@ -0,0 +1,34 @@ +getAmount(); + } + + return (float) $value; + } +} diff --git a/app/Domains/CostUnit/Actions/ChangeCostUnitDetails/ChangeCostUnitDetailsCommand.php b/app/Domains/CostUnit/Actions/ChangeCostUnitDetails/ChangeCostUnitDetailsCommand.php new file mode 100644 index 0000000..341b025 --- /dev/null +++ b/app/Domains/CostUnit/Actions/ChangeCostUnitDetails/ChangeCostUnitDetailsCommand.php @@ -0,0 +1,22 @@ +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; + } +} diff --git a/app/Domains/CostUnit/Actions/ChangeCostUnitDetails/ChangeCostUnitDetailsRequest.php b/app/Domains/CostUnit/Actions/ChangeCostUnitDetails/ChangeCostUnitDetailsRequest.php new file mode 100644 index 0000000..ab3abae --- /dev/null +++ b/app/Domains/CostUnit/Actions/ChangeCostUnitDetails/ChangeCostUnitDetailsRequest.php @@ -0,0 +1,22 @@ +costUnit = $costUnit; + $this->distanceAllowance = $distanceAllowance; + $this->mailOnNew = $mailOnNew; + $this->billingDeadline = $billingDeadline; + } +} diff --git a/app/Domains/CostUnit/Actions/ChangeCostUnitDetails/ChangeCostUnitDetailsResponse.php b/app/Domains/CostUnit/Actions/ChangeCostUnitDetails/ChangeCostUnitDetailsResponse.php new file mode 100644 index 0000000..38ceb70 --- /dev/null +++ b/app/Domains/CostUnit/Actions/ChangeCostUnitDetails/ChangeCostUnitDetailsResponse.php @@ -0,0 +1,11 @@ +success = false; + } +} diff --git a/app/Domains/CostUnit/Actions/ChangeCostUnitState/ChangeCostUnitStateCommand.php b/app/Domains/CostUnit/Actions/ChangeCostUnitState/ChangeCostUnitStateCommand.php new file mode 100644 index 0000000..da4be16 --- /dev/null +++ b/app/Domains/CostUnit/Actions/ChangeCostUnitState/ChangeCostUnitStateCommand.php @@ -0,0 +1,21 @@ +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; + } +} diff --git a/app/Domains/CostUnit/Actions/ChangeCostUnitState/ChangeCostUnitStateRequest.php b/app/Domains/CostUnit/Actions/ChangeCostUnitState/ChangeCostUnitStateRequest.php new file mode 100644 index 0000000..918f136 --- /dev/null +++ b/app/Domains/CostUnit/Actions/ChangeCostUnitState/ChangeCostUnitStateRequest.php @@ -0,0 +1,18 @@ +costUnit = $costUnit; + $this->allowNew = $allowNew; + $this->isArchived = $isArchived; + } +} diff --git a/app/Domains/CostUnit/Actions/ChangeCostUnitState/ChangeCostUnitStateResponse.php b/app/Domains/CostUnit/Actions/ChangeCostUnitState/ChangeCostUnitStateResponse.php new file mode 100644 index 0000000..5c486bf --- /dev/null +++ b/app/Domains/CostUnit/Actions/ChangeCostUnitState/ChangeCostUnitStateResponse.php @@ -0,0 +1,11 @@ +success = false; + } +} diff --git a/app/Domains/CostUnit/Actions/ChangeCostUnitTreasurers/ChangeCostUnitTreasurersCommand.php b/app/Domains/CostUnit/Actions/ChangeCostUnitTreasurers/ChangeCostUnitTreasurersCommand.php new file mode 100644 index 0000000..bacb627 --- /dev/null +++ b/app/Domains/CostUnit/Actions/ChangeCostUnitTreasurers/ChangeCostUnitTreasurersCommand.php @@ -0,0 +1,31 @@ +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; + } +} diff --git a/app/Domains/CostUnit/Actions/ChangeCostUnitTreasurers/ChangeCostUnitTreasurersRequest.php b/app/Domains/CostUnit/Actions/ChangeCostUnitTreasurers/ChangeCostUnitTreasurersRequest.php new file mode 100644 index 0000000..340d436 --- /dev/null +++ b/app/Domains/CostUnit/Actions/ChangeCostUnitTreasurers/ChangeCostUnitTreasurersRequest.php @@ -0,0 +1,15 @@ +treasurers = $treasurers; + $this->costUnit = $costUnit; + } +} diff --git a/app/Domains/CostUnit/Actions/ChangeCostUnitTreasurers/ChangeCostUnitTreasurersResponse.php b/app/Domains/CostUnit/Actions/ChangeCostUnitTreasurers/ChangeCostUnitTreasurersResponse.php new file mode 100644 index 0000000..17f7718 --- /dev/null +++ b/app/Domains/CostUnit/Actions/ChangeCostUnitTreasurers/ChangeCostUnitTreasurersResponse.php @@ -0,0 +1,7 @@ +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; + } +} diff --git a/app/Domains/CostUnit/Actions/CreateCostUnit/CreateCostUnitRequest.php b/app/Domains/CostUnit/Actions/CreateCostUnit/CreateCostUnitRequest.php new file mode 100644 index 0000000..cf32770 --- /dev/null +++ b/app/Domains/CostUnit/Actions/CreateCostUnit/CreateCostUnitRequest.php @@ -0,0 +1,23 @@ +name = $name; + $this->type = $type; + $this->distanceAllowance = $distanceAllowance; + $this->mailOnNew = $mailOnNew; + $this->billingDeadline = $billingDeadline; + } +} diff --git a/app/Domains/CostUnit/Actions/CreateCostUnit/CreateCostUnitResponse.php b/app/Domains/CostUnit/Actions/CreateCostUnit/CreateCostUnitResponse.php new file mode 100644 index 0000000..3a44e14 --- /dev/null +++ b/app/Domains/CostUnit/Actions/CreateCostUnit/CreateCostUnitResponse.php @@ -0,0 +1,15 @@ +success = false; + $this->costUnit = null; + } +} diff --git a/app/Domains/CostUnit/Controllers/ChangeStateController.php b/app/Domains/CostUnit/Controllers/ChangeStateController.php new file mode 100644 index 0000000..724d33e --- /dev/null +++ b/app/Domains/CostUnit/Controllers/ChangeStateController.php @@ -0,0 +1,47 @@ +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.' + ]); + } +} diff --git a/app/Domains/CostUnit/Controllers/CreateController.php b/app/Domains/CostUnit/Controllers/CreateController.php new file mode 100644 index 0000000..d4c6dbe --- /dev/null +++ b/app/Domains/CostUnit/Controllers/CreateController.php @@ -0,0 +1,36 @@ +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([]); + } +} diff --git a/app/Domains/CostUnit/Controllers/DistanceAllowanceController.php b/app/Domains/CostUnit/Controllers/DistanceAllowanceController.php new file mode 100644 index 0000000..f081966 --- /dev/null +++ b/app/Domains/CostUnit/Controllers/DistanceAllowanceController.php @@ -0,0 +1,20 @@ +costUnits->getById($costUnitId, true); + if (null !== $costUnit) { + $distanceAllowance = $costUnit->distance_allowance; + } + + return response()->json([ + 'distanceAllowance' => $distanceAllowance + ]); + } +} diff --git a/app/Domains/CostUnit/Controllers/EditController.php b/app/Domains/CostUnit/Controllers/EditController.php new file mode 100644 index 0000000..9bc5192 --- /dev/null +++ b/app/Domains/CostUnit/Controllers/EditController.php @@ -0,0 +1,58 @@ +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.', + ]); + } +} diff --git a/app/Domains/CostUnit/Controllers/ExportController.php b/app/Domains/CostUnit/Controllers/ExportController.php new file mode 100644 index 0000000..a473033 --- /dev/null +++ b/app/Domains/CostUnit/Controllers/ExportController.php @@ -0,0 +1,110 @@ +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(); + } +} + diff --git a/app/Domains/CostUnit/Controllers/ListController.php b/app/Domains/CostUnit/Controllers/ListController.php new file mode 100644 index 0000000..de9ddfb --- /dev/null +++ b/app/Domains/CostUnit/Controllers/ListController.php @@ -0,0 +1,48 @@ + 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(), + ]); + } + + +} diff --git a/app/Domains/CostUnit/Controllers/OpenController.php b/app/Domains/CostUnit/Controllers/OpenController.php new file mode 100644 index 0000000..8f3ada2 --- /dev/null +++ b/app/Domains/CostUnit/Controllers/OpenController.php @@ -0,0 +1,31 @@ +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, + ]); + } +} diff --git a/app/Domains/CostUnit/Controllers/TreasurersEditController.php b/app/Domains/CostUnit/Controllers/TreasurersEditController.php new file mode 100644 index 0000000..2137e2d --- /dev/null +++ b/app/Domains/CostUnit/Controllers/TreasurersEditController.php @@ -0,0 +1,54 @@ +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.' + ]); + } +} diff --git a/app/Domains/CostUnit/Routes/api.php b/app/Domains/CostUnit/Routes/api.php new file mode 100644 index 0000000..76781e1 --- /dev/null +++ b/app/Domains/CostUnit/Routes/api.php @@ -0,0 +1,55 @@ +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']); + }); + }); + }); + }); + }); diff --git a/app/Domains/CostUnit/Routes/web.php b/app/Domains/CostUnit/Routes/web.php new file mode 100644 index 0000000..ed2400c --- /dev/null +++ b/app/Domains/CostUnit/Routes/web.php @@ -0,0 +1,39 @@ +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']); + + +}); + + + diff --git a/app/Domains/CostUnit/Views/Create.vue b/app/Domains/CostUnit/Views/Create.vue new file mode 100644 index 0000000..fc510b6 --- /dev/null +++ b/app/Domains/CostUnit/Views/Create.vue @@ -0,0 +1,78 @@ + + + diff --git a/app/Domains/CostUnit/Views/Edit.vue b/app/Domains/CostUnit/Views/Edit.vue new file mode 100644 index 0000000..aca6b6a --- /dev/null +++ b/app/Domains/CostUnit/Views/Edit.vue @@ -0,0 +1,83 @@ + + + diff --git a/app/Domains/CostUnit/Views/List.vue b/app/Domains/CostUnit/Views/List.vue new file mode 100644 index 0000000..b842a97 --- /dev/null +++ b/app/Domains/CostUnit/Views/List.vue @@ -0,0 +1,82 @@ + + + diff --git a/app/Domains/CostUnit/Views/Open.vue b/app/Domains/CostUnit/Views/Open.vue new file mode 100644 index 0000000..4a28351 --- /dev/null +++ b/app/Domains/CostUnit/Views/Open.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/Domains/CostUnit/Views/Partials/CostUnitDetails.vue b/app/Domains/CostUnit/Views/Partials/CostUnitDetails.vue new file mode 100644 index 0000000..ebf091d --- /dev/null +++ b/app/Domains/CostUnit/Views/Partials/CostUnitDetails.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/app/Domains/CostUnit/Views/Partials/ListCostUnits.vue b/app/Domains/CostUnit/Views/Partials/ListCostUnits.vue new file mode 100644 index 0000000..c8f85c6 --- /dev/null +++ b/app/Domains/CostUnit/Views/Partials/ListCostUnits.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/app/Domains/CostUnit/Views/Partials/ListInvoices.vue b/app/Domains/CostUnit/Views/Partials/ListInvoices.vue new file mode 100644 index 0000000..00bc03a --- /dev/null +++ b/app/Domains/CostUnit/Views/Partials/ListInvoices.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/app/Domains/CostUnit/Views/Partials/Treasurers.vue b/app/Domains/CostUnit/Views/Partials/Treasurers.vue new file mode 100644 index 0000000..5f8735f --- /dev/null +++ b/app/Domains/CostUnit/Views/Partials/Treasurers.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/app/Domains/Dashboard/Actions/UpdatePersonalData/UpdatePersonalDataCommand.php b/app/Domains/Dashboard/Actions/UpdatePersonalData/UpdatePersonalDataCommand.php new file mode 100644 index 0000000..c95b7f6 --- /dev/null +++ b/app/Domains/Dashboard/Actions/UpdatePersonalData/UpdatePersonalDataCommand.php @@ -0,0 +1,23 @@ +users->updatePersonalData($this->request); + + $response = new UpdatePersonalDataResponse(); + $response->success = true; + + return $response; + } +} diff --git a/app/Domains/Dashboard/Actions/UpdatePersonalData/UpdatePersonalDataRequest.php b/app/Domains/Dashboard/Actions/UpdatePersonalData/UpdatePersonalDataRequest.php new file mode 100644 index 0000000..25046aa --- /dev/null +++ b/app/Domains/Dashboard/Actions/UpdatePersonalData/UpdatePersonalDataRequest.php @@ -0,0 +1,29 @@ +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()]); + } +} diff --git a/app/Domains/Dashboard/Controllers/MessagesController.php b/app/Domains/Dashboard/Controllers/MessagesController.php new file mode 100644 index 0000000..7e671a4 --- /dev/null +++ b/app/Domains/Dashboard/Controllers/MessagesController.php @@ -0,0 +1,14 @@ +render(); + } +} diff --git a/app/Domains/Dashboard/Controllers/PersonalDataController.php b/app/Domains/Dashboard/Controllers/PersonalDataController.php new file mode 100644 index 0000000..849784e --- /dev/null +++ b/app/Domains/Dashboard/Controllers/PersonalDataController.php @@ -0,0 +1,45 @@ +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(); + } +} diff --git a/app/Domains/Dashboard/Controllers/StorePersonalDataController.php b/app/Domains/Dashboard/Controllers/StorePersonalDataController.php new file mode 100644 index 0000000..c809e82 --- /dev/null +++ b/app/Domains/Dashboard/Controllers/StorePersonalDataController.php @@ -0,0 +1,43 @@ +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.']); + } +} diff --git a/app/Domains/Dashboard/Routes/api.php b/app/Domains/Dashboard/Routes/api.php new file mode 100644 index 0000000..73f0df6 --- /dev/null +++ b/app/Domains/Dashboard/Routes/api.php @@ -0,0 +1,21 @@ +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); + }); + + + + }); +}); diff --git a/app/Domains/Dashboard/Routes/web.php b/app/Domains/Dashboard/Routes/web.php new file mode 100644 index 0000000..0945f7d --- /dev/null +++ b/app/Domains/Dashboard/Routes/web.php @@ -0,0 +1,16 @@ +group(function () { + Route::middleware(['auth'])->group(function () { + Route::get('/personal-data', PersonalDataController::class); + Route::get('/messages', MessagesController::class); + + + }); +}); diff --git a/app/Domains/Dashboard/Views/Dashboard.vue b/app/Domains/Dashboard/Views/Dashboard.vue new file mode 100644 index 0000000..7acfa70 --- /dev/null +++ b/app/Domains/Dashboard/Views/Dashboard.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/app/Domains/Dashboard/Views/Messages.vue b/app/Domains/Dashboard/Views/Messages.vue new file mode 100644 index 0000000..31915c0 --- /dev/null +++ b/app/Domains/Dashboard/Views/Messages.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/app/Domains/Dashboard/Views/Partials/Widgets/MyInvoices.vue b/app/Domains/Dashboard/Views/Partials/Widgets/MyInvoices.vue new file mode 100644 index 0000000..74020f4 --- /dev/null +++ b/app/Domains/Dashboard/Views/Partials/Widgets/MyInvoices.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue b/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue new file mode 100644 index 0000000..08254f2 --- /dev/null +++ b/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipationsShort.vue b/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipationsShort.vue new file mode 100644 index 0000000..b174a33 --- /dev/null +++ b/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipationsShort.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/app/Domains/Dashboard/Views/Partials/Widgets/OpenCostUnits.vue b/app/Domains/Dashboard/Views/Partials/Widgets/OpenCostUnits.vue new file mode 100644 index 0000000..baa7053 --- /dev/null +++ b/app/Domains/Dashboard/Views/Partials/Widgets/OpenCostUnits.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/app/Domains/Dashboard/Views/Partials/Widgets/UpcomingEvents.vue b/app/Domains/Dashboard/Views/Partials/Widgets/UpcomingEvents.vue new file mode 100644 index 0000000..47f4557 --- /dev/null +++ b/app/Domains/Dashboard/Views/Partials/Widgets/UpcomingEvents.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/app/Domains/Dashboard/Views/PersonalData.vue b/app/Domains/Dashboard/Views/PersonalData.vue new file mode 100644 index 0000000..4516dfd --- /dev/null +++ b/app/Domains/Dashboard/Views/PersonalData.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckCommand.php b/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckCommand.php new file mode 100644 index 0000000..79b5945 --- /dev/null +++ b/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckCommand.php @@ -0,0 +1,52 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckRequest.php b/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckRequest.php new file mode 100644 index 0000000..d09eba7 --- /dev/null +++ b/app/Domains/Event/Actions/CertificateOfConductionCheck/CertificateOfConductionCheckRequest.php @@ -0,0 +1,12 @@ +status = EfzStatus::EFZ_STATUS_NOT_CHECKED; + } +} diff --git a/app/Domains/Event/Actions/CreateEvent/CreateEventCommand.php b/app/Domains/Event/Actions/CreateEvent/CreateEventCommand.php new file mode 100644 index 0000000..4432649 --- /dev/null +++ b/app/Domains/Event/Actions/CreateEvent/CreateEventCommand.php @@ -0,0 +1,76 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/CreateEvent/CreateEventRequest.php b/app/Domains/Event/Actions/CreateEvent/CreateEventRequest.php new file mode 100644 index 0000000..87844d5 --- /dev/null +++ b/app/Domains/Event/Actions/CreateEvent/CreateEventRequest.php @@ -0,0 +1,40 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/CreateEvent/CreateEventResponse.php b/app/Domains/Event/Actions/CreateEvent/CreateEventResponse.php new file mode 100644 index 0000000..ba4b890 --- /dev/null +++ b/app/Domains/Event/Actions/CreateEvent/CreateEventResponse.php @@ -0,0 +1,15 @@ +success = false; + $this->event = null; + } +} diff --git a/app/Domains/Event/Actions/GenerateIcal/GenerateIcalCommand.php b/app/Domains/Event/Actions/GenerateIcal/GenerateIcalCommand.php new file mode 100644 index 0000000..46c2e66 --- /dev/null +++ b/app/Domains/Event/Actions/GenerateIcal/GenerateIcalCommand.php @@ -0,0 +1,55 @@ +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 + ); + } +} diff --git a/app/Domains/Event/Actions/GenerateIcal/GenerateIcalRequest.php b/app/Domains/Event/Actions/GenerateIcal/GenerateIcalRequest.php new file mode 100644 index 0000000..4bc2f20 --- /dev/null +++ b/app/Domains/Event/Actions/GenerateIcal/GenerateIcalRequest.php @@ -0,0 +1,12 @@ +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 + ); + } +} diff --git a/app/Domains/Event/Actions/GenerateIcalForDeadline/GenerateIcalForDeadlineRequest.php b/app/Domains/Event/Actions/GenerateIcalForDeadline/GenerateIcalForDeadlineRequest.php new file mode 100644 index 0000000..5f9d914 --- /dev/null +++ b/app/Domains/Event/Actions/GenerateIcalForDeadline/GenerateIcalForDeadlineRequest.php @@ -0,0 +1,11 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/ManualCertificateOfConductionCheck/ManualCertificateOfConductionCheckRequest.php b/app/Domains/Event/Actions/ManualCertificateOfConductionCheck/ManualCertificateOfConductionCheckRequest.php new file mode 100644 index 0000000..2a37523 --- /dev/null +++ b/app/Domains/Event/Actions/ManualCertificateOfConductionCheck/ManualCertificateOfConductionCheckRequest.php @@ -0,0 +1,12 @@ +success = false; + } +} diff --git a/app/Domains/Event/Actions/ParticipantPayment/ParticipantPaymentCommand.php b/app/Domains/Event/Actions/ParticipantPayment/ParticipantPaymentCommand.php new file mode 100644 index 0000000..8494c70 --- /dev/null +++ b/app/Domains/Event/Actions/ParticipantPayment/ParticipantPaymentCommand.php @@ -0,0 +1,69 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/ParticipantPayment/ParticipantPaymentRequest.php b/app/Domains/Event/Actions/ParticipantPayment/ParticipantPaymentRequest.php new file mode 100644 index 0000000..e2620e0 --- /dev/null +++ b/app/Domains/Event/Actions/ParticipantPayment/ParticipantPaymentRequest.php @@ -0,0 +1,16 @@ +participant = $participant; + $this->amountPaid = $amountPaid; + } +} diff --git a/app/Domains/Event/Actions/ParticipantPayment/ParticipantPaymentResponse.php b/app/Domains/Event/Actions/ParticipantPayment/ParticipantPaymentResponse.php new file mode 100644 index 0000000..94e04e7 --- /dev/null +++ b/app/Domains/Event/Actions/ParticipantPayment/ParticipantPaymentResponse.php @@ -0,0 +1,20 @@ +amountPaid = null; + $this->amountExpected = null; + $this->success = false; + $this->participant = null; + } +} diff --git a/app/Domains/Event/Actions/SendMissingPaymentMails/SendMissingPaymentMailsCommand.php b/app/Domains/Event/Actions/SendMissingPaymentMails/SendMissingPaymentMailsCommand.php new file mode 100644 index 0000000..6cb00c0 --- /dev/null +++ b/app/Domains/Event/Actions/SendMissingPaymentMails/SendMissingPaymentMailsCommand.php @@ -0,0 +1,37 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/SendMissingPaymentMails/SendMissingPaymentMailsRequest.php b/app/Domains/Event/Actions/SendMissingPaymentMails/SendMissingPaymentMailsRequest.php new file mode 100644 index 0000000..d4e465a --- /dev/null +++ b/app/Domains/Event/Actions/SendMissingPaymentMails/SendMissingPaymentMailsRequest.php @@ -0,0 +1,16 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/SetCostUnit/SetCostUnitRequest.php b/app/Domains/Event/Actions/SetCostUnit/SetCostUnitRequest.php new file mode 100644 index 0000000..f3d6881 --- /dev/null +++ b/app/Domains/Event/Actions/SetCostUnit/SetCostUnitRequest.php @@ -0,0 +1,16 @@ +event = $event; + $this->costUnit = $costUnit; + } +} diff --git a/app/Domains/Event/Actions/SetCostUnit/SetCostUnitResponse.php b/app/Domains/Event/Actions/SetCostUnit/SetCostUnitResponse.php new file mode 100644 index 0000000..7d4d61f --- /dev/null +++ b/app/Domains/Event/Actions/SetCostUnit/SetCostUnitResponse.php @@ -0,0 +1,11 @@ +success = false; + } +} diff --git a/app/Domains/Event/Actions/SetParticipationFees/SetParticipationFeesCommand.php b/app/Domains/Event/Actions/SetParticipationFees/SetParticipationFeesCommand.php new file mode 100644 index 0000000..062e5ea --- /dev/null +++ b/app/Domains/Event/Actions/SetParticipationFees/SetParticipationFeesCommand.php @@ -0,0 +1,93 @@ +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(); + } + + +} diff --git a/app/Domains/Event/Actions/SetParticipationFees/SetParticipationFeesRequest.php b/app/Domains/Event/Actions/SetParticipationFees/SetParticipationFeesRequest.php new file mode 100644 index 0000000..0df1171 --- /dev/null +++ b/app/Domains/Event/Actions/SetParticipationFees/SetParticipationFeesRequest.php @@ -0,0 +1,27 @@ +event = $event; + $this->participationFeeFirst = $participationFeeFirst; + $this->participationFeeSecond = null; + $this->participationFeeThird = null; + $this->participationFeeFourth = null; + $this->siblingReduction = $siblingReduction; + } + +} diff --git a/app/Domains/Event/Actions/SetParticipationFees/SetParticipationFeesResponse.php b/app/Domains/Event/Actions/SetParticipationFees/SetParticipationFeesResponse.php new file mode 100644 index 0000000..d8e0739 --- /dev/null +++ b/app/Domains/Event/Actions/SetParticipationFees/SetParticipationFeesResponse.php @@ -0,0 +1,11 @@ +success = false; + } + +} diff --git a/app/Domains/Event/Actions/SetParticipationState/SetParticipationStateCommand.php b/app/Domains/Event/Actions/SetParticipationState/SetParticipationStateCommand.php new file mode 100644 index 0000000..761dcdd --- /dev/null +++ b/app/Domains/Event/Actions/SetParticipationState/SetParticipationStateCommand.php @@ -0,0 +1,53 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/SetParticipationState/SetParticipationStateReSignonRequest.php b/app/Domains/Event/Actions/SetParticipationState/SetParticipationStateReSignonRequest.php new file mode 100644 index 0000000..f7c6b99 --- /dev/null +++ b/app/Domains/Event/Actions/SetParticipationState/SetParticipationStateReSignonRequest.php @@ -0,0 +1,9 @@ +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; + } + +} diff --git a/app/Domains/Event/Actions/SignUp/SignUpRequest.php b/app/Domains/Event/Actions/SignUp/SignUpRequest.php new file mode 100644 index 0000000..c078cc4 --- /dev/null +++ b/app/Domains/Event/Actions/SignUp/SignUpRequest.php @@ -0,0 +1,50 @@ +success = false; + $this->participant = null; + } + +} diff --git a/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php b/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php new file mode 100644 index 0000000..1acebf3 --- /dev/null +++ b/app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php @@ -0,0 +1,43 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php b/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php new file mode 100644 index 0000000..4c9edd4 --- /dev/null +++ b/app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php @@ -0,0 +1,41 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/UpdateEvent/UpdateEventResponse.php b/app/Domains/Event/Actions/UpdateEvent/UpdateEventResponse.php new file mode 100644 index 0000000..c30be98 --- /dev/null +++ b/app/Domains/Event/Actions/UpdateEvent/UpdateEventResponse.php @@ -0,0 +1,12 @@ +success = false; + } + +} diff --git a/app/Domains/Event/Actions/UpdateManagers/UpdateManagersCommand.php b/app/Domains/Event/Actions/UpdateManagers/UpdateManagersCommand.php new file mode 100644 index 0000000..80dadb0 --- /dev/null +++ b/app/Domains/Event/Actions/UpdateManagers/UpdateManagersCommand.php @@ -0,0 +1,25 @@ +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; + } +} diff --git a/app/Domains/Event/Actions/UpdateManagers/UpdateManagersRequest.php b/app/Domains/Event/Actions/UpdateManagers/UpdateManagersRequest.php new file mode 100644 index 0000000..5c137c9 --- /dev/null +++ b/app/Domains/Event/Actions/UpdateManagers/UpdateManagersRequest.php @@ -0,0 +1,15 @@ +managers = $managers; + $this->event = $event; + } +} diff --git a/app/Domains/Event/Actions/UpdateManagers/UpdateManagersResponse.php b/app/Domains/Event/Actions/UpdateManagers/UpdateManagersResponse.php new file mode 100644 index 0000000..27b654c --- /dev/null +++ b/app/Domains/Event/Actions/UpdateManagers/UpdateManagersResponse.php @@ -0,0 +1,11 @@ +success = false; + } +} diff --git a/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantCommand.php b/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantCommand.php new file mode 100644 index 0000000..221c0fb --- /dev/null +++ b/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantCommand.php @@ -0,0 +1,142 @@ +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; + } + } +} diff --git a/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantRequest.php b/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantRequest.php new file mode 100644 index 0000000..4941794 --- /dev/null +++ b/app/Domains/Event/Actions/UpdateParticipant/UpdateParticipantRequest.php @@ -0,0 +1,39 @@ +success = false; + $this->cocStatus =null; + $this->amountPaid = null; + $this->amountExpected = null; + $this->participant = null; + } +} diff --git a/app/Domains/Event/Controllers/AvailableEventsController.php b/app/Domains/Event/Controllers/AvailableEventsController.php new file mode 100644 index 0000000..fc05b24 --- /dev/null +++ b/app/Domains/Event/Controllers/AvailableEventsController.php @@ -0,0 +1,22 @@ +events->getAvailable(false) as $event) { + $events[] = $event->toResource()->toArray($request); + }; + + + $inertiaProvider = new InertiaProvider('Event/ListAvailable', ['events' => $events]); + return $inertiaProvider->render(); + } +} diff --git a/app/Domains/Event/Controllers/CreateController.php b/app/Domains/Event/Controllers/CreateController.php new file mode 100644 index 0000000..332c716 --- /dev/null +++ b/app/Domains/Event/Controllers/CreateController.php @@ -0,0 +1,100 @@ + auth()->user()->email, + 'eventAccount' => $this->tenant->account_name, + 'eventIban' => $this->tenant->account_iban, + 'eventPayPerDay' => $this->tenant->slug === 'lv' ? true : false, + 'participationFeeType' => $this->tenant->slug === 'lv' ? + ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED : + ParticipationFeeType::PARTICIPATION_FEE_TYPE_SOLIDARITY, + ])->render(); + } + + public function doCreate(Request $request) : JsonResponse { + + $eventBegin = DateTime::createFromFormat('Y-m-d', $request->input('eventBegin')); + $eventEnd = DateTime::createFromFormat('Y-m-d', $request->input('eventEnd')); + $eventEarlyBirdEnd = DateTime::createFromFormat('Y-m-d', $request->input('eventEarlyBirdEnd')); + $registrationFinalEnd = DateTime::createFromFormat('Y-m-d', $request->input('eventRegistrationFinalEnd')); + $participationFeeType = ParticipationFeeType::where('slug', $request->input('eventParticipationFeeType'))->first(); + $payPerDay = $request->input('eventPayPerDay'); + $payDirect = $request->input('eventPayDirectly'); + + $billingDeadline = $eventEnd->modify('+1 month'); + + $createRequest = new CreateEventRequest( + $request->input('eventName'), + $request->input('eventLocation'), + $request->input('eventPostalCode'), + $request->input('eventEmail'), + $eventBegin, + $eventEnd, + $eventEarlyBirdEnd, + $registrationFinalEnd, + $request->input('eventEarlyBirdEndAmountIncrease'), + $participationFeeType, + $request->input('eventAccount'), + $request->input('eventIban'), + $payPerDay, + $payDirect + ); + + $wasSuccessful = false; + + $createCommand = new CreateEventCommand($createRequest); + $result = $createCommand->execute(); + if ($result->success) { + $createCostUnitRequest = new CreateCostUnitRequest( + $result->event->name, + CostUnitType::COST_UNIT_TYPE_EVENT, + Amount::fromString('0,25'), + true, + $billingDeadline + ); + + $createCostUnitCommand = new CreateCostUnitCommand($createCostUnitRequest); + $costUnitResponse = $createCostUnitCommand->execute(); + + if ($costUnitResponse->success) { + $costUnitUpdateRequest = new SetCostUnitRequest($result->event, $costUnitResponse->costUnit); + $costUnitUpdateCommand = new SetCostUnitCommand($costUnitUpdateRequest); + $costUnitSetResponse = $costUnitUpdateCommand->execute(); + $wasSuccessful = $costUnitSetResponse->success; + } + } + + if ($wasSuccessful) { + return response()->json([ + 'status' => 'success', + 'event' => new EventResource($costUnitUpdateRequest->event)->toArray($request) + ]); + } else { + return response()->json([ + 'status' => 'error', + 'message' => 'Die Veranstaltung konnte nicht angelegt werden.' + ]); + } + } +} + diff --git a/app/Domains/Event/Controllers/DetailsController.php b/app/Domains/Event/Controllers/DetailsController.php new file mode 100644 index 0000000..b0d7ebc --- /dev/null +++ b/app/Domains/Event/Controllers/DetailsController.php @@ -0,0 +1,198 @@ +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) + ]); + } +} diff --git a/app/Domains/Event/Controllers/MailCompose/ByGroupController.php b/app/Domains/Event/Controllers/MailCompose/ByGroupController.php new file mode 100644 index 0000000..f265b25 --- /dev/null +++ b/app/Domains/Event/Controllers/MailCompose/ByGroupController.php @@ -0,0 +1,34 @@ +events->getByIdentifier($eventIdentifier, true); + $recipients = []; + switch ($groupType) { + case 'by-local-group': + $participants = $this->eventParticipants->groupByLocalGroup($event, $request, $request->input('groupName')); + $recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]); + break; + case 'by-participation-group': + $participants = $this->eventParticipants->groupByParticipationType($event, $request, $request->input('groupName')); + $recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]); + break; + case 'signed-off': + $participants = $this->eventParticipants->getSignedOffParticipants($event, $request, $request->input('groupName')); + $recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]); + break; + default: + $participants = $this->eventParticipants->getForList($event, $request); + $recipients = $this->eventParticipants->getMailAddresses($participants); + + } + + return response()->json(['recipients' => $recipients]); + } +} diff --git a/app/Domains/Event/Controllers/MailCompose/SendController.php b/app/Domains/Event/Controllers/MailCompose/SendController.php new file mode 100644 index 0000000..d586e8a --- /dev/null +++ b/app/Domains/Event/Controllers/MailCompose/SendController.php @@ -0,0 +1,66 @@ +input('recipients') + |> function (string $value) : string { return str_replace(';', ',', $value); } + |> function (string $value) : array { return explode( ',', $value); }; + + $event = $this->events->getByIdentifier($eventIdentifier, true)->toResource()->toArray($request); + $sentRecipients = []; + $allOkay = true; + $subject = $request->input('subject') ?? 'Neue Nachricht zu Veranstaltung "' . $event['name'] . '"'; + + foreach ($recipients as $recipient) { + if (in_array($recipient, $sentRecipients)) { + continue; + } + + $sentRecipients[] = $recipient; + try { + Mail::to(trim($recipient))->send(new ManualMailsCommonMail( + mailSubject: $subject, + message: $request->input('message'), + event: $event, + )); + } catch (\Exception $e) { + $allOkay = false; + } + } + + $user = auth()->user(); + $reportSubject = sprintf('Sendebericht für Nachricht mit Betreff "%s"', $subject); + + Mail::to($user->email)->send(new ManualMailsReportMail( + mailSubject: $reportSubject, + message: $request->input('message'), + event: $event, + originalRecipients: $sentRecipients + )); + + if ($allOkay) { + return response()->json([ + 'success' => true, + 'message' => sprintf( + 'E-Mail wurde erfolgreich an %1$s Personen versendet. Du hast eine Kopie an deine Mail-Adresse erhalten.', + count($sentRecipients) + ), + ]); + } else { + return response()->json([ + 'success' => false, + 'message' => 'Es gab einen Fehler beim Versenden der Nachrichten.' + ]); + } + } +} diff --git a/app/Domains/Event/Controllers/ParticipantController.php b/app/Domains/Event/Controllers/ParticipantController.php new file mode 100644 index 0000000..e1568f4 --- /dev/null +++ b/app/Domains/Event/Controllers/ParticipantController.php @@ -0,0 +1,34 @@ +eventParticipants->getByIdentifier($participantIdentifier, $this->events); + + $cocRequest = new ManualCertificateOfConductionCheckRequest($participant); + $cocCommand = new ManualCertificateOfConductionCheckCommand($cocRequest); + $cocResponse = $cocCommand->execute(); + + return response()->json([ + 'status' => $cocResponse->success ? 'success' : 'error', + 'message' => $cocResponse->success ? 'Das eFZ wurde als gültig hinterlegt' : 'Beim Aktualisieren des eFZ-Status ist ein Fehler aufgetreten.' + ]); + } + + public function __invoke(string $participantIdentifier, Request $request) { + $participant = $this->eventParticipants->getByIdentifier($participantIdentifier, $this->events)->toResource()->toArray($request); + + return response()->json([ + 'participant' => $participant, + ]); + } +} diff --git a/app/Domains/Event/Controllers/ParticipantIcalController.php b/app/Domains/Event/Controllers/ParticipantIcalController.php new file mode 100644 index 0000000..18a7476 --- /dev/null +++ b/app/Domains/Event/Controllers/ParticipantIcalController.php @@ -0,0 +1,31 @@ +eventParticipants->getMyParticipationByIdentifier($participantIdentifier); + + if ($participant === null) { + abort(403, 'Zugriff verweigert.'); + } + + $icalRequest = new GenerateIcalRequest($participant); + $icalCommand = new GenerateIcalCommand($icalRequest); + $icalResponse = $icalCommand->execute(); + + return response($icalResponse->icalContent, 200, [ + 'Content-Type' => 'text/calendar; charset=utf-8', + 'Content-Disposition' => 'attachment; filename="' . $icalResponse->filename . '"', + ]); + } +} diff --git a/app/Domains/Event/Controllers/ParticipantIcalForPaymentController.php b/app/Domains/Event/Controllers/ParticipantIcalForPaymentController.php new file mode 100644 index 0000000..14fd582 --- /dev/null +++ b/app/Domains/Event/Controllers/ParticipantIcalForPaymentController.php @@ -0,0 +1,33 @@ +eventParticipants->getMyParticipationByIdentifier($participantIdentifier); + + if ($participant === null) { + abort(403, 'Zugriff verweigert.'); + } + + $icalRequest = new GenerateIcalForDeadlineRequest($participant->event()->first()); + $icalCommand = new GenerateIcalForDeadlineCommand($icalRequest); + $icalResponse = $icalCommand->execute(); + + return response($icalResponse->icalContent, 200, [ + 'Content-Type' => 'text/calendar; charset=utf-8', + 'Content-Disposition' => 'attachment; filename="' . $icalResponse->filename . '"', + ]); + } +} diff --git a/app/Domains/Event/Controllers/ParticipantPaymentController.php b/app/Domains/Event/Controllers/ParticipantPaymentController.php new file mode 100644 index 0000000..2b42681 --- /dev/null +++ b/app/Domains/Event/Controllers/ParticipantPaymentController.php @@ -0,0 +1,53 @@ +eventParticipants->getByIdentifier($participantIdentifier, $this->events); + + $paymentRequest = new ParticipantPaymentRequest($participant, $participant->amount); + + $paymentCommand = new ParticipantPaymentCommand($paymentRequest); + $paymentResponse = $paymentCommand->execute(); + + return response()->json([ + 'status' => $paymentResponse->success ? 'success' : 'error', + 'message' => $paymentResponse->success ? 'Die Zahlung wurde erfolgreich gebucht.' : 'Beim Buchen der Zahlung ist ein Fehler aufgetreten.' + ]); + } + + public function partialPaymentComplete(string $participantIdentifier, Request $request) { + $participant = $this->eventParticipants->getByIdentifier($participantIdentifier, $this->events); + + $paymentRequest = new ParticipantPaymentRequest($participant, Amount::fromString($request->input('amount'))); + + $paymentCommand = new ParticipantPaymentCommand($paymentRequest); + $paymentResponse = $paymentCommand->execute(); + + + $amountLeft = clone($paymentResponse->amountExpected); + $amountLeft->subtractAmount($paymentResponse->amountPaid); + + return response()->json([ + 'status' => $paymentResponse->success ? 'success' : 'error', + 'message' => $paymentResponse->success ? 'Die Zahlung wurde erfolgreich gebucht.' : 'Beim Buchen der Zahlung ist ein Fehler aufgetreten.', + 'identifier' => $participant->identifier, + 'amount' => [ + 'paid' => $paymentResponse->amountPaid->toString(), + 'expected' => $paymentResponse->amountExpected->toString(), + 'actions' => $amountLeft->getAmount() != 0 ? 'inline' : 'none', + 'class' => $amountLeft->getAmount() != 0 ? 'not-paid' : 'paid', + ] + ]); + + dd($participant); + } +} diff --git a/app/Domains/Event/Controllers/ParticipantReSignOnController.php b/app/Domains/Event/Controllers/ParticipantReSignOnController.php new file mode 100644 index 0000000..8d65308 --- /dev/null +++ b/app/Domains/Event/Controllers/ParticipantReSignOnController.php @@ -0,0 +1,28 @@ +eventParticipants->getByIdentifier($participantIdentifier, $this->events); + + $request = new SetParticipationStateReSignonRequest($participant); + $command = new SetParticipationStateCommand($request); + $command->execute(); + + return response()->json([ + 'status' => 'success', + 'identifier' => $participant->identifier, + 'message' => 'Die Wiederanmeldung wurde erfolgreich durchgeführt.' + ]); + + } +} diff --git a/app/Domains/Event/Controllers/ParticipantSignOffController.php b/app/Domains/Event/Controllers/ParticipantSignOffController.php new file mode 100644 index 0000000..65ad2bd --- /dev/null +++ b/app/Domains/Event/Controllers/ParticipantSignOffController.php @@ -0,0 +1,29 @@ +eventParticipants->getByIdentifier($participantIdentifier, $this->events); + + $signOffDate = DateTime::createFromFormat('Y-m-d', $request->input('cancel_date')); + + $signOffRequest = new SetParticipationStateSignoffRequest($participant, $signOffDate); + $signOffCommand = new SetParticipationStateCommand($signOffRequest); + $signOffCommand->execute(); + + return response()->json([ + 'status' => 'success', + 'identifier' => $participant->identifier, + 'message' => 'Die Abmeldung wurde erfolgreich durchgeführt.' + ]); + + } +} diff --git a/app/Domains/Event/Controllers/ParticipantUpdateController.php b/app/Domains/Event/Controllers/ParticipantUpdateController.php new file mode 100644 index 0000000..869bc51 --- /dev/null +++ b/app/Domains/Event/Controllers/ParticipantUpdateController.php @@ -0,0 +1,85 @@ +eventParticipants->getByIdentifier($participantIdentifier, $this->events); + + $updateRequest = new UpdateParticipantRequest( + participant: $participant, + firstname: $request->input('firstname'), + lastname: $request->input('lastname'), + nickname: $request->input('nickname'), + address_1: $request->input('address_1'), + address_2: $request->input('address_2'), + postcode: $request->input('postcode'), + city: $request->input('city'), + localgroup: $request->input('localgroup'), + birthday: $request->input('birthday'), + email_1: $request->input('email_1'), + phone_1: $request->input('phone_1'), + contact_person: $request->input('contact_person'), + email_2: $request->input('email_2'), + phone_2: $request->input('phone_2'), + arrival: $request->input('arrival'), + departure: $request->input('departure'), + participationType: $request->input('participationType'), + eatingHabit: $request->input('eatingHabit'), + allergies: $request->input('allergies'), + intolerances: $request->input('intolerances'), + medications: $request->input('medications'), + extendedFirstAid: $request->input('extendedFirstAid'), + swimmingPermission: $request->input('swimmingPermission'), + tetanusVaccination: $request->input('tetanusVaccination'), + notes: $request->input('notes'), + amountPaid: $request->input('amountPaid'), + amountExpected: $request->input('amountExpected'), + cocStatus: $request->input('cocStatus'), + ); + + $command = new UpdateParticipantCommand($updateRequest); + $response = $command->execute(); + + $data = [ + 'status' => $response->success ? 'success' : 'error', + 'identifier' => $participant->identifier, + 'participant' => $response->participant->toResource()->toArray($request), + ]; + + if ($response->cocStatus !== null) { + $data['cocChanged'] = true; + $data['coc']['action'] = in_array($response->cocStatus->slug, [ + EfzStatus::EFZ_STATUS_CHECKED_INVALID, + EfzStatus::EFZ_STATUS_NOT_CHECKED]) ? 'inline' : 'none'; + $data['coc']['statusText'] = $response->cocStatus->name; + $data['coc']['class'] = match($response->cocStatus->slug) { + EfzStatus::EFZ_STATUS_CHECKED_INVALID => 'efz-invalid', + EfzStatus::EFZ_STATUS_NOT_CHECKED => 'efz-not-checked', + default => 'efz-valid', + }; + } + + if ($response->amountPaid !== null) { + $amountLeft = clone($response->amountExpected); + $amountLeft->subtractAmount($response->amountPaid); + + $data['amountChanged'] = true; + $data['amount'] = [ + 'paid' => $response->amountPaid->toString(), + 'expected' => $response->amountExpected->toString(), + 'actions' => $amountLeft->getAmount() != 0 ? 'inline' : 'none', + 'class' => $amountLeft->getAmount() != 0 ? 'not-paid' : 'paid', + ]; + } + + return response()->json($data); + } +} diff --git a/app/Domains/Event/Controllers/PaymentReminderController.php b/app/Domains/Event/Controllers/PaymentReminderController.php new file mode 100644 index 0000000..f85b5f5 --- /dev/null +++ b/app/Domains/Event/Controllers/PaymentReminderController.php @@ -0,0 +1,33 @@ +events->getByIdentifier($eventIdentifier, true); + + $sendPaymentReminderMailsRequest = new SendMissingPaymentMailsRequest( + event: $event, + eventParticipants: $this->eventParticipants, + httpRequest: $request + ); + + $sendPaymentReminderMailsCommand = new SendMissingPaymentMailsCommand(request: $sendPaymentReminderMailsRequest); + $sendPaymentReminderResponse = $sendPaymentReminderMailsCommand->execute(); + + return response()->json([ + 'success' => $sendPaymentReminderResponse->success, + 'message' => $sendPaymentReminderResponse->success ? + sprintf('Es wurden %1$s Personen über fehlende Teilnahmebeiträge informiert', $sendPaymentReminderResponse->remindedParticipants) : + 'Beim Senden der Benachrichtigungen ist ein Fehler aufgetreten.', + + ]); + } +} diff --git a/app/Domains/Event/Controllers/SignupController.php b/app/Domains/Event/Controllers/SignupController.php new file mode 100644 index 0000000..67cc0b6 --- /dev/null +++ b/app/Domains/Event/Controllers/SignupController.php @@ -0,0 +1,182 @@ +events->getAvailable(false) as $event) { + $availableEvents[] = $event->toResource()->toArray($request); + }; + + $event = $this->events->getByIdentifier($eventId, false)?->toResource()->toArray($request); + + $participantData = [ + 'firstname' => '', + 'lastname' => '', + ]; + + if (auth()->check()) { + $user = new UserResource(auth()->user())->toArray($request); + + $participantData = [ + 'id' => $user['id'], + 'firstname' => $user['firstname'], + 'lastname' => $user['lastname'], + 'nickname' => $user['nickname'], + 'email' => $user['email'], + 'phone' => $user['phone'], + 'postcode' => $user['postcode'], + 'city' => $user['city'], + 'address_1' => $user['address_1'], + 'address_2' => $user['address_2'], + 'birthday' => $user['birthday'], + 'localGroup' => $user['localGroup'], + 'allergies' => $user['allergies'], + 'intolerances' => $user['intolerances'], + 'eating_habit' => $user['eating_habits'], + 'medications' => $user['medications'], + 'tetanusVaccination' => $user['tetanus_vaccination'], + ]; + + } + + + $inertiaProvider = new InertiaProvider('Event/Signup', [ + 'event' => $event, + 'availableEvents' => $availableEvents, + 'localGroups' => $event['contributingLocalGroups'], + 'participantData' => $participantData, + ]); + return $inertiaProvider->render(); + } + + public function signUp(int $eventId, Request $request) { + $event = $this->events->getById($eventId, false); + $eventResource = $event->toResource(); + $registrationData = $request->input('registration_data'); + $siblingReduction = $registrationData['sibling'] === 'true'; + + + $arrival = \DateTime::createFromFormat('Y-m-d', $registrationData['arrival']); + $departure = \DateTime::createFromFormat('Y-m-d', $registrationData['departure']); + $tetanusVaccination = $registrationData['tetanusVaccination'] ? \DateTime::createFromFormat('Y-m-d', $registrationData['tetanusVaccination']) : null; + + $doubleCheckEventRegistrationProvider = new DoubleCheckEventRegistrationProvider( + $event, + $registrationData['vorname'], + $registrationData['nachname'], + $registrationData['email_1'], + DateTime::createFromFormat('Y-m-d', $registrationData['geburtsdatum'])); + + if ($doubleCheckEventRegistrationProvider->isRegistered()) { + return response()->json(['status' => 'exists']); + } + + $amount = $eventResource->calculateAmount( + $registrationData['participationType'], + $registrationData['beitrag'], + $arrival, + $departure, + $siblingReduction + ); + + $signupRequest = new SignUpRequest( + $event,$registrationData['userId'], + $registrationData['vorname'], + $registrationData['nachname'], + $registrationData['pfadiname'], + $registrationData['participationType'], + Tenant::findOrFail($registrationData['localGroup']), + \DateTime::createFromFormat('Y-m-d', $registrationData['geburtsdatum']), + $registrationData['address1'], + $registrationData['address2'], + $registrationData['plz'], + $registrationData['ort'], + $registrationData['email_1'], + $registrationData['telefon_1'], + $registrationData['email_2'], + $registrationData['telefon_2'], + $registrationData['ansprechpartner'], + $registrationData['allergien'], + $registrationData['intolerances'], + $registrationData['medikamente'], + $tetanusVaccination, + $registrationData['essgewohnheit'], + $registrationData['badeerlaubnis'], + $registrationData['first_aid'], + $registrationData['foto']['socialmedia'], + $registrationData['foto']['print'], + $registrationData['foto']['webseite'], + $registrationData['foto']['partner'], + $registrationData['foto']['intern'], + $arrival, + $departure, + $registrationData['anreise_essen'], + $registrationData['abreise_essen'], + $registrationData['anmerkungen'], + $amount + ); + + $signupCommand = new SignUpCommand($signupRequest); + $signupResponse = $signupCommand->execute(); + + // 4. Addons registrieren + + + $certificateOfConductionCheckRequest = new CertificateOfConductionCheckRequest($signupResponse->participant); + $certificateOfConductionCheckCommand = new CertificateOfConductionCheckCommand($certificateOfConductionCheckRequest); + $certificateOfConductionCheckResponse = $certificateOfConductionCheckCommand->execute(); + + $signupResponse->participant->efz_status = $certificateOfConductionCheckResponse->status; + $signupResponse->participant->save(); + + Mail::to($signupResponse->participant->email_1)->send(new EventSignUpSuccessfullMail( + participant: $signupResponse->participant, + )); + + if ($signupResponse->participant->email_2 !== null) { + Mail::to($signupResponse->participant->email_2)->send(new EventSignUpSuccessfullMail( + participant: $signupResponse->participant, + )); + } + + return response()->json( + [ + 'participant' => $signupResponse->participant->toResource()->toArray($request), + 'status' => 'success', + ] + ); + } + + public function calculateAmount(int $eventId, Request $request, bool $forDisplay = true) : JsonResponse | float { + $event = $this->events->getById($eventId, false)->toResource(); + + $siblingReduction = $request->input('sibling') === 'true'; + + return response()->json(['amount' => + $event->calculateAmount( + $request->input('participationType'), + $request->input('beitrag'), + \DateTime::createFromFormat('Y-m-d', $request->input('arrival')), + \DateTime::createFromFormat('Y-m-d', $request->input('departure')), + $siblingReduction + )->toString() + ]); + } +} diff --git a/app/Domains/Event/Routes/api.php b/app/Domains/Event/Routes/api.php new file mode 100644 index 0000000..115f4f8 --- /dev/null +++ b/app/Domains/Event/Routes/api.php @@ -0,0 +1,64 @@ +group(function () { + Route::middleware(IdentifyTenant::class)->group(function () { + Route::prefix('event')->group(function () { + Route::post('{eventId}/calculate-amount', [SignupController::class, 'calculateAmount']); + Route::post('{eventId}/signup', [SignupController::class, 'signUp']); + + Route::middleware(['auth'])->group(function () { + Route::post('/create', [CreateController::class, 'doCreate']); + + Route::prefix('{eventIdentifier}/mailing')->group(function () { + Route::post('/compose/to-group/{groupType}', ByGroupController::class); + Route::post('/send', SendController::class); + }); + + Route::get('{eventIdentifier}/send-payment-reminder', PaymentReminderController::class); + + Route::prefix('/details/{eventId}') ->group(function () { + Route::get('/summary', [DetailsController::class, 'summary']); + + Route::get('/participants/{listType}', [DetailsController::class, 'listParticipants']); + + + Route::post('/event-managers', [DetailsController::class, 'updateEventManagers']); + Route::post('/participation-fees', [DetailsController::class, 'updateParticipationFees']); + Route::post('/common-settings', [DetailsController::class, 'updateCommonSettings']); + }); + + + Route::prefix('/participant/{participantIdentifier}')->group(function () { + Route::get('/', ParticipantController::class); + Route::get('/ical', ParticipantIcalController::class); + Route::get('/ical-payment', ParticipantIcalForPaymentController::class); + Route::post('/payment-complete', [ParticipantPaymentController::class, 'paymentComplete']); + Route::post('/partial-payment', [ParticipantPaymentController::class, 'partialPaymentComplete']); + Route::post('/mark-coc-existing', [ParticipantController::class, 'markCocExisting']); + Route::post('/signoff', ParticipantSignOffController::class); + Route::post('/re-signon', ParticipantReSignOnController::class); + Route::post('/update', ParticipantUpdateController::class); + + }); + + }); + }); + }); + }); diff --git a/app/Domains/Event/Routes/web.php b/app/Domains/Event/Routes/web.php new file mode 100644 index 0000000..9b6bbe3 --- /dev/null +++ b/app/Domains/Event/Routes/web.php @@ -0,0 +1,25 @@ +group(function () { + Route::prefix('event')->group(function () { + Route::get('/available-events', AvailableEventsController::class); + + + Route::get('/{eventId}', SignupController::class); + Route::get('/{eventId}/signup', SignupController::class); + + Route::middleware(['auth'])->group(function () { + Route::get('/new', CreateController::class); + Route::get('/details/{eventId}', DetailsController::class); + Route::get('/details/{eventId}/pdf/{listType}', [DetailsController::class, 'downloadPdfList']); + Route::get('/details/{eventId}/csv/{listType}', [DetailsController::class, 'downloadCsvList']); + }); + }); +}); diff --git a/app/Domains/Event/Views/Create.vue b/app/Domains/Event/Views/Create.vue new file mode 100644 index 0000000..2c3c359 --- /dev/null +++ b/app/Domains/Event/Views/Create.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/app/Domains/Event/Views/Details.vue b/app/Domains/Event/Views/Details.vue new file mode 100644 index 0000000..8fdf945 --- /dev/null +++ b/app/Domains/Event/Views/Details.vue @@ -0,0 +1,60 @@ + + + diff --git a/app/Domains/Event/Views/ListAvailable.vue b/app/Domains/Event/Views/ListAvailable.vue new file mode 100644 index 0000000..2fc351f --- /dev/null +++ b/app/Domains/Event/Views/ListAvailable.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/app/Domains/Event/Views/Partials/AvailableEvents.vue b/app/Domains/Event/Views/Partials/AvailableEvents.vue new file mode 100644 index 0000000..7adc941 --- /dev/null +++ b/app/Domains/Event/Views/Partials/AvailableEvents.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/app/Domains/Event/Views/Partials/CommonSettings.vue b/app/Domains/Event/Views/Partials/CommonSettings.vue new file mode 100644 index 0000000..738a2fa --- /dev/null +++ b/app/Domains/Event/Views/Partials/CommonSettings.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/app/Domains/Event/Views/Partials/EventManagement.vue b/app/Domains/Event/Views/Partials/EventManagement.vue new file mode 100644 index 0000000..2be4463 --- /dev/null +++ b/app/Domains/Event/Views/Partials/EventManagement.vue @@ -0,0 +1,63 @@ + + + + diff --git a/app/Domains/Event/Views/Partials/MailCompose.vue b/app/Domains/Event/Views/Partials/MailCompose.vue new file mode 100644 index 0000000..cd8a2f0 --- /dev/null +++ b/app/Domains/Event/Views/Partials/MailCompose.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/app/Domains/Event/Views/Partials/Overview.vue b/app/Domains/Event/Views/Partials/Overview.vue new file mode 100644 index 0000000..dfb34ca --- /dev/null +++ b/app/Domains/Event/Views/Partials/Overview.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/app/Domains/Event/Views/Partials/ParticipantData.vue b/app/Domains/Event/Views/Partials/ParticipantData.vue new file mode 100644 index 0000000..38d90e9 --- /dev/null +++ b/app/Domains/Event/Views/Partials/ParticipantData.vue @@ -0,0 +1,413 @@ + + + + + diff --git a/app/Domains/Event/Views/Partials/ParticipantsList.vue b/app/Domains/Event/Views/Partials/ParticipantsList.vue new file mode 100644 index 0000000..85ac8ce --- /dev/null +++ b/app/Domains/Event/Views/Partials/ParticipantsList.vue @@ -0,0 +1,538 @@ + + + + + diff --git a/app/Domains/Event/Views/Partials/ParticipationFees.vue b/app/Domains/Event/Views/Partials/ParticipationFees.vue new file mode 100644 index 0000000..21cd001 --- /dev/null +++ b/app/Domains/Event/Views/Partials/ParticipationFees.vue @@ -0,0 +1,295 @@ + + + diff --git a/app/Domains/Event/Views/Partials/ParticipationSummary.vue b/app/Domains/Event/Views/Partials/ParticipationSummary.vue new file mode 100644 index 0000000..fedac72 --- /dev/null +++ b/app/Domains/Event/Views/Partials/ParticipationSummary.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue b/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue new file mode 100644 index 0000000..b0c053c --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/after-submit/SubmitAlreadyExists.vue b/app/Domains/Event/Views/Partials/SignUpForm/after-submit/SubmitAlreadyExists.vue new file mode 100644 index 0000000..f992f37 --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/after-submit/SubmitAlreadyExists.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/after-submit/SubmitSuccess.vue b/app/Domains/Event/Views/Partials/SignUpForm/after-submit/SubmitSuccess.vue new file mode 100644 index 0000000..3f5e033 --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/after-submit/SubmitSuccess.vue @@ -0,0 +1,63 @@ + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/composables/useSignupForm.js b/app/Domains/Event/Views/Partials/SignUpForm/composables/useSignupForm.js new file mode 100644 index 0000000..a6ae1d5 --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/composables/useSignupForm.js @@ -0,0 +1,99 @@ +import { ref, reactive, computed } from 'vue' +import axios from 'axios' + +export function useSignupForm(event, participantData) { + const currentStep = ref(1) + const submitting = ref(false) + const summaryLoading = ref(false) + const submitResult = ref(null) // null | { type: 'success'|'exists', data: {} } + + const selectedAddons = reactive({}) + + const formData = reactive({ + eatingHabit: 'EATING_HABIT_VEGAN', + userId: participantData.id, + eventId: event.id, + vorname: participantData.firstname ?? '', + nachname: participantData.lastname ?? '', + pfadiname: participantData.nickname ?? '', + localGroup: participantData.localGroup ?? '-1', + geburtsdatum: participantData.birthday ?? '', + address1: participantData.address_1 ?? '', + address2: participantData.address_2 ?? '', + plz: participantData.postcode ?? '', + ort: participantData.city ?? '', + telefon_1: participantData.phone ?? '', + email_1: participantData.email ?? '', + participationType: '', + ansprechpartner: '', + telefon_2: '', + email_2: '', + badeerlaubnis: '-1', + sibling: '-1', + first_aid: '-1', + participant_group: '', + beitrag: 'regular', + arrival: event.arrivalDefault ?? '', + departure: event.departureDefault ?? '', + anreise_essen: '1', + abreise_essen: '2', + foto: { socialmedia: false, print: false, webseite: false, partner: false, intern: false }, + allergien: participantData.allergies ?? '', + intolerances: participantData.intolerances ?? '', + medikamente: participantData.medications ?? '', + tetanusVaccination: participantData.tetanusVaccination ?? '', + essgewohnheit: 'vegetarian', + anmerkungen: '', + summary_information_correct: false, + summary_accept_terms: false, + legal_accepted: false, + payment: false, + }) + + const summaryAmount = ref('') + + const goToStep = async (step) => { + if (step === 9) { + summaryLoading.value = true + summaryAmount.value = '' + try { + const res = await axios.post('/api/v1/event/' + event.id + '/calculate-amount', { + arrival: formData.arrival, + departure: formData.departure, + event_id: event.id, + participation_group: formData.participant_group, + selected_amount: formData.beitrag, + addons: selectedAddons, + participationType: formData.participationType, + beitrag: formData.beitrag, + sibling: formData.sibling, + }) + summaryAmount.value = res.data.amount + } finally { + summaryLoading.value = false + } + } + currentStep.value = step + } + + const submit = async () => { + if (!formData.summary_information_correct || !formData.summary_accept_terms || !formData.legal_accepted || !formData.payment) { + return + } + submitting.value = true + try { + const res = await axios.post('/api/v1/event/'+ event.id + '/signup', { + addons: selectedAddons, + registration_data: { ...formData }, + }) + submitResult.value = { + status: res.data.status, + participant: res.data.participant, + } + } finally { + submitting.value = false + } + } + + return { currentStep, goToStep, formData, selectedAddons, submit, submitting, submitResult, summaryLoading, summaryAmount } +} diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepAddons.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepAddons.vue new file mode 100644 index 0000000..ad1b60c --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepAddons.vue @@ -0,0 +1,45 @@ + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepAge.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepAge.vue new file mode 100644 index 0000000..7ff297a --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepAge.vue @@ -0,0 +1,103 @@ + + + + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepAllergies.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepAllergies.vue new file mode 100644 index 0000000..a9be072 --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepAllergies.vue @@ -0,0 +1,52 @@ + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepArrival.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepArrival.vue new file mode 100644 index 0000000..6db1dad --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepArrival.vue @@ -0,0 +1,81 @@ + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepContactPerson.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepContactPerson.vue new file mode 100644 index 0000000..5b6367c --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepContactPerson.vue @@ -0,0 +1,144 @@ + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepPersonalData.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepPersonalData.vue new file mode 100644 index 0000000..257e5fa --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepPersonalData.vue @@ -0,0 +1,124 @@ + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepPhotoPermissions.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepPhotoPermissions.vue new file mode 100644 index 0000000..43e77e6 --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepPhotoPermissions.vue @@ -0,0 +1,33 @@ + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepRegistrationMode.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepRegistrationMode.vue new file mode 100644 index 0000000..ca46946 --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepRegistrationMode.vue @@ -0,0 +1,133 @@ + + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue new file mode 100644 index 0000000..fb2caae --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue @@ -0,0 +1,151 @@ + + + diff --git a/app/Domains/Event/Views/Signup.vue b/app/Domains/Event/Views/Signup.vue new file mode 100644 index 0000000..f300baa --- /dev/null +++ b/app/Domains/Event/Views/Signup.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusCommand.php b/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusCommand.php new file mode 100644 index 0000000..d7f59b7 --- /dev/null +++ b/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusCommand.php @@ -0,0 +1,78 @@ +request = $request; + } + + public function execute() : ChangeStatusResponse { + $response = new ChangeStatusResponse(); + + switch ($this->request->status) { + case InvoiceStatus::INVOICE_STATUS_APPROVED: + $this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_APPROVED; + $this->request->invoice->approved_by = auth()->user()->id; + $this->request->invoice->approved_at = now(); + + if ($this->request->invoice->contact_email !== null) { + Mail::to($this->request->invoice->contact_email)->send(new InvoiceMailsApprovedInvoiceMail( + invoice: $this->request->invoice, + costUnit: $this->request->invoice->costUnit()->first(), + )); + } + break; + + case InvoiceStatus::INVOICE_STATUS_DENIED: + $this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_DENIED; + $this->request->invoice->denied_by = auth()->user()->id; + $this->request->invoice->denied_at = now(); + $this->request->invoice->denied_reason = $this->request->comment; + if ($this->request->invoice->contact_email !== null) { + Mail::to($this->request->invoice->contact_email)->send(new InvoiceMailsDeniedInvoiceMail( + invoice: $this->request->invoice, + costUnit: $this->request->invoice->costUnit()->first(), + )); + } + break; + case InvoiceStatus::INVOICE_STATUS_NEW: + $this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_NEW; + $this->request->invoice->approved_by = null; + $this->request->invoice->approved_at = null; + $this->request->invoice->denied_by = null; + $this->request->invoice->denied_at = null; + $this->request->invoice->comment = $this->request->invoice->denied_reason; + $this->request->invoice->denied_reason = null; + if ($this->request->invoice->contact_email !== null) { + Mail::to($this->request->invoice->contact_email)->send(new InvoiceMailsReopenedInvoiceMail( + invoice: $this->request->invoice, + costUnit: $this->request->invoice->costUnit()->first(), + )); + } + break; + case InvoiceStatus::INVOICE_STATUS_DELETED: + $this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_DELETED; + break; + case InvoiceStatus::INVOICE_STATUS_EXPORTED: + $this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_EXPORTED; + $this->request->invoice->upload_required = true; + break; + } + + if ($this->request->invoice->save()) { + $response->success = true; + } + + return $response; + } +} diff --git a/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusRequest.php b/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusRequest.php new file mode 100644 index 0000000..5c2b4ae --- /dev/null +++ b/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusRequest.php @@ -0,0 +1,17 @@ +invoice = $invoice; + $this->status = $status; + $this->comment = $comment; + } +} diff --git a/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusResponse.php b/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusResponse.php new file mode 100644 index 0000000..c7141db --- /dev/null +++ b/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusResponse.php @@ -0,0 +1,12 @@ +success = false; + } +} diff --git a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php new file mode 100644 index 0000000..d9f83c2 --- /dev/null +++ b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php @@ -0,0 +1,84 @@ +request = $request; + } + + public function execute() : CreateInvoiceResponse { + $response = new CreateInvoiceResponse(); + + if ($this->request->accountIban === 'undefined') { + $this->request->accountIban = null; + } + + $invoice = Invoice::create([ + 'tenant' => app('tenant')->slug, + 'cost_unit_id' => $this->request->costUnit->id, + 'invoice_number' => $this->generateInvoiceNumber(), + 'status' => InvoiceStatus::INVOICE_STATUS_NEW, + 'type' => $this->request->invoiceType, + 'type_other' => $this->request->invoiceTypeExtended, + 'donation' => $this->request->isDonation, + 'user_id' => $this->request->userId, + 'contact_name' => $this->request->contactName, + 'contact_email' => $this->request->contactEmail, + 'contact_phone' => $this->request->contactPhone, + 'contact_bank_owner' => $this->request->accountOwner, + 'contact_bank_iban' => $this->request->accountIban, + 'amount' => $this->request->totalAmount, + 'distance' => $this->request->distance, + 'travel_direction' => $this->request->travelRoute, + 'travel_reason' => $this->request->travelReason, + 'passengers' => $this->request->passengers, + 'transportation' => $this->request->transportations, + 'document_filename' => $this->request->receiptFile !== null ? $this->request->receiptFile->fullPath : null, + ]); + + if ($invoice !== null) { + $response->success = true; + $response->invoice = $invoice; + } + + if ($this->request->costUnit->mail_on_new) { + $recipients = [app('tenant')->email_finance]; + + foreach ($this->request->costUnit->treasurers()->get() as $treasurer) { + if (!in_array($treasurer->email, $recipients)) { + $recipients[] = $treasurer->email; + } + } + + foreach ($recipients as $recipient) { + Mail::to($recipient)->send(new InvoiceMailsNewInvoiceMail( + invoice: $invoice, + costUnit: $this->request->costUnit, + )); + } + } + + return $response; + } + + + private function generateInvoiceNumber() : string { + $lastInvoiceNumber = Invoice::query() + ->where('tenant', app('tenant')->slug) + ->whereYear('created_at', date('Y')) + ->count(); + + $invoiceNumber = $lastInvoiceNumber + 1; + $invoiceNumber = str_pad($invoiceNumber, 4, '0', STR_PAD_LEFT); + return sprintf('%1$s-%2$s', date('Y'), $invoiceNumber); + } +} diff --git a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php new file mode 100644 index 0000000..e27eadf --- /dev/null +++ b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php @@ -0,0 +1,72 @@ +costUnit = $costUnit; + $this->contactName = $contactName; + $this->invoiceType = $invoiceType; + $this->invoiceTypeExtended = $invoiceTypeExtended; + $this->travelRoute = $travelRoute; + $this->distance = $distance; + $this->passengers = $passengers; + $this->transportations = $transportations; + $this->receiptFile = $receiptFile; + $this->contactEmail = $contactEmail; + $this->contactPhone = $contactPhone; + $this->accountOwner = $accountOwner; + $this->accountIban = $accountIban; + $this->totalAmount = $totalAmount; + $this->isDonation = $isDonation; + $this->userId = $userId; + $this->travelReason = $travelReason; + + if ($accountIban === 'undefined') { + $this->accountIban = null; + $this->accountOwner = null; + } + } + +} diff --git a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceResponse.php b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceResponse.php new file mode 100644 index 0000000..357fed9 --- /dev/null +++ b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceResponse.php @@ -0,0 +1,15 @@ +success = false; + $this->invoice = null; + } +} diff --git a/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptCommand.php b/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptCommand.php new file mode 100644 index 0000000..d5342bb --- /dev/null +++ b/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptCommand.php @@ -0,0 +1,206 @@ +request = $request; + $this->tempDirectory = Tenant::getTempDirectory(); + } + + public function execute() : CreateInvoiceReceiptResponse { + $response = new CreateInvoiceReceiptResponse(); + + $filename = $this->tempDirectory . $this->request->invoice->invoice_number . '.pdf'; + if (!Storage::exists($this->tempDirectory)) { + Storage::makeDirectory(Tenant::getTempDirectory() . '/' . $this->tempDirectory); + } + + $receipt = $this->request->invoice->document_filename; + if ($receipt === null) { + Storage::put( + $filename, + $this->createPdf( $this->createHtml(), 'portrait', $filename, false ) + ); + + $response->fileName = $filename; + return $response; + } + + $token = (string)rand(1000000000, 9999999999); + $pdf_data = $this->createPdf( $this->createHtml(), 'portrait', $filename, false ); + + $tmpFileName = $this->tempDirectory . 'tmp-' . $token . '.pdf'; + Storage::put($tmpFileName, $pdf_data); + + try { + $merger = new PdfMergeProvider(); + + $merger + ->add( storage_path('app/private/' . $tmpFileName )) + ->add(storage_path('app/private/' . $receipt)) + ->merge(storage_path('app/private/' .$filename) ); + + $response->fileName = $filename; + } catch ( \Exception $e ) { + $zip = new ZipArchive(); + $zip->open( + storage_path('app/private/' .$filename) . '.zip', + ZipArchive::CREATE | ZipArchive::OVERWRITE + ); + + $zip->addFile(storage_path('app/private/' . $tmpFileName ), 'antrag.pdf'); + $zip->addFile(storage_path('app/private/' . $receipt), 'beleg.pdf'); + + $zip->close(); + $response->fileName = $filename . '.zip'; + + + } finally { + Storage::delete($tmpFileName); + } + return $response; + } + + + + private function createHtml() : string { + + + $invoiceReadable = new InvoiceResource($this->request->invoice)->toArray(); + + + $travelPartTemplate = <<Reiseweg:%1\$s + Grund der Reise:%6\$s + Gesamtlänge der Strecke:%2\$s km x %3\$s / km + Materialtransport:%4\$s + Mitfahrende im PKW:%5\$s +HTML; + + $flatTravelPart = sprintf( + $travelPartTemplate, + $invoiceReadable['travelDirection'] , + $invoiceReadable['distance'], + $invoiceReadable['distanceAllowance'], + $invoiceReadable['transportation'], + $invoiceReadable['passengers'], + $invoiceReadable['travelReason'] , + ); + + $invoiceTravelPart = 'Kosten für ÖPNV:' . $invoiceReadable['amount'] . '';; + $expensePart = 'Auslagenerstattung:' . $invoiceReadable['amount'] . ''; + + $content = << + +

Abrechnungstyp %1\$s



+ + + + + + + + + + + %9\$s + + + + + + %11\$s + + + +
Abrechnungsnummer:%2\$s
Name:%3\$s
E-Mail:%4\$s
Telefon:%5\$s
Name der Kostenstelle:%6\$s
Zahlungsgrund:%7\$s
Wird der Betrag gespendet:%8\$s
+ Gesamtbetrag: + + %10\$s +


Beleg digital eingereicht am:%12\$s
Beleg akzeptiert am:%13\$s
Beleg akzeptiert von:%14\$s
+ %15\$s + + +HTML; + + switch ($this->request->invoice->type) { + case InvoiceType::INVOICE_TYPE_TRAVELLING: + $paymentType = $this->request->invoice->distance !== null ? $flatTravelPart : $invoiceTravelPart; + break; + default: + $paymentType = $expensePart; + } + + if ($this->request->invoice->donation) { + $paymentInformation = '' . PageText::where('name', 'CONFIRMATION_DONATE')->first()->content . ''; + } else { + if ($this->request->invoice->contact_bank_iban === null) { + $paymentInformation = ''; + } else { + $paymentInformationTemplate = <<%1\$s + Kontoinhaber*in:%2\$s + INAN:%3\$s +

+HTML; + + $paymentInformation = sprintf( + $paymentInformationTemplate, + PageText::where('name', 'CONFIRMATION_PAYMENT')->first()->content, + $invoiceReadable['accountOwner'], + $invoiceReadable['accountIban'] + ); + } + } + + $changes = $this->request->invoice->changes !== null ? '

' . $this->request->invoice->changes . '

' : ''; + + + return sprintf( + $content, + $invoiceReadable['invoiceTypeShort'], + $invoiceReadable['invoiceNumber'], + $invoiceReadable['contactName'], + $invoiceReadable['contactEmail'], + $invoiceReadable['contactPhone'], + $invoiceReadable['costUnitName'], + $invoiceReadable['invoiceType'], + $invoiceReadable['donationText'], + $paymentType, + $invoiceReadable['amount'], + $paymentInformation, + $invoiceReadable['createdAt'], + $invoiceReadable['approvedAt'], + $invoiceReadable['approvedBy'], + $changes + ); + } + + private function createPdf( string $htmlfile, string $orientation, string $filename, bool $download = true ) { + + $dompdf = new Dompdf(); + $dompdf->loadHtml( $htmlfile, 'UTF-8' ); + $dompdf->setPaper( 'A4', $orientation ); + + $dompdf->render(); + if ( ! $download ) { + return $dompdf->output(); + } + $dompdf->stream( $filename ); + } + +} diff --git a/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptRequest.php b/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptRequest.php new file mode 100644 index 0000000..ae7c6fa --- /dev/null +++ b/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptRequest.php @@ -0,0 +1,13 @@ +invoice = $invoice; + } +} diff --git a/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptResponse.php b/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptResponse.php new file mode 100644 index 0000000..208bdeb --- /dev/null +++ b/app/Domains/Invoice/Actions/CreateInvoiceReceipt/CreateInvoiceReceiptResponse.php @@ -0,0 +1,11 @@ +fileName = ''; + } +} diff --git a/app/Domains/Invoice/Actions/UpdateInvoice/UpdateInvoiceCommand.php b/app/Domains/Invoice/Actions/UpdateInvoice/UpdateInvoiceCommand.php new file mode 100644 index 0000000..d066221 --- /dev/null +++ b/app/Domains/Invoice/Actions/UpdateInvoice/UpdateInvoiceCommand.php @@ -0,0 +1,48 @@ +request = $request; + } + + public function execute() : UpdateInvoiceResponse { + $response = new UpdateInvoiceResponse(); + $changes = $this->request->invoice->changes ?? ''; + + if ($this->request->invoice->amount !== $this->request->amount->getAmount()) { + $changes .= 'Betrag geändert von ' . Amount::fromString($this->request->invoice->amount)->toString() . ' auf ' . Amount::fromString($this->request->amount->getAmount())->toString() . '.
'; + $this->request->invoice->amount = $this->request->amount->getAmount(); + } + + if ($this->request->invoice->invoiceType()->slug !== $this->request->invoiceType->slug) { + $changes .= 'Abrechnungstyp geändert von ' . $this->request->invoice->invoiceType()->name . ' auf ' . $this->request->invoiceType->name . '.
'; + $this->request->invoice->type = $this->request->invoiceType->slug; + } + + if ($this->request->invoice->costUnit()->first()->id !== $this->request->costUnit->id) { + $changes .= 'Kostenstelle geändert von ' . $this->request->invoice->costUnit()->first()->name . ' auf ' . $this->request->costUnit->name . '.
'; + $this->request->invoice->cost_unit_id = $this->request->costUnit->id; + } + + + $this->request->invoice->comment = $this->request->comment; + $this->request->invoice->changes = $changes; + + $this->request->invoice->save(); + + $request = new ChangeStatusRequest($this->request->invoice, InvoiceStatus::INVOICE_STATUS_APPROVED); + $changeStatusCommand = new ChangeStatusCommand($request); + $changeStatusCommand->execute(); + + return $response; + } +} diff --git a/app/Domains/Invoice/Actions/UpdateInvoice/UpdateInvoiceRequest.php b/app/Domains/Invoice/Actions/UpdateInvoice/UpdateInvoiceRequest.php new file mode 100644 index 0000000..d76e477 --- /dev/null +++ b/app/Domains/Invoice/Actions/UpdateInvoice/UpdateInvoiceRequest.php @@ -0,0 +1,24 @@ +comment = $comment; + $this->invoiceType = $invoiceType; + $this->costUnit = $costUnit; + $this->invoice = $invoice; + $this->amount = $amount; + } +} diff --git a/app/Domains/Invoice/Actions/UpdateInvoice/UpdateInvoiceResponse.php b/app/Domains/Invoice/Actions/UpdateInvoice/UpdateInvoiceResponse.php new file mode 100644 index 0000000..e05bd83 --- /dev/null +++ b/app/Domains/Invoice/Actions/UpdateInvoice/UpdateInvoiceResponse.php @@ -0,0 +1,15 @@ +success = false; + $this->invoice = null; + } +} diff --git a/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceCommand.php b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceCommand.php new file mode 100644 index 0000000..044e57a --- /dev/null +++ b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceCommand.php @@ -0,0 +1,50 @@ +request = $request; + } + + public function execute() : UploadInvoiceResponse { + $uploadResponse = new UploadInvoiceResponse(); + + $uploadDir = sprintf( + '%1$s%2$s/%3$s', + WebDavProvider::INVOICE_PREFIX, + app('tenant')->url, + $this->request->invoice->costUnit()->first()->name + ); + + $webDavProvider = new WebDavProvider($uploadDir); + + $createInvoiceReceiptRequest = new CreateInvoiceReceiptRequest($this->request->invoice); + $createInvoiceReceiptCommand = new CreateInvoiceReceiptCommand($createInvoiceReceiptRequest); + $response = $createInvoiceReceiptCommand->execute(); + if ('' === $response->fileName) { + app('taskLogger')->error('PDF oder ZIP zur Abrechnung konnte nicht erstellt werden.'); + return $uploadResponse; + } + + if ($webDavProvider->uploadFile($response->fileName)) { + $this->request->invoice->upload_required = false; + $this->request->invoice->save(); + $uploadResponse->success = true; + } else { + app('taskLogger')->error('PDF oder ZIP zur Abrechnung konnte nicht hochgeladen werden.'); + } + + if (Storage::exists($response->fileName)) { + Storage::delete($response->fileName); + } + + return $uploadResponse; + } +} diff --git a/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceRequest.php b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceRequest.php new file mode 100644 index 0000000..5a0de48 --- /dev/null +++ b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceRequest.php @@ -0,0 +1,13 @@ +invoice = $invoice; + } +} diff --git a/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceResponse.php b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceResponse.php new file mode 100644 index 0000000..a32c685 --- /dev/null +++ b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceResponse.php @@ -0,0 +1,11 @@ +success = false; + } +} diff --git a/app/Domains/Invoice/Controllers/ChangeStateController.php b/app/Domains/Invoice/Controllers/ChangeStateController.php new file mode 100644 index 0000000..40b7c61 --- /dev/null +++ b/app/Domains/Invoice/Controllers/ChangeStateController.php @@ -0,0 +1,27 @@ +invoices->getAsTreasurer($invoiceId); + if ($invoice === null) { + return response()->json([]); + } + + $comment = request()->get('reason') ?? null; + $changeStatusRequest = new ChangeStatusRequest($invoice, $newState, $comment); + $changeStatusCommand = new ChangeStatusCommand($changeStatusRequest); + if ($changeStatusCommand->execute()->success) { + return response()->json(['status' => 'success']); + } + + return response()->json([]); + } +} diff --git a/app/Domains/Invoice/Controllers/EditController.php b/app/Domains/Invoice/Controllers/EditController.php new file mode 100644 index 0000000..4fc6ce2 --- /dev/null +++ b/app/Domains/Invoice/Controllers/EditController.php @@ -0,0 +1,136 @@ +invoices->getAsTreasurer($invoiceId); + if ($invoice === null) { + return response()->json([]); + } + + $receiptfile = null; + if ($invoice->document_filename !== null) { + $receiptfile = new InvoiceFile(); + $receiptfile->filename = $invoice->document_filename; + $receiptfile->fullPath = $invoice->document_filename; + } + $createInvoiceRequest = new CreateInvoiceRequest( + $invoice->costUnit()->first(), + $invoice->contact_name, + $invoice->type, + $invoice->amount, + $receiptfile, + $invoice->donation, + $invoice->user_id, + $invoice->contact_email, + $invoice->contact_phone, + $invoice->contact_bank_owner, + $invoice->contact_bank_iban, + $invoice->type_other, + $invoice->travel_direction, + $invoice->distance, + $invoice->passengers, + $invoice->transportation + ); + + $invoiceCreationCommand = new CreateInvoiceCommand($createInvoiceRequest); + $newInvoice = $invoiceCreationCommand->execute()->invoice; + + $invoiceDenyRequest = new ChangeStatusRequest($invoice,InvoiceStatus::INVOICE_STATUS_DENIED, 'Abrechnungskorrektur in Rechnungsnummer #' . $newInvoice->invoice_number . ' erstellt.'); + $invoiceDenyCommand = new ChangeStatusCommand($invoiceDenyRequest); + $invoiceDenyCommand->execute(); + + + $runningJobs = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_RUNNING_JOB); + $currentEvents = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_EVENT); + + return response()->json([ + 'invoice' => new InvoiceResource($invoice)->toArray(), + 'status' => 'success', + 'costUnits' => array_merge($runningJobs, $currentEvents), + ]); + } + + public function updateInvoice(int $invoiceId, Request $request) : JsonResponse { + $invoice = $this->invoices->getAsTreasurer($invoiceId); + if ($invoice === null) { + return response()->json([]); + } + + $modifyData = $request->get('invoiceData'); + + $newAmount = Amount::fromString($modifyData['amount']); + $amountLeft = Amount::fromString($invoice->amount); + $amountLeft->subtractAmount($newAmount); + + $newCostUnit = $this->costUnits->getById($modifyData['cost_unit'],true); + $invoiceType = InvoiceType::where('slug', $modifyData['type_internal'])->first(); + + + $updateInvoiceRequest = new UpdateInvoiceRequest( + $invoice, + $modifyData['reason_of_correction'] ?? 'Abrechnungskorrektur', + $invoiceType, + $newCostUnit, + $newAmount + ); + $updateInvoiceCommand = new UpdateInvoiceCommand($updateInvoiceRequest); + $updateInvoiceCommand->execute(); + + + $newInvoice = null; + if (isset($modifyData['duplicate']) && $modifyData['duplicate'] === true) { + $receiptfile = null; + if ($invoice->document_filename !== null) { + $receiptfile = new InvoiceFile(); + $receiptfile->filename = $invoice->document_filename; + $receiptfile->fullPath = $invoice->document_filename; + } + $createInvoiceRequest = new CreateInvoiceRequest( + $invoice->costUnit()->first(), + $invoice->contact_name, + $invoice->type, + $amountLeft->getAmount(), + $receiptfile, + $invoice->donation, + $invoice->user_id, + $invoice->contact_email, + $invoice->contact_phone, + $invoice->contact_bank_owner, + $invoice->contact_bank_iban, + $invoice->type_other, + $invoice->travel_direction, + $invoice->distance, + $invoice->passengers, + $invoice->transportation + ); + + $invoiceCreationCommand = new CreateInvoiceCommand($createInvoiceRequest); + $newInvoice = $invoiceCreationCommand->execute()->invoice; + } + + $useInvoice = $newInvoice ?? $invoice; + $do_copy = $newInvoice !== null ? true : false; + return response()->json([ + 'invoice' => new InvoiceResource($useInvoice)->toArray(), + 'do_copy' => $do_copy, + ]); + } +} diff --git a/app/Domains/Invoice/Controllers/ListMyInvoicesController.php b/app/Domains/Invoice/Controllers/ListMyInvoicesController.php new file mode 100644 index 0000000..b4742e9 --- /dev/null +++ b/app/Domains/Invoice/Controllers/ListMyInvoicesController.php @@ -0,0 +1,61 @@ +invoices->getMyInvoicesByStatus($invoiceStatus); + + $subTabIndex = 0; + switch ($invoiceStatus) { + case InvoiceStatus::INVOICE_STATUS_NEW: + $subTabIndex = 0; + break; + case InvoiceStatus::INVOICE_STATUS_APPROVED: + $subTabIndex = 1; + break; + case InvoiceStatus::INVOICE_STATUS_DENIED: + $subTabIndex = 2; + break; + + } + + + $inertiaProvider = new InertiaProvider('Invoice/ListMyInvoices', [ + 'invoices' => $invoices, + 'endpoint' => $invoiceStatus, + 'currentStatus' => $subTabIndex, + ]); + return $inertiaProvider->render(); + } + + public function getMyInvoicesByStatus(string $invoiceStatus) : JsonResponse { + $invoices = $this->invoices->getMyInvoicesByStatus($invoiceStatus); + + $title = ''; + switch ($invoiceStatus) { + case InvoiceStatus::INVOICE_STATUS_NEW: + $title = 'Neue Abrechnungen'; + break; + case InvoiceStatus::INVOICE_STATUS_APPROVED: + $title = 'Freigegebene Abrechnungen, nicht exportierte Abrechnungen'; + break; + case InvoiceStatus::INVOICE_STATUS_DENIED: + $title = 'Abgelehnte Abrechnungen'; + break; + + } + + return response()->json([ + 'title' => $title, + 'endpoint' => $invoiceStatus, + 'invoices' => $invoices, + ]); + } + +} diff --git a/app/Domains/Invoice/Controllers/NewInvoiceController.php b/app/Domains/Invoice/Controllers/NewInvoiceController.php new file mode 100644 index 0000000..25ad83d --- /dev/null +++ b/app/Domains/Invoice/Controllers/NewInvoiceController.php @@ -0,0 +1,135 @@ +users->getCurrentUserDetails(); + + $runningJobs = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_RUNNING_JOB); + $currentEvents = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_EVENT); + + + $inertiaProvider = new InertiaProvider('Invoice/NewInvoice', [ + 'userName' => $userData['userName'], + 'userEmail' => $userData['userEmail'], + 'userTelephone' => $userData['userTelephone'], + 'userAccountOwner' => $userData['userAccountOwner'], + 'userAccountIban' => $userData['userAccountIban'], + 'runningJobs' => $runningJobs, + 'currentEvents' => $currentEvents, + ]); + return $inertiaProvider->render(); + } + + public function saveInvoice(Request $request, int $costUnitId, string $invoiceType) : JsonResponse { + $costUnit = $this->costUnits->getById($costUnitId, true); + if (null === $costUnit) { + return response()->json([ + 'status' => 'error', + 'message' => 'Beim Speichern ist ein Fehler aufgetreten. Bitte starte den Vorgang erneut.' + ]); + } + + $uploadedFile = null; + if (null !== $request->file('receipt')) { + $validation = sprintf('%1$s|%2$s|max:%3$s', + 'required', + 'mimes:pdf', + env('MAX_INVOICE_FILE_SIZE', 16)*10 + ); + + $request->validate([ + 'receipt' => $validation + ]); + + $uploadFileProvider = new UploadFileProvider($request->file('receipt'), $costUnit); + $uploadedFile = $uploadFileProvider->saveUploadedFile(); + } + + switch ($invoiceType) { + case InvoiceType::INVOICE_TYPE_TRAVELLING: + + if ($uploadedFile !== null) { + $amount = Amount::fromString($request->input('amount'))->getAmount(); + $distance = null; + } else { + $distance = Amount::fromString($request->input('amount'))->getRoundedAmount(); + $amount = $distance * $costUnit->distance_allowance; + + } + + $createInvoiceRequest = new CreateInvoiceRequest( + $costUnit, + $request->input('name'), + InvoiceType::INVOICE_TYPE_TRAVELLING, + $amount, + $uploadedFile, + 'donation' === $request->input('decision') ? true : false, + $this->users->getCurrentUserDetails()['userId'], + $request->input('email'), + $request->input('telephone'), + $request->input('accountOwner'), + $request->input('accountIban'), + null, + $request->input('otherText'), + $distance, + $request->input('havePassengers'), + $request->input('materialTransportation'), + $request->input('travelReason'), + ); + + break; + + default: + $createInvoiceRequest = new CreateInvoiceRequest( + $costUnit, + $request->input('name'), + $invoiceType, + Amount::fromString($request->input('amount'))->getAmount(), + $uploadedFile, + 'donation' === $request->input('decision') ? true : false, + $this->users->getCurrentUserDetails()['userId'], + $request->input('email'), + $request->input('telephone'), + $request->input('accountOwner'), + $request->input('accountIban'), + $request->input('otherText'), + null, + null, + $request->input('havePassengers'), + $request->input('materialTransportation'), + null + ); + + break; + } + + $command = new CreateInvoiceCommand($createInvoiceRequest); + $response = $command->execute(); + if ($response->success) { + new FlashMessageProvider( + 'Die Abrechnung wurde erfolgreich angelegt.' . PHP_EOL . PHP_EOL . 'Sollten wir Rückfragen haben, melden wir uns bei dir', + 'success' + ); + + return response()->json([ + 'status' => 'success', + 'message' => 'Alright' + ]); + } + } +} diff --git a/app/Domains/Invoice/Controllers/ShowInvoiceController.php b/app/Domains/Invoice/Controllers/ShowInvoiceController.php new file mode 100644 index 0000000..7865118 --- /dev/null +++ b/app/Domains/Invoice/Controllers/ShowInvoiceController.php @@ -0,0 +1,55 @@ +invoices->getAsTreasurer($invoiceId); + + $runningJobs = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_RUNNING_JOB); + $currentEvents = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_EVENT); + + return response()->json([ + 'invoice' => new InvoiceResource($invoice)->toArray(), + 'costUnits' => array_merge($runningJobs, $currentEvents), + ]); + } + + public function showReceipt(int $invoiceId): BinaryFileResponse + { + $invoice = $this->invoices->getAsTreasurer($invoiceId); + if (null === $invoice) { + abort(404, 'Datei nicht gefunden'); + } + + if (null === $invoice->document_filename) { + abort(404, 'Datei nicht gefunden'); + } + + $path = $invoice->document_filename; + // Pfad zur Datei + $fullPath = 'private/' . $path; + + + + + if (!Storage::exists($path)) { + + + + abort(404, 'Datei nicht gefunden'); + } + + return response()->file(storage_path('app/' . $fullPath), [ + 'Content-Type' => 'application/pdf' + ]); + } +} diff --git a/app/Domains/Invoice/Routes/api.php b/app/Domains/Invoice/Routes/api.php new file mode 100644 index 0000000..cc49bbc --- /dev/null +++ b/app/Domains/Invoice/Routes/api.php @@ -0,0 +1,34 @@ +group(function () { + Route::prefix('api/v1/invoice')->group(function () { + Route::post('/new/{costUnitId}/{invoiceType}', [NewInvoiceController::class, 'saveInvoice']); + Route::middleware(['auth'])->group(function () { + Route::get('/details/{invoiceId}', ShowInvoiceController::class); + Route::get('/showReceipt/{invoiceId}', [ShowInvoiceController::class, 'showReceipt']); + + Route::post('/details/{invoiceId}/change-state/{newState}', ChangeStateController::class); + Route::post('/details/{invoiceId}/copy', [EditController::class, 'copyInvoice']); + Route::post('/details/{invoiceId}/update', [EditController::class, 'updateInvoice']); + Route::get('/my-invoices/{invoiceStatus}', [ListMyInvoicesController::class, 'getMyInvoicesByStatus']); + + + + + Route::get('/create', [CreateController::class, 'showForm']); + }); + + + + }); +}); diff --git a/app/Domains/Invoice/Routes/web.php b/app/Domains/Invoice/Routes/web.php new file mode 100644 index 0000000..2d20b9e --- /dev/null +++ b/app/Domains/Invoice/Routes/web.php @@ -0,0 +1,20 @@ +group(function () { + Route::prefix('invoice')->group(function () { + Route::get('/new', NewInvoiceController::class); + + Route::middleware(['auth'])->group(function () { + Route::get('/my-invoices/{invoiceStatus}', ListMyInvoicesController::class); + + }); + + + + }); +}); diff --git a/app/Domains/Invoice/Views/ListMyInvoices.vue b/app/Domains/Invoice/Views/ListMyInvoices.vue new file mode 100644 index 0000000..caad058 --- /dev/null +++ b/app/Domains/Invoice/Views/ListMyInvoices.vue @@ -0,0 +1,57 @@ + + + diff --git a/app/Domains/Invoice/Views/NewInvoice.vue b/app/Domains/Invoice/Views/NewInvoice.vue new file mode 100644 index 0000000..a9b1910 --- /dev/null +++ b/app/Domains/Invoice/Views/NewInvoice.vue @@ -0,0 +1,79 @@ + + + diff --git a/app/Domains/Invoice/Views/Partials/invoiceDetails/DistanceAllowance.vue b/app/Domains/Invoice/Views/Partials/invoiceDetails/DistanceAllowance.vue new file mode 100644 index 0000000..7ca8eff --- /dev/null +++ b/app/Domains/Invoice/Views/Partials/invoiceDetails/DistanceAllowance.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/app/Domains/Invoice/Views/Partials/invoiceDetails/EditInvoice.vue b/app/Domains/Invoice/Views/Partials/invoiceDetails/EditInvoice.vue new file mode 100644 index 0000000..adba409 --- /dev/null +++ b/app/Domains/Invoice/Views/Partials/invoiceDetails/EditInvoice.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/app/Domains/Invoice/Views/Partials/invoiceDetails/Header.vue b/app/Domains/Invoice/Views/Partials/invoiceDetails/Header.vue new file mode 100644 index 0000000..e424047 --- /dev/null +++ b/app/Domains/Invoice/Views/Partials/invoiceDetails/Header.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/app/Domains/Invoice/Views/Partials/invoiceDetails/InvoiceDetails.vue b/app/Domains/Invoice/Views/Partials/invoiceDetails/InvoiceDetails.vue new file mode 100644 index 0000000..4573d6c --- /dev/null +++ b/app/Domains/Invoice/Views/Partials/invoiceDetails/InvoiceDetails.vue @@ -0,0 +1,285 @@ + + +