Compare commits

29 Commits

Author SHA1 Message Date
43f8621053 All Lists finalized 2026-03-29 00:37:03 +01:00
2d17e61cc8 First Aid list, amount list and kitchen list PDF download implemented 2026-03-28 22:28:55 +01:00
7bea223ded First Aid list, amount list and kitchen list PDF download implemented 2026-03-28 22:22:06 +01:00
df7c14442e Fixed bug in real income 2026-03-25 20:55:35 +01:00
33b4a249cc WiP 2026-03-25 20:28:04 +01:00
37039f082c Event identifiers for anonymizations 2026-03-22 00:14:00 +01:00
405591d6dd Signup for events implemented 2026-03-22 00:06:03 +01:00
b8341890d3 Basic signup for events 2026-03-21 21:02:15 +01:00
23af267896 Running 2026-03-20 22:50:22 +01:00
b1c333648a Bugfixes & Model for participants 2026-02-17 21:58:55 +01:00
fcf41c5d13 Creation and editing of events 2026-02-16 21:59:21 +01:00
2b458eccd7 Invoice upload by robot 2026-02-14 00:04:00 +01:00
4f4dff2edd Invoice PAIN & CSV can be uploaded 2026-02-13 22:37:27 +01:00
cd526231ed Bugfix 2026-02-13 13:55:20 +01:00
fa886aad4d Design improvements 2026-02-13 13:51:07 +01:00
f468814a2f Managing own invoices 2026-02-13 12:38:48 +01:00
ab711109a7 Pdf Viewer 2026-02-13 10:54:17 +01:00
72623df38f Pdf Viewer 2026-02-13 10:52:40 +01:00
9fd6839878 Small fixes 2026-02-13 10:41:20 +01:00
fd403f8520 Operation processes on invoices 2026-02-13 00:11:51 +01:00
882752472e Invoice Widgets completed 2026-02-11 19:38:06 +01:00
87531237c7 Show own invoices 2026-02-11 15:44:43 +01:00
ee7fc637f1 Invoices can be uploaded 2026-02-11 15:40:06 +01:00
bccfc11687 Cost units can be edited 2026-02-08 20:06:38 +01:00
6fc65e195c API-Route to new global variables 2026-02-05 09:18:24 +01:00
e9ae850002 Display nicename in dashboard 2026-02-05 07:40:00 +01:00
11108bdfcc Basic user management 2026-02-05 00:46:22 +01:00
e280fcfba8 Basic design created 2026-02-03 09:33:18 +01:00
3570f442f5 Basic tenant structure 2026-01-31 20:07:41 +01:00
324 changed files with 47181 additions and 800 deletions

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

1
.gitignore vendored
View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,67 @@
<script setup>
import {reactive, inject, onMounted} from 'vue';
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import {toast} from "vue3-toastify";
import ListInvoices from "./Partials/ListInvoices.vue";
const props = defineProps({
costUnit: Object
})
const urlParams = new URLSearchParams(window.location.search)
const initialCostUnitId = props.costUnit.id
const initialInvoiceId = props.invoice_id
const tabs = [
{
title: 'Neue Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/new",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Nichtexportierte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/approved",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Exportierte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/exported",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Abgelehnte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/denied",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
]
onMounted(() => {
if (undefined !== props.message) {
toast.success(props.message)
}
})
</script>
<template>
<AppLayout :title="'Abrechnungen ' + props.costUnit.name">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<tabbed-page :tabs="tabs" :initial-tab-id="initialCostUnitId" :initial-sub-tab-id="initialInvoiceId" />
</shadowed-box>
</AppLayout>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
<?php
use App\Domains\Dashboard\Controllers\DashboardController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::middleware(['auth'])->group(function () {
Route::prefix('api/v1/dashboard')->group(function () {
Route::get('/my-invoices', [DashboardController::class, 'getMyInvoices']);
Route::get('/open-cost-units', [DashboardController::class, 'getOpenCostUnits']);
});
});
});

View File

@@ -0,0 +1 @@
<?php

View File

@@ -0,0 +1,48 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import MyInvoices from "./Partials/Widgets/MyInvoices.vue";
const props = defineProps({
myInvoices: Object,
})
function newInvoice() {
window.location.href = '/invoice/new';
}
</script>
<template>
<AppLayout title='Dashboard'>
<diV class="dashboard-widget-container">
<shadowed-box class="dashboard-widget-box" style="width: 60%;">
Meine Anmeldungen
</shadowed-box>
<shadowed-box class="dashboard-widget-box">
<MyInvoices />
<input type="button" value="Neue Abrechnung" @click="newInvoice" style="margin-top: 20px;">
</shadowed-box>
</diV>
</AppLayout>
</template>
<style>
.dashboard-widget-container {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
position: relative;
}
.dashboard-widget-box {
flex-grow: 1; display: inline-block;
margin: 0 10px;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,173 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesCommand;
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesRequest;
use App\Domains\Event\Actions\UpdateEvent\UpdateEventCommand;
use App\Domains\Event\Actions\UpdateEvent\UpdateEventRequest;
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersCommand;
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersRequest;
use App\Enumerations\ParticipationFeeType;
use App\Enumerations\ParticipationType;
use App\Models\EventParticipant;
use App\Providers\InertiaProvider;
use App\Providers\PdfGenerateAndDownloadProvider;
use App\Resources\EventResource;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class DetailsController extends CommonController {
public function __invoke(int $eventId) {
$event = $this->events->getById($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
];
$participationFeeRequest = new SetParticipationFeesRequest($event, $participationFeeFirst);
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"',
]);
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\CertificateOfConductionCheck\CertificateOfConductionCheckCommand;
use App\Domains\Event\Actions\CertificateOfConductionCheck\CertificateOfConductionCheckRequest;
use App\Domains\Event\Actions\SignUp\SignUpCommand;
use App\Domains\Event\Actions\SignUp\SignUpRequest;
use App\Mail\EventSignUpSuccessfull;
use App\Models\Tenant;
use App\Providers\DoubleCheckEventRegistrationProvider;
use App\Providers\InertiaProvider;
use App\Resources\UserResource;
use App\Scopes\CommonController;
use DateTime;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
class SignupController extends CommonController {
public function __invoke(string $eventId, Request $request) {
$availableEvents = [];
foreach ($this->events->getAvailable(false) as $event) {
$availableEvents[] = $event->toResource()->toArray($request);
};
$event = $this->events->getByIdentifier($eventId, false)?->toResource()->toArray($request);
$participantData = [
'firstname' => '',
'lastname' => '',
];
if (auth()->check()) {
$user = new UserResource(auth()->user())->toArray($request);
$participantData = [
'id' => $user['id'],
'firstname' => $user['firstname'],
'lastname' => $user['lastname'],
'nickname' => $user['nickname'],
'email' => $user['email'],
'phone' => $user['phone'],
'postcode' => $user['postcode'],
'city' => $user['city'],
'address_1' => $user['address_1'],
'address_2' => $user['address_2'],
'birthday' => $user['birthday'],
'localGroup' => $user['localGroup'],
'allergies' => $user['allergies'],
'intolerances' => $user['intolerances'],
'eating_habit' => $user['eating_habits'],
'medications' => $user['medications'],
'tetanusVaccination' => $user['tetanus_vaccination'],
];
}
$inertiaProvider = new InertiaProvider('Event/Signup', [
'event' => $event,
'availableEvents' => $availableEvents,
'localGroups' => $event['contributingLocalGroups'],
'participantData' => $participantData,
]);
return $inertiaProvider->render();
}
public function signUp(int $eventId, Request $request) {
$event = $this->events->getById($eventId, false);
$eventResource = $event->toResource();
$registrationData = $request->input('registration_data');
$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
);
$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 EventSignUpSuccessfull(
participant: $signupResponse->participant,
));
if ($signupResponse->participant->email_2 !== null) {
Mail::to($signupResponse->participant->email_2)->send(new EventSignUpSuccessfull(
participant: $signupResponse->participant,
));
}
return response()->json(
[
'participant' => $signupResponse->participant->toResource()->toArray($request),
'status' => 'success',
]
);
dd($eventId, $registrationData, $amount);
}
public function calculateAmount(int $eventId, Request $request, bool $forDisplay = true) : JsonResponse | float {
$event = $this->events->getById($eventId, false)->toResource();
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'))
)->toString()
]);
}
}

View File

@@ -0,0 +1,35 @@
<?php
use App\Domains\Event\Controllers\CreateController;
use App\Domains\Event\Controllers\DetailsController;
use App\Domains\Event\Controllers\SignupController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::prefix('api/v1')
->group(function () {
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('event')->group(function () {
Route::post('{eventId}/calculate-amount', [SignupController::class, 'calculateAmount']);
Route::post('{eventId}/signup', [SignupController::class, 'signUp']);
Route::middleware(['auth'])->group(function () {
Route::post('/create', [CreateController::class, 'doCreate']);
Route::prefix('/details/{eventId}') ->group(function () {
Route::get('/summary', [DetailsController::class, 'summary']);
Route::post('/event-managers', [DetailsController::class, 'updateEventManagers']);
Route::post('/participation-fees', [DetailsController::class, 'updateParticipationFees']);
Route::post('/common-settings', [DetailsController::class, 'updateCommonSettings']);
});
});
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,152 @@
<script setup>
import { useSignupForm } from './composables/useSignupForm.js'
import StepAge from './steps/StepAge.vue'
import StepContactPerson from './steps/StepContactPerson.vue'
import StepPersonalData from './steps/StepPersonalData.vue'
import StepRegistrationMode from './steps/StepRegistrationMode.vue'
import StepArrival from './steps/StepArrival.vue'
import StepAddons from './steps/StepAddons.vue'
import StepPhotoPermissions from './steps/StepPhotoPermissions.vue'
import StepAllergies from './steps/StepAllergies.vue'
import StepSummary from './steps/StepSummary.vue'
import SubmitSuccess from './after-submit/SubmitSuccess.vue'
import SubmitAlreadyExists from './after-submit/SubmitAlreadyExists.vue'
const props = defineProps({
event: Object,
participantData: Object,
localGroups: Array,
})
const emit = defineEmits(['registrationDone'])
const {
currentStep, goToStep, formData, selectedAddons,
submit, submitting, submitResult, summaryLoading, summaryAmount
} = useSignupForm(props.event, props.participantData)
const steps = [
{ step: 1, label: 'Alter' },
{ step: 2, label: 'Kontaktperson' },
{ step: 3, label: 'Persönliche Daten' },
{ step: 4, label: 'An-/Abreise' },
{ step: 5, label: 'Teilnahmegruppe' },
{ step: 6, label: 'Zusatzoptionen' },
{ step: 7, label: 'Fotoerlaubnis' },
{ step: 8, label: 'Allergien' },
{ step: 9, label: 'Zusammenfassung' },
]
</script>
<template>
<div>
<!-- Nach Submit -->
<SubmitSuccess
v-if="submitResult?.status === 'success'"
:participant="submitResult?.participant"
:event="event"
/>
<SubmitAlreadyExists v-else-if="submitResult?.status === 'exists'" :event="event" />
<template v-else>
<!-- Fortschrittsleiste (ab Step 2) -->
<div v-if="currentStep > 1" style="margin-bottom: 28px;">
<div style="display: flex; gap: 6px; flex-wrap: wrap; align-items: center;">
<template v-for="(s, index) in steps.filter(s => s.step > 1)" :key="s.step">
<!-- Trennlinie zwischen Pills -->
<div v-if="index > 0" style="flex-shrink: 0; width: 16px; height: 2px; background: #e5e7eb; border-radius: 1px;"></div>
<div
:style="{
padding: '5px 14px',
borderRadius: '999px',
fontSize: '0.78rem',
fontWeight: '600',
whiteSpace: 'nowrap',
border: '2px solid',
borderColor: currentStep === s.step ? '#2563eb' : currentStep > s.step ? '#bbf7d0' : '#e5e7eb',
background: currentStep === s.step ? '#2563eb' : currentStep > s.step ? '#f0fdf4' : '#f9fafb',
color: currentStep === s.step ? 'white' : currentStep > s.step ? '#15803d' : '#9ca3af',
cursor: currentStep > s.step ? 'pointer' : 'default',
}"
@click="currentStep > s.step ? goToStep(s.step) : null"
>
<span v-if="currentStep > s.step" style="margin-right: 4px;"></span>
{{ s.label }}
</div>
</template>
</div>
<!-- Fortschrittsbalken -->
<div style="margin-top: 10px; height: 3px; background: #e5e7eb; border-radius: 2px; overflow: hidden;">
<div
:style="{
height: '100%',
background: 'linear-gradient(90deg, #2563eb, #3b82f6)',
borderRadius: '2px',
width: ((currentStep - 2) / (steps.length - 2) * 100) + '%',
transition: 'width 0.3s ease',
}"
></div>
</div>
</div>
<!-- Steps -->
<form @submit.prevent="submit">
<StepAge v-if="currentStep === 1" :event="event" @next="goToStep" />
<StepContactPerson v-if="currentStep === 2" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
<StepPersonalData v-if="currentStep === 3" :formData="formData" :localGroups="localGroups" @next="goToStep" @back="goToStep" />
<StepArrival v-if="currentStep === 4" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
<StepRegistrationMode v-if="currentStep === 5" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
<StepAddons v-if="currentStep === 6" :formData="formData" :event="event" :selectedAddons="selectedAddons" @next="goToStep" @back="goToStep" />
<StepPhotoPermissions v-if="currentStep === 7" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
<StepAllergies v-if="currentStep === 8" :formData="formData" :event="event" @next="goToStep" @back="goToStep" />
<StepSummary
v-if="currentStep === 9"
:formData="formData"
:event="event"
:summaryAmount="summaryAmount"
:summaryLoading="summaryLoading"
:submitting="submitting"
@back="goToStep"
@submit="submit"
/>
</form>
</template>
</div>
</template>
<style>
.form-table { width: 100%; border-collapse: collapse; }
.form-table td { padding: 8px 12px 8px 0; vertical-align: top; }
.form-table td:first-child { width: 220px; color: #374151; font-weight: 500; }
.form-table input[type="text"],
.form-table input[type="date"],
.form-table select,
.form-table textarea {
width: 100%;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.95rem;
box-sizing: border-box;
}
.btn-row { display: flex; gap: 10px; padding-top: 16px; }
.btn-primary {
padding: 8px 20px;
background: #2563eb;
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-secondary {
padding: 8px 20px;
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
</style>

View File

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

View File

@@ -0,0 +1,60 @@
<script setup>
const props = defineProps({
participant: Object,
event: Object,
})
console.log(props.event)
console.log(props.participant)
</script>
<template>
<div style="padding: 20px 0;">
<h3>Hallo {{ props.participant.nicename }},</h3>
<p>Vielen Dank für dein Interesse an der Veranstaltung {{event.name}}<br />Wir haben folgende Daten erhalten:</p>
<table class="form-table" style="margin-bottom: 20px;">
<tr><td>Anreise:</td><td>{{ props.participant.arrival }}</td></tr>
<tr><td>Abreise:</td><td>{{ props.participant.departure }}</td></tr>
<tr><td>Teilnahmegruppe:</td><td>{{ props.participant.participationType }}</td></tr>
</table>
<div v-if="props.participant.efz_status === 'NOT_CHECKED'" style="font-weight: bold; color: #b45309; margin-bottom: 20px;">
Dein erweitertes Führungszeugnis konnte nicht automatisch geprüft werden. Bitte kontaktiere die Aktionsleitung.
</div>
<div v-else-if="props.participant.efz_status === 'CHECKED_INVALID'" style="font-weight: bold; color: #dc2626; margin-bottom: 20px;">
Du hast noch kein erweitertes Führungszeugnis hinterlegt. Bitte reiche es umgehend ein.
</div>
<template v-if="props.participant.needs_payment">
<table class="form-table" style="margin-bottom: 16px;">
<tr>
<td>Kontoinhaber:</td><td>{{ props.event.accountOwner }}</td>
<td rowspan="4" style="vertical-align: top; padding-left: 20px;" v-if="props.participant.identifier !== ''">
<img :src="'/print-girocode/' + props.participant.identifier" alt="GiroCode" style="max-width: 180px;" />
<span style="width: 180px; text-align: center; display: block; font-size: 0.8rem; color: #6b7280; margin-top: 4px;">Giro-Code</span>
</td>
</tr>
<tr><td>IBAN:</td><td>{{ props.event.accountIban }}</td></tr>
<tr><td>Verwendungszweck:</td><td>{{ props.participant.payment_purpose }}</td></tr>
<tr><td>Betrag:</td><td><strong>{{ props.participant.amount_left_string }}</strong></td></tr>
</table>
<p>
Bitte beachte, dass deine Anmeldung erst nach Zahlungseiongang vollständig ist.<br />
Wenn dieser nicht bis zum {{ props.event.registrationFinalEnd.formatted }} erfolgt, kann deine Anmeldung storniert werden.<br /><br />
Solltest du den Beitrag bis zu diesem Datum nicht oder nur teilweise überweisen können, kontaktiere bitte die Aktionsleitung, damit wir eine gemeinsame Lösiung finden können.
</p>
</template>
<p v-else>
Du musst keinen Beitrag überweisen. Deine Anmeldung ist bestätigt.
</p>
<p>
Du erhältst innerhalb von 2 Stunden eine E-Mail mit weiteren Informationen.<br />
Kontakt: <a :href="'mailto:' + props.event.email" style="color: #2563eb;">{{ props.event.email }}</a>
</p>
</div>
</template>

View File

@@ -0,0 +1,98 @@
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({})
console.log(participantData)
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',
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,
})
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 }
}

View File

@@ -0,0 +1,45 @@
<script setup>
const props = defineProps({ formData: Object, event: Object, selectedAddons: Object })
const emit = defineEmits(['next', 'back'])
</script>
<template>
<div>
<!-- Solidarbeitrag-Auswahl -->
<div v-if="event.solidarityPayment" style="margin-bottom: 20px;">
<h3>Beitrag</h3>
<label v-if="event.participationFee_1?.active" style="display: block; margin-bottom: 8px;">
<input type="radio" v-model="formData.beitrag" value="reduced" />
{{ event.participationFee_1.name }} ({{ event.participationFee_1.amount }} )
</label>
<label style="display: block; margin-bottom: 8px;">
<input type="radio" v-model="formData.beitrag" value="regular" />
{{ event.participationFee_2?.name ?? 'Regulärer Beitrag' }} ({{ event.participationFee_2?.amount }} )
</label>
<label v-if="event.participationFee_3?.active" style="display: block; margin-bottom: 8px;">
<input type="radio" v-model="formData.beitrag" value="social" />
{{ event.participationFee_3.name }} ({{ event.participationFee_3.amount }} )
</label>
</div>
<!-- Addons -->
<div v-if="event.addons?.length > 0">
<h3>Zusatzoptionen</h3>
<div v-for="addon in event.addons" :key="addon.id" style="margin-bottom: 16px; padding: 12px; background: #f8fafc; border-radius: 8px;">
<label style="display: flex; gap: 12px; cursor: pointer;">
<input type="checkbox" v-model="selectedAddons[addon.id]" style="margin-top: 4px;" />
<span>
<strong>{{ addon.name }}</strong>
<span style="display: block; color: #6b7280; font-size: 0.875rem;">Betrag: {{ addon.amount }}</span>
<span style="display: block; color: #374151; font-size: 0.875rem; margin-top: 4px;">{{ addon.description }}</span>
</span>
</label>
</div>
</div>
<div class="btn-row">
<button type="button" class="btn-secondary" @click="emit('back', 5)"> Zurück</button>
<button type="button" class="btn-primary" @click="emit('next', 7)">Weiter </button>
</div>
</div>
</template>

View File

@@ -0,0 +1,103 @@
<script setup>
defineProps({ event: Object })
const emit = defineEmits(['next'])
</script>
<template>
<div>
<h3 style="margin: 0 0 6px 0; color: #111827;">Wer nimmt teil?</h3>
<p style="margin: 0 0 24px 0; color: #6b7280; font-size: 0.95rem;">Bitte wähle deine Altersgruppe aus.</p>
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<!-- Kind / Jugendliche:r -->
<div class="age-card" @click="emit('next', 2)">
<div class="age-card__badge">
<img :src="'/images/children.png'" alt="Abzeichen Kind" class="age-card__img" onerror="this.style.display='none'" />
<div class="age-card__badge-fallback">👦</div>
</div>
<div class="age-card__body">
<h4 class="age-card__title">Mein Kind anmelden:</h4>
<p class="age-card__desc">Mein Kind ist <strong>jünger als {{ event.alcoholicsAge }} Jahre.</strong></p>
</div>
</div>
<!-- Erwachsene:r -->
<div class="age-card" @click="emit('next', 3)">
<div class="age-card__badge">
<img :src="'/images/adults.png'" alt="Abzeichen Erwachsene" class="age-card__img" onerror="this.style.display='none'" />
<div class="age-card__badge-fallback">🧑</div>
</div>
<div class="age-card__body">
<h4 class="age-card__title">Mich selbst anmelden</h4>
<p class="age-card__desc">Ich bin <strong>18 Jahre oder älter</strong>.</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.age-card {
flex: 1;
min-width: 220px;
display: flex;
flex-direction: column;
align-items: center;
background: #f8fafc;
border: 2px solid #e5e7eb;
border-radius: 12px;
padding: 28px 20px;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
text-align: center;
}
.age-card:hover {
border-color: #2563eb;
box-shadow: 0 4px 16px rgba(37, 99, 235, 0.12);
transform: translateY(-2px);
}
.age-card__badge {
position: relative;
width: 350px;
height: 200px;
margin-bottom: 16px;
}
.age-card__img {
width: 350px;
height: 200px;
object-fit: contain;
}
.age-card__badge-fallback {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
background: #e0f2fe;
border-radius: 50%;
}
/* Fallback ausblenden wenn Bild geladen ist */
.age-card__img:not([style*="display:none"]) + .age-card__badge-fallback {
display: none;
}
.age-card__body { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.age-card__title { margin: 0; font-size: 1.1rem; font-weight: 700; color: #111827; }
.age-card__desc { margin: 0; font-size: 0.9rem; color: #374151; }
.age-card__hint { margin: 0; font-size: 0.8rem; color: #6b7280; }
.age-card__cta {
margin-top: 10px;
display: inline-block;
padding: 6px 18px;
background: #2563eb;
color: white;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,52 @@
<script setup>
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
</script>
<template>
<div>
<h3>Allergien & Ernährung</h3>
<table class="form-table">
<tr><td>Allergien:</td><td><input type="text" v-model="props.formData.allergien" /></td></tr>
<tr>
<td>
Letzte Teranus-Impfung:
<span style="display: block; font-size: 0.8rem; color: #6b7280; margin-top: 4px;">Lass das Feld frei, wenn die Information nicht vorliegt oder du diese nicht mitteilen willst</span>
</td><td><input type="date" v-model="props.formData.tetanusVaccination" /></td></tr>
<tr><td>Unverträglichkeiten:</td><td><input type="text" v-model="props.formData.intolerances" /></td></tr>
<tr>
<td>
Medikamente:<br />
<span style="display: block; font-size: 0.8rem; color: #6b7280; margin-top: 4px;">Bitte in ausreichender Menge mitbringen</span>
</td>
<td>
<input type="text" v-model="props.formData.medikamente" />
</td>
</tr>
<tr>
<td>Ernährungsweise:</td>
<td>
<select v-model="props.formData.eatingHabit">
<option
v-for="eatingHabit in props.event.eatingHabits"
:value="eatingHabit.data.slug">{{eatingHabit.data.name}}</option>
</select>
</td>
</tr>
<tr>
<td>Anmerkungen:</td>
<td><textarea rows="5" v-model="props.formData.anmerkungen" style="width: 100%;"></textarea></td>
</tr>
<tr>
<td colspan="2" class="btn-row">
<button type="button" class="btn-secondary" @click="emit('back', 7)"> Zurück</button>
<button type="button" class="btn-primary" @click="emit('next', 9)">Weiter </button>
</td>
</tr>
</table>
</div>
</template>

View File

@@ -0,0 +1,81 @@
<script setup>
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
const next = () => {
const arrival = new Date(props.formData.arrival)
arrival.setHours(0,0,0,0);
const departure = new Date(props.formData.departure)
const eventStart = new Date(props.event.eventBeginInternal)
const eventEnd = new Date(props.event.eventEndInternal)
arrival.setHours(0,0,0,0);
departure.setHours(0,0,0,0);
eventStart.setHours(0,0,0,0);
eventEnd.setHours(0,0,0,0);
if (arrival < eventStart) {
alert('Bitte gültige Anreise angeben innerhalb des Veranstaltungszeitraums wählen.')
return
}
if (arrival > eventEnd) {
alert('Bitte gültige Abreise angeben innerhalb des Veranstaltungszeitraums wählen.')
return
}
if (departure < arrival) {
alert('Abreise kann niht vor der Anreise liegen. Bitte korrigieren.')
return
}
const hasAddons = (props.event.addons?.length > 0) || props.event.solidarityPayment
emit('next', 5)
}
const back = () => emit('back', 3)
</script>
<template>
<div>
<h3>An- und Abreise</h3>
<table class="form-table">
<tr>
<td>Anreise:</td>
<td>
<input type="date" v-model="formData.arrival" /><br />
<select v-model="formData.anreise_essen" style="margin-top: 6px;">
<option value="1">Vor dem Abendessen</option>
<option value="2">Vor dem Mittagessen</option>
<option value="3">Vor dem Frühstück</option>
<option value="4">Keine Mahlzeit</option>
</select>
</td>
</tr>
<tr>
<td>Abreise:</td>
<td>
<input type="date" v-model="formData.departure" /><br />
<select v-model="formData.abreise_essen" style="margin-top: 6px;">
<option value="1">Nach dem Frühstück</option>
<option value="2">Nach dem Mittagessen</option>
<option value="3">Nach dem Abendessen</option>
<option value="4">Keine Mahlzeit</option>
</select>
</td>
</tr>
<tr>
<td colspan="2" class="btn-row">
<button type="button" class="btn-secondary" @click="back"> Zurück</button>
<button type="button" class="btn-primary" @click="next">Weiter </button>
</td>
</tr>
</table>
</div>
</template>

View File

@@ -0,0 +1,125 @@
<script setup>
import ErrorText from "../../../../../../Views/Components/ErrorText.vue";
import {reactive} from "vue";
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
const errors = reactive({
ansprechpartner: '',
telefon_2: '',
email_2: '',
badeerlaubnis: '',
first_aid: '',
})
const next = () => {
errors.ansprechpartner = ''
errors.telefon_2 = ''
errors.email_2 = ''
errors.badeerlaubnis = ''
errors.first_aid = ''
let hasError = false
if (!props.formData.ansprechpartner) {
errors.ansprechpartner = 'Bitte eine Kontaktperson angeben.'
hasError = true
}
if (!props.formData.telefon_2) {
errors.telefon_2 = 'Bitte eine Telefonnummer angeben.'
hasError = true
}
if (!props.formData.email_2) {
errors.email_2 = 'Bitte eine E-Mail-Adresse angeben.'
hasError = true
}
if (props.formData.badeerlaubnis === '-1') {
errors.badeerlaubnis = 'Bitte triff eine Entscheidung. Bist du dir unsicher, kontaktiere bitte die Aktionsleitung'
hasError = true
}
if (props.formData.first_aid === '-1') {
errors.first_aid = 'Bitte triff eine Entscheidung. Bist du dir unsicher, kontaktiere bitte die Aktionsleitung.'
hasError = true
}
if (hasError) {
return
}
emit('next', 3)
}
</script>
<template>
<div>
<h3>Kontaktperson</h3>
<table class="form-table">
<tr>
<td>Name (Nachname, Vorname):</td>
<td>
<input type="text" v-model="formData.ansprechpartner" />
<ErrorText :message="errors.ansprechpartner" />
</td>
</tr>
<tr>
<td>Telefon:</td>
<td>
<input type="text" v-model="formData.telefon_2" />
<ErrorText :message="errors.telefon_2" />
</td>
</tr>
<tr>
<td>E-Mail:</td>
<td>
<input type="text" v-model="formData.email_2" />
<ErrorText :message="errors.email_2" />
</td>
</tr>
<tr>
<td>Badeerlaubnis:</td>
<td>
<select v-model="formData.badeerlaubnis">
<option value="-1">Bitte wählen</option>
<option
v-for="swimmingPermission in props.event.swimmingPermissions"
:value="swimmingPermission.slug">{{swimmingPermission.name}}</option>
</select>
<ErrorText :message="errors.badeerlaubnis" />
</td>
</tr>
<tr>
<td>Erweiterte Erste Hilfe erlaubt:*</td>
<td>
<select v-model="formData.first_aid">
<option value="-1">Bitte wählen</option>
<option
v-for="firstAidPermission in props.event.firstAidPermissions"
:value="firstAidPermission.slug">{{firstAidPermission.name}}</option>
</select><br />
<span style="font-size: 0.8rem; color: #6b7280;">
Nicht dringend-notwendige Erste-Hilfe-Maßnahmen, beinhaltet das Entfernen von Zecken und Splittern sowie das Kleben von Pflastern.
</span>
<ErrorText :message="errors.first_aid" />
</td>
</tr>
<tr>
<td></td>
<td>
</td>
</tr>
<tr>
<td colspan="2" class="btn-row">
<button type="button" class="btn-primary" @click="next">Weiter </button>
</td>
</tr>
</table>
</div>
</template>

View File

@@ -0,0 +1,124 @@
<script setup>
import {reactive} from "vue";
const props = defineProps({ formData: Object, localGroups: Array })
const emit = defineEmits(['next', 'back'])
const errors = reactive({
vorname: '',
nachname: '',
geburtsdatum: '',
localGroup: '',
address1: '',
plz: '',
ort: '',
telefon_1: '',
email_1: '',
})
const next = () => {
errors.vorname = ''
errors.nachname = ''
errors.geburtsdatum = ''
errors.localGroup = ''
errors.address1 = ''
errors.plz = ''
errors.ort = ''
errors.telefon_1 = ''
errors.email_1 = ''
let hasError = false
if (!props.formData.vorname) {
errors.vorname = 'Bitte den Vornamen angeben.'
hasError = true
}
if (!props.formData.nachname) {
errors.nachname = 'Bitte den Nachnamen angeben.'
hasError = true
}
if (!props.formData.geburtsdatum) {
errors.geburtsdatum = 'Bitte das Geburtsdatum angeben.'
hasError = true
}
if (props.formData.localGroup === '-1') {
errors.localGroup = 'Bitte den Stamm auswählen.'
hasError = true
}
if (!props.formData.address1) {
errors.address1 = 'Bitte die Adresse angeben.'
hasError = true
}
if (!props.formData.plz) {
errors.plz = 'Bitte die Postleitzahl angeben.'
hasError = true
}
if (!props.formData.ort) {
errors.ort = 'Bitte den Ort angeben.'
hasError = true
}
if (!props.formData.email_1) {
errors.email_1 = 'Bitte eine E-Mail-Adresse angeben.'
hasError = true
}
if (hasError) {
return
}
emit('next', 4)
}
</script>
<template>
<div>
<h3>Persönliche Daten</h3>
<table class="form-table">
<tr><td>Vorname:</td><td><input type="text" v-model="props.formData.vorname" /></td></tr>
<tr><td>Nachname:</td><td><input type="text" v-model="props.formData.nachname" /></td></tr>
<tr><td>Pfadiname:</td><td><input type="text" v-model="props.formData.pfadiname" /></td></tr>
<tr>
<td>Stamm:</td>
<td>
<select v-model="props.formData.localGroup">
<option value="-1">Bitte wählen</option>
<option v-for="lg in localGroups" :key="lg.id" :value="lg.id">{{ lg.name }}</option>
</select>
</td>
</tr>
<tr><td>Geburtsdatum:</td><td><input type="date" v-model="props.formData.geburtsdatum" /></td></tr>
<tr>
<td>Adresse:</td>
<td>
<input type="text" v-model="props.formData.address1" />
</td>
</tr>
<tr>
<td></td>
<td>
<input type="text" v-model="props.formData.address2" />
</td>
</tr>
<tr>
<td>PLZ, Ort:</td>
<td>
<input maxlength="5" type="text" v-model="props.formData.plz" style="width: 100px; margin-right: 8px;" />
<input type="text" v-model="props.formData.ort" style="width: calc(100% - 110px);" />
</td>
</tr>
<tr><td>Telefon:</td><td><input type="text" v-model="props.formData.telefon_1" /></td></tr>
<tr><td>E-Mail:</td><td><input type="text" v-model="props.formData.email_1" /></td></tr>
<tr>
<td colspan="2" class="btn-row">
<button type="button" class="btn-primary" @click="next">Weiter </button>
</td>
</tr>
</table>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup>
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
const acceptAll = () => {
Object.keys(props.formData.foto).forEach(k => props.formData.foto[k] = true)
emit('next', 8)
}
const back = () => {
const hasAddons = (props.event.addons?.length > 0) || props.event.solidarityPayment
emit('back', hasAddons ? 6 : 5)
}
</script>
<template>
<div>
<h3>Fotoerlaubnis</h3>
<div v-for="[key, label] in [['socialmedia','Social Media'],['print','Printmedien'],['webseite','Webseite'],['partner','Partnerorganisationen'],['intern','Interne Zwecke']]"
:key="key"
style="margin-bottom: 10px;">
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.foto[key]" />
{{ label }}
</label>
</div>
<div class="btn-row">
<button type="button" class="btn-secondary" @click="back"> Zurück</button>
<button type="button" class="btn-primary" style="background: #059669;" @click="acceptAll">Alle akzeptieren & weiter</button>
<button type="button" class="btn-primary" @click="emit('next', 8)">Weiter </button>
</div>
</div>
</template>

View File

@@ -0,0 +1,130 @@
<script setup>
import { watch } from "vue";
const props = defineProps({ formData: Object, event: Object })
const emit = defineEmits(['next', 'back'])
watch(
() => props.formData.participationType,
(value) => {
if (!value) {
props.formData.beitrag = 'standard'
return
}
props.formData.beitrag = 'standard'
}
)
const nextStep = () => {
const hasAddons = (props.event.addons?.length ?? 0) > 0
emit('next', hasAddons ? 6 : 7)
}
</script>
<template>
<div>
<h3 v-if="event.solidarityPayment">Solidarbeitrag Teilnahmegruppe</h3>
<h3 v-else>Ich nehme teil als ...</h3>
<table style="width: 100%;">
<tr
v-for="participationType in props.event.participationTypes"
:key="participationType.type.slug"
style="vertical-align: top;"
>
<td style="width: 50px; padding-top: 6px;">
<input
:id="participationType.type.slug"
v-model="props.formData.participationType"
type="radio"
:value="participationType.type.slug"
/>
</td>
<td style="padding-bottom: 16px;">
<label :for="participationType.type.slug" style="line-height: 1.5; font-weight: 600; cursor: pointer;">
{{ participationType.type.name }}
</label><br />
<label
:for="participationType.type.slug"
style="line-height: 1.5; padding-left: 15px; font-style: italic; color: #606060; cursor: pointer;"
>
{{ participationType.description }}
</label>
<div
v-if="props.formData.participationType === participationType.type.slug"
style="margin-top: 10px; margin-left: 15px; padding: 12px 14px; background: #f8fafc; border-left: 3px solid #2563eb; border-radius: 6px;"
>
<template
v-if="participationType.amount_reduced !== null || participationType.amount_solidarity !== null"
>
<div style="margin-bottom: 8px; font-size: 0.95rem; font-weight: 600; color: #374151;">
Beitrag auswählen
</div>
<label style="display: block; margin-bottom: 8px; cursor: pointer;">
<input type="radio" v-model="props.formData.beitrag" value="standard" />
Standardbeitrag
<span style="color: #606060;">
({{ participationType.amount_standard.readable }}
<template v-if="props.event.payPerDay">/ Tag</template>
<template v-else>Gesamt</template>)
</span>
</label>
<label
v-if="participationType.amount_reduced !== null"
style="display: block; margin-bottom: 8px; cursor: pointer;"
>
<input type="radio" v-model="props.formData.beitrag" value="reduced" />
Reduzierter Beitrag
<span style="color: #606060;">
({{ participationType.amount_reduced.readable }}
<template v-if="props.event.payPerDay">/ Tag</template>
<template v-else>Gesamt</template>)
</span>
</label>
<label
v-if="participationType.amount_solidarity !== null"
style="display: block; margin-bottom: 0; cursor: pointer;"
>
<input type="radio" v-model="props.formData.beitrag" value="solidarity" />
Solidaritätsbeitrag
<span style="color: #606060;">
({{ participationType.amount_solidarity.readable }}
<template v-if="props.event.payPerDay">/ Tag</template>
<template v-else>Gesamt</template>)
</span>
</label>
</template>
<template v-else>
<div style="font-size: 0.9rem; color: #606060;">
Standardbeitrag:
<strong>{{ participationType.amount_standard.readable }}</strong>
<template v-if="props.event.payPerDay">/ Tag</template>
<template v-else>Gesamt</template>
</div>
</template>
</div>
</td>
</tr>
</table>
<div class="btn-row">
<button type="button" class="btn-secondary" @click="emit('back', 3)"> Zurück</button>
<button
type="button"
v-if="props.formData.participationType"
class="btn-primary"
@click="nextStep"
>
Weiter
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,151 @@
<script setup>
import {format, parseISO} from "date-fns";
const props = defineProps({
formData: Object,
event: Object,
summaryAmount: String,
summaryLoading: Boolean,
submitting: Boolean,
})
const emit = defineEmits(['back', 'submit'])
function formatDate(dateString) {
if (!dateString) return ''
return format(parseISO(dateString), 'dd.MM.yyyy')
}
function participationGroup() {
if (props.formData.participationType === 'team') {
return 'Kernteam';
}
if (props.formData.participationType === 'participant') {
return 'Teilnehmende';
}
if (props.formData.participationType === 'volunteer') {
return 'Unterstützende';
}
return 'Sonstige';
}
function eatingHabit() {
if (props.formData.eatingHabit === 'EATING_HABIT_VEGAN') {
return 'Vegan';
}
if (props.formData.eatingHabit === 'EATING_HABIT_VEGETARIAN') {
return 'Vegetarisch';
}
return 'Omnivor';
}
</script>
<template>
<div>
<h3>Zusammenfassung</h3>
<div v-if="summaryLoading" style="color: #6b7280; padding: 20px 0;">Wird geladen</div>
<div v-else>
<table class="form-table" style="margin-bottom: 20px;">
<tr>
<td>Dein Name:</td>
<td>{{props.formData.vorname}} {{props.formData.vorname}}</td>
</tr>
<tr>
<td>Deine E-Mail:</td>
<td>{{props.formData.email_1}}</td>
</tr>
<tr v-if="props.formData.ansprechpartner !== ''">
<td>Name deiner Kontaktperson:</td>
<td>{{props.formData.ansprechpartner}}</td>
</tr>
<tr v-if="props.formData.email_2 !== ''">
<td>E-Mail-Adresse deiner Kontaktperson:</td>
<td>{{props.formData.email_2}}</td>
</tr>
<tr v-if="props.formData.telefon_2 !== ''">
<td>Telefonnummer deiner Kontaktperson:</td>
<td>{{props.formData.telefon_2}}</td>
</tr>
<tr>
<td>Teilnahmegruppe:</td>
<td>{{ participationGroup() }}</td>
</tr>
<tr>
<td>Foto-Erlaubnis:</td>
<td>
<strong>Social Media:</strong> {{props.formData.foto.socialmedia ? 'Ja' : 'Nein'}},
<strong>Printmedien:</strong> {{props.formData.foto.print ? 'Ja' : 'Nein'}},
<strong>Webseite:</strong> {{props.formData.foto.webseite ? 'Ja' : 'Nein'}},
<strong>Partnerorganisationen:</strong> {{props.formData.foto.partner ? 'Ja' : 'Nein'}},
<strong>Interne Zwecke:</strong> {{props.formData.foto.intern ? 'Ja' : 'Nein'}}
</td>
</tr>
<tr>
<td>Allergien:</td>
<td>{{props.formData.allergien}}</td>
</tr>
<tr>
<td>Lebensmittelunverträglichkeiten:</td>
<td>{{props.formData.intolerances}}</td>
</tr>
<tr>
<td>Ernährungsweise:</td>
<td>{{eatingHabit()}}</td>
</tr>
<tr><td>Veranstaltung:</td><td><strong>{{ event.name }}</strong></td></tr>
<tr><td>Anreise:</td><td>{{ formatDate(formData.arrival) }}</td></tr>
<tr><td>Abreise:</td><td>{{ formatDate(formData.departure) }}</td></tr>
</table>
<div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px;">
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.summary_information_correct" />
Ich bestätige, dass alle Angaben korrekt sind.
</label>
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.summary_accept_terms" />
Ich akzeptiere die Teilnahmebedingungen.
</label>
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.legal_accepted" />
Ich stimme der Datenschutzerklärung zu.
</label>
<label style="display: flex; gap: 10px; cursor: pointer;">
<input type="checkbox" v-model="formData.payment" />
Ich bestätige, den Betrag von <strong>{{ summaryAmount }}</strong> zu überweisen.
</label>
</div>
<div class="btn-row">
<button type="button" class="btn-secondary" @click="emit('back', 8)"> Zurück</button>
<button
type="submit"
class="btn-primary"
:disabled="!formData.summary_information_correct || !formData.summary_accept_terms || !formData.legal_accepted || !formData.payment || submitting"
style="background: #059669;"
>
{{ submitting ? 'Wird gesendet…' : 'Jetzt anmelden ✓' }}
</button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,119 @@
<script setup>
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import SignupForm from './Partials/SignUpForm/SignupForm.vue'
import FullScreenModal from "../../../Views/Components/FullScreenModal.vue";
import AvailableEvents from "./Partials/AvailableEvents.vue";
import {ref} from "vue";
const props = defineProps({
event: Object,
availableEvents: Array,
participantData: Object,
localGroups: Array,
})
const showSignup = ref(true)
const registrationDone = ref(false)
function close() {
showSignup.value = false
}
</script>
<template>
<AppLayout title="Für Veranstaltung anmelden">
<FullScreenModal :show="showSignup" @close="close">
<shadowed-box style="width: 95%; margin: 30px auto; padding: 0; overflow: hidden; border-radius: 12px;">
<!-- Header -->
<div v-if="props.event.registrationAllowed" style="background: linear-gradient(135deg, #1e40af, #3b82f6); padding: 28px 32px; color: white;">
<div style="display: flex; align-items: center; gap: 14px; flex-wrap: wrap;">
<h2 style="margin: 0; font-size: 1.5rem; font-weight: 700;">{{ props.event.name }}</h2>
<span style="background: rgba(255,255,255,0.2); color: white; padding: 4px 14px; border-radius: 999px; font-size: 0.8rem; font-weight: 600; white-space: nowrap; border: 1px solid rgba(255,255,255,0.4);">
Anmeldung offen
</span>
</div>
<p style="margin: 8px 0 0 0; opacity: 0.85; font-size: 0.95rem;">
📍 {{ props.event.postalCode }} {{ props.event.location }}
</p>
</div>
<div v-else style="background: linear-gradient(135deg, #991b1b, #dc2626); padding: 28px 32px; color: white;">
<div style="display: flex; align-items: center; gap: 14px; flex-wrap: wrap;">
<h2 style="margin: 0; font-size: 1.5rem; font-weight: 700;">{{ props.event.name }}</h2>
<span style="background: rgba(255,0,0,0.2); color: #fecaca; padding: 4px 14px; border-radius: 999px; font-size: 0.8rem; font-weight: 600; white-space: nowrap; border: 1px solid rgba(255,100,100,0.4);">
Anmeldung geschlossen
</span>
</div>
<p style="margin: 8px 0 0 0; opacity: 0.85; font-size: 0.95rem;">
📍 {{ props.event.postalCode }} {{ props.event.location }}
</p>
</div>
<!-- Body -->
<div style="padding: 28px 32px;">
<!-- Info-Grid -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px;">
<!-- Zeile 1 links: Zeitraum -->
<div style="background: #f8fafc; border-radius: 8px; padding: 16px;">
<div style="font-size: 0.75rem; color: #6b7280; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px;">Veranstaltungszeitraum</div>
<div style="font-size: 1rem; color: #111827; font-weight: 500;">{{ props.event.eventBegin }} {{ props.event.eventEnd }}</div>
<div style="font-size: 0.85rem; color: #6b7280;">{{ props.event.duration }} Tage</div>
</div>
<!-- Zeile 1 rechts: Erstattungsrichtlinien -->
<div style="background: #f0f9ff; border-radius: 8px; padding: 16px; border-left: 3px solid #0ea5e9;">
<div style="font-size: 0.75rem; color: #0369a1; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px;">Erstattungsrichtlinien</div>
<div style="color: #0c4a6e; font-size: 0.875rem; line-height: 1.8;">
<div>100 % bis {{ props.event.earlyBirdEnd.formatted }}</div>
<div>{{ props.event.refundAfterEarlyBirdEnd }} % bis {{ props.event.registrationFinalEnd.formatted }}</div>
<div>Danach nur bei Nachrückplätzen</div>
</div>
</div>
<!-- Zeile 2 links: Anmeldeschluss -->
<div style="background: #f8fafc; border-radius: 8px; padding: 16px;">
<div style="font-size: 0.75rem; color: #6b7280; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px;">Anmeldeschluss</div>
<div style="font-size: 1rem; color: #111827; font-weight: 500; margin-bottom: 20px;">{{ props.event.registrationFinalEnd.formatted }}</div>
<div style="font-size: 0.75rem; color: #366a34; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px;">Frühbucher bis</div>
<div style="font-size: 1rem; color: #366a34; font-weight: 500;">{{ props.event.earlyBirdEnd.formatted }}</div>
</div>
<!-- Zeile 2 rechts: Kontakt -->
<div style="background: #f8fafc; border-radius: 8px; padding: 16px;">
<div style="font-size: 0.75rem; color: #6b7280; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px;">Kontakt</div>
<p style="margin: 0 0 6px 0; color: #374151; font-size: 0.9rem; line-height: 1.6;">
Hast du Fragen zur Veranstaltung oder deiner Anmeldung? Kontaktiere uns per E-Mail:
</p>
<a :href="'mailto:' + props.event.email" style="font-size: 0.95rem; color: #2563eb; text-decoration: none; font-weight: 500;">{{ props.event.email }}</a>
</div>
</div>
</div>
<hr style="margin: 0 32px; border: none; border-top: 1px solid #e5e7eb;" />
<div style="padding: 28px 32px;">
<SignupForm
:event="props.event"
:participantData="props.participantData ?? {}"
:localGroups="props.localGroups ?? []"
/>
</div>
</shadowed-box>
</FullScreenModal>
<AvailableEvents v-if="!registrationDone" :events="props.availableEvents" />
<div v-else style="text-align: center; padding: 20px;">
Registrierung komplett
</div>
</AppLayout>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Domains\Invoice\Actions\ChangeStatus;
use App\Enumerations\InvoiceStatus;
class ChangeStatusCommand {
private ChangeStatusRequest $request;
public function __construct(ChangeStatusRequest $request) {
$this->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();
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;
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;
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;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Domains\Invoice\Actions\ChangeStatus;
use App\Models\Invoice;
class ChangeStatusRequest {
public Invoice $invoice;
public string $status;
public ?string $comment;
public function __construct(Invoice $invoice, string $status, ?string $comment = null) {
$this->invoice = $invoice;
$this->status = $status;
$this->comment = $comment;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Invoice\Actions\ChangeStatus;
class ChangeStatusResponse {
public bool $success;
public string $invoiceReceipt;
function __construct() {
$this->success = false;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Domains\Invoice\Actions\CreateInvoice;
use App\Enumerations\InvoiceStatus;
use App\Models\Invoice;
class CreateInvoiceCommand {
private CreateInvoiceRequest $request;
public function __construct(CreateInvoiceRequest $request) {
$this->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,
'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;
}
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);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Domains\Invoice\Actions\CreateInvoice;
use App\Models\CostUnit;
use App\ValueObjects\InvoiceFile;
class CreateInvoiceRequest {
public CostUnit $costUnit;
public string $contactName;
public ?string $contactEmail;
public ?string $contactPhone;
public ?string $accountOwner;
public ?string $accountIban;
public string $invoiceType;
public ?string $invoiceTypeExtended;
public ?string $travelRoute;
public ?int $distance;
public ?int $passengers;
public ?int $transportations;
public ?InvoiceFile $receiptFile;
public float $totalAmount;
public bool $isDonation;
public ?int $userId;
public function __construct(
CostUnit $costUnit,
string $contactName,
string $invoiceType,
float $totalAmount,
?InvoiceFile $receiptFile,
bool $isDonation,
?int $userId = null,
?string $contactEmail = null,
?string $contactPhone = null,
?string $accountOwner = null,
?string $accountIban = null,
?string $invoiceTypeExtended = null,
?string $travelRoute = null,
?int $distance = null,
?int $passengers = null,
?int $transportations,
) {
$this->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;
if ($accountIban === 'undefined') {
$this->accountIban = null;
$this->accountOwner = null;
}
}
}

View File

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

View File

@@ -0,0 +1,204 @@
<?php
namespace App\Domains\Invoice\Actions\CreateInvoiceReceipt;
use App\Enumerations\InvoiceType;
use App\Models\PageText;
use App\Models\Tenant;
use App\Providers\PdfMergeProvider;
use App\Resources\InvoiceResource;
use Dompdf\Dompdf;
use Illuminate\Support\Facades\Storage;
use ZipArchive;
class CreateInvoiceReceiptCommand {
private CreateInvoiceReceiptRequest $request;
private string $tempDirectory;
public function __construct(CreateInvoiceReceiptRequest $request) {
$this->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 = <<<HTML
<tr><td>Reiseweg:</td><td>%1\$s</td></tr>
<tr><td>Gesamtlänge der Strecke:</td><td>%2\$s km x %3\$s / km</td></tr>
<tr><td>Materialtransport:</td><td>%4\$s</td></tr>
<tr><td>Mitfahrende im PKW:</td><td>%5\$s</td></tr>
HTML;
$flatTravelPart = sprintf(
$travelPartTemplate,
$invoiceReadable['travelDirection'] ,
$invoiceReadable['distance'],
$invoiceReadable['distanceAllowance'],
$invoiceReadable['transportation'],
$invoiceReadable['passengers']
);
$invoiceTravelPart = '<tr><td>Kosten für ÖPNV:</td><td>' . $invoiceReadable['amount'] . '</td></tr>';;
$expensePart = '<tr><td>Auslagenerstattung:</td><td>' . $invoiceReadable['amount'] . '</td></tr>';
$content = <<<HTML
<html>
<body style="margin-left: 20mm; margin-top: 17mm">
<h3>Abrechnungstyp %1\$s</h3><br /><br />
<table style="width: 100%%;">
<tr><td>Abrechnungsnummer:</td><td>%2\$s</td></tr>
<tr><td>Name:</td><td>%3\$s</td></tr>
<tr><td>E-Mail:</td><td>%4\$s</td></tr>
<tr><td>Telefon:</td><td>%5\$s</td></tr>
<tr><td>Name der Kostenstelle:</td><td>%6\$s</td></tr>
<tr><td>Zahlungsgrund:</td><td>%7\$s</td></tr>
<tr><td>Wird der Betrag gespendet:</td><td>%8\$s</td></tr>
%9\$s
<tr style="font-weight: bold;">
<td style="border-bottom-width: 1px; border-bottom-style: double;">
Gesamtbetrag:
</td>
<td style="border-bottom-width: 1px; border-bottom-style: double;">
%10\$s
</td>
</tr>
<tr><td colspan="2"><br /><br /></td></tr>
%11\$s
<tr><td>Beleg digital eingereicht am:</td><td>%12\$s</td></tr>
<tr><td>Beleg akzeptiert am:</td><td>%13\$s</td></tr>
<tr><td>Beleg akzeptiert von:</td><td>%14\$s</td></tr>
</table>
%15\$s
</body>
</html>
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 = '<tr><td colspan="2">' . PageText::where('name', 'CONFIRMATION_DONATE')->first()->content . '</td></tr>';
} else {
if ($this->request->invoice->contact_bank_iban === null) {
$paymentInformation = '';
} else {
$paymentInformationTemplate = <<<HTML
<tr><td colspan="2">%1\$s</td></tr>
<tr><td>Kontoinhaber*in:</td><td>%2\$s</td></tr>
<tr><td>INAN:</td><td>%3\$s</td></tr>
<tr><td colspan="2"><br /><br /></td></tr>
HTML;
$paymentInformation = sprintf(
$paymentInformationTemplate,
PageText::where('name', 'CONFIRMATION_PAYMENT')->first()->content,
$invoiceReadable['accountOwner'],
$invoiceReadable['accountIban']
);
}
}
$changes = $this->request->invoice->changes !== null ? '<p>' . $this->request->invoice->changes . '</p>' : '';
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 );
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Domains\Invoice\Actions\CreateInvoiceReceipt;
use App\Models\Invoice;
class CreateInvoiceReceiptRequest {
public Invoice $invoice;
public function __construct(Invoice $invoice) {
$this->invoice = $invoice;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Invoice\Actions\CreateInvoiceReceipt;
class CreateInvoiceReceiptResponse {
public string $fileName;
public function __construct() {
$this->fileName = '';
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Domains\Invoice\Actions\UpdateInvoice;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand;
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest;
use App\Enumerations\InvoiceStatus;
use App\ValueObjects\Amount;
class UpdateInvoiceCommand {
private UpdateInvoiceRequest $request;
public function __construct(UpdateInvoiceRequest $request) {
$this->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() . '.<br />';
$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 . '.<br />';
$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 . '.<br />';
$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;
}
}

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